Ortho and the running object-snap set are stored in the drawing header
($ORTHOMODE / $OSMODE) since they were bound to the file, so persisting them
app-globally in settings.txt too was redundant. Remove the ortho, osnap and
snap_modes keys (and the now-dead modes_from / snap_id / snap_from_id helpers)
from UserSettings, and stop reading/writing them in apply_settings /
current_settings.
No boot behavior change: Snapper::new already defaults to the same running-snap
set (Endpoint, Midpoint, Center, Node, Quadrant, Intersection, Nearest) with
the master off, and Ortho defaults to false — exactly what settings.txt used to
seed. Old settings.txt files keep working; the stale keys are simply ignored.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Coords pill always showed live absolute X,Y,Z, ignoring the drawing's
$COORDS (coords_mode) sysvar. Drive the readout from it:
- 0 static — the last picked point; the readout freezes between picks.
- 1 live — continuous absolute X,Y,Z (previous behavior, default).
- 2 polar — distance<angle relative to the last point while a command is
prompting for a point; absolute otherwise.
The cursor and last point are reported in the active UCS, matching the rest of
the readout. Clicking the pill cycles the mode 0→1→2→0 (CycleCoordsMode), marks
the tab dirty and echoes it; SETVAR COORDS still works. $COORDS round-trips per
drawing via acadrust's coords_mode.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The status bar showed all 17 pills out of the box. Hide the informational /
niche ones by default so the bar stays uncluttered — Coordinates, Show
Lineweight, Dynamic Input, Model/Paper Space, Drawing Units, Show
Transparency, Selection Cycling and Viewport Count. The user turns any of them
back on from the customization (gear) menu, which persists to statusbar.txt.
The main drafting toggles (Ortho, Polar, Otrack, Osnap, …) stay visible.
An existing statusbar.txt (even empty = user showed everything) is
authoritative; only a fresh install with no config gets the new defaults.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
When the viewport got too narrow the top-left render-mode bar and the
top-right ViewCube overlapped. Hide them adaptively, measuring the render
bar's real width rather than guessing:
- Wrap the render bar in DensitySwap, which measures its natural width every
frame and swaps it for an empty spacer once it no longer fits the tile. Add
report_width0 so it publishes that measured width to render_bar_w.
- Compute viewcube_visible from that measured width: the cube shows only while
bar_w + gap + cube region fits. This one flag drives the ViewCube nav/UCS
widgets, the hover hit-test, and — threaded through build_viewports /
build_viewport_for_pane / viewport_data_for — the GPU cube itself, which
previously ignored the pane flag (show_viewcube was inst.active only), so
the cube stayed while its surrounding widgets vanished.
Result: the ViewCube gives way first, then the render bar once it too has no
room — no overlap, sized to the bar's real width (which varies with the
render-mode label, split buttons and tile count).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OFFSET rejected XLINE targets: on_entity_pick only accepted
Line/Circle/Arc/LwPolyline, compute_offset had no XLine arm, and
entity_wire_pts returned no points for it (so hover/preview/through-distance
failed too).
Add offset_xline (parallel infinite line: same direction, base point shifted
perpendicular toward the pick side), a compute_offset arm, and an
entity_wire_pts case (a long segment standing in for the infinite line during
hit-test and preview; the committed XLine renders true). Also widen the pick
filter to every type compute_offset supports — this additionally reaches
Ellipse and Spline, whose offset functions already existed but weren't
selectable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
STRETCH picks its crossing window as two corner points. After the first
corner set last_point, Ortho/Polar constrained the opposite corner to an axis
through it, collapsing the window to a line and making selection impossible.
Add CadCommand::window_corner_pick(); STRETCH returns true for its two window
steps, and the viewport skips the ortho/polar (and OTRACK) lock for that point
in both the preview and commit paths — the corners are now free.
Also render the window like a normal box selection instead of a bare green
outline: add SelectionState::preview_box (a hit-test-free marquee), draw it
with the shared draw_marquee helper (green crossing fill + stroke), and drive
it from the command's new window_first_corner(). Cleared on command end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
seg_intersect_xy found where two segments' XY projections cross and returned
the point at z=0, ignoring height. Two lines at different Z whose plans cross
therefore fired a real Intersection snap when they only *appear* to cross in
top view — that case belongs to Apparent Intersection (view-space).
Rename to seg_intersect_3d and require the segments to be at the same height
at the crossing: compute Z on each at its crossing parameter and reject when
they differ (tolerance scaled to coordinate magnitude). A genuine crossing
now snaps at its true height instead of z=0; a plan-only crossing is left to
Apparent Intersection.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
extension_bases_screen only skipped closed curves (circle/ellipse, keyed on
the Quadrant hint) when finding which endpoints' extension rays a snapped
point lies on. An arc is an open curve with no Quadrant hint, so its
tessellation chords were scanned; one straight chord-extension grazes a
nearby line-line intersection (beyond the arc endpoint, within 4px), drawing
a spurious dashed extension guide radiating from the arc.
Skip every round curve by the Center hint (which arcs, circles and ellipses
all carry and straight lines never do) — the same signal nearest_segment
uses. The intersection snap point itself was already correct; only the stray
guide line is removed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Command aliases (L→LINE, CC→COPYCLIP) were scattered across dispatch match
arms and CommandRegistration name arrays. Move them all into a single
user-editable file:
- assets/ocad.pgp: shipped default alias table (ALIAS,*COMMAND format),
embedded and copied to config/ocad.pgp on first launch; only that user
copy is read after. AA→AREA default closes#286.
- resolve_alias() rewrites the leading command token at the top of
dispatch_command_inner, so aliases resolve for every path (families,
plugins, Repeat menu). Args after the first space are untouched.
- Autocomplete hides alias keys and, when the whole input is an alias,
surfaces its target command as the top (highlighted) suggestion so the
highlight matches what Enter runs (AA→AREA, L→LINE).
- ALIASEDIT opens a new editor modal (add/edit/delete rows) with an Apply
button; Apply persists to ocad.pgp, closing discards unapplied edits.
- Strip alias tokens from all 8 dispatch match-arm files and 71 module
CommandRegistration arrays, keeping canonical/synonym/distinct-command
names. ocad.pgp is now the sole source: deleting or remapping an alias
there takes effect. CUI stays on the keyboard-shortcut panel.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
COPYCLIP/CUTCLIP used the mean of each entity's bounding-box centre as the
paste anchor. Standard clipboard behaviour puts the lower-left corner of the
box enclosing all copied items under the cursor. Union each entity's bbox min
corner instead, and rename clipboard_centroid -> clipboard_base (COPYBASE also
stores a picked point there, so "centroid" was inaccurate).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Command previews (MOVE, COPY, ROTATE, SCALE, STRETCH, MIRROR, ARRAY,
PASTE, INSERT) build ghost WireModels via WireModel::translated/rotated/
scaled/stretched/mirrored, but those helpers moved only `points`, never
`text_verts`, and the ghosts' glyphs were never uploaded — so text
vanished (or froze at its source) during the preview, same root cause as
the grip-drag case.
Transform text_verts alongside points in all five helpers (shared
map_text_verts, reconstructing + re-splitting the double-single world
position), and gather preview_wires' glyphs into the per-frame
preview-text buffer in the render path. This covers every command
preview automatically. The grip-drag else branch no longer sets
preview_text explicitly (the render gather handles it); the grip-slide
fast path keeps its own scene.preview_text channel (empty preview_wires,
so no double-draw).
MIRROR honours MIRRTEXT: on -> true glyph mirror via mirrored(); off ->
readable relocation via translated().
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Dragging text by a grip hid it until release. The dragged entity is
hidden from the base wire set and shown as a preview overlay, but its
SDF glyph quads (text_verts) were never uploaded for the preview, so
the text vanished mid-drag.
Add a per-frame preview_text GPU buffer (sibling to preview_wires). A
rigid whole-entity move of pure text (TEXT/MTEXT, Square insertion grip)
slides the drag-start glyphs by the drag delta instead of re-shaping the
run every move; reshape grips (MTEXT width), dimensions and wire
entities re-tessellate and feed the same buffer.
Also fold preview_text into render_signature: a pure-text slide leaves
preview_wires empty, so without it the scene-render cache froze the
dragged text at its first frame (grip moved but text stayed, then
jumped to place on release).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Three defects in ACIS curved-wall (cone-surface) tessellation, all seen
in a Villa Savoye model:
- Balloon: sample_ellipse_arc ignored the edge's own sense relative to
its curve (distinct from the coedge/loop sense). A reversed edge winds
the ellipse about the opposite normal, so evaluating P(t) with the
stored normal mirrors a short boundary arc across the major axis to the
far side of the circle. That poisoned the face's angular-span estimate
and swept the whole 2pi revolution, drawing a curved wall as a full
cylinder of the surface radius. Honour edge.sense() (negate the sin
term when reversed) so the arc traces its true vertices.
- Seam: the fallback sampler's angular_range used raw min/max of atan2
angles, so an arc straddling the +/-pi seam read as a 2pi span (false
full circle). Derive the arc from the largest angular gap instead.
- Gaps: truck's rsweep/triangulation degenerates to zero triangles on
some short-arc cylinders (profile through the local origin), leaving a
see-through hole while the rest of the solid renders. Fall back to the
bespoke parametric sampler for just that face, into the same body-local
buffers. Also scale tess_cone_face's angular divisions by the arc span
so a short arc isn't tessellated as densely as a full rim.
Verified against the model: ballooned walls become wall-sized, and a
sweep of all 528 solids shows no empty faces and no triangle explosion.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
on_enter/on_escape returned CmdResult::CommitEntity, which commits the
spline but keeps the command armed with its accumulated points — the
cursor stayed in spline mode and the next click began a new spline,
trapping the user in the command loop. Return CommitAndExit so Enter/Esc
finalize the spline and return to the selection state.
The in-progress preview drew only the straight control polygon. Sample
the actual degree-3 clamped B-spline (same control points + knot vector
build() commits) so the rubber-band traces the exact final curve.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Opening a DXF whose active viewport is stored uppercase (*ACTIVE),
editing and re-saving produced a file with two full-window model-space
viewports — the file's own *ACTIVE plus a fresh *Active we add on save.
The reserved viewport name is case-insensitive, so the two didn't
collapse and other CAD apps drew them one on top of the other, which
read as the copied objects being linked / "reacting as one".
save_model_tiles_to_vports dropped existing active viewports with a
case-sensitive `name != "*Active"` filter, so an uppercase *ACTIVE
survived and sat next to the new entry. Route every *Active comparison
(save filter + camera/tile restore) through a case-insensitive
is_active_vport_name helper so the file's record is recognised and
replaced instead of duplicated.
Also bumps acadrust to #99f75fe (merged PR #1): the ASCII DXF writer now
emits shortest round-trippable floats instead of the full 16-digit
expansion, halving the round-tripped file size.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
units_btn and scale_popup_btn were byte-identical apart from the message
they emit. Fold them into a single popup_pill(label, open, msg) helper so
every popup-opening pill goes through shared chrome, matching toggle_pill
(icon toggles) and split_pill (toggle + caret dropdown).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The polar-tracking pill only right-click-cycled 15→30→45→90. Add an
AutoCAD-style angle picker opened from a caret on the pill: presets
90/45/30/22.5/18/15/10/5/1 plus a free-text field for any custom angle,
mirroring the existing units / scale status-bar popups. The chosen angle
persists like the other user settings.
Factor the shared chrome into one `split_pill(main, caret_msg, active,
open)` helper (+ `pill_text_color`) so the OSNAP and POLAR pills stop
hand-rolling the same outer border, background, caret and sizing — each
pill now only supplies its own main region. The caret is sized and
aligned to sit inside the single pill border on both.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Continue option committed a single tangent-continuing arc and left
the command. Now each picked end point commits its arc and re-anchors at
that arc's free end, so the user keeps drawing tangent-continuing arcs
until ending the run with Enter/Esc. A degenerate (collinear) pick stays
active instead of cancelling.
Because arcs now commit via CmdResult::CommitEntity (repeating) rather
than CommitAndExit, advance the ARC_CONT continuation anchor in the
CommitEntity arm too — guarded to line/arc so non-continuable repeats
(point/ray/spline) leave it untouched. This also lets a LINE run feed a
following ARC Continue.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TEXTFILL 1 (default) fills TrueType text; TEXTFILL 0 draws it hollow
(outline only). Settable directly or via SETVAR, 0/1 or ON/OFF. SHX/LFF
stroke fonts are unaffected.
Reuses the existing stroke-SDF path: bake_glyph treats a TTF glyph as a
stroke (outline) field instead of a signed fill field when TEXTFILL is
off. Toggling resets the glyph atlas and re-tessellates text so quads
pick up the re-baked tiles. Persisted in user settings.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run EXTRIM, pick a cutting boundary (line/arc/circle/ellipse/spline/
polyline), then click a side: every object crossing the boundary is
trimmed on that side and objects wholly on it are erased. Side is decided
by a parity crossing count (a segment to the pick point crossing the
boundary an even number of times is on the pick side), reusing the trim
intersection machinery; line boundaries extend to infinity. Line/arc/
circle are trimmed exactly; polyline/ellipse/spline via dense sampling.
Live preview while choosing the side: boundary in yellow, surviving parts
in blue, the removed parts in red.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Space now acts like Enter in the command line so commands advance
token-by-token (CAD convention). The whole value is handed to the submit
path, which tokenises multi-token lines, so a pasted or API-fed
`LINE 0,0 10,10` runs each space as a step separator instead of stopping
at the first token. A leading `>` switches to literal-space mode (for a
text string, a path, etc.) and is stripped on submit. All command-line
entry is shown uppercase.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Run QUICKPRINT (or QP), select objects, then Enter: the selection's
bounding box becomes the plot window and a PDF is written next to the
drawing (<name>_<timestamp>.pdf) using the active page setup, with no
dialog. Model space.
Implemented as a selection-gathering CadCommand (src/modules/view/
quick_print.rs) returning CmdResult::QuickPrint(handles); the host
(on_quick_print_handles) unions the picked entities' AABBs, sets the
plot window, and reuses the tested clipped window-export path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Restructure the plot dialog into a style-manager-style master/detail: a
left list of page setups plus a right settings editor. The list shows
<none> (defaults + PDF) and <previous> (the settings the dialog opened
with), then the drawing's layouts (*Model*, *Layout1*, …) and its
standalone named page setups.
- Named page setups live in the ACAD_PLOTSETTINGS dictionary
(src/scene/page_setup.rs: names/get/save/delete/rename, creating the
dictionary if absent); DWG round-trip verified.
- Toolbar New / Copy / Delete | Set current / Preview / Print. Selecting
a row loads it into the editor; Set current writes to the active
layout. Copy duplicates a layout or a setup into a new named setup;
Delete/Rename apply only to named setups.
- Scene::plot_settings_for(name) generalizes effective_plot_settings so
any layout's settings can be loaded; on open the settings are
snapshotted for <previous>.
- Default selection: model space → <none>; a layout → its own setup.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The batched path is the sole hatch renderer and has no family cap, yet it
was named hatch_batched_gpu while the old capped per-primitive renderer
kept the hatch_gpu / MAX_FAMILIES name — which reads as if visible hatch
fills are truncated at 16 families (they are not). Swap the names so the
active renderer is hatch_gpu.rs / hatch.wgsl and the legacy one, now used
only for wipeout masks, is wipeout_gpu.rs / wipeout.wgsl. MAX_FAMILIES now
lives only in the wipeout module. Pure rename; no behaviour change.
Refs #323.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Snap and hover hit-testing scanned the whole model wire set every cursor move; a heavily block-instanced drawing explodes to millions of wires, so this stalled for over a second per move. Add a cached spatial grid (wire_grid) keyed on the geometry epoch and query only the cursor neighbourhood via Scene::hit_test_wires_near. Gate the O(in-range segments squared) intersection passes on the in-range POINT count, not the wire count — a handful of curved wires, each hundreds of tessellated points, blew the quadratic up while the wire count looked modest. Give zero-size entities away from the origin (POINTs) a real cullable AABB, and fall back to a block sub-wire's own world points when its entity bounding box is degenerate.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Gradient Type (One/Two color), Color 1 and Color 2 editable via a generic field-keyed colour picker path. Background colour editable and stored as HATCHBACKGROUNDCOLOR extended data (group 1071 true colour), rendered in OCS as a solid backdrop drawn behind the pattern. Hide Origin X/Y for gradients (pattern-only concept) and drop the redundant gradient rows from the pattern branch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Merge duplicate Type into pattern_type; Type/Associative/Island detection as dropdown/toggle; Angle/Scale/Spacing/Origin X-Y editable; Color via picker. Reorder Pattern group, drop redundant Color/Transparency/Layer override and the Misc section. Wire the pattern origin (per-line base_point) into the render (x0/y0 relative to world_origin) so it affects dashed/offset patterns; add a circle origin grip at the hatch centroid (new GripShape::Circle).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single Geometry section: Start/Center/End points, Major/Minor radius (editable; minor derives ratio), Radius ratio, Start/End angle editable. Major/Minor axis vectors, parameters, Length and Normal kept read-only at the end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Arc ribbon default + icon + dropdown order set to 3-Point. New ARC_CONT: tangent-continuing arc from the last line/arc endpoint, pick end only; Ctrl flips direction. Exit anchor recorded on commit (disambiguated by last pick) so chaining continues from the correct end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Circle: Diameter/Circumference/Area now editable (each derives radius); Normal X/Y/Z read-only. Arc: reorder to Start/Center/End first; Normal X/Y/Z read-only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
on_preview_wires clones and re-uploads the whole ghost on every cursor
move, so its cost scales with the wire count, not just the point count.
A whole-drawing paste flooded the event loop and locked up placement.
Gate on the wire count as well as the point count, and fall back to a
single bounding-box outline above the budget. The budget still shows the
full-geometry ghost for typical pastes (up to ~20k wires); only a
whole-drawing paste rubber-bands a box.
COPYCLIP/CUTCLIP computed the clipboard anchor by tessellating every
selected entity and averaging all wire vertices — O(total geometry),
~250 ms when copying a whole drawing. The anchor only fixes which point
of the selection sits under the cursor at paste, so the mean of each
entity's bounding-box centre is an equally valid, far cheaper origin.
Rebuilding the Properties panel aggregated shared/varying values across
the whole selection: O(n) per property row, plus an O(n^2) group filter
(group.handles.contains is a linear scan per selected entity). Selecting
or pasting tens of thousands of objects stalled the rebuild for seconds.
Above a cap, show a lightweight '<n> objects selected' panel instead of
the per-entity aggregation; bulk layer/colour/lineweight edits still go
through the ribbon.
Object snap ran several full scans of the drawing on every cursor move,
and the Intersection / Apparent-Intersection passes compare pairs of
in-range segments — O(k^2) in the number of wires whose bounding box
overlaps the aperture. Zoomed out over a large drawing (or with long
lines whose AABB spans the view) that pairwise scan grows to the whole
document and freezes point picking, so MOVE/COPY/paste placement hung.
Count the in-range wires once and gate only the pairwise passes on it:
above the cap they are skipped (you cannot resolve a single intersection
when the aperture spans thousands of segments anyway), while every
single-wire snap (endpoint, midpoint, centre, perpendicular, nearest)
keeps working — so point picking still snaps to corners at any zoom.
Dragging a model-tile pane_grid divider changed each pane's pixel size
every frame, and `ensure_depth_texture` recreated the depth + 4×MSAA +
resolve textures on any size delta. On Windows Firefox (WebGL2 → ANGLE →
D3D11) this per-frame texture realloc hangs the D3D driver — the same
hang class as the native DX12 lock in #120. Linux Firefox (native GL) and
Chromium/Edge tolerate the churn, which is why the freeze is specific to
Windows Firefox.
Round the depth/MSAA/resolve allocation up to a 128px grid and only
recreate when the rounded size changes, so a live drag reallocates at
most once per 128px instead of every frame. Geometry passes still render
at the real size into the [0,0,render] corner of the larger texture, and
the blit UV is scaled by render/alloc so it samples exactly the filled
region — visually a no-op on every platform.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Current Vertex row is now a ◀ / ▶ stepper (new PropValue::Stepper)
showing "i / N". Stepping it moves the panel's focus through the
polyline's vertices — Vertex X/Y and the per-vertex start/end widths show
and edit the focused vertex instead of always the first — and the focused
vertex's grip is drawn hot (filled) in the drawing so it's visible while
navigating. Focus wraps around and resets to the first vertex when the
selection changes. Adds Area and Length to the LwPolyline Geometry group
(Polyline2D already had them). Applies to LwPolyline and Polyline2D.
The focused vertex is threaded to the per-entity property builder/editor
through a dispatch thread-local (like the curve-tolerance override), so
the PropertyEditable trait signatures are unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Copying and pasting a whole drawing froze the app. Three synchronous,
main-thread hot spots scaled with the full document rather than the
selection:
- Copy centroid did a linear document scan per selected handle
(wire_models_for / mirror_preview_parts), making copy O(H*N). Use the
document's O(1) handle index instead.
- The paste ghost translated every point of every clipboard wire on each
mouse move — O(points) per frame, which stalls placement of a huge
selection. Above a point budget, rubber-band a single bounding-box
outline instead (O(1) per frame).
- finalize_paste re-tessellated every solid in the document on each
paste. add_entity already tessellates pasted solids, so switch to the
incremental populate_missing_meshes_from_document.
Leaves just Material in the group. The shadow_flags entity field is
untouched (preserved on save); only the read-only panel row is removed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Reorder the General group so Color precedes Layer. Remove the "Invisible"
row: the entity visibility flag (DXF code 60) is driven by dynamic-block
visibility states, not a manual per-object toggle — setting it from the
panel hid the object with no easy way to reselect it. The flag itself is
unchanged (dynamic blocks still set it, render/save still honour it, and
it stays settable programmatically).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Draw the polyline width bands with the stored width offset to each side
of the centreline rather than half of it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Wide LwPolyline / Polyline2D fills only drew for entities directly in the
layout — the model-space loop skips block-internal ones, and the
block-explode fill pass handled hatches but not wide polylines. So a
polyline with a width nested in a block drew at zero width (its
centreline came from the wire path, the band fill never materialized).
Extend the block-explode fill walk (exploded_insert_hatch_models) to emit
wide-polyline bands at world position, gated by a new block_has_wide_poly
scan mirroring block_has_hatch, with a shared push_block_wide_fills helper
that resolves block-child colour inheritance and applies XCLIP.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The batched hatch path pre-triangulated every boundary on the CPU
(PR#300) with no cap on the resulting vertex buffer. A hatch-dense
drawing (13k solid-filled hatches) built a ~6.75 GB `hatch_batched.vertex`
buffer and panicked in `Device::create_buffer` (device max 256 MB), after
a long stall spent triangulating on load.
Drop the CPU triangulation and always draw each hatch as its AABB quad
(6 verts) with `poly_test = 1`, letting the shader resolve inside/outside
per fragment against the uploaded boundary — the path the shader always
kept as a fallback. The vertex buffer is now 6 verts/hatch regardless of
boundary complexity, so no drawing can overflow it. The interaction-LOD
flicker PR#300 addressed no longer occurs (OCS_HATCH_LOD defaults off).
Removes the now-unused hatch_tess triangulation module.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The main GPU render holds the resident (un-culled, camera-independent)
model wire set from `model_tile_wires_arc`, which called
`wires_for_block_culled` with `view_aabb = None` — the branch that bypassed
the per-entity tessellation memo. So every geometry edit re-tessellated
the whole model, even though `mark_entity_dirty` already drops just the
changed entity from the memo.
Give the resident set its own per-entity memo (`resident_tess_memo`),
separate from the camera-dependent `tess_memo` so the culled path can't
thrash it on zoom. Its guard depends only on annotation scale and
background (the resident set has no view cull or zoom LOD), so it survives
pan/zoom and a single-entity edit re-tessellates just that entity, reusing
every other entity's cached wires — on a large drawing, editing one object
now re-tessellates one object instead of the whole model.
Every `bump_geometry_no_blocks` site was audited to confirm it drops the
edited handle via `mark_entity_dirty` (or adds a new entity — a natural
memo miss); `bump_geometry` still clears both memos on structural change.
`OCS_NO_RESIDENT_MEMO` reverts to full re-tessellation as a kill-switch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Dragging the minor-axis grip past the major length re-derived the
perpendicular from the just-swapped major axis on every mouse move, so
the major flipped 90° and the swap re-fired each frame — both axes grew
without bound once the minor reached the major.
Decide which axis the cursor is dragging by projection (not by a perp of
the current major), keep the non-dragged axis fixed, and store the longer
one as the major so the ratio stays <= 1. The swap is now stable across
the circle: the minor holds the old major length while the dragged axis
grows smoothly.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Once the first point of a segment is placed, OTRACK now offers an
alignment along the line joining that base point and an acquired
tracking point — the direction *between* them, not only the ortho/polar
axes or the corner's own edges. Acquire an existing corner while drawing
and the cursor locks onto the base→corner line, with typed-distance
measured from the base point.
Adds a base→corner ray per acquired point in otrack_snap, grouped with
that corner so it never self-intersects with the corner's other rays.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
OCS renders a dimension live from its definition points but exports a
baked *D block that other applications draw. An in-place edit (DIMTEDIT,
text override, DIMSPACE, any ReplaceEntity/ReplaceMany edit) changed the
dimension while leaving the old block, so the export kept the pre-edit
graphics — BricsCAD/ODA showed the stale dimension while OCS showed the
edit.
Add explode::invalidate_dim_block, which drops a dimension's baked *D
block (record + owned sub-entities, so no orphan blocks accumulate) and
clears block_name; bake_dimension_blocks then rebuilds a fresh block on
the next save. Call it from the in-place edit paths. The transform path
(MOVE/COPY/PASTE) keeps its own block in sync and is left untouched, so
dimensions loaded from other applications and not edited are preserved.
Co-Authored-By: Claude Opus 4.8 <[email protected]>