diff --git a/crates/kernel/src/loft_rails.rs b/crates/kernel/src/loft_rails.rs index 84200ce63d..5c65c5f1ea 100644 --- a/crates/kernel/src/loft_rails.rs +++ b/crates/kernel/src/loft_rails.rs @@ -8,26 +8,89 @@ //! (stretches along that line), leaning to follow the second. At the sections the move is none, //! so the sections and the loft's caps stay exactly as drawn; the loft's smooth surfaces then run //! through the sections and the stations together. +//! +//! Sections are exact NURBS curves ([`Section`], from the sketch segments' record curves); a +//! station is the plain loft's control points moved by an affine map, so each is exact too. -use mt::{BoundedCurve, ParametricCurve}; -use solidcraft_geom::{Plane, Vec3}; -use truck_modeling as mt; +use solidcraft_geom::bspline::CurveN; +use solidcraft_geom::{Plane, Seg2, Vec3}; -use crate::body::{from_p3, p3}; use crate::sweep_path::RailFit; use crate::{KernelError, Result}; -/// Most wires a railed loft is skinned through, sections and stations together. +/// Most curves a railed loft is skinned through per column, sections and stations together. pub(crate) const MAX_WIRES: usize = 33; /// Most stations between two sections. pub(crate) const MAX_PER_SPAN: usize = 15; -type Curve = mt::BSplineCurve; +/// A loft section: its plane and its curves in order, each a NURBS curve with homogeneous control +/// points `[x w, y w, z w, w]`, and the profile segments each curve stands for. +#[derive(Clone, Debug)] +pub(crate) struct Section { + pub plane: Plane, + pub curves: Vec>, + pub numbers: Vec>, +} -/// Where a rail meets a section: the point of the section's wire it guides (as a wire parameter: -/// the edge's index plus how far along the edge), that point, how far it is from where the rail -/// crosses the section's plane (the nudge onto it), and how far along the rail that crossing is -/// (segment and fraction). +impl Section { + /// The sketch segments `segs` on `plane` as a section (their exact record curves), segment i + /// standing for the profile segments `numbers[i]`. + pub(crate) fn of(plane: &Plane, segs: &[Seg2], numbers: Vec>) -> Result
{ + let curves = segs.iter().map(|s| Ok(nurbs(&crate::freeform::segment(plane, s)?.0)?)).collect::>>()?; + Ok(Section { plane: *plane, curves, numbers }) + } +} + +/// A record line or (rational) B-spline as a homogeneous NURBS curve. +fn nurbs(c: &solidcraft_geom::brep::Curve) -> Result> { + use solidcraft_geom::brep::Curve; + let degree = |knots: &[f64], n: usize| knots.len().checked_sub(n + 1).ok_or_else(|| KernelError::Invalid("spline degree".into())); + Ok(match c { + Curve::Line { start, end } => { + CurveN { degree: 1, knots: vec![0.0, 0.0, 1.0, 1.0], ctrl: vec![[start[0], start[1], start[2], 1.0], [end[0], end[1], end[2], 1.0]] } + } + Curve::Spline { knots, points } => { + CurveN { degree: degree(knots, points.len())?, knots: knots.clone(), ctrl: points.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect() } + } + Curve::RationalSpline { knots, homogeneous } => { + CurveN { degree: degree(knots, homogeneous.len())?, knots: knots.clone(), ctrl: homogeneous.clone() } + } + _ => return Err(KernelError::Invalid("a loft section curve that is not a line or a spline".into())), + }) +} + +/// A curve's control points (x, y, z) and weights. +fn points(c: &CurveN<4>) -> (Vec, Vec) { + c.ctrl.iter().map(|h| (Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]), h[3])).unzip() +} + +/// `c` with the control points `pts` (its own weights and knots). +fn with(c: &CurveN<4>, pts: &[Vec3]) -> CurveN<4> { + let ctrl = pts.iter().zip(&c.ctrl).map(|(p, h)| [p.x * h[3], p.y * h[3], p.z * h[3], h[3]]).collect(); + CurveN { degree: c.degree, knots: c.knots.clone(), ctrl } +} + +/// The point `f` (0…1) of the way along a curve's parameter range. +fn point_at(c: &CurveN<4>, f: f64) -> Option { + let (t0, t1) = (*c.knots.get(c.degree)?, *c.knots.get(c.ctrl.len())?); + let ([x, y, z, w], _) = c.eval(t0 + (t1 - t0) * f.clamp(0.0, 1.0))?; + (w.abs() > 1e-300).then(|| Vec3::new(x / w, y / w, z / w)) +} + +/// Whether two curves pair up: the same degree, knots and weights (their control points +/// correspond). +fn pairs(a: &CurveN<4>, b: &CurveN<4>) -> bool { + let near = |x: f64, y: f64| (x - y).abs() <= 1e-9 * (1.0 + x.abs()); + a.degree == b.degree + && a.ctrl.len() == b.ctrl.len() + && a.knots.len() == b.knots.len() + && a.knots.iter().zip(&b.knots).all(|(x, y)| near(*x, *y)) + && a.ctrl.iter().zip(&b.ctrl).all(|(x, y)| near(x[3], y[3])) +} + +/// Where a rail meets a section: the point of the section it guides (as a wire parameter: the +/// curve's index plus how far along its parameter range), that point, the nudge onto it from where +/// the rail meets the section's plane, and how far along the rail that is (segment and fraction). struct Anchor { g: f64, on: Vec3, @@ -55,51 +118,28 @@ impl Affine { } } -/// The loft's wires with stations added between the sections so that the loft follows `rails` -/// (polylines; see the module notes). `planes` are the sections' planes, `wires` their matched -/// wires (as [`crate::build::loft_wires`] or a sheet loft makes them: the k-th edges of every -/// wire are B-splines on one knot vector), both in loft order; `closed` whether the wires close. -/// Without rails the wires come back as they are. -pub(crate) fn railed_wires(planes: &[Plane], wires: &[mt::Wire], closed: bool, rails: &[Vec], fit: RailFit) -> Result> { +/// The loft's sections with stations added between them so that the loft follows `rails` +/// (polylines; see the module notes), in loft order: each entry one section's (or station's) +/// curves. `closed` says whether the sections close. Without rails the sections' own curves come +/// back. With rails, every section's k-th curve has to pair with the others' (the same kind, +/// so their control points correspond). +pub(crate) fn railed(sections: &[Section], closed: bool, rails: &[Vec], fit: RailFit) -> Result>>> { if rails.is_empty() { - return Ok(wires.to_vec()); + return Ok(sections.iter().map(|s| s.curves.clone()).collect()); } if rails.len() > 2 { return Err(KernelError::Invalid("a loft takes one or two guide rails".into())); } let fail = |m: &str| KernelError::Failed(format!("loft rails: {m}")); - let count = wires.len(); - if count < 2 || planes.len() != count { - return Err(fail("sections")); + let count = sections.len(); + let lead = sections.first().map(|s| &s.curves).ok_or_else(|| fail("sections"))?; + let n = lead.len(); + if count < 2 || n == 0 || sections.iter().any(|s| s.curves.len() != n || s.curves.iter().zip(lead).any(|(a, b)| !pairs(a, b))) { + return Err(KernelError::Invalid( + "not supported yet: a loft along guide rails between sections whose curves differ in kind (a line against an arc)".into(), + )); } - // Each section's edge curves, and the knot vector the k-th edges share. - let curves: Vec> = wires - .iter() - .map(|w| { - w.edge_iter() - .map(|e| match e.curve() { - mt::Curve::BSplineCurve(c) if e.orientation() => Some(c), - _ => None, - }) - .collect::>>() - }) - .collect::>() - .ok_or_else(|| fail("section curves"))?; - let first = curves.first().ok_or_else(|| fail("sections"))?; - let n = first.len(); - let knots: Vec = first.iter().map(|c| c.knot_vec().clone()).collect(); - let sizes: Vec = first.iter().map(|c| c.control_points().len()).collect(); - if n == 0 - || curves - .iter() - .any(|cs| cs.len() != n || cs.iter().zip(&knots).zip(&sizes).any(|((c, k), m)| c.knot_vec() != k || c.control_points().len() != *m)) - { - return Err(fail("sections parameterised differently")); - } - let ctrl: Vec>> = - curves.iter().map(|cs| cs.iter().map(|c| c.control_points().iter().map(|p| from_p3(*p)).collect()).collect()).collect(); - let edges: Vec> = wires.iter().map(|w| w.edge_iter().cloned().collect()).collect(); - let along = crate::build::section_parameters(&edges); + let ctrl: Vec>> = sections.iter().map(|s| s.curves.iter().map(|c| points(c).0).collect()).collect(); let mut bounds = solidcraft_geom::Aabb3::EMPTY; for p in ctrl.iter().flatten().flatten() { if !p.is_finite() { @@ -109,86 +149,89 @@ pub(crate) fn railed_wires(planes: &[Plane], wires: &[mt::Wire], closed: bool, r } let size = bounds.diagonal(); let tol = (size * 1e-4).max(1e-6); - let start = centroid(ctrl.first().ok_or_else(|| fail("sections"))?); - let rails: Vec = rails.iter().map(|r| rail(r, start, planes, &curves, tol)).collect::>()?; - if let [a, b] = &rails[..] - && a.anchors.iter().zip(&b.anchors).any(|(p, q)| p.on.dist(q.on) <= tol) - { - return Err(KernelError::Invalid("the two guide rails meet a section at the same point".into())); + // The sections' parameters along the loft: in proportion to the mean distance between their + // curves' starts. + let mut along = vec![0.0]; + for w in ctrl.windows(2) { + let step = w[0].iter().zip(&w[1]).filter_map(|(a, b)| Some(a.first()?.dist(*b.first()?))).sum::() / n as f64; + along.push(along.last().copied().unwrap_or(0.0) + step); } - // The plain loft between the sections: each control point across them, straight between two - // sections, the smooth loft's curve through three or more. - let across: Vec>> = if count == 2 { + let total = along.last().copied().unwrap_or(0.0); + if !(total > 1e-9) || along.windows(2).any(|w| !(w[1] - w[0] > 1e-9 * total)) { + return Err(KernelError::Invalid("two loft sections sit on top of each other".into())); + } + along.iter_mut().for_each(|u| *u /= total); + // The plain loft between the sections: each control point straight between two sections, the + // curve (quadratic, or cubic from four) through them across more. + let degree = (count - 1).min(3); + let across: Vec>> = if count == 2 { Vec::new() } else { (0..n) .map(|k| { - (0..sizes.get(k).copied().unwrap_or(0)) + (0..lead.get(k).map_or(0, |c| c.ctrl.len())) .map(|j| { - let pts: Vec = ctrl.iter().filter_map(|s| s.get(k).and_then(|e| e.get(j)).copied()).collect(); - crate::build::across(&pts, &along) + let pts: Vec<[f64; 3]> = ctrl.iter().filter_map(|s| s.get(k)?.get(j).map(|q| [q.x, q.y, q.z])).collect(); + CurveN::interpolate(&along, &pts, degree).map_err(|e| fail(&e.to_string())) }) - .collect() + .collect::>>() }) - .collect() + .collect::>()? }; let plain = |v: f64| -> Result>> { let (Some(a), Some(b)) = (ctrl.first(), ctrl.last()) else { return Err(fail("sections")) }; if count == 2 { return Ok(a.iter().zip(b).map(|(ea, eb)| ea.iter().zip(eb).map(|(p, q)| p.lerp(*q, v)).collect()).collect()); } - across.iter().map(|cs| cs.iter().map(|c| c.as_ref().map(|c| from_p3(c.subs(v))).ok_or_else(|| fail("the plain loft"))).collect()).collect() + across + .iter() + .map(|cs| cs.iter().map(|c| c.eval(v).map(|([x, y, z], _)| Vec3::new(x, y, z)).ok_or_else(|| fail("the plain loft"))).collect()) + .collect() }; - // The middle of the plain loft (the middles of the sections' control points, across them as - // the control points are: the middle is linear in them) and its direction. - let middles: Vec = ctrl.iter().map(|s| centroid(s)).collect(); - let middle_curve = if count == 2 { None } else { Some(crate::build::across(&middles, &along).ok_or_else(|| fail("the plain loft"))?) }; + let curves_of = |pts: &[Vec]| -> Vec> { lead.iter().zip(pts).map(|(c, p)| with(c, p)).collect() }; + // The plain loft's middle and its direction there. let middle = |v: f64| -> Result<(Vec3, Vec3)> { - let (p, d) = match (&middle_curve, middles.first(), middles.last()) { - (Some(c), ..) => { - let d = c.der(v); - (from_p3(c.subs(v)), Vec3::new(d.x, d.y, d.z)) - } - (None, Some(a), Some(b)) => (a.lerp(*b, v), *b - *a), - _ => return Err(fail("sections")), - }; + let h = 1e-4; + let d = centroid(&plain((v + h).min(1.0))?) - centroid(&plain((v - h).max(0.0))?); let d = d.normalized().ok_or_else(|| KernelError::Invalid("two loft sections sit on top of each other".into()))?; - Ok((p, d)) + Ok((centroid(&plain(v)?), d)) }; + let start = centroid(ctrl.first().ok_or_else(|| fail("sections"))?); + let rails: Vec = rails.iter().map(|r| rail(r, start, sections, tol)).collect::>()?; + if let [a, b] = &rails[..] + && a.anchors.iter().zip(&b.anchors).any(|(p, q)| p.on.dist(q.on) <= tol) + { + return Err(KernelError::Invalid("the two guide rails meet a section at the same point".into())); + } // The sections' normals, turned to agree with the first's. let mut normals: Vec = Vec::with_capacity(count); - for p in planes { - let n = p.normal(); - let prev = normals.last().copied().unwrap_or(n); - normals.push(if n.dot(prev) < 0.0 { n * -1.0 } else { n }); + for s in sections { + let nn = s.plane.normal(); + let prev = normals.last().copied().unwrap_or(nn); + normals.push(if nn.dot(prev) < 0.0 { nn * -1.0 } else { nn }); } let spans = count - 1; let per = (MAX_WIRES.saturating_sub(count) / spans).clamp(1, MAX_PER_SPAN); let mut out = Vec::with_capacity(count + spans * per); for i in 0..spans { - out.push(wires.get(i).cloned().ok_or_else(|| fail("sections"))?); + out.push(sections.get(i).map(|s| s.curves.clone()).ok_or_else(|| fail("sections"))?); let (Some(&v0), Some(&v1), Some(&n0), Some(&n1)) = (along.get(i), along.get(i + 1), normals.get(i), normals.get(i + 1)) else { return Err(fail("sections")); }; - // Each rail's anchors at the span's two sections, and where its walk has got to. - let mut spans_of: Vec<(&Rail, &Anchor, &Anchor, (usize, f64))> = Vec::with_capacity(rails.len()); - for r in &rails { - let (Some(a0), Some(a1)) = (r.anchors.get(i), r.anchors.get(i + 1)) else { return Err(fail("rail")) }; - spans_of.push((r, a0, a1, a0.at)); - } + let mut cursors: Vec<(usize, f64)> = rails.iter().map(|r| r.anchors.get(i).map_or((0, 0.0), |a| a.at)).collect(); for q in 1..=per { let f = q as f64 / (per + 1) as f64; let v = v0 + f * (v1 - v0); - let station = plain(v)?; + let station = curves_of(&plain(v)?); let (mid, dir) = middle(v)?; // The station's plane turns from one section's to the next's. let normal = n0.lerp(n1, f).normalized().ok_or_else(|| fail("section planes"))?; let mut pairs = Vec::with_capacity(rails.len()); - for (r, a0, a1, cursor) in &mut spans_of { - let anchor = wire_point(&station, &knots, interpolate_g(a0.g, a1.g, f, n, closed))?; - // Where the rail crosses the station's plane through the point it guides, nudged - // so that at each section it is the section's own point (a rail drawn a hair off - // a section moves nothing there). + for (r, cursor) in rails.iter().zip(cursors.iter_mut()) { + let (Some(a0), Some(a1)) = (r.anchors.get(i), r.anchors.get(i + 1)) else { return Err(fail("rail")) }; + let anchor = wire_point(&station, interpolate_g(a0.g, a1.g, f, n, closed))?; + // Where the rail crosses the station's plane through the point it guides, nudged so + // that at each section it is the section's own point. let Some((hit, at)) = crate::sweep_path::rail_crossing(&r.pts, anchor, normal, *cursor, tol) else { return Err(KernelError::Invalid(format!( "a guide rail turns back between sections {} and {}: it has to run on from one section to the next", @@ -197,21 +240,25 @@ pub(crate) fn railed_wires(planes: &[Plane], wires: &[mt::Wire], closed: bool, r ))); }; *cursor = at; - let target = hit + a0.nudge * (1.0 - f) + a1.nudge * f; - pairs.push((anchor, target)); + pairs.push((anchor, hit + a0.nudge * (1.0 - f) + a1.nudge * f)); } let map = station_map(&pairs, mid, dir, fit, size)?; - let moved: Vec> = station.iter().map(|e| e.iter().map(|p| map.apply(*p)).collect()).collect(); - out.push(wire_of(&moved, &knots, closed)?); + out.push(station.iter().map(|c| with(c, &points(c).0.iter().map(|p| map.apply(*p)).collect::>())).collect()); } } - out.push(wires.last().cloned().ok_or_else(|| fail("sections"))?); + out.push(sections.last().map(|s| s.curves.clone()).ok_or_else(|| fail("sections"))?); Ok(out) } -/// The rail `pts` from its end nearer `start` (the first section's middle), where it meets each -/// section: across the section's plane, on the section's curve within `tol`. -fn rail(pts: &[Vec3], start: Vec3, planes: &[Plane], curves: &[Vec], tol: f64) -> Result { +/// The rail `pts` from its end nearer `start` (the first section's middle), and where it meets +/// each section, on the section's curve within `tol`. +/// +/// A rail runs from the first section to the last: it meets the last section at its end. In +/// between it meets each section where it first crosses the section's plane after the section +/// before. The end is not a crossing: a rail that lies in the last section's plane (a keel drawn +/// in the bow's sketch plane) crosses that plane all along, first at the far section, and meets the +/// section only where it ends. +fn rail(pts: &[Vec3], start: Vec3, sections: &[Section], tol: f64) -> Result { let mut pts = crate::sweep_path::clean(pts).map_err(|_| KernelError::Invalid("a guide rail needs 2…100000 finite points along its length".into()))?; if let (Some(a), Some(b)) = (pts.first(), pts.last()) @@ -219,17 +266,27 @@ fn rail(pts: &[Vec3], start: Vec3, planes: &[Plane], curves: &[Vec], tol: { pts.reverse(); } + let not_reached = |i: usize| { + KernelError::Invalid(format!( + "a guide rail does not reach section {}: a rail has to run from the first section to the last, through every one", + i + 1 + )) + }; let mut cursor = (0usize, 0.0f64); - let mut anchors = Vec::with_capacity(planes.len()); - for (i, (plane, section)) in planes.iter().zip(curves).enumerate() { - let Some((hit, at)) = crate::sweep_path::rail_crossing(&pts, plane.origin, plane.normal(), cursor, tol) else { - return Err(KernelError::Invalid(format!( - "a guide rail does not reach section {}: a rail has to run from the first section to the last, through every one", - i + 1 - ))); + let mut anchors = Vec::with_capacity(sections.len()); + for (i, s) in sections.iter().enumerate() { + let (plane, curves) = (&s.plane, &s.curves); + let (hit, at) = if i + 1 == sections.len() { + let end = *pts.last().ok_or_else(|| not_reached(i))?; + if plane.height(end).abs() > tol { + return Err(not_reached(i)); + } + (end, (pts.len().saturating_sub(2), 1.0)) + } else { + crate::sweep_path::rail_crossing(&pts, plane.origin, plane.normal(), cursor, tol).ok_or_else(|| not_reached(i))? }; cursor = at; - let (g, on, d) = nearest_on(section, hit).ok_or_else(|| KernelError::Failed("loft rails: section".into()))?; + let (g, on, d) = nearest_on(curves, hit).ok_or_else(|| KernelError::Failed("loft rails: section".into()))?; if d > tol { return Err(KernelError::Invalid(format!( "a guide rail crosses section {}'s plane {d:.3} mm from its curves: a rail has to pass through every section", @@ -241,17 +298,28 @@ fn rail(pts: &[Vec3], start: Vec3, planes: &[Plane], curves: &[Vec], tol: Ok(Rail { pts, anchors }) } -/// The point of a section's wire (its edge curves) nearest `p`: as a wire parameter, the point, -/// and how far it is. -fn nearest_on(section: &[Curve], p: Vec3) -> Option<(f64, Vec3, f64)> { +/// The point of a section (its curves) nearest `p`: as a wire parameter, the point, and how far +/// it is. Each curve is searched from its nearest of 256 even samples by golden-section search. +fn nearest_on(section: &[CurveN<4>], p: Vec3) -> Option<(f64, Vec3, f64)> { + const SAMPLES: usize = 256; let mut best: Option<(f64, Vec3, f64)> = None; for (k, c) in section.iter().enumerate() { - let (t0, t1) = c.range_tuple(); - let t = crate::curveblend::param_on(&mt::Curve::BSplineCurve(c.clone()), p)?; - let q = from_p3(c.subs(t)); - let d = q.dist(p); - if best.as_ref().is_none_or(|b| d < b.2) { - best = Some((k as f64 + (t - t0) / (t1 - t0).max(1e-300), q, d)); + let d = |f: f64| point_at(c, f).map_or(f64::INFINITY, |q| q.dist(p)); + let Some(i) = (0..=SAMPLES).min_by(|a, b| d(*a as f64 / SAMPLES as f64).total_cmp(&d(*b as f64 / SAMPLES as f64))) else { continue }; + let (mut lo, mut hi) = ((i as f64 - 1.0).max(0.0) / SAMPLES as f64, (i as f64 + 1.0).min(SAMPLES as f64) / SAMPLES as f64); + let g = 0.5 * (5f64.sqrt() - 1.0); + for _ in 0..80 { + let (a, b) = (hi - g * (hi - lo), lo + g * (hi - lo)); + if d(a) < d(b) { + hi = b; + } else { + lo = a; + } + } + let f = 0.5 * (lo + hi); + let q = point_at(c, f)?; + if best.as_ref().is_none_or(|b| q.dist(p) < b.2) { + best = Some((k as f64 + f, q, q.dist(p))); } } best @@ -273,18 +341,13 @@ pub(crate) fn interpolate_g(g0: f64, g1: f64, f: f64, n: usize, closed: bool) -> (g0 + f * d).rem_euclid(n) } -/// The point at wire parameter `g` of a wire given by its edges' control points on `knots`. -fn wire_point(ctrl: &[Vec], knots: &[mt::KnotVec], g: f64) -> Result { - let n = ctrl.len(); - let k = (g.floor().max(0.0) as usize).min(n.saturating_sub(1)); - let (Some(pts), Some(kv)) = (ctrl.get(k), knots.get(k)) else { return Err(KernelError::Failed("loft rails: wire".into())) }; - let c = edge_curve(kv, pts)?; - let (t0, t1) = c.range_tuple(); - let f = (g - k as f64).clamp(0.0, 1.0); - Ok(from_p3(c.subs(t0 + (t1 - t0) * f))) +/// The point at wire parameter `g` of a section's curves. +fn wire_point(section: &[CurveN<4>], g: f64) -> Result { + let k = (g.floor().max(0.0) as usize).min(section.len().saturating_sub(1)); + section.get(k).and_then(|c| point_at(c, g - k as f64)).ok_or_else(|| KernelError::Failed("loft rails: section".into())) } -/// The middle of a wire's control points. +/// The middle of a section's control points. fn centroid(ctrl: &[Vec]) -> Vec3 { let (sum, n) = ctrl.iter().flatten().fold((Vec3::ZERO, 0usize), |(s, n), p| (s + *p, n + 1)); sum * (1.0 / n.max(1) as f64) @@ -324,26 +387,3 @@ pub(crate) fn station_map(pairs: &[(Vec3, Vec3)], middle: Vec3, dir: Vec3, fit: let shift = if two { Vec3::ZERO } else { dir * lead }; Ok(Affine { from, to: to + shift, m }) } - -/// A wire of B-spline edges with these control points on these knots, joined end to end -/// (closing back on its first edge's start when `closed`). -fn wire_of(ctrl: &[Vec], knots: &[mt::KnotVec], closed: bool) -> Result { - let fail = || KernelError::Failed("loft rails: station".into()); - let n = ctrl.len(); - let mut verts: Vec = ctrl.iter().map(|e| e.first().map(|p| mt::Vertex::new(p3(*p)))).collect::>().ok_or_else(fail)?; - if !closed { - verts.push(mt::Vertex::new(p3(*ctrl.last().and_then(|e| e.last()).ok_or_else(fail)?))); - } - let mut edges = Vec::with_capacity(n); - for (i, (pts, kv)) in ctrl.iter().zip(knots).enumerate() { - let (Some(a), Some(b)) = (verts.get(i), verts.get(if closed { (i + 1) % n } else { i + 1 })) else { return Err(fail()) }; - let c = edge_curve(kv, pts)?; - edges.push(mt::Edge::new(a, b, mt::Curve::BSplineCurve(c))); - } - Ok(edges.into()) -} - -/// An edge's B-spline from its control points on its knots. -fn edge_curve(kv: &mt::KnotVec, pts: &[Vec3]) -> Result { - Curve::try_new(kv.clone(), pts.iter().map(|p| p3(*p)).collect()).map_err(|e| KernelError::Failed(format!("loft rails: {e}"))) -}