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