Bug:
$ISOLINES (header i16) and Surface.u/v_isolines (file-controlled i16)
were only ever floored at 0, never capped, and were handed to the
tessellation kernel as the per-face isolate count - which draws
0..count isolines per face with no ceiling of its own. SETVAR ISOLINES
32767 (or a DWG carrying that header, or a Surface with u_isolines =
32767) turned every rebuild and every per-cursor preview of
PRESSPULL/REVOLVE/SWEEP/LOFT into a multi-million-vertex computation:
measured 4,259,966 edge vertices for one 12-unit cylinder preview.
Reproduce:
1. SETVAR ISOLINES 32767 (or open a DWG whose $ISOLINES is large, or
edit a Surface whose u_isolines is 32767).
2. Draw a circle and start REVOLVE (or PRESSPULL/SWEEP/LOFT), or shade
any 3D solid.
3. Before: the app allocates without bound - abort on memory failure or
a permanent freeze; previews recompute on every mouse move.
4. After: previews stay interactive; a hostile header renders exactly
like the largest value the Options slider can request.
Fix:
Cap each value at the ceiling this product already agrees on, where the
type changes from i16 to the kernel count: header $ISOLINES <= 64 (the
Options slider's own "Isolines per surface" range), Surface
u/v_isolines <= 200 (the plugin editor's writable range). One helper
(clamp_header_isolines) replaces all 11 copies of .max(0) as usize
across the entity/scene/control/display feed sites,
surface_isoline_counts clamps both axes, and SETVAR ISOLINES stores
x.clamp(0, 64) so the echo, slider and re-saved files agree with what
renders; file-sourced headers keep their raw stored value and are
clamped at consumption. Clamping is density-only: geometry, bounds and
saves are untouched. Three regression tests cover hostile surface
counts, the SETVAR entry, and a file-sourced header measured through
display tessellation (hostile renders like the slider ceiling, negative
renders like zero).
Bug:
control_entities_transform validated the action field only inside the
transform closure - and invoked that closure from three of the match's
arms. Any other action fell into unreachable!("validated by
transform()") and aborted the process. The claim in the panic message
was false: for unknown actions the validating code never ran. The handler
is reachable from every remote surface (REST, MCP, --serve) because they
share this dispatch. Two extra triggers: "move " (trailing space, no
trim) and an all-whitespace action.
Reproduce:
1. Start OpenCADStudio with --http 8765.
2. POST /api/v1/entities (a LINE), take handles[0].
3. POST /api/v1/entities/transform with
{"handles":["<handle>"],"action":"explode"}.
4. Before: "internal error: entered unreachable code" and the process
exits.
5. After: {"ok":false,"code":"invalid_action",...} and the server keeps
serving. "action":"move " is now accepted (trimmed); " " is
rejected.
Fix:
The action is trimmed, lowercased and checked against the six-value
whitelist after require_existing (error precedence unchanged: a bad
handle still reports first) but before push_undo_snapshot, so an invalid
request no longer dirties the undo stack. The match's fallback arm
returns the same invalid_action error instead of unreachable!, so a
future mismatch between the whitelist and the match fails closed.
Add cross-platform SystemSpeller abstraction with three backends:
- Windows: direct COM vtable calls to ISpellChecker (Win32 Spell API),
no external crate — uses the same hand-rolled FFI pattern as
file_association.rs. Handles ALL-CAPS words by also testing lowercase.
- macOS: NSSpellChecker via objc2/objc2-app-kit (already in deps).
- Linux: Hunspell .dic file reader + hunspell CLI pipe fallback.
text_audit gains three new parameters:
system_spellcheck: true — activate OS spell-checker
language: en-US — BCP-47 language tag (default: OS default)
suggest: true — include spelling suggestions
The agent dictionary and system speller work as a layered pipeline:
words matching the agent dictionary are accepted immediately; only
remaining words are forwarded to the OS checker.
Response includes a system_speller object (enabled, available, backend,
language) and an unrecognized_words array with handle, position, and
optional suggestions.
Integration test: text_audit_system_spellcheck_flags_misspelled_words
(20/20 p1_tests pass). Release build verified.
- Adds ext_audit operation under ocs_read, control bridge, and headless server
- Implements pairs / dry_run_pairs simulation returning diffs, coordinates, and unmatched queries
- Implements check_terms suspect matching and dictionary vocabulary verification
- Adds dry_run: true to ext_replace to preview modifications without mutating entities or pushing undo
- Supports bounds, layers, handles, and scope filtering
- Updates MCP tool definitions, schemas, and automation test suites
- Add text_search (read) and text_replace (mutating) operations to control bridge, automation, and MCP
- Support single and batch find/replace pairs under atomic undo transactions
- Safely parse and preserve MText formatting tags, inline fonts, colors, and margins
- Add flexible multiline MText matching and replacement across paragraph breaks (\n <-> \P)
- Add accent/diacritic-insensitive matching across Unicode and ASCII character sets
- Support Windows-1252 / ISO-8859-1 code decoding (%%nnn), DXF Unicode escapes (\U+XXXX), and special character codes
- Expose ignore_accents parameter across ocs_read and ocs_execute MCP tool schemas
Review of #1484:
- The underlay's snap geometry became one hidden wire per segment, but
every piece took the whole page's bounding box, so each cursor move
visited every piece. Each piece now gets its own box.
- Assigning unlayered images to the last layer compared Debug strings of
every layered path and text per drawn item (quadratic, formatted anew
each time); the owners are keyed once in a map.
- The `snap` automation op used (0, 0, 0) for a missing point; it now
answers invalid_point. It also left the live snapper's base point set
to the query's; the base point is restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unlayered raster images go with the PDF layer drawn last before them.
- Circular Bezier arcs import as arcs: bulged polyline segments with Join,
ARC entities without it; straight chains still join.
- Imported PNGs go to "PDF Images" next to the PDF; import blocks are
numbered imp31, imp32, ...
- Background darkness is its HSL lightness below one half.
- All chosen pages attach at the same insertion point.
- PDF geometry snaps as separate pieces: centre from the rim, intersections
and perpendiculars.
- Automation: snap query with an optional base point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graph panel gets New, Open and Save. A .ocg file is JSON holding the
graph (nodes, parameters, links), the canvas (positions, open sections,
pan, zoom) and each object node's entity handles, plus the fingerprint
GUID of the drawing those entities live in. On open, handles are reused
only in that same drawing and only while the entity is still the node's
kind; otherwise evaluation creates new entities, so a graph opened in
another drawing never takes over unrelated objects. A drawing without a
fingerprint gets one when its graph is first saved.
Pins opencadcodec d6d534f (DXF now writes $FINGERPRINTGUID and
$VERSIONGUID), opencadkernel 5d9e921 and opencadgraph 5c9b004 (graph
JSON round trip).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.
A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `--export ... --target-version`, and explicit target_format /
target_version on automation and protocol saves; a conversion that
would drop unsupported records is refused unless allow_lossy is set.
- `audit` read op: dangling layers/blocks, duplicate handles, non-finite
bounds, serialization, lossy records and the source DXF handle graph.
- `save_verified`: writes the requested version from the same snapshot a
normal save writes, reopens it, checks the version and the entity
manifest, audits the ASCII DXF handle graph and returns a SHA-256.
- ZOOM EXTENTS includes SDF text glyphs.
- Live MCP acceptance script and an optional reference-console audit
runner under docs/automation.
Squashed from the PR with review fixes: merged with the DWT save path,
the save snapshot, the tool lists added since, and vendor-neutral
harness names; the dated verification record is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operations docs/automation/API-SPEC.md (PR #1383) describes, brought
over from huaninstratech/OpenCADStudio so the specification no longer runs
ahead of the code:
- entity CRUD: entities_create, entities_delete, entities_transform,
entities_copy_to, and `where` filters on query
- blocks and export: block_define, block_delete, a normalised wblock
- publishing: layout_create, page_setup_set, plot (per_page included)
- metadata: sysvar, xdata_set / xdata_get, file_identity
- collections: group_create, selection_set_save / load
- control plane: batch, events, view_focus, close
- the REST transport: `--http <port>`, the /api/v1 routes and their
OpenAPI 3 description
- person-in-the-loop picks: user_select and getpoint, hosted by the GUI
over the same REST routes, with on-screen pending signals
Taken from the fork's commits b7a7ed80, 69396b42, 64f1cf6a, c3aafc8e,
bb5a7bfa, 52317a5c, 50d293ba, b90e194f, 517c6397, 53a5c4c9, a49ac045,
37de5a46, a78b1822, 6d9aed2a, 33f522da, 5b5cafe2, 02a7e78d, 94df40dc,
fd5bc14d, 485fac61 and 2c5fc994.
Left out on purpose: 37de5a46 also removed the donation surface (the
Patreon fetch, the donation prompt, the Start-page supporters rail, the
DONATE command and its locale strings) and replaced the About dialog's call
to action; that is the fork's own product decision and stays out. The
fork's version bump and its unrelated IFC/BIM work are not part of this.
Adapted while porting: the REST server and the GUI bridge are desktop-only
and a template start is refused in the web build, so the wasm target still
compiles; the two plot tests read the PDF through the shared stream helper,
because exported streams are Flate-compressed; the benches set the new
crosshair field; comments and docs describe each operation in its own
terms rather than by another product's API names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HostApi::run_command drives the real OCS command through the same primitives
the automation channel uses, synchronously on the host thread: run a line,
start a command and answer point/text/token/entity/selection/enter prompts, or
cancel (closing anything the command opened). Refused while another command is
active, for a denylist (QUIT, NEW, OPEN, SAVE*, PY_* ...) and off the active tab.
Python: doc.command, doc.start_command and doc.modify (offset, trim, extend,
fillet, move, copy, rotate, scale, mirror, erase). Real-IPC audit checks exact
geometry for every wrapper plus guards and undo; IPC and Python unit tests; docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
active_command_metadata listed one input kind per step. A point step that
also takes keyword letters (point_step_accepts_keywords) reports
SingleToken, so MOVE and COPY's base point with [Displacement] were
described as accepting only token and enter, and the input example was the
D keyword, though the step takes a point. List point as well; the keyword
letters are still listed as token from the options.
Equal was applied through a generic gather-then-apply flow and, with the
default size-retaining solve mode, never changed anything: the solver
held both sizes and the relation could not be met. Now:
- GCEQUAL joins ECONSTRAINT and GEOMCONSTRAINT Equal; both objects are
picked inside the command (`Select first object or [Multiple]:`,
`Select second object:`; Multiple gathers `Select objects to make equal
to the first:`), a selection made beforehand is not used, a miss says
`No object found.` and re-asks, an unsupported object or the same object
twice is reported and re-asked, a repeat is refused.
- The follower is resized in place before the relation is added: a line or
straight polyline segment keeps its start and direction and its end
moves to the first object's length; a circle or arc keeps its center and
takes the radius.
- During edits the side that is not being edited follows the same rule:
its size is not retained, its start (or center) and other vertices are
held and its direction kept, so only its length or radius changes; a
chain of Equal relations follows along.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A minimized window has a 0x0 surface; the renderer's screenshot path
panicked on the empty read-back buffer and took the whole application
down. The capture op now checks the window size first and answers with
an error instead of requesting the screenshot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the web build, {"op":"open","path":…} resolves the name against the
browser's recent-files cache, so a file that is not already there cannot be
opened by the control channel. Accept {"op":"open","name":…,
"data_base64":…} instead: the bytes go through the same parse, caching
and recovery path as a file chosen in the browser picker
(io::open_bytes_web, now shared with pick_and_load_web).
Native builds keep path-based open and reject data_base64 with
invalid_request.
Refs #1349
- IMAGEEMBED: pack raster images into OLE2FRAME entities so a .dwg is
self-contained; 'Embed Image' entry in the XREF attach menu; control/MCP
op embed_image for external tools; dxf_name for OLE2FRAME queries.
- Scene: populate_images_from_document now rebuilds OLE2FRAME and PDF
underlay models too (they used to vanish after any XREF op).
- Printing: add PRINTER_ENUM_NETWORK pass when listing printers; GDI
spooler fallback (hayro raster at 300 DPI + StretchDIBits) for hosts
whose PDF handler registers no print verb; DEVMODE orientation with a
90-degree raster rotation fallback for v4 drivers; fix the fit-scale
precedence bug that printed blank pages.
- PDF export dialogs on Windows use the async file dialog so the native
overwrite confirmation no longer deadlocks the UI thread.
- Plot dialog: persist the chosen printer (saved on selection) and the
picked plot window across sessions; page setups with no device no
longer reset the output device.
- Community fonts: detect unresolvable .shx references on open and offer
a download from the repository's fonts/ folder (contribution guide in
fonts/README.md); custom font source URL setting for intranet font
servers; downloaded fonts resolve from the per-user fonts directory.