Files
OneCAD-Draft/docs/plugin-host-review-guide.md
T
Felix RiestraandClaude Sonnet 5 41d3613b85 docs: reviewer guide for the scripting host model and Python plugin
What it is, how to check it on Linux or Windows without a Mac, the reading order,
the small edits to existing code, what each area proves, the decisions that are the
maintainer's, and what is deliberately not done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:16:06 +02:00

9.8 KiB
Raw Blame History

Reviewer guide: scripting host model and the bundled Python plugin

This is written for a reviewer who has not followed the work and may not have a Mac. It says what the change is, how to check it on Linux or Windows in a few minutes, where it touches existing code, what to read first, and what is deliberately left out.

What it is

The bundled opencad.python plugin (RustPython, in its own process) can now control OCS through the versioned plugin API (v7), not only add lines and circles:

  • Entities. Create, read, edit, delete and undo for all 43 canvas kinds (41 creatable), validated by the host, each with a DWG and DXF round-trip audit.
  • Drawing tables. Layers, text and dimension styles, blocks and their contents, linetypes and layouts, and kernel-backed solids.
  • Commands. Scripts can run OCS's own commands step by step (offset, trim, fillet, arrays, PEDIT and so on), with guards.

The host owns the drawing. It validates every request, records the undo step and refuses without changing anything when it cannot honour a request. The Python adapter is a thin translation layer. The design and its guarantees are in plugin-host-model.md.

Size. 74 files, +30,697 / −178, against the merge base. About 15,000 added lines are the new plugin crate (including a 3,000-line lockfile and a generator), 5,600 are the API crate (3,500 of them the entity coverage validator), and 8,800 are in src/, of which roughly 6,200 are tests in plugin_host.rs. Existing OCS code changes by 178 deleted lines in total; everything else is additive.

Check it without a Mac

Nothing here needs macOS. The plugin tests are headless: they build the plugin and the application, then spawn the application as the plugin runner and talk to it over the real IPC. stage-bundled.sh already handles Linux (.so) and Windows (.dll), and the host looks for the plugin in plugins/opencad.python/ next to the executable.

bash plugins/opencad-python/tools/stage-bundled.sh work/plugin --debug
cargo build --bin OpenCADStudio
OCS_TEST_PYTHON_PLUGIN=work/plugin/libopencad_python.so \
OCS_PLUGIN_RUNNER_EXE=target/debug/OpenCADStudio \
cargo test --lib -- --test-threads=1 python

(opencad_python.dll and OpenCADStudio.exe on Windows.) Expect 60 passed. Without the two environment variables these tests are skipped and still report success; the workflow below fails instead of skipping.

Already run for you:

Check Where Result
python-host-check.yml on the pull request Linux and Windows 115 plugin API tests, 20 plugin tests, 16 Python model tests, 60 host tests over real IPC, on each
fork-build.yml (release build with the plugin staged) Linux and Windows 60 of 60 host tests on each; unsigned portable packages you can download and run
Local macOS the same, plus the wasm check

What has not been done: launching the GUI on Linux or Windows (the runners have no display), running the whole cargo test --lib suite there, and building an AppImage or MSI that contains the plugin. Two unrelated tests, arc_grips_drive_center_start_and_end_but_not_midpoint and fonts_parse_and_resolve, fail on pristine main in the macOS environment used; they are not part of this change and I do not know how they behave on Linux or Windows.

Reading order

Read the contract first, then the host, then the adapter. The commits are in dependency order but several kinds and features are interleaved, so read by area rather than commit by commit.

  1. The contract: crates/ocs_plugin_api.
    • src/host.rs: the new types and default-refusing methods: SolidOperation, TableOperation, CommandRequest and CommandOutcome, update_entities_transaction, selection, set_selection, solid_operation, table_operation, run_command.
    • src/ipc/**: one additive request and response variant per method. The new variants are appended after the existing ones so existing wire positions do not move. v4/protocol.rs has a bincode round-trip test for each.
    • src/entity_coverage.rs, entity_coverage_policy.json, build.rs: the per-kind validation and the generated coverage catalog. This is the largest file and the most mechanical; the tests in it are the rules in executable form.
  2. The host: src/app/plugin_host.rs, lines 1 to about 2,700 (the rest is tests). Sections in order: scripted-entity normalization (host-derived state), solid operations, table_operation (layers, then styles, blocks, linetypes, layouts), run_command, and the HostApi delegation at the end.
  3. The adapter: plugins/opencad-python. entity_manifest.json and build/generate.rs generate the dict-to-entity conversions from the host's type registry; src/ocs_module.rs is the ocs.* bindings; src/document_model.py is the script-facing document model; PLUGIN.md is the reference.
  4. The small edits to existing OCS files (this is where behaviour of existing code could change):
    File Change
    src/plugin/external.rs, src/ui/window/plugin_manager.rs Bundled-plugin discovery (bundled_plugins_dir, OCS_BUNDLED_PLUGINS_DIR, a "Bundled with Open CAD Studio" label). A per-user plugin with the same id still overrides the bundled one.
    src/plugin/v4_support.rs, src/app/update/mod.rs, src/app/automation.rs New DrawingChanged and CommandStateChanged notifications; a drawing-changed notification after automation ops.
    src/entities/{mline,table,helix,dimension}.rs normalize_scripted_*: derived state a script must not set (MLine geometry, Table merges, Helix spline, Dimension measurement). Called only from the plugin host.
    src/scene/selection.rs, src/scene/entity.rs, src/app/layers.rs replace_selection_exact, preserving storage data on entity replacement, set_current_layer_name.
    src/app/style_ops.rs, src/app/control/mod.rs Four methods and one function widened to pub(super) so the host reuses OCS's own style rename, delete and in-use logic.
    src/app/settings.rs, src/app/mod.rs, src/app/update/file.rs, src/app/commands/styleprops.rs The SCRIPTCOMMANDS preference and command (see the decisions below).
    packaging/build_macos_signed.sh Stages and signs the plugin in the macOS bundle.

What each area proves

Area Evidence
43 entity kinds One real-IPC audit per kind (create, read, edit two properties, delete, undo and redo, invalid input with an atomic rollback, canvas geometry, DWG and DXF reopen and re-edit). The ledger records each kind's exact evidence or its blocker.
Solids Kernel primitives, moves, booleans, extrusions and regions, each verified to lift back from its ACIS payload; refusals leave operands untouched.
Tables audit_python_layer_table, _text_and_dim_styles, _blocks, _block_contents, _linetypes_and_layouts: dozens of refusals each, live and DWG and DXF persistence, one-step undo.
Commands audit_python_modify_wrappers: exact geometry for every wrapper (offset, trim, extend, fillet, chamfer, arrays, explode, join, break, stretch, lengthen, PEDIT edits), guards, an interactive session, undo.
Nested input audit_python_nested_input_is_validated: a mistyped vertex key or a missing location is refused instead of becoming a zero coordinate.
Guardrail script_commands_setting_gates_the_runner and a settings round-trip test.

Decisions that are yours

  1. run_command and the automation switch. Script-driven commands do not consult the automation on/off switch, which governs the external MCP and serve channels; a script is something the user chose to run. Say if you would rather they respected it.
  2. SCRIPTCOMMANDS. A saved preference (on by default) toggled by a command turns script-driven commands off entirely. It has no Options-page control; adding one needs locale and layout work outside this change.
  3. Denylist. A script cannot run QUIT, EXIT, CLOSE*, NEW, QNEW, OPEN, SAVE*, RECOVER, SCRIPT, RUNSCRIPT, SCRIPTCOMMANDS or any PY_*, and only one command may be active. Tell me about any other command that should be refused.
  4. Nesting depth. A command step runs inside the editor's message handling. It is not recursive, but a debug build of one step (PEDIT converting a line to a polyline) needs 2 to 4 MiB of stack; the native builds link with at least 8 MiB (16 MiB on Windows).
  5. Splitting. If 70 commits is too much, the natural seams are: the API and host core, the entity kinds, the tables, and the command runner. Tell me and I will cut it that way.

What is deliberately not done

  • Twelve of the 43 kinds are not Complete (see the ledger). Seven are blocked by DXF bugs in cadcodec, whose fixes are submitted as cadcodec#48 and #51. This change still pins the unfixed cadcodec revision, so its canary tests assert the old behaviour; once those merge, the pin moves, the canaries flip and those kinds can be marked complete. The other five are held by the DWG format, a kind change on DXF save, an image-reactor structure that cannot be verified, an update-only decision, and a missing creation path.
  • Not available from a script: complex (text and shape) linetypes, plot devices and named page setups, PEDIT's fit and spline options, and interactive-only commands such as HATCH.
  • Two DXF gaps are not filed upstream because they need the exact XDATA layout AutoCAD expects (the TrueType family name and the block description); they are listed in cadcodec-reader-gaps.md.
  • release.yml is unchanged: the AppImage, snap and MSI still ship without the plugin.