Merge pull request #772 from gianlucafiore/fix/stretch-multi-selection
Allow multiple selection windows in STRETCH
This commit is contained in:
commit
e901e72d6d
4 changed files with 174 additions and 113 deletions
|
|
@ -2568,48 +2568,56 @@ 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();
|
||||
CmdResult::StretchWindow {
|
||||
mut handles,
|
||||
windows,
|
||||
} => {
|
||||
// Accumulate every entity touched by any crossing window. Keep STRETCH
|
||||
// in its selection stage; Enter is what advances to the base point.
|
||||
{
|
||||
let scene = &self.tabs[i].scene;
|
||||
handles.extend(
|
||||
scene
|
||||
.interaction_handles_in_world_aabb([
|
||||
win_min.x, win_min.y, win_max.x, win_max.y,
|
||||
])
|
||||
.into_iter()
|
||||
.filter(|&h| !scene.is_layer_locked(h)),
|
||||
|
||||
for (win_min, win_max) in &windows {
|
||||
handles.extend(
|
||||
scene
|
||||
.interaction_handles_in_world_aabb([
|
||||
win_min.x,
|
||||
win_min.y,
|
||||
win_max.x,
|
||||
win_max.y,
|
||||
])
|
||||
.into_iter()
|
||||
.filter(|&handle| !scene.is_layer_locked(handle)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handles.sort_unstable_by_key(|handle| handle.value());
|
||||
handles.dedup();
|
||||
|
||||
if handles.is_empty() {
|
||||
self.command_line.push_output(
|
||||
crate::t!("STRETCH: nothing crosses the window.").as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::modify::stretch::StretchCommand;
|
||||
// A window that caught nothing is a missed aim, not a decision
|
||||
// to stop. Ending the command there made the user restart it to
|
||||
// try again; instead say so and ask for the corner afresh, the
|
||||
// way a selection that picks nothing leaves MOVE still asking.
|
||||
// (#676)
|
||||
let cmd = if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("STRETCH: nothing crosses the window.").as_ref());
|
||||
StretchCommand::new(Vec::new(), Vec::new())
|
||||
} else {
|
||||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||||
StretchCommand::with_window(handles, wires, win_min, win_max)
|
||||
};
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
||||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||||
|
||||
let cmd = StretchCommand::with_windows(
|
||||
handles,
|
||||
wires,
|
||||
windows,
|
||||
);
|
||||
|
||||
self.command_line.push_info(&CadCommand::prompt(&cmd));
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
CmdResult::StretchEntities {
|
||||
mut handles,
|
||||
win_min,
|
||||
win_max,
|
||||
windows,
|
||||
delta,
|
||||
} => {
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
|
|
@ -2630,7 +2638,12 @@ impl OpenCADStudio {
|
|||
// Helper: is DXF point (x, y) inside the world-space window?
|
||||
// Drawing plane is world XY (= DXF XY).
|
||||
let in_win = |x: f64, y: f64| -> bool {
|
||||
x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y
|
||||
windows.iter().any(|(win_min, win_max)| {
|
||||
x >= win_min.x
|
||||
&& x <= win_max.x
|
||||
&& y >= win_min.y
|
||||
&& y <= win_max.y
|
||||
})
|
||||
};
|
||||
|
||||
let dx = delta.x as f64;
|
||||
|
|
@ -2739,8 +2752,14 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
acadrust::EntityType::Viewport(vp) => {
|
||||
stretched =
|
||||
crate::entities::viewport::stretch(vp, win_min, win_max, delta);
|
||||
stretched = windows.iter().any(|(win_min, win_max)| {
|
||||
crate::entities::viewport::stretch(
|
||||
vp,
|
||||
*win_min,
|
||||
*win_max,
|
||||
delta,
|
||||
)
|
||||
});
|
||||
}
|
||||
acadrust::EntityType::Dimension(dim) => {
|
||||
use acadrust::entities::Dimension;
|
||||
|
|
|
|||
|
|
@ -1335,18 +1335,21 @@ 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.
|
||||
/// STRETCH crossing-window selection. The command can accumulate several
|
||||
/// independent crossing windows before Enter ends the selection stage.
|
||||
StretchWindow {
|
||||
/// Handles already gathered by previous crossing windows / preselection.
|
||||
handles: Vec<Handle>,
|
||||
/// Every crossing window gathered so far.
|
||||
windows: Vec<(DVec3, DVec3)>,
|
||||
},
|
||||
/// Stretch entities: move only vertices/endpoints inside any gathered
|
||||
/// crossing window.
|
||||
StretchEntities {
|
||||
handles: Vec<Handle>,
|
||||
/// Min corner of the crossing window in world XZ (= DXF XY).
|
||||
win_min: DVec3,
|
||||
/// Max corner of the crossing window in world XZ (= DXF XY).
|
||||
win_max: DVec3,
|
||||
/// Translation vector to apply to vertices inside the window.
|
||||
/// Independent crossing windows that define the points to move.
|
||||
windows: Vec<(DVec3, DVec3)>,
|
||||
/// Translation vector applied once to every selected point.
|
||||
delta: DVec3,
|
||||
},
|
||||
/// Create a Solid3D placeholder entity + associated MeshModel.
|
||||
|
|
|
|||
|
|
@ -37,16 +37,14 @@ pub fn tool() -> ToolDef {
|
|||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
/// Waiting for the first crossing-window corner.
|
||||
/// Waiting for the first corner of another crossing window.
|
||||
WindowCorner1,
|
||||
/// Waiting for the second corner; `c1` is the first corner.
|
||||
/// Waiting for the opposite corner.
|
||||
WindowCorner2(DVec3),
|
||||
/// Crossing window defined; waiting for base point.
|
||||
Base { win_min: DVec3, win_max: DVec3 },
|
||||
/// Waiting for target point.
|
||||
/// Selection is complete; waiting for the displacement base point.
|
||||
Base,
|
||||
/// Waiting for the displacement target.
|
||||
Target {
|
||||
win_min: DVec3,
|
||||
win_max: DVec3,
|
||||
base: DVec3,
|
||||
},
|
||||
}
|
||||
|
|
@ -54,6 +52,8 @@ enum Step {
|
|||
pub struct StretchCommand {
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
/// Independent crossing windows accumulated during the selection stage.
|
||||
windows: Vec<(DVec3, DVec3)>,
|
||||
step: Step,
|
||||
}
|
||||
|
||||
|
|
@ -62,26 +62,25 @@ impl StretchCommand {
|
|||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
windows: Vec::new(),
|
||||
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(
|
||||
/// Continue gathering crossing windows after the host has resolved the
|
||||
/// entities touched by the latest window.
|
||||
pub fn with_windows(
|
||||
handles: Vec<Handle>,
|
||||
wire_models: Vec<WireModel>,
|
||||
win_min: DVec3,
|
||||
win_max: DVec3,
|
||||
windows: Vec<(DVec3, DVec3)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
handles,
|
||||
wire_models,
|
||||
step: Step::Base { win_min, win_max },
|
||||
windows,
|
||||
step: Step::WindowCorner1,
|
||||
}
|
||||
}
|
||||
|
||||
/// A window enclosing every vertex of the current selection, or `None`
|
||||
/// when nothing is selected. Padded so a vertex exactly on the boundary
|
||||
/// counts as inside rather than depending on the comparison's edge.
|
||||
|
|
@ -127,24 +126,22 @@ impl CadCommand for StretchCommand {
|
|||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::WindowCorner1 => {
|
||||
if self.handles.is_empty() {
|
||||
// Implicit mode: the window both selects the objects and
|
||||
// marks which of their points move.
|
||||
if self.windows.is_empty() && self.handles.is_empty() {
|
||||
t!("STRETCH Specify first corner of crossing window:").into_owned()
|
||||
} else {
|
||||
// The window narrows a selection that already exists, so
|
||||
// it is optional — Enter takes the whole of it, the way a
|
||||
// selection ends in MOVE or COPY. (#676)
|
||||
t!(
|
||||
"STRETCH Specify first corner of crossing window, or press Enter to stretch all [%{count} objects]:",
|
||||
count = self.handles.len()
|
||||
"STRETCH Specify first corner of another crossing window, or press Enter to continue:"
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
Step::WindowCorner2(_) => t!("STRETCH Specify opposite corner:").into_owned(),
|
||||
Step::Base { .. } => t!("STRETCH Specify base point:").into_owned(),
|
||||
Step::Target { base, .. } => {
|
||||
Step::WindowCorner2(_) => {
|
||||
t!("STRETCH Specify opposite corner:").into_owned()
|
||||
}
|
||||
Step::Base => {
|
||||
t!("STRETCH Specify base point:").into_owned()
|
||||
}
|
||||
Step::Target { base } => {
|
||||
let bx = format!("{:.3}", base.x);
|
||||
let bz = format!("{:.3}", base.z);
|
||||
t!(
|
||||
|
|
@ -163,37 +160,34 @@ impl CadCommand for StretchCommand {
|
|||
self.step = Step::WindowCorner2(pt);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
let mut windows = self.windows.clone();
|
||||
windows.push((win_min, win_max));
|
||||
|
||||
// Hand the accumulated selection back to the host. The host resolves
|
||||
// the entities touched by this window and relaunches STRETCH still in
|
||||
// the selection stage, so another crossing window can be drawn.
|
||||
CmdResult::StretchWindow {
|
||||
handles: self.handles.clone(),
|
||||
windows,
|
||||
}
|
||||
self.step = Step::Base { win_min, win_max };
|
||||
}
|
||||
|
||||
Step::Base => {
|
||||
self.step = Step::Target { base: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Base { win_min, win_max } => {
|
||||
let (wmin, wmax) = (*win_min, *win_max);
|
||||
self.step = Step::Target {
|
||||
win_min: wmin,
|
||||
win_max: wmax,
|
||||
base: pt,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Target {
|
||||
win_min,
|
||||
win_max,
|
||||
base,
|
||||
} => {
|
||||
|
||||
Step::Target { base } => {
|
||||
let delta = pt - *base;
|
||||
|
||||
CmdResult::StretchEntities {
|
||||
handles: self.handles.clone(),
|
||||
win_min: *win_min,
|
||||
win_max: *win_max,
|
||||
windows: self.windows.clone(),
|
||||
delta,
|
||||
}
|
||||
}
|
||||
|
|
@ -201,17 +195,22 @@ impl CadCommand for StretchCommand {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// Enter ends the selection stage, as it does in MOVE and COPY. With
|
||||
// objects already picked there is nothing left to choose: a window that
|
||||
// contains all of them stretches every vertex, which is the whole
|
||||
// selection moving — what a pick-selected stretch means. Without a
|
||||
// selection there is nothing to end, so Enter still cancels. (#676)
|
||||
if let Step::WindowCorner1 = self.step {
|
||||
if let Some((win_min, win_max)) = self.selection_bounds() {
|
||||
self.step = Step::Base { win_min, win_max };
|
||||
// Preserve the existing preselection behaviour: if STRETCH started
|
||||
// from a selected set and no explicit crossing window was drawn,
|
||||
// treat its complete bounds as the stretch window.
|
||||
if self.windows.is_empty() {
|
||||
if let Some((win_min, win_max)) = self.selection_bounds() {
|
||||
self.windows.push((win_min, win_max));
|
||||
}
|
||||
}
|
||||
|
||||
if !self.handles.is_empty() && !self.windows.is_empty() {
|
||||
self.step = Step::Base;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
}
|
||||
|
||||
CmdResult::Cancel
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
|
|
@ -239,20 +238,25 @@ impl CadCommand for StretchCommand {
|
|||
// marquee by the host (via window_first_corner) so it matches a
|
||||
// normal box selection — nothing to draw here. (#291)
|
||||
Step::WindowCorner2(_) => vec![],
|
||||
Step::Target {
|
||||
win_min,
|
||||
win_max,
|
||||
base,
|
||||
} => {
|
||||
Step::Target { base } => {
|
||||
let delta = pt - *base;
|
||||
// Live ghost: vertices inside the crossing window follow the
|
||||
// cursor, the rest stay anchored. This is the preview/GPU path,
|
||||
// so downcast to f32 only at the WireModel boundary.
|
||||
|
||||
let windows: Vec<_> = self
|
||||
.windows
|
||||
.iter()
|
||||
.map(|(win_min, win_max)| {
|
||||
(win_min.as_vec3(), win_max.as_vec3())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut out: Vec<WireModel> = self
|
||||
.wire_models
|
||||
.iter()
|
||||
.map(|w| w.stretched((*win_min).as_vec3(), (*win_max).as_vec3(), delta.as_vec3()))
|
||||
.map(|wire| {
|
||||
wire.stretched_windows(&windows, delta.as_vec3())
|
||||
})
|
||||
.collect();
|
||||
|
||||
out.push(WireModel::solid(
|
||||
"rubber_band".into(),
|
||||
vec![
|
||||
|
|
@ -262,6 +266,7 @@ impl CadCommand for StretchCommand {
|
|||
WireModel::CYAN,
|
||||
false,
|
||||
));
|
||||
|
||||
out
|
||||
}
|
||||
_ => vec![],
|
||||
|
|
|
|||
|
|
@ -321,30 +321,64 @@ impl WireModel {
|
|||
/// outside stay put. Exact for line/polyline vertices (the primary stretch
|
||||
/// targets); curve tessellation points may deform where a window edge cuts
|
||||
/// through them, matching the per-vertex nature of the operation.
|
||||
pub fn stretched(&self, win_min: glam::Vec3, win_max: glam::Vec3, delta: glam::Vec3) -> Self {
|
||||
pub fn stretched(
|
||||
&self,
|
||||
win_min: glam::Vec3,
|
||||
win_max: glam::Vec3,
|
||||
delta: glam::Vec3,
|
||||
) -> Self {
|
||||
self.stretched_windows(&[(win_min, win_max)], delta)
|
||||
}
|
||||
|
||||
/// Return a clone for a multi-window STRETCH preview. A point moves exactly
|
||||
/// once when it lies inside any of the crossing windows.
|
||||
pub fn stretched_windows(
|
||||
&self,
|
||||
windows: &[(glam::Vec3, glam::Vec3)],
|
||||
delta: glam::Vec3,
|
||||
) -> Self {
|
||||
let mut out = self.clone();
|
||||
out.name = format!("preview_{}", self.name);
|
||||
out.color = Self::CYAN;
|
||||
out.selected = false;
|
||||
|
||||
let inside = |x: f32, y: f32| {
|
||||
windows.iter().any(|(win_min, win_max)| {
|
||||
x >= win_min.x
|
||||
&& x <= win_max.x
|
||||
&& y >= win_min.y
|
||||
&& y <= win_max.y
|
||||
})
|
||||
};
|
||||
|
||||
for p in &mut out.points {
|
||||
if p[0] >= win_min.x && p[0] <= win_max.x && p[1] >= win_min.y && p[1] <= win_max.y {
|
||||
if inside(p[0], p[1]) {
|
||||
p[0] += delta.x;
|
||||
p[1] += delta.y;
|
||||
p[2] += delta.z;
|
||||
}
|
||||
}
|
||||
|
||||
if !out.text_verts.is_empty() {
|
||||
let (mnx, mny) = (win_min.x as f64, win_min.y as f64);
|
||||
let (mxx, mxy) = (win_max.x as f64, win_max.y as f64);
|
||||
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
|
||||
let (dx, dy, dz) =
|
||||
(delta.x as f64, delta.y as f64, delta.z as f64);
|
||||
|
||||
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||
if x >= mnx && x <= mxx && y >= mny && y <= mxy {
|
||||
let inside = windows.iter().any(|(win_min, win_max)| {
|
||||
x >= win_min.x as f64
|
||||
&& x <= win_max.x as f64
|
||||
&& y >= win_min.y as f64
|
||||
&& y <= win_max.y as f64
|
||||
});
|
||||
|
||||
if inside {
|
||||
(x + dx, y + dy, z + dz)
|
||||
} else {
|
||||
(x, y, z)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue