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