diff --git a/.github/COPILOT_INSTRUCTIONS.md b/.github/COPILOT_INSTRUCTIONS.md
index cfbaf3a..86980a4 100644
--- a/.github/COPILOT_INSTRUCTIONS.md
+++ b/.github/COPILOT_INSTRUCTIONS.md
@@ -1,348 +1,348 @@
-# Copilot Instructions: Clean Architecture guidance for ncplot7py
-
-Purpose
--------
-This document tells Copilot (or other AI assistants) how to produce code aligned with a clean, maintainable architecture for a large Python project. It also includes notes for TypeScript when the repo grows into multi-language or monorepo setups.
-
-High-level contract for suggestions
-----------------------------------
-- Inputs: existing code context and a concrete change request (file snippet, issue, or feature description).
-- Outputs: small, focused, well-typed code changes; updated tests; brief rationale in PR/commit message when behavior changes.
-- Error modes: when a suggestion breaks API, include a migration note and tests; prefer additive changes.
-
-Core architecture principles
----------------------------
-- Layering: keep Domain (core rules), Application (use-cases), Interfaces/Adapters, and Infrastructure separate.
-- Dependency rule: code dependencies point inward (outer layers depend on inner layers, inner layers know nothing about outer layers).
-- Dependency inversion: use abstractions (Protocols/Interfaces) so application code depends on contracts, not concrete implementations.
-- Side-effects at the edges: I/O (DB, network, filesystem, plotting libraries) belong only in infrastructure/adapters.
-
-Recommended repository layout (scalable)
-----------------------------------------
-Use a src-style package layout. Example for a single large package:
-
-src/
- ncplot7py/
- domain/
- models.py # dataclasses, value objects, domain errors
- services.py # pure domain logic
- application/
- usecases.py # orchestrators that use domain services & ports
- adapters/
- api.py # controllers/entrypoints (FastAPI, CLI)
- serializers.py # mapping external <-> domain types
- infrastructure/
- netcdf_reader.py # concrete I/O implementations
- persistence.py # DB adapters
- plotting_impl.py # plotting wrapper implementations
- shared/
- config.py
- types.py
- __init__.py
-tests/
- unit/
- integration/
-pyproject.toml
-README.md
-
-For monorepos or multi-package setups, apply the same layer boundaries inside each package.
-
-Typing & interfaces
--------------------
-- Python: use dataclasses for domain models, typing.Protocol for ports, and mypy/pyright in CI.
-- Keep Protocols near the consumer (application/usecase) — not buried in infrastructure.
-
-How to structure a new feature (recommended steps)
------------------------------------------------
-1. Design domain types (dataclasses) and pure validations in `domain/`.
-2. Implement a use-case in `application/` that depends on a small Protocol (port) for I/O.
-3. Provide concrete implementations in `infrastructure/`.
-4. Expose adapters in `adapters/` (HTTP/CLI) that translate external requests into domain inputs and call use-cases.
-5. Add unit tests for domain and application; add integration tests for infrastructure with small test assets.
-
-Small illustrative pattern (reader example)
------------------------------------------
-# domain/models.py
-from dataclasses import dataclass
-
-@dataclass
-class Metadata:
- variable_names: list[str]
- attributes: dict
-
-# application/usecases.py
-from typing import Protocol
-from ..domain.models import Metadata
-
-class MetadataReader(Protocol):
- def read(self, path: str) -> Metadata: ...
-
-def extract_metadata(path: str, reader: MetadataReader) -> Metadata:
- md = reader.read(path)
- # domain validation or transformations
- return md
-
-# infrastructure/netcdf_reader.py
-from ..domain.models import Metadata
-
-class NetCDFReader:
- def read(self, path: str) -> Metadata:
- # use xarray/netCDF4 to read and return domain.Metadata
- ...
-
-Testing strategy
-----------------
-- Unit tests: target `domain/` and `application/` only; mock ports. Fast and deterministic.
-- Integration tests: test `infrastructure/` + `adapters/` with small real assets (temporary files, fixtures).
-- Test pyramid: many unit tests, fewer integration tests, minimal end-to-end tests.
-
-Tooling & CI recommendations
----------------------------
-- Formatting: Black + isort. Linting: ruff/flake8. Type checks: mypy/pyright.
-- Testing: prefer the standard library (`unittest`, `doctest`, `unittest.mock`). Use third-party test runners only with explicit justification and PR rationale.
-- Packaging: `pyproject.toml` (PEP 621) with Poetry or pip-tools; keep `dependencies` minimal or empty unless runtime packages are justified.
-- CI: run format, lint, typecheck, and the stdlib unit tests on PRs; run integration tests on main branch or scheduled runs.
-
-CI / cocompiler test commands (PowerShell)
------------------------------------------
-When running tests in a Windows PowerShell environment (or in CI that emulates it), set the `PYTHONPATH` to the repository `src` directory so imports resolve correctly. The project uses the standard library `unittest` discovery for both unit and integration tests. Example commands to include in your CI job or cocompiler step:
-
-```powershell
-$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v
-$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/integration -p "test_*.py" -v
-```
-
-Place these commands in the CI job that runs tests. If your CI runner uses bash or another shell, adapt the equivalent `PYTHONPATH=src python -m unittest ...` syntax for that shell.
-
-Dependencies policy
--------------------
-- Default: prefer Python standard library for runtime features. Do not add new third-party runtime dependencies unless there is a strong, documented justification.
-- If a third-party package is absolutely necessary, prefer small, well-maintained packages available on PyPI and add them explicitly to `pyproject.toml` or `requirements.txt` with a short rationale in the related PR.
-- Developer tooling (formatters, linters, type-checkers, test runners) are allowed as dev-dependencies, but should not be shipped as runtime requirements.
-- Avoid bringing in heavy, opaque dependencies for simple functionality that can be implemented with the standard library.
-
-Coding guidelines
------------------
-Follow these practical rules for readable, maintainable code across the codebase.
-
-- Style and formatting
- - Use Black formatting and an 88-character line width. Run `isort` for import ordering.
- - Prefer expressive, explicit code over clever one-liners.
- - Use f-strings for string interpolation.
-
-- Naming
- - Modules and packages: short, lowercase, underscore-separated if needed (snake_case).
- - Functions and variables: snake_case.
- - Classes: PascalCase.
- - Constants: UPPER_SNAKE_CASE.
-
-- Types & typing
- - Add type hints for public functions and methods (PEP 484). Prefer `typing` generics over `Any`.
- - Use `dataclasses` for domain models and `typing.Protocol` for interfaces/ports.
- - Run static type checks in CI (mypy or pyright). Keep strictness where practical.
-
-- Docstrings & documentation
- - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs.
- - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs.
- - Add short examples when behavior is non-obvious.
-
- Doctest guidelines
- ------------------
- Follow these rules when adding doctest examples in docstrings so they run reliably with `python -m doctest` in CI.
-
- - Keep examples terse and deterministic
- - Examples must run without interactive user input and produce the same output on every run.
- - Avoid printing memory addresses, timestamps, UUIDs, or platform-dependent output. If randomness is required, set a fixed seed inside the example.
-
- - Importing in examples
- - Use full imports that will work when the module is executed from the project root. Example:
- >>> from ncplot7py.domain.models import Metadata
-
- - Use exact, stable output
- - Show canonical string or repr output that is stable across Python versions when possible.
- - For floating-point examples, either round the result in the example or use doctest directives such as `# doctest: +ELLIPSIS` to match approximate output. Example:
- >>> round(0.1 + 0.2, 6)
- 0.3
- >>> 0.1 + 0.2 # doctest: +ELLIPSIS
- 0.3000000...
-
- - Use doctest option flags when needed
- - Use `# doctest: +ELLIPSIS` to allow partial matching, or `# doctest: +NORMALIZE_WHITESPACE` if whitespace differs.
- - Avoid overusing flags; prefer making examples precise.
-
- - Setup and long examples
- - Keep long examples out of function docstrings; put them in `examples/` or `README.md` and test them separately if needed.
- - If an example requires setup (creating temp files, sample NetCDF), show only the minimal reproducible snippet in the docstring and add a full integration example in `tests/integration/`.
-
- - Avoid side effects
- - Do not perform destructive actions (deleting files, network calls) in doctest examples. If demonstrating I/O, use temporary files within the example or put the full scenario in integration tests.
-
- - Running doctests in CI
- - CI should run: `python -m doctest -v src/ncplot7py/*.py` (or a narrower file list). The `pyproject.toml` includes a `doctest-command` showing the canonical command.
-
- - Example of a good doctest
- def add(a, b):
- """
- Return the sum of two numbers.
-
- >>> add(1, 2)
- 3
- >>> round(add(0.1, 0.2), 6)
- 0.3
- """
- return a + b
-
- - Notes for contributors
- - Keep doctest examples focused on documenting observable behavior, not internal implementation.
- - If you add or change doctest examples, run the doctest command locally before opening a PR.
-
-- Imports
- - Use absolute imports within the package (from ncplot7py.xxx import Y).
- - Group imports: stdlib, third-party, local (use isort to enforce).
-
-- Exceptions & error handling
- - Prefer specific exceptions (ValueError, TypeError, etc.). Avoid bare except: clauses.
- - Define domain-specific exception types in `domain/` when callers need to distinguish error classes.
- - Fail fast and validate inputs early. Keep error messages clear and actionable.
-
-- Logging
- - Use the standard `logging` module. Modules should get a logger via `logger = logging.getLogger(__name__)`.
- - Do not use `print()` for production/logging; reserve `print()` only for quick debugging (remove before commit).
-
-- Resource management
- - Use context managers for files, network connections and other resources (`with` statement).
- - Avoid mutable default arguments. Use `None` and set defaults inside the function.
-
-- Concurrency
- - Prefer simple solutions (threading, multiprocessing, asyncio) only when necessary. Encapsulate concurrency in infrastructure layer adapters and keep domain/use-cases synchronous unless the whole stack is async.
-
- - Tests
- - Follow Arrange-Act-Assert. Keep unit tests fast and deterministic.
- - Use the standard library `unittest` for unit tests (TestCase classes, setUp/tearDown, subTest) and `unittest.mock` for mocking.
- - For table-driven tests, prefer `subTest` or explicit loops inside a TestCase rather than adding test framework dependencies.
- - Use `doctest` for executable examples embedded in docstrings when helpful.
- - For integration tests, use temporary directories (`tempfile.TemporaryDirectory`) and small sample assets. Clean up after tests.
-
-- Commits & PRs
- - Commit message: short title (<=72 chars), optional body with rationale and any migration notes.
- - PR description: explain what changed, why, tests added/updated, and any migration steps for consumers.
- - Keep PRs focused: each PR should implement a single logical change or narrowly related set of changes.
-
-- Code review checklist
- - Does the change respect layer boundaries (domain vs adapters vs infra)?
- - Are side-effects localized to infrastructure/adapters?
- - Are types and docstrings present for public APIs?
- - Are there unit tests for domain/application logic and integration tests for adapters where needed?
- - Is the change small and well-commented (or does it include a rationale in the PR)?
-
-Current project structure
--------------------------
-The repository already follows the recommended clean-architecture layout. Use these locations when adding or editing files so Copilot suggestions target the correct layer.
-
-Top-level files
-- `LICENSE`, `README.md`, `pyproject.toml`, `dev-requirements.txt`
-- `.github/COPILOT_INSTRUCTIONS.md` (this file)
-
-Package layout (src-style)
-- `src/ncplot7py/` — main package root
- - `cli/` — CLI entrypoint and command wiring
- - `main.py` — CLI implementation (simulate, plot subcommands)
- - `domain/` — pure domain dataclasses and exceptions
- - `models.py` — NCNode, ToolpathPoint, MachineSpec, SimulationResult
- - `exceptions.py` — domain-specific exception types
- - `application/` — use-cases / orchestrators
- - `simulate.py` — parse -> simulate orchestration utilities
- - `interfaces/` — Protocols / interface definitions
- - `parser.py` — `Parser` Protocol
- - `machine.py` — `MachineDriver` Protocol
- - `plotter.py` — `Plotter` Protocol
- - (intended) `nc_control.py` — NC control Protocol / base class (interface for NC controllers)
- - `infrastructure/` — concrete adapters and implementations (I/O, drivers, plotters)
- - `parsers/gcode_parser.py` — minimal G-code parser (registers as `gcode`)
- - `machines/generic_machine.py` — simple generic machine driver (registers as `generic`)
- - `plotters/matplotlib_plotter.py` — optional matplotlib adapter (registers `matplotlib` plotter)
- - `persistence/` — storage adapters (file-based persistence stubs)
- - `shared/` — small shared helpers and registry
- - `registry.py` — runtime registry resolving parsers/machines/plotters
-
-Tests and examples
-- `tests/unit/` — unit tests (e.g. `test_simulate_flow.py`)
-- `tests/integration/` — integration tests (parsing+simulate+plot if optional deps installed)
-- `examples/` and `data/nc-examples/` — sample NC files and fixtures for integration tests and demos
-
-Notes
-- Optional dependencies (plotting) are declared under `pyproject.toml` extras: `plotting = ["matplotlib>=3.0,<4.0"]`.
-- Concrete implementations register with the `shared.registry` so CLI and application code can resolve components by name (e.g. parser `gcode`, machine `generic`, plotter `matplotlib`).
-- When Copilot generates code that crosses layers, update or create files in the matching package path above.
-
-
-How Copilot should behave when editing code (updated)
-----------------------------------------------------
-- When implementing features, prefer stdlib solutions first (os, pathlib, csv, json, tempfile, subprocess, builtins, typing, dataclasses).
-- If a suggestion uses a third-party library, add a short justification comment and note that adding a dependency requires an explicit PR description and approval.
-- For packages that must be added, include an update to `pyproject.toml` or `requirements.txt` and a brief migration note in the PR description.
-
-How Copilot should behave when editing code
-------------------------------------------
-- Make minimal, well-scoped edits. If a feature crosses layers, create the smallest set of files required and update tests.
-- Prefer adding a Protocol and refactoring callers to depend on it rather than editing many concrete implementations.
-- Always include or update unit tests for any domain or application logic changes.
-
-Do / Don't (architecture-focused)
----------------------------------
-- Do: keep domain pure, add types and docstrings, add focused unit tests, and include migration notes for breaking changes.
-- Don't: put business logic in HTTP handlers, scatter I/O into domain modules, or introduce large runtime deps without justification.
-
-Security
---------
-- Never add secrets to the repo. Use environment variables or secrets managers and document required env vars in `.env.example`.
-
-Customization & next steps
---------------------------
-This file is a living guideline. To change rules:
-- Edit this file and commit.
-- Optionally add CI checks to enforce specific rules (typecheck, linter, format).
-
-I can also scaffold starter artifacts if you want:
-- `pyproject.toml` + minimal `src/` layout
-- Example `MetadataReader` Protocol + `NetCDFReader` implementation + unit/integration tests
-- A GitHub Actions workflow that runs Black, ruff/flake8, mypy, and the stdlib tests on PRs
-
-
-
-
-
-## Error handling (use the structured system)
-- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`.
-- Prefer the helper `raise_nc_error(...)` to automatically populate trace and caret when possible.
-
-Example:
-```python
-from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
-
-if token not in allowed:
- raise_nc_error(
- ExceptionTyps.NCCodeErrors,
- 1001, # see locales XML id "1:1001"
- value=token,
- file=filename,
- line=lineno,
- source_line=line_text,
- )
-```
-
--## Localization of messages
-- Message templates are in `src/ncplot7py/locales/{lang}.xml`.
-- Keys are `{typ_value}:{code}`. Use placeholders `{value}`, `{line}`, `{code}`, `{typ}`.
-- To format for output:
-```python
-from ncplot7py.domain.i18n import MessageCatalog
-text = MessageCatalog().format_exception(exc, lang="en")
-```
-
-## Tracing
-- Provide `file`, `line`, `source_line` (and `value`) when raising errors to get a caret under the offending token.
-- If you know the exact `column`, pass it; otherwise `raise_nc_error` will try to infer it by searching `value` in `source_line`.
-
-
--- End of architecture-focused Copilot instructions --
+# Copilot Instructions: Clean Architecture guidance for ncplot7py
+
+Purpose
+-------
+This document tells Copilot (or other AI assistants) how to produce code aligned with a clean, maintainable architecture for a large Python project. It also includes notes for TypeScript when the repo grows into multi-language or monorepo setups.
+
+High-level contract for suggestions
+----------------------------------
+- Inputs: existing code context and a concrete change request (file snippet, issue, or feature description).
+- Outputs: small, focused, well-typed code changes; updated tests; brief rationale in PR/commit message when behavior changes.
+- Error modes: when a suggestion breaks API, include a migration note and tests; prefer additive changes.
+
+Core architecture principles
+---------------------------
+- Layering: keep Domain (core rules), Application (use-cases), Interfaces/Adapters, and Infrastructure separate.
+- Dependency rule: code dependencies point inward (outer layers depend on inner layers, inner layers know nothing about outer layers).
+- Dependency inversion: use abstractions (Protocols/Interfaces) so application code depends on contracts, not concrete implementations.
+- Side-effects at the edges: I/O (DB, network, filesystem, plotting libraries) belong only in infrastructure/adapters.
+
+Recommended repository layout (scalable)
+----------------------------------------
+Use a src-style package layout. Example for a single large package:
+
+src/
+ ncplot7py/
+ domain/
+ models.py # dataclasses, value objects, domain errors
+ services.py # pure domain logic
+ application/
+ usecases.py # orchestrators that use domain services & ports
+ adapters/
+ api.py # controllers/entrypoints (FastAPI, CLI)
+ serializers.py # mapping external <-> domain types
+ infrastructure/
+ netcdf_reader.py # concrete I/O implementations
+ persistence.py # DB adapters
+ plotting_impl.py # plotting wrapper implementations
+ shared/
+ config.py
+ types.py
+ __init__.py
+tests/
+ unit/
+ integration/
+pyproject.toml
+README.md
+
+For monorepos or multi-package setups, apply the same layer boundaries inside each package.
+
+Typing & interfaces
+-------------------
+- Python: use dataclasses for domain models, typing.Protocol for ports, and mypy/pyright in CI.
+- Keep Protocols near the consumer (application/usecase) — not buried in infrastructure.
+
+How to structure a new feature (recommended steps)
+-----------------------------------------------
+1. Design domain types (dataclasses) and pure validations in `domain/`.
+2. Implement a use-case in `application/` that depends on a small Protocol (port) for I/O.
+3. Provide concrete implementations in `infrastructure/`.
+4. Expose adapters in `adapters/` (HTTP/CLI) that translate external requests into domain inputs and call use-cases.
+5. Add unit tests for domain and application; add integration tests for infrastructure with small test assets.
+
+Small illustrative pattern (reader example)
+-----------------------------------------
+# domain/models.py
+from dataclasses import dataclass
+
+@dataclass
+class Metadata:
+ variable_names: list[str]
+ attributes: dict
+
+# application/usecases.py
+from typing import Protocol
+from ..domain.models import Metadata
+
+class MetadataReader(Protocol):
+ def read(self, path: str) -> Metadata: ...
+
+def extract_metadata(path: str, reader: MetadataReader) -> Metadata:
+ md = reader.read(path)
+ # domain validation or transformations
+ return md
+
+# infrastructure/netcdf_reader.py
+from ..domain.models import Metadata
+
+class NetCDFReader:
+ def read(self, path: str) -> Metadata:
+ # use xarray/netCDF4 to read and return domain.Metadata
+ ...
+
+Testing strategy
+----------------
+- Unit tests: target `domain/` and `application/` only; mock ports. Fast and deterministic.
+- Integration tests: test `infrastructure/` + `adapters/` with small real assets (temporary files, fixtures).
+- Test pyramid: many unit tests, fewer integration tests, minimal end-to-end tests.
+
+Tooling & CI recommendations
+---------------------------
+- Formatting: Black + isort. Linting: ruff/flake8. Type checks: mypy/pyright.
+- Testing: prefer the standard library (`unittest`, `doctest`, `unittest.mock`). Use third-party test runners only with explicit justification and PR rationale.
+- Packaging: `pyproject.toml` (PEP 621) with Poetry or pip-tools; keep `dependencies` minimal or empty unless runtime packages are justified.
+- CI: run format, lint, typecheck, and the stdlib unit tests on PRs; run integration tests on main branch or scheduled runs.
+
+CI / cocompiler test commands (PowerShell)
+-----------------------------------------
+When running tests in a Windows PowerShell environment (or in CI that emulates it), set the `PYTHONPATH` to the repository `src` directory so imports resolve correctly. The project uses the standard library `unittest` discovery for both unit and integration tests. Example commands to include in your CI job or cocompiler step:
+
+```powershell
+$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v
+$env:PYTHONPATH = 'src'; python -m unittest discover -s tests/integration -p "test_*.py" -v
+```
+
+Place these commands in the CI job that runs tests. If your CI runner uses bash or another shell, adapt the equivalent `PYTHONPATH=src python -m unittest ...` syntax for that shell.
+
+Dependencies policy
+-------------------
+- Default: prefer Python standard library for runtime features. Do not add new third-party runtime dependencies unless there is a strong, documented justification.
+- If a third-party package is absolutely necessary, prefer small, well-maintained packages available on PyPI and add them explicitly to `pyproject.toml` or `requirements.txt` with a short rationale in the related PR.
+- Developer tooling (formatters, linters, type-checkers, test runners) are allowed as dev-dependencies, but should not be shipped as runtime requirements.
+- Avoid bringing in heavy, opaque dependencies for simple functionality that can be implemented with the standard library.
+
+Coding guidelines
+-----------------
+Follow these practical rules for readable, maintainable code across the codebase.
+
+- Style and formatting
+ - Use Black formatting and an 88-character line width. Run `isort` for import ordering.
+ - Prefer expressive, explicit code over clever one-liners.
+ - Use f-strings for string interpolation.
+
+- Naming
+ - Modules and packages: short, lowercase, underscore-separated if needed (snake_case).
+ - Functions and variables: snake_case.
+ - Classes: PascalCase.
+ - Constants: UPPER_SNAKE_CASE.
+
+- Types & typing
+ - Add type hints for public functions and methods (PEP 484). Prefer `typing` generics over `Any`.
+ - Use `dataclasses` for domain models and `typing.Protocol` for interfaces/ports.
+ - Run static type checks in CI (mypy or pyright). Keep strictness where practical.
+
+- Docstrings & documentation
+ - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs.
+ - Document public functions, classes and modules with Google or NumPy style docstrings. Include Args, Returns, and Raises sections for public APIs.
+ - Add short examples when behavior is non-obvious.
+
+ Doctest guidelines
+ ------------------
+ Follow these rules when adding doctest examples in docstrings so they run reliably with `python -m doctest` in CI.
+
+ - Keep examples terse and deterministic
+ - Examples must run without interactive user input and produce the same output on every run.
+ - Avoid printing memory addresses, timestamps, UUIDs, or platform-dependent output. If randomness is required, set a fixed seed inside the example.
+
+ - Importing in examples
+ - Use full imports that will work when the module is executed from the project root. Example:
+ >>> from ncplot7py.domain.models import Metadata
+
+ - Use exact, stable output
+ - Show canonical string or repr output that is stable across Python versions when possible.
+ - For floating-point examples, either round the result in the example or use doctest directives such as `# doctest: +ELLIPSIS` to match approximate output. Example:
+ >>> round(0.1 + 0.2, 6)
+ 0.3
+ >>> 0.1 + 0.2 # doctest: +ELLIPSIS
+ 0.3000000...
+
+ - Use doctest option flags when needed
+ - Use `# doctest: +ELLIPSIS` to allow partial matching, or `# doctest: +NORMALIZE_WHITESPACE` if whitespace differs.
+ - Avoid overusing flags; prefer making examples precise.
+
+ - Setup and long examples
+ - Keep long examples out of function docstrings; put them in `examples/` or `README.md` and test them separately if needed.
+ - If an example requires setup (creating temp files, sample NetCDF), show only the minimal reproducible snippet in the docstring and add a full integration example in `tests/integration/`.
+
+ - Avoid side effects
+ - Do not perform destructive actions (deleting files, network calls) in doctest examples. If demonstrating I/O, use temporary files within the example or put the full scenario in integration tests.
+
+ - Running doctests in CI
+ - CI should run: `python -m doctest -v src/ncplot7py/*.py` (or a narrower file list). The `pyproject.toml` includes a `doctest-command` showing the canonical command.
+
+ - Example of a good doctest
+ def add(a, b):
+ """
+ Return the sum of two numbers.
+
+ >>> add(1, 2)
+ 3
+ >>> round(add(0.1, 0.2), 6)
+ 0.3
+ """
+ return a + b
+
+ - Notes for contributors
+ - Keep doctest examples focused on documenting observable behavior, not internal implementation.
+ - If you add or change doctest examples, run the doctest command locally before opening a PR.
+
+- Imports
+ - Use absolute imports within the package (from ncplot7py.xxx import Y).
+ - Group imports: stdlib, third-party, local (use isort to enforce).
+
+- Exceptions & error handling
+ - Prefer specific exceptions (ValueError, TypeError, etc.). Avoid bare except: clauses.
+ - Define domain-specific exception types in `domain/` when callers need to distinguish error classes.
+ - Fail fast and validate inputs early. Keep error messages clear and actionable.
+
+- Logging
+ - Use the standard `logging` module. Modules should get a logger via `logger = logging.getLogger(__name__)`.
+ - Do not use `print()` for production/logging; reserve `print()` only for quick debugging (remove before commit).
+
+- Resource management
+ - Use context managers for files, network connections and other resources (`with` statement).
+ - Avoid mutable default arguments. Use `None` and set defaults inside the function.
+
+- Concurrency
+ - Prefer simple solutions (threading, multiprocessing, asyncio) only when necessary. Encapsulate concurrency in infrastructure layer adapters and keep domain/use-cases synchronous unless the whole stack is async.
+
+ - Tests
+ - Follow Arrange-Act-Assert. Keep unit tests fast and deterministic.
+ - Use the standard library `unittest` for unit tests (TestCase classes, setUp/tearDown, subTest) and `unittest.mock` for mocking.
+ - For table-driven tests, prefer `subTest` or explicit loops inside a TestCase rather than adding test framework dependencies.
+ - Use `doctest` for executable examples embedded in docstrings when helpful.
+ - For integration tests, use temporary directories (`tempfile.TemporaryDirectory`) and small sample assets. Clean up after tests.
+
+- Commits & PRs
+ - Commit message: short title (<=72 chars), optional body with rationale and any migration notes.
+ - PR description: explain what changed, why, tests added/updated, and any migration steps for consumers.
+ - Keep PRs focused: each PR should implement a single logical change or narrowly related set of changes.
+
+- Code review checklist
+ - Does the change respect layer boundaries (domain vs adapters vs infra)?
+ - Are side-effects localized to infrastructure/adapters?
+ - Are types and docstrings present for public APIs?
+ - Are there unit tests for domain/application logic and integration tests for adapters where needed?
+ - Is the change small and well-commented (or does it include a rationale in the PR)?
+
+Current project structure
+-------------------------
+The repository already follows the recommended clean-architecture layout. Use these locations when adding or editing files so Copilot suggestions target the correct layer.
+
+Top-level files
+- `LICENSE`, `README.md`, `pyproject.toml`, `dev-requirements.txt`
+- `.github/COPILOT_INSTRUCTIONS.md` (this file)
+
+Package layout (src-style)
+- `src/ncplot7py/` — main package root
+ - `cli/` — CLI entrypoint and command wiring
+ - `main.py` — CLI implementation (simulate, plot subcommands)
+ - `domain/` — pure domain dataclasses and exceptions
+ - `models.py` — NCNode, ToolpathPoint, MachineSpec, SimulationResult
+ - `exceptions.py` — domain-specific exception types
+ - `application/` — use-cases / orchestrators
+ - `simulate.py` — parse -> simulate orchestration utilities
+ - `interfaces/` — Protocols / interface definitions
+ - `parser.py` — `Parser` Protocol
+ - `machine.py` — `MachineDriver` Protocol
+ - `plotter.py` — `Plotter` Protocol
+ - (intended) `nc_control.py` — NC control Protocol / base class (interface for NC controllers)
+ - `infrastructure/` — concrete adapters and implementations (I/O, drivers, plotters)
+ - `parsers/gcode_parser.py` — minimal G-code parser (registers as `gcode`)
+ - `machines/generic_machine.py` — simple generic machine driver (registers as `generic`)
+ - `plotters/matplotlib_plotter.py` — optional matplotlib adapter (registers `matplotlib` plotter)
+ - `persistence/` — storage adapters (file-based persistence stubs)
+ - `shared/` — small shared helpers and registry
+ - `registry.py` — runtime registry resolving parsers/machines/plotters
+
+Tests and examples
+- `tests/unit/` — unit tests (e.g. `test_simulate_flow.py`)
+- `tests/integration/` — integration tests (parsing+simulate+plot if optional deps installed)
+- `examples/` and `data/nc-examples/` — sample NC files and fixtures for integration tests and demos
+
+Notes
+- Optional dependencies (plotting) are declared under `pyproject.toml` extras: `plotting = ["matplotlib>=3.0,<4.0"]`.
+- Concrete implementations register with the `shared.registry` so CLI and application code can resolve components by name (e.g. parser `gcode`, machine `generic`, plotter `matplotlib`).
+- When Copilot generates code that crosses layers, update or create files in the matching package path above.
+
+
+How Copilot should behave when editing code (updated)
+----------------------------------------------------
+- When implementing features, prefer stdlib solutions first (os, pathlib, csv, json, tempfile, subprocess, builtins, typing, dataclasses).
+- If a suggestion uses a third-party library, add a short justification comment and note that adding a dependency requires an explicit PR description and approval.
+- For packages that must be added, include an update to `pyproject.toml` or `requirements.txt` and a brief migration note in the PR description.
+
+How Copilot should behave when editing code
+------------------------------------------
+- Make minimal, well-scoped edits. If a feature crosses layers, create the smallest set of files required and update tests.
+- Prefer adding a Protocol and refactoring callers to depend on it rather than editing many concrete implementations.
+- Always include or update unit tests for any domain or application logic changes.
+
+Do / Don't (architecture-focused)
+---------------------------------
+- Do: keep domain pure, add types and docstrings, add focused unit tests, and include migration notes for breaking changes.
+- Don't: put business logic in HTTP handlers, scatter I/O into domain modules, or introduce large runtime deps without justification.
+
+Security
+--------
+- Never add secrets to the repo. Use environment variables or secrets managers and document required env vars in `.env.example`.
+
+Customization & next steps
+--------------------------
+This file is a living guideline. To change rules:
+- Edit this file and commit.
+- Optionally add CI checks to enforce specific rules (typecheck, linter, format).
+
+I can also scaffold starter artifacts if you want:
+- `pyproject.toml` + minimal `src/` layout
+- Example `MetadataReader` Protocol + `NetCDFReader` implementation + unit/integration tests
+- A GitHub Actions workflow that runs Black, ruff/flake8, mypy, and the stdlib tests on PRs
+
+
+
+
+
+## Error handling (use the structured system)
+- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`.
+- Prefer the helper `raise_nc_error(...)` to automatically populate trace and caret when possible.
+
+Example:
+```python
+from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
+
+if token not in allowed:
+ raise_nc_error(
+ ExceptionTyps.NCCodeErrors,
+ 1001, # see locales XML id "1:1001"
+ value=token,
+ file=filename,
+ line=lineno,
+ source_line=line_text,
+ )
+```
+
+-## Localization of messages
+- Message templates are in `src/ncplot7py/locales/{lang}.xml`.
+- Keys are `{typ_value}:{code}`. Use placeholders `{value}`, `{line}`, `{code}`, `{typ}`.
+- To format for output:
+```python
+from ncplot7py.domain.i18n import MessageCatalog
+text = MessageCatalog().format_exception(exc, lang="en")
+```
+
+## Tracing
+- Provide `file`, `line`, `source_line` (and `value`) when raising errors to get a caret under the offending token.
+- If you know the exact `column`, pass it; otherwise `raise_nc_error` will try to infer it by searching `value` in `source_line`.
+
+
+-- End of architecture-focused Copilot instructions --
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b098a35..7aedcf1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,41 +1,41 @@
-name: CI
-
-on:
- push:
- branches: [ master ]
- pull_request:
- branches: [ master ]
-
-jobs:
- test:
- name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest]
- python-version: [3.10, 3.11]
- env:
- # Ensure imports can resolve package under src/
- PYTHONPATH: src
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Upgrade pip
- run: python -m pip install --upgrade pip
-
- - name: Install dev dependencies
- run: |
- if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt; fi
-
- - name: Run unit tests
- run: python -m unittest discover -s tests -p "test_*.py"
-
- - name: Run doctests
- run: python -m doctest -v src/ncplot7py/*.py
+name: CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ test:
+ name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ python-version: [3.10, 3.11]
+ env:
+ # Ensure imports can resolve package under src/
+ PYTHONPATH: src
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Upgrade pip
+ run: python -m pip install --upgrade pip
+
+ - name: Install dev dependencies
+ run: |
+ if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt; fi
+
+ - name: Run unit tests
+ run: python -m unittest discover -s tests -p "test_*.py"
+
+ - name: Run doctests
+ run: python -m doctest -v src/ncplot7py/*.py
diff --git a/.gitignore b/.gitignore
index 82ee2a8..b292d8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,209 +1,210 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[codz]
-*$py.class
-*cgiserver.cgi
-
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py.cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-#poetry.toml
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
-# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
-#pdm.lock
-#pdm.toml
-.pdm-python
-.pdm-build/
-
-# pixi
-# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
-#pixi.lock
-# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
-# in the .venv directory. It is recommended not to include this directory in version control.
-.pixi
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.envrc
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-# Abstra
-# Abstra is an AI-powered process automation framework.
-# Ignore directories containing user credentials, local state, and settings.
-# Learn more at https://abstra.io/docs
-.abstra/
-
-# Visual Studio Code
-# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
-# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
-# and can be added to the global gitignore or merged into this file. However, if you prefer,
-# you could uncomment the following to ignore the entire vscode folder
-# .vscode/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
-
-# Cursor
-# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
-# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
-# refer to https://docs.cursor.com/context/ignore-files
-.cursorignore
-.cursorindexingignore
-
-# Marimo
-marimo/_static/
-marimo/_lsp/
-__marimo__/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$py.class
+
+
+
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+#poetry.toml
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+#pdm.lock
+#pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+#pixi.lock
+# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+# in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Cursor
+# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
+# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
+# refer to https://docs.cursor.com/context/ignore-files
+.cursorignore
+.cursorindexingignore
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 53694b9..e953c35 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,39 +1,39 @@
-{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Python: Current File (with PYTHONPATH)",
- "type": "python",
- "request": "launch",
- "program": "${file}",
- "console": "integratedTerminal",
- "env": {
- "PYTHONPATH": "${workspaceFolder}/src"
- },
- "envFile": "${workspaceFolder}/.env"
- },
- {
- "name": "Python: Run example_O0004.py (with PYTHONPATH)",
- "type": "python",
- "request": "launch",
- "program": "${workspaceFolder}/scripts/run_example_O0004.py",
- "console": "integratedTerminal",
- "env": {
- "PYTHONPATH": "${workspaceFolder}/src"
- },
- "envFile": "${workspaceFolder}/.env"
- },
- {
- "name": "Python: Unittest (discover, with PYTHONPATH)",
- "type": "python",
- "request": "launch",
- "module": "unittest",
- "args": ["discover", "-s", "tests", "-p", "test_*.py"],
- "console": "integratedTerminal",
- "env": {
- "PYTHONPATH": "${workspaceFolder}/src"
- },
- "envFile": "${workspaceFolder}/.env"
- }
- ]
-}
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Python: Current File (with PYTHONPATH)",
+ "type": "python",
+ "request": "launch",
+ "program": "${file}",
+ "console": "integratedTerminal",
+ "env": {
+ "PYTHONPATH": "${workspaceFolder}/src"
+ },
+ "envFile": "${workspaceFolder}/.env"
+ },
+ {
+ "name": "Python: Run example_O0004.py (with PYTHONPATH)",
+ "type": "python",
+ "request": "launch",
+ "program": "${workspaceFolder}/scripts/run_example_O0004.py",
+ "console": "integratedTerminal",
+ "env": {
+ "PYTHONPATH": "${workspaceFolder}/src"
+ },
+ "envFile": "${workspaceFolder}/.env"
+ },
+ {
+ "name": "Python: Unittest (discover, with PYTHONPATH)",
+ "type": "python",
+ "request": "launch",
+ "module": "unittest",
+ "args": ["discover", "-s", "tests", "-p", "test_*.py"],
+ "console": "integratedTerminal",
+ "env": {
+ "PYTHONPATH": "${workspaceFolder}/src"
+ },
+ "envFile": "${workspaceFolder}/.env"
+ }
+ ]
+}
diff --git a/LICENSE b/LICENSE
index 0ad25db..ada1a81 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,661 +1,661 @@
- GNU AFFERO GENERAL PUBLIC LICENSE
- Version 3, 19 November 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU Affero General Public License is a free, copyleft license for
-software and other kinds of works, specifically designed to ensure
-cooperation with the community in the case of network server software.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-our General Public Licenses are intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- Developers that use our General Public Licenses protect your rights
-with two steps: (1) assert copyright on the software, and (2) offer
-you this License which gives you legal permission to copy, distribute
-and/or modify the software.
-
- A secondary benefit of defending all users' freedom is that
-improvements made in alternate versions of the program, if they
-receive widespread use, become available for other developers to
-incorporate. Many developers of free software are heartened and
-encouraged by the resulting cooperation. However, in the case of
-software used on network servers, this result may fail to come about.
-The GNU General Public License permits making a modified version and
-letting the public access it on a server without ever releasing its
-source code to the public.
-
- The GNU Affero General Public License is designed specifically to
-ensure that, in such cases, the modified source code becomes available
-to the community. It requires the operator of a network server to
-provide the source code of the modified version running there to the
-users of that server. Therefore, public use of a modified version, on
-a publicly accessible server, gives the public access to the source
-code of the modified version.
-
- An older license, called the Affero General Public License and
-published by Affero, was designed to accomplish similar goals. This is
-a different license, not a version of the Affero GPL, but Affero has
-released a new version of the Affero GPL which permits relicensing under
-this license.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU Affero General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Remote Network Interaction; Use with the GNU General Public License.
-
- Notwithstanding any other provision of this License, if you modify the
-Program, your modified version must prominently offer all users
-interacting with it remotely through a computer network (if your version
-supports such interaction) an opportunity to receive the Corresponding
-Source of your version by providing access to the Corresponding Source
-from a network server at no charge, through some standard or customary
-means of facilitating copying of software. This Corresponding Source
-shall include the Corresponding Source for any work covered by version 3
-of the GNU General Public License that is incorporated pursuant to the
-following paragraph.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the work with which it is combined will remain governed by version
-3 of the GNU General Public License.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU Affero General Public License from time to time. Such new versions
-will be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU Affero General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU Affero General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU Affero General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If your software can interact with users remotely through a computer
-network, you should also make sure that it provides a way for users to
-get its source. For example, if your program is a web application, its
-interface could display a "Source" link that leads users to an archive
-of the code. There are many ways you could offer source, and different
-solutions will be better for different programs; see section 13 for the
-specific requirements.
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU AGPL, see
-.
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
index 3c53573..db8d5d9 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,156 @@
-# ncplot7py
-NC Code Plot for CNC Code
+# NC-Edit7 CGI Server
+This directory contains the CGI server script for handling plot requests from the NC-Edit7 frontend.
+## Overview
-# run Tests
-& C:/Users/Damian/Project/ncplot7py/ncplot7py/.venv/Scripts/Activate.ps1
+The `cgiserver.cgi` script is a Python CGI script that:
+- Lists available CNC machines
+- Processes NC programs and generates plot data
+- Returns execution results including toolpath segments, variables, and timing
- $env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v
+## API Endpoints
- $env:PYTHONPATH='src'; python -m unittest discover -s tests/integration -p "test_*.py" -v
+### List Machines
-## Error handling and i18n
+**Request:**
+```json
+{
+ "action": "list_machines"
+}
+```
+
+**Response:**
+```json
+{
+ "machines": [
+ {"machineName": "ISO_MILL", "controlType": "MILL"},
+ {"machineName": "FANUC_T", "controlType": "TURN"},
+ ...
+ ],
+ "success": true
+}
+```
+
+### Execute Programs
+
+**Request:**
+```json
+{
+ "machinedata": [
+ {
+ "program": "G90 G54\nG0 X0 Y0 Z10\nG1 X50 Y0 F300\nM30",
+ "machineName": "ISO_MILL",
+ "canalNr": "channel-1"
+ }
+ ]
+}
+```
+
+**Response:**
+```json
+{
+ "canal": {
+ "channel-1": {
+ "segments": [
+ {
+ "type": "RAPID",
+ "lineNumber": 2,
+ "toolNumber": 1,
+ "points": [
+ {"x": 0, "y": 0, "z": 0},
+ {"x": 0, "y": 0, "z": 10}
+ ]
+ }
+ ],
+ "executedLines": [1, 2, 3, 4],
+ "variables": {},
+ "timing": [0.1, 0.1, 0.1, 0.1]
+ }
+ },
+ "message": ["Successfully processed ISO_MILL canal channel-1"],
+ "success": true
+}
+```
+
+## Supported Machines
+
+- `ISO_MILL` - ISO standard milling machine
+- `FANUC_T` - FANUC turning machine
+- `SB12RG_F` - SB12RG front spindle
+- `SB12RG_B` - SB12RG back spindle
+- `SR20JII_F` - SR20JII front spindle
+- `SR20JII_B` - SR20JII back spindle
+
+## Deployment
+
+### Apache Configuration
-This project provides structured, localized errors for CNC/NC parsing and runtime.
+1. Enable CGI module:
+ ```bash
+ sudo a2enmod cgi
+ ```
-- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`.
-- Prefer `raise_nc_error(...)` to attach file/line/column/context and infer the caret position.
--- Localize via XML catalogs under `src/ncplot7py/locales/{lang}.xml` using keys `{typ_value}:{code}`.
+2. Configure virtual host:
+ ```apache
+ ScriptAlias /ncplot7py/scripts/ /var/www/NC-Edit7/ncplot7py/scripts/
+
+
+ Options +ExecCGI
+ AddHandler cgi-script .cgi
+ Require all granted
+
+ ```
-Quick example:
+3. Make script executable:
+ ```bash
+ chmod +x cgiserver.cgi
+ ```
-```python
-from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
-from ncplot7py.domain.i18n import MessageCatalog
+### Testing
-try:
- raise_nc_error(
- ExceptionTyps.NCCodeErrors, 1001,
- value="M30", file="program.nc", line=12,
- source_line="N12 G1 X10 Y10 M30",
- )
-except Exception as exc:
- print(MessageCatalog().format_exception(exc, lang="en"))
+Test the CGI script directly:
+
+```bash
+# List machines
+REQUEST_METHOD=POST CONTENT_LENGTH=30 python3 cgiserver.cgi < test.json <<'TESTEOF'
+{"machinedata": [{"program": "G0 X0 Y0\nG1 X50 Y0", "machineName": "ISO_MILL", "canalNr": "1"}]}
+TESTEOF
+
+REQUEST_METHOD=POST CONTENT_LENGTH=$(wc -c < test.json) python3 cgiserver.cgi < test.json
```
-See `docs/CODING_GUIDELINES.md` and `docs/COPILOT.md` for details.
\ No newline at end of file
+## Current Implementation
+
+The current implementation is a **mock/demo version** that:
+- Generates simple plot data from basic G-code parsing
+- Provides placeholder execution results
+- Does not require external dependencies
+
+## Production Implementation
+
+For production use, this script should be replaced with or extended to:
+- Use the actual NC parser from the Python backend
+- Integrate with the full CNC state machine
+- Support advanced features like tool change detection, synchronization codes, etc.
+- Connect to MariaDB for logging (see rebuild-plan.md for environment variables)
+
+## Development
+
+For local development with Vite dev server, the `dev-cgi-proxy.js` plugin handles CGI requests by spawning the Python script.
+
+## Error Handling
+
+The script returns error responses with `message_TEST` field:
+
+```json
+{
+ "error": "Error description",
+ "message_TEST": ["Detailed error message"]
+}
+```
diff --git a/READMEold.md b/READMEold.md
new file mode 100644
index 0000000..d0499dc
--- /dev/null
+++ b/READMEold.md
@@ -0,0 +1,37 @@
+# ncplot7py
+NC Code Plot for CNC Code
+
+
+
+# run Tests
+& C:/Users/Damian/Project/ncplot7py/ncplot7py/.venv/Scripts/Activate.ps1
+
+ $env:PYTHONPATH = 'src'; python -m unittest discover -s tests/unit -p "test_*.py" -v
+
+ $env:PYTHONPATH='src'; python -m unittest discover -s tests/integration -p "test_*.py" -v
+
+## Error handling and i18n
+
+This project provides structured, localized errors for CNC/NC parsing and runtime.
+
+- Use `ExceptionNode` and `ExceptionTyps` from `ncplot7py.domain.exceptions`.
+- Prefer `raise_nc_error(...)` to attach file/line/column/context and infer the caret position.
+-- Localize via XML catalogs under `src/ncplot7py/locales/{lang}.xml` using keys `{typ_value}:{code}`.
+
+Quick example:
+
+```python
+from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
+from ncplot7py.domain.i18n import MessageCatalog
+
+try:
+ raise_nc_error(
+ ExceptionTyps.NCCodeErrors, 1001,
+ value="M30", file="program.nc", line=12,
+ source_line="N12 G1 X10 Y10 M30",
+ )
+except Exception as exc:
+ print(MessageCatalog().format_exception(exc, lang="en"))
+```
+
+See `docs/CODING_GUIDELINES.md` and `docs/COPILOT.md` for details.
\ No newline at end of file
diff --git a/data/nc-examples/O0004 b/data/nc-examples/O0004
index d0734f0..8f4b95b 100644
--- a/data/nc-examples/O0004
+++ b/data/nc-examples/O0004
@@ -1,31 +1,31 @@
-%
-O8976(EXAMPLE)
-M81
-GOTO789
-M99
-X5
-#500 = 1
-#600 =[#500 / 3]
-#50 = -4
-U#600
-
-#500 = 1
-#600 = #500 - 3
-#50 = -4
-T#600
-G#50 / 4
-#70 = -4 + 4 * 2
-X#70 / 4
-
-%
-O8976(EXAMPLE)
-M200
-M20
-G98 G1 Z20.0 F100
-G2 X10 R40.0
-G1 C10.0
-G1 X0
-G3 Z10 R1
-%
-G266A6.W13.S2000.X15.Z15.F0.02B2.0T100.
+%
+O8976(EXAMPLE)
+M81
+GOTO789
+M99
+X5
+#500 = 1
+#600 =[#500 / 3]
+#50 = -4
+U#600
+
+#500 = 1
+#600 = #500 - 3
+#50 = -4
+T#600
+G#50 / 4
+#70 = -4 + 4 * 2
+X#70 / 4
+
+%
+O8976(EXAMPLE)
+M200
+M20
+G98 G1 Z20.0 F100
+G2 X10 R40.0
+G1 C10.0
+G1 X0
+G3 Z10 R1
+%
+G266A6.W13.S2000.X15.Z15.F0.02B2.0T100.
%
\ No newline at end of file
diff --git a/data/nc-examples/O6POINT b/data/nc-examples/O6POINT
index 34e5ff8..fba1918 100644
--- a/data/nc-examples/O6POINT
+++ b/data/nc-examples/O6POINT
@@ -1,31 +1,31 @@
-O0001(SB12 RG FRONT)
-T1400
-M36S3000
-G99
-F200 G98
-
-G112
-(TEST)
-
-#100 =0
-#101 = 2
-WHILE[#100 LT #101]DO1
-#100 = #100 + 1
-G1 X0 C0
-G1 X-8.0 C0
-G1 X-8.0 C-1.73
-G3 X-7.0 C-2.66 R1.0
-G1 X-1.0 C-4.33
-G3 X1.0 C-4.33 R1.0
-G1 X7.0 C-2.66
-G3 X8.0 C-1.73 R1.0
-G1 X8.0 C1.73
-G3 X7.0 C2.66 R1.0
-G1 X1.0 C4.33
-G3 X-1.0 C4.33 R1.0
-G1 X-7.0 C2.66
-G3 X-8.0 C1.73 R1.0
-G1 X-8.0 C0
-G1W1.0
-END1
-G113
+O0001(SB12 RG FRONT)
+T1400
+M36S3000
+G99
+F200 G98
+
+G112
+(TEST)
+
+#100 =0
+#101 = 2
+WHILE[#100 LT #101]DO1
+#100 = #100 + 1
+G1 X0 C0
+G1 X-8.0 C0
+G1 X-8.0 C-1.73
+G3 X-7.0 C-2.66 R1.0
+G1 X-1.0 C-4.33
+G3 X1.0 C-4.33 R1.0
+G1 X7.0 C-2.66
+G3 X8.0 C-1.73 R1.0
+G1 X8.0 C1.73
+G3 X7.0 C2.66 R1.0
+G1 X1.0 C4.33
+G3 X-1.0 C4.33 R1.0
+G1 X-7.0 C2.66
+G3 X-8.0 C1.73 R1.0
+G1 X-8.0 C0
+G1W1.0
+END1
+G113
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 43bf9cf..0112def 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,4 +1,4 @@
-# Development requirements (install with `pip install -r dev-requirements.txt`)
-# These are dev-only tools; runtime code should prefer the Python stdlib and keep
-# `dependencies` in `pyproject.toml` empty unless an explicit justification exists.
-
+# Development requirements (install with `pip install -r dev-requirements.txt`)
+# These are dev-only tools; runtime code should prefer the Python stdlib and keep
+# `dependencies` in `pyproject.toml` empty unless an explicit justification exists.
+
diff --git a/dist/ncplot7py-0.0.1-py3-none-any.whl b/dist/ncplot7py-0.0.1-py3-none-any.whl
new file mode 100644
index 0000000..e18abab
Binary files /dev/null and b/dist/ncplot7py-0.0.1-py3-none-any.whl differ
diff --git a/dist/ncplot7py-0.0.1.tar.gz b/dist/ncplot7py-0.0.1.tar.gz
new file mode 100644
index 0000000..15cb73a
Binary files /dev/null and b/dist/ncplot7py-0.0.1.tar.gz differ
diff --git a/docs/CGI_API.md b/docs/CGI_API.md
index 01ba18f..f943420 100644
--- a/docs/CGI_API.md
+++ b/docs/CGI_API.md
@@ -1,165 +1,165 @@
-# CGI API for `scripts/cgiserver.cgi`
-
-This document describes the CGI interface implemented by `scripts/cgiserver.cgi`.
-
-## Overview
-- The CGI script accepts a JSON POST and returns a JSON response.
-- The script logs the request into a MariaDB table (if DB credentials are configured).
-- The core processing runs the project's NC execution engine and returns syncro plot data.
-
-## Request payload shapes
-Two accepted shapes:
-
-1) Object with `machinedata` list:
-
-{
- "machinedata": [
- { "program": "", "machineName": "", "canalNr": },
- ...
- ]
-}
-
-2) Direct list of machine-data objects:
-
-[
- { "program": "", "machineName": "", "canalNr": "" },
- ...
-]
-
-Each machine-data entry must include `program`, `machineName`, and `canalNr`.
-
-## Allowed machine names
-- SB12RG_F
-- FANUC_T
-- SR20JII_F
-- SB12RG_B
-- SR20JII_B
-- ISO_MILL
-
-## Validation rules
-- Top-level must be an object containing `machinedata` or a list.
-- Each entry must have `program`, `machineName`, and `canalNr`.
-- `machineName` must be one of the allowed names above.
-- `program` must NOT contain any of these characters: `(`, `)`, `{`, `}` — payloads containing them are rejected.
-
-## Server-side preprocessing
-- The script removes substrings matching `\(.*\)` from the program (comments in parentheses).
-- Newlines are converted to semicolons: `\n` -> `;`.
-- Spaces are removed from the program string.
-
-## Side effects / logging
-- The script attempts to insert a row into MariaDB `log.logNCR` with columns `IP` and `POST`.
-- IP is taken from `REMOTE_ADDR` environment variable (trimmed to 19 characters) or "NAN" if missing.
-- POST body is truncated when logged (approx 1000-1500 characters).
-- MariaDB credentials must be provided in the running environment or script.
-
-## Processing flow
-- Builds initial `CNCState` instances per machine name.
-- Sets X axis unit to `diameter` for lathe-style machines (SB12 and SR20, FANUC_T).
-- Creates a `StatefulIsoTurnNCControl` with `count_of_canals`, `canal_names`, and initial `CNCState` list.
-- Instantiates `NCExecutionEngine(control)` and calls `engine.get_Syncro_plot(programs, True)`.
-
-## Response format
-On success, a JSON object similar to:
-
-{
- "canal": ,
- "message":
-}
-
-- `canal` contains the syncro plot data returned by the execution engine.
-- `message` is the project's message stack (diagnostics/info accumulated during processing).
-
-On error, the script returns a JSON-like error message such as:
-
-{"message_TEST": "", "program": [ ... ]}
-
-or
-
-{"message_T": "", "program": []}
-
-(Exact shape may vary depending on where the exception was raised.)
-
-## Example request (JSON body)
-
-Single program (object with `machinedata`):
-
-{
- "machinedata": [
- {
- "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0",
- "machineName": "SB12RG_F",
- "canalNr": 1
- }
- ]
-}
-
-Equivalent as a plain list:
-
-[
- {
- "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0",
- "machineName": "SB12RG_F",
- "canalNr": "canal1"
- }
-]
-
-## Example PowerShell POST (replace URL)
-
-```powershell
-$json = @'
-{
- "machinedata": [
- { "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", "machineName": "SB12RG_F", "canalNr": 1 }
- ]
-}
-'@
-
-Invoke-RestMethod -Uri 'https://your-server/cgi-bin/cgiserver.cgi' -Method Post -Body $json -ContentType 'application/json'
-```
-
-## Example response (success)
-
-```json
-{
- "canal": [ /* engine-specific syncro plot structure */ ],
- "message": [ /* diagnostic messages */ ]
-}
-```
-
-## Notes / caveats
-- The script has two `request_precheck` blocks; the later one is authoritative for validation.
-- Ensure MariaDB credentials and connectivity are configured in the environment where the CGI runs.
-- Clients need not pre-normalize newlines or spaces; the server will remove spaces and convert newlines to semicolons, but clients must avoid forbidden characters.
-
-## Listing available machines (new)
-
-The CGI supports a lightweight request to retrieve the available machine names and a simple control type description.
-
-Request JSON body:
-
-{
- "action": "list_machines"
-}
-
-or
-
-{
- "action": "get_machines"
-}
-
-Response JSON body:
-
-{
- "machines": [
- { "machineName": "SB12RG_F", "controlType": "StatefulIsoTurnNCControl" },
- { "machineName": "SB12RG_B", "controlType": "StatefulIsoTurnNCControl" },
- ...
- ]
-}
-
-This is useful for clients to discover supported machine names before sending processing requests.
-
----
-
+# CGI API for `scripts/cgiserver.cgi`
+
+This document describes the CGI interface implemented by `scripts/cgiserver.cgi`.
+
+## Overview
+- The CGI script accepts a JSON POST and returns a JSON response.
+- The script logs the request into a MariaDB table (if DB credentials are configured).
+- The core processing runs the project's NC execution engine and returns syncro plot data.
+
+## Request payload shapes
+Two accepted shapes:
+
+1) Object with `machinedata` list:
+
+{
+ "machinedata": [
+ { "program": "", "machineName": "", "canalNr": },
+ ...
+ ]
+}
+
+2) Direct list of machine-data objects:
+
+[
+ { "program": "", "machineName": "", "canalNr": "" },
+ ...
+]
+
+Each machine-data entry must include `program`, `machineName`, and `canalNr`.
+
+## Allowed machine names
+- SB12RG_F
+- FANUC_T
+- SR20JII_F
+- SB12RG_B
+- SR20JII_B
+- ISO_MILL
+
+## Validation rules
+- Top-level must be an object containing `machinedata` or a list.
+- Each entry must have `program`, `machineName`, and `canalNr`.
+- `machineName` must be one of the allowed names above.
+- `program` must NOT contain any of these characters: `(`, `)`, `{`, `}` — payloads containing them are rejected.
+
+## Server-side preprocessing
+- The script removes substrings matching `\(.*\)` from the program (comments in parentheses).
+- Newlines are converted to semicolons: `\n` -> `;`.
+- Spaces are removed from the program string.
+
+## Side effects / logging
+- The script attempts to insert a row into MariaDB `log.logNCR` with columns `IP` and `POST`.
+- IP is taken from `REMOTE_ADDR` environment variable (trimmed to 19 characters) or "NAN" if missing.
+- POST body is truncated when logged (approx 1000-1500 characters).
+- MariaDB credentials must be provided in the running environment or script.
+
+## Processing flow
+- Builds initial `CNCState` instances per machine name.
+- Sets X axis unit to `diameter` for lathe-style machines (SB12 and SR20, FANUC_T).
+- Creates a `StatefulIsoTurnNCControl` with `count_of_canals`, `canal_names`, and initial `CNCState` list.
+- Instantiates `NCExecutionEngine(control)` and calls `engine.get_Syncro_plot(programs, True)`.
+
+## Response format
+On success, a JSON object similar to:
+
+{
+ "canal": ,
+ "message":
+}
+
+- `canal` contains the syncro plot data returned by the execution engine.
+- `message` is the project's message stack (diagnostics/info accumulated during processing).
+
+On error, the script returns a JSON-like error message such as:
+
+{"message_TEST": "", "program": [ ... ]}
+
+or
+
+{"message_T": "", "program": []}
+
+(Exact shape may vary depending on where the exception was raised.)
+
+## Example request (JSON body)
+
+Single program (object with `machinedata`):
+
+{
+ "machinedata": [
+ {
+ "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0",
+ "machineName": "SB12RG_F",
+ "canalNr": 1
+ }
+ ]
+}
+
+Equivalent as a plain list:
+
+[
+ {
+ "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0",
+ "machineName": "SB12RG_F",
+ "canalNr": "canal1"
+ }
+]
+
+## Example PowerShell POST (replace URL)
+
+```powershell
+$json = @'
+{
+ "machinedata": [
+ { "program": "N10 G00 X0 Y0\nN20 G01 X10 Y0", "machineName": "SB12RG_F", "canalNr": 1 }
+ ]
+}
+'@
+
+Invoke-RestMethod -Uri 'https://your-server/cgi-bin/cgiserver.cgi' -Method Post -Body $json -ContentType 'application/json'
+```
+
+## Example response (success)
+
+```json
+{
+ "canal": [ /* engine-specific syncro plot structure */ ],
+ "message": [ /* diagnostic messages */ ]
+}
+```
+
+## Notes / caveats
+- The script has two `request_precheck` blocks; the later one is authoritative for validation.
+- Ensure MariaDB credentials and connectivity are configured in the environment where the CGI runs.
+- Clients need not pre-normalize newlines or spaces; the server will remove spaces and convert newlines to semicolons, but clients must avoid forbidden characters.
+
+## Listing available machines (new)
+
+The CGI supports a lightweight request to retrieve the available machine names and a simple control type description.
+
+Request JSON body:
+
+{
+ "action": "list_machines"
+}
+
+or
+
+{
+ "action": "get_machines"
+}
+
+Response JSON body:
+
+{
+ "machines": [
+ { "machineName": "SB12RG_F", "controlType": "StatefulIsoTurnNCControl" },
+ { "machineName": "SB12RG_B", "controlType": "StatefulIsoTurnNCControl" },
+ ...
+ ]
+}
+
+This is useful for clients to discover supported machine names before sending processing requests.
+
+---
+
Generated from `scripts/cgiserver.cgi` in the repository. If you'd like, I can also add an automated test or a small example script under `scripts/` to POST a sample request and save the response.
\ No newline at end of file
diff --git a/docs/CODING_GUIDELINES.md b/docs/CODING_GUIDELINES.md
index d89948c..0cb89f2 100644
--- a/docs/CODING_GUIDELINES.md
+++ b/docs/CODING_GUIDELINES.md
@@ -1,120 +1,120 @@
-# Coding Guidelines
-
-This project favors clear, small, stdlib-first Python with helpful errors and tests. The key conventions are below.
-
-"""Contributor codeline examples.
-
-This module contains a minimal, stdlib-only example showing a write-only helper
-function. It includes documentation and doctest examples so CI can run
-`python -m doctest` against it.
-
-Guidelines followed:
-- Use only Python standard library.
-- Keep examples deterministic and self-cleaning (remove temporary files).
-- Provide a small doctest demonstrating usage.
-
-"""
-from __future__ import annotations
-
-from pathlib import Path
-from typing import Union
-
-
-def write_text(path: Union[str, Path], text: str, append: bool = False) -> None:
- """Write text to a file using only standard library functions.
-
- Args:
- path: File path to write to. Can be a string or Path.
- text: Text content to write.
- append: If True, append to the file; otherwise overwrite.
-
- Returns:
- None
-
- Doctest example (runs deterministically):
-
- >>> # create a small file, read it back, then remove it
- >>> write_text('tmp_test.txt', 'hello')
- >>> open('tmp_test.txt', 'r', encoding='utf-8').read()
- 'hello'
- >>> # clean up
- >>> import os
- >>> os.remove('tmp_test.txt')
-
- """
- mode = 'a' if append else 'w'
- p = Path(path)
- # Ensure parent directory exists when a Path with directories is provided
- if p.parent and not p.parent.exists():
- p.parent.mkdir(parents=True, exist_ok=True)
-
- # Use explicit encoding for reproducible behavior across platforms
- with p.open(mode, encoding='utf-8') as fh:
- fh.write(text)
-
-
-__all__ = ["write_text"]
-
-
-## Error handling and i18n
-
-Use the structured exception `ExceptionNode` with localized messages.
-
-- Exception types: `ExceptionTyps` (IntEnum)
- - NCCodeErrors = 1
- - NCCanalStarErrors = 2
- - CNCError = 3
-- Raise errors via helper `raise_nc_error` to include trace and infer column:
-
- ```python
- from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
-
- raise_nc_error(
- ExceptionTyps.NCCodeErrors,
- 1001, # maps to XML key "1:1001"
- value="M30", # offending token (used in message formatting)
- file="program.nc",
- line=12,
- source_line="N12 G1 X10 Y10 M30", # for caret positioning and context
- )
- ```
-
-- Exception fields available on `ExceptionNode`:
- - `typ`, `code`, `line`, `message`, `value`
- - Trace: `file`, `column`, `context`
- - `localized(lang)` method returns a localized string with trace.
-
--### Message catalog (XML)
-- Messages live under `src/ncplot7py/locales/{lang}.xml`.
-- Each entry has id `{typ_value}:{code}`. Example (`en.xml`):
-
- ```xml
-
- Invalid NC code '{value}' at line {line}
-
- ```
-
-- Avoid over-coupling to English: use placeholders `{value}`, `{line}`, `{code}`, `{typ}`.
-- Provide translations in other languages (e.g. `de.xml`).
-
-### Choosing codes
-- Codes are domain-specific. Reserve ranges per area if helpful:
- - 1000–1999: NC code syntax/semantics
- - 2000–2999: canal/synchronization
- - 3000–3999: generic CNC runtime
-
-Document new codes by adding entries to XML and referencing them in code.
-
-### Displaying errors
-- For UI/CLI, prefer `MessageCatalog().format_exception(e, lang=...)`.
-- This appends trace info automatically (file, line, column) and prints context with a caret.
-
-## Tests
-- Add unit tests for new error conditions and for new message keys.
-- Keep tests deterministic and small; use the standard library `unittest`.
-
-## Style & structure
-- Prefer small modules and functions.
-- Standard library first; add dependencies only with justification.
-- Add type hints and keep public APIs stable.
-
+# Coding Guidelines
+
+This project favors clear, small, stdlib-first Python with helpful errors and tests. The key conventions are below.
+
+"""Contributor codeline examples.
+
+This module contains a minimal, stdlib-only example showing a write-only helper
+function. It includes documentation and doctest examples so CI can run
+`python -m doctest` against it.
+
+Guidelines followed:
+- Use only Python standard library.
+- Keep examples deterministic and self-cleaning (remove temporary files).
+- Provide a small doctest demonstrating usage.
+
+"""
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Union
+
+
+def write_text(path: Union[str, Path], text: str, append: bool = False) -> None:
+ """Write text to a file using only standard library functions.
+
+ Args:
+ path: File path to write to. Can be a string or Path.
+ text: Text content to write.
+ append: If True, append to the file; otherwise overwrite.
+
+ Returns:
+ None
+
+ Doctest example (runs deterministically):
+
+ >>> # create a small file, read it back, then remove it
+ >>> write_text('tmp_test.txt', 'hello')
+ >>> open('tmp_test.txt', 'r', encoding='utf-8').read()
+ 'hello'
+ >>> # clean up
+ >>> import os
+ >>> os.remove('tmp_test.txt')
+
+ """
+ mode = 'a' if append else 'w'
+ p = Path(path)
+ # Ensure parent directory exists when a Path with directories is provided
+ if p.parent and not p.parent.exists():
+ p.parent.mkdir(parents=True, exist_ok=True)
+
+ # Use explicit encoding for reproducible behavior across platforms
+ with p.open(mode, encoding='utf-8') as fh:
+ fh.write(text)
+
+
+__all__ = ["write_text"]
+
+
+## Error handling and i18n
+
+Use the structured exception `ExceptionNode` with localized messages.
+
+- Exception types: `ExceptionTyps` (IntEnum)
+ - NCCodeErrors = 1
+ - NCCanalStarErrors = 2
+ - CNCError = 3
+- Raise errors via helper `raise_nc_error` to include trace and infer column:
+
+ ```python
+ from ncplot7py.domain.exceptions import ExceptionTyps, raise_nc_error
+
+ raise_nc_error(
+ ExceptionTyps.NCCodeErrors,
+ 1001, # maps to XML key "1:1001"
+ value="M30", # offending token (used in message formatting)
+ file="program.nc",
+ line=12,
+ source_line="N12 G1 X10 Y10 M30", # for caret positioning and context
+ )
+ ```
+
+- Exception fields available on `ExceptionNode`:
+ - `typ`, `code`, `line`, `message`, `value`
+ - Trace: `file`, `column`, `context`
+ - `localized(lang)` method returns a localized string with trace.
+
+-### Message catalog (XML)
+- Messages live under `src/ncplot7py/locales/{lang}.xml`.
+- Each entry has id `{typ_value}:{code}`. Example (`en.xml`):
+
+ ```xml
+
+ Invalid NC code '{value}' at line {line}
+
+ ```
+
+- Avoid over-coupling to English: use placeholders `{value}`, `{line}`, `{code}`, `{typ}`.
+- Provide translations in other languages (e.g. `de.xml`).
+
+### Choosing codes
+- Codes are domain-specific. Reserve ranges per area if helpful:
+ - 1000–1999: NC code syntax/semantics
+ - 2000–2999: canal/synchronization
+ - 3000–3999: generic CNC runtime
+
+Document new codes by adding entries to XML and referencing them in code.
+
+### Displaying errors
+- For UI/CLI, prefer `MessageCatalog().format_exception(e, lang=...)`.
+- This appends trace info automatically (file, line, column) and prints context with a caret.
+
+## Tests
+- Add unit tests for new error conditions and for new message keys.
+- Keep tests deterministic and small; use the standard library `unittest`.
+
+## Style & structure
+- Prefer small modules and functions.
+- Standard library first; add dependencies only with justification.
+- Add type hints and keep public APIs stable.
+
diff --git a/pyproject.toml b/pyproject.toml
index 1184fff..a730f64 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,26 +1,26 @@
-[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[project]
-name = "ncplot7py"
-version = "0.0.1"
-description = "ncplot7py — plotting and utilities for NetCDF data"
-readme = "README.md"
-license = { text = "See LICENSE" }
-authors = [ { name = "d-creations" } ]
-requires-python = ">=3.8"
-dependencies = []
-
-
-[tool.testing]
-# Test policy: prefer the Python standard library test tools (unittest, doctest, unittest.mock).
-# Use third-party test runners only with explicit justification and PR rationale.
-test-command = "python -m unittest discover -s tests -p 'test_*.py'"
-doctest-command = "python -m doctest -v src/ncplot7py/*.py"
-# CI-run command (PowerShell friendly):
-# powershell: python -m unittest discover -s tests -p 'test_*.py'
-
-[project.optional-dependencies]
-plotting = ["matplotlib>=3.0,<4.0"]
-
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "ncplot7py"
+version = "0.0.1"
+description = "ncplot7py — plotting and utilities for NetCDF data"
+readme = "README.md"
+license = { text = "See LICENSE" }
+authors = [ { name = "d-creations" } ]
+requires-python = ">=3.8"
+dependencies = []
+
+
+[tool.testing]
+# Test policy: prefer the Python standard library test tools (unittest, doctest, unittest.mock).
+# Use third-party test runners only with explicit justification and PR rationale.
+test-command = "python -m unittest discover -s tests -p 'test_*.py'"
+doctest-command = "python -m doctest -v src/ncplot7py/*.py"
+# CI-run command (PowerShell friendly):
+# powershell: python -m unittest discover -s tests -p 'test_*.py'
+
+[project.optional-dependencies]
+plotting = ["matplotlib>=3.0,<4.0"]
+
diff --git a/scripts/cgiserver.cgi b/scripts/cgiserver.cgi
new file mode 100644
index 0000000..688234e
--- /dev/null
+++ b/scripts/cgiserver.cgi
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+"""
+CGI Server for NC-Edit7 Plot Interface
+Handles machine data requests and returns plot data
+"""
+
+import sys
+import json
+import os
+from typing import Dict, List, Any, Optional
+
+# Set content type for CGI
+print("Content-Type: application/json")
+print()
+
+
+def get_mock_machines() -> List[Dict[str, str]]:
+ """Return list of available machines"""
+ return [
+ {"machineName": "ISO_MILL", "controlType": "MILL"},
+ {"machineName": "FANUC_T", "controlType": "TURN"},
+ {"machineName": "SB12RG_F", "controlType": "MILL"},
+ {"machineName": "SB12RG_B", "controlType": "MILL"},
+ {"machineName": "SR20JII_F", "controlType": "MILL"},
+ {"machineName": "SR20JII_B", "controlType": "MILL"},
+ ]
+
+
+def parse_nc_program(program: str, machine_name: str) -> Dict[str, Any]:
+ """
+ Parse NC program and generate mock plot data
+ In production, this would call the actual NC parser
+ """
+ lines = [line.strip() for line in program.split('\n') if line.strip()]
+
+ # Generate mock plot segments
+ segments = []
+ current_pos = {"x": 0, "y": 0, "z": 0}
+
+ for i, line in enumerate(lines):
+ # Simple G-code parsing for demo
+ if line.startswith('G0') or line.startswith('G1'):
+ # Extract coordinates
+ new_pos = current_pos.copy()
+
+ parts = line.split()
+ for part in parts:
+ if part.startswith('X'):
+ try:
+ new_pos['x'] = float(part[1:])
+ except ValueError:
+ pass
+ elif part.startswith('Y'):
+ try:
+ new_pos['y'] = float(part[1:])
+ except ValueError:
+ pass
+ elif part.startswith('Z'):
+ try:
+ new_pos['z'] = float(part[1:])
+ except ValueError:
+ pass
+
+ # Create segment
+ segment = {
+ "type": "RAPID" if line.startswith('G0') else "LINEAR",
+ "lineNumber": i + 1,
+ "toolNumber": 1,
+ "points": [
+ current_pos.copy(),
+ new_pos.copy()
+ ]
+ }
+ segments.append(segment)
+ current_pos = new_pos
+
+ return {
+ "segments": segments,
+ "executedLines": list(range(1, len(lines) + 1)),
+ "variables": {},
+ "timing": [0.1] * len(lines)
+ }
+
+
+def handle_list_machines() -> Dict[str, Any]:
+ """Handle machine list request"""
+ return {
+ "machines": get_mock_machines(),
+ "success": True
+ }
+
+
+def handle_execute_programs(programs: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """Handle program execution request"""
+ canal_results = {}
+ messages = []
+
+ for program_entry in programs:
+ program = program_entry.get("program", "")
+ machine_name = program_entry.get("machineName", "ISO_MILL")
+ canal_nr = program_entry.get("canalNr", "1")
+
+ # Validate machine name
+ valid_machines = ["SB12RG_F", "FANUC_T", "SR20JII_F", "SB12RG_B", "SR20JII_B", "ISO_MILL"]
+ if machine_name not in valid_machines:
+ messages.append(f"Invalid machine name: {machine_name}")
+ continue
+
+ # Parse program
+ try:
+ result = parse_nc_program(program, machine_name)
+ canal_results[canal_nr] = result
+ messages.append(f"Successfully processed {machine_name} canal {canal_nr}")
+ except Exception as e:
+ messages.append(f"Error processing {machine_name} canal {canal_nr}: {str(e)}")
+
+ return {
+ "canal": canal_results,
+ "message": messages,
+ "success": True
+ }
+
+
+def main():
+ """Main CGI entry point"""
+ try:
+ # Get request method
+ request_method = os.environ.get("REQUEST_METHOD", "GET")
+
+ if request_method != "POST":
+ response = {
+ "error": "Only POST requests are supported",
+ "message_TEST": ["Method not allowed"]
+ }
+ print(json.dumps(response))
+ return
+
+ # Read POST data
+ content_length = int(os.environ.get("CONTENT_LENGTH", 0))
+ if content_length == 0:
+ response = {
+ "error": "Empty request body",
+ "message_TEST": ["No data provided"]
+ }
+ print(json.dumps(response))
+ return
+
+ post_data = sys.stdin.read(content_length)
+ request_data = json.loads(post_data)
+
+ # Handle different request types
+ if "action" in request_data:
+ action = request_data["action"]
+
+ if action in ["list_machines", "get_machines"]:
+ response = handle_list_machines()
+ else:
+ response = {
+ "error": f"Unknown action: {action}",
+ "message_TEST": [f"Invalid action: {action}"]
+ }
+
+ elif "machinedata" in request_data:
+ programs = request_data["machinedata"]
+ response = handle_execute_programs(programs)
+
+ elif isinstance(request_data, list):
+ # Direct array format
+ response = handle_execute_programs(request_data)
+
+ else:
+ response = {
+ "error": "Invalid request format",
+ "message_TEST": ["Request must contain 'action' or 'machinedata'"]
+ }
+
+ # Return response
+ print(json.dumps(response))
+
+ except json.JSONDecodeError as e:
+ response = {
+ "error": "Invalid JSON",
+ "message_TEST": [f"JSON parse error: {str(e)}"]
+ }
+ print(json.dumps(response))
+
+ except Exception as e:
+ response = {
+ "error": "Internal server error",
+ "message_TEST": [f"Server error: {str(e)}"]
+ }
+ print(json.dumps(response))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/run_example_O0004.py b/scripts/run_example_O0004.py
index 21f2756..318eb6d 100644
--- a/scripts/run_example_O0004.py
+++ b/scripts/run_example_O0004.py
@@ -1,158 +1,158 @@
-"""
-TEST Function example how to use the NC Analyzer
-
-Copyright (C) <2024> .
-"""
-
-from pathlib import Path
-import sys
-from typing import List, Tuple, Any
-
-# Use package imports from the project
-from ncplot7py.application.nc_execution import NCExecutionEngine
-from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n
-from ncplot7py.shared.nc_nodes import NCCommandNode
-from ncplot7py.shared.file_adapter import get_program
-
-
-class _Point:
- def __init__(self, x: float, y: float, z: float):
- self.x = x
- self.y = y
- self.z = z
-
-
-class FileBasedFakeControl:
- """A minimal fake control that converts parsed NC nodes into simple toolpath points.
-
- It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present)
- and creates 1-point lines with a fixed time per command so the engine can
- produce a plot structure for demonstration and testing.
- """
-
- def __init__(self):
- self._canal_nodes = {}
-
- def get_canal_count(self) -> int:
- return 1
-
- def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None:
- # store the nodes for later retrieval
- self._canal_nodes[canal] = list(node_list)
-
- def get_tool_path(self, canal: int):
- nodes = self._canal_nodes.get(canal, [])
- path = []
- for n in nodes:
- params = getattr(n, "command_parameter", {})
- try:
- x = float(params.get("X", 0.0))
- except Exception:
- x = 0.0
- try:
- y = float(params.get("Y", 0.0))
- except Exception:
- y = 0.0
- try:
- z = float(params.get("Z", 0.0))
- except Exception:
- z = 0.0
- # one-point line segment, time 0.1s
- path.append(([_Point(x, y, z)], 0.1))
- return path
-
- def get_exected_nodes(self, canal: int):
- return self._canal_nodes.get(canal, [])
-
- def get_canal_name(self, idx: int) -> str:
- return f"C{idx}"
-
- def synchro_points(self, tool_paths, nodes):
- # no-op for fake control
- return None
-
-
-def read_program_from_file(path: Path) -> list:
- """Read a file and return one or more program strings using the file adapter.
-
- Returns a list of program strings (each string contains commands separated
- by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot.
- """
- # Delegate to the shared file_adapter which handles parentheses removal and
- # splitting into multiple programs if blank lines are used as separators.
- return get_program(path, split_on_blank_line=True)
-
-
-def main(plot: bool = False) -> int:
- # Configure logging to capture messages in the in-memory web buffer so
- # we can print them after the run.
- configure_logging(console=True, web_buffer=True)
- configure_i18n()
-
- # Resolve path to data file (repo root / data / nc-examples / O0004)
- repo_root = Path(__file__).resolve().parent.parent
- data_file = repo_root / "data" / "nc-examples" / "O0004"
-
- if not data_file.exists():
- print(f"Data file not found: {data_file}")
- return 2
-
- programs = read_program_from_file(data_file)
-
- control = FileBasedFakeControl()
- engine = NCExecutionEngine(control)
-
- # get_Syncro_plot expects a list of program strings; our adapter returns
- # that directly, so pass it through.
- plot_result = engine.get_Syncro_plot(programs, synch=False)
-
- print("Returned structure type:", type(plot_result))
- print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a")
- print("Calculated runtime (engine):", engine.get_cacluated_runtime())
-
- print("Captured messages:")
- print(get_message_stack())
-
- # Optionally plot if matplotlib is available and a result was produced
- if plot and isinstance(plot_result, list) and len(plot_result) > 0:
- try:
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d import Axes3D # noqa: F401
-
- all_x, all_y, all_z = [], [], []
- for canal in plot_result:
- for line in canal.get("plot", []):
- all_x.extend(line.get("x", []))
- all_y.extend(line.get("y", []))
- all_z.extend(line.get("z", []))
-
- if all_x and all_y and all_z:
- fig = plt.figure(1)
- ax = fig.add_subplot(111, projection='3d')
- ax.plot(all_z, all_x, all_y, label='parametric curve')
- ax.set_xlabel('z')
- ax.set_ylabel('x')
- ax.set_zlabel('y')
- plt.title('NC example O0004')
- plt.show()
- except Exception as e:
- print(f"Matplotlib plot failed: {e}")
-
- return 0
-
-
-if __name__ == '__main__':
- sys.exit(main(plot=True))
+"""
+TEST Function example how to use the NC Analyzer
+
+Copyright (C) <2024> .
+"""
+
+from pathlib import Path
+import sys
+from typing import List, Tuple, Any
+
+# Use package imports from the project
+from ncplot7py.application.nc_execution import NCExecutionEngine
+from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n
+from ncplot7py.shared.nc_nodes import NCCommandNode
+from ncplot7py.shared.file_adapter import get_program
+
+
+class _Point:
+ def __init__(self, x: float, y: float, z: float):
+ self.x = x
+ self.y = y
+ self.z = z
+
+
+class FileBasedFakeControl:
+ """A minimal fake control that converts parsed NC nodes into simple toolpath points.
+
+ It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present)
+ and creates 1-point lines with a fixed time per command so the engine can
+ produce a plot structure for demonstration and testing.
+ """
+
+ def __init__(self):
+ self._canal_nodes = {}
+
+ def get_canal_count(self) -> int:
+ return 1
+
+ def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None:
+ # store the nodes for later retrieval
+ self._canal_nodes[canal] = list(node_list)
+
+ def get_tool_path(self, canal: int):
+ nodes = self._canal_nodes.get(canal, [])
+ path = []
+ for n in nodes:
+ params = getattr(n, "command_parameter", {})
+ try:
+ x = float(params.get("X", 0.0))
+ except Exception:
+ x = 0.0
+ try:
+ y = float(params.get("Y", 0.0))
+ except Exception:
+ y = 0.0
+ try:
+ z = float(params.get("Z", 0.0))
+ except Exception:
+ z = 0.0
+ # one-point line segment, time 0.1s
+ path.append(([_Point(x, y, z)], 0.1))
+ return path
+
+ def get_exected_nodes(self, canal: int):
+ return self._canal_nodes.get(canal, [])
+
+ def get_canal_name(self, idx: int) -> str:
+ return f"C{idx}"
+
+ def synchro_points(self, tool_paths, nodes):
+ # no-op for fake control
+ return None
+
+
+def read_program_from_file(path: Path) -> list:
+ """Read a file and return one or more program strings using the file adapter.
+
+ Returns a list of program strings (each string contains commands separated
+ by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot.
+ """
+ # Delegate to the shared file_adapter which handles parentheses removal and
+ # splitting into multiple programs if blank lines are used as separators.
+ return get_program(path, split_on_blank_line=True)
+
+
+def main(plot: bool = False) -> int:
+ # Configure logging to capture messages in the in-memory web buffer so
+ # we can print them after the run.
+ configure_logging(console=True, web_buffer=True)
+ configure_i18n()
+
+ # Resolve path to data file (repo root / data / nc-examples / O0004)
+ repo_root = Path(__file__).resolve().parent.parent
+ data_file = repo_root / "data" / "nc-examples" / "O0004"
+
+ if not data_file.exists():
+ print(f"Data file not found: {data_file}")
+ return 2
+
+ programs = read_program_from_file(data_file)
+
+ control = FileBasedFakeControl()
+ engine = NCExecutionEngine(control)
+
+ # get_Syncro_plot expects a list of program strings; our adapter returns
+ # that directly, so pass it through.
+ plot_result = engine.get_Syncro_plot(programs, synch=False)
+
+ print("Returned structure type:", type(plot_result))
+ print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a")
+ print("Calculated runtime (engine):", engine.get_cacluated_runtime())
+
+ print("Captured messages:")
+ print(get_message_stack())
+
+ # Optionally plot if matplotlib is available and a result was produced
+ if plot and isinstance(plot_result, list) and len(plot_result) > 0:
+ try:
+ import matplotlib.pyplot as plt
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401
+
+ all_x, all_y, all_z = [], [], []
+ for canal in plot_result:
+ for line in canal.get("plot", []):
+ all_x.extend(line.get("x", []))
+ all_y.extend(line.get("y", []))
+ all_z.extend(line.get("z", []))
+
+ if all_x and all_y and all_z:
+ fig = plt.figure(1)
+ ax = fig.add_subplot(111, projection='3d')
+ ax.plot(all_z, all_x, all_y, label='parametric curve')
+ ax.set_xlabel('z')
+ ax.set_ylabel('x')
+ ax.set_zlabel('y')
+ plt.title('NC example O0004')
+ plt.show()
+ except Exception as e:
+ print(f"Matplotlib plot failed: {e}")
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(plot=True))
diff --git a/scripts/run_example_O6POINT.py b/scripts/run_example_O6POINT.py
index d8975a8..b062fa7 100644
--- a/scripts/run_example_O6POINT.py
+++ b/scripts/run_example_O6POINT.py
@@ -1,269 +1,269 @@
-"""
-TEST Function example how to use the NC Analyzer
-
-Copyright (C) <2024> .
-"""
-
-from pathlib import Path
-import sys
-import os
-
-# Configuration: set to True to save the produced plot to a PNG file instead
-# of (or in addition to) showing it interactively. Adjust the path as needed.
-SAVE_PLOT_TO_PNG = False
-# Example: 'plots/O6POINT.png' (relative to repo root). Parent directories
-# will be created automatically when saving.
-PLOT_OUTPUT_PATH = Path("plots") / "O6POINT.png"
-from typing import List, Tuple, Any
-
-# Use package imports from the project
-from ncplot7py.application.nc_execution import NCExecutionEngine
-from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n
-from ncplot7py.shared.nc_nodes import NCCommandNode
-from ncplot7py.shared.file_adapter import get_program
-from ncplot7py.infrastructure.machines.stateful_star_turn_control import StatefulIsoTurnNCControl
-
-
-class _Point:
- def __init__(self, x: float, y: float, z: float):
- self.x = x
- self.y = y
- self.z = z
-
-
-class FileBasedFakeControl:
- """A minimal fake control that converts parsed NC nodes into simple toolpath points.
-
- It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present)
- and creates 1-point lines with a fixed time per command so the engine can
- produce a plot structure for demonstration and testing.
- """
-
- def __init__(self):
- self._canal_nodes = {}
-
- def get_canal_count(self) -> int:
- return 1
-
- def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None:
- # store the nodes for later retrieval
- self._canal_nodes[canal] = list(node_list)
-
- def get_tool_path(self, canal: int):
- nodes = self._canal_nodes.get(canal, [])
- path = []
- for n in nodes:
- params = getattr(n, "command_parameter", {})
- try:
- x = float(params.get("X", 0.0))
- except Exception:
- x = 0.0
- try:
- y = float(params.get("Y", 0.0))
- except Exception:
- y = 0.0
- try:
- z = float(params.get("Z", 0.0))
- except Exception:
- z = 0.0
- # one-point line segment, time 0.1s
- path.append(([_Point(x, y, z)], 0.1))
- return path
-
- def get_exected_nodes(self, canal: int):
- return self._canal_nodes.get(canal, [])
-
- def get_canal_name(self, idx: int) -> str:
- return f"C{idx}"
-
- def synchro_points(self, tool_paths, nodes):
- # no-op for fake control
- return None
-
-
-def read_program_from_file(path: Path) -> list:
- """Read a file and return one or more program strings using the file adapter.
-
- Returns a list of program strings (each string contains commands separated
- by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot.
- """
- # Delegate to the shared file_adapter which handles parentheses removal and
- # splitting into multiple programs if blank lines are used as separators.
- return get_program(path, split_on_blank_line=False)
-
-
-def main(plot: bool = False) -> int:
- # Configure logging to capture messages in the in-memory web buffer so
- # we can print them after the run.
- configure_logging(console=True, web_buffer=True)
- configure_i18n()
-
- # Resolve path to data file (repo root / data / nc-examples / O0004)
- repo_root = Path(__file__).resolve().parent.parent
- data_file = repo_root / "data" / "nc-examples" / "O6POINT"
-
- if not data_file.exists():
- print(f"Data file not found: {data_file}")
- return 2
-
- programs = read_program_from_file(data_file)
-
- # Use the stateful ISO-turn control implementation for execution.
- # If the input file contains multiple programs (split on blank lines),
- # create a control with the matching number of canals to avoid
- # "Canal X not configured" errors when each program is intended for a
- # separate canal.
- control = StatefulIsoTurnNCControl(count_of_canals=max(1, len(programs)))
- engine = NCExecutionEngine(control)
-
- # get_Syncro_plot expects a list of program strings; our adapter returns
- # that directly, so pass it through.
- plot_result = engine.get_Syncro_plot(programs, synch=False)
-
- print("Returned structure type:", type(plot_result))
- print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a")
- print("Calculated runtime (engine):", engine.get_cacluated_runtime())
-
- print("Captured messages:")
- print(get_message_stack())
-
- # Optionally plot if matplotlib is available and a result was produced
- if plot and isinstance(plot_result, list) and len(plot_result) > 0:
- try:
- import matplotlib.pyplot as plt
- from mpl_toolkits.mplot3d import Axes3D # noqa: F401
-
- all_x, all_y, all_z = [], [], []
- for canal in plot_result:
- # canal may be a dict with a 'plot' key (new API) or a plain list
- # (legacy/alternate implementations). Handle both safely.
- if isinstance(canal, dict):
- plot_lines = canal.get("plot", [])
- elif isinstance(canal, (list, tuple)):
- plot_lines = canal
- else:
- # unknown format, skip
- continue
-
- for line in plot_lines:
- if isinstance(line, dict):
- # expected shape from NCExecutionEngine
- all_x.extend(line.get("x", []))
- all_y.extend(line.get("y", []))
- all_z.extend(line.get("z", []))
- else:
- # fallback: line may be a (points_list, time) tuple
- try:
- pts = line[0]
- for p in pts:
- all_x.append(getattr(p, "x", None))
- all_y.append(getattr(p, "y", None))
- all_z.append(getattr(p, "z", None))
- except Exception:
- # give up on this line
- continue
-
- if all_x and all_y:
- # decide whether to show 3D or 2D: if Z is essentially flat
- # (all zeros or None) prefer a 2D X vs Y plot similar to the
- # provided reference image.
- z_vals = [v for v in all_z if v is not None] if all_z else []
- z_range = (max(z_vals) - min(z_vals)) if z_vals else 0.0
-
- if z_range > 1e-6:
- # 3D plot
- fig = plt.figure(1)
- ax = fig.add_subplot(111, projection='3d')
- ax.plot(all_x, all_y, all_z, label='parametric curve')
- ax.set_xlabel('x')
- ax.set_ylabel('y')
- ax.set_zlabel('z')
- plt.title('NC example O6POINT (3D)')
- # save if requested, otherwise show
- if SAVE_PLOT_TO_PNG:
- PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
- fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
- print(f"Saved plot to {PLOT_OUTPUT_PATH}")
- else:
- plt.show()
- else:
- # 2D plot (X horizontal, Y vertical) with red axis lines
- fig, ax = plt.subplots(figsize=(5, 6))
-
- # Prepare XY lists, filter out None values
- xs = [v for v in all_x if v is not None]
- ys = [v for v in all_y if v is not None]
- if not xs or not ys:
- # nothing to plot
- if SAVE_PLOT_TO_PNG:
- PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
- fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
- print(f"Saved plot to {PLOT_OUTPUT_PATH}")
- else:
- plt.show()
- else:
- # Close the polygon path if not already closed
- if xs[0] != xs[-1] or ys[0] != ys[-1]:
- xs = xs + [xs[0]]
- ys = ys + [ys[0]]
-
- # Draw polygon with rounded joins to resemble the reference
- ax.plot(xs, ys, color='black', linewidth=2, solid_capstyle='round', solid_joinstyle='round')
-
- ax.set_xlabel('X')
- ax.set_ylabel('Y')
- ax.set_title('NC example O6POINT')
- ax.set_aspect('equal', adjustable='datalim')
-
- # Compute centroid and draw red axes through it (like reference)
- try:
- cx = sum(xs[:-1]) / (len(xs) - 1)
- cy = sum(ys[:-1]) / (len(ys) - 1)
- except Exception:
- cx, cy = 0.0, 0.0
-
- # draw red axes lines through centroid
- ax.axhline(cy, color='red', linewidth=1)
- ax.axvline(cx, color='red', linewidth=1)
-
- # annotate axis labels similar to reference image
- xlim = ax.get_xlim()
- ylim = ax.get_ylim()
- # place X label to the right, Y label on top
- ax.text(xlim[1], cy, 'X', color='red', verticalalignment='bottom', horizontalalignment='right')
- ax.text(cx, ylim[1], 'Y', color='red', verticalalignment='top', horizontalalignment='left')
-
- # tighten limits with small padding
- pad_x = (max(xs) - min(xs)) * 0.12 if (max(xs) - min(xs)) > 0 else 1.0
- pad_y = (max(ys) - min(ys)) * 0.12 if (max(ys) - min(ys)) > 0 else 1.0
- ax.set_xlim(min(xs) - pad_x, max(xs) + pad_x)
- ax.set_ylim(min(ys) - pad_y, max(ys) + pad_y)
-
- # save if requested, otherwise show
- if SAVE_PLOT_TO_PNG:
- PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
- fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
- print(f"Saved plot to {PLOT_OUTPUT_PATH}")
- else:
- plt.show()
- except Exception as e:
- print(f"Matplotlib plot failed: {e}")
-
- return 0
-
-
-if __name__ == '__main__':
- sys.exit(main(plot=True))
+"""
+TEST Function example how to use the NC Analyzer
+
+Copyright (C) <2024> .
+"""
+
+from pathlib import Path
+import sys
+import os
+
+# Configuration: set to True to save the produced plot to a PNG file instead
+# of (or in addition to) showing it interactively. Adjust the path as needed.
+SAVE_PLOT_TO_PNG = False
+# Example: 'plots/O6POINT.png' (relative to repo root). Parent directories
+# will be created automatically when saving.
+PLOT_OUTPUT_PATH = Path("plots") / "O6POINT.png"
+from typing import List, Tuple, Any
+
+# Use package imports from the project
+from ncplot7py.application.nc_execution import NCExecutionEngine
+from ncplot7py.shared import configure_logging, get_message_stack, configure_i18n
+from ncplot7py.shared.nc_nodes import NCCommandNode
+from ncplot7py.shared.file_adapter import get_program
+from ncplot7py.infrastructure.machines.stateful_star_turn_control import StatefulIsoTurnNCControl
+
+
+class _Point:
+ def __init__(self, x: float, y: float, z: float):
+ self.x = x
+ self.y = y
+ self.z = z
+
+
+class FileBasedFakeControl:
+ """A minimal fake control that converts parsed NC nodes into simple toolpath points.
+
+ It doesn't perform kinematics — it simply reads X/Y/Z parameters (if present)
+ and creates 1-point lines with a fixed time per command so the engine can
+ produce a plot structure for demonstration and testing.
+ """
+
+ def __init__(self):
+ self._canal_nodes = {}
+
+ def get_canal_count(self) -> int:
+ return 1
+
+ def run_nc_code_list(self, node_list: List[NCCommandNode], canal: int) -> None:
+ # store the nodes for later retrieval
+ self._canal_nodes[canal] = list(node_list)
+
+ def get_tool_path(self, canal: int):
+ nodes = self._canal_nodes.get(canal, [])
+ path = []
+ for n in nodes:
+ params = getattr(n, "command_parameter", {})
+ try:
+ x = float(params.get("X", 0.0))
+ except Exception:
+ x = 0.0
+ try:
+ y = float(params.get("Y", 0.0))
+ except Exception:
+ y = 0.0
+ try:
+ z = float(params.get("Z", 0.0))
+ except Exception:
+ z = 0.0
+ # one-point line segment, time 0.1s
+ path.append(([_Point(x, y, z)], 0.1))
+ return path
+
+ def get_exected_nodes(self, canal: int):
+ return self._canal_nodes.get(canal, [])
+
+ def get_canal_name(self, idx: int) -> str:
+ return f"C{idx}"
+
+ def synchro_points(self, tool_paths, nodes):
+ # no-op for fake control
+ return None
+
+
+def read_program_from_file(path: Path) -> list:
+ """Read a file and return one or more program strings using the file adapter.
+
+ Returns a list of program strings (each string contains commands separated
+ by ';') suitable for passing directly to NCExecutionEngine.get_Syncro_plot.
+ """
+ # Delegate to the shared file_adapter which handles parentheses removal and
+ # splitting into multiple programs if blank lines are used as separators.
+ return get_program(path, split_on_blank_line=False)
+
+
+def main(plot: bool = False) -> int:
+ # Configure logging to capture messages in the in-memory web buffer so
+ # we can print them after the run.
+ configure_logging(console=True, web_buffer=True)
+ configure_i18n()
+
+ # Resolve path to data file (repo root / data / nc-examples / O0004)
+ repo_root = Path(__file__).resolve().parent.parent
+ data_file = repo_root / "data" / "nc-examples" / "O6POINT"
+
+ if not data_file.exists():
+ print(f"Data file not found: {data_file}")
+ return 2
+
+ programs = read_program_from_file(data_file)
+
+ # Use the stateful ISO-turn control implementation for execution.
+ # If the input file contains multiple programs (split on blank lines),
+ # create a control with the matching number of canals to avoid
+ # "Canal X not configured" errors when each program is intended for a
+ # separate canal.
+ control = StatefulIsoTurnNCControl(count_of_canals=max(1, len(programs)))
+ engine = NCExecutionEngine(control)
+
+ # get_Syncro_plot expects a list of program strings; our adapter returns
+ # that directly, so pass it through.
+ plot_result = engine.get_Syncro_plot(programs, synch=False)
+
+ print("Returned structure type:", type(plot_result))
+ print("Number of canals:", len(plot_result) if isinstance(plot_result, list) else "n/a")
+ print("Calculated runtime (engine):", engine.get_cacluated_runtime())
+
+ print("Captured messages:")
+ print(get_message_stack())
+
+ # Optionally plot if matplotlib is available and a result was produced
+ if plot and isinstance(plot_result, list) and len(plot_result) > 0:
+ try:
+ import matplotlib.pyplot as plt
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401
+
+ all_x, all_y, all_z = [], [], []
+ for canal in plot_result:
+ # canal may be a dict with a 'plot' key (new API) or a plain list
+ # (legacy/alternate implementations). Handle both safely.
+ if isinstance(canal, dict):
+ plot_lines = canal.get("plot", [])
+ elif isinstance(canal, (list, tuple)):
+ plot_lines = canal
+ else:
+ # unknown format, skip
+ continue
+
+ for line in plot_lines:
+ if isinstance(line, dict):
+ # expected shape from NCExecutionEngine
+ all_x.extend(line.get("x", []))
+ all_y.extend(line.get("y", []))
+ all_z.extend(line.get("z", []))
+ else:
+ # fallback: line may be a (points_list, time) tuple
+ try:
+ pts = line[0]
+ for p in pts:
+ all_x.append(getattr(p, "x", None))
+ all_y.append(getattr(p, "y", None))
+ all_z.append(getattr(p, "z", None))
+ except Exception:
+ # give up on this line
+ continue
+
+ if all_x and all_y:
+ # decide whether to show 3D or 2D: if Z is essentially flat
+ # (all zeros or None) prefer a 2D X vs Y plot similar to the
+ # provided reference image.
+ z_vals = [v for v in all_z if v is not None] if all_z else []
+ z_range = (max(z_vals) - min(z_vals)) if z_vals else 0.0
+
+ if z_range > 1e-6:
+ # 3D plot
+ fig = plt.figure(1)
+ ax = fig.add_subplot(111, projection='3d')
+ ax.plot(all_x, all_y, all_z, label='parametric curve')
+ ax.set_xlabel('x')
+ ax.set_ylabel('y')
+ ax.set_zlabel('z')
+ plt.title('NC example O6POINT (3D)')
+ # save if requested, otherwise show
+ if SAVE_PLOT_TO_PNG:
+ PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
+ print(f"Saved plot to {PLOT_OUTPUT_PATH}")
+ else:
+ plt.show()
+ else:
+ # 2D plot (X horizontal, Y vertical) with red axis lines
+ fig, ax = plt.subplots(figsize=(5, 6))
+
+ # Prepare XY lists, filter out None values
+ xs = [v for v in all_x if v is not None]
+ ys = [v for v in all_y if v is not None]
+ if not xs or not ys:
+ # nothing to plot
+ if SAVE_PLOT_TO_PNG:
+ PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
+ print(f"Saved plot to {PLOT_OUTPUT_PATH}")
+ else:
+ plt.show()
+ else:
+ # Close the polygon path if not already closed
+ if xs[0] != xs[-1] or ys[0] != ys[-1]:
+ xs = xs + [xs[0]]
+ ys = ys + [ys[0]]
+
+ # Draw polygon with rounded joins to resemble the reference
+ ax.plot(xs, ys, color='black', linewidth=2, solid_capstyle='round', solid_joinstyle='round')
+
+ ax.set_xlabel('X')
+ ax.set_ylabel('Y')
+ ax.set_title('NC example O6POINT')
+ ax.set_aspect('equal', adjustable='datalim')
+
+ # Compute centroid and draw red axes through it (like reference)
+ try:
+ cx = sum(xs[:-1]) / (len(xs) - 1)
+ cy = sum(ys[:-1]) / (len(ys) - 1)
+ except Exception:
+ cx, cy = 0.0, 0.0
+
+ # draw red axes lines through centroid
+ ax.axhline(cy, color='red', linewidth=1)
+ ax.axvline(cx, color='red', linewidth=1)
+
+ # annotate axis labels similar to reference image
+ xlim = ax.get_xlim()
+ ylim = ax.get_ylim()
+ # place X label to the right, Y label on top
+ ax.text(xlim[1], cy, 'X', color='red', verticalalignment='bottom', horizontalalignment='right')
+ ax.text(cx, ylim[1], 'Y', color='red', verticalalignment='top', horizontalalignment='left')
+
+ # tighten limits with small padding
+ pad_x = (max(xs) - min(xs)) * 0.12 if (max(xs) - min(xs)) > 0 else 1.0
+ pad_y = (max(ys) - min(ys)) * 0.12 if (max(ys) - min(ys)) > 0 else 1.0
+ ax.set_xlim(min(xs) - pad_x, max(xs) + pad_x)
+ ax.set_ylim(min(ys) - pad_y, max(ys) + pad_y)
+
+ # save if requested, otherwise show
+ if SAVE_PLOT_TO_PNG:
+ PLOT_OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
+ fig.savefig(str(PLOT_OUTPUT_PATH), bbox_inches='tight')
+ print(f"Saved plot to {PLOT_OUTPUT_PATH}")
+ else:
+ plt.show()
+ except Exception as e:
+ print(f"Matplotlib plot failed: {e}")
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(plot=True))
diff --git a/src/ncplot7py/__init__.py b/src/ncplot7py/__init__.py
index 5bf848f..52643d1 100644
--- a/src/ncplot7py/__init__.py
+++ b/src/ncplot7py/__init__.py
@@ -1,7 +1,7 @@
-"""ncplot7py package.
-
-Lightweight package initialiser. The package follows a clean architecture
-layout: domain, application, interfaces, infrastructure, shared, and cli.
-"""
-
-__all__ = ["cli", "domain", "application", "interfaces", "infrastructure", "shared"]
+"""ncplot7py package.
+
+Lightweight package initialiser. The package follows a clean architecture
+layout: domain, application, interfaces, infrastructure, shared, and cli.
+"""
+
+__all__ = ["cli", "domain", "application", "interfaces", "infrastructure", "shared"]
diff --git a/src/ncplot7py/__pycache__/__init__.cpython-313.pyc b/src/ncplot7py/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..1dbcb28
Binary files /dev/null and b/src/ncplot7py/__pycache__/__init__.cpython-313.pyc differ
diff --git a/src/ncplot7py/application/__pycache__/nc_execution.cpython-313.pyc b/src/ncplot7py/application/__pycache__/nc_execution.cpython-313.pyc
new file mode 100644
index 0000000..128327c
Binary files /dev/null and b/src/ncplot7py/application/__pycache__/nc_execution.cpython-313.pyc differ
diff --git a/src/ncplot7py/application/nc_execution.py b/src/ncplot7py/application/nc_execution.py
index 81fd32e..baac224 100644
--- a/src/ncplot7py/application/nc_execution.py
+++ b/src/ncplot7py/application/nc_execution.py
@@ -1,221 +1,221 @@
-"""NC execution engine (renamed from `nc_analyzer`).
-
-Provides `NCExecutionEngine` which is the new name for the orchestrator that
-parses NC programs, delegates execution to an NC control implementation and
-produces toolpath plot data. This keeps the public return values compatible
-with the previous implementation.
-"""
-from __future__ import annotations
-
-import time
-from typing import List, Dict, Optional, Any
-
-from ncplot7py.shared import (
- configure_logging,
- print_error,
- print_message,
- print_translated_error,
- get_message_stack,
- configure_i18n,
-)
-from ncplot7py.shared.registry import registry
-
-
-class NCExecutionEngine:
- """NC Execution Engine.
-
- This class preserves the original `get_Syncro_plot` return structure
- (a list of canal dictionaries, or `[[],[]]` on error) to avoid changing
- the public API while improving internal structure and naming.
- """
-
- def __init__(
- self,
- cnc_control: Any,
- ) -> None:
- """Initialize the engine.
-
- Parameters
- ----------
- cnc_control:
- An object implementing the NC control interface (run_nc_code_list,
- get_tool_path, get_exected_nodes, get_canal_name, get_canal_count,
- synchro_points).
- """
- self.cnc_control = cnc_control
- self.caclulatet_runtime: float = -1.0
- # If control offers canal count, use it, otherwise default to 1
- try:
- self.count_of_canals = int(self.cnc_control.get_canal_count())
- except Exception:
- self.count_of_canals = 1
-
- # Ensure logging and i18n are configured (caller can reconfigure)
- configure_logging(console=True, web_buffer=False)
- configure_i18n()
-
- def get_cacluated_runtime(self) -> float:
- return self.caclulatet_runtime
-
- def _ensure_parser(self):
- # Ensure a parser is registered (same strategy as cli.bootstrap)
- if registry.get("parser", "nc_command") is None:
- try:
- from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p
-
- _reg_p(registry)
- except Exception:
- # If parser registration fails, let parse attempts raise
- pass
-
- def _get_parser(self):
- self._ensure_parser()
- parser_cls = registry.get("parser", "nc_command")
- if parser_cls is None:
- # try by interface name
- parser_cls = registry.get("parser", "BaseNCCommandParser")
- if parser_cls is None:
- raise RuntimeError("No NC command parser registered")
- return parser_cls()
-
- def get_Syncro_plot(self, programs: List[str], synch: bool) -> List[Dict]:
- """Create the plot for the given NC `programs`.
-
- Parameters
- ----------
- programs: list[str]
- Each program is a string containing NC commands separated by ';'.
- synch: bool
- Whether to attempt synchronization across canals.
-
- Returns
- -------
- list[dict]
- A list of canal dictionaries in the same shape as the original
- implementation. On error returns `[[],[]]` to match previous API.
- """
- parser = None
- try:
- parser = self._get_parser()
- except Exception as e:
- print_error(f"Parser setup failed: {e}")
- return [[], []]
-
- canal_number = 0
- tool_paths: List[Any] = []
- nodes: List[Any] = []
- error = False
-
- for program in programs:
- # Parse program into a list of command nodes
- node_list = []
- i = 0
- try:
- for raw_line in [p for p in program.split(";") if p.strip()]:
- node = parser.parse(raw_line, i)
- node_list.append(node)
- i += 1
-
- # Delegate execution to control; many controls accept an iterable
- # of nodes. Keep canal numbering consistent with callers (+1).
- self.cnc_control.run_nc_code_list(node_list, canal_number + 1)
- except Exception as exc:
- # Try to handle known control exception style (has log_route)
- log_route = getattr(exc, "log_route", None)
- if log_route:
- # Try to format exception nodes using MessageCatalog when available
- try:
- from ncplot7py.domain.i18n import MessageCatalog
-
- catalog = MessageCatalog()
- for node in log_route:
- msg = catalog.format_exception(node)
- print_error(msg)
- except Exception:
- print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}")
- else:
- # generic parser/control error
- print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}")
- error = True
- break
-
- # Try to collect results for this canal
- try:
- tool_paths.append(self.cnc_control.get_tool_path(canal_number + 1))
- nodes.append(self.cnc_control.get_exected_nodes(canal_number + 1))
- except Exception as exc:
- print_error(f"Error creating tool path: {exc}")
- error = True
- break
-
- canal_number += 1
-
- # Synchronize across canals if requested
- try:
- if not error and self.count_of_canals > 1 and synch:
- self.cnc_control.synchro_points(tool_paths, nodes)
- except Exception as e:
- log_route = getattr(e, "log_route", None)
- if log_route:
- try:
- from ncplot7py.domain.i18n import MessageCatalog
-
- catalog = MessageCatalog()
- for node in log_route:
- msg = catalog.format_exception(node)
- print_error(msg)
- except Exception:
- print_error(f"Synchronization error: {e}")
- else:
- print_error(f"Synchronization error: {e}")
- error = True
-
- # Build plots if no error
- if not error:
- lines_list: List[Dict] = []
- runtime = 0.0
- canal_index = 0
- for tool_path in tool_paths:
- lines: List[Dict] = []
- linesExec: List[int] = []
- for line in tool_path:
- # each line expected to be (points_list, t)
- try:
- l, t = line
- except Exception:
- # unknown format, skip
- continue
- x = []
- y = []
- z = []
- for point in l:
- if point is not None:
- x.append(getattr(point, "x", None))
- y.append(getattr(point, "y", None))
- z.append(getattr(point, "z", None))
- lines.append({"x": x, "y": y, "z": z, "t": t})
- try:
- runtime += float(t)
- except Exception:
- pass
-
- self.caclulatet_runtime = runtime
-
- if len(tool_paths) == len(nodes):
- for node in nodes[canal_index]:
- linesExec.append(getattr(node, "nc_code_line_nr", None))
-
- canal = {
- "plot": lines,
- "canalNr": self.cnc_control.get_canal_name(canal_index),
- "programExec": linesExec,
- }
- canal_index += 1
- lines_list.append(canal)
-
- return lines_list
-
- # On error, preserve prior API return value
- print_message("Error in NC Code")
- print_message(get_message_stack())
- return [[], []]
+"""NC execution engine (renamed from `nc_analyzer`).
+
+Provides `NCExecutionEngine` which is the new name for the orchestrator that
+parses NC programs, delegates execution to an NC control implementation and
+produces toolpath plot data. This keeps the public return values compatible
+with the previous implementation.
+"""
+from __future__ import annotations
+
+import time
+from typing import List, Dict, Optional, Any
+
+from ncplot7py.shared import (
+ configure_logging,
+ print_error,
+ print_message,
+ print_translated_error,
+ get_message_stack,
+ configure_i18n,
+)
+from ncplot7py.shared.registry import registry
+
+
+class NCExecutionEngine:
+ """NC Execution Engine.
+
+ This class preserves the original `get_Syncro_plot` return structure
+ (a list of canal dictionaries, or `[[],[]]` on error) to avoid changing
+ the public API while improving internal structure and naming.
+ """
+
+ def __init__(
+ self,
+ cnc_control: Any,
+ ) -> None:
+ """Initialize the engine.
+
+ Parameters
+ ----------
+ cnc_control:
+ An object implementing the NC control interface (run_nc_code_list,
+ get_tool_path, get_exected_nodes, get_canal_name, get_canal_count,
+ synchro_points).
+ """
+ self.cnc_control = cnc_control
+ self.caclulatet_runtime: float = -1.0
+ # If control offers canal count, use it, otherwise default to 1
+ try:
+ self.count_of_canals = int(self.cnc_control.get_canal_count())
+ except Exception:
+ self.count_of_canals = 1
+
+ # Ensure logging and i18n are configured (caller can reconfigure)
+ configure_logging(console=True, web_buffer=False)
+ configure_i18n()
+
+ def get_cacluated_runtime(self) -> float:
+ return self.caclulatet_runtime
+
+ def _ensure_parser(self):
+ # Ensure a parser is registered (same strategy as cli.bootstrap)
+ if registry.get("parser", "nc_command") is None:
+ try:
+ from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p
+
+ _reg_p(registry)
+ except Exception:
+ # If parser registration fails, let parse attempts raise
+ pass
+
+ def _get_parser(self):
+ self._ensure_parser()
+ parser_cls = registry.get("parser", "nc_command")
+ if parser_cls is None:
+ # try by interface name
+ parser_cls = registry.get("parser", "BaseNCCommandParser")
+ if parser_cls is None:
+ raise RuntimeError("No NC command parser registered")
+ return parser_cls()
+
+ def get_Syncro_plot(self, programs: List[str], synch: bool) -> List[Dict]:
+ """Create the plot for the given NC `programs`.
+
+ Parameters
+ ----------
+ programs: list[str]
+ Each program is a string containing NC commands separated by ';'.
+ synch: bool
+ Whether to attempt synchronization across canals.
+
+ Returns
+ -------
+ list[dict]
+ A list of canal dictionaries in the same shape as the original
+ implementation. On error returns `[[],[]]` to match previous API.
+ """
+ parser = None
+ try:
+ parser = self._get_parser()
+ except Exception as e:
+ print_error(f"Parser setup failed: {e}")
+ return [[], []]
+
+ canal_number = 0
+ tool_paths: List[Any] = []
+ nodes: List[Any] = []
+ error = False
+
+ for program in programs:
+ # Parse program into a list of command nodes
+ node_list = []
+ i = 0
+ try:
+ for raw_line in [p for p in program.split(";") if p.strip()]:
+ node = parser.parse(raw_line, i)
+ node_list.append(node)
+ i += 1
+
+ # Delegate execution to control; many controls accept an iterable
+ # of nodes. Keep canal numbering consistent with callers (+1).
+ self.cnc_control.run_nc_code_list(node_list, canal_number + 1)
+ except Exception as exc:
+ # Try to handle known control exception style (has log_route)
+ log_route = getattr(exc, "log_route", None)
+ if log_route:
+ # Try to format exception nodes using MessageCatalog when available
+ try:
+ from ncplot7py.domain.i18n import MessageCatalog
+
+ catalog = MessageCatalog()
+ for node in log_route:
+ msg = catalog.format_exception(node)
+ print_error(msg)
+ except Exception:
+ print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}")
+ else:
+ # generic parser/control error
+ print_error(f"NC PARSER/CONTROL ERROR AT LINE {i+1}: {exc}")
+ error = True
+ break
+
+ # Try to collect results for this canal
+ try:
+ tool_paths.append(self.cnc_control.get_tool_path(canal_number + 1))
+ nodes.append(self.cnc_control.get_exected_nodes(canal_number + 1))
+ except Exception as exc:
+ print_error(f"Error creating tool path: {exc}")
+ error = True
+ break
+
+ canal_number += 1
+
+ # Synchronize across canals if requested
+ try:
+ if not error and self.count_of_canals > 1 and synch:
+ self.cnc_control.synchro_points(tool_paths, nodes)
+ except Exception as e:
+ log_route = getattr(e, "log_route", None)
+ if log_route:
+ try:
+ from ncplot7py.domain.i18n import MessageCatalog
+
+ catalog = MessageCatalog()
+ for node in log_route:
+ msg = catalog.format_exception(node)
+ print_error(msg)
+ except Exception:
+ print_error(f"Synchronization error: {e}")
+ else:
+ print_error(f"Synchronization error: {e}")
+ error = True
+
+ # Build plots if no error
+ if not error:
+ lines_list: List[Dict] = []
+ runtime = 0.0
+ canal_index = 0
+ for tool_path in tool_paths:
+ lines: List[Dict] = []
+ linesExec: List[int] = []
+ for line in tool_path:
+ # each line expected to be (points_list, t)
+ try:
+ l, t = line
+ except Exception:
+ # unknown format, skip
+ continue
+ x = []
+ y = []
+ z = []
+ for point in l:
+ if point is not None:
+ x.append(getattr(point, "x", None))
+ y.append(getattr(point, "y", None))
+ z.append(getattr(point, "z", None))
+ lines.append({"x": x, "y": y, "z": z, "t": t})
+ try:
+ runtime += float(t)
+ except Exception:
+ pass
+
+ self.caclulatet_runtime = runtime
+
+ if len(tool_paths) == len(nodes):
+ for node in nodes[canal_index]:
+ linesExec.append(getattr(node, "nc_code_line_nr", None))
+
+ canal = {
+ "plot": lines,
+ "canalNr": self.cnc_control.get_canal_name(canal_index),
+ "programExec": linesExec,
+ }
+ canal_index += 1
+ lines_list.append(canal)
+
+ return lines_list
+
+ # On error, preserve prior API return value
+ print_message("Error in NC Code")
+ print_message(get_message_stack())
+ return [[], []]
diff --git a/src/ncplot7py/cli/__pycache__/__init__.cpython-313.pyc b/src/ncplot7py/cli/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..a3e2457
Binary files /dev/null and b/src/ncplot7py/cli/__pycache__/__init__.cpython-313.pyc differ
diff --git a/src/ncplot7py/cli/__pycache__/main.cpython-313.pyc b/src/ncplot7py/cli/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..f3e8719
Binary files /dev/null and b/src/ncplot7py/cli/__pycache__/main.cpython-313.pyc differ
diff --git a/src/ncplot7py/cli/main.py b/src/ncplot7py/cli/main.py
index e14bb45..aa62abe 100644
--- a/src/ncplot7py/cli/main.py
+++ b/src/ncplot7py/cli/main.py
@@ -1,39 +1,39 @@
-"""CLI bootstrap helpers.
-
-Provide a small `bootstrap()` function that registers builtin adapters
-into the shared `registry`. This keeps import-time side-effects out of
-module import and makes tests deterministic.
-"""
-from __future__ import annotations
-
-from typing import Optional
-
-from ncplot7py.shared.registry import registry
-
-
-def bootstrap(reg: Optional[object] = None) -> None:
- """Register built-in adapters (idempotent).
-
- Parameters:
- reg: Optional registry object; if omitted the shared `registry` is used.
- """
- reg = reg or registry
-
- # Avoid repeated registration
- if reg.get("parser", "nc_command") is None:
- try:
- from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p
-
- _reg_p(reg)
- except Exception:
- # Keep bootstrap safe even if optional adapters fail
- pass
-
-
-def main() -> None: # pragma: no cover - CLI entrypoint
- bootstrap()
-
-
-if __name__ == "__main__":
- main()
-
+"""CLI bootstrap helpers.
+
+Provide a small `bootstrap()` function that registers builtin adapters
+into the shared `registry`. This keeps import-time side-effects out of
+module import and makes tests deterministic.
+"""
+from __future__ import annotations
+
+from typing import Optional
+
+from ncplot7py.shared.registry import registry
+
+
+def bootstrap(reg: Optional[object] = None) -> None:
+ """Register built-in adapters (idempotent).
+
+ Parameters:
+ reg: Optional registry object; if omitted the shared `registry` is used.
+ """
+ reg = reg or registry
+
+ # Avoid repeated registration
+ if reg.get("parser", "nc_command") is None:
+ try:
+ from ncplot7py.infrastructure.parsers.nc_command_parser import register as _reg_p
+
+ _reg_p(reg)
+ except Exception:
+ # Keep bootstrap safe even if optional adapters fail
+ pass
+
+
+def main() -> None: # pragma: no cover - CLI entrypoint
+ bootstrap()
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/src/ncplot7py/domain/__pycache__/cnc_state.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/cnc_state.cpython-313.pyc
new file mode 100644
index 0000000..c3d7ce0
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/cnc_state.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/__pycache__/exceptions.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/exceptions.cpython-313.pyc
new file mode 100644
index 0000000..5c6f770
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/exceptions.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/__pycache__/exec_chain.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/exec_chain.cpython-313.pyc
new file mode 100644
index 0000000..2ea0716
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/exec_chain.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/__pycache__/i18n.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/i18n.cpython-313.pyc
new file mode 100644
index 0000000..a0cdb86
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/i18n.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/__pycache__/models.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/models.cpython-313.pyc
new file mode 100644
index 0000000..c08f3d8
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/models.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/__pycache__/nc_nodes.cpython-313.pyc b/src/ncplot7py/domain/__pycache__/nc_nodes.cpython-313.pyc
new file mode 100644
index 0000000..9958f02
Binary files /dev/null and b/src/ncplot7py/domain/__pycache__/nc_nodes.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/cnc_state.py b/src/ncplot7py/domain/cnc_state.py
index 9a83f9b..cbe112a 100644
--- a/src/ncplot7py/domain/cnc_state.py
+++ b/src/ncplot7py/domain/cnc_state.py
@@ -1,195 +1,195 @@
-"""CNCState model moved into domain package.
-
-Same content as the previous `shared.cnc_state` but placed under domain to
-reflect that this is core domain model logic.
-"""
-from __future__ import annotations
-
-from dataclasses import dataclass, field, asdict
-from copy import deepcopy
-from typing import Dict, List, Optional, Tuple
-
-
-AxisName = str
-Numeric = float
-
-
-@dataclass
-class CNCState:
- """Represents the machine state during NC interpretation.
-
- Key pieces of data:
- - modal_groups: mapping of modal group name -> active code (e.g. 'G0', 'G1')
- - axes: mapping axis name -> current coordinate (e.g. 'X': 0.0)
- - offsets: tool/cs offsets applied to axes (same shape as `axes`)
- - feed_rate: current feed value (units depend on `units` modal)
- - spindle_speed: current spindle speed
- - tool_radius: numeric tool radius (for compensation)
- - parameters: generic user/parameter variables (#-style) stored as dict
- - extra: place to store vendor-specific flags (e.g., polar mode axis name)
-
- Methods on the state are intentionally small and side-effecting; callers
- should control transactional behaviour (clone/restore) when needed.
- """
-
- # Modal groups — keys are group identifiers (string), values the active code
- modal_groups: Dict[str, Optional[str]] = field(default_factory=dict)
-
- # Axis positions — use dict to support arbitrary axis sets (X,Y,Z,A,B,C...)
- axes: Dict[AxisName, Numeric] = field(default_factory=lambda: {"X": 0.0, "Y": 0.0, "Z": 0.0})
-
- # Offset axes (tool length, coordinate system offsets, etc.)
- offsets: Dict[AxisName, Numeric] = field(default_factory=dict)
-
- # Multipliers and override feeds per axis (e.g., for scaling or axis-specific feed)
- axis_multipliers: Dict[AxisName, Numeric] = field(default_factory=dict)
- axis_override_feeds: Dict[AxisName, Numeric] = field(default_factory=dict)
-
- # Per-axis unit interpretation: 'radius' or 'diameter'. Internally we
- # prefer to work in radius (true distances). When an axis is marked as
- # 'diameter' incoming values on that axis should be divided by 2.
- axis_units: Dict[AxisName, str] = field(default_factory=lambda: {"X": "radius", "Y": "radius", "Z": "radius"})
-
- # Motion & tooling fields
- feed_rate: Optional[Numeric] = None
- spindle_speed: Optional[Numeric] = None
- tool_radius: Optional[Numeric] = None
- tool_quadrant: Optional[int] = None
-
- # Program parameters / variables (#500 style) and DDDP table if present
- parameters: Dict[str, Numeric] = field(default_factory=dict)
- dddp_set: Dict[str, Numeric] = field(default_factory=dict)
-
- # Misc
- line_number: int = 0
- loop_command: List[str] = field(default_factory=list)
- extra: Dict[str, object] = field(default_factory=dict)
-
- def clone(self) -> "CNCState":
- """Return a deep copy of the state for transactional updates."""
- return deepcopy(self)
-
- def as_dict(self) -> Dict:
- """Serialize to a plain dict (helpful for debugging/tests)."""
- return asdict(self)
-
- # --- axis helpers -------------------------------------------------
- def get_axis(self, name: AxisName) -> Numeric:
- return self.axes.get(name, 0.0)
-
- def set_axis(self, name: AxisName, value: Numeric) -> None:
- self.axes[name] = float(value)
-
- def update_axes(self, updates: Dict[AxisName, Numeric]) -> None:
- for k, v in updates.items():
- self.set_axis(k, float(v))
-
- # --- axis unit helpers ------------------------------------------
- def get_axis_unit(self, name: AxisName) -> str:
- """Return the unit interpretation for an axis: 'radius' or 'diameter'."""
- return str(self.axis_units.get(name, "radius"))
-
- def set_axis_unit(self, name: AxisName, unit: str) -> None:
- """Set the unit interpretation for an axis. Accepts 'radius' or 'diameter'."""
- self.axis_units[name] = str(unit)
-
- def is_axis_diameter(self, name: AxisName) -> bool:
- return self.get_axis_unit(name).lower() == "diameter"
-
- def normalize_axis_value(self, name: AxisName, value: Numeric) -> Numeric:
- """Normalize a numeric value for the given axis to internal units (radius).
-
- If the axis is marked as 'diameter' this divides the value by 2. Preserves
- sign and numeric type.
- """
- try:
- v = float(value)
- except Exception:
- return 0.0
- return v / 2.0 if self.is_axis_diameter(name) else v
-
- def normalize_target_spec(self, target_spec: Dict[AxisName, Numeric]) -> Dict[AxisName, Numeric]:
- """Return a new target spec with per-axis normalization applied.
-
- This does not mutate the original dict.
- """
- return {ax: self.normalize_axis_value(ax, val) for ax, val in target_spec.items()}
-
- def normalize_arc_params(self, params: Dict[str, Numeric]) -> Dict[str, Numeric]:
- """Normalize common arc parameters (I,J,K,R) into internal units.
-
- - I -> X offset, J -> Y offset, K -> Z offset
- - R is treated as a radial distance: if either X or Y is set to
- 'diameter' we assume R was provided in diameter units and divide by 2.
- This is a reasonable heuristic for XY-plane arcs on lathes.
- """
- p = dict(params)
- if "I" in p:
- p["I"] = float(self.normalize_axis_value("X", p.get("I", 0.0)))
- if "J" in p:
- p["J"] = float(self.normalize_axis_value("Y", p.get("J", 0.0)))
- if "K" in p:
- p["K"] = float(self.normalize_axis_value("Z", p.get("K", 0.0)))
- if "R" in p:
- # Treat R as a true radial distance. Do not alter R based on
- # per-axis 'diameter' interpretation — R is a geometric radius
- # and should match the units of the provided coordinates.
- p["R"] = float(p.get("R", 0.0))
- return p
-
- def apply_offsets(self) -> Dict[AxisName, Numeric]:
- """Return axes with offsets applied (doesn't mutate state)."""
- result = {}
- for ax, pos in self.axes.items():
- off = self.offsets.get(ax, 0.0)
- result[ax] = pos + off
- return result
-
- # --- modal helpers -----------------------------------------------
- def set_modal(self, group: str, code: Optional[str]) -> None:
- """Set the active code for a modal group."""
- self.modal_groups[group] = code
-
- def get_modal(self, group: str) -> Optional[str]:
- return self.modal_groups.get(group)
-
- # --- parameter helpers -------------------------------------------
- def set_parameter(self, name: str, value: Numeric) -> None:
- self.parameters[name] = float(value)
-
- def get_parameter(self, name: str, default: Optional[Numeric] = None) -> Optional[Numeric]:
- return self.parameters.get(name, default)
-
- # --- coordinate resolution ---------------------------------------
- def resolve_target(self, target_spec: Dict[AxisName, Numeric], absolute: bool = True) -> Dict[AxisName, Numeric]:
- """Given a target spec (possibly partial), return resolved absolute coords.
-
- If `absolute` is False, the values in `target_spec` are treated as deltas
- and applied to the current axis positions.
- """
- resolved: Dict[AxisName, Numeric] = {}
- if absolute:
- # start with current axes, then update with provided values
- for ax in set(list(self.axes.keys()) + list(target_spec.keys())):
- if ax in target_spec:
- resolved[ax] = float(target_spec[ax])
- else:
- resolved[ax] = self.get_axis(ax)
- else:
- for ax in set(list(self.axes.keys()) + list(target_spec.keys())):
- delta = float(target_spec.get(ax, 0.0))
- resolved[ax] = self.get_axis(ax) + delta
- return resolved
-
- # --- small utility ------------------------------------------------
- def compute_distance(self, a: Dict[AxisName, Numeric], b: Dict[AxisName, Numeric], axes: Optional[List[AxisName]] = None) -> float:
- """Euclidean distance between two axis positions using given axes list."""
- if axes is None:
- axes = list(set(list(a.keys()) + list(b.keys())))
- s = 0.0
- for ax in axes:
- s += (a.get(ax, 0.0) - b.get(ax, 0.0)) ** 2
- return float(s ** 0.5)
-
-
-__all__ = ["CNCState"]
+"""CNCState model moved into domain package.
+
+Same content as the previous `shared.cnc_state` but placed under domain to
+reflect that this is core domain model logic.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass, field, asdict
+from copy import deepcopy
+from typing import Dict, List, Optional, Tuple
+
+
+AxisName = str
+Numeric = float
+
+
+@dataclass
+class CNCState:
+ """Represents the machine state during NC interpretation.
+
+ Key pieces of data:
+ - modal_groups: mapping of modal group name -> active code (e.g. 'G0', 'G1')
+ - axes: mapping axis name -> current coordinate (e.g. 'X': 0.0)
+ - offsets: tool/cs offsets applied to axes (same shape as `axes`)
+ - feed_rate: current feed value (units depend on `units` modal)
+ - spindle_speed: current spindle speed
+ - tool_radius: numeric tool radius (for compensation)
+ - parameters: generic user/parameter variables (#-style) stored as dict
+ - extra: place to store vendor-specific flags (e.g., polar mode axis name)
+
+ Methods on the state are intentionally small and side-effecting; callers
+ should control transactional behaviour (clone/restore) when needed.
+ """
+
+ # Modal groups — keys are group identifiers (string), values the active code
+ modal_groups: Dict[str, Optional[str]] = field(default_factory=dict)
+
+ # Axis positions — use dict to support arbitrary axis sets (X,Y,Z,A,B,C...)
+ axes: Dict[AxisName, Numeric] = field(default_factory=lambda: {"X": 0.0, "Y": 0.0, "Z": 0.0})
+
+ # Offset axes (tool length, coordinate system offsets, etc.)
+ offsets: Dict[AxisName, Numeric] = field(default_factory=dict)
+
+ # Multipliers and override feeds per axis (e.g., for scaling or axis-specific feed)
+ axis_multipliers: Dict[AxisName, Numeric] = field(default_factory=dict)
+ axis_override_feeds: Dict[AxisName, Numeric] = field(default_factory=dict)
+
+ # Per-axis unit interpretation: 'radius' or 'diameter'. Internally we
+ # prefer to work in radius (true distances). When an axis is marked as
+ # 'diameter' incoming values on that axis should be divided by 2.
+ axis_units: Dict[AxisName, str] = field(default_factory=lambda: {"X": "radius", "Y": "radius", "Z": "radius"})
+
+ # Motion & tooling fields
+ feed_rate: Optional[Numeric] = None
+ spindle_speed: Optional[Numeric] = None
+ tool_radius: Optional[Numeric] = None
+ tool_quadrant: Optional[int] = None
+
+ # Program parameters / variables (#500 style) and DDDP table if present
+ parameters: Dict[str, Numeric] = field(default_factory=dict)
+ dddp_set: Dict[str, Numeric] = field(default_factory=dict)
+
+ # Misc
+ line_number: int = 0
+ loop_command: List[str] = field(default_factory=list)
+ extra: Dict[str, object] = field(default_factory=dict)
+
+ def clone(self) -> "CNCState":
+ """Return a deep copy of the state for transactional updates."""
+ return deepcopy(self)
+
+ def as_dict(self) -> Dict:
+ """Serialize to a plain dict (helpful for debugging/tests)."""
+ return asdict(self)
+
+ # --- axis helpers -------------------------------------------------
+ def get_axis(self, name: AxisName) -> Numeric:
+ return self.axes.get(name, 0.0)
+
+ def set_axis(self, name: AxisName, value: Numeric) -> None:
+ self.axes[name] = float(value)
+
+ def update_axes(self, updates: Dict[AxisName, Numeric]) -> None:
+ for k, v in updates.items():
+ self.set_axis(k, float(v))
+
+ # --- axis unit helpers ------------------------------------------
+ def get_axis_unit(self, name: AxisName) -> str:
+ """Return the unit interpretation for an axis: 'radius' or 'diameter'."""
+ return str(self.axis_units.get(name, "radius"))
+
+ def set_axis_unit(self, name: AxisName, unit: str) -> None:
+ """Set the unit interpretation for an axis. Accepts 'radius' or 'diameter'."""
+ self.axis_units[name] = str(unit)
+
+ def is_axis_diameter(self, name: AxisName) -> bool:
+ return self.get_axis_unit(name).lower() == "diameter"
+
+ def normalize_axis_value(self, name: AxisName, value: Numeric) -> Numeric:
+ """Normalize a numeric value for the given axis to internal units (radius).
+
+ If the axis is marked as 'diameter' this divides the value by 2. Preserves
+ sign and numeric type.
+ """
+ try:
+ v = float(value)
+ except Exception:
+ return 0.0
+ return v / 2.0 if self.is_axis_diameter(name) else v
+
+ def normalize_target_spec(self, target_spec: Dict[AxisName, Numeric]) -> Dict[AxisName, Numeric]:
+ """Return a new target spec with per-axis normalization applied.
+
+ This does not mutate the original dict.
+ """
+ return {ax: self.normalize_axis_value(ax, val) for ax, val in target_spec.items()}
+
+ def normalize_arc_params(self, params: Dict[str, Numeric]) -> Dict[str, Numeric]:
+ """Normalize common arc parameters (I,J,K,R) into internal units.
+
+ - I -> X offset, J -> Y offset, K -> Z offset
+ - R is treated as a radial distance: if either X or Y is set to
+ 'diameter' we assume R was provided in diameter units and divide by 2.
+ This is a reasonable heuristic for XY-plane arcs on lathes.
+ """
+ p = dict(params)
+ if "I" in p:
+ p["I"] = float(self.normalize_axis_value("X", p.get("I", 0.0)))
+ if "J" in p:
+ p["J"] = float(self.normalize_axis_value("Y", p.get("J", 0.0)))
+ if "K" in p:
+ p["K"] = float(self.normalize_axis_value("Z", p.get("K", 0.0)))
+ if "R" in p:
+ # Treat R as a true radial distance. Do not alter R based on
+ # per-axis 'diameter' interpretation — R is a geometric radius
+ # and should match the units of the provided coordinates.
+ p["R"] = float(p.get("R", 0.0))
+ return p
+
+ def apply_offsets(self) -> Dict[AxisName, Numeric]:
+ """Return axes with offsets applied (doesn't mutate state)."""
+ result = {}
+ for ax, pos in self.axes.items():
+ off = self.offsets.get(ax, 0.0)
+ result[ax] = pos + off
+ return result
+
+ # --- modal helpers -----------------------------------------------
+ def set_modal(self, group: str, code: Optional[str]) -> None:
+ """Set the active code for a modal group."""
+ self.modal_groups[group] = code
+
+ def get_modal(self, group: str) -> Optional[str]:
+ return self.modal_groups.get(group)
+
+ # --- parameter helpers -------------------------------------------
+ def set_parameter(self, name: str, value: Numeric) -> None:
+ self.parameters[name] = float(value)
+
+ def get_parameter(self, name: str, default: Optional[Numeric] = None) -> Optional[Numeric]:
+ return self.parameters.get(name, default)
+
+ # --- coordinate resolution ---------------------------------------
+ def resolve_target(self, target_spec: Dict[AxisName, Numeric], absolute: bool = True) -> Dict[AxisName, Numeric]:
+ """Given a target spec (possibly partial), return resolved absolute coords.
+
+ If `absolute` is False, the values in `target_spec` are treated as deltas
+ and applied to the current axis positions.
+ """
+ resolved: Dict[AxisName, Numeric] = {}
+ if absolute:
+ # start with current axes, then update with provided values
+ for ax in set(list(self.axes.keys()) + list(target_spec.keys())):
+ if ax in target_spec:
+ resolved[ax] = float(target_spec[ax])
+ else:
+ resolved[ax] = self.get_axis(ax)
+ else:
+ for ax in set(list(self.axes.keys()) + list(target_spec.keys())):
+ delta = float(target_spec.get(ax, 0.0))
+ resolved[ax] = self.get_axis(ax) + delta
+ return resolved
+
+ # --- small utility ------------------------------------------------
+ def compute_distance(self, a: Dict[AxisName, Numeric], b: Dict[AxisName, Numeric], axes: Optional[List[AxisName]] = None) -> float:
+ """Euclidean distance between two axis positions using given axes list."""
+ if axes is None:
+ axes = list(set(list(a.keys()) + list(b.keys())))
+ s = 0.0
+ for ax in axes:
+ s += (a.get(ax, 0.0) - b.get(ax, 0.0)) ** 2
+ return float(s ** 0.5)
+
+
+__all__ = ["CNCState"]
diff --git a/src/ncplot7py/domain/exceptions.py b/src/ncplot7py/domain/exceptions.py
index 3fa7fd3..288910c 100644
--- a/src/ncplot7py/domain/exceptions.py
+++ b/src/ncplot7py/domain/exceptions.py
@@ -1,139 +1,139 @@
-"""Domain-specific exceptions.
-
-This module provides a small, typed exception class suitable for tracing
-errors originating from CNC/NC code parsing and handling. The class is
-lightweight, serializable and has a pleasant string representation for
-consumers (CLI/IDE/web) so we can present clean error messages to users.
-"""
-
-from __future__ import annotations
-
-from enum import IntEnum
-from dataclasses import dataclass, asdict
-from typing import Any
-
-
-class ExceptionTyps(IntEnum):
- """Types of domain exceptions.
-
- Values are small ints to make serialization compact where necessary.
- """
-
- NCCodeErrors = 1
- NCCanalStarErrors = 2
- CNCError = 3
-
-
-@dataclass
-class ExceptionNode(Exception):
- """A structured exception carrying CNC error information.
-
- Fields:
- typ: Which category of error (ExceptionTyps).
- code: Numeric error code (domain-specific).
- line: Source line number where the error occurred (1-based), 0 if unknown.
- message: Human readable message describing the error.
- value: Optional value/context (e.g. offending token or text).
-
- The class inherits from Exception so it can be raised. It also provides
- `to_dict()` and a friendly `__str__()` for user-facing output.
- """
-
- typ: ExceptionTyps
- code: int = 0
- line: int = 0
- message: str = ""
- value: Any = ""
- # Traceback-ish metadata for better diagnostics
- file: str = ""
- column: int = 0
- context: str = ""
-
- def __post_init__(self) -> None:
- # Ensure base Exception gets the message so built-in tooling sees it.
- Exception.__init__(self, self.message)
-
- def to_dict(self) -> dict:
- """Return a plain-serializable representation of the exception."""
- data = asdict(self)
- # Represent enum as its name (and maybe value) for consumers.
- data["typ"] = {"name": self.typ.name, "value": int(self.typ)}
- return data
-
- def __str__(self) -> str: # pragma: no cover - simple formatting
- parts = [f"{self.typ.name} (code={self.code})"]
- if self.line:
- parts.append(f"line={self.line}")
- if self.message:
- parts.append(f"message={self.message}")
- if self.value not in ("", None):
- parts.append(f"value={self.value}")
- if self.file:
- parts.append(f"file={self.file}")
- if self.column:
- parts.append(f"col={self.column}")
- return "; ".join(parts)
-
- def localized(self, lang: str = "en") -> str:
- """Return a localized message if a catalog is available.
-
- This avoids importing i18n at module import time to keep domain slim.
- """
- try:
- from .i18n import MessageCatalog
-
- catalog = MessageCatalog()
- return catalog.format_exception(self, lang=lang, include_trace=True)
- except Exception:
- return str(self)
-
-
-def raise_nc_error(
- typ: ExceptionTyps,
- code: int,
- *,
- message: str = "",
- value: Any = "",
- file: str = "",
- line: int = 0,
- column: int = 0,
- source_line: str = "",
-) -> None:
- """Raise an ExceptionNode with best-effort column/context extraction.
-
- Parameters:
- typ: Exception type (ExceptionTyps)
- code: Numeric error code
- message: Human readable fallback message if i18n template not found
- value: Offending value/token
- file: Source file name/path
- line: 1-based line number
- column: 1-based column number; if 0 and source_line+value are provided,
- column will be inferred from the first match of value within
- source_line.
- source_line: The full line text to include as context (optional)
- """
-
- inferred_col = column
- ctx = source_line
- if (not inferred_col) and value and source_line:
- try:
- idx = source_line.find(str(value))
- if idx >= 0:
- inferred_col = idx + 1 # 1-based
- except Exception:
- inferred_col = column or 0
-
- exc = ExceptionNode(
- typ=typ,
- code=code,
- line=line,
- message=message,
- value=value,
- file=file,
- column=inferred_col or column,
- context=ctx,
- )
- raise exc
-
-
+"""Domain-specific exceptions.
+
+This module provides a small, typed exception class suitable for tracing
+errors originating from CNC/NC code parsing and handling. The class is
+lightweight, serializable and has a pleasant string representation for
+consumers (CLI/IDE/web) so we can present clean error messages to users.
+"""
+
+from __future__ import annotations
+
+from enum import IntEnum
+from dataclasses import dataclass, asdict
+from typing import Any
+
+
+class ExceptionTyps(IntEnum):
+ """Types of domain exceptions.
+
+ Values are small ints to make serialization compact where necessary.
+ """
+
+ NCCodeErrors = 1
+ NCCanalStarErrors = 2
+ CNCError = 3
+
+
+@dataclass
+class ExceptionNode(Exception):
+ """A structured exception carrying CNC error information.
+
+ Fields:
+ typ: Which category of error (ExceptionTyps).
+ code: Numeric error code (domain-specific).
+ line: Source line number where the error occurred (1-based), 0 if unknown.
+ message: Human readable message describing the error.
+ value: Optional value/context (e.g. offending token or text).
+
+ The class inherits from Exception so it can be raised. It also provides
+ `to_dict()` and a friendly `__str__()` for user-facing output.
+ """
+
+ typ: ExceptionTyps
+ code: int = 0
+ line: int = 0
+ message: str = ""
+ value: Any = ""
+ # Traceback-ish metadata for better diagnostics
+ file: str = ""
+ column: int = 0
+ context: str = ""
+
+ def __post_init__(self) -> None:
+ # Ensure base Exception gets the message so built-in tooling sees it.
+ Exception.__init__(self, self.message)
+
+ def to_dict(self) -> dict:
+ """Return a plain-serializable representation of the exception."""
+ data = asdict(self)
+ # Represent enum as its name (and maybe value) for consumers.
+ data["typ"] = {"name": self.typ.name, "value": int(self.typ)}
+ return data
+
+ def __str__(self) -> str: # pragma: no cover - simple formatting
+ parts = [f"{self.typ.name} (code={self.code})"]
+ if self.line:
+ parts.append(f"line={self.line}")
+ if self.message:
+ parts.append(f"message={self.message}")
+ if self.value not in ("", None):
+ parts.append(f"value={self.value}")
+ if self.file:
+ parts.append(f"file={self.file}")
+ if self.column:
+ parts.append(f"col={self.column}")
+ return "; ".join(parts)
+
+ def localized(self, lang: str = "en") -> str:
+ """Return a localized message if a catalog is available.
+
+ This avoids importing i18n at module import time to keep domain slim.
+ """
+ try:
+ from .i18n import MessageCatalog
+
+ catalog = MessageCatalog()
+ return catalog.format_exception(self, lang=lang, include_trace=True)
+ except Exception:
+ return str(self)
+
+
+def raise_nc_error(
+ typ: ExceptionTyps,
+ code: int,
+ *,
+ message: str = "",
+ value: Any = "",
+ file: str = "",
+ line: int = 0,
+ column: int = 0,
+ source_line: str = "",
+) -> None:
+ """Raise an ExceptionNode with best-effort column/context extraction.
+
+ Parameters:
+ typ: Exception type (ExceptionTyps)
+ code: Numeric error code
+ message: Human readable fallback message if i18n template not found
+ value: Offending value/token
+ file: Source file name/path
+ line: 1-based line number
+ column: 1-based column number; if 0 and source_line+value are provided,
+ column will be inferred from the first match of value within
+ source_line.
+ source_line: The full line text to include as context (optional)
+ """
+
+ inferred_col = column
+ ctx = source_line
+ if (not inferred_col) and value and source_line:
+ try:
+ idx = source_line.find(str(value))
+ if idx >= 0:
+ inferred_col = idx + 1 # 1-based
+ except Exception:
+ inferred_col = column or 0
+
+ exc = ExceptionNode(
+ typ=typ,
+ code=code,
+ line=line,
+ message=message,
+ value=value,
+ file=file,
+ column=inferred_col or column,
+ context=ctx,
+ )
+ raise exc
+
+
diff --git a/src/ncplot7py/domain/exec_chain.py b/src/ncplot7py/domain/exec_chain.py
index 547b9ce..38b4e57 100644
--- a/src/ncplot7py/domain/exec_chain.py
+++ b/src/ncplot7py/domain/exec_chain.py
@@ -1,32 +1,32 @@
-"""Chain-of-Responsibility base Handler moved to domain.
-
-This module contains the Handler primitive; placing it in `domain` makes it
-the canonical location for execution primitives that operate on domain state.
-"""
-from __future__ import annotations
-
-from typing import Optional, Tuple, List, TYPE_CHECKING, Any
-
-from ncplot7py.shared.nc_nodes import NCCommandNode
-from ncplot7py.shared.state_protocol import StateProtocol
-
-if TYPE_CHECKING:
- CNCStateType = StateProtocol
-else:
- CNCStateType = Any
-
-
-class Handler:
- """Base handler with optional next pointer."""
-
- def __init__(self, next_handler: Optional["Handler"] = None):
- self.next_handler = next_handler
-
- def handle(self, node: NCCommandNode, state: CNCStateType) -> Tuple[Optional[List], Optional[float]]:
- """Handle a node; return (points_list, duration) or delegate."""
- if self.next_handler is not None:
- return self.next_handler.handle(node, state)
- return None, None
-
-
-__all__ = ["Handler"]
+"""Chain-of-Responsibility base Handler moved to domain.
+
+This module contains the Handler primitive; placing it in `domain` makes it
+the canonical location for execution primitives that operate on domain state.
+"""
+from __future__ import annotations
+
+from typing import Optional, Tuple, List, TYPE_CHECKING, Any
+
+from ncplot7py.shared.nc_nodes import NCCommandNode
+from ncplot7py.shared.state_protocol import StateProtocol
+
+if TYPE_CHECKING:
+ CNCStateType = StateProtocol
+else:
+ CNCStateType = Any
+
+
+class Handler:
+ """Base handler with optional next pointer."""
+
+ def __init__(self, next_handler: Optional["Handler"] = None):
+ self.next_handler = next_handler
+
+ def handle(self, node: NCCommandNode, state: CNCStateType) -> Tuple[Optional[List], Optional[float]]:
+ """Handle a node; return (points_list, duration) or delegate."""
+ if self.next_handler is not None:
+ return self.next_handler.handle(node, state)
+ return None, None
+
+
+__all__ = ["Handler"]
diff --git a/src/ncplot7py/domain/handlers/__pycache__/control_flow.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/control_flow.cpython-313.pyc
new file mode 100644
index 0000000..74ba854
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/control_flow.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/gcode_group0.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/gcode_group0.cpython-313.pyc
new file mode 100644
index 0000000..dcdc362
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/gcode_group0.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/gcode_group2.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/gcode_group2.cpython-313.pyc
new file mode 100644
index 0000000..a78a568
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/gcode_group2.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/gcode_group5.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/gcode_group5.cpython-313.pyc
new file mode 100644
index 0000000..1a90769
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/gcode_group5.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/modal.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/modal.cpython-313.pyc
new file mode 100644
index 0000000..79d9ac7
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/modal.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/motion.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/motion.cpython-313.pyc
new file mode 100644
index 0000000..ec202a4
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/motion.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/star_turn_handler.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/star_turn_handler.cpython-313.pyc
new file mode 100644
index 0000000..60643ad
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/star_turn_handler.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/__pycache__/variable.cpython-313.pyc b/src/ncplot7py/domain/handlers/__pycache__/variable.cpython-313.pyc
new file mode 100644
index 0000000..2381d8a
Binary files /dev/null and b/src/ncplot7py/domain/handlers/__pycache__/variable.cpython-313.pyc differ
diff --git a/src/ncplot7py/domain/handlers/control_flow.py b/src/ncplot7py/domain/handlers/control_flow.py
index d2ef2b3..173ee1d 100644
--- a/src/ncplot7py/domain/handlers/control_flow.py
+++ b/src/ncplot7py/domain/handlers/control_flow.py
@@ -1,318 +1,318 @@
-"""Control-flow handler for simple loop constructs in NC programs.
-
-Supports a small subset of constructs found in some controllers:
-- GOTO
-- IF ... GOTO
-- WHILE DO / END (basic skip-if-false behaviour)
-
-This handler manipulates node linked pointers (`_next_ncCode`) so the
-canal execution loop can jump to the desired node. It expects nodes to be
-linked via `_next_ncCode` and `_before_ncCode` beforehand.
-"""
-from __future__ import annotations
-
-import re
-from typing import Optional, Tuple
-import os
-import logging
-
-from ncplot7py.domain.exec_chain import Handler
-from ncplot7py.shared.nc_nodes import NCCommandNode
-from ncplot7py.domain.cnc_state import CNCState
-from ncplot7py.domain.handlers.variable import VariableHandler
-
-
-class ControlFlowHandler(Handler):
- TOKEN_RE = re.compile(r"(GOTO|THEN|IF|END|DO|WHILE)")
-
- def __init__(self, next_handler: Optional[Handler] = None):
- super().__init__(next_handler=next_handler)
- # use variable handler utilities for expression evaluation
- self._eval_helper = VariableHandler()
- # runtime maps (populated by canal before execution)
- self._n_map = None
- self._do_map = None
- self._end_map = None
- self._nodes = None
- self._loop_counters = {}
-
- def _find_node_with_N(self, start: NCCommandNode, pos: str) -> Optional[NCCommandNode]:
- # Prefer lookup via precomputed map when available
- try:
- target = float(pos)
- except Exception:
- return None
-
- if getattr(self, "_n_map", None) is not None:
- return self._n_map.get(target)
-
- # fallback: search forward then backward
- node = start
- # search forwards
- while node is not None:
- try:
- nval = node.command_parameter.get("N")
- except Exception:
- nval = None
- if nval is not None:
- try:
- if float(nval) == target:
- return node
- except Exception:
- pass
- node = getattr(node, "_next_ncCode", None)
-
- # search backwards
- node = start
- while node is not None:
- try:
- nval = node.command_parameter.get("N")
- except Exception:
- nval = None
- if nval is not None:
- try:
- if float(nval) == target:
- return node
- except Exception:
- pass
- node = getattr(node, "_before_ncCode", None)
-
- return None
-
- def _find_end_for_do(self, start: NCCommandNode, pos: str) -> Optional[NCCommandNode]:
- # Prefer map lookup when available
- if getattr(self, "_end_map", None) is not None:
- lst = self._end_map.get(pos)
- if lst:
- # return first end node after start
- if self._nodes is not None:
- try:
- start_idx = self._nodes.index(start)
- except ValueError:
- start_idx = -1
- for n in lst:
- try:
- idx = self._nodes.index(n)
- if idx > start_idx:
- return n
- except ValueError:
- continue
- return lst[0]
-
- node = start
- while node is not None:
- lc = node.loop_command
- if lc and f"END{pos}" in lc:
- return node
- node = getattr(node, "_next_ncCode", None)
- return None
-
- def _is_true(self, cond_text: str, state: CNCState) -> bool:
- logger = logging.getLogger(__name__)
- # strip surrounding brackets which may come from parsed tokens
- try:
- cond_text = cond_text.strip()
- if cond_text.startswith("[") and cond_text.endswith("]"):
- cond_text = cond_text[1:-1]
- except Exception:
- pass
- # detect operators GT, LT, GE, LE, EQ
- for op in ("GE", "LE", "GT", "LT", "EQ"):
- if op in cond_text:
- left, right = cond_text.split(op, 1)
- try:
- lv = self._eval_helper._eval_expression(left, state)
- rv = self._eval_helper._eval_expression(right, state)
- except Exception:
- logger.debug("_is_true failed eval for cond=%s left=%s right=%s", cond_text, left, right, exc_info=True)
- try:
- logger.debug("_is_true state.parameters snapshot: %s", getattr(state, 'parameters', None))
- except Exception:
- pass
- return False
- if op == "GT":
- result = float(lv) > float(rv)
- logger.debug("_is_true: %s GT %s -> %s", lv, rv, result)
- return result
- if op == "LT":
- result = float(lv) < float(rv)
- logger.debug("_is_true: %s LT %s -> %s", lv, rv, result)
- return result
- if op == "GE":
- result = float(lv) >= float(rv)
- logger.debug("_is_true: %s GE %s -> %s", lv, rv, result)
- return result
- if op == "LE":
- result = float(lv) <= float(rv)
- logger.debug("_is_true: %s LE %s -> %s", lv, rv, result)
- return result
- if op == "EQ":
- result = float(lv) == float(rv)
- logger.debug("_is_true: %s EQ %s -> %s", lv, rv, result)
- return result
- return False
-
- def handle(self, node: NCCommandNode, state: CNCState) -> Tuple[Optional[list], Optional[float]]:
- lc = node.loop_command
- if not lc:
- return super().handle(node, state)
-
- # Insert spaces before tokens to help splitting (parser removed spaces)
- command = re.sub(self.TOKEN_RE, r" \1", lc)
- tokens = [t for t in command.split(" ") if t]
-
- # simple scan for IF...GOTO or plain GOTO
- for token in tokens:
- if token.startswith("IF"):
- cond = token[2:]
- if self._is_true(cond, state):
- # find GOTO token in same command
- for t2 in tokens:
- if t2.startswith("GOTO"):
- pos = t2.split("GOTO", 1)[1]
- target = self._find_node_with_N(node, pos)
- if target is None:
- # nothing we can do; fallthrough
- break
- node._next_ncCode = target
- break
- # whether true or false, stop processing IF
- break
- elif token.startswith("GOTO"):
- pos = token.split("GOTO", 1)[1]
- # GOTO may reference a DO-label or an N label
- target = None
- try:
- key = float(pos)
- except Exception:
- key = None
- if key is not None and getattr(self, "_n_map", None) is not None:
- target = self._n_map.get(key)
- if target is None and getattr(self, "_do_map", None) is not None:
- # do_map entries are lists; pick first
- lst = self._do_map.get(pos)
- if lst:
- target = lst[0]
- if target is None:
- target = self._find_node_with_N(node, pos)
- if target is not None:
- node._next_ncCode = target
- break
- elif token.startswith("WHILE"):
- cond = token[5:]
- # if condition is false, skip forward to matching END
- if not self._is_true(cond, state):
- # find DO token to get pos
- for t2 in tokens:
- if t2.startswith("DO"):
- pos = t2.split("DO", 1)[1]
- end_node = self._find_end_for_do(node, pos)
- if end_node is not None:
- node._next_ncCode = getattr(end_node, "_next_ncCode", None)
- break
- # otherwise, continue into loop (no change)
- break
- elif token.startswith("DO"):
- # DO may be a label or start of a counted loop. If the DO node
- # provides parameter 'L' we treat it as a loop counter.
- label = token[2:]
- # initialize loop counter if present on this node
- try:
- lval = node.command_parameter.get("L")
- except Exception:
- lval = None
- if lval is not None:
- try:
- cnt = int(float(self._eval_helper._eval_expression(str(lval), state)))
- except Exception:
- try:
- cnt = int(float(lval))
- except Exception:
- cnt = None
- if cnt is not None:
- # store counter by label so END can find it
- self._loop_counters[label] = cnt
- break
- elif token.startswith("END"):
- label = token[3:]
- # find matching DO node (prefer map)
- do_node = None
- if getattr(self, "_do_map", None) is not None and label in self._do_map:
- # pick the nearest DO before this END
- candidates = self._do_map[label]
- if self._nodes is not None:
- try:
- end_idx = self._nodes.index(node)
- except ValueError:
- end_idx = -1
- best = None
- best_idx = -1
- for n in candidates:
- try:
- idx = self._nodes.index(n)
- if idx < end_idx and idx > best_idx:
- best = n
- best_idx = idx
- except ValueError:
- continue
- do_node = best or (candidates[0] if candidates else None)
- else:
- do_node = candidates[0] if candidates else None
- else:
- # fallback: search backward for a DO with same label
- cur = getattr(node, "_before_ncCode", None)
- while cur is not None:
- lc = cur.loop_command
- if lc and f"DO{label}" in lc:
- do_node = cur
- break
- cur = getattr(cur, "_before_ncCode", None)
-
- if do_node is not None:
- # if there's a loop counter for this label, decrement and jump
- cnt = self._loop_counters.get(label)
- if cnt is not None:
- if cnt > 1:
- self._loop_counters[label] = cnt - 1
- node._next_ncCode = getattr(do_node, "_next_ncCode", do_node)
- else:
- # completed loop
- del self._loop_counters[label]
- else:
- # If this DO was actually a WHILE (e.g. "WHILE[...]DO