Commit graph cad-editor/src/app/update.rs
Author SHA1 Message Date
Hakan Seven
10e4c18ba7 chore: silence native and wasm build warnings
Gate target-specific code so neither build warns about the other's items:
native-only helpers (view, window icon, file dialogs, file association, plugin
host/guard/registry, self-update) are cfg'd to non-wasm; the wasm-only font
fallback (script_of / cjk_lang / request) is cfg'd to wasm. Drop an unused mut,
allow the few genuinely-conditional dead items (mesh_click_hit, GPU layout
helpers, parity stubs), and gate the embedded PlotSettings-unrelated dead
constants. Both targets now build clean (only the unavoidable nom/quick-xml
future-incompat dependency notice remains).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 02:03:34 +03:00
Hakan Seven
26479b6faa fix(grid): independent, clipped grid per viewport
Grid (and grid-snap) state was shared through the global show_grid flag, so
toggling it in model space also turned it on in paper space, and a floating
viewport's grid painted across the whole paper — and beyond the 3-D view when
zoomed in.

- Store grid/snap per view: a model tile in model space, the active floating
  viewport or the layout's sheet viewport in paper space, round-tripped via the
  viewport status flags.
- adopt_view_display on layout switch and viewport enter/exit so the live
  toggle follows the view you are in.
- Render one grid per view (paper_viewport_grid_views): the sheet over the
  paper, each floating viewport clipped to its screen rectangle.
- Clamp the grid clip to the widget so an overflowing viewport rectangle never
  paints grid outside the 3-D view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 00:18:01 +03:00
Hakan Seven
ba834fb166 fix(layout): faithful paper-space round-trip and page setup (#156)
Builds on the acadrust viewport/PlotSettings round-trip fixes:

- Write the sheet viewport's view position into view_center (DCS) with
  view_target at the origin, matching AutoCAD; the old view_target form shifted
  the layout.
- PAGESETUP now writes paper size, rotation, origin and units onto the Layout's
  embedded PlotSettings (not just a side object), and bumps geometry, so an edit
  shows on the sheet immediately and survives a save.
- Draw the printable-area guide (paper inset by plot margins, rotation-aware) as
  a dashed rectangle, matching AutoCAD's layout view.
- Plot/PDF export falls back to the Layout's embedded PlotSettings via
  effective_plot_settings(), so a loaded file's rotation/origin/scale apply.

Bumps acadrust to pick up the viewport-status and PlotSettings round-trip fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:47:36 +03:00
Hakan Seven
8c48233dcf merge: TEXTEDIT command + TEXTEDITMODE sysvar (#130)
Merge PR #130 (KarimJerbi) and resolve review findings:

- Conflict (settings.rs): keep current persisted-settings layout, append
  texteditmode field.
- Conflict (command/mod.rs): keep both new CmdResult variants — main's
  live-entity ops and the PR's text-edit suspend/resume.
- Bug: invalid TEXTEDITMODE value no longer aborts via the Measurement
  arm; it now re-prompts with the error and stays active.
- Dedup: single parse_texteditmode() helper replaces the value parsing
  duplicated across commands.rs, settings.rs and textedit.rs.
- Strip trailing whitespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:05:54 +03:00
Hakan Seven
9f447308a6 feat(block): double-click to edit a block in place (#136)
Double-clicking a block reference (its wire or its solid body) now enters
in-place block edit (REFEDIT), so its geometry can be modified and the change
reflects in every instance on REFCLOSE.

Also drop REFEDIT's "non-uniform scale not supported" restriction: the
session now carries the INSERT's full forward/inverse affine (OCS, rotation
and non-uniform / mirrored scale) and applies it via the entity transform,
instead of a uniform scale + rotate + translate sequence. Mirrored blocks
(e.g. x_scale = -1) are editable now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:43:59 +03:00
Hakan Seven
cd10aa0363 fix(select): click the front-most solid across top-level and block bodies
Clicking a block whose body is a solid tried top-level solid meshes first
and block-internal ones only as a fallback, so a stray solid behind the
block always won and the block couldn't be picked. Merge both into one
depth-sorted hit-test (solid_click_hit) keyed by the solid's own handle or
the parent INSERT, so the front-most body wins regardless of kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:24:31 +03:00
Hakan Seven
ae9940929c fix(select): box/lasso-select blocks whose body is a solid
Box/lasso only tested top-level solid meshes, so a block whose visible body
is a solid (its mesh lives in the per-INSERT instanced cache, and it has no
wires to catch) was never picked up by a window/crossing/lasso. Add
block_mesh_box_hit / block_mesh_poly_hit that test each INSERT's instanced
block solid meshes and resolve a hit to the parent INSERT, and wire them
into the selection paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:03:56 +03:00
Hakan Seven
d5f1348f1b fix(3d): box/lasso-select 3D solids and refresh them after a transform
Box, window, crossing and lasso selection only tested wires and hatches, so
3D solids (which render as meshes) could never be selected that way. Add
mesh_box_hit / mesh_poly_hit that project the solid mesh and test it against
the box / lasso (window = all vertices inside, crossing = any vertex inside
or the region sitting inside the solid), and wire them into the selection
paths.

Also re-tessellate solids after a move/rotate/scale/mirror/copy: the entity
moved but its cached mesh didn't, so the solid stayed drawn at its old
position. Rebuild the mesh caches when a transformed/copied handle is an
ACIS volume.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:45:49 +03:00
Hakan Seven
5f3eaa621c fix(properties): recolour solid meshes on a colour/layer change
ACIS solids bake their colour into the mesh, so a property-panel or ribbon
colour (or layer) change left the solid showing its old colour — only the
wires, which resolve colour at render time, updated. Run recolor_meshes
(now covering block-definition meshes too) when property targets are
invalidated, and route the ribbon colour change through the same path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:12:58 +03:00
Hakan Seven
fd15dd0aac feat(select): invert selection from the right-click menu
Add an Invert Selection entry to the selection context menu (below
Select Similar): it replaces the current selection with every other
selectable object in the active layout. Candidates come from the visible
wire set, so objects on off/frozen layers stay excluded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:31:05 +03:00
Hakan Seven
082018756e fix(hover): defer rollover pick until the cursor stops
The rollover hit-test now sweeps wires, hatches, block-internal hatches
and shaded solid bodies, which is O(N) per cursor frame and was freezing
the crosshair on large drawings. Each idle move now clears the live
highlight and arms a dwell timer; HoverDwellTick runs the pick once the
cursor has been still for HOVER_DWELL_MS (120 ms), using the active
tile's bounds so the deferred pick matches the entity actually under
the cursor in tiled viewports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:43:33 +03:00
Hakan Seven
f860fb39c2 feat(plugin): wrap external-plugin calls in a panic guard
A buggy external plugin that panicked anywhere — during load (version,
register, manifest), during ribbon build / dispatch, or inside an
interactive command (prompt, on_point, on_enter, on_object_pick) — took
the host down with it (#134). Run every cdylib entry point under
catch_unwind so the host stays up: the plugin's call is treated as
inert (load fails, dispatch returns "didn't handle it", interactive
hooks resolve to Cancel) and a "Plugin <hook> panicked: …" line lands
in the command line on the next tick. (#145)

Scope is Rust panics across the cdylib boundary, not genuine UB —
real isolation (segfault-proof) still requires out-of-process plugins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:17:37 +03:00
Hakan Seven
e1c5917644 fix(web): paste clipboard text into the TEXT / MText editors
Ctrl+V was globally bound to the entity paste command, so it never pasted
into an open text editor, and even when routed there iced's clipboard read
is a no-op on the web. Route Ctrl+V to the focused editor and, on the web,
read the system clipboard via the browser's async Clipboard API (the
keypress is a user gesture, so the read is permitted):

- MText editor: insert at the caret (web + native).
- TEXT field: append on the web; native lets the text_input paste itself.
- No editor open: falls through to the entity paste command as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:05:02 +03:00
Hakan Seven
54106ca9b0 feat(web): lazy per-script font loading for international CAD text
The web build has no system-font access, so CAD text in non-Latin scripts
(Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard
None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts
(one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the
first time a drawing uses that script, then outline glyphs with ttf-parser.

- web_font: Script enum + script_of (char → script font), a per-script
  store with lazy fetch, and a pending queue drained by the app loop.
- CJK is split by language (chinese/japanese/korean). Han ideographs share
  code points but differ by language, so the shared block is routed by the
  document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always
  Japanese, Hangul always Korean. Re-tessellates when the language changes.
- build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache
  lets glyphs that missed while a font was in flight reappear on load.
- PollWebFonts subscription (web only) + WebFontLoaded message drive fetches.
- Trunk copy-dir serves web/fonts; nothing is bundled into the native binary
  (desktop keeps using system fonts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 16:13:26 +03:00
Hakan Seven
79bb1d67b1 feat(cli): full command-line interface via clap
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 <noreply@anthropic.com>
2026-06-20 13:59:20 +03:00
Hakan Seven
7e70a2a97c feat(window): surface the unsaved-changes prompt on close
A close signal can arrive while the app is backgrounded, leaving the
in-window unsaved-changes prompt unseen behind other windows. Funnel the
prompt opener through gain_focus + a Critical user-attention request so
the window raises (Windows/macOS/X11) or at least flags for attention
(Wayland via xdg_activation). Every unsaved-dialog caller shares this path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:40:47 +03:00
Hakan Seven
36d5891572 fix(layers): freeze toggle in ribbon dropdown + sortable manager columns
Ribbon layer dropdown rows were a single button, so a click on the
visible/freeze/lock icon fell through to the make-active handler instead
of toggling that state. Split the row: each icon is its own toggle
button, only the swatch+label selects the active layer. Toggle handlers
now resync the ribbon so the icon updates while the dropdown stays open.

Layer Manager columns are now sortable: clicking a header sorts by that
column, clicking again flips direction (SVG arrow indicator). Sort is
reapplied on every panel sync so it survives edits, preserves the
selection by name, and keeps the ribbon dropdown order in step.

Fixes #133

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:39:09 +03:00
Hakan Seven
e7eeff5945 feat(select): orange hover / blue selection highlight for all objects
Solids (meshes) now tint on rollover and selection: hover washes orange,
selection washes blue, with selection winning when both apply. Meshes
re-upload their tint when the highlight set changes, and the rollover pick
now also hits shaded solid bodies and hatches (mirroring the click-select
order: wire → hatch → block hatch → solid).

Wires get the same scheme: the selection xray overlay recolours selected
entities blue and the hovered entity orange (previously both blue). Drops
the now-unused highlight_handles union helper and its cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:33:53 +03:00
Hakan Seven
25c347f9b0 feat(3d): instance block-definition solid meshes per INSERT
3D solid meshes inside block definitions were drawn once at block-local
coordinates without the INSERT transform, so a block authored in mm and
placed into a metre drawing at a 0.001 INSERT scale rendered ~1000x too
large. Split solid meshes into top-level (layout-owned, offset and drawn
flat) and block-definition (kept block-local) sets, and emit a transformed
instance of each block-def mesh per model-space INSERT, accumulating nested
transforms. Layout blocks are identified from the Layout objects'
block_record handles, since BlockRecord::is_layout() also flags ordinary
blocks in some files. (#123)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 01:32:17 +03:00
Hakan Seven
2be26e9e2e fix(snap): object snap takes precedence over ortho/polar
When drawing with Ortho (or Polar) on, a point that snapped to an object
(e.g. an Endpoint) was still pulled onto the ortho axis, so you couldn't
close a shape exactly on an existing vertex. Skip the ortho/polar
constraint while an object snap is locked, in both the preview and commit
paths. Grid snap is positional rather than an object snap, so it still
combines with ortho/polar. (#132)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:05:41 +03:00
Hakan Seven
d4a665a34c feat: per-viewport grid/snap, adopted from the active viewport
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 <noreply@anthropic.com>
2026-06-18 22:25:44 +03:00
Hakan Seven
b2feeb716b refactor: make grid visibility per-drawing, not a global setting
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 <noreply@anthropic.com>
2026-06-18 22:11:27 +03:00
Hakan Seven
cd17096f65 feat(io): persist grid/snap display state to the active VPort
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 <noreply@anthropic.com>
2026-06-18 22:05:25 +03:00
Hakan Seven
02b0fc4dcf chore: drop brand names and orphaned PICK_HIGHLIGHT colours
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 <noreply@anthropic.com>
2026-06-18 21:28:07 +03:00
Hakan Seven
5a36494ed7 chore: clear compiler warnings
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 <noreply@anthropic.com>
2026-06-18 21:18:09 +03:00
Karim Jerbi
02f70ab0b3 fix(editor): prevent empty text commit from advancing command edit_count
- Propagates commit status out of inline editors so commands know whether a real change occurred.

- Fixes TEXTEDIT's Undo step rolling back unrelated document actions when the user presses OK on empty/cleared existing MText.
2026-06-18 17:55:30 +01:00
Karim Jerbi
9a258f29ce feat: implement TEXTEDIT command and TEXTEDITMODE system variable
- Adds TEXTEDIT command to seamlessly edit annotation objects in-place.
- Matches industry-standard CAD tools' prompt behavior and "Current settings" banner logic.
- Implements the TEXTEDITMODE system variable (0/1) for Multiple vs Single editing modes.
- Enhances command lifecycle with CmdResult::SuspendForTextEdit and post_editor_closed hooks, allowing commands to cleanly pause and resume after external UI editors close.
2026-06-18 15:56:27 +01:00
Hakan Seven
e1d128c4dc fix(snap): priority-based osnap selection + true perpendicular
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 <noreply@anthropic.com>
2026-06-18 16:30:37 +03:00
Hakan Seven
5ce3aaf260 fix: modal dialogs capture the keyboard
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 <noreply@anthropic.com>
2026-06-18 14:00:03 +03:00
Hakan Seven
aafced76d7 fix(web): honor selected DWG/DXF version when saving
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 <noreply@anthropic.com>
2026-06-18 13:46:55 +03:00
Hakan Seven
157412419e feat(plugin): uninstall + clean upgrade/reinstall of external plugins
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 <noreply@anthropic.com>
2026-06-17 15:57:44 +03:00
Hakan Seven
020a3a4c84 feat(plugin): marketplace — install plugins from a curated registry
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 <noreply@anthropic.com>
2026-06-17 15:24:56 +03:00
Hakan Seven
f6219a9609 feat(plugin): discover external plugin packages from the plugins folder
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 <noreply@anthropic.com>
2026-06-17 11:43:24 +03:00
Hakan Seven
96ec672d5b feat(points): point style commands + DDPTYPE dialog, viewport-aware size
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 <noreply@anthropic.com>
2026-06-17 11:13:12 +03:00
Hakan Seven
50825b6346 feat(plugin): ModuleEvent::PluginFileDialog for native file import
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 <noreply@anthropic.com>
2026-06-17 10:22:08 +03:00
Hakan Seven
844069b4ef feat(plugin): enable/disable plugins from the Plugin Manager
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 <noreply@anthropic.com>
2026-06-17 10:15:38 +03:00
Hakan Seven
195415af84 feat(modal): drag dialogs by a title-bar grip, clamped to the window
In-canvas modals were locked to screen-centre. Add a green move-arrow
grip beside the ✕ that drags the dialog; positioning is an offset from
centre applied via asymmetric padding. The drag offset is clamped to the
window so a dialog stops at the edge instead of being squeezed, and the
offset resets when the dialog closes so each opens centred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:59:17 +03:00
Hakan Seven
ec3161f7f7 fix(snap): unify snap aperture so OTRACK catch matches OSNAP
Tracking and polar pickup used the full crosshair-arm radius (60 px)
while OSNAP used the smaller ¼ radius, so the vector-intersection lock
grabbed over a much larger area than object snaps. Collapse to a single
osnap_radius_px shared by OSNAP, tracking, polar and extension; the
crosshair keeps following the cursor and the point snaps on contact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:25:11 +03:00
Hakan Seven
58e4d696ba feat(otrack): lock onto the intersection of two active vectors
When two tracking vectors were active, clicking placed a free point on
one of them instead of their crossing. otrack_snap now builds the active
rays (OTRACK rays from tracking points, plus POLAR rays from the last
point) and, when the cursor is near the crossing of two vectors from
different origins, locks onto that intersection. Covers OTRACK×OTRACK and
POLAR×OTRACK; single-vector alignment is unchanged when no crossing is
near.

Closes #111
Closes #112

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 05:01:41 +03:00
Hakan Seven
ca8d249173 fix(otrack): add 250ms dwell timeout before acquiring a tracking point
A tracking point was created as soon as a snap point was highlighted,
so moving the mouse across the view spawned accidental tracking points.
Acquisition now requires the cursor to rest near the snap point for
250ms (wall-clock) rather than a fixed number of move events.

Closes #110

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 04:59:06 +03:00
Hakan Seven
fefae274d4 feat(wasm): web Save/Save As go through the dialog with a direct download
On the web, Save and Save As now open the in-app Save dialog (filename +
format) like the desktop, and the unsaved-changes prompt's Save routes
through it too — instead of an immediate rfd download. Confirm serializes
to bytes and downloads directly via a Blob + hidden `<a download>` click
(a user gesture), so there is no intermediate "click to download" link.

Replaces the rfd-based download_web / WebSaveResult round-trip with a
synchronous sys::download_bytes. Native save is unchanged.

Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:52:20 +03:00
Hakan Seven
3dd1276b6a refactor(ui): remove dead OS-window machinery after Plan B
With every dialog now an in-canvas modal, the per-dialog
`*_window: Option<window::Id>` fields are never set. Remove all 17
fields and their inits, trim `OsWindowClosed` to just the main window
(exit), drop the dialog branches from the window `title` closure, the
leftover `gain_focus` guards in the open handlers, and the now-unused
`iced::window` import in update.rs. Page-setup apply clears
`active_modal` instead of closing a window.

No behaviour change; native + web build, 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:09:04 +03:00
Hakan Seven
94b906d95b feat(ui): migrate the last dialogs to modals — fully single-window (Plan B)
Move the unsaved-changes prompt, the Save As browser and the
file-association prompt onto `active_modal` / `modal_content`, reusing
their existing open/close helpers (now just set/clear active_modal). The
daemon `view` no longer dispatches per window — every dialog is an
in-canvas modal, so the app is single-window on native and web alike.

`close_active_modal` handles their ✕: the unsaved prompt cancels the
pending close, the association prompt records that it was answered.

Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:59:38 +03:00
Hakan Seven
c24c6885d0 fix(ui): modal close discards staged edits and clears the ribbon tool
Closing a dialog as an OS window used to discard a style editor's
un-applied (staged) changes and de-highlight the ribbon tool that
launched it. The in-canvas modal ✕ (CloseModal) lost that, so a Cancel
kept staged edits and left the button stuck active.

Add `close_active_modal`: it runs `style_stage_discard` for the style
editors and deactivates the matching ribbon tool by ModalKind, then
clears `active_modal`. CloseModal and the dialog close handlers route
through it. Apply still commits (re-baselined), so Apply-then-✕ keeps
the change.

Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:52:21 +03:00
Hakan Seven
28232f1247 feat(ui): migrate style editors + nested colour picker (Plan B)
Move Table Style, MLeader Style and Dimension Style editors off OS
windows onto `active_modal` / `modal_content`, and turn the colour
picker into a nested modal that stacks over whichever dialog requested
it (`color_pick_target.is_some()` drives the overlay; CloseColorPicker
dismisses it). OpenColorWindow/ColorWindowPick no longer open/close an
OS window.

All thirteen former pop-up windows now render as in-canvas modals on
both native and web; only the native-only assoc/unsaved/save dialogs
remain as OS windows. Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:42:14 +03:00
Hakan Seven
7cd938443e feat(ui): migrate Text Style and Multiline Style to modals (Plan B)
Move the Text Style and Multiline Style editors off OS windows onto the
shared `active_modal` / `modal_content` path. Their open handlers set
`active_modal` (keeping the existing buffer/stage setup); construction
moves from the daemon `view` dispatch into `modal_content`.

The remaining style editors (Table, MLeader, Dimension) still use OS
windows for now — they carry a nested colour picker that needs its own
handling. Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:29:50 +03:00
Hakan Seven
b21d10f6e8 feat(ui): migrate 4 more dialogs to in-canvas modals (Plan B)
Consolidate all migrated dialogs into a single `modal_content` builder
matched on `active_modal`, rendered by view_main. Move Layers, Page
Setup, Layout Manager and Plot Style off OS windows onto it (their
construction relocated from the daemon `view` dispatch, which now only
handles the not-yet-migrated dialogs). Each keeps a bounded size.

Open/close handlers set `active_modal` instead of opening/closing
windows; ToggleLayers toggles it.

Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 23:22:54 +03:00
Hakan Seven
6a3663b3f1 feat(ui): unified modal dispatch; migrate 4 dialogs (Plan B)
Replace the per-dialog open flag with a single `active_modal:
Option<ModalKind>` and one `CloseModal` message. view_main matches it to
render the open dialog as an in-canvas modal.

Migrated off OS windows: About (content-sized), Shortcuts, Plugin
Manager and the Update notice (the latter three keep a bounded size via
a wrapping container, since their lists/scroll need defined bounds).

Native + web build; 75 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 19:07:25 +03:00
Hakan Seven
e6213eafbb feat(ui): shared in-canvas modal overlay; migrate About (Plan B)
Start Plan B — replace the daemon's pop-up OS windows with in-canvas
modal overlays so dialogs work identically on native (one main window)
and web (canvas only).

- New `ui::modal::modal(base, content, on_close)`: dims the view behind
  a backdrop that blocks clicks but does NOT dismiss, frames the content,
  and adds a top-right ✕ as the sole close affordance. Sizes to the
  content (no `Fill`), so dialogs shrink to fit.
- view_main stacks the active modal on top, so both run paths show it.
- Migrate About as the pilot: `about_open` bool + AboutOpen/AboutClose
  instead of an OS window; About's view shrinks to content.

Remaining dialogs migrate to the same pattern next. Native + 75 tests OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:59:13 +03:00
Hakan Seven
9d378a0533 feat(wasm): save files from the browser
Wire up save for the web build:

- `io::save_to_bytes`: serialize a document to a byte buffer (DXF via
  write_to_vec, DWG via write_to_writer over a Cursor).
- `io::download_web`: browser save dialog → write bytes (triggers a
  download).
- Save and Save As on wasm both go through a new `web_save` helper:
  serialize the active tab and offer a download (no persistent path or
  in-app save dialog on the web). Format follows the tab name's
  extension, defaulting to DWG. Result reported via `WebSaveResult`.

Native build + 75 tests unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:20:22 +03:00