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]>
Picks up acadrust 716d4b4: DWG reader synthesizes the mandatory
*Model_Space/*Paper_Space block records when a file's BLOCK_HEADER for
them never materialised, so model-space geometry is no longer dropped on
save (file shrank + reopened blank).
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]>
The release profile's strip = true (added in #324) strips the wasm
target_features custom section. Without it, wasm-opt can't tell that
bulk-memory is enabled and rejects the module's memory.fill/copy ops
("operations require bulk memory"), failing the release web build — the
newer Rust toolchain now emits those ops by default. Override strip only
for the wasm target with -Cstrip=none, so native release binaries stay
stripped while the wasm keeps its feature section.
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]>
Pulls acadrust f1f4dbf, which makes the DXF reader parse hatch pattern
definition lines (and types group code 79 correctly) so custom/dashed
patterns load identically to the DWG instead of collapsing to flat
horizontal lines.
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]>
Pulls acadrust d32c9cf so DXF import reads RGB layer colours (code 420)
instead of collapsing every true-colour layer to Index(7)/white; ByLayer
entities then resolve to the correct colour.
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]>