use crate::{
model_scale, KernelTolerances, NurbsCurve, NurbsSurface, Vec3, Vec4, VERTEX_MATCH_FLOOR,
};
#[path = "coons/compat.rs"]
pub(crate) mod compat;
#[path = "coons/ribbons.rs"]
mod ribbons;
pub use compat::CompatWork;
pub use ribbons::coons_with_ribbons;
const CORNER_IDENTITY_K: f64 = 2e-5;
pub(crate) const WEIGHT_CLOSURE_BAND: f64 = 1e-9;
const BOUNDARY_SAMPLES: usize = 32;
#[derive(Clone, Debug, PartialEq)]
pub enum CoonsRefusal {
DegenerateBoundary {
boundary: usize,
extent: f64,
band: f64,
},
CornerGap {
corner: usize,
gap: f64,
band: f64,
},
CornerWeightClosure { ratio: f64, band: f64 },
NonPositiveWeight {
row: usize,
column: usize,
weight: f64,
},
Incompatible { direction: char, detail: String },
RationalRibbon {
boundary: usize,
weight_spread: f64,
},
RibbonCornerTangent {
corner: usize,
gap: f64,
band: f64,
},
RibbonPairing { direction: char, prescribed: usize },
Geometry(String),
}
impl std::fmt::Display for CoonsRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DegenerateBoundary {
boundary,
extent,
band,
} => write!(
formatter,
"coons: boundary {boundary} is degenerate — extent {extent:.3e} is inside the \
identity band {band:.3e}"
),
Self::CornerGap { corner, gap, band } => write!(
formatter,
"coons: boundary {corner} does not meet boundary {} — gap {gap:.3e} exceeds the \
vertex identity band {band:.3e}",
(corner + 1) % 4
),
Self::CornerWeightClosure { ratio, band } => write!(
formatter,
"coons: rational corner weights do not close — the loop multiplies weights by \
{ratio:.12} (band {band:.3e} about 1)"
),
Self::NonPositiveWeight {
row,
column,
weight,
} => write!(
formatter,
"coons: blended control point ({row}, {column}) has non-positive weight \
{weight:.3e} — the patch would pass through infinity"
),
Self::Incompatible { direction, detail } => {
write!(formatter, "coons: {direction} direction incompatible — {detail}")
}
Self::RationalRibbon {
boundary,
weight_spread,
} => write!(
formatter,
"coons: ribbon constructor is polynomial-only — boundary {boundary} is rational \
(max |w - 1| = {weight_spread:.3e})"
),
Self::RibbonCornerTangent { corner, gap, band } => write!(
formatter,
"coons: the ribbon at corner {corner} contradicts the boundary tangent there — \
gap {gap:.3e} exceeds {band:.3e}"
),
Self::RibbonPairing {
direction,
prescribed,
} => write!(
formatter,
"coons: the {direction} direction has {prescribed} of its 2 ribbons — give both \
boundaries of a direction a ribbon, or neither"
),
Self::Geometry(message) => formatter.write_str(message),
}
}
}
impl From<CoonsRefusal> for String {
fn from(refusal: CoonsRefusal) -> Self {
refusal.to_string()
}
}
#[derive(Clone, Debug)]
pub struct CoonsReport {
pub reversed: [bool; 4],
pub corner_gaps: [f64; 4],
pub corner_band: f64,
pub weight_scales: [f64; 4],
pub weight_closure: f64,
pub work: [CompatWork; 4],
pub degree_u: usize,
pub degree_v: usize,
pub control_rows: usize,
pub control_columns: usize,
pub min_weight: f64,
pub boundary_residual: f64,
pub ribbons: Option<RibbonReport>,
}
#[derive(Clone, Debug)]
pub struct RibbonReport {
pub prescribed: [bool; 4],
pub corner_tangent_gap: f64,
pub twist_discrepancy: f64,
pub cross_derivative_residual: f64,
pub cross_derivative_scale: f64,
}
pub fn coons_bilinear(boundaries: [NurbsCurve; 4]) -> Result<NurbsSurface, CoonsRefusal> {
coons_bilinear_reported(boundaries).map(|(surface, _)| surface)
}
pub fn coons_bilinear_reported(
boundaries: [NurbsCurve; 4],
) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
let prepared = prepare(&boundaries)?;
build_bilinear(prepared)
}
fn homogeneous_distance(first: Vec4, second: Vec4) -> f64 {
((first.x - second.x).powi(2)
+ (first.y - second.y).powi(2)
+ (first.z - second.z).powi(2)
+ (first.w - second.w).powi(2))
.sqrt()
}
fn homogeneous_midpoint(first: Vec4, second: Vec4) -> Vec4 {
first.add(second).scale(0.5)
}
fn samples(curve: &NurbsCurve, count: usize) -> Result<Vec<Vec3>, String> {
let [start, end] = curve.domain()?;
(0..=count)
.map(|index| curve.evaluate(start + (end - start) * index as f64 / count as f64))
.collect()
}
pub(crate) fn corner_band(scale: f64) -> f64 {
KernelTolerances::default()
.heal_band(scale, CORNER_IDENTITY_K)
.max(VERTEX_MATCH_FLOOR)
}
pub(crate) struct Prepared {
c0: NurbsCurve,
c1: NurbsCurve,
d0: NurbsCurve,
d1: NurbsCurve,
report: CoonsReport,
}
pub(crate) struct Oriented {
pub(crate) curves: Vec<NurbsCurve>,
pub(crate) reversed: [bool; 4],
pub(crate) corner_gaps: [f64; 4],
pub(crate) band: f64,
pub(crate) weight_scales: [f64; 4],
pub(crate) weight_closure: f64,
}
pub(crate) fn orient_and_reconcile(
boundaries: &[NurbsCurve; 4],
) -> Result<Oriented, CoonsRefusal> {
let mut points: Vec<Vec3> = Vec::with_capacity(4 * (BOUNDARY_SAMPLES + 1));
for boundary in boundaries {
points.extend(samples(boundary, BOUNDARY_SAMPLES).map_err(CoonsRefusal::Geometry)?);
}
let scale = model_scale(points.iter().copied());
let band = corner_band(scale);
for (index, boundary) in boundaries.iter().enumerate() {
let boundary_points =
samples(boundary, BOUNDARY_SAMPLES).map_err(CoonsRefusal::Geometry)?;
let extent = model_scale(boundary_points.iter().copied());
let spread = boundary_points
.iter()
.map(|point| point.sub(boundary_points[0]).length())
.fold(0.0f64, f64::max);
let extent = extent.min(spread);
if extent <= band {
return Err(CoonsRefusal::DegenerateBoundary {
boundary: index,
extent,
band,
});
}
}
let end_point = |curve: &NurbsCurve| -> Result<Vec3, CoonsRefusal> {
let [_, end] = curve.domain().map_err(CoonsRefusal::Geometry)?;
curve.evaluate(end).map_err(CoonsRefusal::Geometry)
};
let start_point = |curve: &NurbsCurve| -> Result<Vec3, CoonsRefusal> {
let [start, _] = curve.domain().map_err(CoonsRefusal::Geometry)?;
curve.evaluate(start).map_err(CoonsRefusal::Geometry)
};
let first_forward = end_point(&boundaries[0])?;
let first_backward = start_point(&boundaries[0])?;
let next_start = start_point(&boundaries[1])?;
let next_end = end_point(&boundaries[1])?;
let forward_gap = first_forward
.sub(next_start)
.length()
.min(first_forward.sub(next_end).length());
let backward_gap = first_backward
.sub(next_start)
.length()
.min(first_backward.sub(next_end).length());
let mut reversed = [false; 4];
reversed[0] = backward_gap < forward_gap;
let mut oriented: Vec<NurbsCurve> = Vec::with_capacity(4);
oriented.push(if reversed[0] {
boundaries[0].reversed().map_err(CoonsRefusal::Geometry)?
} else {
boundaries[0].clone()
});
let mut corner_gaps = [0.0f64; 4];
for index in 1..4 {
let previous_end = end_point(&oriented[index - 1])?;
let candidate_start = start_point(&boundaries[index])?;
let candidate_end = end_point(&boundaries[index])?;
let forward = previous_end.sub(candidate_start).length();
let backward = previous_end.sub(candidate_end).length();
reversed[index] = backward < forward;
corner_gaps[index - 1] = forward.min(backward);
oriented.push(if reversed[index] {
boundaries[index]
.reversed()
.map_err(CoonsRefusal::Geometry)?
} else {
boundaries[index].clone()
});
}
corner_gaps[3] = end_point(&oriented[3])?
.sub(start_point(&oriented[0])?)
.length();
for (corner, &gap) in corner_gaps.iter().enumerate() {
if gap > band {
return Err(CoonsRefusal::CornerGap { corner, gap, band });
}
}
let start_weight = |curve: &NurbsCurve| curve.control_points[0].w;
let end_weight = |curve: &NurbsCurve| curve.control_points[curve.control_points.len() - 1].w;
let mut weight_scales = [1.0f64; 4];
for index in 1..4 {
weight_scales[index] =
weight_scales[index - 1] * end_weight(&oriented[index - 1]) / start_weight(&oriented[index]);
}
let weight_closure =
weight_scales[3] * end_weight(&oriented[3]) / start_weight(&oriented[0]);
if (weight_closure - 1.0).abs() > WEIGHT_CLOSURE_BAND {
return Err(CoonsRefusal::CornerWeightClosure {
ratio: weight_closure,
band: WEIGHT_CLOSURE_BAND,
});
}
for (index, curve) in oriented.iter_mut().enumerate() {
if weight_scales[index] != 1.0 {
let scaled = curve
.control_points
.iter()
.map(|point| point.scale(weight_scales[index]))
.collect();
*curve = NurbsCurve::new(curve.degree, curve.knots.clone(), scaled)
.map_err(CoonsRefusal::Geometry)?;
}
}
Ok(Oriented {
curves: oriented,
reversed,
corner_gaps,
band,
weight_scales,
weight_closure,
})
}
pub(crate) fn report_skeleton(oriented: &Oriented, work: [CompatWork; 4]) -> CoonsReport {
CoonsReport {
reversed: oriented.reversed,
corner_gaps: oriented.corner_gaps,
corner_band: oriented.band,
weight_scales: oriented.weight_scales,
weight_closure: oriented.weight_closure,
work,
degree_u: 0,
degree_v: 0,
control_rows: 0,
control_columns: 0,
min_weight: f64::INFINITY,
boundary_residual: 0.0,
ribbons: None,
}
}
fn prepare(boundaries: &[NurbsCurve; 4]) -> Result<Prepared, CoonsRefusal> {
let oriented = orient_and_reconcile(boundaries)?;
let c0 = oriented.curves[0].clone();
let d1 = oriented.curves[1].clone();
let c1 = oriented.curves[2].reversed().map_err(CoonsRefusal::Geometry)?;
let d0 = oriented.curves[3].reversed().map_err(CoonsRefusal::Geometry)?;
let (u_family, u_work) = compat::make_family_compatible(&[c0, c1], 1).map_err(|detail| {
CoonsRefusal::Incompatible {
direction: 'u',
detail,
}
})?;
let (v_family, v_work) = compat::make_family_compatible(&[d0, d1], 1).map_err(|detail| {
CoonsRefusal::Incompatible {
direction: 'v',
detail,
}
})?;
let mut work = [CompatWork::default(); 4];
work[0] = u_work[0];
work[2] = u_work[1];
work[3] = v_work[0];
work[1] = v_work[1];
let mut report = report_skeleton(&oriented, work);
report.degree_u = u_family[0].degree;
report.degree_v = v_family[0].degree;
report.control_rows = u_family[0].control_points.len();
report.control_columns = v_family[0].control_points.len();
Ok(Prepared {
c0: u_family[0].clone(),
c1: u_family[1].clone(),
d0: v_family[0].clone(),
d1: v_family[1].clone(),
report,
})
}
fn build_bilinear(prepared: Prepared) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
let Prepared {
c0,
c1,
d0,
d1,
mut report,
} = prepared;
let rows = c0.control_points.len();
let columns = d0.control_points.len();
let eta = compat::greville(&c0.knots, c0.degree);
let xi = compat::greville(&d0.knots, d0.degree);
let corner_00 = homogeneous_midpoint(c0.control_points[0], d0.control_points[0]);
let corner_10 = homogeneous_midpoint(c0.control_points[rows - 1], d1.control_points[0]);
let corner_01 = homogeneous_midpoint(c1.control_points[0], d0.control_points[columns - 1]);
let corner_11 =
homogeneous_midpoint(c1.control_points[rows - 1], d1.control_points[columns - 1]);
let mut net: Vec<Vec<Vec4>> = vec![
vec![
Vec4 {
x: 0.0,
y: 0.0,
z: 0.0,
w: 1.0
};
columns
];
rows
];
for (row, net_row) in net.iter_mut().enumerate() {
for (column, cell) in net_row.iter_mut().enumerate() {
let ruled_v = c0.control_points[row]
.scale(1.0 - xi[column])
.add(c1.control_points[row].scale(xi[column]));
let ruled_u = d0.control_points[column]
.scale(1.0 - eta[row])
.add(d1.control_points[column].scale(eta[row]));
let bilinear = corner_00
.scale((1.0 - eta[row]) * (1.0 - xi[column]))
.add(corner_10.scale(eta[row] * (1.0 - xi[column])))
.add(corner_01.scale((1.0 - eta[row]) * xi[column]))
.add(corner_11.scale(eta[row] * xi[column]));
*cell = ruled_v.add(ruled_u).add(bilinear.scale(-1.0));
}
}
for row in 0..rows {
net[row][0] = c0.control_points[row];
net[row][columns - 1] = c1.control_points[row];
}
for column in 0..columns {
net[0][column] = d0.control_points[column];
net[rows - 1][column] = d1.control_points[column];
}
net[0][0] = corner_00;
net[rows - 1][0] = corner_10;
net[0][columns - 1] = corner_01;
net[rows - 1][columns - 1] = corner_11;
let mut min_weight = f64::INFINITY;
let mut worst = (0usize, 0usize);
for (row, net_row) in net.iter().enumerate() {
for (column, cell) in net_row.iter().enumerate() {
if cell.w < min_weight {
min_weight = cell.w;
worst = (row, column);
}
}
}
if !(min_weight > 0.0) || !min_weight.is_finite() {
return Err(CoonsRefusal::NonPositiveWeight {
row: worst.0,
column: worst.1,
weight: min_weight,
});
}
report.min_weight = min_weight;
report.boundary_residual = boundary_residual(&net, &c0, &c1, &d0, &d1);
let surface = NurbsSurface::new(
c0.degree,
d0.degree,
c0.knots.clone(),
d0.knots.clone(),
net,
)
.map_err(CoonsRefusal::Geometry)?;
Ok((surface, report))
}
fn boundary_residual(
net: &[Vec<Vec4>],
c0: &NurbsCurve,
c1: &NurbsCurve,
d0: &NurbsCurve,
d1: &NurbsCurve,
) -> f64 {
let rows = net.len();
let columns = net[0].len();
let mut worst = 0.0f64;
for row in 0..rows {
worst = worst.max(homogeneous_distance(net[row][0], c0.control_points[row]));
worst = worst.max(homogeneous_distance(
net[row][columns - 1],
c1.control_points[row],
));
}
for column in 0..columns {
worst = worst.max(homogeneous_distance(net[0][column], d0.control_points[column]));
worst = worst.max(homogeneous_distance(
net[rows - 1][column],
d1.control_points[column],
));
}
worst
}