Refresh associations after complete SpaceMouse camera updates and recognize imported paper-space owners without Layout objects. Use kernel containment, intersection, perpendicular and spline tangent solving; pin cadkernel 7ce27f2 and add regressions.
Removes PR3's `viewport_frame` seam in `viewport_ref.rs`; the frame is
PR1's camera-derived one everywhere.
The five dimension commands (DIMLINEAR / DIMALIGNED / DIMRADIUS /
DIMDIAMETER / DIMANGULAR) now record a real association when they
measure through a viewport. The commit path's one association gate
(`dimension_association_allowed`, added when PR2 was stacked) gains an
else-branch: where PR2 committed the dimension unassociated and PR1's
inference guard returned an empty source list, `Scene::attach_viewport_
dimension_association` now stores the whole chain — dimension ->
viewport -> block path -> entity -> feature — from the AcceptedSnaps PR1
captured, in definition-point slot order.
* `Scene::dimension_association_slot_points` names that order, including
the radial dimensions `dimension_reference_points` does not cover.
* `OpenCADStudio::viewport_dimension_snaps` builds the slot array. A snap
whose paper point is no longer the definition point it fills has its
model point recomputed from that definition point through the frame:
the object pick records one click for both extension origins, and a
rotated DIMLINEAR projects its origins onto the measuring axis.
* Radial dimensions take their own path inside
`attach_viewport_dimension_association`. Their single reference is an
osnap type 10 carrying the radial marker (which circle / which polyline
arc segment) and the angle around it; `source_reference` would hand
back the generic curve-parameter marker -2, which
`radial_source_for_marker` cannot find.
One measurement rule, documented in `viewport_ref::measurement_rule`:
`actual_measurement` is always the RAW measurement in the dimension's own
space (paper, for a viewport dimension), and the viewport compensation
lives only in the magnitude of the negative DIMLFAC override, applied
once by the formatter. PR2's persistence already followed it; PR3's
refresh did not — it substituted the model distance times
`model_factor()`, which made a refreshed dimension read `user_lfac` times
too much versus an identical freshly-created one. Refresh now writes the
paper measurement (a debug assert pins resolved paper points to the model
points through the frame), and `axis_distance` goes with it.
Block sub-entity snaps: verified that `block_cache` stamps the top-level
INSERT's handle onto every expanded and clipped sub-wire, so
`wire_source` can only ever report the INSERT — nested inserts included.
`OpenCADStudio::resolve_source_block_path` now descends from that INSERT
at accept time (using the object-pick path's existing
`resolve_measurable_entity`, so arbitrary depth, not just one level) and
fills `SnapSourceRef.block_path` with the real inner entity. Where the
descent cannot identify one — a non-planar-pickable type such as text,
hatch, spline or solid, or nesting past eight levels — the source stays
the INSERT with an empty path, which is coarser but never wrong. Both the
limitation and where it bites are documented on
`Scene::resolve_block_snap_source` and `SnapResult::source`.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Panning, zooming, twisting or rescaling a layout viewport moves model
geometry across the sheet without touching that geometry, so nothing in
the ordinary entity-change path noticed and the dimensions drawn through
the viewport stayed where they were — detached from what they measure.
`Scene::notify_viewport_changed` announces the viewport itself as a
modified entity. That is all it takes: every trans-space reference
carries the viewport handle as the first link of its chain, so the
dependency lookup in `refresh_associative_dimensions` already finds the
associations from there, and the refresh re-places and re-measures them.
Called from each mutator that changes the paper<->model mapping: pan,
zoom, fit-to-bounds, view normalisation on entering MSPACE, the
view-direction/twist snaps, the viewcube camera path, and both
annotation-scale setters.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
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 <[email protected]>
This wires the chain walker into the association system proper, and adds
the entry point PR2 calls.
Resolution. `resolve_reference_chain` walks the chain, evaluates the
osnap feature in the entity's own coordinates, lifts it to model space
through the block path, and — when the chain goes through a viewport —
projects it onto the sheet with that viewport's `ViewportFrame`. Both
points are kept: the dimension holds paper points, the value it displays
is a model length. `ChainMap` is the one place that conversion lives,
including a `map_radial` that carries a radial source's plane into the
dimension's space so the radius/diameter/arc paths need no viewport
special-casing of their own. Which space a stored osnap point was
written in is not recoverable from the record — our own writer used
model/WCS, AutoCAD's trans-space records use the dimension's space, and
both turn up in the same drawing — so both readings are mapped down to
the entity and the one that lands on the geometry wins. It is only ever
a hint that disambiguates candidates, never the answer.
Creation. `Scene::attach_viewport_dimension_association` takes one
`AcceptedSnap` per definition point and writes the whole chain: viewport
handle, block path, entity, osnap type, sub-entity marker and parameter,
plus `trans_space` on the association. Every link becomes a reactor
target, not just the innermost entity, so a change anywhere along a
chain reaches the refresh through the ordinary dependency index.
Refresh. `refresh_associative_dimensions` now matches a changed handle
anywhere in a chain (including the second chain of an intersection), so
a viewport change reaches the dimensions drawn through it. Perpendicular
and tangent references resolve in a second pass, measured from the other
definition point. A dimension carries its sheet annotation with it: the
dimension-line standoff and a user-dragged text position move by the
centroid shift of the points that actually moved, so a one-ended edit
keeps the dimension line centred instead of collapsing it. For a
trans-space linear or aligned dimension the displayed value is
recomputed from the model points and multiplied only by the user's
DIMLFAC (`MeasurementScale::model_factor`) — the viewport compensation
is inherent in having measured in model space, and applying it again is
the double-scaling this three-PR effort exists to stop.
Broken references. Brokenness is derived, never written: a reference
whose source was erased simply stops resolving, the dimension keeps its
last valid appearance, and the record is left byte-for-byte as it was.
An undo that restores the source restores the link with no extra
bookkeeping. `Scene::dimension_association_status` reports it per slot.
`dimension_is_associative` now walks the chain too, instead of accepting
any surviving handle in `xrefs` — which was true even for a trans-space
reference whose actual geometry had gone, because the viewport was still
there.
Undo. Creating an association journals the new object and every reactor
list it touches; refreshing a dimension journals the dimension's
before-image. Both were previously invisible to the delta recorder.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
An `AssocDimensionReference` addresses a feature through a chain of
handles — dimension -> [viewport] -> [insert ...] -> entity -> feature —
stored in `xrefs` (AutoCAD's `mainObjectIds`). The existing resolver read
only `xrefs[0]`. That is right for a model-space dimension on top-level
geometry and wrong for everything else: a paper-space dimension measuring
through a layout viewport has the VIEWPORT first, so resolution stopped
on a viewport entity, found no curve and gave up. Nine of the ten
association objects in the 115448 regression drawing are that shape.
`dimension_assoc_chain` walks the chain positionally, which is all the
file gives us: a leading VIEWPORT is the trans-space hop, INSERTs after
it are the block-instance path (with their composed transform), and the
first handle that is neither is the geometry. A chain of nothing but
INSERTs ends on the innermost one, which is how a block reference's own
insertion point is dimensioned. A handle that is no longer in the
document comes back as `ChainError::Missing` rather than as a silent
failure, so the caller can tell a broken link from an unsupported one.
The module also evaluates the osnap feature itself, in the entity's own
coordinates: centre, midpoint, quadrant, node/insertion, nearest,
perpendicular, tangent (closed form on a circle or arc; a Newton
refinement of (C(u) - P) . C'(u) = 0 on a spline, seeded from the stored
parameter — the shape of the 115448 tangent-to-spline reference), and
intersection / apparent intersection against the second object chain.
Types we do not evaluate return None and keep their stored data, so an
unsupported reference is left unresolved instead of being replaced by a
guess.
`Transform` has no inverse of its own and a block path is always affine,
so the 3x3 cofactor inverse lives here too. All arithmetic is f64.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
PR3 has to map a resolved model point onto the sheet, which needs the
layout viewport's paper<->model mapping. PR1 (feat/viewport-snap) owns
`Scene::viewport_frame` and is not visible from this branch, so this adds
the minimal stand-in the shared contract in `viewport_ref.rs` describes:
paper_center from the viewport rectangle's centre, model_target from the
saved view target, scale from `vp_effective_scale`, twist from the saved
view twist.
It is fenced off in one clearly marked block with merge instructions.
PR1's version is camera-derived, because it must agree with what is
actually drawn — including the auto-fit rescue `camera_for_viewport`
applies to stale saved views. Nothing else in PR3 reads viewport fields
directly, so replacing this one function is the whole merge.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Removes PR2's two seams now that PR1 (viewport snapping) is underneath.
* `Scene::viewport_frame` is PR1's camera-derived implementation. The
viewport-record version is gone; PR1's returns `None` for oblique /
perspective viewports, which is what we want — every entry point in
`viewport_dim_seam` then declines rather than guessing a planar
mapping. `viewport_dim_seam` is now just the dimension object-pick
path, and its module doc says so.
* There is one accepted-snap store: PR1's `OpenCADStudio::accepted_snaps`.
PR2's parallel `dim_accepted_snaps` (and its command-name ownership
tracking, which PR1's clear-on-command-start/end already does) is
deleted, along with the `record_dimension_accepted_snap` seam.
`accepted_snap_for_point` reads PR1's list.
Two things layer on top:
- the explicit object pick is not a point step, so it still records its
own snaps, now through `push_accepted_snap` into PR1's list, and now
carrying the picked entity handle + block path as a `SnapSourceRef`
so PR3 can associate it;
- a typed coordinate carries no snap, so `measure_snap` re-attaches the
content viewport under a snap that is free *and* source-less. That
keeps `DIMLINEAR` with typed coordinates inside a 1:10 viewport
measuring the model, without ever reinterpreting a real paper-sheet
snap that happens to sit over a viewport.
* One association gate, not two. `dimension_association_allowed` is the
single predicate, derived from the same `dimension_measure_space`
classification that decides whether to compensate the measurement, so
the two can never disagree. `infer_dimension_sources_guarded` delegates
to it, and the commit path's "commit unassociated when measured through
a viewport" condition is now that predicate rather than a second,
independently-computed bool. It also covers the explicit-pick and
explicit-source-list branches, which the old inference-only guard did
not. Because the classification ignores the trailing dimension-line
placement click, an ordinary paper dimension whose placement click lands
inside a viewport rectangle keeps its association.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Colocated unit tests pin the persisted representation: a 1:10 viewport with
no user factor writes DIMLFAC -10, a 10-unit paper span then formats as 100,
and the same dimension read back in model space does not scale. A positive
user factor folds into the magnitude, and a drawing-wide negative DIMLFAC
that already carries the compensation writes no override at all. Another
test pins the viewport model->paper affine against ViewportFrame's own
mapping, so object-pick projection and snap projection cannot drift.
Also clears the recorded snaps when a multi-dimension command commits.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
DXF gives DIMLFAC a sign convention: a positive value scales every
dimension, a negative one applies |DIMLFAC| only to dimensions that live in
a paper-space layout (where it compensates a viewport's scale). The linear
formatter multiplied by the raw signed value, so a drawing with negative
DIMLFAC (e.g. 115448.dxf) rendered negative measurements.
Resolve the sign once, in `resolved_dimension_style`, the single place an
effective style is derived, so dimension text, alternate units, exploded and
baked text and the properties panel all agree. `extended_data` keeps the raw
signed value, so DXF/DWG export is unchanged. The properties "Measurement"
row now reports the value the text shows rather than the raw distance, while
`actual_measurement` (group 42) stays raw as the format requires.
Also adds the PR2 seams the dimension work builds on:
* MeasurementScale::user_lfac_for_space / viewport_dimlfac_override —
the persisted representation of viewport compensation.
* Scene::viewport_frame and paper-point viewport resolution.
* Model-geometry hit testing through a content viewport, including
descent into block instances with the instance transform baked in.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Paper-space commands could not pick model geometry displayed by a layout
viewport: `hit_test_wires` deliberately excludes viewport content in PSPACE,
and that has to stay true for selection. This adds the capability as a separate
query rather than by changing those semantics.
`OpenCADStudio::paper_viewport_snap` walks the current layout's displayed
content viewports top-most first, skips those whose display is off or whose
clip boundary excludes the cursor, and for each one runs the EXISTING snap
engine against that viewport's model wires using the viewport's own camera and
screen rectangle (`viewport_edit_frame_for`). The accepted model point is
mapped back onto the sheet with `ViewportFrame::model_to_paper`, so everything
downstream keeps working in paper coordinates — LINE still draws at the
projected paper point. Grid snap and the paper-space rubber-band origin are
suppressed for that pass; results outside the clip are rejected so no snap
lands on a clip cut. Locked viewports snap like unlocked ones.
No projected geometry is rebuilt per pointer event: the wire set is the
renderer's resident per-viewport Arc (`model_wires_for_viewport_arc`) and the
broad phase reuses the shared interaction index over it.
The result is merged with the ordinary paper-sheet snap by `snap::merge_snap`,
which applies the engine's own priority-then-distance rule, in both the
cursor-move and the click paths.
`SnapResult` gains Copy fields `viewport: Option<Handle>` and
`source: Option<DimensionAssociationSource>`; the engine fills `source` from
the owning wire wherever it knows it. `AcceptedSnap::from_snap` converts a
result plus frame into the shared PR2/PR3 contract type.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
`Scene::viewport_frame` derives the paper<->model mapping (paper_center,
model_target, paper-per-model scale, in-plane twist, lock state) from
`camera_for_viewport` — the same camera the renderer displays the viewport's
contents with — so a snap computed through the frame lands on the pixels the
GPU drew. Oblique / mirrored / perspective viewports return None.
Also generalises the MSPACE editing adapter: `viewport_edit_frame_for(handle)`
returns the camera + screen rect for any viewport, and `viewport_edit_frame`
becomes a wrapper over it for the active one. Adds
`viewport_displays_paper_point` (rectangular bounds plus a non-rectangular clip
boundary polygon, tested in paper coordinates) and public accessors for the
layout's content viewports and their VP ON state.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Contract module for the viewport snapping, viewport dimension measurement,
and dimension association work that builds on it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>