1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
use crate::json_support::vec3_or;
use super::*;
// Components are derived from authored ACOMP features and namespaced display
// solids. Kernel records stay on the runner; namespace parsing uses the kernel's
// shared parser so both sides agree on component ownership.
/// One assembly component instance as the APP sees it: the owning ACOMP feature
/// id, the parts-library part name, the authored rigid pose (`inputParams.
/// transform` — `{translate, rotateEulerDeg}`, intrinsic-XYZ degrees), the
/// grounded flag, and the member solid names currently in the display scene.
#[derive(Debug, Clone, PartialEq)]
pub struct ComponentInfo {
/// The owning ACOMP feature id (`ACOMP<digits>`) — also the namespace prefix.
pub id: String,
/// The parts-library entry this instance places (`inputParams.partName`).
pub part_name: String,
/// `inputParams.transform.translate` (numbers-or-zero: an expression-valued
/// slot reads 0 here, matching the transform gizmo's numeric read).
pub translate: [f64; 3],
/// `inputParams.transform.rotateEulerDeg` (degrees, intrinsic XYZ).
pub rotate_deg: [f64; 3],
/// Grounded: an explicit `isFixed` wins; ABSENT mirrors the kernel's
/// auto-ground rule (grounded iff no ACOMP precedes it in the history).
pub fixed: bool,
/// Member solid names in display-scene order (`{id}:{part solid name}`).
/// Empty when the instance is rolled back / failed to build.
pub members: Vec<String>,
}
/// The ACOMP dispatch predicate, mirroring the kernel's `is_acomp_type` (the
/// two literals `execute_feature` matches on).
pub(crate) fn is_acomp_feature_type(feature_type: &str) -> bool {
matches!(feature_type, "ACOMP" | "ASSEMBLY COMPONENT")
}
impl EngineState {
/// The OWNING component feature id of a scene solid — the OUTERMOST
/// `ACOMP<digits>:` namespace segment of its name, verified against the
/// history (the segment must be an ACOMP feature of THIS document; a nested
/// chain's inner segments belong to the sub-assembly's own document).
/// `None` for ordinary modeling solids (no prefix) and for sketch-child
/// names (`S1:G20` — `S1` is not an ACOMP segment).
pub fn component_of_solid(&self, solid_name: &str) -> Option<String> {
let (chain, _local) = brep_kernel::split_component_namespace(solid_name);
let head = *chain.first()?;
let index = self.history.index_of(head)?;
self.history
.feature_type(index)
.filter(|ty| is_acomp_feature_type(ty))
.map(|_| head.to_string())
}
/// Every ACOMP feature id in history order (the structure tree's row order).
pub fn component_ids(&self) -> Vec<String> {
(0..self.history.len())
.filter(|&i| {
self.history
.feature_type(i)
.is_some_and(|ty| is_acomp_feature_type(&ty))
})
.filter_map(|i| self.history.feature_id(i))
.collect()
}
/// The derived [`ComponentInfo`] for an ACOMP feature id (`None` when the id
/// is missing or not an ACOMP feature).
pub fn component_info(&self, feature_id: &str) -> Option<ComponentInfo> {
let index = self.history.index_of(feature_id)?;
self.history
.feature_type(index)
.filter(|ty| is_acomp_feature_type(ty))?;
let params = self.history.feature_params(index).unwrap_or_default();
let transform = params.get("transform");
let translate = vec3_or(transform.and_then(|t| t.get("translate")), [0.0; 3]);
let rotate_deg = vec3_or(transform.and_then(|t| t.get("rotateEulerDeg")), [0.0; 3]);
// Grounded: explicit boolean wins; ABSENT (or null) mirrors the kernel's
// auto-ground — the FIRST component of the document is grounded.
let fixed = match params.get("isFixed") {
Some(serde_json::Value::Bool(flag)) => *flag,
_ => self
.component_ids()
.first()
.is_some_and(|first| first == feature_id),
};
let members: Vec<String> = self
.scene
.solids()
.iter()
.filter(|solid| self.component_of_solid(&solid.name).as_deref() == Some(feature_id))
.map(|solid| solid.name.clone())
.collect();
Some(ComponentInfo {
id: feature_id.to_string(),
part_name: params
.get("partName")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
translate,
rotate_deg,
fixed,
members,
})
}
/// The world bbox CENTER of a component's member solids — the Move gizmo's
/// attach point (build-spec §8.5). `None` when the component has no resident
/// members (rolled back / failed build).
pub fn component_bbox_center(&self, feature_id: &str) -> Option<[f64; 3]> {
let mut bbox = crate::camera::Aabb::empty();
for solid in self.scene.solids() {
if self.component_of_solid(&solid.name).as_deref() == Some(feature_id) {
bbox.union(&solid.bbox);
}
}
(!bbox.is_empty()).then(|| bbox.center())
}
}
// BREP private tests: 3dc5f1ea27a76535
// BREP private tests: 2f934a3442001a7e