brep_kernel/io/three_mf/mod.rs
1//! 3MF (3D Manufacturing Format) import — the core specification only.
2//!
3//! [`read_3mf`] reads a 3MF package (an OPC ZIP container) and returns the
4//! meshes its `<resources>` define plus the flattened list of instances the
5//! `<build>` places, in millimetres. It is a READER: the triangle soup it
6//! produces goes into the same mesh-to-solid chain STL and OBJ use
7//! (`brep_reconstruction::stl_conversion::convert_stl_mesh_to_step`); no
8//! geometry is built here.
9//!
10//! # What is supported
11//!
12//! The core specification (3MF Core Specification 1.x,
13//! `http://schemas.microsoft.com/3dmanufacturing/core/2015/02`):
14//!
15//! * `<model unit>` — `micron`, `millimeter`, `centimeter`, `inch`, `foot`,
16//! `meter`, defaulting to `millimeter`; every coordinate and translation is
17//! converted to millimetres, the unit the kernel works in.
18//! * `<resources>` with `<object id type>` carrying either a `<mesh>`
19//! (`<vertices><vertex x y z>`, `<triangles><triangle v1 v2 v3>`) or a
20//! `<components>` list of `<component objectid transform>`.
21//! * `<build>` with `<item objectid transform>`.
22//! * The 3x4 row-vector `transform` attribute on components and items, composed
23//! inner-to-outer, with the winding of a mirrored instance reversed so the
24//! soup stays outward-facing.
25//!
26//! # What is ignored, by design
27//!
28//! Everything outside the core namespace: the materials-and-properties, slice,
29//! production, beam-lattice and secure-content extensions. Extension elements
30//! are skipped as whole subtrees and prefixed attributes (`p:UUID`, a
31//! triangle's `pid`/`p1..p3` property indices) are never read, so a file that
32//! carries them imports as pure geometry rather than failing. A package that
33//! declares such an extension REQUIRED (`requiredextensions`) is refused
34//! instead, because honouring it is exactly what this reader cannot do.
35//! `<metadata>` and thumbnails carry no geometry and are skipped. An object's
36//! `name` is kept on each [`ThreeMfInstance`] only so a consumer can say WHICH
37//! object a refusal is about.
38//!
39//! Anything the core specification makes required and the file omits — the
40//! start-part relationship, an object id, a triangle's vertex indices, a
41//! component's or item's `objectid` — is refused with a [`ThreeMfError`]
42//! naming the package part it was read from.
43//!
44//! # Bounds
45//!
46//! Nothing here grows to fit its input. The container refuses encryption,
47//! ZIP64, multi-disk packages and any compression but stored and deflate; a
48//! part is refused before decompression when it DECLARES more than
49//! [`MAX_PART_BYTES`], the deflate output stops at the declared size, and every
50//! part's CRC-32 and length are verified after it. The XML reader refuses a
51//! `<!DOCTYPE>` outright (so no entity is expanded, internal or external) and
52//! caps nesting. The model is held to [`MAX_MESH_VERTICES`],
53//! [`MAX_MESH_TRIANGLES`], [`MAX_INSTANCES`], [`MAX_TOTAL_TRIANGLES`] and
54//! [`MAX_COMPONENT_DEPTH`]. Every offset in the container is computed with
55//! `checked_add`, because the kernel's release profile sets
56//! `overflow-checks = false`.
57//!
58//! # Dependency review (why the container is hand-rolled)
59//!
60//! `BREP_kernel` publishes to crates.io and compiles to wasm on nine declared
61//! dependencies, seven of them unconditional; `zip` and `flate2` appear nowhere
62//! in `BREP_kernel/Cargo.lock` (they are in `BREP_app`'s lock only, pulled in by
63//! the egui/image stack, not by anything the kernel links). `zip` with
64//! `default-features = false, features = ["deflate"]` still adds `flate2`,
65//! `miniz_oxide`, `crc32fast`, `adler2` and their support crates to a published
66//! geometry kernel. The container here is ~1000 lines of reader for a format the
67//! kernel needs in one place, matching how STEP, IGES, STL and OBJ are already
68//! read: [`zip`] parses the central directory, [`inflate`] implements RFC 1951,
69//! [`xml`] is a bounded pull parser. Every one of them refuses rather than grows.
70
71mod inflate;
72mod model;
73mod xml;
74mod zip;
75
76
77use serde::Serialize;
78
79/// The 3MF core-specification namespace this reader accepts elements from.
80pub const CORE_NAMESPACE: &str = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
81/// The OPC relationships namespace of `_rels/.rels`.
82const RELATIONSHIP_NAMESPACE: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
83/// The relationship type naming the package's primary 3D model part.
84const MODEL_RELATIONSHIP: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
85/// The package-level relationships part; required of every OPC package.
86const RELATIONSHIP_PART: &str = "_rels/.rels";
87
88/// Largest part this reader will expand, compressed or stored. A 3MF model part
89/// is XML: 256 MiB is roughly five million triangles of it.
90pub const MAX_PART_BYTES: usize = 256 * 1024 * 1024;
91/// Largest vertex and triangle counts accepted for one `<mesh>`.
92pub const MAX_MESH_VERTICES: usize = 8_000_000;
93pub const MAX_MESH_TRIANGLES: usize = 16_000_000;
94/// Largest number of placements the `<build>` may flatten to.
95pub const MAX_INSTANCES: usize = 65_536;
96/// Largest flattened triangle total across all instances.
97pub const MAX_TOTAL_TRIANGLES: usize = 32_000_000;
98/// Deepest `<components>` nesting followed when flattening. The core spec's
99/// "an object must be defined before it is referenced" already forbids cycles;
100/// this bounds the work a legal but pathological file can ask for.
101pub const MAX_COMPONENT_DEPTH: usize = 32;
102
103/// Why a 3MF package was refused. Every variant that belongs to a package part
104/// names it, so the message says where in the container the fault is.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum ThreeMfError {
107 /// The bytes are not a readable OPC/ZIP container at all.
108 Container(String),
109 /// A part is in the container but its bytes cannot be recovered: a bad
110 /// local header, a short read, a failed CRC-32.
111 Damaged { part: String, detail: String },
112 /// A part the core specification requires is absent.
113 MissingPart { part: String, detail: String },
114 /// A container or format feature this reader refuses on purpose.
115 Unsupported { part: String, detail: String },
116 /// A part exceeds one of this reader's documented bounds.
117 Bounds { part: String, detail: String },
118 /// A part is not well-formed XML.
119 Xml { part: String, detail: String },
120 /// A part is well-formed XML but violates the core specification.
121 Model { part: String, detail: String },
122}
123
124impl ThreeMfError {
125 /// The package part the failure was found in, when it belongs to one.
126 pub fn part(&self) -> Option<&str> {
127 match self {
128 Self::Container(_) => None,
129 Self::Damaged { part, .. }
130 | Self::MissingPart { part, .. }
131 | Self::Unsupported { part, .. }
132 | Self::Bounds { part, .. }
133 | Self::Xml { part, .. }
134 | Self::Model { part, .. } => Some(part),
135 }
136 }
137}
138
139impl core::fmt::Display for ThreeMfError {
140 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141 match self {
142 Self::Container(detail) => write!(f, "3MF package: {detail}"),
143 Self::Damaged { part, detail } => write!(f, "3MF '{part}' is damaged: {detail}"),
144 Self::MissingPart { part, detail } => write!(f, "3MF '{part}' is missing: {detail}"),
145 Self::Unsupported { part, detail } => write!(f, "3MF '{part}': {detail}"),
146 Self::Bounds { part, detail } => write!(f, "3MF '{part}' exceeds a bound: {detail}"),
147 Self::Xml { part, detail } => write!(f, "3MF '{part}' is not well-formed: {detail}"),
148 Self::Model { part, detail } => write!(f, "3MF '{part}': {detail}"),
149 }
150 }
151}
152
153impl std::error::Error for ThreeMfError {}
154
155/// The `unit` a model part declares, with its conversion to millimetres.
156#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
157pub enum ThreeMfUnit {
158 Micron,
159 Millimeter,
160 Centimeter,
161 Inch,
162 Foot,
163 Meter,
164}
165
166impl ThreeMfUnit {
167 /// The spec's unit table (3MF Core, `<model unit>`), in millimetres.
168 pub fn millimetres(self) -> f64 {
169 match self {
170 Self::Micron => 0.001,
171 Self::Millimeter => 1.0,
172 Self::Centimeter => 10.0,
173 Self::Inch => 25.4,
174 Self::Foot => 304.8,
175 Self::Meter => 1000.0,
176 }
177 }
178
179 pub fn name(self) -> &'static str {
180 match self {
181 Self::Micron => "micron",
182 Self::Millimeter => "millimeter",
183 Self::Centimeter => "centimeter",
184 Self::Inch => "inch",
185 Self::Foot => "foot",
186 Self::Meter => "meter",
187 }
188 }
189
190 fn parse(text: &str) -> Option<Self> {
191 Some(match text {
192 "micron" => Self::Micron,
193 "millimeter" => Self::Millimeter,
194 "centimeter" => Self::Centimeter,
195 "inch" => Self::Inch,
196 "foot" => Self::Foot,
197 "meter" => Self::Meter,
198 _ => return None,
199 })
200 }
201}
202
203/// One `<object>`'s mesh, in millimetres and in the object's own coordinates.
204/// Shared: an object placed three times is ONE entry here and three
205/// [`ThreeMfInstance`]s.
206#[derive(Clone, Debug, Default, PartialEq, Serialize)]
207pub struct ThreeMfMesh {
208 /// The id of the `<object>` this mesh belongs to.
209 pub object_id: u32,
210 /// Vertex coordinates as xyz triples, millimetres.
211 pub positions: Vec<f64>,
212 /// Triangle corners, three indices into `positions`/3.
213 pub indices: Vec<u32>,
214}
215
216impl ThreeMfMesh {
217 pub fn vertex_count(&self) -> usize {
218 self.positions.len() / 3
219 }
220 pub fn triangle_count(&self) -> usize {
221 self.indices.len() / 3
222 }
223}
224
225/// One placement of one mesh: a `<build><item>`, or a `<component>` reached
226/// through one, with the transforms from the item down composed.
227#[derive(Clone, Debug, PartialEq, Serialize)]
228pub struct ThreeMfInstance {
229 /// The mesh-bearing object placed (not the item's `objectid` when the item
230 /// pointed at a `<components>` object).
231 pub object_id: u32,
232 /// Index into [`ThreeMfReadResult::meshes`].
233 pub mesh: usize,
234 /// The composed 3x4 row-vector transform, in 3MF attribute order
235 /// (`m00 m01 m02 m10 m11 m12 m20 m21 m22 m30 m31 m32`), translation in mm.
236 pub transform: [f64; 12],
237 /// Where this instance's geometry sits in the flattened buffers: its
238 /// vertices are `positions[first_vertex * 3 ..][.. vertex_count * 3]` and
239 /// its triangles `indices[first_triangle * 3 ..][.. triangle_count * 3]`.
240 /// Instances are laid out in order and never share a vertex, which is what
241 /// lets a consumer take one body out without re-flattening the build.
242 pub first_vertex: usize,
243 pub vertex_count: usize,
244 pub first_triangle: usize,
245 pub triangle_count: usize,
246 /// The placed object's `name`, or failing that the nearest named
247 /// `<components>` object above it; `None` when nothing on the way was
248 /// named. Carries no geometry.
249 pub name: Option<String>,
250}
251
252impl ThreeMfInstance {
253 /// Place an object-local point: `x' = x·m00 + y·m10 + z·m20 + m30`.
254 pub fn transform_point(&self, point: [f64; 3]) -> [f64; 3] {
255 transform_point(&self.transform, point)
256 }
257}
258
259/// Everything [`read_3mf`] recovered from a package.
260#[derive(Clone, Debug, Serialize)]
261pub struct ThreeMfReadResult {
262 /// The part the model was read from (from the start-part relationship).
263 pub model_part: String,
264 /// The unit the file declared, before conversion.
265 pub unit: ThreeMfUnit,
266 /// One entry per mesh-bearing `<object>` in `<resources>`, in document
267 /// order — including any the build never places.
268 pub meshes: Vec<ThreeMfMesh>,
269 /// The `<build>`, flattened through `<components>`: one entry per placed
270 /// mesh. Objects of a type other than `model` are not placed.
271 pub instances: Vec<ThreeMfInstance>,
272 /// Every instance's triangles in world coordinates, millimetres — the soup
273 /// the mesh-to-solid chain reads.
274 pub positions: Vec<f64>,
275 /// Triangle corners into `positions`/3, winding preserved (reversed for a
276 /// mirrored instance so the result stays outward-facing).
277 pub indices: Vec<u32>,
278}
279
280impl ThreeMfReadResult {
281 pub fn vertex_count(&self) -> usize {
282 self.positions.len() / 3
283 }
284 pub fn triangle_count(&self) -> usize {
285 self.indices.len() / 3
286 }
287
288 /// ONE instance's triangle soup: its world-space vertices and its triangles
289 /// rebased onto them. A multi-instance build is several bodies in one
290 /// buffer — this is how a consumer takes them apart by PLACEMENT without
291 /// knowing how `read_3mf` laid them out. The importer does not need it: the
292 /// mesh-to-solid chain splits the whole soup into its vertex-connected
293 /// components and reconstructs each as its own body. It is the answer when
294 /// the question is which instance a body came from, which the geometry
295 /// alone cannot say (two instances that touch are one component).
296 pub fn instance_mesh(&self, instance: usize) -> Option<(Vec<f64>, Vec<u32>)> {
297 let instance = self.instances.get(instance)?;
298 let positions = self
299 .positions
300 .get(instance.first_vertex * 3..(instance.first_vertex + instance.vertex_count) * 3)?
301 .to_vec();
302 let base = instance.first_vertex as u32;
303 let indices = self
304 .indices
305 .get(
306 instance.first_triangle * 3
307 ..(instance.first_triangle + instance.triangle_count) * 3,
308 )?
309 .iter()
310 .map(|index| index - base)
311 .collect();
312 Some((positions, indices))
313 }
314}
315
316/// The identity 3x4 transform, in 3MF attribute order.
317pub const IDENTITY: [f64; 12] = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
318
319/// Place a point with a 3MF row-vector transform.
320pub fn transform_point(transform: &[f64; 12], point: [f64; 3]) -> [f64; 3] {
321 let [x, y, z] = point;
322 [
323 x * transform[0] + y * transform[3] + z * transform[6] + transform[9],
324 x * transform[1] + y * transform[4] + z * transform[7] + transform[10],
325 x * transform[2] + y * transform[5] + z * transform[8] + transform[11],
326 ]
327}
328
329/// Compose `inner` then `outer` (row vectors: `p · inner · outer`) — a
330/// component's transform inside the item that placed it.
331pub fn compose(inner: &[f64; 12], outer: &[f64; 12]) -> [f64; 12] {
332 let mut out = [0.0; 12];
333 for row in 0..4 {
334 for column in 0..3 {
335 let mut sum = if row == 3 { outer[9 + column] } else { 0.0 };
336 for k in 0..3 {
337 sum += inner[row * 3 + k] * outer[k * 3 + column];
338 }
339 out[row * 3 + column] = sum;
340 }
341 }
342 out
343}
344
345/// The determinant of the transform's linear part; negative means the instance
346/// is mirrored and its triangle winding has to be reversed.
347fn determinant(transform: &[f64; 12]) -> f64 {
348 let m = transform;
349 m[0] * (m[4] * m[8] - m[5] * m[7]) - m[1] * (m[3] * m[8] - m[5] * m[6])
350 + m[2] * (m[3] * m[7] - m[4] * m[6])
351}
352
353/// Read a 3MF package: locate the model part through the OPC relationships,
354/// parse the core model, and flatten the build into a millimetre triangle soup.
355///
356/// See the module documentation for what is read, what is ignored, and the
357/// bounds every part is held to.
358pub fn read_3mf(bytes: &[u8]) -> Result<ThreeMfReadResult, ThreeMfError> {
359 let archive = zip::ZipArchive::open(bytes).map_err(map_zip)?;
360 let relationships = archive.find(RELATIONSHIP_PART).ok_or_else(|| {
361 ThreeMfError::MissingPart {
362 part: RELATIONSHIP_PART.into(),
363 detail: "an OPC package must carry the package relationships part".into(),
364 }
365 })?;
366 let relationships_text = read_text(&archive, relationships, RELATIONSHIP_PART)?;
367 let model_part = start_part(&relationships_text)?;
368 let model_entry = archive.find(&model_part).ok_or_else(|| ThreeMfError::MissingPart {
369 part: model_part.clone(),
370 detail: format!("'{RELATIONSHIP_PART}' names it as the 3D model start part"),
371 })?;
372 let model_text = read_text(&archive, model_entry, &model_part)?;
373 let document = model::parse_model(&model_part, &model_text)?;
374 flatten(model_part, document)
375}
376
377/// Map a container fault onto the typed refusal, keeping the part name.
378fn map_zip(error: zip::ZipError) -> ThreeMfError {
379 let part = error.entry.clone().unwrap_or_else(|| "[package]".into());
380 match error.fault {
381 zip::ZipFault::Container => match &error.entry {
382 Some(entry) => ThreeMfError::Container(format!("{entry}: {}", error.message)),
383 None => ThreeMfError::Container(error.message),
384 },
385 zip::ZipFault::Damaged => ThreeMfError::Damaged { part, detail: error.message },
386 zip::ZipFault::Unsupported => ThreeMfError::Unsupported { part, detail: error.message },
387 zip::ZipFault::Bounds => ThreeMfError::Bounds { part, detail: error.message },
388 }
389}
390
391fn read_text(
392 archive: &zip::ZipArchive<'_>,
393 entry: &zip::ZipEntry,
394 part: &str,
395) -> Result<String, ThreeMfError> {
396 let bytes = archive.read(entry, MAX_PART_BYTES).map_err(map_zip)?;
397 String::from_utf8(bytes).map_err(|_| ThreeMfError::Xml {
398 part: part.to_owned(),
399 detail: "the part is not UTF-8 text".into(),
400 })
401}
402
403/// The `Target` of the package's 3D-model start-part relationship.
404fn start_part(text: &str) -> Result<String, ThreeMfError> {
405 let mut reader = xml::XmlReader::new(text);
406 loop {
407 let event = reader.next().map_err(|error| ThreeMfError::Xml {
408 part: RELATIONSHIP_PART.into(),
409 detail: error.to_string(),
410 })?;
411 match event {
412 xml::Event::Start(tag) => {
413 if tag.namespace != RELATIONSHIP_NAMESPACE {
414 reader.skip_subtree(&tag).map_err(|error| ThreeMfError::Xml {
415 part: RELATIONSHIP_PART.into(),
416 detail: error.to_string(),
417 })?;
418 continue;
419 }
420 if tag.name != "Relationship" {
421 continue;
422 }
423 if tag.attribute("Type") != Some(MODEL_RELATIONSHIP) {
424 continue;
425 }
426 let target = tag.attribute("Target").ok_or_else(|| ThreeMfError::Model {
427 part: RELATIONSHIP_PART.into(),
428 detail: "the 3D model relationship has no Target".into(),
429 })?;
430 let target = target.trim_start_matches('/');
431 if target.is_empty() {
432 return Err(ThreeMfError::Model {
433 part: RELATIONSHIP_PART.into(),
434 detail: "the 3D model relationship has an empty Target".into(),
435 });
436 }
437 return Ok(target.to_owned());
438 }
439 xml::Event::End { .. } => {}
440 xml::Event::Eof => break,
441 }
442 }
443 Err(ThreeMfError::MissingPart {
444 part: RELATIONSHIP_PART.into(),
445 detail: format!("no relationship of type '{MODEL_RELATIONSHIP}' names a 3D model part"),
446 })
447}
448
449/// Walk the build, composing transforms, into instances and one triangle soup.
450fn flatten(
451 model_part: String,
452 document: model::ModelDocument,
453) -> Result<ThreeMfReadResult, ThreeMfError> {
454 let scale = document.unit.millimetres();
455 let meshes = document
456 .meshes
457 .iter()
458 .map(|mesh| ThreeMfMesh {
459 object_id: mesh.object_id,
460 positions: mesh.positions.iter().map(|value| value * scale).collect(),
461 indices: mesh.indices.clone(),
462 })
463 .collect::<Vec<_>>();
464 let mut instances = Vec::new();
465 for item in &document.items {
466 let mut transform = item.transform;
467 for value in &mut transform[9..12] {
468 *value *= scale;
469 }
470 place(&document, &model_part, item.object_id, transform, 0, None, &mut instances)?;
471 }
472 let mut positions = Vec::new();
473 let mut indices = Vec::new();
474 let mut triangles = 0usize;
475 for instance in &mut instances {
476 let mesh = &meshes[instance.mesh];
477 triangles += mesh.triangle_count();
478 if triangles > MAX_TOTAL_TRIANGLES {
479 return Err(ThreeMfError::Bounds {
480 part: model_part,
481 detail: format!(
482 "the build places more than {MAX_TOTAL_TRIANGLES} triangles in total"
483 ),
484 });
485 }
486 instance.first_vertex = positions.len() / 3;
487 instance.vertex_count = mesh.vertex_count();
488 instance.first_triangle = indices.len() / 3;
489 instance.triangle_count = mesh.triangle_count();
490 let base = instance.first_vertex as u32;
491 for point in mesh.positions.chunks_exact(3) {
492 let placed = instance.transform_point([point[0], point[1], point[2]]);
493 positions.extend(placed);
494 }
495 // A mirrored placement turns the mesh inside out; reversing the winding
496 // keeps every triangle facing out of the body, which is what the
497 // mesh-to-solid chain reads.
498 let mirrored = determinant(&instance.transform) < 0.0;
499 for triangle in mesh.indices.chunks_exact(3) {
500 if mirrored {
501 indices.extend([base + triangle[0], base + triangle[2], base + triangle[1]]);
502 } else {
503 indices.extend([base + triangle[0], base + triangle[1], base + triangle[2]]);
504 }
505 }
506 }
507 Ok(ThreeMfReadResult {
508 model_part,
509 unit: document.unit,
510 meshes,
511 instances,
512 positions,
513 indices,
514 })
515}
516
517/// Place `object_id` under `transform`, recursing through `<components>`.
518fn place(
519 document: &model::ModelDocument,
520 model_part: &str,
521 object_id: u32,
522 transform: [f64; 12],
523 depth: usize,
524 inherited_name: Option<&str>,
525 instances: &mut Vec<ThreeMfInstance>,
526) -> Result<(), ThreeMfError> {
527 if depth > MAX_COMPONENT_DEPTH {
528 return Err(ThreeMfError::Bounds {
529 part: model_part.to_owned(),
530 detail: format!("components nest deeper than {MAX_COMPONENT_DEPTH} levels"),
531 });
532 }
533 let object = document.object(object_id).ok_or_else(|| ThreeMfError::Model {
534 part: model_part.to_owned(),
535 detail: format!("object {object_id} is placed but never defined"),
536 })?;
537 let name = object.name.as_deref().or(inherited_name);
538 match &object.content {
539 model::ObjectContent::Mesh(mesh) => {
540 // Only PRINTABLE geometry reaches the soup: support, surface and
541 // "other" objects describe the print job, not the part.
542 if object.model_type {
543 if instances.len() + 1 > MAX_INSTANCES {
544 return Err(ThreeMfError::Bounds {
545 part: model_part.to_owned(),
546 detail: format!("the build places more than {MAX_INSTANCES} instances"),
547 });
548 }
549 instances.push(ThreeMfInstance {
550 object_id: object.id,
551 mesh: *mesh,
552 transform,
553 // Filled by `flatten` when the soup is laid out.
554 first_vertex: 0,
555 vertex_count: 0,
556 first_triangle: 0,
557 triangle_count: 0,
558 name: name.map(str::to_owned),
559 });
560 }
561 Ok(())
562 }
563 model::ObjectContent::Components(components) => {
564 for component in components {
565 let mut inner = component.transform;
566 // The component's translation is in the file's unit; the
567 // enclosing transform is already in millimetres, so scaling
568 // here (not in `flatten`) keeps one conversion per level.
569 let scale = document.unit.millimetres();
570 for value in &mut inner[9..12] {
571 *value *= scale;
572 }
573 let composed = compose(&inner, &transform);
574 place(
575 document,
576 model_part,
577 component.object_id,
578 composed,
579 depth + 1,
580 name,
581 instances,
582 )?;
583 }
584 Ok(())
585 }
586 }
587}