fix(paste): pasted associative hatches follow the pasted boundary

Paste (and WBLOCK, which shares the kernel) kept the source boundary
handles, so the pasted hatch stayed tied to the original outline or to
unrelated handles in another drawing. The COPY remap now lives at the
document level and runs for both.

Refs #1370

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-28 20:33:54 +03:00
1 parent 71b300cc44
commit 3754426361
3 files changed
+39 -28

No files matched your search

+3
View File
@@ -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| {
+35 -27
View File
@@ -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<Handle, Handle>,
) -> bool {
let mut touched = false;
for &copy 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<codec::entities::BoundaryPath>) {
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<Handle, Handle>,
) {
let mut touched = false;
for &copy 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();
}
}
+1 -1
View File
@@ -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