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 <noreply@anthropic.com>
This commit is contained in:
parent
f5870b011d
commit
c249a99d4c
6 changed files with 40 additions and 38 deletions
|
|
@ -790,7 +790,7 @@ impl OpenCADStudio {
|
|||
if self.clipboard.is_empty() {
|
||||
self.command_line.push_error("Clipboard is empty.");
|
||||
} else {
|
||||
let delta = base_pt - self.clipboard_centroid;
|
||||
let delta = base_pt - self.clipboard_base;
|
||||
let translate = crate::command::EntityTransform::Translate(delta);
|
||||
self.push_undo_snapshot(i, "PASTECLIP");
|
||||
let count = self.clipboard.len();
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl OpenCADStudio {
|
|||
.iter()
|
||||
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
||||
.collect();
|
||||
self.clipboard_centroid = super::super::helpers::entities_centroid_by_bbox(
|
||||
self.clipboard_base = super::super::helpers::entities_lower_left_by_bbox(
|
||||
&self.tabs[i].scene.document,
|
||||
&handles,
|
||||
);
|
||||
|
|
@ -87,7 +87,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
// COPYBASE — copy the selection to the clipboard with a picked base
|
||||
// point (vs COPYCLIP, which uses the selection centroid).
|
||||
// point (vs COPYCLIP, which uses the selection's lower-left corner).
|
||||
"COPYBASE" => {
|
||||
let handles: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
|
|
@ -127,7 +127,7 @@ impl OpenCADStudio {
|
|||
.iter()
|
||||
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
||||
.collect();
|
||||
self.clipboard_centroid = base;
|
||||
self.clipboard_base = base;
|
||||
self.clipboard = entities;
|
||||
self.clipboard_deps = super::super::ClipboardDeps::capture(
|
||||
&self.tabs[i].scene.document,
|
||||
|
|
@ -159,7 +159,7 @@ impl OpenCADStudio {
|
|||
.iter()
|
||||
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
||||
.collect();
|
||||
self.clipboard_centroid = super::super::helpers::entities_centroid_by_bbox(
|
||||
self.clipboard_base = super::super::helpers::entities_lower_left_by_bbox(
|
||||
&self.tabs[i].scene.document,
|
||||
&handles,
|
||||
);
|
||||
|
|
@ -184,11 +184,11 @@ impl OpenCADStudio {
|
|||
self.command_line.push_error("Clipboard is empty.");
|
||||
} else {
|
||||
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
|
||||
let centroid = self.clipboard_centroid;
|
||||
let base = self.clipboard_base;
|
||||
use crate::modules::draw::clipboard::paste::PasteCommand;
|
||||
// The ghost anchor is a display-only offset; the precise
|
||||
// paste delta is computed in f64 at commit time.
|
||||
let cmd = PasteCommand::new(wires, centroid.as_vec3());
|
||||
let cmd = PasteCommand::new(wires, base.as_vec3());
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ impl OpenCADStudio {
|
|||
// block's nested insert keeps its clip. (#xclip-paste)
|
||||
let ext_roots = self.recreate_clipboard_ext_roots(i);
|
||||
let name = self.unique_block_name("Block");
|
||||
let base = self.clipboard_centroid;
|
||||
let base = self.clipboard_base;
|
||||
let mut entities = self.clipboard.clone();
|
||||
for (idx, root) in ext_roots {
|
||||
if let Some(e) = entities.get_mut(idx) {
|
||||
|
|
@ -246,7 +246,7 @@ impl OpenCADStudio {
|
|||
Ok(()) => {
|
||||
// Block defined; now place it interactively so the
|
||||
// user picks the drop point (insertion uses the
|
||||
// clipboard centroid as the block's base). The
|
||||
// clipboard lower-left corner as the block's base). The
|
||||
// clipboard wires rubber-band under the cursor.
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
self.tabs[i].dirty = true;
|
||||
|
|
|
|||
|
|
@ -252,32 +252,31 @@ pub(super) fn polar_constrain_near(
|
|||
|
||||
// ── Clipboard / selection helpers ──────────────────────────────────────────
|
||||
|
||||
/// Cheap copy/paste anchor: the mean of each entity's bounding-box centre.
|
||||
/// Replaces averaging every tessellated wire vertex, which cost O(total
|
||||
/// geometry) and stalled a whole-drawing copy. The anchor only sets which point
|
||||
/// of the selection sits under the cursor, so a per-entity bbox centre is an
|
||||
/// equally valid, far cheaper origin.
|
||||
pub(super) fn entities_centroid_by_bbox(
|
||||
/// Copy/paste anchor: the lower-left corner of the bounding box that encloses
|
||||
/// every copied entity. Standard clipboard behaviour puts this corner under the
|
||||
/// cursor at paste time, so the whole selection drops down-and-right of the pick.
|
||||
///
|
||||
/// Unions each entity's own bounding box (O(entities)) rather than averaging
|
||||
/// every tessellated wire vertex, which cost O(total geometry) and stalled a
|
||||
/// whole-drawing copy. The per-entity `min` corners give the exact enclosing
|
||||
/// box's lower-left.
|
||||
pub(super) fn entities_lower_left_by_bbox(
|
||||
doc: &acadrust::CadDocument,
|
||||
handles: &[acadrust::Handle],
|
||||
) -> glam::DVec3 {
|
||||
let mut sum = glam::DVec3::ZERO;
|
||||
let mut count = 0usize;
|
||||
let mut min = glam::DVec3::splat(f64::INFINITY);
|
||||
let mut any = false;
|
||||
for &h in handles {
|
||||
let Some(e) = doc.get_entity(h) else { continue };
|
||||
let bb = e.as_entity().bounding_box();
|
||||
let c = glam::DVec3::new(
|
||||
(bb.min.x + bb.max.x) * 0.5,
|
||||
(bb.min.y + bb.max.y) * 0.5,
|
||||
(bb.min.z + bb.max.z) * 0.5,
|
||||
);
|
||||
if c.x.is_finite() && c.y.is_finite() && c.z.is_finite() {
|
||||
sum += c;
|
||||
count += 1;
|
||||
let lo = glam::DVec3::new(bb.min.x, bb.min.y, bb.min.z);
|
||||
if lo.x.is_finite() && lo.y.is_finite() && lo.z.is_finite() {
|
||||
min = min.min(lo);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
sum / count as f64
|
||||
if any {
|
||||
min
|
||||
} else {
|
||||
glam::DVec3::ZERO
|
||||
}
|
||||
|
|
|
|||
|
|
@ -500,8 +500,10 @@ pub(super) struct OpenCADStudio {
|
|||
clipboard: Vec<acadrust::EntityType>,
|
||||
/// Entities removed by the most recent ERASE, kept so OOPS can restore them.
|
||||
oops_cache: Vec<acadrust::EntityType>,
|
||||
/// Centroid of the clipboard entities (world XY plane).
|
||||
clipboard_centroid: glam::DVec3,
|
||||
/// Paste anchor: lower-left corner of the clipboard entities' bounding box
|
||||
/// (or the point picked by COPYBASE). This point lands under the cursor at
|
||||
/// paste time.
|
||||
clipboard_base: glam::DVec3,
|
||||
/// Table records (layer / linetype / text + dim style) the clipboard
|
||||
/// entities reference, captured from the source drawing at copy time so a
|
||||
/// paste into a *different* drawing can recreate any that are missing —
|
||||
|
|
@ -2152,7 +2154,7 @@ impl OpenCADStudio {
|
|||
update_notice_body: None,
|
||||
clipboard: Vec::new(),
|
||||
oops_cache: Vec::new(),
|
||||
clipboard_centroid: glam::DVec3::ZERO,
|
||||
clipboard_base: glam::DVec3::ZERO,
|
||||
clipboard_deps: ClipboardDeps::default(),
|
||||
shift_down: false,
|
||||
ctrl_down: false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue