Skip to main content

step_io/scene/
geometry.rs

1//! Read-only b-rep geometry navigation handles over a [`Scene`].
2//!
3//! The walk is `Solid → Face → Surface` and `Face → Bound → Edge → Curve /
4//! Vertex → Point`. Surfaces and curves report their analytic kind
5//! (`Plane`, `Cylindrical`, …); a shape outside the handled set comes back
6//! as `SurfaceKind::Other` / `CurveKind::Other` carrying the raw STEP
7//! keyword, never hidden.
8
9// Every public method here is a pure read accessor; `#[must_use]` on each would be
10// pure noise, so allow the pedantic lint module-wide.
11#![allow(clippy::must_use_candidate)]
12
13use crate::RefGraph;
14use crate::StepModel;
15use crate::scene::{Ctx, Scene};
16// Alias the generated model module: bare supertype structs (`Face`, `Surface`,
17// `Curve`, `Edge`, `Vertex`, `Point`, `Loop`) would shadow our handle names, so we
18// never import them by bare name — everything generated is reached via `m::`.
19use crate::generated::model as m;
20
21/// Model-wide by-type queries. Naming convention: `all_*` returns every instance
22/// of a type in the whole model (the storage is flat — one arena per type — so a
23/// model-level query is always "find all of this type"); navigating *from* a
24/// handle (e.g. `solid.faces()`) is unprefixed and scoped to that element.
25impl Scene<'_> {
26    /// Every b-rep solid in the model — `MANIFOLD_SOLID_BREP` and
27    /// `BREP_WITH_VOIDS` (solids with internal cavities).
28    pub fn all_solids(&self) -> impl Iterator<Item = Solid<'_>> + '_ {
29        let cx = self.ctx();
30        let manifold = (0..cx.model.manifold_solid_brep_arena.items.len())
31            .map(move |i| Solid::from_id(cx, m::ManifoldSolidBrepId(i)));
32        let with_voids = (0..cx.model.brep_with_voids_arena.items.len())
33            .map(move |i| Solid::from_void_id(cx, m::BrepWithVoidsId(i)));
34        manifold.chain(with_voids)
35    }
36
37    /// Every face in the model (`ADVANCED_FACE` and `FACE_SURFACE`).
38    pub fn all_faces(&self) -> impl Iterator<Item = Face<'_>> + '_ {
39        let cx = self.ctx();
40        let advanced = (0..cx.model.advanced_face_arena.items.len()).map(move |i| Face {
41            cx,
42            which: FaceImpl::Advanced(m::AdvancedFaceId(i)),
43        });
44        let surfaces = (0..cx.model.face_surface_arena.items.len()).map(move |i| Face {
45            cx,
46            which: FaceImpl::Surface(m::FaceSurfaceId(i)),
47        });
48        advanced.chain(surfaces)
49    }
50
51    /// Every edge in the model (`EDGE_CURVE`).
52    pub fn all_edges(&self) -> impl Iterator<Item = Edge<'_>> + '_ {
53        let cx = self.ctx();
54        (0..cx.model.edge_curve_arena.items.len()).map(move |i| Edge {
55            cx,
56            id: m::EdgeCurveId(i),
57        })
58    }
59
60    /// Every vertex in the model (`VERTEX_POINT`).
61    pub fn all_vertices(&self) -> impl Iterator<Item = Vertex<'_>> + '_ {
62        let cx = self.ctx();
63        (0..cx.model.vertex_point_arena.items.len()).map(move |i| Vertex {
64            cx,
65            id: m::VertexPointId(i),
66        })
67    }
68
69    /// Every geometric point in the model (`CARTESIAN_POINT`).
70    pub fn all_points(&self) -> impl Iterator<Item = Point<'_>> + '_ {
71        let cx = self.ctx();
72        (0..cx.model.cartesian_point_arena.items.len()).map(move |i| Point {
73            cx,
74            id: m::CartesianPointId(i),
75        })
76    }
77}
78
79// ---------------------------------------------------------------------------
80// Solid
81// ---------------------------------------------------------------------------
82
83/// A b-rep solid — a `MANIFOLD_SOLID_BREP`, or a `BREP_WITH_VOIDS` (a body with
84/// internal cavities). Use [`voids`](Solid::voids) to reach the cavity shells.
85#[derive(Clone, Copy)]
86pub struct Solid<'m> {
87    cx: Ctx<'m>,
88    which: SolidImpl,
89}
90
91#[derive(Clone, Copy)]
92enum SolidImpl {
93    Manifold(m::ManifoldSolidBrepId),
94    WithVoids(m::BrepWithVoidsId),
95}
96
97impl<'m> Solid<'m> {
98    /// Construct a solid handle from a `MANIFOLD_SOLID_BREP` id — used by other
99    /// scene modules (e.g. `product`) to bridge into geometry; the fields are
100    /// otherwise private.
101    pub(crate) fn from_id(cx: Ctx<'m>, id: m::ManifoldSolidBrepId) -> Self {
102        Solid {
103            cx,
104            which: SolidImpl::Manifold(id),
105        }
106    }
107
108    /// Construct a solid handle from a `BREP_WITH_VOIDS` id.
109    pub(crate) fn from_void_id(cx: Ctx<'m>, id: m::BrepWithVoidsId) -> Self {
110        Solid {
111            cx,
112            which: SolidImpl::WithVoids(id),
113        }
114    }
115
116    /// (`name`, `outer` shell) — `MANIFOLD_SOLID_BREP` and `BREP_WITH_VOIDS`
117    /// share this layout.
118    fn name_and_outer(&self) -> (&'m str, &'m m::ClosedShellRef) {
119        match self.which {
120            SolidImpl::Manifold(i) => {
121                let s = self.cx.model.manifold_solid_brep_arena.get(i.0);
122                (&s.name, &s.outer)
123            }
124            SolidImpl::WithVoids(i) => {
125                let s = self.cx.model.brep_with_voids_arena.get(i.0);
126                (&s.name, &s.outer)
127            }
128        }
129    }
130
131    pub fn name(&self) -> &'m str {
132        self.name_and_outer().0
133    }
134
135    /// This solid's global identity (a `Copy` key for maps / deduplication; two
136    /// handles to the same entity share one `key()`).
137    pub fn key(&self) -> m::EntityKey {
138        match self.which {
139            SolidImpl::Manifold(i) => m::EntityKey::ManifoldSolidBrep(i),
140            SolidImpl::WithVoids(i) => m::EntityKey::BrepWithVoids(i),
141        }
142    }
143
144    /// The colour styled onto this solid, if any.
145    pub fn color(&self) -> Option<crate::scene::presentation::Rgb> {
146        crate::scene::presentation::colour_of(self.cx, self.key())
147    }
148
149    /// The transparency styled onto this solid (`0.0` opaque … `1.0` fully
150    /// transparent), if any.
151    pub fn transparency(&self) -> Option<f64> {
152        crate::scene::presentation::transparency_of(self.cx, self.key())
153    }
154
155    /// The name of the presentation layer this solid belongs to, if any.
156    pub fn layer(&self) -> Option<&'m str> {
157        crate::scene::presentation::layer_of(self.cx, self.key())
158    }
159
160    /// Whether this solid is visible — `false` if an `INVISIBILITY` hides it (via
161    /// its styled item or its layer). Defaults to visible.
162    pub fn is_visible(&self) -> bool {
163        crate::scene::presentation::is_visible(self.cx, self.key())
164    }
165
166    /// Faces of the solid's outer shell.
167    pub fn faces(&self) -> impl Iterator<Item = Face<'m>> + 'm {
168        let cx = self.cx;
169        resolve_closed_shell(cx.model, self.name_and_outer().1)
170            .into_iter()
171            .flat_map(|shell| shell.cfs_faces.iter())
172            .filter_map(move |fr| Face::from_ref(cx, fr))
173    }
174
175    /// Faces of each internal void shell — one inner `Vec` per cavity of a
176    /// `BREP_WITH_VOIDS`. Empty for a plain `MANIFOLD_SOLID_BREP`.
177    pub fn voids(&self) -> Vec<Vec<Face<'m>>> {
178        let cx = self.cx;
179        let SolidImpl::WithVoids(id) = self.which else {
180            return Vec::new();
181        };
182        cx.model
183            .brep_with_voids_arena
184            .get(id.0)
185            .voids
186            .iter()
187            .map(|ocs| {
188                let shell = match ocs {
189                    m::OrientedClosedShellRef::OrientedClosedShell(o) => resolve_closed_shell(
190                        cx.model,
191                        &cx.model
192                            .oriented_closed_shell_arena
193                            .get(o.0)
194                            .closed_shell_element,
195                    ),
196                    m::OrientedClosedShellRef::Complex(_) => None,
197                };
198                shell
199                    .into_iter()
200                    .flat_map(|s| s.cfs_faces.iter())
201                    .filter_map(|fr| Face::from_ref(cx, fr))
202                    .collect()
203            })
204            .collect()
205    }
206}
207
208// ---------------------------------------------------------------------------
209// Face
210// ---------------------------------------------------------------------------
211
212#[derive(Clone, Copy)]
213enum FaceImpl {
214    Advanced(m::AdvancedFaceId),
215    Surface(m::FaceSurfaceId),
216}
217
218/// A bounded face of a solid (`ADVANCED_FACE` / `FACE_SURFACE`) — a surface
219/// trimmed by boundary loops, with an orientation.
220#[derive(Clone, Copy)]
221pub struct Face<'m> {
222    cx: Ctx<'m>,
223    which: FaceImpl,
224}
225
226impl<'m> Face<'m> {
227    /// Construct a face handle from an `ADVANCED_FACE` id — used by other scene
228    /// modules (e.g. `pmi` feature geometry) to bridge into geometry.
229    pub(crate) fn from_advanced_face(cx: Ctx<'m>, id: m::AdvancedFaceId) -> Self {
230        Face {
231            cx,
232            which: FaceImpl::Advanced(id),
233        }
234    }
235
236    fn from_ref(cx: Ctx<'m>, r: &m::FaceRef) -> Option<Self> {
237        match r {
238            m::FaceRef::AdvancedFace(i) => Some(Face {
239                cx,
240                which: FaceImpl::Advanced(*i),
241            }),
242            m::FaceRef::FaceSurface(i) => Some(Face {
243                cx,
244                which: FaceImpl::Surface(*i),
245            }),
246            // Bare `Face` carries no geometry; complex faces are out of scope.
247            m::FaceRef::Face(_) | m::FaceRef::Complex(_) => None,
248        }
249    }
250
251    /// (`name`, `bounds`, `face_geometry`, `same_sense`) — `ADVANCED_FACE` and
252    /// `FACE_SURFACE` share this layout.
253    fn parts(&self) -> (&'m str, &'m [m::FaceBoundRef], &'m m::SurfaceRef, bool) {
254        match self.which {
255            FaceImpl::Advanced(i) => {
256                let f = self.cx.model.advanced_face_arena.get(i.0);
257                (&f.name, &f.bounds, &f.face_geometry, f.same_sense)
258            }
259            FaceImpl::Surface(i) => {
260                let f = self.cx.model.face_surface_arena.get(i.0);
261                (&f.name, &f.bounds, &f.face_geometry, f.same_sense)
262            }
263        }
264    }
265
266    pub fn name(&self) -> &'m str {
267        self.parts().0
268    }
269
270    pub fn same_sense(&self) -> bool {
271        self.parts().3
272    }
273
274    /// The underlying (unbounded) surface this face sits on.
275    pub fn surface(&self) -> Surface<'m> {
276        Surface {
277            cx: self.cx,
278            r: self.parts().2,
279        }
280    }
281
282    /// The boundary loops trimming this face.
283    pub fn bounds(&self) -> impl Iterator<Item = Bound<'m>> + 'm {
284        let cx = self.cx;
285        self.parts()
286            .1
287            .iter()
288            .filter_map(move |br| Bound::from_ref(cx, br))
289    }
290
291    /// The point set used to size a bounded patch for an unbounded surface:
292    /// the control points of every boundary edge's NURBS form. The control
293    /// hull contains the curve, so extents taken from these points are a
294    /// *guaranteed* cover — a curved edge bulging past its vertices is
295    /// included (vertices alone could under-cover). An edge that does not
296    /// convert falls back to its two vertex points.
297    fn boundary_points(&self) -> Vec<[f64; 3]> {
298        let mut points = Vec::new();
299        for bound in self.bounds() {
300            for edge in bound.edges() {
301                if let Some(n) = edge.to_nurbs() {
302                    points.extend(n.control_points);
303                } else {
304                    for v in [edge.start(), edge.end()].into_iter().flatten() {
305                        if let Some(pt) = v.point() {
306                            points.push(pt.xyz());
307                        }
308                    }
309                }
310            }
311        }
312        points
313    }
314
315    /// The NURBS ([`NurbsSurface`](crate::scene::NurbsSurface)) form of this
316    /// face's geometry. A closed analytic surface (sphere, torus, B-spline) is
317    /// returned whole via [`Surface::to_nurbs`]. An *unbounded* surface (plane,
318    /// cylinder, cone, linear extrusion) is sized to the control points of its
319    /// boundary edges' NURBS forms — a guaranteed cover of the boundary — and
320    /// is the untrimmed base patch (the edge loops trim it separately).
321    /// `None` if the kind is unsupported or the bounds are degenerate.
322    pub fn to_nurbs(&self) -> Option<crate::scene::nurbs::NurbsSurface> {
323        if let Some(n) = self.surface().to_nurbs() {
324            return Some(n);
325        }
326        let cx = self.cx;
327        let pts = self.boundary_points();
328        match self.surface().kind() {
329            SurfaceKind::Plane(p) => crate::scene::nurbs::plane_patch(cx, p, &pts),
330            SurfaceKind::Cylindrical(c) => {
331                crate::scene::nurbs::cylinder_cone_patch(cx, &c.position, c.radius, 0.0, &pts)
332            }
333            SurfaceKind::Conical(c) => crate::scene::nurbs::cylinder_cone_patch(
334                cx,
335                &c.position,
336                c.radius,
337                c.semi_angle,
338                &pts,
339            ),
340            SurfaceKind::LinearExtrusion(s) => {
341                let profile = Curve::from_curve_ref(cx, &s.swept_curve).to_nurbs()?;
342                crate::scene::nurbs::extrude_patch(cx, &profile, &s.extrusion_axis, &pts)
343            }
344            // Bounded profiles were already handled whole by the surface-level
345            // conversion above; what remains is the unbounded LINE profile
346            // (a lathe-style cylinder/cone/hyperboloid), bounded here by the
347            // face's vertices.
348            SurfaceKind::Revolution(s) => {
349                let CurveKind::Line(line) = Curve::from_curve_ref(cx, &s.swept_curve).kind() else {
350                    return None;
351                };
352                let frame = crate::scene::nurbs::axis1_frame(cx, &s.axis_position)?;
353                let seg = crate::scene::nurbs::revolved_line_segment(cx, line, frame, &pts)?;
354                Some(crate::scene::nurbs::revolve_curve(&seg, frame))
355            }
356            _ => None,
357        }
358    }
359
360    /// This face's global identity (a `Copy` key for maps / deduplication; two
361    /// handles to the same entity share one `key()`).
362    pub fn key(&self) -> m::EntityKey {
363        match self.which {
364            FaceImpl::Advanced(i) => m::EntityKey::AdvancedFace(i),
365            FaceImpl::Surface(i) => m::EntityKey::FaceSurface(i),
366        }
367    }
368
369    /// The colour styled onto this face, if any.
370    pub fn color(&self) -> Option<crate::scene::presentation::Rgb> {
371        crate::scene::presentation::colour_of(self.cx, self.key())
372    }
373
374    /// The transparency styled onto this face (`0.0` opaque … `1.0` fully
375    /// transparent), if any.
376    pub fn transparency(&self) -> Option<f64> {
377        crate::scene::presentation::transparency_of(self.cx, self.key())
378    }
379
380    /// The name of the presentation layer this face belongs to, if any.
381    pub fn layer(&self) -> Option<&'m str> {
382        crate::scene::presentation::layer_of(self.cx, self.key())
383    }
384
385    /// Whether this face is visible — `false` if an `INVISIBILITY` hides it (via
386    /// its styled item or its layer). Defaults to visible.
387    pub fn is_visible(&self) -> bool {
388        crate::scene::presentation::is_visible(self.cx, self.key())
389    }
390
391    /// The b-rep solid this face belongs to, if any (reverse: a `CLOSED_SHELL`
392    /// listing this face → a `MANIFOLD_SOLID_BREP` or `BREP_WITH_VOIDS` whose
393    /// shell is that shell, directly or through an `ORIENTED_CLOSED_SHELL`).
394    pub fn solid(&self) -> Option<Solid<'m>> {
395        let rg = self.cx.ref_graph();
396        for &r in rg.referrers(self.key()) {
397            if let m::EntityKey::ClosedShell(cs) = r {
398                if let Some(which) = closed_shell_to_solid(rg, cs) {
399                    return Some(Solid { cx: self.cx, which });
400                }
401            }
402        }
403        None
404    }
405}
406
407// ---------------------------------------------------------------------------
408// Surface
409// ---------------------------------------------------------------------------
410
411/// The geometry under a [`Face`]. Call [`Surface::kind`] to dispatch.
412#[derive(Clone, Copy)]
413pub struct Surface<'m> {
414    cx: Ctx<'m>,
415    r: &'m m::SurfaceRef,
416}
417
418/// The concrete surface geometry. Common kinds are typed; everything else is
419/// `Other(keyword)` (the surface still exists — it is surfaced, not dropped).
420#[derive(Clone, Copy, Debug)]
421pub enum SurfaceKind<'m> {
422    Plane(&'m m::Plane),
423    Cylindrical(&'m m::CylindricalSurface),
424    Conical(&'m m::ConicalSurface),
425    Spherical(&'m m::SphericalSurface),
426    Toroidal(&'m m::ToroidalSurface),
427    BSpline(&'m m::BSplineSurface),
428    BSplineWithKnots(&'m m::BSplineSurfaceWithKnots),
429    LinearExtrusion(&'m m::SurfaceOfLinearExtrusion),
430    Revolution(&'m m::SurfaceOfRevolution),
431    QuasiUniform(&'m m::QuasiUniformSurface),
432    Uniform(&'m m::UniformSurface),
433    Bezier(&'m m::BezierSurface),
434    Other(&'static str),
435}
436
437impl<'m> Surface<'m> {
438    /// This surface's global identity (a `Copy` key for maps / deduplication; a
439    /// surface shared by several faces yields the same `key()`).
440    pub fn key(&self) -> m::EntityKey {
441        self.r.entity_key()
442    }
443
444    /// The rational-B-spline ([`NurbsSurface`](crate::scene::NurbsSurface))
445    /// form of this surface, if the kind is supported. Only *closed* analytic
446    /// surfaces convert as-is — a full sphere or torus. Open surfaces (plane,
447    /// cylinder, cone) are unbounded and need a face's bounds, so they return
448    /// `None` here — use [`Face::to_nurbs`] instead, which has the bounds.
449    pub fn to_nurbs(&self) -> Option<crate::scene::nurbs::NurbsSurface> {
450        let cx = self.cx;
451        match self.r {
452            m::SurfaceRef::SphericalSurface(i) => {
453                crate::scene::nurbs::sphere_to_nurbs(cx, cx.model.spherical_surface_arena.get(i.0))
454            }
455            m::SurfaceRef::ToroidalSurface(i) => {
456                crate::scene::nurbs::torus_to_nurbs(cx, cx.model.toroidal_surface_arena.get(i.0))
457            }
458            m::SurfaceRef::BSplineSurfaceWithKnots(i) => {
459                crate::scene::nurbs::bspline_surf_wk_to_nurbs(
460                    cx,
461                    cx.model.b_spline_surface_with_knots_arena.get(i.0),
462                )
463            }
464            // A rational B-spline surface arrives as a complex instance.
465            m::SurfaceRef::Complex(i) => crate::scene::nurbs::complex_bspline_surface_to_nurbs(
466                cx,
467                &cx.model.complex_unit_arena.get(i.0).parts,
468            ),
469            // A full revolution is closed in u, so a bounded profile gives the
470            // whole surface (an unbounded LINE profile needs a face's bounds).
471            m::SurfaceRef::SurfaceOfRevolution(i) => {
472                let s = cx.model.surface_of_revolution_arena.get(i.0);
473                let profile = Curve::from_curve_ref(cx, &s.swept_curve).to_nurbs()?;
474                let frame = crate::scene::nurbs::axis1_frame(cx, &s.axis_position)?;
475                Some(crate::scene::nurbs::revolve_curve(&profile, frame))
476            }
477            // Subtypes whose knot vectors are derived by a standard rule.
478            m::SurfaceRef::UniformSurface(i) => {
479                let s = cx.model.uniform_surface_arena.get(i.0);
480                crate::scene::nurbs::uniform_family_surface_to_nurbs(
481                    cx,
482                    (s.u_degree, s.v_degree),
483                    &s.control_points_list,
484                    crate::scene::nurbs::KnotFamily::Uniform,
485                )
486            }
487            m::SurfaceRef::QuasiUniformSurface(i) => {
488                let s = cx.model.quasi_uniform_surface_arena.get(i.0);
489                crate::scene::nurbs::uniform_family_surface_to_nurbs(
490                    cx,
491                    (s.u_degree, s.v_degree),
492                    &s.control_points_list,
493                    crate::scene::nurbs::KnotFamily::QuasiUniform,
494                )
495            }
496            m::SurfaceRef::BezierSurface(i) => {
497                let s = cx.model.bezier_surface_arena.get(i.0);
498                crate::scene::nurbs::uniform_family_surface_to_nurbs(
499                    cx,
500                    (s.u_degree, s.v_degree),
501                    &s.control_points_list,
502                    crate::scene::nurbs::KnotFamily::Bezier,
503                )
504            }
505            _ => None,
506        }
507    }
508
509    pub fn kind(&self) -> SurfaceKind<'m> {
510        let model = self.cx.model;
511        match self.r {
512            m::SurfaceRef::Plane(i) => SurfaceKind::Plane(model.plane_arena.get(i.0)),
513            m::SurfaceRef::CylindricalSurface(i) => {
514                SurfaceKind::Cylindrical(model.cylindrical_surface_arena.get(i.0))
515            }
516            m::SurfaceRef::ConicalSurface(i) => {
517                SurfaceKind::Conical(model.conical_surface_arena.get(i.0))
518            }
519            m::SurfaceRef::SphericalSurface(i) => {
520                SurfaceKind::Spherical(model.spherical_surface_arena.get(i.0))
521            }
522            m::SurfaceRef::ToroidalSurface(i) => {
523                SurfaceKind::Toroidal(model.toroidal_surface_arena.get(i.0))
524            }
525            m::SurfaceRef::BSplineSurface(i) => {
526                SurfaceKind::BSpline(model.b_spline_surface_arena.get(i.0))
527            }
528            m::SurfaceRef::BSplineSurfaceWithKnots(i) => {
529                SurfaceKind::BSplineWithKnots(model.b_spline_surface_with_knots_arena.get(i.0))
530            }
531            m::SurfaceRef::BezierSurface(i) => {
532                SurfaceKind::Bezier(model.bezier_surface_arena.get(i.0))
533            }
534            m::SurfaceRef::BoundedSurface(_) => SurfaceKind::Other("BOUNDED_SURFACE"),
535            m::SurfaceRef::DegenerateToroidalSurface(_) => {
536                SurfaceKind::Other("DEGENERATE_TOROIDAL_SURFACE")
537            }
538            m::SurfaceRef::ElementarySurface(_) => SurfaceKind::Other("ELEMENTARY_SURFACE"),
539            m::SurfaceRef::OffsetSurface(_) => SurfaceKind::Other("OFFSET_SURFACE"),
540            m::SurfaceRef::QuasiUniformSurface(i) => {
541                SurfaceKind::QuasiUniform(model.quasi_uniform_surface_arena.get(i.0))
542            }
543            m::SurfaceRef::RationalBSplineSurface(_) => {
544                SurfaceKind::Other("RATIONAL_B_SPLINE_SURFACE")
545            }
546            m::SurfaceRef::Surface(_) => SurfaceKind::Other("SURFACE"),
547            m::SurfaceRef::SurfaceOfLinearExtrusion(i) => {
548                SurfaceKind::LinearExtrusion(model.surface_of_linear_extrusion_arena.get(i.0))
549            }
550            m::SurfaceRef::SurfaceOfRevolution(i) => {
551                SurfaceKind::Revolution(model.surface_of_revolution_arena.get(i.0))
552            }
553            m::SurfaceRef::SweptSurface(_) => SurfaceKind::Other("SWEPT_SURFACE"),
554            m::SurfaceRef::UniformSurface(i) => {
555                SurfaceKind::Uniform(model.uniform_surface_arena.get(i.0))
556            }
557            m::SurfaceRef::Complex(_) => SurfaceKind::Other("COMPLEX"),
558        }
559    }
560}
561
562// ---------------------------------------------------------------------------
563// Bound (face boundary loop)
564// ---------------------------------------------------------------------------
565
566#[derive(Clone, Copy)]
567enum BoundImpl {
568    Inner(m::FaceBoundId),
569    Outer(m::FaceOuterBoundId),
570}
571
572/// A boundary loop of a [`Face`] (`FACE_BOUND` / `FACE_OUTER_BOUND`).
573#[derive(Clone, Copy)]
574pub struct Bound<'m> {
575    cx: Ctx<'m>,
576    which: BoundImpl,
577}
578
579impl<'m> Bound<'m> {
580    fn from_ref(cx: Ctx<'m>, r: &m::FaceBoundRef) -> Option<Self> {
581        match r {
582            m::FaceBoundRef::FaceBound(i) => Some(Bound {
583                cx,
584                which: BoundImpl::Inner(*i),
585            }),
586            m::FaceBoundRef::FaceOuterBound(i) => Some(Bound {
587                cx,
588                which: BoundImpl::Outer(*i),
589            }),
590            m::FaceBoundRef::Complex(_) => None,
591        }
592    }
593
594    /// `true` for `FACE_OUTER_BOUND`.
595    pub fn is_outer(&self) -> bool {
596        matches!(self.which, BoundImpl::Outer(_))
597    }
598
599    /// This bound's global identity (a `Copy` key for maps / deduplication).
600    pub fn key(&self) -> m::EntityKey {
601        match self.which {
602            BoundImpl::Inner(i) => m::EntityKey::FaceBound(i),
603            BoundImpl::Outer(i) => m::EntityKey::FaceOuterBound(i),
604        }
605    }
606
607    fn loop_ref(&self) -> &'m m::LoopRef {
608        match self.which {
609            BoundImpl::Inner(i) => &self.cx.model.face_bound_arena.get(i.0).bound,
610            BoundImpl::Outer(i) => &self.cx.model.face_outer_bound_arena.get(i.0).bound,
611        }
612    }
613
614    /// Whether this loop runs in its stated sense with respect to the face —
615    /// the `FACE_BOUND` `orientation` flag; `false` means the loop is used
616    /// reversed. One of the three orientation layers a kernel composes:
617    /// [`Face::same_sense`] (surface normal), this flag (whole loop), and the
618    /// per-edge flag of [`Bound::oriented_edges`].
619    pub fn orientation(&self) -> bool {
620        match self.which {
621            BoundImpl::Inner(i) => self.cx.model.face_bound_arena.get(i.0).orientation,
622            BoundImpl::Outer(i) => self.cx.model.face_outer_bound_arena.get(i.0).orientation,
623        }
624    }
625
626    /// Edges of this boundary loop (only `EDGE_LOOP` loops carry edges),
627    /// without their per-edge orientation — use [`Bound::oriented_edges`] to
628    /// assemble a wire.
629    pub fn edges(&self) -> impl Iterator<Item = Edge<'m>> + 'm {
630        self.oriented_edges().map(|(e, _)| e)
631    }
632
633    /// Edges of this boundary loop with each `ORIENTED_EDGE`'s orientation:
634    /// `true` means the edge is traversed in its own start → end direction at
635    /// this position in the loop, `false` reversed — what a kernel needs to
636    /// assemble the loop into a wire without end-matching heuristics.
637    pub fn oriented_edges(&self) -> impl Iterator<Item = (Edge<'m>, bool)> + 'm {
638        let cx = self.cx;
639        resolve_edge_loop(cx.model, self.loop_ref())
640            .into_iter()
641            .flat_map(|el| el.edge_list.iter())
642            .filter_map(move |oer| {
643                let oe = resolve_oriented_edge(cx.model, oer)?;
644                Some((Edge::from_edge_ref(cx, &oe.edge_element)?, oe.orientation))
645            })
646    }
647}
648
649// ---------------------------------------------------------------------------
650// Edge
651// ---------------------------------------------------------------------------
652
653/// An edge of a loop (`EDGE_CURVE`) — a curve bounded by two vertices.
654#[derive(Clone, Copy)]
655pub struct Edge<'m> {
656    cx: Ctx<'m>,
657    id: m::EdgeCurveId,
658}
659
660impl<'m> Edge<'m> {
661    /// Construct an edge handle from an `EDGE_CURVE` id — used by other scene
662    /// modules (e.g. `pmi` feature geometry) to bridge into geometry.
663    pub(crate) fn from_id(cx: Ctx<'m>, id: m::EdgeCurveId) -> Self {
664        Edge { cx, id }
665    }
666
667    fn from_edge_ref(cx: Ctx<'m>, r: &m::EdgeRef) -> Option<Self> {
668        match r {
669            m::EdgeRef::EdgeCurve(i) => Some(Edge { cx, id: *i }),
670            // Bare `Edge`, nested `OrientedEdge`, and complex edges are out of scope.
671            m::EdgeRef::Edge(_) | m::EdgeRef::OrientedEdge(_) | m::EdgeRef::Complex(_) => None,
672        }
673    }
674
675    fn raw(&self) -> &'m m::EdgeCurve {
676        self.cx.model.edge_curve_arena.get(self.id.0)
677    }
678
679    /// This edge's global identity (a `Copy` key for maps / deduplication; a
680    /// shared edge reached from two faces yields the same `key()`).
681    pub fn key(&self) -> m::EntityKey {
682        m::EntityKey::EdgeCurve(self.id)
683    }
684
685    pub fn start(&self) -> Option<Vertex<'m>> {
686        Vertex::from_ref(self.cx, &self.raw().edge_start)
687    }
688
689    pub fn end(&self) -> Option<Vertex<'m>> {
690        Vertex::from_ref(self.cx, &self.raw().edge_end)
691    }
692
693    /// The underlying (unbounded) curve this edge lies on.
694    pub fn curve(&self) -> Curve<'m> {
695        Curve::from_curve_ref(self.cx, &self.raw().edge_geometry)
696    }
697
698    /// Whether the edge's start → end direction agrees with its curve's
699    /// parameter direction (`EDGE_CURVE` `same_sense`). [`Edge::to_nurbs`]
700    /// already accounts for it; an importer consuming the exact curve via
701    /// [`Edge::curve`] composes it itself.
702    pub fn same_sense(&self) -> bool {
703        self.raw().same_sense
704    }
705
706    /// The NURBS ([`NurbsCurve`](crate::scene::NurbsCurve)) form of this
707    /// edge: its geometry bounded between the two vertex points, in the edge's
708    /// direction (start → end). Contrast with [`Curve::to_nurbs`], which
709    /// converts the *whole* curve — here an unbounded line becomes the finite
710    /// segment, a circle the arc between the vertices, and an edge whose
711    /// vertices coincide (a closed edge) the whole closed curve.
712    pub fn to_nurbs(&self) -> Option<crate::scene::nurbs::NurbsCurve> {
713        let cx = self.cx;
714        let coords = |v: Option<Vertex<'m>>| Some(v?.point()?.xyz());
715        let (Some(p0), Some(p1)) = (coords(self.start()), coords(self.end())) else {
716            cx.warn("EDGE_CURVE.to_nurbs: unresolved edge vertex".to_owned());
717            return None;
718        };
719        let raw = self.raw();
720        crate::scene::nurbs::edge_to_nurbs(cx, &raw.edge_geometry, p0, p1, raw.same_sense)
721    }
722}
723
724// ---------------------------------------------------------------------------
725// Curve
726// ---------------------------------------------------------------------------
727
728/// The geometry under an [`Edge`], or a curve a feature designates. Call
729/// [`Curve::kind`] to dispatch.
730#[derive(Clone, Copy)]
731pub struct Curve<'m> {
732    cx: Ctx<'m>,
733    which: CurveImpl,
734}
735
736/// The concrete curve geometry. Common kinds are typed; everything else is
737/// `Other(keyword)`.
738#[derive(Clone, Copy, Debug)]
739pub enum CurveKind<'m> {
740    Line(&'m m::Line),
741    Circle(&'m m::Circle),
742    Ellipse(&'m m::Ellipse),
743    BSpline(&'m m::BSplineCurve),
744    BSplineWithKnots(&'m m::BSplineCurveWithKnots),
745    Trimmed(&'m m::TrimmedCurve),
746    Polyline(&'m m::Polyline),
747    Composite(&'m m::CompositeCurve),
748    QuasiUniform(&'m m::QuasiUniformCurve),
749    Uniform(&'m m::UniformCurve),
750    Bezier(&'m m::BezierCurve),
751    SurfaceCurve(&'m m::SurfaceCurve),
752    SeamCurve(&'m m::SeamCurve),
753    BoundedSurfaceCurve(&'m m::BoundedSurfaceCurve),
754    IntersectionCurve(&'m m::IntersectionCurve),
755    Other(&'static str),
756}
757
758// The curve reference is one id per curve subtype. An id-based cursor (like
759// `Face`) keeps `Curve` `Copy` and lets it be built from an arena id — needed to
760// reach a curve through a feature's `GEOMETRIC_ITEM_SPECIFIC_USAGE`, where only
761// the id is available. This table generates the cursor enum, `key`, and the two
762// constructors; `kind` is hand-written below.
763macro_rules! curve_impl {
764    ($( $V:ident, $id:ident );+ $(;)?) => {
765        #[derive(Clone, Copy)]
766        enum CurveImpl {
767            $( $V(m::$id), )+
768            Complex(m::ComplexUnitId),
769        }
770
771        impl<'m> Curve<'m> {
772            /// This curve's global identity (a `Copy` key for maps / dedup; a
773            /// curve shared by several edges yields the same `key()`).
774            pub fn key(&self) -> m::EntityKey {
775                match self.which {
776                    $( CurveImpl::$V(i) => m::EntityKey::$V(i), )+
777                    CurveImpl::Complex(i) => m::EntityKey::ComplexUnit(i),
778                }
779            }
780
781            pub(crate) fn from_curve_ref(cx: Ctx<'m>, r: &m::CurveRef) -> Self {
782                let which = match r {
783                    $( m::CurveRef::$V(i) => CurveImpl::$V(*i), )+
784                    m::CurveRef::Complex(i) => CurveImpl::Complex(*i),
785                };
786                Curve { cx, which }
787            }
788
789            /// A curve reached as a `GEOMETRIC_ITEM_SPECIFIC_USAGE` identified item
790            /// (used by `pmi` feature geometry); `None` for a non-curve item.
791            pub(crate) fn from_model_item(
792                cx: Ctx<'m>,
793                r: &m::GeometricModelItemRef,
794            ) -> Option<Self> {
795                let which = match r {
796                    $( m::GeometricModelItemRef::$V(i) => CurveImpl::$V(*i), )+
797                    _ => return None,
798                };
799                Some(Curve { cx, which })
800            }
801        }
802    };
803}
804
805curve_impl! {
806    BSplineCurve, BSplineCurveId;
807    BSplineCurveWithKnots, BSplineCurveWithKnotsId;
808    BezierCurve, BezierCurveId;
809    BoundedCurve, BoundedCurveId;
810    BoundedPcurve, BoundedPcurveId;
811    BoundedSurfaceCurve, BoundedSurfaceCurveId;
812    Circle, CircleId;
813    CompositeCurve, CompositeCurveId;
814    Conic, ConicId;
815    Curve, CurveId;
816    Ellipse, EllipseId;
817    Hyperbola, HyperbolaId;
818    IntersectionCurve, IntersectionCurveId;
819    Line, LineId;
820    Pcurve, PcurveId;
821    Polyline, PolylineId;
822    QuasiUniformCurve, QuasiUniformCurveId;
823    RationalBSplineCurve, RationalBSplineCurveId;
824    SeamCurve, SeamCurveId;
825    SurfaceCurve, SurfaceCurveId;
826    TrimmedCurve, TrimmedCurveId;
827    UniformCurve, UniformCurveId;
828}
829
830impl<'m> Curve<'m> {
831    /// The colour of this curve's line style (`CURVE_STYLE.curve_colour`), if any.
832    pub fn color(&self) -> Option<crate::scene::presentation::Rgb> {
833        crate::scene::presentation::curve_colour_of(self.cx, self.key())
834    }
835
836    /// The line width of this curve's style (`CURVE_STYLE.curve_width`, a raw
837    /// measure value), if any.
838    pub fn width(&self) -> Option<f64> {
839        crate::scene::presentation::curve_width_of(self.cx, self.key())
840    }
841
842    /// The line-font name of this curve's style (`CURVE_STYLE.curve_font`, a line
843    /// pattern like `continuous` / `dashed`), if any.
844    pub fn line_font(&self) -> Option<&'m str> {
845        crate::scene::presentation::curve_font_of(self.cx, self.key())
846    }
847
848    /// The rational-B-spline ([`NurbsCurve`](crate::scene::NurbsCurve))
849    /// form of this curve, if the kind is supported (circle, ellipse, B-spline
850    /// with knots). Returns the *full* analytic curve — trimming applied by an
851    /// edge that uses this curve is **not** reflected here. `None` for a curve
852    /// that needs bounds it alone lacks — an unbounded line, say; use
853    /// [`Edge::to_nurbs`] there.
854    pub fn to_nurbs(&self) -> Option<crate::scene::nurbs::NurbsCurve> {
855        let cx = self.cx;
856        match self.which {
857            CurveImpl::Circle(i) => {
858                crate::scene::nurbs::circle_to_nurbs(cx, cx.model.circle_arena.get(i.0))
859            }
860            CurveImpl::Ellipse(i) => {
861                crate::scene::nurbs::ellipse_to_nurbs(cx, cx.model.ellipse_arena.get(i.0))
862            }
863            CurveImpl::BSplineCurveWithKnots(i) => crate::scene::nurbs::bspline_wk_to_nurbs(
864                cx,
865                cx.model.b_spline_curve_with_knots_arena.get(i.0),
866            ),
867            // A rational B-spline arrives as a complex instance; dig its parts.
868            CurveImpl::Complex(i) => crate::scene::nurbs::complex_bspline_curve_to_nurbs(
869                cx,
870                &cx.model.complex_unit_arena.get(i.0).parts,
871            ),
872            // A trimmed line/circle/ellipse → a bounded segment/arc.
873            CurveImpl::TrimmedCurve(i) => {
874                crate::scene::nurbs::trimmed_to_nurbs(cx, cx.model.trimmed_curve_arena.get(i.0))
875            }
876            // Subtypes whose knot vector is derived by a standard rule.
877            CurveImpl::UniformCurve(i) => {
878                let c = cx.model.uniform_curve_arena.get(i.0);
879                crate::scene::nurbs::uniform_family_curve_to_nurbs(
880                    cx,
881                    c.degree,
882                    &c.control_points_list,
883                    crate::scene::nurbs::KnotFamily::Uniform,
884                )
885            }
886            CurveImpl::QuasiUniformCurve(i) => {
887                let c = cx.model.quasi_uniform_curve_arena.get(i.0);
888                crate::scene::nurbs::uniform_family_curve_to_nurbs(
889                    cx,
890                    c.degree,
891                    &c.control_points_list,
892                    crate::scene::nurbs::KnotFamily::QuasiUniform,
893                )
894            }
895            CurveImpl::BezierCurve(i) => {
896                let c = cx.model.bezier_curve_arena.get(i.0);
897                crate::scene::nurbs::uniform_family_curve_to_nurbs(
898                    cx,
899                    c.degree,
900                    &c.control_points_list,
901                    crate::scene::nurbs::KnotFamily::Bezier,
902                )
903            }
904            CurveImpl::Polyline(i) => {
905                crate::scene::nurbs::polyline_to_nurbs(cx, cx.model.polyline_arena.get(i.0))
906            }
907            // A segment chain joined into one exact NURBS.
908            CurveImpl::CompositeCurve(i) => {
909                crate::scene::nurbs::composite_to_nurbs(cx, cx.model.composite_curve_arena.get(i.0))
910            }
911            // Surface-curve containers delegate to their 3D curve.
912            CurveImpl::SurfaceCurve(i) => crate::scene::nurbs::surface_curve_3d_to_nurbs(
913                cx,
914                &cx.model.surface_curve_arena.get(i.0).curve_3d,
915            ),
916            CurveImpl::SeamCurve(i) => crate::scene::nurbs::surface_curve_3d_to_nurbs(
917                cx,
918                &cx.model.seam_curve_arena.get(i.0).curve_3d,
919            ),
920            CurveImpl::BoundedSurfaceCurve(i) => crate::scene::nurbs::surface_curve_3d_to_nurbs(
921                cx,
922                &cx.model.bounded_surface_curve_arena.get(i.0).curve_3d,
923            ),
924            CurveImpl::IntersectionCurve(i) => crate::scene::nurbs::surface_curve_3d_to_nurbs(
925                cx,
926                &cx.model.intersection_curve_arena.get(i.0).curve_3d,
927            ),
928            _ => None,
929        }
930    }
931
932    pub fn kind(&self) -> CurveKind<'m> {
933        let model = self.cx.model;
934        match self.which {
935            CurveImpl::Line(i) => CurveKind::Line(model.line_arena.get(i.0)),
936            CurveImpl::Circle(i) => CurveKind::Circle(model.circle_arena.get(i.0)),
937            CurveImpl::Ellipse(i) => CurveKind::Ellipse(model.ellipse_arena.get(i.0)),
938            CurveImpl::BSplineCurve(i) => CurveKind::BSpline(model.b_spline_curve_arena.get(i.0)),
939            CurveImpl::BSplineCurveWithKnots(i) => {
940                CurveKind::BSplineWithKnots(model.b_spline_curve_with_knots_arena.get(i.0))
941            }
942            CurveImpl::TrimmedCurve(i) => CurveKind::Trimmed(model.trimmed_curve_arena.get(i.0)),
943            CurveImpl::BezierCurve(i) => CurveKind::Bezier(model.bezier_curve_arena.get(i.0)),
944            CurveImpl::BoundedCurve(_) => CurveKind::Other("BOUNDED_CURVE"),
945            CurveImpl::BoundedPcurve(_) => CurveKind::Other("BOUNDED_PCURVE"),
946            CurveImpl::BoundedSurfaceCurve(i) => {
947                CurveKind::BoundedSurfaceCurve(model.bounded_surface_curve_arena.get(i.0))
948            }
949            CurveImpl::CompositeCurve(i) => {
950                CurveKind::Composite(model.composite_curve_arena.get(i.0))
951            }
952            CurveImpl::Conic(_) => CurveKind::Other("CONIC"),
953            CurveImpl::Curve(_) => CurveKind::Other("CURVE"),
954            CurveImpl::Hyperbola(_) => CurveKind::Other("HYPERBOLA"),
955            CurveImpl::IntersectionCurve(i) => {
956                CurveKind::IntersectionCurve(model.intersection_curve_arena.get(i.0))
957            }
958            CurveImpl::Pcurve(_) => CurveKind::Other("PCURVE"),
959            CurveImpl::Polyline(i) => CurveKind::Polyline(model.polyline_arena.get(i.0)),
960            CurveImpl::QuasiUniformCurve(i) => {
961                CurveKind::QuasiUniform(model.quasi_uniform_curve_arena.get(i.0))
962            }
963            CurveImpl::RationalBSplineCurve(_) => CurveKind::Other("RATIONAL_B_SPLINE_CURVE"),
964            CurveImpl::SeamCurve(i) => CurveKind::SeamCurve(model.seam_curve_arena.get(i.0)),
965            CurveImpl::SurfaceCurve(i) => {
966                CurveKind::SurfaceCurve(model.surface_curve_arena.get(i.0))
967            }
968            CurveImpl::UniformCurve(i) => CurveKind::Uniform(model.uniform_curve_arena.get(i.0)),
969            CurveImpl::Complex(_) => CurveKind::Other("COMPLEX"),
970        }
971    }
972}
973
974// ---------------------------------------------------------------------------
975// Vertex / Point
976// ---------------------------------------------------------------------------
977
978/// A topological vertex (`VERTEX_POINT`).
979#[derive(Clone, Copy)]
980pub struct Vertex<'m> {
981    cx: Ctx<'m>,
982    id: m::VertexPointId,
983}
984
985impl<'m> Vertex<'m> {
986    fn from_ref(cx: Ctx<'m>, r: &m::VertexRef) -> Option<Self> {
987        match r {
988            m::VertexRef::VertexPoint(i) => Some(Vertex { cx, id: *i }),
989            m::VertexRef::Vertex(_) | m::VertexRef::Complex(_) => None,
990        }
991    }
992
993    /// This vertex's global identity (a `Copy` key for maps / deduplication).
994    pub fn key(&self) -> m::EntityKey {
995        m::EntityKey::VertexPoint(self.id)
996    }
997
998    /// The vertex's geometric point (only `CARTESIAN_POINT` is resolved here).
999    pub fn point(&self) -> Option<Point<'m>> {
1000        let r = &self
1001            .cx
1002            .model
1003            .vertex_point_arena
1004            .get(self.id.0)
1005            .vertex_geometry;
1006        match r {
1007            m::PointRef::CartesianPoint(i) => Some(Point {
1008                cx: self.cx,
1009                id: *i,
1010            }),
1011            m::PointRef::ApllPoint(_)
1012            | m::PointRef::ApllPointWithSurface(_)
1013            | m::PointRef::Point(_)
1014            | m::PointRef::Complex(_) => None,
1015        }
1016    }
1017
1018    /// The edges that begin or end at this vertex (reverse: an `EDGE_CURVE` whose
1019    /// `edge_start` or `edge_end` is this vertex). A closed edge whose start and
1020    /// end are this same vertex is yielded once.
1021    pub fn edges(&self) -> impl Iterator<Item = Edge<'m>> + 'm {
1022        let cx = self.cx;
1023        let rg = cx.ref_graph();
1024        let mut out: Vec<Edge<'m>> = Vec::new();
1025        for &r in rg.referrers(m::EntityKey::VertexPoint(self.id)) {
1026            if let m::EntityKey::EdgeCurve(eid) = r {
1027                if !out.iter().any(|e| e.id == eid) {
1028                    out.push(Edge { cx, id: eid });
1029                }
1030            }
1031        }
1032        out.into_iter()
1033    }
1034}
1035
1036/// A geometric point (`CARTESIAN_POINT`).
1037#[derive(Clone, Copy)]
1038pub struct Point<'m> {
1039    cx: Ctx<'m>,
1040    id: m::CartesianPointId,
1041}
1042
1043impl<'m> Point<'m> {
1044    /// Construct a point handle from a `CARTESIAN_POINT` id — used by other scene
1045    /// modules (e.g. `pmi` feature geometry) to bridge into geometry.
1046    pub(crate) fn from_id(cx: Ctx<'m>, id: m::CartesianPointId) -> Self {
1047        Point { cx, id }
1048    }
1049
1050    /// This point's global identity (a `Copy` key for maps / deduplication).
1051    pub fn key(&self) -> m::EntityKey {
1052        m::EntityKey::CartesianPoint(self.id)
1053    }
1054
1055    /// The point's coordinates (`x, y[, z]`).
1056    /// The coordinates as a 3-vector, missing components padded with `0.0` —
1057    /// the form every consumer wants (see [`Point::coords`] for the raw slice).
1058    pub fn xyz(&self) -> [f64; 3] {
1059        let c = self.coords();
1060        [
1061            c.first().copied().unwrap_or(0.0),
1062            c.get(1).copied().unwrap_or(0.0),
1063            c.get(2).copied().unwrap_or(0.0),
1064        ]
1065    }
1066
1067    pub fn coords(&self) -> &'m [f64] {
1068        &self
1069            .cx
1070            .model
1071            .cartesian_point_arena
1072            .get(self.id.0)
1073            .coordinates
1074    }
1075}
1076
1077// ---------------------------------------------------------------------------
1078// Reference resolvers (hand-written for the spike)
1079// ---------------------------------------------------------------------------
1080
1081/// The solid whose shell is this closed shell — a `MANIFOLD_SOLID_BREP` or a
1082/// `BREP_WITH_VOIDS`, directly or through an `ORIENTED_CLOSED_SHELL` wrapper
1083/// (the wrapper is how a `BREP_WITH_VOIDS` cavity references its shell). Reverse
1084/// over [`RefGraph`].
1085fn closed_shell_to_solid(rg: &RefGraph, cs: m::ClosedShellId) -> Option<SolidImpl> {
1086    for &r in rg.referrers(m::EntityKey::ClosedShell(cs)) {
1087        match r {
1088            m::EntityKey::ManifoldSolidBrep(msb) => return Some(SolidImpl::Manifold(msb)),
1089            m::EntityKey::BrepWithVoids(bwv) => return Some(SolidImpl::WithVoids(bwv)),
1090            m::EntityKey::OrientedClosedShell(ocs) => {
1091                for &r2 in rg.referrers(m::EntityKey::OrientedClosedShell(ocs)) {
1092                    match r2 {
1093                        m::EntityKey::ManifoldSolidBrep(msb) => {
1094                            return Some(SolidImpl::Manifold(msb));
1095                        }
1096                        m::EntityKey::BrepWithVoids(bwv) => return Some(SolidImpl::WithVoids(bwv)),
1097                        _ => {}
1098                    }
1099                }
1100            }
1101            _ => {}
1102        }
1103    }
1104    None
1105}
1106
1107/// Resolve a `ClosedShellRef` to the underlying closed shell, following an
1108/// `OrientedClosedShell` indirection. `Complex` shells are not resolved.
1109fn resolve_closed_shell<'m>(
1110    model: &'m StepModel,
1111    r: &m::ClosedShellRef,
1112) -> Option<&'m m::ClosedShell> {
1113    match r {
1114        m::ClosedShellRef::ClosedShell(i) => Some(model.closed_shell_arena.get(i.0)),
1115        m::ClosedShellRef::OrientedClosedShell(i) => {
1116            let oc = model.oriented_closed_shell_arena.get(i.0);
1117            resolve_closed_shell(model, &oc.closed_shell_element)
1118        }
1119        m::ClosedShellRef::Complex(_) => None,
1120    }
1121}
1122
1123/// Resolve a `LoopRef` to an `EdgeLoop` (only edge loops carry edges).
1124fn resolve_edge_loop<'m>(model: &'m StepModel, r: &m::LoopRef) -> Option<&'m m::EdgeLoop> {
1125    match r {
1126        m::LoopRef::EdgeLoop(i) => Some(model.edge_loop_arena.get(i.0)),
1127        m::LoopRef::Loop(_)
1128        | m::LoopRef::PolyLoop(_)
1129        | m::LoopRef::VertexLoop(_)
1130        | m::LoopRef::Complex(_) => None,
1131    }
1132}
1133
1134/// Resolve an `OrientedEdgeRef` to an `OrientedEdge`.
1135fn resolve_oriented_edge<'m>(
1136    model: &'m StepModel,
1137    r: &m::OrientedEdgeRef,
1138) -> Option<&'m m::OrientedEdge> {
1139    match r {
1140        m::OrientedEdgeRef::OrientedEdge(i) => Some(model.oriented_edge_arena.get(i.0)),
1141        m::OrientedEdgeRef::Complex(_) => None,
1142    }
1143}