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 let index = self.history.index_of(head)?;
49 self.history
50 .feature_type(index)
51 .filter(|ty| is_acomp_feature_type(ty))
52 .map(|_| head.to_string())
53 }
54
55 /// Every ACOMP feature id in history order (the structure tree's row order).
56 pub fn component_ids(&self) -> Vec<String> {
57 (0..self.history.len())
58 .filter(|&i| {
59 self.history
60 .feature_type(i)
61 .is_some_and(|ty| is_acomp_feature_type(&ty))
62 })
63 .filter_map(|i| self.history.feature_id(i))
64 .collect()
65 }
66
67 /// The derived [`ComponentInfo`] for an ACOMP feature id (`None` when the id
68 /// is missing or not an ACOMP feature).
69 pub fn component_info(&self, feature_id: &str) -> Option<ComponentInfo> {
70 let index = self.history.index_of(feature_id)?;
71 self.history
72 .feature_type(index)
73 .filter(|ty| is_acomp_feature_type(ty))?;
74 let params = self.history.feature_params(index).unwrap_or_default();
75 let transform = params.get("transform");
76 let translate = vec3_or(transform.and_then(|t| t.get("translate")), [0.0; 3]);
77 let rotate_deg = vec3_or(transform.and_then(|t| t.get("rotateEulerDeg")), [0.0; 3]);
78 // Grounded: explicit boolean wins; ABSENT (or null) mirrors the kernel's
79 // auto-ground — the FIRST component of the document is grounded.
80 let fixed = match params.get("isFixed") {
81 Some(serde_json::Value::Bool(flag)) => *flag,
82 _ => self
83 .component_ids()
84 .first()
85 .is_some_and(|first| first == feature_id),
86 };
87 let members = self.member_solid_names(feature_id);
88 Some(ComponentInfo {
89 id: feature_id.to_string(),
90 part_name: params
91 .get("partName")
92 .and_then(|v| v.as_str())
93 .unwrap_or_default()
94 .to_string(),
95 translate,
96 rotate_deg,
97 fixed,
98 members,
99 })
100 }
101
102 /// The world bbox CENTER of a component's member solids — the Move gizmo's
103 /// attach point. `None` when the component has no resident members (rolled
104 /// back / failed build).
105 pub fn component_bbox_center(&self, feature_id: &str) -> Option<[f64; 3]> {
106 let mut bbox = crate::camera::Aabb::empty();
107 for solid in self.scene.solids() {
108 if owned_by(&solid.name, feature_id) {
109 bbox.union(&solid.bbox);
110 }
111 }
112 (!bbox.is_empty()).then(|| bbox.center())
113 }
114
115 /// The member solid names of ACOMP `feature_id`, in display-scene order.
116 /// The caller has already resolved `feature_id` to an ACOMP feature of
117 /// THIS document, so ownership is the outermost namespace segment alone —
118 /// no per-solid history lookup. Routing every solid through
119 /// [`Self::component_of_solid`] (a linear `index_of` scan each) made this
120 /// O(solids × features); the BOM paid it once per row per frame.
121 fn member_solid_names(&self, feature_id: &str) -> Vec<String> {
122 self.scene
123 .solids()
124 .iter()
125 .filter(|solid| owned_by(&solid.name, feature_id))
126 .map(|solid| solid.name.clone())
127 .collect()
128 }
129}
130
131/// Whether `solid_name`'s OUTERMOST `ACOMP<digits>:` segment is `feature_id`.
132/// A nested chain's inner segments belong to the sub-assembly's own document,
133/// exactly as [`EngineState::component_of_solid`] reads them.
134fn owned_by(solid_name: &str, feature_id: &str) -> bool {
135 let (chain, _local) = brep_kernel::split_component_namespace(solid_name);
136 chain.first().copied() == Some(feature_id)
137}
138
139