forked from 1b-agent/opencadstudio-upstream
feat(clipboard): carry referenced records across drawings on paste
Copy/paste between drawings dropped the entities' layer (and linetype / text + dim style): the clipboard held only the entities, which reference those records by name, so pasting into a drawing without a matching record left the entity dangling on a non-existent layer. (#129) Snapshot the referenced table records from the source drawing at copy time (ClipboardDeps::capture) and, on paste, recreate any the target drawing lacks with a fresh handle (merge_clipboard_deps). A record that already exists by name in the target is left untouched — the target's own definition wins; only missing ones are imported from the source. Same-document pastes are unaffected (every record already exists). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ce3aaf260
commit
70e73877b2
3 files changed
+132
No files matched your search
@@ -547,6 +547,9 @@ impl OpenCADStudio {
|
|||||||
let delta = base_pt - self.clipboard_centroid;
|
let delta = base_pt - self.clipboard_centroid;
|
||||||
let translate = crate::command::EntityTransform::Translate(delta);
|
let translate = crate::command::EntityTransform::Translate(delta);
|
||||||
self.push_undo_snapshot(i, "PASTECLIP");
|
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 count = self.clipboard.len();
|
||||||
let new_handles: Vec<Handle> = self
|
let new_handles: Vec<Handle> = self
|
||||||
.clipboard
|
.clipboard
|
||||||
@@ -1675,6 +1678,47 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restore the tangent-snap / ortho state that was in effect before the command started.
|
/// 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) {
|
fn restore_pre_cmd_tangent(&mut self) {
|
||||||
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
if let Some(was_on) = self.pre_cmd_tangent.take() {
|
||||||
if !was_on {
|
if !was_on {
|
||||||
|
|||||||
@@ -527,6 +527,10 @@ impl OpenCADStudio {
|
|||||||
&self.tabs[i].scene.wire_models_for(&handles),
|
&self.tabs[i].scene.wire_models_for(&handles),
|
||||||
);
|
);
|
||||||
self.clipboard = entities;
|
self.clipboard = entities;
|
||||||
|
self.clipboard_deps = super::ClipboardDeps::capture(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&self.clipboard,
|
||||||
|
);
|
||||||
self.command_line.push_info(&format!(
|
self.command_line.push_info(&format!(
|
||||||
"{} object(s) copied to clipboard.",
|
"{} object(s) copied to clipboard.",
|
||||||
self.clipboard.len()
|
self.clipboard.len()
|
||||||
@@ -556,6 +560,10 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
let count = entities.len();
|
let count = entities.len();
|
||||||
self.clipboard = entities;
|
self.clipboard = entities;
|
||||||
|
self.clipboard_deps = super::ClipboardDeps::capture(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&self.clipboard,
|
||||||
|
);
|
||||||
self.push_undo_snapshot(i, "CUTCLIP");
|
self.push_undo_snapshot(i, "CUTCLIP");
|
||||||
self.tabs[i].scene.erase_entities(&handles);
|
self.tabs[i].scene.erase_entities(&handles);
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
|
|||||||
@@ -343,6 +343,11 @@ pub(super) struct OpenCADStudio {
|
|||||||
clipboard: Vec<acadrust::EntityType>,
|
clipboard: Vec<acadrust::EntityType>,
|
||||||
/// Centroid of the clipboard entities (world XY plane).
|
/// Centroid of the clipboard entities (world XY plane).
|
||||||
clipboard_centroid: glam::Vec3,
|
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
|
/// True while the Shift key is held — drives subtractive pick (Shift+click
|
||||||
/// removes the picked entity from the selection). Tracked from keyboard
|
/// removes the picked entity from the selection). Tracked from keyboard
|
||||||
/// modifier-change events since mouse click messages carry no modifiers.
|
/// modifier-change events since mouse click messages carry no modifiers.
|
||||||
@@ -610,6 +615,80 @@ pub enum ColorPickTarget {
|
|||||||
Layer(usize),
|
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<acadrust::tables::Layer>,
|
||||||
|
pub linetypes: Vec<acadrust::tables::LineType>,
|
||||||
|
pub text_styles: Vec<acadrust::tables::TextStyle>,
|
||||||
|
pub dim_styles: Vec<acadrust::tables::DimStyle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
/// 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
|
/// at a time; dialog-specific data lives in its own fields. Closed via the
|
||||||
/// modal's ✕ (`Message::CloseModal`).
|
/// modal's ✕ (`Message::CloseModal`).
|
||||||
@@ -1517,6 +1596,7 @@ impl OpenCADStudio {
|
|||||||
update_notice_body: None,
|
update_notice_body: None,
|
||||||
clipboard: Vec::new(),
|
clipboard: Vec::new(),
|
||||||
clipboard_centroid: glam::Vec3::ZERO,
|
clipboard_centroid: glam::Vec3::ZERO,
|
||||||
|
clipboard_deps: ClipboardDeps::default(),
|
||||||
shift_down: false,
|
shift_down: false,
|
||||||
mtext_editor: None,
|
mtext_editor: None,
|
||||||
text_inline: None,
|
text_inline: None,
|
||||||
|
|||||||
Reference in new issue
Block a user