forked from 1b-agent/opencadstudio-upstream
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:
1 parent
474bfcc7db
commit
b92b8df997
4 files changed
+263
-21
No files matched your search
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user