Add MTP (Midpoint Between Two Points) as a keyword that can be typed
during any point-picking command (e.g. LINE) to suspend the current
command, prompt for two points, and return the midpoint to the parent.
- New Mid2PointCommand registered under both "MTP" and "M2P" aliases
- ReturnPoint CmdResult variant feeds the resolved midpoint back to
the suspended parent command
- MTP/M2P typed during a point step suspends the active command and
starts the midpoint modifier; Escape restores the parent command
- Preview wires show a rubber-band line with a triangle marker at
the midpoint
- Character-by-character typing is routed correctly even with dynamic
input active (digits no longer leak into dyn fields)
- Three automation tests cover the full workflow, ESC restoration, and
dynamic-input character routing
Brings in 54 commits from origin/main: a selection-performance overhaul,
surface thickening, GPU-native wide/tapered stroke rendering, continued
ocs_doc_api evolution, and assorted fixes/polish (block double-click
editing, DWG hatch/database repair, clipboard, collapsible Properties
sections).
Six real conflicts, all where this branch had independently re-applied
a fix upstream later revised or already carried natively:
- src/app/automation.rs, src/app/command_driver.rs: combined both
sides — kept this branch's sketch-constraint/named-parameter loading
on "new"/"open", adopted main's Scene::clear()/material_base_dir and
its newer rebuild_derived_caches() over the older bump_geometry().
Scene::clear() (src/scene/entity.rs) now also resets
sketch_constraints/named_parameters, so every caller gets this for
free instead of each call site repeating it.
- src/io/mod.rs, src/scene/render_graph.rs: adopted main's design
outright (commit 973af87d, authored by Hakan directly) — a large
MINSERT is no longer purged as "corrupt" at load, only its rendering
is bounded, preserving the source data. This branch's own earlier
design (reject at load) predates that revision.
- src/scene/pipeline/mesh_gpu.rs: adopted main's valid_rgba_size()
guard (a real robustness improvement over this branch's version).
- src/snap.rs: replaced wholesale with main's version — this branch's
copy was an explicit verbatim snapshot from an earlier upstream PR
(commit 4c264b8c) with zero constraint-system-specific content, and
main has since evolved it considerably further.
Also removed two now-dead duplicates the merge surfaced without a
conflict marker (both branches had independently fixed the same
things): the OPTIONS/OP command dispatch/autocomplete entries, and
automation.rs's open_finalizes_and_purges_like_the_ui_open_path test
(kept main's version, which covers strictly more: solid_models/
material_base_dir reset on "new").
Full workspace suite passes (812 lib tests + this branch's own 24
constraint/persistence integration tests) except the one pre-existing,
unrelated font-test failure already present on main. Zero clippy
findings in the hand-written merge resolution itself; the ~1,171
warnings clippy reports are pre-existing in the 54 absorbed commits,
not introduced here.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Named parameters (docs/named_parameters_design.md, stages 3-4):
- ParameterTable (src/scene/named_parameters.rs): a document-wide table of
named formulas, each resolved through a shared expression evaluator with
cross-parameter references, cycle detection, and division-by-zero/parse
error reporting. SketchConstraint::driving_param becomes DrivingValue
(Literal | Named) instead of a bare f64, resolved at solve time by
sketch_solve::build_constraint via Scene::named_parameters.
- Persistence (src/scene/named_parameters_persist.rs): same lazy XRecord
model sketch_persist.rs already established, synced around open/save.
Bumps the sketch-constraint XRecord's own format version (1 -> 2) since
DrivingValue's shape doesn't line up with a version-1 blob's bytes.
- PARAMETERS command and its editor (src/ui/window/named_parameters.rs):
add/remove/edit rows, live validation, duplicate-name detection: Apply
defines the table and re-solves every constraint scope with a
named-parameter-driven constraint (command_driver.rs). Distance/Angle
constraint prompts (modules/draw/constrain/value.rs) now accept either a
literal or an existing parameter's name, mirroring how the constraint's
own value prompt already worked.
- Undo scope-down carried over from this project's own established pattern
(constraint-system stage 9): the geometry an Apply moves is one undo
step, but the parameter table edit itself has no undo entry yet — a
documented follow-up, not a silent gap.
UI scale setting (src/app/settings.rs, app/mod.rs, ui/window/options.rs):
a whole-interface scale percentage (50-200%), wired directly into iced's
own per-window scale_factor hook rather than per-panel font-size
plumbing, so it applies immediately to every panel, dialog, and the
ribbon at once. Persisted alongside the existing lineweight-display-scale
setting.
OPTIONS/OP command dispatch (app/commands/view.rs, app/commands/mod.rs):
OPTIONS was registered for autocomplete but had no dispatch arm — the
Start-tab welcome screen's own button was the only way to reach it.
Found live-testing the UI-scale setting, unrelated to that feature
itself. "OP" is AutoCAD's own short alias.
help/: a new self-contained static user-help manual (manual.md as the
authored source, tools/build_help.py splits it into the committed static
HTML pages plus search index) — not yet wired into the application.
64 new/updated tests across the named-parameters and constraint-prompt
changes.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Resolve curve intersections through cadkernel, retain empty exact results, gather analytic candidates independently of tessellation, and bound material uploads. Preserve large MINSERT data while limiting rendering work; remove obsolete review narratives.
Ports the fixes from fix/osnap-circle-arc-ellipse-intersection (and
this session's audit follow-up) onto this branch. All are independent
correctness/reliability fixes, unrelated to the parametric-constraint-
system work also in progress here.
1. OSNAP Intersection snap used a tessellated approximation instead of
exact geometry (GitHub discussion #1052): a circle/arc always
tessellates at a fixed 48 segments in the resident wire set
regardless of radius or zoom, so every circle/arc intersection snap
was off by construction. Added exact_circle_intersections, a
closed-form, frame-independent circle/arc-vs-circle/arc solver,
wired into both Intersection-snap code paths ahead of the
tessellated sweep. This branch's TangentGeom predates the
PlanarEllipse variant added upstream, so the ellipse/line
generalization from the upstream fix isn't ported here — this
covers the literal reported bug (a circle-circle case) using only
variants this branch already has.
2. Unbounded MINSERT allocation: array_offsets() reserved one Vec
element per array instance with row/column counts parsed as
unchecked u16s — a corrupt 65535x65535 pair requests ~96 GiB.
Fixed with a documented instance budget (20,000), enforced at both
the load-time corrupt-entity filter and a render-graph failsafe.
3. MINSERT spacing was scaled by the block's own scale:
insert_instance_transform composed the offset translation before
the instance transform, so x/y block scale multiplied the row/
column spacing too. Fixed by orienting the offset with rotation +
OCS only, then translating the already-scaled instance transform.
4. Automation "open" skipped the finalization (block-origin
normalization, raster-path resolution, source_path) and corrupt-
entity purge a UI open always runs. Added
io::load_bytes_finalized(path, bytes), used by automation's "open"
handler, which now also reports the purge count.
5. Material maps were uploaded without checking
device.limits().max_texture_dimension_2d, unlike raster-image
rendering (which already tiles to that limit). A mesh material is
sampled by UV, so it can't use the same tiling trick — instead,
upload_rgba_texture now downscales an oversized image before
upload, falling back to the 1x1 material color if malformed.
6. Sketch-constraint persistence/undo, audit-found while reviewing
this branch's own constraint system:
- Clearing every constraint in a scope after a prior non-empty save
left the stale XRecord in place, resurrecting the deleted
constraints on the next load. materialize_sketch_constraints_for_
save now actively removes the XRecord when a scope goes back to
empty.
- ERASE removing an entity took its constraints with it (design
doc's deletion policy), but sketch_constraints lives on Scene,
outside document/document.objects, so nothing captured that
mutation for undo — the erased geometry came back on undo, but
its constraints didn't. refresh_sketch_constraints's deletion
pass now records the touched scope's before-image into the same
UndoRecording the entity removal rides in, threaded through a new
sketch_constraints channel on DeltaSnapshot, so one undo restores
both together. HistorySnapshot::Delta is now boxed (clippy
flagged the resulting size skew across undo/redo stack variants).
31 new/adapted tests across all six fixes. Full workspace suite passes
except the one pre-existing, unrelated font-test failure also present
on main.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Automation's "open" op called io::load_bytes directly and installed
the result as-is, skipping everything the canonical path-based loader
(load_file_for_open / finalize_loaded_outcome) runs after parsing:
block-origin normalization for legacy non-zero-origin blocks,
relative raster/material path resolution, source_path assignment,
and the corrupt-entity purge the UI open path always applies. The
same file opened two ways behaved differently, and headless/automation
use was more exposed to malformed geometry with no filter at all.
Added io::load_bytes_finalized(path, bytes), which runs load_bytes
plus that same normalization and purge using the real path (not just
its basename, which the old code passed to load_bytes as the "name"
parameter — losing the directory needed for raster-path resolution
and source_path). automation.rs's "open" handler now uses it and
reports the purge count as a "purged" field in the response.
load_bytes itself is unchanged, since the byte round-trip tests
(sketch_persist.rs, named_parameters_persist.rs) deliberately want
the simpler byte-only path with no real filesystem path behind it.
New test constructs a document with one normal and one corrupt
(zero-radius) entity, saves it, opens it through automation, and
checks the corrupt entity was purged and source_path was set —
proving the automation path now matches the UI path.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Ports FreeCAD's planegcs 2D geometric constraint solver to Rust as a new
crates/ocs_gcs workspace member, and wires it into the document/scene model
as a persistent SketchConstraintSet: Coincident, Horizontal/Vertical,
Parallel/Perpendicular, Equal, Tangent, Distance, Angle and Radius
constraints that re-solve live (including mid-drag, not just on release),
render glyphs and a DOF badge, flag conflicts with a one-click resolver
pill, and round-trip through save/load via an XRecord blob.
Also includes docs/command_reference.md (a statically generated reference
of every registered command and its prompt sequence) and
docs/named_parameters_design.md + docs/backlog.md, which hand off the next
queued feature (a named-parameter/expression table) and consolidate every
deliberate scope-down and open gap found along the way, so a fresh session
has a complete starting point.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Reuse the application library from the binary entry point instead of compiling the module tree twice. Keep desktop-only transports and caches out of web builds, remove unused test code, and check executable targets in CI.
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
Saving to a path that already holds a drawing fails on Windows with
"could not verify <path> before saving: ... (os error 33)". Save As over any
existing drawing is affected; saving to a new path is not.
EditLease::acquire() opens the destination and takes the platform drawing
lock on it, then the save fingerprints the same path to check nothing changed
underneath. LockFileEx is mandatory and scoped to the handle that took it, so
that second open is refused against our own lock, lands in the non-NotFound
arm and aborts the save. flock on Unix is advisory and the second open
succeeds, so neither CI nor a Linux desktop sees it.
Read through the handle the lease already holds instead. A duplicate shares
the lock ownership, so the pre-replace re-check on the save worker can use
one too rather than re-opening the path.
The same collision reached the headless open op, which read a path this
session may hold leased: automation's save_then_open_round_trips fails on
main for that reason, one line after the save it no longer reaches.
Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten.
Offer retry, reload, Save As, or explicit overwrite recovery.
Closes#498
PICKADD 0 = click/box/lasso replaces the selection (Shift toggles),
empty click clears; PICKADD 1 (default) keeps today's accumulate.
PICKDRAG 1 = press-drag spans a rectangle marquee through the box
machinery; PICKDRAG 0 (default) keeps the freeform lasso. Both persist
in settings.json. Also re-points the start-page gate test at the
start_allowed fn it moved into (#388/389 refactor left it broken).
Closes#226
Co-Authored-By: Claude Fable 5 <[email protected]>
Deleting/renaming Standard was already blocked in the five style
managers, but a file saved without the built-ins (foreign or damaged)
opened with broken style dropdowns and nothing to reference — re-seed
missing Standard text/dim/table/mleader/mline styles on every open.
Every modal dialog now shows its name in the title bar: centred at
15px across the dialog width, with the move grip and close button
overlaid at the right edge. The bar takes an explicit width from the
caller — a Fill child inside the Shrink frame would blow the dialog
out to the full screen.
Co-Authored-By: Claude Fable 5 <[email protected]>
Never fail silently on PDF export; add EXPORTPDF <path> (#369).
The export flow handed off to an async rfd save dialog and mapped a None
result straight to Task::none(). On Linux None also means the dialog never
opened (broken XDG portal / missing zenity), and rfd reports that only via
log, so PDF export could end in total silence. The dialog-None arms now
print a message pointing at EXPORTPDF <path>, and EXPORT/EXPORTPDF take an
optional path that exports straight to a file with no dialog — the
workaround where the native dialog can't open, and a headless entry point.
Conflict: the fix (display.rs, file.rs, mod.rs, COMMANDS.md) auto-merged;
only the additive test block in automation.rs conflicted. Resolved by
keeping main's tests and adding the two #369 tests.
# Conflicts:
# src/app/automation.rs
Match text style and height between TEXT/MTEXT objects (#361).
MATCHPROP copied only the generic properties, so Text->Text and
Text->MText matching left the destination's style and height unchanged.
The MatchProperties handler now also carries style + height between
text-like objects, both directions, like AutoCAD's "Text" special
property. bump_geometry re-tessellates so matched text repaints at its
new size.
Conflict: the fix (command_driver.rs) auto-merged; only the additive
test block in automation.rs conflicted with tests main added since the
PR branched. Resolved by keeping all four tests.
# Conflicts:
# src/app/automation.rs
Donate, Send Feedback, OCS Web and Manage > About all answered "No
drawing open — use New or Open first." and did nothing.
Two doors guarded the welcome tab and the outer one shadowed the inner.
`dispatch_command` already knew which commands stand alone — About,
Donate, Report and the links were all on its list — but
`on_ribbon_tool_click` refused every ribbon event before dispatch was
ever reached.
The welcome page's own buttons emit RibbonToolClick, so they are only
ever clickable while `is_start` holds: the refusal covered exactly the
case they exist in, and they had never worked. New Drawing / Open File /
Plugins send their own messages, which is why the page still looked
alive.
So the ribbon door now defers commands to dispatch and keeps refusing
only what touches the scene. The policy stays in one place instead of
being half-remembered in two. A drawing tool is still turned away on the
welcome tab (#299) — the wording just comes from dispatch now.
The same gap was swallowing tools the issues never mentioned: CUI and
ALIASEDIT configure the application, not a drawing, and were missing
from the list, so fixing only the ribbon door would have moved their
refusal one door down rather than removing it. HELP and CUILOAD /
CUIIMPORT join them.
The new test drives the real message path and fails on the old code with
the reported wording. It reads the allowlist from source rather than
dispatching DONATE / REPORT / WEBVERSION, which shell out to a browser —
that also fails the day someone adds a welcome-page button and forgets
the list.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
MATCHPROP only copied the generic properties (layer, color, linetype,
linetype scale, lineweight), so matching text to text left the
destination's style and height unchanged. Copy the source's text style
and height to TEXT and MTEXT destinations, in both directions, like
AutoCAD's "Text" special property.
Adds an in-process regression test that drives the real interactive
command path (source pick -> destination selection) and fails on the
old code with the reported symptom.
Fixes#361
Co-Authored-By: Claude Fable 5 <[email protected]>
Export to PDF could end in complete silence: the flow hands off to an
async rfd save dialog, and when that future resolves to None the app
mapped it straight to Task::none(). None does not only mean the user
cancelled — on Linux the XDG portal SaveFile call can fail outright
(rfd then tries a zenity fallback, which may not be installed) and the
future still just resolves to None. rfd reports those failures only
through the `log` crate, and logging is opt-in (--log / RUST_LOG), so
the reporter saw exactly what #369 describes: no dialog, no error, no
console output, no file.
- EXPORT / EXPORTPDF now accept an optional path argument that exports
straight to the file with no dialog — both the workaround for systems
where the native save dialog cannot open and a headless-testable
entry point. A relative path lands next to the drawing; a missing
extension gets .pdf.
- The dialog-None arms (PlotExportPath / PlotWindowExportPath) now push
a command-line message pointing at EXPORTPDF <path> instead of doing
nothing.
- The success message prints the full output path, not just the file
name, so users can find the PDF.
Verified end to end on the real binary via --serve: LINE + CIRCLE then
EXPORTPDF <path> writes a valid PDF with the expected geometry. Both
new tests fail on the previous code.
Fixes#369
Co-Authored-By: Claude Fable 5 <[email protected]>
ROTATE's reference-point step prompts "or skip: type angle directly", but
on_text_input only handled the later Angle step. So picking a centre and
typing the angle did nothing, and the next Enter cancelled the command —
the objects never rotated, which reads as the rotate "erasing" / losing
them.
Accept a typed angle at the reference-point step too, rotating directly
about the centre (the reference point stays optional). Added an automation
regression test that rotates a line 90° with centre + angle only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Bare PDMODE / PDSIZE / LTSCALE / CELTSCALE now enter a generic
ValuePromptCommand front-end that prompts for the value and delegates to the
existing inline `<name> <value>` handler via CmdResult::Dispatch, so they
prompt step-by-step like UCS. A bare Enter reports the current value;
`PDMODE 3` still works as one typed / headless line.
Only pure single-value setting commands are converted. Dialog-opening
commands (LAYER, PURGE, DIMSTYLE, STYLE …) intentionally keep their
bare-name behaviour (open the panel), and subcommand-tree commands keep
their inline handlers — all already typable with arguments since F1.
Adds headless test value_prompt_commands_inline_args. Fourth phase of the
unified step driver.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Space is now a literal character in the command line, so a whole command
line — `UCS Z 90`, `LINE 0,0 10,10`, `PDMODE 3` — is typed before Enter
instead of the first Space submitting the buffer. Enter tokenises the line
and runs it.
The command line and the headless automation feeder now share one runner,
`run_command_line` (moved to the non-wasm-gated cmd_result.rs): a single or
inline-argument command dispatches as-is; a multi-token line whose first
word starts an interactive tool feeds the rest as steps; plugin commands
get the whole line first (#162). The shared feeder is now UCS-aware —
typed coordinates are interpreted in the active UCS in both the GUI and
headless. `run_headless` is a thin wrapper over it.
A free-form text command (TEXT / MTEXT / a name with spaces) still receives
the whole line as one input, via the existing `wants_text_with_spaces`
guard. Unfocused Space still repeats the last command.
Closes the command-line half of #169 generically for every command, not
just UCS. Third phase of the unified step driver: every input source funnels
through feed_command.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Bare `UCS` now enters an interactive front-end (UcsCommand) that gathers the
option then its value as separate steps, instead of the command line
submitting `UCS` alone on the first Space. The front-end delegates execution
back to the existing inline `UCS <option> …` handler via a new
`CmdResult::Dispatch(String)`, so the coordinate-system math and the
save/persist path are untouched.
Now `UCS Z 90` works the same from the command line (Space-separated),
headless automation, and any source that feeds the active command — closing
the input half of #169. Inline `UCS Z 90` (one-shot / paste) still hits the
inline handler directly.
Second phase of routing input sources through the unified step driver: the
interactive command is driven by feed_command like any tool. Adds headless
test ucs_interactive_inline_args.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Introduce the single place every input source funnels through to drive the
active command's step machine: a `StepInput` vocabulary (one variant per
`CadCommand::on_*` method) and `OpenCADStudio::feed_command`, which routes
the input to the matching `on_*` and applies the result. Sources keep their
own classification (raw input → which StepInput); only the routing is
centralized.
Migrate the headless automation feeder onto it: feed_active_cmd builds a
StepInput, the terminator sends StepInput::Enter. No behavior change —
automation_ops_round_trip still passes.
Also fixes headless inline-argument loss for plugin commands (#162):
run_headless now offers the whole line to plugin dispatch first (plugins
parse their own inline args, e.g. `HC_PIPE 2B 2C 1.25 0.013`), falling back
to the first-word + fed-tokens path for built-in interactive tools.
First phase of routing all input sources (command line, dynamic input,
plugin API, viewport clicks) through one driver.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Whole command/snap/grip/cursor chain now builds on f64 DVec3 end to end,
so picked points, snaps and grip drags stay precise at UTM-scale
coordinates instead of quantizing to ~0.5 m through f32.
- All ~80 command files migrated: draw (line/circle/arc/ellipse/shapes
in full f64; others via point methods), modify, annotate, insert, view,
inquiry, layers, groups, clipboard, layout.
- Entity apply_grip impls updated for GripApply::{Absolute,Translate}(DVec3).
- Construction sites (EntityTransform/CmdResult/DynAnchor) emit DVec3;
wire-preview boundaries cast to f32 only at the GPU edge.
- Modify/annotate commands keep internal Vec3 geometry where precision is
non-critical, casting at the DVec3 boundary; key draw commands store f64.
Builds clean (no warnings). Visual verification of crosshair/snap/grip at
extreme UTM zoom pending.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the ad-hoc `--serve` arg sniff with a clap-parsed CLI:
- positional <file> to open at startup (also the OS file-association path)
- --new, --read-only, --backend, --safe-mode/--no-gpu
- --serve/--port (headless JSON automation, unchanged behaviour)
- --export IN OUT (headless format convert via the automation load/save infra)
- --script FILE (run command lines at startup)
- --log/RUST_LOG, plus --version/--help for free
GUI-only options pass to app::boot through a OnceLock config; read-only
gates the save paths. The web build skips parsing (no args).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OpenCADStudio --serve --port <N> listens on 127.0.0.1:<N> and serves the
same line-based JSON protocol over the socket (one client at a time; the
document session persists across reconnects), as an alternative to
stdin/stdout. ocs.py grows a port= option to connect either way.
Co-Authored-By: Claude Opus 4.8 <[email protected]>