Skip to main content

ogeom_topo/
entity.rs

1//! What hangs off a topology node: geometry, tolerances, edge representations.
2//!
3//! `docs/DATA_MODEL.md` §5 and §6.
4//!
5//! # Tolerances are per entity
6//!
7//! Every vertex, edge and face carries its own [`Tolerance`]: the radius
8//! within which it is considered to lie. Operations may only widen one, and the
9//! containment rule `tol(vertex) >= tol(edge) >= tol(face)` holds between
10//! entities in a boundary relationship ([`check_containment`]).
11//!
12//! This is not a workaround for imprecise code. Exact arithmetic cannot
13//! represent the intersection curve of two curved surfaces, so a
14//! tolerance-carrying topology is the only known way to build a kernel whose
15//! results close up. Every production kernel works this way.
16//!
17//! # An edge carries a list of representations
18//!
19//! Not one curve: a list. A single edge holds a 3D curve, *one pcurve per
20//! adjacent face*, two pcurves where it is a seam on a closed surface, and
21//! cached polylines. Face splitting during a boolean happens in a surface's
22//! 2D parameter space, so without a pcurve on each face there is nothing to
23//! split with; and since surfaces are parameterized differently, one 2D curve
24//! cannot serve two faces.
25
26use ogeom_core::{Arena, Key, OgeomResult, Tolerance, Tolerances, ogeom_bail};
27use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry};
28use ogeom_math::Point;
29use smallvec::SmallVec;
30
31use crate::location::Location;
32use crate::tessellation::Triangulation;
33
34/// A handle to a space curve.
35pub type CurveId = Key<Curve>;
36/// A handle to a curve in a surface's parameter space.
37pub type PCurveId = Key<PlanarCurve>;
38/// A handle to a surface.
39pub type SurfaceId = Key<SurfaceGeometry>;
40
41/// A handle to a cached triangulation.
42pub type TriangulationId = Key<Triangulation>;
43
44/// The geometry a model's topology refers into.
45#[derive(Debug, Clone, Default)]
46pub struct GeometryStore {
47    curves: Arena<Curve>,
48    pcurves: Arena<PlanarCurve>,
49    surfaces: Arena<SurfaceGeometry>,
50    triangulations: Arena<Triangulation>,
51}
52
53impl GeometryStore {
54    /// An empty store.
55    #[must_use]
56    pub const fn new() -> Self {
57        Self {
58            curves: Arena::new(),
59            pcurves: Arena::new(),
60            surfaces: Arena::new(),
61            triangulations: Arena::new(),
62        }
63    }
64
65    /// Add a space curve.
66    pub fn add_curve(&mut self, curve: Curve) -> CurveId {
67        self.curves.insert(curve)
68    }
69
70    /// Add a curve in parameter space.
71    pub fn add_pcurve(&mut self, curve: PlanarCurve) -> PCurveId {
72        self.pcurves.insert(curve)
73    }
74
75    /// Add a surface.
76    pub fn add_surface(&mut self, surface: SurfaceGeometry) -> SurfaceId {
77        self.surfaces.insert(surface)
78    }
79
80    /// Add a cached triangulation.
81    pub fn add_triangulation(&mut self, mesh: Triangulation) -> TriangulationId {
82        self.triangulations.insert(mesh)
83    }
84
85    /// The space curve behind `id`.
86    #[must_use]
87    pub fn curve(&self, id: CurveId) -> Option<&Curve> {
88        self.curves.get(id)
89    }
90
91    /// The parameter-space curve behind `id`.
92    #[must_use]
93    pub fn pcurve(&self, id: PCurveId) -> Option<&PlanarCurve> {
94        self.pcurves.get(id)
95    }
96
97    /// A pcurve, for rewriting in place: an image shifted by whole periods
98    /// onto its neighbour's branch is the same curve on the same edge.
99    pub fn pcurve_mut(&mut self, id: PCurveId) -> Option<&mut PlanarCurve> {
100        self.pcurves.get_mut(id)
101    }
102
103    /// The surface behind `id`.
104    #[must_use]
105    pub fn surface(&self, id: SurfaceId) -> Option<&SurfaceGeometry> {
106        self.surfaces.get(id)
107    }
108
109    /// A surface, for rewriting in place: a plane or a cylinder widened to
110    /// hold the trims a file puts on it is the same surface, asked over a
111    /// wider window.
112    pub fn surface_mut(&mut self, id: SurfaceId) -> Option<&mut SurfaceGeometry> {
113        self.surfaces.get_mut(id)
114    }
115
116    /// The cached triangulation behind `id`.
117    #[must_use]
118    pub fn triangulation(&self, id: TriangulationId) -> Option<&Triangulation> {
119        self.triangulations.get(id)
120    }
121
122    /// How many curves, pcurves and surfaces are held.
123    #[must_use]
124    pub fn counts(&self) -> (usize, usize, usize) {
125        (self.curves.len(), self.pcurves.len(), self.surfaces.len())
126    }
127
128    /// The identifiers this store's arenas issue keys under.
129    ///
130    /// For [`Model::from_parts`](crate::Model::from_parts), which has to bind
131    /// handles rebuilt by a reader to the arenas they will actually live in.
132    pub(crate) const fn scopes(&self) -> GeometryScopes {
133        GeometryScopes {
134            curves: self.curves.scope(),
135            pcurves: self.pcurves.scope(),
136            surfaces: self.surfaces.scope(),
137            triangulations: self.triangulations.scope(),
138        }
139    }
140
141    /// Whether every arena has only ever been appended to.
142    ///
143    /// The precondition for extending the store by offset; see
144    /// [`Arena::is_dense`](ogeom_core::Arena::is_dense).
145    pub(crate) fn is_dense(&self) -> bool {
146        self.curves.is_dense()
147            && self.pcurves.is_dense()
148            && self.surfaces.is_dense()
149            && self.triangulations.is_dense()
150    }
151
152    /// Append another store's contents, returning where each kind landed.
153    ///
154    /// The returned offsets are this store's lengths before the append: an
155    /// entry that sat at index `i` in `other` now sits at `i + offset`. The
156    /// receiving arenas hand out the keys, so the values travel bare.
157    pub(crate) fn append(&mut self, other: Self) -> GeometryOffsets {
158        let offsets = GeometryOffsets {
159            curves: arena_len(&self.curves),
160            pcurves: arena_len(&self.pcurves),
161            surfaces: arena_len(&self.surfaces),
162            triangulations: arena_len(&self.triangulations),
163        };
164        for curve in other.curves.into_values() {
165            self.curves.insert(curve);
166        }
167        for pcurve in other.pcurves.into_values() {
168            self.pcurves.insert(pcurve);
169        }
170        for surface in other.surfaces.into_values() {
171            self.surfaces.insert(surface);
172        }
173        for mesh in other.triangulations.into_values() {
174            self.triangulations.insert(mesh);
175        }
176        offsets
177    }
178
179    /// Whether every piece of geometry a representation names is held here.
180    ///
181    /// The check a restored model needs: a handle that does not resolve is not
182    /// a finding, it is a document that does not describe itself.
183    #[must_use]
184    pub fn holds(&self, repr: &EdgeRepr) -> bool {
185        match repr {
186            EdgeRepr::Curve3d { curve, .. } => self.curve(*curve).is_some(),
187            EdgeRepr::PCurve { curve, surface, .. } => {
188                self.pcurve(*curve).is_some() && self.surface(*surface).is_some()
189            }
190            EdgeRepr::Seam {
191                forward,
192                reversed,
193                surface,
194                ..
195            } => {
196                self.pcurve(*forward).is_some()
197                    && self.pcurve(*reversed).is_some()
198                    && self.surface(*surface).is_some()
199            }
200            // Carries its points itself, so there is nothing to resolve.
201            EdgeRepr::Polyline { .. } => true,
202            EdgeRepr::PolygonOnTriangulation { triangulation, .. } => {
203                self.triangulation(*triangulation).is_some()
204            }
205        }
206    }
207
208    /// Every space curve, with its handle, in arena order.
209    pub fn curves(&self) -> impl Iterator<Item = (CurveId, &Curve)> {
210        self.curves.iter()
211    }
212
213    /// Every parameter-space curve, with its handle, in arena order.
214    pub fn pcurves(&self) -> impl Iterator<Item = (PCurveId, &PlanarCurve)> {
215        self.pcurves.iter()
216    }
217
218    /// Every surface, with its handle, in arena order.
219    pub fn surfaces(&self) -> impl Iterator<Item = (SurfaceId, &SurfaceGeometry)> {
220        self.surfaces.iter()
221    }
222
223    /// Every cached triangulation, with its handle, in arena order.
224    pub fn triangulations(&self) -> impl Iterator<Item = (TriangulationId, &Triangulation)> {
225        self.triangulations.iter()
226    }
227
228    /// How many cached triangulations are held.
229    #[must_use]
230    pub fn triangulation_count(&self) -> usize {
231        self.triangulations.len()
232    }
233}
234
235/// Which arena issues each kind of geometry handle in one store.
236#[derive(Debug, Clone, Copy)]
237pub(crate) struct GeometryScopes {
238    pub curves: u32,
239    pub pcurves: u32,
240    pub surfaces: u32,
241    pub triangulations: u32,
242}
243
244/// Where each kind of geometry landed in an append: the lengths of the
245/// receiving arenas before it.
246#[derive(Debug, Clone, Copy)]
247pub(crate) struct GeometryOffsets {
248    pub curves: u32,
249    pub pcurves: u32,
250    pub surfaces: u32,
251    pub triangulations: u32,
252}
253
254/// An arena's length as the index its next append will land at.
255///
256/// # Panics
257///
258/// If the arena exceeds `u32::MAX` slots, which [`Arena::insert`] already
259/// refuses to reach.
260#[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
261pub(crate) fn arena_len<T>(arena: &ogeom_core::Arena<T>) -> u32 {
262    u32::try_from(arena.len()).expect("arena exceeded u32::MAX slots")
263}
264
265/// Whether a key is unscoped and at generation zero: the state a reader
266/// leaves handles in, and the only state an absorb accepts.
267pub(crate) fn key_is_unbound<T>(key: ogeom_core::Key<T>) -> bool {
268    key.scope() == ogeom_core::UNSCOPED && key.generation() == 0
269}
270
271/// A key shifted `offset` slots along, for landing parts in a live model.
272///
273/// The result stays unscoped: shifting says where an entry will sit, binding
274/// says which arena it sits in, and the two are separate steps on purpose.
275///
276/// # Panics
277///
278/// If the shifted index exceeds `u32::MAX`, which
279/// [`Arena::insert`](ogeom_core::Arena::insert) already refuses to reach.
280#[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
281pub(crate) fn shifted_key<T>(key: ogeom_core::Key<T>, offset: u32) -> ogeom_core::Key<T> {
282    ogeom_core::Key::from_parts(
283        key.index()
284            .checked_add(offset)
285            .expect("arena exceeded u32::MAX slots"),
286        key.generation(),
287    )
288}
289
290/// One way of describing where an edge runs.
291///
292/// An edge holds several at once, and they must agree; see
293/// [`EdgeData::same_parameter`].
294#[derive(Debug, Clone, PartialEq)]
295#[non_exhaustive]
296pub enum EdgeRepr {
297    /// The edge as a curve in space.
298    Curve3d {
299        /// The curve.
300        curve: CurveId,
301        /// Where the curve sits.
302        location: Location,
303        /// The portion of the curve this edge covers.
304        range: (f64, f64),
305    },
306    /// The edge as a curve in one surface's parameter space.
307    PCurve {
308        /// The curve in `(u, v)`.
309        curve: PCurveId,
310        /// The surface whose parameter space it lives in.
311        surface: SurfaceId,
312        /// Where that surface sits.
313        location: Location,
314        /// The portion of the curve this edge covers.
315        range: (f64, f64),
316    },
317    /// The edge as a seam on a closed surface, needing two pcurves.
318    ///
319    /// A seam runs along a surface's closure (a cylinder's join, a sphere's
320    /// date line) where the same points have two parameter values. One pcurve
321    /// per side; a single one could not express both, and using only one leaves
322    /// the face split open along the seam.
323    Seam {
324        /// The pcurve on the side the face's forward orientation sees.
325        forward: PCurveId,
326        /// The pcurve on the other side.
327        reversed: PCurveId,
328        /// The surface.
329        surface: SurfaceId,
330        /// Where that surface sits.
331        location: Location,
332        /// The portion both curves cover.
333        range: (f64, f64),
334    },
335    /// The edge's path through one face's cached triangulation, as indices.
336    ///
337    /// What a renderer wants to draw a shared edge without hunting for
338    /// coincident vertices: consecutive indices are consecutive mesh nodes
339    /// along the edge, in the edge's own direction. Only meaningful while
340    /// the named triangulation is the one stored; retessellating replaces
341    /// both together.
342    PolygonOnTriangulation {
343        /// The face triangulation the indices point into.
344        triangulation: TriangulationId,
345        /// Node indices along the edge, in curve order.
346        indices: Vec<u32>,
347        /// Where the edge occurrence sits.
348        location: Location,
349    },
350    /// A cached polyline approximation, with the deflection it was built to.
351    ///
352    /// Carried alongside the exact geometry rather than replacing it: display
353    /// and coarse spatial queries want it, and rebuilding it on every frame
354    /// would dominate their cost.
355    Polyline {
356        /// The points, in order along the edge.
357        points: Vec<Point>,
358        /// The curve parameter each point came from.
359        ///
360        /// Kept, not discarded. A face's cached triangulation has to place its
361        /// boundary vertices where this polyline puts them, and it reaches them
362        /// through its own pcurve, so it needs the parameters, not just the
363        /// points. Without them the two caches drift and the stored mesh has
364        /// gaps that the exact geometry does not.
365        parameters: Vec<f64>,
366        /// Where they sit.
367        location: Location,
368        /// The maximum distance from the exact curve.
369        deflection: f64,
370    },
371}
372
373impl EdgeRepr {
374    /// Bind this representation's handles to the arenas that hold them.
375    ///
376    /// For reading a document back. Only the arena identifier changes; index
377    /// and generation came from the file and are already right.
378    pub(crate) fn rebind(&mut self, geometry: &GeometryScopes, datums: u32) {
379        match self {
380            Self::Curve3d {
381                curve, location, ..
382            } => {
383                *curve = curve.with_scope(geometry.curves);
384                *location = location.with_datum_scope(datums);
385            }
386            Self::PCurve {
387                curve,
388                surface,
389                location,
390                ..
391            } => {
392                *curve = curve.with_scope(geometry.pcurves);
393                *surface = surface.with_scope(geometry.surfaces);
394                *location = location.with_datum_scope(datums);
395            }
396            Self::Seam {
397                forward,
398                reversed,
399                surface,
400                location,
401                ..
402            } => {
403                *forward = forward.with_scope(geometry.pcurves);
404                *reversed = reversed.with_scope(geometry.pcurves);
405                *surface = surface.with_scope(geometry.surfaces);
406                *location = location.with_datum_scope(datums);
407            }
408            Self::Polyline { location, .. } => {
409                *location = location.with_datum_scope(datums);
410            }
411            Self::PolygonOnTriangulation {
412                triangulation,
413                location,
414                ..
415            } => {
416                *triangulation = triangulation.with_scope(geometry.triangulations);
417                *location = location.with_datum_scope(datums);
418            }
419        }
420    }
421
422    /// Shift this representation's handles for landing in a live model.
423    ///
424    /// [`rebind`](Self::rebind)'s sibling for absorbing parts: indices move by
425    /// where the source document's geometry and datums land in the target,
426    /// and the handles stay unscoped for the binding pass that follows.
427    pub(crate) fn shift(&mut self, geometry: &GeometryOffsets, datums: u32) {
428        match self {
429            Self::Curve3d {
430                curve, location, ..
431            } => {
432                *curve = shifted_key(*curve, geometry.curves);
433                *location = location.with_datum_offset(datums);
434            }
435            Self::PCurve {
436                curve,
437                surface,
438                location,
439                ..
440            } => {
441                *curve = shifted_key(*curve, geometry.pcurves);
442                *surface = shifted_key(*surface, geometry.surfaces);
443                *location = location.with_datum_offset(datums);
444            }
445            Self::Seam {
446                forward,
447                reversed,
448                surface,
449                location,
450                ..
451            } => {
452                *forward = shifted_key(*forward, geometry.pcurves);
453                *reversed = shifted_key(*reversed, geometry.pcurves);
454                *surface = shifted_key(*surface, geometry.surfaces);
455                *location = location.with_datum_offset(datums);
456            }
457            Self::Polyline { location, .. } => {
458                *location = location.with_datum_offset(datums);
459            }
460            Self::PolygonOnTriangulation {
461                triangulation,
462                location,
463                ..
464            } => {
465                *triangulation = shifted_key(*triangulation, geometry.triangulations);
466                *location = location.with_datum_offset(datums);
467            }
468        }
469    }
470
471    /// Whether every handle here is unscoped and at generation zero: the
472    /// state a reader leaves them in, and the only state an absorb accepts.
473    pub(crate) fn is_unbound(&self) -> bool {
474        let local_location = |location: &Location| {
475            location
476                .chain()
477                .iter()
478                .all(|&(datum, _)| key_is_unbound(datum))
479        };
480        match self {
481            Self::Curve3d {
482                curve, location, ..
483            } => key_is_unbound(*curve) && local_location(location),
484            Self::PCurve {
485                curve,
486                surface,
487                location,
488                ..
489            } => key_is_unbound(*curve) && key_is_unbound(*surface) && local_location(location),
490            Self::Seam {
491                forward,
492                reversed,
493                surface,
494                location,
495                ..
496            } => {
497                key_is_unbound(*forward)
498                    && key_is_unbound(*reversed)
499                    && key_is_unbound(*surface)
500                    && local_location(location)
501            }
502            Self::Polyline { location, .. } => local_location(location),
503            Self::PolygonOnTriangulation {
504                triangulation,
505                location,
506                ..
507            } => key_is_unbound(*triangulation) && local_location(location),
508        }
509    }
510
511    /// The surface this representation belongs to, if any.
512    #[must_use]
513    pub const fn surface(&self) -> Option<SurfaceId> {
514        match self {
515            Self::PCurve { surface, .. } | Self::Seam { surface, .. } => Some(*surface),
516            Self::Curve3d { .. } | Self::Polyline { .. } | Self::PolygonOnTriangulation { .. } => {
517                None
518            }
519        }
520    }
521
522    /// Where this representation sits.
523    #[must_use]
524    pub const fn location(&self) -> Option<&Location> {
525        match self {
526            Self::Curve3d { location, .. }
527            | Self::PCurve { location, .. }
528            | Self::Seam { location, .. }
529            | Self::Polyline { location, .. }
530            | Self::PolygonOnTriangulation { location, .. } => Some(location),
531        }
532    }
533
534    /// The parameter range this representation covers, if it has one.
535    #[must_use]
536    pub const fn range(&self) -> Option<(f64, f64)> {
537        match self {
538            Self::Curve3d { range, .. } | Self::PCurve { range, .. } | Self::Seam { range, .. } => {
539                Some(*range)
540            }
541            Self::Polyline { .. } | Self::PolygonOnTriangulation { .. } => None,
542        }
543    }
544
545    /// Whether this is the edge's curve in space.
546    #[must_use]
547    pub const fn is_curve3d(&self) -> bool {
548        matches!(self, Self::Curve3d { .. })
549    }
550
551    /// Whether this describes the edge in a surface's parameter space.
552    #[must_use]
553    pub const fn is_parametric(&self) -> bool {
554        matches!(self, Self::PCurve { .. } | Self::Seam { .. })
555    }
556}
557
558/// A vertex: a point and how far it may be from where it claims to be.
559#[derive(Debug, Clone, PartialEq)]
560pub struct VertexData {
561    /// The position.
562    pub point: Point,
563    /// The radius within which the vertex lies.
564    pub tolerance: Tolerance,
565}
566
567impl VertexData {
568    /// A vertex at `point` with the minimum tolerance.
569    #[must_use]
570    pub fn new(point: Point) -> Self {
571        Self {
572            point,
573            tolerance: Tolerance::MIN,
574        }
575    }
576
577    /// A vertex with an explicit tolerance.
578    ///
579    /// # Errors
580    ///
581    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
582    /// tolerance is not finite and non-negative, or the point is non-finite.
583    pub fn with_tolerance(point: Point, tolerance: f64) -> OgeomResult<Self> {
584        if !point.is_finite() {
585            ogeom_bail!(Construction, "vertex position is not finite");
586        }
587        Ok(Self {
588            point,
589            tolerance: Tolerance::new(tolerance)?,
590        })
591    }
592
593    /// Widen this vertex's tolerance.
594    pub fn widen(&mut self, to: Tolerance) {
595        self.tolerance = self.tolerance.widen(to);
596    }
597}
598
599/// An edge: a tolerance, a set of representations, and the flags that say
600/// whether they agree.
601#[derive(Debug, Clone, PartialEq)]
602pub struct EdgeData {
603    /// The radius within which the edge lies.
604    pub tolerance: Tolerance,
605    /// Every way this edge is described. See the module documentation.
606    pub representations: SmallVec<[EdgeRepr; 3]>,
607    /// Whether the representations agree on parameterization.
608    ///
609    /// A *claim*, and one that can be false; see [`EdgeData::same_parameter`].
610    same_parameter: bool,
611    /// Whether the edge has no length: a cone's apex, a sphere's pole.
612    ///
613    /// A degenerate edge still bounds a face in parameter space even though it
614    /// covers no distance in space, which is why it exists rather than being
615    /// dropped.
616    pub degenerate: bool,
617}
618
619impl EdgeData {
620    /// An edge with no representations yet.
621    #[must_use]
622    pub fn new() -> Self {
623        Self {
624            tolerance: Tolerance::MIN,
625            representations: SmallVec::new(),
626            same_parameter: true,
627            degenerate: false,
628        }
629    }
630
631    /// An edge on a curve in space.
632    #[must_use]
633    pub fn on_curve(curve: CurveId, location: Location, range: (f64, f64)) -> Self {
634        let mut edge = Self::new();
635        edge.representations.push(EdgeRepr::Curve3d {
636            curve,
637            location,
638            range,
639        });
640        edge
641    }
642
643    /// Add a representation.
644    ///
645    /// Adding one invalidates the [`EdgeData::same_parameter`] claim: the new
646    /// representation has not been shown to agree with the others. Re-establish
647    /// it deliberately, with [`EdgeData::assert_same_parameter`].
648    pub fn add(&mut self, repr: EdgeRepr) {
649        self.representations.push(repr);
650        self.same_parameter = false;
651    }
652
653    /// Whether every representation agrees on parameterization.
654    ///
655    /// The claim is that `curve3d(t)` and `surface(pcurve(t))` are the same
656    /// point, within the edge's tolerance, for the same `t`. It matters because
657    /// nearly every algorithm evaluates whichever representation is convenient
658    /// and assumes the answer is interchangeable.
659    ///
660    /// It can be false (an imported edge whose pcurve was fitted independently
661    /// of its 3D curve routinely is), which is why it is a flag to be checked
662    /// rather than an invariant to be assumed.
663    #[must_use]
664    pub const fn same_parameter(&self) -> bool {
665        self.same_parameter
666    }
667
668    /// Record that the representations have been checked and agree.
669    ///
670    /// Only for a caller that has actually verified it: evaluate each
671    /// representation at the same parameters and confirm they land within the
672    /// edge's tolerance of one another. Setting it without checking is how an
673    /// edge ends up with a pcurve that does not follow its own curve, and
674    /// nothing downstream will notice until a face fails to close.
675    pub const fn assert_same_parameter(&mut self, agrees: bool) {
676        self.same_parameter = agrees;
677    }
678
679    /// The representation on a given curve in space, if any.
680    #[must_use]
681    pub fn curve3d(&self) -> Option<&EdgeRepr> {
682        self.representations.iter().find(|r| r.is_curve3d())
683    }
684
685    /// The representation in `surface`'s parameter space, if any.
686    #[must_use]
687    pub fn pcurve_on(&self, surface: SurfaceId) -> Option<&EdgeRepr> {
688        self.representations
689            .iter()
690            .find(|r| r.surface() == Some(surface))
691    }
692
693    /// The representation in `surface`'s parameter space for an occurrence at
694    /// `location`.
695    ///
696    /// One edge node can bound one face at more than one placement (the top
697    /// and bottom of a prism are the same edge, moved), and those two
698    /// occurrences run along different lines of the same parameter space. Asked
699    /// by surface alone, the lookup returns whichever was attached first and
700    /// both ends of the prism collapse onto one.
701    ///
702    /// Falls back to a representation attached without a placement, which is
703    /// what every unplaced edge has and what keeps the simple case simple.
704    #[must_use]
705    pub fn pcurve_for(&self, surface: SurfaceId, location: &Location) -> Option<&EdgeRepr> {
706        self.representations
707            .iter()
708            .find(|r| r.surface() == Some(surface) && r.location() == Some(location))
709            .or_else(|| {
710                // The whole shape placed again: the occurrence's chain is
711                // the stored one with placements outside it, and the
712                // longest such tail names the occurrence meant.
713                self.representations
714                    .iter()
715                    .filter(|r| {
716                        r.surface() == Some(surface)
717                            && r.location()
718                                .is_some_and(|l| !l.is_identity() && location.ends_with(l))
719                    })
720                    .max_by_key(|r| r.location().map_or(0, Location::depth))
721            })
722            .or_else(|| {
723                self.representations.iter().find(|r| {
724                    r.surface() == Some(surface) && r.location().is_some_and(Location::is_identity)
725                })
726            })
727    }
728
729    /// Every surface this edge has a pcurve on.
730    #[must_use]
731    pub fn parametric_surfaces(&self) -> Vec<SurfaceId> {
732        self.representations
733            .iter()
734            .filter_map(EdgeRepr::surface)
735            .collect()
736    }
737
738    /// Widen this edge's tolerance.
739    pub fn widen(&mut self, to: Tolerance) {
740        self.tolerance = self.tolerance.widen(to);
741    }
742}
743
744impl Default for EdgeData {
745    fn default() -> Self {
746        Self::new()
747    }
748}
749
750/// A face: a surface, where it sits, and how far the face may stray from it.
751#[derive(Debug, Clone, PartialEq)]
752pub struct FaceData {
753    /// The surface.
754    pub surface: SurfaceId,
755    /// Where the surface sits.
756    pub location: Location,
757    /// The radius within which the face lies.
758    pub tolerance: Tolerance,
759    /// Whether the face covers its surface's whole domain, with no trimming
760    /// wires of its own.
761    ///
762    /// Worth knowing: a face with natural restriction needs no point-in-face
763    /// classification at all, and that test is one of the costliest in a
764    /// boolean.
765    pub natural_restriction: bool,
766    /// The cached triangulation of this face, if one has been built.
767    ///
768    /// A representation like a pcurve, not a replacement for the surface: it
769    /// answers display and coarse queries, and is rebuilt when a finer
770    /// deflection is asked for.
771    pub triangulation: Option<TriangulationId>,
772}
773
774impl FaceData {
775    /// A face on `surface`, trimmed by its own wires.
776    #[must_use]
777    pub fn new(surface: SurfaceId, location: Location) -> Self {
778        Self {
779            surface,
780            location,
781            tolerance: Tolerance::MIN,
782            natural_restriction: false,
783            triangulation: None,
784        }
785    }
786
787    /// A face covering the whole of `surface`.
788    #[must_use]
789    pub fn natural(surface: SurfaceId, location: Location) -> Self {
790        Self {
791            natural_restriction: true,
792            ..Self::new(surface, location)
793        }
794    }
795
796    /// Widen this face's tolerance.
797    pub fn widen(&mut self, to: Tolerance) {
798        self.tolerance = self.tolerance.widen(to);
799    }
800}
801
802/// What a topology node holds, beyond its children.
803///
804/// Stored inline in the node rather than in side tables keyed by handle: a
805/// traversal that has the node has the data, without a second lookup, and there
806/// is no way for the two to fall out of step.
807#[derive(Debug, Clone, PartialEq)]
808pub enum NodeData {
809    /// A vertex.
810    Vertex(VertexData),
811    /// An edge.
812    Edge(Box<EdgeData>),
813    /// A face.
814    Face(Box<FaceData>),
815    /// A wire, shell, solid, compsolid or compound: structure, no geometry of
816    /// its own.
817    Container,
818}
819
820impl NodeData {
821    /// The tolerance this node carries, if it carries one.
822    ///
823    /// Containers have none: a wire's uncertainty is that of the edges in it,
824    /// and inventing a separate number for it would be a second source of truth.
825    #[must_use]
826    pub fn tolerance(&self) -> Option<Tolerance> {
827        match self {
828            Self::Vertex(v) => Some(v.tolerance),
829            Self::Edge(e) => Some(e.tolerance),
830            Self::Face(f) => Some(f.tolerance),
831            Self::Container => None,
832        }
833    }
834
835    /// Widen this node's tolerance, if it has one.
836    pub fn widen(&mut self, to: Tolerance) {
837        match self {
838            Self::Vertex(v) => v.widen(to),
839            Self::Edge(e) => e.widen(to),
840            Self::Face(f) => f.widen(to),
841            Self::Container => {}
842        }
843    }
844
845    /// The vertex data, if this is a vertex.
846    #[must_use]
847    pub const fn as_vertex(&self) -> Option<&VertexData> {
848        match self {
849            Self::Vertex(v) => Some(v),
850            _ => None,
851        }
852    }
853
854    /// The edge data, if this is an edge.
855    #[must_use]
856    pub const fn as_edge(&self) -> Option<&EdgeData> {
857        match self {
858            Self::Edge(e) => Some(e),
859            _ => None,
860        }
861    }
862
863    /// The face data, if this is a face.
864    #[must_use]
865    pub const fn as_face(&self) -> Option<&FaceData> {
866        match self {
867            Self::Face(f) => Some(f),
868            _ => None,
869        }
870    }
871}
872
873/// Check the containment rule between a bounding entity and what it bounds.
874///
875/// `docs/DATA_MODEL.md` §5. A vertex must be at least as uncertain as the edge
876/// it caps, and an edge at least as uncertain as the face it borders. If it
877/// were not, the boundary would not reliably lie on the thing it bounds, and
878/// every containment test built on it would be answering about geometry that
879/// does not quite meet.
880///
881/// # Errors
882///
883/// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if `bounding` is tighter
884/// than `bounded`.
885pub fn check_containment(bounding: Tolerance, bounded: Tolerance) -> OgeomResult<()> {
886    ogeom_core::check_containment(bounding, bounded)
887}
888
889/// Widen `bounding` just enough to satisfy the containment rule against
890/// `bounded`.
891///
892/// The sanctioned repair: tolerances only ever grow, so restoring the rule
893/// means raising the bounding entity rather than lowering what it bounds.
894#[must_use]
895pub fn enforce_containment(bounding: Tolerance, bounded: Tolerance) -> Tolerance {
896    bounding.widen(bounded)
897}
898
899/// Whether a parameter range is usable.
900///
901/// # Errors
902///
903/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the range is
904/// empty or non-finite.
905pub fn check_range(range: (f64, f64), tol: Tolerances) -> OgeomResult<()> {
906    let (a, b) = range;
907    if !a.is_finite() || !b.is_finite() || b <= a + tol.parametric() {
908        ogeom_bail!(
909            Construction,
910            "parameter range [{a}, {b}] is empty or non-finite"
911        );
912    }
913    Ok(())
914}
915
916#[cfg(test)]
917#[allow(clippy::unwrap_used)]
918mod tests {
919    use super::*;
920    use ogeom_geom::{CircleCurve, LineCurve, PlaneSurface};
921    use ogeom_math::{Circle, Direction, Frame, Plane};
922
923    const T: Tolerances = Tolerances::millimetres();
924
925    fn store() -> (GeometryStore, CurveId, SurfaceId, PCurveId) {
926        let mut s = GeometryStore::new();
927        let curve = s.add_curve(
928            LineCurve::segment(Point::ORIGIN, Point::new(10.0, 0.0, 0.0), T)
929                .unwrap()
930                .into(),
931        );
932        let surface = s.add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
933        let pcurve = s.add_pcurve(
934            ogeom_geom::Line2d::segment(
935                ogeom_math::Point2::ORIGIN,
936                ogeom_math::Point2::new(10.0, 0.0),
937                T,
938            )
939            .unwrap()
940            .into(),
941        );
942        (s, curve, surface, pcurve)
943    }
944
945    #[test]
946    fn the_geometry_store_hands_back_what_it_was_given() {
947        let (s, curve, surface, pcurve) = store();
948        assert!(s.curve(curve).is_some());
949        assert!(s.surface(surface).is_some());
950        assert!(s.pcurve(pcurve).is_some());
951        assert_eq!(s.counts(), (1, 1, 1));
952
953        // Handles are typed, so a curve handle cannot address a surface. That
954        // is a compile-time guarantee rather than a runtime check.
955        // A plain loop, not a lazy iterator: `map(..).next_back()` would run
956        // the closure once, so nothing would actually be inserted past index 0
957        // and the handle would resolve after all.
958        let mut other = GeometryStore::new();
959        let mut beyond = curve;
960        for _ in 0..5 {
961            beyond = other.add_curve(
962                LineCurve::segment(Point::ORIGIN, Point::new(1.0, 0.0, 0.0), T)
963                    .unwrap()
964                    .into(),
965            );
966        }
967        assert!(
968            s.curve(beyond).is_none(),
969            "a handle past the end does not resolve"
970        );
971    }
972
973    #[test]
974    fn tolerances_start_at_the_minimum_and_only_widen() {
975        let mut v = VertexData::new(Point::ORIGIN);
976        assert_eq!(v.tolerance, Tolerance::MIN);
977
978        let wide = Tolerance::new(1e-3).unwrap();
979        v.widen(wide);
980        assert_eq!(v.tolerance, wide);
981
982        // Widening to something tighter leaves it alone: tolerances never
983        // shrink, since narrowing one asserts an accuracy the geometry does not
984        // have.
985        v.widen(Tolerance::new(1e-9).unwrap());
986        assert_eq!(v.tolerance, wide);
987    }
988
989    #[test]
990    fn degenerate_vertex_data_is_refused() {
991        assert!(VertexData::with_tolerance(Point::ORIGIN, -1.0).is_err());
992        assert!(VertexData::with_tolerance(Point::ORIGIN, f64::NAN).is_err());
993        assert!(VertexData::with_tolerance(Point::new(f64::INFINITY, 0.0, 0.0), 1e-6).is_err());
994        assert!(VertexData::with_tolerance(Point::ORIGIN, 1e-3).is_ok());
995    }
996
997    #[test]
998    fn the_containment_rule_holds_downward_and_is_repaired_upward() {
999        let fine = Tolerance::new(1e-6).unwrap();
1000        let coarse = Tolerance::new(1e-3).unwrap();
1001
1002        assert!(
1003            check_containment(coarse, fine).is_ok(),
1004            "vertex coarser than edge"
1005        );
1006        assert!(
1007            check_containment(fine, coarse).is_err(),
1008            "and not the other way"
1009        );
1010
1011        // The repair raises the bounding entity, never lowers what it bounds.
1012        let repaired = enforce_containment(fine, coarse);
1013        assert_eq!(repaired, coarse);
1014        assert!(check_containment(repaired, coarse).is_ok());
1015    }
1016
1017    #[test]
1018    fn an_edge_holds_several_representations_at_once() {
1019        // The whole point of §6: one edge, a curve in space and a pcurve per
1020        // adjacent face. A single curve could not serve two differently
1021        // parameterized surfaces.
1022        let (mut s, curve, surface, pcurve) = store();
1023        let other_surface =
1024            s.add_surface(PlaneSurface::new(Plane::through(Point::ORIGIN, Direction::Y)).into());
1025        let other_pcurve = s.add_pcurve(
1026            ogeom_geom::Line2d::segment(
1027                ogeom_math::Point2::ORIGIN,
1028                ogeom_math::Point2::new(0.0, 10.0),
1029                T,
1030            )
1031            .unwrap()
1032            .into(),
1033        );
1034
1035        let mut edge = EdgeData::on_curve(curve, Location::identity(), (0.0, 10.0));
1036        edge.add(EdgeRepr::PCurve {
1037            curve: pcurve,
1038            surface,
1039            location: Location::identity(),
1040            range: (0.0, 10.0),
1041        });
1042        edge.add(EdgeRepr::PCurve {
1043            curve: other_pcurve,
1044            surface: other_surface,
1045            location: Location::identity(),
1046            range: (0.0, 10.0),
1047        });
1048
1049        assert_eq!(edge.representations.len(), 3);
1050        assert!(edge.curve3d().is_some());
1051        assert!(edge.pcurve_on(surface).is_some());
1052        assert!(edge.pcurve_on(other_surface).is_some());
1053        assert_eq!(edge.parametric_surfaces().len(), 2);
1054    }
1055
1056    #[test]
1057    fn adding_a_representation_withdraws_the_same_parameter_claim() {
1058        // The claim is that every representation lands on the same point for
1059        // the same parameter. A newly added one has not been shown to, and
1060        // quietly keeping the claim is how an edge ends up with a pcurve that
1061        // does not follow its own curve.
1062        let (_, curve, surface, pcurve) = store();
1063        let mut edge = EdgeData::on_curve(curve, Location::identity(), (0.0, 10.0));
1064        assert!(
1065            edge.same_parameter(),
1066            "a lone curve trivially agrees with itself"
1067        );
1068
1069        edge.add(EdgeRepr::PCurve {
1070            curve: pcurve,
1071            surface,
1072            location: Location::identity(),
1073            range: (0.0, 10.0),
1074        });
1075        assert!(
1076            !edge.same_parameter(),
1077            "the new representation is unverified"
1078        );
1079
1080        edge.assert_same_parameter(true);
1081        assert!(edge.same_parameter());
1082    }
1083
1084    #[test]
1085    fn a_seam_carries_two_pcurves_because_one_cannot_express_both_sides() {
1086        // On a closed surface the same points have two parameter values. A
1087        // single pcurve names one of them, which leaves the face open along the
1088        // seam.
1089        let mut s = GeometryStore::new();
1090        let cylinder = s.add_surface(
1091            ogeom_geom::CylinderSurface::new(
1092                ogeom_math::Cylinder::new(Frame::WORLD, 2.0, T).unwrap(),
1093                (0.0, 5.0),
1094            )
1095            .unwrap()
1096            .into(),
1097        );
1098        let at_zero = s.add_pcurve(
1099            ogeom_geom::Line2d::segment(
1100                ogeom_math::Point2::ORIGIN,
1101                ogeom_math::Point2::new(0.0, 5.0),
1102                T,
1103            )
1104            .unwrap()
1105            .into(),
1106        );
1107        let at_tau = s.add_pcurve(
1108            ogeom_geom::Line2d::segment(
1109                ogeom_math::Point2::new(core::f64::consts::TAU, 0.0),
1110                ogeom_math::Point2::new(core::f64::consts::TAU, 5.0),
1111                T,
1112            )
1113            .unwrap()
1114            .into(),
1115        );
1116
1117        let mut edge = EdgeData::new();
1118        edge.add(EdgeRepr::Seam {
1119            forward: at_zero,
1120            reversed: at_tau,
1121            surface: cylinder,
1122            location: Location::identity(),
1123            range: (0.0, 5.0),
1124        });
1125
1126        let seam = &edge.representations[0];
1127        assert!(seam.is_parametric());
1128        assert_eq!(seam.surface(), Some(cylinder));
1129        assert_eq!(seam.range(), Some((0.0, 5.0)));
1130        assert!(matches!(seam, EdgeRepr::Seam { .. }));
1131    }
1132
1133    #[test]
1134    fn a_polyline_representation_records_what_it_was_built_to() {
1135        // Without the deflection the cache cannot be judged: a polyline good
1136        // enough for a thumbnail is not good enough for a machining path, and
1137        // nothing else recorded says which this is.
1138        let repr = EdgeRepr::Polyline {
1139            points: vec![Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
1140            parameters: vec![0.0, 1.0],
1141            location: Location::identity(),
1142            deflection: 1e-3,
1143        };
1144        assert!(!repr.is_curve3d() && !repr.is_parametric());
1145        assert_eq!(repr.surface(), None);
1146        assert_eq!(repr.range(), None, "a polyline has no parameter range");
1147    }
1148
1149    #[test]
1150    fn a_degenerate_edge_is_marked_rather_than_dropped() {
1151        // A cone's apex has no length in space but still bounds the face in
1152        // parameter space. Dropping it leaves the face's boundary open.
1153        let mut edge = EdgeData::new();
1154        edge.degenerate = true;
1155        assert!(edge.degenerate);
1156        assert!(edge.representations.is_empty());
1157    }
1158
1159    #[test]
1160    fn a_natural_face_covers_its_whole_surface() {
1161        let (_, _, surface, _) = store();
1162        let trimmed = FaceData::new(surface, Location::identity());
1163        let whole = FaceData::natural(surface, Location::identity());
1164        assert!(!trimmed.natural_restriction);
1165        assert!(whole.natural_restriction);
1166        assert_eq!(whole.tolerance, Tolerance::MIN);
1167    }
1168
1169    #[test]
1170    fn node_data_exposes_only_what_it_holds() {
1171        let vertex = NodeData::Vertex(VertexData::new(Point::ORIGIN));
1172        let edge = NodeData::Edge(Box::default());
1173        let container = NodeData::Container;
1174
1175        assert!(vertex.as_vertex().is_some());
1176        assert!(vertex.as_edge().is_none());
1177        assert!(edge.as_edge().is_some());
1178        assert!(edge.as_face().is_none());
1179
1180        // A container has no tolerance of its own: a wire's uncertainty is that
1181        // of its edges, and a second number for it would be a second source of
1182        // truth to keep in step.
1183        assert_eq!(container.tolerance(), None);
1184        assert_eq!(vertex.tolerance(), Some(Tolerance::MIN));
1185    }
1186
1187    #[test]
1188    fn widening_a_container_is_a_no_op_rather_than_an_error() {
1189        // Traversal widens whatever it walks over; a container simply has
1190        // nothing to widen, and making that an error would push the case
1191        // analysis onto every caller.
1192        let mut container = NodeData::Container;
1193        container.widen(Tolerance::new(1e-3).unwrap());
1194        assert_eq!(container.tolerance(), None);
1195    }
1196
1197    #[test]
1198    fn empty_parameter_ranges_are_refused() {
1199        assert!(check_range((0.0, 1.0), T).is_ok());
1200        assert!(check_range((1.0, 0.0), T).is_err());
1201        assert!(check_range((1.0, 1.0), T).is_err());
1202        assert!(check_range((0.0, f64::NAN), T).is_err());
1203    }
1204
1205    #[test]
1206    fn a_circular_edge_can_carry_its_own_curve() {
1207        let mut s = GeometryStore::new();
1208        let circle =
1209            s.add_curve(CircleCurve::new(Circle::new(Frame::WORLD, 3.0, T).unwrap()).into());
1210        let edge = EdgeData::on_curve(circle, Location::identity(), (0.0, core::f64::consts::TAU));
1211        assert!(edge.curve3d().is_some());
1212        assert_eq!(
1213            edge.curve3d().unwrap().range(),
1214            Some((0.0, core::f64::consts::TAU))
1215        );
1216        assert!(edge.parametric_surfaces().is_empty());
1217    }
1218}