From 8eb85dab8d7cc1390b66d03110384aa46227ac3f Mon Sep 17 00:00:00 2001 From: Felix Riestra Date: Mon, 21 Sep 2026 18:27:59 +0200 Subject: [PATCH] docs: final review pass over the Python host documentation Rewrite the host-model design note for the current architecture (entities, tables, commands, nested validation, known limits), refresh the plan's current-state header, the ledger and gaps-doc status lines (cadcodec PR #48), the README status and the superseded ocs.command notes, and drop dangling references to a sibling project that is not in this repository. Co-Authored-By: Claude Sonnet 5 --- docs/cadcodec-reader-gaps.md | 7 +- docs/plugin-host-model-completion-plan.md | 25 +-- docs/plugin-host-model-coverage-ledger.md | 4 +- docs/plugin-host-model.md | 216 +++++++++++++--------- plugins/opencad-python/PLUGIN.md | 50 ++--- plugins/opencad-python/README.md | 23 ++- plugins/opencad-python/ROADMAP.md | 18 +- 7 files changed, 179 insertions(+), 164 deletions(-) diff --git a/docs/cadcodec-reader-gaps.md b/docs/cadcodec-reader-gaps.md index a23110ff2..f446c121e 100644 --- a/docs/cadcodec-reader-gaps.md +++ b/docs/cadcodec-reader-gaps.md @@ -1,7 +1,10 @@ # cadcodec DXF reader/writer gaps found by the OCS Python host audit -Draft issue reports for `HakanSeven12/cadcodec` (the `acadrust` crate). Nothing -here has been filed. Every finding was made against **cadcodec revision +Reports on `HakanSeven12/cadcodec` (the `acadrust` crate). **Status (21 September +2026):** fixes for issues 1-6 below, with regression tests, are submitted as +[HakanSeven12/cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48) (branch +`fix/dxf-reader-writer-roundtrip` on `felixriestra/cadcodec`); the five findings under +"Observed but not filed" and the style and block ones added later are not filed. Every finding was made against **cadcodec revision `5b682ed`** (OCS pins `acadrust` to it) by saving a document with `DxfWriter` or `DwgWriter`, reloading it, and comparing entity fields. diff --git a/docs/plugin-host-model-completion-plan.md b/docs/plugin-host-model-completion-plan.md index 4f7b05400..fc5037976 100644 --- a/docs/plugin-host-model-completion-plan.md +++ b/docs/plugin-host-model-completion-plan.md @@ -2,20 +2,14 @@ ## Goal and current state -Build a general Python document model over the OCS host API for all **43 canvas kinds**. A kind is **complete** only after the per-kind gate below passes. Merely appearing in a converter or allowing a `layer` edit is partial coverage. Keep a property-level record of read/write, read-only, snapshot-only, and unsupported fields in the [43-kind coverage ledger](plugin-host-model-coverage-ledger.md). Do not resume the separate Lisp-to-Python conversion work. +Build a general Python document model over the OCS host API so a script can control OCS: read and change every canvas entity, manage the drawing's tables, and run OCS's own commands. Keep a property-level record of read/write, read-only, snapshot-only, and unsupported fields in the [43-kind coverage ledger](plugin-host-model-coverage-ledger.md). The architecture and guarantees are in [plugin-host-model.md](plugin-host-model.md); the progress log at the end of this file records each increment with its evidence. Do not resume the separate Lisp-to-Python conversion work. -As of 20 September 2026, the working branch is `plugin/host-model-api` at -`a8dd3640` plus the Leader increment, published as draft PR -[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is -v7. The Python feature has explicit writes for **43 of 43** canvas kinds: 41 -are creatable (the legacy `Polyline` and `Body` are update-only). This is **not** 41 completed -kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh, Wipeout, Point, Line, Circle, Arc, Ellipse, LwPolyline, Spline, Text, MText, Ray, XLine, Solid, Face3D, Solid3D, Region, Surface, Ole2Frame and Viewport pass every completion gate; Insert now passes every gate too; the audited Polyline (update-only by decision), Polyline2D and Polyline3D pass every gate that their format allows. -AttributeDefinition passes its host and real IPC lifecycle but remains short of -`Complete` because the previously pinned CAD codec dropped optional ATTDEF -fields during DXF reads; revalidate that blocker against the current -`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay, ViewBorder, Light and SectionSymbol are mapped and integration-tested but blocked at W by acadrust persistence gaps, and RasterImage is held short of `Complete` by unverifiable image-dictionary linkage (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. Every canvas kind now has explicit writes; SectionSymbol is mapped and integration-tested but blocked at DXF persistence by cadcodec -writes through the document model. All 43 have raw typed snapshots. There are -three internal records and two opaque fallbacks outside the 43 canvas kinds. +**State as of 21 September 2026.** The working branch is `plugin/host-model-api` (host API v7), backed up on the `felixriestra/OpenCADStudio` fork. The first draft PR, [#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391), was closed on 21 September with a note that a more complete version will follow; no new PR will be opened until the work is finished and tested as far as it can be. + +- **Entities:** explicit writes for all 43 canvas kinds (41 creatable; the legacy `Polyline` and `Body` are update-only). **31 kinds are `Complete`** (every C/R/E/D/U/I/W/V/P gate passes). The remaining kinds are integration-tested and held short of `Complete` by a named blocker in the ledger: seven by cadcodec DXF bugs (fixes submitted as [cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48), not yet merged, so OCS still pins the unfixed revision), Polyline3D by the DWG format, Polyline2D by a kind change on DXF save, RasterImage by an image-reactor structure that cannot be verified, and Body by having no creation path. +- **Drawing tables:** layers, text and dimension styles, blocks and their contents, linetypes and layouts, through the additive `TableOperation` request (see the log entries dated 2026-09-21). +- **Commands:** `HostApi::run_command` drives the real OCS commands step by step; `doc.command`, `doc.start_command` and `doc.modify` build on it. +- **Known open items:** the unfiled cadcodec findings (`cadcodec-reader-gaps.md`), complex (text and shape) linetypes, plot devices and named page setups, PEDIT's remaining options, and an on/off setting for script-driven commands. Repositories: @@ -31,9 +25,8 @@ Repositories: Preserve unrelated untracked files. Host and adapter changes now live in the same OCS repository and commit. Push the feature branch only to the -`felixriestra/OpenCADStudio` fork; PR #1391 updates automatically. Never push a -feature commit directly to H7's `main`. Verify both upstream `main` and the fork -branch before each push. +`felixriestra/OpenCADStudio` fork, and only when asked. Never push to H7's +repository. Verify both upstream `main` and the fork branch before each push. ## First: complete the originally agreed Phase 8 diff --git a/docs/plugin-host-model-coverage-ledger.md b/docs/plugin-host-model-coverage-ledger.md index 6d9c346a7..e2f8fe0f2 100644 --- a/docs/plugin-host-model-coverage-ledger.md +++ b/docs/plugin-host-model-coverage-ledger.md @@ -1,6 +1,8 @@ # OCS canvas kind coverage ledger -Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **43 of 43 canvas kinds have explicit geometry writes** (41 creatable, plus update-only legacy `Polyline` and `Body`), **31 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`. +Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 21 September 2026 (the object tables, block contents and command runner added since are described in [the design note](plugin-host-model.md) and the plan's progress log; they are not entity kinds): **43 of 43 canvas kinds have explicit geometry writes** (41 creatable, plus update-only legacy `Polyline` and `Body`), **31 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`. + +**Rows blocked at W by cadcodec.** Fixes for six of the DXF bugs behind them are submitted as [HakanSeven12/cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48) (ATTDEF fields, boolean group codes for Helix and Light, UNDERLAY rotation, LEADER link and vectors, DRAWINGVIEW/SECTIONLINE dispatch, TABLE merged ranges). OCS still pins the unfixed revision `5b682ed`, so those rows stay blocked and their canaries still assert the wrong result; the expectation is that they flip and the rows move to `Complete` once #48 is merged and OCS repins. That has been checked in cadcodec's own round-trip tests, not yet in OCS. Completion gate codes: **C** = create through Python document model; **R** = read; **E** = edit defining geometry and another property; **D** = delete; **U** = undo/redo and atomic failure; **I** = real IPC plus GUI/canvas pick or observable behavior; **W** = DWG and DXF save/reopen; **V** = invalid-input checks and untouched-field/reference preservation; **P** = build and load the in-tree bundled plugin with a manifest matching the host toolchain and acadrust source. A kind is `Complete` only when **C/R/E/D/U/I/W/V/P** all pass and its property catalog matches implementation. `Blocked` requires a specific dependency or failing fixture. diff --git a/docs/plugin-host-model.md b/docs/plugin-host-model.md index 39a91be8c..76a7758f6 100644 --- a/docs/plugin-host-model.md +++ b/docs/plugin-host-model.md @@ -1,99 +1,133 @@ # Scripting host model (API v7) -The host owns the drawing. Plugins can read the full `CadDocument` snapshot and -commit cloned `EntityType` values through IPC. `update_entities_transaction` -validates every replacement before changing the drawing, records one undo -entry, refreshes scene caches and the shared document view, and marks the -document dirty. The request preserves handles, owners, and entity kinds. A -missing or duplicate handle, a kind/owner change, or a locked source layer -rejects the whole batch. An empty batch does nothing. +This note describes how a script controls OCS and what the host guarantees. The +per-kind evidence is in the [coverage ledger](plugin-host-model-coverage-ledger.md), +the increment-by-increment record in the [completion plan](plugin-host-model-completion-plan.md), +and the script-facing reference in `plugins/opencad-python/PLUGIN.md`. -This is a full-entity replacement API, not a promise that every CAD property -is editable through Python. A client should clone the current value and patch -only fields it understands; unknown fields remain intact. The host can read -all `acadrust::EntityType` variants through `document()`. Its generic write -path accepts existing variants subject to the validation above. Entity -creation and deletion still use `add_entity` and `remove_entity` and are not -part of this transaction request. +## Shape -The host embeds a versioned coverage catalog generated from its traced -`EntityType` registry and `crates/ocs_plugin_api/entity_coverage_policy.json`. -It classifies all 48 variants: 43 canvas kinds, three internal records -(`Block`, `BlockEnd`, `Seqend`), and two opaque fallbacks (`Extended`, -`Unknown`). Each property records its source field, type, optional/sequence -shape, snapshot readability, Python access (`read_write`, `read_only`, or -`unmapped`), and validation status. `unmapped` means the typed snapshot carries -the field but the Python document model has no getter or setter for it. -The v7 transaction path checks changed Point locations, Line endpoints, -Circle/Arc centers, Ray/XLine base points, and Solid/Face3D corners for finite -coordinates. Circle/Arc radii must be finite and positive; Ray/XLine directions -must be unit vectors; Solid normals must be nonzero, its thickness finite, -and Face3D invisible-edge flags limited to four known bits. Insert transactions -validate placement, finite rotation and spacing, nonzero finite scale and normal, -and positive array counts. They reject changes to the referenced block and -attached attribute and sequence records. Those fields report `transaction_geometry` in the -catalog. All 43 canvas kinds also support nonempty `layer` changes through -the same undoable transaction; internal and opaque variants remain read-only -through the document model. The adapter accepts only `layer` patches for -canvas kinds outside its 43 geometry converters. Other writable properties -still report `type_conversion_only`; broader CAD range and cross-property -checks remain outstanding. -The host's `entity_snapshot` helper serializes any typed entity as a detached -JSON value keyed by its catalog variant name. This read path carries every -serializable field, including nested and unmapped values, without granting a -write path for them. The Python adapter exposes its variant payload through -`entity.snapshot`; changing that dictionary does not change the drawing. -It is an inspection view: JSON converts non-finite floating-point values to -null and cannot be used as a lossless replacement entity. +Python runs in RustPython inside a separate plugin process +(`plugins/opencad-python`), so a script cannot crash the editor. It talks to the +host over the versioned IPC in `crates/ocs_plugin_api`. **The host owns the +drawing**: it validates every request, records the undo step, refreshes scene +caches and the shared document view, and marks the document dirty. The Python +adapter is thin: it turns dictionaries into typed values and back, and holds no +CAD rules of its own. Everything below is additive to API v7; a plugin built +without it keeps working, and the default `HostApi` methods refuse. -The bundled first-party adapter under `plugins/opencad-python` currently maps -these entity kinds to Python dictionaries: Point, Line, Circle, Arc, Ellipse, -Polyline, Polyline2D, Polyline3D, LwPolyline, Spline, Text, MText, Ray, XLine, -Solid, Face3D, Insert, Tolerance, Shape, AttributeDefinition, -AttributeEntity, and Hatch. An Insert is created by naming an existing -ordinary block (the host refuses unknown blocks, model or paper space and -containment cycles); the block name cannot be changed afterwards. Attached -attributes stay with the AttributeEntity flow and remain in the raw snapshot. -The legacy Polyline is update-only: create a Polyline2D or Polyline3D. The authoritative per-field mapping is -`plugins/opencad-python/entity_manifest.json`. The generated -mapping excludes fields it cannot represent, including common color, line -weight, transparency, Polyline3D smooth type, and MText background color. -Absent geometry fields are not editable through the Python document model. -A feature test compares the generated Python keys with the -host catalog. The adapter's `experimental-host-model` feature requires API -v7 and builds against the repository-relative `ocs_plugin_api`. RustPython is -loaded inside the separate plugin runner process; OCS does not link the Python -runtime into the editor executable. +## What the host offers -API v7 also exposes a synchronous, ordered selection query and an atomic -selection replacement scoped to the session tab. All handles must exist before -replacement begins, duplicate handles are rejected, and the scripted -replacement keeps exactly the requested handles and order. UI picks continue -to group linked leaders and annotations. The V4 notification channel still broadcasts best-effort -document and selection changes. A new `DrawingChanged` notification carries -the scene epoch even when no shared document view is open. A new `CommandStateChanged` notification -reports transitions of the active interactive command at app message -boundaries, with the tab id and command name (or `None` when it ends). A -noninteractive command that starts and finishes within one message does not -produce an active-state transition. +| Area | `HostApi` entry | What it does | +|---|---|---| +| Read | `document()`, `entity_snapshot` | The full `CadDocument` snapshot; a detached JSON view of any typed entity. | +| Entities | `add_entity`, `remove_entity`, `update_entities_transaction` | Create, delete and replace entities. A transaction validates every replacement first and either commits all of them as one undo step or none. | +| Selection | `selection`, `set_selection` | Ordered, tab-scoped, exact (missing or duplicate handles reject the request). | +| Solids | `solid_operation` | Kernel-backed create, transform, booleans, extrude, region and surface from a profile, picture embedding. Every result is verified to lift back from its ACIS payload without loss; a refusal leaves the operands untouched. | +| Tables | `table_operation` | Layers, text and dimension styles, blocks and their contents, linetypes, layouts (see below). | +| Commands | `run_command` | Drives the real OCS command one step at a time (see below). | +| Settings | `system_variable`, `set_system_variable` | `CLAYER`, `SNAPANG`, and a read-only `CTAB` (the current layout). | -The host's existing `InteractiveCommand` machinery collects either a point or -an entity pick. The bundled v7 adapter exposes a token-based request and -poll API because `PY_RUN` uses a fresh Python interpreter per invocation; a -script cannot suspend in place while the user clicks. Enter and command -cancellation report a cancelled result. The host releases the runner's pick -command when it ends, including Escape cancellation. Drawing, selection, and command -notifications are retained in a bounded 256-event queue per tab between runs -and can be drained with `doc.poll_events()`. If a tab exceeds that bound, the -next poll begins with `{"type": "overflow", "dropped": n}`; scripts should -refresh drawing and selection state when they receive it. Closing a tab clears -its pending events, and a script can poll only its active tab. -Input tokens are bound to the drawing tab that requested the pick. Polling a -token from another tab returns no result and does not consume it. +Entity kinds outside a generated schema still read (handle, kind, layer) and take +`layer` changes; nothing is writable that the host has not validated. -Python `doc.entities[handle]` returns a descriptor for every entity. For kinds -outside the 43-kind generated schema it contains only handle, kind, and layer; -`layer` is editable on canvas kinds. `doc.coverage()` enumerates the -catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For -covered kinds, the coverage object lists actual readable and editable -dictionary keys, plus the unmapped snapshot fields and their types. +## Entity validation + +The host embeds a coverage catalog generated from its traced `EntityType` +registry and `crates/ocs_plugin_api/entity_coverage_policy.json`. It classifies all +48 variants: 43 canvas kinds, three internal records (`Block`, `BlockEnd`, +`Seqend`) and two opaque fallbacks (`Extended`, `Unknown`), and records for each +property its source field, type, shape, snapshot readability, Python access +(`read_write`, `read_only`, `unmapped`) and validation status. + +Creation and edits are validated per kind (finite coordinates, positive radii and +scales, unit directions, non-parallel vectors, in-range flags, reference targets +that exist and have the right kind, and so on). Creation must be valid; an edit +is refused only for problems it introduces, so already-invalid legacy values stay +editable. Derived state is computed by the host, never trusted from a script (MLine +geometry, Dimension measurement, Helix spline, Table merge dimensions, Spline +rational flag, SectionSymbol counts, Viewport id and scale). + +Nested records are checked too. A dictionary for a vertex, edge or face may not +carry an unknown key, and the fields that define its geometry are required +(`entity_manifest.json`, `required_struct_fields`); a typo can no longer turn into +a zero coordinate. Everything else in a nested record defaults when absent. + +Every kind is exercised through the real plugin runner by a per-kind audit that +checks create, read, edit, delete, undo, invalid input with an atomic rollback, +and a DWG and DXF round trip. A kind is `Complete` in the ledger only when all of +those pass. + +## Tables + +`TableOperation` is one additive request with typed variants, executed by +`HostSession::table_operation`. Every refusal is decided **before** the undo step is +recorded, so a failure changes nothing and leaves the history alone. + +- **Layers:** create, modify, rename (entities follow), delete (refused while it + holds objects unless erasing them is asked for), set current. Layer `0`, + `Defpoints` and the current layer are protected. +- **Text and dimension styles:** create, modify, rename, delete, set current. Dimension + style fields travel as a validated JSON object; handles, xref fields and the name are + host-managed. Renaming a text style updates the dimension styles that name it. + `Standard` and any style that is current or in use are protected. +- **Blocks:** create from entities (copied in shifted by the base point, optionally + erasing the originals), modify, rename (inserts follow), delete (refused while + referenced), and add an entity to a definition through the normal validation path. + A block may not nest itself, directly or through other blocks. +- **Linetypes:** simple dash, gap and dot patterns; rename follows layers and + entities; delete is refused while anything uses it. +- **Layouts:** create with the default page setup and sheet viewport, rename, delete, + switch (through OCS's own layout switch), and page setup (paper size, rotation, + custom scale). `Model` is protected. + +Style and layout operations act on the active document, because they use the +application's shared style and view machinery. + +## Commands + +`run_command` takes a `CommandRequest` (`Run`, `Start`, `Point`, `Text`, `Token`, +`Entity`, `Selection`, `Enter`, `Cancel`) and answers with a `CommandOutcome`: +whether the command is done or waiting, its prompt, what input it accepts, its +keyword options, how many entities it added, tokens nobody asked for, and any +error. It uses the same primitives as the automation channel and finishes each +step synchronously on the host thread, so a step never waits for anything that has +to arrive from outside; an earlier fire-and-forget replay could hang, this does not. + +Guards: refused while another command is active or off the active tab; refused for +commands that could end the session or re-enter Python (`QUIT`, `EXIT`, `CLOSE*`, +`NEW`, `QNEW`, `OPEN`, `SAVE*`, `RECOVER`, `SCRIPT`, `RUNSCRIPT`, `PY_*`); an editor or +dialog a command opens is reported and closed by `Cancel`. It does not consult the +automation on/off switch, which governs the external MCP and serve channels; a script +is something the user chose to run. The Python layer cancels any step sequence that +leaves a command waiting. + +## Python surface + +`ocs.active_document` exposes `entities`, `create_entity`, `delete_entity`, +`transaction`, `selection`, `solids`, `layers`, `text_styles`, `dim_styles`, `blocks`, +`linetypes`, `layouts`, `command`, `start_command` and `modify` (offset, trim, extend, +fillet, chamfer, move, copy, rotate, scale, mirror, erase, rectangular, polar, path and +3-D arrays, explode, join, break, stretch, lengthen and polyline edits). `doc.coverage()` +lists the catalog, and `doc.entities[handle].coverage` one entry, with the readable and +editable keys and the fields the typed snapshot carries but the model does not map. + +Interactive picks use a token-based request and poll API because `PY_RUN` uses a +fresh interpreter per invocation and a script cannot suspend while the user clicks. +Drawing, selection and command notifications are kept in a bounded 256-event queue +per tab between runs and drained with `doc.poll_events()`; an overflow is reported +as `{"type": "overflow", "dropped": n}`. Tokens are bound to the tab that asked. + +## Known limits + +- Twelve of the 43 canvas kinds are not yet `Complete`; each has a named blocker in the + ledger. Seven are cadcodec DXF bugs whose fixes are submitted upstream but not yet adopted. +- The DXF codec also drops some table properties (text style generation flags, block + descriptions, a dimension style's text-style name) and mis-scales one angle; these are + pinned by canaries and listed in `cadcodec-reader-gaps.md`. +- Not available from a script: complex (text and shape) linetypes, plot devices and named + page setups, PEDIT's fit and spline options, interactive-only commands such as HATCH, and + a setting to turn script-driven commands off. +- A command step nests inside the editor's message handling; in a debug build a deep step + (PEDIT converting a line to a polyline) needs more than 2 MiB of stack, which the + application's 8 MiB main thread provides and a release build needs far less of. diff --git a/plugins/opencad-python/PLUGIN.md b/plugins/opencad-python/PLUGIN.md index c02694cac..28ed53c53 100644 --- a/plugins/opencad-python/PLUGIN.md +++ b/plugins/opencad-python/PLUGIN.md @@ -117,8 +117,8 @@ expression, not statements). Phases 1.2 (read), 1.3 (2D write) and part of | `ocs.read_record(handle, app_name)` | Returns `{app_name, values}` for the XDATA record `app_name` on `handle`, or `None` if the entity or the record doesn't exist. See "XDATA" below for the `values` shape. | | `ocs.write_record(handle, app_name, values)` | Attaches an XDATA record to `handle` for `app_name`, replacing any existing record for that application. Raises if `handle` doesn't exist. | | `ocs.remove_record(handle, app_name)` | Removes the XDATA record for `app_name` from `handle`, if any. Returns `True` if a record was actually removed. | -| `ocs.command(cmd)` | Experimental only; absent from the portable default build. Requires unreleased host API support and can hang on OCS 2026.37. See historical notes below. | -| `ocs.select(handles)` | Experimental only; absent from the portable default build. Requires unreleased host API support. | +| `ocs.command(cmd)` | Superseded. Only the `experimental-command-replay` build has it (the old fire-and-forget replay). Use `doc.command`, `doc.start_command` and `doc.modify` from the document model (see "Running OCS commands" below). | +| `ocs.select(handles)` | Replaces the selection with exactly these handles, in order (document-model build); `doc.selection = [...]` is the same. | | `ocs.system_variable(name)` / `ocs.set_system_variable(name, value)` | Development-only `experimental-host-settings` feature. Reads or sets host-managed CLAYER (text) and SNAPANG (degrees) without nested command dispatch. Requires the Felix OCS fork's `plugin/host-model-api` branch; absent from the portable default build. | `add_line`/`add_circle`/`add_arc` take plain Python numbers — @@ -325,10 +325,13 @@ is the raw single step behind all of this. ### Historical command-replay experiment (not in the default build) -The following records an earlier experiment. The bundled build does **not** -expose `ocs.command()` or `ocs.select()`. An OCS 2026.37 diagnostic found -nested `ocs.command("CLAYER ...")` could hang, so these -examples are not instructions to run against the released host. +The following records an earlier experiment and is kept for the findings, not as +instructions. It used a fire-and-forget `ocs.command()` that replayed a line through the +message loop; on OCS 2026.37 that could hang. It has been superseded by +`doc.command` / `doc.start_command` (see "Running OCS commands" above), which run the +real command synchronously through the host's `run_command` step API and were exercised +against every command tried without a hang. The prompt-sequence findings below (selection +prompts, `ALIGN`'s extra Enters) still describe how the commands behave. **`ocs.command()` + `ocs.select()` together can apply a real, persistent constraint, entirely from a script** — this required two host changes @@ -504,37 +507,12 @@ Script…` (above) is the most a plugin can do today: reuse the host's own native file picker via the existing `PluginFileDialog` event, which was already there for other plugins to import files. -## lisp2py — AutoLISP macro transpiler +## AutoLISP migration -`../lisp2py/` (an independent sibling workspace — zero dependency on this -crate or `ocs_plugin_api`) is a **best-effort, offline, source-to-source** -translator from AutoLISP (`.lsp`) to the `ocs.*` Python dialect documented -above. Not an interpreter: it never executes AutoLISP, and its output is a -draft a human reviews and finishes, then runs through `PY_RUN` like any -other script. This is Phase 2.2/2.3 of `ROADMAP.md` — see -[`../lisp2py/README.md`](../lisp2py/README.md) -for the full pipeline, the corpus-measured builtin coverage, and every -known limitation; this is just the quick-start. - -```sh -cargo run --manifest-path ../lisp2py/Cargo.toml --bin ocs-lisp2py -- macro.lsp > macro.py -# then, inside OCS: -PY_RUN macro.py -``` - -(Or pipe source on stdin: `cat macro.lsp | cargo run --manifest-path ../lisp2py/Cargo.toml --bin -ocs-lisp2py -- > macro.py`.) - -Anything the translator can't confidently map — an unrecognized builtin, a -construct with no Python equivalent — becomes a `_todo_manual_port(...)` -call in the generated file (raises `NotImplementedError` if actually run) -plus a warning printed to stderr, never a silent guess. `vla-*`/`vlax-*` -(ActiveX/COM) and `vlr-*` (reactors) are explicitly out of scope, for the -same reason as everywhere else in this plugin's design: COM doesn't run -under AutoCAD for Mac either. Builtin coverage was widened using measured -call-frequency data from a real AutoLISP corpus sample rather than -guesswork, biased toward 2D geometry (`angle`/`distance`/`polar`) over -3D/solid-modeling per current project priority. +An AutoLISP-to-Python translator is a separate project and is not part of this +repository: scripts written for this plugin use the `ocs` module and the document +model described above, and a translated macro would run through `PY_RUN` like any +other script. Nothing in the plugin depends on it. ## XDATA diff --git a/plugins/opencad-python/README.md b/plugins/opencad-python/README.md index 94a7796f5..cf29cf2f9 100644 --- a/plugins/opencad-python/README.md +++ b/plugins/opencad-python/README.md @@ -17,13 +17,22 @@ found while building this: `PLUGIN.md` in this directory. ## Status The API v7 build provides `PY_EVAL`/`PY_RUN`, the read/write `ocs` module, and -the ribbon integration; these have live OCS 2026.37 tests. Experimental -`ocs.command()`/`ocs.select()` constraint work -is documented in `PLUGIN.md` and `examples/example.py`, but those functions -are not in the default build: their host APIs are unreleased, and nested -command replay can hang. The general document model is built through the -`experimental-host-model` feature until that name is retired in a later API -cleanup. +the ribbon integration. On top of that, the `experimental-host-model` feature +(the name stays until a later API cleanup; the staged bundled plugin is built with +it) adds a general Python document model over the host API: + +- **Entities:** create, read, edit, delete and undo for all 43 canvas kinds + (41 creatable), validated by the host, with per-kind DWG/DXF round-trip audits. +- **Drawing tables:** `doc.layers`, `text_styles`, `dim_styles`, `blocks` (and their + contents), `linetypes` and `layouts`, plus kernel-backed solids (`doc.solids`). +- **Commands:** `doc.command`, `doc.start_command` and `doc.modify` (offset, trim, + extend, fillet, chamfer, move, copy, rotate, scale, mirror, arrays, explode, join, + break, stretch, lengthen, PEDIT polyline edits) drive the real OCS commands. + +The design and guarantees are in `../../docs/plugin-host-model.md`, the per-kind +evidence in `../../docs/plugin-host-model-coverage-ledger.md`, and the script-facing +reference in `PLUGIN.md`. The older experimental `ocs.command()` replay is +superseded and not in the default build. ## Files diff --git a/plugins/opencad-python/ROADMAP.md b/plugins/opencad-python/ROADMAP.md index 1d13796d7..6077b769a 100644 --- a/plugins/opencad-python/ROADMAP.md +++ b/plugins/opencad-python/ROADMAP.md @@ -1,13 +1,10 @@ # Open CAD Studio — Python Scripting & AutoLISP Migration Plan -**Status:** Phase 1 done and host-verified; Phase 2 (the `lisp2py` -transpiler) has progressed well past what's checked off below — see -[`../lisp2py/README.md`](../lisp2py/README.md) and -[`../../PLUGIN_DEVELOPMENT_RECIPE.md`](../../PLUGIN_DEVELOPMENT_RECIPE.md) for -current status. Kept here as the original planning document and Phase 1 -build log, moved from `OpenCADStudio`'s `docs/` (where it was originally -drafted, before this repo held the plugin work) unchanged except for -cross-repo reference fixes below. +**Status:** Phase 1 is done and host-verified, and the general document model that +followed (entities, drawing tables, block contents and the command runner) is described in +[`../../docs/plugin-host-model.md`](../../docs/plugin-host-model.md). Phase 2 (an AutoLISP +translator) is a separate project that is not part of this repository. This file is kept as +the original planning document and Phase 1 build log. **Author:** Open CAD Studio contributors **Date:** September 2026 @@ -305,8 +302,7 @@ cost. ## Phase 2 — AutoLISP → Python parser/translator -**See [`../lisp2py/`](../lisp2py) — this phase has its own crate, README, -and builtin-coverage writeup now.** The sub-goals below are the original +**This phase lives in a separate project that is not part of this repository.** The sub-goals below are the original scoping; §2.2/2.3 shipped with substantially wider builtin coverage than first planned (measured from a real AutoLISP corpus sample, not just the constructs listed here). @@ -347,7 +343,7 @@ them). - [x] `getvar`/`setvar` — confirmed no host system-variable surface exists; documented as unsupported rather than guessed at. - [x] Widened well past this original list via measured corpus frequency — - see `lisp2py`'s own README for the full table. + see that project's own README for the full table. ### 2.4 — Validation loop