Two leftovers from the UCS grid work:
- The coloured X/Y/Z axis lines (draw_axes) were drawn at the world
origin along world axes; they now run through the UCS origin along the
UCS axes, matching the grid lines.
- Grid snap rounded the cursor on the world XYZ lattice; it now rounds in
the UCS grid frame (to-UCS, round, back-to-world) so it lands on the
grid the user sees. The UCS origin (wire space) + rotation reach the
snapper via DocumentTab::ucs_grid_basis; identity outside model space.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OTRACK alignment rays (0°/90° and polar steps, plus the polar pencil from
the last point) were built along world X/Y. They now rotate with the
active UCS via a Mat4 passed into otrack_snap (scene.viewcube_ucs_mat() —
the UCS in model space, identity in paper), so tracking lines and the
intersection lock align to the user's coordinate system like ortho,
polar, and the grid. sync_ucs_to_scene now reuses UcsXform::rotation_mat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a ViewCube face rotated the gaze into the UCS frame but derived
the up vector from world X/Y/Z, so the snapped view landed world-aligned
(most visibly Top, where the UCS shares world Z and only the in-plane
roll differs). snap_to_direction now takes the UCS basis and picks its up
from the UCS axes, so a face snap lands square to the user's coordinate
system. Identity UCS reproduces the world-aligned snap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ViewCube showed world Front/Top/Right. It now composes the camera
rotation with the active UCS (scene.viewcube_ucs, synced from the tab's
active_ucs), so its faces follow the user's coordinate system in model
space. Render uniform, hover/click hit-test, and the click→snap direction
all go through scene.viewcube_ucs_mat() so they stay in lock-step;
identity outside model space leaves paper-viewport navigation unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ortho and polar drawing constraints snapped to world X/Y; under an active
UCS they now snap to the UCS axes, routed through the shared UcsXform
(identity = unchanged world-XY behaviour). Applied at all three input
sites (grip drag, command point, command preview). Typed coordinates
already flow through the converter via ucs_to_wcs / ucs_rotate_vec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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.
- 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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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#111Closes#112
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>