Design note for a future migration: motivation, blockers (single-shader
rendering, VPORT rect vs split-tree fidelity, per-pane overlays), a phased
plan starting with a shader-per-pane spike, and the note that pane_grid
does not enable floating viewports.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Thin vertical separators between every control in the top-left chip
(split / split / render-mode / grid / snap) for clearer grouping.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move grid and grid-snap toggles off the status bar (and its customization
menu) into the top-left viewport chip, now a single container holding the
horizontal/vertical split buttons, render-mode picker, and grid/snap
toggles, sized to the render-mode picker. Add grid + magnet (snap) icons.
Render the grid per model tile from each tile's own grid_on, in its own
bounds and camera, instead of a single grid in the active tile. This stops
the grid flickering on/off as the cursor crosses panes or leaves the
viewport, and lets each pane show its own grid independently.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Make grid-snap a per-drawing view setting like grid visibility: drop it
from the persisted OSNAP set (settings.txt) and round-trip it through each
viewport's VPort SNAPMODE instead.
Grid display and grid-snap are now stored per model tile (like render
mode), written to and read from each tile's *Active VPort entry. The live
toggles mirror the active tile, and grid/snap are adopted from the active
viewport whenever it changes — on file load, tab switch, model-tile
activation, and new drawing — so they follow the active viewport rather
than a global preference.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Grid on/off now round-trips through the drawing's *Active VPort, so it is a
per-file view setting like every other CAD app treats it. Drop show_grid
from UserSettings (and its settings.txt key) and instead adopt the opened
file's saved grid_on on load. A legacy "grid=" line in an old settings file
is harmlessly ignored.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Mirror the in-app grid and grid-snap toggles (plus the adaptive grid
spacing) onto the *Active VPort entries so a saved DWG/DXF reflects what
the user actually has on, instead of always writing VPort defaults. The
flags are stamped when the toggles change and again before a save, and
carried across the camera-sync VPort rebuild so they survive an orbit/pan.
Only grid_on, snap_on and grid_spacing have an OCS counterpart; the other
VPort view fields (view twist, clip planes, lens length, etc.) have no
in-app source and stay at their faithful defaults.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
acadrust's DWG VPORT writer hardcoded grid_on=true, so every file OCS saved
showed a phantom grid in other CAD apps (BricsCAD, DWG TrueView) that was
invisible and unselectable in OCS. The writer now emits the VPort struct's
real grid_on (default false for OCS, which never forced it on), so the
stray grid no longer appears. (#121)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Neutralise vendor references in comments to plain behavioural descriptions
(object acquisition / object snap). Remove the PICK_HIGHLIGHT and
PICK_HIGHLIGHT_DIM wire colours: their only consumer was the stripped
storm-sewer add-on, and the retained plugin acquisition pathway draws the
ObjectPick snap glyph and per-command hover previews instead, so the
constants are dead.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Remove accidental dead code (unused imports, the superseded paste tool()
helper, a never-read TORIENT handles field, a duplicate let binding, an
unneeded mut) and mark intentional-but-unwired scaffolding with
allow(dead_code): the web-only save_to_bytes, the plugin loadable() probe,
the planned PICK_HIGHLIGHT colours, the DynFieldSpec label, and the layout
ribbon helpers. Native build is now warning-clean apart from the upstream
nom/quick-xml future-incompat notices.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The dim-line break test used text_half + DIMGAP as its vertical
threshold, but DIMTAD 1/4 places the text exactly text_half + DIMGAP off
the line. The two terms cancel at the same DIMSCALE-scaled value, leaving
the comparison on the knife's edge — so a gap flickered into the line
depending on the DIMGAP/DIMSCALE combination. Drop DIMGAP from the
vertical threshold so above/below text stays strictly clear and the line
stays whole; centred text (DIMTAD 0) still breaks for the DIMTFILL
background. The horizontal half-width keeps DIMGAP so a real break clears
the text. (#94)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The polyline was only committed to the document on finish, so its partial
geometry produced no snap points — you could not snap to vertices already
placed. Publish it as a real LwPolyline at the second vertex and replace
its geometry in place as later vertices land, so it lives in the document
(and the snapper) throughout the draw.
New CmdResult::CommitLiveEntity / UpdateLiveEntity{finish} plus a
set_live_handle() hook let a command own an entity mid-run. The create
pushes a single undo snapshot; in-place updates push none, so the whole
object reverts as one unit. Updates preserve the entity's handle and
layer (a fresh built entity carries a NULL handle that would desync it
from the document map key and drop it from rendering). (#119)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The DYN angle box was placed at full cursor radius along the mid-angle
direction. With a near-zero sweep that direction collapses onto the
cursor ray, planting the box on top of the cursor / snap point and
hiding it. Pull the box back along the ray and offset it to the side
opposite the distance box so the snap point stays visible. (#124)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Object snaps were chosen purely by nearest screen distance, so the
"continuous" snaps that sit on the geometry (Nearest, Perpendicular)
were almost always closer than a discrete Endpoint/Midpoint/Center and
silently suppressed every other enabled snap. Selection is now
priority-then-distance: a higher-priority snap inside the aperture wins,
distance only breaks ties within a priority. (#118)
The aperture gate is `!(d2 < radius2)` rather than `d2 >= radius2` so a
NaN distance from degenerate geometry is rejected instead of passing the
gate and being chosen on rank alone (which fed a NaN point to the fill
tessellator and panicked).
Perpendicular now drops its foot from the command's drawing origin
(`Snapper::from_point`, set before each snap) instead of the cursor, so
the new segment is genuinely perpendicular to the target line rather than
just the nearest point on it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Turn the ribbon Clipboard "Paste" button into a dropdown:
- Paste — existing pick-a-point paste.
- Paste to Original Coordinates — drops the clipboard at its source
coordinates with no prompt.
- Paste as Block — wraps the clipboard contents in a new block definition
and starts an interactive insert (prompts for the drop point, with the
geometry rubber-banding under the cursor).
Also fixes two bugs the new commands exposed:
- entities_centroid summed the NaN separator points wire models carry,
so the paste base point came out NaN; non-finite points are now
skipped. A NaN base previously froze block tessellation outright.
- PASTEORIG added clipboard entities with their original handles
(duplicates on same-document paste); it now goes through
add_entity_clone and merges referenced records like the other pastes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
`document.add_entity` only assigns the top-level handle, leaving inline
sub-entities that carry their own — INSERT attributes and 3D-polyline
vertices — with the source's handles. Copying such an entity therefore
produced duplicate handles in the document, which corrupts the saved DWG
so other CAD apps (QCAD, DWG TrueView) refuse to open it.
Add `Scene::add_entity_clone` / `reset_clone_subhandles`, which allocate
fresh handles for the entity and every inline sub-entity, and route all
duplication paths through it: COPY/ARRAY/MIRROR-copy (via copy_entities)
and clipboard paste. (#129)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Copy/paste between drawings dropped the entities' layer (and linetype /
text + dim style): the clipboard held only the entities, which reference
those records by name, so pasting into a drawing without a matching
record left the entity dangling on a non-existent layer. (#129)
Snapshot the referenced table records from the source drawing at copy
time (ClipboardDeps::capture) and, on paste, recreate any the target
drawing lacks with a fresh handle (merge_clipboard_deps). A record that
already exists by name in the target is left untouched — the target's
own definition wins; only missing ones are imported from the source.
Same-document pastes are unaffected (every record already exists).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Modals blocked the mouse but not the keyboard: keystrokes from the global
key subscription leaked past an open dialog into the command line and
fired as commands once it closed. Guard `update()` so that while a modal
is open, Escape closes it and every other keystroke-derived message
(command-line input/edit, F-key toggles, edit shortcuts, undo/redo) is
swallowed. A modal's own text fields keep working — they emit their own
messages, which are not in the blocked set. (#126)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The web save path discarded the chosen format version (`let _ = version`)
and `save_to_bytes` never applied it, so every browser download was
written at the document's default version (AC1032 / DWG 2018) regardless
of the format picked in the Save dialog. (#122)
Thread `version` into `save_to_bytes` and set `doc.version` there, the
byte-buffer counterpart to `save_as_version`. Saving as DWG 2013 now
writes an AC1027 file.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Dynamic-input value boxes, the prompt pill and the UCS axis labels are
drawn with `frame.fill_text`, whose `canvas::Text` defaults to
`Shaping::Auto`. Auto picks Basic shaping for ASCII-only strings, which
renders on desktop but draws nothing on the web (wgpu/webgl) backend, so
the all-digit distance/coordinate boxes and X/Y/Z labels were blank
(the angle box rendered only because its `°` upgraded it to Advanced).
Typed values landed correctly but were invisible. (#117)
Set `shaping: Advanced` on every canvas fill_text in the overlay.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
UI chrome (dropdown carets, undo/redo, checkmarks, close/menu/osnap,
layer/file-menu icons) and ribbon tool icons were drawn as Unicode
glyphs. Desktop fills missing glyphs via system-font fallback, but the
web build bundles only Fira Sans with no fallback, so they rendered as
empty tofu boxes (issue #116).
Add a shared src/ui/icons.rs (SVG-backed, tintable) plus bundled assets,
and switch every glyph site and IconKind::Glyph tool def to SVG.
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]>
query now emits geometry for Ellipse/Text/MText/Polyline/Insert (on top
of Line/Circle/Arc/Point). Add layers op (name, ACI/RGB colour, on/off,
frozen, locked + current layer) and header op (units, PDMODE/PDSIZE,
LTSCALE, annotation scale, current layer/text style). ocs.py gains
layers()/header(). Lets an agent read the drawing's structure and
settings, not just entity counts.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Extend InteractiveCommand with needs_object_pick / on_object_pick so a
plugin tool can prompt the user to pick an existing entity (handle + point)
rather than a free point — the structure-pick path Storm Sewer's SS_PIPE
needs to connect existing structures. The host adapter delegates to the
internal entity-pick flow; over --serve the pick is fed as a hex handle.
Stays API v2 (no released plugin implements InteractiveCommand yet).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Plugins could dispatch commands but not register interactive (click-to-
place) tools — the gap mf4633 flagged on #100 for Storm Sewer's SS_INLET
/ SS_PIPE. Add an InteractiveCommand trait + CommandStep to ocs_plugin_api
and HostApi::start_interactive; a host adapter bridges it to the internal
CadCommand, so a plugin tool drives the host's point-collection flow.
Hybrid by construction: the same command works by clicking in the viewport
AND by feeding coordinates over --serve (run "CMD x,y x,y"). Adding a
HostApi method changes the contract vtable, so API_VERSION bumps to 2 —
v1 plugin binaries are now refused at load.
Part of the #100 extensibility epic.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add undo and redo ops to the headless server (synchronous via update()).
Combined with select, the modify commands MOVE/COPY work through the
existing dispatcher — "select type=Line" then run("MOVE 0,0 10,0")
translates the selection. ocs.py gains undo()/redo().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a select op that sets the document selection (by hex handles from
query, or by type/layer, or clear). Selection drives modify commands, so
an agent can query → select → run("ERASE") to act on specific entities.
Completes the modify half of the act/observe loop. ocs.py gains select().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a query op to the headless server: list entities with handle, type,
layer and basic geometry (Line start/end, Circle/Arc centre+radius, Point
location), filtered by type and/or layer and capped by limit. Closes the
observe half of an agent's act/observe loop. ocs.py gains ocs.query().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
run now feeds coordinate tokens to interactive tools: "LINE 0,0 10,10"
starts the tool, feeds the points, and terminates as if Enter were
pressed — creating real geometry through the existing command system. The
headless session also leaves the welcome (Start) tab so drawing commands
aren'\''t blocked. Verified end-to-end: LINE/CIRCLE create entities that
save and reopen.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add an external automation API (issue #29 / #100 track 2): `OpenCADStudio
--serve` runs without a GUI and is driven over a line-based JSON protocol
on stdin/stdout — open / new / run / entities / save. State persists
across requests so a script or AI agent can act, observe, and act again.
`run` drives the app's existing command system rather than a separate
binding, so coverage grows with the app; synchronous commands apply now,
pick-based interactive ones come once coordinate feeding is wired. Ships
a ~100-line example ocs.py client over the same protocol — no FFI to
maintain. Headless works because the app already constructs and dispatches
GUI-less in tests.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OpenCADStudio ships no built-in plugins, so the spec and scaffold no
longer describe the in-tree inventory path, demo_plugin, or the fictional
Storm Sewer reference. The architecture doc now covers the external cdylib
model end to end (ocs_plugin_api contract, export_plugin!, building +
publishing per-platform releases, runtime loading, the marketplace +
curated registry, the approach-B ABI caveat). The template becomes a
standalone cdylib crate (Cargo.toml + src/lib.rs + release workflow)
instead of an in-tree module.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add an Uninstall button on installed external plugin cards that deletes
the package folder (effective next restart — the library stays resident
for the session). Reinstalling or upgrading via the marketplace now
clears any stale native library with a different name before writing the
new one, so the loader always picks the freshly installed binary.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OpenCADStudio no longer bundles plugins — every add-on is an external
cdylib loaded from the plugins folder or installed via the marketplace.
- Remove the demo plugin and the in-tree ocs_example_plugin crate.
- Remove the inventory-based built-in registration path (PluginRegistration,
all_plugins, installed_manifests); registry now serves core ribbon tabs
plus loaded external plugins, and try_dispatch routes only to externals.
- Plugin Manager drops the 'compiled into this build' section; the
enable/disable toggle moves onto loaded external cards.
- Prune the now-dead HostSession/DocumentTab helpers and re-exports.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a Plugin Manager marketplace that installs external add-ons from a
GitHub repo's Releases. A curated registry (plugins/registry.json in this
repo, extended by PR) lists discoverable plugins; the host fetches it,
shows each entry with a release dropdown, and on install downloads the
asset matching the user's platform plus plugin.toml into the plugins
folder (API-version checked first). Users can also link arbitrary
owner/repo manually. Linked repos persist in settings.
Desktop only (ureq + serde_json); the install path reuses the phase-2
loader. Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move BuiltinPlugin into ocs_plugin_api (host feature) so out-of-tree
crates can implement it, and add export_plugin! to emit the two C
symbols a cdylib exposes: ocs_plugin_api_version (checked first, so an
ABI-incompatible build never runs) and ocs_plugin_register -> boxed
BuiltinPlugin. The host loads every compatible package from the plugins
folder at startup via libloading (desktop only), keeps the library
resident for the session, and merges its ribbon tab + command dispatch
into the same paths as built-ins (honouring enable/disable). The Plugin
Manager shows external packages with a Loaded / incompatible status.
Approach B: the plugin hands back a boxed trait object, assuming a
matching toolchain + ocs_plugin_api version (the version symbol enforces
the latter). crates/ocs_example_plugin is the reference cdylib.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase-2 groundwork: scan <config>/OpenCADStudio/plugins/<id>/plugin.toml,
parse the documented keys (no new TOML dep), check for a platform native
library beside it, and gate on the API version. The Plugin Manager gains
an "External (plugins folder)" section showing each package with a
status pill (Ready to load / API incompatible / No library). Discovery
only — actually loading the cdylib is the next step.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Plugins targeted the host's concrete HostSession type. Add a HostApi
trait in ocs_plugin_api behind an optional `host` feature (the only thing
that pulls acadrust, so the core crate stays dependency-free). HostSession
implements it and BuiltinPlugin::dispatch now takes `&mut dyn HostApi`, so
an out-of-tree add-on compiles against the contract crate alone. Per-tab
plugin state is reached through object-safe plugin_state* helpers.
Completes the phase-1 host-surface extraction in the #100 epic.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Points could only render as a 1px dot — no way to change PDMODE/PDSIZE.
- PDMODE / PDSIZE commands set the header value and rebuild the glyphs.
- DDPTYPE opens a Point Style dialog: a 5x4 grid of rendered glyphs
(shape x enclosure), a size field, and relative/absolute radios.
- Relative (<=0) PDSIZE now sizes from the current world-per-pixel so the
marker stays a roughly constant on-screen size across zoom, rebuilding
on zoom via the existing camera-generation cache key. Absolute (>0)
scales with the world as before. Switching to absolute with a zero size
seeds a positive value from the on-screen size so it stays representable.
Part of the #100 extensibility epic (point style surfaced in #106).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Plugins persisted domain data via the raw acadrust::xdata API with
boilerplate and no APPID registration. Add read_record / write_record /
remove_record keyed by entity handle; write_record replaces an existing
record for the same app and registers the application in the APPID table
so the data round-trips through DWG/DXF for other CAD apps.
Part of the #100 extensibility epic (surfaced in #106).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add-ons had no way to request a file picker — only the host's own Open
dialog. PluginFileDialog lets a plugin tool ask the host to open a native
picker; on selection the host dispatches "<command> <path>" back to the
plugin with original case preserved (bypassing the command-line
upper-casing that mangles case-sensitive paths on Linux/macOS). The demo
plugin gains an Import tool exercising it.
Part of the #100 extensibility epic (surfaced in #106).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The manager was a read-only inventory. Each plugin now has an
Enable/Disable toggle: a disabled plugin keeps its manifest listed but
drops its ribbon tab and command dispatch. The disabled-id set persists
in settings.txt (disabled_plugins=) and the ribbon rebuilds on toggle.
Part of the #100 extensibility epic.
Co-Authored-By: Claude Opus 4.8 <[email protected]>