Skip to main content

brep_render/engine_state/
components.rs

1use crate::json_support::vec3_or;
2use super::*;
3
4// Components are derived from authored ACOMP features and namespaced display
5// solids. Kernel records stay on the runner; namespace parsing uses the kernel's
6// shared parser so both sides agree on component ownership.
7
8/// One assembly component instance as the APP sees it: the owning ACOMP feature
9/// id, the parts-library part name, the authored rigid pose (`inputParams.
10/// transform` — `{translate, rotateEulerDeg}`, intrinsic-XYZ degrees), the
11/// grounded flag, and the member solid names currently in the display scene.
12#[derive(Debug, Clone, PartialEq)]
13pub struct ComponentInfo {
14    /// The owning ACOMP feature id (`ACOMP<digits>`) — also the namespace prefix.
15    pub id: String,
16    /// The parts-library entry this instance places (`inputParams.partName`).
17    pub part_name: String,
18    /// `inputParams.transform.translate` (numbers-or-zero: an expression-valued
19    /// slot reads 0 here, matching the transform gizmo's numeric read).
20    pub translate: [f64; 3],
21    /// `inputParams.transform.rotateEulerDeg` (degrees, intrinsic XYZ).
22    pub rotate_deg: [f64; 3],
23    /// Grounded: an explicit `isFixed` wins; ABSENT mirrors the kernel's
24    /// auto-ground rule (grounded iff no ACOMP precedes it in the history).
25    pub fixed: bool,
26    /// Member solid names in display-scene order (`{id}:{part solid name}`).
27    /// Empty when the instance is rolled back / failed to build.
28    pub members: Vec<String>,
29}
30
31/// The ACOMP dispatch predicate, mirroring the kernel's `is_acomp_type` (the
32/// two literals `execute_feature` matches on).
33pub(crate) fn is_acomp_feature_type(feature_type: &str) -> bool {
34    matches!(feature_type, "ACOMP" | "ASSEMBLY COMPONENT")
35}
36
37
38impl EngineState {
39    /// The OWNING component feature id of a scene solid — the OUTERMOST
40    /// `ACOMP<digits>:` namespace segment of its name, verified against the
41    /// history (the segment must be an ACOMP feature of THIS document; a nested
42    /// chain's inner segments belong to the sub-assembly's own document).
43    /// `None` for ordinary modeling solids (no prefix) and for sketch-child
44    /// names (`S1:G20` — `S1` is not an ACOMP segment).
45    pub fn component_of_solid(&self, solid_name: &str) -> Option<String> {
46        let (chain, _local) = brep_kernel::split_component_namespace(solid_name);
47        let head = *chain.first()?;
48        // A Pattern Component instance is a component its pattern emits.
49        if brep_kernel::pattern_component_instance(head).is_some() {
50            return self.assembly_components().iter().any(|c| c.id == head).then(|| head.to_string());
51        }
52        let index = self.history.index_of(head)?;
53        self.history
54            .feature_type(index)
55            .filter(|ty| is_acomp_feature_type(ty))
56            .map(|_| head.to_string())
57    }
58
59    /// The owning top-level occurrence of every selected vertex (vertices
60    /// carry no names, so the app's selection read cannot resolve them), or
61    /// `None` when any selected vertex is not component geometry.
62    pub fn selected_vertex_components(&self) -> Option<Vec<String>> {
63        self.emphasis
64            .selected_vertices
65            .iter()
66            .map(|vertex| self.component_of_solid(&vertex.solid))
67            .collect()
68    }
69
70    /// Every ACOMP feature id in history order (the structure tree's row order).
71    pub fn component_ids(&self) -> Vec<String> {
72        (0..self.history.len())
73            .filter(|&i| {
74                self.history
75                    .feature_type(i)
76                    .is_some_and(|ty| is_acomp_feature_type(&ty))
77            })
78            .filter_map(|i| self.history.feature_id(i))
79            .collect()
80    }
81
82    /// The derived [`ComponentInfo`] for an ACOMP feature id (`None` when the id
83    /// is missing or not an ACOMP feature).
84    pub fn component_info(&self, feature_id: &str) -> Option<ComponentInfo> {
85        let index = self.history.index_of(feature_id)?;
86        self.history
87            .feature_type(index)
88            .filter(|ty| is_acomp_feature_type(ty))?;
89        let params = self.history.feature_params(index).unwrap_or_default();
90        let transform = params.get("transform");
91        let translate = vec3_or(transform.and_then(|t| t.get("translate")), [0.0; 3]);
92        let rotate_deg = vec3_or(transform.and_then(|t| t.get("rotateEulerDeg")), [0.0; 3]);
93        // Grounded: explicit boolean wins; ABSENT (or null) mirrors the kernel's
94        // auto-ground — the FIRST component of the document is grounded.
95        let fixed = match params.get("isFixed") {
96            Some(serde_json::Value::Bool(flag)) => *flag,
97            _ => self
98                .component_ids()
99                .first()
100                .is_some_and(|first| first == feature_id),
101        };
102        let members = self.member_solid_names(feature_id);
103        Some(ComponentInfo {
104            id: feature_id.to_string(),
105            part_name: params
106                .get("partName")
107                .and_then(|v| v.as_str())
108                .unwrap_or_default()
109                .to_string(),
110            translate,
111            rotate_deg,
112            fixed,
113            members,
114        })
115    }
116
117    /// The world bbox CENTER of a component's member solids — the Move gizmo's
118    /// attach point. `None` when the component has no resident members (rolled
119    /// back / failed build).
120    pub fn component_bbox_center(&self, feature_id: &str) -> Option<[f64; 3]> {
121        let mut bbox = crate::camera::Aabb::empty();
122        for solid in self.scene.solids() {
123            if owned_by(&solid.name, feature_id) {
124                bbox.union(&solid.bbox);
125            }
126        }
127        (!bbox.is_empty()).then(|| bbox.center())
128    }
129
130    /// The member solids of a placed component or of a Pattern Component
131    /// instance (which has no feature of its own).
132    pub fn component_members(&self, id: &str) -> Vec<String> {
133        match self.component_info(id) {
134            Some(info) => info.members,
135            None => self.member_solid_names(id),
136        }
137    }
138
139    /// Every Pattern Component instance the last run placed, in run order.
140    pub fn pattern_instance_ids(&self) -> Vec<String> {
141        self.assembly_components()
142            .iter()
143            .filter(|c| brep_kernel::pattern_component_instance(&c.id).is_some())
144            .map(|c| c.id.clone())
145            .collect()
146    }
147
148    /// The member solid names of ACOMP `feature_id`, in display-scene order.
149    /// The caller has already resolved `feature_id` to an ACOMP feature of
150    /// THIS document, so ownership is the outermost namespace segment alone —
151    /// no per-solid history lookup. Routing every solid through
152    /// [`Self::component_of_solid`] (a linear `index_of` scan each) made this
153    /// O(solids × features); the BOM paid it once per row per frame.
154    pub(super) fn member_solid_names(&self, feature_id: &str) -> Vec<String> {
155        self.scene
156            .solids()
157            .iter()
158            .filter(|solid| owned_by(&solid.name, feature_id))
159            .map(|solid| solid.name.clone())
160            .collect()
161    }
162}
163
164/// Whether `solid_name`'s OUTERMOST `ACOMP<digits>:` segment is `feature_id`.
165/// A nested chain's inner segments belong to the sub-assembly's own document,
166/// exactly as [`EngineState::component_of_solid`] reads them.
167pub(super) fn owned_by(solid_name: &str, feature_id: &str) -> bool {
168    let (chain, _local) = brep_kernel::split_component_namespace(solid_name);
169    chain.first().copied() == Some(feature_id)
170}
171
172