Skip to main content

step_io/scene/
product.rs

1//! Read-only product / assembly navigation handles over a [`Scene`].
2//!
3//! A [`Product`] is a part — its [`versions`](Product::versions),
4//! [`definitions`](Product::definitions), and PLM metadata (contributors,
5//! approvals, documents) hang off it. A [`ProductDef`] descends the assembly
6//! through its [`occurrences`](ProductDef::occurrences), each a placed
7//! instance of a child definition, and bridges to the geometry handles via
8//! [`solids`](ProductDef::solids). Enter from [`Scene::all_products`] or the
9//! assembly roots [`Scene::root_definitions`].
10
11// Every public method here is a pure read accessor (see geometry.rs).
12#![allow(clippy::must_use_candidate)]
13
14use std::collections::HashSet;
15
16use crate::generated::model as m;
17use crate::scene::geometry::Solid;
18use crate::scene::pmi;
19use crate::scene::{Ctx, Scene};
20
21impl Scene<'_> {
22    /// Every product (`PRODUCT`) in the model.
23    pub fn all_products(&self) -> impl Iterator<Item = Product<'_>> + '_ {
24        let cx = self.ctx();
25        (0..cx.model.product_arena.items.len()).map(move |i| Product {
26            cx,
27            id: m::ProductId(i),
28        })
29    }
30
31    /// Every product definition (`PRODUCT_DEFINITION`) in the model.
32    pub fn all_product_definitions(&self) -> impl Iterator<Item = ProductDef<'_>> + '_ {
33        let cx = self.ctx();
34        (0..cx.model.product_definition_arena.items.len()).map(move |i| ProductDef {
35            cx,
36            id: m::ProductDefinitionId(i),
37        })
38    }
39
40    /// The assembly roots: definitions no `NEXT_ASSEMBLY_USAGE_OCCURRENCE`
41    /// uses as a child (`related`). Start an assembly walk here — a single
42    /// part file simply yields all of its definitions.
43    pub fn root_definitions(&self) -> impl Iterator<Item = ProductDef<'_>> + '_ {
44        let cx = self.ctx();
45        let rg = cx.ref_graph();
46        self.all_product_definitions().filter(move |def| {
47            !rg.referrers(def.key()).iter().any(|r| {
48                let m::EntityKey::NextAssemblyUsageOccurrence(nid) = r else {
49                    return false;
50                };
51                let nauo = cx.model.next_assembly_usage_occurrence_arena.get(nid.0);
52                matches!(
53                    &nauo.related_product_definition,
54                    m::ProductDefinitionOrReferenceRef::ProductDefinition(d) if m::EntityKey::ProductDefinition(*d) == def.key()
55                )
56            })
57        })
58    }
59}
60
61// ---------------------------------------------------------------------------
62// Product
63// ---------------------------------------------------------------------------
64
65/// A product (`PRODUCT`) — the identity of a part (id + name).
66#[derive(Clone, Copy)]
67pub struct Product<'m> {
68    cx: Ctx<'m>,
69    id: m::ProductId,
70}
71
72impl<'m> Product<'m> {
73    fn raw(&self) -> &'m m::Product {
74        self.cx.model.product_arena.get(self.id.0)
75    }
76
77    /// This product's global identity (a `Copy` key for maps / deduplication;
78    /// distinct from [`Product::id`], which is the STEP `id` string attribute).
79    pub fn key(&self) -> m::EntityKey {
80        m::EntityKey::Product(self.id)
81    }
82
83    pub fn id(&self) -> &'m str {
84        &self.raw().id
85    }
86
87    pub fn name(&self) -> &'m str {
88        &self.raw().name
89    }
90
91    pub fn description(&self) -> Option<&'m str> {
92        self.raw().description.as_deref()
93    }
94
95    /// The definitions of this product (reverse: a `PRODUCT_DEFINITION_FORMATION`
96    /// whose `of_product` is this product → a `PRODUCT_DEFINITION` whose `formation`
97    /// is that formation).
98    ///
99    /// No field re-check is needed: a formation's only product reference is
100    /// `of_product` and a definition's only formation reference is `formation`, so
101    /// `referrers` already pins both edges to this product.
102    pub fn definitions(&self) -> impl Iterator<Item = ProductDef<'m>> + 'm {
103        let cx = self.cx;
104        let rg = cx.ref_graph();
105        let mut out: Vec<ProductDef<'m>> = Vec::new();
106        for &fr in rg.referrers(m::EntityKey::Product(self.id)) {
107            if !matches!(
108                fr,
109                m::EntityKey::ProductDefinitionFormation(_)
110                    | m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(_)
111            ) {
112                continue;
113            }
114            for &pr in rg.referrers(fr) {
115                if let m::EntityKey::ProductDefinition(pdid) = pr {
116                    out.push(ProductDef { cx, id: pdid });
117                }
118            }
119        }
120        out.into_iter()
121    }
122
123    /// Every key metadata may attach to for *this whole product*: the product
124    /// itself plus all of its formations and definitions. Metadata in STEP is
125    /// mostly version-level (assigned to a formation or definition), so a
126    /// product-wide view must reach down the tree, not just the product entity.
127    /// Mirrors the `product → formation → definition` traversal of
128    /// [`Product::definitions`].
129    fn metadata_targets(&self) -> Vec<m::EntityKey> {
130        let rg = self.cx.ref_graph();
131        let mut targets = vec![self.key()];
132        for &fr in rg.referrers(self.key()) {
133            if !matches!(
134                fr,
135                m::EntityKey::ProductDefinitionFormation(_)
136                    | m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(_)
137            ) {
138                continue;
139            }
140            targets.push(fr);
141            for &pr in rg.referrers(fr) {
142                if matches!(pr, m::EntityKey::ProductDefinition(_)) {
143                    targets.push(pr);
144                }
145            }
146        }
147        targets
148    }
149
150    /// The people/organizations associated with this product across all its
151    /// versions and definitions, each with the role they play.
152    pub fn contributors(&self) -> Vec<Contributor<'m>> {
153        contributors_of(self.cx, &self.metadata_targets())
154    }
155
156    /// The approvals anywhere in this product's tree (product, any formation, or
157    /// any definition).
158    pub fn approvals(&self) -> Vec<Approval<'m>> {
159        approvals_of(self.cx, &self.metadata_targets())
160    }
161
162    /// The documents referenced anywhere in this product's tree.
163    pub fn documents(&self) -> Vec<Document<'m>> {
164        documents_of(self.cx, &self.metadata_targets())
165    }
166
167    /// The security classifications assigned anywhere in this product's tree.
168    pub fn security_classifications(&self) -> Vec<SecurityClassification<'m>> {
169        security_classifications_of(self.cx, &self.metadata_targets())
170    }
171
172    /// This product's versions — its `PRODUCT_DEFINITION_FORMATION`s (reverse:
173    /// their `of_product` is this product). Usually one; a file may carry several
174    /// revisions. Navigate `version.definitions()` to group definitions by version.
175    pub fn versions(&self) -> Vec<Version<'m>> {
176        let cx = self.cx;
177        let rg = cx.ref_graph();
178        let mut out: Vec<Version<'m>> = Vec::new();
179        for r in rg.referrers(self.key()) {
180            let which = match r {
181                m::EntityKey::ProductDefinitionFormation(id) => FormImpl::Plain(*id),
182                m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(id) => {
183                    FormImpl::WithSource(*id)
184                }
185                _ => continue,
186            };
187            out.push(Version { cx, which });
188        }
189        out
190    }
191}
192
193/// A version (revision) of a product — a `PRODUCT_DEFINITION_FORMATION`.
194///
195/// Unifies the plain formation and the `WITH_SPECIFIED_SOURCE` variant, which
196/// share id / description (its make-or-buy source is out of scope for now).
197#[derive(Clone, Copy)]
198pub struct Version<'m> {
199    cx: Ctx<'m>,
200    which: FormImpl,
201}
202
203#[derive(Clone, Copy)]
204enum FormImpl {
205    Plain(m::ProductDefinitionFormationId),
206    WithSource(m::ProductDefinitionFormationWithSpecifiedSourceId),
207}
208
209impl<'m> Version<'m> {
210    /// The shared id + description fields, resolved once so the accessors don't
211    /// each repeat the backing-type match.
212    fn fields(&self) -> (&'m str, Option<&'m str>) {
213        let model = self.cx.model;
214        match self.which {
215            FormImpl::Plain(i) => {
216                let f = model.product_definition_formation_arena.get(i.0);
217                (&f.id, f.description.as_deref())
218            }
219            FormImpl::WithSource(i) => {
220                let f = model
221                    .product_definition_formation_with_specified_source_arena
222                    .get(i.0);
223                (&f.id, f.description.as_deref())
224            }
225        }
226    }
227
228    /// This version's global identity (a `Copy` key for maps / dedup).
229    pub fn key(&self) -> m::EntityKey {
230        match self.which {
231            FormImpl::Plain(i) => m::EntityKey::ProductDefinitionFormation(i),
232            FormImpl::WithSource(i) => {
233                m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(i)
234            }
235        }
236    }
237
238    /// The version label (`RevB`, `1`, `A`, …).
239    pub fn id(&self) -> &'m str {
240        self.fields().0
241    }
242
243    /// The version description, if any.
244    pub fn description(&self) -> Option<&'m str> {
245        self.fields().1
246    }
247
248    /// The definitions belonging to this version (reverse: a `PRODUCT_DEFINITION`
249    /// whose `formation` is this version).
250    pub fn definitions(&self) -> impl Iterator<Item = ProductDef<'m>> + 'm {
251        let cx = self.cx;
252        let rg = cx.ref_graph();
253        let mut out: Vec<ProductDef<'m>> = Vec::new();
254        for &r in rg.referrers(self.key()) {
255            if let m::EntityKey::ProductDefinition(pdid) = r {
256                out.push(ProductDef { cx, id: pdid });
257            }
258        }
259        out.into_iter()
260    }
261}
262
263// ---------------------------------------------------------------------------
264// ProductDef
265// ---------------------------------------------------------------------------
266
267/// A product definition (`PRODUCT_DEFINITION`) — a view of a product that owns
268/// shape and participates in the assembly tree.
269#[derive(Clone, Copy)]
270pub struct ProductDef<'m> {
271    cx: Ctx<'m>,
272    id: m::ProductDefinitionId,
273}
274
275impl<'m> ProductDef<'m> {
276    fn raw(&self) -> &'m m::ProductDefinition {
277        self.cx.model.product_definition_arena.get(self.id.0)
278    }
279
280    /// This definition's global identity (a `Copy` key for maps / deduplication;
281    /// distinct from [`ProductDef::id`], the STEP `id` string attribute).
282    pub fn key(&self) -> m::EntityKey {
283        m::EntityKey::ProductDefinition(self.id)
284    }
285
286    pub fn id(&self) -> &'m str {
287        &self.raw().id
288    }
289
290    pub fn description(&self) -> Option<&'m str> {
291        self.raw().description.as_deref()
292    }
293
294    /// The product this definition belongs to (forward: `formation → of_product`).
295    pub fn product(&self) -> Option<Product<'m>> {
296        let model = self.cx.model;
297        let of_product: &m::ProductRef = match &self.raw().formation {
298            m::ProductDefinitionFormationRef::ProductDefinitionFormation(i) => {
299                &model.product_definition_formation_arena.get(i.0).of_product
300            }
301            m::ProductDefinitionFormationRef::ProductDefinitionFormationWithSpecifiedSource(i) => {
302                &model
303                    .product_definition_formation_with_specified_source_arena
304                    .get(i.0)
305                    .of_product
306            }
307            m::ProductDefinitionFormationRef::Complex(_) => return None,
308        };
309        match of_product {
310            m::ProductRef::Product(pid) => Some(Product {
311                cx: self.cx,
312                id: *pid,
313            }),
314            m::ProductRef::Complex(_) => None,
315        }
316    }
317
318    /// The component occurrences directly under this definition — each a usage
319    /// (`NAUO`) carrying the child definition *and* its placement transform
320    /// (reverse: a `NAUO` whose `relating` is this definition).
321    pub fn occurrences(&self) -> impl Iterator<Item = Occurrence<'m>> + 'm {
322        let cx = self.cx;
323        let rg = cx.ref_graph();
324        let me = self.id;
325        let mut out: Vec<Occurrence<'m>> = Vec::new();
326        for r in rg.referrers(m::EntityKey::ProductDefinition(me)) {
327            if let m::EntityKey::NextAssemblyUsageOccurrence(nid) = r {
328                let nauo = cx.model.next_assembly_usage_occurrence_arena.get(nid.0);
329                if pdor_is(&nauo.relating_product_definition, me) {
330                    out.push(Occurrence { cx, id: *nid });
331                }
332            }
333        }
334        out.into_iter()
335    }
336
337    /// Component definitions directly under this one in the assembly tree — the
338    /// occurrences resolved to their child definitions (drops the placement).
339    pub fn children(&self) -> impl Iterator<Item = ProductDef<'m>> + 'm {
340        self.occurrences().filter_map(|o| o.definition())
341    }
342
343    /// Assembly definitions this one is a component of (reverse: a `NAUO` whose
344    /// `related` is this definition → its `relating`).
345    pub fn parents(&self) -> impl Iterator<Item = ProductDef<'m>> + 'm {
346        let cx = self.cx;
347        let rg = cx.ref_graph();
348        let me = self.id;
349        let mut out: Vec<ProductDef<'m>> = Vec::new();
350        for r in rg.referrers(m::EntityKey::ProductDefinition(me)) {
351            if let m::EntityKey::NextAssemblyUsageOccurrence(nid) = r {
352                let nauo = cx.model.next_assembly_usage_occurrence_arena.get(nid.0);
353                if pdor_is(&nauo.related_product_definition, me) {
354                    if let Some(d) = pdor_to_def(cx, &nauo.relating_product_definition) {
355                        out.push(d);
356                    }
357                }
358            }
359        }
360        out.into_iter()
361    }
362
363    /// The b-rep solids of this definition's shape — the bridge into the geometry
364    /// handles. Reverse: a `PRODUCT_DEFINITION_SHAPE` referencing this definition →
365    /// a `SHAPE_DEFINITION_REPRESENTATION` referencing that shape → forward through
366    /// its representation's items to each `MANIFOLD_SOLID_BREP`. When the geometry
367    /// lives in a separate representation bridged by a plain
368    /// `SHAPE_REPRESENTATION_RELATIONSHIP` (the common AP242 assembly layout), the
369    /// equivalence bridge is followed to collect those solids too.
370    pub fn solids(&self) -> impl Iterator<Item = Solid<'m>> + 'm {
371        let cx = self.cx;
372        let rg = cx.ref_graph();
373        let me = self.id;
374        let mut out: Vec<Solid<'m>> = Vec::new();
375        // Shared across every representation reached: guards relationship cycles
376        // and keeps a shape-equivalence bridge from re-collecting a rep.
377        let mut visited: HashSet<m::EntityKey> = HashSet::new();
378        for r in rg.referrers(m::EntityKey::ProductDefinition(me)) {
379            let m::EntityKey::ProductDefinitionShape(pds_id) = r else {
380                continue;
381            };
382            let pds = cx.model.product_definition_shape_arena.get(pds_id.0);
383            if !matches!(&pds.definition, m::CharacterizedDefinitionRef::ProductDefinition(pd) if *pd == me)
384            {
385                continue;
386            }
387            for s in rg.referrers(m::EntityKey::ProductDefinitionShape(*pds_id)) {
388                let m::EntityKey::ShapeDefinitionRepresentation(sdr_id) = s else {
389                    continue;
390                };
391                let sdr = cx.model.shape_definition_representation_arena.get(sdr_id.0);
392                if matches!(&sdr.definition, m::RepresentedDefinitionRef::ProductDefinitionShape(p) if *p == *pds_id)
393                {
394                    collect_solids_from_repr(cx, &sdr.used_representation, &mut out, &mut visited);
395                }
396            }
397        }
398        // A brep shared by two representations could be collected twice.
399        let mut seen: HashSet<m::EntityKey> = HashSet::new();
400        out.retain(|s| seen.insert(s.key()));
401        out.into_iter()
402    }
403
404    /// The shape features of this part (`SHAPE_ASPECT`s whose `of_shape`
405    /// resolves to this definition). The model-wide [`Scene::features`] filtered
406    /// to this part.
407    pub fn features(&self) -> impl Iterator<Item = pmi::Feature<'m>> + 'm {
408        pmi::features_of(self.cx, self.id).into_iter()
409    }
410
411    /// The dimensions of this part (those whose targeted feature is on it).
412    pub fn dimensions(&self) -> impl Iterator<Item = pmi::Dimension<'m>> + 'm {
413        pmi::dimensions_of(self.cx, self.id).into_iter()
414    }
415
416    /// The geometric tolerances of this part (those whose target — a feature or
417    /// the whole part — is on it).
418    pub fn tolerances(&self) -> impl Iterator<Item = pmi::Tolerance<'m>> + 'm {
419        pmi::tolerances_of(self.cx, self.id).into_iter()
420    }
421
422    /// The datums of this part (`DATUM` / `COMMON_DATUM` whose `of_shape` is on it).
423    pub fn datums(&self) -> impl Iterator<Item = pmi::Datum<'m>> + 'm {
424        pmi::datums_of(self.cx, self.id).into_iter()
425    }
426
427    /// The version identifier of this part — its `PRODUCT_DEFINITION_FORMATION.id`.
428    pub fn version(&self) -> Option<&'m str> {
429        let model = self.cx.model;
430        match &self.raw().formation {
431            m::ProductDefinitionFormationRef::ProductDefinitionFormation(i) => {
432                Some(&model.product_definition_formation_arena.get(i.0).id)
433            }
434            m::ProductDefinitionFormationRef::ProductDefinitionFormationWithSpecifiedSource(i) => {
435                Some(
436                    &model
437                        .product_definition_formation_with_specified_source_arena
438                        .get(i.0)
439                        .id,
440                )
441            }
442            m::ProductDefinitionFormationRef::Complex(_) => None,
443        }
444    }
445
446    /// The people / organizations involved with this part and their roles
447    /// (creator, design owner, supplier, …), from the person-and-organization
448    /// assignments targeting this definition, its product, or its formation.
449    /// The identities a management assignment (person/org, approval, document)
450    /// may target for this part: the definition, its product, and its formation.
451    /// Ordered broadest-first (product, then formation, then definition) so the
452    /// walkers' first-hit de-dup labels each result with its broadest scope.
453    fn metadata_targets(&self) -> Vec<m::EntityKey> {
454        let mut targets = Vec::new();
455        if let Some(p) = self.product() {
456            targets.push(p.key());
457        }
458        match &self.raw().formation {
459            m::ProductDefinitionFormationRef::ProductDefinitionFormation(i) => {
460                targets.push(m::EntityKey::ProductDefinitionFormation(*i));
461            }
462            m::ProductDefinitionFormationRef::ProductDefinitionFormationWithSpecifiedSource(i) => {
463                targets.push(m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(
464                    *i,
465                ));
466            }
467            m::ProductDefinitionFormationRef::Complex(_) => {}
468        }
469        targets.push(m::EntityKey::ProductDefinition(self.id));
470        targets
471    }
472
473    /// The people/organizations assigned to this part's definition, product, or
474    /// formation, each with the role they play.
475    pub fn contributors(&self) -> Vec<Contributor<'m>> {
476        contributors_of(self.cx, &self.metadata_targets())
477    }
478
479    /// The approvals on this part — the `APPROVAL`s assigned to its definition,
480    /// product, or formation (each with a status, level, and approvers).
481    pub fn approvals(&self) -> Vec<Approval<'m>> {
482        approvals_of(self.cx, &self.metadata_targets())
483    }
484
485    /// The documents referenced by this part — drawings, specs, standards
486    /// (reverse: an `APPLIED_DOCUMENT_REFERENCE` whose `items` include this
487    /// definition, its product, or its formation).
488    pub fn documents(&self) -> Vec<Document<'m>> {
489        documents_of(self.cx, &self.metadata_targets())
490    }
491
492    /// The security classifications assigned to this part — its confidentiality
493    /// level and why (assigned to its definition, product, or formation).
494    pub fn security_classifications(&self) -> Vec<SecurityClassification<'m>> {
495        security_classifications_of(self.cx, &self.metadata_targets())
496    }
497}
498
499// ---------------------------------------------------------------------------
500// Shared metadata walkers
501//
502// Metadata (contributors, approvals, documents, classifications) attaches to a
503// part by *reverse* assignment: an assignment entity lists the definition,
504// product, or formation in its `items`. Each family gathers its handles by
505// scanning the referrers of a set of target keys; only the target set differs
506// between a `ProductDef` (its own def + product + formation) and a `Product`
507// (the whole product tree), so the walks are factored out here and parameterised
508// by `targets`. Per-family de-dup/extraction stays distinct (a single generic
509// walker doesn't fit — the de-dup keys differ).
510//
511// Each result is labelled with the `Scope` of the target it was reached through.
512// Because `metadata_targets` is ordered broadest-first and the walkers keep the
513// first hit, a multi-target assignment is labelled with its broadest scope.
514// ---------------------------------------------------------------------------
515
516/// Which level of the product structure a piece of metadata is attached to.
517#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
518pub enum Scope {
519    /// Attached to the product itself — applies across every version.
520    Product,
521    /// Attached to a specific version (`PRODUCT_DEFINITION_FORMATION`).
522    Version,
523    /// Attached to a specific definition (`PRODUCT_DEFINITION`).
524    Definition,
525}
526
527/// The scope of a metadata target key. Targets only ever come from
528/// `metadata_targets`, which yields product / formation / definition keys.
529fn scope_of(k: m::EntityKey) -> Scope {
530    match k {
531        m::EntityKey::Product(_) => Scope::Product,
532        m::EntityKey::ProductDefinitionFormation(_)
533        | m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(_) => Scope::Version,
534        _ => Scope::Definition,
535    }
536}
537
538/// A single product-structure entity a piece of metadata is attached to — the
539/// full detail behind [`Scope`]. Unlike [`Contributor::assigned_scope`] (which
540/// answers *which level*), this names *which entity*, so multiple targets at the
541/// same level stay distinct.
542#[derive(Clone, Copy)]
543pub enum Target<'m> {
544    Product(Product<'m>),
545    Version(Version<'m>),
546    Definition(ProductDef<'m>),
547}
548
549impl Target<'_> {
550    /// The structure level of this target.
551    pub fn scope(&self) -> Scope {
552        match self {
553            Target::Product(_) => Scope::Product,
554            Target::Version(_) => Scope::Version,
555            Target::Definition(_) => Scope::Definition,
556        }
557    }
558}
559
560/// Map an assignment-item key to a [`Target`] handle. Items that are not
561/// product-structure entities (product / formation / definition) yield `None`.
562fn target_of(cx: Ctx<'_>, k: m::EntityKey) -> Option<Target<'_>> {
563    Some(match k {
564        m::EntityKey::Product(id) => Target::Product(Product { cx, id }),
565        m::EntityKey::ProductDefinitionFormation(id) => Target::Version(Version {
566            cx,
567            which: FormImpl::Plain(id),
568        }),
569        m::EntityKey::ProductDefinitionFormationWithSpecifiedSource(id) => {
570            Target::Version(Version {
571                cx,
572                which: FormImpl::WithSource(id),
573            })
574        }
575        m::EntityKey::ProductDefinition(id) => Target::Definition(ProductDef { cx, id }),
576        _ => return None,
577    })
578}
579
580/// Collect the distinct product-structure targets from a stream of item keys.
581fn targets_from_keys<'m>(cx: Ctx<'m>, keys: impl Iterator<Item = m::EntityKey>) -> Vec<Target<'m>> {
582    let mut out: Vec<Target<'m>> = Vec::new();
583    let mut seen: Vec<m::EntityKey> = Vec::new();
584    for k in keys {
585        if seen.contains(&k) {
586            continue;
587        }
588        seen.push(k);
589        if let Some(t) = target_of(cx, k) {
590            out.push(t);
591        }
592    }
593    out
594}
595
596fn contributors_of<'m>(cx: Ctx<'m>, targets: &[m::EntityKey]) -> Vec<Contributor<'m>> {
597    let rg = cx.ref_graph();
598    let mut out: Vec<Contributor<'m>> = Vec::new();
599    let mut seen: Vec<m::EntityKey> = Vec::new();
600    for &t in targets {
601        let scope = scope_of(t);
602        for r in rg.referrers(t) {
603            let which = match r {
604                m::EntityKey::CcDesignPersonAndOrganizationAssignment(id) => {
605                    ContributorImpl::CcDesign(*id)
606                }
607                m::EntityKey::AppliedPersonAndOrganizationAssignment(id) => {
608                    ContributorImpl::Applied(*id)
609                }
610                _ => continue,
611            };
612            if seen.contains(r) {
613                continue;
614            }
615            seen.push(*r);
616            out.push(Contributor { cx, which, scope });
617        }
618    }
619    out
620}
621
622fn approvals_of<'m>(cx: Ctx<'m>, targets: &[m::EntityKey]) -> Vec<Approval<'m>> {
623    let rg = cx.ref_graph();
624    let mut out: Vec<Approval<'m>> = Vec::new();
625    let mut seen: Vec<m::ApprovalId> = Vec::new();
626    for &t in targets {
627        let scope = scope_of(t);
628        for r in rg.referrers(t) {
629            let m::ApprovalRef::Approval(aid) = match r {
630                m::EntityKey::CcDesignApproval(id) => {
631                    &cx.model
632                        .cc_design_approval_arena
633                        .get(id.0)
634                        .assigned_approval
635                }
636                m::EntityKey::AppliedApprovalAssignment(id) => {
637                    &cx.model
638                        .applied_approval_assignment_arena
639                        .get(id.0)
640                        .assigned_approval
641                }
642                _ => continue,
643            };
644            if seen.contains(aid) {
645                continue;
646            }
647            seen.push(*aid);
648            out.push(Approval {
649                cx,
650                id: *aid,
651                scope,
652            });
653        }
654    }
655    out
656}
657
658/// A complex `assigned_document` cannot be resolved to a document handle — it is
659/// skipped and surfaced through [`Scene::warnings`](crate::scene::Scene::warnings).
660fn documents_of<'m>(cx: Ctx<'m>, targets: &[m::EntityKey]) -> Vec<Document<'m>> {
661    let rg = cx.ref_graph();
662    let mut out: Vec<Document<'m>> = Vec::new();
663    let mut seen: Vec<m::EntityKey> = Vec::new();
664    for &t in targets {
665        let scope = scope_of(t);
666        for r in rg.referrers(t) {
667            let m::EntityKey::AppliedDocumentReference(id) = r else {
668                continue;
669            };
670            let which = match &cx
671                .model
672                .applied_document_reference_arena
673                .get(id.0)
674                .assigned_document
675            {
676                m::DocumentRef::Document(i) => DocImpl::Document(*i),
677                m::DocumentRef::DocumentFile(i) => DocImpl::DocumentFile(*i),
678                m::DocumentRef::Complex(_) => {
679                    cx.warn(
680                        "APPLIED_DOCUMENT_REFERENCE.assigned_document is a complex instance; \
681                         document left unread"
682                            .to_owned(),
683                    );
684                    continue;
685                }
686            };
687            let doc = Document { cx, which, scope };
688            let k = doc.key();
689            if seen.contains(&k) {
690                continue;
691            }
692            seen.push(k);
693            out.push(doc);
694        }
695    }
696    out
697}
698
699fn security_classifications_of<'m>(
700    cx: Ctx<'m>,
701    targets: &[m::EntityKey],
702) -> Vec<SecurityClassification<'m>> {
703    let rg = cx.ref_graph();
704    let mut out: Vec<SecurityClassification<'m>> = Vec::new();
705    let mut seen: Vec<m::SecurityClassificationId> = Vec::new();
706    for &t in targets {
707        let scope = scope_of(t);
708        for r in rg.referrers(t) {
709            let m::SecurityClassificationRef::SecurityClassification(sid) = match r {
710                m::EntityKey::CcDesignSecurityClassification(id) => {
711                    &cx.model
712                        .cc_design_security_classification_arena
713                        .get(id.0)
714                        .assigned_security_classification
715                }
716                m::EntityKey::AppliedSecurityClassificationAssignment(id) => {
717                    &cx.model
718                        .applied_security_classification_assignment_arena
719                        .get(id.0)
720                        .assigned_security_classification
721                }
722                _ => continue,
723            };
724            if seen.contains(sid) {
725                continue;
726            }
727            seen.push(*sid);
728            out.push(SecurityClassification {
729                cx,
730                id: *sid,
731                scope,
732            });
733        }
734    }
735    out
736}
737
738// ---------------------------------------------------------------------------
739// Contributor / Person (management metadata: who is involved with a part)
740// ---------------------------------------------------------------------------
741
742#[derive(Clone, Copy)]
743enum ContributorImpl {
744    CcDesign(m::CcDesignPersonAndOrganizationAssignmentId),
745    Applied(m::AppliedPersonAndOrganizationAssignmentId),
746}
747
748/// A person + organization assigned to a part, with the role they play
749/// (`creator`, `design_owner`, `part_supplier`, …).
750#[derive(Clone, Copy)]
751pub struct Contributor<'m> {
752    cx: Ctx<'m>,
753    which: ContributorImpl,
754    scope: Scope,
755}
756
757impl<'m> Contributor<'m> {
758    /// Which level of the product structure this assignment attaches to.
759    pub fn assigned_scope(&self) -> Scope {
760        self.scope
761    }
762
763    /// The exact product-structure entities this assignment attaches to (its
764    /// `items`) — versions and definitions stay distinct even at the same level.
765    pub fn assigned_targets(&self) -> Vec<Target<'m>> {
766        let cx = self.cx;
767        let keys: Vec<m::EntityKey> = match self.which {
768            ContributorImpl::CcDesign(i) => cx
769                .model
770                .cc_design_person_and_organization_assignment_arena
771                .get(i.0)
772                .items
773                .iter()
774                .map(m::CcPersonOrganizationItemRef::entity_key)
775                .collect(),
776            ContributorImpl::Applied(i) => cx
777                .model
778                .applied_person_and_organization_assignment_arena
779                .get(i.0)
780                .items
781                .iter()
782                .map(m::PersonAndOrganizationItemRef::entity_key)
783                .collect(),
784        };
785        targets_from_keys(cx, keys.into_iter())
786    }
787
788    /// The assignment's person-and-organization and role ids (both refs are
789    /// single-variant, so they always resolve).
790    fn ids(&self) -> (m::PersonAndOrganizationId, m::PersonAndOrganizationRoleId) {
791        let model = self.cx.model;
792        let (pao, role) = match self.which {
793            ContributorImpl::CcDesign(i) => {
794                let a = model
795                    .cc_design_person_and_organization_assignment_arena
796                    .get(i.0);
797                (&a.assigned_person_and_organization, &a.role)
798            }
799            ContributorImpl::Applied(i) => {
800                let a = model
801                    .applied_person_and_organization_assignment_arena
802                    .get(i.0);
803                (&a.assigned_person_and_organization, &a.role)
804            }
805        };
806        let m::PersonAndOrganizationRef::PersonAndOrganization(pid) = pao;
807        let m::PersonAndOrganizationRoleRef::PersonAndOrganizationRole(rid) = role;
808        (*pid, *rid)
809    }
810
811    /// The role this contributor plays (`creator`, `design_owner`, …).
812    pub fn role(&self) -> &'m str {
813        let (_, rid) = self.ids();
814        &self
815            .cx
816            .model
817            .person_and_organization_role_arena
818            .get(rid.0)
819            .name
820    }
821
822    /// The person.
823    pub fn person(&self) -> Person<'m> {
824        let (pao, _) = self.ids();
825        let m::PersonRef::Person(pid) = &self
826            .cx
827            .model
828            .person_and_organization_arena
829            .get(pao.0)
830            .the_person;
831        Person {
832            cx: self.cx,
833            id: *pid,
834        }
835    }
836
837    /// The organization's name.
838    pub fn organization(&self) -> &'m str {
839        let (pao, _) = self.ids();
840        let m::OrganizationRef::Organization(oid) = &self
841            .cx
842            .model
843            .person_and_organization_arena
844            .get(pao.0)
845            .the_organization;
846        &self.cx.model.organization_arena.get(oid.0).name
847    }
848}
849
850/// A person (`PERSON`).
851#[derive(Clone, Copy)]
852pub struct Person<'m> {
853    cx: Ctx<'m>,
854    id: m::PersonId,
855}
856
857impl<'m> Person<'m> {
858    fn raw(&self) -> &'m m::Person {
859        self.cx.model.person_arena.get(self.id.0)
860    }
861
862    /// This person's global identity (a `Copy` key for maps / dedup).
863    pub fn key(&self) -> m::EntityKey {
864        m::EntityKey::Person(self.id)
865    }
866
867    /// The person's STEP `id`.
868    pub fn id(&self) -> &'m str {
869        &self.raw().id
870    }
871
872    /// The person's given name, if recorded.
873    pub fn first_name(&self) -> Option<&'m str> {
874        self.raw().first_name.as_deref()
875    }
876
877    /// The person's family name, if recorded.
878    pub fn last_name(&self) -> Option<&'m str> {
879        self.raw().last_name.as_deref()
880    }
881}
882
883// ---------------------------------------------------------------------------
884// Occurrence (assembly component usage) + placement transform
885// ---------------------------------------------------------------------------
886
887/// A coordinate frame (`AXIS2_PLACEMENT_3D`): an origin plus the local Z axis
888/// and X reference direction (Y is `Z × X`). Omitted axes use the STEP defaults.
889#[derive(Clone, Copy, Debug)]
890pub struct Placement {
891    pub origin: [f64; 3],
892    pub axis: [f64; 3],
893    pub ref_direction: [f64; 3],
894}
895
896/// An item-defined transformation as two frames: the relative transform maps
897/// `from` (`transform_item_1`) to `to` (`transform_item_2`) — the faithful
898/// two-frame form; [`Transform::matrix`] composes them on demand.
899#[derive(Clone, Copy, Debug)]
900pub struct Transform {
901    pub from: Placement,
902    pub to: Placement,
903}
904
905// Small vector helpers for the frame math (local on purpose: the nurbs module
906// keeps its own private set, and a shared math module would be more coupling
907// than these one-liners are worth).
908fn vdot(a: [f64; 3], b: [f64; 3]) -> f64 {
909    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
910}
911fn vsub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
912    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
913}
914fn vscale(a: [f64; 3], s: f64) -> [f64; 3] {
915    [a[0] * s, a[1] * s, a[2] * s]
916}
917fn vcross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
918    [
919        a[1] * b[2] - a[2] * b[1],
920        a[2] * b[0] - a[0] * b[2],
921        a[0] * b[1] - a[1] * b[0],
922    ]
923}
924fn vnorm(a: [f64; 3]) -> Option<[f64; 3]> {
925    let n = vdot(a, a).sqrt();
926    (n >= 1e-12).then(|| vscale(a, 1.0 / n))
927}
928
929impl Placement {
930    /// The orthonormal frame `(x, y, z)` this placement defines: z is the
931    /// axis, x is the reference direction projected orthogonal to z (the STEP
932    /// derivation), y completes the right-handed set. `None` for a degenerate
933    /// placement (a zero axis, or a reference direction parallel to the axis).
934    fn frame(&self) -> Option<([f64; 3], [f64; 3], [f64; 3])> {
935        let z = vnorm(self.axis)?;
936        let x = vnorm(vsub(
937            self.ref_direction,
938            vscale(z, vdot(self.ref_direction, z)),
939        ))?;
940        Some((x, vcross(z, x), z))
941    }
942}
943
944impl Transform {
945    /// The relative placement composed into one homogeneous 4×4 matrix — an
946    /// opt-in derived view of the two stored frames (`M = F_to · F_from⁻¹`:
947    /// geometry defined in the `from` frame lands in the `to` frame).
948    ///
949    /// Convention: `m[row][col]`, points map as column vectors `p' = M · p`,
950    /// and the last row is `[0, 0, 0, 1]` (STEP placements carry rotation and
951    /// translation only). `None` if either placement is degenerate (a zero
952    /// axis, or a reference direction parallel to the axis) — the matrix is
953    /// not fabricated from bad frames.
954    // Matrix index arithmetic reads clearest with explicit indices.
955    #[allow(clippy::needless_range_loop)]
956    pub fn matrix(&self) -> Option<[[f64; 4]; 4]> {
957        let rf = self.from.frame()?; // columns of R_from
958        let rt = self.to.frame()?; // columns of R_to
959        let (rf, rt) = ([rf.0, rf.1, rf.2], [rt.0, rt.1, rt.2]);
960        let mut m = [[0.0_f64; 4]; 4];
961        // R = R_to · R_fromᵀ (an orthonormal frame inverts as its transpose).
962        for i in 0..3 {
963            for j in 0..3 {
964                m[i][j] = (0..3).map(|k| rt[k][i] * rf[k][j]).sum();
965            }
966        }
967        // t = o_to − R · o_from
968        for i in 0..3 {
969            m[i][3] =
970                self.to.origin[i] - (0..3).map(|j| m[i][j] * self.from.origin[j]).sum::<f64>();
971        }
972        m[3][3] = 1.0;
973        Some(m)
974    }
975}
976
977/// A component usage in an assembly (`NEXT_ASSEMBLY_USAGE_OCCURRENCE`): the
978/// child definition plus where it is placed.
979#[derive(Clone, Copy)]
980pub struct Occurrence<'m> {
981    cx: Ctx<'m>,
982    id: m::NextAssemblyUsageOccurrenceId,
983}
984
985impl<'m> Occurrence<'m> {
986    fn raw(&self) -> &'m m::NextAssemblyUsageOccurrence {
987        self.cx
988            .model
989            .next_assembly_usage_occurrence_arena
990            .get(self.id.0)
991    }
992
993    /// This occurrence's global identity (a `Copy` key for maps / deduplication).
994    pub fn key(&self) -> m::EntityKey {
995        m::EntityKey::NextAssemblyUsageOccurrence(self.id)
996    }
997
998    /// The component definition this occurrence places (forward: `related`).
999    pub fn definition(&self) -> Option<ProductDef<'m>> {
1000        pdor_to_def(self.cx, &self.raw().related_product_definition)
1001    }
1002
1003    /// The component's placement transform in the assembly, if present (reverse:
1004    /// a `PRODUCT_DEFINITION_SHAPE` defined by this NAUO → a
1005    /// `CONTEXT_DEPENDENT_SHAPE_REPRESENTATION` → its transformation operator's
1006    /// two `AXIS2_PLACEMENT_3D` frames). A transform item that is not an
1007    /// `AXIS2_PLACEMENT_3D` is reported via [`Scene::warnings`](crate::scene::Scene)
1008    /// and yields `None` rather than a silently wrong frame.
1009    pub fn transform(&self) -> Option<Transform> {
1010        let cx = self.cx;
1011        let rg = cx.ref_graph();
1012        let me = self.id;
1013        for r in rg.referrers(m::EntityKey::NextAssemblyUsageOccurrence(me)) {
1014            let m::EntityKey::ProductDefinitionShape(pds_id) = r else {
1015                continue;
1016            };
1017            let pds = cx.model.product_definition_shape_arena.get(pds_id.0);
1018            if !matches!(&pds.definition, m::CharacterizedDefinitionRef::NextAssemblyUsageOccurrence(n) if *n == me)
1019            {
1020                continue;
1021            }
1022            for c in rg.referrers(m::EntityKey::ProductDefinitionShape(*pds_id)) {
1023                let m::EntityKey::ContextDependentShapeRepresentation(cdsr_id) = c else {
1024                    continue;
1025                };
1026                let cdsr = cx
1027                    .model
1028                    .context_dependent_shape_representation_arena
1029                    .get(cdsr_id.0);
1030                if !matches!(&cdsr.represented_product_relation, m::ProductDefinitionShapeRef::ProductDefinitionShape(p) if *p == *pds_id)
1031                {
1032                    continue;
1033                }
1034                if let Some(idt) = idt_of_cdsr(cx, &cdsr.representation_relation) {
1035                    let from = resolve_placement(cx, &idt.transform_item_1)?;
1036                    let to = resolve_placement(cx, &idt.transform_item_2)?;
1037                    return Some(Transform { from, to });
1038                }
1039            }
1040        }
1041        None
1042    }
1043}
1044
1045// ---------------------------------------------------------------------------
1046// MappedInstance (MAPPED_ITEM placement)
1047// ---------------------------------------------------------------------------
1048
1049/// Model-wide mapped-item enumeration.
1050impl Scene<'_> {
1051    /// Every `MAPPED_ITEM` in the model — the second placement mechanism
1052    /// besides [`Occurrence`]: a representation's geometry stamped from its
1053    /// origin frame onto a target frame (assembly component reuse, and
1054    /// drawing/PMI view instancing).
1055    pub fn all_mapped_instances(&self) -> impl Iterator<Item = MappedInstance<'_>> + '_ {
1056        let cx = self.ctx();
1057        (0..cx.model.mapped_item_arena.items.len()).map(move |i| MappedInstance {
1058            cx,
1059            id: m::MappedItemId(i),
1060        })
1061    }
1062}
1063
1064/// One `MAPPED_ITEM`: an instance of a source representation's geometry,
1065/// placed by a pair of frames (the map's origin and this item's target).
1066#[derive(Clone, Copy)]
1067pub struct MappedInstance<'m> {
1068    cx: Ctx<'m>,
1069    id: m::MappedItemId,
1070}
1071
1072impl<'m> MappedInstance<'m> {
1073    fn raw(&self) -> &'m m::MappedItem {
1074        self.cx.model.mapped_item_arena.get(self.id.0)
1075    }
1076
1077    pub fn name(&self) -> &'m str {
1078        &self.raw().name
1079    }
1080
1081    /// This instance's global identity (a `Copy` key for maps / deduplication).
1082    pub fn key(&self) -> m::EntityKey {
1083        m::EntityKey::MappedItem(self.id)
1084    }
1085
1086    fn map(&self) -> Option<&'m m::RepresentationMap> {
1087        let m::RepresentationMapRef::RepresentationMap(id) = &self.raw().mapping_source else {
1088            self.cx
1089                .warn("MAPPED_ITEM.mapping_source is not a REPRESENTATION_MAP".to_owned());
1090            return None;
1091        };
1092        Some(self.cx.model.representation_map_arena.get(id.0))
1093    }
1094
1095    /// The placement as two frames, like [`Occurrence::transform`]: the source
1096    /// geometry is defined relative to `from` (the map's origin) and placed at
1097    /// `to` (this item's target). A frame that is not an `AXIS2_PLACEMENT_3D`
1098    /// (a camera origin, a 2D drawing target) is reported via
1099    /// [`Scene::warnings`](crate::scene::Scene) and yields `None`.
1100    pub fn transform(&self) -> Option<Transform> {
1101        let cx = self.cx;
1102        let map = self.map()?;
1103        let from = resolve_placement(cx, &map.mapping_origin)?;
1104        let to = resolve_placement(cx, &self.raw().mapping_target)?;
1105        Some(Transform { from, to })
1106    }
1107
1108    /// The b-rep solids of the source representation — the geometry this
1109    /// instance stamps. A non-shape source (a presentation view) has none.
1110    pub fn solids(&self) -> Vec<Solid<'m>> {
1111        let cx = self.cx;
1112        let Some(map) = self.map() else {
1113            return Vec::new();
1114        };
1115        rep_items(cx, &map.mapped_representation)
1116            .into_iter()
1117            .flatten()
1118            .filter_map(|it| match it {
1119                m::RepresentationItemRef::ManifoldSolidBrep(id) => Some(Solid::from_id(cx, *id)),
1120                _ => None,
1121            })
1122            .collect()
1123    }
1124
1125    /// Nested mapped items inside the source representation — one level of an
1126    /// assembly chain; walk them to traverse the instance tree.
1127    pub fn mapped_children(&self) -> Vec<MappedInstance<'m>> {
1128        let cx = self.cx;
1129        let Some(map) = self.map() else {
1130            return Vec::new();
1131        };
1132        rep_items(cx, &map.mapped_representation)
1133            .into_iter()
1134            .flatten()
1135            .filter_map(|it| match it {
1136                m::RepresentationItemRef::MappedItem(id) => Some(MappedInstance { cx, id: *id }),
1137                _ => None,
1138            })
1139            .collect()
1140    }
1141}
1142
1143/// The `items` of a shape-carrying representation (the kinds mapped items
1144/// point at in practice); other representation kinds carry no b-rep items.
1145fn rep_items<'m>(cx: Ctx<'m>, r: &m::RepresentationRef) -> Option<&'m [m::RepresentationItemRef]> {
1146    match r {
1147        m::RepresentationRef::ShapeRepresentation(id) => {
1148            Some(&cx.model.shape_representation_arena.get(id.0).items)
1149        }
1150        m::RepresentationRef::AdvancedBrepShapeRepresentation(id) => Some(
1151            &cx.model
1152                .advanced_brep_shape_representation_arena
1153                .get(id.0)
1154                .items,
1155        ),
1156        m::RepresentationRef::GeometricallyBoundedWireframeShapeRepresentation(id) => Some(
1157            &cx.model
1158                .geometrically_bounded_wireframe_shape_representation_arena
1159                .get(id.0)
1160                .items,
1161        ),
1162        _ => None,
1163    }
1164}
1165
1166// ---------------------------------------------------------------------------
1167// Helpers
1168// ---------------------------------------------------------------------------
1169
1170/// The item-defined transformation behind a context-dependent shape
1171/// representation: its `representation_relation` is the complex
1172/// `(…, REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION, …)` whose operator is an
1173/// `ITEM_DEFINED_TRANSFORMATION`.
1174fn idt_of_cdsr<'m>(
1175    cx: Ctx<'m>,
1176    rr: &m::ShapeRepresentationRelationshipRef,
1177) -> Option<&'m m::ItemDefinedTransformation> {
1178    let m::ShapeRepresentationRelationshipRef::Complex(cuid) = rr else {
1179        return None;
1180    };
1181    let op = cx
1182        .model
1183        .complex_unit_arena
1184        .get(cuid.0)
1185        .parts
1186        .iter()
1187        .find_map(|p| match p {
1188            m::UnitPart::RepresentationRelationshipWithTransformation {
1189                transformation_operator,
1190            } => Some(transformation_operator),
1191            _ => None,
1192        })?;
1193    let m::TransformationRef::ItemDefinedTransformation(id) = op else {
1194        return None;
1195    };
1196    Some(cx.model.item_defined_transformation_arena.get(id.0))
1197}
1198
1199/// Resolve a transform item to a [`Placement`]. Only `AXIS2_PLACEMENT_3D` is a
1200/// full 3D frame; anything else is recorded as a warning and yields `None`
1201/// (the other placement kinds store values in their own coordinate system, so
1202/// padding them with default axes would be wrong).
1203fn resolve_placement(cx: Ctx<'_>, item: &m::RepresentationItemRef) -> Option<Placement> {
1204    let m::RepresentationItemRef::Axis2Placement3d(id) = item else {
1205        cx.warn("transform item is not an AXIS2_PLACEMENT_3D".to_owned());
1206        return None;
1207    };
1208    let a = cx.model.axis2_placement3d_arena.get(id.0);
1209    Some(Placement {
1210        origin: point3(cx, &a.location),
1211        axis: a.axis.as_ref().map_or([0.0, 0.0, 1.0], |d| dir3(cx, d)),
1212        ref_direction: a
1213            .ref_direction
1214            .as_ref()
1215            .map_or([1.0, 0.0, 0.0], |d| dir3(cx, d)),
1216    })
1217}
1218
1219/// A cartesian point's coordinates as a 3-vector (missing components are 0).
1220fn point3(cx: Ctx<'_>, r: &m::CartesianPointRef) -> [f64; 3] {
1221    if let m::CartesianPointRef::CartesianPoint(id) = r {
1222        let c = &cx.model.cartesian_point_arena.get(id.0).coordinates;
1223        [
1224            c.first().copied().unwrap_or(0.0),
1225            c.get(1).copied().unwrap_or(0.0),
1226            c.get(2).copied().unwrap_or(0.0),
1227        ]
1228    } else {
1229        [0.0; 3]
1230    }
1231}
1232
1233/// A direction's ratios as a 3-vector (missing components are 0).
1234fn dir3(cx: Ctx<'_>, r: &m::DirectionRef) -> [f64; 3] {
1235    if let m::DirectionRef::Direction(id) = r {
1236        let c = &cx.model.direction_arena.get(id.0).direction_ratios;
1237        [
1238            c.first().copied().unwrap_or(0.0),
1239            c.get(1).copied().unwrap_or(0.0),
1240            c.get(2).copied().unwrap_or(0.0),
1241        ]
1242    } else {
1243        [0.0; 3]
1244    }
1245}
1246
1247/// Whether a `ProductDefinitionOrReferenceRef` is exactly this plain definition.
1248fn pdor_is(r: &m::ProductDefinitionOrReferenceRef, id: m::ProductDefinitionId) -> bool {
1249    matches!(r, m::ProductDefinitionOrReferenceRef::ProductDefinition(i) if *i == id)
1250}
1251
1252/// Resolve a `ProductDefinitionOrReferenceRef` to a plain definition handle
1253/// (occurrence / generic-reference / complex variants are out of scope).
1254fn pdor_to_def<'m>(cx: Ctx<'m>, r: &m::ProductDefinitionOrReferenceRef) -> Option<ProductDef<'m>> {
1255    match r {
1256        m::ProductDefinitionOrReferenceRef::ProductDefinition(i) => Some(ProductDef { cx, id: *i }),
1257        _ => None,
1258    }
1259}
1260
1261/// Push every `MANIFOLD_SOLID_BREP` reachable from a representation's `items`,
1262/// following plain `SHAPE_REPRESENTATION_RELATIONSHIP` (shape equivalence) to
1263/// bridged representations. The common assembly export puts a component's
1264/// placement in one `SHAPE_REPRESENTATION` and its geometry in a separate
1265/// `ADVANCED_BREP_SHAPE_REPRESENTATION`, linked by such a relationship. The
1266/// `WITH_TRANSFORMATION` variant is a complex instance (not in this arena), so
1267/// occurrence placement is never followed here. `visited` guards cycles.
1268fn collect_solids_from_repr<'m>(
1269    cx: Ctx<'m>,
1270    r: &m::RepresentationRef,
1271    out: &mut Vec<Solid<'m>>,
1272    visited: &mut HashSet<m::EntityKey>,
1273) {
1274    let (key, items): (m::EntityKey, &[m::RepresentationItemRef]) = match r {
1275        m::RepresentationRef::ShapeRepresentation(i) => (
1276            m::EntityKey::ShapeRepresentation(*i),
1277            &cx.model.shape_representation_arena.get(i.0).items,
1278        ),
1279        m::RepresentationRef::AdvancedBrepShapeRepresentation(i) => (
1280            m::EntityKey::AdvancedBrepShapeRepresentation(*i),
1281            &cx.model
1282                .advanced_brep_shape_representation_arena
1283                .get(i.0)
1284                .items,
1285        ),
1286        _ => return,
1287    };
1288    if !visited.insert(key) {
1289        return;
1290    }
1291    for it in items {
1292        if let m::RepresentationItemRef::ManifoldSolidBrep(sid) = it {
1293            out.push(Solid::from_id(cx, *sid));
1294        }
1295    }
1296    // Hop across plain shape-equivalence relationships to the other endpoint.
1297    let rg = cx.ref_graph();
1298    for referrer in rg.referrers(key) {
1299        let m::EntityKey::ShapeRepresentationRelationship(srr_id) = referrer else {
1300            continue;
1301        };
1302        let srr = cx
1303            .model
1304            .shape_representation_relationship_arena
1305            .get(srr_id.0);
1306        let (k1, k2) = (srr.rep_1.entity_key(), srr.rep_2.entity_key());
1307        let other = if k1 == key {
1308            k2
1309        } else if k2 == key {
1310            k1
1311        } else {
1312            continue;
1313        };
1314        if let Ok(other_ref) = m::RepresentationRef::from_any(other) {
1315            collect_solids_from_repr(cx, &other_ref, out, visited);
1316        }
1317    }
1318}
1319
1320// ---------------------------------------------------------------------------
1321// Approval / Approver (management metadata: a part's approval state and who
1322// authorised it)
1323// ---------------------------------------------------------------------------
1324
1325/// An approval on a part (`APPROVAL`) — its status, level, and approvers.
1326#[derive(Clone, Copy)]
1327pub struct Approval<'m> {
1328    cx: Ctx<'m>,
1329    id: m::ApprovalId,
1330    scope: Scope,
1331}
1332
1333impl<'m> Approval<'m> {
1334    fn raw(&self) -> &'m m::Approval {
1335        self.cx.model.approval_arena.get(self.id.0)
1336    }
1337
1338    /// This approval's global identity (a `Copy` key for maps / dedup).
1339    pub fn key(&self) -> m::EntityKey {
1340        m::EntityKey::Approval(self.id)
1341    }
1342
1343    /// Which level of the product structure this approval attaches to (distinct
1344    /// from [`Approval::level`], the approval's own status level).
1345    pub fn assigned_scope(&self) -> Scope {
1346        self.scope
1347    }
1348
1349    /// The exact product-structure entities this approval is assigned to (across
1350    /// every assignment that carries it).
1351    pub fn assigned_targets(&self) -> Vec<Target<'m>> {
1352        let cx = self.cx;
1353        let mut keys: Vec<m::EntityKey> = Vec::new();
1354        for r in cx.ref_graph().referrers(self.key()) {
1355            match r {
1356                m::EntityKey::CcDesignApproval(i) => keys.extend(
1357                    cx.model
1358                        .cc_design_approval_arena
1359                        .get(i.0)
1360                        .items
1361                        .iter()
1362                        .map(m::ApprovedItemRef::entity_key),
1363                ),
1364                m::EntityKey::AppliedApprovalAssignment(i) => keys.extend(
1365                    cx.model
1366                        .applied_approval_assignment_arena
1367                        .get(i.0)
1368                        .items
1369                        .iter()
1370                        .map(m::ApprovalItemRef::entity_key),
1371                ),
1372                _ => {}
1373            }
1374        }
1375        targets_from_keys(cx, keys.into_iter())
1376    }
1377
1378    /// The approval status name (`approved`, `not_yet_approved`, …).
1379    pub fn status(&self) -> &'m str {
1380        let m::ApprovalStatusRef::ApprovalStatus(sid) = &self.raw().status;
1381        &self.cx.model.approval_status_arena.get(sid.0).name
1382    }
1383
1384    /// The approval level.
1385    pub fn level(&self) -> &'m str {
1386        &self.raw().level
1387    }
1388
1389    /// Who authorised this approval, with their role (reverse: an
1390    /// `APPROVAL_PERSON_ORGANIZATION` whose `authorized_approval` is this approval).
1391    pub fn approvers(&self) -> Vec<Approver<'m>> {
1392        let cx = self.cx;
1393        let mut out: Vec<Approver<'m>> = Vec::new();
1394        for r in cx.ref_graph().referrers(self.key()) {
1395            if let m::EntityKey::ApprovalPersonOrganization(id) = r {
1396                out.push(Approver { cx, id: *id });
1397            }
1398        }
1399        out
1400    }
1401
1402    /// When this approval was made (reverse: an `APPROVAL_DATE_TIME` whose
1403    /// `dated_approval` is this approval), flattened into an [`ApprovalDate`].
1404    ///
1405    /// `None` when no date is recorded. A top-level complex date instance cannot
1406    /// be decoded into calendar/clock fields — it also yields `None`, but is
1407    /// surfaced through [`Scene::warnings`](crate::scene::Scene::warnings) so the
1408    /// "no date" and "unreadable date" cases stay distinguishable.
1409    pub fn date(&self) -> Option<ApprovalDate> {
1410        let cx = self.cx;
1411        for r in cx.ref_graph().referrers(self.key()) {
1412            if let m::EntityKey::ApprovalDateTime(id) = r {
1413                let dt = &cx.model.approval_date_time_arena.get(id.0).date_time;
1414                return resolve_datetime(cx, dt);
1415            }
1416        }
1417        None
1418    }
1419}
1420
1421/// A calendar/clock instant flattened out of the STEP `date_time_select` union
1422/// (`CALENDAR_DATE` / `DATE` / `DATE_AND_TIME` / `LOCAL_TIME`). Components absent
1423/// from the source stay `None`; seconds and time-zone are intentionally out of
1424/// scope for now.
1425#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1426pub struct ApprovalDate {
1427    pub year: Option<i64>,
1428    pub month: Option<i64>,
1429    pub day: Option<i64>,
1430    pub hour: Option<i64>,
1431    pub minute: Option<i64>,
1432}
1433
1434/// Flatten a `date_time_select` into an [`ApprovalDate`]. Every non-complex arm
1435/// fills at least one component (calendar dates carry year/month/day, local
1436/// times carry the hour), so an all-`None` result only arises from a top-level
1437/// complex instance — which is reported as `None` (see [`Approval::date`]).
1438fn resolve_datetime(cx: Ctx<'_>, r: &m::DateTimeSelectRef) -> Option<ApprovalDate> {
1439    let mut d = ApprovalDate::default();
1440    match r {
1441        m::DateTimeSelectRef::CalendarDate(i) => set_calendar(cx, &mut d, *i),
1442        m::DateTimeSelectRef::Date(i) => {
1443            d.year = Some(cx.model.date_arena.get(i.0).year_component);
1444        }
1445        m::DateTimeSelectRef::DateAndTime(i) => {
1446            let dt = cx.model.date_and_time_arena.get(i.0);
1447            set_date(cx, &mut d, &dt.date_component);
1448            let m::LocalTimeRef::LocalTime(lt) = &dt.time_component;
1449            set_time(cx, &mut d, *lt);
1450        }
1451        m::DateTimeSelectRef::LocalTime(i) => set_time(cx, &mut d, *i),
1452        m::DateTimeSelectRef::Complex(_) => {
1453            cx.warn(
1454                "APPROVAL_DATE_TIME.date_time is a complex instance; date left unread".to_owned(),
1455            );
1456            return None;
1457        }
1458    }
1459    Some(d)
1460}
1461
1462/// Set year/month/day from a `date_select` (a complex date part is left unread).
1463fn set_date(cx: Ctx<'_>, d: &mut ApprovalDate, r: &m::DateRef) {
1464    match r {
1465        m::DateRef::CalendarDate(i) => set_calendar(cx, d, *i),
1466        m::DateRef::Date(i) => d.year = Some(cx.model.date_arena.get(i.0).year_component),
1467        m::DateRef::Complex(_) => {}
1468    }
1469}
1470
1471fn set_calendar(cx: Ctx<'_>, d: &mut ApprovalDate, i: m::CalendarDateId) {
1472    let c = cx.model.calendar_date_arena.get(i.0);
1473    d.year = Some(c.year_component);
1474    d.month = Some(c.month_component);
1475    d.day = Some(c.day_component);
1476}
1477
1478fn set_time(cx: Ctx<'_>, d: &mut ApprovalDate, i: m::LocalTimeId) {
1479    let t = cx.model.local_time_arena.get(i.0);
1480    d.hour = Some(t.hour_component);
1481    d.minute = t.minute_component;
1482}
1483
1484/// The person / organization that authorised an approval, and their role.
1485#[derive(Clone, Copy)]
1486pub struct Approver<'m> {
1487    cx: Ctx<'m>,
1488    id: m::ApprovalPersonOrganizationId,
1489}
1490
1491impl<'m> Approver<'m> {
1492    fn raw(&self) -> &'m m::ApprovalPersonOrganization {
1493        self.cx
1494            .model
1495            .approval_person_organization_arena
1496            .get(self.id.0)
1497    }
1498
1499    /// The approver's role (`approver`, `security_approval`, …).
1500    pub fn role(&self) -> &'m str {
1501        let m::ApprovalRoleRef::ApprovalRole(rid) = &self.raw().role;
1502        &self.cx.model.approval_role_arena.get(rid.0).role
1503    }
1504
1505    /// The approver's person, if the approver is a person or a
1506    /// person-and-organization.
1507    pub fn person(&self) -> Option<Person<'m>> {
1508        let pid = match &self.raw().person_organization {
1509            m::PersonOrganizationSelectRef::Person(i) => *i,
1510            m::PersonOrganizationSelectRef::PersonAndOrganization(i) => {
1511                let m::PersonRef::Person(p) = &self
1512                    .cx
1513                    .model
1514                    .person_and_organization_arena
1515                    .get(i.0)
1516                    .the_person;
1517                *p
1518            }
1519            m::PersonOrganizationSelectRef::Organization(_) => return None,
1520        };
1521        Some(Person {
1522            cx: self.cx,
1523            id: pid,
1524        })
1525    }
1526
1527    /// The approver's organization name, if the approver is an organization or a
1528    /// person-and-organization.
1529    pub fn organization(&self) -> Option<&'m str> {
1530        let oid = match &self.raw().person_organization {
1531            m::PersonOrganizationSelectRef::Organization(i) => *i,
1532            m::PersonOrganizationSelectRef::PersonAndOrganization(i) => {
1533                let m::OrganizationRef::Organization(o) = &self
1534                    .cx
1535                    .model
1536                    .person_and_organization_arena
1537                    .get(i.0)
1538                    .the_organization;
1539                *o
1540            }
1541            m::PersonOrganizationSelectRef::Person(_) => return None,
1542        };
1543        Some(&self.cx.model.organization_arena.get(oid.0).name)
1544    }
1545}
1546
1547#[derive(Clone, Copy)]
1548enum DocImpl {
1549    Document(m::DocumentId),
1550    DocumentFile(m::DocumentFileId),
1551}
1552
1553/// A document referenced by a part — a drawing, specification, or standard.
1554///
1555/// Unifies STEP's `DOCUMENT` and `DOCUMENT_FILE`, which share id / name /
1556/// description / kind (the file-specific fields of `DOCUMENT_FILE` are out of
1557/// scope for now).
1558#[derive(Clone, Copy)]
1559pub struct Document<'m> {
1560    cx: Ctx<'m>,
1561    which: DocImpl,
1562    scope: Scope,
1563}
1564
1565impl<'m> Document<'m> {
1566    /// Which level of the product structure this document reference attaches to.
1567    pub fn assigned_scope(&self) -> Scope {
1568        self.scope
1569    }
1570
1571    /// The exact product-structure entities this document is referenced from
1572    /// (across every `APPLIED_DOCUMENT_REFERENCE` that carries it).
1573    pub fn assigned_targets(&self) -> Vec<Target<'m>> {
1574        let cx = self.cx;
1575        let mut keys: Vec<m::EntityKey> = Vec::new();
1576        for r in cx.ref_graph().referrers(self.key()) {
1577            if let m::EntityKey::AppliedDocumentReference(i) = r {
1578                keys.extend(
1579                    cx.model
1580                        .applied_document_reference_arena
1581                        .get(i.0)
1582                        .items
1583                        .iter()
1584                        .map(m::DocumentReferenceItemRef::entity_key),
1585                );
1586            }
1587        }
1588        targets_from_keys(cx, keys.into_iter())
1589    }
1590
1591    /// The four shared fields (id, name, description, kind), resolved once so the
1592    /// public accessors don't each repeat the backing-type match.
1593    fn fields(&self) -> (&'m str, &'m str, Option<&'m str>, &'m m::DocumentTypeRef) {
1594        let model = self.cx.model;
1595        match self.which {
1596            DocImpl::Document(i) => {
1597                let d = model.document_arena.get(i.0);
1598                (&d.id, &d.name, d.description.as_deref(), &d.kind)
1599            }
1600            DocImpl::DocumentFile(i) => {
1601                let d = model.document_file_arena.get(i.0);
1602                (&d.id, &d.name, d.description.as_deref(), &d.kind)
1603            }
1604        }
1605    }
1606
1607    /// This document's global identity (a `Copy` key for maps / dedup).
1608    pub fn key(&self) -> m::EntityKey {
1609        match self.which {
1610            DocImpl::Document(i) => m::EntityKey::Document(i),
1611            DocImpl::DocumentFile(i) => m::EntityKey::DocumentFile(i),
1612        }
1613    }
1614
1615    /// The document identifier (`DOC-1`, a part number, …).
1616    pub fn id(&self) -> &'m str {
1617        self.fields().0
1618    }
1619
1620    /// The document name / title.
1621    pub fn name(&self) -> &'m str {
1622        self.fields().1
1623    }
1624
1625    /// The document description, if any.
1626    pub fn description(&self) -> Option<&'m str> {
1627        self.fields().2
1628    }
1629
1630    /// The document kind (the `product_data_type` of its `DOCUMENT_TYPE`, e.g.
1631    /// `drawing`, `specification`).
1632    pub fn kind(&self) -> &'m str {
1633        let m::DocumentTypeRef::DocumentType(tid) = self.fields().3;
1634        &self
1635            .cx
1636            .model
1637            .document_type_arena
1638            .get(tid.0)
1639            .product_data_type
1640    }
1641}
1642
1643/// A security classification assigned to a part — its confidentiality level
1644/// (`confidential`, `export-controlled`, …) and the reason for it.
1645#[derive(Clone, Copy)]
1646pub struct SecurityClassification<'m> {
1647    cx: Ctx<'m>,
1648    id: m::SecurityClassificationId,
1649    scope: Scope,
1650}
1651
1652impl<'m> SecurityClassification<'m> {
1653    fn raw(&self) -> &'m m::SecurityClassification {
1654        self.cx.model.security_classification_arena.get(self.id.0)
1655    }
1656
1657    /// This classification's global identity (a `Copy` key for maps / dedup).
1658    pub fn key(&self) -> m::EntityKey {
1659        m::EntityKey::SecurityClassification(self.id)
1660    }
1661
1662    /// Which level of the product structure this classification attaches to.
1663    pub fn assigned_scope(&self) -> Scope {
1664        self.scope
1665    }
1666
1667    /// The exact product-structure entities this classification is assigned to
1668    /// (across every assignment that carries it).
1669    pub fn assigned_targets(&self) -> Vec<Target<'m>> {
1670        let cx = self.cx;
1671        let mut keys: Vec<m::EntityKey> = Vec::new();
1672        for r in cx.ref_graph().referrers(self.key()) {
1673            match r {
1674                m::EntityKey::CcDesignSecurityClassification(i) => keys.extend(
1675                    cx.model
1676                        .cc_design_security_classification_arena
1677                        .get(i.0)
1678                        .items
1679                        .iter()
1680                        .map(m::CcClassifiedItemRef::entity_key),
1681                ),
1682                m::EntityKey::AppliedSecurityClassificationAssignment(i) => keys.extend(
1683                    cx.model
1684                        .applied_security_classification_assignment_arena
1685                        .get(i.0)
1686                        .items
1687                        .iter()
1688                        .map(m::SecurityClassificationItemRef::entity_key),
1689                ),
1690                _ => {}
1691            }
1692        }
1693        targets_from_keys(cx, keys.into_iter())
1694    }
1695
1696    /// The classification name / identifier.
1697    pub fn name(&self) -> &'m str {
1698        &self.raw().name
1699    }
1700
1701    /// Why the part is classified.
1702    pub fn purpose(&self) -> &'m str {
1703        &self.raw().purpose
1704    }
1705
1706    /// The confidentiality level (the `name` of its `SECURITY_CLASSIFICATION_LEVEL`,
1707    /// e.g. `confidential`, `unclassified`).
1708    pub fn level(&self) -> &'m str {
1709        let m::SecurityClassificationLevelRef::SecurityClassificationLevel(lid) =
1710            &self.raw().security_level;
1711        &self
1712            .cx
1713            .model
1714            .security_classification_level_arena
1715            .get(lid.0)
1716            .name
1717    }
1718}