feat: a slanted or curved edge across a sheet metal bend folds to a smooth curve of few pieces (#1799)
Thanks @NOWN-DEV.
This commit is contained in:
1 parent
bb13799246
commit
c7bb64dd74
4 files changed
+251
-88
No files matched your search
@@ -1995,7 +1995,7 @@ fn sew_connected_sections(
|
||||
let [p0, p1, p2, p3] = cubic_of(sg)?;
|
||||
let c = same_chain([welded(p0, z), at(p1, z), at(p2, z), welded(p3, z)]);
|
||||
let mid = (c[0] + (c[1] + c[2]) * 3.0 + c[3]) * 0.125;
|
||||
same_edge(EdgeSpec::Chain { ctrl: c.to_vec().into() }, c[0], c[3], mid)
|
||||
same_edge(EdgeSpec::Chain { degree: 3, ctrl: c.to_vec().into() }, c[0], c[3], mid)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
+81
-27
@@ -1,5 +1,5 @@
|
||||
//! Sewing a solid from face descriptions: each face is its boundary loops (lines, circular
|
||||
//! arcs and cubic Bézier chains, counter-clockwise about the outward normal; holes clockwise)
|
||||
//! arcs and Bézier chains, counter-clockwise about the outward normal; holes clockwise)
|
||||
//! and its surface (planar, revolved from a line, extruded from an arc, or ruled between two
|
||||
//! Bézier chains). Vertices and edges at the same places are shared, so the faces close into
|
||||
//! one shell without booleans — for solids whose faces are known exactly, such as folded
|
||||
@@ -17,6 +17,8 @@ use crate::{KernelError, Result, guard};
|
||||
|
||||
/// Most control points of one Bézier chain edge.
|
||||
pub const MAX_CHAIN_POINTS: usize = 3 * 1024 + 1;
|
||||
/// Highest degree of a Bézier chain's pieces.
|
||||
pub const MAX_CHAIN_DEGREE: usize = 9;
|
||||
|
||||
/// A boundary edge.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -31,10 +33,12 @@ pub enum EdgeSpec {
|
||||
b: Vec3,
|
||||
mid: Vec3,
|
||||
},
|
||||
/// A chain of cubic Bézier pieces: 3n + 1 control points (n ≥ 1); the curve passes
|
||||
/// through `ctrl[0]`, `ctrl[3]`, … `ctrl[3n]`. For curves with no exact line, arc or conic
|
||||
/// form, such as a cut-out edge traced across a sheet metal bend.
|
||||
/// A chain of Bézier pieces of degree `degree` (1…[`MAX_CHAIN_DEGREE`]): `degree`·n + 1
|
||||
/// control points (n ≥ 1); the curve passes through `ctrl[0]`, `ctrl[degree]`, …
|
||||
/// `ctrl[degree·n]`. For curves with no exact line, arc or conic form, such as a font's
|
||||
/// outline (cubic) or a cut-out edge traced across a sheet metal bend.
|
||||
Chain {
|
||||
degree: usize,
|
||||
ctrl: Arc<[Vec3]>,
|
||||
},
|
||||
}
|
||||
@@ -43,7 +47,7 @@ impl EdgeSpec {
|
||||
fn ends(&self) -> (Vec3, Vec3) {
|
||||
match self {
|
||||
EdgeSpec::Line { a, b } | EdgeSpec::Arc { a, b, .. } => (*a, *b),
|
||||
EdgeSpec::Chain { ctrl } => (ctrl.first().copied().unwrap_or(Vec3::ZERO), ctrl.last().copied().unwrap_or(Vec3::ZERO)),
|
||||
EdgeSpec::Chain { ctrl, .. } => (ctrl.first().copied().unwrap_or(Vec3::ZERO), ctrl.last().copied().unwrap_or(Vec3::ZERO)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +61,7 @@ impl EdgeSpec {
|
||||
let (m, e) = (*mid - *a, *b - *a);
|
||||
m * (1.0 / m.len2()) - e * (1.0 / e.len2())
|
||||
}
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
EdgeSpec::Chain { ctrl, .. } => {
|
||||
let first = *ctrl.first()?;
|
||||
ctrl.iter().skip(1).map(|p| *p - first).find(|d| d.len() > 1e-12)?
|
||||
}
|
||||
@@ -70,7 +74,7 @@ impl EdgeSpec {
|
||||
match self {
|
||||
EdgeSpec::Line { a, b } => EdgeSpec::Line { a: *b, b: *a },
|
||||
EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: *b, b: *a, mid: *mid },
|
||||
EdgeSpec::Chain { ctrl } => EdgeSpec::Chain { ctrl: ctrl.iter().rev().copied().collect() },
|
||||
EdgeSpec::Chain { degree, ctrl } => EdgeSpec::Chain { degree: *degree, ctrl: ctrl.iter().rev().copied().collect() },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +83,7 @@ impl EdgeSpec {
|
||||
match self {
|
||||
EdgeSpec::Line { .. } => None,
|
||||
EdgeSpec::Arc { mid, .. } => Some(key(*mid)),
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
EdgeSpec::Chain { ctrl, .. } => {
|
||||
// Summed in pairs from both ends, so that the chain run either way gives the very
|
||||
// same point: a font's control points are simple fractions, whose mean can fall
|
||||
// exactly halfway between two steps of the key, where summing in another order
|
||||
@@ -99,8 +103,12 @@ impl EdgeSpec {
|
||||
match self {
|
||||
EdgeSpec::Line { a, b } => a.is_finite() && b.is_finite(),
|
||||
EdgeSpec::Arc { a, b, mid } => a.is_finite() && b.is_finite() && mid.is_finite(),
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
ctrl.len() >= 4 && ctrl.len() <= MAX_CHAIN_POINTS && (ctrl.len() - 1) % 3 == 0 && ctrl.iter().all(|p| p.is_finite())
|
||||
EdgeSpec::Chain { degree, ctrl } => {
|
||||
(1..=MAX_CHAIN_DEGREE).contains(degree)
|
||||
&& ctrl.len() > *degree
|
||||
&& ctrl.len() <= MAX_CHAIN_POINTS
|
||||
&& (ctrl.len() - 1) % degree == 0
|
||||
&& ctrl.iter().all(|p| p.is_finite())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,8 +123,9 @@ pub enum SurfSpec {
|
||||
Revolved { a: Vec3, b: Vec3, axis_origin: Vec3, axis: Vec3, angle: f64 },
|
||||
/// The surface an arc sweeps moving along `dir`.
|
||||
Extruded { a: Vec3, b: Vec3, mid: Vec3, dir: Vec3 },
|
||||
/// The straight lines joining equal parameters of two Bézier chains with the same number
|
||||
/// of pieces, `a` and `b` running the same way; both chains lie on it as boundary edges.
|
||||
/// The straight lines joining equal parameters of two Bézier chains of one degree with the
|
||||
/// same number of pieces, `a` and `b` running the same way; both chains lie on it as
|
||||
/// boundary edges.
|
||||
Ruled { a: EdgeSpec, b: EdgeSpec },
|
||||
}
|
||||
|
||||
@@ -184,7 +193,7 @@ fn welded(faces: &[FaceSpec]) -> Option<Vec<FaceSpec>> {
|
||||
let edge = |e: &EdgeSpec| match e {
|
||||
EdgeSpec::Line { a, b } => EdgeSpec::Line { a: weld(*a), b: weld(*b) },
|
||||
EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: weld(*a), b: weld(*b), mid: *mid },
|
||||
EdgeSpec::Chain { ctrl } => {
|
||||
EdgeSpec::Chain { degree, ctrl } => {
|
||||
let mut ctrl = ctrl.to_vec();
|
||||
let n = ctrl.len();
|
||||
for i in [0, n.saturating_sub(1)] {
|
||||
@@ -192,7 +201,7 @@ fn welded(faces: &[FaceSpec]) -> Option<Vec<FaceSpec>> {
|
||||
*p = weld(*p);
|
||||
}
|
||||
}
|
||||
EdgeSpec::Chain { ctrl: ctrl.into() }
|
||||
EdgeSpec::Chain { degree: *degree, ctrl: ctrl.into() }
|
||||
}
|
||||
};
|
||||
Some(
|
||||
@@ -245,18 +254,20 @@ impl Cache {
|
||||
Ok(made)
|
||||
}
|
||||
|
||||
/// A Bézier chain as a cubic B-spline (triple inner knots) ending on the shared vertices.
|
||||
/// A Bézier chain as a B-spline of its degree (inner knots as many times as the degree)
|
||||
/// ending on the shared vertices.
|
||||
fn chain(&mut self, e: &EdgeSpec) -> Result<mt::BSplineCurve<mt::Point3>> {
|
||||
let EdgeSpec::Chain { ctrl } = e else { return Err(KernelError::Invalid("sew: not a Bézier chain".into())) };
|
||||
let EdgeSpec::Chain { degree, ctrl } = e else { return Err(KernelError::Invalid("sew: not a Bézier chain".into())) };
|
||||
if !e.valid() {
|
||||
return Err(KernelError::Invalid("sew: a Bézier chain needs 3n + 1 finite control points".into()));
|
||||
return Err(KernelError::Invalid("sew: a Bézier chain needs degree·n + 1 finite control points".into()));
|
||||
}
|
||||
let n = (ctrl.len() - 1) / 3;
|
||||
let mut knots = vec![0.0; 4];
|
||||
let d = *degree;
|
||||
let n = (ctrl.len() - 1) / d;
|
||||
let mut knots = vec![0.0; d + 1];
|
||||
for i in 1..n {
|
||||
knots.extend([i as f64; 3]);
|
||||
knots.extend(std::iter::repeat_n(i as f64, d));
|
||||
}
|
||||
knots.extend([n as f64; 4]);
|
||||
knots.extend(std::iter::repeat_n(n as f64, d + 1));
|
||||
let mut pts: Vec<mt::Point3> = ctrl.iter().map(|p| p3(*p)).collect();
|
||||
let (a, b) = e.ends();
|
||||
if let Some(first) = pts.first_mut() {
|
||||
@@ -491,7 +502,7 @@ fn plane_normal(face: &FaceSpec) -> Option<Vec3> {
|
||||
let n = face.loops.first()?.iter().fold(Vec3::ZERO, |n, e| match e {
|
||||
EdgeSpec::Line { a, b } => n + a.cross(*b),
|
||||
EdgeSpec::Arc { a, b, mid } => n + a.cross(*mid) + mid.cross(*b),
|
||||
EdgeSpec::Chain { ctrl } => ctrl.iter().zip(ctrl.iter().skip(1)).fold(n, |n, (p, q)| n + p.cross(*q)),
|
||||
EdgeSpec::Chain { ctrl, .. } => ctrl.iter().zip(ctrl.iter().skip(1)).fold(n, |n, (p, q)| n + p.cross(*q)),
|
||||
});
|
||||
n.normalized()
|
||||
}
|
||||
@@ -830,7 +841,7 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
}
|
||||
SurfSpec::Ruled { a, b } => {
|
||||
let (ca, cb) = (cache.chain(a)?, cache.chain(b)?);
|
||||
if ca.control_points().len() != cb.control_points().len() {
|
||||
if ca.degree() != cb.degree() || ca.control_points().len() != cb.control_points().len() {
|
||||
return Err(KernelError::Invalid("sew: ruled chains differ in pieces".into()));
|
||||
}
|
||||
let surface = mt::Surface::BSplineSurface(mt::BSplineSurface::homotopy(ca, cb));
|
||||
@@ -974,7 +985,7 @@ mod tests {
|
||||
p(35.32833796296296, 11.885361111111111),
|
||||
p(35.53081944444444, 11.885361111111111),
|
||||
];
|
||||
let forward = EdgeSpec::Chain { ctrl: ctrl.to_vec().into() };
|
||||
let forward = EdgeSpec::Chain { degree: 3, ctrl: ctrl.to_vec().into() };
|
||||
assert_eq!(forward.mid_key(), forward.reversed().mid_key());
|
||||
}
|
||||
|
||||
@@ -1130,7 +1141,22 @@ mod tests {
|
||||
/// The bulge's cubic split at its middle (de Casteljau), at height `z`.
|
||||
fn bulge(z: f64) -> EdgeSpec {
|
||||
let ctrl = [(4.0, 0.0), (4.5, 0.0), (4.75, 0.75), (4.75, 1.5), (4.75, 2.25), (4.5, 3.0), (4.0, 3.0)];
|
||||
EdgeSpec::Chain { ctrl: ctrl.iter().map(|(x, y)| Vec3::new(*x, *y, z)).collect() }
|
||||
EdgeSpec::Chain { degree: 3, ctrl: ctrl.iter().map(|(x, y)| Vec3::new(*x, *y, z)).collect() }
|
||||
}
|
||||
|
||||
/// The bulge's cubic raised to degree 5, (4,0) (4.6,0) (4.9,0.9) (4.9,2.1) (4.6,3) (4,3), and
|
||||
/// split at its middle (de Casteljau), at height `z`: the same curve as a quintic chain.
|
||||
fn quintic_bulge(z: f64) -> EdgeSpec {
|
||||
let raised = [(4.0, 0.0), (4.6, 0.0), (4.9, 0.9), (4.9, 2.1), (4.6, 3.0), (4.0, 3.0)].map(|(x, y)| Vec3::new(x, y, z));
|
||||
let (mut left, mut right, mut w) = (Vec::new(), Vec::new(), raised.to_vec());
|
||||
while let (Some(first), Some(last)) = (w.first().copied(), w.last().copied()) {
|
||||
left.push(first);
|
||||
right.push(last);
|
||||
w = w.windows(2).map(|p| (p[0] + p[1]) * 0.5).collect();
|
||||
}
|
||||
right.reverse();
|
||||
left.extend(right.into_iter().skip(1));
|
||||
EdgeSpec::Chain { degree: 5, ctrl: left.into() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1145,10 +1171,22 @@ mod tests {
|
||||
assert!((m.bbox.max.x - 4.75).abs() < 1e-3, "{:?}", m.bbox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ruled_wall_between_quintic_chains_closes_the_same_solid() {
|
||||
let EdgeSpec::Chain { ctrl, .. } = quintic_bulge(0.0) else { panic!() };
|
||||
assert_eq!(ctrl.len(), 11);
|
||||
let body = sew(&bulged_box(quintic_bulge(0.0), quintic_bulge(2.0))).unwrap();
|
||||
assert_eq!(body.face_count(), 6);
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let m = crate::measure(&body).unwrap();
|
||||
assert!((m.volume - 27.6).abs() < 2e-3, "{}", m.volume);
|
||||
assert!((m.bbox.max.x - 4.75).abs() < 1e-3, "{:?}", m.bbox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_bezier_chains_are_refused() {
|
||||
let chain = |ctrl: Vec<Vec3>| EdgeSpec::Chain { ctrl: ctrl.into() };
|
||||
let EdgeSpec::Chain { ctrl } = bulge(2.0) else { panic!() };
|
||||
let chain = |ctrl: Vec<Vec3>| EdgeSpec::Chain { degree: 3, ctrl: ctrl.into() };
|
||||
let EdgeSpec::Chain { ctrl, .. } = bulge(2.0) else { panic!() };
|
||||
// Not 3n + 1 control points; a control point that is not a number; too many pieces.
|
||||
let mut nan = ctrl.to_vec();
|
||||
nan[2].x = f64::NAN;
|
||||
@@ -1160,6 +1198,22 @@ mod tests {
|
||||
let whole = chain(vec![Vec3::new(4.0, 0.0, 2.0), Vec3::new(5.0, 0.0, 2.0), Vec3::new(5.0, 3.0, 2.0), Vec3::new(4.0, 3.0, 2.0)]);
|
||||
let e = sew(&bulged_box(bulge(0.0), whole)).unwrap_err().to_string();
|
||||
assert!(e.contains("ruled chains differ"), "{e}");
|
||||
// Degree 0, or above the highest, or a count of control points that doesn't fit the degree
|
||||
// (11 control points are no chain of quartics).
|
||||
let EdgeSpec::Chain { ctrl: fifth, .. } = quintic_bulge(2.0) else { panic!() };
|
||||
let degree = |degree: usize, ctrl: &[Vec3]| EdgeSpec::Chain { degree, ctrl: ctrl.to_vec().into() };
|
||||
// `n` control points bulging out from (4, 0) to (4, 3) at height `z`.
|
||||
let arch = |n: usize, z: f64| -> Vec<Vec3> {
|
||||
let at = |i: usize| i as f64 / (n - 1) as f64;
|
||||
(0..n).map(|i| Vec3::new(4.0 + (std::f64::consts::PI * at(i)).sin(), 3.0 * at(i), z)).collect()
|
||||
};
|
||||
for top in [degree(0, &ctrl), degree(MAX_CHAIN_DEGREE + 1, &arch(MAX_CHAIN_DEGREE + 2, 2.0)), degree(4, &fifth)] {
|
||||
assert!(sew(&bulged_box(bulge(0.0), top)).is_err());
|
||||
}
|
||||
// A ruled wall needs chains of one degree: 16 control points are five cubic pieces or
|
||||
// three quintic ones.
|
||||
let e = sew(&bulged_box(degree(3, &arch(16, 0.0)), degree(5, &arch(16, 2.0)))).unwrap_err().to_string();
|
||||
assert!(e.contains("ruled chains differ"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,12 +22,12 @@ fn helix(r: f64, z0: f64, pitch: f64, a0: f64, a1: f64) -> EdgeSpec {
|
||||
let (s, e) = (a0 + h * i as f64, a0 + h * (i + 1) as f64);
|
||||
ctrl.extend([at(s) + turn(s) + rise, at(e) - turn(e) - rise, at(e)]);
|
||||
}
|
||||
EdgeSpec::Chain { ctrl: ctrl.into() }
|
||||
EdgeSpec::Chain { degree: 3, ctrl: ctrl.into() }
|
||||
}
|
||||
|
||||
fn rev(e: &EdgeSpec) -> EdgeSpec {
|
||||
match e {
|
||||
EdgeSpec::Chain { ctrl } => EdgeSpec::Chain { ctrl: ctrl.iter().rev().copied().collect::<Arc<[Vec3]>>() },
|
||||
EdgeSpec::Chain { degree, ctrl } => EdgeSpec::Chain { degree: *degree, ctrl: ctrl.iter().rev().copied().collect::<Arc<[Vec3]>>() },
|
||||
EdgeSpec::Line { a, b } => EdgeSpec::Line { a: *b, b: *a },
|
||||
EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: *b, b: *a, mid: *mid },
|
||||
}
|
||||
@@ -39,7 +39,7 @@ fn line(a: Vec3, b: Vec3) -> EdgeSpec {
|
||||
|
||||
fn ends(e: &EdgeSpec) -> (Vec3, Vec3) {
|
||||
match e {
|
||||
EdgeSpec::Chain { ctrl } => (ctrl[0], ctrl[ctrl.len() - 1]),
|
||||
EdgeSpec::Chain { ctrl, .. } => (ctrl[0], ctrl[ctrl.len() - 1]),
|
||||
EdgeSpec::Line { a, b } | EdgeSpec::Arc { a, b, .. } => (*a, *b),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user