fix(sketch): merged adjacent profiles have whole edges where shared ones ended (#509)
When adjacent profiles are merged for one extrude, collinear lines on either side of where a shared boundary ended are joined back into one edge, so the body has no split faces or extra vertices for later booleans to trip on. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2f7046efac
commit
bb431f7ac2
3 files changed
+82
-1
No files matched your search
@@ -363,6 +363,51 @@ fn revolve_a_spherical_shell_off_the_origin() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extrude_of_adjacent_profiles_has_whole_edges() {
|
||||
// A holder from two regions of one sketch (a disc and the ring around it, both cut off by a
|
||||
// chord): the chord used to stay split where the inner circle met it, leaving vertices right
|
||||
// on the surface of a handle of that radius, and cutting the handle out of the holder failed.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "Holder"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 40}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 35}));
|
||||
let x = (40.0f64 * 40.0 - 33.0 * 33.0).sqrt();
|
||||
run(&mut s, "sketch.line", json!({"points": [[-x, -33], [x, -33]]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let si = run(&mut s, "sketch.inspect", json!({"sketch": "Holder"}));
|
||||
let below: Vec<Value> = si["profiles"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|p| (500.0..4900.0).contains(&p["area_mm2"].as_f64().unwrap()))
|
||||
.map(|p| p["index"].clone())
|
||||
.collect();
|
||||
assert_eq!(below.len(), 2, "{si}");
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Holder", "profiles": below, "distance": 23, "body_name": "Holder"}));
|
||||
let topo = run(&mut s, "model.faces", json!({"body": "Holder"}));
|
||||
assert_eq!(topo["faces"].as_array().unwrap().len(), 5, "a cut-off cylinder: {topo}");
|
||||
let lo = run(&mut s, "inspect.measure", json!({"bodies": ["Holder"]}))["bodies"][0]["bbox"]["min"][1].as_f64().unwrap();
|
||||
run(&mut s, "solid.move", json!({"bodies": ["Holder"], "translate": [0, -5.0 - lo, 0]}));
|
||||
// The handle: r35 on the Y axis with a bevelled end.
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Handle"}));
|
||||
for pts in [
|
||||
[[0.0, 0.0], [0.0, 230.0]],
|
||||
[[0.0, 0.0], [25.0, 0.0]],
|
||||
[[25.0, 0.0], [35.0, 11.5]],
|
||||
[[35.0, 11.5], [35.0, 230.0]],
|
||||
[[35.0, 230.0], [0.0, 230.0]],
|
||||
] {
|
||||
run(&mut s, "sketch.line", json!({ "points": pts }));
|
||||
}
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.revolve", json!({"sketch": "Handle", "profiles": [0], "axis": "Y", "body_name": "Handle"}));
|
||||
let before = run(&mut s, "inspect.measure", json!({"bodies": ["Holder"]}))["total"]["volume_mm3"].as_f64().unwrap();
|
||||
run(&mut s, "solid.combine", json!({"target": "Holder", "tools": ["Handle"], "operation": "cut", "keep_tools": true}));
|
||||
let after = run(&mut s, "inspect.measure", json!({"bodies": ["Holder"]}))["total"]["volume_mm3"].as_f64().unwrap();
|
||||
assert!(after > 1000.0 && after < before * 0.5, "{before} -> {after}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_tools_and_constraints() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -985,6 +985,28 @@ fn same_seg(a: &Seg2, b: &Seg2, tol: f64) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// A closed loop's segments with each run that `joins` says continue one another made one line.
|
||||
fn rejoined(segs: Vec<Seg2>, joins: &impl Fn(&Seg2, &Seg2) -> bool) -> Vec<Seg2> {
|
||||
let n = segs.len();
|
||||
let join = |i: usize| segs.get((i + n - 1) % n).zip(segs.get(i)).is_some_and(|(s, t)| joins(s, t));
|
||||
// Start where a run starts; a loop that is all one run (it can't close) stays as it is.
|
||||
let Some(k) = (0..n).find(|&i| !join(i)) else { return segs };
|
||||
let mut out: Vec<Seg2> = Vec::with_capacity(n);
|
||||
for j in 0..n {
|
||||
let i = (k + j) % n;
|
||||
let Some(s) = segs.get(i).copied() else { continue };
|
||||
if j > 0
|
||||
&& join(i)
|
||||
&& let Some(Seg2::Line { b, .. }) = out.last_mut()
|
||||
{
|
||||
*b = s.end();
|
||||
continue;
|
||||
}
|
||||
out.push(s);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Union of regions that touch along shared boundary segments (one extrude of several adjacent
|
||||
/// profiles makes one body). Disjoint regions stay separate.
|
||||
pub fn merge_regions(regions: &[Region2]) -> Vec<Region2> {
|
||||
@@ -1022,6 +1044,17 @@ pub fn merge_regions(regions: &[Region2]) -> Vec<Region2> {
|
||||
if !shared {
|
||||
return regions.to_vec();
|
||||
}
|
||||
// Where the shared boundaries ended, the outline is left split in two: the lines on either
|
||||
// side are one edge again (else they make split faces and vertices that booleans trip on).
|
||||
let ends: Vec<Vec2> = segs.iter().zip(&alive).filter(|(_, a)| !**a).flat_map(|(s, _)| [s.start(), s.end()]).collect();
|
||||
let rejoin = |s: &Seg2, t: &Seg2| match (*s, *t) {
|
||||
(Seg2::Line { a, b }, Seg2::Line { a: b2, b: c }) => {
|
||||
b.dist(b2) <= tol
|
||||
&& ends.iter().any(|e| e.dist(b) <= tol)
|
||||
&& (b - a).normalized().zip((c - b).normalized()).is_some_and(|(u, v)| u.dot(v) > 1.0 - 1e-12)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let rest: Vec<Seg2> = segs.iter().zip(&alive).filter(|(_, a)| **a).map(|(s, _)| *s).collect();
|
||||
// Chain into loops, taking the sharpest left turn where several continue.
|
||||
let mut used = vec![false; rest.len()];
|
||||
@@ -1058,7 +1091,7 @@ pub fn merge_regions(regions: &[Region2]) -> Vec<Region2> {
|
||||
}
|
||||
}
|
||||
if !lp.is_empty() && lp.first().zip(lp.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= tol) {
|
||||
loops.push(Loop2 { segs: lp });
|
||||
loops.push(Loop2 { segs: rejoined(lp, &rejoin) });
|
||||
}
|
||||
}
|
||||
let outers: Vec<&Loop2> = loops.iter().filter(|l| l.signed_area() > 0.0).collect();
|
||||
|
||||
@@ -334,6 +334,9 @@ fn merging_adjacent_regions() {
|
||||
let mut areas: Vec<f64> = m.iter().map(|r| r.area()).collect();
|
||||
areas.sort_by(f64::total_cmp);
|
||||
assert!((areas[0] - 50.0).abs() < 1e-9 && (areas[1] - 200.0).abs() < 1e-9, "{areas:?}");
|
||||
// The two squares make one rectangle: the outline isn't left split where the shared edge was.
|
||||
let sides = m.iter().map(|r| r.outer.segs.len()).collect::<Vec<_>>();
|
||||
assert!(sides.contains(&4) && sides.contains(&3), "{sides:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user