feat(edit): STRETCH crossing window selects objects implicitly (#338)

Bare STRETCH used to need a separate object-selection round before the
crossing window. Now the window itself picks the objects: with no prior
selection the command emits StretchWindow, the host gathers entities
whose world AABB touches the window (locked layers excluded) and
restarts at the base-point step. Over-selection is harmless since only
points inside the window move. The noun-verb flow (select first, then
STRETCH) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 22:35:35 +03:00
1 parent 5383c64d5d
commit dc10c5dfba
4 files changed
+94 -15

No files matched your search

+43
View File
@@ -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<Handle> = Vec::new();
{
let scene = &self.tabs[i].scene;
let mut seen: rustc_hash::FxHashSet<Handle> = 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,
+13 -11
View File
@@ -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" => {
+4
View File
@@ -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<Handle>,
+34 -4
View File
@@ -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<Handle>,
wire_models: Vec<WireModel>,
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
}