diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index c708a60a5..d566fa7fa 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -543,6 +543,9 @@ pub(crate) fn paste_entities_kernel( 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); let leader_links: Vec<(Handle, Handle)> = entities .iter() .filter_map(|source| { diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index 6db167936..da573722e 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -720,6 +720,39 @@ pub(crate) fn boundary_entities_from_sources( .collect() } +/// A duplicated associative hatch must follow the duplicated boundary, not the +/// source one — otherwise editing the source regenerates the duplicate onto +/// the source's outline and it seems to vanish. A path whose boundary was not +/// duplicated along loses its association. `handle_map` maps source handles +/// to their duplicates (COPY and paste both build one). (#1370) +pub(crate) fn remap_hatch_associations( + doc: &mut codec::CadDocument, + handle_map: &rustc_hash::FxHashMap, +) -> bool { + let mut touched = false; + for © in handle_map.values() { + let Some(EntityType::Hatch(hatch)) = doc.get_entity_mut(copy) else { + continue; + }; + if !hatch.is_associative { + continue; + } + for path in &mut hatch.paths { + if path.boundary_handles.iter().all(|source| handle_map.contains_key(source)) { + for source in &mut path.boundary_handles { + *source = handle_map[source]; + } + } else { + path.boundary_handles.clear(); + path.flags.set_external(false); + } + } + hatch.is_associative = hatch.paths.iter().any(|path| !path.boundary_handles.is_empty()); + touched = true; + } + touched +} + impl Scene { pub(crate) fn replace_hatch_association(&mut self,handle:Handle,paths:Vec) { let Some(EntityType::Hatch(hatch))=self.document.get_entity_mut(handle) else{return;}; @@ -729,37 +762,12 @@ impl Scene { self.refresh_fill_model(handle); } - /// A copied associative hatch must follow the copied boundary, not the - /// source one — otherwise editing the source regenerates the copy onto the - /// source's outline and the copy seems to vanish. A path whose boundary - /// was not copied along loses its association. (#1370) + /// COPY's half of [`remap_hatch_associations`], plus the cache it feeds. pub(crate) fn copy_hatch_associations( &mut self, handle_map: &rustc_hash::FxHashMap, ) { - let mut touched = false; - for © in handle_map.values() { - let Some(EntityType::Hatch(hatch)) = self.document.get_entity_mut(copy) else { - continue; - }; - if !hatch.is_associative { - continue; - } - for path in &mut hatch.paths { - if path.boundary_handles.iter().all(|source| handle_map.contains_key(source)) { - for source in &mut path.boundary_handles { - *source = handle_map[source]; - } - } else { - path.boundary_handles.clear(); - path.flags.set_external(false); - } - } - hatch.is_associative = - hatch.paths.iter().any(|path| !path.boundary_handles.is_empty()); - touched = true; - } - if touched { + if remap_hatch_associations(&mut self.document, handle_map) { self.associative_hatch_source_cache.borrow_mut().take(); } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6fe667be0..98f658bcf 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -60,7 +60,7 @@ mod parametric_solve; pub(crate) use boundary::{ boundary_entities, boundary_entities_from_sources, boundary_faces, boundary_polyline_entities, exact_hatch_paths, hatch_boundary_rings, hatch_path_directions, hatch_path_ring, - ring_source_handles, separated_hatch_path_groups, BoundarySource, + remap_hatch_associations, ring_source_handles, separated_hatch_path_groups, BoundarySource, }; // Parallel tessellation free functions live in `convert::tess` (alongside the