1 parent
7ba2d5db47
commit
98ce6a4ee8
22 files changed
+804
-55
No files matched your search
@@ -1,8 +1,9 @@
|
||||
//! Sweeps along 3D point paths (3D sketch curves: projected, intersection and included curves).
|
||||
//! The profile follows the path with a rotation-minimising frame. Smooth paths get one B-spline
|
||||
//! wall per profile segment; paths with sharp corners (or profiles with holes) get ruled walls
|
||||
//! between stations, mitred at the corners. Sweep options (orientation, twist, end scale; see
|
||||
//! [`SweepControl`]) reshape the stations; their default leaves them exactly as described.
|
||||
//! between stations, mitred at the corners. Sweep options (orientation, twist, end scale, guide
|
||||
//! rails; see [`SweepControl`]) reshape the stations; their default leaves them exactly as
|
||||
//! described.
|
||||
|
||||
use mt::builder;
|
||||
use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
|
||||
@@ -48,21 +49,44 @@ pub enum SweepOrientation {
|
||||
Direction(Vec3),
|
||||
}
|
||||
|
||||
/// How guide rails reshape the profile at each station.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RailFit {
|
||||
/// The profile grows or shrinks evenly with its distance across to the rail (between the
|
||||
/// two rails).
|
||||
#[default]
|
||||
Scale,
|
||||
/// The profile stretches only across to the rail (between the rails), keeping its size the
|
||||
/// other way.
|
||||
Stretch,
|
||||
}
|
||||
|
||||
/// Sweep options beyond following the path. `twist` turns the profile about the path by that
|
||||
/// many radians over the path's length (right hand about the direction of travel, in proportion
|
||||
/// to the length run); `scale` is the profile's size at the end, reached linearly along the
|
||||
/// path's length, about the path's start point. The default (perpendicular, no twist, scale 1)
|
||||
/// is a plain sweep.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
/// path's length, about the path's start point. The default (perpendicular, no twist, scale 1,
|
||||
/// no rails) is a plain sweep.
|
||||
///
|
||||
/// `rails` are guide rails, as polylines from the end nearest the profile. Each station of the
|
||||
/// path is square to it; a rail meets the station's plane at one point. With one rail, the
|
||||
/// profile turns about the path so that its point where the rail starts stays on the rail, and
|
||||
/// is scaled (or stretched, see [`RailFit`]) by how far the rail is from the path compared with
|
||||
/// the start. With two rails, the profile's points where the rails start stay on both: it moves
|
||||
/// with the first rail, turns with the line between them and scales (stretches) with their
|
||||
/// distance apart. Rails decide the profile's turn and size, so they take the default
|
||||
/// orientation, no twist and scale 1, and a path without sharp corners.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SweepControl {
|
||||
pub orientation: SweepOrientation,
|
||||
pub twist: f64,
|
||||
pub scale: f64,
|
||||
pub rails: Vec<Vec<Vec3>>,
|
||||
pub rail_fit: RailFit,
|
||||
}
|
||||
|
||||
impl Default for SweepControl {
|
||||
fn default() -> Self {
|
||||
SweepControl { orientation: SweepOrientation::Perpendicular, twist: 0.0, scale: 1.0 }
|
||||
SweepControl { orientation: SweepOrientation::Perpendicular, twist: 0.0, scale: 1.0, rails: Vec::new(), rail_fit: RailFit::Scale }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +103,15 @@ impl SweepControl {
|
||||
*self == SweepControl::default()
|
||||
}
|
||||
|
||||
/// Whether the profile changes along the path (it twists or scales).
|
||||
/// Whether the profile changes along the path (it twists, scales or follows rails).
|
||||
fn varies(&self) -> bool {
|
||||
self.twist != 0.0 || self.scale != 1.0
|
||||
self.twist != 0.0 || self.scale != 1.0 || !self.rails.is_empty()
|
||||
}
|
||||
|
||||
/// Refuses options out of range: a twist beyond [`Self::MAX_TWIST`], a scale outside
|
||||
/// [`Self::MIN_SCALE`]…[`Self::MAX_SCALE`], a direction that is not finite or has no length.
|
||||
/// [`Self::MIN_SCALE`]…[`Self::MAX_SCALE`], a direction that is not finite or has no length,
|
||||
/// more than two rails, a rail that is not 2…100000 finite points, and rails with any other
|
||||
/// orientation, a twist or an end scale.
|
||||
pub fn check(&self) -> Result<()> {
|
||||
if !(self.twist.is_finite() && self.twist.abs() <= Self::MAX_TWIST) {
|
||||
return Err(KernelError::Invalid("the sweep twist must be within four turns (1440 deg)".into()));
|
||||
@@ -98,6 +124,17 @@ impl SweepControl {
|
||||
{
|
||||
return Err(KernelError::Invalid("the sweep orientation direction must be a finite, non-zero vector".into()));
|
||||
}
|
||||
if self.rails.len() > 2 {
|
||||
return Err(KernelError::Invalid("a sweep takes one or two guide rails".into()));
|
||||
}
|
||||
for rail in &self.rails {
|
||||
clean(rail).map_err(|_| KernelError::Invalid("a guide rail needs 2…100000 finite points along its length".into()))?;
|
||||
}
|
||||
if !self.rails.is_empty() && (self.orientation != SweepOrientation::Perpendicular || self.twist != 0.0 || self.scale != 1.0) {
|
||||
return Err(KernelError::Invalid(
|
||||
"guide rails set the profile's turn and size along the path: leave the orientation, twist and end scale at their defaults".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -228,6 +265,9 @@ fn frames_toward(tangents: &[Vec3], d: Vec3) -> Result<Vec<[Vec3; 3]>> {
|
||||
/// turned by the orientation's frames), optionally stretched across a corner (mitre). Twist and
|
||||
/// scale grow with the length run along `pts`.
|
||||
fn controlled_stations(plane: &Plane, pts: &[Vec3], tangents: &[Vec3], mitre: &[Option<(Vec3, f64)>], ctl: &SweepControl) -> Result<Vec<Station>> {
|
||||
if !ctl.rails.is_empty() {
|
||||
return railed_stations(pts, tangents, mitre, ctl);
|
||||
}
|
||||
let bad = || KernelError::Invalid("sweep path".into());
|
||||
let t0 = *tangents.first().ok_or_else(bad)?;
|
||||
let f: Vec<[Vec3; 3]> = match ctl.orientation {
|
||||
@@ -284,6 +324,113 @@ fn controlled_stations(plane: &Plane, pts: &[Vec3], tangents: &[Vec3], mitre: &[
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Where a rail polyline crosses the plane through `p` square to `t`: the first crossing at or
|
||||
/// after `from` (segment index and fraction along it), with the position it was found at, so a
|
||||
/// walk along the path finds each rail point after the last. Within `tol` of the plane a rail's
|
||||
/// first and last points count as on it (a rail drawn to end where the path ends).
|
||||
fn rail_crossing(rail: &[Vec3], p: Vec3, t: Vec3, from: (usize, f64), tol: f64) -> Option<(Vec3, (usize, f64))> {
|
||||
let f = |q: Vec3| (q - p).dot(t);
|
||||
let (k0, u0) = from;
|
||||
if k0 == 0 && u0 == 0.0 && rail.first().is_some_and(|q| f(*q).abs() <= tol) {
|
||||
return rail.first().map(|q| (*q, (0, 0.0)));
|
||||
}
|
||||
for k in k0..rail.len().saturating_sub(1) {
|
||||
let (a, b) = (*rail.get(k)?, *rail.get(k + 1)?);
|
||||
let a = if k == k0 { a.lerp(b, u0) } else { a };
|
||||
let (fa, fb) = (f(a), f(b));
|
||||
if fa == 0.0 && (k != k0 || u0 == 0.0) {
|
||||
return Some((a, (k, if k == k0 { u0 } else { 0.0 })));
|
||||
}
|
||||
if (fa < 0.0) != (fb < 0.0) || fb == 0.0 {
|
||||
let s = fa / (fa - fb);
|
||||
let q = a.lerp(b, s);
|
||||
// The fraction along the whole segment, for the next walk.
|
||||
let base = if k == k0 { u0 } else { 0.0 };
|
||||
return Some((q, (k, base + (1.0 - base) * s)));
|
||||
}
|
||||
}
|
||||
let last = rail.last()?;
|
||||
(f(*last).abs() <= tol).then(|| (*last, (rail.len().saturating_sub(2), 1.0)))
|
||||
}
|
||||
|
||||
/// Stations shaped by guide rails (see [`SweepControl`]): each is square to the path, turned
|
||||
/// about it to follow the rail (the line between the rails) and scaled or stretched by the
|
||||
/// rail's distance across (the rails' distance apart) against the start's. A rail has to meet
|
||||
/// the plane of every station, in order along its length.
|
||||
fn railed_stations(pts: &[Vec3], tangents: &[Vec3], mitre: &[Option<(Vec3, f64)>], ctl: &SweepControl) -> Result<Vec<Station>> {
|
||||
// A polyline path's points bend it a little (its arcs' chords); a corner is a bend past
|
||||
// [`CORNER`], where the stations would have to be mitred.
|
||||
let corner = 1.0 / (CORNER / 2.0).cos();
|
||||
if mitre.iter().flatten().any(|(_, s)| *s > corner + 1e-12) {
|
||||
return Err(KernelError::Invalid("not supported yet: guide rails along a path with sharp corners".into()));
|
||||
}
|
||||
let rails: Vec<Vec<Vec3>> = ctl.rails.iter().map(|r| clean(r)).collect::<Result<_>>()?;
|
||||
let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
|
||||
let tol = 1e-3f64.max(1e-6 * len);
|
||||
let mut cursors = vec![(0usize, 0.0f64); rails.len()];
|
||||
// At each station: the anchor the profile moves with, and the vector across that turns and
|
||||
// sizes it (to the rail, or from the first rail to the second).
|
||||
let mut across = Vec::with_capacity(pts.len());
|
||||
for (i, (p, t)) in pts.iter().zip(tangents).enumerate() {
|
||||
let mut hits = Vec::with_capacity(rails.len());
|
||||
for (rail, cursor) in rails.iter().zip(cursors.iter_mut()) {
|
||||
let Some((q, at)) = rail_crossing(rail, *p, *t, *cursor, tol) else {
|
||||
let done = 100.0 * i as f64 / pts.len().saturating_sub(1).max(1) as f64;
|
||||
return Err(KernelError::Invalid(format!(
|
||||
"a guide rail does not reach the path's plane {done:.0}% of the way along it: the rail has to run the whole length of the path"
|
||||
)));
|
||||
};
|
||||
*cursor = at;
|
||||
hits.push(q);
|
||||
}
|
||||
let (anchor, v) = match hits[..] {
|
||||
[r] => (*p, r - *p),
|
||||
[a, b] => (a, b - a),
|
||||
_ => return Err(KernelError::Invalid("a sweep takes one or two guide rails".into())),
|
||||
};
|
||||
across.push((anchor, v - *t * v.dot(*t)));
|
||||
}
|
||||
let (Some(&(a0, d0)), Some(&t0)) = (across.first(), tangents.first()) else { return Err(KernelError::Invalid("sweep path".into())) };
|
||||
let s0 = *pts.first().ok_or_else(|| KernelError::Invalid("sweep path".into()))?;
|
||||
let near = |d: Vec3| d.len() <= 1e-6 * (1.0 + len);
|
||||
if near(d0) {
|
||||
return Err(KernelError::Invalid(if rails.len() == 1 {
|
||||
"the guide rail starts on the path: it has to start away from the path, at the point of the profile it guides".into()
|
||||
} else {
|
||||
"the two guide rails start at the same point".into()
|
||||
}));
|
||||
}
|
||||
let (u0, w0) = {
|
||||
let u = d0.normalized().ok_or_else(|| KernelError::Invalid("guide rail".into()))?;
|
||||
(u, t0.cross(u))
|
||||
};
|
||||
let mut out = Vec::with_capacity(pts.len());
|
||||
for ((anchor, d), t) in across.iter().zip(tangents) {
|
||||
if near(*d) {
|
||||
return Err(KernelError::Invalid(if rails.len() == 1 {
|
||||
format!("the guide rail meets the path near ({:.3}, {:.3}, {:.3}): the profile would shrink to nothing", anchor.x, anchor.y, anchor.z)
|
||||
} else {
|
||||
format!("the guide rails meet near ({:.3}, {:.3}, {:.3}): the profile would shrink to nothing", anchor.x, anchor.y, anchor.z)
|
||||
}));
|
||||
}
|
||||
let k = d.len() / d0.len();
|
||||
let k_other = match ctl.rail_fit {
|
||||
RailFit::Scale => k,
|
||||
RailFit::Stretch => 1.0,
|
||||
};
|
||||
let u = d.normalized().ok_or_else(|| KernelError::Invalid("guide rail".into()))?;
|
||||
let w = t.cross(u);
|
||||
// Along the path stays along it; across to the rail turns onto the rail and sizes with
|
||||
// it; the third way follows, scaled evenly or kept.
|
||||
let map = |v: Vec3| *t * v.dot(t0) + u * (k * v.dot(u0)) + w * (k_other * v.dot(w0));
|
||||
let m = [map(Vec3::X), map(Vec3::Y), map(Vec3::Z)];
|
||||
let st = Station { at: Vec3::default(), m };
|
||||
// The anchor's start position lands on the anchor.
|
||||
out.push(Station { at: *anchor - st.apply(a0 - s0), m });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The volume a profile of `area` on `plane` sweeps through the stations: each station's
|
||||
/// section area times how squarely the path crosses it, summed along the path (trapezoids).
|
||||
fn swept_volume(plane: &Plane, area: f64, st: &[Station], tangents: &[Vec3]) -> f64 {
|
||||
|
||||
Reference in new issue
Block a user