lib3mf-core 0.4.0

Parse and validate 3MF files for manufacturing workflows - production-ready with streaming parser and comprehensive validation
Documentation
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
use crate::error::{Lib3mfError, Result};
use crate::model::{
    BaseMaterialsGroup, ColorGroup, CompositeMaterials, Displacement2D, KeyStore, MultiProperties,
    Object, SliceStack, Texture2D, Texture2DGroup, VolumetricStack,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Unique identifier for a resource within the model.
///
/// A type-safe wrapper around `u32` that prevents accidentally mixing resource IDs
/// with raw integers. All resources in a 3MF model share a global ID namespace,
/// meaning an ID can only be used once across all resource types (objects, materials,
/// textures, etc.).
///
/// # Examples
///
/// ```
/// use lib3mf_core::model::ResourceId;
///
/// let id = ResourceId(42);
/// assert_eq!(id.0, 42);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct ResourceId(pub u32);

/// Central registry for all resources in a 3MF model.
///
/// The `ResourceCollection` manages all reusable resources including objects,
/// materials, textures, and extension-specific resources. It enforces the global
/// ID namespace requirement: each [`ResourceId`] can only be used once across
/// all resource types.
///
/// Resources are stored in separate `HashMap<ResourceId, T>` collections internally,
/// allowing efficient lookup by ID.
///
/// # Examples
///
/// ```
/// use lib3mf_core::model::{ResourceCollection, Object, ResourceId, Geometry, Mesh, ObjectType};
///
/// let mut resources = ResourceCollection::new();
///
/// let obj = Object {
///     id: ResourceId(1),
///     object_type: ObjectType::Model,
///     name: None,
///     part_number: None,
///     uuid: None,
///     pid: None,
///     pindex: None,
///     thumbnail: None,
///     geometry: Geometry::Mesh(Mesh::default()),
/// };
///
/// resources.add_object(obj).expect("Failed to add object");
/// assert!(resources.exists(ResourceId(1)));
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceCollection {
    objects: HashMap<ResourceId, Object>,
    base_materials: HashMap<ResourceId, BaseMaterialsGroup>,
    color_groups: HashMap<ResourceId, ColorGroup>,
    slice_stacks: HashMap<ResourceId, SliceStack>,
    volumetric_stacks: HashMap<ResourceId, VolumetricStack>,
    texture_2d: HashMap<ResourceId, Texture2D>,
    texture_2d_groups: HashMap<ResourceId, Texture2DGroup>,
    composite_materials: HashMap<ResourceId, CompositeMaterials>,
    multi_properties: HashMap<ResourceId, MultiProperties>,
    displacement_2d: HashMap<ResourceId, Displacement2D>,
    /// Optional Secure Content key store for this model (at most one per model).
    pub key_store: Option<KeyStore>, // Usually one KeyStore per model/part
}

impl ResourceCollection {
    /// Creates a new empty resource collection.
    pub fn new() -> Self {
        Self::default()
    }

    /// Checks if a resource with the given ID exists in any resource type.
    ///
    /// Returns `true` if the ID is used by any resource (object, material, texture, etc.).
    pub fn exists(&self, id: ResourceId) -> bool {
        self.objects.contains_key(&id)
            || self.base_materials.contains_key(&id)
            || self.color_groups.contains_key(&id)
            || self.slice_stacks.contains_key(&id)
            || self.volumetric_stacks.contains_key(&id)
            || self.texture_2d.contains_key(&id)
            || self.texture_2d_groups.contains_key(&id)
            || self.composite_materials.contains_key(&id)
            || self.multi_properties.contains_key(&id)
            || self.displacement_2d.contains_key(&id)
    }

    /// Adds an object to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_object(&mut self, object: Object) -> Result<()> {
        if self.exists(object.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                object.id.0
            )));
        }
        self.objects.insert(object.id, object);
        Ok(())
    }

    /// Adds a base materials group to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_base_materials(&mut self, group: BaseMaterialsGroup) -> Result<()> {
        if self.exists(group.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                group.id.0
            )));
        }
        self.base_materials.insert(group.id, group);
        Ok(())
    }

    /// Adds a color group to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_color_group(&mut self, group: ColorGroup) -> Result<()> {
        if self.exists(group.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                group.id.0
            )));
        }
        self.color_groups.insert(group.id, group);
        Ok(())
    }

    /// Adds a slice stack resource to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_slice_stack(&mut self, stack: SliceStack) -> Result<()> {
        if self.exists(stack.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                stack.id.0
            )));
        }
        self.slice_stacks.insert(stack.id, stack);
        Ok(())
    }

    /// Adds a volumetric stack resource to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_volumetric_stack(&mut self, stack: VolumetricStack) -> Result<()> {
        if self.exists(stack.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                stack.id.0
            )));
        }
        self.volumetric_stacks.insert(stack.id, stack);
        Ok(())
    }

    /// Sets the Secure Content key store for this model (replaces any existing key store).
    pub fn set_key_store(&mut self, store: KeyStore) {
        self.key_store = Some(store);
    }

    /// Retrieves an object by its ID.
    ///
    /// Returns `None` if no object with the given ID exists.
    pub fn get_object(&self, id: ResourceId) -> Option<&Object> {
        self.objects.get(&id)
    }

    /// Retrieves a base materials group by its ID.
    ///
    /// Returns `None` if no base materials group with the given ID exists.
    pub fn get_base_materials(&self, id: ResourceId) -> Option<&BaseMaterialsGroup> {
        self.base_materials.get(&id)
    }

    /// Retrieves a color group by its ID.
    ///
    /// Returns `None` if no color group with the given ID exists.
    pub fn get_color_group(&self, id: ResourceId) -> Option<&ColorGroup> {
        self.color_groups.get(&id)
    }

    /// Retrieves a slice stack by its ID.
    ///
    /// Returns `None` if no slice stack with the given ID exists.
    pub fn get_slice_stack(&self, id: ResourceId) -> Option<&SliceStack> {
        self.slice_stacks.get(&id)
    }

    /// Retrieves a volumetric stack by its ID.
    ///
    /// Returns `None` if no volumetric stack with the given ID exists.
    pub fn get_volumetric_stack(&self, id: ResourceId) -> Option<&VolumetricStack> {
        self.volumetric_stacks.get(&id)
    }

    /// Adds a 2D texture resource to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_texture_2d(&mut self, texture: Texture2D) -> Result<()> {
        if self.exists(texture.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                texture.id.0
            )));
        }
        self.texture_2d.insert(texture.id, texture);
        Ok(())
    }

    /// Adds a 2D texture coordinate group to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_texture_2d_group(&mut self, group: Texture2DGroup) -> Result<()> {
        if self.exists(group.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                group.id.0
            )));
        }
        self.texture_2d_groups.insert(group.id, group);
        Ok(())
    }

    /// Retrieves a 2D texture coordinate group by its ID.
    ///
    /// Returns `None` if no texture 2D group with the given ID exists.
    pub fn get_texture_2d_group(&self, id: ResourceId) -> Option<&Texture2DGroup> {
        self.texture_2d_groups.get(&id)
    }

    /// Adds a composite materials group to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_composite_materials(&mut self, group: CompositeMaterials) -> Result<()> {
        if self.exists(group.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                group.id.0
            )));
        }
        self.composite_materials.insert(group.id, group);
        Ok(())
    }

    /// Retrieves a composite materials group by its ID.
    ///
    /// Returns `None` if no composite materials group with the given ID exists.
    pub fn get_composite_materials(&self, id: ResourceId) -> Option<&CompositeMaterials> {
        self.composite_materials.get(&id)
    }

    /// Adds a multi-properties group to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_multi_properties(&mut self, group: MultiProperties) -> Result<()> {
        if self.exists(group.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                group.id.0
            )));
        }
        self.multi_properties.insert(group.id, group);
        Ok(())
    }

    /// Retrieves a multi-properties group by its ID.
    ///
    /// Returns `None` if no multi-properties group with the given ID exists.
    pub fn get_multi_properties(&self, id: ResourceId) -> Option<&MultiProperties> {
        self.multi_properties.get(&id)
    }

    /// Returns the number of base material groups in the collection.
    pub fn base_material_groups_count(&self) -> usize {
        self.base_materials.len()
    }

    /// Returns the number of color groups in the collection.
    pub fn color_groups_count(&self) -> usize {
        self.color_groups.len()
    }

    /// Returns the number of volumetric stacks in the collection.
    pub fn volumetric_stacks_count(&self) -> usize {
        self.volumetric_stacks.len()
    }

    /// Returns the number of 2D texture coordinate groups in the collection.
    pub fn texture_2d_groups_count(&self) -> usize {
        self.texture_2d_groups.len()
    }

    /// Returns the number of composite materials groups in the collection.
    pub fn composite_materials_count(&self) -> usize {
        self.composite_materials.len()
    }

    /// Returns the number of multi-properties groups in the collection.
    pub fn multi_properties_count(&self) -> usize {
        self.multi_properties.len()
    }

    /// Returns an iterator over all objects in the collection.
    pub fn iter_objects(&self) -> impl Iterator<Item = &Object> {
        self.objects.values()
    }

    /// Returns a mutable iterator over all objects in the collection.
    pub fn iter_objects_mut(&mut self) -> impl Iterator<Item = &mut Object> {
        self.objects.values_mut()
    }

    /// Returns an iterator over all base material groups in the collection.
    pub fn iter_base_materials(&self) -> impl Iterator<Item = &BaseMaterialsGroup> {
        self.base_materials.values()
    }

    /// Returns an iterator over all color groups in the collection.
    pub fn iter_color_groups(&self) -> impl Iterator<Item = &ColorGroup> {
        self.color_groups.values()
    }

    /// Returns an iterator over all texture 2D groups in the collection.
    pub fn iter_textures(&self) -> impl Iterator<Item = &Texture2DGroup> {
        self.texture_2d_groups.values()
    }

    /// Returns an iterator over all composite material groups in the collection.
    pub fn iter_composite_materials(&self) -> impl Iterator<Item = &CompositeMaterials> {
        self.composite_materials.values()
    }

    /// Returns an iterator over all multi-property groups in the collection.
    pub fn iter_multi_properties(&self) -> impl Iterator<Item = &MultiProperties> {
        self.multi_properties.values()
    }

    /// Adds a 2D displacement texture resource to the collection.
    ///
    /// # Errors
    ///
    /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
    pub fn add_displacement_2d(&mut self, res: Displacement2D) -> Result<()> {
        if self.exists(res.id) {
            return Err(Lib3mfError::Validation(format!(
                "Duplicate resource ID: {}",
                res.id.0
            )));
        }
        self.displacement_2d.insert(res.id, res);
        Ok(())
    }

    /// Retrieves a 2D displacement texture resource by its ID.
    ///
    /// Returns `None` if no displacement 2D resource with the given ID exists.
    pub fn get_displacement_2d(&self, id: ResourceId) -> Option<&Displacement2D> {
        self.displacement_2d.get(&id)
    }

    /// Returns the number of 2D displacement texture resources in the collection.
    pub fn displacement_2d_count(&self) -> usize {
        self.displacement_2d.len()
    }

    /// Returns an iterator over all 2D displacement texture resources in the collection.
    pub fn iter_displacement_2d(&self) -> impl Iterator<Item = &Displacement2D> {
        self.displacement_2d.values()
    }

    /// Returns an iterator over all Texture2D resources in the collection.
    pub fn iter_texture_2d(&self) -> impl Iterator<Item = &Texture2D> {
        self.texture_2d.values()
    }

    /// Returns an iterator over all slice stacks in the collection.
    pub fn iter_slice_stacks(&self) -> impl Iterator<Item = &SliceStack> {
        self.slice_stacks.values()
    }

    /// Returns an iterator over all volumetric stacks in the collection.
    pub fn iter_volumetric_stacks(&self) -> impl Iterator<Item = &VolumetricStack> {
        self.volumetric_stacks.values()
    }
}