From fcf1b96fdf7622619e5e563c8ec72b02940e8f09 Mon Sep 17 00:00:00 2001 From: Christopher Overbeck Date: Tue, 15 Sep 2026 12:29:21 -0500 Subject: [PATCH] Resolve dimension object picks against model geometry inside a viewport Paper-space hit testing only sees sheet entities, so DIMLINEAR / DIMALIGNED / DIMRADIUS / DIMDIAMETER / DIMANGULAR in "select object" mode found nothing when the user clicked the model geometry a viewport displays. A pick that lands inside a content viewport is now mapped into model space through the viewport frame and tested against that viewport's resident model wires - the same tessellation it draws, so the pick matches what the user sees. Ordinary block instances are descended into, with the instance transform (recursively, up to eight levels) baked into the extracted geometry, so a line inside a block measures its placed length. The picked entity is handed to the command already projected onto the sheet, so all five commands keep working entirely in paper coordinates and unchanged; the pick is recorded as a viewport snap, so the committed dimension is compensated exactly like a point-input one. A miss falls through to the ordinary paper-space pick. Also narrows mixed-space classification to the measuring points: the final click of each of these commands is the dimension-line placement, which lands on bare sheet for viewport measurements and inside a viewport rectangle for paper ones, and says nothing about what is being measured. Co-Authored-By: Claude Fable 5.1 --- src/app/dim_viewport.rs | 81 ++++++++++++++++++++++---------------- src/app/update/viewport.rs | 24 ++++++++++- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/src/app/dim_viewport.rs b/src/app/dim_viewport.rs index bf4510f94..55a84961a 100644 --- a/src/app/dim_viewport.rs +++ b/src/app/dim_viewport.rs @@ -98,7 +98,9 @@ impl OpenCADStudio { } /// The accepted snap for the `idx`-th collected point of the active - /// command, if one was recorded. + /// command, if one was recorded. The contracted accessor PR1 and PR3 read; + /// PR2 itself classifies the whole set at commit time. + #[allow(dead_code)] pub(crate) fn accepted_snap_for_point(&self, idx: usize) -> Option<&AcceptedSnap> { self.dim_accepted_snaps.get(idx) } @@ -130,13 +132,21 @@ impl OpenCADStudio { if scene.current_layout == "Model" || scene.active_viewport.is_some() { return DimensionMeasureSpace::Direct; } - let snaps = &self.dim_accepted_snaps; - if snaps.is_empty() { + // The final collected point of every dimension command in scope is the + // dimension-line / text placement click, which says nothing about what + // is being measured — it routinely lands on bare sheet even for a + // viewport measurement, and inside a viewport rectangle even for a + // plain paper one. Only the measuring points are classified. + let measuring = match self.dim_accepted_snaps.len() { + 0 | 1 => self.dim_accepted_snaps.as_slice(), + n => &self.dim_accepted_snaps[..n - 1], + }; + if measuring.is_empty() { return DimensionMeasureSpace::Direct; } let mut frame: Option = None; let mut any_free = false; - for snap in snaps { + for snap in measuring { match snap.frame { Some(f) => match frame { None => frame = Some(f), @@ -148,39 +158,13 @@ impl OpenCADStudio { } match (frame, any_free) { (None, _) => DimensionMeasureSpace::Direct, - // A dimension-line / text placement point on bare sheet is normal - // and expected, so only *measuring* points matter. The commands - // collect the measuring points first, so a trailing free point is - // the placement click; a free point before a viewport point is a - // genuine mixed-space measurement. (Some(f), false) => DimensionMeasureSpace::Viewport(f), - (Some(f), true) => { - if self.dimension_leading_points_share_viewport(f) { - DimensionMeasureSpace::Viewport(f) - } else { - DimensionMeasureSpace::Mixed - } - } + // One origin through a viewport and the other on the sheet: the + // two points are not in a common measurable space. + (Some(_), true) => DimensionMeasureSpace::Mixed, } } - /// True when every snap up to the first free one came through `frame`'s - /// viewport and no later snap came through a viewport. - fn dimension_leading_points_share_viewport(&self, frame: ViewportFrame) -> bool { - let mut seen_free = false; - for snap in &self.dim_accepted_snaps { - match snap.frame { - Some(f) => { - if seen_free || f.viewport != frame.viewport { - return false; - } - } - None => seen_free = true, - } - } - true - } - /// Apply viewport compensation to a dimension about to be committed. /// /// Returns `true` when the dimension measures through a viewport, which @@ -204,7 +188,7 @@ impl OpenCADStudio { DimensionMeasureSpace::Mixed => { self.command_line.push_warning( crate::t!( - "Mixed-space measurement is not supported: the points come from different viewports, or from a viewport and the sheet. Measuring on the sheet; viewport scale is not compensated." + "Mixed-space measurement is not supported: the extension origins come from different viewports, or from a viewport and the sheet. Measuring in paper space instead (no viewport scale compensation)." ) .as_ref(), ); @@ -234,6 +218,35 @@ impl OpenCADStudio { true } + /// Explicit "select object" pick for a dimension command, resolved through + /// a paper-space content viewport. + /// + /// Paper-space hit testing only sees sheet entities, so a click on model + /// geometry displayed inside a viewport finds nothing. This maps the + /// cursor into model space through the viewport frame, hit-tests the + /// viewport's resident model wires (descending into block instances and + /// baking the instance transform), then hands the command a copy of the + /// picked entity projected onto the sheet. The commands therefore keep + /// working entirely in paper coordinates, and the measurement is + /// compensated at commit time like any other viewport measurement. + /// + /// Returns `None` when the click is not inside a content viewport or hits + /// nothing, so the caller falls back to the ordinary paper-space pick. + pub(crate) fn try_dimension_viewport_entity_pick( + &mut self, + i: usize, + paper: DVec3, + aperture_paper: f64, + ) -> Option { + let pick = self.tabs[i] + .scene + .dimension_pick_through_viewport(paper, aperture_paper)?; + self.record_dimension_viewport_pick(i, pick.frame, pick.paper_point, pick.model_point); + let command = self.tabs[i].active_cmd.as_mut()?; + command.inject_picked_entity(pick.paper_entity); + Some(command.on_entity_pick(pick.entity_handle, pick.paper_point)) + } + /// The DIMLFAC the pending dimension would inherit *without* a viewport /// override: its own existing override if it has one, otherwise its named /// dimension style's value. diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 6290e08ea..bd2708751 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3824,7 +3824,29 @@ impl OpenCADStudio { command.set_ctrl(self.ctrl_down); command.set_shift(self.shift_down); } - let result = if self.tabs[i] + // PR2: a dimension "select object" pick that lands inside a paper + // content viewport is resolved against the model geometry that + // viewport displays. Paper hit testing only sees sheet entities, + // so this runs first and falls through when it finds nothing. + let dimension_viewport_pick = if self.tabs[i].active_cmd.as_ref().is_some_and( + |command| command.needs_entity_pick() && command.measures_through_viewports(), + ) { + let aperture_paper = { + let camera = self.tabs[i].scene.camera.borrow(); + let per_px = if bounds.height > 0.0 { + camera.ortho_size() as f64 * 2.0 / bounds.height as f64 + } else { + 0.0 + }; + crate::ui::overlay::pick_box_aperture_px(self.pick_box) as f64 * per_px + }; + self.try_dimension_viewport_entity_pick(i, pick_wcs, aperture_paper) + } else { + None + }; + let result = if let Some(result) = dimension_viewport_pick { + Some(result) + } else if self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_structure_point_pick())