From 8a45a84c436e3189ec6e512dfd2dbe88f94c911d Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 12 Sep 2026 21:41:34 -0300 Subject: [PATCH] fix(stretch): highlight affected vertices during selection --- src/app/command_driver.rs | 75 +++++++++++++++++++++++++++++++++++++++ src/app/view/mod.rs | 8 ++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 15c3ea5b7..d56df0dad 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4169,6 +4169,81 @@ impl OpenCADStudio { self.command_line.push_output( crate::t!("STRETCH: nothing crosses the window.").as_ref(), ); + } else { + // Visual feedback must only highlight entities that actually have a + // stretchable point inside one of the crossing windows. `handles` can + // contain the whole candidate/preselection set, while `windows` decides + // which vertices will really move. + let point_inside = |point: glam::DVec3| { + const EPS: f64 = 1.0e-9; + + windows.iter().any(|(win_min, win_max)| { + point.x >= win_min.x - EPS + && point.x <= win_max.x + EPS + && point.y >= win_min.y - EPS + && point.y <= win_max.y + EPS + && point.z >= win_min.z - EPS + && point.z <= win_max.z + EPS + }) + }; + + let mut visual_handles = Vec::new(); + + for &handle in &handles { + let wires = self.tabs[i].scene.wire_models_for(&[handle]); + + let affected = wires.iter().any(|wire| { + // LINE / LWPOLYLINE / other vertex-based entities. + let vertex_hit = wire + .key_vertices + .iter() + .any(|point| point_inside(glam::DVec3::from_array(*point))); + + if vertex_hit { + return true; + } + + // Whole-object STRETCH cases such as circles/arcs/inserts/points: + // use only their meaningful anchor snap, not quadrants or + // tessellated display points. + wire.snap_pts.iter().any(|(point, hint)| { + matches!( + hint, + crate::scene::model::wire_model::SnapHint::Center + | crate::scene::model::wire_model::SnapHint::Insertion + | crate::scene::model::wire_model::SnapHint::Node + ) && point_inside(*point) + }) + }); + + if affected { + visual_handles.push(handle); + } + } + + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entities(&visual_handles); + self.refresh_selected_grips(); + // STRETCH temporary grips: keep only the actual vertices/endpoints + // that lie inside one of the accumulated crossing windows. + // + // Mid-segment grips are deliberately excluded: STRETCH moves vertices, + // not the midpoint affordances used by normal grip editing. + let mut stretch_grips = Vec::new(); + let mut stretch_grip_handles = Vec::new(); + + for (handle, grip) in std::mem::take(&mut self.tabs[i].selected_grip_handles) + .into_iter() + .zip(std::mem::take(&mut self.tabs[i].selected_grips)) + { + if !grip.is_midpoint && point_inside(grip.world) { + stretch_grip_handles.push(handle); + stretch_grips.push(grip); + } + } + + self.tabs[i].selected_grip_handles = stretch_grip_handles; + self.tabs[i].selected_grips = stretch_grips; } use crate::command::CadCommand; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 4d9e4c785..7e937e0e0 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -489,8 +489,14 @@ bg={bg_ms:.1}ms n={view_count}" let snap_ext_base = tab.snap_result.and_then(|s| s.extension_base); let snap_ext_base2 = tab.snap_result.and_then(|s| s.extension_base2); + let show_grips = tab.active_cmd.is_none() + || tab + .active_cmd + .as_ref() + .is_some_and(|cmd| cmd.name() == "STRETCH"); + let grips: Vec = - if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { + if show_grips && !tab.selected_grips.is_empty() { let (vw, vh) = sel_ref.vp_size; // Overlays project through the active tile's camera, so // they must use the active tile's screen rectangle (with