From 6b5c2758e109a855f99feb586c3f0ec97f2b9f5e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 4 Oct 2026 20:44:08 +0300 Subject: [PATCH] fix(hatch): keep the association when TRIM splits a boundary Splitting a boundary object erased it and added the pieces, so the hatch lost that source and rebuilt from what was left, often the whole circle. The pieces now take over the old object's association before it is erased. Fixes #1572 Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/clipboard.rs | 11 +++++++- src/app/command_driver/mod.rs | 9 ++++--- src/scene/boundary.rs | 42 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index d566fa7fa..74738f0f7 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -170,11 +170,20 @@ impl OpenCADStudio { } let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); - self.tabs[i].scene.erase_entities(&[handle]); + // 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!( diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 21b19199e..33edae656 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -381,8 +381,7 @@ impl OpenCADStudio { .document .get_entity(handle) .map(|entity| entity.common().owner_handle); - self.tabs[tab].scene.erase_entities(&[handle]); - entities + let pieces: Vec = entities .into_iter() .map(|mut entity| { entity.common_mut().handle = Handle::NULL; @@ -391,7 +390,11 @@ impl OpenCADStudio { } self.tabs[tab].scene.add_entity(entity) }) - .collect() + .collect(); + // Hand the hatch associations over before the original goes. + self.tabs[tab].scene.split_hatch_source(handle, &pieces); + self.tabs[tab].scene.erase_entities(&[handle]); + pieces }; for &updated in &handles { if matches!( diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index da573722e..1c7aae65e 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -816,6 +816,48 @@ impl Scene { true } + /// A boundary source split into pieces (TRIM, BREAK): every associative + /// hatch bounded by `old` is bounded by all of `pieces` instead, so the + /// region survives `old` being erased. + pub(crate) fn split_hatch_source(&mut self, old: Handle, pieces: &[Handle]) { + if pieces.is_empty() { + return; + } + let dependents = self.associative_hatch_dependents(&std::iter::once(old).collect()); + if dependents.is_empty() { + return; + } + for &hatch_handle in &dependents { + if self.is_recording_undo() { + let before = self.document.get_entity_arc(hatch_handle); + self.record_undo_before(hatch_handle, before); + } + let Some(EntityType::Hatch(hatch)) = self.document.get_entity_mut(hatch_handle) else { + continue; + }; + for path in &mut hatch.paths { + if !path.boundary_handles.contains(&old) { + continue; + } + path.boundary_handles.retain(|source| *source != old); + for &piece in pieces { + if !path.boundary_handles.contains(&piece) { + path.boundary_handles.push(piece); + } + } + } + } + self.associative_hatch_source_cache.borrow_mut().take(); + let changes: Vec<_> = pieces + .iter() + .map(|&piece| (piece, ChangeKind::Modified)) + .collect(); + let refreshed = self.refresh_associative_hatches(&changes); + if !refreshed.is_empty() { + self.bump_entities(&refreshed); + } + } + fn associative_hatch_dependents( &self, changed: &rustc_hash::FxHashSet,