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
Adds a DefaultPrograms component publishing Capabilities and a
RegisteredApplications entry under HKLM. This makes Open CAD Studio
appear in Settings > Default Apps and the shell "Open with" list, so
users can set it as the default handler for .dwg and .dxf files.
Modern Windows guards the actual default behind a per-user UserChoice
hash that an installer cannot forge, so this is the supported path to
becoming the default rather than silently overwriting the association.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
Moves the WixUI_Minimal UI and finish-screen launch checkbox out of
main.wxs into a dedicated ui.wxs fragment, referenced from main.wxs via
<UIRef Id='OpenCADStudioUI'>. Keeps the package/feature definition and
the UI concerns in separate files.
Updates the release workflow to compile both .wxs files (candle now
writes per-source .wixobj into a directory) and link both objects.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
Adds WixUI_Minimal UI to the WiX installer so users see a proper
Welcome → Progress → Finish wizard. The finish screen includes a
pre-ticked "Launch Open CAD Studio" checkbox; clicking Finish with
it checked starts the application immediately after the installer closes.
Also updates the light.exe invocation in release.yml to include
-ext WixUIExtension, which is required to link the WixUI dialogs.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
hdiutil create intermittently fails with "Resource busy" on the hosted
macOS runner (stale mount / background indexer), failing an otherwise
successful build. Detach any leftover volume and retry up to five times
before giving up, then assert the image exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
build_geos only turned Line/Arc/Circle/Ray/XLine/Ellipse/Spline into
cutting-edge geometry, so a polyline contributed no boundary and a line
could not be trimmed (or extended) against it — even though trimming the
polyline itself against lines already worked. Explode each polyline
(LwPolyline / Polyline / Polyline2D / Polyline3D) into its Line + Arc
segments and add them as boundaries, tagged with the polyline's handle so
the click-target self-exclusion still holds. Factored the per-entity Geo
build into geo_from_entity and exposed explode_polyline_segments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
ZOOM EXTENTS routes through fit_all, which built its bounds from the
wire set and returned early when there were none. A drawing of only 3D
solids has no wires, so ZOOM EXTENTS did nothing. Collect the
layout's mesh AABBs alongside the wires, keep going when only meshes
exist, and fold them into the fitted bounds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ZOOM EXTENTS / auto-fit derived the drawing bounds from wire AABBs and
tessellated key-vertices only. 3D solids (Solid3D/Region/Body) render as
meshes with an empty wire path, so they contributed nothing and a
drawing containing only solids zoomed to nothing. Fold each mesh's
offset-relative XY AABB into the extents on both the wire-cache and
tessellate-fallback paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Solid3D / Region / Body have no truck wire conversion, so the wire path
fell through to the generic fallback, which returns a 1-unit placeholder
segment at the origin — a stray little line next to every 3D solid. Give
these types their own fallback that emits only the entity's pre-computed
edge wires (empty for binary SAB) plus an insertion snap, never the
placeholder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ACIS keeps a solid's geometry in body-local space and records the world
placement in a `transform` record (3×3 affine + translation + scale).
The tessellator ignored it, so solids rendered at their local origin
instead of where the drawing puts them. Apply the body transform to the
mesh vertices (and rotate the normals) after tessellation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Binary (SAB/ASM) solids no longer collapse every vertex to the origin,
so 3D solids from AutoCAD 2013+ render with real geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>