DensitySwap now takes a third, narrowest variant: the compact panels
flex-wrapped (via WrapFlow) across as many rows as the width needs. The
tool area therefore degrades full -> compact -> wrapped as the window
shrinks. build_tool_row becomes build_tool_groups, returning fixed-height
panel elements so they lay out identically in a row or a wrap flow.
The tool bar is no longer a fixed TOOL_BAR_H; DensitySwap reports its
measured height into a cell and the ribbon dropdown overlays anchor at
tab_bar_height() + tool_bar_height() so they track the taller bar.
Second stage of the adaptive tool area (shrink -> wrap -> collapse).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a DensitySwap widget that renders the widest variant of the tool
area that fits the current width. The ribbon builds two variants via the
new build_tool_row(compact) helper: full-size panels, and a compact one
where LargeTool/LargeDropdown collapse to small icon columns (the
layer/properties/style combos always stay full). render_small now also
matches the Large* variants so they can be drawn small.
First stage of the adaptive tool area (shrink -> wrap -> collapse).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Start tab, drawing tabs and the "+" button now live in a single
WrapFlow instead of a fixed-height Row, so they spill onto lower rows
when there are more tabs than the width can hold. Drops the fixed 30px
height and the trailing Fill spacer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add shared adaptive-bar widgets in src/ui/wrap_bar.rs:
- WrapBar: two-block bar (lead + optional Fill middle + trail). Fits on
one row, otherwise the trailing block drops below; with no middle both
blocks flex-wrap in their own row bands and stack so a wrapped lead item
never shares a row with a trailing item.
- WrapFlow: CSS-style flex-wrap row that spreads its items across as many
rows as the width needs.
Ribbon tab row: quick-access buttons and undo/redo are merged into one
WrapFlow; the tabs are a second WrapFlow. Dropdown overlays anchor to the
measured (possibly taller) bar height instead of a hardcoded 28px.
Status bar: the hamburger menu plus the Model/layout tabs form one
WrapFlow; the status pills form another. The layout-tab scroll arrows and
horizontal scrollable are removed in favour of wrapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Position the snap popup so the anchor point (by the Osnap button) is its
bottom-LEFT corner, opening up and to the right of the button rather than
extending left from its right edge. Pad by the anchor minus the panel
width, clamped so a narrow bar can't push it off-screen.
Refs #248
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The snap popup used a hardcoded 4 px right offset, pinning it to the
window's right edge — but the Osnap button sits well to the left, with up
to ten pills after it (Space, Scale, Units, Transparency, Isolate, …).
Anchor it by the summed width of the pills that follow Osnap, honouring
the user's per-pill visibility. iced gives the view no real layout
metrics, so the pill widths are estimated (icon pills fixed, labelled
pills approximate); the popup now opens by the button instead of the
corner.
Closes#248
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two refinements to Parallel snap. The acquired reference now keeps a
point on the line and draws a ∥ marker there, so the user sees which line
the parallel is measured from. And dwelling on that same reference line
again removes it (toggle), matching how OTRACK tracking points clear;
dwelling on a different line switches the reference. Same-line detection
uses parallel direction plus perpendicular screen distance, so it holds
across zoom.
Refs #277
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parallel snap did nothing — the type was in the osnap set, priority and
marker, but there was no logic behind it. Implement it: hovering a line
(or polyline segment) for a short dwell acquires its direction as a
reference; then while drawing from the last point, when the cursor runs
parallel to that reference the point locks onto the line through the last
point parallel to it, showing the ∥ marker and the alignment guide.
Curves (circle/arc/ellipse) are ignored — parallel to a curve is
undefined. Works with only the Parallel object snap on, independent of
OTRACK; the reference clears with the command's tracking state.
Closes#277
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While a LINE's first point is a deferred circle tangent, the rubber band
stuck at the provisional pick point. Recompute the start on each mouse
move as the tangent from the cursor to that circle (keeping the side the
user first picked), so the tangent point on the first circle slides as
the cursor moves toward the second point.
Refs #274
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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.
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.
`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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`full_circle` from `angular_range` is only read, never reassigned — clears
the unused-mut warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
#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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Typing a bare system-variable name (MIRRTEXT, FILLMODE, LTSCALE, …) now
echoes the current value and prompts "Enter new value for <NAME> <cur>:",
capturing the next command-line entry as the new value — Enter keeps the
current one, Escape cancels. The single-line "MIRRTEXT 1" form still works.
Mirrors the awaiting_vports pattern: a pending_setvar field on the app,
consumed at the top of on_command_submit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On German/European layouts the numpad decimal key emits a comma, which the
coordinate parser rejects. Match the physical NumpadDecimal / NumpadComma
key and emit '.' regardless of the OS layout. (#201)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the OCS-logo button and the dropdown menu it opened; the top strip
now shows direct New / Open / Save / Save As / Print buttons ahead of the
undo/redo controls. Drop the HELP and OPTIONS commands (HELP dumped text to
the command line, OPTIONS aliased the drafting-settings popup).
The app-menu overlay is gone, so its rendering code, MENU_ITEMS, four dead
messages, and the LOGO_RED / GEAR / HELP assets are removed. The recents
list it held is the only survivor — moved into OpenCADStudio.recent_files
via a new app/recent.rs, backing the Start page's Recent Documents panel.
Quick-access icons are redrawn in a cleaner style (file-plus, folder-open,
device-floppy, file-export, printer) and tinted light so they read on the
dark strip; FOLDER / DOC keep their old glyphs for the file browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>