Skip to main content

brep_kernel/geometry/
gordon.rs

1//! Gordon surface through two crossing curve families.
2//!
3//! [`gordon_surface`] takes a family of u-curves (each runs in u at a
4//! constant v) and a family of v-curves (each runs in v at a constant u) that
5//! cross each other once per pair, and returns ONE NURBS surface that
6//! interpolates every one of them.  Geometry only: no face, no naming, no
7//! feature, no schema.
8//!
9//! # The construction
10//!
11//! The Boolean sum `S = L_v + L_u - T`, carried out in homogeneous space:
12//!
13//! * `L_v` skins the u-curves across v: `sum_a F_a(u) beta_a(v)`,
14//! * `L_u` skins the v-curves across u: `sum_b G_b(v) alpha_b(u)`,
15//! * `T` is the tensor interpolant of the crossing grid:
16//!   `sum_a sum_b P_ab alpha_b(u) beta_a(v)`,
17//!
18//! where `alpha_b` and `beta_a` are the CARDINAL B-spline functions of the
19//! crossing parameters — `alpha_b(u_c) = delta_bc` — of degree
20//! `min(3, count - 1)` on the knot-averaging vector of those parameters.  All
21//! three terms use the same cardinal functions, which is what makes the sum
22//! interpolate the curves between the crossings and not only the crossings:
23//! along `v = v_a` the `L_u` and `T` terms differ by
24//! `sum_b alpha_b(u) (G_b(v_a) - P_ab)`, which vanishes when the curves meet.
25//!
26//! Two curves per family is degree-1 cardinal blending: the bilinear Coons
27//! patch of [`crate::coons_bilinear`], reproduced to round-off.
28//!
29//! # Orientation
30//!
31//! The caller's u-curve 0 names +u and orders the v-curves; the caller's
32//! v-curve 0 names +v and orders the u-curves.  Neither is ever reversed, so
33//! the surface's handedness — and its normal `S_u x S_v` — follows those two
34//! curves.  Every other curve is reversed and re-ordered to match, and
35//! [`GordonReport`] says which.
36//!
37//! # What is exact
38//!
39//! * **Every input curve, to round-off.**  The cardinal functions are brought
40//!   into the curves' own compatible knot structure by the Coons compatibility
41//!   algebra — degree elevation and knot insertion, never sampling — so each
42//!   iso-curve of the result at a network parameter is its input curve up to
43//!   the arithmetic of one collocation solve.
44//!   [`GordonReport::interpolation_bound`] is that residual, measured on the
45//!   control nets; [`GordonReport::sampled_interpolation_residual`] measures
46//!   it again with the kernel's surface and curve evaluators.
47//! * **The four boundary curves, bit for bit** after orientation, weight
48//!   reconciliation and compatibility, exactly as in the Coons fill: their
49//!   rows are ASSIGNED into the net, and the four corners take the midpoint of
50//!   the two incident boundary ends.
51//! * **Any surface either skinning reproduces.**  `S` reproduces a surface
52//!   when EITHER family's cardinal space contains it in the skinning
53//!   direction: the iso-curves of a single-span bicubic reproduce it with four
54//!   curves in one family and any number in the other, and the iso-curves of a
55//!   biquadratic rational quadric patch reproduce it with three.
56//!
57//! # What is approximate
58//!
59//! * **The interior, against any surface you had in mind**, wherever neither
60//!   cardinal space contains it.  A Gordon surface is defined by its network.
61//! * **Rational networks** are blended in homogeneous space, so the interior
62//!   is the projective Gordon surface.  Each curve's homogeneous scale is
63//!   reconciled at the crossings first ([`GordonReport::u_weight_scales`]).
64//! * **A network whose curves meet only within the band.**  An interior
65//!   crossing's grid point is the midpoint of the two curves there, so both
66//!   are off by half the gap.  A crossing ON a boundary curve is not split:
67//!   the boundary is kept, and the interior curve absorbs the whole gap at its
68//!   end.  A corner takes the midpoint of its two boundaries, as in the Coons
69//!   fill, and — as there — a corner gap moves the blend near it.
70//!   [`GordonReport::interpolation_bound`] and
71//!   [`GordonReport::sampled_interpolation_residual`] report the result.
72//!
73//! # What refuses
74//!
75//! Everything is checked BEFORE the solve, and every refusal carries the
76//! number it measured — see [`GordonRefusal`].  Out of scope, by name:
77//! closed (periodic) curves, a pair that crosses more than once, a family
78//! that does not cross the other in one consistent order, curves that run
79//! past the network boundary, and crossings that do not share one parameter
80//! per curve across a family (that needs a re-parameterisation, which this
81//! constructor will not do behind the caller's back).  Interior regularity is
82//! not checked: a network that folds builds a surface that folds.
83
84use crate::coons::compat::{self, CompatWork};
85use crate::coons::{corner_band, WEIGHT_CLOSURE_BAND};
86use crate::{
87    intersect_curves, model_scale, solve_dense, KnotVector, NurbsCurve, NurbsSurface, Vec3, Vec4,
88    KNOT_IDENTITY_TOL,
89};
90
91
92/// Largest degree of a cardinal interpolation across a family.  Cubic is the
93/// usual skinning degree; fewer curves lower it to `count - 1`, so two curves
94/// blend linearly (Coons) and three quadratically.
95const MAX_SKIN_DEGREE: usize = 3;
96
97/// Uniform samples per curve used to size the network (`model_scale`), to
98/// judge extents, and to measure the achieved interpolation.
99const CURVE_SAMPLES: usize = 32;
100
101/// Samples per curve for the closest-approach search that measures the gap of
102/// a pair that does not meet.
103const APPROACH_SAMPLES: usize = 96;
104
105/// Newton iterations for polishing a crossing.  A transversal crossing
106/// converges quadratically in a handful; the cap only bounds a pathology.
107const CROSSING_ITERATIONS: usize = 60;
108
109/// A Gordon constructor's refusal, with the measurement that caused it.
110///
111/// `family` is `'u'` for the `u_curves` argument and `'v'` for `v_curves`;
112/// curve indices are the CALLER's indices into those slices.
113#[derive(Clone, Debug, PartialEq)]
114pub enum GordonRefusal {
115    /// A family needs at least two curves: its two boundaries.
116    TooFewCurves { family: char, count: usize },
117    /// A caller-stated crossing band that is not a positive finite distance.
118    InvalidBand { band: f64 },
119    /// The curve has no extent: it is a point, not a curve.
120    DegenerateCurve {
121        family: char,
122        curve: usize,
123        extent: f64,
124        band: f64,
125    },
126    /// The curve's start and end meet: a closed curve.  Periodic networks are
127    /// out of scope — which of the coincident ends a crossing belongs to is
128    /// not decidable from the curves.
129    ClosedCurve {
130        family: char,
131        curve: usize,
132        gap: f64,
133        band: f64,
134    },
135    /// The two curves do not meet: `gap` is their measured closest approach.
136    CurvesDoNotMeet {
137        u_curve: usize,
138        v_curve: usize,
139        gap: f64,
140        band: f64,
141    },
142    /// The two curves meet more than once inside the band.  `crossings` holds
143    /// each `(u-curve parameter, v-curve parameter)` found.
144    MultipleCrossings {
145        u_curve: usize,
146        v_curve: usize,
147        crossings: Vec<(f64, f64)>,
148        band: f64,
149    },
150    /// Along this curve the other family's crossings do not run in one order
151    /// — neither increasing nor decreasing in the order the family's first
152    /// curve sees them — so the two families do not form a grid.
153    /// `parameters` are this curve's crossing parameters in that order.
154    CrossingOrder {
155        family: char,
156        curve: usize,
157        parameters: Vec<f64>,
158    },
159    /// Two curves of `family` cross the other family at the same parameter
160    /// (`separation` apart): the skinning across them has no solution.
161    CoincidentCrossings {
162        family: char,
163        first: usize,
164        second: usize,
165        separation: f64,
166    },
167    /// The curve runs past the network boundary: its crossing with the
168    /// outermost curve of the other family at its `end` (0 = start, 1 = end,
169    /// after orientation) sits at `parameter` instead of the end, and the
170    /// overhang measures `gap` along the curve.
171    Overhang {
172        family: char,
173        curve: usize,
174        end: usize,
175        parameter: f64,
176        gap: f64,
177        band: f64,
178    },
179    /// The two curves meet, but not at their families' shared crossing
180    /// parameters: at those parameters they are `gap` apart.  A Gordon surface
181    /// needs one parameter per crossing curve across the whole family;
182    /// `u_parameter_offset` and `v_parameter_offset` say how far this pair's
183    /// own crossing sits from the shared ones.
184    InconsistentCrossing {
185        u_curve: usize,
186        v_curve: usize,
187        gap: f64,
188        band: f64,
189        u_parameter_offset: f64,
190        v_parameter_offset: f64,
191    },
192    /// The network is rational and no per-curve re-scaling makes the two
193    /// curves' homogeneous weights agree at every crossing: at this pair the
194    /// reconciled weights differ by `ratio`.
195    WeightClosure {
196        u_curve: usize,
197        v_curve: usize,
198        ratio: f64,
199        band: f64,
200    },
201    /// The blend put a control point at or beyond infinity: the rational net
202    /// weight at `(row, column)` is not positive.
203    NonPositiveWeight {
204        row: usize,
205        column: usize,
206        weight: f64,
207    },
208    /// Degree/knot compatibility, or the cardinal collocation, could not be
209    /// reached in `family`.
210    Incompatible { family: char, detail: String },
211    /// An underlying kernel geometry error, verbatim.
212    Geometry(String),
213}
214
215impl std::fmt::Display for GordonRefusal {
216    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            Self::TooFewCurves { family, count } => write!(
219                formatter,
220                "gordon: the {family} family has {count} curve(s) — a network needs at least two \
221                 per family"
222            ),
223            Self::InvalidBand { band } => write!(
224                formatter,
225                "gordon: crossing band {band:.3e} is not a positive finite distance"
226            ),
227            Self::DegenerateCurve {
228                family,
229                curve,
230                extent,
231                band,
232            } => write!(
233                formatter,
234                "gordon: {family}-curve {curve} is degenerate — extent {extent:.3e} is inside the \
235                 band {band:.3e}"
236            ),
237            Self::ClosedCurve {
238                family,
239                curve,
240                gap,
241                band,
242            } => write!(
243                formatter,
244                "gordon: {family}-curve {curve} is closed — its ends are {gap:.3e} apart, inside \
245                 the band {band:.3e}; periodic networks are not supported"
246            ),
247            Self::CurvesDoNotMeet {
248                u_curve,
249                v_curve,
250                gap,
251                band,
252            } => write!(
253                formatter,
254                "gordon: u-curve {u_curve} and v-curve {v_curve} do not meet — closest approach \
255                 {gap:.3e} exceeds the crossing band {band:.3e}"
256            ),
257            Self::MultipleCrossings {
258                u_curve,
259                v_curve,
260                crossings,
261                band,
262            } => write!(
263                formatter,
264                "gordon: u-curve {u_curve} and v-curve {v_curve} meet {} times within the band \
265                 {band:.3e} (at {crossings:?}) — a network pair must cross once",
266                crossings.len()
267            ),
268            Self::CrossingOrder {
269                family,
270                curve,
271                parameters,
272            } => write!(
273                formatter,
274                "gordon: along {family}-curve {curve} the crossings run out of order \
275                 ({parameters:?}) — the families do not form a grid"
276            ),
277            Self::CoincidentCrossings {
278                family,
279                first,
280                second,
281                separation,
282            } => write!(
283                formatter,
284                "gordon: {family}-curves {first} and {second} cross the other family at the same \
285                 parameter ({separation:.3e} apart)"
286            ),
287            Self::Overhang {
288                family,
289                curve,
290                end,
291                parameter,
292                gap,
293                band,
294            } => write!(
295                formatter,
296                "gordon: {family}-curve {curve} runs past the network boundary at its {} — the \
297                 boundary crossing sits at parameter {parameter:.6}, {gap:.3e} along the curve \
298                 (band {band:.3e})",
299                if *end == 0 { "start" } else { "end" }
300            ),
301            Self::InconsistentCrossing {
302                u_curve,
303                v_curve,
304                gap,
305                band,
306                u_parameter_offset,
307                v_parameter_offset,
308            } => write!(
309                formatter,
310                "gordon: u-curve {u_curve} and v-curve {v_curve} are {gap:.3e} apart at the \
311                 families' shared crossing parameters (band {band:.3e}); their own crossing is \
312                 offset by {u_parameter_offset:.3e} in u and {v_parameter_offset:.3e} in v"
313            ),
314            Self::WeightClosure {
315                u_curve,
316                v_curve,
317                ratio,
318                band,
319            } => write!(
320                formatter,
321                "gordon: rational weights do not reconcile at u-curve {u_curve} x v-curve \
322                 {v_curve} — ratio {ratio:.12} (band {band:.3e} about 1)"
323            ),
324            Self::NonPositiveWeight {
325                row,
326                column,
327                weight,
328            } => write!(
329                formatter,
330                "gordon: blended control point ({row}, {column}) has non-positive weight \
331                 {weight:.3e} — the surface would pass through infinity"
332            ),
333            Self::Incompatible { family, detail } => {
334                write!(formatter, "gordon: {family} family incompatible — {detail}")
335            }
336            Self::Geometry(message) => formatter.write_str(message),
337        }
338    }
339}
340
341/// The lossy exit for stringly callers, exactly like [`crate::CoonsRefusal`]'s.
342impl From<GordonRefusal> for String {
343    fn from(refusal: GordonRefusal) -> Self {
344        refusal.to_string()
345    }
346}
347
348/// What the constructor had to do, and what it measured doing it.
349///
350/// Per-curve vectors indexed by caller index say so; everything else about a
351/// curve's POSITION is in network order — v increasing for u-curves, u
352/// increasing for v-curves.
353#[derive(Clone, Debug)]
354pub struct GordonReport {
355    /// Caller index of the u-curve at each network position (v increasing).
356    pub u_order: Vec<usize>,
357    /// Caller index of the v-curve at each network position (u increasing).
358    pub v_order: Vec<usize>,
359    /// Which u-curves were reversed to run with +u.  Caller order.
360    pub u_reversed: Vec<bool>,
361    /// Which v-curves were reversed to run with +v.  Caller order.
362    pub v_reversed: Vec<bool>,
363    /// The u at which each network v-curve sits: `0.0`, the shared interior
364    /// crossing parameters, `1.0`.
365    pub u_parameters: Vec<f64>,
366    /// The v at which each network u-curve sits.
367    pub v_parameters: Vec<f64>,
368    /// The band every crossing was judged against.
369    pub crossing_band: f64,
370    /// Worst closest-approach distance of a pair at its own crossing.
371    pub closest_approach: f64,
372    /// The ADMITTED CROSSING MISS: the worst distance between a u-curve and a
373    /// v-curve at the families' shared crossing parameters.  No surface passes
374    /// through two curves that disagree there, so this is the input's own
375    /// disagreement, and it is what separates "interpolated to round-off"
376    /// (this reads round-off too) from "within the bound because the input
377    /// missed by this much".  Read it beside [`Self::interpolation_bound`].
378    pub crossing_gap: f64,
379    /// Worst offset of a pair's own crossing parameter from the shared one.
380    pub parameter_spread: f64,
381    /// Factor each u-curve's homogeneous control points were scaled by to
382    /// reconcile the weights at the crossings.  All `1.0` for polynomial
383    /// input.  Caller order.
384    pub u_weight_scales: Vec<f64>,
385    /// As [`Self::u_weight_scales`], for the v-curves.
386    pub v_weight_scales: Vec<f64>,
387    /// The reconciled weight ratio farthest from 1 over every crossing.
388    pub weight_closure: f64,
389    /// Degree elevation / knot insertion / domain re-mapping per u-curve.
390    pub u_work: Vec<CompatWork>,
391    /// As [`Self::u_work`], for the v-curves.
392    pub v_work: Vec<CompatWork>,
393    /// Degree of the cardinal functions that skin the v-curves across u.
394    pub skin_degree_u: usize,
395    /// Degree of the cardinal functions that skin the u-curves across v.
396    pub skin_degree_v: usize,
397    pub degree_u: usize,
398    pub degree_v: usize,
399    pub control_rows: usize,
400    pub control_columns: usize,
401    /// Smallest rational weight in the blended control net.
402    pub min_weight: f64,
403    /// Worst homogeneous distance between the surface's iso-curve at a
404    /// network parameter and its compatible (weight-reconciled) input curve,
405    /// control point against control point.  The basis is a non-negative
406    /// partition of unity, so this bounds the homogeneous curve deviation
407    /// everywhere along the curve, not just at samples — in exact arithmetic.
408    /// It is not a bound on [`Self::sampled_interpolation_residual`]: that
409    /// reading is Euclidean and carries two evaluators' round-off, so at
410    /// round-off scale it can read above this, and for rational input the
411    /// Euclidean deviation can exceed the homogeneous one by up to
412    /// `(1 + |point|) / weight`.  Round-off when
413    /// [`Self::crossing_gap`] is; otherwise it carries that miss — half of it
414    /// through an interior crossing, all of it at an interior curve's end on
415    /// a boundary.
416    pub interpolation_bound: f64,
417    /// Worst Euclidean distance between the surface and an input curve,
418    /// evaluated by the kernel's surface and curve evaluators at uniform
419    /// samples of every curve — an independent reading of the same promise.
420    pub sampled_interpolation_residual: f64,
421}
422
423/// The Gordon surface through two crossing curve families, judged against the
424/// kernel's vertex identity band.
425///
426/// See the module documentation for the contract.  Use
427/// [`gordon_surface_reported`] to state the crossing band and read what was
428/// measured.
429pub fn gordon_surface(
430    u_curves: &[NurbsCurve],
431    v_curves: &[NurbsCurve],
432) -> Result<NurbsSurface, GordonRefusal> {
433    gordon_surface_reported(u_curves, v_curves, None).map(|(surface, _)| surface)
434}
435
436/// [`gordon_surface`] with a caller-stated crossing band, plus the
437/// [`GordonReport`] of what it did and measured.
438///
439/// `crossing_band` is the distance within which two curves meet.  `None`
440/// takes the Coons corner band — the kernel's vertex identity band at the
441/// network's size, `heal_band(diagonal, 2e-5)` floored at
442/// [`crate::VERTEX_MATCH_FLOOR`] — so a network this accepts meets where the
443/// validator would call two edge ends one vertex.
444pub fn gordon_surface_reported(
445    u_curves: &[NurbsCurve],
446    v_curves: &[NurbsCurve],
447    crossing_band: Option<f64>,
448) -> Result<(NurbsSurface, GordonReport), GordonRefusal> {
449    let network = prepare(u_curves, v_curves, crossing_band)?;
450    build(network)
451}
452
453/// One family's curves in network order, oriented and weight-reconciled.
454struct Family {
455    /// Caller index at each network position.
456    order: Vec<usize>,
457    /// Oriented, domain-normalised curves in network order (unscaled).
458    curves: Vec<NurbsCurve>,
459    /// Caller order.
460    reversed: Vec<bool>,
461    /// Caller order.
462    weight_scales: Vec<f64>,
463    /// Caller order.
464    work: Vec<CompatWork>,
465    /// The parameter at which each curve of the OTHER family crosses this
466    /// family, in the other family's network order.
467    parameters: Vec<f64>,
468}
469
470/// Everything the solve needs, every check already passed.
471struct Network {
472    u: Family,
473    v: Family,
474    /// `crossings[a][b]`: the homogeneous grid point of network u-curve `a`
475    /// and network v-curve `b` (see `prepare` for which curve's point).
476    crossings: Vec<Vec<Vec4>>,
477    band: f64,
478    closest_approach: f64,
479    crossing_gap: f64,
480    parameter_spread: f64,
481    weight_closure: f64,
482}
483
484/// Uniform 3D samples of a curve over its own domain.
485fn samples(curve: &NurbsCurve, count: usize) -> Result<Vec<Vec3>, String> {
486    let [start, end] = curve.domain()?;
487    (0..=count)
488        .map(|index| curve.evaluate(start + (end - start) * index as f64 / count as f64))
489        .collect()
490}
491
492fn geometry(message: String) -> GordonRefusal {
493    GordonRefusal::Geometry(message)
494}
495
496/// Homogeneous distance between two control points.
497fn homogeneous_distance(first: Vec4, second: Vec4) -> f64 {
498    ((first.x - second.x).powi(2)
499        + (first.y - second.y).powi(2)
500        + (first.z - second.z).powi(2)
501        + (first.w - second.w).powi(2))
502    .sqrt()
503}
504
505/// Midpoint of two homogeneous points.  Exactly either one when they are
506/// equal: `(a + a) * 0.5 == a` in IEEE arithmetic.
507fn homogeneous_midpoint(first: Vec4, second: Vec4) -> Vec4 {
508    first.add(second).scale(0.5)
509}
510
511/// Polish a crossing of `first` and `second` from a seed by Newton on the
512/// squared distance, run to full precision and clamped to both domains.
513///
514/// `intersect_curves` finds and counts crossings but stops at an absolute
515/// 1e-12 residual; a network reproduced "to round-off" needs its crossing
516/// parameters to the last ulp, so each accepted crossing is polished here.
517/// A parameter pinned at a domain end drops to a one-variable Newton in the
518/// other.  Every step is kept only if it does not increase the distance.
519fn polish_crossing(
520    first: &NurbsCurve,
521    second: &NurbsCurve,
522    seed_s: f64,
523    seed_t: f64,
524) -> Result<(f64, f64, f64), String> {
525    let mut s = seed_s.clamp(0.0, 1.0);
526    let mut t = seed_t.clamp(0.0, 1.0);
527    let mut gap = first.evaluate(s)?.sub(second.evaluate(t)?).length();
528    for _ in 0..CROSSING_ITERATIONS {
529        let a = first.derivatives(s, 2)?;
530        let b = second.derivatives(t, 2)?;
531        let residual = a[0].sub(b[0]);
532        let gradient_s = a[1].dot(residual);
533        let gradient_t = -b[1].dot(residual);
534        let h_ss = a[1].dot(a[1]) + a[2].dot(residual);
535        let h_st = -a[1].dot(b[1]);
536        let h_tt = b[1].dot(b[1]) - b[2].dot(residual);
537        let determinant = h_ss * h_tt - h_st * h_st;
538        let (mut step_s, mut step_t) = if determinant.abs() > f64::MIN_POSITIVE {
539            (
540                (-gradient_s * h_tt + gradient_t * h_st) / determinant,
541                (-gradient_t * h_ss + gradient_s * h_st) / determinant,
542            )
543        } else {
544            (0.0, 0.0)
545        };
546        let pinned_s = (s <= 0.0 && step_s < 0.0) || (s >= 1.0 && step_s > 0.0);
547        let pinned_t = (t <= 0.0 && step_t < 0.0) || (t >= 1.0 && step_t > 0.0);
548        if pinned_s && !pinned_t {
549            step_s = 0.0;
550            step_t = if h_tt.abs() > f64::MIN_POSITIVE { -gradient_t / h_tt } else { 0.0 };
551        } else if pinned_t && !pinned_s {
552            step_t = 0.0;
553            step_s = if h_ss.abs() > f64::MIN_POSITIVE { -gradient_s / h_ss } else { 0.0 };
554        } else if pinned_s && pinned_t {
555            break;
556        }
557        let next_s = (s + step_s).clamp(0.0, 1.0);
558        let next_t = (t + step_t).clamp(0.0, 1.0);
559        if next_s == s && next_t == t {
560            break;
561        }
562        let next_gap = first.evaluate(next_s)?.sub(second.evaluate(next_t)?).length();
563        if next_gap > gap {
564            break;
565        }
566        s = next_s;
567        t = next_t;
568        gap = next_gap;
569    }
570    Ok((s, t, gap))
571}
572
573/// The closest approach of two curves that `intersect_curves` found no
574/// crossing for: the best pair of a dense sample grid, polished.
575fn closest_approach(first: &NurbsCurve, second: &NurbsCurve) -> Result<(f64, f64, f64), String> {
576    let first_points = samples(first, APPROACH_SAMPLES)?;
577    let second_points = samples(second, APPROACH_SAMPLES)?;
578    let mut best = (0usize, 0usize, f64::INFINITY);
579    for (i, a) in first_points.iter().enumerate() {
580        for (j, b) in second_points.iter().enumerate() {
581            let distance = a.sub(*b).length();
582            if distance < best.2 {
583                best = (i, j, distance);
584            }
585        }
586    }
587    polish_crossing(
588        first,
589        second,
590        best.0 as f64 / APPROACH_SAMPLES as f64,
591        best.1 as f64 / APPROACH_SAMPLES as f64,
592    )
593}
594
595/// Median of a non-empty slice.  For an exact network every entry is the same
596/// float, and the median IS that float — a mean would re-round it.
597fn median(values: &[f64]) -> f64 {
598    let mut sorted = values.to_vec();
599    sorted.sort_by(f64::total_cmp);
600    let middle = sorted.len() / 2;
601    if sorted.len() % 2 == 1 {
602        sorted[middle]
603    } else if sorted[middle - 1] == sorted[middle] {
604        sorted[middle]
605    } else {
606        0.5 * (sorted[middle - 1] + sorted[middle])
607    }
608}
609
610/// Decide the order of the crossing family along a curve, and whether the
611/// curve runs against it.  `sequence` is the curve's crossing parameters in
612/// the crossing family's network order.  `Ok(false)` keeps it, `Ok(true)`
613/// reverses it.
614fn orientation(
615    family: char,
616    curve: usize,
617    sequence: &[f64],
618    crossing_family: char,
619    crossing_order: &[usize],
620) -> Result<bool, GordonRefusal> {
621    for (index, pair) in sequence.windows(2).enumerate() {
622        let separation = (pair[1] - pair[0]).abs();
623        if separation <= KNOT_IDENTITY_TOL {
624            return Err(GordonRefusal::CoincidentCrossings {
625                family: crossing_family,
626                first: crossing_order[index],
627                second: crossing_order[index + 1],
628                separation,
629            });
630        }
631    }
632    if sequence.windows(2).all(|pair| pair[1] > pair[0]) {
633        Ok(false)
634    } else if sequence.windows(2).all(|pair| pair[1] < pair[0]) {
635        Ok(true)
636    } else {
637        Err(GordonRefusal::CrossingOrder {
638            family,
639            curve,
640            parameters: sequence.to_vec(),
641        })
642    }
643}
644
645/// Orient, order, check and reconcile the network.  Nothing is solved until
646/// every refusal here has been passed.
647fn prepare(
648    u_input: &[NurbsCurve],
649    v_input: &[NurbsCurve],
650    crossing_band: Option<f64>,
651) -> Result<Network, GordonRefusal> {
652    for (family, curves) in [('u', u_input), ('v', v_input)] {
653        if curves.len() < 2 {
654            return Err(GordonRefusal::TooFewCurves {
655                family,
656                count: curves.len(),
657            });
658        }
659    }
660    if let Some(band) = crossing_band {
661        if !(band > 0.0 && band.is_finite()) {
662            return Err(GordonRefusal::InvalidBand { band });
663        }
664    }
665
666    // Domains onto [0, 1] — exact, and the frame every parameter below is in.
667    let (u_curves, u_work) = normalise_domains(u_input)?;
668    let (v_curves, v_work) = normalise_domains(v_input)?;
669    let mut points: Vec<Vec3> = Vec::new();
670    for curve in u_curves.iter().chain(&v_curves) {
671        points.extend(samples(curve, CURVE_SAMPLES).map_err(geometry)?);
672    }
673    let band = crossing_band.unwrap_or_else(|| corner_band(model_scale(points.iter().copied())));
674    judge_curves('u', &u_curves, band)?;
675    judge_curves('v', &v_curves, band)?;
676
677    let crossings = find_crossings(&u_curves, &v_curves, band)?;
678    let order = order_network(&crossings)?;
679
680    // Network order, oriented, crossings re-polished on the curves actually
681    // used: a reversed curve's knots are `1 - k`, re-rounded.
682    let f = oriented(&u_curves, &order.u_order, &order.u_reversed)?;
683    let g = oriented(&v_curves, &order.v_order, &order.v_reversed)?;
684    let (rows, columns) = (f.len(), g.len());
685    // `own_s[a][b]` / `own_t[a][b]`: network pair (a, b)'s own crossing.
686    let mut own_s = vec![vec![0.0f64; columns]; rows];
687    let mut own_t = vec![vec![0.0f64; columns]; rows];
688    for a in 0..rows {
689        for b in 0..columns {
690            let (j, i) = (order.u_order[a], order.v_order[b]);
691            let (s, t) = (crossings.s[j][i], crossings.t[j][i]);
692            let seed_s = if order.u_reversed[j] { 1.0 - s } else { s };
693            let seed_t = if order.v_reversed[i] { 1.0 - t } else { t };
694            let (polished_s, polished_t, _) =
695                polish_crossing(&f[a], &g[b], seed_s, seed_t).map_err(geometry)?;
696            own_s[a][b] = polished_s;
697            own_t[a][b] = polished_t;
698        }
699    }
700
701    // Shared parameters: the network's ends by contract, the median of the
702    // family's own crossings in between.
703    let u_parameters = shared_parameters(columns, |b| (0..rows).map(|a| own_s[a][b]).collect());
704    let v_parameters = shared_parameters(rows, |a| (0..columns).map(|b| own_t[a][b]).collect());
705    for (family, parameters, network_order) in [
706        ('v', &u_parameters, &order.v_order),
707        ('u', &v_parameters, &order.u_order),
708    ] {
709        for (index, pair) in parameters.windows(2).enumerate() {
710            let separation = pair[1] - pair[0];
711            if separation <= KNOT_IDENTITY_TOL {
712                return Err(GordonRefusal::CoincidentCrossings {
713                    family,
714                    first: network_order[index],
715                    second: network_order[index + 1],
716                    separation,
717                });
718            }
719        }
720    }
721    let shared = SharedCrossings {
722        f: &f,
723        g: &g,
724        u_parameters: &u_parameters,
725        v_parameters: &v_parameters,
726        own_s: &own_s,
727        own_t: &own_t,
728        u_order: &order.u_order,
729        v_order: &order.v_order,
730    };
731    let (crossing_gap, parameter_spread) = judge_shared_crossings(&shared, band)?;
732    let (f_scales, g_scales, weight_closure) = reconcile_weights(&shared)?;
733    let f_scaled = scaled(&f, &f_scales)?;
734    let g_scaled = scaled(&g, &g_scales)?;
735    let grid = crossing_grid(&f_scaled, &g_scaled, &u_parameters, &v_parameters)?;
736
737    // Report vectors go back to caller order.
738    let mut u_weight_scales = vec![1.0f64; rows];
739    for (a, &j) in order.u_order.iter().enumerate() {
740        u_weight_scales[j] = f_scales[a];
741    }
742    let mut v_weight_scales = vec![1.0f64; columns];
743    for (b, &i) in order.v_order.iter().enumerate() {
744        v_weight_scales[i] = g_scales[b];
745    }
746    // `u_work` / `v_work` carry the domain records; `build` adds the
747    // elevation and insertion counts.
748    Ok(Network {
749        u: Family {
750            order: order.u_order,
751            curves: f_scaled,
752            reversed: order.u_reversed,
753            weight_scales: u_weight_scales,
754            work: u_work,
755            parameters: u_parameters,
756        },
757        v: Family {
758            order: order.v_order,
759            curves: g_scaled,
760            reversed: order.v_reversed,
761            weight_scales: v_weight_scales,
762            work: v_work,
763            parameters: v_parameters,
764        },
765        crossings: grid,
766        band,
767        closest_approach: crossings.closest,
768        crossing_gap,
769        parameter_spread,
770        weight_closure,
771    })
772}
773
774/// Re-map every curve's knot domain onto `[0, 1]`, recording which moved.
775fn normalise_domains(
776    curves: &[NurbsCurve],
777) -> Result<(Vec<NurbsCurve>, Vec<CompatWork>), GordonRefusal> {
778    let mut normalised = Vec::with_capacity(curves.len());
779    let mut work = vec![CompatWork::default(); curves.len()];
780    for (index, curve) in curves.iter().enumerate() {
781        let (moved, remapped) = compat::normalise_domain(curve).map_err(geometry)?;
782        work[index].domain_remapped = remapped;
783        normalised.push(moved);
784    }
785    Ok((normalised, work))
786}
787
788/// Refuse a point-like or closed curve.
789fn judge_curves(family: char, curves: &[NurbsCurve], band: f64) -> Result<(), GordonRefusal> {
790    for (index, curve) in curves.iter().enumerate() {
791        let points = samples(curve, CURVE_SAMPLES).map_err(geometry)?;
792        let spread = points
793            .iter()
794            .map(|point| point.sub(points[0]).length())
795            .fold(0.0f64, f64::max);
796        let extent = model_scale(points.iter().copied()).min(spread);
797        if extent <= band {
798            return Err(GordonRefusal::DegenerateCurve {
799                family,
800                curve: index,
801                extent,
802                band,
803            });
804        }
805        let gap = points[0].sub(points[points.len() - 1]).length();
806        if gap <= band {
807            return Err(GordonRefusal::ClosedCurve {
808                family,
809                curve: index,
810                gap,
811                band,
812            });
813        }
814    }
815    Ok(())
816}
817
818/// Every caller pair's single crossing.
819struct Crossings {
820    /// `s[j][i]`: the parameter on caller u-curve `j` where it meets caller
821    /// v-curve `i`.
822    s: Vec<Vec<f64>>,
823    /// `t[j][i]`: the parameter on caller v-curve `i`.
824    t: Vec<Vec<f64>>,
825    /// Worst closest-approach distance over every pair.
826    closest: f64,
827}
828
829/// Find the one crossing of every u-curve with every v-curve, or refuse the
830/// pair that has none or several.
831fn find_crossings(
832    u_curves: &[NurbsCurve],
833    v_curves: &[NurbsCurve],
834    band: f64,
835) -> Result<Crossings, GordonRefusal> {
836    let mut crossings = Crossings {
837        s: vec![vec![0.0f64; v_curves.len()]; u_curves.len()],
838        t: vec![vec![0.0f64; v_curves.len()]; u_curves.len()],
839        closest: 0.0,
840    };
841    for (j, u_curve) in u_curves.iter().enumerate() {
842        for (i, v_curve) in v_curves.iter().enumerate() {
843            let hits = intersect_curves(u_curve, v_curve, band).map_err(geometry)?;
844            let (s, t, gap) = match hits.len() {
845                1 => polish_crossing(u_curve, v_curve, hits[0].s, hits[0].t).map_err(geometry)?,
846                0 => {
847                    let approach = closest_approach(u_curve, v_curve).map_err(geometry)?;
848                    if approach.2 > band {
849                        return Err(GordonRefusal::CurvesDoNotMeet {
850                            u_curve: j,
851                            v_curve: i,
852                            gap: approach.2,
853                            band,
854                        });
855                    }
856                    approach
857                }
858                _ => {
859                    return Err(GordonRefusal::MultipleCrossings {
860                        u_curve: j,
861                        v_curve: i,
862                        crossings: hits.iter().map(|hit| (hit.s, hit.t)).collect(),
863                        band,
864                    })
865                }
866            };
867            crossings.s[j][i] = s;
868            crossings.t[j][i] = t;
869            crossings.closest = crossings.closest.max(gap);
870        }
871    }
872    Ok(crossings)
873}
874
875/// The network order and each curve's orientation.
876struct NetworkOrder {
877    /// Caller index of the u-curve at each network position.
878    u_order: Vec<usize>,
879    v_order: Vec<usize>,
880    /// Caller order.
881    u_reversed: Vec<bool>,
882    v_reversed: Vec<bool>,
883}
884
885/// Caller u-curve 0 names +u and orders the v-curves; caller v-curve 0 names
886/// +v and orders the u-curves.  Every other curve must see the other family in
887/// that order or its reverse.
888fn order_network(crossings: &Crossings) -> Result<NetworkOrder, GordonRefusal> {
889    let (s, t) = (&crossings.s, &crossings.t);
890    let (rows, columns) = (s.len(), s[0].len());
891    let mut v_order: Vec<usize> = (0..columns).collect();
892    v_order.sort_by(|&a, &b| s[0][a].total_cmp(&s[0][b]));
893    let mut u_order: Vec<usize> = (0..rows).collect();
894    u_order.sort_by(|&a, &b| t[a][0].total_cmp(&t[b][0]));
895    let mut u_reversed = vec![false; rows];
896    for (j, reversed) in u_reversed.iter_mut().enumerate() {
897        let sequence: Vec<f64> = v_order.iter().map(|&i| s[j][i]).collect();
898        *reversed = orientation('u', j, &sequence, 'v', &v_order)?;
899    }
900    let mut v_reversed = vec![false; columns];
901    for (i, reversed) in v_reversed.iter_mut().enumerate() {
902        let sequence: Vec<f64> = u_order.iter().map(|&j| t[j][i]).collect();
903        *reversed = orientation('v', i, &sequence, 'u', &u_order)?;
904    }
905    Ok(NetworkOrder {
906        u_order,
907        v_order,
908        u_reversed,
909        v_reversed,
910    })
911}
912
913/// The caller's curves in network order, reversed where the order says.
914fn oriented(
915    curves: &[NurbsCurve],
916    order: &[usize],
917    reversed: &[bool],
918) -> Result<Vec<NurbsCurve>, GordonRefusal> {
919    order
920        .iter()
921        .map(|&index| {
922            if reversed[index] {
923                curves[index].reversed().map_err(geometry)
924            } else {
925                Ok(curves[index].clone())
926            }
927        })
928        .collect()
929}
930
931/// `0.0`, then the median of each interior position's own crossings, then
932/// `1.0`.
933fn shared_parameters(count: usize, own: impl Fn(usize) -> Vec<f64>) -> Vec<f64> {
934    (0..count)
935        .map(|index| {
936            if index == 0 {
937                0.0
938            } else if index == count - 1 {
939                1.0
940            } else {
941                median(&own(index))
942            }
943        })
944        .collect()
945}
946
947/// The oriented network at its shared crossing parameters.
948struct SharedCrossings<'a> {
949    f: &'a [NurbsCurve],
950    g: &'a [NurbsCurve],
951    u_parameters: &'a [f64],
952    v_parameters: &'a [f64],
953    own_s: &'a [Vec<f64>],
954    own_t: &'a [Vec<f64>],
955    u_order: &'a [usize],
956    v_order: &'a [usize],
957}
958
959/// Judge every pair at the shared parameters and return the worst gap and
960/// parameter offset.  Overhangs first — a boundary crossing that sits inside a
961/// curve is a curve running past the network — then the WORST inconsistent
962/// pair, so the refusal names the culprit rather than the first pair it moved.
963fn judge_shared_crossings(
964    shared: &SharedCrossings<'_>,
965    band: f64,
966) -> Result<(f64, f64), GordonRefusal> {
967    let (rows, columns) = (shared.f.len(), shared.g.len());
968    let mut crossing_gap = 0.0f64;
969    let mut spread = 0.0f64;
970    let mut worst: Option<GordonRefusal> = None;
971    for a in 0..rows {
972        for b in 0..columns {
973            let own_s = shared.own_s[a][b];
974            let own_t = shared.own_t[a][b];
975            let on_u = shared.f[a].evaluate(shared.u_parameters[b]).map_err(geometry)?;
976            let on_v = shared.g[b].evaluate(shared.v_parameters[a]).map_err(geometry)?;
977            let gap = on_u.sub(on_v).length();
978            let u_offset = (own_s - shared.u_parameters[b]).abs();
979            let v_offset = (own_t - shared.v_parameters[a]).abs();
980            if b == 0 || b == columns - 1 {
981                let along = on_u.sub(shared.f[a].evaluate(own_s).map_err(geometry)?).length();
982                if along > band {
983                    return Err(GordonRefusal::Overhang {
984                        family: 'u',
985                        curve: shared.u_order[a],
986                        end: usize::from(b == columns - 1),
987                        parameter: own_s,
988                        gap: along,
989                        band,
990                    });
991                }
992            }
993            if a == 0 || a == rows - 1 {
994                let along = on_v.sub(shared.g[b].evaluate(own_t).map_err(geometry)?).length();
995                if along > band {
996                    return Err(GordonRefusal::Overhang {
997                        family: 'v',
998                        curve: shared.v_order[b],
999                        end: usize::from(a == rows - 1),
1000                        parameter: own_t,
1001                        gap: along,
1002                        band,
1003                    });
1004                }
1005            }
1006            if gap > band && gap > crossing_gap {
1007                worst = Some(GordonRefusal::InconsistentCrossing {
1008                    u_curve: shared.u_order[a],
1009                    v_curve: shared.v_order[b],
1010                    gap,
1011                    band,
1012                    u_parameter_offset: u_offset,
1013                    v_parameter_offset: v_offset,
1014                });
1015            }
1016            crossing_gap = crossing_gap.max(gap);
1017            spread = spread.max(u_offset).max(v_offset);
1018        }
1019    }
1020    match worst {
1021        Some(refusal) => Err(refusal),
1022        None => Ok((crossing_gap, spread)),
1023    }
1024}
1025
1026/// Per-curve homogeneous scales that make the two curves' weights agree at
1027/// every crossing, and the reconciled ratio farthest from 1.
1028///
1029/// Two curves share ONE homogeneous crossing point only when their weights
1030/// there agree, and a curve may be re-scaled freely.  Network u-curve 0 keeps
1031/// scale 1, every v-curve is scaled against it, every other u-curve against
1032/// v-curve 0, and then every remaining crossing must close.  Polynomial input
1033/// is left untouched, bit for bit.
1034fn reconcile_weights(
1035    shared: &SharedCrossings<'_>,
1036) -> Result<(Vec<f64>, Vec<f64>, f64), GordonRefusal> {
1037    let (rows, columns) = (shared.f.len(), shared.g.len());
1038    let mut f_scales = vec![1.0f64; rows];
1039    let mut g_scales = vec![1.0f64; columns];
1040    let polynomial = shared
1041        .f
1042        .iter()
1043        .chain(shared.g)
1044        .all(|curve| curve.control_points.iter().all(|point| point.w == 1.0));
1045    if polynomial {
1046        return Ok((f_scales, g_scales, 1.0));
1047    }
1048    let mut f_weight = vec![vec![1.0f64; columns]; rows];
1049    let mut g_weight = vec![vec![1.0f64; columns]; rows];
1050    for a in 0..rows {
1051        for b in 0..columns {
1052            f_weight[a][b] = shared.f[a]
1053                .evaluate_homogeneous(shared.u_parameters[b])
1054                .map_err(geometry)?
1055                .w;
1056            g_weight[a][b] = shared.g[b]
1057                .evaluate_homogeneous(shared.v_parameters[a])
1058                .map_err(geometry)?
1059                .w;
1060        }
1061    }
1062    for b in 0..columns {
1063        g_scales[b] = f_weight[0][b] / g_weight[0][b];
1064    }
1065    for a in 1..rows {
1066        f_scales[a] = g_scales[0] * g_weight[a][0] / f_weight[a][0];
1067    }
1068    let mut weight_closure = 1.0f64;
1069    for a in 0..rows {
1070        for b in 0..columns {
1071            let ratio = f_scales[a] * f_weight[a][b] / (g_scales[b] * g_weight[a][b]);
1072            if (ratio - 1.0).abs() > (weight_closure - 1.0).abs() {
1073                weight_closure = ratio;
1074            }
1075            if (ratio - 1.0).abs() > WEIGHT_CLOSURE_BAND {
1076                return Err(GordonRefusal::WeightClosure {
1077                    u_curve: shared.u_order[a],
1078                    v_curve: shared.v_order[b],
1079                    ratio,
1080                    band: WEIGHT_CLOSURE_BAND,
1081                });
1082            }
1083        }
1084    }
1085    Ok((f_scales, g_scales, weight_closure))
1086}
1087
1088/// Each curve's homogeneous control points times its scale; a scale of
1089/// exactly 1 returns the curve unchanged.
1090fn scaled(curves: &[NurbsCurve], scales: &[f64]) -> Result<Vec<NurbsCurve>, GordonRefusal> {
1091    curves
1092        .iter()
1093        .zip(scales)
1094        .map(|(curve, &factor)| {
1095            if factor == 1.0 {
1096                return Ok(curve.clone());
1097            }
1098            NurbsCurve::new(
1099                curve.degree,
1100                curve.knots.clone(),
1101                curve.control_points.iter().map(|point| point.scale(factor)).collect(),
1102            )
1103            .map_err(geometry)
1104        })
1105        .collect()
1106}
1107
1108/// The homogeneous crossing grid `P_ab` the tensor term interpolates.
1109///
1110/// Along `v = v_a` the surface is `F_a(u)` plus
1111/// `sum_b alpha_b(u) (G_b(v_a) - P_ab)`, so a curve is interpolated exactly
1112/// when the grid takes the OTHER family's values across it.  A corner and an
1113/// interior crossing take the midpoint and split any gap.  A crossing of a
1114/// boundary curve with an interior curve takes the INTERIOR curve's point:
1115/// that is what makes the boundary row the boundary curve before it is
1116/// assigned, so the assignment moves nothing but round-off, and the interior
1117/// curve absorbs the gap at its end.
1118fn crossing_grid(
1119    f: &[NurbsCurve],
1120    g: &[NurbsCurve],
1121    u_parameters: &[f64],
1122    v_parameters: &[f64],
1123) -> Result<Vec<Vec<Vec4>>, GordonRefusal> {
1124    let (last_row, last_column) = (f.len() - 1, g.len() - 1);
1125    let mut grid = Vec::with_capacity(f.len());
1126    for (a, u_curve) in f.iter().enumerate() {
1127        let mut row = Vec::with_capacity(g.len());
1128        for (b, v_curve) in g.iter().enumerate() {
1129            let on_u = u_curve.evaluate_homogeneous(u_parameters[b]).map_err(geometry)?;
1130            let on_v = v_curve.evaluate_homogeneous(v_parameters[a]).map_err(geometry)?;
1131            let boundary_u = a == 0 || a == last_row;
1132            let boundary_v = b == 0 || b == last_column;
1133            row.push(match (boundary_u, boundary_v) {
1134                (true, false) => on_v,
1135                (false, true) => on_u,
1136                _ => homogeneous_midpoint(on_u, on_v),
1137            });
1138        }
1139        grid.push(row);
1140    }
1141    Ok(grid)
1142}
1143
1144/// The cardinal B-spline functions of a strictly increasing parameter list
1145/// running from exactly `0.0` to exactly `1.0`: degree `min(3, count - 1)` on
1146/// the knot-averaging vector (Piegl & Tiller 9.8, which satisfies
1147/// Schoenberg-Whitney), with coefficients `A^-1 e_j` of the collocation matrix
1148/// `A[r][c] = N_c(parameters[r])`.
1149///
1150/// Each function comes back as a SCALAR curve — the value in `x`, weight 1 —
1151/// so the Coons compatibility algebra can elevate and refine it alongside the
1152/// curves it will multiply.  The first and last coefficients are the values at
1153/// the clamped ends and are set exactly: `delta_j0` and `delta_jn`.
1154fn cardinal_functions(parameters: &[f64]) -> Result<(usize, Vec<NurbsCurve>), String> {
1155    let count = parameters.len();
1156    let degree = MAX_SKIN_DEGREE.min(count - 1);
1157    let mut knots = vec![0.0f64; count + degree + 1];
1158    for knot in &mut knots[count..] {
1159        *knot = 1.0;
1160    }
1161    for j in 1..count - degree {
1162        knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
1163    }
1164    let knot_vector = KnotVector::new(knots.clone(), degree)?;
1165    let mut matrix = vec![vec![0.0f64; count]; count];
1166    for (row, &parameter) in parameters.iter().enumerate() {
1167        let span = knot_vector.find_span(parameter);
1168        for (offset, value) in knot_vector
1169            .basis_functions(span, parameter)
1170            .into_iter()
1171            .enumerate()
1172        {
1173            matrix[row][span - degree + offset] = value;
1174        }
1175    }
1176    let mut functions = Vec::with_capacity(count);
1177    for j in 0..count {
1178        let mut rhs = vec![0.0f64; count];
1179        rhs[j] = 1.0;
1180        let mut coefficients = solve_dense(matrix.clone(), rhs)
1181            .map_err(|error| format!("cardinal collocation at {parameters:?}: {error}"))?;
1182        coefficients[0] = if j == 0 { 1.0 } else { 0.0 };
1183        coefficients[count - 1] = if j == count - 1 { 1.0 } else { 0.0 };
1184        functions.push(NurbsCurve::new(
1185            degree,
1186            knots.clone(),
1187            coefficients
1188                .into_iter()
1189                .map(|value| Vec4 { x: value, y: 0.0, z: 0.0, w: 1.0 })
1190                .collect(),
1191        )?);
1192    }
1193    Ok((degree, functions))
1194}
1195
1196/// Make one family and the OTHER family's cardinal functions compatible in
1197/// this family's running direction.  Returns the compatible curves, their
1198/// cardinal coefficient rows, and the per-curve work.
1199fn compatible_direction(
1200    family: char,
1201    curves: &[NurbsCurve],
1202    cardinals: &[NurbsCurve],
1203) -> Result<(Vec<NurbsCurve>, Vec<Vec<f64>>, Vec<CompatWork>), GordonRefusal> {
1204    let combined: Vec<NurbsCurve> = curves.iter().chain(cardinals).cloned().collect();
1205    let (compatible, work) = compat::make_family_compatible(&combined, 1)
1206        .map_err(|detail| GordonRefusal::Incompatible { family, detail })?;
1207    let coefficients = compatible[curves.len()..]
1208        .iter()
1209        .map(|function| function.control_points.iter().map(|point| point.x).collect())
1210        .collect();
1211    Ok((
1212        compatible[..curves.len()].to_vec(),
1213        coefficients,
1214        work[..curves.len()].to_vec(),
1215    ))
1216}
1217
1218/// Solve the Boolean sum on the prepared network and measure the result.
1219fn build(network: Network) -> Result<(NurbsSurface, GordonReport), GordonRefusal> {
1220    let Network {
1221        mut u,
1222        mut v,
1223        crossings,
1224        band,
1225        closest_approach,
1226        crossing_gap,
1227        parameter_spread,
1228        weight_closure,
1229    } = network;
1230    let rows_in_network = u.curves.len();
1231    let columns_in_network = v.curves.len();
1232
1233    // alpha_b(u) skins the v-curves across u; beta_a(v) skins the u-curves
1234    // across v.
1235    let (skin_degree_u, alphas) = cardinal_functions(&u.parameters).map_err(|detail| {
1236        GordonRefusal::Incompatible {
1237            family: 'v',
1238            detail,
1239        }
1240    })?;
1241    let (skin_degree_v, betas) = cardinal_functions(&v.parameters).map_err(|detail| {
1242        GordonRefusal::Incompatible {
1243            family: 'u',
1244            detail,
1245        }
1246    })?;
1247    let (f, alpha, f_work) = compatible_direction('u', &u.curves, &alphas)?;
1248    let (g, beta, g_work) = compatible_direction('v', &v.curves, &betas)?;
1249    for (a, &j) in u.order.iter().enumerate() {
1250        u.work[j].elevated_by = f_work[a].elevated_by;
1251        u.work[j].knots_inserted = f_work[a].knots_inserted;
1252    }
1253    for (b, &i) in v.order.iter().enumerate() {
1254        v.work[i].elevated_by = g_work[b].elevated_by;
1255        v.work[i].knots_inserted = g_work[b].knots_inserted;
1256    }
1257
1258    let rows = f[0].control_points.len();
1259    let columns = g[0].control_points.len();
1260    let zero = Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
1261    let mut net = vec![vec![zero; columns]; rows];
1262    // The tensor term's u-side, once per (row, u-curve): sum_b P_ab alpha_b[k].
1263    let crossing_rows: Vec<Vec<Vec4>> = (0..rows)
1264        .map(|k| {
1265            (0..rows_in_network)
1266                .map(|a| {
1267                    (0..columns_in_network).fold(zero, |sum, b| {
1268                        sum.add(crossings[a][b].scale(alpha[b][k]))
1269                    })
1270                })
1271                .collect()
1272        })
1273        .collect();
1274    for (k, net_row) in net.iter_mut().enumerate() {
1275        for (l, cell) in net_row.iter_mut().enumerate() {
1276            let mut sum = zero;
1277            for a in 0..rows_in_network {
1278                // L_v - T along u-curve a: (F_a[k] - sum_b P_ab alpha_b[k]) beta_a[l].
1279                sum = sum.add(
1280                    f[a].control_points[k]
1281                        .add(crossing_rows[k][a].scale(-1.0))
1282                        .scale(beta[a][l]),
1283                );
1284            }
1285            for b in 0..columns_in_network {
1286                sum = sum.add(g[b].control_points[l].scale(alpha[b][k]));
1287            }
1288            *cell = sum;
1289        }
1290    }
1291    // The boundary rows are ASSIGNED, not summed, as in the Coons fill: the
1292    // four boundary curves come back bit for bit, the corners take the
1293    // midpoint of their two boundary ends.
1294    let (last_row, last_column) = (rows - 1, columns - 1);
1295    let (f_first, f_last) = (&f[0], &f[rows_in_network - 1]);
1296    let (g_first, g_last) = (&g[0], &g[columns_in_network - 1]);
1297    for (k, net_row) in net.iter_mut().enumerate() {
1298        net_row[0] = f_first.control_points[k];
1299        net_row[last_column] = f_last.control_points[k];
1300    }
1301    for l in 0..columns {
1302        net[0][l] = g_first.control_points[l];
1303        net[last_row][l] = g_last.control_points[l];
1304    }
1305    net[0][0] = homogeneous_midpoint(f_first.control_points[0], g_first.control_points[0]);
1306    net[last_row][0] =
1307        homogeneous_midpoint(f_first.control_points[last_row], g_last.control_points[0]);
1308    net[0][last_column] =
1309        homogeneous_midpoint(f_last.control_points[0], g_first.control_points[last_column]);
1310    net[last_row][last_column] = homogeneous_midpoint(
1311        f_last.control_points[last_row],
1312        g_last.control_points[last_column],
1313    );
1314
1315    let mut min_weight = f64::INFINITY;
1316    let mut worst = (0usize, 0usize);
1317    for (row, net_row) in net.iter().enumerate() {
1318        for (column, cell) in net_row.iter().enumerate() {
1319            if !(cell.w >= min_weight) {
1320                min_weight = cell.w;
1321                worst = (row, column);
1322            }
1323        }
1324    }
1325    if !(min_weight > 0.0) || !min_weight.is_finite() {
1326        return Err(GordonRefusal::NonPositiveWeight {
1327            row: worst.0,
1328            column: worst.1,
1329            weight: min_weight,
1330        });
1331    }
1332
1333    let surface = NurbsSurface::new(
1334        f[0].degree,
1335        g[0].degree,
1336        f[0].knots.clone(),
1337        g[0].knots.clone(),
1338        net,
1339    )
1340    .map_err(geometry)?;
1341
1342    // Measured, never claimed: each input curve against the surface's
1343    // iso-curve at its network parameter — control nets first, then the
1344    // kernel's evaluators at samples.
1345    let mut interpolation_bound = 0.0f64;
1346    let mut sampled = 0.0f64;
1347    for (a, curve) in f.iter().enumerate() {
1348        let iso = surface.iso_curve_v(v.parameters[a]).map_err(geometry)?;
1349        for (on_surface, on_curve) in iso.control_points.iter().zip(&curve.control_points) {
1350            interpolation_bound =
1351                interpolation_bound.max(homogeneous_distance(*on_surface, *on_curve));
1352        }
1353        for index in 0..=CURVE_SAMPLES {
1354            let parameter = index as f64 / CURVE_SAMPLES as f64;
1355            let on_surface = surface.evaluate(parameter, v.parameters[a]).map_err(geometry)?;
1356            let on_curve = u.curves[a].evaluate(parameter).map_err(geometry)?;
1357            sampled = sampled.max(on_surface.sub(on_curve).length());
1358        }
1359    }
1360    for (b, curve) in g.iter().enumerate() {
1361        let iso = surface.iso_curve_u(u.parameters[b]).map_err(geometry)?;
1362        for (on_surface, on_curve) in iso.control_points.iter().zip(&curve.control_points) {
1363            interpolation_bound =
1364                interpolation_bound.max(homogeneous_distance(*on_surface, *on_curve));
1365        }
1366        for index in 0..=CURVE_SAMPLES {
1367            let parameter = index as f64 / CURVE_SAMPLES as f64;
1368            let on_surface = surface.evaluate(u.parameters[b], parameter).map_err(geometry)?;
1369            let on_curve = v.curves[b].evaluate(parameter).map_err(geometry)?;
1370            sampled = sampled.max(on_surface.sub(on_curve).length());
1371        }
1372    }
1373
1374    let report = GordonReport {
1375        u_order: u.order,
1376        v_order: v.order,
1377        u_reversed: u.reversed,
1378        v_reversed: v.reversed,
1379        u_parameters: u.parameters,
1380        v_parameters: v.parameters,
1381        crossing_band: band,
1382        closest_approach,
1383        crossing_gap,
1384        parameter_spread,
1385        u_weight_scales: u.weight_scales,
1386        v_weight_scales: v.weight_scales,
1387        weight_closure,
1388        u_work: u.work,
1389        v_work: v.work,
1390        skin_degree_u,
1391        skin_degree_v,
1392        degree_u: surface.degree_u,
1393        degree_v: surface.degree_v,
1394        control_rows: rows,
1395        control_columns: columns,
1396        min_weight,
1397        interpolation_bound,
1398        sampled_interpolation_residual: sampled,
1399    };
1400    Ok((surface, report))
1401}