Commit graph

734 commits

Author SHA1 Message Date
Hakan Seven
572415485e chore: bump acadrust — 3DSOLID AcDs geometry loading
Loads ACIS/ASM modeler geometry for 3DSOLID/REGION/BODY from the AcDs
data store (AutoCAD 2013+), so solids stored there now render instead of
coming through empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 23:38:31 +03:00
Hakan Seven
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 Seven
1f54b1241f chore: bump acadrust — dynamic-block visibility parsing
Brings in AcDbBlockVisibilityParameter / AcDbBlockRepresentationData
parsing and the dynamic_visibility_for_insert resolver, the foundation
for the upcoming visibility-state UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:44:53 +03:00
Hakan Seven
f366aae33c fix(blocks): hide invisible sub-entities in block definitions
A dynamic block with a visibility-state parameter stores the geometry
for every state in one anonymous block and marks all but the active
state's entities invisible. The block-cache builder tessellated every
sub-entity regardless, so a visibility block rendered all variants
stacked on top of each other instead of the one selected profile.
Skip invisible sub-entities in both the render pass and the precision-
offset centroid pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 19:06:55 +03:00
Hakan Seven
28a30de15d fix(wipeout): close rectangular fill loop so the mask stops bleeding
The GPU in_polygon ray-cast walks sequential boundary pairs and never
wraps last→first, so it needs an explicit closing vertex. The polygon
fill branch already appends one, but the rectangular branch returned a
bare 4-corner quad — the v3→v0 (left) edge went untested and the solid
mask bled past the boundary. Newly created wipeouts take this branch,
so the fill rendered wider than the clicked box while the border/grips
(drawn as wires) stayed correct. Repeat corner 0 to close the loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:10:47 +03:00
Hakan Seven
ece5d50796 chore: bump acadrust to current main
Picks up the AC18 section-name fix (cut at first null), so DWGs whose
writer left garbage after the name terminator load their entities
instead of opening with zero. Also rolls in the intervening upstream
ellipse-WCS and reflecting-transform fixes already on main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:28:21 +03:00
Hakan Seven
cf817ff9eb fix(io): purge near-collinear arcs that hang load
A 35-unit arc sweeping 6.5e-7 rad has arc length 2.3e-5 — past the
arc-length floor — yet start/mid/end bow off the chord by only ~2e-12.
truck's 3-point circle_arc fit then returns a near-infinite radius and
parameter_division subdivides without bound, hanging the load. Gate on
the sagitta (r·(1−cos(sweep/2))), the true measure of the fit's
conditioning, so long thin arcs survive and sub-precision ones drop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:10:00 +03:00
Hakan Seven
8f39e77a0d chore: release v0.5.2 2026-06-11 14:41:24 +03:00
Hakan Seven
d09614be39 fix(ellipse,mirror): WCS ellipse rendering + delegate hatch MIRROR upstream
Two follow-ups from the boundary-arc convention audit:

- ELLIPSE is a WCS entity in DXF (codes 10/11 are world coordinates,
  unlike ARC/CIRCLE). The renderer ran center and major_axis through the
  arbitrary-axis OCS, which misplaced any ellipse whose normal isn't Z-up
  — e.g. the (0,0,-1) result of a mirrored-block explode. Read them as
  WCS; pairs with acadrust 569bd4d, which stores WCS on its explode
  paths. Z-up ellipses are unaffected (the conversion was an identity).

- The MIRROR command's hand-rolled hatch closure swapped boundary-arc
  angles itself — only valid for ccw arcs and ignorant of the stored
  mirrored-angle / wrap-encoded-sweep conventions. It now builds a
  reflection Transform (new reflection_about_xy_line helper) and
  delegates to acadrust's transform_hatch, the single source of truth
  for those conventions. Covered by a Line→Arc→Line continuity + sweep
  regression test.

Note: full effect of the ellipse change lands once the acadrust pin is
bumped to 569bd4d; until then only the (previously broken) non-Z-up
corner differs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:03:18 +03:00
Hakan Seven
80059922c5 fix(mirror): negate polyline bulges in the MIRROR command
The MIRROR command's LwPolyline / Polyline2D closures reflected the
vertices but left the bulges untouched, so every curved segment of a
mirrored polyline bowed to the wrong side. A reflection reverses the
side a bulge arc bows to — negate it alongside the vertex reflection
(same convention acadrust applies on its reflecting-transform paths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:17:47 +03:00
Hakan Seven
6d60ef45d5 fix(hatch): wrong-way giant boundary arcs in blocks — bump acadrust
Hatches inside blocks (xref_Plan-Sade) drew some boundary arcs as their
near-full-circle complement, covering huge areas — in the pattern clip
when rendered through the INSERT, and in both pattern and boundary wires
after EXPLODE.

Root cause (acadrust 64efaf8): the stored sweep of a boundary arc is
always forward and AutoCAD encodes a wrap through 0 by writing the end
angle ABOVE 2π (5.81 → 6.64 for a 0.83 rad arc). transform_hatch
normalized angles into [0, 2π), turning such arcs into their 5.46 rad
complement on ANY insert pass-through — even a pure translation, which
is why the same hatch looked fine outside the block. The fix transforms
only the start angle and carries the stored sweep over unchanged
(invariant under rotation and mirror); RAW vs exploded edge angles are
now bit-identical on the reporting file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:01:56 +03:00
Hakan Seven
14b5067e25 fix(hatch): mirrored-block arcs — bump acadrust, document angle convention
Hatches inside mirrored INSERTs (x_scale = -1, e.g. the *U blocks in
xref_Plan-Sade) drew some boundary arcs swept the wrong way, covering
huge complementary regions — in the pattern clip before EXPLODE, and in
both pattern and boundary wires after (the exploded entity bakes the
broken angles).

Root cause was in acadrust's transform_hatch, not the renderer: DXF
stores CW (ccw=false) boundary-arc angles MIRRORED (verified against
AutoCAD output by endpoint continuity — mirrored interpretation gives
Δ=0.0 against adjacent edges), and the transform stored geometric angles
after flipping the direction flag. The legacy (TAU-θ) sampling flip in
arc_signed_span is exactly right for file data and stays; its comment now
records the verification so the convention isn't "fixed" away again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:41:16 +03:00
Hakan Seven
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 Seven
ed29933d87 fix(hover): drop the geometry-only pick cache — it served stale culled wires
hit_test_wires got a cache keyed on geometry_epoch alone (462d9b4), but
the underlying wire set IS view-culled (wires_for_block culls to
view_world_aabb). After zoom-in + edit, the cache pinned the set culled
to that close-up view; zooming back out, blocks that had been off-screen
were missing from the pick set, so hover/selection stopped working on
them until the next geometry change.

Revert to the camera-keyed entity_wires_arc for picking — it re-culls
when the view changes. The actual 0.5.1 hover regression fix (hatch
rebuild/re-upload on rollover) stays.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:41:51 +03:00
Hakan Seven
462d9b4b66 perf(hover): don't rebuild/re-upload hatches on every rollover (0.5.1 regr)
Selected hatches carry a tint (#71), so 0.5.1 keyed both the hatch model
cache and the static GPU buffer re-upload on `selection_generation`. But
that counter also bumps on hover, so moving the cursor over a new entity
rebuilt EVERY hatch model and re-uploaded the hatch + face3d buffers each
time — the large-file hover stutter introduced between 0.5.0 and 0.5.1.

The hatch tint depends only on the *selected set*, never on hover. Key
both on an order-independent signature of `selected` instead, so a
rollover keeps the caches warm; an actual select/deselect still rebuilds.

Also cache the picking wire set on `geometry_epoch` alone (separate from
the camera-keyed render cache) so a pan/zoom no longer re-tessellates the
whole un-culled model on the next mouse move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:20:45 +03:00
Hakan Seven
14e85171b3 perf(hover): stop scanning every wire on hover for large drawings
Two per-hover O(N) costs on big drawings, both keyed off the cursor:

- The selection/hover xray overlay rebuilt by scanning ALL wires and
  string-parsing each `name` into a handle on every hover change. Index
  handle → wire slots once per wire upload instead, so the overlay gathers
  only the highlighted entity's wires (O(highlighted)).

- click_hit (runs on every mouse move) projected every vertex of every
  wire to screen. In the flat top-down ortho view it now pre-rejects a
  wire by its world-space AABB (four corners projected, so a Z-rotated
  plan view is still correct) and only projects points for wires actually
  near the cursor. Tilted/orbit views fall back to the full test, so no
  pick is ever missed; covered by a unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:01:12 +03:00
Hakan Seven
ebae67166e docs(roadmap): collapse completed items to headings, drop 1.2
Strip the body of every  DONE item down to its heading, mark 3.5
(glyph-stroke batching) done — fonts already parse once via OnceLock and a
text emits one grouped WireModel, not one-per-stroke — and remove 1.2
(background XREF resolve). Priority list updated to the remaining items.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:28:01 +03:00
Hakan Seven
0ca1e58b1f fix(io): purge zero-length point-collapsed splines that hang load
sample_layout_03/04.dwg would not open: a periodic 9-control-point spline
with every control point pinned at the origin (zero extent) reaches truck's
parameter_division, which never converges on a zero-length curve and hangs
the first-frame tessellation. The spline passed every existing guard
(finite points, no knots, degree 3). Reject splines whose control-point
extent is sub-precision (< 1e-6 on all axes).

Verified: all five sample_layout DWGs now load + tessellate (03/04 went
from a hang to ~356 ms; 01/02/05 unchanged, no valid spline dropped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:27:45 +03:00
Hakan Seven
e5d0c3950b feat(styles): persist current text/dim/mline style change to DWG
DWG stores the current text/dim/multiline style as a header handle; the
writer keeps a valid stored handle and otherwise falls back to Standard,
so a Set Current change (which only updates the name) was lost on DWG
save. Treat the current-style names as the source of truth and re-resolve
the header handles from them at save time, alongside the table/multileader
variable-dictionary sync. Now every current-style choice round-trips
through DWG, not just DXF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:33:56 +03:00
Hakan Seven
b3c1c8b4cb feat(styles): write current table/multileader style back to DWG
On save, mirror the current table/multileader style from the header into
the variable dictionary (CTABLESTYLE / CMLEADERSTYLE DICTIONARYVAR
entries) so a Set Current choice round-trips through DWG, which stores
them there rather than in the header. Done once in save_as_version (the
shared clone point), so every save path and both formats stay in sync.
No-op when the document has no variable dictionary (fresh files).

Verified: mutate the entry, write+read a real DWG, value survives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:29:17 +03:00
Hakan Seven
2044755418 feat(styles): read current table/multileader style from DWG too
DWG has no header field for the current table / multileader style — they
live in the variable dictionary as DICTIONARYVAR entries keyed CTABLESTYLE
/ CMLEADERSTYLE. Resolve them on load via the document's dictionaries and
set the header names, so the green ✓ marks the right style for DWG files
as well (DXF already worked via the header vars). A missing entry leaves
the value untouched, so there is no risk to the binary header parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:17:07 +03:00
Hakan Seven
31cdd6632f chore: bump acadrust to current main (upstream sync)
Picks up the fork's merge of upstream acadrust (geodata decode, DXF
roundtrip newline sanitization, DWG alignment) on top of the current
table/multileader style work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:09:46 +03:00
Hakan Seven
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 Seven
b3ba3a53d1 feat(styles): green ✓ for current style + resolve it from DWG files
- The current-style marker is now a green ✓ in its own fixed-width column
  so names stay aligned; rendered as a separate coloured glyph rather than
  baked into the label string.
- Resolve the current text / dimension / multiline style from the handle
  the DWG header stores into the name the app reads (fix_current_style_names
  on load). DXF already provides these as names ($TEXTSTYLE / $DIMSTYLE /
  $CMLSTYLE); DWG only stores handles, so the marker previously always fell
  back to "Standard". Only overrides when the handle resolves.

Note: table and multileader current styles are not surfaced by the file
reader (no header name/handle exposed), so those markers still default to
the app's active selection until acadrust exposes them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:56:13 +03:00
Hakan Seven
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 Seven
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 Seven
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 Seven
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 Seven
9cdb5ea5fd fix(io): purge degenerate arcs/splines that OOM on load
Large survey drawings (105k entities, multi-million unit extents) could
exhaust all RAM + swap on one core during load and get OOM-killed before
display. Two parser-garbage entities slipped past the corrupt-entity
purge and blew up truck's parameter_division tessellation:

- Near-zero-sweep arcs: the zero-sweep guard only caught sweep < 1e-9,
  but a tiny non-zero sweep (e.g. 1.6e-6 rad) still places start/mid/end
  within truck's coincidence tolerance and recurses unboundedly. Gate on
  arc length (radius x sweep) instead so large-radius small-sweep arcs
  (still visible curves) survive while sub-precision arcs drop.
- Desync splines with 100k control points and an invalid knot vector had
  no guard at all. Add a Spline arm rejecting the 100k desync signature,
  non-finite control points, degree < 1, and bad knot vectors.

Also cap complex-linetype dash expansion (complex_lt::apply_along) at
1e6 pattern repeats, falling back to a solid stroke — a latent OOM of
the same class on map-scale entities carrying a finely-scaled pattern.

Adds regression tests for the arc-length and spline guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:39:50 +03:00
Hakan Seven
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 Seven
fb9f5bd4e5 chore(release): bump to 0.5.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:00:46 +03:00
Hakan Seven
534535be98 feat(trim): trim closed/open polylines (#65)
TRIM had no LwPolyline arm in its pick dispatch, so clicking a polyline
segment did nothing (only EXTEND handled polylines). Add trim_lwpolyline:
parameterise the polyline by segment+u, find boundary crossings per
segment, and remove the span containing the click between the adjacent
cuts. A closed polyline needs >=2 cuts and yields the surviving open
arc; an open one yields the surviving piece(s). Fully-surviving segment
bulges are kept; the partial ends become straight. Hover now previews
the trim too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:57:29 +03:00
Hakan Seven
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 Seven
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 Seven
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
Hakan Seven
3f9c29d6df fix(hatch): render dot patterns (zero-length dashes)
A zero-length dash entry is a dot (e.g. the DOTS pattern), but every
render path treated only positive entries as marks and skipped it, so
dot patterns drew nothing.

- GPU shaders: light exactly the one pixel containing each dot's lattice
  centre. The dot grid rotates with the pattern angle while the lit pixel
  stays a steady single pixel, so dots no longer flicker at oblique
  angles or zoom (an earlier coverage/round test still aliased).
- PDF export (pattern_segments): walk the dash sequence with absolute
  phase and emit a short mark for each dot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:15:45 +03:00
Hakan Seven
6b4272521c fix(hatch): deselect refresh, boundary offset, fill-click
Three hatch fixes:

- Selected hatches are tinted, but the hatch model cache and GPU upload
  were keyed only on geometry_epoch, so Esc cleared the selection set
  without repainting — the hatch stayed blue (#71). Key both on
  selection_generation too.

- add_hatch stored the boundary entity in local render space, so the
  boundary wire re-projected `world_offset` away from the fill on
  offset (large-coordinate) drawings. Build it in WCS.

- Clicking a hatch only selected it on its boundary wire, not its fill.
  Two causes: visible_hatches_for_click used a narrower block-visibility
  test than the render path (fill drawn but not clickable), and
  point_in_polygon closed every NaN sub-path — degrading the explicit
  2-point edge encoding of a boundary into zero-area loops. Mirror the
  render's visibility test and only auto-close sub-paths of >=3 verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:47:48 +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
9f35456205 Strip Storm Sewer from plugin-host branch (framework only)
Removes storm_sewer module and stormsewer crate so this branch is suitable for upstream PR. Plugin host, docs, and CadCommand acquisition hooks remain.
2026-06-09 13:43:06 -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 Seven
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 Seven
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 Seven
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 Seven
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 Seven
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 Seven
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 Flynn
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 Flynn
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 Flynn
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