use crate::coons::compat::{self, CompatWork};
use crate::coons::{corner_band, WEIGHT_CLOSURE_BAND};
use crate::{
intersect_curves, model_scale, solve_dense, KnotVector, NurbsCurve, NurbsSurface, Vec3, Vec4,
KNOT_IDENTITY_TOL,
};
const MAX_SKIN_DEGREE: usize = 3;
const CURVE_SAMPLES: usize = 32;
const APPROACH_SAMPLES: usize = 96;
const CROSSING_ITERATIONS: usize = 60;
#[derive(Clone, Debug, PartialEq)]
pub enum GordonRefusal {
TooFewCurves { family: char, count: usize },
InvalidBand { band: f64 },
DegenerateCurve {
family: char,
curve: usize,
extent: f64,
band: f64,
},
ClosedCurve {
family: char,
curve: usize,
gap: f64,
band: f64,
},
CurvesDoNotMeet {
u_curve: usize,
v_curve: usize,
gap: f64,
band: f64,
},
MultipleCrossings {
u_curve: usize,
v_curve: usize,
crossings: Vec<(f64, f64)>,
band: f64,
},
CrossingOrder {
family: char,
curve: usize,
parameters: Vec<f64>,
},
CoincidentCrossings {
family: char,
first: usize,
second: usize,
separation: f64,
},
Overhang {
family: char,
curve: usize,
end: usize,
parameter: f64,
gap: f64,
band: f64,
},
InconsistentCrossing {
u_curve: usize,
v_curve: usize,
gap: f64,
band: f64,
u_parameter_offset: f64,
v_parameter_offset: f64,
},
WeightClosure {
u_curve: usize,
v_curve: usize,
ratio: f64,
band: f64,
},
NonPositiveWeight {
row: usize,
column: usize,
weight: f64,
},
Incompatible { family: char, detail: String },
Geometry(String),
}
impl std::fmt::Display for GordonRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooFewCurves { family, count } => write!(
formatter,
"gordon: the {family} family has {count} curve(s) — a network needs at least two \
per family"
),
Self::InvalidBand { band } => write!(
formatter,
"gordon: crossing band {band:.3e} is not a positive finite distance"
),
Self::DegenerateCurve {
family,
curve,
extent,
band,
} => write!(
formatter,
"gordon: {family}-curve {curve} is degenerate — extent {extent:.3e} is inside the \
band {band:.3e}"
),
Self::ClosedCurve {
family,
curve,
gap,
band,
} => write!(
formatter,
"gordon: {family}-curve {curve} is closed — its ends are {gap:.3e} apart, inside \
the band {band:.3e}; periodic networks are not supported"
),
Self::CurvesDoNotMeet {
u_curve,
v_curve,
gap,
band,
} => write!(
formatter,
"gordon: u-curve {u_curve} and v-curve {v_curve} do not meet — closest approach \
{gap:.3e} exceeds the crossing band {band:.3e}"
),
Self::MultipleCrossings {
u_curve,
v_curve,
crossings,
band,
} => write!(
formatter,
"gordon: u-curve {u_curve} and v-curve {v_curve} meet {} times within the band \
{band:.3e} (at {crossings:?}) — a network pair must cross once",
crossings.len()
),
Self::CrossingOrder {
family,
curve,
parameters,
} => write!(
formatter,
"gordon: along {family}-curve {curve} the crossings run out of order \
({parameters:?}) — the families do not form a grid"
),
Self::CoincidentCrossings {
family,
first,
second,
separation,
} => write!(
formatter,
"gordon: {family}-curves {first} and {second} cross the other family at the same \
parameter ({separation:.3e} apart)"
),
Self::Overhang {
family,
curve,
end,
parameter,
gap,
band,
} => write!(
formatter,
"gordon: {family}-curve {curve} runs past the network boundary at its {} — the \
boundary crossing sits at parameter {parameter:.6}, {gap:.3e} along the curve \
(band {band:.3e})",
if *end == 0 { "start" } else { "end" }
),
Self::InconsistentCrossing {
u_curve,
v_curve,
gap,
band,
u_parameter_offset,
v_parameter_offset,
} => write!(
formatter,
"gordon: u-curve {u_curve} and v-curve {v_curve} are {gap:.3e} apart at the \
families' shared crossing parameters (band {band:.3e}); their own crossing is \
offset by {u_parameter_offset:.3e} in u and {v_parameter_offset:.3e} in v"
),
Self::WeightClosure {
u_curve,
v_curve,
ratio,
band,
} => write!(
formatter,
"gordon: rational weights do not reconcile at u-curve {u_curve} x v-curve \
{v_curve} — ratio {ratio:.12} (band {band:.3e} about 1)"
),
Self::NonPositiveWeight {
row,
column,
weight,
} => write!(
formatter,
"gordon: blended control point ({row}, {column}) has non-positive weight \
{weight:.3e} — the surface would pass through infinity"
),
Self::Incompatible { family, detail } => {
write!(formatter, "gordon: {family} family incompatible — {detail}")
}
Self::Geometry(message) => formatter.write_str(message),
}
}
}
impl From<GordonRefusal> for String {
fn from(refusal: GordonRefusal) -> Self {
refusal.to_string()
}
}
#[derive(Clone, Debug)]
pub struct GordonReport {
pub u_order: Vec<usize>,
pub v_order: Vec<usize>,
pub u_reversed: Vec<bool>,
pub v_reversed: Vec<bool>,
pub u_parameters: Vec<f64>,
pub v_parameters: Vec<f64>,
pub crossing_band: f64,
pub closest_approach: f64,
pub crossing_gap: f64,
pub parameter_spread: f64,
pub u_weight_scales: Vec<f64>,
pub v_weight_scales: Vec<f64>,
pub weight_closure: f64,
pub u_work: Vec<CompatWork>,
pub v_work: Vec<CompatWork>,
pub skin_degree_u: usize,
pub skin_degree_v: usize,
pub degree_u: usize,
pub degree_v: usize,
pub control_rows: usize,
pub control_columns: usize,
pub min_weight: f64,
pub interpolation_bound: f64,
pub sampled_interpolation_residual: f64,
}
pub fn gordon_surface(
u_curves: &[NurbsCurve],
v_curves: &[NurbsCurve],
) -> Result<NurbsSurface, GordonRefusal> {
gordon_surface_reported(u_curves, v_curves, None).map(|(surface, _)| surface)
}
pub fn gordon_surface_reported(
u_curves: &[NurbsCurve],
v_curves: &[NurbsCurve],
crossing_band: Option<f64>,
) -> Result<(NurbsSurface, GordonReport), GordonRefusal> {
let network = prepare(u_curves, v_curves, crossing_band)?;
build(network)
}
struct Family {
order: Vec<usize>,
curves: Vec<NurbsCurve>,
reversed: Vec<bool>,
weight_scales: Vec<f64>,
work: Vec<CompatWork>,
parameters: Vec<f64>,
}
struct Network {
u: Family,
v: Family,
crossings: Vec<Vec<Vec4>>,
band: f64,
closest_approach: f64,
crossing_gap: f64,
parameter_spread: f64,
weight_closure: f64,
}
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()
}
fn geometry(message: String) -> GordonRefusal {
GordonRefusal::Geometry(message)
}
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 polish_crossing(
first: &NurbsCurve,
second: &NurbsCurve,
seed_s: f64,
seed_t: f64,
) -> Result<(f64, f64, f64), String> {
let mut s = seed_s.clamp(0.0, 1.0);
let mut t = seed_t.clamp(0.0, 1.0);
let mut gap = first.evaluate(s)?.sub(second.evaluate(t)?).length();
for _ in 0..CROSSING_ITERATIONS {
let a = first.derivatives(s, 2)?;
let b = second.derivatives(t, 2)?;
let residual = a[0].sub(b[0]);
let gradient_s = a[1].dot(residual);
let gradient_t = -b[1].dot(residual);
let h_ss = a[1].dot(a[1]) + a[2].dot(residual);
let h_st = -a[1].dot(b[1]);
let h_tt = b[1].dot(b[1]) - b[2].dot(residual);
let determinant = h_ss * h_tt - h_st * h_st;
let (mut step_s, mut step_t) = if determinant.abs() > f64::MIN_POSITIVE {
(
(-gradient_s * h_tt + gradient_t * h_st) / determinant,
(-gradient_t * h_ss + gradient_s * h_st) / determinant,
)
} else {
(0.0, 0.0)
};
let pinned_s = (s <= 0.0 && step_s < 0.0) || (s >= 1.0 && step_s > 0.0);
let pinned_t = (t <= 0.0 && step_t < 0.0) || (t >= 1.0 && step_t > 0.0);
if pinned_s && !pinned_t {
step_s = 0.0;
step_t = if h_tt.abs() > f64::MIN_POSITIVE { -gradient_t / h_tt } else { 0.0 };
} else if pinned_t && !pinned_s {
step_t = 0.0;
step_s = if h_ss.abs() > f64::MIN_POSITIVE { -gradient_s / h_ss } else { 0.0 };
} else if pinned_s && pinned_t {
break;
}
let next_s = (s + step_s).clamp(0.0, 1.0);
let next_t = (t + step_t).clamp(0.0, 1.0);
if next_s == s && next_t == t {
break;
}
let next_gap = first.evaluate(next_s)?.sub(second.evaluate(next_t)?).length();
if next_gap > gap {
break;
}
s = next_s;
t = next_t;
gap = next_gap;
}
Ok((s, t, gap))
}
fn closest_approach(first: &NurbsCurve, second: &NurbsCurve) -> Result<(f64, f64, f64), String> {
let first_points = samples(first, APPROACH_SAMPLES)?;
let second_points = samples(second, APPROACH_SAMPLES)?;
let mut best = (0usize, 0usize, f64::INFINITY);
for (i, a) in first_points.iter().enumerate() {
for (j, b) in second_points.iter().enumerate() {
let distance = a.sub(*b).length();
if distance < best.2 {
best = (i, j, distance);
}
}
}
polish_crossing(
first,
second,
best.0 as f64 / APPROACH_SAMPLES as f64,
best.1 as f64 / APPROACH_SAMPLES as f64,
)
}
fn median(values: &[f64]) -> f64 {
let mut sorted = values.to_vec();
sorted.sort_by(f64::total_cmp);
let middle = sorted.len() / 2;
if sorted.len() % 2 == 1 {
sorted[middle]
} else if sorted[middle - 1] == sorted[middle] {
sorted[middle]
} else {
0.5 * (sorted[middle - 1] + sorted[middle])
}
}
fn orientation(
family: char,
curve: usize,
sequence: &[f64],
crossing_family: char,
crossing_order: &[usize],
) -> Result<bool, GordonRefusal> {
for (index, pair) in sequence.windows(2).enumerate() {
let separation = (pair[1] - pair[0]).abs();
if separation <= KNOT_IDENTITY_TOL {
return Err(GordonRefusal::CoincidentCrossings {
family: crossing_family,
first: crossing_order[index],
second: crossing_order[index + 1],
separation,
});
}
}
if sequence.windows(2).all(|pair| pair[1] > pair[0]) {
Ok(false)
} else if sequence.windows(2).all(|pair| pair[1] < pair[0]) {
Ok(true)
} else {
Err(GordonRefusal::CrossingOrder {
family,
curve,
parameters: sequence.to_vec(),
})
}
}
fn prepare(
u_input: &[NurbsCurve],
v_input: &[NurbsCurve],
crossing_band: Option<f64>,
) -> Result<Network, GordonRefusal> {
for (family, curves) in [('u', u_input), ('v', v_input)] {
if curves.len() < 2 {
return Err(GordonRefusal::TooFewCurves {
family,
count: curves.len(),
});
}
}
if let Some(band) = crossing_band {
if !(band > 0.0 && band.is_finite()) {
return Err(GordonRefusal::InvalidBand { band });
}
}
let (u_curves, u_work) = normalise_domains(u_input)?;
let (v_curves, v_work) = normalise_domains(v_input)?;
let mut points: Vec<Vec3> = Vec::new();
for curve in u_curves.iter().chain(&v_curves) {
points.extend(samples(curve, CURVE_SAMPLES).map_err(geometry)?);
}
let band = crossing_band.unwrap_or_else(|| corner_band(model_scale(points.iter().copied())));
judge_curves('u', &u_curves, band)?;
judge_curves('v', &v_curves, band)?;
let crossings = find_crossings(&u_curves, &v_curves, band)?;
let order = order_network(&crossings)?;
let f = oriented(&u_curves, &order.u_order, &order.u_reversed)?;
let g = oriented(&v_curves, &order.v_order, &order.v_reversed)?;
let (rows, columns) = (f.len(), g.len());
let mut own_s = vec![vec![0.0f64; columns]; rows];
let mut own_t = vec![vec![0.0f64; columns]; rows];
for a in 0..rows {
for b in 0..columns {
let (j, i) = (order.u_order[a], order.v_order[b]);
let (s, t) = (crossings.s[j][i], crossings.t[j][i]);
let seed_s = if order.u_reversed[j] { 1.0 - s } else { s };
let seed_t = if order.v_reversed[i] { 1.0 - t } else { t };
let (polished_s, polished_t, _) =
polish_crossing(&f[a], &g[b], seed_s, seed_t).map_err(geometry)?;
own_s[a][b] = polished_s;
own_t[a][b] = polished_t;
}
}
let u_parameters = shared_parameters(columns, |b| (0..rows).map(|a| own_s[a][b]).collect());
let v_parameters = shared_parameters(rows, |a| (0..columns).map(|b| own_t[a][b]).collect());
for (family, parameters, network_order) in [
('v', &u_parameters, &order.v_order),
('u', &v_parameters, &order.u_order),
] {
for (index, pair) in parameters.windows(2).enumerate() {
let separation = pair[1] - pair[0];
if separation <= KNOT_IDENTITY_TOL {
return Err(GordonRefusal::CoincidentCrossings {
family,
first: network_order[index],
second: network_order[index + 1],
separation,
});
}
}
}
let shared = SharedCrossings {
f: &f,
g: &g,
u_parameters: &u_parameters,
v_parameters: &v_parameters,
own_s: &own_s,
own_t: &own_t,
u_order: &order.u_order,
v_order: &order.v_order,
};
let (crossing_gap, parameter_spread) = judge_shared_crossings(&shared, band)?;
let (f_scales, g_scales, weight_closure) = reconcile_weights(&shared)?;
let f_scaled = scaled(&f, &f_scales)?;
let g_scaled = scaled(&g, &g_scales)?;
let grid = crossing_grid(&f_scaled, &g_scaled, &u_parameters, &v_parameters)?;
let mut u_weight_scales = vec![1.0f64; rows];
for (a, &j) in order.u_order.iter().enumerate() {
u_weight_scales[j] = f_scales[a];
}
let mut v_weight_scales = vec![1.0f64; columns];
for (b, &i) in order.v_order.iter().enumerate() {
v_weight_scales[i] = g_scales[b];
}
Ok(Network {
u: Family {
order: order.u_order,
curves: f_scaled,
reversed: order.u_reversed,
weight_scales: u_weight_scales,
work: u_work,
parameters: u_parameters,
},
v: Family {
order: order.v_order,
curves: g_scaled,
reversed: order.v_reversed,
weight_scales: v_weight_scales,
work: v_work,
parameters: v_parameters,
},
crossings: grid,
band,
closest_approach: crossings.closest,
crossing_gap,
parameter_spread,
weight_closure,
})
}
fn normalise_domains(
curves: &[NurbsCurve],
) -> Result<(Vec<NurbsCurve>, Vec<CompatWork>), GordonRefusal> {
let mut normalised = Vec::with_capacity(curves.len());
let mut work = vec![CompatWork::default(); curves.len()];
for (index, curve) in curves.iter().enumerate() {
let (moved, remapped) = compat::normalise_domain(curve).map_err(geometry)?;
work[index].domain_remapped = remapped;
normalised.push(moved);
}
Ok((normalised, work))
}
fn judge_curves(family: char, curves: &[NurbsCurve], band: f64) -> Result<(), GordonRefusal> {
for (index, curve) in curves.iter().enumerate() {
let points = samples(curve, CURVE_SAMPLES).map_err(geometry)?;
let spread = points
.iter()
.map(|point| point.sub(points[0]).length())
.fold(0.0f64, f64::max);
let extent = model_scale(points.iter().copied()).min(spread);
if extent <= band {
return Err(GordonRefusal::DegenerateCurve {
family,
curve: index,
extent,
band,
});
}
let gap = points[0].sub(points[points.len() - 1]).length();
if gap <= band {
return Err(GordonRefusal::ClosedCurve {
family,
curve: index,
gap,
band,
});
}
}
Ok(())
}
struct Crossings {
s: Vec<Vec<f64>>,
t: Vec<Vec<f64>>,
closest: f64,
}
fn find_crossings(
u_curves: &[NurbsCurve],
v_curves: &[NurbsCurve],
band: f64,
) -> Result<Crossings, GordonRefusal> {
let mut crossings = Crossings {
s: vec![vec![0.0f64; v_curves.len()]; u_curves.len()],
t: vec![vec![0.0f64; v_curves.len()]; u_curves.len()],
closest: 0.0,
};
for (j, u_curve) in u_curves.iter().enumerate() {
for (i, v_curve) in v_curves.iter().enumerate() {
let hits = intersect_curves(u_curve, v_curve, band).map_err(geometry)?;
let (s, t, gap) = match hits.len() {
1 => polish_crossing(u_curve, v_curve, hits[0].s, hits[0].t).map_err(geometry)?,
0 => {
let approach = closest_approach(u_curve, v_curve).map_err(geometry)?;
if approach.2 > band {
return Err(GordonRefusal::CurvesDoNotMeet {
u_curve: j,
v_curve: i,
gap: approach.2,
band,
});
}
approach
}
_ => {
return Err(GordonRefusal::MultipleCrossings {
u_curve: j,
v_curve: i,
crossings: hits.iter().map(|hit| (hit.s, hit.t)).collect(),
band,
})
}
};
crossings.s[j][i] = s;
crossings.t[j][i] = t;
crossings.closest = crossings.closest.max(gap);
}
}
Ok(crossings)
}
struct NetworkOrder {
u_order: Vec<usize>,
v_order: Vec<usize>,
u_reversed: Vec<bool>,
v_reversed: Vec<bool>,
}
fn order_network(crossings: &Crossings) -> Result<NetworkOrder, GordonRefusal> {
let (s, t) = (&crossings.s, &crossings.t);
let (rows, columns) = (s.len(), s[0].len());
let mut v_order: Vec<usize> = (0..columns).collect();
v_order.sort_by(|&a, &b| s[0][a].total_cmp(&s[0][b]));
let mut u_order: Vec<usize> = (0..rows).collect();
u_order.sort_by(|&a, &b| t[a][0].total_cmp(&t[b][0]));
let mut u_reversed = vec![false; rows];
for (j, reversed) in u_reversed.iter_mut().enumerate() {
let sequence: Vec<f64> = v_order.iter().map(|&i| s[j][i]).collect();
*reversed = orientation('u', j, &sequence, 'v', &v_order)?;
}
let mut v_reversed = vec![false; columns];
for (i, reversed) in v_reversed.iter_mut().enumerate() {
let sequence: Vec<f64> = u_order.iter().map(|&j| t[j][i]).collect();
*reversed = orientation('v', i, &sequence, 'u', &u_order)?;
}
Ok(NetworkOrder {
u_order,
v_order,
u_reversed,
v_reversed,
})
}
fn oriented(
curves: &[NurbsCurve],
order: &[usize],
reversed: &[bool],
) -> Result<Vec<NurbsCurve>, GordonRefusal> {
order
.iter()
.map(|&index| {
if reversed[index] {
curves[index].reversed().map_err(geometry)
} else {
Ok(curves[index].clone())
}
})
.collect()
}
fn shared_parameters(count: usize, own: impl Fn(usize) -> Vec<f64>) -> Vec<f64> {
(0..count)
.map(|index| {
if index == 0 {
0.0
} else if index == count - 1 {
1.0
} else {
median(&own(index))
}
})
.collect()
}
struct SharedCrossings<'a> {
f: &'a [NurbsCurve],
g: &'a [NurbsCurve],
u_parameters: &'a [f64],
v_parameters: &'a [f64],
own_s: &'a [Vec<f64>],
own_t: &'a [Vec<f64>],
u_order: &'a [usize],
v_order: &'a [usize],
}
fn judge_shared_crossings(
shared: &SharedCrossings<'_>,
band: f64,
) -> Result<(f64, f64), GordonRefusal> {
let (rows, columns) = (shared.f.len(), shared.g.len());
let mut crossing_gap = 0.0f64;
let mut spread = 0.0f64;
let mut worst: Option<GordonRefusal> = None;
for a in 0..rows {
for b in 0..columns {
let own_s = shared.own_s[a][b];
let own_t = shared.own_t[a][b];
let on_u = shared.f[a].evaluate(shared.u_parameters[b]).map_err(geometry)?;
let on_v = shared.g[b].evaluate(shared.v_parameters[a]).map_err(geometry)?;
let gap = on_u.sub(on_v).length();
let u_offset = (own_s - shared.u_parameters[b]).abs();
let v_offset = (own_t - shared.v_parameters[a]).abs();
if b == 0 || b == columns - 1 {
let along = on_u.sub(shared.f[a].evaluate(own_s).map_err(geometry)?).length();
if along > band {
return Err(GordonRefusal::Overhang {
family: 'u',
curve: shared.u_order[a],
end: usize::from(b == columns - 1),
parameter: own_s,
gap: along,
band,
});
}
}
if a == 0 || a == rows - 1 {
let along = on_v.sub(shared.g[b].evaluate(own_t).map_err(geometry)?).length();
if along > band {
return Err(GordonRefusal::Overhang {
family: 'v',
curve: shared.v_order[b],
end: usize::from(a == rows - 1),
parameter: own_t,
gap: along,
band,
});
}
}
if gap > band && gap > crossing_gap {
worst = Some(GordonRefusal::InconsistentCrossing {
u_curve: shared.u_order[a],
v_curve: shared.v_order[b],
gap,
band,
u_parameter_offset: u_offset,
v_parameter_offset: v_offset,
});
}
crossing_gap = crossing_gap.max(gap);
spread = spread.max(u_offset).max(v_offset);
}
}
match worst {
Some(refusal) => Err(refusal),
None => Ok((crossing_gap, spread)),
}
}
fn reconcile_weights(
shared: &SharedCrossings<'_>,
) -> Result<(Vec<f64>, Vec<f64>, f64), GordonRefusal> {
let (rows, columns) = (shared.f.len(), shared.g.len());
let mut f_scales = vec![1.0f64; rows];
let mut g_scales = vec![1.0f64; columns];
let polynomial = shared
.f
.iter()
.chain(shared.g)
.all(|curve| curve.control_points.iter().all(|point| point.w == 1.0));
if polynomial {
return Ok((f_scales, g_scales, 1.0));
}
let mut f_weight = vec![vec![1.0f64; columns]; rows];
let mut g_weight = vec![vec![1.0f64; columns]; rows];
for a in 0..rows {
for b in 0..columns {
f_weight[a][b] = shared.f[a]
.evaluate_homogeneous(shared.u_parameters[b])
.map_err(geometry)?
.w;
g_weight[a][b] = shared.g[b]
.evaluate_homogeneous(shared.v_parameters[a])
.map_err(geometry)?
.w;
}
}
for b in 0..columns {
g_scales[b] = f_weight[0][b] / g_weight[0][b];
}
for a in 1..rows {
f_scales[a] = g_scales[0] * g_weight[a][0] / f_weight[a][0];
}
let mut weight_closure = 1.0f64;
for a in 0..rows {
for b in 0..columns {
let ratio = f_scales[a] * f_weight[a][b] / (g_scales[b] * g_weight[a][b]);
if (ratio - 1.0).abs() > (weight_closure - 1.0).abs() {
weight_closure = ratio;
}
if (ratio - 1.0).abs() > WEIGHT_CLOSURE_BAND {
return Err(GordonRefusal::WeightClosure {
u_curve: shared.u_order[a],
v_curve: shared.v_order[b],
ratio,
band: WEIGHT_CLOSURE_BAND,
});
}
}
}
Ok((f_scales, g_scales, weight_closure))
}
fn scaled(curves: &[NurbsCurve], scales: &[f64]) -> Result<Vec<NurbsCurve>, GordonRefusal> {
curves
.iter()
.zip(scales)
.map(|(curve, &factor)| {
if factor == 1.0 {
return Ok(curve.clone());
}
NurbsCurve::new(
curve.degree,
curve.knots.clone(),
curve.control_points.iter().map(|point| point.scale(factor)).collect(),
)
.map_err(geometry)
})
.collect()
}
fn crossing_grid(
f: &[NurbsCurve],
g: &[NurbsCurve],
u_parameters: &[f64],
v_parameters: &[f64],
) -> Result<Vec<Vec<Vec4>>, GordonRefusal> {
let (last_row, last_column) = (f.len() - 1, g.len() - 1);
let mut grid = Vec::with_capacity(f.len());
for (a, u_curve) in f.iter().enumerate() {
let mut row = Vec::with_capacity(g.len());
for (b, v_curve) in g.iter().enumerate() {
let on_u = u_curve.evaluate_homogeneous(u_parameters[b]).map_err(geometry)?;
let on_v = v_curve.evaluate_homogeneous(v_parameters[a]).map_err(geometry)?;
let boundary_u = a == 0 || a == last_row;
let boundary_v = b == 0 || b == last_column;
row.push(match (boundary_u, boundary_v) {
(true, false) => on_v,
(false, true) => on_u,
_ => homogeneous_midpoint(on_u, on_v),
});
}
grid.push(row);
}
Ok(grid)
}
fn cardinal_functions(parameters: &[f64]) -> Result<(usize, Vec<NurbsCurve>), String> {
let count = parameters.len();
let degree = MAX_SKIN_DEGREE.min(count - 1);
let mut knots = vec![0.0f64; count + degree + 1];
for knot in &mut knots[count..] {
*knot = 1.0;
}
for j in 1..count - degree {
knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
}
let knot_vector = KnotVector::new(knots.clone(), degree)?;
let mut matrix = vec![vec![0.0f64; count]; count];
for (row, ¶meter) in parameters.iter().enumerate() {
let span = knot_vector.find_span(parameter);
for (offset, value) in knot_vector
.basis_functions(span, parameter)
.into_iter()
.enumerate()
{
matrix[row][span - degree + offset] = value;
}
}
let mut functions = Vec::with_capacity(count);
for j in 0..count {
let mut rhs = vec![0.0f64; count];
rhs[j] = 1.0;
let mut coefficients = solve_dense(matrix.clone(), rhs)
.map_err(|error| format!("cardinal collocation at {parameters:?}: {error}"))?;
coefficients[0] = if j == 0 { 1.0 } else { 0.0 };
coefficients[count - 1] = if j == count - 1 { 1.0 } else { 0.0 };
functions.push(NurbsCurve::new(
degree,
knots.clone(),
coefficients
.into_iter()
.map(|value| Vec4 { x: value, y: 0.0, z: 0.0, w: 1.0 })
.collect(),
)?);
}
Ok((degree, functions))
}
fn compatible_direction(
family: char,
curves: &[NurbsCurve],
cardinals: &[NurbsCurve],
) -> Result<(Vec<NurbsCurve>, Vec<Vec<f64>>, Vec<CompatWork>), GordonRefusal> {
let combined: Vec<NurbsCurve> = curves.iter().chain(cardinals).cloned().collect();
let (compatible, work) = compat::make_family_compatible(&combined, 1)
.map_err(|detail| GordonRefusal::Incompatible { family, detail })?;
let coefficients = compatible[curves.len()..]
.iter()
.map(|function| function.control_points.iter().map(|point| point.x).collect())
.collect();
Ok((
compatible[..curves.len()].to_vec(),
coefficients,
work[..curves.len()].to_vec(),
))
}
fn build(network: Network) -> Result<(NurbsSurface, GordonReport), GordonRefusal> {
let Network {
mut u,
mut v,
crossings,
band,
closest_approach,
crossing_gap,
parameter_spread,
weight_closure,
} = network;
let rows_in_network = u.curves.len();
let columns_in_network = v.curves.len();
let (skin_degree_u, alphas) = cardinal_functions(&u.parameters).map_err(|detail| {
GordonRefusal::Incompatible {
family: 'v',
detail,
}
})?;
let (skin_degree_v, betas) = cardinal_functions(&v.parameters).map_err(|detail| {
GordonRefusal::Incompatible {
family: 'u',
detail,
}
})?;
let (f, alpha, f_work) = compatible_direction('u', &u.curves, &alphas)?;
let (g, beta, g_work) = compatible_direction('v', &v.curves, &betas)?;
for (a, &j) in u.order.iter().enumerate() {
u.work[j].elevated_by = f_work[a].elevated_by;
u.work[j].knots_inserted = f_work[a].knots_inserted;
}
for (b, &i) in v.order.iter().enumerate() {
v.work[i].elevated_by = g_work[b].elevated_by;
v.work[i].knots_inserted = g_work[b].knots_inserted;
}
let rows = f[0].control_points.len();
let columns = g[0].control_points.len();
let zero = Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
let mut net = vec![vec![zero; columns]; rows];
let crossing_rows: Vec<Vec<Vec4>> = (0..rows)
.map(|k| {
(0..rows_in_network)
.map(|a| {
(0..columns_in_network).fold(zero, |sum, b| {
sum.add(crossings[a][b].scale(alpha[b][k]))
})
})
.collect()
})
.collect();
for (k, net_row) in net.iter_mut().enumerate() {
for (l, cell) in net_row.iter_mut().enumerate() {
let mut sum = zero;
for a in 0..rows_in_network {
sum = sum.add(
f[a].control_points[k]
.add(crossing_rows[k][a].scale(-1.0))
.scale(beta[a][l]),
);
}
for b in 0..columns_in_network {
sum = sum.add(g[b].control_points[l].scale(alpha[b][k]));
}
*cell = sum;
}
}
let (last_row, last_column) = (rows - 1, columns - 1);
let (f_first, f_last) = (&f[0], &f[rows_in_network - 1]);
let (g_first, g_last) = (&g[0], &g[columns_in_network - 1]);
for (k, net_row) in net.iter_mut().enumerate() {
net_row[0] = f_first.control_points[k];
net_row[last_column] = f_last.control_points[k];
}
for l in 0..columns {
net[0][l] = g_first.control_points[l];
net[last_row][l] = g_last.control_points[l];
}
net[0][0] = homogeneous_midpoint(f_first.control_points[0], g_first.control_points[0]);
net[last_row][0] =
homogeneous_midpoint(f_first.control_points[last_row], g_last.control_points[0]);
net[0][last_column] =
homogeneous_midpoint(f_last.control_points[0], g_first.control_points[last_column]);
net[last_row][last_column] = homogeneous_midpoint(
f_last.control_points[last_row],
g_last.control_points[last_column],
);
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(GordonRefusal::NonPositiveWeight {
row: worst.0,
column: worst.1,
weight: min_weight,
});
}
let surface = NurbsSurface::new(
f[0].degree,
g[0].degree,
f[0].knots.clone(),
g[0].knots.clone(),
net,
)
.map_err(geometry)?;
let mut interpolation_bound = 0.0f64;
let mut sampled = 0.0f64;
for (a, curve) in f.iter().enumerate() {
let iso = surface.iso_curve_v(v.parameters[a]).map_err(geometry)?;
for (on_surface, on_curve) in iso.control_points.iter().zip(&curve.control_points) {
interpolation_bound =
interpolation_bound.max(homogeneous_distance(*on_surface, *on_curve));
}
for index in 0..=CURVE_SAMPLES {
let parameter = index as f64 / CURVE_SAMPLES as f64;
let on_surface = surface.evaluate(parameter, v.parameters[a]).map_err(geometry)?;
let on_curve = u.curves[a].evaluate(parameter).map_err(geometry)?;
sampled = sampled.max(on_surface.sub(on_curve).length());
}
}
for (b, curve) in g.iter().enumerate() {
let iso = surface.iso_curve_u(u.parameters[b]).map_err(geometry)?;
for (on_surface, on_curve) in iso.control_points.iter().zip(&curve.control_points) {
interpolation_bound =
interpolation_bound.max(homogeneous_distance(*on_surface, *on_curve));
}
for index in 0..=CURVE_SAMPLES {
let parameter = index as f64 / CURVE_SAMPLES as f64;
let on_surface = surface.evaluate(u.parameters[b], parameter).map_err(geometry)?;
let on_curve = v.curves[b].evaluate(parameter).map_err(geometry)?;
sampled = sampled.max(on_surface.sub(on_curve).length());
}
}
let report = GordonReport {
u_order: u.order,
v_order: v.order,
u_reversed: u.reversed,
v_reversed: v.reversed,
u_parameters: u.parameters,
v_parameters: v.parameters,
crossing_band: band,
closest_approach,
crossing_gap,
parameter_spread,
u_weight_scales: u.weight_scales,
v_weight_scales: v.weight_scales,
weight_closure,
u_work: u.work,
v_work: v.work,
skin_degree_u,
skin_degree_v,
degree_u: surface.degree_u,
degree_v: surface.degree_v,
control_rows: rows,
control_columns: columns,
min_weight,
interpolation_bound,
sampled_interpolation_residual: sampled,
};
Ok((surface, report))
}