kernel: corner rules for direction, rails and closed paths; no feature gate on SMLib sweeps

The three refusals of the cornered controlled sweep become the owner's rules:
- an orientation direction turns the section about the corner's binormal like
  any sweep, then turns it back about its normal to the direction's frame
  evenly along the next run, as a twist (direction_offset: the turn between
  the carried section and the direction law's section there);
- guide rails follow a path with corners when each rail has a corner of its
  own at every corner of the path, in the plane halving it and by the same
  turn about the same axis; the rails are cut there into each run's
  (run_rails), anything else is refused with the corner named;
- round a closed path with corners the first run starts from the closing
  mitre and the last ends on it; a twist is allowed where the profile maps
  onto itself (closing_offset), an end scale is refused.
Each run's station count now follows its own turn, twist included.

solidcraft-smlib is a dependency in every build (it answers NotBuilt without
SMLib), so smlib_sweeps' imp is no longer behind the smlib feature and its
stubs and not_built go; polyhedron calls offset_backend in every build.
Deleted as unused: sweep_path::{closed_path, turn, stations}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:49:18 -04:00
parent caa5ed0e63
commit dead22ba7a
3 changed files with 156 additions and 156 deletions
+4 -28
View File
@@ -179,30 +179,14 @@ fn polyhedron_record(hs: &[HalfSpace], verts: &[Vec3], faces: &[(usize, Vec<usiz
/// Hollow a body: walls of `thickness` inside, open at the faces under `open` (SMLib's shell).
/// The result's history is over `[b]`: the inner walls are offsets of the faces they line.
pub fn shell(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
#[cfg(feature = "smlib")]
{
crate::offset_backend::shell(b, open, thickness)
}
#[cfg(not(feature = "smlib"))]
{
let _ = (b, open, thickness);
Err(crate::smlib_sweeps::not_built())
}
crate::offset_backend::shell(b, open, thickness)
}
/// Tilt the faces near `faces` by `angle` about their line on the neutral plane, so that they
/// lean in (positive angle) going along `pull` (SMLib's draft).
pub fn draft(b: &Body, faces: &[Vec3], neutral: &solidcraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
b.require_brep("draft")?;
#[cfg(feature = "smlib")]
{
crate::offset_backend::draft(b, faces, neutral, pull, angle)
}
#[cfg(not(feature = "smlib"))]
{
let _ = (faces, neutral, pull, angle);
Err(crate::smlib_sweeps::not_built())
}
crate::offset_backend::draft(b, faces, neutral, pull, angle)
}
/// Move the faces at the given points along their outward normals by `distance` (negative: into
@@ -233,14 +217,6 @@ pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
}
}
let chosen = crate::offset::with_same_surface(&healed, &chosen);
#[cfg(feature = "smlib")]
{
let moved = crate::offset_backend::offset_faces(&healed, &chosen, distance)?;
Ok(crate::record::after(moved, &healed, &crate::record::counts(&[b])))
}
#[cfg(not(feature = "smlib"))]
{
let _ = chosen;
Err(crate::smlib_sweeps::not_built())
}
let moved = crate::offset_backend::offset_faces(&healed, &chosen, distance)?;
Ok(crate::record::after(moved, &healed, &crate::record::counts(&[b])))
}
+152 -97
View File
@@ -24,7 +24,6 @@ use crate::numbered::{NumberedLoop, NumberedRegion};
use crate::{KernelError, Result};
/// A profile segment as SMLib gets it, with the profile segments it stands for.
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
pub(crate) struct Piece {
pub seg: Seg2,
pub numbers: Vec<usize>,
@@ -61,7 +60,6 @@ fn not_supported(what: &str) -> KernelError {
/// How an extrusion's walls lean.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
pub(crate) struct Draft {
/// Radians; positive grows the profile (as [`crate::extrude_tapered`] takes it).
pub taper: f64,
@@ -70,70 +68,16 @@ pub(crate) struct Draft {
pub foot: f64,
}
#[cfg(feature = "smlib")]
pub(crate) use imp::{
body_from_record, curve, extrude, face_section, loft, loft_rails, loft_to_point, path_curve, project_onto_face, revolve, screw, sphere, sweep,
sweep_controlled,
};
/// The error of an SMLib operation in a build without SMLib.
#[cfg(not(feature = "smlib"))]
pub(crate) fn not_built() -> KernelError {
not_supported("this build has no SMLib (feature `smlib`)")
}
#[cfg(not(feature = "smlib"))]
mod stubs {
use super::*;
pub(crate) fn body_from_record(_: &solidcraft_geom::brep::Brep, _: &str) -> Result<Body> {
Err(not_built())
}
pub(crate) fn sphere(_: Vec3, _: f64) -> Result<Body> {
Err(not_built())
}
pub(crate) fn project_onto_face(_: &Body, _: Vec3, _: &Plane, _: &[Seg2], _: Vec3) -> Result<Vec<(solidcraft_geom::brep::Curve, [f64; 2])>> {
Err(not_built())
}
pub(crate) fn face_section(_: &Body, _: Vec3, _: Vec3, _: Vec3) -> Result<Vec<(solidcraft_geom::brep::Curve, [f64; 2])>> {
Err(not_built())
}
pub(crate) fn extrude(_: &Plane, _: &[Piece], _: Option<&Region2>, _: f64, _: f64, _: f64, _: Option<Draft>) -> Result<Body> {
Err(not_built())
}
pub(crate) fn revolve(_: &Plane, _: &[Piece], _: Vec3, _: Vec3, _: f64) -> Result<Body> {
Err(not_built())
}
pub(crate) fn sweep(_: &Plane, _: &[Piece], _: &[crate::PathSeg], _: bool) -> Result<Body> {
Err(not_built())
}
pub(crate) fn loft(_: &[(Plane, Vec<Piece>)], _: usize) -> Result<Body> {
Err(not_built())
}
pub(crate) fn screw(_: &Plane, _: &[Piece], _: Vec3, _: Vec3, _: f64, _: f64) -> Result<Body> {
Err(not_built())
}
pub(crate) fn sweep_controlled(_: &Plane, _: &NumberedRegion, _: &[crate::PathSeg], _: &crate::SweepControl) -> Result<Body> {
Err(not_built())
}
pub(crate) fn loft_rails(_: &[(Plane, Vec<Piece>)], _: usize, _: &[Vec<Vec3>], _: crate::RailFit) -> Result<Body> {
Err(not_built())
}
pub(crate) fn loft_to_point(_: &Plane, _: &[Piece], _: Vec3) -> Result<Body> {
Err(not_built())
}
}
#[cfg(not(feature = "smlib"))]
pub(crate) use stubs::{
body_from_record, extrude, face_section, loft, loft_rails, loft_to_point, project_onto_face, revolve, screw, sphere, sweep, sweep_controlled,
};
/// Refuses what no SMLib sweep does, before any work.
pub(crate) fn refuse(what: &str) -> KernelError {
not_supported(&format!("SMLib {what}"))
}
#[cfg(feature = "smlib")]
mod imp {
use std::collections::BTreeSet;
@@ -954,20 +898,23 @@ mod imp {
const IDENTITY: Affine = Affine { m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], t: [0.0; 3] };
/// A sweep that twists or scales along a path with corners, by the corner model of the plain
/// sweep ([`cornered`]): each smooth run of the path is swept through stations by the same
/// laws as a smooth path (the twist and the scale running with the length along the whole
/// path, each run going on from the section the last one ended with), and the runs meet at
/// each corner in a mitre. Turning with the path, each run is carried on straight past its
/// corners as a prism of its section there (the laws hold still across a mitre: the two runs'
/// sections meet on the plane halving the corner only when both are the corner's section) and
/// cut back to that plane; only moving along the path, the runs meet at the corner's section.
/// A sweep that twists, scales, keeps an orientation direction or follows guide rails along
/// a path with corners, by the corner model of the plain sweep ([`cornered`]): each smooth run
/// of the path is swept through stations by the laws a smooth path has (the twist and the
/// scale running with the length along the whole path, each run going on from the section the
/// last one ended with), and the runs meet at each corner in a mitre. Turning with the path,
/// each run is carried on straight past its corners as a prism of its section there (the laws
/// hold still across a mitre: the two runs' sections meet on the plane halving the corner only
/// when both are the corner's section), cut back to that plane, and the section turned through
/// the corner about its binormal; only moving along the path, the runs meet at the corner's
/// section.
///
/// Defined only where the laws keep their meaning at a sharp corner: an orientation direction
/// turns the profile there by a turn of its own, not the corner's, so its sections don't meet
/// in a mitre; guide rails place each section by where they cross its plane, which jumps at the
/// corner; and round a closed path the last section has to land on the first, which a twist or
/// scale breaks. Those are refused.
/// At a corner an orientation direction turns the section as the corner does, and along the
/// next run it turns back about the path to the direction's frame, evenly along the run's
/// length, as a twist. Guide rails must turn with the path: each rail has a corner of its own
/// at every corner of the path, in the plane halving it and by the same turn, where it is cut
/// into the runs' rails. Round a closed path the last section has to land on the first: a
/// twist where the profile maps onto itself; an end scale never does.
fn cornered_controlled(
plane: &Plane,
region: &NumberedRegion,
@@ -975,18 +922,7 @@ mod imp {
runs: &[std::ops::Range<usize>],
ctl: &crate::SweepControl,
) -> Result<Body> {
let translate = match ctl.orientation {
_ if !ctl.rails.is_empty() => {
return Err(refuse("sweep along guide rails round a sharp corner of the path (the rails' sections jump there)"));
}
crate::SweepOrientation::Direction(_) => {
return Err(refuse(
"sweep kept at an orientation direction round a sharp corner of the path (its sections turn by their own turn there, not the corner's, and don't meet)",
));
}
crate::SweepOrientation::Perpendicular => false,
crate::SweepOrientation::Parallel => true,
};
let translate = ctl.orientation == crate::SweepOrientation::Parallel;
let pieces = pieces(region.loops());
let c = curves(plane, &pieces)?;
let bad = || KernelError::Invalid("a path segment has no direction".into());
@@ -997,11 +933,16 @@ mod imp {
if !(total > 1e-9 && total.is_finite()) {
return Err(KernelError::Invalid("the sweep path has no length".into()));
}
if end.dist(start) <= 1e-6 {
return Err(refuse(
"sweep with a twist or an end scale round a closed path with corners (its last section would have to land on its first)",
let tangent = |s: Option<&crate::PathSeg>| s.and_then(crate::PathSeg::tangents).ok_or_else(bad);
let (first_t, _) = tangent(path.first())?;
let (_, last_t) = tangent(path.last())?;
let closed = end.dist(start) <= 1e-6;
if closed && ctl.scale != 1.0 {
return Err(KernelError::Invalid(
"a sweep with an end scale can't go round a closed path: its last section would have to land on its first".into(),
));
}
let closing = (closed && last_t.dot(first_t) < (1e-3f64).cos()).then_some(first_t);
let reach = pieces
.iter()
.flat_map(|x| (0..=8).map(move |i| x.seg.point_at(f64::from(i) / 8.0)))
@@ -1010,15 +951,22 @@ mod imp {
let size = 4.0 * (total + reach) + 1.0;
let law = |u: f64| 1.0 + (ctl.scale - 1.0) * u;
let n0 = plane.normal().normalized().ok_or_else(|| KernelError::Invalid("profile plane".into()))?;
// Each run's rails, cut at the corners.
let rails = run_rails(path, runs, &ctl.rails, closing)?;
// The map carrying the profile as drawn onto the section where the next run starts, the
// mitre that run starts from (its point, its normal along the run, its turn) and how far
// along the path it starts.
let mut at_start = IDENTITY;
let mut entry: Option<(Vec3, Vec3, f64)> = None;
let mut entry: Option<(Vec3, Vec3, f64)> = match closing {
Some(_) if !translate => {
let (m, turn) = mitre_of(last_t, first_t)?;
Some((start, m, turn))
}
_ => None,
};
let mut before = 0.0f64;
let mut body: Option<Brep> = None;
let mut inside = Vec::new();
let mut last_map = IDENTITY;
for (ri, run) in runs.iter().enumerate() {
let segs = path.get(run.clone()).ok_or_else(|| KernelError::Invalid("path run".into()))?;
let length: f64 = lens.get(run.clone()).ok_or_else(|| KernelError::Invalid("path run".into()))?.iter().sum();
@@ -1026,20 +974,26 @@ mod imp {
let (Some(first), Some(last)) = (segs.first(), segs.last()) else { return Err(KernelError::Invalid("path run".into())) };
let ((t0, _), (_, t1)) = (first.tangents().ok_or_else(bad)?, last.tangents().ok_or_else(bad)?);
let (a0, b1) = (first.start(), last.end());
// The run's own laws on its section at the start: the twist over its length, the scale
// from where the last run left it to where this one ends.
let normal = affine_turn(&at_start, n0).normalized().ok_or_else(|| KernelError::Invalid("the sweep's section is degenerate".into()))?;
let pose = Plane::from_normal(a0, normal).ok_or_else(|| KernelError::Invalid("the sweep's section is degenerate".into()))?;
// The run's own laws on its section at the start: the twist over its length (less the
// turn back to the orientation direction after a corner), the scale from where the
// last run left it to where this one ends, the rails from corner to corner.
let back = match ctl.orientation {
crate::SweepOrientation::Direction(_) if ri > 0 => direction_offset(plane, start, first_t, a0, t0, u0, ctl, &at_start)?,
_ => 0.0,
};
let run_ctl = crate::SweepControl {
orientation: ctl.orientation,
twist: ctl.twist * (u1 - u0),
twist: ctl.twist * (u1 - u0) - back,
scale: law(u1) / law(u0),
rails: Vec::new(),
rails: rails.get(ri).cloned().unwrap_or_default(),
rail_fit: ctl.rail_fit,
};
// A station per degree the section turns, with the path and the twist.
let turn: f64 = segs.iter().map(path_turn).sum::<f64>() + run_ctl.twist.abs();
let n = ((turn / (std::f64::consts::PI / 180.0)).ceil() as usize).clamp(MIN_STATIONS, MAX_STATIONS);
let (pts, tangents) = path_samples(segs, n)?;
let normal = affine_turn(&at_start, n0).normalized().ok_or_else(|| KernelError::Invalid("the sweep's section is degenerate".into()))?;
let pose = Plane::from_normal(a0, normal).ok_or_else(|| KernelError::Invalid("the sweep's section is degenerate".into()))?;
let st = crate::sweep_path::controlled_stations(&pose, &pts, &tangents, &[], &run_ctl)?;
let maps: Vec<Affine> = st
.iter()
@@ -1052,7 +1006,10 @@ mod imp {
let mid = maps.get(maps.len() / 2).copied().unwrap_or(end_map);
inside.extend(seeds(plane, &pieces, |q| affine_apply(&mid, q)));
let mut piece = Brep::sweep_stations(&c, &maps, Ends::Capped).map_err(|e| failed("sweep", e))?;
let next = runs.get(ri + 1).and_then(|r| path.get(r.start)).map(|s| s.tangents().ok_or_else(bad).map(|t| t.0)).transpose()?;
let next = match runs.get(ri + 1).and_then(|r| path.get(r.start)) {
Some(s) => Some(tangent(Some(s))?.0),
None => closing,
};
let exit = next.map(|t2| mitre_of(t1, t2)).transpose()?;
if !translate {
// On past each mitre, the run's section there held still.
@@ -1076,7 +1033,6 @@ mod imp {
Some(b) => combined(&b, &piece, solidcraft_smlib::BooleanOp::Union, "sweep corner")?,
});
// The section at the run's end, turned through the corner onto the next run.
last_map = end_map;
at_start = end_map;
entry = None;
if let (Some(t2), Some((m, turn)), false) = (next, exit, translate) {
@@ -1087,13 +1043,112 @@ mod imp {
before += length;
}
let brep = body.ok_or_else(|| KernelError::Invalid("empty path".into()))?;
if closed {
// Back at the start, the section has to be the profile again (a whole number of its
// symmetries of twist).
let to_station = |a: &Affine| {
let v = |q: [f64; 3]| Vec3::new(q[0], q[1], q[2]);
crate::sweep_path::Station { at: affine_apply(a, start), m: [v(a.m[0]), v(a.m[1]), v(a.m[2])] }
};
for l in region.loops() {
crate::sweep_path::closing_offset(plane, l, &[to_station(&IDENTITY), to_station(&at_start)])?;
}
}
let body = body_of(&brep, "sweep")?;
let end_plane = Plane::new(affine_apply(&last_map, plane.origin), affine_turn(&last_map, plane.x), affine_turn(&last_map, plane.y))
.ok_or_else(|| KernelError::Invalid("the sweep's end section is degenerate".into()))?;
let roles = roles(&body, plane, Some(&end_plane), &seeds(plane, &pieces, |q| q), &inside, false)?;
let roles = if closed {
roles(&body, &plane.offset(size * 10.0), None, &[], &inside, false)?
} else {
let end_plane = Plane::new(affine_apply(&at_start, plane.origin), affine_turn(&at_start, plane.x), affine_turn(&at_start, plane.y))
.ok_or_else(|| KernelError::Invalid("the sweep's end section is degenerate".into()))?;
roles(&body, plane, Some(&end_plane), &seeds(plane, &pieces, |q| q), &inside, false)?
};
with_history(body, &roles, &pieces)
}
/// After a corner, how far the section at `a0` (the profile carried there by `at_start`) is
/// turned about its normal (toward the run's direction `t0`) from where the orientation direction's law puts it
/// (the law of a smooth path from `start`, leaving along `first_t`, `u0` of the way along):
/// the turn the next run takes back, as a twist. Radians, right-handed about `t0`.
#[allow(clippy::too_many_arguments)]
fn direction_offset(
plane: &Plane,
start: Vec3,
first_t: Vec3,
a0: Vec3,
t0: Vec3,
u0: f64,
ctl: &crate::SweepControl,
at_start: &Affine,
) -> Result<f64> {
let law = crate::SweepControl { twist: ctl.twist * u0, scale: 1.0 + (ctl.scale - 1.0) * u0, rails: Vec::new(), ..ctl.clone() };
// A two-station path from the start to here with the two directions: the law's section here.
let st = crate::sweep_path::controlled_stations(plane, &[start, a0], &[first_t, t0], &[], &law)?;
let want = st.last().map(|s| s.apply(plane.x)).ok_or_else(|| KernelError::Invalid("sweep path".into()))?;
let have = affine_turn(at_start, plane.x);
// About the section's normal, pointing the way the run leaves it (the twist's axis).
let n = affine_turn(at_start, plane.normal());
let axis = (if n.dot(t0) < 0.0 { n * -1.0 } else { n })
.normalized()
.ok_or_else(|| KernelError::Invalid("the sweep's section is degenerate".into()))?;
let flat = |v: Vec3| (v - axis * v.dot(axis)).normalized();
let (Some(w), Some(h)) = (flat(want), flat(have)) else {
return Err(KernelError::Invalid("the profile lies along the sweep path at a corner".into()));
};
Ok(w.cross(h).dot(axis).atan2(w.dot(h)))
}
/// The guide rails cut at the path's corners, one set per run: each rail must turn with the
/// path there, with a corner of its own in the plane halving the path's corner and by the
/// same turn about the same axis.
fn run_rails(
path: &[crate::PathSeg],
runs: &[std::ops::Range<usize>],
rails: &[Vec<Vec3>],
closing: Option<Vec3>,
) -> Result<Vec<Vec<Vec<Vec3>>>> {
if rails.is_empty() {
return Ok(vec![Vec::new(); runs.len()]);
}
if closing.is_some() {
return Err(KernelError::Invalid("guide rails can't follow a closed path with corners: a rail runs from the start to the end".into()));
}
let refused = |i: usize| {
KernelError::Invalid(format!(
"at corner {} of the path the guide rails have to turn with it: each rail needs a corner of its own there, in the plane halving the path's corner and by the same turn",
i + 1
))
};
let mut rest: Vec<Vec<Vec3>> = rails.iter().map(|r| crate::sweep_path::clean(r)).collect::<Result<_>>()?;
let mut out = Vec::with_capacity(runs.len());
for (ci, w) in runs.windows(2).enumerate() {
let (Some(a), Some(b)) = (w[0].end.checked_sub(1).and_then(|i| path.get(i)), path.get(w[1].start)) else { return Err(refused(ci)) };
let ((_, t1), (t2, _)) = (a.tangents().ok_or_else(|| refused(ci))?, b.tangents().ok_or_else(|| refused(ci))?);
let (m, turn) = mitre_of(t1, t2)?;
let at = a.end();
let k = t1.cross(t2).normalized().ok_or_else(|| refused(ci))?;
let mut here = Vec::with_capacity(rest.len());
for rail in &mut rest {
let size = rail.windows(2).map(|q| q[0].dist(q[1])).sum::<f64>().max(1e-9);
let tol = (size * 1e-6).max(1e-6);
let corner = (1..rail.len().saturating_sub(1)).find(|&j| {
let (Some(p), Some(q), Some(r)) = (rail.get(j - 1), rail.get(j), rail.get(j + 1)) else { return false };
let (Some(d1), Some(d2)) = ((*q - *p).normalized(), (*r - *q).normalized()) else { return false };
(*q - at).dot(m).abs() <= tol
&& (d1.dot(d2).clamp(-1.0, 1.0).acos() - turn).abs() <= 1e-3
&& d1.cross(d2).normalized().is_some_and(|kr| kr.dot(k) > 1.0 - 1e-6)
});
let j = corner.ok_or_else(|| refused(ci))?;
let tail = rail.split_off(j);
let mut head = std::mem::replace(rail, tail);
head.extend(rail.first().copied());
here.push(head);
}
out.push(here);
}
out.push(rest);
Ok(out)
}
/// The plane halving the corner from direction `t1` to `t2` (its unit normal) and the turn
/// there; a path that turns back on itself has none.
fn mitre_of(t1: Vec3, t2: Vec3) -> Result<(Vec3, f64)> {
-31
View File
@@ -152,10 +152,6 @@ pub(crate) fn clean(path: &[Vec3]) -> Result<Vec<Vec3>> {
Ok(out)
}
pub(crate) fn closed_path(pts: &[Vec3]) -> bool {
pts.len() > 3 && pts.first().zip(pts.last()).is_some_and(|(a, b)| a.dist(*b) <= 1e-7)
}
/// The cyclic correspondence of a loop's last section to its first. Matching whole curves
/// (rather than vertices alone) prevents sewing an incompatible twist or scale shut.
pub(crate) fn closing_offset(plane: &Plane, profile: &NumberedLoop, st: &[Station]) -> Result<usize> {
@@ -185,13 +181,6 @@ pub(crate) fn closing_offset(plane: &Plane, profile: &NumberedLoop, st: &[Statio
.ok_or_else(fail)
}
fn turn(a: Vec3, b: Vec3) -> f64 {
match (a.normalized(), b.normalized()) {
(Some(a), Some(b)) => a.dot(b).clamp(-1.0, 1.0).acos(),
_ => 0.0,
}
}
/// Rotation-minimising frames (double reflection) at the points, for the given unit tangents;
/// each station maps the first frame onto its own.
fn frames(pts: &[Vec3], tangents: &[Vec3]) -> Option<Vec<[Vec3; 3]>> {
@@ -214,26 +203,6 @@ fn frames(pts: &[Vec3], tangents: &[Vec3]) -> Option<Vec<[Vec3; 3]>> {
Some(out)
}
/// Stations: each maps a world vector at the first station to the same vector at its own,
/// optionally stretched across a corner (mitre).
fn stations(pts: &[Vec3], tangents: &[Vec3], mitre: &[Option<(Vec3, f64)>]) -> Result<Vec<Station>> {
let f = frames(pts, tangents).ok_or_else(|| KernelError::Invalid("the sweep path doubles back".into()))?;
let f0 = *f.first().ok_or_else(|| KernelError::Invalid("sweep path".into()))?;
let mut out = Vec::with_capacity(f.len());
for (i, fi) in f.iter().enumerate() {
// Rotation taking f0 to fi: columns are the images of the world axes.
let col = |e: Vec3| fi[0] * f0[0].dot(e) + fi[1] * f0[1].dot(e) + fi[2] * f0[2].dot(e);
let mut m = [col(Vec3::X), col(Vec3::Y), col(Vec3::Z)];
if let Some(Some((b, s))) = mitre.get(i) {
for c in &mut m {
*c += *b * ((s - 1.0) * c.dot(*b));
}
}
out.push(Station { at: *pts.get(i).ok_or_else(|| KernelError::Invalid("sweep path".into()))?, m });
}
Ok(out)
}
/// Frames square to the path whose turn about it keeps `d`'s projection: none where the path
/// runs along `d`.
fn frames_toward(tangents: &[Vec3], d: Vec3) -> Result<Vec<[Vec3; 3]>> {