axiolid_nurbs/
certified_surface_surface_intersection.rs

1//! Certified bounded intersections between clamped NURBS surfaces.
2//!
3//! The complete trace certificate implemented here is deliberately narrow: both
4//! inputs must be single-span polynomial affine patches with continuous clamped
5//! axes. General patch pairs are still bounded and conservatively returned as
6//! unresolved candidates rather than passed through heuristic marching.
7
8use axiolid_contracts::{GeomError, GeomResult};
9use axiolid_core::{Point3, Scalar};
10use axiolid_curve::{BSplineCurve3, KnotSpec};
11use axiolid_surface::BSplineSurface;
12
13use crate::{
14    certified_bezier::Interval,
15    certified_curve_surface_intersection::{
16        intersect_curve_surface_certified, CertifiedCurveSurfaceIntersection3,
17        CertifiedCurveSurfaceIntersectionOptions, TransverseCurveSurfaceIntersection3,
18    },
19    certified_projection::ParameterInterval,
20    certified_refinement::RefinementBudget,
21    certified_surface_bezier::{piecewise_bezier_patches, Patch},
22};
23
24const MAX_REFINEMENT_WORK: u32 = 100_000;
25const MAX_BOUNDARY_NODES: u32 = 100_000;
26const MAX_DEPTH: u16 = 64;
27const BOUNDARY_QUERY_COUNT: u8 = 8;
28
29/// Bounded policy for certified surface/surface intersection and affine tracing.
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct CertifiedSurfaceSurfaceIntersectionOptions {
32    parameter_tolerance: Scalar,
33    max_refinement_work: u32,
34    max_boundary_nodes: u32,
35    max_depth: u16,
36}
37
38impl CertifiedSurfaceSurfaceIntersectionOptions {
39    /// Construct a finite policy.
40    ///
41    /// `max_refinement_work` is shared by both tensor refinements and the initial
42    /// patch-pair product. At most eight boundary curve/surface queries are run,
43    /// each independently capped by `max_boundary_nodes` and `max_depth`.
44    pub fn new(
45        parameter_tolerance: Scalar,
46        max_refinement_work: u32,
47        max_boundary_nodes: u32,
48        max_depth: u16,
49    ) -> GeomResult<Self> {
50        if !parameter_tolerance.is_finite() || parameter_tolerance <= 0.0 {
51            return Err(GeomError::InvalidInput(
52                "surface/surface parameter tolerance must be finite and positive".to_owned(),
53            ));
54        }
55        if max_refinement_work == 0 || max_refinement_work > MAX_REFINEMENT_WORK {
56            return Err(GeomError::InvalidInput(format!(
57                "surface/surface max_refinement_work must be in 1..={MAX_REFINEMENT_WORK}"
58            )));
59        }
60        if max_boundary_nodes == 0 || max_boundary_nodes > MAX_BOUNDARY_NODES {
61            return Err(GeomError::InvalidInput(format!(
62                "surface/surface max_boundary_nodes must be in 1..={MAX_BOUNDARY_NODES}"
63            )));
64        }
65        if max_depth == 0 || max_depth > MAX_DEPTH {
66            return Err(GeomError::InvalidInput(format!(
67                "surface/surface max_depth must be in 1..={MAX_DEPTH}"
68            )));
69        }
70        Ok(Self {
71            parameter_tolerance,
72            max_refinement_work,
73            max_boundary_nodes,
74            max_depth,
75        })
76    }
77}
78
79impl Default for CertifiedSurfaceSurfaceIntersectionOptions {
80    fn default() -> Self {
81        Self {
82            parameter_tolerance: 1.0e-8,
83            max_refinement_work: MAX_REFINEMENT_WORK,
84            max_boundary_nodes: MAX_BOUNDARY_NODES,
85            max_depth: MAX_DEPTH,
86        }
87    }
88}
89
90/// Native four-parameter box for a pair of tensor-product surfaces.
91#[derive(Debug, Clone, Copy, PartialEq)]
92#[non_exhaustive]
93pub struct SurfaceSurfaceParameterBox {
94    /// First-surface `u` enclosure.
95    pub first_u: ParameterInterval,
96    /// First-surface `v` enclosure.
97    pub first_v: ParameterInterval,
98    /// Second-surface `u` enclosure.
99    pub second_u: ParameterInterval,
100    /// Second-surface `v` enclosure.
101    pub second_v: ParameterInterval,
102}
103
104/// Certified endpoint enclosure for an affine surface/surface trace.
105#[derive(Debug, Clone, PartialEq)]
106#[non_exhaustive]
107pub struct SurfaceSurfaceTraceEndpoint3 {
108    /// Native parameter enclosure on both surfaces.
109    pub parameters: SurfaceSurfaceParameterBox,
110    /// Representative midpoint of the two evaluated images.
111    pub point: Point3,
112    /// Conservative endpoint residual upper bound.
113    pub residual_upper_bound: Scalar,
114}
115
116/// Certificate for one complete regular affine intersection segment.
117#[derive(Debug, Clone, PartialEq)]
118#[non_exhaustive]
119pub struct TransverseSurfaceSurfaceTrace3 {
120    /// Lexicographically ordered first endpoint certificate.
121    pub start: SurfaceSurfaceTraceEndpoint3,
122    /// Lexicographically ordered second endpoint certificate.
123    pub end: SurfaceSurfaceTraceEndpoint3,
124    /// Positive interval lower bound for `|(S1_u x S1_v) x (S2_u x S2_v)|^2`.
125    pub normal_cross_squared_lower_bound: Scalar,
126}
127
128/// Certified surface/surface query outcome.
129#[derive(Debug, Clone, PartialEq)]
130#[non_exhaustive]
131pub enum CertifiedSurfaceSurfaceIntersection3 {
132    /// Every patch pair was excluded or represented by a complete certified trace.
133    Complete {
134        /// Certified regular affine trace segments.
135        traces: Vec<TransverseSurfaceSurfaceTrace3>,
136        /// Number of initial patch pairs visited by the broad phase.
137        visited_patch_pairs: u32,
138        /// Number of certified boundary queries executed.
139        boundary_queries: u8,
140    },
141    /// One or more conservative patch-pair candidates remain unresolved.
142    Unresolved {
143        /// Trace segments proved independently of unresolved candidates.
144        traces: Vec<TransverseSurfaceSurfaceTrace3>,
145        /// Conservative native boxes that may contain untraced intersections.
146        candidate_boxes: Vec<SurfaceSurfaceParameterBox>,
147        /// Number of initial patch pairs visited by the broad phase.
148        visited_patch_pairs: u32,
149        /// Number of certified boundary queries executed.
150        boundary_queries: u8,
151    },
152}
153
154/// Certify bounded intersections of two clamped, internally continuous NURBS surfaces.
155///
156/// Coordinate-hull exclusion is available for general polynomial and positive-weight
157/// rational inputs. Complete traced segments currently require single-span polynomial
158/// affine patches. Non-affine, multispan, coincident, tangential, corner-owned, and
159/// proof-insufficient candidates remain [`Unresolved`](CertifiedSurfaceSurfaceIntersection3::Unresolved).
160pub fn intersect_surface_surface_certified(
161    first: &BSplineSurface,
162    second: &BSplineSurface,
163    options: CertifiedSurfaceSurfaceIntersectionOptions,
164) -> GeomResult<CertifiedSurfaceSurfaceIntersection3> {
165    let options = CertifiedSurfaceSurfaceIntersectionOptions::new(
166        options.parameter_tolerance,
167        options.max_refinement_work,
168        options.max_boundary_nodes,
169        options.max_depth,
170    )?;
171    let mut budget = RefinementBudget::new(
172        options.max_refinement_work,
173        "certified surface/surface refinement budget",
174    );
175    let first_patches = piecewise_bezier_patches(first, &mut budget)?;
176    let second_patches = piecewise_bezier_patches(second, &mut budget)?;
177    let pair_count = first_patches
178        .len()
179        .checked_mul(second_patches.len())
180        .ok_or(GeomError::BudgetExceeded {
181            resource: "certified surface/surface refinement budget",
182        })?;
183    budget.charge(u128::try_from(pair_count).ok())?;
184    let visited_patch_pairs = u32::try_from(pair_count).map_err(|_| GeomError::BudgetExceeded {
185        resource: "certified surface/surface refinement budget",
186    })?;
187    let mut candidates = Vec::new();
188    candidates
189        .try_reserve_exact(pair_count)
190        .map_err(|_| allocation_error("certified surface/surface candidate allocation"))?;
191    for first_patch in &first_patches {
192        for second_patch in &second_patches {
193            if !patches_are_disjoint(first_patch, second_patch)? {
194                candidates.push(parameter_box(first_patch, second_patch));
195            }
196        }
197    }
198    if candidates.is_empty() {
199        return Ok(CertifiedSurfaceSurfaceIntersection3::Complete {
200            traces: Vec::new(),
201            visited_patch_pairs,
202            boundary_queries: 0,
203        });
204    }
205    if candidates.len() != 1
206        || first_patches.len() != 1
207        || second_patches.len() != 1
208        || !is_exact_single_span_affine(first)
209        || !is_exact_single_span_affine(second)
210    {
211        return unresolved(candidates, visited_patch_pairs, 0);
212    }
213    let normal_lower = normal_cross_squared_lower_bound(&first_patches[0], &second_patches[0])?;
214    if normal_lower <= 0.0 {
215        return unresolved(candidates, visited_patch_pairs, 0);
216    }
217    match trace_affine_pair(first, second, options, normal_lower)? {
218        AffineTraceOutcome::Complete(traces) => {
219            Ok(CertifiedSurfaceSurfaceIntersection3::Complete {
220                traces,
221                visited_patch_pairs,
222                boundary_queries: BOUNDARY_QUERY_COUNT,
223            })
224        }
225        AffineTraceOutcome::Unresolved(traces) => {
226            Ok(CertifiedSurfaceSurfaceIntersection3::Unresolved {
227                traces,
228                candidate_boxes: candidates,
229                visited_patch_pairs,
230                boundary_queries: BOUNDARY_QUERY_COUNT,
231            })
232        }
233    }
234}
235
236fn unresolved(
237    candidate_boxes: Vec<SurfaceSurfaceParameterBox>,
238    visited_patch_pairs: u32,
239    boundary_queries: u8,
240) -> GeomResult<CertifiedSurfaceSurfaceIntersection3> {
241    Ok(CertifiedSurfaceSurfaceIntersection3::Unresolved {
242        traces: Vec::new(),
243        candidate_boxes,
244        visited_patch_pairs,
245        boundary_queries,
246    })
247}
248
249fn allocation_error(resource: &'static str) -> GeomError {
250    GeomError::BudgetExceeded { resource }
251}
252
253fn parameter_box(first: &Patch, second: &Patch) -> SurfaceSurfaceParameterBox {
254    SurfaceSurfaceParameterBox {
255        first_u: ParameterInterval {
256            start: first.u_start,
257            end: first.u_end,
258        },
259        first_v: ParameterInterval {
260            start: first.v_start,
261            end: first.v_end,
262        },
263        second_u: ParameterInterval {
264            start: second.u_start,
265            end: second.u_end,
266        },
267        second_v: ParameterInterval {
268            start: second.v_start,
269            end: second.v_end,
270        },
271    }
272}
273
274pub(crate) fn patches_are_disjoint(first: &Patch, second: &Patch) -> GeomResult<bool> {
275    let first_bounds = first.coordinate_intervals()?;
276    let second_bounds = second.coordinate_intervals()?;
277    Ok((0..3).any(|axis| {
278        first_bounds[axis].upper() < second_bounds[axis].lower()
279            || second_bounds[axis].upper() < first_bounds[axis].lower()
280    }))
281}
282
283fn is_exact_single_span_affine(surface: &BSplineSurface) -> bool {
284    if surface.u_degree != 1
285        || surface.v_degree != 1
286        || surface.control_points.len() != 2
287        || surface.control_points.iter().any(|row| row.len() != 2)
288        || surface.weights.is_some()
289        || surface.u_knots.len() != 2
290        || surface.v_knots.len() != 2
291    {
292        return false;
293    }
294    let p00 = surface.control_points[0][0];
295    let p01 = surface.control_points[0][1];
296    let p10 = surface.control_points[1][0];
297    let p11 = surface.control_points[1][1];
298    [
299        [p11.x, -p10.x, -p01.x, p00.x],
300        [p11.y, -p10.y, -p01.y, p00.y],
301        [p11.z, -p10.z, -p01.z, p00.z],
302    ]
303    .into_iter()
304    .all(exact_sum_is_zero)
305}
306
307// Shewchuk-style nonoverlapping expansion addition. Each binary64 input is an
308// exact dyadic rational, so an all-zero final expansion proves the affine
309// cross-term identity without a tolerance or rounded cancellation decision.
310fn exact_sum_is_zero(values: [Scalar; 4]) -> bool {
311    let mut expansion = [0.0; 8];
312    let mut length = 1usize;
313    expansion[0] = values[0];
314    for value in values.into_iter().skip(1) {
315        let mut next = [0.0; 8];
316        let mut next_length = 0usize;
317        let mut accumulator = value;
318        for component in expansion.iter().take(length).copied() {
319            let (sum, error) = two_sum(accumulator, component);
320            if error != 0.0 {
321                next[next_length] = error;
322                next_length += 1;
323            }
324            accumulator = sum;
325        }
326        if accumulator != 0.0 || next_length == 0 {
327            next[next_length] = accumulator;
328            next_length += 1;
329        }
330        expansion = next;
331        length = next_length;
332    }
333    expansion
334        .iter()
335        .take(length)
336        .all(|component| *component == 0.0)
337}
338
339fn two_sum(left: Scalar, right: Scalar) -> (Scalar, Scalar) {
340    let sum = left + right;
341    let right_virtual = sum - left;
342    let left_virtual = sum - right_virtual;
343    let right_roundoff = right - right_virtual;
344    let left_roundoff = left - left_virtual;
345    (sum, left_roundoff + right_roundoff)
346}
347
348pub(crate) fn normal_cross_squared_lower_bound(
349    first: &Patch,
350    second: &Patch,
351) -> GeomResult<Scalar> {
352    let first_normal = cross_intervals(first.partial_u_intervals()?, first.partial_v_intervals()?)?;
353    let second_normal =
354        cross_intervals(second.partial_u_intervals()?, second.partial_v_intervals()?)?;
355    let cross = cross_intervals(first_normal, second_normal)?;
356    let mut squared = Interval::exact(0.0)?;
357    for component in cross {
358        let lower = Interval::exact(component.absolute_lower_bound())?;
359        squared = squared.add(lower.multiply(lower)?)?;
360    }
361    Ok(squared.lower().max(0.0))
362}
363
364fn cross_intervals(left: [Interval; 3], right: [Interval; 3]) -> GeomResult<[Interval; 3]> {
365    Ok([
366        left[1]
367            .multiply(right[2])?
368            .subtract(left[2].multiply(right[1])?)?,
369        left[2]
370            .multiply(right[0])?
371            .subtract(left[0].multiply(right[2])?)?,
372        left[0]
373            .multiply(right[1])?
374            .subtract(left[1].multiply(right[0])?)?,
375    ])
376}
377
378#[derive(Debug, Clone, Copy)]
379enum Boundary {
380    UStart,
381    UEnd,
382    VStart,
383    VEnd,
384}
385
386const BOUNDARIES: [Boundary; 4] = [
387    Boundary::UStart,
388    Boundary::UEnd,
389    Boundary::VStart,
390    Boundary::VEnd,
391];
392
393enum AffineTraceOutcome {
394    Complete(Vec<TransverseSurfaceSurfaceTrace3>),
395    Unresolved(Vec<TransverseSurfaceSurfaceTrace3>),
396}
397
398fn trace_affine_pair(
399    first: &BSplineSurface,
400    second: &BSplineSurface,
401    options: CertifiedSurfaceSurfaceIntersectionOptions,
402    normal_lower: Scalar,
403) -> GeomResult<AffineTraceOutcome> {
404    let boundary_options = CertifiedCurveSurfaceIntersectionOptions::new(
405        options.parameter_tolerance,
406        options.max_boundary_nodes,
407        options.max_depth,
408    )?;
409    let mut endpoints = Vec::new();
410    endpoints
411        .try_reserve_exact(BOUNDARY_QUERY_COUNT.into())
412        .map_err(|_| allocation_error("certified surface/surface endpoint allocation"))?;
413    let mut any_unresolved = false;
414    for boundary in BOUNDARIES {
415        if let Err(error) = collect_boundary_roots(
416            first,
417            second,
418            boundary,
419            true,
420            boundary_options,
421            &mut endpoints,
422            &mut any_unresolved,
423        ) {
424            if matches!(error, GeomError::BudgetExceeded { .. }) {
425                any_unresolved = true;
426                continue;
427            }
428            return Err(error);
429        }
430    }
431    for boundary in BOUNDARIES {
432        if let Err(error) = collect_boundary_roots(
433            second,
434            first,
435            boundary,
436            false,
437            boundary_options,
438            &mut endpoints,
439            &mut any_unresolved,
440        ) {
441            if matches!(error, GeomError::BudgetExceeded { .. }) {
442                any_unresolved = true;
443                continue;
444            }
445            return Err(error);
446        }
447    }
448    // A chord ending ON a boundary of BOTH patches is discovered twice:
449    // once scanning the first surface's boundaries, once the second's.
450    // Both reports denote one point, so fold them before counting.
451    // Step 1 made these boundary roots certifiable, which is what
452    // exposed the duplication -- previously they were never found.
453    dedupe_coincident_endpoints(&mut endpoints);
454    if any_unresolved || endpoints.len() == 1 || endpoints.len() > 2 {
455        return Ok(AffineTraceOutcome::Unresolved(Vec::new()));
456    }
457    if endpoints.is_empty() {
458        return Ok(AffineTraceOutcome::Complete(Vec::new()));
459    }
460    endpoints.sort_by(|left, right| lexicographic_point_cmp(left.point, right.point));
461    if endpoint_boxes_overlap(&endpoints[0], &endpoints[1]) {
462        return Ok(AffineTraceOutcome::Unresolved(Vec::new()));
463    }
464    let mut traces = Vec::new();
465    traces
466        .try_reserve_exact(1)
467        .map_err(|_| allocation_error("certified surface/surface trace allocation"))?;
468    traces.push(TransverseSurfaceSurfaceTrace3 {
469        start: endpoints.remove(0),
470        end: endpoints.remove(0),
471        normal_cross_squared_lower_bound: normal_lower,
472    });
473    Ok(AffineTraceOutcome::Complete(traces))
474}
475
476fn collect_boundary_roots(
477    owner: &BSplineSurface,
478    other: &BSplineSurface,
479    boundary: Boundary,
480    owner_is_first: bool,
481    options: CertifiedCurveSurfaceIntersectionOptions,
482    endpoints: &mut Vec<SurfaceSurfaceTraceEndpoint3>,
483    any_unresolved: &mut bool,
484) -> GeomResult<()> {
485    let curve = boundary_curve(owner, boundary)?;
486    match intersect_curve_surface_certified(&curve, other, options)? {
487        CertifiedCurveSurfaceIntersection3::Complete { intersections, .. } => {
488            for intersection in intersections {
489                push_endpoint(
490                    endpoints,
491                    map_endpoint(owner, boundary, owner_is_first, intersection),
492                )?;
493            }
494        }
495        CertifiedCurveSurfaceIntersection3::Unresolved { .. } => *any_unresolved = true,
496    }
497    Ok(())
498}
499
500fn push_endpoint(
501    endpoints: &mut Vec<SurfaceSurfaceTraceEndpoint3>,
502    endpoint: SurfaceSurfaceTraceEndpoint3,
503) -> GeomResult<()> {
504    if endpoints.len() == endpoints.capacity() {
505        endpoints
506            .try_reserve_exact(1)
507            .map_err(|_| allocation_error("certified surface/surface endpoint allocation"))?;
508    }
509    endpoints.push(endpoint);
510    Ok(())
511}
512
513fn boundary_curve(surface: &BSplineSurface, boundary: Boundary) -> GeomResult<BSplineCurve3> {
514    let (degree, knots, multiplicities, controls) = match boundary {
515        Boundary::UStart => (
516            surface.v_degree,
517            &surface.v_knots,
518            &surface.v_multiplicities,
519            clone_points(surface.control_points[0].iter().copied())?,
520        ),
521        Boundary::UEnd => (
522            surface.v_degree,
523            &surface.v_knots,
524            &surface.v_multiplicities,
525            clone_points(surface.control_points[1].iter().copied())?,
526        ),
527        Boundary::VStart => (
528            surface.u_degree,
529            &surface.u_knots,
530            &surface.u_multiplicities,
531            clone_points(surface.control_points.iter().map(|row| row[0]))?,
532        ),
533        Boundary::VEnd => (
534            surface.u_degree,
535            &surface.u_knots,
536            &surface.u_multiplicities,
537            clone_points(surface.control_points.iter().map(|row| row[1]))?,
538        ),
539    };
540    Ok(BSplineCurve3 {
541        degree,
542        control_points: controls,
543        knots: clone_scalars(knots)?,
544        multiplicities: clone_multiplicities(multiplicities)?,
545        weights: None,
546        knot_spec: KnotSpec::Unspecified,
547        closed: false,
548        self_intersect: None,
549    })
550}
551
552fn map_endpoint(
553    owner: &BSplineSurface,
554    boundary: Boundary,
555    owner_is_first: bool,
556    root: TransverseCurveSurfaceIntersection3,
557) -> SurfaceSurfaceTraceEndpoint3 {
558    let fixed_u = |value: Scalar| ParameterInterval {
559        start: value,
560        end: value,
561    };
562    let (owner_u, owner_v) = match boundary {
563        Boundary::UStart => (fixed_u(owner.u_knots[0]), root.curve_parameter),
564        Boundary::UEnd => (
565            fixed_u(owner.u_knots[owner.u_knots.len() - 1]),
566            root.curve_parameter,
567        ),
568        Boundary::VStart => (root.curve_parameter, fixed_u(owner.v_knots[0])),
569        Boundary::VEnd => (
570            root.curve_parameter,
571            fixed_u(owner.v_knots[owner.v_knots.len() - 1]),
572        ),
573    };
574    let other_u = root.surface_u_parameter;
575    let other_v = root.surface_v_parameter;
576    let parameters = if owner_is_first {
577        SurfaceSurfaceParameterBox {
578            first_u: owner_u,
579            first_v: owner_v,
580            second_u: other_u,
581            second_v: other_v,
582        }
583    } else {
584        SurfaceSurfaceParameterBox {
585            first_u: other_u,
586            first_v: other_v,
587            second_u: owner_u,
588            second_v: owner_v,
589        }
590    };
591    SurfaceSurfaceTraceEndpoint3 {
592        parameters,
593        point: root.point,
594        residual_upper_bound: root.residual_upper_bound,
595    }
596}
597
598/// Fold endpoint reports that denote the same intersection point.
599///
600/// Two reports are the same point when their certified parameter boxes
601/// overlap on ALL FOUR axes -- both surfaces' u and v. That is the same
602/// test the trace builder already uses to reject a degenerate chord, so
603/// coincidence is decided one way in this file.
604///
605/// Conservative by construction: boxes that merely touch are folded, and
606/// two genuinely distinct roots cannot have overlapping boxes on every
607/// axis without violating the certificate that isolated each of them.
608fn dedupe_coincident_endpoints(endpoints: &mut Vec<SurfaceSurfaceTraceEndpoint3>) {
609    let mut kept: Vec<SurfaceSurfaceTraceEndpoint3> = Vec::new();
610    for endpoint in endpoints.drain(..) {
611        if kept
612            .iter()
613            .any(|existing| endpoint_boxes_overlap(existing, &endpoint))
614        {
615            continue;
616        }
617        kept.push(endpoint);
618    }
619    *endpoints = kept;
620}
621
622fn endpoint_boxes_overlap(
623    first: &SurfaceSurfaceTraceEndpoint3,
624    second: &SurfaceSurfaceTraceEndpoint3,
625) -> bool {
626    let left = first.parameters;
627    let right = second.parameters;
628    intervals_overlap(left.first_u, right.first_u)
629        && intervals_overlap(left.first_v, right.first_v)
630        && intervals_overlap(left.second_u, right.second_u)
631        && intervals_overlap(left.second_v, right.second_v)
632}
633
634fn intervals_overlap(first: ParameterInterval, second: ParameterInterval) -> bool {
635    first.start <= second.end && second.start <= first.end
636}
637
638fn lexicographic_point_cmp(left: Point3, right: Point3) -> std::cmp::Ordering {
639    left.x
640        .total_cmp(&right.x)
641        .then_with(|| left.y.total_cmp(&right.y))
642        .then_with(|| left.z.total_cmp(&right.z))
643}
644
645fn clone_points(values: impl ExactSizeIterator<Item = Point3>) -> GeomResult<Vec<Point3>> {
646    let count = values.len();
647    let mut output = Vec::new();
648    output
649        .try_reserve_exact(count)
650        .map_err(|_| allocation_error("certified surface/surface boundary allocation"))?;
651    for value in values {
652        output.push(value);
653    }
654    Ok(output)
655}
656
657fn clone_scalars(values: &[Scalar]) -> GeomResult<Vec<Scalar>> {
658    let mut output = Vec::new();
659    output
660        .try_reserve_exact(values.len())
661        .map_err(|_| allocation_error("certified surface/surface boundary allocation"))?;
662    output.extend(values.iter().copied());
663    Ok(output)
664}
665
666fn clone_multiplicities(values: &[u32]) -> GeomResult<Vec<u32>> {
667    let mut output = Vec::new();
668    output
669        .try_reserve_exact(values.len())
670        .map_err(|_| allocation_error("certified surface/surface boundary allocation"))?;
671    output.extend(values.iter().copied());
672    Ok(output)
673}