Commit Graph
1299 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 4.8 e366e47b05 feat(draw): LINE deferred tangent — a line tangent to two circles
A line whose FIRST point is a Tangent snap on a circle can't be fixed
until its direction is known, and a line tangent to TWO circles is their
common tangent — neither is resolvable at a single pick. A plain LINE
dropped the tangent object, so it drew a wrong line (TAN-TAN between two
circles).

Add an opt-in on_point_with_tangent hook (default ignores the tangent, so
every other command is byte-for-byte unchanged) and route the running
Tangent object through the point commit. LINE defers a first-point circle
tangent and resolves it on the next pick: against a second circle tangent
it picks the common tangent of the two circles (up to four — chosen by
the two cursor hits, so hovering selects inner/outer + side); against a
plain point it draws the tangent from that point to the circle.
last_point syncs to the resolved anchor. Common-tangent and point-tangent
math is f64 with perpendicularity unit tests.

Refs #274

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 13:06:36 +03:00
Hakan SevenandClaude Opus 4.8 2da21146c4 fix(snap): compute a real tangent point, not the nearest circle point
The earlier #274 fix only corrected the tangent point's coordinate — the
point was still the circle edge facing the cursor, so Tangent snapped
like Nearest. Compute the true tangent from the point the command is
drawing from (from_point): the tangent point's radius is perpendicular to
the line from that point, giving two solutions at acos(r/|CP|) either
side of the C→P direction — take the one nearer the cursor. Falls back to
the facing point for a deferred tangent (object picked first) or a point
inside the circle. A line is tangent to itself everywhere, so the Line
case stays nearest-on-line. Covered by a perpendicularity unit test.

Refs #274

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 12:49:36 +03:00
Hakan SevenandClaude Opus 4.8 d81979b807 fix(snap): don't draw extension guides from circles on intersection
Catching a plain line-line Intersection re-runs extension_bases_screen to
draw the dashed guide(s) for an *extended* intersection (#247/#259). It
scanned every wire including tessellated circles, and a full circle's
chords point in every direction, so one almost always has an outward
extension that grazes the crossing — drawing spurious guide lines
radiating from nearby circles.

Skip closed curves (full circle / ellipse) in that scan, identified by
their Quadrant snap hints (arcs never carry them, so arc extension guides
are unaffected).

Closes #276

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 12:03:52 +03:00
Hakan SevenandClaude Opus 4.8 42e11b9250 fix(hatch): render user-defined hatches from angle + spacing
A user-defined hatch (angle + spacing + optional cross) carries the
placeholder pattern name "_USER", which isn't in the predefined catalog,
so it fell to a fallback that forced every one to flat horizontal lines
at the wrong spacing — ignoring the angle, the spacing and the double
flag.

Build base line families (angle 0, plus 90 when `is_double`) with unit
perpendicular spacing and let the HatchModel's existing angle_offset
(= pattern_angle) and scale (= pattern_scale) rotate and space them, the
same way predefined .PAT patterns are applied — so the angle/scale is
applied once, not doubled. Verified against the sample (0°/30°/90°-cross/
315°-cross at spacing 5/10).

Closes #278

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 11:58:44 +03:00
Hakan SevenandClaude Opus 4.8 e512e57a7c fix(snap): correct circle tangent point + no endpoint on closed curves
Two object-snap fixes:

- Tangent on a circle built the snap point as
  `(cx + r·nx, cy, cy + r·ny)` — the y-offset landed in Z and Y was stuck
  at the centre, so the tangent point sat at the wrong place. Build it as
  `(cx + r·nx, cy + r·ny, cz)`.

- Endpoint snapped to a full circle / ellipse: for tessellated curves it
  snaps the first/last point, but a closed curve's first/last is a seam,
  not an endpoint. Skip Endpoint for closed curves — identified by their
  Quadrant snap hints, which arcs never carry — so arcs still snap their
  real endpoints.

Closes #274, closes #275

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 11:58:44 +03:00
Hakan SevenandClaude Opus 4.8 d0639b3e38 Merge PR #273: perf(render) scene-render cache + hatch interaction LOD
Skip re-rasterizing an unchanged frame (per-viewport render signature →
re-blit the cached resolve texture) and suppress the full-screen hatch
pass while navigating, rendering hatches once on settle. Reviewed:
signature covers every pixel-affecting input, per-viewport caches are
isolated, LOD settle timing renders + caches the full-quality frame, no
busy-loop. Targets #258.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 11:35:42 +03:00
sLuCHa a55e1ac4f8 perf(render): skip hatch pass while navigating (interaction LOD)
The batched hatch pass evaluates its fill procedurally per fragment — a
point-in-polygon test over the whole boundary plus the pattern test, with a
`discard` that disables early-Z. When the view sits inside a hatch its quad
covers the screen, so panning / zooming re-runs that full-screen shader every
frame (~40 ms/frame on a hatch-heavy plan) even though the rest of the scene
is cheap — the wire pass and CPU prepare are negligible by comparison.

Suppress the hatch pass while the view is actively panning / zooming /
orbiting, detected from `camera_generation` plus a short settle timer
(`Scene::navigating_lod`), and fold that state into the render signature so
the moment the view settles it renders hatches once and the scene-render
cache holds that full-quality frame. A brief `is_settling` subscription keeps
requesting frames just past the settle point so the hatched frame renders
even when no input event would otherwise fire. Mirrors AutoCAD's adaptive
degradation. The paper sheet keeps its fills.
2026-07-05 09:48:49 +02:00
sLuCHa f99f1cd7a5 perf(render): cache rendered scene, skip re-raster on unchanged frames
Every cursor movement over the viewport re-rendered the entire GPU scene
(all wires, hatches, meshes) even when nothing visible changed: iced redraws
the whole window on each input message, and the shader's draw/prepare/render
runs every time. On large drawings one such redraw exceeds the frame budget,
so moving the mouse or panning stutters — and it gets worse the bigger the
drawing.

The pipeline already resolves each frame into a persistent, clip-sized
resolve texture and blits that to the surface. This hashes the inputs that
determine the rendered image (camera uniforms, geometry/selection
generations, wire content id, render-mode flags, clip size, live preview
coordinates) into a per-viewport signature. When a frame's signature matches
the previous one the image is pixel-identical, so `prepare` skips the buffer
uploads + per-frame scissor/LOD recompute and `render` skips passes 1-7 plus
the MSAA resolve, re-blitting the resolve texture instead. A pure cursor move
becomes a single fullscreen blit rather than an O(N) re-rasterization.

The ViewCube is excluded from the signature (it renders in its own always-on
pass) so cube hover still tracks while the scene is cached. Any real change —
edit (geometry_epoch), select/hover (selection_generation), camera move or
render-mode toggle — differs the signature and forces a full render, so
nothing goes stale. Navigation still renders every frame (the view genuinely
changes); this targets the wasted re-render on cursor-only movement.
2026-07-05 08:11:35 +02:00
Hakan SevenandClaude Opus 4.8 aa63634d95 fix(camera): orbit target in f64 to avoid extent-scale jitter
`orbit` revolved the target around the pivot as
`p + (delta * (self.target - p).as_vec3()).as_dvec3()`, casting the
target-to-pivot offset to f32. For a large model that offset is
extent-scale (the cursor can sit far from the view centre), so the f32
cast quantized it — nudging the whole view a fraction of a unit each orbit
step regardless of the drawing's origin. Do the revolve entirely in f64.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 03:26:07 +03:00
Hakan SevenandClaude Opus 4.8 626031de00 perf(render): reuse static wire set for model-space pick/snap
Every pan/rotate on a large drawing jerked once at the start of the
gesture. Instrumentation traced it to `paper_sheet_wires_arc` re-running a
full ~300 ms tessellation of the whole model block on the first hit-test
after each camera move.

`entity_wires_arc` (the pick/snap source in Model layout) delegated to the
camera_generation-keyed, view-culled `paper_sheet_wires_arc`. A camera
move bumps `camera_generation`, so the next hit-test cold-missed that cache
and re-tessellated every visible entity — O(visible), which is why the
jump grew with on-screen object count and shrank when zoomed in. The GPU
render was unaffected (it holds the separate camera-independent
`model_static_wires`), so only that one CPU frame stalled and the
accumulated mouse motion applied at once, reading as a jump.

Point the Model pick path at the same resident static set the render holds
(keyed on geometry_epoch only). Pick wants every entity, not a view-culled
subset, so this is also more correct — off-screen entities stay pickable —
and it never re-tessellates on a camera move.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 03:26:07 +03:00
Hakan SevenandClaude Opus 4.8 845194e05b refactor(render): reuse block_has_hatch in synced_hatch_models
The hatch-free-block skip added in the previous commit duplicated the
existing `block_has_hatch` helper (already used by
`insert_hatches_for_click`). Drop the parallel `block_contains_hatch` and
call the shared, memoised helper instead — same result, one implementation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 02:10:56 +03:00
Hakan SevenandClaude Opus 4.8 0ce918da68 perf(render): skip exploding hatch-free blocks in hatch build
Opening an xref-heavy drawing froze for ~22s before the first frame.
`synced_hatch_models` walked every INSERT and called
`explode_from_document()` to scan the result for hatch fills — but explode
CLONES every child of the block, including 3D solids that each carry tens
of KB of SAB geometry. Clinic.dwg's five xref INSERTs each reference a
discipline block of thousands of solids and no hatch, so this materialized
~25000 solid clones (~1 GB) on every open just to find zero hatches.

Add a memoized `block_contains_hatch`: a read-only scan of the block's
entity types (recursing nested INSERTs, cycle-guarded) that skips the
explode for blocks containing no hatch, transitively. Blocks that do hold
a hatch still explode exactly as before, so the hatch set is unchanged.

Hatch build for Clinic.dwg drops from ~21.7s to ~0.02s, cutting the total
open-to-first-frame time from ~25s to ~5s.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 02:03:16 +03:00
Hakan SevenandClaude Opus 4.8 c91593ab9c perf(render): bump acadrust for O(1) ACIS record lookup
Pull in acadrust 4d51947, which makes SatDocument::record() an O(1) hit
instead of a linear scan. Tessellating an ACIS-heavy drawing was
O(records²) per solid; this drops the post-load mesh build for Clinic.dwg
(~9900 xref'd solids) from ~2.2s to ~0.85s of parallel tessellation, with
identical geometry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 01:25:57 +03:00
Hakan SevenandClaude Opus 4.8 8946fadcb2 perf(xref): parse referenced files in parallel
`resolve_xrefs` parsed each xref DWG/DXF one after another, so opening a
drawing that references several large discipline files paid the sum of
their parse times. Split into two phases: parse every file in parallel
(each `load_file` is independent and never touches the host document),
then merge the parsed results serially in the original block order (merge
mutates the host doc, so it stays sequential and deterministic).

Resolving Clinic.dwg's 5 discipline xrefs drops from ~4s to ~2.1s with an
identical result (5 loaded, 0 dropped, same entity count). The speedup is
sub-linear because parsing is memory-bound (each file decompresses ~95 MB
of SAB geometry), not because of the serial merge.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 01:09:44 +03:00
Hakan SevenandClaude Opus 4.8 13de968ee9 perf(xref): fix O(n²) AcDs parse and skip host re-tessellation
Opening a drawing that xrefs several 3D-heavy DWGs (e.g. Clinic.dwg with
5 discipline files) took ~137s, effectively all of it inside the DWG
parser's AcDs SAB blob extraction.

Bump the acadrust pin to ceef045, which bounds the AcDs end-marker search
so a missing marker family no longer scans the whole buffer per record.
Total xref resolve for Clinic.dwg drops from ~97s to ~4s.

Also make the post-xref mesh pass incremental: the host solids were
already tessellated by the background loader and the merge assigns fresh
handles to every imported entity, so `populate_missing_meshes_from_document`
keeps the cached host meshes and tessellates only the merged xref solids
instead of clearing and rebuilding the whole document.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 00:38:38 +03:00
Hakan SevenandClaude Opus 4.8 cbf454979d chore(solid): drop needless mut on full_circle
`full_circle` from `angular_range` is only read, never reassigned — clears
the unused-mut warning.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:47:00 +03:00
Hakan SevenandClaude Opus 4.8 f491e8436b fix(snap): stop Extension tracking points from vanishing while in use
Extension-driven acquisition (OTRACK off) acquired any dwelled snap
point, including the extension foot the cursor paused on while tracing an
extension. Each such spurious acquisition filled the 4-point buffer and
evicted, oldest-first, the endpoint the user had actually acquired — so
after a few pauses its + marker and its extension disappeared.

Restrict Extension-driven acquisition to real segment endpoints (points
with an incident edge); midpoints, centres, intersections and extension
feet are ignored. OTRACK still acquires any snap point.

Refs #262

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:42:40 +03:00
Hakan SevenandClaude Opus 4.8 d93bcda889 fix(snap): OSNAP Extension tracks only acquired endpoints
Extension snap extended every segment's line in the whole drawing, so in
a larger project an extension foot was live almost everywhere and the
cursor snapped to it constantly.

Extension now behaves like a temporary tracking point: an endpoint must
first be acquired by dwelling on it, and only then does that segment's
line extend. Acquisition — and the tracking-point marker — is active
whenever OTRACK is on OR the Extension snap is on, so Extension works
independently of OTRACK's on/off state (and is not tied to POLAR). Both
the per-segment extension and the extended-intersection candidates are
gated on an acquired endpoint, and the whole scan is skipped when nothing
is acquired (the common case), keeping large drawings responsive.

Closes #262

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:30:12 +03:00
Hakan SevenandClaude Opus 4.8 2bbd3cfcd2 feat(pick): select mesh entities by clicking their shaded faces
Mesh entities (PolyfaceMesh / PolygonMesh / SubD Mesh) could only be picked
on their thin edges — click_hit tested wire segments, and the solid face pick
only covers ACIS solids. When no edge is close, click_hit now also tests the
mesh's fill_tris with the same projected-triangle containment mesh_click_hit
uses for solids, so clicking a mesh's surface selects it, front-most first.
Edge proximity still takes priority.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:11:47 +03:00
Hakan SevenandClaude Opus 4.8 67bcdb1731 fix(mesh): ear-clip concave mesh faces instead of fan-triangulating
PolyfaceMesh and SubD Mesh faces were fan-triangulated from vertex 0, which
is only valid for convex faces. A concave face (e.g. an L-shaped / notched
n-gon) fanned into triangles that spill outside the outline — spurious
"triangles that shouldn't be there" that covered neighbouring geometry and
its edges. Replace the fan with an ear-clipping triangulator that projects
the planar face to 2D, respects its winding, and clips only real ears, so
concave faces triangulate to exactly their own area. Convex faces (and the
cylinder-cap n-gons) are unchanged in result. Falls back to a fan for a
degenerate / self-intersecting polygon.

Verified on Annotation-2D-Mesh-Solid-BIM.dwg (concave 5-/6-gon mesh faces)
and by a unit test: a concave L-hexagon triangulates to n-2 triangles whose
total area equals the polygon's, with no overshoot.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:01:59 +03:00
Hakan SevenandClaude Opus 4.8 77c0c5a9d0 feat(plugin): surface loaded plugin commands in command-line autocomplete
Autocomplete suggestions came only from all_registered_command_names() —
the compile-time inventory registry returning &'static str — so
runtime-loaded plugin commands could never appear. Typing a plugin's
command prefix (e.g. `LS_`) showed nothing even with the plugin loaded
and its ribbon tab active; dispatch worked, only discovery was blind.

Collect each enabled plugin's ribbon ToolDef command ids plus its
manifest command_prefixes into a dynamic candidate pool
(OwnedRibbonGroup::command_ids → PluginManager::command_names →
plugin_command_names), refreshed in rebuild_ribbon_modules on startup
load, settings reload, and every enable/disable toggle. ranked_matches
now merges this pool with the static registry and returns owned strings.

A plugin.toml `commands = [...]` list for sub-verbs with no ribbon button
(e.g. LS_AUTOLABEL) stays a follow-up — it's a plugin-API surface change.

Closes #272

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 23:01:53 +03:00
Hakan SevenandClaude Opus 4.8 60872da62a chore(deps): bump acadrust to f76e41a for the xref save fix
Picks up the acadrust change that writes xref blocks as external
references instead of serializing their resolved contents as owned
entities. An xref attached with XATTACH now survives a save + reopen as
a reference instead of being bound/exploded into loose objects.

Closes #268

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 22:40:25 +03:00
Hakan SevenandClaude Opus 4.8 d644a85c72 feat(layers): alphabetical default order + scroll to a new layer
The Layer Manager table and the ribbon's quick layer dropdown both
showed layers in document (insertion) order. Default the panel sort to
Name so both list layers alphabetically; a header click still re-sorts
by any other column. `apply_sort` reorders the panel's layer vector in
place, which the ribbon dropdown reads, so one default covers both.

With alphabetical ordering a freshly created layer can land anywhere in
a long list, so give the table scrollable an id and scroll its row into
view after creation, keeping the rename prompt visible.

Closes #270, closes #271

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 22:28:07 +03:00
Hakan SevenandClaude Opus 4.8 450dbf79ac fix(dynin): read a comma-separated second point as cartesian, not W/H
A rectangle's opposite corner is entered through Width/Height dynamic
fields, whose sign is taken from the cursor quadrant on commit. Typing
"50,50" is a cartesian coordinate pair, not two cursor-signed
dimensions, but the comma advance kept the Width/Height roles so the
committed corner mirrored into whatever quadrant the cursor sat in.

Key off `dyn_user_reshaped` (set only by `,`, never by Tab) to treat a
comma-separated entry as a literal cartesian coordinate for every field,
in both the point resolver and the live dyn-box display. Tab-separated
entry still reads as signed Width/Height dimensions.

Closes #269

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 22:27:59 +03:00
Hakan SevenandClaude Opus 4.8 62680883cd fix(pick): don't XY-cull the pick wire set in tilted 3D views
view_world_aabb built a world-XY cull rectangle from the camera target and
ortho size without checking the view angle, so in a tilted / 3D view it
produced a top-down box that does not bound the on-screen footprint. That
box culls the pick wire set (while the render holds the full set via the
static hold), so a mesh running partially off-screen fell outside the box
and became unselectable — by edge or by face — even though it still
rendered. Top views were unaffected (the box is correct there), and 3D
solids pick through their mesh AABBs (not this wire set) so they never
showed it.

Return None for tilted views (fwd.z.abs() < 0.999), matching view_cull_aabb,
so no world-XY cull is applied and the pick reaches every on-screen wire.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 22:13:37 +03:00
Hakan SevenandClaude Opus 4.8 17077ba8bf fix(render): don't apply the draw-order z-bias to 3D mesh outline edges
Mesh (PolyfaceMesh / PolygonMesh) outline edges are drawn through the wire
pipeline, whose shader pulls each wire toward the camera by
draw_depth * DRAW_ORDER_BIAS (0.001) for 2-D draw-order layering. Every
entity gets a draw_depth in [-1, 1], so a mesh's edges were pulled forward
too — and when the mesh's depth extent projects to less than the bias (a
small or distant mesh), its hidden back-face edges peeked through its own
shaded fill. Solid feature edges use the mesh shader, which has no such
bias, which is why only meshes showed it and why adjusting the fill's depth
bias had no effect.

3-D mesh outline edges are real geometry occluded by true depth, so force
their draw_depth to 0 (they are already grouped as `mesh_edge` in
upload_wires). They now occlude exactly like solid edges.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 21:04:37 +03:00
Hakan Seven 23d9a261b1 chore(release): v0.7.5 v0.7.5 2026-07-04 20:18:47 +03:00
Hakan SevenandClaude Opus 4.8 f8ceddd303 fix(solid): read rational NURBS spline-surface control points with weights
An ACIS spline-surface stored as `nurbs` (rational) carries a weight on each
control point, so the control net is a stream of x, y, z, w tuples. The
parser read plain x, y, z triples, shifting every control point by one float
and scrambling the whole net — the surface, and the solid it bounds,
rendered badly deformed. Non-rational `nubs` surfaces were unaffected, which
is why only some solids (rational lofts/sweeps with varying weights) broke.

Detect `nurbs` vs `nubs`; for rational surfaces read the fourth weight
component and build a truck `NurbsSurface` from homogeneous (xw, yw, zw, w)
control points so the weights actually shape the surface. Both the truck
B-rep path and the bespoke fallback share this parser.

Verified on VillaSavoye_MartinHedin2.dwg solid 0x24A1EE (handle 2400750): the
tessellated surface now stays within the solid's true extent instead of
flying outside it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 20:15:17 +03:00
Hakan SevenandClaude Opus 4.8 e515fa3156 feat(spline): draw fit-point splines via Catmull-Rom interpolation
A spline stored by fit points (DWG R2013+ scenario 2) has no control points
or knots, so the renderer — which built its curve from control points —
produced nothing and the spline was invisible. When a spline has fewer than
two control points but at least two fit points, interpolate a smooth
Catmull-Rom curve through the fit points and emit it as a dense polyline.
Open ends use reflected phantom points; closed/periodic splines wrap.

Pairs with the acadrust bump (e8e422a) that decodes the R2013+ fit-point
scenario correctly — together they make the fit-point spline in
Annotation-2D-Mesh-Solid-BIM.dwg render.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 19:52:56 +03:00
Hakan SevenandClaude Opus 4.8 e6a961be6c feat(render): draw 3D mesh outline edges black in filled modes
Extends the solid feature-edge behaviour to mesh entities (PolyfaceMesh /
PolygonMesh): their triangulation outline edges now render black when a
filled-with-edges mode is active, and keep the entity colour in the
lines-only modes (wireframe / hidden-line).

A mesh entity emits its face fill and its outline edges as separate
WireModels sharing the entity handle: the fill carries fill_tris plus a
non-empty fill_tris_low (real 3D depth), the edge carries points. The old
is_3d_mesh_edge flag keyed on `!fill_tris.is_empty()`, which tagged the
points-less fill wire rather than the edge wire, so the edges were never
recognised. upload_wires now collects the handles of real 3D fill wires and
flags any points-wire sharing one as the mesh edge (text greek, whose fill
has an empty fill_tris_low, is excluded — matching the face3d test). The
wire draw loop then routes flagged edges through a new black wire pipeline
(fs_black) in filled-with-edges modes.

fs_black lives in wire_indexed.wgsl (the native shader) and wire.wgsl (the
wasm shader).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 18:28:27 +03:00
Hakan SevenandClaude Opus 4.8 1065bc12b2 feat(render): draw ACIS solids as B-rep feature edges, meshes as tri edges
Solid wireframes rendered every triangle edge of the tessellation, so a
curved solid read as a dense mesh instead of its real outline. Now each ACIS
solid's B-rep face-boundary edges are collected from the SAT `edge` records
(straight edges as segments, ellipse/circle edges sampled along their bounded
arc), body-transformed and split into the double-single pair, and stored on
the MeshLodSet. The block-instance and top-level offset paths transform these
edges alongside the mesh vertices.

The batch builder routes a solid with feature edges into a dedicated LineList
vertex buffer and skips its triangle wireframe; a mesh with no B-rep edges
keeps its triangulation edges. Edge colour follows the display mode: the flat
`fs_edge` shader draws the entity colour in the lines-only modes (wireframe,
hidden-line) and a `fs_edge_black` twin forces black for the edge overlay in
filled render modes.

Verified: the Clinic storefront arch bars now yield ~14-16 clean edge
segments (vs the full triangulation) bounded to the solid's true extent.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 17:57:00 +03:00
Hakan SevenandClaude Opus 4.8 a08cf477c1 fix(render): draw transparent solids without depth writes
Once solids carried a real sub-1 alpha (after the ENC transparency fix),
the single depth-writing mesh pass made a transparent solid occlude the
geometry behind it: it wrote depth at its near surface, so anything farther
failed the LessEqual test and was never drawn — the transparent fill looked
like it erased what was behind it.

The batched mesh builder now routes each solid's triangles into an opaque or
a transparent index stream by its baked colour alpha (< 0.999). The opaque
fills draw first and write depth; the transparent fills draw last through a
new depth-write-disabled pipeline, so they blend over the geometry behind
them instead of culling it. The hidden-line depth prepass still rasterises
both streams so solid surfaces keep occluding hidden edges.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 16:27:59 +03:00
Hakan SevenandClaude Opus 4.8 9f7fdc5f8f fix(solid): tessellate cone-surface faces to their arc, not a full circle
Curved 3D-solid faces backed by an ACIS cone-surface (e.g. the arched
mullion bars of a storefront curtain wall) were swept a full revolution
regardless of the face's actual angular extent, so a bar bounded to an
8-16 degree arc rendered as a full circle of the surface radius — a large
jagged loop instead of a small arch.

cone_faces (the truck B-rep path) now derives the angular span from the
face's boundary loop and rsweeps only that arc; the bespoke fallback
tess_cone_face no longer forces a full revolution when a thin face
collapses to a single height. Arc extraction uses a seam-robust
largest-gap method so faces straddling +/-pi (the crown of an arch) are
not mistaken for full circles.

Verified against Clinic_Architectural.dwg: the seven arch-bar solids drop
from a ~12.9 (2R) bbox to their true ~1-2 unit extent, and a file-wide
check of 155 cone-bearing solids shows none ballooned and none collapsed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 16:11:24 +03:00
Hakan SevenandClaude Opus 4.8 a8c368aef9 chore(deps): bump acadrust to 4f0599a (ENC transparency field-order + opacity fix)
Fixes entity transparency dropping to opaque on DWG read (e.g. the 85%
area-highlight solids in Clinic_Architectural.dwg). The ENC codec read/wrote
the transparency BL before the rgb BL (real order is rgb then transparency)
and stored the on-disk opacity byte without inverting to the internal
transparency-amount representation.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 14:48:15 +03:00
Hakan SevenandClaude Opus 4.8 c94e50a8e1 chore(deps): bump acadrust (AcDs pre-table blob pool — HVAC 2446/2446 vs reference)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 07:22:31 +03:00
Hakan SevenandClaude Opus 4.8 a360da25fb chore(deps): bump acadrust to edfe3bd (AcDs record-table data-offset blob pairing)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 07:00:24 +03:00
Hakan SevenandClaude Opus 4.8 d6ad3100fe chore(deps): bump acadrust to f65a7e4 (AcDs record-table blob pairing)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 05:49:45 +03:00
Hakan Seven 33f1150fad chore(release): v0.7.4 v0.7.4 2026-07-04 01:46:33 +03:00
Hakan SevenandClaude Opus 4.8 b3ead4f691 chore(deps): bump acadrust to 063da6f (AcDs SAB blob handle-pairing fix)
Fixes BIM 3D solids rendering at wrong positions — the AcDs SAB blob→solid
pairing now follows handle order instead of object-stream order, so every
solid gets its own geometry and body transform. Corrects the regression that
broke 3D solids in 0.7.3 (they were correct in 0.7.2) and places wall/floor
solids that previously piled near the origin.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-04 01:42:31 +03:00
Hakan SevenandClaude Opus 4.8 e78e137664 fix(paper): keep model 3D solids off the paper-space sheet
The paper layout sheet pass reused the full model mesh set, so every
top-level 3D solid piled onto the paper origin — the sheet camera works
in paper coordinates, not model space. Guard `meshes` on
`inst.paper_sheet` the same way wires/hatches/images already are: the
sheet shows only paper-native content; the model's solids render inside
the floating content viewports (model camera + per-viewport scissor).

Also name 3DSOLID/REGION/BODY/SURFACE/MESH/SOLID/MULTILEADER in dxf_name
(they fell through to "ENTITY"), and add a SELHANDLES diagnostic that
reports the active space plus a per-type / per-block selection breakdown.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 23:29:18 +03:00
Hakan SevenandClaude Opus 4.8 313a0eacfd chore(deps): bump acadrust to 6d5c47b (issue 225 — DWG opens in strict readers)
Pulls the acadrust fixes that make a saved DWG containing 3D solids open
cleanly in a strict conformance reader: the AcDsPrototype datastore section is
now built correctly (segment-header ds_version, datidx, and the handle-sorted
search index) so 3D solids keep their geometry instead of being dropped, and a
dim-style/block handle collision on read is repaired so layouts round-trip.
Validated end-to-end on two real 107- and 273-solid files.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 21:17:04 +03:00
Hakan SevenandClaude Opus 4.8 c3e86d08cc feat(snap): OTRACK ortho lock + edge-extension tracking (#218, #219)
#218: with Ortho on and a base point the axis is now a hard lock — a lone
tracking ray can't pull the cursor off it. Only a tracking ray crossing
the ortho axis locks (the corner-finding case); otherwise the ortho
constraint applies as the fallback, so Ortho reliably locks when OTRACK
is also on.

#219: an acquired corner now contributes an alignment ray along its own
edge direction(s) (captured once at acquisition), so OTRACK follows a
segment's extension, not just the ortho/polar axes. Acquisition is also
captured on leave — a perfectly still cursor emits no move events, so
"pause on a corner, then drag" now reliably acquires it. The acquired
marker projects through the active pane's camera + rect (canvas offset
included) instead of vanishing, and the active alignment path is drawn
as a full-length dashed guide through the corner.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 20:34:20 +03:00
Hakan SevenandClaude Opus 4.8 0322128f48 fix(snap): extension guides stay visible at extended intersections (#247, #259)
Extension osnap now offers a ray off every polyline vertex, not just the
first and last, and snaps to where two segments' extension lines would
cross. That crossing is emitted as an Intersection so it outranks the
per-segment extension feet on their own lines. When it wins, both
contributing extensions are re-detected — off-ray distance measured in
screen space so the tolerance holds at UTM coordinates — and the overlay
draws a dashed guide from each endpoint through the crossing, so both
extension paths remain visible instead of vanishing when caught.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 17:46:08 +03:00
Hakan SevenandClaude Opus 4.8 f4fffa1359 chore(release): v0.7.3
Co-Authored-By: Claude Opus 4.8 <[email protected]>
v0.7.3
2026-07-03 16:55:43 +03:00
Hakan SevenandClaude Opus 4.8 726984e3a5 feat(properties): show Thickness in the General group
Thickness (DXF 39) is a General-group property in the palette, but it is
stored per-entity, so it was previously shown (inconsistently) only in the
Geometry section of a few entities. Surface it in General for every entity
type that carries an extrusion thickness (arc, circle, line, lwpolyline,
point, polyface mesh, 2D polyline, shape, 2D solid, text) via
entity_thickness / set_entity_thickness, routed through apply_common_prop.

Removes the now-duplicate Thickness rows (and dead apply arms) from the
arc/circle/shape/solid Geometry sections so it is never shown twice.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:51:05 +03:00
Hakan SevenandClaude Opus 4.8 ab0a4f790c fix(insert): place block attributes by their ATTDEF geometry, follow the block (#255)
Inserting a block piled every attribute at the origin: each AttributeEntity
was built from just tag + value with default geometry. Carry the full
AttributeDefinition through the attreq flow and build attributes with
AttributeEntity::from_definition, then apply the block's insertion transform
so each lands in WCS at its proper position / alignment / rotation.

Attributes are stored in world space, so moving the block must move them
too. The MOVE command already did (acadrust translate_insert); apply_grip
(grip-drag) and the Position X/Y/Z property edit now translate the
attributes by the same world delta so they no longer stay behind.

Rotate/scale/mirror of a block still don't carry the attributes — acadrust's
transform_insert has no attribute pass — tracked separately.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:47:39 +03:00
Hakan SevenandClaude Opus 4.8 818638d3aa feat(ui): bulk layer on/off, lock, freeze, transparency + Ctrl+A (#236)
The Layer Manager already applied colour/linetype/lineweight in bulk;
extend that to the rest:

- Visibility, lock and freeze toggles now act on every selected layer, not
  just the clicked row. The clicked row's new state becomes the target for
  all. Transparency edits apply to the whole selection too.
- Clicking a linetype/lineweight combo on a row that is already part of a
  multi-selection no longer collapses the selection (the whole row is one
  mouse_area whose plain-click select would otherwise reset it), so those
  edits stay bulk.
- Ctrl/Cmd+A selects every layer row while the Layer Manager is open, and
  all drawing objects otherwise.

Two now-unused Scene layer-toggle helpers are removed (the handlers set the
flags directly and bump once).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:06:10 +03:00
Hakan SevenandClaude Opus 4.8 5e4e7c1020 fix(commands): ADDSELECTED keeps CLAYER, adopts dimension style (#239)
Two follow-ups to ADDSELECTED:

- It no longer permanently changes the current layer/colour/linetype/
  lineweight. The template's properties are captured as the drawing
  defaults only while the launched draw command runs; a snapshot taken in
  cmd_add_selected is restored the moment that command ends (central hook
  in apply_cmd_result) or is interrupted by another command.
- Dimensions are supported: add_selected_verb maps each DimensionType to
  its DIM* command, and the template's dimension style is adopted as the
  current DIMSTYLE (restored afterwards). commit_entity_handle now stamps
  every new dimension with the current DIMSTYLE — the DIM commands left the
  default "Standard" on the entity, so a normal dimension also picks up
  the active style now.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 16:05:54 +03:00
Hakan SevenandClaude Opus 4.8 f9b6834ad9 feat(export): exact ACIS geometry for planar solids on save
OCS models 3-D solids with truck, but extrude/boolean results carried no ACIS
body, so other CAD apps dropped them on open. Add an exact planar exporter and
run it at save time.

- scene::convert::acis_export::planar_solid_to_sat walks a truck Solid whose
  faces are all planes bounded by straight edges into a vertex/face-ring mesh
  and builds an exact ACIS body via acadrust's build_planar_body. Curved
  faces/edges return None (left for the NURBS path). A signed-volume check
  flips the winding if the shell comes out inside-out.
- OpenCADStudio::sync_truck_solids_to_acis fills in ACIS geometry for every
  cached truck solid that still has none, just before a Save / Save-As, so the
  written DWG/DXF carries real modeler geometry.
- Bump acadrust to 9142a50 (build_planar_body + classic-ACIS AcDs roundtrip fix).

Booleans (already cached as truck solids) now export exact geometry. Extruded
polygons will too once the EXTRUDE command caches its truck solid (a separate
change kept out of this commit because command_driver.rs holds unrelated
in-progress edits).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 15:43:37 +03:00
Hakan SevenandClaude Opus 4.8 79a5ac6fed fix(scene): correct MIRROR of TEXT under MIRRTEXT (commit + preview)
Root cause: the glyph tessellator advanced the pen by advance*width_factor
while the transform already scales the pen position by the width factor, so
a backward (negative width factor) run kept its word order and grew on the
same side of the anchor instead of mirroring across it. Advance in unscaled
glyph units to match text_local_bounds; the transform applies the factor once.

On top of that:
- The renderer honours the entity's own text-generation flags (DXF 71: bit 2
  backward / bit 4 upside-down) XOR the text style's flags, and signs the
  horizontal anchor by the width factor so Center/Right backward text mirrors
  about its anchor.
- MIRRTEXT on: a MIRROR toggles both group-71 bits so the reflected rotation
  renders a true glyph mirror (backward-upright for a vertical axis, flipped
  for a horizontal one).
- MIRRTEXT off: the text flips left<->right and lands mirror-symmetric to the
  source (right-reading) instead of hugging the axis.
- The MIRROR preview re-derives text ghosts to match the commit; mirrored()
  now reflects the double-single residual too.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 12:42:55 +03:00