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
+166
-57
@@ -578,14 +578,22 @@ fn rev_edge(e: EdgeSpec) -> EdgeSpec {
|
||||
e.reversed()
|
||||
}
|
||||
|
||||
/// Largest distance between a folded bend edge and the cubic chain standing in for it.
|
||||
/// Largest distance between a folded bend edge and the chain standing in for it.
|
||||
const BEND_CHAIN_TOL: f64 = 1e-7;
|
||||
/// Most halvings of a bend edge's parameter while fitting its chain.
|
||||
const BEND_CHAIN_DEPTH: usize = 10;
|
||||
/// Degree of the pieces of a folded bend edge's chain. Hermite pieces of degree 7 match the
|
||||
/// edge's point and first three derivatives at both ends, so one or two of them follow a helix
|
||||
/// round a whole bend, where cubic pieces (point and tangent) took 32 and more.
|
||||
const BEND_CHAIN_DEGREE: usize = 7;
|
||||
/// Most pieces of a folded bend edge's chain.
|
||||
const BEND_CHAIN_PIECES: usize = 256;
|
||||
|
||||
fn cubic_at(p0: Vec3, c1: Vec3, c2: Vec3, p1: Vec3, t: f64) -> Vec3 {
|
||||
let u = 1.0 - t;
|
||||
p0 * (u * u * u) + c1 * (3.0 * u * u * t) + c2 * (3.0 * u * t * t) + p1 * (t * t * t)
|
||||
/// The point at `t` of the Bézier over `c` (de Casteljau).
|
||||
fn bezier_at(c: &[Vec3], t: f64) -> Vec3 {
|
||||
let mut w = c.to_vec();
|
||||
while w.len() > 1 {
|
||||
w = w.windows(2).map(|p| p[0].lerp(p[1], t)).collect();
|
||||
}
|
||||
w.first().copied().unwrap_or(Vec3::ZERO)
|
||||
}
|
||||
|
||||
fn dedup(p: &[Vec2]) -> Vec<Vec2> {
|
||||
@@ -973,8 +981,10 @@ impl SheetBody {
|
||||
}
|
||||
|
||||
/// A flat point of flange `f`'s bend zone folded onto the bend at height `z` (not clamped
|
||||
/// to the zone), with its rates of change along the zone's flat `n` and `d` directions.
|
||||
fn zone_point(&self, f: &SheetFlange, q: Vec2, z: f64) -> (Vec3, Vec3, Vec3) {
|
||||
/// to the zone), with its first three derivatives along the zone's flat `n` direction and
|
||||
/// its first along `d` (along `d` the bend is straight: the higher ones are zero, and so are
|
||||
/// the mixed ones).
|
||||
fn zone_point(&self, f: &SheetFlange, q: Vec2, z: f64) -> (Vec3, [Vec3; 3], Vec3) {
|
||||
let s = (q - f.p).dot(f.n);
|
||||
let u = (q - f.p).dot(f.d);
|
||||
let neutral = (f.radius + self.k * self.t).max(1e-12);
|
||||
@@ -987,58 +997,91 @@ impl SheetBody {
|
||||
let (sn, cs) = phi.sin_cos();
|
||||
let w = self.world(f.parent);
|
||||
let local = axis_o + v3(f.d) * u + ez * cs + ek * sn;
|
||||
// Each derivative along n turns the last a right angle about the axis and divides it by
|
||||
// the neutral radius.
|
||||
let ds = (ez * -sn + ek * cs) * (1.0 / neutral);
|
||||
(apply_point(&w, local), apply_vector(&w, ds), apply_vector(&w, v3(f.d)))
|
||||
let dss = (ez * cs + ek * sn) * (-1.0 / (neutral * neutral));
|
||||
let dsss = ds * (-1.0 / (neutral * neutral));
|
||||
let along_n = [ds, dss, dsss].map(|v| apply_vector(&w, v));
|
||||
(apply_point(&w, local), along_n, apply_vector(&w, v3(f.d)))
|
||||
}
|
||||
|
||||
/// A flat edge of flange `f`'s bend zone, folded at height `z`, at parameter `t`: the point
|
||||
/// and its derivative.
|
||||
fn zone_edge_at(&self, f: &SheetFlange, e: &Seg2, z: f64, t: f64) -> (Vec3, Vec3) {
|
||||
let (p, ds, du) = self.zone_point(f, e.point_at(t), z);
|
||||
let dq = e.derivative(t);
|
||||
(p, ds * dq.dot(f.n) + du * dq.dot(f.d))
|
||||
/// and its first three derivatives.
|
||||
fn zone_edge_at(&self, f: &SheetFlange, e: &Seg2, z: f64, t: f64) -> [Vec3; 4] {
|
||||
let (p, [g1, g2, g3], du) = self.zone_point(f, e.point_at(t), z);
|
||||
let [q1, q2, q3] = match e.derivatives(t, 3).as_deref() {
|
||||
Some(&[q1, q2, q3]) => [q1, q2, q3],
|
||||
_ => [e.derivative(t), Vec2::ZERO, Vec2::ZERO],
|
||||
};
|
||||
let ([s1, s2, s3], [u1, u2, u3]) = ([q1, q2, q3].map(|q| q.dot(f.n)), [q1, q2, q3].map(|q| q.dot(f.d)));
|
||||
// The chain rule (Faà di Bruno) through s(t) along n and u(t) along d.
|
||||
[p, g1 * s1 + du * u1, g2 * (s1 * s1) + g1 * s2 + du * u2, g3 * (s1 * s1 * s1) + g2 * (3.0 * s1 * s2) + g1 * s3 + du * u3]
|
||||
}
|
||||
|
||||
/// Where to break a bend zone's flat edge so cubic Hermite pieces follow the folded edge
|
||||
/// to within [`BEND_CHAIN_TOL`] on both faces. The breaks depend on the flat edge only, so
|
||||
/// the top face, bottom face and wall of a cut-out share one chain per height.
|
||||
fn zone_breaks(&self, f: &SheetFlange, e: &Seg2) -> Vec<f64> {
|
||||
let mut out = vec![0.0];
|
||||
let mut todo = vec![(0.0, 1.0, 0usize)];
|
||||
while let Some((t0, t1, depth)) = todo.pop() {
|
||||
let fits = depth >= BEND_CHAIN_DEPTH
|
||||
|| [0.0, self.t].iter().all(|&z| {
|
||||
let h = t1 - t0;
|
||||
let ((p0, d0), (p1, d1)) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1));
|
||||
let (c1, c2) = (p0 + d0 * (h / 3.0), p1 - d1 * (h / 3.0));
|
||||
[0.25, 0.5, 0.75].iter().all(|&x| cubic_at(p0, c1, c2, p1, x).dist(self.zone_edge_at(f, e, z, t0 + h * x).0) <= BEND_CHAIN_TOL)
|
||||
});
|
||||
if fits {
|
||||
out.push(t1);
|
||||
} else {
|
||||
let m = (t0 + t1) / 2.0;
|
||||
// Depth first, right half last on the stack: breaks come out in order.
|
||||
todo.push((m, t1, depth + 1));
|
||||
todo.push((t0, m, depth + 1));
|
||||
/// The control points of the Hermite piece of degree 7 of a folded bend edge from `t0` to
|
||||
/// `t1`: it has the edge's point and first three derivatives at both ends. (The k-th
|
||||
/// derivative of a Bézier of degree 7 at its start is 7!/(7 − k)! times the k-th forward
|
||||
/// difference of its first control points, and the edge's k-th derivative scales by
|
||||
/// (t1 − t0)^k to the piece's parameter; at its end the same, backwards.)
|
||||
fn zone_piece(&self, f: &SheetFlange, e: &Seg2, z: f64, t0: f64, t1: f64) -> [Vec3; BEND_CHAIN_DEGREE + 1] {
|
||||
let h = t1 - t0;
|
||||
let ([p0, a1, a2, a3], [p1, b1, b2, b3]) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1));
|
||||
let (k1, k2, k3) = (h / 7.0, h * h / 42.0, h * h * h / 210.0);
|
||||
[
|
||||
p0,
|
||||
p0 + a1 * k1,
|
||||
p0 + a1 * (2.0 * k1) + a2 * k2,
|
||||
p0 + a1 * (3.0 * k1) + a2 * (3.0 * k2) + a3 * k3,
|
||||
p1 - b1 * (3.0 * k1) + b2 * (3.0 * k2) - b3 * k3,
|
||||
p1 - b1 * (2.0 * k1) + b2 * k2,
|
||||
p1 - b1 * k1,
|
||||
p1,
|
||||
]
|
||||
}
|
||||
|
||||
/// Into how many equal pieces (of its parameter) a bend zone's flat edge is folded, so that
|
||||
/// the Hermite pieces are within [`BEND_CHAIN_TOL`] of the folded edge on both faces. The
|
||||
/// count depends on the flat edge only, so the top face, bottom face and wall of a cut-out
|
||||
/// share one chain per height; and as the pieces are equal, the chain's parameter runs evenly
|
||||
/// with the edge's, and the pieces join with equal derivatives up to the third.
|
||||
fn zone_pieces(&self, f: &SheetFlange, e: &Seg2) -> usize {
|
||||
// The largest distance between the pieces and the folded edge, at points inside each.
|
||||
let worst = |n: usize| {
|
||||
let mut worst = 0.0f64;
|
||||
for k in 0..n {
|
||||
let (t0, t1) = (k as f64 / n as f64, (k + 1) as f64 / n as f64);
|
||||
for z in [0.0, self.t] {
|
||||
let c = self.zone_piece(f, e, z, t0, t1);
|
||||
for j in 1..8 {
|
||||
let t = t0 + (t1 - t0) * j as f64 / 8.0;
|
||||
worst = worst.max(bezier_at(&c, j as f64 / 8.0).dist(self.zone_edge_at(f, e, z, t)[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
worst
|
||||
};
|
||||
let mut n = 1;
|
||||
loop {
|
||||
let off = worst(n);
|
||||
if off <= BEND_CHAIN_TOL || !off.is_finite() || n >= BEND_CHAIN_PIECES {
|
||||
return n;
|
||||
}
|
||||
// The distance falls as the eighth power of the pieces' length.
|
||||
let guess = (n as f64 * (off / BEND_CHAIN_TOL).powf(1.0 / 8.0)).ceil();
|
||||
n = (guess as usize).clamp(n + 1, BEND_CHAIN_PIECES);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A bend zone's flat edge folded at height `z`: a chain of cubic Hermite pieces.
|
||||
/// A bend zone's flat edge folded at height `z`: a chain of Hermite pieces of degree 7.
|
||||
fn zone_chain(&self, f: &SheetFlange, e: &Seg2, z: f64) -> EdgeSpec {
|
||||
let breaks = self.zone_breaks(f, e);
|
||||
let mut ctrl = Vec::with_capacity(breaks.len() * 3);
|
||||
for w in breaks.windows(2) {
|
||||
let (t0, t1) = (w[0], w[1]);
|
||||
let h = t1 - t0;
|
||||
let ((p0, d0), (p1, d1)) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1));
|
||||
if ctrl.is_empty() {
|
||||
ctrl.push(p0);
|
||||
}
|
||||
ctrl.extend([p0 + d0 * (h / 3.0), p1 - d1 * (h / 3.0), p1]);
|
||||
let n = self.zone_pieces(f, e);
|
||||
let mut ctrl = Vec::with_capacity(BEND_CHAIN_DEGREE * n + 1);
|
||||
for k in 0..n {
|
||||
let c = self.zone_piece(f, e, z, k as f64 / n as f64, (k + 1) as f64 / n as f64);
|
||||
ctrl.extend_from_slice(if k == 0 { &c } else { &c[1..] });
|
||||
}
|
||||
EdgeSpec::Chain { ctrl: ctrl.into() }
|
||||
EdgeSpec::Chain { degree: BEND_CHAIN_DEGREE, ctrl: ctrl.into() }
|
||||
}
|
||||
|
||||
/// A flat edge of a piece as a 3D edge at height `z`.
|
||||
@@ -4918,21 +4961,34 @@ mod tests {
|
||||
assert!(s.zone_point(&f, q, z).0.dist(s.map3(Piece::Zone(0), q, z)) < 1e-12);
|
||||
}
|
||||
// A flat line across the whole zone, 15 mm along the bend: the bend angle and the
|
||||
// distance along the axis both grow evenly along it.
|
||||
// distance along the axis both grow evenly along it. Two pieces of degree 7 follow it
|
||||
// (cubic ones took 32).
|
||||
let e = Seg2::Line { a: f.p + f.d * 20.0, b: f.p + f.n * s.allowance(&f) + f.d * 35.0 };
|
||||
let breaks = s.zone_breaks(&f, &e);
|
||||
let n = s.zone_pieces(&f, &e);
|
||||
assert_eq!(n, 2);
|
||||
for z in [0.0, 2.0] {
|
||||
let helix = |t: f64| {
|
||||
let (phi, rho) = (angle * t, 3.0 - z);
|
||||
Vec3::new(e.point_at(t).x, setback - rho * phi.sin(), 3.0 - rho * phi.cos())
|
||||
};
|
||||
let EdgeSpec::Chain { ctrl } = s.zone_chain(&f, &e, z) else { panic!("a slanted edge folds to a chain") };
|
||||
assert_eq!(ctrl.len(), 3 * (breaks.len() - 1) + 1);
|
||||
for (k, w) in breaks.windows(2).enumerate() {
|
||||
for x in [0.0, 0.1, 0.37, 0.5, 0.81, 1.0] {
|
||||
let p = cubic_at(ctrl[3 * k], ctrl[3 * k + 1], ctrl[3 * k + 2], ctrl[3 * k + 3], x);
|
||||
let exact = helix(w[0] + (w[1] - w[0]) * x);
|
||||
assert!(p.dist(exact) < BEND_CHAIN_TOL, "height {z}, piece {k}: {}", p.dist(exact));
|
||||
let EdgeSpec::Chain { degree: 7, ctrl } = s.zone_chain(&f, &e, z) else { panic!("a slanted edge folds to a chain of degree 7") };
|
||||
assert_eq!(ctrl.len(), 7 * n + 1);
|
||||
for k in 0..n {
|
||||
for i in 0..=40 {
|
||||
let x = i as f64 / 40.0;
|
||||
let off = bezier_at(&ctrl[7 * k..=7 * k + 7], x).dist(helix((k as f64 + x) / n as f64));
|
||||
assert!(off < BEND_CHAIN_TOL, "height {z}, piece {k}, {x}: {off}");
|
||||
}
|
||||
}
|
||||
// The pieces are equal, so the chain is one smooth curve: on either side of a joint
|
||||
// the first three differences of the control points are the same.
|
||||
let diff = |p: &[Vec3]| p.windows(2).map(|w| w[1] - w[0]).collect::<Vec<_>>();
|
||||
for k in 1..n {
|
||||
let (mut before, mut after) = (ctrl[7 * k - 3..=7 * k].to_vec(), ctrl[7 * k..=7 * k + 3].to_vec());
|
||||
for order in 1..=3 {
|
||||
(before, after) = (diff(&before), diff(&after));
|
||||
let (b, a) = (before[before.len() - 1], after[0]);
|
||||
assert!(b.dist(a) < 1e-12, "height {z}, joint {k}, order {order}: {b:?} vs {a:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7436,6 +7492,59 @@ mod tests {
|
||||
assert!(s.solid().is_ok() && s.flat_regions().unwrap().len() == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_torn_bend_end_folds_to_two_smooth_pieces_on_its_helix() {
|
||||
// The tray of `a_tear_cuts_each_bend_from_the_corner_to_its_flanges_end` (T = 2, R = 1):
|
||||
// each of its eight torn bend ends is the wall ruled between a helix on the bend's inner
|
||||
// face and one on its outer face, `bend_end` at the turn φ from 0 to θ. Cubic pieces took
|
||||
// 32 at either angle, which a viewer draws as 32 strips; two pieces of degree 7 hold the
|
||||
// helix to within the chains' tolerance.
|
||||
for deg in [90.0f64, 72.0] {
|
||||
let what = format!("{deg}°");
|
||||
let (s, _) = tray(&torn(), deg, 38.6);
|
||||
let mut work = FlatWork::new();
|
||||
let cells = s.cells_with_work(&mut work).unwrap();
|
||||
let faces = s.faces(&cells, 0.0, s.t, &mut work).unwrap();
|
||||
let walls: Vec<(&EdgeSpec, &EdgeSpec)> = faces
|
||||
.iter()
|
||||
.filter_map(|f| match &f.surface {
|
||||
SurfSpec::Ruled { a, b } => Some((a, b)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(walls.len(), 8, "{what}");
|
||||
let th = deg.to_radians();
|
||||
for (bottom, top) in walls {
|
||||
for (chain, z) in [(bottom, 0.0), (top, s.t)] {
|
||||
let EdgeSpec::Chain { ctrl, .. } = chain else { panic!("{what}: a torn end folds to a chain") };
|
||||
assert_eq!(ctrl.len(), 15, "{what}: two pieces of degree 7");
|
||||
// The torn end this chain runs along, and which way.
|
||||
let (first, last) = (ctrl[0], ctrl[ctrl.len() - 1]);
|
||||
let end = s.flanges.iter().flat_map(|f| [(f, false), (f, true)]).find_map(|(f, at_end)| {
|
||||
let at = |phi: f64| s.bend_end(f, at_end, phi, z);
|
||||
if at(0.0).dist(first) < 1e-9 && at(th).dist(last) < 1e-9 {
|
||||
Some((f, at_end, false))
|
||||
} else if at(th).dist(first) < 1e-9 && at(0.0).dist(last) < 1e-9 {
|
||||
Some((f, at_end, true))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let Some((f, at_end, back)) = end else { panic!("{what}: {first:?} to {last:?} is no torn end") };
|
||||
for k in 0..2 {
|
||||
for i in 0..=40 {
|
||||
let t = (k as f64 + i as f64 / 40.0) / 2.0;
|
||||
let exact = s.bend_end(f, at_end, th * if back { 1.0 - t } else { t }, z);
|
||||
let off = bezier_at(&ctrl[7 * k..=7 * k + 7], i as f64 / 40.0).dist(exact);
|
||||
assert!(off < BEND_CHAIN_TOL, "{what}: height {z}, {t}: {off}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
holds_the_flat_material(&s, &what);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_torn_bend_end_is_no_flat_face() {
|
||||
use std::f64::consts::{FRAC_PI_2, SQRT_2};
|
||||
|
||||
Reference in new issue
Block a user