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

+14 -1
View File
@@ -2,11 +2,22 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
/// Shared icon for the Paste button and its dropdown entries.
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/paste.svg"));
/// Paste-menu entries: (command id, label, icon). The first is the default
/// fired by clicking the button body; the rest open from the ▾.
pub const MENU_ITEMS: &[(&str, &str, IconKind)] = &[
("PASTECLIP", "Paste", ICON),
("PASTEORIG", "Paste to Original Coordinates", ICON),
("PASTEBLOCK", "Paste as Block", ICON),
];
pub fn tool() -> ToolDef {
ToolDef {
id: "PASTECLIP",
label: "Paste",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/paste.svg")),
icon: ICON,
event: ModuleEvent::Command("PASTECLIP".to_string()),
}
}
@@ -62,3 +73,5 @@ impl CadCommand for PasteCommand {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["PASTECLIP", "PC"] }); // PasteCommand
inventory::submit!(crate::command::CommandRegistration { names: &["PASTEORIG"] });
inventory::submit!(crate::command::CommandRegistration { names: &["PASTEBLOCK"] });
+7 -1
View File
@@ -214,7 +214,13 @@ impl CadModule for DrawModule {
RibbonGroup {
title: "Clipboard",
tools: vec![
RibbonItem::LargeTool(paste::tool()),
RibbonItem::LargeDropdown {
id: "PASTE_MENU",
label: "Paste",
icon: paste::ICON,
items: paste::MENU_ITEMS.to_vec(),
default: "PASTECLIP",
},
copy_clip::tool().into(),
cut::tool().into(),
],
+26 -2
View File
@@ -37,6 +37,10 @@ pub struct InsertBlockCommand {
step: Step,
/// Pending Insert entity stored while attr-filling is in progress.
pending_insert: Option<Insert>,
/// Optional drag preview: the block's wire geometry plus the base point it
/// is measured from, so `on_preview_wires` can rubber-band it to the
/// cursor. Set by paste-as-block; empty for a plain INSERT.
preview: Option<(Vec<WireModel>, Vec3)>,
}
impl InsertBlockCommand {
@@ -45,6 +49,20 @@ impl InsertBlockCommand {
available,
step: Step::Name,
pending_insert: None,
preview: None,
}
}
/// Start the command already locked to `name`, skipping the name prompt and
/// going straight to "specify insertion point". `preview_wires` (measured
/// from `base`) rubber-band under the cursor. Used by paste-as-block, which
/// has just defined the block and only needs the drop point.
pub fn new_for_block(name: String, preview_wires: Vec<WireModel>, base: Vec3) -> Self {
Self {
available: vec![name.clone()],
step: Step::Point { name },
pending_insert: None,
preview: Some((preview_wires, base)),
}
}
}
@@ -138,8 +156,14 @@ impl CadCommand for InsertBlockCommand {
}
}
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
vec![]
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
match (&self.step, &self.preview) {
(Step::Point { .. }, Some((wires, base))) => {
let delta = pt - *base;
wires.iter().map(|w| w.translated(delta)).collect()
}
_ => vec![],
}
}
fn attreq_set_attdefs(&mut self, attdefs: Vec<(String, String, String)>) {