diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 87635b15f..8b80557e9 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -312,6 +312,33 @@ impl OpenCADStudio { true } + /// Refresh screen-space feature picking immediately before point dispatch. + pub(super) fn refresh_command_point_pick_context(&mut self, i: usize) { + if self.tabs[i].active_cmd.as_ref() + .is_some_and(|command| command.wants_point_pick_context()) + { + let scene = &self.tabs[i].scene; + let size = scene.selection.borrow().vp_size; + let edit = scene.viewport_edit_frame(size); + let tile = edit.as_ref().map(|(_, rect)| *rect) + .unwrap_or_else(|| scene.active_model_tile_bounds(size.0, size.1)); + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: tile.width, height: tile.height }; + let context = if bounds.width > 0.0 && bounds.height > 0.0 { + let (view, eye) = if let Some((camera, _)) = edit { + (camera.view_proj_rte(bounds), camera.eye()) + } else { + let camera = scene.camera.borrow(); + (camera.view_proj_rte(bounds), camera.eye()) + }; + Some(crate::command::PointPickContext { view, eye, bounds, + aperture_px: crate::ui::overlay::pick_box_aperture_px(self.pick_box) }) + } else { None }; + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + command.set_point_pick_context(context); + } + } + } + /// Drive the active command's step machine with one [`StepInput`], then /// apply the result. This is the single entry point every input source /// (command line, headless, dynamic input, plugin API, viewport) funnels @@ -414,6 +441,7 @@ impl OpenCADStudio { command.inject_selection_entities(entities); } } + if matches!(&input, StepInput::Point(_)) { self.refresh_command_point_pick_context(i); } let ctrl = self.ctrl_down; let shift = self.shift_down; let result: Option = { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 606ffa871..8dee50e81 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3887,6 +3887,7 @@ impl OpenCADStudio { } else if self.tabs[i].active_cmd.as_ref() .is_some_and(|command| command.entity_pick_accepts_points()) { + self.refresh_command_point_pick_context(i); self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs)) } else if self.tabs[i] .active_cmd @@ -3963,6 +3964,7 @@ impl OpenCADStudio { // (LINE tangent to two circles, which needs both). When // it does, sync last_point to the command's resolved // anchor since it replaced the picked coordinate. + self.refresh_command_point_pick_context(i); let handled = self.tabs[i] .active_cmd .as_mut() diff --git a/src/command.rs b/src/command.rs index d69859462..177d25894 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1939,6 +1939,15 @@ impl InputKind { } } +/// Current screen projection and configured aperture for point-feature picking. +#[derive(Clone, Copy)] +pub struct PointPickContext { + pub view: glam::Mat4, + pub eye: DVec3, + pub bounds: iced::Rectangle, + pub aperture_px: f32, +} + pub trait CadCommand: Send { /// Keep the layer already carried by entities committed by this command /// instead of replacing it with the current drawing layer. @@ -2006,6 +2015,12 @@ pub trait CadCommand: Send { /// Called when the user left-clicks in the viewport (point pick). fn on_point(&mut self, pt: DVec3) -> CmdResult; + /// Opt in only while a point input selects an existing point feature. + fn wants_point_pick_context(&self) -> bool { false } + + /// Refreshed for each point input so zoom and viewport changes are reflected. + fn set_point_pick_context(&mut self, _context: Option) {} + /// Called when the user presses Enter (finalize / next option). fn on_enter(&mut self) -> CmdResult; diff --git a/src/modules/draw/modify/splinedit.rs b/src/modules/draw/modify/splinedit.rs index e17c0da1a..379620e27 100644 --- a/src/modules/draw/modify/splinedit.rs +++ b/src/modules/draw/modify/splinedit.rs @@ -47,11 +47,31 @@ pub struct SplineditCommand { spline: Option, pending: Option, history: Vec<(acadrust::Handle, acadrust::entities::Spline)>, + pick_context: Option, } impl SplineditCommand { pub fn new() -> Self { - Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new() } + Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None } + } + + fn picked_vertex(&self, point: DVec3) -> Option { + let context = self.pick_context?; + let project = |point: DVec3| { + let clip = context.view * (point - context.eye).as_vec3().extend(1.0); + if !clip.is_finite() || clip.w <= 0.0 { return None; } + let screen = crate::scene::pick::hit_test::world_to_screen( + point, context.view, context.eye, context.bounds); + (screen.x.is_finite() && screen.y.is_finite() + && screen.x >= 0.0 && screen.x <= context.bounds.width + && screen.y >= 0.0 && screen.y <= context.bounds.height).then_some(screen) + }; + let cursor = project(point)?; + self.spline.as_ref()?.control_points.iter().enumerate().filter_map(|(index, vertex)| { + let screen = project(DVec3::new(vertex.x, vertex.y, vertex.z))?; + let distance = (screen.x - cursor.x).hypot(screen.y - cursor.y); + (distance.is_finite() && distance <= context.aperture_px).then_some((index, distance)) + }).min_by(|a, b| a.1.total_cmp(&b.1)).map(|(index, _)| index) } fn replace(&mut self, spline: acadrust::entities::Spline) -> CmdResult { @@ -194,25 +214,24 @@ impl CadCommand for SplineditCommand { } Some(CmdResult::NeedPoint) } + fn wants_point_pick_context(&self) -> bool { + matches!(self.step, Step::Delete | Step::SelectVertex { .. }) + } + fn set_point_pick_context(&mut self, context: Option) { + self.pick_context = context; + } fn on_point(&mut self, point: DVec3) -> CmdResult { if !point.is_finite() { return CmdResult::NeedPoint; } match self.step { Step::SelectVertex { weight, refine } => { - if let Some(index) = self.spline.as_ref().and_then(|spline| spline.control_points.iter().enumerate() - .min_by(|(_, a), (_, b)| { - let distance = |p: &Vector3| point.distance_squared(DVec3::new(p.x, p.y, p.z)); - distance(a).total_cmp(&distance(b)) - }).map(|(index, _)| index)) { + if let Some(index) = self.picked_vertex(point) { self.step = if weight { Step::Weight { index } } else { Step::Move { index, refine } }; } CmdResult::NeedPoint } Step::Delete => { let Some(source) = self.spline.as_ref() else { return CmdResult::NeedPoint; }; - let Some(index) = source.control_points.iter().enumerate().min_by(|(_, a), (_, b)| { - let distance = |p: &Vector3| point.distance_squared(DVec3::new(p.x, p.y, p.z)); - distance(a).total_cmp(&distance(b)) - }).map(|(index, _)| index) else { return CmdResult::NeedPoint; }; + let Some(index) = self.picked_vertex(point) else { return CmdResult::NeedPoint; }; let controls = source.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(); let weights = if source.weights.is_empty() { vec![1.0; source.control_points.len()] } else { source.weights.clone() };