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