fix(clipboard): paste anchor at bounding-box lower-left, not center (#285)

COPYCLIP/CUTCLIP used the mean of each entity's bounding-box centre as the
paste anchor. Standard clipboard behaviour puts the lower-left corner of the
box enclosing all copied items under the cursor. Union each entity's bbox min
corner instead, and rename clipboard_centroid -> clipboard_base (COPYBASE also
stores a picked point there, so "centroid" was inaccurate).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-10 11:16:13 +03:00
1 parent f5870b011d
commit c249a99d4c
6 files changed
+40 -38

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
// COPYBASE — copy the current selection to the clipboard using a base point the
// user picks (rather than the selection's centroid, as COPYCLIP does). The
// command only collects the base point; the host performs the copy when it
// user picks (rather than the selection's lower-left corner, as COPYCLIP does).
// The command only collects the base point; the host performs the copy when it
// receives the dispatched `COPYBASE_AT <x> <y> <z>` token.
use glam::DVec3;
+7 -6
View File
@@ -32,8 +32,9 @@ pub struct PasteCommand {
/// just this 5-point rectangle instead — O(1) per frame. `None` = ghost the
/// full wires.
bbox_wire: Option<WireModel>,
/// Centroid of the clipboard entities (offset origin for translation).
centroid: Vec3,
/// Paste anchor — lower-left corner of the clipboard entities (offset origin
/// for translation; the point that tracks the cursor).
base: Vec3,
}
impl PasteCommand {
@@ -44,17 +45,17 @@ impl PasteCommand {
const MAX_PREVIEW_WIRES: usize = 20_000;
const MAX_PREVIEW_POINTS: usize = 300_000;
pub fn new(wires: Vec<WireModel>, centroid: Vec3) -> Self {
pub fn new(wires: Vec<WireModel>, base: Vec3) -> Self {
let total_points: usize = wires.iter().map(|w| w.points.len()).sum();
let too_heavy =
wires.len() > Self::MAX_PREVIEW_WIRES || total_points > Self::MAX_PREVIEW_POINTS;
if too_heavy {
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 };
return Self { wires: Vec::new(), bbox_wire: Some(bbox_wire), base };
}
}
Self { wires, bbox_wire: None, centroid }
Self { wires, bbox_wire: None, base }
}
/// Build a closed rectangle outline around the XY extent of `wires`, in the
@@ -119,7 +120,7 @@ impl CadCommand for PasteCommand {
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let pt = pt.as_vec3();
let delta = pt - self.centroid;
let delta = pt - self.base;
// Large clipboard: ghost just the bounding box — O(1) per frame.
if let Some(bbox) = &self.bbox_wire {
return vec![bbox.translated(delta)];