Skip to main content

hypercurve/
planar_pcurve.rs

1//! Retained planar pcurve image-equality evidence.
2//!
3//! BREP trims are usually carried as parameter-space curves on a supporting
4//! surface. For planar faces, the first exact question is not a sampled 3D
5//! proximity test: it is whether two pcurves lie on the same retained planar
6//! surface and replay the same UV image. This module keeps that evidence
7//! explicit, following Yap, "Towards Exact Geometric Computation,"
8//! *Computational Geometry* 7(1-2), 3-23 (1997), and the pcurve-on-surface
9//! representation used in Piegl and Tiller, *The NURBS Book* (2nd ed., 1997).
10
11use crate::{
12    Classification, Contour2, ContourPointLocation, CurveError, CurvePolicy, CurveResult,
13    CurveString2, Point2, PreparedRegionView2, RegionPointLocation, RegionView2, Segment2,
14    UncertaintyReason,
15};
16
17/// Opaque identity of a retained planar support surface.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct RetainedPlanarSurfaceIdentity2 {
20    source_index: u64,
21}
22
23/// Exact image relation between two retained planar pcurves.
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum PlanarPcurveImageRelation2 {
26    /// Both pcurves are on the same retained planar surface and have the same
27    /// UV segment image with the same traversal direction.
28    SameDirected,
29    /// Both pcurves are on the same retained planar surface and have the same
30    /// UV segment image with opposite traversal direction.
31    SameReversed,
32    /// The retained planar support surfaces differ, so the image equality
33    /// predicate is blocked before comparing UV curves.
34    SurfaceMismatch,
35    /// Both pcurves are on the same retained planar surface, but their exact
36    /// UV segment images differ.
37    Different,
38}
39
40/// Evidence report for one planar pcurve image-equality predicate.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct PlanarPcurveImageEqualityReport2 {
43    relation: PlanarPcurveImageRelation2,
44    surface: Option<RetainedPlanarSurfaceIdentity2>,
45    segment_count: usize,
46}
47
48/// Open retained pcurve on a planar support surface.
49#[derive(Clone, Debug, PartialEq)]
50pub struct RetainedPlanarPcurve2 {
51    surface: RetainedPlanarSurfaceIdentity2,
52    curve: CurveString2,
53}
54
55/// Closed retained trim-loop pcurve on a planar support surface.
56#[derive(Clone, Debug, PartialEq)]
57pub struct RetainedPlanarTrimLoop2 {
58    surface: RetainedPlanarSurfaceIdentity2,
59    contour: Contour2,
60}
61
62/// Retained planar face assembled from material and hole pcurve trim loops.
63#[derive(Clone, Debug, PartialEq)]
64pub struct RetainedPlanarFace2 {
65    surface: RetainedPlanarSurfaceIdentity2,
66    material_loops: Vec<RetainedPlanarTrimLoop2>,
67    hole_loops: Vec<RetainedPlanarTrimLoop2>,
68}
69
70/// Prepared retained planar face for repeated support-surface and UV queries.
71///
72/// The prepared object keeps the retained BREP support identity beside a
73/// prepared borrowed UV region. Cached boxes and prepared segment predicates
74/// are only broad-phase evidence: support-surface mismatch, boundary hits, and
75/// inside/outside status still replay through the exact classifiers. That
76/// separation follows Yap, "Towards Exact Geometric Computation,"
77/// *Computational Geometry* 7(1-2), 3-23 (1997), and the pcurve-on-surface
78/// face model in Piegl and Tiller, *The NURBS Book* (2nd ed., 1997).
79#[derive(Clone, Debug, PartialEq)]
80pub struct PreparedRetainedPlanarFace2<'a> {
81    face: &'a RetainedPlanarFace2,
82    region: PreparedRegionView2<'a>,
83}
84
85/// Point classification result for a retained planar face.
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum RetainedPlanarFacePointLocation2 {
88    /// The query was made against a different retained support surface.
89    SurfaceMismatch,
90    /// The UV point is outside the retained face.
91    Outside,
92    /// The UV point lies on a material or hole trim boundary.
93    Boundary,
94    /// The UV point is inside the retained face.
95    Inside,
96}
97
98/// Evidence report for an exact UV point-in-face query.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct RetainedPlanarFacePointReport2 {
101    location: RetainedPlanarFacePointLocation2,
102    surface: Option<RetainedPlanarSurfaceIdentity2>,
103    material_loop_count: usize,
104    hole_loop_count: usize,
105}
106
107/// Role of the retained trim loop that owns a matched pcurve edge-use.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub enum RetainedPlanarTrimLoopRole2 {
110    /// The matched pcurve lies on a material trim loop.
111    Material,
112    /// The matched pcurve lies on a hole trim loop.
113    Hole,
114}
115
116/// Exact edge-use agreement between a retained planar pcurve and face trims.
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
118pub enum RetainedPlanarFaceEdgeUseRelation2 {
119    /// The query was made against a different retained support surface.
120    SurfaceMismatch,
121    /// The pcurve's exact UV image matches a contiguous trim subchain in the
122    /// same traversal direction.
123    BoundarySameDirected,
124    /// The pcurve's exact UV image matches a contiguous trim subchain in the
125    /// opposite traversal direction.
126    BoundarySameReversed,
127    /// The support surface matches, but the pcurve image is not a retained
128    /// trim-boundary subchain of this face.
129    NotTrimBoundary,
130}
131
132/// Evidence report for a retained planar pcurve edge-use query.
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub struct RetainedPlanarFaceEdgeUseReport2 {
135    relation: RetainedPlanarFaceEdgeUseRelation2,
136    surface: Option<RetainedPlanarSurfaceIdentity2>,
137    trim_role: Option<RetainedPlanarTrimLoopRole2>,
138    trim_loop_index: Option<usize>,
139    trim_segment_index: Option<usize>,
140    segment_count: usize,
141    trim_role_loop_count: Option<usize>,
142    trim_loop_segment_count: Option<usize>,
143}
144
145impl RetainedPlanarSurfaceIdentity2 {
146    /// Constructs an opaque retained planar surface identity.
147    pub const fn new(source_index: u64) -> Self {
148        Self { source_index }
149    }
150
151    /// Returns the opaque source index for this planar support surface.
152    pub const fn source_index(self) -> u64 {
153        self.source_index
154    }
155}
156
157impl PlanarPcurveImageRelation2 {
158    /// Returns true when the reports certify equal UV images.
159    pub const fn is_same_image(self) -> bool {
160        matches!(self, Self::SameDirected | Self::SameReversed)
161    }
162
163    /// Returns true when equal images have opposite traversal orientation.
164    pub const fn is_reversed(self) -> bool {
165        matches!(self, Self::SameReversed)
166    }
167}
168
169impl PlanarPcurveImageEqualityReport2 {
170    /// Constructs a planar pcurve image-equality report.
171    pub fn new(
172        relation: PlanarPcurveImageRelation2,
173        surface: Option<RetainedPlanarSurfaceIdentity2>,
174        segment_count: usize,
175    ) -> CurveResult<Self> {
176        validate_planar_pcurve_image_report(relation, surface, segment_count)?;
177        Ok(Self {
178            relation,
179            surface,
180            segment_count,
181        })
182    }
183
184    /// Returns the certified relation.
185    pub const fn relation(&self) -> PlanarPcurveImageRelation2 {
186        self.relation
187    }
188
189    /// Returns the common retained surface when both pcurves share one.
190    pub const fn surface(&self) -> Option<RetainedPlanarSurfaceIdentity2> {
191        self.surface
192    }
193
194    /// Returns the segment count in the compared UV image when it matched.
195    pub const fn segment_count(&self) -> usize {
196        self.segment_count
197    }
198}
199
200impl RetainedPlanarPcurve2 {
201    /// Constructs an open retained planar pcurve.
202    pub const fn new(surface: RetainedPlanarSurfaceIdentity2, curve: CurveString2) -> Self {
203        Self { surface, curve }
204    }
205
206    /// Returns the retained planar surface identity.
207    pub const fn surface(&self) -> RetainedPlanarSurfaceIdentity2 {
208        self.surface
209    }
210
211    /// Returns the retained UV curve string.
212    pub const fn curve(&self) -> &CurveString2 {
213        &self.curve
214    }
215
216    /// Compares two open planar pcurves by exact UV image.
217    ///
218    /// This is a structural exact predicate over already split native segments:
219    /// equal images must have identical segment boundaries in UV, either in
220    /// the same order or in exact reverse order. It deliberately does not
221    /// sample or merge unsplit overlaps; those remain later trim-splitting
222    /// work under Yap's construction/predicate boundary.
223    pub fn image_equality_report(
224        &self,
225        other: &Self,
226    ) -> CurveResult<PlanarPcurveImageEqualityReport2> {
227        if self.surface != other.surface {
228            return PlanarPcurveImageEqualityReport2::new(
229                PlanarPcurveImageRelation2::SurfaceMismatch,
230                None,
231                0,
232            );
233        }
234        let relation = if same_directed_segments(self.curve.segments(), other.curve.segments()) {
235            PlanarPcurveImageRelation2::SameDirected
236        } else if same_reversed_segments(self.curve.segments(), other.curve.segments()) {
237            PlanarPcurveImageRelation2::SameReversed
238        } else {
239            PlanarPcurveImageRelation2::Different
240        };
241        let segment_count = usize::from(relation.is_same_image()) * self.curve.len();
242        PlanarPcurveImageEqualityReport2::new(relation, Some(self.surface), segment_count)
243    }
244}
245
246impl RetainedPlanarTrimLoop2 {
247    /// Constructs a closed retained planar trim-loop pcurve.
248    pub const fn new(surface: RetainedPlanarSurfaceIdentity2, contour: Contour2) -> Self {
249        Self { surface, contour }
250    }
251
252    /// Returns the retained planar surface identity.
253    pub const fn surface(&self) -> RetainedPlanarSurfaceIdentity2 {
254        self.surface
255    }
256
257    /// Returns the retained UV contour.
258    pub const fn contour(&self) -> &Contour2 {
259        &self.contour
260    }
261
262    /// Compares two closed planar trim loops by exact cyclic UV image.
263    ///
264    /// Closed loops may start at different trim vertices, so this accepts
265    /// cyclic rotations as well as opposite traversal direction. Fill rules are
266    /// not part of pcurve image equality; this is only the support-surface/UV
267    /// image predicate needed before face-role policy can run.
268    pub fn image_equality_report(
269        &self,
270        other: &Self,
271    ) -> CurveResult<PlanarPcurveImageEqualityReport2> {
272        if self.surface != other.surface {
273            return PlanarPcurveImageEqualityReport2::new(
274                PlanarPcurveImageRelation2::SurfaceMismatch,
275                None,
276                0,
277            );
278        }
279        let relation =
280            if same_directed_segment_cycle(self.contour.segments(), other.contour.segments()) {
281                PlanarPcurveImageRelation2::SameDirected
282            } else if same_reversed_segment_cycle(self.contour.segments(), other.contour.segments())
283            {
284                PlanarPcurveImageRelation2::SameReversed
285            } else {
286                PlanarPcurveImageRelation2::Different
287            };
288        let segment_count = usize::from(relation.is_same_image()) * self.contour.len();
289        PlanarPcurveImageEqualityReport2::new(relation, Some(self.surface), segment_count)
290    }
291}
292
293impl RetainedPlanarFace2 {
294    /// Constructs a retained planar face from material and hole trim loops.
295    ///
296    /// Every trim loop must reference the same retained planar support surface.
297    /// This validates the support-surface part of the BREP face before any
298    /// point-in-face predicate is allowed to consume UV topology. That is the
299    /// construction/predicate boundary from Yap, "Towards Exact Geometric
300    /// Computation" (1997), applied to planar pcurves as described by Piegl
301    /// and Tiller, *The NURBS Book* (2nd ed., 1997).
302    pub fn try_new(
303        surface: RetainedPlanarSurfaceIdentity2,
304        material_loops: Vec<RetainedPlanarTrimLoop2>,
305        hole_loops: Vec<RetainedPlanarTrimLoop2>,
306    ) -> CurveResult<Self> {
307        if material_loops.is_empty() {
308            return Err(CurveError::InvalidPlanarFace);
309        }
310        if material_loops
311            .iter()
312            .chain(hole_loops.iter())
313            .any(|trim| trim.surface != surface)
314        {
315            return Err(CurveError::InvalidPlanarFace);
316        }
317        validate_planar_face_simple_trim_loops(&material_loops)?;
318        validate_planar_face_simple_trim_loops(&hole_loops)?;
319        validate_planar_face_distinct_trim_loops(&material_loops, &hole_loops)?;
320        validate_planar_face_same_role_trim_separation(&material_loops)?;
321        validate_planar_face_same_role_trim_separation(&hole_loops)?;
322        validate_planar_face_hole_ownership(&material_loops, &hole_loops)?;
323        Ok(Self {
324            surface,
325            material_loops,
326            hole_loops,
327        })
328    }
329
330    /// Returns the retained planar support surface.
331    pub const fn surface(&self) -> RetainedPlanarSurfaceIdentity2 {
332        self.surface
333    }
334
335    /// Returns material trim loops.
336    pub fn material_loops(&self) -> &[RetainedPlanarTrimLoop2] {
337        &self.material_loops
338    }
339
340    /// Returns hole trim loops.
341    pub fn hole_loops(&self) -> &[RetainedPlanarTrimLoop2] {
342        &self.hole_loops
343    }
344
345    /// Prepares this face for repeated support-surface and UV point queries.
346    ///
347    /// Preparation borrows the retained trim loops and caches the UV
348    /// [`PreparedRegionView2`] used by repeated point-in-face calls. It does
349    /// not certify any query by itself; every call still first checks the
350    /// retained support-surface identity and then delegates to the exact
351    /// boundary-first region classifier.
352    pub fn prepare_point_queries(&self, policy: &CurvePolicy) -> PreparedRetainedPlanarFace2<'_> {
353        let material = self
354            .material_loops
355            .iter()
356            .map(|trim| trim.contour())
357            .collect::<Vec<_>>();
358        let holes = self
359            .hole_loops
360            .iter()
361            .map(|trim| trim.contour())
362            .collect::<Vec<_>>();
363        let region = RegionView2::from_contours(material, holes);
364        PreparedRetainedPlanarFace2 {
365            face: self,
366            region: PreparedRegionView2::from_region_view(&region, policy),
367        }
368    }
369
370    /// Prepares this face for repeated retained topology queries.
371    ///
372    /// This currently exposes the same point-query package as
373    /// [`RetainedPlanarFace2::prepare_point_queries`]. Segment/edge-use and
374    /// analytic-surface frame packages can extend the prepared face handle
375    /// without changing the support-surface-first report contract.
376    pub fn prepare_topology_queries(
377        &self,
378        policy: &CurvePolicy,
379    ) -> PreparedRetainedPlanarFace2<'_> {
380        self.prepare_point_queries(policy)
381    }
382
383    /// Classifies a UV point against this retained planar face.
384    ///
385    /// The query first checks retained support-surface identity. Only matching
386    /// surfaces are passed to the exact UV region classifier, which checks trim
387    /// boundaries before winding/inside status. This preserves the BREP
388    /// distinction between support-surface agreement and trim containment
389    /// rather than collapsing both into a sampled point-in-polygon test.
390    pub fn classify_uv_point(
391        &self,
392        query_surface: RetainedPlanarSurfaceIdentity2,
393        uv: &Point2,
394        policy: &CurvePolicy,
395    ) -> CurveResult<Classification<RetainedPlanarFacePointReport2>> {
396        if query_surface != self.surface {
397            return Ok(Classification::Decided(
398                RetainedPlanarFacePointReport2::new(
399                    RetainedPlanarFacePointLocation2::SurfaceMismatch,
400                    None,
401                    self.material_loops.len(),
402                    self.hole_loops.len(),
403                )?,
404            ));
405        }
406
407        let material = self
408            .material_loops
409            .iter()
410            .map(|trim| trim.contour())
411            .collect::<Vec<_>>();
412        let holes = self
413            .hole_loops
414            .iter()
415            .map(|trim| trim.contour())
416            .collect::<Vec<_>>();
417        let region = RegionView2::from_contours(material, holes);
418        face_point_report_from_region_classification(
419            region.classify_point(uv, policy),
420            self.surface,
421            self.material_loops.len(),
422            self.hole_loops.len(),
423        )
424    }
425
426    /// Reports whether an open retained planar pcurve is a face trim edge-use.
427    ///
428    /// This predicate is structural over retained UV segments: the pcurve must
429    /// be an exact contiguous subchain of a material or hole trim loop, either
430    /// directed or reversed. It deliberately does not project, sample, or
431    /// overlap-split arbitrary curves. That mirrors Yap's EGC requirement that
432    /// combinatorial topology be accepted only after replaying exact
433    /// construction evidence, and it follows the BREP pcurve edge-use model
434    /// described by Piegl and Tiller, *The NURBS Book* (2nd ed., 1997).
435    pub fn edge_use_report(
436        &self,
437        pcurve: &RetainedPlanarPcurve2,
438    ) -> CurveResult<RetainedPlanarFaceEdgeUseReport2> {
439        if pcurve.surface != self.surface {
440            return RetainedPlanarFaceEdgeUseReport2::new(
441                RetainedPlanarFaceEdgeUseRelation2::SurfaceMismatch,
442                None,
443                None,
444                None,
445                None,
446                0,
447            );
448        }
449
450        face_edge_use_report_from_loops(self, pcurve.curve.segments())
451    }
452}
453
454impl<'a> PreparedRetainedPlanarFace2<'a> {
455    /// Returns the retained planar face that supplied this prepared view.
456    pub const fn face(&self) -> &'a RetainedPlanarFace2 {
457        self.face
458    }
459
460    /// Returns the retained planar support surface.
461    pub const fn surface(&self) -> RetainedPlanarSurfaceIdentity2 {
462        self.face.surface
463    }
464
465    /// Returns the prepared borrowed UV region.
466    pub const fn prepared_region(&self) -> &PreparedRegionView2<'a> {
467        &self.region
468    }
469
470    /// Returns the number of retained material trim loops.
471    pub fn material_loop_count(&self) -> usize {
472        self.face.material_loops.len()
473    }
474
475    /// Returns the number of retained hole trim loops.
476    pub fn hole_loop_count(&self) -> usize {
477        self.face.hole_loops.len()
478    }
479
480    /// Classifies a UV point against this prepared retained planar face.
481    ///
482    /// The support-surface identity check intentionally stays outside the
483    /// prepared UV region. In Yap's EGC terms, preparation only retains
484    /// reusable object structure; it does not turn a query against the wrong
485    /// supporting surface into a geometric predicate.
486    pub fn classify_uv_point(
487        &self,
488        query_surface: RetainedPlanarSurfaceIdentity2,
489        uv: &Point2,
490        policy: &CurvePolicy,
491    ) -> CurveResult<Classification<RetainedPlanarFacePointReport2>> {
492        if query_surface != self.face.surface {
493            return Ok(Classification::Decided(
494                RetainedPlanarFacePointReport2::new(
495                    RetainedPlanarFacePointLocation2::SurfaceMismatch,
496                    None,
497                    self.material_loop_count(),
498                    self.hole_loop_count(),
499                )?,
500            ));
501        }
502
503        face_point_report_from_region_classification(
504            self.region.classify_point(uv, policy),
505            self.face.surface,
506            self.material_loop_count(),
507            self.hole_loop_count(),
508        )
509    }
510
511    /// Reports whether an open retained planar pcurve is a prepared face trim edge-use.
512    ///
513    /// Preparation does not change the proof obligation: support-surface
514    /// identity is still checked first, and the accepted edge-use must replay
515    /// as an exact contiguous UV subchain of a retained trim. The prepared face
516    /// owns the borrowed trim structure needed by future broad-phase segment
517    /// filters while keeping this exact image predicate authoritative.
518    pub fn edge_use_report(
519        &self,
520        pcurve: &RetainedPlanarPcurve2,
521    ) -> CurveResult<RetainedPlanarFaceEdgeUseReport2> {
522        if pcurve.surface != self.face.surface {
523            return RetainedPlanarFaceEdgeUseReport2::new(
524                RetainedPlanarFaceEdgeUseRelation2::SurfaceMismatch,
525                None,
526                None,
527                None,
528                None,
529                0,
530            );
531        }
532
533        face_edge_use_report_from_loops(self.face, pcurve.curve.segments())
534    }
535}
536
537fn validate_planar_face_distinct_trim_loops(
538    material_loops: &[RetainedPlanarTrimLoop2],
539    hole_loops: &[RetainedPlanarTrimLoop2],
540) -> CurveResult<()> {
541    for (index, trim) in material_loops.iter().enumerate() {
542        if material_loops[index + 1..].contains(trim) || hole_loops.contains(trim) {
543            return Err(CurveError::InvalidPlanarFace);
544        }
545    }
546    for (index, trim) in hole_loops.iter().enumerate() {
547        if hole_loops[index + 1..].contains(trim) {
548            return Err(CurveError::InvalidPlanarFace);
549        }
550    }
551    Ok(())
552}
553
554fn validate_planar_face_simple_trim_loops(loops: &[RetainedPlanarTrimLoop2]) -> CurveResult<()> {
555    let policy = CurvePolicy::certified();
556    for trim in loops {
557        match trim.contour.has_self_contacts(&policy)? {
558            Classification::Decided(false) => {}
559            Classification::Decided(true) | Classification::Uncertain(_) => {
560                return Err(CurveError::InvalidPlanarFace);
561            }
562        }
563    }
564    Ok(())
565}
566
567fn validate_planar_face_same_role_trim_separation(
568    loops: &[RetainedPlanarTrimLoop2],
569) -> CurveResult<()> {
570    let policy = CurvePolicy::certified();
571    for (index, trim) in loops.iter().enumerate() {
572        for other in &loops[index + 1..] {
573            if !trim
574                .contour
575                .intersect_contour(&other.contour, &policy)?
576                .is_empty()
577            {
578                return Err(CurveError::InvalidPlanarFace);
579            }
580        }
581    }
582    Ok(())
583}
584
585fn validate_planar_face_hole_ownership(
586    material_loops: &[RetainedPlanarTrimLoop2],
587    hole_loops: &[RetainedPlanarTrimLoop2],
588) -> CurveResult<()> {
589    let policy = CurvePolicy::certified();
590    for hole in hole_loops {
591        let Some(point) = hole
592            .contour
593            .segments()
594            .first()
595            .map(|segment| segment.start())
596        else {
597            return Err(CurveError::InvalidPlanarFace);
598        };
599        let mut owned_by_material = false;
600        for material in material_loops {
601            if !material
602                .contour
603                .intersect_contour(&hole.contour, &policy)?
604                .is_empty()
605            {
606                return Err(CurveError::InvalidPlanarFace);
607            }
608            match material.contour.classify_point(point, &policy) {
609                Classification::Decided(ContourPointLocation::Inside) => {
610                    owned_by_material = true;
611                }
612                Classification::Decided(
613                    ContourPointLocation::Boundary | ContourPointLocation::Outside,
614                ) => {}
615                Classification::Uncertain(_) => return Err(CurveError::InvalidPlanarFace),
616            }
617        }
618        if !owned_by_material {
619            return Err(CurveError::InvalidPlanarFace);
620        }
621    }
622    Ok(())
623}
624
625impl RetainedPlanarFacePointLocation2 {
626    /// Returns true when the query reached an exact inside/outside/boundary result.
627    pub const fn is_trim_classification(self) -> bool {
628        !matches!(self, Self::SurfaceMismatch)
629    }
630}
631
632impl RetainedPlanarTrimLoopRole2 {
633    /// Returns true for material loops.
634    pub const fn is_material(self) -> bool {
635        matches!(self, Self::Material)
636    }
637
638    /// Returns true for hole loops.
639    pub const fn is_hole(self) -> bool {
640        matches!(self, Self::Hole)
641    }
642}
643
644impl RetainedPlanarFaceEdgeUseRelation2 {
645    /// Returns true when the pcurve is certified as a retained trim boundary.
646    pub const fn is_boundary(self) -> bool {
647        matches!(
648            self,
649            Self::BoundarySameDirected | Self::BoundarySameReversed
650        )
651    }
652
653    /// Returns true when the matched boundary image has opposite traversal.
654    pub const fn is_reversed(self) -> bool {
655        matches!(self, Self::BoundarySameReversed)
656    }
657}
658
659impl RetainedPlanarFaceEdgeUseReport2 {
660    /// Constructs a retained planar face edge-use report.
661    ///
662    /// Boundary reports are produced by retained-face query methods because
663    /// they require face extent evidence to certify trim-loop and segment
664    /// indices. This constructor accepts only self-contained blocker reports.
665    pub fn new(
666        relation: RetainedPlanarFaceEdgeUseRelation2,
667        surface: Option<RetainedPlanarSurfaceIdentity2>,
668        trim_role: Option<RetainedPlanarTrimLoopRole2>,
669        trim_loop_index: Option<usize>,
670        trim_segment_index: Option<usize>,
671        segment_count: usize,
672    ) -> CurveResult<Self> {
673        validate_planar_face_edge_use_report(
674            relation,
675            surface,
676            trim_role,
677            trim_loop_index,
678            trim_segment_index,
679            segment_count,
680            None,
681            None,
682        )?;
683        Ok(Self {
684            relation,
685            surface,
686            trim_role,
687            trim_loop_index,
688            trim_segment_index,
689            segment_count,
690            trim_role_loop_count: None,
691            trim_loop_segment_count: None,
692        })
693    }
694
695    #[allow(clippy::too_many_arguments)]
696    fn new_with_face_extent_evidence(
697        relation: RetainedPlanarFaceEdgeUseRelation2,
698        surface: RetainedPlanarSurfaceIdentity2,
699        trim_role: RetainedPlanarTrimLoopRole2,
700        trim_loop_index: usize,
701        trim_segment_index: usize,
702        segment_count: usize,
703        trim_role_loop_count: usize,
704        trim_loop_segment_count: usize,
705    ) -> CurveResult<Self> {
706        validate_planar_face_edge_use_report(
707            relation,
708            Some(surface),
709            Some(trim_role),
710            Some(trim_loop_index),
711            Some(trim_segment_index),
712            segment_count,
713            Some(trim_role_loop_count),
714            Some(trim_loop_segment_count),
715        )?;
716        Ok(Self {
717            relation,
718            surface: Some(surface),
719            trim_role: Some(trim_role),
720            trim_loop_index: Some(trim_loop_index),
721            trim_segment_index: Some(trim_segment_index),
722            segment_count,
723            trim_role_loop_count: Some(trim_role_loop_count),
724            trim_loop_segment_count: Some(trim_loop_segment_count),
725        })
726    }
727
728    /// Returns the certified edge-use relation or blocker.
729    pub const fn relation(&self) -> RetainedPlanarFaceEdgeUseRelation2 {
730        self.relation
731    }
732
733    /// Returns the matching retained surface when edge-use matching ran.
734    pub const fn surface(&self) -> Option<RetainedPlanarSurfaceIdentity2> {
735        self.surface
736    }
737
738    /// Returns the role of the matched trim loop, when boundary evidence exists.
739    pub const fn trim_role(&self) -> Option<RetainedPlanarTrimLoopRole2> {
740        self.trim_role
741    }
742
743    /// Returns the matched trim loop index inside its material or hole bin.
744    pub const fn trim_loop_index(&self) -> Option<usize> {
745        self.trim_loop_index
746    }
747
748    /// Returns the matched trim segment index where the pcurve traversal starts.
749    ///
750    /// For reversed matches, this is the original trim segment whose reversed
751    /// image supplies the first pcurve segment.
752    pub const fn trim_segment_index(&self) -> Option<usize> {
753        self.trim_segment_index
754    }
755
756    /// Returns the number of pcurve segments accepted as trim-boundary evidence.
757    pub const fn segment_count(&self) -> usize {
758        self.segment_count
759    }
760}
761
762impl RetainedPlanarFacePointReport2 {
763    /// Constructs a retained planar face point-query report.
764    pub fn new(
765        location: RetainedPlanarFacePointLocation2,
766        surface: Option<RetainedPlanarSurfaceIdentity2>,
767        material_loop_count: usize,
768        hole_loop_count: usize,
769    ) -> CurveResult<Self> {
770        validate_planar_face_point_report(location, surface, material_loop_count)?;
771        Ok(Self {
772            location,
773            surface,
774            material_loop_count,
775            hole_loop_count,
776        })
777    }
778
779    /// Returns the exact query location or blocker.
780    pub const fn location(&self) -> RetainedPlanarFacePointLocation2 {
781        self.location
782    }
783
784    /// Returns the matching retained surface when the query reached trim classification.
785    pub const fn surface(&self) -> Option<RetainedPlanarSurfaceIdentity2> {
786        self.surface
787    }
788
789    /// Returns the number of material trim loops in the face.
790    pub const fn material_loop_count(&self) -> usize {
791        self.material_loop_count
792    }
793
794    /// Returns the number of hole trim loops in the face.
795    pub const fn hole_loop_count(&self) -> usize {
796        self.hole_loop_count
797    }
798}
799
800fn validate_planar_pcurve_image_report(
801    relation: PlanarPcurveImageRelation2,
802    surface: Option<RetainedPlanarSurfaceIdentity2>,
803    segment_count: usize,
804) -> CurveResult<()> {
805    match relation {
806        PlanarPcurveImageRelation2::SurfaceMismatch => {
807            if surface.is_some() || segment_count != 0 {
808                return Err(CurveError::Topology(
809                    "surface-mismatch pcurve image report must not carry image evidence".into(),
810                ));
811            }
812        }
813        PlanarPcurveImageRelation2::Different => {
814            if surface.is_none() || segment_count != 0 {
815                return Err(CurveError::Topology(
816                    "different pcurve image report must carry only matching-surface evidence"
817                        .into(),
818                ));
819            }
820        }
821        PlanarPcurveImageRelation2::SameDirected | PlanarPcurveImageRelation2::SameReversed => {
822            if surface.is_none() || segment_count == 0 {
823                return Err(CurveError::Topology(
824                    "same-image pcurve report must carry surface and positive segment evidence"
825                        .into(),
826                ));
827            }
828        }
829    }
830    Ok(())
831}
832
833fn validate_planar_face_point_report(
834    location: RetainedPlanarFacePointLocation2,
835    surface: Option<RetainedPlanarSurfaceIdentity2>,
836    material_loop_count: usize,
837) -> CurveResult<()> {
838    if material_loop_count == 0 {
839        return Err(CurveError::Topology(
840            "retained planar face point report must reference a face with material loops".into(),
841        ));
842    }
843    match location {
844        RetainedPlanarFacePointLocation2::SurfaceMismatch => {
845            if surface.is_some() {
846                return Err(CurveError::Topology(
847                    "surface-mismatch point report must not carry trim-classification surface evidence"
848                        .into(),
849                ));
850            }
851        }
852        RetainedPlanarFacePointLocation2::Outside
853        | RetainedPlanarFacePointLocation2::Boundary
854        | RetainedPlanarFacePointLocation2::Inside => {
855            if surface.is_none() {
856                return Err(CurveError::Topology(
857                    "trim-classified point report must carry matching surface evidence".into(),
858                ));
859            }
860        }
861    }
862    Ok(())
863}
864
865fn validate_planar_face_edge_use_report(
866    relation: RetainedPlanarFaceEdgeUseRelation2,
867    surface: Option<RetainedPlanarSurfaceIdentity2>,
868    trim_role: Option<RetainedPlanarTrimLoopRole2>,
869    trim_loop_index: Option<usize>,
870    trim_segment_index: Option<usize>,
871    segment_count: usize,
872    trim_role_loop_count: Option<usize>,
873    trim_loop_segment_count: Option<usize>,
874) -> CurveResult<()> {
875    match relation {
876        RetainedPlanarFaceEdgeUseRelation2::SurfaceMismatch => {
877            if surface.is_some()
878                || trim_role.is_some()
879                || trim_loop_index.is_some()
880                || trim_segment_index.is_some()
881                || segment_count != 0
882                || trim_role_loop_count.is_some()
883                || trim_loop_segment_count.is_some()
884            {
885                return Err(CurveError::Topology(
886                    "surface-mismatch edge-use report must not carry trim evidence".into(),
887                ));
888            }
889        }
890        RetainedPlanarFaceEdgeUseRelation2::NotTrimBoundary => {
891            if surface.is_none()
892                || trim_role.is_some()
893                || trim_loop_index.is_some()
894                || trim_segment_index.is_some()
895                || segment_count != 0
896                || trim_role_loop_count.is_some()
897                || trim_loop_segment_count.is_some()
898            {
899                return Err(CurveError::Topology(
900                    "non-boundary edge-use report must carry only matching-surface evidence".into(),
901                ));
902            }
903        }
904        RetainedPlanarFaceEdgeUseRelation2::BoundarySameDirected
905        | RetainedPlanarFaceEdgeUseRelation2::BoundarySameReversed => {
906            if surface.is_none()
907                || trim_role.is_none()
908                || trim_loop_index.is_none()
909                || trim_segment_index.is_none()
910                || segment_count == 0
911                || trim_role_loop_count.is_none()
912                || trim_loop_segment_count.is_none()
913            {
914                return Err(CurveError::Topology(
915                    "boundary edge-use report must carry complete positive trim evidence".into(),
916                ));
917            }
918            let (
919                Some(trim_loop_index),
920                Some(trim_segment_index),
921                Some(trim_role_loop_count),
922                Some(trim_loop_segment_count),
923            ) = (
924                trim_loop_index,
925                trim_segment_index,
926                trim_role_loop_count,
927                trim_loop_segment_count,
928            )
929            else {
930                return Err(CurveError::Topology(
931                    "boundary edge-use report must carry complete positive trim evidence".into(),
932                ));
933            };
934            if trim_role_loop_count == 0
935                || trim_loop_segment_count == 0
936                || trim_loop_index >= trim_role_loop_count
937                || trim_segment_index >= trim_loop_segment_count
938                || segment_count > trim_loop_segment_count
939            {
940                return Err(CurveError::Topology(
941                    "boundary edge-use report trim indices must be certified by face extent evidence"
942                        .into(),
943                ));
944            }
945        }
946    }
947    Ok(())
948}
949
950fn same_directed_segments(first: &[Segment2], second: &[Segment2]) -> bool {
951    first == second
952}
953
954fn same_reversed_segments(first: &[Segment2], second: &[Segment2]) -> bool {
955    first.len() == second.len()
956        && first
957            .iter()
958            .zip(second.iter().rev())
959            .all(|(left, right)| left == &right.reversed())
960}
961
962fn same_directed_segment_cycle(first: &[Segment2], second: &[Segment2]) -> bool {
963    let len = first.len();
964    if len != second.len() {
965        return false;
966    }
967    (0..len).any(|offset| {
968        first
969            .iter()
970            .enumerate()
971            .all(|(index, segment)| segment == &second[(offset + index) % len])
972    })
973}
974
975fn same_reversed_segment_cycle(first: &[Segment2], second: &[Segment2]) -> bool {
976    let len = first.len();
977    if len != second.len() {
978        return false;
979    }
980    (0..len).any(|offset| {
981        first.iter().enumerate().all(|(index, segment)| {
982            let reversed_index = (offset + len - 1 - index) % len;
983            segment == &second[reversed_index].reversed()
984        })
985    })
986}
987
988fn face_edge_use_report_from_loops(
989    face: &RetainedPlanarFace2,
990    query_segments: &[Segment2],
991) -> CurveResult<RetainedPlanarFaceEdgeUseReport2> {
992    for (loop_index, trim) in face.material_loops.iter().enumerate() {
993        if let Some((relation, segment_index)) =
994            segment_subchain_relation(query_segments, trim.contour.segments())
995        {
996            return RetainedPlanarFaceEdgeUseReport2::new_with_face_extent_evidence(
997                relation,
998                face.surface,
999                RetainedPlanarTrimLoopRole2::Material,
1000                loop_index,
1001                segment_index,
1002                query_segments.len(),
1003                face.material_loops.len(),
1004                trim.contour.len(),
1005            );
1006        }
1007    }
1008    for (loop_index, trim) in face.hole_loops.iter().enumerate() {
1009        if let Some((relation, segment_index)) =
1010            segment_subchain_relation(query_segments, trim.contour.segments())
1011        {
1012            return RetainedPlanarFaceEdgeUseReport2::new_with_face_extent_evidence(
1013                relation,
1014                face.surface,
1015                RetainedPlanarTrimLoopRole2::Hole,
1016                loop_index,
1017                segment_index,
1018                query_segments.len(),
1019                face.hole_loops.len(),
1020                trim.contour.len(),
1021            );
1022        }
1023    }
1024
1025    RetainedPlanarFaceEdgeUseReport2::new(
1026        RetainedPlanarFaceEdgeUseRelation2::NotTrimBoundary,
1027        Some(face.surface),
1028        None,
1029        None,
1030        None,
1031        0,
1032    )
1033}
1034
1035fn segment_subchain_relation(
1036    query_segments: &[Segment2],
1037    loop_segments: &[Segment2],
1038) -> Option<(RetainedPlanarFaceEdgeUseRelation2, usize)> {
1039    if query_segments.is_empty() || query_segments.len() > loop_segments.len() {
1040        return None;
1041    }
1042    if let Some(segment_index) = directed_segment_subchain_start(query_segments, loop_segments) {
1043        return Some((
1044            RetainedPlanarFaceEdgeUseRelation2::BoundarySameDirected,
1045            segment_index,
1046        ));
1047    }
1048    reversed_segment_subchain_start(query_segments, loop_segments).map(|segment_index| {
1049        (
1050            RetainedPlanarFaceEdgeUseRelation2::BoundarySameReversed,
1051            segment_index,
1052        )
1053    })
1054}
1055
1056fn directed_segment_subchain_start(
1057    query_segments: &[Segment2],
1058    loop_segments: &[Segment2],
1059) -> Option<usize> {
1060    let len = loop_segments.len();
1061    (0..len).find(|&offset| {
1062        query_segments
1063            .iter()
1064            .enumerate()
1065            .all(|(index, segment)| segment == &loop_segments[(offset + index) % len])
1066    })
1067}
1068
1069fn reversed_segment_subchain_start(
1070    query_segments: &[Segment2],
1071    loop_segments: &[Segment2],
1072) -> Option<usize> {
1073    let len = loop_segments.len();
1074    (0..len).find(|&offset| {
1075        query_segments.iter().enumerate().all(|(index, segment)| {
1076            let loop_index = (offset + len - index) % len;
1077            segment == &loop_segments[loop_index].reversed()
1078        })
1079    })
1080}
1081
1082fn face_point_report_from_region_classification(
1083    classification: Classification<RegionPointLocation>,
1084    surface: RetainedPlanarSurfaceIdentity2,
1085    material_loop_count: usize,
1086    hole_loop_count: usize,
1087) -> CurveResult<Classification<RetainedPlanarFacePointReport2>> {
1088    let location = match classification {
1089        Classification::Decided(RegionPointLocation::Outside) => {
1090            RetainedPlanarFacePointLocation2::Outside
1091        }
1092        Classification::Decided(RegionPointLocation::Boundary) => {
1093            RetainedPlanarFacePointLocation2::Boundary
1094        }
1095        Classification::Decided(RegionPointLocation::Inside) => {
1096            RetainedPlanarFacePointLocation2::Inside
1097        }
1098        Classification::Uncertain(UncertaintyReason::Boundary) => {
1099            RetainedPlanarFacePointLocation2::Boundary
1100        }
1101        Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
1102    };
1103    Ok(Classification::Decided(
1104        RetainedPlanarFacePointReport2::new(
1105            location,
1106            Some(surface),
1107            material_loop_count,
1108            hole_loop_count,
1109        )?,
1110    ))
1111}