Sketch: rebuilding a projection keeps the sketch's own curves on its points (#5388) (#5537)

* claim: #5388

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dudedagger42
2026-11-02 02:09:59 +00:00
committed by GitHub
parent 6e98bd474e
commit b470d46ffc
2 changed files with 55 additions and 0 deletions
+20
View File
@@ -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<usize> = 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) {
+35
View File
@@ -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::<Vec<_>>();
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");
}