Commit Graph
331 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 f7734b698f feat(ui): use the shared colour selector in properties, ribbon and layers
Replace the bespoke colour pickers with the shared color_selector / color_list
widget so colour selection is identical everywhere:
- left properties panel (selected entities' colour)
- ribbon Home-tab colour dropdown
- layer manager per-layer colour

Each routes its "More…" entry to the standalone Select Color window via a
ColorPickTarget (Properties / Ribbon / Layer). Extracted color_list as the
shared popup content; the colour selector button now fills its container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:22:24 +03:00
Hakan SevenandClaude Opus 4.8 3d050fe1ab feat(ui): move full ACI palette to a separate "Select Color" window (#94)
The colour selector popup now lists only the named colours (ByLayer,
ByBlock, ACI 1-9) plus a "More…" entry. "More…" opens a standalone
"Select Color" window with the full 256-colour grid; picking there applies
to the originating field (dimension/multileader/table style) and closes
the window. Routed via a ColorPickTarget so one window serves every field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:14:14 +03:00
Hakan SevenandClaude Opus 4.8 3a87a18f32 fix(ui): float the colour-selector popup instead of pushing the form (#94)
The colour list/palette was laid out inline, shifting everything below it
down when opened. Wrap it in a small custom widget (Floating) that renders
the button inline and the popup as an iced overlay anchored below it, so it
floats over the UI like a real dropdown. Picking a colour now also closes
the popup (the style editors clear their open-field state on edit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 11:00:43 +03:00
Hakan SevenandClaude Opus 4.8 8cdb17a6b4 feat(ui): shared colour selector used across all style editors (#94)
Add a reusable color_selector widget (ui/color_select.rs): a main-colour
dropdown plus a "more colours" button that expands the full 255-entry ACI
palette — the same design the properties panel uses. Replace the flat ACI
swatch rows added earlier in the dimension, multileader and table style
editors with it, wired through each editor's existing colour edit message
(the chosen colour is encoded as an ACI string) plus a per-field
palette-open toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:42:39 +03:00
Hakan SevenandClaude Opus 4.8 5f6c4029e9 fix(mleader): new multileader inherits the current MLeader style (#94)
A new multileader was built from defaults and ignored the current
MLeaderStyle, so the style editor's settings had no effect on it (the
renderer reads the entity's own fields, not the style). commit_entity_handle
now copies the style's content/path type, text attachment/alignment/angle,
colours, sizes, scale and landing into the new entity (enum types
round-trip through i16 since the entity and style enums are distinct).

Text styles already render correctly and tables already resolve their
style live via table_style_handle, so no change was needed there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:59:12 +03:00
Hakan SevenandClaude Opus 4.8 68a3efa83d fix(style): new mleader/table use the current style (#92)
Follow-up to the text/dimension fix: MultiLeader and Table were created
at their default style too. Their styles live in the objects dictionary,
so commit_entity_handle now resolves header.current_mleader_style_name /
current_table_style_name to the matching style object's handle and
assigns it, unless the command already set one or no such style exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 01:19:00 +03:00
Hakan SevenandClaude Opus 4.8 92e32243eb fix(style): new text/mtext/dimension use the current style (#92)
New annotation entities were created at the entity "Standard" default
instead of the document's current style, so making a style current had
no effect on creation.

- Text / MText: seed the new entity (and the MText editor preview) from
  header.current_text_style_name.
- Dimension: commit_entity_handle fills the new dimension's style_name
  from header.current_dimstyle_name, covering every dimension command
  centrally.

Applied only when the entity is still at its default so an explicitly
styled entity is preserved. MLeader and Table styles need their style
tables modelled first and are left for follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 01:15:52 +03:00
Hakan SevenandClaude Opus 4.8 29ccfcfc46 fix(command): accept typed points/Enter in keyword steps when DYN is off (#97)
A command step that takes both a point and option keywords (PLINE,
MLINE) returns true from wants_text_input to receive the keywords, but
CommandSubmit then routed the text only to on_text_input and, on a None
(unhandled) result, re-prompted instead of falling through. So with
dynamic input off, typed coordinates and a bare Enter were swallowed —
points couldn't be added by keyboard and the command couldn't be ended.

Fall through to the existing Enter / coordinate handling when
on_text_input doesn't consume the text. Coordinates (including @ and #
prefixes) and Enter now work in these steps with DYN both on and off;
commands that fully handle their text step still return Some and are
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:59:52 +03:00
Hakan SevenandClaude Opus 4.8 e141fb6df3 fix(command): block drawing commands on the Start tab (#96)
The Start (welcome) tab has no drawing, so commands like LINE dispatched
from the command line or ribbon ran as silent no-ops. Allow only the
commands that make sense there (NEW / OPEN / EXIT / QUIT; OPEN_RECENT is
handled earlier) and inform the user otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:42:02 +03:00
Hakan SevenandClaude Opus 4.8 bc92590725 fix(stretch): re-tessellate stretched entities so the viewport updates (#95)
STRETCH edited geometry directly via document.get_entity_mut, which the
scene's tessellation cache doesn't observe, so the viewport kept showing
the pre-stretch shape until an unrelated redraw. Mark each stretched
entity dirty and bump geometry on completion, matching MOVE's
transform_entities path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:36:17 +03:00
Hakan SevenandClaude Opus 4.8 8d6e21ef5f fix(layers): sync header CLAYER when making a layer current (#93)
Making a layer current via the layer manager (LayerSetCurrent) or
LAYMCUR updated only the per-tab active_layer, not the document
header's current_layer_name/handle. The no-selection ribbon refresh
reads the header layer, so after the change an Esc (deselect ->
refresh_properties) snapped the active-layer dropdown back to the
stale header value ("0") even though new entities still used the
correct active layer. Mirror the change into the header in both paths,
matching the ribbon dropdown's own set-current path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 00:16:06 +03:00
Hakan SevenandClaude Opus 4.8 57d3e620ea fix(command): keyword/radius input for ribbon-started modify commands (#90)
Starting FILLET/CHAMFER from the ribbon left the command-line field
unfocused, so its Enter arrived as CommandFinalize and discarded a
typed option keyword ("R"/"D"), cancelling the command. Several
related dynamic-input issues surfaced from the same flow:

- CommandFinalize now forwards a non-empty command-line buffer through
  CommandSubmit, so a typed keyword is handled the same whether the
  command was started from the ribbon or the command line.
- sync_dyn_fields shows no coordinate box during an object-pick step
  (needs_entity_pick / needs_structure_point_pick) — there is no point
  to enter there, so typed keywords reach the command line cleanly.
- apply_cmd_result rebuilds the dynamic-input fields on NeedPoint, so a
  step change (object pick -> radius entry) shows the matching box
  immediately and typed digits land in it instead of the command line.
- Object-pick steps now draw a prompt-only hint pill at the cursor
  (no input box).
- FILLET/CHAMFER remember the first pick across a mid-selection radius
  or distance entry, resuming the second pick instead of restarting
  selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:58:56 +03:00
Hakan SevenandClaude Opus 4.8 cd4d1c2535 fix(layout): focus inline rename field on RENAME
Renaming a layout tab left keyboard focus on the command line, so typed
characters landed there until the user clicked the tab. Give the inline
rename text_input a stable id and return a focus task when the rename
starts so the user types straight into the field. (issue #86)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:18:31 +03:00
Hakan SevenandClaude Opus 4.8 dc26c8128c fix(otrack): reset tracking points after each committed point
OTRACK acquired points only ever cleared when a command ended, so a
multi-point command (LINE, etc.) accumulated tracking points across
every input, swamping the user with alignment options. Clear the
acquired points and live alignment vector each time a point is committed
to the active command, at all five point-accept paths. (issue #85)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:15:53 +03:00
Hakan SevenandClaude Opus 4.8 9de2760dee fix(selection): keep active selection on empty box/lasso
An empty box or lasso drag called deselect_all, discarding a selection
the user may have built up over several picks. Drop the empty-clear from
the lasso and two-click box paths so a stray empty drag leaves the
selection untouched; Esc remains the deliberate clear. (issue #83)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:07:57 +03:00
Claude 5ba7101022 feat(app): silently register as a .dwg/.dxf handler so portable builds show in "Open with"
Installed builds already register a handler (MSI ProgIDs, the Linux
.desktop MimeType, the macOS Info.plist), but the portable .exe and the
AppImage register nothing, so they never appear in the OS "Open with"
list. Add a best-effort, idempotent runtime registration that runs
silently on a detached thread at startup:

  * Windows — writes HKCU\Software\Classes\Applications\OpenCADStudio.exe
    (open command + FriendlyAppName + SupportedTypes .dwg/.dxf), which is
    what lists the exe in "Open with → Choose another app".
  * Linux — writes a user-level .desktop pointing at the running binary
    ($APPIMAGE when set) and refreshes the desktop database. Skipped when
    a system package already provides the desktop entry, so we never
    shadow a distro install.
  * macOS — no-op; .app bundles auto-register with LaunchServices.

Unlike the default-app prompt this changes no defaults and shows no UI;
it only makes the binary discoverable as a handler.

https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
2026-06-12 08:00:18 +00:00
Claude a7d85fe148 feat(app): first-launch prompt to set Open CAD Studio as default for .dwg/.dxf
On the first launch that hasn't answered it yet, the app shows a small
dialog offering to become the default handler for .dwg and .dxf files.
The choice is persisted (default_assoc_prompted) so the prompt never
reappears.

The platform plumbing lives in io/file_association.rs:
  * Windows — opens the OS's own per-app default-programs dialog via
    IApplicationAssociationRegistrationUI::LaunchAdvancedAssociationUI,
    passing the "Open CAD Studio" RegisteredApplications name the MSI
    registers. Modern Windows guards the actual default behind a
    UserChoice hash, so this is the supported consent path.
  * Linux — xdg-mime default against the installed .desktop file, whose
    MimeType entries already cover image/vnd.dwg and image/vnd.dxf.
  * macOS — LSSetDefaultRoleHandlerForContentType binds the DWG/DXF UTIs
    declared in Info.plist to the app bundle id.

The work runs on a dedicated thread (the Windows dialog is modal) with
the result delivered back through a oneshot channel and surfaced in the
command line.

https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
2026-06-12 07:50:41 +00:00
Hakan SevenandClaude Opus 4.8 71837895b0 feat(ui): right-edge side toolbar; drop the paper-space ribbon tab
Switching to a paper layout used to hijack the ribbon with a contextual
"Layout" tab. Replace that with a reusable right-edge vertical toolbar:
a single centred column of icon buttons floated over the canvas, built
from a flat ToolDef list and dispatching through the existing
RibbonToolClick path (ui::side_toolbar). Paper space now shows Viewport /
Page Setup / Export PDF there and leaves the ribbon on the user's tab;
the contextual layout tab and its three auto-switch hooks are removed.
The toolbar is generic infrastructure for future context action sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 02:07:26 +03:00
Hakan SevenandClaude Opus 4.8 b47e1bf706 fix(ribbon): load style/layer dropdowns from the document
The Annotate style dropdowns (text / dimension / multileader / table) and
the layer dropdown were hard-coded to "Standard" / "0" and never synced
from the opened document, so they showed placeholder values even with no
file open and stale values after closing one.

- Start the ribbon's style and layer lists empty instead of with a
  placeholder entry.
- Sync the style dropdowns from the document on file open and on tab
  new/switch/close (the layer sync already ran there).
- Guard sync_ribbon_layers / sync_ribbon_styles / sync_ribbon_from_selection
  for the Start (welcome) tab so all three dropdowns stay empty there.
- When a style family is absent (e.g. no table/multileader styles), fall
  back to empty rather than a non-existent "Standard".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:46:17 +03:00
Hakan SevenandClaude Opus 4.8 2519d64907 fix(select): box/lasso adds to the selection (#83)
Box selection is a two-click window (corner, corner): the first empty
click armed the box AND cleared the selection, and the box completion
cleared again before applying its hits — so a second box dropped
everything the first one picked. Stop clearing on the arming click and
make the completion accumulate like individual picks (#47): a plain box
or lasso adds, Shift+box/lasso removes the enclosed entities, an empty
box/lasso (or Esc) clears.

Fixes #83

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:22:59 +03:00
Hakan SevenandClaude Opus 4.8 cea84ccb64 fix(edit): correct entity-pick hover previews on offset drawings
The trim/extend hover preview (on_hover_entity) gets the cursor in
offset-relative space but compares it against WCS document entities and
returns WCS preview wires. On a drawing with a non-zero world_offset the
preview highlighted the wrong segment and the wires were shifted off the
geometry by the offset. Pass the world-offset-corrected click to
on_hover_entity and shift its returned wires back to the offset-relative
frame the renderer uses. Point-creation previews keep the local cursor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 01:00:34 +03:00
Hakan SevenandClaude Opus 4.8 14d047ce9a fix(edit): add world_offset to entity-pick clicks
The pick point handed to commands is in offset-relative (local) space,
matching the camera and point-creation commands. Entity-pick, tangent
and structure-pick handlers (TRIM / EXTEND / FILLET / OFFSET, …) instead
compare the click against WCS document entities, so on a drawing with a
non-zero world_offset (UTM-scale coordinates) the click landed ~1e6
units off the geometry — TRIM cut the wrong side or reported the click
as off the segment. Add world_offset back to the pick (model space only)
before passing it to those handlers, matching what the grip path already
does. Point-creation commands keep the local point unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:53:20 +03:00
Hakan SevenandClaude Opus 4.8 9b1bd011ca feat(blocks): switch dynamic-block visibility states from a lookup grip
A block reference with a visibility parameter now shows a triangular
lookup grip at the parameter point. Clicking it opens a dropdown of the
named states (current one marked); picking a state toggles which of the
anonymous block's members are visible and rebuilds, so the reference
shows the chosen profile instead of being frozen on the saved state.

The state list is data-driven (owned strings from the file) and applying
it mutates other entities (the anonymous block's members), so it stays
separate from the static GripMenuAction grip-menu: a dedicated
VisibilityPopup and an app-level apply path. The anonymous block is a
parallel-ordered clone of its definition, so a member visible in the
definition's state maps to the same index in the inserted block; the
chosen visibility flags round-trip through the DWG writer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:11:19 +03:00
Hakan SevenandClaude Opus 4.8 25715d1b03 fix(grips): compute grip positions in f64 so they sit on the wire
Grip markers drew visibly offset from the entity on large-coordinate
(UTM-scale) drawings: every Grippable impl cast the f64 entity coordinates
to f32 BEFORE the world-offset subtraction, and at 1e7 magnitude an f32
holds ~0.9 drawing units of rounding error. The wires subtract the offset
in f64 first (precision-preserving), so the grips landed up to ~1 unit
away from the geometry they belong to.

GripDef.world is now DVec3: producers fill it straight from the f64
entity data, refresh_selected_grips subtracts the world offset in f64,
and only the small offset-local result is cast to f32 for screen-space
projection (grips_to_screen / find_hit_grip) and drag state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:04:37 +03:00
Hakan SevenandClaude Opus 4.8 e65a7a6d67 feat(styles): seed current table/multileader style from the file
Bump acadrust to pick up the new $CTABLESTYLE / $CMLEADERSTYLE header
variables, then on load seed the ribbon's active table style and the
tab's active multileader style from the document header so the ✓ marks
the style the file actually declares (DXF). Set Current writes the header
field as the round-trip source of truth, and the style-manager staging
snapshot now covers it. sync_ribbon_styles honours the active table style
instead of always falling back to the first one.

DWG still leaves these at "Standard" until acadrust parses them from the
binary header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:05:07 +03:00
Hakan SevenandClaude Opus 4.8 885bacd0f6 feat(styles): mark the current style with a ✓ in the list
Every style manager now shows a leading ✓ next to the current style
(current text/dim style, active table/multileader/multiline style), with
the other rows padded so names stay aligned. The marker replaces the
multileader/multiline-only ◀ and is wired for all five managers by
threading the current-style name into each view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:45:48 +03:00
Hakan SevenandClaude Opus 4.8 de4eb31003 feat(styles): make style managers transactional — persist only on Apply
A style manager is now a staged transaction. When it opens, a snapshot of
the style tables, style objects and current-style pointers is taken. Every
New / Copy / Delete / Rename / Set Current / property edit mutates the
document live so the dialog previews the result, but the tab is not marked
dirty, nothing is pushed to undo, and the drawing is not rebuilt.

- Apply commits: one undo entry (restoring the pre-edit state), the tab is
  marked dirty, the drawing rebuilds, and the baseline is reset so further
  edits stage afresh.
- Closing the window without Apply discards: the opening snapshot is
  restored, so none of the changes persist.

Side effects (dirty / undo / bump_geometry) were removed from every style
edit handler and routed exclusively through the commit. Multiline's Apply
is a structural-only commit since its editor has no editable properties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:38:32 +03:00
Hakan SevenandClaude Opus 4.8 65a25033bc fix(styles): make double-click inline rename actually fire
The list row wrapped a `button` inside the `mouse_area`. The button
captured the press, so the parent `mouse_area` never saw the second
click and `on_double_click` never fired — double-clicking a style name
did nothing.

Drop the inner button: the row is now a plain `mouse_area` carrying both
`on_press` (select) and `on_double_click` (start rename), with selection
shown via the container background. Focus the rename field on start so
the user can type immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:10:09 +03:00
Hakan SevenandClaude Opus 4.8 f2dfd01795 feat(styles): unify the five style managers behind a shared frame
The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).

Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:

- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
  delete / rename) handling handle allocation, ribbon sync, Table re-key
  and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
  Delete on the left, Set Current + Apply on the right), style list and
  chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
  starts an inline rename.

New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
  MLine apply is a documented placeholder until its editor is editable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:03:34 +03:00
Hakan SevenandClaude Opus 4.8 e6ab0e70ac fix(settings): cover SnapType::ObjectPick in persisted-mode match
PR #80 added the internal SnapType::ObjectPick variant, which left the
exhaustive snap_id match in the settings module non-exhaustive and broke
the build after the merge. Map it to a stable id; it is not a
user-toggleable OSNAP mode, so it never enters SNAP_ORDER or the
persisted set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:02:18 +03:00
Hakan Seven 6a70e99dd5 Merge pull request #80 from mf4633/feature/plugin-host
Add plugin host (Phase 1)
2026-06-10 00:37:37 +03:00
Hakan SevenandClaude Opus 4.8 3c97111d8f fix(polar): only snap near a polar ray (#70)
POLAR locked the cursor to the nearest angle everywhere, so it could
never point between angles. Snap only when the cursor is within the snap
aperture (in screen pixels) of a polar ray; otherwise leave it free, so
POLAR behaves as if off when pointing away from every angle. The polar
guide line now shows only while actually snapped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:44:31 +03:00
Hakan SevenandClaude Opus 4.8 f2fd36b5fa feat(otrack): polar coordinates from tracking point (#69)
OTRACK alignment was computed in the wrong plane (it used Z as the
second axis while the model is XY / Z-up), so tracking never aligned
correctly and the aligned point was never fed to a pick.

Rework it: project the cursor onto a ray from a tracking point in the
XY plane — horizontal/vertical by default, or every POLAR increment
when POLAR is on. The aligned point is now used for both the live
preview and the committed click, and a dashed guide shows the ray.

While aligned, a typed distance places the point along the ray from the
tracking point, via the command line and the dynamic-input box (which
shows a single Distance field). The alignment clears with the command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:38:08 +03:00
Hakan SevenandClaude Opus 4.8 d62690b652 fix(otrack): clear tracking points when command ends (#64)
OTRACK acquired tracking points by dwell during a command or grip drag,
but only cleared them when OTRACK was toggled off — so the temporary
tracking-point symbols (drawn straight from the point set) lingered in
the view after the command ended.

Drop the tracking points once neither a command nor a grip drag is
active. Acquisition only happens during those, so this covers every
exit path (Esc, cancel, commit) from one place. The tracking vector is
a preview wire already cleared on cancel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:22:34 +03:00
Michael Flynn 82993bc1db Add demo_plugin and plugin host integration tests
Minimal in-tree add-on (DP_HELLO) validates registry, ribbon merge, and try_dispatch. Five new lib tests; EntryKind gains PartialEq for test helpers.
2026-06-09 13:52:15 -04:00
Michael Flynn e46701456f Add plugin host architecture and Storm Sewer add-on package
Generic plugin runtime (src/plugin/, HostSession, per-tab plugin_state, command router). QGIS-style add-on layout with plugin.toml and single PluginRegistration. Storm Sewer refactored as reference consumer with dispatch.rs, catchments, LandXML, headless tests (43 passing). Core update.rs decoupled from storm_sewer via CadCommand object-pick hooks. Docs: plugin-architecture.md, plugin-template, PR and issue-78 reply drafts.
2026-06-09 13:41:21 -04:00
Hakan SevenandClaude Opus 4.8 ebf12780c5 feat(settings): persist UI preferences across sessions (#68)
DYN, ORTHO, POLAR, the polar increment, the grid toggle and the
object-snap configuration (OSNAP on/off, active snap modes, OTRACK)
reset to defaults on every launch. Persist them to a per-user
settings.txt, matching the existing recent-files / status-bar stores
(plain key=value, no serialization crate).

Settings load on boot and apply to live state; the update wrapper
snapshots them after each message and writes only on change, so a
toggle survives a restart without thrashing the file. LWT is left out
on purpose — it is a drawing header variable, not a UI preference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:50:23 +03:00
Hakan SevenandClaude Opus 4.8 6520195220 fix(layers): allocate real handles for new layers (#67)
New layers were added to the table with a NULL handle, and the first
patch used the non-advancing `next_handle()` getter, so two new layers
(or a layer and the next allocated object) shared one handle. The DWG
format is handle-based, so the collision dropped the layer on save;
DXF is name-based and survived, which is why a rename appeared to fix
it.

Allocate a unique advancing handle via `allocate_handle()` in LAYER NEW
(panel + command line) and on rename when the source handle is invalid.
Rename now also clones the whole record so colour/linetype/lineweight
are kept, not just the flags.

Adds DWG/DXF round-trip tests covering one and multiple new layers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:37:42 +03:00
Hakan SevenandClaude Opus 4.8 ff10f8a89a feat(ui): elide overflowing text, tooltip full layer name
iced has no built-in ellipsis, so long user content (file names, layer
names, paths, property values) wrapped or spilled past fixed-width
panels. Add an `elide` helper (char-budget truncation with a trailing
ellipsis) and apply it to the layer manager, properties panel, document
tabs, recent-files list and the save dialog.

In the layer manager a truncated name reveals its full text in a
cursor-following tooltip on hover, so the column need not be widened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:04:18 +03:00
Hakan SevenandClaude Opus 4.8 b3aadf569e fix(commandline): pin active step prompt instead of fading
The current step's prompt was written into the command-line widget
label. Instead, keep it as a normal history line but freeze its
cooldown: the step prompt is pinned so it does not fade while the user
is still on that step. When the step changes the previous line's
cooldown restarts; when the command ends the pin clears and it fades.

`update` is now a thin wrapper that mirrors the active command's prompt
into the command line after each message; the body moved to
`update_inner`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:46:45 +03:00
Hakan SevenandClaude Opus 4.8 0ee8f5265d fix(save): append format extension to bare filename
The save dialog wrote the typed name verbatim, so entering "foo" with
DWG 2018 selected produced an extensionless "foo". The extension was
only added when the format pick-list changed, not on confirm.

Now confirm appends the selected format's extension when the name has
none (empty name falls back to drawing.<ext>); a name that already
carries an extension is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:38:35 +03:00
Hakan SevenandClaude Opus 4.8 4b878b88c4 feat(input): scalar entry via dynamic input
Text-input steps (OFFSET distance, POLYGON sides, radii, counts) were
swallowed by the dynamic-input overlay: typed digits landed in the field
buffer and Enter committed a point pick, so the value never reached
on_text_input (issue #61). Now each text step shows one scalar box whose
value routes to on_text_input on commit; non-numeric prompts (names,
keywords) keep going to the command line so mixed text like "Bolt12"
isn't split.

OFFSET reworked: object first, then through-mode by default — the offset
follows the cursor at the perpendicular distance to the object. Typing a
number locks the magnitude (cursor then only picks the side); a value
typed and clicked without Enter is applied before the point pick.

The dynamic-input overlay and the command line now show the active
step's prompt and hold it until the step completes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:32:03 +03:00
Michael FlynnandClaude Opus 4.8 1199895f0e Storm Sewer: make placement click-driven so it always works
Reports showed structure placement could stall if command-line text entry
didn't register (ribbon-invoked commands, dynamic-input/focus interactions).

Make a canvas click ALWAYS place a structure, using whatever values were typed
so far (defaults for the rest) — so dropping an inlet works regardless of typed
entry. If typing works, enter invert/rim/area/C first, then click to place with
those values. Pipes are pick-first: click START then END structure; the pipe
commits on the second click with the current diameter/n.

Drop the early focus_cmd_input() return in the dispatch arms (the normal
dispatch tail already focuses the command line and records last_cmd).

15 module tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:06:18 -04:00
Michael FlynnandClaude Opus 4.8 057de504b2 Storm Sewer: fix prompt flow — collect typed values before the click
The structure/pipe commands took a viewport interaction first and then asked
for typed values. But a point-pick result (NeedPoint) does not re-focus the
command line, so after the click the typed digits went nowhere and Enter hit
on_enter -> Cancel ("Command cancelled.").

Reorder both commands to the OFFSET-style flow: collect typed values first
(command line focused at dispatch and kept focused through text input), then
take the viewport click last and commit from it.

- PlaceStructure: enter invert/rim/area/C, then click the location.
- PlacePipe: enter diameter/n, then click START and END structures; commit
  from the second pick using the click points (connectivity stays exact via
  structure handles).
- Dispatch arms focus the command line so the first prompt is typeable.
- Reverts the update.rs inject_picked_entity allow-list change (the pt-based
  pipe no longer needs the picked entity).

15 module tests updated and green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 08:42:47 -04:00
Michael FlynnandClaude Opus 4.8 1e68219b43 Storm Sewer: interactive in-app network — snapping, tracked objects, analyze the drawing
Replaces the .ssn-file workflow with an interactive one driven by the canvas:

- Structures (SS_INLET / SS_JUNCTION / SS_OUTFALL) prompt invert/rim/area/C on
  the command line and commit a CIRCLE tagged with STORMSEWER_STRUCT XDATA
  [kind, invert, rim, area, C]. Outfalls skip area/C.
- SS_PIPE picks a START and END structure (entity-pick), snapping its endpoints
  to their centers, prompts diameter/n, and stores connectivity (the two
  structure handles) in STORMSEWER_PIPE XDATA [diameter, n, from, to].
- SS_ANALYZE / SS_REPORT / SS_PROFILE rebuild the network from the drawn
  entities (data.rs::network_from_entities) and analyze it directly; SS_ANALYZE
  overlays flow/HGL labels without redrawing the geometry.

Because the data lives on the entities as XDATA, the network round-trips to
DWG/DXF. One additive line in src/app/update.rs adds SS_PIPE to the
inject_picked_entity allow-list so the command can read the picked structure's
center for snapping.

15 module tests cover the XDATA round-trip, analyze-from-drawing, and the
command step machines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 07:29:14 -04:00
Michael FlynnandClaude Opus 4.8 9b44eb6400 Add Storm Sewer module: storm-drain network analysis (Rational + Manning + HGL)
Adds a "Storm Sewer" ribbon tab backed by a new in-repo `stormsewer` engine
crate (vendored under crates/). The engine implements standard public-domain
methods — Rational-method peak-flow accumulation, Manning circular-pipe
hydraulics, time-of-concentration / IDF intensity, and an HGL backwater pass
with junction losses — with 24 unit tests and no external dependencies.

Module (src/modules/storm_sewer/):
- Drafting: SS_INLET / SS_JUNCTION / SS_OUTFALL place structure markers and
  SS_PIPE chains pipe runs (interactive CadCommands).
- Analysis: SS_ANALYZE / SS_REPORT / SS_PROFILE open a .ssn network file, run
  the engine, draw the plan and HGL long-section, and print a report.
- SVG icons; SS_* commands registered for command-line autocomplete.

Integration is additive: build.rs auto-discovers the module and one dispatch
block is added to src/app/commands.rs. 7 module integration tests.

See src/modules/storm_sewer/INTEGRATION.md for the design and the optional
follow-up (drive analysis from canvas geometry via XDATA).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:16:30 -04:00
Hakan Seven 28b5e1367a fix(properties): refresh scene after property edits 2026-06-08 22:53:55 +03:00
Nick Ziv f890ed864c Implement Expression Evaluation for numerical inputs and the CLI 2026-06-07 13:17:46 -04:00
Hakan SevenandClaude Opus 4.8 eb89717b47 perf(scene): per-entity tessellation memo for the render path
Phase 2.2. wires_for_block_culled re-tessellated every visible entity on
any geometry bump, so a single-entity edit cost a full model re-tess
(~30ms baseline after the block_epoch fix).

Add a per-entity memo (tess_memo: Handle -> Arc<Vec<WireModel>>) on the
culled Model render path. It's guarded by a hash of the tessellation
parameters (tol / view / anno / world_offset / bg / entered viewport); a
mismatch (zoom, layout switch) clears it. The render path now classifies
the visible set against the memo, materializes hits and tessellates only
the misses in parallel, then stores the misses back.

- bump_geometry clears the memo (structural change — safe default);
- incremental edits drop just the changed handle via mark_entity_dirty
  and bump with bump_geometry_no_blocks: line commit (new handle is a
  natural miss) and grip commit / Esc-cancel (mark the dragged handle).

The hit-test (view_aabb == None), paper and per-viewport paths bypass the
memo so their differing cull parameters don't thrash it. A single-entity
edit now skips the per-entity geometry math for the entire unchanged set;
the Vec assembly + batched-buffer upload stay O(N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:46:31 +03:00
Hakan SevenandClaude Opus 4.8 3b2f8431a3 perf(scene): keep block cache across non-block edits
The block-definition tessellation cache (BlockCache::build — every block
defn) was keyed on geometry_epoch, so every edit rebuilt all block defns.
On a block-heavy drawing that turned the ~30ms visible-wire re-tess into a
~400ms spike at line-commit, grip first-move and grip release.

Key the block cache on a separate block_epoch. bump_geometry bumps both
geometry_epoch and block_epoch (safe default — xref, block create,
explode, REFCLOSE, undo, open all keep rebuilding blocks). Operations that
provably can't change a block definition now call bump_geometry_no_blocks,
which bumps only geometry_epoch so the visible wires re-tessellate against
the reused block cache:

- add_entity for a non-Insert/Block entity (line, arc, text, hatch, …) —
  scene.add_entity only ever routes to model/paper space, never block
  content;
- grip drag hide / commit / Esc-cancel — moving a top-level entity or an
  insert never edits a block defn.

Line-commit and grip start/end drop from ~400ms to the ~30ms baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:24:02 +03:00