From b470d46ffcb666059c2fb057ea5bedcf2b167f27 Mon Sep 17 00:00:00 2001 From: dudedagger42 <76926918+dudedagger42@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:09:59 +0000 Subject: [PATCH] Sketch: rebuilding a projection keeps the sketch's own curves on its points (#5388) (#5537) * claim: #5388 Co-Authored-By: Claude Opus 5.5 * Sketch: rebuilding a projection keeps the sketch's own curves on its points (#5388) When a projected face changed shape, update_link rebuilt the link with drop_link_entities, whose remove_points also deleted every curve using a projected point: a circle the user drew on a projected hole's centre vanished, and the extrude on it lost its profile. Before the projected points go, the sketch's own curves on them move to a free point at the same place. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- crates/sketch/src/link.rs | 20 ++++++++++++++ crates/sketch/src/link_identity_tests.rs | 35 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/crates/sketch/src/link.rs b/crates/sketch/src/link.rs index 0c3cd518ed..c9cd312c0e 100644 --- a/crates/sketch/src/link.rs +++ b/crates/sketch/src/link.rs @@ -1186,11 +1186,31 @@ impl Sketch { let keep = self.links.clone(); self.remove_curves(&curves); let pts = self.link_points(id); + self.free_own_curves(&pts); self.remove_points(&pts); // Removal prunes empty links; this one stays. self.links = keep; } + /// The sketch's own curves that use one of `pts` (a circle drawn on a projected hole's + /// centre) move onto a free point of their own at the same place, so removing `pts` doesn't + /// delete them with the link (#5388). Constraints on `pts` still go with them. + fn free_own_curves(&mut self, pts: &[usize]) { + for &p in pts { + let own: Vec = self.curves.iter().enumerate().filter(|(_, c)| c.link.is_none() && c.kind.uses(p)).map(|(i, _)| i).collect(); + if own.is_empty() { + continue; + } + let Some(pos) = self.points.get(p).map(|q| q.pos) else { continue }; + let Ok(free) = self.add_point(pos, None) else { continue }; + for ci in own { + if let Some(c) = self.curves.get_mut(ci) { + c.kind.map_points(&|q| if q == p { free } else { q }); + } + } + } + } + /// Mark a link as lost (its reference was not found); its geometry stays as it was. pub fn set_link_lost(&mut self, id: &str, lost: bool) { if let Some(l) = self.links.iter_mut().find(|l| l.id == id) { diff --git a/crates/sketch/src/link_identity_tests.rs b/crates/sketch/src/link_identity_tests.rs index bfa5118543..31c91d2292 100644 --- a/crates/sketch/src/link_identity_tests.rs +++ b/crates/sketch/src/link_identity_tests.rs @@ -423,3 +423,38 @@ fn edge_on_face_saved_as_a_line_pair_keeps_the_line_an_offset_follows() { let end = sk.points[sk.link_points(&link)[1]].pos; assert_eq!(end, Vec2::new(17.0, 5.0), "the line moved to the new geometry"); } + +/// #5388: a circle the user drew on a projected hole's centre survives when the face changes +/// shape and its projection is rebuilt (a second hole added to the face). +#[test] +fn rebuilt_face_keeps_a_users_circle_on_its_projected_centre() { + let mut sk = Sketch::new(); + let hole = |c: Vec2| LinkGeom::Circle(c, 2.1); + let face_with = |holes: &[Vec2]| square(15.0).into_iter().chain(holes.iter().map(|c| hole(*c))).collect::>(); + let link = sk.add_link(LinkKind::Project, face(), &face_with(&[Vec2::ZERO])).unwrap(); + // The projected hole's centre point. + let centre = sk + .link_curves(&link) + .into_iter() + .find_map(|i| match sk.curves[i].kind { + CurveKind::Circle { c, .. } => Some(c), + _ => None, + }) + .unwrap(); + let mine = sk.add_curve(CurveKind::Circle { c: centre, r: 5.25 }, Some("c_mine")).unwrap(); + assert!(sk.curves[mine].link.is_none()); + // The face gains two more holes: the projection's shape changes, so it is rebuilt. + assert!(sk.update_link(&link, &face_with(&[Vec2::ZERO, Vec2::new(-10.0, 0.0), Vec2::new(10.0, 0.0)])).unwrap()); + let kept = sk.curves.iter().find(|c| c.id == "c_mine").expect("the user's circle survives the rebuild"); + assert!(kept.link.is_none()); + match kept.kind { + CurveKind::Circle { c, r } => { + assert_eq!(r, 5.25); + let p = &sk.points[c]; + assert!(p.link.is_none(), "it sits on a point of its own, not a projected one"); + assert!(p.pos.len() < 1e-9, "still at the hole's centre: {:?}", p.pos); + } + ref k => panic!("not a circle: {k:?}"), + } + assert_eq!(sk.link_curves(&link).len(), 4 + 3, "the projection is the new outline and holes"); +}