use super::NurbsSurface;
use crate::curve::{knot_domain, KNOT_IDENTITY_TOL};
use crate::{NurbsCurve, Vec3, Vec4};
const WEIGHT_EPS: f64 = 1e-12;
pub const MAXIMUM_FOLD_ANGLE: f64 = std::f64::consts::FRAC_PI_2;
pub const MAXIMUM_GROWTH_RATIO: f64 = 1.0;
const CROSS_SAMPLES: usize = 9;
const FOLD_STATIONS: [f64; 4] = [0.25, 0.5, 0.75, 1.0];
const EXTENT_SAMPLES: usize = 17;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum WeightSite {
Control { row: usize, column: usize },
Parameter { u: f64, v: f64 },
}
impl WeightSite {
fn describe(&self) -> String {
match self {
Self::Control { row, column } => {
format!("the extrapolated control point at row {row}, column {column}")
}
Self::Parameter { u, v } => format!("({u:.6}, {v:.6}) inside the appended span"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SurfaceSide {
UMin,
UMax,
VMin,
VMax,
}
impl SurfaceSide {
pub fn is_u(self) -> bool {
matches!(self, Self::UMin | Self::UMax)
}
pub fn is_low(self) -> bool {
matches!(self, Self::UMin | Self::VMin)
}
}
#[derive(Clone, Debug)]
pub enum ExtendRefusal {
ClosedDirection { direction_u: bool },
DegenerateIncrement { delta: f64 },
UnsupportedKnots { multiplicity: usize, expected: usize },
InvalidWeight { weight: f64, site: WeightSite },
Fold { angle: f64, limit: f64 },
ExcessiveGrowth { ratio: f64, limit: f64 },
Failed(String),
}
impl ExtendRefusal {
pub fn describe(&self) -> String {
match self {
Self::ClosedDirection { direction_u } => format!(
"the {} direction is closed — a closed direction wraps rather than extending",
if *direction_u { "u" } else { "v" }
),
Self::DegenerateIncrement { delta } => format!(
"the parameter increment {delta:e} is not a usable one — it must be positive, \
finite, and larger than the {KNOT_IDENTITY_TOL:e} band within which two knots \
are the same knot"
),
Self::UnsupportedKnots {
multiplicity,
expected,
} => format!(
"the extended end's knot multiplicity is {multiplicity}, not the clamped \
{expected} this construction needs"
),
Self::InvalidWeight { weight, site } => format!(
"the rational extension's weight is {weight:.3e} at {} — it has passed a root \
of its own weight polynomial",
site.describe()
),
Self::Fold { angle, limit } => format!(
"the extension folds back: the cross-boundary tangent turns {:.2}° \
(limit {:.2}°)",
angle.to_degrees(),
limit.to_degrees()
),
Self::ExcessiveGrowth { ratio, limit } => format!(
"the extension reaches {ratio:.3}× the surface's own extent (limit {limit:.3})"
),
Self::Failed(message) => message.clone(),
}
}
}
impl std::fmt::Display for ExtendRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.describe())
}
}
impl NurbsCurve {
pub fn extend_natural(&self, low: bool, delta_param: f64) -> Result<NurbsCurve, ExtendRefusal> {
if !delta_param.is_finite() || delta_param <= KNOT_IDENTITY_TOL {
return Err(ExtendRefusal::DegenerateIncrement { delta: delta_param });
}
let knots = extend_knots(&self.knots, self.degree, low, delta_param)?;
let strip = extrapolate_line(&self.control_points, &self.knots, self.degree, low, delta_param)?;
if let Some(bad) = strip.iter().find(|point| !(point.w > 0.0)) {
return Err(ExtendRefusal::Failed(format!(
"NurbsCurve::extend_natural: an appended weight is {:.3e}",
bad.w
)));
}
let mut points = Vec::with_capacity(self.control_points.len() + self.degree);
if low {
points.extend_from_slice(&strip);
points.extend_from_slice(&self.control_points);
} else {
points.extend_from_slice(&self.control_points);
points.extend_from_slice(&strip);
}
NurbsCurve::new(self.degree, knots, points).map_err(ExtendRefusal::Failed)
}
}
impl NurbsSurface {
pub fn extend_natural(
&self,
side: SurfaceSide,
delta_param: f64,
) -> Result<NurbsSurface, ExtendRefusal> {
if !delta_param.is_finite() || delta_param <= KNOT_IDENTITY_TOL {
return Err(ExtendRefusal::DegenerateIncrement {
delta: delta_param,
});
}
let along_u = side.is_u();
let (closed_u, closed_v) = self.closed_directions().map_err(ExtendRefusal::Failed)?;
if (along_u && closed_u) || (!along_u && closed_v) {
return Err(ExtendRefusal::ClosedDirection {
direction_u: along_u,
});
}
let (degree, knots) = if along_u {
(self.degree_u, &self.knots_u)
} else {
(self.degree_v, &self.knots_v)
};
let extended_knots = extend_knots(knots, degree, side.is_low(), delta_param)?;
let rows = self.control_points.len();
let columns = self.control_points[0].len();
let lines: Vec<Vec<Vec4>> = if along_u {
(0..columns)
.map(|column| (0..rows).map(|row| self.control_points[row][column]).collect())
.collect()
} else {
self.control_points.clone()
};
let mut strips: Vec<Vec<Vec4>> = Vec::with_capacity(lines.len());
for line in &lines {
strips.push(extrapolate_line(
line,
knots,
degree,
side.is_low(),
delta_param,
)?);
}
let mut extended: Vec<Vec<Vec4>> = if along_u {
let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows + degree);
if side.is_low() {
for step in 0..degree {
net.push(strips.iter().map(|strip| strip[step]).collect());
}
}
net.extend(self.control_points.iter().cloned());
if !side.is_low() {
for step in 0..degree {
net.push(strips.iter().map(|strip| strip[step]).collect());
}
}
net
} else {
self.control_points
.iter()
.zip(&strips)
.map(|(row, strip)| {
let mut line: Vec<Vec4> = Vec::with_capacity(columns + degree);
if side.is_low() {
line.extend_from_slice(strip);
line.extend_from_slice(row);
} else {
line.extend_from_slice(row);
line.extend_from_slice(strip);
}
line
})
.collect()
};
let appended_rows: Vec<usize> = if along_u {
if side.is_low() {
(0..degree).collect()
} else {
(extended.len() - degree..extended.len()).collect()
}
} else {
(0..extended.len()).collect()
};
let appended_columns: Vec<usize> = if along_u {
(0..columns).collect()
} else if side.is_low() {
(0..degree).collect()
} else {
(columns..columns + degree).collect()
};
for &row in &appended_rows {
for &column in &appended_columns {
let point = extended[row][column];
if !point.w.is_finite() || point.w <= WEIGHT_EPS {
return Err(ExtendRefusal::InvalidWeight {
weight: point.w,
site: WeightSite::Control { row, column },
});
}
if ![point.x, point.y, point.z].iter().all(|v| v.is_finite()) {
return Err(ExtendRefusal::InvalidWeight {
weight: f64::NAN,
site: WeightSite::Control { row, column },
});
}
}
}
let (degree_u, degree_v) = (self.degree_u, self.degree_v);
let (knots_u, knots_v) = if along_u {
(extended_knots, self.knots_v.clone())
} else {
(self.knots_u.clone(), extended_knots)
};
let net = std::mem::take(&mut extended);
let candidate = NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net)
.map_err(ExtendRefusal::Failed)?;
check_weight_field(&candidate, side, delta_param)?;
check_fold(self, &candidate, side, delta_param)?;
check_growth(self, &candidate, side, delta_param)?;
Ok(candidate)
}
}
fn extend_knots(
knots: &[f64],
degree: usize,
low: bool,
delta: f64,
) -> Result<Vec<f64>, ExtendRefusal> {
let [start, end] = knot_domain(knots, degree);
let boundary = if low { start } else { end };
let multiplicity = knots
.iter()
.filter(|knot| (**knot - boundary).abs() <= KNOT_IDENTITY_TOL)
.count();
if multiplicity != degree + 1 {
return Err(ExtendRefusal::UnsupportedKnots {
multiplicity,
expected: degree + 1,
});
}
let mut extended = Vec::with_capacity(knots.len() + degree);
if low {
extended.extend(std::iter::repeat_n(start - delta, degree + 1));
extended.extend_from_slice(&knots[1..]);
} else {
extended.extend_from_slice(&knots[..knots.len() - 1]);
extended.extend(std::iter::repeat_n(end + delta, degree + 1));
}
Ok(extended)
}
fn extrapolate_line(
line: &[Vec4],
knots: &[f64],
degree: usize,
low: bool,
delta: f64,
) -> Result<Vec<Vec4>, ExtendRefusal> {
let curve = NurbsCurve::new(degree, knots.to_vec(), line.to_vec())
.map_err(ExtendRefusal::Failed)?;
let [start, end] = knot_domain(knots, degree);
let (interval_start, interval_end) = if low {
let next = knots
.iter()
.copied()
.find(|knot| *knot > start + KNOT_IDENTITY_TOL)
.unwrap_or(end);
(start, next)
} else {
let previous = knots
.iter()
.rev()
.copied()
.find(|knot| *knot < end - KNOT_IDENTITY_TOL)
.unwrap_or(start);
(previous, end)
};
let split_at = if low { interval_end } else { interval_start };
let refined = curve
.insert_knot(split_at, degree)
.map_err(ExtendRefusal::Failed)?;
let bezier: Vec<Vec4> = if low {
refined.control_points[..=degree].to_vec()
} else {
refined.control_points[refined.control_points.len() - degree - 1..].to_vec()
};
let span = interval_end - interval_start;
if !(span > 0.0) {
return Err(ExtendRefusal::Failed(format!(
"NurbsSurface::extend_natural: terminal span [{interval_start}, {interval_end}] \
has no width"
)));
}
let ratio = if low {
-delta / span
} else {
1.0 + delta / span
};
let mut triangle: Vec<Vec<Vec4>> = Vec::with_capacity(degree + 1);
triangle.push(bezier);
for level in 1..=degree {
let previous = &triangle[level - 1];
let next: Vec<Vec4> = (0..previous.len() - 1)
.map(|index| {
previous[index]
.scale(1.0 - ratio)
.add(previous[index + 1].scale(ratio))
})
.collect();
triangle.push(next);
}
Ok(if low {
(0..degree).map(|i| triangle[degree - i][0]).collect()
} else {
(1..=degree).map(|i| triangle[i][degree - i]).collect()
})
}
fn check_weight_field(
extended: &NurbsSurface,
side: SurfaceSide,
delta: f64,
) -> Result<(), ExtendRefusal> {
let along_u = side.is_u();
let [u0, u1] = extended.domain_u().map_err(ExtendRefusal::Failed)?;
let [v0, v1] = extended.domain_v().map_err(ExtendRefusal::Failed)?;
let (from, to) = if side.is_low() {
let start = if along_u { u0 } else { v0 };
(start, start + delta)
} else {
let end = if along_u { u1 } else { v1 };
(end - delta, end)
};
for cross in cross_stations(extended, along_u)? {
for index in 0..=CROSS_SAMPLES {
let at = from + (to - from) * index as f64 / CROSS_SAMPLES as f64;
let (u, v) = if along_u { (at, cross) } else { (cross, at) };
let weight = extended
.evaluate_homogeneous(u, v)
.map_err(ExtendRefusal::Failed)?
.w;
if !weight.is_finite() || weight <= WEIGHT_EPS {
return Err(ExtendRefusal::InvalidWeight {
weight,
site: WeightSite::Parameter { u, v },
});
}
}
}
Ok(())
}
fn cross_stations(surface: &NurbsSurface, along_u: bool) -> Result<Vec<f64>, ExtendRefusal> {
let [low, high] = if along_u {
surface.domain_v().map_err(ExtendRefusal::Failed)?
} else {
surface.domain_u().map_err(ExtendRefusal::Failed)?
};
Ok((0..CROSS_SAMPLES)
.map(|index| low + (high - low) * index as f64 / (CROSS_SAMPLES - 1) as f64)
.collect())
}
fn check_fold(
original: &NurbsSurface,
extended: &NurbsSurface,
side: SurfaceSide,
delta: f64,
) -> Result<(), ExtendRefusal> {
let along_u = side.is_u();
let [start, end] = if along_u {
original.domain_u().map_err(ExtendRefusal::Failed)?
} else {
original.domain_v().map_err(ExtendRefusal::Failed)?
};
let boundary = if side.is_low() { start } else { end };
let direction = if side.is_low() { -1.0 } else { 1.0 };
let mut worst = 0.0f64;
for cross in cross_stations(original, along_u)? {
let reference = cross_tangent(original, along_u, boundary, cross)?;
let Ok(reference) = reference.normalized() else {
continue;
};
for fraction in FOLD_STATIONS {
let at = boundary + direction * delta * fraction;
let tangent = cross_tangent(extended, along_u, at, cross)?;
let Ok(tangent) = tangent.normalized() else {
return Err(ExtendRefusal::Fold {
angle: std::f64::consts::PI,
limit: MAXIMUM_FOLD_ANGLE,
});
};
let angle = reference.dot(tangent).clamp(-1.0, 1.0).acos();
worst = worst.max(angle);
}
}
if worst > MAXIMUM_FOLD_ANGLE {
return Err(ExtendRefusal::Fold {
angle: worst,
limit: MAXIMUM_FOLD_ANGLE,
});
}
Ok(())
}
fn cross_tangent(
surface: &NurbsSurface,
along_u: bool,
at: f64,
cross: f64,
) -> Result<Vec3, ExtendRefusal> {
let (u, v) = if along_u { (at, cross) } else { (cross, at) };
let derivatives = surface
.derivatives(u, v, 1)
.map_err(ExtendRefusal::Failed)?;
Ok(if along_u {
derivatives[1][0]
} else {
derivatives[0][1]
})
}
fn check_growth(
original: &NurbsSurface,
extended: &NurbsSurface,
side: SurfaceSide,
delta: f64,
) -> Result<(), ExtendRefusal> {
let along_u = side.is_u();
let [start, end] = if along_u {
original.domain_u().map_err(ExtendRefusal::Failed)?
} else {
original.domain_v().map_err(ExtendRefusal::Failed)?
};
let boundary = if side.is_low() { start } else { end };
let outside = if side.is_low() {
start - delta
} else {
end + delta
};
let evaluate = |surface: &NurbsSurface, at: f64, cross: f64| -> Result<Vec3, ExtendRefusal> {
let (u, v) = if along_u { (at, cross) } else { (cross, at) };
surface.evaluate(u, v).map_err(ExtendRefusal::Failed)
};
let mut reach = 0.0f64;
let mut extent = 0.0f64;
for cross in cross_stations(original, along_u)? {
let anchor = evaluate(original, boundary, cross)?;
reach = reach.max(evaluate(extended, outside, cross)?.sub(anchor).length());
let mut length = 0.0;
let mut previous = evaluate(original, start, cross)?;
for index in 1..EXTENT_SAMPLES {
let at = start + (end - start) * index as f64 / (EXTENT_SAMPLES - 1) as f64;
let point = evaluate(original, at, cross)?;
length += point.sub(previous).length();
previous = point;
}
extent = extent.max(length);
}
if extent <= 0.0 {
return Err(ExtendRefusal::Failed(
"NurbsSurface::extend_natural: the surface has no extent in the extended direction"
.into(),
));
}
let ratio = reach / extent;
if ratio > MAXIMUM_GROWTH_RATIO {
return Err(ExtendRefusal::ExcessiveGrowth {
ratio,
limit: MAXIMUM_GROWTH_RATIO,
});
}
Ok(())
}