From e2f558ae4ffdeeefd9acf648342c5bf2031a33b0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 8 Oct 2026 08:34:49 +0300 Subject: [PATCH] fix(paste): pasted entities land in the current space The paste kernel added every entity to model space. It now takes the space a new entity goes to: the block being edited, a layout's paper space when no viewport is active, or model space. Fixes #1652 Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/clipboard.rs | 82 +++++++++++++++++++++++------ src/app/command_driver/mod.rs | 2 +- src/app/mod.rs | 2 +- src/modules/insert/wblock.rs | 8 ++- 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 151c683ae..73baecd4d 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -308,13 +308,23 @@ impl OpenCADStudio { // populate_missing_meshes_from_document / bumps … CALLER-SIDE let clipboard = self.clipboard.clone(); let deps = self.clipboard_deps.clone(); - let owner_override = self.tabs[i].scene.block_edit_block; + // 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(), - owner_override, + space, ); // Batched equivalent of `add_entity`'s per-add bump (same pattern as // `Scene::add_entities`): fresh top-level solids tessellate once here @@ -474,12 +484,22 @@ pub(crate) fn merge_block_defs(doc: &mut codec::CadDocument, blocks: &[crate::ap /// `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>, - owner_override: Option, + space: PasteSpace, ) -> ( Vec, rustc_hash::FxHashMap, @@ -520,7 +540,7 @@ pub(crate) fn paste_entities_kernel( } } } - add_entity_clone_doc(doc, entity, owner_override) + add_entity_clone_doc(doc, entity, space) }) .collect(); @@ -584,12 +604,12 @@ pub(crate) fn paste_entities_kernel( /// `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 -/// `doc.add_entity`. The `entity_mode` default (Model vs paper space) and all -/// tessellation/render caching stay caller-side in the Scene shell. +/// 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, - owner_override: Option, + space: PasteSpace, ) -> Handle { match &mut entity { codec::EntityType::Insert(ins) => { @@ -605,8 +625,16 @@ fn add_entity_clone_doc( _ => {} } entity.common_mut().handle = Handle::NULL; - entity.common_mut().owner_handle = owner_override.unwrap_or(Handle::NULL); - doc.add_entity(entity).unwrap_or(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` @@ -1160,8 +1188,13 @@ mod paste_entities_kernel_tests { 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), None); + 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]; @@ -1194,7 +1227,13 @@ mod paste_entities_kernel_tests { 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, None); + 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]) { @@ -1237,7 +1276,13 @@ mod paste_entities_kernel_tests { let mut dst = codec::CadDocument::new(); let before: Vec = dst.objects.keys().copied().collect(); - let (handles, _) = paste_entities_kernel(&mut dst, &entities, &deps, None, None); + let (handles, _) = paste_entities_kernel( + &mut dst, + &entities, + &deps, + None, + crate::app::PasteSpace::Model, + ); assert_eq!(handles.len(), 1); let pasted = handles[0]; @@ -1302,8 +1347,13 @@ mod headless_reuse_proof_tests { 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), None); + 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]; @@ -1389,7 +1439,7 @@ mod headless_reuse_proof_tests { &entities, &deps, None, - Some(block_handle), + crate::app::PasteSpace::Block(block_handle), ); assert_eq!(handles.len(), 1); diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index b82bef978..17355fdf3 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -95,7 +95,7 @@ mod constraint; mod dim_edit; pub(crate) use clipboard::{ - copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, + copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace, }; impl OpenCADStudio { diff --git a/src/app/mod.rs b/src/app/mod.rs index 39b4b2260..8faf02008 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod config; pub use automation::{export_headless, serve}; mod annotation_data; mod command_driver; -pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel}; +pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace}; pub(crate) mod commands; pub(crate) mod dim_viewport; #[cfg(test)] diff --git a/src/modules/insert/wblock.rs b/src/modules/insert/wblock.rs index f325cbd31..7073fbc03 100644 --- a/src/modules/insert/wblock.rs +++ b/src/modules/insert/wblock.rs @@ -114,7 +114,13 @@ fn copy_with_dependencies( }) .collect(); let (entities, deps) = crate::app::copy_to_clipboard_kernel(src, &handles); - crate::app::paste_entities_kernel(out, &entities, &deps, translate, None); + crate::app::paste_entities_kernel( + out, + &entities, + &deps, + translate, + crate::app::PasteSpace::Model, + ); } /// Translate every entity of `out` so the overall bounds minimum lands on