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
478impl<'m> Tolerance<'m> {
479    /// The datum reference frame this tolerance is controlled against, in
480    /// precedence order (primary → secondary → tertiary).
481    ///
482    /// Empty for a form tolerance (flatness, straightness, …) that references no
483    /// datum. A common datum (`A-B`) is flattened to its constituent datums; a
484    /// reference the walk cannot resolve (a complex facet) is reported via
485    /// [`Scene::warnings`](crate::scene::Scene).
486    pub fn datums(&self) -> Vec<Datum<'m>> {
487        // `precedence` is authored per DATUM_REFERENCE but a DATUM_SYSTEM orders
488        // its compartments positionally; collect a sort key from whichever form
489        // applies, then order by it so the frame reads primary-first regardless
490        // of file order.
491        let mut keyed: Vec<(i64, Datum<'m>)> = Vec::new();
492        for r in self.datum_system() {
493            self.collect_system_ref(r, &mut keyed);
494        }
495        keyed.sort_by_key(|(k, _)| *k);
496        keyed.into_iter().map(|(_, d)| d).collect()
497    }
498
499    /// The tolerance's `datum_system` list: on the dedicated arena for the eight
500    /// leaf subtypes that carry one, on the `GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE`
501    /// facet for a complex tolerance, and empty for the datum-free subtypes.
502    fn datum_system(&self) -> &'m [m::DatumSystemOrReferenceRef] {
503        let model = self.cx.model;
504        match self.which {
505            TolImpl::AngularityTolerance(i) => {
506                &model.angularity_tolerance_arena.get(i.0).datum_system
507            }
508            TolImpl::CircularRunoutTolerance(i) => {
509                &model.circular_runout_tolerance_arena.get(i.0).datum_system
510            }
511            TolImpl::CoaxialityTolerance(i) => {
512                &model.coaxiality_tolerance_arena.get(i.0).datum_system
513            }
514            TolImpl::ConcentricityTolerance(i) => {
515                &model.concentricity_tolerance_arena.get(i.0).datum_system
516            }
517            TolImpl::ParallelismTolerance(i) => {
518                &model.parallelism_tolerance_arena.get(i.0).datum_system
519            }
520            TolImpl::PerpendicularityTolerance(i) => {
521                &model.perpendicularity_tolerance_arena.get(i.0).datum_system
522            }
523            TolImpl::SymmetryTolerance(i) => &model.symmetry_tolerance_arena.get(i.0).datum_system,
524            TolImpl::TotalRunoutTolerance(i) => {
525                &model.total_runout_tolerance_arena.get(i.0).datum_system
526            }
527            TolImpl::Complex(cuid) => model
528                .complex_unit_arena
529                .get(cuid.0)
530                .parts
531                .iter()
532                .find_map(|p| match p {
533                    m::UnitPart::GeometricToleranceWithDatumReference { datum_system } => {
534                        Some(datum_system.as_slice())
535                    }
536                    _ => None,
537                })
538                .unwrap_or(&[]),
539            _ => &[],
540        }
541    }
542
543    /// One `datum_system` element → its datum(s). A `DATUM_REFERENCE` carries an
544    /// explicit precedence; a `DATUM_SYSTEM`'s compartments are ordered, so their
545    /// index is the precedence.
546    fn collect_system_ref(
547        &self,
548        r: &m::DatumSystemOrReferenceRef,
549        out: &mut Vec<(i64, Datum<'m>)>,
550    ) {
551        match r {
552            m::DatumSystemOrReferenceRef::DatumReference(id) => {
553                let dr = self.cx.model.datum_reference_arena.get(id.0);
554                self.collect_datum_ref(dr.precedence, &dr.referenced_datum, out);
555            }
556            m::DatumSystemOrReferenceRef::DatumSystem(id) => {
557                let ds = self.cx.model.datum_system_arena.get(id.0);
558                for (idx, c) in ds.constituents.iter().enumerate() {
559                    let m::DatumReferenceCompartmentRef::DatumReferenceCompartment(cid) = c;
560                    let comp = self.cx.model.datum_reference_compartment_arena.get(cid.0);
561                    let key = i64::try_from(idx).unwrap_or(i64::MAX);
562                    self.collect_base(key, &comp.base, out, 0);
563                }
564            }
565            m::DatumSystemOrReferenceRef::Complex(_) => {
566                self.cx
567                    .warn("tolerance datum reference is a complex instance".to_owned());
568            }
569        }
570    }
571
572    /// A `DatumRef` (the target of a `DATUM_REFERENCE`) → a datum handle.
573    fn collect_datum_ref(&self, key: i64, r: &m::DatumRef, out: &mut Vec<(i64, Datum<'m>)>) {
574        match r {
575            m::DatumRef::Datum(id) => out.push((key, self.datum(DatumImpl::Datum(*id)))),
576            m::DatumRef::CommonDatum(id) => {
577                out.push((key, self.datum(DatumImpl::CommonDatum(*id))));
578            }
579            m::DatumRef::Complex(_) => {
580                self.cx
581                    .warn("tolerance references a complex datum instance".to_owned());
582            }
583        }
584    }
585
586    /// A compartment `base` → its datum(s). Resolves the `DATUM_REFERENCE_ELEMENT`
587    /// and common-datum-list indirections down to plain datums.
588    fn collect_base(
589        &self,
590        key: i64,
591        r: &m::DatumOrCommonDatumRef,
592        out: &mut Vec<(i64, Datum<'m>)>,
593        depth: u32,
594    ) {
595        if depth > 8 {
596            self.cx
597                .warn("datum reference nesting is too deep to resolve".to_owned());
598            return;
599        }
600        match r {
601            m::DatumOrCommonDatumRef::Datum(id) => {
602                out.push((key, self.datum(DatumImpl::Datum(*id))));
603            }
604            m::DatumOrCommonDatumRef::CommonDatum(id) => {
605                out.push((key, self.datum(DatumImpl::CommonDatum(*id))));
606            }
607            m::DatumOrCommonDatumRef::DatumReferenceElement(id) => {
608                let e = self.cx.model.datum_reference_element_arena.get(id.0);
609                self.collect_base(key, &e.base, out, depth + 1);
610            }
611            m::DatumOrCommonDatumRef::CommonDatumList(refs) => {
612                for er in refs {
613                    let m::DatumReferenceElementRef::DatumReferenceElement(eid) = er;
614                    let e = self.cx.model.datum_reference_element_arena.get(eid.0);
615                    self.collect_base(key, &e.base, out, depth + 1);
616                }
617            }
618            m::DatumOrCommonDatumRef::Complex(_) => {
619                self.cx
620                    .warn("tolerance references a complex datum instance".to_owned());
621            }
622        }
623    }
624
625    fn datum(&self, which: DatumImpl) -> Datum<'m> {
626        Datum { cx: self.cx, which }
627    }
628}
629
630/// A shape feature (`SHAPE_ASPECT` and its subtypes) that a dimension or
631/// tolerance applies to — its name, description, and identity.
632///
633/// Every entity here is a `SHAPE_ASPECT` subtype in the schema (that is why the
634/// same set forms both `applies_to` and `toleranced_shape_aspect` targets), so a
635/// `Feature` can be a plain aspect, a datum feature, a composite/derived aspect,
636/// or occasionally a datum / tolerance-zone construct. [`key`](Feature::key)
637/// reveals the exact kind when it matters.
638#[derive(Clone, Copy)]
639pub struct Feature<'m> {
640    cx: Ctx<'m>,
641    which: FeatureImpl,
642}
643
644// The 20 SHAPE_ASPECT-family entities all share `name` / `description` / a
645// dedicated arena / an `EntityKey` variant. This table generates the cursor enum,
646// the accessors, and the two reference-union converters — all covering the full
647// family, since a dimension / tolerance target may point at any of them.
648//
649// The family splits in two: `regions` are the SHAPE_ASPECT subtypes that denote a
650// portion of the part's shape (a feature) and are what `Scene::features` lists;
651// `aspects` are also SHAPE_ASPECTs in the schema but are datum callouts, datum
652// reference structures, tolerance zones, views, or dimensions — reached via
653// their own queries or the raw model. Corpus-checked: no dimension / tolerance
654// ever targets an `aspects` type, so this split drops no real feature.
655macro_rules! feature_family {
656    (
657        regions: { $( $rent:ident, $rid:ident, $rarena:ident );+ $(;)? }
658        aspects: { $( $aent:ident, $aid:ident, $aarena:ident );+ $(;)? }
659    ) => {
660        #[derive(Clone, Copy)]
661        enum FeatureImpl {
662            $( $rent(m::$rid), )+
663            $( $aent(m::$aid), )+
664        }
665
666        /// The concrete kind of a [`Feature`] — which `SHAPE_ASPECT`-family
667        /// entity backs it, without matching on [`Feature::key`]'s `EntityKey`.
668        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
669        pub enum FeatureKind {
670            $( $rent, )+
671            $( $aent, )+
672        }
673
674        impl<'m> Feature<'m> {
675            /// The concrete kind backing this feature.
676            pub fn kind(&self) -> FeatureKind {
677                match self.which {
678                    $( FeatureImpl::$rent(_) => FeatureKind::$rent, )+
679                    $( FeatureImpl::$aent(_) => FeatureKind::$aent, )+
680                }
681            }
682            /// The feature's name.
683            pub fn name(&self) -> &'m str {
684                match self.which {
685                    $( FeatureImpl::$rent(i) => &self.cx.model.$rarena.get(i.0).name, )+
686                    $( FeatureImpl::$aent(i) => &self.cx.model.$aarena.get(i.0).name, )+
687                }
688            }
689
690            /// The feature's description, if any.
691            pub fn description(&self) -> Option<&'m str> {
692                match self.which {
693                    $( FeatureImpl::$rent(i) => {
694                        self.cx.model.$rarena.get(i.0).description.as_deref()
695                    } )+
696                    $( FeatureImpl::$aent(i) => {
697                        self.cx.model.$aarena.get(i.0).description.as_deref()
698                    } )+
699                }
700            }
701
702            /// This feature's global identity (a `Copy` key for maps / dedup).
703            pub fn key(&self) -> m::EntityKey {
704                match self.which {
705                    $( FeatureImpl::$rent(i) => m::EntityKey::$rent(i), )+
706                    $( FeatureImpl::$aent(i) => m::EntityKey::$aent(i), )+
707                }
708            }
709
710            /// The `of_shape` product-definition-shape this feature belongs to.
711            fn of_shape(&self) -> &'m m::ProductDefinitionShapeRef {
712                match self.which {
713                    $( FeatureImpl::$rent(i) => &self.cx.model.$rarena.get(i.0).of_shape, )+
714                    $( FeatureImpl::$aent(i) => &self.cx.model.$aarena.get(i.0).of_shape, )+
715                }
716            }
717        }
718
719        /// A `ShapeAspectRef` (a dimension's target) → a feature cursor. Every
720        /// non-`Complex` variant is a shape-aspect-family member.
721        fn feature_impl_from_shape_aspect_ref(r: &m::ShapeAspectRef) -> Option<FeatureImpl> {
722            match r {
723                $( m::ShapeAspectRef::$rent(i) => Some(FeatureImpl::$rent(*i)), )+
724                $( m::ShapeAspectRef::$aent(i) => Some(FeatureImpl::$aent(*i)), )+
725                m::ShapeAspectRef::Complex(_) => None,
726            }
727        }
728
729        /// A `GeometricToleranceTargetRef` (a tolerance's target) → a feature
730        /// cursor. The shape-aspect-family arms resolve; the dimension / product /
731        /// complex arms are not features and yield `None`.
732        fn feature_impl_from_tolerance_target(
733            r: &m::GeometricToleranceTargetRef,
734        ) -> Option<FeatureImpl> {
735            match r {
736                $( m::GeometricToleranceTargetRef::$rent(i) => Some(FeatureImpl::$rent(*i)), )+
737                $( m::GeometricToleranceTargetRef::$aent(i) => Some(FeatureImpl::$aent(*i)), )+
738                _ => None,
739            }
740        }
741
742        /// Every shape-region feature (the `regions` group) in the model — what
743        /// [`Scene::features`] enumerates.
744        fn push_all_features<'m>(cx: Ctx<'m>, out: &mut Vec<Feature<'m>>) {
745            $(
746                for i in 0..cx.model.$rarena.items.len() {
747                    out.push(Feature { cx, which: FeatureImpl::$rent(m::$rid(i)) });
748                }
749            )+
750        }
751    };
752}
753
754feature_family! {
755    // regions — SHAPE_ASPECT subtypes that denote a portion of the shape.
756    regions: {
757        ShapeAspect,                     ShapeAspectId,                     shape_aspect_arena;
758        DatumFeature,                    DatumFeatureId,                    datum_feature_arena;
759        AllAroundShapeAspect,            AllAroundShapeAspectId,            all_around_shape_aspect_arena;
760        CentreOfSymmetry,                CentreOfSymmetryId,                centre_of_symmetry_arena;
761        CompositeShapeAspect,            CompositeShapeAspectId,            composite_shape_aspect_arena;
762        CompositeGroupShapeAspect,       CompositeGroupShapeAspectId,       composite_group_shape_aspect_arena;
763        ContinuousShapeAspect,           ContinuousShapeAspectId,           continuous_shape_aspect_arena;
764        DerivedShapeAspect,              DerivedShapeAspectId,              derived_shape_aspect_arena;
765    }
766    // aspects — SHAPE_ASPECTs in the schema, but not features here (datum
767    // callouts / reference structures, tolerance zones, views, dimensions).
768    aspects: {
769        Datum,                           DatumId,                           datum_arena;
770        CommonDatum,                     CommonDatumId,                     common_datum_arena;
771        DatumTarget,                     DatumTargetId,                     datum_target_arena;
772        DatumReferenceCompartment,       DatumReferenceCompartmentId,       datum_reference_compartment_arena;
773        DatumReferenceElement,           DatumReferenceElementId,           datum_reference_element_arena;
774        DatumSystem,                     DatumSystemId,                     datum_system_arena;
775        DefaultModelGeometricView,       DefaultModelGeometricViewId,       default_model_geometric_view_arena;
776        GeneralDatumReference,           GeneralDatumReferenceId,           general_datum_reference_arena;
777        PlacedDatumTargetFeature,        PlacedDatumTargetFeatureId,        placed_datum_target_feature_arena;
778        ToleranceZone,                   ToleranceZoneId,                   tolerance_zone_arena;
779        ToleranceZoneWithDatum,          ToleranceZoneWithDatumId,          tolerance_zone_with_datum_arena;
780        DimensionalSizeWithDatumFeature, DimensionalSizeWithDatumFeatureId, dimensional_size_with_datum_feature_arena;
781    }
782}
783
784impl Scene<'_> {
785    /// Every geometric feature in the model — the `SHAPE_ASPECT` subtypes that
786    /// denote a region of the part's shape (plain aspects, datum features,
787    /// composite / derived / all-around / centre-of-symmetry / continuous
788    /// aspects). Datum callouts, datum-system references, tolerance zones, and
789    /// views are also `SHAPE_ASPECT`s in the schema but are not features here —
790    /// datums are reached via [`Scene::datums`], the rest via the raw model.
791    pub fn features(&self) -> impl Iterator<Item = Feature<'_>> + '_ {
792        let cx = self.ctx();
793        let mut out: Vec<Feature<'_>> = Vec::new();
794        push_all_features(cx, &mut out);
795        out.into_iter()
796    }
797}
798
799impl<'m> Dimension<'m> {
800    /// The shape feature(s) this dimension applies to. A size (`DIMENSIONAL_SIZE`
801    /// / `ANGULAR_SIZE`) yields the one feature it measures; a location
802    /// (`DIMENSIONAL_LOCATION` / `ANGULAR_LOCATION`) yields the two it spans, in
803    /// `[relating, related]` order.
804    pub fn features(&self) -> Vec<Feature<'m>> {
805        let model = self.cx.model;
806        let refs: Vec<&m::ShapeAspectRef> = match self.which {
807            DimImpl::Size(i) => vec![&model.dimensional_size_arena.get(i.0).applies_to],
808            DimImpl::AngularSize(i) => vec![&model.angular_size_arena.get(i.0).applies_to],
809            DimImpl::Location(i) => {
810                let e = model.dimensional_location_arena.get(i.0);
811                vec![&e.relating_shape_aspect, &e.related_shape_aspect]
812            }
813            DimImpl::AngularLocation(i) => {
814                let e = model.angular_location_arena.get(i.0);
815                vec![&e.relating_shape_aspect, &e.related_shape_aspect]
816            }
817        };
818        refs.into_iter()
819            .filter_map(|r| self.feature_from_sa_ref(r))
820            .collect()
821    }
822
823    fn feature_from_sa_ref(&self, r: &m::ShapeAspectRef) -> Option<Feature<'m>> {
824        if matches!(r, m::ShapeAspectRef::Complex(_)) {
825            self.cx
826                .warn("dimension feature is a complex instance".to_owned());
827            return None;
828        }
829        feature_impl_from_shape_aspect_ref(r).map(|which| Feature { cx: self.cx, which })
830    }
831}
832
833impl<'m> Tolerance<'m> {
834    /// The shape feature this tolerance applies to (its `toleranced_shape_aspect`).
835    ///
836    /// `None` when the target is not a named shape aspect — a tolerance placed on
837    /// a dimension or on the whole part (`PRODUCT_DEFINITION_SHAPE`) — or when the
838    /// target is a complex instance the walk cannot resolve (reported via
839    /// [`Scene::warnings`](crate::scene::Scene)).
840    pub fn feature(&self) -> Option<Feature<'m>> {
841        let target = self.tolerance_target()?;
842        if matches!(target, m::GeometricToleranceTargetRef::Complex(_)) {
843            self.cx
844                .warn("tolerance target is a complex instance".to_owned());
845            return None;
846        }
847        feature_impl_from_tolerance_target(target).map(|which| Feature { cx: self.cx, which })
848    }
849
850    /// The tolerance's `toleranced_shape_aspect`: from the dedicated arena for a
851    /// leaf tolerance, or the `GEOMETRIC_TOLERANCE` facet for a complex one.
852    fn tolerance_target(&self) -> Option<&'m m::GeometricToleranceTargetRef> {
853        if let Some(t) = self.dedicated_target() {
854            return Some(t);
855        }
856        let TolImpl::Complex(cuid) = self.which else {
857            return None;
858        };
859        self.cx
860            .model
861            .complex_unit_arena
862            .get(cuid.0)
863            .parts
864            .iter()
865            .find_map(|p| match p {
866                m::UnitPart::GeometricTolerance {
867                    toleranced_shape_aspect,
868                    ..
869                } => Some(toleranced_shape_aspect),
870                _ => None,
871            })
872    }
873}
874
875/// A concrete geometry item a [`Feature`] designates (via
876/// `GEOMETRIC_ITEM_SPECIFIC_USAGE`). The common cases resolve to a typed handle;
877/// anything else is surfaced as `Other` carrying its key rather than dropped.
878pub enum FeatureGeometry<'m> {
879    /// An `ADVANCED_FACE`.
880    Face(Face<'m>),
881    /// An `EDGE_CURVE`.
882    Edge(Edge<'m>),
883    /// A `CARTESIAN_POINT`.
884    Point(Point<'m>),
885    /// A `MANIFOLD_SOLID_BREP`.
886    Solid(Solid<'m>),
887    /// A curve (`TRIMMED_CURVE`, `COMPOSITE_CURVE`, `CIRCLE`, …); dispatch its
888    /// exact kind via [`Curve::kind`](crate::scene::geometry::Curve::kind).
889    Curve(Curve<'m>),
890    /// Any other identified item (a tessellated face, a geometric set, …), by key.
891    Other(m::EntityKey),
892}
893
894impl<'m> Feature<'m> {
895    /// The concrete geometry this feature designates — the faces / edges / points
896    /// / solids linked to it by `GEOMETRIC_ITEM_SPECIFIC_USAGE` (GISU).
897    ///
898    /// A feature may designate several items (one per GISU). Items outside the
899    /// typed set (curves, tessellated faces, …) come back as
900    /// [`FeatureGeometry::Other`] with their key. Only GISUs whose `definition`
901    /// points directly at this feature are followed; indirect links through a
902    /// shape-aspect relationship are not yet resolved.
903    pub fn geometry(&self) -> Vec<FeatureGeometry<'m>> {
904        let cx = self.cx;
905        let rg = cx.ref_graph();
906        let mut out = Vec::new();
907        for r in rg.referrers(self.key()) {
908            let m::EntityKey::GeometricItemSpecificUsage(gid) = r else {
909                continue;
910            };
911            let item = &cx
912                .model
913                .geometric_item_specific_usage_arena
914                .get(gid.0)
915                .identified_item;
916            out.push(match item {
917                m::GeometricModelItemRef::AdvancedFace(id) => {
918                    FeatureGeometry::Face(Face::from_advanced_face(cx, *id))
919                }
920                m::GeometricModelItemRef::EdgeCurve(id) => {
921                    FeatureGeometry::Edge(Edge::from_id(cx, *id))
922                }
923                m::GeometricModelItemRef::CartesianPoint(id) => {
924                    FeatureGeometry::Point(Point::from_id(cx, *id))
925                }
926                m::GeometricModelItemRef::ManifoldSolidBrep(id) => {
927                    FeatureGeometry::Solid(Solid::from_id(cx, *id))
928                }
929                other => match Curve::from_model_item(cx, other) {
930                    Some(c) => FeatureGeometry::Curve(c),
931                    None => FeatureGeometry::Other(other.entity_key()),
932                },
933            });
934        }
935        out
936    }
937}
938
939// ---------------------------------------------------------------------------
940// Per-part PMI: which product definition a feature / datum / tolerance belongs
941// to, and the filtered per-part queries backing `ProductDef`.
942// ---------------------------------------------------------------------------
943
944/// The product definition a `PRODUCT_DEFINITION_SHAPE` defines, if its
945/// `definition` is a plain `PRODUCT_DEFINITION`.
946fn product_def_of_pds(
947    cx: Ctx<'_>,
948    id: m::ProductDefinitionShapeId,
949) -> Option<m::ProductDefinitionId> {
950    match cx.model.product_definition_shape_arena.get(id.0).definition {
951        m::CharacterizedDefinitionRef::ProductDefinition(pd) => Some(pd),
952        _ => None,
953    }
954}
955
956/// The product definition an `of_shape` reference resolves to (its PDS's
957/// `definition`); `None` for a complex PDS or a non-`PRODUCT_DEFINITION` owner.
958fn product_def_of_shape(
959    cx: Ctx<'_>,
960    r: &m::ProductDefinitionShapeRef,
961) -> Option<m::ProductDefinitionId> {
962    match r {
963        m::ProductDefinitionShapeRef::ProductDefinitionShape(id) => product_def_of_pds(cx, *id),
964        m::ProductDefinitionShapeRef::Complex(_) => None,
965    }
966}
967
968impl Feature<'_> {
969    /// The product definition this feature belongs to (`of_shape` → PDS →
970    /// definition), if it resolves to a plain `PRODUCT_DEFINITION`.
971    fn part_def_id(&self) -> Option<m::ProductDefinitionId> {
972        product_def_of_shape(self.cx, self.of_shape())
973    }
974}
975
976impl Datum<'_> {
977    /// The product definition this datum belongs to (`of_shape` → PDS → part).
978    fn part_def_id(&self) -> Option<m::ProductDefinitionId> {
979        let of_shape = match self.which {
980            DatumImpl::Datum(i) => &self.cx.model.datum_arena.get(i.0).of_shape,
981            DatumImpl::CommonDatum(i) => &self.cx.model.common_datum_arena.get(i.0).of_shape,
982        };
983        product_def_of_shape(self.cx, of_shape)
984    }
985}
986
987impl Tolerance<'_> {
988    /// The product definition this tolerance belongs to: its target may be a
989    /// shape-aspect feature (→ that feature's part) or the whole part's
990    /// `PRODUCT_DEFINITION_SHAPE` directly (an all-over tolerance).
991    fn part_def_id(&self) -> Option<m::ProductDefinitionId> {
992        let target = self.tolerance_target()?;
993        if let m::GeometricToleranceTargetRef::ProductDefinitionShape(id) = target {
994            return product_def_of_pds(self.cx, *id);
995        }
996        let which = feature_impl_from_tolerance_target(target)?;
997        Feature { cx: self.cx, which }.part_def_id()
998    }
999}
1000
1001/// The shape-region features (narrow set) belonging to `part`.
1002pub(crate) fn features_of(cx: Ctx<'_>, part: m::ProductDefinitionId) -> Vec<Feature<'_>> {
1003    let mut all = Vec::new();
1004    push_all_features(cx, &mut all);
1005    all.retain(|f| f.part_def_id() == Some(part));
1006    all
1007}
1008
1009/// The datums belonging to `part`.
1010pub(crate) fn datums_of(cx: Ctx<'_>, part: m::ProductDefinitionId) -> Vec<Datum<'_>> {
1011    let mut all = all_datums(cx);
1012    all.retain(|d| d.part_def_id() == Some(part));
1013    all
1014}
1015
1016/// The dimensions belonging to `part` — those whose targeted feature is on it.
1017pub(crate) fn dimensions_of(cx: Ctx<'_>, part: m::ProductDefinitionId) -> Vec<Dimension<'_>> {
1018    let mut all = all_dimensions(cx);
1019    all.retain(|d| d.features().iter().any(|f| f.part_def_id() == Some(part)));
1020    all
1021}
1022
1023/// The tolerances belonging to `part` — those whose target (feature or whole
1024/// part) is on it.
1025pub(crate) fn tolerances_of(cx: Ctx<'_>, part: m::ProductDefinitionId) -> Vec<Tolerance<'_>> {
1026    let mut all = all_tolerances(cx);
1027    all.retain(|t| t.part_def_id() == Some(part));
1028    all
1029}