forked from 1b-agent/opencadstudio-upstream
fix(blocks): a dynamic block copied to another drawing stays dynamic (#1816)
Only the definition's entities crossed drawings: its record's object graph (parameters, visibility states, grips) stayed behind, and a reference showing an evaluated copy lost the link to its definition. The clipboard now captures the graph and the dynamic definition of an evaluated reference, and paste and the block palette's insert-from-file recreate them on the new records, re-pointing members and element tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca408f4329
commit
091331f3bd
6 files changed
+109
-21
No files matched your search
@@ -469,8 +469,9 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let captures = self.clipboard_deps.ext_objects.clone();
|
let captures = self.clipboard_deps.ext_objects.clone();
|
||||||
let doc = &mut self.tabs[i].scene.document;
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
|
let records = block_record_map(doc, &self.clipboard_deps.blocks);
|
||||||
for cap in &captures {
|
for cap in &captures {
|
||||||
if let Some(new_root) = recreate_ext_subtree(doc, cap, None) {
|
if let Some(new_root) = recreate_ext_subtree(doc, cap, None, &records) {
|
||||||
out.insert(cap.entity_index, new_root);
|
out.insert(cap.entity_index, new_root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,13 +525,56 @@ pub(crate) fn merge_block_defs(
|
|||||||
doc: &mut codec::CadDocument,
|
doc: &mut codec::CadDocument,
|
||||||
blocks: &[crate::app::BlockDef],
|
blocks: &[crate::app::BlockDef],
|
||||||
) -> usize {
|
) -> usize {
|
||||||
let mut created = 0;
|
let mut created = Vec::new();
|
||||||
for def in blocks {
|
for def in blocks {
|
||||||
let before = doc.block_records.len();
|
let before = doc.block_records.len();
|
||||||
define_block_raw_doc(doc, &def.name, def.base_point, &def.entities);
|
define_block_raw_doc(doc, &def.name, def.base_point, &def.entities);
|
||||||
created += usize::from(doc.block_records.len() > before);
|
if doc.block_records.len() > before {
|
||||||
|
created.push(def);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
created
|
// Graphs name other records (an evaluated copy names its definition), so
|
||||||
|
// they go in once every record exists.
|
||||||
|
let records = block_record_map(doc, blocks);
|
||||||
|
for def in &created {
|
||||||
|
import_block_def_graph(doc, def, &records);
|
||||||
|
}
|
||||||
|
created.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Source record → this drawing's record of the same name, per definition.
|
||||||
|
pub(crate) fn block_record_map(
|
||||||
|
doc: &codec::CadDocument,
|
||||||
|
blocks: &[crate::app::BlockDef],
|
||||||
|
) -> std::collections::HashMap<Handle, Handle> {
|
||||||
|
blocks
|
||||||
|
.iter()
|
||||||
|
.filter_map(|def| Some((def.source, doc.block_records.get(&def.name)?.handle)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carry `def`'s object graph (a dynamic block's parameters and states) onto
|
||||||
|
/// the record just defined from it here. `records` maps the source records
|
||||||
|
/// the graph may name.
|
||||||
|
pub(crate) fn import_block_def_graph(
|
||||||
|
doc: &mut codec::CadDocument,
|
||||||
|
def: &crate::app::BlockDef,
|
||||||
|
records: &std::collections::HashMap<Handle, Handle>,
|
||||||
|
) {
|
||||||
|
let Some(graph) = &def.graph else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(record) = doc.block_records.get(&def.name).map(|record| record.handle) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let members = crate::scene::block_authoring::members(doc, record);
|
||||||
|
if members.len() != def.entities.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut map = records.clone();
|
||||||
|
map.extend(def.entities.iter().map(|e| e.common().handle).zip(members));
|
||||||
|
doc.import_block_graph(graph, record, &map);
|
||||||
|
crate::scene::block_authoring::retag_members(doc, record);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// (`NULL` where an add failed) plus the source-handle → pasted-handle map.
|
/// (`NULL` where an add failed) plus the source-handle → pasted-handle map.
|
||||||
@@ -612,6 +656,7 @@ pub(crate) fn paste_entities_kernel(
|
|||||||
Some(crate::command::EntityTransform::Translate(delta)) => *delta,
|
Some(crate::command::EntityTransform::Translate(delta)) => *delta,
|
||||||
_ => glam::DVec3::ZERO,
|
_ => glam::DVec3::ZERO,
|
||||||
};
|
};
|
||||||
|
let records = block_record_map(doc, &deps.blocks);
|
||||||
for cap in &deps.ext_objects {
|
for cap in &deps.ext_objects {
|
||||||
let Some(&new_entity) = by_index.get(cap.entity_index) else {
|
let Some(&new_entity) = by_index.get(cap.entity_index) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -619,7 +664,7 @@ pub(crate) fn paste_entities_kernel(
|
|||||||
if new_entity.is_null() {
|
if new_entity.is_null() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(new_root) = recreate_ext_subtree(doc, cap, Some(new_entity)) {
|
if let Some(new_root) = recreate_ext_subtree(doc, cap, Some(new_entity), &records) {
|
||||||
if let Some(e) = doc.get_entity_mut(new_entity) {
|
if let Some(e) = doc.get_entity_mut(new_entity) {
|
||||||
e.common_mut().xdictionary_handle = Some(new_root);
|
e.common_mut().xdictionary_handle = Some(new_root);
|
||||||
}
|
}
|
||||||
@@ -837,9 +882,10 @@ fn recreate_ext_subtree(
|
|||||||
doc: &mut codec::CadDocument,
|
doc: &mut codec::CadDocument,
|
||||||
cap: &crate::app::ClipExtObjects,
|
cap: &crate::app::ClipExtObjects,
|
||||||
entity_handle: Option<Handle>,
|
entity_handle: Option<Handle>,
|
||||||
|
records: &std::collections::HashMap<Handle, Handle>,
|
||||||
) -> Option<Handle> {
|
) -> Option<Handle> {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
let mut remap: HashMap<Handle, Handle> = HashMap::new();
|
let mut remap: HashMap<Handle, Handle> = records.clone();
|
||||||
if let Some(eh) = entity_handle {
|
if let Some(eh) = entity_handle {
|
||||||
remap.insert(cap.src_entity_handle, eh);
|
remap.insert(cap.src_entity_handle, eh);
|
||||||
}
|
}
|
||||||
@@ -854,6 +900,14 @@ fn recreate_ext_subtree(
|
|||||||
let mut obj = obj.clone();
|
let mut obj = obj.clone();
|
||||||
let new_h = remap[old];
|
let new_h = remap[old];
|
||||||
remap_object(&mut obj, new_h, &remap);
|
remap_object(&mut obj, new_h, &remap);
|
||||||
|
// A dynamic reference's link to its definition, indexed.
|
||||||
|
if let codec::objects::ObjectType::DynamicBlock(object) = &obj {
|
||||||
|
if let codec::objects::DynamicBlockData::Representation(link) = &object.data {
|
||||||
|
if object.dxf_name == "ACDB_BLOCKREPRESENTATION_DATA" {
|
||||||
|
doc.block_representations.insert(new_h, link.block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
doc.objects.insert(new_h, obj);
|
doc.objects.insert(new_h, obj);
|
||||||
}
|
}
|
||||||
remap.get(&cap.root).copied()
|
remap.get(&cap.root).copied()
|
||||||
@@ -1013,6 +1067,10 @@ pub(crate) fn remap_object(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ObjectType::DynamicBlock(object) => {
|
||||||
|
object.handle = new_handle;
|
||||||
|
object.visit_handles_mut(&mut |h| *h = map(*h));
|
||||||
|
}
|
||||||
// Other leaf object kinds don't appear in an entity xdictionary; if one
|
// Other leaf object kinds don't appear in an entity xdictionary; if one
|
||||||
// does, it's inserted with the fresh handle below via the caller's key,
|
// does, it's inserted with the fresh handle below via the caller's key,
|
||||||
// but its internal owner is left as-is (best effort).
|
// but its internal owner is left as-is (best effort).
|
||||||
@@ -1082,7 +1140,7 @@ mod clipboard_xdict_clone_tests {
|
|||||||
let before: Vec<Handle> = dst.objects.keys().copied().collect();
|
let before: Vec<Handle> = dst.objects.keys().copied().collect();
|
||||||
let dst_entity = dst.allocate_handle();
|
let dst_entity = dst.allocate_handle();
|
||||||
|
|
||||||
let new_root = recreate_ext_subtree(&mut dst, &cap, Some(dst_entity))
|
let new_root = recreate_ext_subtree(&mut dst, &cap, Some(dst_entity), &Default::default())
|
||||||
.expect("recreated xdictionary root");
|
.expect("recreated xdictionary root");
|
||||||
assert!(!new_root.is_null());
|
assert!(!new_root.is_null());
|
||||||
assert!(!before.contains(&new_root), "clone must use a fresh handle");
|
assert!(!before.contains(&new_root), "clone must use a fresh handle");
|
||||||
@@ -1119,7 +1177,8 @@ mod clipboard_xdict_clone_tests {
|
|||||||
let mut dst = codec::CadDocument::new();
|
let mut dst = codec::CadDocument::new();
|
||||||
let before_len = dst.objects.len();
|
let before_len = dst.objects.len();
|
||||||
|
|
||||||
let root = recreate_ext_subtree(&mut dst, &cap, None).expect("recreated xdictionary root");
|
let root = recreate_ext_subtree(&mut dst, &cap, None, &Default::default())
|
||||||
|
.expect("recreated xdictionary root");
|
||||||
let leaf = leaf_of(&dst, root);
|
let leaf = leaf_of(&dst, root);
|
||||||
match dst.objects.get(&leaf) {
|
match dst.objects.get(&leaf) {
|
||||||
Some(ObjectType::XRecord(x)) => assert!(
|
Some(ObjectType::XRecord(x)) => assert!(
|
||||||
|
|||||||
@@ -915,8 +915,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Some(crate::app::BlockDef {
|
Some(crate::app::BlockDef {
|
||||||
name: used,
|
name: used,
|
||||||
base_point: def.base_point,
|
|
||||||
entities,
|
entities,
|
||||||
|
..def.clone()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
+16
-2
@@ -1916,6 +1916,10 @@ pub struct BlockDef {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub base_point: codec::types::Vector3,
|
pub base_point: codec::types::Vector3,
|
||||||
pub entities: Vec<codec::EntityType>,
|
pub entities: Vec<codec::EntityType>,
|
||||||
|
/// The source drawing's record, which other captured graphs name.
|
||||||
|
pub source: codec::Handle,
|
||||||
|
/// The record's object graph: a dynamic block's parameters and states.
|
||||||
|
pub graph: Option<codec::BlockGraph>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The extension-dictionary object graph captured for one copied entity.
|
/// The extension-dictionary object graph captured for one copied entity.
|
||||||
@@ -2131,8 +2135,16 @@ impl ClipboardDeps {
|
|||||||
let mut queue: Vec<String> = Vec::new();
|
let mut queue: Vec<String> = Vec::new();
|
||||||
for e in entities {
|
for e in entities {
|
||||||
if let EntityType::Insert(ins) = e {
|
if let EntityType::Insert(ins) = e {
|
||||||
if seen.insert(ins.block_name.clone()) {
|
// A reference showing an evaluated copy needs its dynamic
|
||||||
queue.push(ins.block_name.clone());
|
// definition too.
|
||||||
|
let definition = doc
|
||||||
|
.dynamic_definition_for_insert(ins.common.handle)
|
||||||
|
.and_then(|h| doc.block_records.iter().find(|r| r.handle == h))
|
||||||
|
.map(|record| record.name.clone());
|
||||||
|
for name in std::iter::once(ins.block_name.clone()).chain(definition) {
|
||||||
|
if seen.insert(name.clone()) {
|
||||||
|
queue.push(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2182,6 +2194,8 @@ impl ClipboardDeps {
|
|||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
base_point,
|
base_point,
|
||||||
entities: owned,
|
entities: owned,
|
||||||
|
source: br.handle,
|
||||||
|
graph: doc.export_block_graph(br.handle),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -424,18 +424,27 @@ impl OpenCADStudio {
|
|||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.push_undo_snapshot(i, "INSERT");
|
self.push_undo_snapshot(i, "INSERT");
|
||||||
self.merge_dependencies(i, &deps);
|
self.merge_dependencies(i, &deps);
|
||||||
for def in deps.blocks {
|
let mut created = Vec::new();
|
||||||
|
for def in &deps.blocks {
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
if redefine && def.name.eq_ignore_ascii_case(name) {
|
if redefine && def.name.eq_ignore_ascii_case(name) {
|
||||||
if let Err(err) = scene.redefine_block_raw(&def.name, def.base_point, def.entities)
|
let entities = def.entities.clone();
|
||||||
{
|
if let Err(err) = scene.redefine_block_raw(&def.name, def.base_point, entities) {
|
||||||
self.command_line.push_error(&err);
|
self.command_line.push_error(&err);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
} else if scene.document.block_records.get(&def.name).is_none() {
|
} else if scene.document.block_records.get(&def.name).is_none() {
|
||||||
scene.define_block_raw(&def.name, def.base_point, def.entities);
|
scene.define_block_raw(&def.name, def.base_point, def.entities.clone());
|
||||||
|
created.push(def);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A dynamic definition keeps its parameters and states.
|
||||||
|
use crate::app::command_driver::clipboard::{block_record_map, import_block_def_graph};
|
||||||
|
let document = &mut self.tabs[i].scene.document;
|
||||||
|
let records = block_record_map(document, &deps.blocks);
|
||||||
|
for def in created {
|
||||||
|
import_block_def_graph(document, def, &records);
|
||||||
|
}
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ pub(super) fn dynamic(
|
|||||||
object
|
object
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
|
pub(crate) fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
|
||||||
document
|
document
|
||||||
.block_records
|
.block_records
|
||||||
.iter()
|
.iter()
|
||||||
@@ -409,6 +409,16 @@ pub(super) fn set_entity_tag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Point each tagged member's element tag back at the member itself, after
|
||||||
|
/// the definition was copied onto new handles.
|
||||||
|
pub(crate) fn retag_members(document: &mut CadDocument, definition: Handle) {
|
||||||
|
for handle in members(document, definition) {
|
||||||
|
if let Some(id) = document.get_entity(handle).and_then(element_id) {
|
||||||
|
set_entity_tag(document, handle, element_tag(id, handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Give every object of the definition its element tag, numbering the new
|
/// Give every object of the definition its element tag, numbering the new
|
||||||
/// ones after the highest id in use, and record the next id on the block.
|
/// ones after the highest id in use, and record the next id on the block.
|
||||||
pub(super) fn tag_members(document: &mut CadDocument, definition: Handle) {
|
pub(super) fn tag_members(document: &mut CadDocument, definition: Handle) {
|
||||||
|
|||||||
@@ -521,11 +521,7 @@ impl Scene {
|
|||||||
record.flags.anonymous = true;
|
record.flags.anonymous = true;
|
||||||
let copy = record.handle;
|
let copy = record.handle;
|
||||||
// The copies keep their element ids, now naming themselves.
|
// The copies keep their element ids, now naming themselves.
|
||||||
for handle in members(&self.document, copy) {
|
super::block_authoring::retag_members(&mut self.document, copy);
|
||||||
if let Some(id) = self.document.get_entity(handle).and_then(element_id) {
|
|
||||||
set_entity_tag(&mut self.document, handle, element_tag(id, handle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut tag = ExtendedDataRecord::new(REPRESENTATION_TAG);
|
let mut tag = ExtendedDataRecord::new(REPRESENTATION_TAG);
|
||||||
tag.add_value(XDataValue::Integer16(1));
|
tag.add_value(XDataValue::Integer16(1));
|
||||||
tag.add_value(XDataValue::Handle(definition));
|
tag.add_value(XDataValue::Handle(definition));
|
||||||
|
|||||||
Reference in new issue
Block a user