diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b8f6c501c..e6b5e0a35 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1404,6 +1404,49 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); return self.on_quick_print_handles(handles); } + CmdResult::StretchWindow { win_min, win_max } => { + // Implicit STRETCH selection (#338): the crossing window drawn + // with no prior selection picks the objects itself. Entities + // whose world AABB touches the window are handed back to the + // command at the base-point step — over-selection is harmless, + // since only points INSIDE the window move anyway. + let mut handles: Vec = Vec::new(); + { + let scene = &self.tabs[i].scene; + let mut seen: rustc_hash::FxHashSet = rustc_hash::FxHashSet::default(); + for w in scene.entity_wires().iter() { + let Some(h) = crate::scene::Scene::handle_from_wire_name(&w.name) else { + continue; + }; + if !seen.insert(h) || scene.is_layer_locked(h) { + continue; + } + let [ax, ay, bx, by] = w.aabb; + if ax as f64 <= win_max.x + && bx as f64 >= win_min.x + && ay as f64 <= win_max.y + && by as f64 >= win_min.y + { + handles.push(h); + } + } + } + if handles.is_empty() { + self.command_line + .push_output("STRETCH: nothing crosses the window."); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } else { + use crate::command::CadCommand; + use crate::modules::draw::modify::stretch::StretchCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let cmd = StretchCommand::with_window(handles, wires, win_min, win_max); + self.command_line.push_info(&CadCommand::prompt(&cmd)); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } CmdResult::StretchEntities { handles, win_min, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index c9ea08849..ccadcbbc4 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -738,24 +738,26 @@ impl OpenCADStudio { } "STRETCH" => { + // With a prior selection the window only marks which points + // move. With none, the crossing window itself selects the + // objects — one round instead of a separate object-selection + // step (#338); the command emits StretchWindow for the host + // to resolve. + use crate::modules::draw::modify::stretch::StretchCommand; let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("STRETCH"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); + let wires = if handles.is_empty() { + Vec::new() } else { - use crate::modules::draw::modify::stretch::StretchCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = StretchCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } + self.tabs[i].scene.wire_models_for(&handles) + }; + let new_cmd = StretchCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "FILLET" => { diff --git a/src/command.rs b/src/command.rs index f854107b6..637ced4be 100644 --- a/src/command.rs +++ b/src/command.rs @@ -908,6 +908,10 @@ pub enum CmdResult { scale: f32, angle: f32, }, + /// Implicit STRETCH selection: the crossing window was drawn with no prior + /// selection, so the host resolves which entities it touches and restarts + /// the command at the base-point step with them. (#338) + StretchWindow { win_min: DVec3, win_max: DVec3 }, /// Stretch entities: move only vertices/endpoints inside the crossing window. StretchEntities { handles: Vec, diff --git a/src/modules/draw/modify/stretch.rs b/src/modules/draw/modify/stretch.rs index 53c5d52f1..a758527dd 100644 --- a/src/modules/draw/modify/stretch.rs +++ b/src/modules/draw/modify/stretch.rs @@ -64,6 +64,22 @@ impl StretchCommand { step: Step::WindowCorner1, } } + + /// Restart at the base-point step with the window already known — the + /// implicit-selection flow: the host resolved which entities the crossing + /// window touches and hands them in together with the window. (#338) + pub fn with_window( + handles: Vec, + wire_models: Vec, + win_min: DVec3, + win_max: DVec3, + ) -> Self { + Self { + handles, + wire_models, + step: Step::Base { win_min, win_max }, + } + } } impl CadCommand for StretchCommand { @@ -73,10 +89,18 @@ impl CadCommand for StretchCommand { fn prompt(&self) -> String { match &self.step { - Step::WindowCorner1 => format!( - "STRETCH Specify first corner of crossing window [{} objects]:", - self.handles.len() - ), + Step::WindowCorner1 => { + if self.handles.is_empty() { + // Implicit mode: the window both selects the objects and + // marks which of their points move. + "STRETCH Specify first corner of crossing window:".into() + } else { + format!( + "STRETCH Specify first corner of crossing window [{} objects]:", + self.handles.len() + ) + } + } Step::WindowCorner2(_) => "STRETCH Specify opposite corner:".into(), Step::Base { .. } => "STRETCH Specify base point:".into(), Step::Target { base, .. } => format!( @@ -95,6 +119,12 @@ impl CadCommand for StretchCommand { Step::WindowCorner2(c1) => { let win_min = c1.min(pt); let win_max = c1.max(pt); + if self.handles.is_empty() { + // Implicit mode: hand the window to the host, which + // resolves the touched entities and restarts the command + // at the base-point step via `with_window`. (#338) + return CmdResult::StretchWindow { win_min, win_max }; + } self.step = Step::Base { win_min, win_max }; CmdResult::NeedPoint }