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