Rolling-ball blends: a strip ends on its joint where its neighbour is too short to lend sections (#1667) (#1702)

Thanks @erkexzcx.
This commit is contained in:
Erikas authored and GitHub committed 2026-10-16 09:26:23 -04:00
1 parent 37ebe24839
commit 77b33efd8f
2 files changed
+53 -14

No files matched your search

+17 -14
View File
@@ -1723,10 +1723,14 @@ fn curve_blend_sized(solid: &Solid, ids: &[mt::EdgeID], size: f64, size_law: Siz
continue;
}
let h = length(k) / (samples - 1) as f64;
let spaced = |nb: usize, from_end: bool| -> Result<Option<Vec<Section>>> {
// A neighbour shorter than two of the strip's steps lends none: its own sections are
// spaced far closer (a small round beside a long line), and borrowing them made the
// strip overshoot and fold back on itself past the joint, which its faces' meshes
// then folded with (#1667). The strip ends on the joint's section on its own.
let spaced = |nb: usize, from_end: bool| -> Result<Vec<Section>> {
let l = length(nb);
if !(l > 2.0 * h && h > 0.0) {
return Ok(None);
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(2);
for j in [1.0, 2.0] {
@@ -1735,7 +1739,7 @@ fn curve_blend_sized(solid: &Solid, ids: &[mt::EdgeID], size: f64, size_law: Siz
if from_end {
out.reverse();
}
Ok(Some(out))
Ok(out)
};
let (prev, next) = ((k + n - 1) % n, (k + 1) % n);
let list = esec.get(k).ok_or_else(|| KernelError::Failed("section".into()))?;
@@ -1748,23 +1752,22 @@ fn curve_blend_sized(solid: &Solid, ids: &[mt::EdgeID], size: f64, size_law: Siz
_ => false,
};
let before = match (closed || k > 0) && rolls(prev) {
true => spaced(prev, true)?
.map(|b| if smooth([list.first(), list.get(1), list.get(2)], [list.first(), b.get(1), b.first()]) { b } else { Vec::new() }),
false => Some(Vec::new()),
true => {
let b = spaced(prev, true)?;
if smooth([list.first(), list.get(1), list.get(2)], [list.first(), b.get(1), b.first()]) { b } else { Vec::new() }
}
false => Vec::new(),
};
let after = match (closed || k + 1 < n) && rolls(next) {
true => spaced(next, false)?.map(|a| {
true => {
let a = spaced(next, false)?;
let m = list.len();
let tail = [list.last(), m.checked_sub(2).and_then(|i| list.get(i)), m.checked_sub(3).and_then(|i| list.get(i))];
if smooth(tail, [list.last(), a.first(), a.get(1)]) { a } else { Vec::new() }
}),
false => Some(Vec::new()),
}
false => Vec::new(),
};
let fit = match (before, after) {
(Some(b), Some(a)) => crate::guard("fit curve blend", || fit_strip(list, &b, &a, round))?,
_ => crate::guard("fit curve blend", || fit_sections(&esec, k, closed, round))?,
};
let mut fit = fit;
let mut fit = crate::guard("fit curve blend", || fit_strip(list, &before, &after, round))?;
fit.span = guides.get(k).and_then(Option::as_ref).map(|g| g.span).unwrap_or((0.0, 1.0));
fits.push(Some(fit));
}
+36
View File
@@ -693,3 +693,39 @@ fn chamfer_a_chain_of_a_line_and_an_arc_into_a_slanted_end() {
let got = volume(&body) - volume(&made);
assert!((got - want).abs() < want * 1e-3, "removed {got} vs {want}");
}
/// #1667: an R1 round around the end of a long bar whose four long edges are R5. The chain is
/// 290 mm lines and quarter turns of 7.85 mm, so the lines' sections are spaced far wider than
/// the turns'; borrowing the turns' sections for the lines' ends made their strips overshoot and
/// fold back past the joints, the strips' meshes folded with them, and the measured volume came
/// out thousands of mm³ off. The R1 pipe is exact: the corner's section, `1 - π/4`, along the
/// lines, and round each turn's axis at the section's centroid (Pappus).
#[test]
fn round_along_long_lines_and_small_turns_measures_its_volume() {
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4};
let (w, d, h) = (300.0, 60.0, 40.0);
let bar = crate::build::box_solid(Vec3::new(-w / 2.0, 0.0, 0.0), Vec3::new(w / 2.0, d, h)).unwrap();
let long = [Vec3::new(-w / 2.0, d / 2.0, 0.0), Vec3::new(w / 2.0, d / 2.0, 0.0), Vec3::new(-w / 2.0, d / 2.0, h), Vec3::new(w / 2.0, d / 2.0, h)];
let rounded = fillet(&bar, &long, 5.0).unwrap();
// The rolling ball along the end face's loop (the exact blends would take this loop; a
// loop next to free-form faces, as in the issue, comes here).
let ids: Vec<mt::EdgeID> = crate::body::Body::unique_edges(&rounded.solid)
.into_iter()
.filter(|e| [e.front(), e.back()].iter().all(|v| crate::blend::vtx(v).y.abs() < 1e-9))
.map(|e| e.id())
.collect();
assert_eq!(ids.len(), 8);
let (solid, _) = crate::guard("test", || curve_blend(&rounded.solid, &ids, rounded.size(), 1.0, true)).unwrap();
let body = Body::new(solid).unwrap();
assert!(body.validity().is_empty(), "{:?}", body.validity());
let section = 1.0 - FRAC_PI_4;
// The section's centroid lies this far from its corner along each side.
let inset = (5.0 / 6.0 - FRAC_PI_4) / section;
let lines = 2.0 * (w - 10.0) + 2.0 * (h - 10.0);
let turns = 4.0 * FRAC_PI_2 * (5.0 - inset);
let want = w * d * h - 4.0 * 25.0 * section * d - section * (lines + turns);
let m = crate::measure(&body).unwrap();
assert!((m.volume - want).abs() < 1e-5 * want, "{} vs {want}", m.volume);
// Centred across the bar, as the part is.
assert!(m.centroid.x.abs() < 1e-3, "centre of mass x {}", m.centroid.x);
}