forked from 1b-agent/opencadstudio-upstream
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:
1 parent
5383c64d5d
commit
dc10c5dfba
4 files changed
+94
-15
No files matched your search
@@ -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
@@ -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" => {
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user