feat(clipboard): paste menu with paste-to-original and paste-as-block

Turn the ribbon Clipboard "Paste" button into a dropdown:
- Paste — existing pick-a-point paste.
- Paste to Original Coordinates — drops the clipboard at its source
  coordinates with no prompt.
- Paste as Block — wraps the clipboard contents in a new block definition
  and starts an interactive insert (prompts for the drop point, with the
  geometry rubber-banding under the cursor).

Also fixes two bugs the new commands exposed:
- entities_centroid summed the NaN separator points wire models carry,
  so the paste base point came out NaN; non-finite points are now
  skipped. A NaN base previously froze block tessellation outright.
- PASTEORIG added clipboard entities with their original handles
  (duplicates on same-document paste); it now goes through
  add_entity_clone and merges referenced records like the other pastes.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-18 16:09:47 +03:00
1 parent 446430d2b3
commit 47f74a9b98
7 files changed
+183 -10

No files matched your search

+1 -1
View File
@@ -1682,7 +1682,7 @@ impl OpenCADStudio {
/// this drawing doesn't already have. Each recreated record gets a fresh
/// handle from the target document so it can't collide with an existing
/// one. No-op for same-document pastes (the records already exist). (#129)
fn merge_clipboard_deps(&mut self, i: usize) {
pub(super) fn merge_clipboard_deps(&mut self, i: usize) {
use acadrust::TableEntry;
if self.clipboard_deps.is_empty() {
return;
+56 -3
View File
@@ -5,6 +5,20 @@ use iced::Task;
use std::path::PathBuf;
impl OpenCADStudio {
/// First `"{prefix}{n}"` (n ≥ 1) not already used by a block record in the
/// active drawing. Used to auto-name a paste-as-block definition.
fn unique_block_name(&self, prefix: &str) -> String {
let i = self.active_tab;
let mut n = 1;
loop {
let name = format!("{prefix}{n}");
if self.tabs[i].scene.document.block_records.get(&name).is_none() {
return name;
}
n += 1;
}
}
pub(super) fn dispatch_command(&mut self, cmd: &str) -> Task<Message> {
let i = self.active_tab;
// Starting a command closes any open ribbon dropdown (e.g. a style
@@ -586,7 +600,7 @@ impl OpenCADStudio {
}
}
// PASTEORIG — paste at original coordinates (no move to pick point)
// PASTEORIG — paste at the entities' original coordinates (no pick).
"PASTEORIG" => {
if self.clipboard.is_empty() {
self.command_line
@@ -594,10 +608,15 @@ impl OpenCADStudio {
} else {
let count = self.clipboard.len();
self.push_undo_snapshot(i, "PASTEORIG");
for entity in &self.clipboard {
self.tabs[i].scene.add_entity(entity.clone());
// Recreate any layer / style this drawing lacks, then add
// each entity with fresh handles (top-level + inline subs)
// so a same-document paste can't duplicate handles. (#129)
self.merge_clipboard_deps(i);
for entity in self.clipboard.clone() {
self.tabs[i].scene.add_entity_clone(entity);
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line.push_output(&format!(
"PASTEORIG: {} object(s) pasted at original coordinates.",
count
@@ -605,6 +624,40 @@ impl OpenCADStudio {
}
}
// PASTEBLOCK — wrap the clipboard contents in a new block definition
// and place one insert of it at the clipboard's original location.
"PASTEBLOCK" => {
if self.clipboard.is_empty() {
self.command_line
.push_error("PASTEBLOCK: clipboard is empty.");
} else {
self.push_undo_snapshot(i, "PASTEBLOCK");
self.merge_clipboard_deps(i);
let name = self.unique_block_name("Block");
let base = self.clipboard_centroid;
let entities = self.clipboard.clone();
match self
.tabs[i]
.scene
.define_block_from_owned_entities(entities, &name, base)
{
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 wires rubber-band under the cursor.
self.tabs[i].dirty = true;
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
use crate::modules::insert::insert_block::InsertBlockCommand;
let cmd = InsertBlockCommand::new_for_block(name, wires, base);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
Err(e) => self.command_line.push_error(&format!("PASTEBLOCK: {e}")),
}
}
}
"BLOCK" => {
let handles: Vec<_> = self.tabs[i]
.scene
+8 -2
View File
@@ -186,8 +186,14 @@ pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 {
let mut count = 0usize;
for w in wires {
for p in &w.points {
sum += glam::Vec3::from(*p);
count += 1;
let v = glam::Vec3::from(*p);
// Wire models carry NaN points as separators between disjoint
// segments; summing them poisons the whole centroid into NaN,
// which then makes every paste base point NaN. (#129)
if v.is_finite() {
sum += v;
count += 1;
}
}
}
if count > 0 {