use super::*; impl OpenCADStudio { pub(super) fn handle_copy_to_clipboard(&mut self, handles: Vec, base: glam::DVec3) { let i = self.active_tab; let count = self.copy_entities_to_clipboard(i, &handles, base); self.command_line .push_info(crate::tf!("{} object(s) copied to clipboard.", count).as_ref()); let prompt = self.tabs[i] .active_cmd .as_ref() .map(|command| command.prompt()); if let Some(prompt) = prompt { self.command_line.push_info(&prompt); } self.command_line.set_step_options( self.tabs[i] .active_cmd .as_ref() .map(|command| command.options()) .unwrap_or_default(), ); if !self.tabs[i] .active_cmd .as_ref() .is_some_and(|command| command.entity_pick_highlights_hover()) { self.tabs[i].scene.set_hover_highlight(None); } self.sync_dyn_fields(); self.refresh_area_preview(i); } pub(super) fn handle_batch_copy( &mut self, mut handles: Vec, transforms: Vec, ) -> Option> { let i = self.active_tab; handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); if handles.is_empty() { self.tabs[i].active_cmd = None; return Some(Task::none()); } let label = self.history_label_from_active_cmd(i, "ARRAY"); let count = transforms.len(); // Same gate as COPY (dimension-free), sized by the total number // of copies the array will add. let delta_safe = self.delta_copy_safe(i, &handles); let pending = self.begin_undo(i, label.clone(), handles.len() * count, delta_safe); for t in &transforms { self.tabs[i].scene.copy_entities(&handles, t); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.restore_pre_cmd_tangent(); let noun = if count == 1 { "copy" } else { "copies" }; self.command_line .push_output(crate::tf!("{label}: {count} {noun} created.").as_ref()); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } /// ARRAYRECT / ARRAYPOLAR with Associative on: the objects become the /// source of an associative array, in one undo step. pub(super) fn handle_assoc_array( &mut self, mut handles: Vec, spec: crate::scene::assoc_array::AssocArraySpec, ) { let i = self.active_tab; handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.restore_pre_cmd_tangent(); if handles.is_empty() { return; } let label = self.history_label_from_active_cmd(i, "ARRAY"); let source_point = crate::app::helpers::entities_lower_left_by_bbox( &self.tabs[i].scene.document, &handles, ); self.push_undo_snapshot(i, label.clone()); match crate::scene::assoc_array::create_from_spec( &mut self.tabs[i].scene, &handles, source_point, &spec, ) { Ok(array) => { self.tabs[i].scene.deselect_all(); self.tabs[i].scene.select_entity(array, false); self.command_line .push_output(crate::tf!("{label}: associative array created.").as_ref()); } Err(error) => self.command_line.push_error(&format!("{label}: {error}")), } self.tabs[i].dirty = true; self.refresh_properties(); } pub(super) fn handle_replace_many( &mut self, replacements: Vec<(Handle, Vec)>, additions: Vec, ) -> Option> { let i = self.active_tab; if let Some((handle, _)) = replacements .iter() .find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle)) { self.reject_locked_edit(i, *handle); self.tabs[i].active_cmd = None; return Some(Task::none()); } let label = self.history_label_from_active_cmd(i, "FILLET"); let was_catchment = self.tabs[i] .active_cmd .as_ref() .is_some_and(|c| c.name() == "SS_CATCHMENT"); self.push_undo_snapshot(i, label); for (handle, entities) in replacements { self.replace_command_entity(i, handle, entities); } for entity in additions { self.tabs[i].scene.add_entity(entity); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if was_catchment { self.command_line .push_info(crate::t!("Catchment tagged successfully.").as_ref()); } self.refresh_properties(); None } pub(super) fn handle_replace_many_continue( &mut self, replacements: Vec<(Handle, Vec)>, ) -> Option> { let i = self.active_tab; if let Some((handle, _)) = replacements .iter() .find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle)) { self.reject_locked_edit(i, *handle); return Some(Task::none()); } let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); for (handle, entities) in replacements { let new_handles = self.replace_command_entity(i, handle, entities); if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.on_entity_replaced(handle, &new_handles); } } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].snap_result = None; if let Some(prompt) = self.tabs[i] .active_cmd .as_ref() .map(|command| command.prompt()) { self.command_line.push_info(&prompt); } self.refresh_properties(); None } pub(super) fn handle_replace_entity( &mut self, handle: Handle, new_entities: Vec, ) -> Option> { let i = self.active_tab; if self.reject_locked_edit(i, handle) { return Some(Task::none()); } // Detect SPLINEDIT sentinel: a single XLine with a magic layer name. if new_entities.len() == 1 { if let codec::EntityType::XLine(ref xl) = new_entities[0] { let op = xl.common.layer.clone(); if op.starts_with("__SPLINEDIT_") { let label = self.history_label_from_active_cmd(i, "SPLINEDIT"); self.push_undo_snapshot(i, label); crate::modules::draw::modify::splinedit::apply_spline_op( &mut self.tabs[i].scene.document, handle, &op, ); self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } return Some(Task::none()); } } } let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); // A replacement reusing the original handle must land after the erase; // fresh pieces take over its hatch associations before it goes. let reuses_handle = new_entities.iter().any(|e| e.common().handle == handle); if reuses_handle { self.tabs[i].scene.erase_entities(&[handle]); } let new_handles: Vec = new_entities .into_iter() .map(|e| self.tabs[i].scene.add_entity(e)) .collect(); if !reuses_handle { self.tabs[i].scene.split_hatch_source(handle, &new_handles); self.tabs[i].scene.erase_entities(&[handle]); } // Rebuild replaced dimensions from edited data. for &nh in &new_handles { if matches!( self.tabs[i].scene.document.get_entity(nh), Some(codec::EntityType::Dimension(_)) ) { self.tabs[i].scene.invalidate_dim_block_recorded(nh); } } let pedit_entities = if self.tabs[i] .active_cmd .as_ref() .is_some_and(|command| command.name() == "PEDIT") { new_handles .iter() .filter_map(|new| self.tabs[i].scene.document.get_entity(*new).cloned()) .collect::>() } else { Vec::new() }; if let Some(cmd) = &mut self.tabs[i].active_cmd { cmd.on_entity_replaced(handle, &new_handles); for entity in pedit_entities { cmd.inject_picked_entity(entity); } } self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } None } pub(super) fn handle_paste_clipboard(&mut self, base_pt: glam::DVec3) { let i = self.active_tab; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); if self.clipboard.is_empty() { self.command_line .push_error(crate::t!("Clipboard is empty.").as_ref()); } else { let delta = base_pt - self.clipboard_base; let translate = crate::command::EntityTransform::Translate(delta); self.push_undo_snapshot(i, "PASTECLIP"); let count = self.clipboard.len(); 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); } self.tabs[i].dirty = true; // Surface any layers the paste brought in (cross-drawing) // in the layer manager and the layer dropdown. self.refresh_layer_panel(); self.refresh_properties(); self.command_line .push_info(crate::tf!("{count} object(s) pasted.").as_ref()); } } pub(crate) fn merge_clipboard_deps(&mut self, i: usize) { let deps = self.clipboard_deps.clone(); self.merge_dependencies(i, &deps); } /// 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(crate) fn merge_clipboard_blocks(&mut self, i: usize) { if self.clipboard_deps.blocks.is_empty() { return; } let blocks = self.clipboard_deps.blocks.clone(); // Single shared copy — see `merge_block_defs`. One batched geometry // rebuild instead of one per definition; same end state as the // per-definition bumps `Scene::define_block_raw` used to run. if merge_block_defs(&mut self.tabs[i].scene.document, &blocks) > 0 { self.tabs[i].scene.bump_geometry(); } } /// Shared paste finalize for every paste path (PASTECLIP, PASTEORIG): /// document-level paste runs in [`paste_entities_kernel`] (fresh handles, /// dependency records, block definitions, `*D` re-point, xdictionary /// graphs, LEADER relink); the Scene/render tail stays here (tessellation /// bumps, group + constraint recreation, annotation-context display sync). /// Returns the new handles, index-aligned with the clipboard /// (NULL where an add failed). Keeping the document half in one kernel /// means a new cross-drawing concern is wired once, not re-implemented /// per command — and headless callers share it without a Scene. pub(crate) fn finalize_paste( &mut self, i: usize, translate: Option, ) -> Vec { // Per-helper narrow/keep report (Task 4): the kernel ports what // `&mut CadDocument` can do and this shell keeps the rest — // merge_dependencies (table records) … KEEP in kernel (verbatim // port; pure document-table work, no Document-level free fn existed) // merge_clipboard_blocks / define_block_raw … KEEP in kernel minus // the trailing bump_geometry (render cache — shelled below) // add_entity_clone … NARROW: sub-handle reset + NULL handles + // doc.add_entity in kernel; entity_mode default, layer/app-id // ensure, ImageDefinition auto-create, BEDIT/paper-space owner // routing, undo recording and per-add tessellation stay caller-side // (Scene-only; NO Scene methods refactored) // define_transformed_block (`*D` dim blocks) … KEEP in kernel // (verbatim port; no render-cache tail exists) // recreate_ext_subtree / remap_object … USE existing Document-level // free fns directly (no Scene involved) // translate_annotation_contexts … USE existing Document-level free fn // LEADER annotation_handle relink … KEEP in kernel (get_entity_mut); // sync_displayed_annotation_context stays caller-side (Scene) // recreate_groups … CALLER-SIDE (Scene::recreate_groups: undo // recording + group-dict naming; no Document equivalent exists) // duplicate_parametric_constraints_for … CALLER-SIDE (Scene-owned // constraint sets; no Document equivalent exists) // populate_missing_meshes_from_document / bumps … CALLER-SIDE let clipboard = self.clipboard.clone(); let deps = self.clipboard_deps.clone(); // Into the space new entities go: the edited block, the layout's // paper space (no viewport active), or model space. let scene = &self.tabs[i].scene; let layout = scene.current_layout.clone(); let space = match scene.block_edit_block { Some(block) => PasteSpace::Block(block), None if layout != "Model" && scene.active_viewport.is_none() => { PasteSpace::Layout(&layout) } None => PasteSpace::Model, }; let (by_index, handle_map) = paste_entities_kernel( &mut self.tabs[i].scene.document, &clipboard, &deps, translate.as_ref(), space, ); // Batched equivalent of `add_entity`'s per-add bump (same pattern as // `Scene::add_entities`): fresh top-level solids tessellate once here // instead of once per entity. Block-definition merges (and pasted // Block/BlockEnd sentinels) need the full geometry rebuild, matching // the `bump_geometry` tails the Scene helpers used to run. let mut needs_geometry_bump = !deps.blocks.is_empty(); let mut changes = Vec::new(); for &handle in &by_index { if handle.is_null() { continue; } // `add_entity` seeds fill and raster models; the document-level // add above does not, so a pasted hatch without an association // to rebuild it was never drawn. (#1807) if matches!( self.tabs[i].scene.document.get_entity(handle), Some( codec::EntityType::Hatch(_) | codec::EntityType::Solid(_) | codec::EntityType::RasterImage(_) | codec::EntityType::Ole2Frame(_) | codec::EntityType::Underlay(_) ) ) { self.tabs[i].scene.reseed_derived_caches(handle); } if matches!( self.tabs[i].scene.document.get_entity(handle), Some(codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_)) ) { needs_geometry_bump = true; } else { changes.push((handle, crate::scene::ChangeKind::Added)); } } if needs_geometry_bump { self.tabs[i].scene.bump_geometry(); } else if !changes.is_empty() { self.tabs[i].scene.bump_entities(&changes); } // The wires tessellated above predate the freshly attached clip object // graphs; refresh the pasted entities so XCLIP-filtered inserts // render clipped immediately. if !deps.ext_objects.is_empty() { let changes: Vec<_> = by_index .iter() .copied() .filter(|handle| !handle.is_null()) .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); } // Source handles stored in the clipboard map one-to-one to the freshly // pasted handles (see `handle_map`); refresh each pasted LEADER's // displayed annotation context now that the relink ran in the kernel. for &handle in by_index.iter().filter(|h| !h.is_null()) { if matches!( self.tabs[i].scene.document.get_entity(handle), Some(codec::EntityType::Leader(_)) ) { let _ = self.tabs[i].scene.sync_displayed_annotation_context(handle); } } // Recreate any group whose whole membership was copied, so a pasted // group stays grouped — cross-drawing too, since the groups were // snapshotted into the clipboard at copy time. `by_index` is aligned // with `self.clipboard`, so the kernel's `handle_map` maps each // clipboard clone to its new handle. Same shared `recreate_groups` // the in-drawing COPY path uses. (#440) if !deps.groups.is_empty() { self.tabs[i] .scene .recreate_groups(deps.groups.clone(), &handle_map); } // Constraints wholly within the pasted selection follow it. self.tabs[i] .scene .duplicate_parametric_constraints_for(&handle_map); // Incremental: fresh top-level solids already tessellated above, and // existing document solids are still cached — so only newly // introduced block-definition solids need building. The full rebuild // would clear and re-tessellate the entire document (every solid in the // drawing) on each paste, which is what made a large paste stall. self.tabs[i].scene.populate_missing_meshes_from_document(); by_index } /// 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(crate) 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; let records = block_record_map(doc, &self.clipboard_deps.blocks); for cap in &captures { if let Some(new_root) = recreate_ext_subtree(doc, cap, None, &records) { out.insert(cap.entity_index, new_root); } } out } } /// PASTECLIP kernel: paste a clipboard payload (`entities` + `deps`, as /// captured by [`copy_to_clipboard_kernel`]) into `doc`, optionally /// translated, returning the new handles index-aligned with `entities` /// Recreate missing layer/linetype/text-style/dim-style records with fresh /// handles from the target document. Shared by `merge_dependencies` (PASTEBLOCK /// path) and `paste_entities_kernel` — one copy, not a port per caller. pub(crate) fn merge_table_records(doc: &mut codec::CadDocument, deps: &crate::app::ClipboardDeps) { use codec::TableEntry; for rec in &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 &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 &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 &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); } } } /// Recreate block definitions the pasted INSERTs reference but the target /// document lacks. Shared by `merge_clipboard_blocks` and /// `paste_entities_kernel`. Returns how many definitions were created, so /// render-cache callers can bump once instead of per definition. pub(crate) fn merge_block_defs( doc: &mut codec::CadDocument, blocks: &[crate::app::BlockDef], ) -> usize { let mut created = Vec::new(); for def in blocks { let before = doc.block_records.len(); define_block_raw_doc(doc, &def.name, def.base_point, &def.entities); if doc.block_records.len() > before { created.push(def); } } // 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 { 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, ) { 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. /// /// Document-level work lives here; Scene/render concerns stay caller-side in /// `finalize_paste` (tessellation bumps, `populate_missing_meshes`, /// `recreate_groups`, `duplicate_parametric_constraints_for`, /// `sync_displayed_annotation_context`, undo/dirty/selection/echo/panels). /// See the per-helper report in `finalize_paste`. /// Where pasted entities go, the way `Scene::add_entity` routes a new one. #[derive(Clone, Copy)] pub(crate) enum PasteSpace<'a> { Model, /// A layout's paper space, when no viewport of it is active. Layout(&'a str), /// The block definition being edited (BEDIT). Block(Handle), } pub(crate) fn paste_entities_kernel( doc: &mut codec::CadDocument, entities: &[codec::EntityType], deps: &crate::app::ClipboardDeps, translate: Option<&crate::command::EntityTransform>, space: PasteSpace, ) -> (Vec, rustc_hash::FxHashMap) { // Shared table-record merge (also used by `merge_dependencies`): recreate // missing records with fresh handles from the target document. merge_table_records(doc, deps); // Shared block-definition merge (also used by `merge_clipboard_blocks`). merge_block_defs(doc, &deps.blocks); let by_index: Vec = entities .iter() .cloned() .map(|mut entity| { let picture_placement = crate::scene::render_graph::dim_picture_placement(&entity); if let Some(t) = translate { crate::scene::view::dispatch::apply_transform(&mut entity, t); } crate::scene::render_graph::clear_dim_picture_placement(&mut entity); // Same `*D`-block re-point as `finalize_paste` (#290, #161): the // baked dimension geometry lives in WCS, so the paste gets its own // transformed copy of the snapshotted block. if let codec::EntityType::Dimension(d) = &entity { let bn = d.base().block_name.clone(); if !bn.trim().is_empty() { if let Some(subs) = deps .dim_blocks .iter() .find(|b| b.name.eq_ignore_ascii_case(&bn)) .map(|def| def.entities.clone()) { let mut subs = subs; if let Some(placement) = &picture_placement { for sub in &mut subs { crate::scene::view::dispatch::apply_transform(sub, placement); } } let bt = translate.cloned().unwrap_or( crate::command::EntityTransform::Translate(glam::DVec3::ZERO), ); if let Some(new_bn) = define_transformed_block_doc(doc, &subs, &bt) { if let codec::EntityType::Dimension(d) = &mut entity { d.base_mut().block_name = new_bn; } } } } } add_entity_clone_doc(doc, entity, space) }) .collect(); // Port of `merge_clipboard_ext_objects` minus the trailing `bump_entities` // (render cache — caller-side): recreate each captured xdictionary graph // with fresh handles and re-point the pasted entity at its new root. let annotation_delta = match translate { Some(crate::command::EntityTransform::Translate(delta)) => *delta, _ => glam::DVec3::ZERO, }; let records = block_record_map(doc, &deps.blocks); for cap in &deps.ext_objects { let Some(&new_entity) = by_index.get(cap.entity_index) else { continue; }; if new_entity.is_null() { continue; } if let Some(new_root) = recreate_ext_subtree(doc, cap, Some(new_entity), &records) { if let Some(e) = doc.get_entity_mut(new_entity) { e.common_mut().xdictionary_handle = Some(new_root); } crate::scene::annotative::translate_annotation_contexts( doc, new_entity, annotation_delta, ); } } // Source handles map one-to-one to the freshly pasted handles; reconnect // LEADER -> copied annotation through the map (the // `sync_displayed_annotation_context` refresh stays caller-side). let mut handle_map: rustc_hash::FxHashMap = rustc_hash::FxHashMap::default(); for (source, &copied) in entities.iter().zip(by_index.iter()) { if !copied.is_null() { handle_map.insert(source.common().handle, copied); } } // A pasted associative hatch follows the pasted boundary, or drops the // association when the boundary was not pasted with it. (#1370) crate::scene::remap_hatch_associations(doc, &handle_map); // Reactors on the source drawing's objects do not travel with the paste. crate::scene::remap_copied_reactors(doc, &handle_map); let leader_links: Vec<(Handle, Handle)> = entities .iter() .filter_map(|source| { let codec::EntityType::Leader(leader) = source else { return None; }; let copied_leader = handle_map.get(&source.common().handle).copied()?; let copied_annotation = handle_map .get(&leader.annotation_handle) .copied() .unwrap_or(Handle::NULL); Some((copied_leader, copied_annotation)) }) .collect(); for (leader_handle, annotation_handle) in leader_links { if let Some(codec::EntityType::Leader(leader)) = doc.get_entity_mut(leader_handle) { leader.annotation_handle = annotation_handle; } } (by_index, handle_map) } /// Document-level clone-add: the `&mut CadDocument` half of /// `Scene::add_entity_clone` — fresh inline sub-handles (INSERT attributes, /// 3D-polyline vertices; `reset_clone_subhandles` is `scene`-private so the /// two lines are mirrored here), NULL top-level/owner handles, then /// adds it to `space`. Tessellation/render caching stays caller-side in the /// Scene shell. fn add_entity_clone_doc( doc: &mut codec::CadDocument, mut entity: codec::EntityType, space: PasteSpace, ) -> Handle { match &mut entity { codec::EntityType::Insert(ins) => { for att in ins.attributes.iter_mut() { att.common.handle = doc.allocate_handle(); } } codec::EntityType::Polyline3D(p) => { for v in p.vertices.iter_mut() { v.handle = doc.allocate_handle(); } } _ => {} } entity.common_mut().handle = Handle::NULL; entity.common_mut().owner_handle = Handle::NULL; match space { PasteSpace::Model => doc.add_entity(entity), PasteSpace::Layout(layout) => doc.add_entity_to_layout(entity, layout), PasteSpace::Block(block) => { entity.common_mut().owner_handle = block; doc.add_entity(entity) } } .unwrap_or(Handle::NULL) } /// Document-level verbatim block-definition recreate: the `&mut CadDocument` /// half of `Scene::define_block_raw` (no-op when the block exists). The /// trailing `bump_geometry` stays caller-side. pub(crate) fn define_block_raw_doc( doc: &mut codec::CadDocument, name: &str, base_point: codec::types::Vector3, entities: &[codec::EntityType], ) { if name.is_empty() || doc.block_records.get(name).is_some() { return; } let next = doc.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 = codec::tables::BlockRecord::new(name); block_record.handle = br_handle; block_record.block_entity_handle = block_handle; block_record.block_end_handle = end_handle; if doc.block_records.add(block_record).is_err() { return; } let mut block = codec::entities::Block::new(name, base_point); block.common.handle = block_handle; block.common.owner_handle = br_handle; let _ = doc.add_entity(codec::EntityType::Block(block)); let mut block_end = codec::entities::BlockEnd::new(); block_end.common.handle = end_handle; block_end.common.owner_handle = br_handle; let _ = doc.add_entity(codec::EntityType::BlockEnd(block_end)); for mut entity in entities.iter().cloned() { add_entity_clone_doc_inner(doc, &mut entity, br_handle); let _ = doc.add_entity(entity); } } /// Shared sub-handle reset + owner stamp for block-definition members. fn add_entity_clone_doc_inner( doc: &mut codec::CadDocument, entity: &mut codec::EntityType, owner: Handle, ) { match entity { codec::EntityType::Insert(ins) => { for att in ins.attributes.iter_mut() { att.common.handle = doc.allocate_handle(); } } codec::EntityType::Polyline3D(p) => { for v in p.vertices.iter_mut() { v.handle = doc.allocate_handle(); } } _ => {} } entity.common_mut().handle = Handle::NULL; entity.common_mut().owner_handle = owner; // An extension dictionary is exclusively owned; the clone must not claim // the source's (cross-drawing it would dangle into the source document), // or file validation reports "Extension dictionary Cannot access". // Paste reattaches a recreated subtree afterwards (finalize_paste). entity.common_mut().xdictionary_handle = None; } /// Document-level fresh `*D` block from `subs`: the `&mut CadDocument` /// half of `Scene::define_transformed_block` (which has no render-cache tail, /// so this is a verbatim port). Returns the new block name, or `None` when /// `subs` is empty. fn define_transformed_block_doc( doc: &mut codec::CadDocument, subs: &[codec::EntityType], t: &crate::command::EntityTransform, ) -> Option { if subs.is_empty() { return None; } let mut n = 0u64; let new_name = loop { let cand = format!("*D{n}"); if doc.block_records.get(&cand).is_none() { break cand; } n += 1; }; let next = doc.next_handle(); let br_handle = Handle::new(next); let block_handle = Handle::new(next + 1); let end_handle = Handle::new(next + 2); let mut br = codec::tables::BlockRecord::new(&new_name); br.handle = br_handle; br.block_entity_handle = block_handle; br.block_end_handle = end_handle; doc.block_records.add(br).ok()?; let mut block = codec::entities::Block::new(&new_name, codec::types::Vector3::ZERO); block.common.handle = block_handle; block.common.owner_handle = br_handle; doc.add_entity(codec::EntityType::Block(block)).ok()?; let mut block_end = codec::entities::BlockEnd::new(); block_end.common.handle = end_handle; block_end.common.owner_handle = br_handle; doc.add_entity(codec::EntityType::BlockEnd(block_end)) .ok()?; for sub in subs { let mut sub = sub.clone(); crate::scene::view::dispatch::apply_transform(&mut sub, t); add_entity_clone_doc_inner(doc, &mut sub, br_handle); let _ = doc.add_entity(sub); } Some(new_name) } /// COPYCLIP kernel: clone the selected entities out of `doc` and snapshot /// the table records / block definitions / xdictionary graphs they depend on, /// so a later paste can recreate them (including cross-drawing). Clipboard /// storage (`clipboard`, `clipboard_base`, `clipboard_deps`) and the echo stay /// caller-side in `copy_entities_to_clipboard` / `handle_copy_to_clipboard`. pub(crate) fn copy_to_clipboard_kernel( doc: &codec::CadDocument, handles: &[Handle], ) -> (Vec, crate::app::ClipboardDeps) { let entities: Vec<_> = handles .iter() .filter_map(|&handle| doc.get_entity(handle).cloned()) .collect(); let deps = crate::app::ClipboardDeps::capture(doc, &entities); (entities, deps) } fn recreate_ext_subtree( doc: &mut codec::CadDocument, cap: &crate::app::ClipExtObjects, entity_handle: Option, records: &std::collections::HashMap, ) -> Option { use std::collections::HashMap; let mut remap: HashMap = records.clone(); if let Some(eh) = entity_handle { remap.insert(cap.src_entity_handle, eh); } for (old, scale) in &cap.annotation_scales { let target = crate::scene::annotative::ensure_scale_object(doc, scale); remap.insert(*old, target); } 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); // 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); } remap.get(&cap.root).copied() } /// Replace references to a clipboard entity inside one recreated extension /// dictionary graph after its final block-owned handle becomes known. pub(crate) fn remap_ext_subtree_reference( doc: &mut codec::CadDocument, root: Handle, source_entity: Handle, target_entity: Handle, ) { use codec::objects::ObjectType; use rustc_hash::FxHashSet; use std::collections::HashMap; let remap = HashMap::from([(source_entity, target_entity)]); let mut seen = FxHashSet::default(); let mut pending = vec![root]; while let Some(handle) = pending.pop() { if handle.is_null() || !seen.insert(handle) { continue; } let children = match doc.objects.get(&handle) { Some(ObjectType::Dictionary(dictionary)) => { let mut children: Vec<_> = dictionary.entries.iter().map(|(_, child)| *child).collect(); if let Some(extension) = dictionary.xdictionary_handle { children.push(extension); } children } Some(ObjectType::DictionaryWithDefault(dictionary)) => { let mut children: Vec<_> = dictionary.entries.iter().map(|(_, child)| *child).collect(); children.push(dictionary.default_handle); children } _ => Vec::new(), }; pending.extend(children); if let Some(mut object) = doc.objects.remove(&handle) { remap_object(&mut object, handle, &remap); doc.objects.insert(handle, object); } } } /// 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 /// correct for cross-references that point outside the captured subtree). pub(crate) fn remap_object( obj: &mut codec::objects::ObjectType, new_handle: codec::Handle, remap: &std::collections::HashMap, ) { use codec::objects::ObjectType; let map = |h: codec::Handle| remap.get(&h).copied().unwrap_or(h); match obj { ObjectType::Dictionary(d) => { d.handle = new_handle; d.owner = map(d.owner); for (_, h) in d.entries.iter_mut() { *h = map(*h); } if let Some(x) = d.xdictionary_handle.as_mut() { *x = map(*x); } for r in d.reactors.iter_mut() { *r = map(*r); } } ObjectType::DictionaryWithDefault(d) => { d.handle = new_handle; d.owner = map(d.owner); for (_, h) in d.entries.iter_mut() { *h = map(*h); } d.default_handle = map(d.default_handle); } ObjectType::DictionaryVariable(v) => { v.handle = new_handle; v.owner_handle = map(v.owner_handle); } ObjectType::SpatialFilter(s) => { s.handle = new_handle; s.owner = map(s.owner); } ObjectType::XRecord(x) => { x.handle = new_handle; x.owner = map(x.owner); for entry in &mut x.entries { if let codec::objects::XRecordValue::Handle(handle) = &mut entry.value { *handle = map(*handle); } } } ObjectType::Group(g) => { g.handle = new_handle; g.owner = map(g.owner); for h in g.entities.iter_mut() { *h = map(*h); } } ObjectType::ObjectContextData(context) => { context.handle = new_handle; context.owner_handle = map(context.owner_handle); for reactor in &mut context.reactors { *reactor = map(*reactor); } if let Some(dictionary) = &mut context.xdictionary_handle { *dictionary = map(*dictionary); } context.scale = map(context.scale); match &mut context.kind { codec::objects::ObjectContextKind::Dim(dimension) => { dimension.block = map(dimension.block); } codec::objects::ObjectContextKind::HatchView(hatch) => { hatch.view = map(hatch.view); } codec::objects::ObjectContextKind::MTextAttribute(attribute) => { if let Some(embedded) = &mut attribute.context { embedded.owner_handle = map(embedded.owner_handle); for reactor in &mut embedded.reactors { *reactor = map(*reactor); } if let Some(dictionary) = &mut embedded.xdictionary_handle { *dictionary = map(*dictionary); } embedded.scale = map(embedded.scale); } } codec::objects::ObjectContextKind::MLeader(mleader) => { if let Some(handle) = &mut mleader.text_style_handle { *handle = map(*handle); } if let Some(handle) = &mut mleader.block_content_handle { *handle = map(*handle); } if let Some(handle) = &mut mleader.scale_handle { *handle = map(*handle); } for root in &mut mleader.leader_roots { for line in &mut root.lines { if let Some(handle) = &mut line.line_type_handle { *handle = map(*handle); } if let Some(handle) = &mut line.arrowhead_handle { *handle = map(*handle); } } } } _ => {} } } 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 // does, it's inserted with the fresh handle below via the caller's key, // but its internal owner is left as-is (best effort). _ => {} } } #[cfg(test)] mod clipboard_xdict_clone_tests { use super::recreate_ext_subtree; use super::remap_object; use codec::Handle; // Same re-export path `commands/blocks.rs:356` uses — this import proves // the `mod.rs` re-export still covers external callers. use super::super::remap_ext_subtree_reference; use codec::objects::{ObjectType, XRecordValue}; /// A dictionary root pointing at one XRecord leaf that references the /// source entity — the minimal shape of a captured XCLIP filter graph. fn xdict_capture_fixture() -> crate::app::ClipExtObjects { let mut src = codec::CadDocument::new(); let src_entity = src.allocate_handle(); let src_root = src.allocate_handle(); let src_leaf = src.allocate_handle(); let mut dict = codec::objects::Dictionary::new(); dict.handle = src_root; dict.owner = src_entity; dict.add_entry("XCLIP", src_leaf); let mut xrec = codec::objects::XRecord::new(); xrec.handle = src_leaf; xrec.owner = src_root; xrec.entries.push(codec::objects::XRecordEntry::new( 340, XRecordValue::Handle(src_entity), )); crate::app::ClipExtObjects { entity_index: 0, src_entity_handle: src_entity, root: src_root, objects: vec![ (src_root, ObjectType::Dictionary(dict)), (src_leaf, ObjectType::XRecord(xrec)), ], annotation_scales: Vec::new(), } } fn leaf_of(doc: &codec::CadDocument, root: Handle) -> Handle { match doc.objects.get(&root) { Some(ObjectType::Dictionary(d)) => { assert_eq!(d.entries.len(), 1); d.entries[0].1 } other => panic!("expected recreated Dictionary root, got {other:?}"), } } /// Headless cross-drawing clone: recreate a captured xdictionary subtree /// in a fresh document and assert every remapped handle resolves. #[test] fn cross_drawing_clone_remaps_xdict_handles() { let cap = xdict_capture_fixture(); let mut dst = codec::CadDocument::new(); let before: Vec = dst.objects.keys().copied().collect(); let dst_entity = dst.allocate_handle(); let new_root = recreate_ext_subtree(&mut dst, &cap, Some(dst_entity), &Default::default()) .expect("recreated xdictionary root"); assert!(!new_root.is_null()); assert!(!before.contains(&new_root), "clone must use a fresh handle"); assert_eq!(dst.objects.len(), before.len() + 2); let leaf = leaf_of(&dst, new_root); assert!(!before.contains(&leaf), "leaf must use a fresh handle"); match dst.objects.get(&new_root) { Some(ObjectType::Dictionary(d)) => { assert_eq!(d.handle, new_root); assert_eq!(d.owner, dst_entity); } other => panic!("expected recreated Dictionary root, got {other:?}"), } match dst.objects.get(&leaf) { Some(ObjectType::XRecord(x)) => { assert_eq!(x.handle, leaf); assert_eq!(x.owner, new_root); assert_eq!(x.entries.len(), 1); assert!( matches!(&x.entries[0].value, XRecordValue::Handle(h) if *h == dst_entity), "leaf entity reference must point at the pasted entity" ); } other => panic!("expected recreated XRecord leaf, got {other:?}"), } } /// PASTEBLOCK-style two-phase clone: recreate without a host entity, then /// re-point the subtree at the final block-owned handle. #[test] fn cross_drawing_clone_two_phase_remap_resolves_reference() { let cap = xdict_capture_fixture(); let mut dst = codec::CadDocument::new(); let before_len = dst.objects.len(); let root = recreate_ext_subtree(&mut dst, &cap, None, &Default::default()) .expect("recreated xdictionary root"); let leaf = leaf_of(&dst, root); match dst.objects.get(&leaf) { Some(ObjectType::XRecord(x)) => assert!( matches!(&x.entries[0].value, XRecordValue::Handle(h) if *h == cap.src_entity_handle), "unbound clone must keep the source-space reference" ), other => panic!("expected recreated XRecord leaf, got {other:?}"), } let target = dst.allocate_handle(); remap_ext_subtree_reference(&mut dst, root, cap.src_entity_handle, target); assert_eq!(dst.objects.len(), before_len + 2); match dst.objects.get(&leaf) { Some(ObjectType::XRecord(x)) => assert!( matches!(&x.entries[0].value, XRecordValue::Handle(h) if *h == target), "remapped leaf must point at the block-owned entity" ), other => panic!("expected recreated XRecord leaf, got {other:?}"), } } /// The widened `pub(crate)` kernel rewrites a cloned dictionary onto /// fresh handles through the remap table. #[test] fn remap_object_rewrites_dictionary_handles() { use std::collections::HashMap; let mut scratch = codec::CadDocument::new(); let old_self = scratch.allocate_handle(); let old_owner = scratch.allocate_handle(); let old_child = scratch.allocate_handle(); let new_self = scratch.allocate_handle(); let new_owner = scratch.allocate_handle(); let new_child = scratch.allocate_handle(); let mut dict = codec::objects::Dictionary::new(); dict.handle = old_self; dict.owner = old_owner; dict.add_entry("K", old_child); let mut obj = ObjectType::Dictionary(dict); let remap = HashMap::from([(old_owner, new_owner), (old_child, new_child)]); remap_object(&mut obj, new_self, &remap); match obj { ObjectType::Dictionary(d) => { assert_eq!(d.handle, new_self); assert_eq!(d.owner, new_owner); assert_eq!(d.entries[0].1, new_child); } other => panic!("expected Dictionary, got {other:?}"), } } } #[cfg(test)] mod copy_to_clipboard_kernel_tests { use super::copy_to_clipboard_kernel; fn line_on(layer: &str) -> codec::EntityType { let mut line = codec::entities::Line::new(); line.common.layer = layer.to_string(); codec::EntityType::Line(line) } #[test] fn clones_entities_and_captures_layer_deps() { let mut doc = codec::CadDocument::new(); doc.layers .add(codec::tables::Layer::new("WALLS")) .expect("add layer"); let a = doc.add_entity(line_on("WALLS")).unwrap(); let b = doc.add_entity(line_on("WALLS")).unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&doc, &[a, b]); assert_eq!(entities.len(), 2); assert!(entities.iter().all(|e| e.common().layer == "WALLS")); let handles: Vec<_> = entities.iter().map(|e| e.common().handle).collect(); assert!(handles.contains(&a)); assert!(handles.contains(&b)); assert!(deps.layers.iter().any(|l| l.name == "WALLS")); } #[test] fn skips_handles_missing_from_the_document() { let mut doc = codec::CadDocument::new(); let kept = doc.add_entity(line_on("0")).unwrap(); let missing = doc.allocate_handle(); let (entities, _) = copy_to_clipboard_kernel(&doc, &[kept, missing]); assert_eq!(entities.len(), 1); assert_eq!(entities[0].common().handle, kept); } #[test] fn empty_selection_yields_empty_payload() { let doc = codec::CadDocument::new(); let (entities, deps) = copy_to_clipboard_kernel(&doc, &[]); assert!(entities.is_empty()); assert!(deps.layers.is_empty()); assert!(deps.blocks.is_empty()); } } #[cfg(test)] mod paste_entities_kernel_tests { use super::{copy_to_clipboard_kernel, paste_entities_kernel}; use codec::Handle; fn line_on(layer: &str) -> codec::EntityType { let mut line = codec::entities::Line::new(); line.common.layer = layer.to_string(); codec::EntityType::Line(line) } #[test] fn pastes_with_fresh_handles_and_translate() { let mut seed = codec::CadDocument::new(); seed.layers .add(codec::tables::Layer::new("WALLS")) .expect("add layer"); // Position the seed line away from the origin so the translate is visible. let mut seed_line = codec::entities::Line::from_points( codec::types::Vector3::new(0.0, 0.0, 0.0), codec::types::Vector3::new(10.0, 0.0, 0.0), ); seed_line.common.layer = "WALLS".to_string(); let seed_handle = seed.add_entity(codec::EntityType::Line(seed_line)).unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&seed, &[seed_handle]); let mut dst = codec::CadDocument::new(); let delta = glam::DVec3::new(5.0, 7.0, 0.0); let translate = crate::command::EntityTransform::Translate(delta); let (handles, handle_map) = paste_entities_kernel( &mut dst, &entities, &deps, Some(&translate), crate::app::PasteSpace::Model, ); assert_eq!(handles.len(), 1); let pasted = handles[0]; assert!(!pasted.is_null()); assert_ne!(pasted, seed_handle, "paste must use a fresh handle"); assert_eq!(handle_map.get(&seed_handle), Some(&pasted)); let line = match dst.get_entity(pasted) { Some(codec::EntityType::Line(l)) => l, other => panic!("expected pasted Line, got {other:?}"), }; assert!( (line.start.x - 5.0).abs() < 1e-9, "start.x = {}", line.start.x ); assert!( (line.start.y - 7.0).abs() < 1e-9, "start.y = {}", line.start.y ); assert!((line.end.x - 15.0).abs() < 1e-9, "end.x = {}", line.end.x); // The layer dependency must have been recreated in the target document. assert!(dst.layers.contains("WALLS")); assert_eq!(line.common.layer, "WALLS"); } #[test] fn remaps_leader_annotation_link_to_pasted_handles() { let mut seed = codec::CadDocument::new(); let note = seed .add_entity(codec::EntityType::MText(codec::entities::MText::new())) .unwrap(); let mut leader = codec::entities::Leader::default(); leader.annotation_handle = note; let leader_handle = seed.add_entity(codec::EntityType::Leader(leader)).unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&seed, &[leader_handle, note]); let mut dst = codec::CadDocument::new(); let (handles, _) = paste_entities_kernel( &mut dst, &entities, &deps, None, crate::app::PasteSpace::Model, ); assert_eq!(handles.len(), 2); let pasted_leader = match dst.get_entity(handles[0]) { Some(codec::EntityType::Leader(l)) => l, other => panic!("expected pasted Leader, got {other:?}"), }; assert_eq!( pasted_leader.annotation_handle, handles[1], "leader must point at the pasted annotation, not the source handle" ); } #[test] fn recreates_ext_subtree_and_repoints_xdictionary() { // Minimal XCLIP-shaped capture: a dictionary root over one XRecord leaf // that references the source entity (mirrors clipboard_xdict_clone_tests). use codec::objects::{Dictionary, ObjectType, XRecord, XRecordEntry, XRecordValue}; let mut seed = codec::CadDocument::new(); let seed_entity = seed.add_entity(line_on("0")).unwrap(); let src_root = seed.allocate_handle(); let src_leaf = seed.allocate_handle(); let mut dict = Dictionary::new(); dict.handle = src_root; dict.owner = seed_entity; dict.add_entry("XCLIP", src_leaf); let mut xrec = XRecord::new(); xrec.handle = src_leaf; xrec.owner = src_root; xrec.entries .push(XRecordEntry::new(340, XRecordValue::Handle(seed_entity))); seed.objects.insert(src_root, ObjectType::Dictionary(dict)); seed.objects.insert(src_leaf, ObjectType::XRecord(xrec)); if let Some(e) = seed.get_entity_mut(seed_entity) { e.common_mut().xdictionary_handle = Some(src_root); } let (entities, deps) = copy_to_clipboard_kernel(&seed, &[seed_entity]); assert_eq!(deps.ext_objects.len(), 1, "capture must snapshot the xdict"); let mut dst = codec::CadDocument::new(); let before: Vec = dst.objects.keys().copied().collect(); let (handles, _) = paste_entities_kernel( &mut dst, &entities, &deps, None, crate::app::PasteSpace::Model, ); assert_eq!(handles.len(), 1); let pasted = handles[0]; let new_root = dst .get_entity(pasted) .and_then(|e| e.common().xdictionary_handle) .expect("pasted entity must point at a recreated xdictionary root"); // Handles are document-local: freshness is judged within the target // document, not against the source document's numbers. assert!( !before.contains(&new_root), "recreated root must use a fresh handle in the target document" ); let leaf = match dst.objects.get(&new_root) { Some(ObjectType::Dictionary(d)) => { assert_eq!(d.entries.len(), 1); d.entries[0].1 } other => panic!("expected recreated Dictionary root, got {other:?}"), }; match dst.objects.get(&leaf) { Some(ObjectType::XRecord(x)) => assert!( matches!(&x.entries[0].value, XRecordValue::Handle(h) if *h == pasted), "leaf reference must point at the pasted entity" ), other => panic!("expected recreated XRecord leaf, got {other:?}"), } } } #[cfg(test)] mod headless_reuse_proof_tests { // Task 5 reuse proof — headless only. This module constructs NO // `OpenCADStudio` value and touches no UI/scene state: only // `codec::CadDocument`, entities, and the shared kernels // (`copy_to_clipboard_kernel`, `paste_entities_kernel`, // `match_layer_kernel`, `match_properties_kernel` + `MatchOpts`). use super::{copy_to_clipboard_kernel, paste_entities_kernel}; use crate::entities::match_props::{match_layer_kernel, match_properties_kernel, MatchOpts}; #[test] fn headless_copy_paste_then_match_kernels_share_one_path() { // Phase 1: copy in doc A, paste into doc B with a translate. let mut doc_a = codec::CadDocument::new(); doc_a .layers .add(codec::tables::Layer::new("WALLS")) .expect("add layer"); let mut seed_line = codec::entities::Line::from_points( codec::types::Vector3::new(0.0, 0.0, 0.0), codec::types::Vector3::new(10.0, 0.0, 0.0), ); seed_line.common.layer = "WALLS".to_string(); let seed_handle = doc_a .add_entity(codec::EntityType::Line(seed_line)) .unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&doc_a, &[seed_handle]); assert_eq!(entities.len(), 1); assert_eq!(entities[0].common().layer, "WALLS"); let mut doc_b = codec::CadDocument::new(); let translate = crate::command::EntityTransform::Translate(glam::DVec3::new(5.0, 7.0, 0.0)); let (handles, handle_map) = paste_entities_kernel( &mut doc_b, &entities, &deps, Some(&translate), crate::app::PasteSpace::Model, ); assert_eq!(handles.len(), 1); let pasted = handles[0]; assert!(!pasted.is_null()); assert_ne!(pasted, seed_handle, "paste must use a fresh handle"); assert_eq!(handle_map.get(&seed_handle), Some(&pasted)); assert!(doc_b.layers.contains("WALLS"), "layer dep must follow"); match doc_b.get_entity(pasted) { Some(codec::EntityType::Line(l)) => { assert_eq!(l.common.layer, "WALLS"); assert!((l.start.x - 5.0).abs() < 1e-9, "start.x = {}", l.start.x); assert!((l.start.y - 7.0).abs() < 1e-9, "start.y = {}", l.start.y); assert!((l.end.x - 15.0).abs() < 1e-9, "end.x = {}", l.end.x); } other => panic!("expected pasted Line, got {other:?}"), } // Phase 2: match kernels on a src/dst pair in the same headless doc. let mut src_line = codec::entities::Line::new(); src_line.common.layer = "WALLS".to_string(); src_line.common.linetype_scale = 2.5; src_line.thickness = 1.25; let src = doc_b.add_entity(codec::EntityType::Line(src_line)).unwrap(); let mut dst_line = codec::entities::Line::new(); dst_line.common.layer = "0".to_string(); dst_line.common.linetype_scale = 1.0; dst_line.thickness = 0.0; let dst = doc_b.add_entity(codec::EntityType::Line(dst_line)).unwrap(); let count = match_layer_kernel(&mut doc_b, &[dst], src).expect("match layer"); assert_eq!(count, 1); assert_eq!( doc_b.get_entity(dst).unwrap().common().layer, "WALLS", "match_layer_kernel must transfer the layer" ); let src_clone = doc_b.get_entity(src).cloned().unwrap(); { let dst_entity = doc_b.get_entity_mut(dst).unwrap(); match_properties_kernel(&src_clone, dst_entity, &MatchOpts::all()); } match doc_b.get_entity(dst) { Some(codec::EntityType::Line(l)) => { assert_eq!(l.common.layer, "WALLS"); assert!((l.common.linetype_scale - 2.5).abs() < 1e-9); assert!((l.thickness - 1.25).abs() < 1e-9); } other => panic!("expected matched Line, got {other:?}"), } } #[test] fn paste_owner_override_routes_entity_into_block_record() { let mut source = codec::CadDocument::new(); let line = codec::entities::Line::from_points( codec::types::Vector3::new(0.0, 0.0, 0.0), codec::types::Vector3::new(10.0, 0.0, 0.0), ); let source_handle = source .add_entity(codec::EntityType::Line(line)) .expect("source line"); let (entities, deps) = copy_to_clipboard_kernel(&source, &[source_handle]); let mut target = codec::CadDocument::new(); let block_handle = target.allocate_handle(); let mut block = codec::tables::BlockRecord::new("BEDIT_TARGET"); block.handle = block_handle; target .block_records .add(block) .expect("target block record"); let (handles, _) = paste_entities_kernel( &mut target, &entities, &deps, None, crate::app::PasteSpace::Block(block_handle), ); assert_eq!(handles.len(), 1); let pasted = handles[0]; assert!(!pasted.is_null()); let entity = target.get_entity(pasted).expect("pasted entity must exist"); assert_eq!( entity.common().owner_handle, block_handle, "paste inside BEDIT must belong to the edited block record" ); let record = target .block_records .get("BEDIT_TARGET") .expect("block record must still exist"); assert!( record.entity_handles.contains(&pasted), "pasted entity must be registered as a member of the edited block" ); } } #[cfg(test)] mod define_transformed_block_doc_xdict_tests { use super::define_transformed_block_doc; /// No extension dictionary may be referenced by more than one entity. fn assert_no_shared_xdicts(doc: &codec::CadDocument) { use std::collections::HashMap; let mut owners: HashMap = HashMap::new(); for entity in doc.entities() { if let Some(xd) = entity.common().xdictionary_handle { if !xd.is_null() { *owners.entry(xd).or_default() += 1; } } } let shared: Vec<_> = owners .iter() .filter(|(_, &n)| n > 1) .map(|(h, n)| format!("{h:?} x{n}")) .collect(); assert!( shared.is_empty(), "shared extension dictionaries: {}", shared.join(", ") ); } /// Pasted/derived sub-entities must not share the source's extension /// dictionary. `define_transformed_block_doc` is the verbatim /// document-level port of `Scene::define_transformed_block`, and its /// clone helper likewise preserves `xdictionary_handle`; across drawings /// that handle would dangle into the source document. #[test] fn cloned_subs_do_not_share_source_xdict() { let mut doc = codec::CadDocument::new(); let dict_handle = doc.allocate_handle(); let mut dict = codec::objects::Dictionary::new(); dict.handle = dict_handle; doc.objects .insert(dict_handle, codec::objects::ObjectType::Dictionary(dict)); let mut line = codec::entities::Line::new(); line.common.xdictionary_handle = Some(dict_handle); let src_handle = doc.add_entity(codec::EntityType::Line(line)).unwrap(); let subs = vec![doc.get_entity(src_handle).unwrap().clone()]; let name = define_transformed_block_doc( &mut doc, &subs, &crate::command::EntityTransform::Translate(glam::DVec3::ZERO), ) .expect("clone block must be created"); assert!( name.starts_with("*D"), "anonymous block expected, got {name}" ); assert_no_shared_xdicts(&doc); // Source keeps its dictionary. match doc.get_entity(src_handle) { Some(e) => assert_eq!( e.common().xdictionary_handle, Some(dict_handle), "the source must keep its own dictionary" ), None => panic!("source entity missing"), } // Clones carry none. let owned = doc.block_records.get(&name).unwrap().entity_handles.clone(); assert!(!owned.is_empty(), "clone block should own sub-entities"); for h in &owned { match doc.get_entity(*h) { Some(e) => assert!( e.common().xdictionary_handle.is_none(), "cloned child {h:?} must not carry an extension dictionary" ), None => panic!("cloned child {h:?} missing"), } } } }