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