fix(clipboard): carry block definitions across drawings on paste

Copying an INSERT and pasting it into another drawing rendered nothing:
the clipboard snapshotted referenced layers/linetypes/styles but not the
block definition itself, so the pasted reference pointed at a block the
target drawing didn't have.

Capture every block an copied INSERT references — walking nested INSERTs
transitively, skipping model/paper space and xref blocks — and store the
definition's base point plus its owned entities. On paste, recreate any
of those blocks the target drawing lacks via a new define_block_raw
(entities kept in block-local coordinates), before adding the pasted
references. Same-document pastes are unaffected since the blocks already
exist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-20 21:23:59 +03:00
1 parent 490c0f6cf9
commit e562738d9e
3 files changed
+138

No files matched your search

+22
View File
@@ -600,6 +600,9 @@ impl OpenCADStudio {
// Recreate any layer / linetype / style the copied entities
// need but this drawing lacks (cross-drawing paste). (#129)
self.merge_clipboard_deps(i);
// …and any block definition the pasted INSERTs reference,
// else a block reference renders empty. (#135)
self.merge_clipboard_blocks(i);
let count = self.clipboard.len();
let new_handles: Vec<Handle> = self
.clipboard
@@ -1768,6 +1771,25 @@ impl OpenCADStudio {
}
}
/// Recreate any block definition the pasted INSERTs reference but tab
/// `i`'s document lacks (cross-drawing paste), so the block reference
/// renders its geometry instead of nothing. No-op for same-document
/// pastes. (#135)
pub(super) fn merge_clipboard_blocks(&mut self, i: usize) {
if self.clipboard_deps.blocks.is_empty() {
return;
}
let blocks = self.clipboard_deps.blocks.clone();
for def in blocks {
if self.tabs[i].scene.document.block_records.get(&def.name).is_some() {
continue;
}
self.tabs[i]
.scene
.define_block_raw(&def.name, def.base_point, def.entities);
}
}
fn restore_pre_cmd_tangent(&mut self) {
if let Some(was_on) = self.pre_cmd_tangent.take() {
if !was_on {
+71
View File
@@ -651,6 +651,21 @@ pub struct ClipboardDeps {
pub linetypes: Vec<acadrust::tables::LineType>,
pub text_styles: Vec<acadrust::tables::TextStyle>,
pub dim_styles: Vec<acadrust::tables::DimStyle>,
/// Block definitions the copied INSERTs reference (transitively),
/// snapshotted from the source drawing. Recreated on paste so a block
/// reference doesn't render empty in a drawing that lacks the
/// definition. (#135)
pub blocks: Vec<BlockDef>,
}
/// A captured block definition: its base point and the entities it owns
/// (in block-local coordinates), minus the structural Block/BlockEnd
/// markers which are rebuilt on paste.
#[derive(Clone)]
pub struct BlockDef {
pub name: String,
pub base_point: acadrust::types::Vector3,
pub entities: Vec<acadrust::EntityType>,
}
impl ClipboardDeps {
@@ -704,14 +719,70 @@ impl ClipboardDeps {
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(),
blocks: Self::capture_blocks(doc, entities),
}
}
/// Snapshot every block definition the `entities` reference through an
/// INSERT, walking nested INSERTs transitively. Model/paper space and
/// xref blocks are skipped — those aren't portable definitions.
fn capture_blocks(doc: &acadrust::CadDocument, entities: &[acadrust::EntityType]) -> Vec<BlockDef> {
use acadrust::EntityType;
use rustc_hash::FxHashSet;
let mut seen: FxHashSet<String> = FxHashSet::default();
let mut queue: Vec<String> = Vec::new();
for e in entities {
if let EntityType::Insert(ins) = e {
if seen.insert(ins.block_name.clone()) {
queue.push(ins.block_name.clone());
}
}
}
let mut defs = Vec::new();
while let Some(name) = queue.pop() {
let Some(br) = doc.block_records.get(&name) else {
continue;
};
if name.starts_with("*Model_Space")
|| name.starts_with("*Paper_Space")
|| br.flags.is_xref
{
continue;
}
let base_point = match doc.get_entity(br.block_entity_handle) {
Some(EntityType::Block(b)) => b.base_point,
_ => acadrust::types::Vector3::ZERO,
};
let mut owned = Vec::new();
for &eh in &br.entity_handles {
let Some(e) = doc.get_entity(eh) else {
continue;
};
if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
continue;
}
if let EntityType::Insert(ins) = e {
if seen.insert(ins.block_name.clone()) {
queue.push(ins.block_name.clone());
}
}
owned.push(e.clone());
}
defs.push(BlockDef {
name,
base_point,
entities: owned,
});
}
defs
}
pub fn is_empty(&self) -> bool {
self.layers.is_empty()
&& self.linetypes.is_empty()
&& self.text_styles.is_empty()
&& self.dim_styles.is_empty()
&& self.blocks.is_empty()
}
}
+45
View File
@@ -3820,6 +3820,51 @@ impl Scene {
Ok(())
}
/// Recreate a block definition verbatim — the entities are already in
/// block-local coordinates (unlike `define_block_from_owned_entities`,
/// which folds in a base offset). No-op if the block already exists.
/// Used when pasting an INSERT whose block this drawing lacks. (#135)
pub fn define_block_raw(
&mut self,
name: &str,
base_point: acadrust::types::Vector3,
entities: Vec<EntityType>,
) {
if name.is_empty() || self.document.block_records.get(name).is_some() {
return;
}
let next = self.document.next_handle();
let br_handle = Handle::new(next);
let block_handle = Handle::new(next + 1);
let end_handle = Handle::new(next + 2);
let mut block_record = acadrust::tables::BlockRecord::new(name);
block_record.handle = br_handle;
block_record.block_entity_handle = block_handle;
block_record.block_end_handle = end_handle;
if self.document.block_records.add(block_record).is_err() {
return;
}
let mut block = Block::new(name, base_point);
block.common.handle = block_handle;
block.common.owner_handle = br_handle;
let _ = self.document.add_entity(EntityType::Block(block));
let mut block_end = BlockEnd::new();
block_end.common.handle = end_handle;
block_end.common.owner_handle = br_handle;
let _ = self.document.add_entity(EntityType::BlockEnd(block_end));
for mut entity in entities {
Self::reset_clone_subhandles(&mut self.document, &mut entity);
entity.common_mut().handle = Handle::NULL;
entity.common_mut().owner_handle = br_handle;
let _ = self.document.add_entity(entity);
}
self.bump_geometry();
}
fn synced_hatch_models(&self) -> Vec<HatchModel> {
let layout_block = self.current_layout_block_handle();
let hatch_offset = if self.current_layout == "Model" {