feat(app): record accepted snaps per point step and guard dimension inference

Point input now keeps an `AcceptedSnap` alongside each point a command
collects: the paper point, the model point it came from, the viewport and
frame it was seen through, and the geometry identity the snap engine
attributed. Interactive picks record in the click handler, typed / dynamic /
headless points record in `feed_command`, and the list is cleared whenever a
command starts or the active space changes. Commands that don't care keep
consuming the plain paper point unchanged; PR2 reads the model point and
frame, PR3 the source.

Dimension association inference is suppressed when the dimension's points came
through a viewport. Inference matches geometry against the dimension's own
definition points in whatever space it is handed, so paper definition points
projected from a viewport would associate with unrelated paper-sheet geometry
at those coordinates, or match model geometry using paper coordinates. An
empty source list makes `attach_dimension_association` a no-op. PR3 replaces
this with real viewport-aware associations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Christopher OverbeckandClaude Fable 5.1 committed 2026-09-15 12:27:33 -05:00
1 parent 6def465ba5
commit dbdddb7dc2
3 files changed
+38 -12

No files matched your search

+13 -12
View File
@@ -102,6 +102,10 @@ impl OpenCADStudio {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].snap_result = None;
self.last_point = None;
// Points collected in the space being left are meaningless in the new
// one (PR1).
self.clear_accepted_snaps();
self.pending_click_snap = None;
self.snapper.from_point = None;
self.snapper.clear_tracking();
self.otrack_active = None;
@@ -368,6 +372,11 @@ impl OpenCADStudio {
if !self.command_point_allowed(i, *point) {
return Task::none();
}
// Typed / dynamic-input / headless points carry no snap, but the
// accepted-snap list must stay index-parallel with the points the
// command collects (PR1). Interactive picks record themselves in
// the click handler and never reach here.
self.record_accepted_snap(None, None, *point);
}
if default_start {
let StepInput::Point(point) = &input else {
@@ -1590,9 +1599,7 @@ impl OpenCADStudio {
};
if is_associative_dimension && association_enabled {
if let Some(handle) = committed {
let sources = self.tabs[i]
.scene
.infer_dimension_sources(handle);
let sources = self.infer_dimension_sources_guarded(i, handle);
self.tabs[i]
.scene
.attach_dimension_association(handle, sources);
@@ -2063,9 +2070,7 @@ impl OpenCADStudio {
let committed = self.commit_entity_handle(entity);
if is_associative_dimension && association_enabled {
if let Some(handle) = committed {
let sources = self.tabs[i]
.scene
.infer_dimension_sources(handle);
let sources = self.infer_dimension_sources_guarded(i, handle);
self.tabs[i]
.scene
.attach_dimension_association(handle, sources);
@@ -2175,9 +2180,7 @@ impl OpenCADStudio {
crate::command::DimensionAssociationInput::Infer(source) => {
let sources: Vec<_> = source.map_or_else(
|| {
self.tabs[i]
.scene
.infer_dimension_sources(handle)
self.infer_dimension_sources_guarded(i, handle)
},
|source| {
if single_source_dimension {
@@ -2302,9 +2305,7 @@ impl OpenCADStudio {
crate::command::DimensionAssociationInput::Infer(source) => {
source.map_or_else(
|| {
self.tabs[i]
.scene
.infer_dimension_sources(handle)
self.infer_dimension_sources_guarded(i, handle)
.into_iter()
.map(|source| {
source.map(
+3
View File
@@ -120,6 +120,9 @@ impl OpenCADStudio {
// Reset the last committed point so the first click of the new command
// is not constrained by ortho/polar relative to a previous command's endpoint.
self.last_point = None;
// A new command collects its own points, so the previous command's
// accepted snaps must not leak into it (PR1).
self.clear_accepted_snaps();
// Starting a command restarts the right-click cycle, so its first
// right-click acts as Enter rather than opening the context menu.
self.tabs[i]
+22
View File
@@ -707,6 +707,25 @@ pub(super) struct OpenCADStudio {
show_layout_tabs: bool,
/// Last point committed by a drawing command — used as ortho/polar base.
last_point: Option<glam::DVec3>,
/// Viewport frame the live snap marker is currently looking through (PR1).
/// Set by the cursor-move handler when the displayed snap came from a
/// layout viewport, cleared otherwise. Display/bookkeeping only — the point
/// itself is already projected onto the sheet.
pub(crate) vp_snap_frame: Option<crate::scene::viewport_ref::ViewportFrame>,
/// Snaps accepted by the active command's point steps, parallel to the
/// points it collected (PR1). A command that does not care keeps using the
/// plain paper point; dimension commands (PR2) read the model point,
/// viewport and frame from here, and PR3 reads `source`. Cleared whenever a
/// command starts or ends.
accepted_snaps: Vec<crate::scene::viewport_ref::AcceptedSnap>,
/// Snap resolved by the click currently being handled, with the viewport
/// frame it came through (PR1). Set inside the click handler's point
/// resolution so the point-accept path further down can record the
/// [`crate::scene::viewport_ref::AcceptedSnap`] without recomputing.
pending_click_snap: Option<(
crate::snap::SnapResult,
Option<crate::scene::viewport_ref::ViewportFrame>,
)>,
/// Endpoint + unit exit-tangent of the most recently drawn line/arc, so
/// `ARC_CONT` (Arc → Continue) can start tangentially from where drawing
/// ended. `None` once a non-line/arc entity is committed.
@@ -3689,6 +3708,9 @@ impl OpenCADStudio {
show_file_tabs: true,
show_layout_tabs: true,
last_point: None,
vp_snap_frame: None,
accepted_snaps: Vec::new(),
pending_click_snap: None,
main_window: None,
thumbnail_capture_clean: false,
#[cfg(not(target_arch = "wasm32"))]