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