Replace the raw sentinel input with typed CAD lineweight options so special
values display correctly and unsupported values cannot be entered.
Refs #522
Hold edit leases and compare disk fingerprints before replacement so another editor cannot be silently overwritten.
Offer retry, reload, Save As, or explicit overwrite recovery.
Closes#498
Keep resident wires, draw depths, GPU slabs, and edit history incremental across entity changes. Defer rollover work to avoid cursor jumps on dense drawings.
Replace full-document snapshots with Arc first-touch deltas and batch cache invalidation. Reuse interaction and GPU category caches across unrelated edits.
Anchor the opposite viewport corner during grip and STRETCH resizing.
Match plotted stroke-font pen widths to the SDF atlas unless CTB overrides.
Closes#435
Selecting a proxy object we render from its cached graphics (an AEC
Wall/Door/Window and the like) showed "Unknown" as its type. The entity
now carries its real DXF class name from the CLASSES section (acadrust
bump), so surface it: ui_name_or_class title-cases the class ("AEC_WALL"
→ "Aec Wall") for the properties Type row and the status-bar label, and
still says "Unknown" only when the class truly can't be named.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
PR #446 kept a fully-copied group grouped for the in-drawing COPY/ARRAY
path, but clipboard paste (Ctrl+C / Ctrl+V, PASTECLIP) runs through
finalize_paste, a separate route that never touched groups — so pasted
copies came out ungrouped, and a cross-drawing paste had no group data at
all.
Unify all copy routes on one shared Scene::recreate_groups(sources,
handle_map): copy_complete_groups now gathers the live source groups and
delegates to it, while paste snapshots the complete groups into the
clipboard at copy time (ClipboardDeps.groups) and recreates them over the
pasted handles. A group whose whole membership is copied now stays grouped
whether the copy lands in the same drawing, via the clipboard, or in a
different file; partially-selected groups still stay ungrouped.
Closes#440
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The embedded DWG preview was the whole drawing fit to a canvas, encoded
as a 24-bit uncompressed DIB (~180 KB at 256²) — the bulk of the gap
between an OCS-saved file and AutoCAD's. Three changes:
- Frame it like the screen. `from_scene` now projects every wire vertex
through the live camera into a viewport-aspect canvas, so the thumbnail
is the current pan / zoom / rotation and only the visible region, not
the full extent. Off-screen / clipped segments don't draw (a `None`
projection or NaN separator breaks the polyline run). Works for any
view. `stamp_thumbnail` passes the model pane's pixel size and the
target version.
- PNG for R2013+ (AC1027+). A line drawing on a flat background is almost
one colour, so PNG collapses to a few KB. PNG previews are only valid
from R2013, so older targets keep a BMP.
- BMP is now 8-bit palettised, BI_RLE8-compressed (was 24-bit raw). The
long single-colour runs of a preview compress to a handful of KB; a
view with >256 colours (rare) falls back to the 24-bit DIB. A full
256-entry palette is written so the reader's `(1<<bpp)*4` pixel-data
offset lands correctly.
Net: the preview drops from ~120 KB to ~2-5 KB on every version.
Co-Authored-By: Claude Fable 5 <[email protected]>
PURGE left behind three classes of dead data that AutoCAD removes,
so an OCS-purged drawing stayed far larger than AutoCAD's (a 23 MB test
file purged to 3.5 MB in OCS vs 0.58 MB in AutoCAD):
- Anonymous blocks (`*U`/`*D`/`*T`/dynamic-block variants) were skipped
wholesale. Leftover unreferenced ones now purge like AutoCAD's, held
live only when actually referenced.
- Block liveness now comes from REACHABILITY: a block is kept only if it
is reached from a real layout container by following block references
(INSERT names, dimension *D, table *T / cell blocks, MultiLeader
content). The old flat "named by some INSERT" scan could not free a
mutually-referencing dead subgraph — e.g. the dependent blocks a
detached xref leaves behind, whose head carried a DANGLING layout
handle that `is_layout()` mistook for a real layout and shielded. That
subgraph (here 467k lines) is now purged; live xrefs, reached from the
layout that inserts them, stay. The used-set also gains the by-handle
block references (table/MultiLeader/dimstyle-arrow) so purging
anonymous blocks can't delete an in-use block no INSERT names.
- SortEntitiesTable objects (per-block draw order) were never removed, so
each purged block's table lingered still listing the handle of every
deleted entity — 341k stale handles / ~2.7 MB of dead weight the save
re-emitted. Drop every table whose owning block is gone.
Net on the test file: 3.5 MB -> 0.81 MB (AutoCAD 0.58 MB).
Co-Authored-By: Claude Fable 5 <[email protected]>
The Save-As dialog hard-coded the format dropdown to the latest
(DWG/DXF 2018) regardless of what was opened, so a round-trip silently
re-targeted the version. Default it to the loaded document's own version
and DWG-vs-DXF kind instead (from `document.version` + the file
extension), which always matches one of the dropdown options. A new,
unsaved drawing keeps its document default. QSAVE on an existing file
already preserved the version; only Save-As was wrong.
Co-Authored-By: Claude Fable 5 <[email protected]>
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <[email protected]>
PICKADD 0 = click/box/lasso replaces the selection (Shift toggles),
empty click clears; PICKADD 1 (default) keeps today's accumulate.
PICKDRAG 1 = press-drag spans a rectangle marquee through the box
machinery; PICKDRAG 0 (default) keeps the freeform lasso. Both persist
in settings.json. Also re-points the start-page gate test at the
start_allowed fn it moved into (#388/389 refactor left it broken).
Closes#226
Co-Authored-By: Claude Fable 5 <[email protected]>
MATCHPROP copied only the general properties (and text style/height),
so matching dimension-to-dimension or hatch-to-hatch looked like it did
nothing — the visible differences live in type-specific style fields.
General set gains transparency. match_special_props transfers every
STYLE-affecting field the destination supports — never content or
placement:
- Text: style, height, width factor, oblique (style+height cross
TEXT <-> MTEXT); MText adds line spacing, drawing direction and the
background fill block.
- Dimension / Leader / Tolerance: the dim style name crosses all
three, and the ACAD_DSTYLE override record is replicated through
set_entity_xdata (cleared on the destination when the source carries
none, so the match is exact). Restyled dimensions drop their baked
*D block and repaint immediately.
- Hatch: pattern definition, type, angle, scale, solid flag and
gradient — the boundary stays.
- LwPolyline: constant width (vertex widths included) and linetype
generation.
- MultiLeader: style plus all style overrides (path type, line
color/type/weight, landing/dogleg, arrowhead, text style/color/
frame/height, overall scale, override flags); leader geometry, text
and block content stay.
Closes#281
Co-Authored-By: Claude Fable 5 <[email protected]>
PEDIT reworked around a mode machine:
- Picking a line/arc asks "Object is not a polyline. Turn it into one?
[Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
editing continues on the result. Pickfirst: an already-selected
polyline skips the select step, a selected line/arc goes straight to
the convert prompt.
- Join gathers with the normal selection system (single picks AND
window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
arcs per segment meeting tangentially at an inserted knee, both
segments at a vertex sharing that vertex's tangent — the whole run is
tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
shapes). Spline resamples the vertex frame as a uniform cubic
B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
vertex grip hover menu gains Break — a closed polyline opens at the
vertex, an open one splits into two (interior vertices only),
handled driver-side as an entity replacement.
JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.
Closes#263
Co-Authored-By: Claude Fable 5 <[email protected]>
PLAN acted like a ViewCube TOP click the moment it ran — already on Top
meant an immediate flip to Bottom, and the options never appeared. It
now prompts "[Current ucs / Ucs / World] <Current>:" right on
activation and only changes the view once an option runs: Current =
plan of the active UCS, World = world XY, Ucs = prompts for a named UCS
(case-insensitive lookup). The new plan_snap goes straight to the plan
view with no flip-to-opposite cube behaviour.
KeywordCommand gains with_default(): a bare Enter at the verb step
dispatches the default verb (PLAN's <Current>) instead of cancelling.
Closes#326
Co-Authored-By: Claude Fable 5 <[email protected]>
Holding Shift while a command asks for a point captures the nearest
polar increment (or ortho axis, UCS-aware) from the rubber-band base
toward the cursor and pins the pick to that ray. Every candidate —
osnap hits included — projects onto the locked direction, so a snap far
off-axis contributes only its along-axis component; the click commits
exactly what the preview shows. The direction stays fixed while Shift
is held and drops the moment it releases. Grip drags get the same lock
from the grip origin.
Guards: engages only when the active command wants a point — never
during entity picks (Shift there is the TRIM/EXTEND swap), tangent
picks, selection gathering or window-corner picks.
Closes#312
Co-Authored-By: Claude Fable 5 <[email protected]>
Grip drags mutated the document without ever pushing history — dragging
a polyline vertex (or any grip) could not be undone. The release-commit
now swaps the pre-drag backup in, snapshots that PRE state as a "GRIP"
entry, swaps the dragged result back and clears the redo stack; the
Esc-cancel path is unchanged and a no-move engaging click records
nothing.
Ctrl+Z during a drawing command steps the command instead of the
document undo swallowing the in-progress object (which also killed the
active command): new CadCommand::on_undo_step hook, consulted before
undo_active_tab. PLINE pops its last vertex (shared with the U option),
LINE reverts the last committed segment and keeps drawing from the
previous point (rides its existing U/UndoDocument path), and ARC
Continue reverts the last arc and resumes from the prior anchor with
the correct tangent via a new anchor history. With nothing placed yet
each falls through to the document undo.
Also drops the two warnings the #336 work left behind (unused Entity
import, dead Snapper::override_active).
Closes#332
Co-Authored-By: Claude Fable 5 <[email protected]>
Both commands gain a shared option mode machine:
- Cutting/Boundary edges (T/B): pick which objects act as the boundary
(picked edges highlight yellow, the hovered candidate half-strength);
Enter rebuilds the boundary set, empty = everything (quick mode).
- Fence (F): draw a dashed fence; every object crossing it is trimmed /
extended at each crossing, re-applied to the surviving pieces so a
fence across both overhanging ends clears both. Live red/cyan result
preview while placing points.
- Crossing (C): crossing-selection semantics — the dashed green window
picks objects crossing its edges AND wholly inside; runs through the
fence machinery with a synthesized inside click.
- Edge (E): boundaries extrapolate (lines run past their ends, arcs
close to full circles). Cuts that land on an implied extension draw a
dashed yellow guide from the boundary's drawn end to the cut point,
so the user sees which edge causes the cut. Extend previews also show
the added tail as a bright dashed slice of the true curve.
- Erase (R, TRIM): picks delete without needing an intersection, with a
red hover preview.
- Shift+click swaps trim<->extend per pick (new set_shift plumbing into
the entity-pick and hover paths); the hover preview follows the held
state. Project is out of scope for the 2D pipeline.
The per-type pick bodies moved to shared free fns (pick_trim_at /
pick_extend_at) used by direct picks, hover previews and the fence
pass; EXTEND's snapshot bookkeeping unified with TRIM's placeholder
flow. Option keywords now take precedence over the hex-handle reading
in feed_active_cmd ("F"/"C"/"E" are valid hex), and fence geos carry
sentinel handles — probing with NULL against NULL-handle geos excluded
the whole fence. Regression test: fence_trims_x_arm.
Closes#336
Co-Authored-By: Claude Fable 5 <[email protected]>
iced's clipboard rides window_clipboard, which has no wasm backend — a
focused text_input captured Ctrl+C/V on the web and did nothing.
Ctrl+V (captured, wasm only): read the async browser clipboard (the
keypress is a user gesture, so it's permitted) and replay the text as
synthetic KeyboardEvents on the winit canvas — the only route into a
focused text_input, since iced exposes no insert-text operation.
Control characters are dropped and the length is capped. The MText and
inline-TEXT editors keep their dedicated paste paths (no double feed),
and an unfocused Ctrl+V still runs PASTECLIP.
Ctrl+C (captured, wasm only): a widget operation pairs each text_input
with its focus state (iced fires both callbacks back-to-back on the
same widget) to collect the focused field's visible text, then writes
it with navigator.clipboard.writeText. Whole-field text, not the
selection — iced doesn't expose the selected range.
Native behaviour is unchanged.
Closes#346
Co-Authored-By: Claude Fable 5 <[email protected]>
Shift+right-click in the viewport opens a cursor-anchored grid of snap
icons (names as hover tooltips). Picking one engages a one-shot
override: only that snap applies — even with running osnap off — until
the next point pick commits, which restores the saved configuration.
Esc drops an unconsumed override; an outside click closes the menu.
The status-bar snap toggles are untouched — this is a separate,
temporary mechanism.
Covers items 1 and 2 of #337 (mid-between-two-points still pending).
Co-Authored-By: Claude Fable 5 <[email protected]>
Three fixes to the polyline arc-segment grip:
- bulge_from_midpoint keyed the sign off which chord side the midpoint
lies on — the opposite convention of BulgeArc::from_bulge, so every
grip-drag frame flipped the arc to the other side and the radius
jumped wildly ("snaps to a restricted range of radii"). It now
derives sign and sweep from the p0 -> mid -> p1 traversal
orientation, verified as the exact inverse of BulgeArc's midpoint.
- The arc segment's mid grip dragged in Translate mode, feeding
per-frame deltas through the recomputed arc midpoint — the drag
deadlocked the moment the cursor crossed the chord. Arc grips now
drag in Absolute mode: the arc re-fits through the cursor itself.
Straight-segment grips keep the Translate stretch.
- Picking Convert to Arc in the grip menu now engages that segment
grip (the Add Leader placement mechanic), so the new arc follows the
mouse immediately and the next click seats it.
Closes#339
Co-Authored-By: Claude Fable 5 <[email protected]>
Dropping a supported file (dwg / dxf / bak / sv$, case-insensitive)
opens it through the existing open flow: an already-open drawing
switches to its tab, a load in progress queues the drop behind it
(multi-file drops arrive one event each), anything unsupported reports
on the command line instead of failing silently in the parser.
Platform support follows winit 0.30: Windows, macOS and Linux/X11
(incl. XWayland) deliver FileDropped; the Wayland backend does not
implement file drag & drop yet, so drops are inert there until winit
gains it.
Closes#344
Co-Authored-By: Claude Fable 5 <[email protected]>
U / UNDO (typed or via the prompt button) pops the last placed vertex
without ending the command, repeatably down to and past the start
point. The exit tangent of the remaining last segment is recomputed
from its bulge so a following Arc segment stays tangent-continuous.
New CmdResult::RemoveLiveEntity takes the live polyline out of the
document when Undo drops it below two vertices; the next point
re-creates it under the same undo unit.
Closes#352
Co-Authored-By: Claude Fable 5 <[email protected]>
- RENAME gains the BLOCK type (stepped button + inline arm) via the
shared Scene::rename_block.
- Stepped command front-ends (Rename, UserReg, Keyword, TwoValue,
SelectThenTwoValue) returned None after consuming a typed token, and
the driver treats None as "not consumed" — the same text was offered
again and advanced two steps at once (RENAME dispatched old==new,
SETVAR "X" became "SETVAR X X"). Consumed-but-prompting inputs now
return NeedPoint.
- Symbol renames re-key the name-keyed Table (remove -> rename -> add)
instead of mutating .name in place, which left the map keyed by the
old name so later lookups silently failed. References (entity
layer/linetype/style fields, layer linetypes, header current-* names,
dim style refs on Dimension/Leader/Tolerance) are chased
case-insensitively; layer 0/Defpoints and the built-in linetypes are
protected; the undo snapshot is taken before the mutation so undo
actually restores the old name.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Block reference Name (Misc) is editable: the dropdown re-points the
insert to another definition; typing a new name renames the definition
everywhere (record re-key, Block marker, every INSERT reference).
Anonymous (*) and xref blocks stay read-only, with the same guards
added to Scene::rename_block.
- Custom single-control dropdown (text field + caret) built on the
shared floating_below overlay so the list always opens downward,
unlike iced's space-based menu placement.
- Consecutive "<Base> X/Y/Z" rows (Position, Scale, Start, Center, ...)
collapse into one expandable summary row across all entities; the
expand state persists across rebuilds and selection changes.
- The BEDIT block tab no longer offers the layout Rename/Delete
context menu.
Co-Authored-By: Claude Fable 5 <[email protected]>
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.
Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.
On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.
Co-Authored-By: Claude Fable 5 <[email protected]>