use super::*;
pub const MAX_JOINT_TANGENT_BREAK: f64 = 0.02;
const MAX_JOINT_CURVATURE_BREAK: f64 = 1e-9;
const MAX_PLANARITY_DEVIATION: f64 = 1e-7;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JointContinuity {
G0,
G1,
G2,
}
impl JointContinuity {
pub fn label(self) -> &'static str {
match self {
JointContinuity::G0 => "G0",
JointContinuity::G1 => "G1",
JointContinuity::G2 => "G2",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PathJoint {
pub before: usize,
pub after: usize,
pub tangent_break: f64,
pub curvature_break: f64,
pub continuity: JointContinuity,
pub closing: bool,
}
#[derive(Debug, Clone)]
pub struct SweepPath {
pub curves: Vec<NurbsCurve>,
pub names: Vec<String>,
pub closed: bool,
pub planar: Option<Vec3>,
pub joints: Vec<PathJoint>,
pub straight: bool,
pub screw_axes: Vec<Option<Vec3>>,
}
impl SweepPath {
pub fn new(curves: Vec<NurbsCurve>, names: Vec<String>) -> Result<Self, KernelRefusal> {
if curves.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chain_empty",
"sweepSolid: path chain is empty",
));
}
let mut endpoints = Vec::with_capacity(curves.len());
for curve in &curves {
let [t0, t1] = curve.domain().or_refuse(KernelStage::Classify, "domain")?;
endpoints.push((curve.evaluate(t0).or_refuse(KernelStage::Classify, "evaluate")?, curve.evaluate(t1).or_refuse(KernelStage::Classify, "evaluate")?));
}
let first_start = endpoints[0].0;
let scale = endpoints
.iter()
.flat_map(|(a, b)| [a, b])
.map(|point| point.sub(first_start).length())
.fold(1.0_f64, f64::max);
let join_tolerance = 1e-5 * scale;
let mut samples = Vec::with_capacity(16 * curves.len() + 1);
for curve in &curves {
let [t0, t1] = curve.domain().or_refuse(KernelStage::Classify, "domain")?;
for index in 0..16 {
samples.push(curve.evaluate(t0 + (t1 - t0) * index as f64 / 16.0).or_refuse(KernelStage::Classify, "evaluate")?);
}
}
samples.push(endpoints[curves.len() - 1].1);
let travel: f64 = samples
.windows(2)
.map(|pair| pair[1].sub(pair[0]).length())
.sum();
let closed = travel > join_tolerance
&& endpoints[curves.len() - 1]
.1
.sub(first_start)
.length()
<= join_tolerance;
let planar = match crate::polygon::newell_normal(&samples).normalized() {
Ok(normal) => {
let deviation = samples
.iter()
.map(|point| point.sub(first_start).dot(normal).abs())
.fold(0.0_f64, f64::max);
(deviation <= MAX_PLANARITY_DEVIATION * scale).then_some(normal)
}
Err(_) => None,
};
let mut joints = Vec::with_capacity(curves.len());
for index in 1..curves.len() {
joints.push(Self::joint(&curves, index - 1, index, false)?);
}
if closed {
joints.push(Self::joint(&curves, curves.len() - 1, 0, true)?);
}
let straight = curves
.iter()
.all(|curve| curve.degree == 1 && curve.control_points.len() == 2);
let screw_axes = vec![None; curves.len()];
Ok(Self {
curves,
names,
closed,
planar,
joints,
straight,
screw_axes,
})
}
pub fn with_screw_axes(mut self, axes: Vec<Option<Vec3>>) -> Result<Self, KernelRefusal> {
if axes.len() != self.curves.len() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"screw_axes_count",
format!(
"sweepSolid: {} screw axes for a {}-segment path",
axes.len(),
self.curves.len()
),
));
}
self.screw_axes = axes
.into_iter()
.enumerate()
.map(|(index, axis)| {
axis.map(|axis| {
axis.normalized().map_err(|_| {
KernelRefusal::input(
KernelStage::Collect,
"screw_axis",
format!(
"sweepSolid: path segment '{}' has a degenerate screw axis",
self.name(index)
),
)
})
})
.transpose()
})
.collect::<Result<_, _>>()?;
Ok(self)
}
pub fn from_curves(curves: &[NurbsCurve], names: &[String]) -> Result<Self, KernelRefusal> {
Self::new(curves.to_vec(), names.to_vec())
}
fn joint(
curves: &[NurbsCurve],
before: usize,
after: usize,
closing: bool,
) -> Result<PathJoint, KernelRefusal> {
let arriving = Self::frame_at(&curves[before], true, before, "end")?;
let departing = Self::frame_at(&curves[after], false, after, "start")?;
let tangent_break = arriving.0.dot(departing.0).clamp(-1.0, 1.0).acos();
let curvature_break = departing.1.sub(arriving.1).length();
let curvature_bar = MAX_JOINT_CURVATURE_BREAK
* (1.0 + arriving.1.length() + departing.1.length());
let continuity = if tangent_break > MAX_JOINT_TANGENT_BREAK {
JointContinuity::G0
} else if curvature_break > curvature_bar {
JointContinuity::G1
} else {
JointContinuity::G2
};
Ok(PathJoint {
before,
after,
tangent_break,
curvature_break,
continuity,
closing,
})
}
fn frame_at(
curve: &NurbsCurve,
at_end: bool,
index: usize,
which: &str,
) -> Result<(Vec3, Vec3), KernelRefusal> {
let [t0, t1] = curve.domain().or_refuse(KernelStage::Classify, "domain")?;
let parameter = if at_end { t1 } else { t0 };
let derivatives = curve.derivatives(parameter, 2).or_refuse(KernelStage::Classify, "derivatives")?;
let speed = derivatives[1].length();
let tangent = derivatives[1].normalized().map_err(|_| {
KernelRefusal::input(
KernelStage::Classify,
"path_tangent",
format!("sweepSolid: path tangent is degenerate at the {which} of segment {index}"),
)
})?;
let curvature = derivatives[1]
.cross(derivatives[2])
.cross(derivatives[1])
.scale(1.0 / speed.powi(4));
Ok((tangent, curvature))
}
pub fn len(&self) -> usize {
self.curves.len()
}
pub fn is_empty(&self) -> bool {
self.curves.is_empty()
}
pub fn name(&self, index: usize) -> &str {
self.names
.get(index)
.map(String::as_str)
.unwrap_or("<unnamed>")
}
pub fn corner(&self) -> Option<&PathJoint> {
self.joints
.iter()
.find(|joint| joint.continuity == JointContinuity::G0)
}
pub fn cornered_polyline(&self) -> bool {
self.straight && self.corner().is_some()
}
}