mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
* claim: #9001 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sketch: a projected face follows a resize that moves its corners far (#9001) A sketch on the face of an angle kept its projected face boundary (and reported "projected face boundary identity is ambiguous") when the wall thickness changed: the inner corner moved by more than a tenth of the face, past the nearest-point match, and the same-order fallback refused faces. Accept the same-order layout when, for every point, the point read in that order is also clearly the nearest one on the same loop with lines the same way. Rims and holes, and rotated readings of a rectangle, stay refused. 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:
@@ -1098,3 +1098,56 @@ fn a_rebuilt_projection_keeps_user_constraints_without_a_warning() {
|
||||
assert!(warning.is_none(), "{warning:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #9001: an angle (an L profile, wall thickness `d1`, legs `d2`) with a sketch on its face and
|
||||
/// a hole dimensioned to the inner wall's projected edge. Making the wall thicker moves the
|
||||
/// inner corner by more than a tenth of the face; the projection follows it (it was kept as it
|
||||
/// was, "boundary identity is ambiguous"), and the hole with it, as it already did for `d2`.
|
||||
#[test]
|
||||
fn a_thicker_angle_moves_the_projected_face_of_the_sketch_on_it() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.change", json!({"name": "d1", "expression": "20 mm"}));
|
||||
run(&mut s, "parameters.change", json!({"name": "d2", "expression": "80 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [80, 0], [80, 20], [20, 20], [20, 80], [0, 80]], "closed": true}))["curves"]);
|
||||
for (i, line) in l.iter().enumerate() {
|
||||
let mode = if i % 2 == 0 { "horizontal" } else { "vertical" };
|
||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": line, "mode": mode}));
|
||||
}
|
||||
run(&mut s, "sketch.constraint.coincident", json!({"a": format!("{}.start", l[0]), "b": "origin"}));
|
||||
for (k, value) in [(0, "d2"), (1, "d1"), (4, "d1"), (5, "d2")] {
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [l[k]], "value": value}));
|
||||
}
|
||||
assert_eq!(inspect(&mut s)["dof"], 0);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 30}));
|
||||
let sketch = run(&mut s, "sketch.create", json!({"plane": {"face": [10, 10, 30]}}))["sketch"].as_u64().unwrap();
|
||||
let si = inspect(&mut s);
|
||||
// The inner wall's edge, from (20, 20) to (20, 80) on the face.
|
||||
let wall = linked_curves(&si)
|
||||
.into_iter()
|
||||
.find(|c| {
|
||||
["start_at", "end_at"].iter().all(|e| (c[e][0].as_f64().unwrap().abs() - 20.).abs() < 1e-6 && c[e][1].as_f64().unwrap().abs() > 19.)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("{si}"));
|
||||
let hole = ids(&run(&mut s, "sketch.circle.center", json!({"center": [10, 50], "radius": 3}))["curves"])[0].clone();
|
||||
run(&mut s, "sketch.dimension", json!({"entities": [format!("{hole}.center"), wall["id"]], "value": 10}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "parameters.change", json!({"name": "d1", "expression": "30 mm"}));
|
||||
let check = |s: &Session| {
|
||||
let warning = s.model.result(sketch).and_then(|r| r.warning.clone());
|
||||
assert!(warning.is_none(), "{warning:?}");
|
||||
let st = s.model.state();
|
||||
let ss = st.sketch(sketch).unwrap();
|
||||
assert!(ss.sketch.links.iter().all(|l| !l.lost));
|
||||
let world = |p: solidcraft_geom::Vec2| ss.plane.to_world(p);
|
||||
let at = |x: f64, y: f64| ss.sketch.points.iter().any(|p| world(p.pos).dist(solidcraft_geom::Vec3::new(x, y, 30.)) < 1e-6);
|
||||
// The inner corner is at (30, 30) now, and the hole 10 from the wall that moved.
|
||||
assert!(at(30., 30.) && !at(20., 20.));
|
||||
let center = ss.sketch.points.iter().find(|p| p.id == format!("{hole}.center")).unwrap();
|
||||
assert!((world(center.pos).x - 20.).abs() < 1e-6, "{:?}", world(center.pos));
|
||||
assert_eq!(ss.sketch.constraints.len(), 1);
|
||||
};
|
||||
check(&s);
|
||||
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
|
||||
}
|
||||
|
||||
@@ -895,6 +895,7 @@ impl Sketch {
|
||||
}
|
||||
// A resized face can enumerate the same boundary in another order. Match the
|
||||
// shared-point graph in normalized sketch space before discarding its identity.
|
||||
let mut same_order_agrees = false;
|
||||
if let Some(cur) = &cur
|
||||
&& points.len() == lay.pts.len()
|
||||
&& cur.len() == lay.curves.len()
|
||||
@@ -1035,11 +1036,34 @@ impl Sketch {
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
// A face resized by more than the match above allows (#9001: an angle's wall made
|
||||
// thicker moves the inner corner by more than a tenth of the face) is still the same
|
||||
// boundary when two witnesses agree: the new boundary was read in the same order
|
||||
// with the same curves, and each point read in that order is, among the points on
|
||||
// the same loop with lines the same way, clearly the nearest to where it was.
|
||||
same_order_agrees = same
|
||||
&& loop_roles.as_ref().is_some_and(|roles| {
|
||||
(0..points.len()).all(|k| {
|
||||
let (Some(&fresh_i), Some(p)) = (order.get(k), a.get(k)) else { return false };
|
||||
let mut candidates: Vec<(usize, f64)> = b
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| {
|
||||
old_roles.get(k) == fresh_roles.get(*i)
|
||||
&& roles.old.get(k) == roles.fresh.get(*i)
|
||||
&& roles.old_lines.get(k).zip(roles.fresh_lines.get(*i)).is_some_and(|(x, y)| loops::same_lines(x, y))
|
||||
})
|
||||
.map(|(i, q)| (i, p.dist(*q)))
|
||||
.collect();
|
||||
candidates.sort_by(|x, y| x.1.total_cmp(&y.1));
|
||||
matches!(candidates.first(), Some((i, d)) if *i == fresh_i && candidates.get(1).is_none_or(|(_, second)| *d < *second * 0.75))
|
||||
})
|
||||
});
|
||||
}
|
||||
// Equal topology alone does not identify a face boundary: an outer rim and a
|
||||
// hole can have identical point graphs. Never move saved curve ids between them
|
||||
// when the geometric match is ambiguous; the document reports a lost link.
|
||||
if same && self.link(id).is_some_and(|l| matches!(l.source, LinkSource::Face { .. })) {
|
||||
if same && !same_order_agrees && self.link(id).is_some_and(|l| matches!(l.source, LinkSource::Face { .. })) {
|
||||
self.set_link_lost(id, true);
|
||||
return Err(SketchError::Invalid("projected face boundary identity is ambiguous; last geometry retained".into()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user