diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index ed0168764..b883031f6 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -547,6 +547,9 @@ impl OpenCADStudio { let delta = base_pt - self.clipboard_centroid; let translate = crate::command::EntityTransform::Translate(delta); self.push_undo_snapshot(i, "PASTECLIP"); + // Recreate any layer / linetype / style the copied entities + // need but this drawing lacks (cross-drawing paste). (#129) + self.merge_clipboard_deps(i); let count = self.clipboard.len(); let new_handles: Vec = self .clipboard @@ -1675,6 +1678,47 @@ impl OpenCADStudio { } /// Restore the tangent-snap / ortho state that was in effect before the command started. + /// Recreate clipboard-dependency records (layer / linetype / text + dim + /// style) in tab `i`'s document for any the copied entities reference but + /// 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) { + use acadrust::TableEntry; + if self.clipboard_deps.is_empty() { + return; + } + let doc = &mut self.tabs[i].scene.document; + for rec in &self.clipboard_deps.layers { + if !doc.layers.contains(rec.name()) { + let mut r = rec.clone(); + r.set_handle(doc.allocate_handle()); + let _ = doc.layers.add(r); + } + } + for rec in &self.clipboard_deps.linetypes { + if !doc.line_types.contains(rec.name()) { + let mut r = rec.clone(); + r.set_handle(doc.allocate_handle()); + let _ = doc.line_types.add(r); + } + } + for rec in &self.clipboard_deps.text_styles { + if !doc.text_styles.contains(rec.name()) { + let mut r = rec.clone(); + r.set_handle(doc.allocate_handle()); + let _ = doc.text_styles.add(r); + } + } + for rec in &self.clipboard_deps.dim_styles { + if !doc.dim_styles.contains(rec.name()) { + let mut r = rec.clone(); + r.set_handle(doc.allocate_handle()); + let _ = doc.dim_styles.add(r); + } + } + } + fn restore_pre_cmd_tangent(&mut self) { if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { diff --git a/src/app/commands.rs b/src/app/commands.rs index 4f347f201..29fc710d9 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -527,6 +527,10 @@ impl OpenCADStudio { &self.tabs[i].scene.wire_models_for(&handles), ); self.clipboard = entities; + self.clipboard_deps = super::ClipboardDeps::capture( + &self.tabs[i].scene.document, + &self.clipboard, + ); self.command_line.push_info(&format!( "{} object(s) copied to clipboard.", self.clipboard.len() @@ -556,6 +560,10 @@ impl OpenCADStudio { ); let count = entities.len(); self.clipboard = entities; + self.clipboard_deps = super::ClipboardDeps::capture( + &self.tabs[i].scene.document, + &self.clipboard, + ); self.push_undo_snapshot(i, "CUTCLIP"); self.tabs[i].scene.erase_entities(&handles); self.tabs[i].scene.deselect_all(); diff --git a/src/app/mod.rs b/src/app/mod.rs index ce09d5728..ec4b6972f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -343,6 +343,11 @@ pub(super) struct OpenCADStudio { clipboard: Vec, /// Centroid of the clipboard entities (world XY plane). clipboard_centroid: glam::Vec3, + /// 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 — + /// otherwise the pasted entities would dangle on a non-existent layer. + clipboard_deps: ClipboardDeps, /// True while the Shift key is held — drives subtractive pick (Shift+click /// removes the picked entity from the selection). Tracked from keyboard /// modifier-change events since mouse click messages carry no modifiers. @@ -610,6 +615,80 @@ pub enum ColorPickTarget { Layer(usize), } +/// Table records the clipboard entities depend on, snapshotted from the source +/// drawing at copy time. On paste into a drawing that lacks any of them, the +/// missing records are recreated (with a fresh handle) so the entities keep +/// their layer / linetype / style instead of dangling. See #129. +#[derive(Default, Clone)] +pub struct ClipboardDeps { + pub layers: Vec, + pub linetypes: Vec, + pub text_styles: Vec, + pub dim_styles: Vec, +} + +impl ClipboardDeps { + /// Snapshot the records `entities` reference that exist in `doc`. + pub fn capture(doc: &acadrust::CadDocument, entities: &[acadrust::EntityType]) -> Self { + use acadrust::EntityType; + use std::collections::BTreeSet; + let (mut layers, mut ltypes, mut tstyles, mut dstyles) = ( + BTreeSet::new(), + BTreeSet::new(), + BTreeSet::new(), + BTreeSet::new(), + ); + let is_special = |n: &str| { + n.is_empty() + || n.eq_ignore_ascii_case("ByLayer") + || n.eq_ignore_ascii_case("ByBlock") + }; + for e in entities { + let c = e.common(); + if !c.layer.is_empty() { + layers.insert(c.layer.clone()); + } + if !is_special(&c.linetype) && !c.linetype.eq_ignore_ascii_case("Continuous") { + ltypes.insert(c.linetype.clone()); + } + match e { + EntityType::Text(t) if !t.style.is_empty() => { + tstyles.insert(t.style.clone()); + } + EntityType::MText(t) if !t.style.is_empty() => { + tstyles.insert(t.style.clone()); + } + EntityType::AttributeEntity(a) if !a.text_style.is_empty() => { + tstyles.insert(a.text_style.clone()); + } + EntityType::AttributeDefinition(a) if !a.text_style.is_empty() => { + tstyles.insert(a.text_style.clone()); + } + EntityType::Dimension(d) if !d.base().style_name.is_empty() => { + dstyles.insert(d.base().style_name.clone()); + } + EntityType::Leader(l) if !l.dimension_style.is_empty() => { + dstyles.insert(l.dimension_style.clone()); + } + _ => {} + } + } + ClipboardDeps { + layers: layers.iter().filter_map(|n| doc.layers.get(n).cloned()).collect(), + linetypes: ltypes.iter().filter_map(|n| doc.line_types.get(n).cloned()).collect(), + text_styles: tstyles.iter().filter_map(|n| doc.text_styles.get(n).cloned()).collect(), + dim_styles: dstyles.iter().filter_map(|n| doc.dim_styles.get(n).cloned()).collect(), + } + } + + pub fn is_empty(&self) -> bool { + self.layers.is_empty() + && self.linetypes.is_empty() + && self.text_styles.is_empty() + && self.dim_styles.is_empty() + } +} + /// Which in-canvas modal dialog is currently open (Plan B). At most one shows /// at a time; dialog-specific data lives in its own fields. Closed via the /// modal's ✕ (`Message::CloseModal`). @@ -1517,6 +1596,7 @@ impl OpenCADStudio { update_notice_body: None, clipboard: Vec::new(), clipboard_centroid: glam::Vec3::ZERO, + clipboard_deps: ClipboardDeps::default(), shift_down: false, mtext_editor: None, text_inline: None,