diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 336e2a3c3..9250e7def 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -620,36 +620,12 @@ impl OpenCADStudio { base_pt - self.clipboard_centroid.as_dvec3() + wo_corr; 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); - // …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(); - // Index-aligned with `clipboard` (NULL = add failed) so the - // captured extension-dictionary subtrees can be matched back - // to their pasted entity by index. - let by_index: Vec = self - .clipboard - .clone() - .into_iter() - .map(|mut entity| { - crate::scene::view::dispatch::apply_transform(&mut entity, &translate); - self.tabs[i].scene.add_entity_clone(entity) - }) - .collect(); - // Recreate each pasted entity's xdictionary graph (XCLIP - // spatial filters etc.) in this document. (#xclip-paste) - self.merge_clipboard_ext_objects(i, &by_index); + let by_index = self.finalize_paste(i, Some(translate)); self.tabs[i].scene.deselect_all(); for h in by_index.iter().copied().filter(|h| !h.is_null()) { self.tabs[i].scene.select_entity(h, false); } - // Tessellate any pasted ACIS solids — top-level ones and - // those inside a recreated block definition — so they - // render instead of staying invisible. (#135) - self.tabs[i].scene.populate_meshes_from_document(); self.tabs[i].dirty = true; // Surface any layers the paste brought in (cross-drawing) // in the layer manager and the layer dropdown. @@ -1872,6 +1848,36 @@ impl OpenCADStudio { } } + /// Shared paste finalize for every paste path (PASTECLIP, PASTEORIG): + /// recreate the clipboard's dependency records and block definitions, add + /// each entity with fresh handles (optionally transformed), recreate each + /// entity's xdictionary graph (XCLIP filters etc.), and tessellate pasted + /// solids. Returns the new handles, index-aligned with the clipboard + /// (NULL where an add failed). Keeping this in one place means a new + /// cross-drawing concern is wired once, not re-implemented per command. + pub(super) fn finalize_paste( + &mut self, + i: usize, + translate: Option, + ) -> Vec { + self.merge_clipboard_deps(i); + self.merge_clipboard_blocks(i); + let by_index: Vec = self + .clipboard + .clone() + .into_iter() + .map(|mut entity| { + if let Some(t) = &translate { + crate::scene::view::dispatch::apply_transform(&mut entity, t); + } + self.tabs[i].scene.add_entity_clone(entity) + }) + .collect(); + self.merge_clipboard_ext_objects(i, &by_index); + self.tabs[i].scene.populate_meshes_from_document(); + by_index + } + /// Recreate the extension-dictionary object graph (XCLIP spatial filters, /// attached XRecords, …) captured for each copied entity, cloning every /// object into this document with fresh handles, remapping all internal @@ -1879,37 +1885,19 @@ impl OpenCADStudio { /// the new root. `by_index` is the paste's new entity handles, aligned with /// the clipboard order (NULL where the add failed). No-op without captures. pub(super) fn merge_clipboard_ext_objects(&mut self, i: usize, by_index: &[Handle]) { - use std::collections::HashMap; if self.clipboard_deps.ext_objects.is_empty() { return; } let captures = self.clipboard_deps.ext_objects.clone(); let doc = &mut self.tabs[i].scene.document; - for cap in captures { + for cap in &captures { let Some(&new_entity) = by_index.get(cap.entity_index) else { continue; }; if new_entity.is_null() { continue; } - // old handle → fresh handle for the whole subtree, plus the entity - // itself so an object owned by the entity remaps onto the new copy. - let mut remap: HashMap = HashMap::new(); - remap.insert(cap.src_entity_handle, new_entity); - for (old, _) in &cap.objects { - // `allocate_handle` advances the counter; `next_handle()` only - // peeks, so reusing it here would hand every object the same - // handle and they'd overwrite each other in `doc.objects`. - remap.insert(*old, doc.allocate_handle()); - } - for (old, obj) in &cap.objects { - let mut obj = obj.clone(); - let new_h = remap[old]; - remap_object(&mut obj, new_h, &remap); - doc.objects.insert(new_h, obj); - } - // Point the pasted entity at the cloned root dictionary. - if let Some(new_root) = remap.get(&cap.root).copied() { + if let Some(new_root) = recreate_ext_subtree(doc, cap, Some(new_entity)) { if let Some(e) = doc.get_entity_mut(new_entity) { e.common_mut().xdictionary_handle = Some(new_root); } @@ -1920,6 +1908,30 @@ impl OpenCADStudio { self.tabs[i].scene.bump_geometry(); } + /// Recreate the captured xdictionary subtrees in this document (fresh + /// handles, remapped references) WITHOUT an added host entity, returning + /// `entity_index → new xdictionary root`. Used by PASTEBLOCK, which folds + /// the clipboard into a new block definition: the caller stamps each new + /// root onto the matching entity's `xdictionary_handle` before defining the + /// block, so the block's nested insert keeps its XCLIP filter. + pub(super) fn recreate_clipboard_ext_roots( + &mut self, + i: usize, + ) -> std::collections::HashMap { + let mut out = std::collections::HashMap::new(); + if self.clipboard_deps.ext_objects.is_empty() { + return out; + } + let captures = self.clipboard_deps.ext_objects.clone(); + let doc = &mut self.tabs[i].scene.document; + for cap in &captures { + if let Some(new_root) = recreate_ext_subtree(doc, cap, None) { + out.insert(cap.entity_index, new_root); + } + } + out + } + fn restore_pre_cmd_tangent(&mut self) { if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { @@ -1934,6 +1946,33 @@ impl OpenCADStudio { } } +/// Clone one captured xdictionary subtree into `doc` with fresh handles, +/// remapping every internal reference (and the owning entity, when known), +/// returning the new root handle. `allocate_handle` advances the document's +/// handle counter — `next_handle()` only peeks, so reusing it would hand every +/// object the same handle and collapse the dictionary chain. +fn recreate_ext_subtree( + doc: &mut acadrust::CadDocument, + cap: &crate::app::ClipExtObjects, + entity_handle: Option, +) -> Option { + use std::collections::HashMap; + let mut remap: HashMap = HashMap::new(); + if let Some(eh) = entity_handle { + remap.insert(cap.src_entity_handle, eh); + } + for (old, _) in &cap.objects { + remap.insert(*old, doc.allocate_handle()); + } + for (old, obj) in &cap.objects { + let mut obj = obj.clone(); + let new_h = remap[old]; + remap_object(&mut obj, new_h, &remap); + doc.objects.insert(new_h, obj); + } + remap.get(&cap.root).copied() +} + /// Rewrite a cloned extension-dictionary object onto fresh handles: set its own /// handle to `new_handle` and remap its owner and any handle references it holds /// through `remap` (a handle still in the source space stays unchanged, which is diff --git a/src/app/commands.rs b/src/app/commands.rs index 1079dbcae..02ece38f3 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -623,27 +623,8 @@ impl OpenCADStudio { } else { let count = self.clipboard.len(); self.push_undo_snapshot(i, "PASTEORIG"); - // Recreate any layer / style this drawing lacks, then add - // each entity with fresh handles (top-level + inline subs) - // so a same-document paste can't duplicate handles. (#129) - self.merge_clipboard_deps(i); - // …and any block definition the pasted INSERTs reference, - // else a block reference renders empty in a drawing that - // lacks the definition. (#135 / #158) - self.merge_clipboard_blocks(i); - let by_index: Vec = self - .clipboard - .clone() - .into_iter() - .map(|entity| self.tabs[i].scene.add_entity_clone(entity)) - .collect(); - // Recreate each pasted entity's xdictionary graph (XCLIP - // spatial filters etc.) so a cross-drawing paste keeps its - // clip instead of showing the whole block. (#xclip-paste) - self.merge_clipboard_ext_objects(i, &by_index); - // Tessellate any pasted ACIS solids (top-level + inside a - // recreated block) so they render instead of staying blank. - self.tabs[i].scene.populate_meshes_from_document(); + // No transform: entities keep their original coordinates. + let _ = self.finalize_paste(i, None); self.tabs[i].dirty = true; self.refresh_layer_panel(); self.refresh_properties(); @@ -667,9 +648,18 @@ impl OpenCADStudio { // reference, so nested blocks inside the new wrapper block // don't render empty. (#135 / #158) self.merge_clipboard_blocks(i); + // Recreate each entity's xdictionary graph (XCLIP filters) + // and stamp the new root onto the wrapped entity, so the + // block's nested insert keeps its clip. (#xclip-paste) + let ext_roots = self.recreate_clipboard_ext_roots(i); let name = self.unique_block_name("Block"); let base = self.clipboard_centroid; - let entities = self.clipboard.clone(); + let mut entities = self.clipboard.clone(); + for (idx, root) in ext_roots { + if let Some(e) = entities.get_mut(idx) { + e.common_mut().xdictionary_handle = Some(root); + } + } match self .tabs[i] .scene diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 6c5dbff10..7c97fc7ad 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -82,6 +82,12 @@ pub struct NestedRef { pub lt_is_byblock: bool, pub lw_is_byblock: bool, pub instance_offsets: Vec<[f64; 3]>, + /// XCLIP boundary for this nested insert, in the parent defn's local frame + /// (`None` = unclipped). Baked at build time because the clip's spatial + /// filter lives in `doc.objects`, which isn't reachable at expand time; on + /// expansion it is mapped to world by the accumulated transform and the + /// nested wires are clipped to it. + pub clip_poly: Option>, } #[derive(Clone, Debug)] @@ -413,6 +419,12 @@ fn build_nested_ref( crate::scene::view::render::render_style_for(doc, &EntityType::Insert(nested_ins.clone())); let _ = bg_color; + // Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps + // its clip when the parent block is expanded — the spatial filter object + // isn't reachable at expand time. + let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins) + .map(|sf| crate::scene::pick::xclip::world_clip_polygon_f64(sf, nested_ins, [0.0; 3])); + NestedRef { block_name: nested_ins.block_name.clone(), xform: nested_ins.get_transform(), @@ -424,6 +436,7 @@ fn build_nested_ref( lt_is_byblock: nested_ins.common.linetype.eq_ignore_ascii_case("byblock"), lw_is_byblock: matches!(nested_ins.common.line_weight, LineWeight::ByBlock), instance_offsets: array_offsets(nested_ins), + clip_poly, } } @@ -1063,6 +1076,11 @@ struct Batches { by_style: HashMap, /// Batches that overflowed `MAX_POINTS_PER_BATCH` and have been closed. closed: Vec, + /// Already-finalized wires that bypass the point batcher — currently the + /// clipped output of an XCLIP'd nested insert, which is produced as whole + /// WireModels by `clip_wires`. Appended verbatim at `finalize` (only their + /// name/selected flag are stamped to match the host insert). + extra_wires: Vec, } impl BatchEntry { @@ -1098,7 +1116,9 @@ impl BatchEntry { impl Batches { fn finalize(self, name: &str, selected: bool, bg_color: [f32; 4]) -> Vec { - self.closed + let extra = self.extra_wires; + let mut out: Vec = self + .closed .into_iter() .chain(self.by_style.into_values()) .map(|b| { @@ -1133,7 +1153,15 @@ impl Batches { fill_tris_low: Vec::new(), } }) - .collect() + .collect(); + // Clipped nested-insert wires are already whole WireModels; stamp the + // host insert's name/selected so picking maps them back correctly. + for mut w in extra { + w.name = name.to_string(); + w.selected = selected; + out.push(w); + } + out } } @@ -1302,23 +1330,46 @@ fn expand_defn( bg_color: ctx.bg_color, }; visited.push(nref.block_name.clone()); - for offset in &nref.instance_offsets { - let composed = if offset == &[0.0; 3] { - nref.xform.then(accum_xform) - } else { - let translation = Transform::from_translation(Vector3::new( - offset[0], offset[1], offset[2], - )); - translation.then(&nref.xform).then(accum_xform) - }; - expand_defn( - nested_defn, - &composed, - &inner_ctx, - out, - visited, - depth + 1, - ); + if let Some(cp) = &nref.clip_poly { + // XCLIP'd nested insert: expand into an isolated batch set, + // finalize to whole wires, clip them to the boundary mapped + // into world by the accumulated transform, then carry the + // clipped wires out via `extra_wires`. Single instance only — + // an array of clipped nested inserts is vanishingly rare, so + // the array case is not handled (boundary placement per + // instance would differ). + let composed = nref.xform.then(accum_xform); + let mut sub = Batches::default(); + expand_defn(nested_defn, &composed, &inner_ctx, &mut sub, visited, depth + 1); + let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); + let world_poly: Vec<[f64; 2]> = cp + .iter() + .map(|&[x, y]| { + let w = accum_xform.apply(Vector3::new(x, y, 0.0)); + [w.x, w.y] + }) + .collect(); + crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly); + out.extra_wires.append(&mut wires); + } else { + for offset in &nref.instance_offsets { + let composed = if offset == &[0.0; 3] { + nref.xform.then(accum_xform) + } else { + let translation = Transform::from_translation(Vector3::new( + offset[0], offset[1], offset[2], + )); + translation.then(&nref.xform).then(accum_xform) + }; + expand_defn( + nested_defn, + &composed, + &inner_ctx, + out, + visited, + depth + 1, + ); + } } visited.pop(); }