use crate::curve::{NurbsCurve, Vec4};
use crate::fit::interpolate_curve;
use crate::surface::NurbsSurface;
use crate::Vec3;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImageCurveTier {
Affine,
Iso,
Approximated,
}
#[derive(Clone, Debug)]
pub struct ImageCurve {
pub curve: NurbsCurve,
pub t0: f64,
pub t1: f64,
pub tier: ImageCurveTier,
pub deviation: f64,
}
const VERIFY_SAMPLES: usize = 24;
const PROBES_PER_INTERVAL: usize = 5;
const MAX_ROUNDS: usize = 14;
const MAX_SAMPLES: usize = 4096;
pub fn affine_image_curve(
sheet: &NurbsSurface,
pcurve: &NurbsCurve,
) -> Result<NurbsCurve, String> {
let [u0, _] = sheet.domain_u()?;
let [v0, _] = sheet.domain_v()?;
let frame = sheet.derivatives(u0, v0, 1)?;
let origin = frame[0][0];
let du = frame[1][0];
let dv = frame[0][1];
let control_points = pcurve
.control_points
.iter()
.map(|control| {
let position = origin
.scale(control.w)
.add(du.scale(control.x - control.w * u0))
.add(dv.scale(control.y - control.w * v0));
Vec4 {
x: position.x,
y: position.y,
z: position.z,
w: control.w,
}
})
.collect();
NurbsCurve::new(pcurve.degree, pcurve.knots.clone(), control_points)
}
fn compose(surface: &NurbsSurface, pcurve: &NurbsCurve, q: f64) -> Result<Vec3, String> {
let uv = pcurve.evaluate(q)?;
surface.evaluate_extended(uv.x, uv.y)
}
fn sweep_deviation(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
curve: &NurbsCurve,
t0: f64,
t1: f64,
count: usize,
) -> Result<f64, String> {
let [q0, q1] = pcurve.domain()?;
let mut worst = 0.0f64;
for index in 0..=count {
let fraction = index as f64 / count as f64;
let target = compose(surface, pcurve, q0 + (q1 - q0) * fraction)?;
let value = curve.evaluate(t0 + (t1 - t0) * fraction)?;
worst = worst.max(value.sub(target).length());
}
Ok(worst)
}
fn iso_line(pcurve: &NurbsCurve, eps_u: f64, eps_v: f64) -> Result<Option<(bool, f64, f64)>, String> {
let [q0, q1] = pcurve.domain()?;
let first = pcurve.evaluate(q0)?;
let last = pcurve.evaluate(q1)?;
let constant_u = (first.x - last.x).abs() <= eps_u;
let constant_v = (first.y - last.y).abs() <= eps_v;
if constant_u == constant_v {
return Ok(None);
}
let span = q1 - q0;
for index in 1..8 {
let fraction = index as f64 / 8.0;
let uv = pcurve.evaluate(q0 + span * fraction)?;
let (held, varying, expected, eps_held, eps_vary) = if constant_u {
(
uv.x - first.x,
uv.y,
first.y + (last.y - first.y) * fraction,
eps_u,
eps_v,
)
} else {
(
uv.y - first.y,
uv.x,
first.x + (last.x - first.x) * fraction,
eps_v,
eps_u,
)
};
if held.abs() > eps_held || (varying - expected).abs() > eps_vary {
return Ok(None);
}
}
Ok(Some(if constant_u {
(true, first.y, last.y)
} else {
(false, first.x, last.x)
}))
}
fn iso_constant(pcurve: &NurbsCurve, constant_u: bool) -> Result<f64, String> {
let [q0, q1] = pcurve.domain()?;
let first = pcurve.evaluate(q0)?;
let last = pcurve.evaluate(q1)?;
Ok(if constant_u {
0.5 * (first.x + last.x)
} else {
0.5 * (first.y + last.y)
})
}
fn iso_epsilons(surface: &NurbsSurface) -> Result<(f64, f64), String> {
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
Ok((1e-7 * (u1 - u0).abs(), 1e-7 * (v1 - v0).abs()))
}
fn seed_parameters(pcurve: &NurbsCurve) -> Result<Vec<f64>, String> {
let [q0, q1] = pcurve.domain()?;
let span = q1 - q0;
let mut parameters = vec![q0, q1];
for knot in &pcurve.knots {
if *knot > q0 && *knot < q1 {
parameters.push(*knot);
}
}
for index in 1..8 {
parameters.push(q0 + span * index as f64 / 8.0);
}
parameters.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
parameters.dedup_by(|a, b| (*a - *b).abs() <= span.abs() * 1e-9);
Ok(parameters)
}
fn fit_and_measure(
surfaces: &[&NurbsSurface],
pcurve: &NurbsCurve,
parameters: &[f64],
which: usize,
) -> Result<(NurbsCurve, f64, Vec<f64>), String> {
let surface = surfaces[which];
let points = parameters
.iter()
.map(|q| compose(surface, pcurve, *q))
.collect::<Result<Vec<_>, String>>()?;
let degree = 3.min(points.len() - 1);
let curve = interpolate_curve(&points, degree, parameters)?;
let mut worst = 0.0f64;
let mut per_interval = Vec::with_capacity(parameters.len() - 1);
for window in parameters.windows(2) {
let (a, b) = (window[0], window[1]);
let mut local = 0.0f64;
for probe in 1..=PROBES_PER_INTERVAL {
let q = a + (b - a) * probe as f64 / (PROBES_PER_INTERVAL + 1) as f64;
let target = compose(surface, pcurve, q)?;
local = local.max(curve.evaluate(q)?.sub(target).length());
}
worst = worst.max(local);
per_interval.push(local);
}
Ok((curve, worst, per_interval))
}
fn approximate(
surfaces: &[&NurbsSurface],
pcurve: &NurbsCurve,
tolerance: f64,
site: &str,
) -> Result<Vec<ImageCurve>, String> {
let [q0, q1] = pcurve.domain()?;
let mut parameters = seed_parameters(pcurve)?;
let mut best = f64::INFINITY;
for _ in 0..MAX_ROUNDS {
let mut fits = Vec::with_capacity(surfaces.len());
let mut worst = 0.0f64;
let mut per_interval = vec![0.0f64; parameters.len() - 1];
for which in 0..surfaces.len() {
let (curve, sheet_worst, sheet_intervals) =
fit_and_measure(surfaces, pcurve, ¶meters, which)?;
worst = worst.max(sheet_worst);
for (slot, value) in per_interval.iter_mut().zip(&sheet_intervals) {
*slot = slot.max(*value);
}
fits.push(curve);
}
best = best.min(worst);
if worst <= tolerance {
return Ok(fits
.into_iter()
.map(|curve| ImageCurve {
curve,
t0: q0,
t1: q1,
tier: ImageCurveTier::Approximated,
deviation: worst,
})
.collect());
}
let mut refined = Vec::with_capacity(parameters.len() * 2);
for (index, window) in parameters.windows(2).enumerate() {
refined.push(window[0]);
if per_interval[index] > tolerance {
refined.push(0.5 * (window[0] + window[1]));
}
}
refined.push(parameters[parameters.len() - 1]);
if refined.len() == parameters.len() || refined.len() > MAX_SAMPLES {
break;
}
parameters = refined;
}
Err(format!(
"{site}: the 3D image of a general pcurve could not be fitted to tolerance \
(worst off-node deviation {best:.3e} > {tolerance:.3e} after {} samples) — refusing",
parameters.len()
))
}
pub fn image_curve(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
tolerance: f64,
site: &str,
) -> Result<ImageCurve, String> {
let [q0, q1] = pcurve.domain()?;
if !(q1 - q0).is_finite() || (q1 - q0).abs() <= 0.0 {
return Err(format!("{site}: pcurve has an empty parameter domain"));
}
if surface.is_affine()? {
let curve = affine_image_curve(surface, pcurve)?;
let deviation = sweep_deviation(surface, pcurve, &curve, q0, q1, VERIFY_SAMPLES)?;
if deviation <= tolerance {
return Ok(ImageCurve {
curve,
t0: q0,
t1: q1,
tier: ImageCurveTier::Affine,
deviation,
});
}
}
let (eps_u, eps_v) = iso_epsilons(surface)?;
if let Some((constant_u, start, end)) = iso_line(pcurve, eps_u, eps_v)? {
let constant = iso_constant(pcurve, constant_u)?;
let curve = if constant_u {
surface.iso_curve_u(constant)?
} else {
surface.iso_curve_v(constant)?
};
let deviation = sweep_deviation(surface, pcurve, &curve, start, end, VERIFY_SAMPLES)?;
if deviation <= tolerance {
return Ok(ImageCurve {
curve,
t0: start,
t1: end,
tier: ImageCurveTier::Iso,
deviation,
});
}
}
Ok(approximate(&[surface], pcurve, tolerance, site)?
.pop()
.expect("one surface in, one image out"))
}
pub fn image_curve_pair(
first: &NurbsSurface,
second: &NurbsSurface,
pcurve: &NurbsCurve,
tolerance: f64,
site: &str,
) -> Result<(ImageCurve, ImageCurve), String> {
let [q0, q1] = pcurve.domain()?;
if !(q1 - q0).is_finite() || (q1 - q0).abs() <= 0.0 {
return Err(format!("{site}: pcurve has an empty parameter domain"));
}
if first.is_affine()? && second.is_affine()? {
let a = affine_image_curve(first, pcurve)?;
let b = affine_image_curve(second, pcurve)?;
let deviation = sweep_deviation(first, pcurve, &a, q0, q1, VERIFY_SAMPLES)?
.max(sweep_deviation(second, pcurve, &b, q0, q1, VERIFY_SAMPLES)?);
if deviation <= tolerance {
return Ok((
ImageCurve {
curve: a,
t0: q0,
t1: q1,
tier: ImageCurveTier::Affine,
deviation,
},
ImageCurve {
curve: b,
t0: q0,
t1: q1,
tier: ImageCurveTier::Affine,
deviation,
},
));
}
}
let (eps_u, eps_v) = iso_epsilons(first)?;
if let Some((constant_u, start, end)) = iso_line(pcurve, eps_u, eps_v)? {
let constant = iso_constant(pcurve, constant_u)?;
let (a, b) = if constant_u {
(first.iso_curve_u(constant)?, second.iso_curve_u(constant)?)
} else {
(first.iso_curve_v(constant)?, second.iso_curve_v(constant)?)
};
let deviation = sweep_deviation(first, pcurve, &a, start, end, VERIFY_SAMPLES)?
.max(sweep_deviation(second, pcurve, &b, start, end, VERIFY_SAMPLES)?);
if deviation <= tolerance {
return Ok((
ImageCurve {
curve: a,
t0: start,
t1: end,
tier: ImageCurveTier::Iso,
deviation,
},
ImageCurve {
curve: b,
t0: start,
t1: end,
tier: ImageCurveTier::Iso,
deviation,
},
));
}
}
let mut images = approximate(&[first, second], pcurve, tolerance, site)?;
let second_image = images.pop().expect("two surfaces in, two images out");
let first_image = images.pop().expect("two surfaces in, two images out");
Ok((first_image, second_image))
}