mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel: loft rails on exact NURBS sections, the last section met where the rail ends
loft_rails.rs placed stations between truck wires. Its law now works on the
sections' exact NURBS curves (Section, from the sketch segments' record curves)
and returns the sections and stations, each a curve list a loft is skinned
through; nothing of truck is left.
Case 4056 (a keel rail lying in the bow's sketch plane): rail() anchored every
section at the rail's first crossing of the section's plane after the section
before. A rail lying in the last section's plane crosses it all along, first at
the far section, 100 mm from the bow's curve, and the loft was refused ("a guide
rail crosses section 2's plane"). A rail runs from the first section to the
last, so the last section is met where the rail ends; the sections between are
met at their first crossing, as before.
smlib_sweeps.rs's solid loft_rails keeps its own copy of the anchoring and has
the same cause; it should call loft_rails::railed.
(not built: base in progress)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
+191
-151
@@ -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<mt::Point3>;
|
||||
/// 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<CurveN<4>>,
|
||||
pub numbers: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// 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<Vec<usize>>) -> Result<Section> {
|
||||
let curves = segs.iter().map(|s| Ok(nurbs(&crate::freeform::segment(plane, s)?.0)?)).collect::<Result<Vec<_>>>()?;
|
||||
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<CurveN<4>> {
|
||||
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<Vec3>, Vec<f64>) {
|
||||
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<Vec3> {
|
||||
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<Vec3>], fit: RailFit) -> Result<Vec<mt::Wire>> {
|
||||
/// 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<Vec3>], fit: RailFit) -> Result<Vec<Vec<CurveN<4>>>> {
|
||||
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<Vec<Curve>> = wires
|
||||
.iter()
|
||||
.map(|w| {
|
||||
w.edge_iter()
|
||||
.map(|e| match e.curve() {
|
||||
mt::Curve::BSplineCurve(c) if e.orientation() => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
})
|
||||
.collect::<Option<_>>()
|
||||
.ok_or_else(|| fail("section curves"))?;
|
||||
let first = curves.first().ok_or_else(|| fail("sections"))?;
|
||||
let n = first.len();
|
||||
let knots: Vec<mt::KnotVec> = first.iter().map(|c| c.knot_vec().clone()).collect();
|
||||
let sizes: Vec<usize> = 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<Vec<Vec<Vec3>>> =
|
||||
curves.iter().map(|cs| cs.iter().map(|c| c.control_points().iter().map(|p| from_p3(*p)).collect()).collect()).collect();
|
||||
let edges: Vec<Vec<mt::Edge>> = wires.iter().map(|w| w.edge_iter().cloned().collect()).collect();
|
||||
let along = crate::build::section_parameters(&edges);
|
||||
let ctrl: Vec<Vec<Vec<Vec3>>> = 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<Rail> = rails.iter().map(|r| rail(r, start, planes, &curves, tol)).collect::<Result<_>>()?;
|
||||
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::<f64>() / 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<Vec<Option<Curve>>> = 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<Vec<CurveN<3>>> = 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<Vec3> = 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::<Result<Vec<_>>>()
|
||||
})
|
||||
.collect()
|
||||
.collect::<Result<_>>()?
|
||||
};
|
||||
let plain = |v: f64| -> Result<Vec<Vec<Vec3>>> {
|
||||
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<Vec3> = 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<Vec3>]| -> Vec<CurveN<4>> { 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<Rail> = rails.iter().map(|r| rail(r, start, sections, tol)).collect::<Result<_>>()?;
|
||||
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<Vec3> = 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<Vec<Vec3>> = 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::<Vec<_>>())).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<Curve>], tol: f64) -> Result<Rail> {
|
||||
/// 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<Rail> {
|
||||
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<Curve>], 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<Curve>], 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<Vec3>], knots: &[mt::KnotVec], g: f64) -> Result<Vec3> {
|
||||
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<Vec3> {
|
||||
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>]) -> 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<Vec3>], knots: &[mt::KnotVec], closed: bool) -> Result<mt::Wire> {
|
||||
let fail = || KernelError::Failed("loft rails: station".into());
|
||||
let n = ctrl.len();
|
||||
let mut verts: Vec<mt::Vertex> = ctrl.iter().map(|e| e.first().map(|p| mt::Vertex::new(p3(*p)))).collect::<Option<_>>().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> {
|
||||
Curve::try_new(kv.clone(), pts.iter().map(|p| p3(*p)).collect()).map_err(|e| KernelError::Failed(format!("loft rails: {e}")))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user