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:
Hakan Seven committed 2026-08-05 00:26:29 +03:00
1 parent 474bfcc7db
commit b92b8df997
4 files changed
+263 -21

No files matched your search

+25
View File
@@ -1169,6 +1169,31 @@ impl OpenCADStudio {
}
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) => {
// Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
if new_entities.len() == 1 {
+159 -2
View File
@@ -2525,6 +2525,12 @@ impl OpenCADStudio {
.as_ref()
.map(|c| c.is_selection_gathering())
.unwrap_or(false);
let selection_box_active = self.tabs[i]
.scene
.selection
.borrow()
.box_anchor
.is_some();
let selection_pick_add = self.pick_add
|| self.tabs[i]
.active_cmd
@@ -2538,7 +2544,12 @@ impl OpenCADStudio {
.map(|c| c.window_corner_pick())
.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 bounds = iced::Rectangle {
x: 0.0,
@@ -2893,8 +2904,21 @@ impl OpenCADStudio {
}
}
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 {
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
}
} else if self.tabs[i]
@@ -3023,6 +3047,91 @@ impl OpenCADStudio {
};
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 {
// PICKDRAG 1 (#226): the press-drag spanned a
// rectangle through the box machinery — complete
@@ -3431,6 +3540,54 @@ impl OpenCADStudio {
}
} else {
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 (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)]
+20
View File
@@ -1025,6 +1025,8 @@ pub enum CmdResult {
/// Replace / delete multiple entities and add new ones; command ends.
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
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,
/// 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.
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.
/// Return preview wires showing the operation result under the cursor.
/// Default: empty (no preview).
+59 -19
View File
@@ -3085,6 +3085,23 @@ impl TrimCommand {
}
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 {
@@ -3247,31 +3264,54 @@ impl CadCommand for TrimCommand {
}
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
// that were appended with NULL handles. Assign their real document handles.
let start = self.all_entities.len().saturating_sub(new_handles.len());
for (e, &h) in self.all_entities[start..]
// Batch gestures stage several NULL-handle replacement groups before
// the document assigns real handles. The host applies them in the same
// order, so fill the first remaining placeholders on each callback.
let mut handles = new_handles.iter().copied();
for entity in self
.all_entities
.iter_mut()
.zip(new_handles.iter())
.filter(|entity| entity.common().handle.is_null())
{
match e {
EntityType::Line(l) => l.common.handle = h,
EntityType::Arc(a) => a.common.handle = h,
EntityType::Ray(r) => r.common.handle = h,
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,
_ => {}
}
let Some(handle) = handles.next() else {
break;
};
entity.as_entity_mut().set_handle(handle);
}
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> {
match self.mode {
TrimMode::SelectEdges => {