fix(trim): handle lasso and box selection
Keep selection gestures in the active trim flow so crossed segments are modified instead of selected.\n\nRefs #487
This commit is contained in:
parent
474bfcc7db
commit
b92b8df997
4 changed files with 263 additions and 21 deletions
|
|
@ -1169,6 +1169,31 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
}
|
}
|
||||||
|
CmdResult::ReplaceManyContinue(replacements) => {
|
||||||
|
let label = self.history_label_from_active_cmd(i, "TRIM");
|
||||||
|
self.push_undo_snapshot(i, label);
|
||||||
|
for (handle, entities) in replacements {
|
||||||
|
self.tabs[i].scene.erase_entities(&[handle]);
|
||||||
|
let new_handles: Vec<Handle> = entities
|
||||||
|
.into_iter()
|
||||||
|
.map(|entity| self.tabs[i].scene.add_entity(entity))
|
||||||
|
.collect();
|
||||||
|
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||||
|
command.on_entity_replaced(handle, &new_handles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
if let Some(prompt) = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|command| command.prompt())
|
||||||
|
{
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
}
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
CmdResult::ReplaceEntity(handle, new_entities) => {
|
CmdResult::ReplaceEntity(handle, new_entities) => {
|
||||||
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
|
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
|
||||||
if new_entities.len() == 1 {
|
if new_entities.len() == 1 {
|
||||||
|
|
|
||||||
|
|
@ -2525,6 +2525,12 @@ impl OpenCADStudio {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.is_selection_gathering())
|
.map(|c| c.is_selection_gathering())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let selection_box_active = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selection
|
||||||
|
.borrow()
|
||||||
|
.box_anchor
|
||||||
|
.is_some();
|
||||||
let selection_pick_add = self.pick_add
|
let selection_pick_add = self.pick_add
|
||||||
|| self.tabs[i]
|
|| self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
|
|
@ -2538,7 +2544,12 @@ impl OpenCADStudio {
|
||||||
.map(|c| c.window_corner_pick())
|
.map(|c| c.window_corner_pick())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering {
|
if is_down
|
||||||
|
&& is_click
|
||||||
|
&& self.tabs[i].active_cmd.is_some()
|
||||||
|
&& !is_gathering
|
||||||
|
&& !selection_box_active
|
||||||
|
{
|
||||||
let (vw, vh) = (tile_vw, tile_vh);
|
let (vw, vh) = (tile_vw, tile_vh);
|
||||||
let bounds = iced::Rectangle {
|
let bounds = iced::Rectangle {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
|
|
@ -2893,8 +2904,21 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
|
} else if self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|command| command.accepts_drag_selection())
|
||||||
|
{
|
||||||
|
let anchor_world = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||||
|
let mut selection = self.tabs[i].scene.selection.borrow_mut();
|
||||||
|
selection.box_anchor = Some(p_full);
|
||||||
|
selection.box_current = Some(p_full);
|
||||||
|
selection.box_anchor_world = Some(anchor_world);
|
||||||
|
selection.box_crossing = false;
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_info(crate::t!("Nothing found at that point.").as_ref());
|
self.command_line
|
||||||
|
.push_info(crate::t!("Nothing found at that point.").as_ref());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else if self.tabs[i]
|
} else if self.tabs[i]
|
||||||
|
|
@ -3023,6 +3047,91 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_dragging {
|
if is_dragging {
|
||||||
|
let drag_geometry = if poly_drag {
|
||||||
|
let mut points: Vec<iced::Point> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selection
|
||||||
|
.borrow()
|
||||||
|
.poly_points
|
||||||
|
.iter()
|
||||||
|
.map(|point| iced::Point {
|
||||||
|
x: point.x - tile_off.x,
|
||||||
|
y: point.y - tile_off.y,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if points.last().is_none_or(|last| {
|
||||||
|
(last.x - p.x).abs() > f32::EPSILON
|
||||||
|
|| (last.y - p.y).abs() > f32::EPSILON
|
||||||
|
}) {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
if let Some(first) = points.first().copied() {
|
||||||
|
points.push(first);
|
||||||
|
}
|
||||||
|
let fence = points
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| {
|
||||||
|
let world = self.cursor_model_point(i, &edit_cam, point, bounds);
|
||||||
|
[world.x, world.y]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Some((fence, None))
|
||||||
|
} else if let Some(anchor) = box_anchor {
|
||||||
|
let points = [
|
||||||
|
anchor,
|
||||||
|
iced::Point::new(anchor.x, p.y),
|
||||||
|
p,
|
||||||
|
iced::Point::new(p.x, anchor.y),
|
||||||
|
anchor,
|
||||||
|
];
|
||||||
|
let fence = points
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| {
|
||||||
|
let world = self.cursor_model_point(i, &edit_cam, point, bounds);
|
||||||
|
[world.x, world.y]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let (min, max) = fence.iter().fold(
|
||||||
|
(
|
||||||
|
[f64::INFINITY, f64::INFINITY],
|
||||||
|
[f64::NEG_INFINITY, f64::NEG_INFINITY],
|
||||||
|
),
|
||||||
|
|(mut min, mut max), point| {
|
||||||
|
min[0] = min[0].min(point[0]);
|
||||||
|
min[1] = min[1].min(point[1]);
|
||||||
|
max[0] = max[0].max(point[0]);
|
||||||
|
max[1] = max[1].max(point[1]);
|
||||||
|
(min, max)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Some((fence, Some((min, max))))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((fence, window)) = drag_geometry {
|
||||||
|
let result = self.tabs[i].active_cmd.as_mut().and_then(|command| {
|
||||||
|
command.set_shift(self.shift_down);
|
||||||
|
command.on_drag_selection(&fence, window)
|
||||||
|
});
|
||||||
|
if let Some(result) = result {
|
||||||
|
let mut selection = self.tabs[i].scene.selection.borrow_mut();
|
||||||
|
selection.left_down = false;
|
||||||
|
selection.left_press_pos = None;
|
||||||
|
selection.left_press_time = None;
|
||||||
|
selection.left_dragging = false;
|
||||||
|
selection.poly_active = false;
|
||||||
|
selection.poly_points.clear();
|
||||||
|
selection.poly_crossing = false;
|
||||||
|
selection.box_anchor = None;
|
||||||
|
selection.box_anchor_world = None;
|
||||||
|
selection.box_current = None;
|
||||||
|
selection.box_crossing = false;
|
||||||
|
drop(selection);
|
||||||
|
return self.apply_cmd_result(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !poly_drag {
|
if !poly_drag {
|
||||||
// PICKDRAG 1 (#226): the press-drag spanned a
|
// PICKDRAG 1 (#226): the press-drag spanned a
|
||||||
// rectangle through the box machinery — complete
|
// rectangle through the box machinery — complete
|
||||||
|
|
@ -3431,6 +3540,54 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let a = box_anchor.unwrap();
|
let a = box_anchor.unwrap();
|
||||||
|
let box_points = [
|
||||||
|
a,
|
||||||
|
iced::Point::new(a.x, p.y),
|
||||||
|
p,
|
||||||
|
iced::Point::new(p.x, a.y),
|
||||||
|
a,
|
||||||
|
];
|
||||||
|
let command_fence = box_points
|
||||||
|
.into_iter()
|
||||||
|
.map(|point| {
|
||||||
|
let world = self.cursor_model_point(i, &edit_cam, point, bounds);
|
||||||
|
[world.x, world.y]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let (command_min, command_max) = command_fence.iter().fold(
|
||||||
|
(
|
||||||
|
[f64::INFINITY, f64::INFINITY],
|
||||||
|
[f64::NEG_INFINITY, f64::NEG_INFINITY],
|
||||||
|
),
|
||||||
|
|(mut min, mut max), point| {
|
||||||
|
min[0] = min[0].min(point[0]);
|
||||||
|
min[1] = min[1].min(point[1]);
|
||||||
|
max[0] = max[0].max(point[0]);
|
||||||
|
max[1] = max[1].max(point[1]);
|
||||||
|
(min, max)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let command_result = self.tabs[i].active_cmd.as_mut().and_then(|command| {
|
||||||
|
command.set_shift(self.shift_down);
|
||||||
|
command.on_drag_selection(
|
||||||
|
&command_fence,
|
||||||
|
Some((command_min, command_max)),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if let Some(result) = command_result {
|
||||||
|
let mut selection = self.tabs[i].scene.selection.borrow_mut();
|
||||||
|
selection.left_down = false;
|
||||||
|
selection.left_press_pos = None;
|
||||||
|
selection.left_press_time = None;
|
||||||
|
selection.left_dragging = false;
|
||||||
|
selection.box_anchor = None;
|
||||||
|
selection.box_anchor_world = None;
|
||||||
|
selection.box_current = None;
|
||||||
|
selection.box_crossing = false;
|
||||||
|
drop(selection);
|
||||||
|
return self.apply_cmd_result(result);
|
||||||
|
}
|
||||||
|
|
||||||
let crossing = box_crossing;
|
let crossing = box_crossing;
|
||||||
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
|
||||||
let world_aabb = [a, iced::Point::new(a.x, p.y), p, iced::Point::new(p.x, a.y)]
|
let world_aabb = [a, iced::Point::new(a.x, p.y), p, iced::Point::new(p.x, a.y)]
|
||||||
|
|
|
||||||
|
|
@ -1025,6 +1025,8 @@ pub enum CmdResult {
|
||||||
/// Replace / delete multiple entities and add new ones; command ends.
|
/// Replace / delete multiple entities and add new ones; command ends.
|
||||||
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
|
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
|
||||||
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
|
||||||
|
/// Replace several entities as one undo step while keeping the command active.
|
||||||
|
ReplaceManyContinue(Vec<(Handle, Vec<EntityType>)>),
|
||||||
/// Cancel: discard any preview and end the command.
|
/// Cancel: discard any preview and end the command.
|
||||||
Cancel,
|
Cancel,
|
||||||
/// Cancel because the active drawing space changed. Cleanup is identical
|
/// Cancel because the active drawing space changed. Cleanup is identical
|
||||||
|
|
@ -1561,6 +1563,24 @@ pub trait CadCommand: Send {
|
||||||
/// Commands that stay active across replaces should update their internal snapshots here.
|
/// Commands that stay active across replaces should update their internal snapshots here.
|
||||||
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
|
fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) {}
|
||||||
|
|
||||||
|
/// Consume a lasso or drag-box gesture while the command is active.
|
||||||
|
/// `fence` is the gesture boundary in drawing coordinates; `window` is
|
||||||
|
/// present for rectangular gestures. Returning `Some` prevents the normal
|
||||||
|
/// selection system from selecting the crossed entities.
|
||||||
|
fn on_drag_selection(
|
||||||
|
&mut self,
|
||||||
|
_fence: &[[f64; 2]],
|
||||||
|
_window: Option<([f64; 2], [f64; 2])>,
|
||||||
|
) -> Option<CmdResult> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an empty click may start a two-corner selection box for this
|
||||||
|
/// command instead of being reported as a missed entity pick.
|
||||||
|
fn accepts_drag_selection(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Called on every mouse-move when `needs_entity_pick()` is true.
|
/// Called on every mouse-move when `needs_entity_pick()` is true.
|
||||||
/// Return preview wires showing the operation result under the cursor.
|
/// Return preview wires showing the operation result under the cursor.
|
||||||
/// Default: empty (no preview).
|
/// Default: empty (no preview).
|
||||||
|
|
|
||||||
|
|
@ -3085,6 +3085,23 @@ impl TrimCommand {
|
||||||
}
|
}
|
||||||
CmdResult::ReplaceMany(repl, Vec::new())
|
CmdResult::ReplaceMany(repl, Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stage_replacements(&mut self, replacements: &[(Handle, Vec<EntityType>)]) {
|
||||||
|
for (handle, _) in replacements {
|
||||||
|
if let Some(index) = self
|
||||||
|
.all_entities
|
||||||
|
.iter()
|
||||||
|
.position(|entity| entity.common().handle == *handle)
|
||||||
|
{
|
||||||
|
self.all_entities.remove(index);
|
||||||
|
}
|
||||||
|
self.edge_set.retain(|edge| edge != handle);
|
||||||
|
}
|
||||||
|
for (_, entities) in replacements {
|
||||||
|
self.all_entities.extend(entities.iter().cloned());
|
||||||
|
}
|
||||||
|
self.rebuild_geos();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for TrimCommand {
|
impl CadCommand for TrimCommand {
|
||||||
|
|
@ -3247,31 +3264,54 @@ impl CadCommand for TrimCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[acadrust::Handle]) {
|
fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[acadrust::Handle]) {
|
||||||
// The last new_handles.len() entries in all_entities are the trimmed pieces
|
// Batch gestures stage several NULL-handle replacement groups before
|
||||||
// that were appended with NULL handles. Assign their real document handles.
|
// the document assigns real handles. The host applies them in the same
|
||||||
let start = self.all_entities.len().saturating_sub(new_handles.len());
|
// order, so fill the first remaining placeholders on each callback.
|
||||||
for (e, &h) in self.all_entities[start..]
|
let mut handles = new_handles.iter().copied();
|
||||||
|
for entity in self
|
||||||
|
.all_entities
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(new_handles.iter())
|
.filter(|entity| entity.common().handle.is_null())
|
||||||
{
|
{
|
||||||
match e {
|
let Some(handle) = handles.next() else {
|
||||||
EntityType::Line(l) => l.common.handle = h,
|
break;
|
||||||
EntityType::Arc(a) => a.common.handle = h,
|
};
|
||||||
EntityType::Ray(r) => r.common.handle = h,
|
entity.as_entity_mut().set_handle(handle);
|
||||||
EntityType::XLine(x) => x.common.handle = h,
|
|
||||||
EntityType::Ellipse(e) => e.common.handle = h,
|
|
||||||
EntityType::Spline(s) => s.common.handle = h,
|
|
||||||
// A trimmed (closed or open) polyline is re-emitted as an
|
|
||||||
// LwPolyline; without its real handle it can't be found on a
|
|
||||||
// second pick, so the same polyline couldn't be trimmed twice
|
|
||||||
// in one TRIM command.
|
|
||||||
EntityType::LwPolyline(p) => p.common.handle = h,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.rebuild_geos();
|
self.rebuild_geos();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_drag_selection(
|
||||||
|
&mut self,
|
||||||
|
fence: &[[f64; 2]],
|
||||||
|
window: Option<([f64; 2], [f64; 2])>,
|
||||||
|
) -> Option<CmdResult> {
|
||||||
|
if !matches!(self.mode, TrimMode::Pick) || fence.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let window = window.map(|(min, max)| CrossingWindow {
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
pick: fence[0],
|
||||||
|
});
|
||||||
|
let replacements = fence_pass(
|
||||||
|
&self.all_entities,
|
||||||
|
&self.geos,
|
||||||
|
fence,
|
||||||
|
window,
|
||||||
|
self.shift,
|
||||||
|
);
|
||||||
|
if replacements.is_empty() {
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
self.stage_replacements(&replacements);
|
||||||
|
Some(CmdResult::ReplaceManyContinue(replacements))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepts_drag_selection(&self) -> bool {
|
||||||
|
matches!(self.mode, TrimMode::Pick)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
|
fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
TrimMode::SelectEdges => {
|
TrimMode::SelectEdges => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue