use crate::analytic_surface::circle_angle_to_parameter;
use crate::{interpolate_curve, NurbsCurve, NurbsSurface, Vec3};
const TAU: f64 = std::f64::consts::TAU;
const FIT_SAMPLES: usize = 33;
const REFINEMENT_SAMPLES: [usize; 4] = [FIT_SAMPLES, 65, 129, 257];
const REFINEMENT_TARGET: f64 = 0.01;
const VERIFY_SAMPLES: usize = 97;
#[derive(Clone, Debug)]
pub(super) struct EmittedFrame {
pub origin: Vec3,
pub x_axis: Vec3,
pub y_axis: Vec3,
pub axis: Vec3,
pub azimuth_sign: f64,
}
impl EmittedFrame {
fn azimuth(&self, offset: Vec3) -> Option<f64> {
let x = offset.dot(self.x_axis);
let y = offset.dot(self.y_axis);
let scale = 1.0 + offset.length();
(x.hypot(y) > 1e-12 * scale).then(|| y.atan2(x))
}
}
#[derive(Clone, Debug)]
pub(super) enum EmittedSurface {
Plane {
origin: Vec3,
x_axis: Vec3,
y_axis: Vec3,
},
Cylinder { frame: EmittedFrame, radius: f64 },
Cone {
frame: EmittedFrame,
radius: f64,
semi_angle: f64,
},
Sphere { frame: EmittedFrame, radius: f64 },
Torus {
frame: EmittedFrame,
major_radius: f64,
minor_radius: f64,
},
Spline,
}
impl EmittedSurface {
fn evaluate(&self, stored: &NurbsSurface, u: f64, v: f64) -> Result<Vec3, String> {
let radial = |frame: &EmittedFrame, rho: f64, along: f64| {
frame
.origin
.add(frame.x_axis.scale(rho * u.cos()))
.add(frame.y_axis.scale(rho * u.sin()))
.add(frame.axis.scale(along))
};
Ok(match self {
EmittedSurface::Plane {
origin,
x_axis,
y_axis,
} => origin.add(x_axis.scale(u)).add(y_axis.scale(v)),
EmittedSurface::Cylinder { frame, radius } => radial(frame, *radius, v),
EmittedSurface::Cone {
frame,
radius,
semi_angle,
} => radial(frame, radius + v * semi_angle.tan(), v),
EmittedSurface::Sphere { frame, radius } => {
radial(frame, radius * v.cos(), radius * v.sin())
}
EmittedSurface::Torus {
frame,
major_radius,
minor_radius,
} => radial(
frame,
major_radius + minor_radius * v.cos(),
minor_radius * v.sin(),
),
EmittedSurface::Spline => return stored.evaluate_extended(u, v),
})
}
fn frame(&self) -> Option<&EmittedFrame> {
match self {
EmittedSurface::Plane { .. } | EmittedSurface::Spline => None,
EmittedSurface::Cylinder { frame, .. }
| EmittedSurface::Cone { frame, .. }
| EmittedSurface::Sphere { frame, .. }
| EmittedSurface::Torus { frame, .. } => Some(frame),
}
}
fn invert(&self, point: Vec3) -> Option<(Option<f64>, f64)> {
match self {
EmittedSurface::Plane {
origin,
x_axis,
y_axis,
} => {
let offset = point.sub(*origin);
Some((Some(offset.dot(*x_axis)), offset.dot(*y_axis)))
}
EmittedSurface::Cylinder { frame, .. } | EmittedSurface::Cone { frame, .. } => {
let offset = point.sub(frame.origin);
Some((frame.azimuth(offset), offset.dot(frame.axis)))
}
EmittedSurface::Sphere { frame, .. } => {
let offset = point.sub(frame.origin);
let rho = offset.dot(frame.x_axis).hypot(offset.dot(frame.y_axis));
Some((frame.azimuth(offset), offset.dot(frame.axis).atan2(rho)))
}
EmittedSurface::Torus {
frame,
major_radius,
..
} => {
let offset = point.sub(frame.origin);
let rho = offset.dot(frame.x_axis).hypot(offset.dot(frame.y_axis));
Some((
frame.azimuth(offset),
offset.dot(frame.axis).atan2(rho - major_radius),
))
}
EmittedSurface::Spline => None,
}
}
fn periods(&self) -> (Option<f64>, Option<f64>) {
match self {
EmittedSurface::Plane { .. } | EmittedSurface::Spline => (None, None),
EmittedSurface::Cylinder { .. }
| EmittedSurface::Cone { .. }
| EmittedSurface::Sphere { .. } => (Some(TAU), None),
EmittedSurface::Torus { .. } => (Some(TAU), Some(TAU)),
}
}
}
#[derive(Clone, Debug)]
pub(super) enum EmittedCurve {
Line { start: Vec3, end: Vec3 },
Circle {
center: Vec3,
x_axis: Vec3,
y_axis: Vec3,
radius: f64,
sweep: f64,
spans: usize,
},
Spline { curve: NurbsCurve },
}
impl EmittedCurve {
pub(super) fn domain(&self) -> Result<[f64; 2], String> {
Ok(match self {
EmittedCurve::Line { .. } => [0.0, 1.0],
EmittedCurve::Circle { sweep, .. } => [0.0, *sweep],
EmittedCurve::Spline { curve } => curve.domain()?,
})
}
fn evaluate(&self, parameter: f64) -> Result<Vec3, String> {
Ok(match self {
EmittedCurve::Line { start, end } => start.add(end.sub(*start).scale(parameter)),
EmittedCurve::Circle {
center,
x_axis,
y_axis,
radius,
..
} => center
.add(x_axis.scale(radius * parameter.cos()))
.add(y_axis.scale(radius * parameter.sin())),
EmittedCurve::Spline { curve } => curve.evaluate(parameter)?,
})
}
fn parameter_is_affine_in_fraction(&self) -> bool {
matches!(
self,
EmittedCurve::Line { .. } | EmittedCurve::Spline { .. }
)
}
fn fraction_at(&self, parameter: f64) -> Result<f64, String> {
Ok(match self {
EmittedCurve::Line { .. } => parameter,
EmittedCurve::Circle { sweep, spans, .. } => {
circle_angle_to_parameter(*spans, *sweep, parameter)
}
EmittedCurve::Spline { curve } => {
let [t0, t1] = curve.domain()?;
if (t1 - t0).abs() <= f64::EPSILON {
return Err("export_step: emitted spline has an empty domain".into());
}
(parameter - t0) / (t1 - t0)
}
})
}
}
#[derive(Clone, Debug)]
pub(super) enum Pcurve2d {
Line { point: [f64; 2], vector: [f64; 2] },
Circle {
center: [f64; 2],
ref_direction: [f64; 2],
radius: f64,
},
Spline(NurbsCurve),
}
impl Pcurve2d {
fn evaluate(&self, parameter: f64) -> Result<(f64, f64), String> {
Ok(match self {
Pcurve2d::Line { point, vector } => (
point[0] + parameter * vector[0],
point[1] + parameter * vector[1],
),
Pcurve2d::Circle {
center,
ref_direction,
radius,
} => {
let (sin, cos) = parameter.sin_cos();
(
center[0] + radius * (cos * ref_direction[0] - sin * ref_direction[1]),
center[1] + radius * (cos * ref_direction[1] + sin * ref_direction[0]),
)
}
Pcurve2d::Spline(curve) => {
let point = curve.evaluate(parameter)?;
(point.x, point.y)
}
})
}
}
pub(super) struct PcurveOutcome {
pub curve: Option<Pcurve2d>,
pub deviation: f64,
}
pub(super) fn build_pcurve(
stored_surface: &NurbsSurface,
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
pcurve: &NurbsCurve,
tolerance: f64,
) -> Result<PcurveOutcome, String> {
let [s0, s1] = emitted_curve.domain()?;
if s1 <= s0 {
return Ok(PcurveOutcome {
curve: None,
deviation: f64::INFINITY,
});
}
let parameters: Vec<f64> = (0..FIT_SAMPLES)
.map(|index| s0 + (s1 - s0) * index as f64 / (FIT_SAMPLES - 1) as f64)
.collect();
let samples = sample_uv(
stored_surface,
emitted_surface,
emitted_curve,
pcurve,
¶meters,
)?;
let mut best_rejected = f64::INFINITY;
let mut accept = |candidate: Option<Pcurve2d>| -> Option<PcurveOutcome> {
let candidate = candidate?;
let deviation = verify(
stored_surface,
emitted_surface,
emitted_curve,
&candidate,
s0,
s1,
)
.ok()?;
if deviation <= tolerance {
return Some(PcurveOutcome {
curve: Some(candidate),
deviation,
});
}
best_rejected = best_rejected.min(deviation);
None
};
if let Some(points) = samples.as_ref() {
if let Some(outcome) = accept(line_candidate(points, s0, s1)) {
return Ok(outcome);
}
}
if let Some(outcome) = accept(circle_candidate(emitted_surface, emitted_curve)) {
return Ok(outcome);
}
if let Some(outcome) = accept(stored_net_candidate(
emitted_surface,
emitted_curve,
pcurve,
)?) {
return Ok(outcome);
}
let mut best: Option<(Pcurve2d, f64)> = None;
for &count in &REFINEMENT_SAMPLES {
let dense: Vec<f64> = (0..count)
.map(|index| s0 + (s1 - s0) * index as f64 / (count - 1) as f64)
.collect();
let Some(points) = sample_uv(
stored_surface,
emitted_surface,
emitted_curve,
pcurve,
&dense,
)?
else {
break;
};
let Some(candidate) = fitted_candidate(&points, &dense)? else {
break;
};
let deviation = verify(
stored_surface,
emitted_surface,
emitted_curve,
&candidate,
s0,
s1,
)?;
let improved = best.as_ref().is_none_or(|(_, best)| deviation < *best);
if improved {
best = Some((candidate, deviation));
}
if deviation <= tolerance * REFINEMENT_TARGET {
break;
}
}
if let Some((candidate, deviation)) = best {
if deviation <= tolerance {
return Ok(PcurveOutcome {
curve: Some(candidate),
deviation,
});
}
best_rejected = best_rejected.min(deviation);
}
Ok(PcurveOutcome {
curve: None,
deviation: best_rejected,
})
}
fn sample_uv(
stored_surface: &NurbsSurface,
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
pcurve: &NurbsCurve,
parameters: &[f64],
) -> Result<Option<Vec<(f64, f64)>>, String> {
if matches!(emitted_surface, EmittedSurface::Spline) {
let [q0, q1] = pcurve.domain()?;
let mut points = Vec::with_capacity(parameters.len());
for ¶meter in parameters {
let fraction = emitted_curve.fraction_at(parameter)?;
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
points.push((uv.x, uv.y));
}
return Ok(Some(points));
}
let mut azimuths: Vec<Option<f64>> = Vec::with_capacity(parameters.len());
let mut others: Vec<f64> = Vec::with_capacity(parameters.len());
for ¶meter in parameters {
let point = emitted_curve.evaluate(parameter)?;
let Some((azimuth, other)) = emitted_surface.invert(point) else {
return Ok(None);
};
azimuths.push(azimuth);
others.push(other);
}
if azimuths.iter().all(Option::is_none) {
return Ok(None);
}
let mut us: Vec<f64> = Vec::with_capacity(azimuths.len());
for index in 0..azimuths.len() {
let filled = azimuths[index].or_else(|| {
(1..azimuths.len())
.flat_map(|offset| [index.checked_sub(offset), index.checked_add(offset)])
.flatten()
.filter(|probe| *probe < azimuths.len())
.find_map(|probe| azimuths[probe])
});
us.push(filled.ok_or("export_step: pcurve sample has no resolvable azimuth")?);
}
let (u_period, v_period) = emitted_surface.periods();
unwrap_in_place(&mut us, u_period);
unwrap_in_place(&mut others, v_period);
anchor_to_stored(
stored_surface,
emitted_surface,
emitted_curve,
pcurve,
parameters,
&mut us,
&mut others,
)?;
Ok(Some(us.into_iter().zip(others).collect()))
}
fn unwrap_in_place(values: &mut [f64], period: Option<f64>) {
let Some(period) = period else {
return;
};
for index in 1..values.len() {
let step = values[index] - values[index - 1];
values[index] -= period * (step / period).round();
}
}
fn anchor_to_stored(
stored_surface: &NurbsSurface,
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
pcurve: &NurbsCurve,
parameters: &[f64],
us: &mut [f64],
vs: &mut [f64],
) -> Result<(), String> {
let (u_period, v_period) = emitted_surface.periods();
if u_period.is_none() && v_period.is_none() {
return Ok(());
}
let sign = emitted_surface
.frame()
.map(|frame| frame.azimuth_sign)
.unwrap_or(1.0);
let [stored_u0, stored_u1] = stored_surface.domain_u()?;
let [stored_v0, stored_v1] = stored_surface.domain_v()?;
let [q0, q1] = pcurve.domain()?;
let mut u_offset = 0.0;
let mut v_offset = 0.0;
for (index, ¶meter) in parameters.iter().enumerate() {
let fraction = emitted_curve.fraction_at(parameter)?;
let stored = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
if let Some(period) = u_period {
u_offset += sign * period * (stored.x - stored_u0) / (stored_u1 - stored_u0) - us[index];
}
if let Some(period) = v_period {
v_offset += period * (stored.y - stored_v0) / (stored_v1 - stored_v0) - vs[index];
}
}
let count = parameters.len().max(1) as f64;
if let Some(period) = u_period {
let shift = period * ((u_offset / count) / period).round();
for value in us.iter_mut() {
*value += shift;
}
}
if let Some(period) = v_period {
let shift = period * ((v_offset / count) / period).round();
for value in vs.iter_mut() {
*value += shift;
}
}
Ok(())
}
fn line_candidate(points: &[(f64, f64)], s0: f64, s1: f64) -> Option<Pcurve2d> {
let (u0, v0) = *points.first()?;
let (u1, v1) = *points.last()?;
let span = s1 - s0;
if span.abs() <= f64::EPSILON {
return None;
}
let vector = [(u1 - u0) / span, (v1 - v0) / span];
let magnitude = vector[0].hypot(vector[1]);
let scale = 1.0 + u0.abs().max(v0.abs());
if magnitude <= 1e-12 * scale {
return None;
}
Some(Pcurve2d::Line {
point: [u0 - s0 * vector[0], v0 - s0 * vector[1]],
vector,
})
}
fn circle_candidate(
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
) -> Option<Pcurve2d> {
let EmittedSurface::Plane {
origin,
x_axis,
y_axis,
} = emitted_surface
else {
return None;
};
let EmittedCurve::Circle {
center,
x_axis: arc_x_axis,
radius,
..
} = emitted_curve
else {
return None;
};
let offset = center.sub(*origin);
Some(Pcurve2d::Circle {
center: [offset.dot(*x_axis), offset.dot(*y_axis)],
ref_direction: [arc_x_axis.dot(*x_axis), arc_x_axis.dot(*y_axis)],
radius: *radius,
})
}
fn stored_net_candidate(
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
pcurve: &NurbsCurve,
) -> Result<Option<Pcurve2d>, String> {
if !matches!(emitted_surface, EmittedSurface::Spline)
|| !emitted_curve.parameter_is_affine_in_fraction()
{
return Ok(None);
}
let [s0, s1] = emitted_curve.domain()?;
let [q0, q1] = pcurve.domain()?;
if (q1 - q0).abs() <= f64::EPSILON || (s1 - s0).abs() <= f64::EPSILON {
return Ok(None);
}
let knots = pcurve
.knots
.iter()
.map(|knot| s0 + (s1 - s0) * (knot - q0) / (q1 - q0))
.collect();
Ok(Some(Pcurve2d::Spline(NurbsCurve::new(
pcurve.degree,
knots,
pcurve.control_points.clone(),
)?)))
}
fn fitted_candidate(
points: &[(f64, f64)],
parameters: &[f64],
) -> Result<Option<Pcurve2d>, String> {
if points.len() != parameters.len() || points.len() < 2 {
return Ok(None);
}
let coordinates: Vec<Vec3> = points
.iter()
.map(|(u, v)| Vec3::new(*u, *v, 0.0))
.collect();
Ok(interpolate_curve(&coordinates, 3, parameters)
.ok()
.map(Pcurve2d::Spline))
}
fn verify(
stored_surface: &NurbsSurface,
emitted_surface: &EmittedSurface,
emitted_curve: &EmittedCurve,
candidate: &Pcurve2d,
s0: f64,
s1: f64,
) -> Result<f64, String> {
let mut worst: f64 = 0.0;
for index in 0..VERIFY_SAMPLES {
let fraction = (index as f64 + 0.37) / VERIFY_SAMPLES as f64;
let parameter = s0 + (s1 - s0) * fraction;
let (u, v) = candidate.evaluate(parameter)?;
let on_surface = emitted_surface.evaluate(stored_surface, u, v)?;
let on_curve = emitted_curve.evaluate(parameter)?;
worst = worst.max(on_surface.sub(on_curve).length());
}
Ok(worst)
}