Skip to main content

brep_kernel/geometry/coons/
ribbons.rs

1//! Coons fill with prescribed tangent ribbons.
2//!
3//! [`coons_with_ribbons`] is deliberately a SEPARATE constructor from
4//! [`super::coons_bilinear`]: boundary interpolation and tangent-ribbon
5//! interpolation are different promises with different preconditions, and
6//! mixing them would let a caller believe a patch carries a cross-boundary
7//! constraint it was never given.
8//!
9//! # The ribbon convention
10//!
11//! Boundary roles follow the caller's cyclic order and never move: boundary 0
12//! is `v = 0`, boundary 1 is `u = 1`, boundary 2 is `v = 1`, boundary 3 is
13//! `u = 0`.  Ribbon `k` is a vector-valued curve — a [`NurbsCurve`] whose
14//! control points ARE the vectors — giving the patch's derivative with respect
15//! to the parameter that crosses that boundary, in its INCREASING direction:
16//!
17//! * ribbons 0 and 2 are `dS/dv` along `v = 0` and `v = 1`;
18//! * ribbons 3 and 1 are `dS/du` along `u = 0` and `u = 1`.
19//!
20//! Both parameters run `0 -> 1` across the patch, so two patches that share a
21//! boundary share ONE ribbon field there, with no sign to get wrong.  Each
22//! ribbon is parameterised over its own boundary's parameter as supplied; if
23//! the constructor reverses that boundary it reverses the ribbon with it.
24//!
25//! # What is exact
26//!
27//! * With ribbons on ONE direction only (the other blended linearly), the
28//!   prescribed cross-derivative field is interpolated EXACTLY — the correction
29//!   term cancels the linear direction's own contribution identically, and no
30//!   corner twist enters.  [`super::RibbonReport::cross_derivative_residual`]
31//!   measures it on the built patch regardless.
32//! * The boundaries stay exact exactly as in the bilinear constructor.
33//!
34//! # What is approximate, and why nothing here is called G1 unprompted
35//!
36//! With ribbons on BOTH directions each corner is handed two twist vectors —
37//! `d(dS/dv)/du` from the v-ribbon and `d(dS/du)/dv` from the u-ribbon — and a
38//! bicubically blended Coons patch has room for one.  This constructor carries
39//! their mean and REPORTS the disagreement
40//! ([`super::RibbonReport::twist_discrepancy`]); the cross-derivative residual
41//! it then measures is the honest statement of what the patch achieved.  Read
42//! that number before calling the join G1.
43//!
44//! # What refuses
45//!
46//! Rational input (a prescribed 3D cross-derivative does not determine a
47//! homogeneous ribbon, so this is refused rather than approximated), half a
48//! ribbon pair, and a ribbon whose end vector contradicts the tangent of the
49//! boundary it meets there — without that agreement no patch can carry both.
50
51use super::compat;
52use super::{
53    corner_band, report_skeleton, CoonsRefusal, CoonsReport, CompatWork, RibbonReport,
54    BOUNDARY_SAMPLES,
55};
56use crate::{model_scale, NurbsCurve, NurbsSurface, Vec3, Vec4};
57
58/// Largest `|w - 1|` a curve may carry and still count as polynomial.
59const POLYNOMIAL_WEIGHT_BAND: f64 = 1e-12;
60
61/// The Coons patch through four boundaries that also interpolates the
62/// prescribed cross-boundary derivative fields.
63///
64/// See the module documentation for the ribbon convention, the exactness
65/// contract and the refusals.  With every ribbon `None` this is exactly
66/// [`super::coons_bilinear_reported`].
67pub fn coons_with_ribbons(
68    boundaries: [NurbsCurve; 4],
69    ribbons: [Option<NurbsCurve>; 4],
70) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
71    let v_prescribed = usize::from(ribbons[0].is_some()) + usize::from(ribbons[2].is_some());
72    let u_prescribed = usize::from(ribbons[1].is_some()) + usize::from(ribbons[3].is_some());
73    if v_prescribed == 1 {
74        return Err(CoonsRefusal::RibbonPairing {
75            direction: 'v',
76            prescribed: v_prescribed,
77        });
78    }
79    if u_prescribed == 1 {
80        return Err(CoonsRefusal::RibbonPairing {
81            direction: 'u',
82            prescribed: u_prescribed,
83        });
84    }
85    if v_prescribed == 0 && u_prescribed == 0 {
86        return super::coons_bilinear_reported(boundaries);
87    }
88    build(boundaries, ribbons, v_prescribed == 2, u_prescribed == 2)
89}
90
91/// Euclidean point of a homogeneous control point.
92fn point_of(control: Vec4) -> Vec3 {
93    Vec3::new(control.x / control.w, control.y / control.w, control.z / control.w)
94}
95
96/// Clamped Bezier knot vector of the given degree.
97fn bezier_knots(degree: usize) -> Vec<f64> {
98    let mut knots = vec![0.0; degree + 1];
99    knots.extend(std::iter::repeat_n(1.0, degree + 1));
100    knots
101}
102
103/// Express a single Bezier segment's control points in the target degree and
104/// knot vector, by elevation and knot insertion only.
105///
106/// This is how the cubic Hermite (and linear) blend functions enter the
107/// patch's own tensor space without a fit: a polynomial of degree `n` lives in
108/// every spline space of degree >= `n` over any knot vector, and elevation
109/// plus insertion is the exact change of basis.
110fn express(
111    points: &[Vec3],
112    target_degree: usize,
113    target_knots: &[f64],
114) -> Result<Vec<Vec3>, String> {
115    let native_degree = points.len() - 1;
116    let curve = NurbsCurve::new(
117        native_degree,
118        bezier_knots(native_degree),
119        points
120            .iter()
121            .map(|point| Vec4::from_point(*point, 1.0))
122            .collect(),
123    )?;
124    let elevated = compat::elevate_degree(&curve, target_degree)?;
125    let carrier = NurbsCurve::new(
126        target_degree,
127        target_knots.to_vec(),
128        vec![
129            Vec4 {
130                x: 0.0,
131                y: 0.0,
132                z: 0.0,
133                w: 1.0
134            };
135            target_knots.len() - target_degree - 1
136        ],
137    )?;
138    let (_, refined, _, _) = compat::merge_knots(&carrier, &elevated)?;
139    Ok(refined.control_points.iter().copied().map(point_of).collect())
140}
141
142/// The cubic Hermite Bezier control points of a span with prescribed end
143/// points and end derivatives (with respect to the span's own `[0, 1]`).
144fn hermite(start: Vec3, start_derivative: Vec3, end: Vec3, end_derivative: Vec3) -> [Vec3; 4] {
145    [
146        start,
147        start.add(start_derivative.scale(1.0 / 3.0)),
148        end.sub(end_derivative.scale(1.0 / 3.0)),
149        end,
150    ]
151}
152
153/// Worst `|w - 1|` over a curve's control points.
154fn weight_spread(curve: &NurbsCurve) -> f64 {
155    curve
156        .control_points
157        .iter()
158        .map(|control| (control.w - 1.0).abs())
159        .fold(0.0f64, f64::max)
160}
161
162/// First derivative of a curve with respect to its own normalised parameter.
163fn derivative_at(curve: &NurbsCurve, fraction: f64) -> Result<Vec3, String> {
164    let [start, end] = curve.domain()?;
165    let derivatives = curve.derivatives(start + (end - start) * fraction, 1)?;
166    Ok(derivatives[1].scale(end - start))
167}
168
169fn build(
170    boundaries: [NurbsCurve; 4],
171    ribbons: [Option<NurbsCurve>; 4],
172    v_hermite: bool,
173    u_hermite: bool,
174) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
175    // Polynomial only: a prescribed 3D cross-derivative does not determine the
176    // homogeneous ribbon a rational patch would need.
177    for (index, curve) in boundaries.iter().enumerate() {
178        let spread = weight_spread(curve);
179        if spread > POLYNOMIAL_WEIGHT_BAND {
180            return Err(CoonsRefusal::RationalRibbon {
181                boundary: index,
182                weight_spread: spread,
183            });
184        }
185    }
186    for (index, ribbon) in ribbons.iter().enumerate() {
187        if let Some(curve) = ribbon {
188            let spread = weight_spread(curve);
189            if spread > POLYNOMIAL_WEIGHT_BAND {
190                return Err(CoonsRefusal::RationalRibbon {
191                    boundary: index,
192                    weight_spread: spread,
193                });
194            }
195        }
196    }
197
198    let oriented = super::orient_and_reconcile(&boundaries)?;
199    let flip = |curve: &NurbsCurve, reversed: bool| -> Result<NurbsCurve, CoonsRefusal> {
200        if reversed {
201            curve.reversed().map_err(CoonsRefusal::Geometry)
202        } else {
203            Ok(curve.clone())
204        }
205    };
206    // Ribbons follow their boundary's orientation; the transverse direction
207    // they describe is untouched by that flip, so there is no sign change.
208    let mut ribbon_curves: [Option<NurbsCurve>; 4] = [None, None, None, None];
209    for index in 0..4 {
210        if let Some(ribbon) = &ribbons[index] {
211            ribbon_curves[index] = Some(flip(ribbon, oriented.reversed[index])?);
212        }
213    }
214    let c0 = oriented.curves[0].clone();
215    let d1 = oriented.curves[1].clone();
216    let c1 = oriented.curves[2].reversed().map_err(CoonsRefusal::Geometry)?;
217    let d0 = oriented.curves[3].reversed().map_err(CoonsRefusal::Geometry)?;
218    let reverse_ribbon = |ribbon: &Option<NurbsCurve>| -> Result<Option<NurbsCurve>, CoonsRefusal> {
219        match ribbon {
220            Some(curve) => Ok(Some(curve.reversed().map_err(CoonsRefusal::Geometry)?)),
221            None => Ok(None),
222        }
223    };
224    // Along +u at v = 0 and v = 1; along +v at u = 0 and u = 1.
225    let tv0 = ribbon_curves[0].clone();
226    let tv1 = reverse_ribbon(&ribbon_curves[2])?;
227    let tu0 = reverse_ribbon(&ribbon_curves[3])?;
228    let tu1 = ribbon_curves[1].clone();
229
230    // One compatible family per direction: the two boundaries plus, where the
231    // direction is blended cubically, the two ribbons that travel with them.
232    let mut u_family: Vec<NurbsCurve> = vec![c0, c1];
233    if v_hermite {
234        u_family.push(tv0.clone().expect("paired"));
235        u_family.push(tv1.clone().expect("paired"));
236    }
237    let mut v_family: Vec<NurbsCurve> = vec![d0, d1];
238    if u_hermite {
239        v_family.push(tu0.clone().expect("paired"));
240        v_family.push(tu1.clone().expect("paired"));
241    }
242    let u_native = if u_hermite { 3 } else { 1 };
243    let v_native = if v_hermite { 3 } else { 1 };
244    let (u_family, u_work) =
245        compat::make_family_compatible(&u_family, u_native).map_err(|detail| {
246            CoonsRefusal::Incompatible {
247                direction: 'u',
248                detail,
249            }
250        })?;
251    let (v_family, v_work) =
252        compat::make_family_compatible(&v_family, v_native).map_err(|detail| {
253            CoonsRefusal::Incompatible {
254                direction: 'v',
255                detail,
256            }
257        })?;
258    let mut work = [CompatWork::default(); 4];
259    work[0] = u_work[0];
260    work[2] = u_work[1];
261    work[3] = v_work[0];
262    work[1] = v_work[1];
263
264    let degree_u = u_family[0].degree;
265    let degree_v = v_family[0].degree;
266    let knots_u = u_family[0].knots.clone();
267    let knots_v = v_family[0].knots.clone();
268    let rows = u_family[0].control_points.len();
269    let columns = v_family[0].control_points.len();
270    let net_of = |curve: &NurbsCurve| -> Vec<Vec3> {
271        curve.control_points.iter().copied().map(point_of).collect()
272    };
273    let c0_net = net_of(&u_family[0]);
274    let c1_net = net_of(&u_family[1]);
275    let d0_net = net_of(&v_family[0]);
276    let d1_net = net_of(&v_family[1]);
277    let tv0_net = v_hermite.then(|| net_of(&u_family[2]));
278    let tv1_net = v_hermite.then(|| net_of(&u_family[3]));
279    let tu0_net = u_hermite.then(|| net_of(&v_family[2]));
280    let tu1_net = u_hermite.then(|| net_of(&v_family[3]));
281
282    // Corner points and the boundary tangents there.  These — not the ribbons
283    // — are what the correction term must carry, so that it cancels the ruled
284    // halves' own contribution identically along every boundary.
285    let corner = |net: &[Vec3], last: bool| net[if last { net.len() - 1 } else { 0 }];
286    let corners = [
287        [corner(&c0_net, false), corner(&c1_net, false)],
288        [corner(&c0_net, true), corner(&c1_net, true)],
289    ];
290    let tangent_u = [
291        [
292            derivative_at(&u_family[0], 0.0).map_err(CoonsRefusal::Geometry)?,
293            derivative_at(&u_family[1], 0.0).map_err(CoonsRefusal::Geometry)?,
294        ],
295        [
296            derivative_at(&u_family[0], 1.0).map_err(CoonsRefusal::Geometry)?,
297            derivative_at(&u_family[1], 1.0).map_err(CoonsRefusal::Geometry)?,
298        ],
299    ];
300    let tangent_v = [
301        [
302            derivative_at(&v_family[0], 0.0).map_err(CoonsRefusal::Geometry)?,
303            derivative_at(&v_family[0], 1.0).map_err(CoonsRefusal::Geometry)?,
304        ],
305        [
306            derivative_at(&v_family[1], 0.0).map_err(CoonsRefusal::Geometry)?,
307            derivative_at(&v_family[1], 1.0).map_err(CoonsRefusal::Geometry)?,
308        ],
309    ];
310
311    // A ribbon that contradicts the boundary tangent it meets cannot be
312    // carried; measure the disagreement and refuse above the same vertex
313    // identity band the corners were judged by.
314    let mut points: Vec<Vec3> = Vec::new();
315    for boundary in &boundaries {
316        let [start, end] = boundary.domain().map_err(CoonsRefusal::Geometry)?;
317        for index in 0..=BOUNDARY_SAMPLES {
318            points.push(
319                boundary
320                    .evaluate(start + (end - start) * index as f64 / BOUNDARY_SAMPLES as f64)
321                    .map_err(CoonsRefusal::Geometry)?,
322            );
323        }
324    }
325    let band = corner_band(model_scale(points.iter().copied()));
326    let mut corner_tangent_gap = 0.0f64;
327    if v_hermite {
328        for (v_end, ribbon) in [(0usize, &u_family[2]), (1usize, &u_family[3])] {
329            for (u_side, fraction) in [(0usize, 0.0f64), (1usize, 1.0f64)] {
330                let [start, end] = ribbon.domain().map_err(CoonsRefusal::Geometry)?;
331                let value = ribbon
332                    .evaluate(start + (end - start) * fraction)
333                    .map_err(CoonsRefusal::Geometry)?;
334                let expected = tangent_v[u_side][v_end];
335                let gap = value.sub(expected).length();
336                corner_tangent_gap = corner_tangent_gap.max(gap);
337                if gap > band {
338                    return Err(CoonsRefusal::RibbonCornerTangent {
339                        corner: u_side * 2 + v_end,
340                        gap,
341                        band,
342                    });
343                }
344            }
345        }
346    }
347    if u_hermite {
348        for (u_side, ribbon) in [(0usize, &v_family[2]), (1usize, &v_family[3])] {
349            for (v_end, fraction) in [(0usize, 0.0f64), (1usize, 1.0f64)] {
350                let [start, end] = ribbon.domain().map_err(CoonsRefusal::Geometry)?;
351                let value = ribbon
352                    .evaluate(start + (end - start) * fraction)
353                    .map_err(CoonsRefusal::Geometry)?;
354                let expected = tangent_u[u_side][v_end];
355                let gap = value.sub(expected).length();
356                corner_tangent_gap = corner_tangent_gap.max(gap);
357                if gap > band {
358                    return Err(CoonsRefusal::RibbonCornerTangent {
359                        corner: u_side * 2 + v_end,
360                        gap,
361                        band,
362                    });
363                }
364            }
365        }
366    }
367
368    // Corner twists.  Prescribed twice when both directions are cubic; the
369    // patch carries the mean and the disagreement is reported, never hidden.
370    let mut twist = [[Vec3::default(); 2]; 2];
371    let mut twist_discrepancy = 0.0f64;
372    if v_hermite && u_hermite {
373        for side in 0..2 {
374            for corner_index in 0..2 {
375                // d(dS/dv)/du from the v-ribbon along v = corner_index.
376                let from_v = derivative_at(
377                    &u_family[2 + corner_index],
378                    if side == 0 { 0.0 } else { 1.0 },
379                )
380                .map_err(CoonsRefusal::Geometry)?;
381                // d(dS/du)/dv from the u-ribbon along u = side.
382                let from_u = derivative_at(
383                    &v_family[2 + side],
384                    if corner_index == 0 { 0.0 } else { 1.0 },
385                )
386                .map_err(CoonsRefusal::Geometry)?;
387                twist_discrepancy = twist_discrepancy.max(from_v.sub(from_u).length());
388                twist[side][corner_index] = from_v.add(from_u).scale(0.5);
389            }
390        }
391    }
392
393    // ---- S1: blend the u-direction boundaries across v. ----
394    let mut first: Vec<Vec<Vec3>> = Vec::with_capacity(rows);
395    for row in 0..rows {
396        let data: Vec<Vec3> = if v_hermite {
397            hermite(
398                c0_net[row],
399                tv0_net.as_ref().expect("paired")[row],
400                c1_net[row],
401                tv1_net.as_ref().expect("paired")[row],
402            )
403            .to_vec()
404        } else {
405            vec![c0_net[row], c1_net[row]]
406        };
407        first.push(express(&data, degree_v, &knots_v).map_err(CoonsRefusal::Geometry)?);
408    }
409    // ---- S2: blend the v-direction boundaries across u. ----
410    let mut second: Vec<Vec<Vec3>> = vec![vec![Vec3::default(); columns]; rows];
411    for column in 0..columns {
412        let data: Vec<Vec3> = if u_hermite {
413            hermite(
414                d0_net[column],
415                tu0_net.as_ref().expect("paired")[column],
416                d1_net[column],
417                tu1_net.as_ref().expect("paired")[column],
418            )
419            .to_vec()
420        } else {
421            vec![d0_net[column], d1_net[column]]
422        };
423        let expressed = express(&data, degree_u, &knots_u).map_err(CoonsRefusal::Geometry)?;
424        for (row, value) in expressed.into_iter().enumerate() {
425            second[row][column] = value;
426        }
427    }
428    // ---- S3: the corner correction, in its own native degrees first. ----
429    let native_u = if u_hermite { 4 } else { 2 };
430    let native_v = if v_hermite { 4 } else { 2 };
431    let mut correction: Vec<Vec<Vec3>> = vec![vec![Vec3::default(); native_v]; native_u];
432    for side in 0..2 {
433        for corner_index in 0..2 {
434            let row = if u_hermite { side * 3 } else { side };
435            let column = if v_hermite { corner_index * 3 } else { corner_index };
436            correction[row][column] = corners[side][corner_index];
437        }
438    }
439    if u_hermite {
440        for corner_index in 0..2 {
441            let column = if v_hermite { corner_index * 3 } else { corner_index };
442            correction[1][column] = corners[0][corner_index]
443                .add(tangent_u[0][corner_index].scale(1.0 / 3.0));
444            correction[2][column] = corners[1][corner_index]
445                .sub(tangent_u[1][corner_index].scale(1.0 / 3.0));
446        }
447    }
448    if v_hermite {
449        for side in 0..2 {
450            let row = if u_hermite { side * 3 } else { side };
451            correction[row][1] =
452                corners[side][0].add(tangent_v[side][0].scale(1.0 / 3.0));
453            correction[row][2] =
454                corners[side][1].sub(tangent_v[side][1].scale(1.0 / 3.0));
455        }
456    }
457    if u_hermite && v_hermite {
458        // The four interior cells carry both corner tangents and the twist.
459        for side in 0..2 {
460            for corner_index in 0..2 {
461                let row = if side == 0 { 1 } else { 2 };
462                let column = if corner_index == 0 { 1 } else { 2 };
463                let u_sign = if side == 0 { 1.0 } else { -1.0 };
464                let v_sign = if corner_index == 0 { 1.0 } else { -1.0 };
465                correction[row][column] = corners[side][corner_index]
466                    .add(tangent_u[side][corner_index].scale(u_sign / 3.0))
467                    .add(tangent_v[side][corner_index].scale(v_sign / 3.0))
468                    .add(twist[side][corner_index].scale(u_sign * v_sign / 9.0));
469            }
470        }
471    }
472    // Express the correction in the patch's tensor space: v first, then u.
473    let mut correction_v: Vec<Vec<Vec3>> = Vec::with_capacity(native_u);
474    for row in correction.iter() {
475        correction_v.push(express(row, degree_v, &knots_v).map_err(CoonsRefusal::Geometry)?);
476    }
477    let mut third: Vec<Vec<Vec3>> = vec![vec![Vec3::default(); columns]; rows];
478    for column in 0..columns {
479        let data: Vec<Vec3> = correction_v.iter().map(|row| row[column]).collect();
480        let expressed = express(&data, degree_u, &knots_u).map_err(CoonsRefusal::Geometry)?;
481        for (row, value) in expressed.into_iter().enumerate() {
482            third[row][column] = value;
483        }
484    }
485
486    // ---- Assemble.  Boundary rows are assigned, never summed. ----
487    let mut net: Vec<Vec<Vec4>> = vec![
488        vec![
489            Vec4 {
490                x: 0.0,
491                y: 0.0,
492                z: 0.0,
493                w: 1.0
494            };
495            columns
496        ];
497        rows
498    ];
499    for row in 0..rows {
500        for column in 0..columns {
501            let blended = first[row][column]
502                .add(second[row][column])
503                .sub(third[row][column]);
504            net[row][column] = Vec4::from_point(blended, 1.0);
505        }
506    }
507    for (row, control) in u_family[0].control_points.iter().enumerate() {
508        net[row][0] = Vec4::from_point(point_of(*control), 1.0);
509    }
510    for (row, control) in u_family[1].control_points.iter().enumerate() {
511        net[row][columns - 1] = Vec4::from_point(point_of(*control), 1.0);
512    }
513    for (column, control) in v_family[0].control_points.iter().enumerate() {
514        net[0][column] = Vec4::from_point(point_of(*control), 1.0);
515    }
516    for (column, control) in v_family[1].control_points.iter().enumerate() {
517        net[rows - 1][column] = Vec4::from_point(point_of(*control), 1.0);
518    }
519    net[0][0] = Vec4::from_point(corners[0][0], 1.0);
520    net[rows - 1][0] = Vec4::from_point(corners[1][0], 1.0);
521    net[0][columns - 1] = Vec4::from_point(corners[0][1], 1.0);
522    net[rows - 1][columns - 1] = Vec4::from_point(corners[1][1], 1.0);
523
524    let surface = NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net)
525        .map_err(CoonsRefusal::Geometry)?;
526
527    // ---- Measure what the patch actually achieved. ----
528    let mut residual = 0.0f64;
529    let mut ribbon_scale = 0.0f64;
530    let mut prescribed = [false; 4];
531    let mut measure = |ribbon: &NurbsCurve,
532                       along_u: bool,
533                       at: f64,
534                       surface: &NurbsSurface|
535     -> Result<(), CoonsRefusal> {
536        let [start, end] = ribbon.domain().map_err(CoonsRefusal::Geometry)?;
537        for index in 0..=BOUNDARY_SAMPLES {
538            let fraction = index as f64 / BOUNDARY_SAMPLES as f64;
539            let expected = ribbon
540                .evaluate(start + (end - start) * fraction)
541                .map_err(CoonsRefusal::Geometry)?;
542            let (u, v) = if along_u { (fraction, at) } else { (at, fraction) };
543            let derivatives = surface.derivatives(u, v, 1).map_err(CoonsRefusal::Geometry)?;
544            let achieved = if along_u {
545                derivatives[0][1]
546            } else {
547                derivatives[1][0]
548            };
549            residual = residual.max(achieved.sub(expected).length());
550            ribbon_scale = ribbon_scale.max(expected.length());
551        }
552        Ok(())
553    };
554    if v_hermite {
555        prescribed[0] = true;
556        prescribed[2] = true;
557        measure(&u_family[2], true, 0.0, &surface)?;
558        measure(&u_family[3], true, 1.0, &surface)?;
559    }
560    if u_hermite {
561        prescribed[1] = true;
562        prescribed[3] = true;
563        measure(&v_family[2], false, 0.0, &surface)?;
564        measure(&v_family[3], false, 1.0, &surface)?;
565    }
566
567    let mut report = report_skeleton(&oriented, work);
568    report.degree_u = degree_u;
569    report.degree_v = degree_v;
570    report.control_rows = rows;
571    report.control_columns = columns;
572    report.min_weight = 1.0;
573    report.boundary_residual = boundary_residual(&surface, &u_family, &v_family);
574    report.ribbons = Some(RibbonReport {
575        prescribed,
576        corner_tangent_gap,
577        twist_discrepancy,
578        cross_derivative_residual: residual,
579        cross_derivative_scale: ribbon_scale,
580    });
581    Ok((surface, report))
582}
583
584/// Worst distance between an assembled boundary row and the compatible
585/// boundary curve it was built from.
586fn boundary_residual(
587    surface: &NurbsSurface,
588    u_family: &[NurbsCurve],
589    v_family: &[NurbsCurve],
590) -> f64 {
591    let net = &surface.control_points;
592    let rows = net.len();
593    let columns = net[0].len();
594    let mut worst = 0.0f64;
595    for row in 0..rows {
596        worst = worst.max(
597            point_of(net[row][0])
598                .sub(point_of(u_family[0].control_points[row]))
599                .length(),
600        );
601        worst = worst.max(
602            point_of(net[row][columns - 1])
603                .sub(point_of(u_family[1].control_points[row]))
604                .length(),
605        );
606    }
607    for column in 0..columns {
608        worst = worst.max(
609            point_of(net[0][column])
610                .sub(point_of(v_family[0].control_points[column]))
611                .length(),
612        );
613        worst = worst.max(
614            point_of(net[rows - 1][column])
615                .sub(point_of(v_family[1].control_points[column]))
616                .length(),
617        );
618    }
619    worst
620}