diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b3a314073..bffdc70b5 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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 = 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 { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index eb7b40e1b..003f9e64b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -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 = 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::>(); + 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::>(); + 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::>(); + 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)] diff --git a/src/command.rs b/src/command.rs index f2bc67398..c00826877 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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)>, Vec), + /// Replace several entities as one undo step while keeping the command active. + ReplaceManyContinue(Vec<(Handle, Vec)>), /// 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 { + 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). diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 7a2fbd100..3cd6ac5bc 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3085,6 +3085,23 @@ impl TrimCommand { } CmdResult::ReplaceMany(repl, Vec::new()) } + + fn stage_replacements(&mut self, replacements: &[(Handle, Vec)]) { + 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 { + 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 { match self.mode { TrimMode::SelectEdges => {