Skip to main content

step_io/scene/
pmi.rs

1//! PMI — the product and manufacturing information annotating a shape.
2//!
3//! Model-wide slices so far: **dimensions** ([`Scene::dimensions`]),
4//! **geometric tolerances** ([`Scene::tolerances`]), and **datums**
5//! ([`Scene::datums`]). A dimension carries a [`kind`](Dimension::kind) and a
6//! nominal [`value`](Dimension::value); a tolerance its
7//! [`kind`](Tolerance::kind), [`magnitude`](Tolerance::magnitude), and
8//! referenced [`datums`](Tolerance::datums). Both point at the shape
9//! [`Feature`] they apply to ([`Dimension::features`] /
10//! [`Tolerance::feature`]).
11
12// Every public method here is a pure read accessor; `#[must_use]` on each would be
13// pure noise, so allow the pedantic lint module-wide.
14#![allow(clippy::must_use_candidate)]
15
16use crate::generated::model as m;
17use crate::scene::geometry::{Curve, Edge, Face, Point, Solid};
18use crate::scene::{Ctx, Scene};
19
20/// What a [`Dimension`] measures.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum DimensionKind {
23    /// `DIMENSIONAL_SIZE` — the size of one feature.
24    Size,
25    /// `DIMENSIONAL_LOCATION` — a distance between two features.
26    Location,
27    /// `ANGULAR_SIZE` — the angle of one feature.
28    AngularSize,
29    /// `ANGULAR_LOCATION` — an angle between two features.
30    AngularLocation,
31}
32
33#[derive(Clone, Copy)]
34enum DimImpl {
35    Size(m::DimensionalSizeId),
36    Location(m::DimensionalLocationId),
37    AngularSize(m::AngularSizeId),
38    AngularLocation(m::AngularLocationId),
39}
40
41/// A dimension (`DIMENSIONAL_SIZE` / `_LOCATION` / `ANGULAR_SIZE` /
42/// `ANGULAR_LOCATION`) — its kind, name, and nominal value.
43#[derive(Clone, Copy)]
44pub struct Dimension<'m> {
45    cx: Ctx<'m>,
46    which: DimImpl,
47}
48
49impl Scene<'_> {
50    /// Every dimension in the model. Covers the four base dimension types;
51    /// the `…_WITH_PATH` / `…_WITH_DATUM_FEATURE` / `DIRECTED_…` subtypes are
52    /// not yet surfaced.
53    pub fn dimensions(&self) -> impl Iterator<Item = Dimension<'_>> + '_ {
54        all_dimensions(self.ctx()).into_iter()
55    }
56}
57
58/// Every dimension in the model (the four base types). Shared by
59/// [`Scene::dimensions`] and the per-part [`crate::scene::ProductDef`] queries.
60fn all_dimensions(cx: Ctx<'_>) -> Vec<Dimension<'_>> {
61    let mut out = Vec::new();
62    for i in 0..cx.model.dimensional_size_arena.items.len() {
63        out.push(Dimension {
64            cx,
65            which: DimImpl::Size(m::DimensionalSizeId(i)),
66        });
67    }
68    for i in 0..cx.model.dimensional_location_arena.items.len() {
69        out.push(Dimension {
70            cx,
71            which: DimImpl::Location(m::DimensionalLocationId(i)),
72        });
73    }
74    for i in 0..cx.model.angular_size_arena.items.len() {
75        out.push(Dimension {
76            cx,
77            which: DimImpl::AngularSize(m::AngularSizeId(i)),
78        });
79    }
80    for i in 0..cx.model.angular_location_arena.items.len() {
81        out.push(Dimension {
82            cx,
83            which: DimImpl::AngularLocation(m::AngularLocationId(i)),
84        });
85    }
86    out
87}
88
89impl<'m> Dimension<'m> {
90    /// What this dimension measures.
91    pub fn kind(&self) -> DimensionKind {
92        match self.which {
93            DimImpl::Size(_) => DimensionKind::Size,
94            DimImpl::Location(_) => DimensionKind::Location,
95            DimImpl::AngularSize(_) => DimensionKind::AngularSize,
96            DimImpl::AngularLocation(_) => DimensionKind::AngularLocation,
97        }
98    }
99
100    /// The dimension's name.
101    pub fn name(&self) -> &'m str {
102        match self.which {
103            DimImpl::Size(i) => &self.cx.model.dimensional_size_arena.get(i.0).name,
104            DimImpl::Location(i) => &self.cx.model.dimensional_location_arena.get(i.0).name,
105            DimImpl::AngularSize(i) => &self.cx.model.angular_size_arena.get(i.0).name,
106            DimImpl::AngularLocation(i) => &self.cx.model.angular_location_arena.get(i.0).name,
107        }
108    }
109
110    /// This dimension's global identity (a `Copy` key for maps / deduplication).
111    pub fn key(&self) -> m::EntityKey {
112        match self.which {
113            DimImpl::Size(i) => m::EntityKey::DimensionalSize(i),
114            DimImpl::Location(i) => m::EntityKey::DimensionalLocation(i),
115            DimImpl::AngularSize(i) => m::EntityKey::AngularSize(i),
116            DimImpl::AngularLocation(i) => m::EntityKey::AngularLocation(i),
117        }
118    }
119
120    /// The nominal value (reverse: a `DIMENSIONAL_CHARACTERISTIC_REPRESENTATION`
121    /// referencing this dimension → its `SHAPE_DIMENSION_REPRESENTATION`'s first
122    /// `MEASURE_REPRESENTATION_ITEM`).
123    ///
124    /// The number is raw: interpret its unit via [`kind`](Dimension::kind)
125    /// (`Size`/`Location` are lengths, `Angular*` are angles) and
126    /// [`Scene::units`], exactly as point coordinates are read. A representation
127    /// the walk cannot resolve (a complex one) is reported via
128    /// [`Scene::warnings`](crate::scene::Scene); a dimension with no value
129    /// representation simply yields `None`.
130    pub fn value(&self) -> Option<f64> {
131        let cx = self.cx;
132        let rg = cx.ref_graph();
133        for r in rg.referrers(self.key()) {
134            let m::EntityKey::DimensionalCharacteristicRepresentation(dcr_id) = r else {
135                continue;
136            };
137            let dcr = cx
138                .model
139                .dimensional_characteristic_representation_arena
140                .get(dcr_id.0);
141            let sdr_id = match &dcr.representation {
142                m::ShapeDimensionRepresentationRef::ShapeDimensionRepresentation(id) => id,
143                m::ShapeDimensionRepresentationRef::Complex(_) => {
144                    cx.warn("dimension value representation is a complex instance".to_owned());
145                    continue;
146                }
147            };
148            let sdr = cx.model.shape_dimension_representation_arena.get(sdr_id.0);
149            for item in &sdr.items {
150                if let m::RepresentationItemRef::MeasureRepresentationItem(mid) = item {
151                    return Some(
152                        cx.model
153                            .measure_representation_item_arena
154                            .get(mid.0)
155                            .value_component
156                            .value
157                            .as_f64(),
158                    );
159                }
160            }
161        }
162        None
163    }
164}
165
166/// The GD&T characteristic a [`Tolerance`] specifies (its control-frame symbol).
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum ToleranceKind {
169    /// `POSITION_TOLERANCE`.
170    Position,
171    /// `FLATNESS_TOLERANCE`.
172    Flatness,
173    /// `STRAIGHTNESS_TOLERANCE`.
174    Straightness,
175    /// `ROUNDNESS_TOLERANCE`.
176    Roundness,
177    /// `CYLINDRICITY_TOLERANCE`.
178    Cylindricity,
179    /// `LINE_PROFILE_TOLERANCE`.
180    LineProfile,
181    /// `SURFACE_PROFILE_TOLERANCE`.
182    SurfaceProfile,
183    /// `ANGULARITY_TOLERANCE`.
184    Angularity,
185    /// `PARALLELISM_TOLERANCE`.
186    Parallelism,
187    /// `PERPENDICULARITY_TOLERANCE`.
188    Perpendicularity,
189    /// `CONCENTRICITY_TOLERANCE`.
190    Concentricity,
191    /// `COAXIALITY_TOLERANCE`.
192    Coaxiality,
193    /// `SYMMETRY_TOLERANCE`.
194    Symmetry,
195    /// `CIRCULAR_RUNOUT_TOLERANCE`.
196    CircularRunout,
197    /// `TOTAL_RUNOUT_TOLERANCE`.
198    TotalRunout,
199    /// A geometric tolerance whose characteristic subtype was not recognized
200    /// (reported via [`Scene::warnings`](crate::scene::Scene)).
201    Other,
202}
203
204/// A geometric tolerance (`GEOMETRIC_TOLERANCE` and its subtypes) — its
205/// characteristic [`kind`](Tolerance::kind), name, and nominal
206/// [`magnitude`](Tolerance::magnitude).
207#[derive(Clone, Copy)]
208pub struct Tolerance<'m> {
209    cx: Ctx<'m>,
210    which: TolImpl,
211}
212
213// The 15 leaf tolerance subtypes each have a dedicated arena and an `EntityKey`
214// variant, and all carry the `name` + `magnitude` fields inherited from
215// `GEOMETRIC_TOLERANCE`. This table generates the `TolImpl` cursor enum plus the
216// per-variant `kind` / `key` / field access, so only the complex-instance path
217// below is hand-written.
218macro_rules! leaf_tolerances {
219    ($( $ent:ident, $id:ident, $arena:ident, $kind:ident );+ $(;)?) => {
220        #[derive(Clone, Copy)]
221        enum TolImpl {
222            $( $ent(m::$id), )+
223            Complex(m::ComplexUnitId),
224        }
225
226        fn push_dedicated_tolerances<'m>(cx: Ctx<'m>, out: &mut Vec<Tolerance<'m>>) {
227            $(
228                for i in 0..cx.model.$arena.items.len() {
229                    out.push(Tolerance { cx, which: TolImpl::$ent(m::$id(i)) });
230                }
231            )+
232        }
233
234        impl<'m> Tolerance<'m> {
235            /// The GD&T characteristic this tolerance specifies.
236            pub fn kind(&self) -> ToleranceKind {
237                match self.which {
238                    $( TolImpl::$ent(_) => ToleranceKind::$kind, )+
239                    TolImpl::Complex(cuid) => self.complex_kind(cuid),
240                }
241            }
242
243            /// This tolerance's global identity (a `Copy` key for maps / dedup).
244            pub fn key(&self) -> m::EntityKey {
245                match self.which {
246                    $( TolImpl::$ent(i) => m::EntityKey::$ent(i), )+
247                    TolImpl::Complex(cuid) => m::EntityKey::ComplexUnit(cuid),
248                }
249            }
250
251            /// `(name, magnitude)` for a dedicated-arena leaf tolerance.
252            fn dedicated_fields(&self) -> Option<(&'m str, Option<&'m m::LengthMeasureWithUnitRef>)> {
253                match self.which {
254                    $( TolImpl::$ent(i) => {
255                        let e = self.cx.model.$arena.get(i.0);
256                        Some((&e.name, e.magnitude.as_ref()))
257                    } )+
258                    TolImpl::Complex(_) => None,
259                }
260            }
261
262            /// The toleranced feature target for a dedicated-arena leaf tolerance.
263            fn dedicated_target(&self) -> Option<&'m m::GeometricToleranceTargetRef> {
264                match self.which {
265                    $( TolImpl::$ent(i) => {
266                        Some(&self.cx.model.$arena.get(i.0).toleranced_shape_aspect)
267                    } )+
268                    TolImpl::Complex(_) => None,
269                }
270            }
271        }
272    };
273}
274
275leaf_tolerances! {
276    PositionTolerance,        PositionToleranceId,        position_tolerance_arena,         Position;
277    FlatnessTolerance,        FlatnessToleranceId,        flatness_tolerance_arena,         Flatness;
278    StraightnessTolerance,    StraightnessToleranceId,    straightness_tolerance_arena,     Straightness;
279    RoundnessTolerance,       RoundnessToleranceId,       roundness_tolerance_arena,        Roundness;
280    CylindricityTolerance,    CylindricityToleranceId,    cylindricity_tolerance_arena,     Cylindricity;
281    LineProfileTolerance,     LineProfileToleranceId,     line_profile_tolerance_arena,     LineProfile;
282    SurfaceProfileTolerance,  SurfaceProfileToleranceId,  surface_profile_tolerance_arena,  SurfaceProfile;
283    AngularityTolerance,      AngularityToleranceId,      angularity_tolerance_arena,       Angularity;
284    ParallelismTolerance,     ParallelismToleranceId,     parallelism_tolerance_arena,      Parallelism;
285    PerpendicularityTolerance,PerpendicularityToleranceId,perpendicularity_tolerance_arena, Perpendicularity;
286    ConcentricityTolerance,   ConcentricityToleranceId,   concentricity_tolerance_arena,    Concentricity;
287    CoaxialityTolerance,      CoaxialityToleranceId,      coaxiality_tolerance_arena,       Coaxiality;
288    SymmetryTolerance,        SymmetryToleranceId,        symmetry_tolerance_arena,         Symmetry;
289    CircularRunoutTolerance,  CircularRunoutToleranceId,  circular_runout_tolerance_arena,  CircularRunout;
290    TotalRunoutTolerance,     TotalRunoutToleranceId,     total_runout_tolerance_arena,     TotalRunout;
291}
292
293impl Scene<'_> {
294    /// Every geometric tolerance in the model. Covers both standalone leaf
295    /// entities (in their dedicated arenas) and complex multi-supertype
296    /// instances (any `ComplexUnit` carrying a `GEOMETRIC_TOLERANCE` facet).
297    pub fn tolerances(&self) -> impl Iterator<Item = Tolerance<'_>> + '_ {
298        all_tolerances(self.ctx()).into_iter()
299    }
300}
301
302/// Every geometric tolerance in the model — standalone leaf entities plus complex
303/// multi-supertype instances. Shared by [`Scene::tolerances`] and per-part queries.
304fn all_tolerances(cx: Ctx<'_>) -> Vec<Tolerance<'_>> {
305    let mut out: Vec<Tolerance<'_>> = Vec::new();
306    push_dedicated_tolerances(cx, &mut out);
307    for i in 0..cx.model.complex_unit_arena.items.len() {
308        let cu = cx.model.complex_unit_arena.get(i);
309        if cu
310            .parts
311            .iter()
312            .any(|p| matches!(p, m::UnitPart::GeometricTolerance { .. }))
313        {
314            out.push(Tolerance {
315                cx,
316                which: TolImpl::Complex(m::ComplexUnitId(i)),
317            });
318        }
319    }
320    out
321}
322
323impl<'m> Tolerance<'m> {
324    /// The tolerance's name.
325    pub fn name(&self) -> &'m str {
326        self.dedicated_fields()
327            .or_else(|| self.complex_geometric_tolerance())
328            .map_or("", |(name, _)| name)
329    }
330
331    /// The nominal magnitude (the tolerance-zone size).
332    ///
333    /// The number is raw, in the model's length unit — interpret it via
334    /// [`Scene::units`], exactly as point coordinates and dimension values are
335    /// read. `None` when the tolerance carries no magnitude, or its magnitude is
336    /// a complex measure instance the walk cannot resolve (reported via
337    /// [`Scene::warnings`](crate::scene::Scene)).
338    pub fn magnitude(&self) -> Option<f64> {
339        let (_, mag) = self
340            .dedicated_fields()
341            .or_else(|| self.complex_geometric_tolerance())?;
342        mag.and_then(|r| length_measure_f64(self.cx, r))
343    }
344
345    /// The `GEOMETRIC_TOLERANCE` supertype facet of a complex tolerance, as
346    /// `(name, magnitude)`; `None` for a dedicated-arena tolerance.
347    fn complex_geometric_tolerance(
348        &self,
349    ) -> Option<(&'m str, Option<&'m m::LengthMeasureWithUnitRef>)> {
350        let TolImpl::Complex(cuid) = self.which else {
351            return None;
352        };
353        let cu = self.cx.model.complex_unit_arena.get(cuid.0);
354        cu.parts.iter().find_map(|p| match p {
355            m::UnitPart::GeometricTolerance {
356                name, magnitude, ..
357            } => Some((name.as_str(), magnitude.as_ref())),
358            _ => None,
359        })
360    }
361
362    /// The characteristic of a complex tolerance, read from its leaf-subtype
363    /// facet. Only the leaf markers that occur in the corpus exist as
364    /// `UnitPart` variants; a complex tolerance without a recognized one is
365    /// surfaced as `Other`.
366    fn complex_kind(&self, cuid: m::ComplexUnitId) -> ToleranceKind {
367        let cu = self.cx.model.complex_unit_arena.get(cuid.0);
368        for p in &cu.parts {
369            let kind = match p {
370                m::UnitPart::PositionTolerance => ToleranceKind::Position,
371                m::UnitPart::FlatnessTolerance => ToleranceKind::Flatness,
372                m::UnitPart::StraightnessTolerance => ToleranceKind::Straightness,
373                m::UnitPart::RoundnessTolerance => ToleranceKind::Roundness,
374                m::UnitPart::CylindricityTolerance => ToleranceKind::Cylindricity,
375                m::UnitPart::LineProfileTolerance => ToleranceKind::LineProfile,
376                m::UnitPart::SurfaceProfileTolerance => ToleranceKind::SurfaceProfile,
377                m::UnitPart::ParallelismTolerance => ToleranceKind::Parallelism,
378                m::UnitPart::PerpendicularityTolerance => ToleranceKind::Perpendicularity,
379                m::UnitPart::CircularRunoutTolerance => ToleranceKind::CircularRunout,
380                _ => continue,
381            };
382            return kind;
383        }
384        self.cx
385            .warn("geometric tolerance has no recognized characteristic subtype".to_owned());
386        ToleranceKind::Other
387    }
388}
389
390/// Resolve a `LENGTH_MEASURE_WITH_UNIT` reference to its scalar value; a complex
391/// measure instance is reported and yields `None`.
392fn length_measure_f64(cx: Ctx<'_>, r: &m::LengthMeasureWithUnitRef) -> Option<f64> {
393    match r {
394        m::LengthMeasureWithUnitRef::LengthMeasureWithUnit(id) => Some(
395            cx.model
396                .length_measure_with_unit_arena
397                .get(id.0)
398                .value_component
399                .value
400                .as_f64(),
401        ),
402        m::LengthMeasureWithUnitRef::Complex(_) => {
403            cx.warn("tolerance magnitude is a complex measure instance".to_owned());
404            None
405        }
406    }
407}
408
409/// A datum (`DATUM` / `COMMON_DATUM`) — the geometric reference a tolerance is
410/// measured against. Its identifying letter (A, B, C, …) is [`letter`](Datum::letter).
411#[derive(Clone, Copy)]
412pub struct Datum<'m> {
413    cx: Ctx<'m>,
414    which: DatumImpl,
415}
416
417#[derive(Clone, Copy)]
418enum DatumImpl {
419    Datum(m::DatumId),
420    CommonDatum(m::CommonDatumId),
421}
422
423impl Scene<'_> {
424    /// Every datum in the model — `DATUM`s and `COMMON_DATUM`s (the reference
425    /// features that carry an identifying letter). The physical `DATUM_FEATURE`
426    /// a datum is established from is a different entity, reached later.
427    pub fn datums(&self) -> impl Iterator<Item = Datum<'_>> + '_ {
428        all_datums(self.ctx()).into_iter()
429    }
430}
431
432/// Every datum (`DATUM` / `COMMON_DATUM`) in the model. Shared by
433/// [`Scene::datums`] and per-part queries.
434fn all_datums(cx: Ctx<'_>) -> Vec<Datum<'_>> {
435    let mut out: Vec<Datum<'_>> = Vec::new();
436    for i in 0..cx.model.datum_arena.items.len() {
437        out.push(Datum {
438            cx,
439            which: DatumImpl::Datum(m::DatumId(i)),
440        });
441    }
442    for i in 0..cx.model.common_datum_arena.items.len() {
443        out.push(Datum {
444            cx,
445            which: DatumImpl::CommonDatum(m::CommonDatumId(i)),
446        });
447    }
448    out
449}
450
451impl<'m> Datum<'m> {
452    /// The datum's identifying label — STEP `identification`. Usually a single
453    /// letter (A, B, C, …); a common datum may carry a compound label ("A-B").
454    pub fn letter(&self) -> &'m str {
455        match self.which {
456            DatumImpl::Datum(i) => &self.cx.model.datum_arena.get(i.0).identification,
457            DatumImpl::CommonDatum(i) => &self.cx.model.common_datum_arena.get(i.0).identification,
458        }
459    }
460
461    /// The datum's name.
462    pub fn name(&self) -> &'m str {
463        match self.which {
464            DatumImpl::Datum(i) => &self.cx.model.datum_arena.get(i.0).name,
465            DatumImpl::CommonDatum(i) => &self.cx.model.common_datum_arena.get(i.0).name,
466        }
467    }
468
469    /// This datum's global identity (a `Copy` key for maps / dedup).
470    pub fn key(&self) -> m::EntityKey {
471        match self.which {
472            DatumImpl::Datum(i) => m::EntityKey::Datum(i),
473            DatumImpl::CommonDatum(i) => m::EntityKey::CommonDatum(i),
474        }
475    }
476
477    /// The physical feature this datum is established from — the `DATUM_FEATURE`
478    /// linked to it by a `SHAPE_ASPECT_RELATIONSHIP`. Returned as a [`Feature`],
479    /// whose faces/edges are reachable via [`Feature::geometry`]. `None` when the
480    /// datum is defined only by datum targets, or carries no such link.
481    pub fn datum_feature(&self) -> Option<Feature<'m>> {
482        let cx = self.cx;
483        let rg = cx.ref_graph();
484        for r in rg.referrers(self.key()) {
485            let m::EntityKey::ShapeAspectRelationship(sid) = r else {
486                continue;
487            };
488            let sar = cx.model.shape_aspect_relationship_arena.get(sid.0);
489            // The datum is one end of the relationship; the datum feature is the
490            // other (checking both ends naturally selects it).
491            for aspect in [&sar.relating_shape_aspect, &sar.related_shape_aspect] {
492                if let m::ShapeAspectRef::DatumFeature(fid) = aspect {
493                    return Some(Feature {
494                        cx,
495                        which: FeatureImpl::DatumFeature(*fid),
496                    });
497                }
498            }
499        }
500        None
501    }
502}
503
504impl<'m> Tolerance<'m> {
505    /// The datum reference frame this tolerance is controlled against, in
506    /// precedence order (primary → secondary → tertiary).
507    ///
508    /// Empty for a form tolerance (flatness, straightness, …) that references no
509    /// datum. A common datum (`A-B`) is flattened to its constituent datums; a
510    /// reference the walk cannot resolve (a complex facet) is reported via
511    /// [`Scene::warnings`](crate::scene::Scene).
512    pub fn datums(&self) -> Vec<Datum<'m>> {
513        // `precedence` is authored per DATUM_REFERENCE but a DATUM_SYSTEM orders
514        // its compartments positionally; collect a sort key from whichever form
515        // applies, then order by it so the frame reads primary-first regardless
516        // of file order.
517        let mut keyed: Vec<(i64, Datum<'m>)> = Vec::new();
518        for r in self.datum_system() {
519            self.collect_system_ref(r, &mut keyed);
520        }
521        keyed.sort_by_key(|(k, _)| *k);
522        keyed.into_iter().map(|(_, d)| d).collect()
523    }
524
525    /// The tolerance's `datum_system` list: on the dedicated arena for the eight
526    /// leaf subtypes that carry one, on the `GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE`
527    /// facet for a complex tolerance, and empty for the datum-free subtypes.
528    fn datum_system(&self) -> &'m [m::DatumSystemOrReferenceRef] {
529        let model = self.cx.model;
530        match self.which {
531            TolImpl::AngularityTolerance(i) => {
532                &model.angularity_tolerance_arena.get(i.0).datum_system
533            }
534            TolImpl::CircularRunoutTolerance(i) => {
535                &model.circular_runout_tolerance_arena.get(i.0).datum_system
536            }
537            TolImpl::CoaxialityTolerance(i) => {
538                &model.coaxiality_tolerance_arena.get(i.0).datum_system
539            }
540            TolImpl::ConcentricityTolerance(i) => {
541                &model.concentricity_tolerance_arena.get(i.0).datum_system
542            }
543            TolImpl::ParallelismTolerance(i) => {
544                &model.parallelism_tolerance_arena.get(i.0).datum_system
545            }
546            TolImpl::PerpendicularityTolerance(i) => {
547                &model.perpendicularity_tolerance_arena.get(i.0).datum_system
548            }
549            TolImpl::SymmetryTolerance(i) => &model.symmetry_tolerance_arena.get(i.0).datum_system,
550            TolImpl::TotalRunoutTolerance(i) => {
551                &model.total_runout_tolerance_arena.get(i.0).datum_system
552            }
553            TolImpl::Complex(cuid) => model
554                .complex_unit_arena
555                .get(cuid.0)
556                .parts
557                .iter()
558                .find_map(|p| match p {
559                    m::UnitPart::GeometricToleranceWithDatumReference { datum_system } => {
560                        Some(datum_system.as_slice())
561                    }
562                    _ => None,
563                })
564                .unwrap_or(&[]),
565            _ => &[],
566        }
567    }
568
569    /// One `datum_system` element → its datum(s). A `DATUM_REFERENCE` carries an
570    /// explicit precedence; a `DATUM_SYSTEM`'s compartments are ordered, so their
571    /// index is the precedence.
572    fn collect_system_ref(
573        &self,
574        r: &m::DatumSystemOrReferenceRef,
575        out: &mut Vec<(i64, Datum<'m>)>,
576    ) {
577        match r {
578            m::DatumSystemOrReferenceRef::DatumReference(id) => {
579                let dr = self.cx.model.datum_reference_arena.get(id.0);
580                self.collect_datum_ref(dr.precedence, &dr.referenced_datum, out);
581            }
582            m::DatumSystemOrReferenceRef::DatumSystem(id) => {
583                let ds = self.cx.model.datum_system_arena.get(id.0);
584                for (idx, c) in ds.constituents.iter().enumerate() {
585                    let m::DatumReferenceCompartmentRef::DatumReferenceCompartment(cid) = c;
586                    let comp = self.cx.model.datum_reference_compartment_arena.get(cid.0);
587                    let key = i64::try_from(idx).unwrap_or(i64::MAX);
588                    self.collect_base(key, &comp.base, out, 0);
589                }
590            }
591            m::DatumSystemOrReferenceRef::Complex(_) => {
592                self.cx
593                    .warn("tolerance datum reference is a complex instance".to_owned());
594            }
595        }
596    }
597
598    /// A `DatumRef` (the target of a `DATUM_REFERENCE`) → a datum handle.
599    fn collect_datum_ref(&self, key: i64, r: &m::DatumRef, out: &mut Vec<(i64, Datum<'m>)>) {
600        match r {
601            m::DatumRef::Datum(id) => out.push((key, self.datum(DatumImpl::Datum(*id)))),
602            m::DatumRef::CommonDatum(id) => {
603                out.push((key, self.datum(DatumImpl::CommonDatum(*id))));
604            }
605            m::DatumRef::Complex(_) => {
606                self.cx
607                    .warn("tolerance references a complex datum instance".to_owned());
608            }
609        }
610    }
611
612    /// A compartment `base` → its datum(s). Resolves the `DATUM_REFERENCE_ELEMENT`
613    /// and common-datum-list indirections down to plain datums.
614    fn collect_base(
615        &self,
616        key: i64,
617        r: &m::DatumOrCommonDatumRef,
618        out: &mut Vec<(i64, Datum<'m>)>,
619        depth: u32,
620    ) {
621        if depth > 8 {
622            self.cx
623                .warn("datum reference nesting is too deep to resolve".to_owned());
624            return;
625        }
626        match r {
627            m::DatumOrCommonDatumRef::Datum(id) => {
628                out.push((key, self.datum(DatumImpl::Datum(*id))));
629            }
630            m::DatumOrCommonDatumRef::CommonDatum(id) => {
631                out.push((key, self.datum(DatumImpl::CommonDatum(*id))));
632            }
633            m::DatumOrCommonDatumRef::DatumReferenceElement(id) => {
634                let e = self.cx.model.datum_reference_element_arena.get(id.0);
635                self.collect_base(key, &e.base, out, depth + 1);
636            }
637            m::DatumOrCommonDatumRef::CommonDatumList(refs) => {
638                for er in refs {
639                    let m::DatumReferenceElementRef::DatumReferenceElement(eid) = er;
640                    let e = self.cx.model.datum_reference_element_arena.get(eid.0);
641                    self.collect_base(key, &e.base, out, depth + 1);
642                }
643            }
644            m::DatumOrCommonDatumRef::Complex(_) => {
645                self.cx
646                    .warn("tolerance references a complex datum instance".to_owned());
647            }
648        }
649    }
650
651    fn datum(&self, which: DatumImpl) -> Datum<'m> {
652        Datum { cx: self.cx, which }
653    }
654}
655
656/// A shape feature (`SHAPE_ASPECT` and its subtypes) that a dimension or
657/// tolerance applies to — its name, description, and identity.
658///
659/// Every entity here is a `SHAPE_ASPECT` subtype in the schema (that is why the
660/// same set forms both `applies_to` and `toleranced_shape_aspect` targets), so a
661/// `Feature` can be a plain aspect, a datum feature, a composite/derived aspect,
662/// or occasionally a datum / tolerance-zone construct. [`key`](Feature::key)
663/// reveals the exact kind when it matters.
664#[derive(Clone, Copy)]
665pub struct Feature<'m> {
666    cx: Ctx<'m>,
667    which: FeatureImpl,
668}
669
670// The 20 SHAPE_ASPECT-family entities all share `name` / `description` / a
671// dedicated arena / an `EntityKey` variant. This table generates the cursor enum,
672// the accessors, and the two reference-union converters — all covering the full
673// family, since a dimension / tolerance target may point at any of them.
674//
675// The family splits in two: `regions` are the SHAPE_ASPECT subtypes that denote a
676// portion of the part's shape (a feature) and are what `Scene::features` lists;
677// `aspects` are also SHAPE_ASPECTs in the schema but are datum callouts, datum
678// reference structures, tolerance zones, views, or dimensions — reached via
679// their own queries or the raw model. Corpus-checked: no dimension / tolerance
680// ever targets an `aspects` type, so this split drops no real feature.
681macro_rules! feature_family {
682    (
683        regions: { $( $rent:ident, $rid:ident, $rarena:ident );+ $(;)? }
684        aspects: { $( $aent:ident, $aid:ident, $aarena:ident );+ $(;)? }
685    ) => {
686        #[derive(Clone, Copy)]
687        enum FeatureImpl {
688            $( $rent(m::$rid), )+
689            $( $aent(m::$aid), )+
690        }
691
692        /// The concrete kind of a [`Feature`] — which `SHAPE_ASPECT`-family
693        /// entity backs it, without matching on [`Feature::key`]'s `EntityKey`.
694        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
695        pub enum FeatureKind {
696            $( $rent, )+
697            $( $aent, )+
698        }
699
700        impl<'m> Feature<'m> {
701            /// The concrete kind backing this feature.
702            pub fn kind(&self) -> FeatureKind {
703                match self.which {
704                    $( FeatureImpl::$rent(_) => FeatureKind::$rent, )+
705                    $( FeatureImpl::$aent(_) => FeatureKind::$aent, )+
706                }
707            }
708            /// The feature's name.
709            pub fn name(&self) -> &'m str {
710                match self.which {
711                    $( FeatureImpl::$rent(i) => &self.cx.model.$rarena.get(i.0).name, )+
712                    $( FeatureImpl::$aent(i) => &self.cx.model.$aarena.get(i.0).name, )+
713                }
714            }
715
716            /// The feature's description, if any.
717            pub fn description(&self) -> Option<&'m str> {
718                match self.which {
719                    $( FeatureImpl::$rent(i) => {
720                        self.cx.model.$rarena.get(i.0).description.as_deref()
721                    } )+
722                    $( FeatureImpl::$aent(i) => {
723                        self.cx.model.$aarena.get(i.0).description.as_deref()
724                    } )+
725                }
726            }
727
728            /// This feature's global identity (a `Copy` key for maps / dedup).
729            pub fn key(&self) -> m::EntityKey {
730                match self.which {
731                    $( FeatureImpl::$rent(i) => m::EntityKey::$rent(i), )+
732                    $( FeatureImpl::$aent(i) => m::EntityKey::$aent(i), )+
733                }
734            }
735
736            /// The `of_shape` product-definition-shape this feature belongs to.
737            fn of_shape(&self) -> &'m m::ProductDefinitionShapeRef {
738                match self.which {
739                    $( FeatureImpl::$rent(i) => &self.cx.model.$rarena.get(i.0).of_shape, )+
740                    $( FeatureImpl::$aent(i) => &self.cx.model.$aarena.get(i.0).of_shape, )+
741                }
742            }
743        }
744
745        /// A `ShapeAspectRef` (a dimension's target) → a feature cursor. Every
746        /// non-`Complex` variant is a shape-aspect-family member.
747        fn feature_impl_from_shape_aspect_ref(r: &m::ShapeAspectRef) -> Option<FeatureImpl> {
748            match r {
749                $( m::ShapeAspectRef::$rent(i) => Some(FeatureImpl::$rent(*i)), )+
750                $( m::ShapeAspectRef::$aent(i) => Some(FeatureImpl::$aent(*i)), )+
751                m::ShapeAspectRef::Complex(_) => None,
752            }
753        }
754
755        /// A `GeometricToleranceTargetRef` (a tolerance's target) → a feature
756        /// cursor. The shape-aspect-family arms resolve; the dimension / product /
757        /// complex arms are not features and yield `None`.
758        fn feature_impl_from_tolerance_target(
759            r: &m::GeometricToleranceTargetRef,
760        ) -> Option<FeatureImpl> {
761            match r {
762                $( m::GeometricToleranceTargetRef::$rent(i) => Some(FeatureImpl::$rent(*i)), )+
763                $( m::GeometricToleranceTargetRef::$aent(i) => Some(FeatureImpl::$aent(*i)), )+
764                _ => None,
765            }
766        }
767
768        /// Every shape-region feature (the `regions` group) in the model — what
769        /// [`Scene::features`] enumerates.
770        fn push_all_features<'m>(cx: Ctx<'m>, out: &mut Vec<Feature<'m>>) {
771            $(
772                for i in 0..cx.model.$rarena.items.len() {
773                    out.push(Feature { cx, which: FeatureImpl::$rent(m::$rid(i)) });
774                }
775            )+
776        }
777    };
778}
779
780feature_family! {
781    // regions — SHAPE_ASPECT subtypes that denote a portion of the shape.
782    regions: {
783        ShapeAspect,                     ShapeAspectId,                     shape_aspect_arena;
784        DatumFeature,                    DatumFeatureId,                    datum_feature_arena;
785        AllAroundShapeAspect,            AllAroundShapeAspectId,            all_around_shape_aspect_arena;
786        CentreOfSymmetry,                CentreOfSymmetryId,                centre_of_symmetry_arena;
787        CompositeShapeAspect,            CompositeShapeAspectId,            composite_shape_aspect_arena;
788        CompositeGroupShapeAspect,       CompositeGroupShapeAspectId,       composite_group_shape_aspect_arena;
789        ContinuousShapeAspect,           ContinuousShapeAspectId,           continuous_shape_aspect_arena;
790        DerivedShapeAspect,              DerivedShapeAspectId,              derived_shape_aspect_arena;
791    }
792    // aspects — SHAPE_ASPECTs in the schema, but not features here (datum
793    // callouts / reference structures, tolerance zones, views, dimensions).
794    aspects: {
795        Datum,                           DatumId,                           datum_arena;
796        CommonDatum,                     CommonDatumId,                     common_datum_arena;
797        DatumTarget,                     DatumTargetId,                     datum_target_arena;
798        DatumReferenceCompartment,       DatumReferenceCompartmentId,       datum_reference_compartment_arena;
799        DatumReferenceElement,           DatumReferenceElementId,           datum_reference_element_arena;
800        DatumSystem,                     DatumSystemId,                     datum_system_arena;
801        DefaultModelGeometricView,       DefaultModelGeometricViewId,       default_model_geometric_view_arena;
802        GeneralDatumReference,           GeneralDatumReferenceId,           general_datum_reference_arena;
803        PlacedDatumTargetFeature,        PlacedDatumTargetFeatureId,        placed_datum_target_feature_arena;
804        ToleranceZone,                   ToleranceZoneId,                   tolerance_zone_arena;
805        ToleranceZoneWithDatum,          ToleranceZoneWithDatumId,          tolerance_zone_with_datum_arena;
806        DimensionalSizeWithDatumFeature, DimensionalSizeWithDatumFeatureId, dimensional_size_with_datum_feature_arena;
807    }
808}
809
810impl Scene<'_> {
811    /// Every geometric feature in the model — the `SHAPE_ASPECT` subtypes that
812    /// denote a region of the part's shape (plain aspects, datum features,
813    /// composite / derived / all-around / centre-of-symmetry / continuous
814    /// aspects). Datum callouts, datum-system references, tolerance zones, and
815    /// views are also `SHAPE_ASPECT`s in the schema but are not features here —
816    /// datums are reached via [`Scene::datums`], the rest via the raw model.
817    pub fn features(&self) -> impl Iterator<Item = Feature<'_>> + '_ {
818        let cx = self.ctx();
819        let mut out: Vec<Feature<'_>> = Vec::new();
820        push_all_features(cx, &mut out);
821        out.into_iter()
822    }
823}
824
825impl<'m> Dimension<'m> {
826    /// The shape feature(s) this dimension applies to. A size (`DIMENSIONAL_SIZE`
827    /// / `ANGULAR_SIZE`) yields the one feature it measures; a location
828    /// (`DIMENSIONAL_LOCATION` / `ANGULAR_LOCATION`) yields the two it spans, in
829    /// `[relating, related]` order.
830    pub fn features(&self) -> Vec<Feature<'m>> {
831        let model = self.cx.model;
832        let refs: Vec<&m::ShapeAspectRef> = match self.which {
833            DimImpl::Size(i) => vec![&model.dimensional_size_arena.get(i.0).applies_to],
834            DimImpl::AngularSize(i) => vec![&model.angular_size_arena.get(i.0).applies_to],
835            DimImpl::Location(i) => {
836                let e = model.dimensional_location_arena.get(i.0);
837                vec![&e.relating_shape_aspect, &e.related_shape_aspect]
838            }
839            DimImpl::AngularLocation(i) => {
840                let e = model.angular_location_arena.get(i.0);
841                vec![&e.relating_shape_aspect, &e.related_shape_aspect]
842            }
843        };
844        refs.into_iter()
845            .filter_map(|r| self.feature_from_sa_ref(r))
846            .collect()
847    }
848
849    fn feature_from_sa_ref(&self, r: &m::ShapeAspectRef) -> Option<Feature<'m>> {
850        if matches!(r, m::ShapeAspectRef::Complex(_)) {
851            self.cx
852                .warn("dimension feature is a complex instance".to_owned());
853            return None;
854        }
855        feature_impl_from_shape_aspect_ref(r).map(|which| Feature { cx: self.cx, which })
856    }
857}
858
859impl<'m> Tolerance<'m> {
860    /// The shape feature this tolerance applies to (its `toleranced_shape_aspect`).
861    ///
862    /// `None` when the target is not a named shape aspect — a tolerance placed on
863    /// a dimension or on the whole part (`PRODUCT_DEFINITION_SHAPE`) — or when the
864    /// target is a complex instance the walk cannot resolve (reported via
865    /// [`Scene::warnings`](crate::scene::Scene)).
866    pub fn feature(&self) -> Option<Feature<'m>> {
867        let target = self.tolerance_target()?;
868        if matches!(target, m::GeometricToleranceTargetRef::Complex(_)) {
869            self.cx
870                .warn("tolerance target is a complex instance".to_owned());
871            return None;
872        }
873        feature_impl_from_tolerance_target(target).map(|which| Feature { cx: self.cx, which })
874    }
875
876    /// The tolerance's `toleranced_shape_aspect`: from the dedicated arena for a
877    /// leaf tolerance, or the `GEOMETRIC_TOLERANCE` facet for a complex one.
878    fn tolerance_target(&self) -> Option<&'m m::GeometricToleranceTargetRef> {
879        if let Some(t) = self.dedicated_target() {
880            return Some(t);
881        }
882        let TolImpl::Complex(cuid) = self.which else {
883            return None;
884        };
885        self.cx
886            .model
887            .complex_unit_arena
888            .get(cuid.0)
889            .parts
890            .iter()
891            .find_map(|p| match p {
892                m::UnitPart::GeometricTolerance {
893                    toleranced_shape_aspect,
894                    ..
895                } => Some(toleranced_shape_aspect),
896                _ => None,
897            })
898    }
899}
900
901/// A concrete geometry item a [`Feature`] designates (via
902/// `GEOMETRIC_ITEM_SPECIFIC_USAGE`). The common cases resolve to a typed handle;
903/// anything else is surfaced as `Other` carrying its key rather than dropped.
904pub enum FeatureGeometry<'m> {
905    /// An `ADVANCED_FACE`.
906    Face(Face<'m>),
907    /// An `EDGE_CURVE`.
908    Edge(Edge<'m>),
909    /// A `CARTESIAN_POINT`.
910    Point(Point<'m>),
911    /// A `MANIFOLD_SOLID_BREP`.
912    Solid(Solid<'m>),
913    /// A curve (`TRIMMED_CURVE`, `COMPOSITE_CURVE`, `CIRCLE`, …); dispatch its
914    /// exact kind via [`Curve::kind`](crate::scene::geometry::Curve::kind).
915    Curve(Curve<'m>),
916    /// Any other identified item (a tessellated face, a geometric set, …), by key.
917    Other(m::EntityKey),
918}
919
920impl<'m> Feature<'m> {
921    /// The concrete geometry this feature designates — the faces / edges / points
922    /// / solids linked to it by `GEOMETRIC_ITEM_SPECIFIC_USAGE` (GISU).
923    ///
924    /// A feature may designate several items (one per GISU). Items outside the
925    /// typed set (curves, tessellated faces, …) come back as
926    /// [`FeatureGeometry::Other`] with their key. Only GISUs whose `definition`
927    /// points directly at this feature are followed; indirect links through a
928    /// shape-aspect relationship are not yet resolved.
929    pub fn geometry(&self) -> Vec<FeatureGeometry<'m>> {
930        let cx = self.cx;
931        let rg = cx.ref_graph();
932        let mut out = Vec::new();
933        for r in rg.referrers(self.key()) {
934            let m::EntityKey::GeometricItemSpecificUsage(gid) = r else {
935                continue;
936            };
937            let item = &cx
938                .model
939                .geometric_item_specific_usage_arena
940                .get(gid.0)
941                .identified_item;
942            out.push(match item {
943                m::GeometricModelItemRef::AdvancedFace(id) => {
944                    FeatureGeometry::Face(Face::from_advanced_face(cx, *id))
945                }
946                m::GeometricModelItemRef::EdgeCurve(id) => {
947                    FeatureGeometry::Edge(Edge::from_id(cx, *id))
948                }
949                m::GeometricModelItemRef::CartesianPoint(id) => {
950                    FeatureGeometry::Point(Point::from_id(cx, *id))
951                }
952                m::GeometricModelItemRef::ManifoldSolidBrep(id) => {
953                    FeatureGeometry::Solid(Solid::from_id(cx, *id))
954                }
955                other => match Curve::from_model_item(cx, other) {
956                    Some(c) => FeatureGeometry::Curve(c),
957                    None => FeatureGeometry::Other(other.entity_key()),
958                },
959            });
960        }
961        out
962    }
963}
964
965// ---------------------------------------------------------------------------
966// Per-part PMI: which product definition a feature / datum / tolerance belongs
967// to, and the filtered per-part queries backing `ProductDef`.
968// ---------------------------------------------------------------------------
969
970/// The product definition a `PRODUCT_DEFINITION_SHAPE` defines, as an
971/// [`EntityKey`](m::EntityKey), if its `definition` is a `PRODUCT_DEFINITION` or
972/// its `PRODUCT_DEFINITION_WITH_ASSOCIATED_DOCUMENTS` subtype (both are surfaced
973/// as a [`ProductDef`](crate::scene::product::ProductDef)); `None` otherwise.
974fn product_def_of_pds(cx: Ctx<'_>, id: m::ProductDefinitionShapeId) -> Option<m::EntityKey> {
975    match cx.model.product_definition_shape_arena.get(id.0).definition {
976        m::CharacterizedDefinitionRef::ProductDefinition(pd) => {
977            Some(m::EntityKey::ProductDefinition(pd))
978        }
979        m::CharacterizedDefinitionRef::ProductDefinitionWithAssociatedDocuments(w) => {
980            Some(m::EntityKey::ProductDefinitionWithAssociatedDocuments(w))
981        }
982        _ => None,
983    }
984}
985
986/// The product definition an `of_shape` reference resolves to (its PDS's
987/// `definition`); `None` for a complex PDS or a non-`PRODUCT_DEFINITION` owner.
988fn product_def_of_shape(cx: Ctx<'_>, r: &m::ProductDefinitionShapeRef) -> Option<m::EntityKey> {
989    match r {
990        m::ProductDefinitionShapeRef::ProductDefinitionShape(id) => product_def_of_pds(cx, *id),
991        m::ProductDefinitionShapeRef::Complex(_) => None,
992    }
993}
994
995impl Feature<'_> {
996    /// The product definition this feature belongs to (`of_shape` → PDS →
997    /// definition), as an [`EntityKey`](m::EntityKey) — a `PRODUCT_DEFINITION` or
998    /// its `..._WITH_ASSOCIATED_DOCUMENTS` subtype.
999    fn part_def_id(&self) -> Option<m::EntityKey> {
1000        product_def_of_shape(self.cx, self.of_shape())
1001    }
1002}
1003
1004impl Datum<'_> {
1005    /// The product definition this datum belongs to (`of_shape` → PDS → part).
1006    fn part_def_id(&self) -> Option<m::EntityKey> {
1007        let of_shape = match self.which {
1008            DatumImpl::Datum(i) => &self.cx.model.datum_arena.get(i.0).of_shape,
1009            DatumImpl::CommonDatum(i) => &self.cx.model.common_datum_arena.get(i.0).of_shape,
1010        };
1011        product_def_of_shape(self.cx, of_shape)
1012    }
1013}
1014
1015impl Tolerance<'_> {
1016    /// The product definition this tolerance belongs to: its target may be a
1017    /// shape-aspect feature (→ that feature's part) or the whole part's
1018    /// `PRODUCT_DEFINITION_SHAPE` directly (an all-over tolerance).
1019    fn part_def_id(&self) -> Option<m::EntityKey> {
1020        let target = self.tolerance_target()?;
1021        if let m::GeometricToleranceTargetRef::ProductDefinitionShape(id) = target {
1022            return product_def_of_pds(self.cx, *id);
1023        }
1024        let which = feature_impl_from_tolerance_target(target)?;
1025        Feature { cx: self.cx, which }.part_def_id()
1026    }
1027}
1028
1029/// The shape-region features (narrow set) belonging to `part`.
1030pub(crate) fn features_of(cx: Ctx<'_>, part: m::EntityKey) -> Vec<Feature<'_>> {
1031    let mut all = Vec::new();
1032    push_all_features(cx, &mut all);
1033    all.retain(|f| f.part_def_id() == Some(part));
1034    all
1035}
1036
1037/// The datums belonging to `part`.
1038pub(crate) fn datums_of(cx: Ctx<'_>, part: m::EntityKey) -> Vec<Datum<'_>> {
1039    let mut all = all_datums(cx);
1040    all.retain(|d| d.part_def_id() == Some(part));
1041    all
1042}
1043
1044/// The dimensions belonging to `part` — those whose targeted feature is on it.
1045pub(crate) fn dimensions_of(cx: Ctx<'_>, part: m::EntityKey) -> Vec<Dimension<'_>> {
1046    let mut all = all_dimensions(cx);
1047    all.retain(|d| d.features().iter().any(|f| f.part_def_id() == Some(part)));
1048    all
1049}
1050
1051/// The tolerances belonging to `part` — those whose target (feature or whole
1052/// part) is on it.
1053pub(crate) fn tolerances_of(cx: Ctx<'_>, part: m::EntityKey) -> Vec<Tolerance<'_>> {
1054    let mut all = all_tolerances(cx);
1055    all.retain(|t| t.part_def_id() == Some(part));
1056    all
1057}