Skip to main content

teamcraft/mesh/creo/
freeform.rs

1//! The freeform surfaces of the Creo B-rep: interpolating bicubic splines
2//! (`splsrf`), rolling-ball fillets (`fillet_srf`) and ruled surfaces
3//! (`ruled_srf`). All three carry absolute 3D data, so they need no
4//! convention and even seed the resolution of the analytic faces.
5//!
6//! Curves and grids are clamped C² cubic splines through their points with
7//! the stored end derivatives (de Boor), evaluated as piecewise cubic
8//! Hermite segments.
9
10use super::brep::{cross, dot, lin, norm, scale, sub, Frame, P3};
11use super::tagged::Tree;
12
13/// Largest number of knots of one spline direction.
14const MAX_KNOTS: usize = 10_000;
15
16/// A clamped C² cubic spline through points at knots.
17#[derive(Debug, Clone)]
18pub struct Spline {
19    pub t: Vec<f64>,
20    pub p: Vec<P3>,
21    /// First derivatives at the knots (with respect to the real parameter).
22    pub d: Vec<P3>,
23}
24
25/// Derivatives of a clamped C² cubic spline at its knots (tridiagonal
26/// system, Thomas algorithm). `d0`/`d1` are the end derivatives.
27fn spline_derivs(t: &[f64], p: &[P3], d0: P3, d1: P3) -> Vec<P3> {
28    let n = t.len();
29    match n {
30        0 => return Vec::new(),
31        1 => return vec![d1],
32        2 => return vec![d0, d1],
33        _ => {}
34    }
35    let h: Vec<f64> = t.iter().zip(t.iter().skip(1)).map(|(a, b)| b - a).collect();
36    let m = n - 2;
37    let hk = |i: usize| h.get(i).copied().unwrap_or(1.0);
38    let pk = |i: usize| p.get(i).copied().unwrap_or_default();
39    // one row per interior knot i = k + 1:
40    // lower·d[i−1] + diag·d[i] + upper·d[i+1] = rhs
41    let mut rows: Vec<(f64, f64, f64, P3)> = Vec::with_capacity(m);
42    for k in 0..m {
43        let i = k + 1;
44        let (h0, h1) = (hk(i - 1), hk(i));
45        let mut rhs = scale(
46            3.0,
47            lin(
48                scale(h1 / h0, sub(pk(i), pk(i - 1))),
49                h0 / h1,
50                sub(pk(i + 1), pk(i)),
51            ),
52        );
53        // the known end derivatives move to the right-hand side
54        if k == 0 {
55            rhs = lin(rhs, -h1, d0);
56        }
57        if k + 1 == m {
58            rhs = lin(rhs, -h0, d1);
59        }
60        let lower = if k > 0 { h1 } else { 0.0 };
61        let upper = if k + 1 < m { h0 } else { 0.0 };
62        rows.push((lower, 2.0 * (h0 + h1), upper, rhs));
63    }
64    // Thomas algorithm: forward elimination …
65    let mut fwd: Vec<(f64, P3)> = Vec::with_capacity(m);
66    let (mut c_prev, mut r_prev) = (0.0, [0.0; 3]);
67    for &(lower, diag, upper, rhs) in &rows {
68        let mut den = diag - lower * c_prev;
69        if den.abs() < 1e-300 {
70            den = 1e-300;
71        }
72        c_prev = upper / den;
73        r_prev = scale(1.0 / den, lin(rhs, -lower, r_prev));
74        fwd.push((c_prev, r_prev));
75    }
76    // … and back substitution (the last row has upper = 0)
77    let mut x = vec![[0.0; 3]; m];
78    let mut next = [0.0; 3];
79    for (xk, &(ck, rk)) in x.iter_mut().zip(&fwd).rev() {
80        *xk = lin(rk, -ck, next);
81        next = *xk;
82    }
83    let mut d = Vec::with_capacity(n);
84    d.push(d0);
85    d.extend(x);
86    d.push(d1);
87    d
88}
89
90/// Are the knots strictly increasing?
91fn increasing(t: &[f64]) -> bool {
92    t.iter().zip(t.iter().skip(1)).all(|(a, b)| b > a)
93}
94
95fn hermite(s: f64) -> [f64; 4] {
96    let s2 = s * s;
97    let s3 = s2 * s;
98    [
99        2.0 * s3 - 3.0 * s2 + 1.0,
100        s3 - 2.0 * s2 + s,
101        -2.0 * s3 + 3.0 * s2,
102        s3 - s2,
103    ]
104}
105
106/// Segment index for parameter `x` (clamped to the knot range).
107fn segment(t: &[f64], x: f64) -> usize {
108    if t.len() < 2 {
109        return 0;
110    }
111    let mut i = t.partition_point(|&k| k <= x);
112    i = i.saturating_sub(1);
113    i.min(t.len() - 2)
114}
115
116impl Spline {
117    pub fn new(t: Vec<f64>, p: Vec<P3>, d0: P3, d1: P3) -> Option<Spline> {
118        if t.len() != p.len() || t.is_empty() || t.len() > MAX_KNOTS {
119            return None;
120        }
121        if t.iter().chain(p.iter().flatten()).any(|x| !x.is_finite()) {
122            return None;
123        }
124        if !increasing(&t) {
125            return None;
126        }
127        let d = spline_derivs(&t, &p, d0, d1);
128        Some(Spline { t, p, d })
129    }
130
131    pub fn at(&self, x: f64) -> P3 {
132        let i = segment(&self.t, x);
133        let (t0, t1) = (
134            self.t.get(i).copied().unwrap_or(0.0),
135            self.t.get(i + 1).copied().unwrap_or(1.0),
136        );
137        let h = t1 - t0;
138        if h.abs() < 1e-300 || self.t.len() < 2 {
139            return self.p.first().copied().unwrap_or_default();
140        }
141        let s = (x - t0) / h;
142        let b = hermite(s);
143        let (p0, p1) = (
144            self.p.get(i).copied().unwrap_or_default(),
145            self.p.get(i + 1).copied().unwrap_or_default(),
146        );
147        let (d0, d1) = (
148            self.d.get(i).copied().unwrap_or_default(),
149            self.d.get(i + 1).copied().unwrap_or_default(),
150        );
151        let out = lin(scale(b[0], p0), b[1] * h, d0);
152        lin(lin(out, b[2], p1), b[3] * h, d1)
153    }
154
155    pub fn first_knot(&self) -> f64 {
156        self.t.first().copied().unwrap_or(0.0)
157    }
158
159    pub fn last_knot(&self) -> f64 {
160        self.t.last().copied().unwrap_or(1.0)
161    }
162}
163
164/// Bicubic C² interpolation through a `nu × nv` grid with end tangents and
165/// corner twists (`splsrf`).
166#[derive(Debug, Clone)]
167pub struct Grid {
168    tu: Vec<f64>,
169    tv: Vec<f64>,
170    nv: usize,
171    g: Vec<P3>,
172    pu: Vec<P3>,
173    pv: Vec<P3>,
174    puv: Vec<P3>,
175}
176
177impl Grid {
178    fn idx(&self, i: usize, j: usize) -> usize {
179        i * self.nv + j
180    }
181
182    pub fn eval(&self, u: f64, v: f64) -> P3 {
183        let i = segment(&self.tu, u);
184        let j = segment(&self.tv, v);
185        let (u0, u1) = (
186            self.tu.get(i).copied().unwrap_or(0.0),
187            self.tu.get(i + 1).copied().unwrap_or(1.0),
188        );
189        let (v0, v1) = (
190            self.tv.get(j).copied().unwrap_or(0.0),
191            self.tv.get(j + 1).copied().unwrap_or(1.0),
192        );
193        let (hu, hv) = (u1 - u0, v1 - v0);
194        if hu.abs() < 1e-300 || hv.abs() < 1e-300 {
195            return self.g.get(self.idx(i, j)).copied().unwrap_or_default();
196        }
197        let bu = hermite((u - u0) / hu);
198        let bv = hermite((v - v0) / hv);
199        let mut out = [0.0; 3];
200        // Hermite basis order: value at 0, derivative at 0, value at 1, derivative at 1
201        let corners = [(0, 0), (0, 1), (1, 0), (1, 1)];
202        for (wa, (di, du)) in bu.into_iter().zip(corners) {
203            for (wb, (dj, dv)) in bv.into_iter().zip(corners) {
204                let k = self.idx(i + di, j + dj);
205                let q = match (du, dv) {
206                    (0, 0) => self.g.get(k).copied().unwrap_or_default(),
207                    (1, 0) => self.pu.get(k).copied().unwrap_or_default().map(|x| x * hu),
208                    (0, 1) => self.pv.get(k).copied().unwrap_or_default().map(|x| x * hv),
209                    _ => self
210                        .puv
211                        .get(k)
212                        .copied()
213                        .unwrap_or_default()
214                        .map(|x| x * hu * hv),
215                };
216                out = lin(out, wa * wb, q);
217            }
218        }
219        out
220    }
221}
222
223/// One freeform surface.
224#[derive(Debug, Clone)]
225pub enum Freeform {
226    Spline(Grid),
227    /// Rolling-ball blend: `s = v0 + v (v1 − v0)`, centre `ctr(s)`, radius
228    /// vector `pnt(s) − ctr(s)`, circle axis `tan(s)`; `u` is the angle.
229    Fillet {
230        pnt: Spline,
231        ctr: Spline,
232        tan: Spline,
233        v0: f64,
234        v1: f64,
235    },
236    /// `C1(u) + v (C2(u) − C1(u))`, `u` normalised along both curves.
237    Ruled {
238        c1: Curve,
239        c2: Curve,
240    },
241}
242
243/// A boundary curve of a ruled surface.
244#[derive(Debug, Clone)]
245pub enum Curve {
246    /// Ellipse `O + c1 cos t X + c2 sin t Y`, `t = t0 + u (t1 − t0)`.
247    Conic {
248        frame: Frame,
249        c1: f64,
250        c2: f64,
251        t0: f64,
252        t1: f64,
253    },
254    Spline(Spline),
255}
256
257impl Curve {
258    fn at(&self, u: f64) -> P3 {
259        match self {
260            Curve::Conic {
261                frame,
262                c1,
263                c2,
264                t0,
265                t1,
266            } => {
267                let t = t0 + u * (t1 - t0);
268                lin(lin(frame.o, c1 * t.cos(), frame.x), c2 * t.sin(), frame.y)
269            }
270            Curve::Spline(s) => s.at(s.first_knot() + u * (s.last_knot() - s.first_knot())),
271        }
272    }
273}
274
275impl Freeform {
276    pub fn eval(&self, u: f64, v: f64) -> P3 {
277        match self {
278            Freeform::Spline(g) => g.eval(u, v),
279            Freeform::Fillet {
280                pnt,
281                ctr,
282                tan,
283                v0,
284                v1,
285            } => {
286                let s = v0 + v * (v1 - v0);
287                let c = ctr.at(s);
288                let a = sub(pnt.at(s), c);
289                let r2 = dot(a, a);
290                let mut t = tan.at(s);
291                if r2 > 1e-300 {
292                    t = lin(t, -dot(t, a) / r2, a);
293                }
294                let lt = norm(t);
295                if lt > 1e-300 {
296                    t = t.map(|x| x / lt);
297                }
298                let b = cross(t, a);
299                let (cu, su) = (u.cos(), u.sin());
300                lin(lin(c, cu, a), su, b)
301            }
302            Freeform::Ruled { c1, c2 } => {
303                let a = c1.at(u);
304                let b = c2.at(u);
305                lin(a, v, sub(b, a))
306            }
307        }
308    }
309}
310
311fn p3s(tree: &Tree, node: usize) -> Vec<P3> {
312    tree.rows(node)
313        .into_iter()
314        .filter_map(|r| <P3>::try_from(r.as_slice()).ok())
315        .collect()
316}
317
318fn spline_of(tree: &Tree, node: usize) -> Option<Spline> {
319    let t = tree.dbls(tree.child(node, "params")?);
320    let p = p3s(tree, tree.child(node, "i_pnts")?);
321    let e = p3s(tree, tree.child(node, "end_tangts")?);
322    Spline::new(
323        t,
324        p,
325        e.first().copied().unwrap_or_default(),
326        e.get(1).copied().unwrap_or_default(),
327    )
328}
329
330/// Reads a freeform surface from its `srf_prim_ptr(<kind>)` node.
331pub fn read(tree: &Tree, kind: &str, prim: usize) -> Option<Freeform> {
332    match kind {
333        "splsrf" => {
334            let tu = tree.dbls(tree.child(prim, "u_params")?);
335            let tv = tree.dbls(tree.child(prim, "v_params")?);
336            let (nu, nv) = (tu.len(), tv.len());
337            if nu < 2
338                || nv < 2
339                || nu > MAX_KNOTS
340                || nv > MAX_KNOTS
341                || nu.checked_mul(nv)? > 4_000_000
342            {
343                return None;
344            }
345            let g = p3s(tree, tree.child(prim, "i_points")?);
346            let eu = p3s(tree, tree.child(prim, "end_u_tangts")?);
347            let ev = p3s(tree, tree.child(prim, "end_v_tangts")?);
348            let tw = p3s(tree, tree.child(prim, "end_uv_deriv")?);
349            if g.len() != nu * nv || eu.len() != 2 * nv || ev.len() != 2 * nu || tw.len() != 4 {
350                return None;
351            }
352            if !increasing(&tu) || !increasing(&tv) {
353                return None;
354            }
355            if g.iter()
356                .chain(eu.iter())
357                .chain(ev.iter())
358                .chain(tw.iter())
359                .flatten()
360                .any(|x| !x.is_finite())
361            {
362                return None;
363            }
364            // grid value at (i, j); the sizes were checked above
365            let at = |v: &[P3], i: usize, j: usize| v.get(i * nv + j).copied().unwrap_or_default();
366            let get = |v: &[P3], k: usize| v.get(k).copied().unwrap_or_default();
367            let put = |v: &mut Vec<P3>, i: usize, j: usize, x: P3| {
368                if let Some(slot) = v.get_mut(i * nv + j) {
369                    *slot = x;
370                }
371            };
372            let mut pu = vec![[0.0; 3]; nu * nv];
373            let mut pv = vec![[0.0; 3]; nu * nv];
374            let mut puv = vec![[0.0; 3]; nu * nv];
375            for j in 0..nv {
376                let col: Vec<P3> = (0..nu).map(|i| at(&g, i, j)).collect();
377                let d = spline_derivs(&tu, &col, get(&eu, j), get(&eu, nv + j));
378                for (i, x) in d.into_iter().enumerate() {
379                    put(&mut pu, i, j, x);
380                }
381            }
382            for i in 0..nu {
383                let row: Vec<P3> = (0..nv).map(|j| at(&g, i, j)).collect();
384                let d = spline_derivs(&tv, &row, get(&ev, i), get(&ev, nu + i));
385                for (j, x) in d.into_iter().enumerate() {
386                    put(&mut pv, i, j, x);
387                }
388            }
389            // twists along the first and last u rows, then across u
390            for (e, i) in [(0usize, 0usize), (1, nu - 1)] {
391                let row: Vec<P3> = (0..nv).map(|j| at(&pu, i, j)).collect();
392                let d = spline_derivs(&tv, &row, get(&tw, e * 2), get(&tw, e * 2 + 1));
393                for (j, x) in d.into_iter().enumerate() {
394                    put(&mut puv, i, j, x);
395                }
396            }
397            for j in 0..nv {
398                let col: Vec<P3> = (0..nu).map(|i| at(&pv, i, j)).collect();
399                let d = spline_derivs(&tu, &col, at(&puv, 0, j), at(&puv, nu - 1, j));
400                for (i, x) in d.into_iter().enumerate() {
401                    put(&mut puv, i, j, x);
402                }
403            }
404            Some(Freeform::Spline(Grid {
405                tu,
406                tv,
407                nv,
408                g,
409                pu,
410                pv,
411                puv,
412            }))
413        }
414        "fillet_srf" => {
415            let pnt = spline_of(tree, tree.child(prim, "pnt_spline")?)?;
416            let ctr = spline_of(tree, tree.child(prim, "ctr_spline")?)?;
417            let tan = spline_of(tree, tree.child(prim, "tan_spline")?)?;
418            let v0 = tree.dbl_at(prim, "par_v_0")?;
419            let v1 = tree.dbl_at(prim, "par_v_1")?;
420            if !(v0.is_finite() && v1.is_finite()) {
421                return None;
422            }
423            Some(Freeform::Fillet {
424                pnt,
425                ctr,
426                tan,
427                v0,
428                v1,
429            })
430        }
431        "ruled_srf" => {
432            let c1 = ruled_curve(tree, prim, "curve_1")?;
433            let c2 = ruled_curve(tree, prim, "curve_2")?;
434            Some(Freeform::Ruled { c1, c2 })
435        }
436        _ => None,
437    }
438}
439
440fn ruled_curve(tree: &Tree, prim: usize, key: &str) -> Option<Curve> {
441    let (name, node) = tree
442        .children_with_prefix(prim, &format!("{key}("))
443        .into_iter()
444        .next()?;
445    let ty = name
446        .trim_start_matches(key)
447        .trim_start_matches('(')
448        .trim_end_matches(')');
449    match ty {
450        "conic" => {
451            let rows = tree.rows(tree.child(node, "local_sys")?);
452            let frame = super::brep::frame_of(&rows)?;
453            let c1 = tree.dbl_at(node, "c1")?;
454            let c2 = tree.dbl_at(node, "c2")?;
455            let t0 = tree.dbl_at(node, "t0")?;
456            let t1 = tree.dbl_at(node, "t1")?;
457            if ![c1, c2, t0, t1].iter().all(|x| x.is_finite()) {
458                return None;
459            }
460            Some(Curve::Conic {
461                frame,
462                c1,
463                c2,
464                t0,
465                t1,
466            })
467        }
468        "spline" => Some(Curve::Spline(spline_of(tree, node)?)),
469        _ => None,
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn a_clamped_spline_interpolates_its_points_and_end_tangents() {
479        let t = vec![0.0, 1.0, 2.0, 3.0];
480        let p: Vec<P3> = t.iter().map(|&x| [x, x * x, 0.0]).collect();
481        let s = Spline::new(t.clone(), p.clone(), [1.0, 0.0, 0.0], [1.0, 6.0, 0.0]).unwrap();
482        for (x, q) in t.iter().zip(&p) {
483            let r = s.at(*x);
484            assert!(norm(sub(r, *q)) < 1e-12);
485        }
486        // a parabola with exact end derivatives is reproduced exactly
487        let r = s.at(1.5);
488        assert!((r[1] - 2.25).abs() < 1e-9, "{r:?}");
489        assert_eq!(segment(&t, -1.0), 0);
490        assert_eq!(segment(&t, 9.0), 2);
491    }
492}