diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 45fec88b0..388c20485 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2080,7 +2080,12 @@ impl OpenCADStudio { }) .collect(); self.merge_clipboard_ext_objects(i, &by_index); - self.tabs[i].scene.populate_meshes_from_document(); + // Incremental: `add_entity` already tessellated every pasted top-level + // solid, and existing document solids are still cached — so only newly + // introduced block-definition solids need building. The full rebuild + // would clear and re-tessellate the entire document (every solid in the + // drawing) on each paste, which is what made a large paste stall. + self.tabs[i].scene.populate_missing_meshes_from_document(); by_index } diff --git a/src/modules/draw/clipboard/paste.rs b/src/modules/draw/clipboard/paste.rs index a2d5ce337..0bc8a6b58 100644 --- a/src/modules/draw/clipboard/paste.rs +++ b/src/modules/draw/clipboard/paste.rs @@ -22,15 +22,75 @@ use crate::command::{CadCommand, CmdResult}; use crate::scene::model::wire_model::WireModel; pub struct PasteCommand { - /// Wire models of the clipboard entities (used for preview). + /// Wire models of the clipboard entities (used for the ghost preview). + /// Emptied when `bbox_wire` is used, so a huge clipboard isn't kept twice. wires: Vec, + /// Lightweight bounding-box outline that replaces `wires` for very large + /// clipboards. `on_preview_wires` runs every mouse move and translates every + /// point of the ghost; for a whole-drawing paste that is O(hundreds of + /// thousands) per frame and freezes placement. Above a point budget we ghost + /// just this 5-point rectangle instead — O(1) per frame — the same way + /// AutoCAD rubber-bands a box for a huge selection. `None` = ghost the full + /// wires. + bbox_wire: Option, /// Centroid of the clipboard entities (offset origin for translation). centroid: Vec3, } impl PasteCommand { + /// Per-frame preview cost is proportional to the total point count across + /// the ghost wires. Above this, switch to the bounding-box outline so + /// placement stays smooth. ~50k points is well under a frame budget yet + /// still shows the full ghost for typical multi-object pastes. + const MAX_PREVIEW_POINTS: usize = 50_000; + pub fn new(wires: Vec, centroid: Vec3) -> Self { - Self { wires, centroid } + let total_points: usize = wires.iter().map(|w| w.points.len()).sum(); + if total_points > Self::MAX_PREVIEW_POINTS { + if let Some(bbox_wire) = Self::bbox_outline(&wires) { + // Drop the full wires — the box is all the ghost needs now. + return Self { wires: Vec::new(), bbox_wire: Some(bbox_wire), centroid }; + } + } + Self { wires, bbox_wire: None, centroid } + } + + /// Build a closed rectangle outline around the XY extent of `wires`, in the + /// same world coordinates so the standard `translated(delta)` shift applies. + /// Uses the double-single `points + points_low` sum so the box stays precise + /// at UTM-scale coordinates. Returns `None` when there are no points. + fn bbox_outline(wires: &[WireModel]) -> Option { + let mut min = [f64::INFINITY; 3]; + let mut max = [f64::NEG_INFINITY; 3]; + let mut any = false; + for w in wires { + for (i, p) in w.points.iter().enumerate() { + let lo = w.points_low.get(i).copied().unwrap_or([0.0; 3]); + for k in 0..3 { + let v = p[k] as f64 + lo[k] as f64; + min[k] = min[k].min(v); + max[k] = max[k].max(v); + } + any = true; + } + } + if !any { + return None; + } + let z = min[2]; + let pts = vec![ + [min[0], min[1], z], + [max[0], min[1], z], + [max[0], max[1], z], + [min[0], max[1], z], + [min[0], min[1], z], + ]; + Some(WireModel::solid_f64( + "paste_bbox".into(), + pts, + WireModel::CYAN, + false, + )) } } @@ -55,8 +115,13 @@ impl CadCommand for PasteCommand { vec![] } - fn on_preview_wires(&mut self, pt: DVec3) -> Vec { let pt = pt.as_vec3(); + fn on_preview_wires(&mut self, pt: DVec3) -> Vec { + let pt = pt.as_vec3(); let delta = pt - self.centroid; + // Large clipboard: ghost just the bounding box — O(1) per frame. + if let Some(bbox) = &self.bbox_wire { + return vec![bbox.translated(delta)]; + } self.wires.iter().map(|w| w.translated(delta)).collect() } } diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 32982f740..4c2200def 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -26,7 +26,7 @@ impl Scene { handles .iter() .flat_map(|h| { - match self.document.entities().find(|e| e.common().handle == *h) { + match self.document.get_entity(*h) { // Hatches carry no outline in the normal wire set, but an // edit preview (move / copy / array / grip-drag) needs to // show the shape following the cursor. Build a live boundary @@ -54,7 +54,7 @@ impl Scene { let mut plain: Vec = Vec::new(); let mut texts: Vec<(WireModel, glam::DVec3)> = Vec::new(); for h in handles { - let Some(e) = self.document.entities().find(|e| e.common().handle == *h) else { + let Some(e) = self.document.get_entity(*h) else { continue; }; if matches!(e, EntityType::Text(_)) {