1use std::collections::HashMap;
10
11use brepkit_math::aabb::Aabb3;
12use brepkit_math::mat::Mat4;
13use brepkit_math::vec::Point3;
14use brepkit_topology::Topology;
15use brepkit_topology::solid::SolidId;
16
17use crate::OperationsError;
18
19pub type ComponentId = usize;
21
22#[derive(Debug, Clone)]
24pub struct Component {
25 pub name: String,
27 pub solid: SolidId,
29 pub transform: Mat4,
31 pub parent: Option<ComponentId>,
33 pub children: Vec<ComponentId>,
35}
36
37#[derive(Debug, Default, Clone)]
46pub struct Assembly {
47 components: HashMap<ComponentId, Component>,
49 roots: Vec<ComponentId>,
51 next_id: ComponentId,
53 name: String,
55}
56
57impl Assembly {
58 #[must_use]
60 pub fn new(name: impl Into<String>) -> Self {
61 Self {
62 name: name.into(),
63 ..Self::default()
64 }
65 }
66
67 #[must_use]
69 pub fn name(&self) -> &str {
70 &self.name
71 }
72
73 pub fn add_root_component(
75 &mut self,
76 name: impl Into<String>,
77 solid: SolidId,
78 transform: Mat4,
79 ) -> ComponentId {
80 let id = self.next_id;
81 self.next_id += 1;
82
83 self.components.insert(
84 id,
85 Component {
86 name: name.into(),
87 solid,
88 transform,
89 parent: None,
90 children: Vec::new(),
91 },
92 );
93 self.roots.push(id);
94 id
95 }
96
97 pub fn add_child_component(
102 &mut self,
103 parent: ComponentId,
104 name: impl Into<String>,
105 solid: SolidId,
106 transform: Mat4,
107 ) -> Result<ComponentId, OperationsError> {
108 if !self.components.contains_key(&parent) {
109 return Err(OperationsError::InvalidInput {
110 reason: format!("parent component {parent} not found"),
111 });
112 }
113
114 let id = self.next_id;
115 self.next_id += 1;
116
117 self.components.insert(
118 id,
119 Component {
120 name: name.into(),
121 solid,
122 transform,
123 parent: Some(parent),
124 children: Vec::new(),
125 },
126 );
127
128 if let Some(parent_comp) = self.components.get_mut(&parent) {
129 parent_comp.children.push(id);
130 }
131
132 Ok(id)
133 }
134
135 #[must_use]
137 pub fn component(&self, id: ComponentId) -> Option<&Component> {
138 self.components.get(&id)
139 }
140
141 #[must_use]
143 pub fn roots(&self) -> &[ComponentId] {
144 &self.roots
145 }
146
147 #[must_use]
149 pub fn component_count(&self) -> usize {
150 self.components.len()
151 }
152
153 #[must_use]
156 pub fn world_transform(&self, id: ComponentId) -> Option<Mat4> {
157 let comp = self.components.get(&id)?;
158 let mut result = comp.transform;
159
160 let mut current_parent = comp.parent;
161 while let Some(pid) = current_parent {
162 let parent = self.components.get(&pid)?;
163 result = parent.transform * result;
164 current_parent = parent.parent;
165 }
166
167 Some(result)
168 }
169
170 #[must_use]
175 pub fn flatten(&self) -> Vec<(SolidId, Mat4)> {
176 let mut result = Vec::new();
177 for &root_id in &self.roots {
178 self.flatten_recursive(root_id, Mat4::identity(), &mut result);
179 }
180 result
181 }
182
183 fn flatten_recursive(
184 &self,
185 id: ComponentId,
186 parent_transform: Mat4,
187 result: &mut Vec<(SolidId, Mat4)>,
188 ) {
189 let Some(comp) = self.components.get(&id) else {
190 return;
191 };
192
193 let world = parent_transform * comp.transform;
194
195 if comp.children.is_empty() {
196 result.push((comp.solid, world));
197 } else {
198 for &child_id in &comp.children {
199 self.flatten_recursive(child_id, world, result);
200 }
201 }
202 }
203
204 pub fn bounding_box(&self, topo: &Topology) -> Result<Aabb3, OperationsError> {
209 let mut min_x = f64::MAX;
210 let mut min_y = f64::MAX;
211 let mut min_z = f64::MAX;
212 let mut max_x = f64::MIN;
213 let mut max_y = f64::MIN;
214 let mut max_z = f64::MIN;
215
216 for (solid_id, transform) in self.flatten() {
217 let bbox = crate::measure::solid_bounding_box(topo, solid_id)?;
218 let lo = bbox.min;
219 let hi = bbox.max;
220 let corners = [
221 Point3::new(lo.x(), lo.y(), lo.z()),
222 Point3::new(hi.x(), lo.y(), lo.z()),
223 Point3::new(lo.x(), hi.y(), lo.z()),
224 Point3::new(hi.x(), hi.y(), lo.z()),
225 Point3::new(lo.x(), lo.y(), hi.z()),
226 Point3::new(hi.x(), lo.y(), hi.z()),
227 Point3::new(lo.x(), hi.y(), hi.z()),
228 Point3::new(hi.x(), hi.y(), hi.z()),
229 ];
230
231 for corner in &corners {
232 let transformed = transform.mul_point(*corner);
233 min_x = min_x.min(transformed.x());
234 min_y = min_y.min(transformed.y());
235 min_z = min_z.min(transformed.z());
236 max_x = max_x.max(transformed.x());
237 max_y = max_y.max(transformed.y());
238 max_z = max_z.max(transformed.z());
239 }
240 }
241
242 Ok(Aabb3 {
243 min: Point3::new(min_x, min_y, min_z),
244 max: Point3::new(max_x, max_y, max_z),
245 })
246 }
247
248 #[must_use]
250 pub fn bill_of_materials(&self) -> Vec<BomEntry> {
251 let mut solid_counts: HashMap<usize, (String, usize)> = HashMap::new();
252
253 for comp in self.components.values() {
254 let entry = solid_counts
255 .entry(comp.solid.index())
256 .or_insert_with(|| (comp.name.clone(), 0));
257 entry.1 += 1;
258 }
259
260 solid_counts
261 .into_iter()
262 .map(|(solid_idx, (name, count))| BomEntry {
263 name,
264 solid_index: solid_idx,
265 instance_count: count,
266 })
267 .collect()
268 }
269}
270
271#[derive(Debug, Clone)]
273pub struct BomEntry {
274 pub name: String,
276 pub solid_index: usize,
278 pub instance_count: usize,
280}
281
282#[cfg(test)]
283#[allow(clippy::unwrap_used)]
284mod tests {
285 use super::*;
286 use crate::primitives::make_box;
287
288 #[test]
289 fn empty_assembly() {
290 let asm = Assembly::new("test");
291 assert_eq!(asm.component_count(), 0);
292 assert!(asm.roots().is_empty());
293 assert!(asm.flatten().is_empty());
294 }
295
296 #[test]
297 fn add_root_component() {
298 let mut topo = Topology::new();
299 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
300
301 let mut asm = Assembly::new("test");
302 let id = asm.add_root_component("box1", box1, Mat4::identity());
303
304 assert_eq!(asm.component_count(), 1);
305 assert_eq!(asm.roots().len(), 1);
306
307 let comp = asm.component(id).unwrap();
308 assert_eq!(comp.name, "box1");
309 assert!(comp.parent.is_none());
310 }
311
312 #[test]
313 fn add_child_component() {
314 let mut topo = Topology::new();
315 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
316 let box2 = make_box(&mut topo, 0.5, 0.5, 0.5).unwrap();
317
318 let mut asm = Assembly::new("test");
319 let parent = asm.add_root_component("parent", box1, Mat4::identity());
320 let child = asm
321 .add_child_component(parent, "child", box2, Mat4::translation(2.0, 0.0, 0.0))
322 .unwrap();
323
324 assert_eq!(asm.component_count(), 2);
325 assert_eq!(asm.roots().len(), 1);
326
327 let parent_comp = asm.component(parent).unwrap();
328 assert_eq!(parent_comp.children.len(), 1);
329
330 let child_comp = asm.component(child).unwrap();
331 assert_eq!(child_comp.parent, Some(parent));
332 }
333
334 #[test]
335 fn flatten_assembly() {
336 let mut topo = Topology::new();
337 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
338
339 let mut asm = Assembly::new("test");
340 asm.add_root_component("box_a", box1, Mat4::identity());
341 asm.add_root_component("box_b", box1, Mat4::translation(3.0, 0.0, 0.0));
342
343 let flat = asm.flatten();
344 assert_eq!(flat.len(), 2, "two root components = two instances");
345 }
346
347 #[test]
348 fn world_transform_chain() {
349 let mut topo = Topology::new();
350 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
351
352 let mut asm = Assembly::new("test");
353 let parent = asm.add_root_component("parent", box1, Mat4::translation(1.0, 0.0, 0.0));
354 let child = asm
355 .add_child_component(parent, "child", box1, Mat4::translation(0.0, 2.0, 0.0))
356 .unwrap();
357
358 let world = asm.world_transform(child).unwrap();
359 let origin = world.mul_point(Point3::new(0.0, 0.0, 0.0));
360
361 assert!((origin.x() - 1.0).abs() < 1e-10);
364 assert!((origin.y() - 2.0).abs() < 1e-10);
365 assert!((origin.z() - 0.0).abs() < 1e-10);
366 }
367
368 #[test]
369 fn bill_of_materials() {
370 let mut topo = Topology::new();
371 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
372
373 let mut asm = Assembly::new("test");
374 asm.add_root_component("wheel_1", box1, Mat4::identity());
375 asm.add_root_component("wheel_2", box1, Mat4::translation(1.0, 0.0, 0.0));
376 asm.add_root_component("wheel_3", box1, Mat4::translation(2.0, 0.0, 0.0));
377
378 let bom = asm.bill_of_materials();
379 assert_eq!(bom.len(), 1);
381 assert_eq!(bom[0].instance_count, 3);
382 }
383
384 #[test]
385 fn invalid_parent_error() {
386 let mut topo = Topology::new();
387 let box1 = make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
388
389 let mut asm = Assembly::new("test");
390 let result = asm.add_child_component(999, "child", box1, Mat4::identity());
391 assert!(result.is_err());
392 }
393
394 #[test]
395 fn assembly_bounding_box() {
396 let mut topo = Topology::new();
397 let box1 = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
398
399 let mut asm = Assembly::new("test");
400 asm.add_root_component("box_a", box1, Mat4::identity());
401 asm.add_root_component("box_b", box1, Mat4::translation(10.0, 0.0, 0.0));
402
403 let bbox = asm.bounding_box(&topo).unwrap();
404
405 assert!(bbox.min.x() < 0.5);
407 assert!(bbox.max.x() > 11.5);
408 }
409}