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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
A dimension's *D block is baked in absolute WCS, so the DWG group-12
insertion point must be the origin. OCS's dimension commands seeded it
with the text anchor, so a reader that positions the block by that point
(BricsCAD / ODA) drew the dimension shifted by the offset, while OCS —
which renders the block in place — showed it correctly.
Reset base.insertion_point to the origin in bake_dimension_blocks, where
the absolute-WCS block is created. Only dimensions whose block OCS bakes
are touched; blocks preserved from other applications (baseline/continue
dimensions with a legitimate non-zero clone insertion) are left alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a MANUAL_SUPPORTERS list in patreon.rs for donations that arrive
outside Patreon; both the native and web boot paths merge it with the
fetched patrons and sort the combined list by amount (highest first,
then alphabetically). This also sorts the web supporters list, which
previously arrived unordered, and keeps hand-maintained supporters
visible when the Patreon fetch returns nothing (offline / no token).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commands now expose their step options as clickable buttons rendered
inline beside the command-line prompt, so options need not be typed.
Adds a CmdOption/options() API to CadCommand: each command returns the
options for its current step; the command line renders them as
uppercase buttons and a click feeds the keyword to the command exactly
as if typed (an empty keyword submits like Enter).
Construction commands become "central" — CIRCLE offers [3P/2P/TTR/TTT/
DIAMETER], ELLIPSE [ARC/AXIS], ARC [SCE/SCA/SEA/SER/CSA/3P], RECTANGLE
[ROTATION/CENTER], POLYGON [CIRCUMSCRIBED/EDGE] — each keyword hands off
to the existing dedicated variant command via CmdResult::Dispatch,
instead of the variants only being reachable as isolated commands.
Migrates the existing bracketed-option prompts (PLINE, PEDIT, SPLINE,
SCALE, FILLET, CHAMFER, HATCH, GRADIENT, SOLID, POINT, RAY, XLINE, TRIM,
EXTEND) to the same button API and strips the option text from the
prompt strings, keeping value/coordinate hints. PEDIT's Width option
now enters a value sub-step so the button acts on click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ABOUT command and its version-reporting window already exist, but
the ToolDef sat dead-code in the draw module with no ribbon entry, so
the only way in was typing ABOUT. Move it to the manage module and give
it an Application group on the Manage tab, as a discoverable fallback
for version diagnostics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports PR #301 (by Kevin Griffin) to the SDF text renderer. Block-nested MTEXT
with inline colour codes (\C1;, \c…) collapsed to a single colour: the block
cache applies ByBlock / layer-0 colour inheritance per entity, so at emit time
every segment's colour was replaced with the inherited colour — including
segments carrying an explicit inline override.
- block_cache tessellate_sub_local: gate ByBlock / layer-0 colour inheritance
on `wire_on_base_color` (a wire whose colour differs from the entity base
carries an explicit override and keeps it) — #301's fix, still correct for
the remaining stroke wires.
- block_cache emit_wire: the per-vertex analogue for SDF text — a glyph whose
colour equals the wire's base inherits the resolved colour; a glyph with an
inline \C / \c override keeps it. (Previously every block glyph was recoloured
with final_color, collapsing colour-split MTEXT.)
- tests/text_font_rendering.rs: restore the block colour-split regression test
(it had stopped compiling on main when expand_insert gained the InheritStyle
param) and adapt it — and drawable_point_count — to SDF text_verts, since
block MTEXT now renders as glyph quads, not colour-split stroke wires.
\C (ACI) / \c (true-colour) are parsed by acadrust (mtext_format
SpanProperties.color) → OCS InlineColor → the glyph's per-vertex colour.
Co-Authored-By: Kevin Griffin <117586586+KevinGriffin-new@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
External contribution by sLuCHa. A dimension draws from its baked *D block
(baked in absolute WCS); the in-drawing COPY re-points block_name to a
transformed copy (#161) but clipboard PASTE did not, so a pasted dimension
rendered at the source location (same drawing) or was missing (cross drawing).
Copy now snapshots each copied dimension's *D block into the clipboard
(dim_blocks); paste builds a fresh copy transformed by the paste offset and
re-points block_name — same- and cross-drawing alike, no orphan blocks.
Refactors out shared define_transformed_block / snapshot_block helpers
(behaviour-preserving).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>