mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
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:
@@ -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])))
|
||||
}
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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]>> {
|
||||
|
||||
Reference in New Issue
Block a user