Follow-up to the block-attribute grips picked from #1386.
- Movability no longer includes the invisible flag: an attribute ATTMODE 2
displays is grippable, so the grip the display-aware list shows can be
dragged. The document-less grips() still skips invisible attributes,
since it cannot consult ATTMODE.
- The grip sits on the displayed attribute, which an annotative insert
scales about its insertion point; Scene::apply_grip now unscales the
dragged target the same way before the attribute stores it, so the grip
no longer jumps to twice the drag at scale 2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
COPY, MIRROR and ARRAY duplicated a dimension's baked block but left the
copy with no association at all. Now a copy keeps one, on one condition:
every handle in every chain — the viewport, each INSERT on the block
path, the source entity, and an intersection's second chain — must have
been copied too. Then the whole chain is remapped onto the copies, so
the copy measures the copied geometry.
The condition is all-or-nothing on purpose. A partially-remapped chain
would leave a copy quietly driven by the original's geometry, which is
worse than no association; and copying a dimension on its own still
produces an unassociated dimension, which is exactly what the model
space path did before. It is the same rule `copy_complete_groups` uses
for groups, applied to a longer chain.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ports FreeCAD's planegcs 2D geometric constraint solver to Rust as a new
crates/ocs_gcs workspace member, and wires it into the document/scene model
as a persistent SketchConstraintSet: Coincident, Horizontal/Vertical,
Parallel/Perpendicular, Equal, Tangent, Distance, Angle and Radius
constraints that re-solve live (including mid-drag, not just on release),
render glyphs and a DOF badge, flag conflicts with a one-click resolver
pill, and round-trip through save/load via an XRecord blob.
Also includes docs/command_reference.md (a statically generated reference
of every registered command and its prompt sequence) and
docs/named_parameters_design.md + docs/backlog.md, which hand off the next
queued feature (a named-parameter/expression table) and consolidate every
deliberate scope-down and open gap found along the way, so a fresh session
has a complete starting point.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
Replace full-document snapshots with Arc first-touch deltas and batch cache invalidation. Reuse interaction and GPU category caches across unrelated edits.
A reopened file renders dimensions from the *D block baked on save, so
grip drags only moved definition points while the stale block kept
drawing — grips worked on fresh dimensions but died after a roundtrip.
Route the grip drag, grip-menu actions and typed grip-menu values
through invalidate_dim_block (#181) so tessellation falls back to live
geometry and the next save re-bakes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.
They now record only the touched entities' before/after images:
- Scene self-records through the five mutation primitives when a recording is
open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
the original handle / remove) plus a per-handle cache reseed, then a single
bump_entities so the incremental caches patch instead of a full re-tess —
undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
between the undo/redo stacks without re-capturing. Commands that touch
non-entity state (new layer, group, *D block, viewport) fall back to the
full snapshot via per-command predicates, and a poison flag debug-asserts
any mis-scope.
remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.
Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every geometry mutation previously bumped a monotonic geometry_epoch and each
derived cache compared its stamp and, on a miss, re-walked the whole document —
so drawing or moving one line rebuilt every hatch / image / mesh / wipeout model
even though none of them changed.
Add a per-mutation delta journal: bump_entities(&[(Handle, ChangeKind)]) records
exactly which handles changed (Added / Removed / Modified), folding the memo drop
and the epoch bump into one call a site cannot forget. A bounded ring keeps the
recent deltas; replay_since(last_epoch) coalesces the changes past a consumer or
returns None (full rebuild) when a spanned delta is `full` or the consumer fell
off the ring. bump_geometry / bump_geometry_no_blocks push a `full` delta so the
journal is never missing a step.
Route the common single-entity ops through it: add (Added), update (Modified),
erase (Removed), move/rotate/scale/mirror (Modified), copy (Added). Gate the four
per-category model caches on category relevance — an edit whose handles are not in
a category leaves that category cache warm; a removal is treated conservatively
(the erased handle can no longer be classified), and the mesh cache also
invalidates on any INSERT edit since it carries the instanced block meshes.
Net: a plain-entity edit no longer rebuilds the hatch/image/mesh/wipeout model
sets. The resident wire clone-all and the GPU re-upload are still whole-set — the
journal is the groundwork for making those per-entity next. Adds 6 journal unit
tests (replay, coalescing, full-fallback, ring overflow, category gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy-paste (CTRL+C / CTRL+V) dropped dimensions. A dimension draws from
its baked *D block, whose geometry is in absolute WCS coordinates, and
the clipboard paste path added the dimension entity but never gave it its
own transformed block — unlike the in-drawing COPY, which does (#161). So
the pasted dimension kept pointing at the source block and rendered at
the original location instead of the paste point (same drawing), or had
no block at all (cross-drawing).
Capture each copied dimension's *D block into the clipboard, and on paste
build a fresh transformed copy of it and re-point the dimension's
block_name — for same- and cross-drawing pastes alike. Factored the
block-building out of clone_transformed_block into a shared
define_transformed_block, and the block snapshotting out of
capture_blocks into snapshot_block.
Root cause: the glyph tessellator advanced the pen by advance*width_factor
while the transform already scales the pen position by the width factor, so
a backward (negative width factor) run kept its word order and grew on the
same side of the anchor instead of mirroring across it. Advance in unscaled
glyph units to match text_local_bounds; the transform applies the factor once.
On top of that:
- The renderer honours the entity's own text-generation flags (DXF 71: bit 2
backward / bit 4 upside-down) XOR the text style's flags, and signs the
horizontal anchor by the width factor so Center/Right backward text mirrors
about its anchor.
- MIRRTEXT on: a MIRROR toggles both group-71 bits so the reflected rotation
renders a true glyph mirror (backward-upright for a vertical axis, flipped
for a horizontal one).
- MIRRTEXT off: the text flips left<->right and lands mirror-symmetric to the
source (right-reading) instead of hugging the axis.
- The MIRROR preview re-derives text ghosts to match the commit; mirrored()
now reflects the double-single residual too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>