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:
parent
5383c64d5d
commit
dc10c5dfba
4 changed files with 94 additions and 15 deletions
|
|
@ -1404,6 +1404,49 @@ impl OpenCADStudio {
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
return self.on_quick_print_handles(handles);
|
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 {
|
CmdResult::StretchEntities {
|
||||||
handles,
|
handles,
|
||||||
win_min,
|
win_min,
|
||||||
|
|
|
||||||
|
|
@ -738,25 +738,27 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
"STRETCH" => {
|
"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]
|
let handles: Vec<_> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.selected_entities()
|
.selected_entities()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(h, _)| h)
|
.map(|(h, _)| h)
|
||||||
.collect();
|
.collect();
|
||||||
if handles.is_empty() {
|
let wires = if handles.is_empty() {
|
||||||
use crate::modules::draw::select::SelectObjectsCommand;
|
Vec::new()
|
||||||
let cmd = SelectObjectsCommand::new("STRETCH");
|
|
||||||
self.command_line.push_info(&cmd.prompt());
|
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
||||||
} else {
|
} else {
|
||||||
use crate::modules::draw::modify::stretch::StretchCommand;
|
self.tabs[i].scene.wire_models_for(&handles)
|
||||||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
};
|
||||||
let new_cmd = StretchCommand::new(handles, wires);
|
let new_cmd = StretchCommand::new(handles, wires);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
"FILLET" => {
|
"FILLET" => {
|
||||||
use crate::modules::draw::modify::fillet::FilletCommand;
|
use crate::modules::draw::modify::fillet::FilletCommand;
|
||||||
|
|
|
||||||
|
|
@ -908,6 +908,10 @@ pub enum CmdResult {
|
||||||
scale: f32,
|
scale: f32,
|
||||||
angle: 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.
|
/// Stretch entities: move only vertices/endpoints inside the crossing window.
|
||||||
StretchEntities {
|
StretchEntities {
|
||||||
handles: Vec<Handle>,
|
handles: Vec<Handle>,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,22 @@ impl StretchCommand {
|
||||||
step: Step::WindowCorner1,
|
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 {
|
impl CadCommand for StretchCommand {
|
||||||
|
|
@ -73,10 +89,18 @@ impl CadCommand for StretchCommand {
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
Step::WindowCorner1 => format!(
|
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]:",
|
"STRETCH Specify first corner of crossing window [{} objects]:",
|
||||||
self.handles.len()
|
self.handles.len()
|
||||||
),
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Step::WindowCorner2(_) => "STRETCH Specify opposite corner:".into(),
|
Step::WindowCorner2(_) => "STRETCH Specify opposite corner:".into(),
|
||||||
Step::Base { .. } => "STRETCH Specify base point:".into(),
|
Step::Base { .. } => "STRETCH Specify base point:".into(),
|
||||||
Step::Target { base, .. } => format!(
|
Step::Target { base, .. } => format!(
|
||||||
|
|
@ -95,6 +119,12 @@ impl CadCommand for StretchCommand {
|
||||||
Step::WindowCorner2(c1) => {
|
Step::WindowCorner2(c1) => {
|
||||||
let win_min = c1.min(pt);
|
let win_min = c1.min(pt);
|
||||||
let win_max = c1.max(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 };
|
self.step = Step::Base { win_min, win_max };
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue