lib3mf_core/model/resources.rs
1use crate::error::{Lib3mfError, Result};
2use crate::model::{
3 BaseMaterialsGroup, ColorGroup, CompositeMaterials, Displacement2D, KeyStore, MultiProperties,
4 Object, SliceStack, Texture2D, Texture2DGroup, VolumetricStack,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Unique identifier for a resource within the model.
10///
11/// A type-safe wrapper around `u32` that prevents accidentally mixing resource IDs
12/// with raw integers. All resources in a 3MF model share a global ID namespace,
13/// meaning an ID can only be used once across all resource types (objects, materials,
14/// textures, etc.).
15///
16/// # Examples
17///
18/// ```
19/// use lib3mf_core::model::ResourceId;
20///
21/// let id = ResourceId(42);
22/// assert_eq!(id.0, 42);
23/// ```
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
25pub struct ResourceId(pub u32);
26
27/// Central registry for all resources in a 3MF model.
28///
29/// The `ResourceCollection` manages all reusable resources including objects,
30/// materials, textures, and extension-specific resources. It enforces the global
31/// ID namespace requirement: each [`ResourceId`] can only be used once across
32/// all resource types.
33///
34/// Resources are stored in separate `HashMap<ResourceId, T>` collections internally,
35/// allowing efficient lookup by ID.
36///
37/// # Examples
38///
39/// ```
40/// use lib3mf_core::model::{ResourceCollection, Object, ResourceId, Geometry, Mesh, ObjectType};
41///
42/// let mut resources = ResourceCollection::new();
43///
44/// let obj = Object {
45/// id: ResourceId(1),
46/// object_type: ObjectType::Model,
47/// name: None,
48/// part_number: None,
49/// uuid: None,
50/// pid: None,
51/// pindex: None,
52/// thumbnail: None,
53/// geometry: Geometry::Mesh(Mesh::default()),
54/// };
55///
56/// resources.add_object(obj).expect("Failed to add object");
57/// assert!(resources.exists(ResourceId(1)));
58/// ```
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct ResourceCollection {
61 objects: HashMap<ResourceId, Object>,
62 base_materials: HashMap<ResourceId, BaseMaterialsGroup>,
63 color_groups: HashMap<ResourceId, ColorGroup>,
64 slice_stacks: HashMap<ResourceId, SliceStack>,
65 volumetric_stacks: HashMap<ResourceId, VolumetricStack>,
66 texture_2d: HashMap<ResourceId, Texture2D>,
67 texture_2d_groups: HashMap<ResourceId, Texture2DGroup>,
68 composite_materials: HashMap<ResourceId, CompositeMaterials>,
69 multi_properties: HashMap<ResourceId, MultiProperties>,
70 displacement_2d: HashMap<ResourceId, Displacement2D>,
71 /// Optional Secure Content key store for this model (at most one per model).
72 pub key_store: Option<KeyStore>, // Usually one KeyStore per model/part
73}
74
75impl ResourceCollection {
76 /// Creates a new empty resource collection.
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 /// Checks if a resource with the given ID exists in any resource type.
82 ///
83 /// Returns `true` if the ID is used by any resource (object, material, texture, etc.).
84 pub fn exists(&self, id: ResourceId) -> bool {
85 self.objects.contains_key(&id)
86 || self.base_materials.contains_key(&id)
87 || self.color_groups.contains_key(&id)
88 || self.slice_stacks.contains_key(&id)
89 || self.volumetric_stacks.contains_key(&id)
90 || self.texture_2d.contains_key(&id)
91 || self.texture_2d_groups.contains_key(&id)
92 || self.composite_materials.contains_key(&id)
93 || self.multi_properties.contains_key(&id)
94 || self.displacement_2d.contains_key(&id)
95 }
96
97 /// Adds an object to the collection.
98 ///
99 /// # Errors
100 ///
101 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
102 pub fn add_object(&mut self, object: Object) -> Result<()> {
103 if self.exists(object.id) {
104 return Err(Lib3mfError::Validation(format!(
105 "Duplicate resource ID: {}",
106 object.id.0
107 )));
108 }
109 self.objects.insert(object.id, object);
110 Ok(())
111 }
112
113 /// Adds a base materials group to the collection.
114 ///
115 /// # Errors
116 ///
117 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
118 pub fn add_base_materials(&mut self, group: BaseMaterialsGroup) -> Result<()> {
119 if self.exists(group.id) {
120 return Err(Lib3mfError::Validation(format!(
121 "Duplicate resource ID: {}",
122 group.id.0
123 )));
124 }
125 self.base_materials.insert(group.id, group);
126 Ok(())
127 }
128
129 /// Adds a color group to the collection.
130 ///
131 /// # Errors
132 ///
133 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
134 pub fn add_color_group(&mut self, group: ColorGroup) -> Result<()> {
135 if self.exists(group.id) {
136 return Err(Lib3mfError::Validation(format!(
137 "Duplicate resource ID: {}",
138 group.id.0
139 )));
140 }
141 self.color_groups.insert(group.id, group);
142 Ok(())
143 }
144
145 /// Adds a slice stack resource to the collection.
146 ///
147 /// # Errors
148 ///
149 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
150 pub fn add_slice_stack(&mut self, stack: SliceStack) -> Result<()> {
151 if self.exists(stack.id) {
152 return Err(Lib3mfError::Validation(format!(
153 "Duplicate resource ID: {}",
154 stack.id.0
155 )));
156 }
157 self.slice_stacks.insert(stack.id, stack);
158 Ok(())
159 }
160
161 /// Adds a volumetric stack resource to the collection.
162 ///
163 /// # Errors
164 ///
165 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
166 pub fn add_volumetric_stack(&mut self, stack: VolumetricStack) -> Result<()> {
167 if self.exists(stack.id) {
168 return Err(Lib3mfError::Validation(format!(
169 "Duplicate resource ID: {}",
170 stack.id.0
171 )));
172 }
173 self.volumetric_stacks.insert(stack.id, stack);
174 Ok(())
175 }
176
177 /// Sets the Secure Content key store for this model (replaces any existing key store).
178 pub fn set_key_store(&mut self, store: KeyStore) {
179 self.key_store = Some(store);
180 }
181
182 /// Retrieves an object by its ID.
183 ///
184 /// Returns `None` if no object with the given ID exists.
185 pub fn get_object(&self, id: ResourceId) -> Option<&Object> {
186 self.objects.get(&id)
187 }
188
189 /// Retrieves a base materials group by its ID.
190 ///
191 /// Returns `None` if no base materials group with the given ID exists.
192 pub fn get_base_materials(&self, id: ResourceId) -> Option<&BaseMaterialsGroup> {
193 self.base_materials.get(&id)
194 }
195
196 /// Retrieves a color group by its ID.
197 ///
198 /// Returns `None` if no color group with the given ID exists.
199 pub fn get_color_group(&self, id: ResourceId) -> Option<&ColorGroup> {
200 self.color_groups.get(&id)
201 }
202
203 /// Retrieves a slice stack by its ID.
204 ///
205 /// Returns `None` if no slice stack with the given ID exists.
206 pub fn get_slice_stack(&self, id: ResourceId) -> Option<&SliceStack> {
207 self.slice_stacks.get(&id)
208 }
209
210 /// Retrieves a volumetric stack by its ID.
211 ///
212 /// Returns `None` if no volumetric stack with the given ID exists.
213 pub fn get_volumetric_stack(&self, id: ResourceId) -> Option<&VolumetricStack> {
214 self.volumetric_stacks.get(&id)
215 }
216
217 /// Adds a 2D texture resource to the collection.
218 ///
219 /// # Errors
220 ///
221 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
222 pub fn add_texture_2d(&mut self, texture: Texture2D) -> Result<()> {
223 if self.exists(texture.id) {
224 return Err(Lib3mfError::Validation(format!(
225 "Duplicate resource ID: {}",
226 texture.id.0
227 )));
228 }
229 self.texture_2d.insert(texture.id, texture);
230 Ok(())
231 }
232
233 /// Adds a 2D texture coordinate group to the collection.
234 ///
235 /// # Errors
236 ///
237 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
238 pub fn add_texture_2d_group(&mut self, group: Texture2DGroup) -> Result<()> {
239 if self.exists(group.id) {
240 return Err(Lib3mfError::Validation(format!(
241 "Duplicate resource ID: {}",
242 group.id.0
243 )));
244 }
245 self.texture_2d_groups.insert(group.id, group);
246 Ok(())
247 }
248
249 /// Retrieves a 2D texture coordinate group by its ID.
250 ///
251 /// Returns `None` if no texture 2D group with the given ID exists.
252 pub fn get_texture_2d_group(&self, id: ResourceId) -> Option<&Texture2DGroup> {
253 self.texture_2d_groups.get(&id)
254 }
255
256 /// Adds a composite materials group to the collection.
257 ///
258 /// # Errors
259 ///
260 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
261 pub fn add_composite_materials(&mut self, group: CompositeMaterials) -> Result<()> {
262 if self.exists(group.id) {
263 return Err(Lib3mfError::Validation(format!(
264 "Duplicate resource ID: {}",
265 group.id.0
266 )));
267 }
268 self.composite_materials.insert(group.id, group);
269 Ok(())
270 }
271
272 /// Retrieves a composite materials group by its ID.
273 ///
274 /// Returns `None` if no composite materials group with the given ID exists.
275 pub fn get_composite_materials(&self, id: ResourceId) -> Option<&CompositeMaterials> {
276 self.composite_materials.get(&id)
277 }
278
279 /// Adds a multi-properties group to the collection.
280 ///
281 /// # Errors
282 ///
283 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
284 pub fn add_multi_properties(&mut self, group: MultiProperties) -> Result<()> {
285 if self.exists(group.id) {
286 return Err(Lib3mfError::Validation(format!(
287 "Duplicate resource ID: {}",
288 group.id.0
289 )));
290 }
291 self.multi_properties.insert(group.id, group);
292 Ok(())
293 }
294
295 /// Retrieves a multi-properties group by its ID.
296 ///
297 /// Returns `None` if no multi-properties group with the given ID exists.
298 pub fn get_multi_properties(&self, id: ResourceId) -> Option<&MultiProperties> {
299 self.multi_properties.get(&id)
300 }
301
302 /// Returns the number of base material groups in the collection.
303 pub fn base_material_groups_count(&self) -> usize {
304 self.base_materials.len()
305 }
306
307 /// Returns the number of color groups in the collection.
308 pub fn color_groups_count(&self) -> usize {
309 self.color_groups.len()
310 }
311
312 /// Returns the number of volumetric stacks in the collection.
313 pub fn volumetric_stacks_count(&self) -> usize {
314 self.volumetric_stacks.len()
315 }
316
317 /// Returns the number of 2D texture coordinate groups in the collection.
318 pub fn texture_2d_groups_count(&self) -> usize {
319 self.texture_2d_groups.len()
320 }
321
322 /// Returns the number of composite materials groups in the collection.
323 pub fn composite_materials_count(&self) -> usize {
324 self.composite_materials.len()
325 }
326
327 /// Returns the number of multi-properties groups in the collection.
328 pub fn multi_properties_count(&self) -> usize {
329 self.multi_properties.len()
330 }
331
332 /// Returns an iterator over all objects in the collection.
333 pub fn iter_objects(&self) -> impl Iterator<Item = &Object> {
334 self.objects.values()
335 }
336
337 /// Returns a mutable iterator over all objects in the collection.
338 pub fn iter_objects_mut(&mut self) -> impl Iterator<Item = &mut Object> {
339 self.objects.values_mut()
340 }
341
342 /// Returns an iterator over all base material groups in the collection.
343 pub fn iter_base_materials(&self) -> impl Iterator<Item = &BaseMaterialsGroup> {
344 self.base_materials.values()
345 }
346
347 /// Returns an iterator over all color groups in the collection.
348 pub fn iter_color_groups(&self) -> impl Iterator<Item = &ColorGroup> {
349 self.color_groups.values()
350 }
351
352 /// Returns an iterator over all texture 2D groups in the collection.
353 pub fn iter_textures(&self) -> impl Iterator<Item = &Texture2DGroup> {
354 self.texture_2d_groups.values()
355 }
356
357 /// Returns an iterator over all composite material groups in the collection.
358 pub fn iter_composite_materials(&self) -> impl Iterator<Item = &CompositeMaterials> {
359 self.composite_materials.values()
360 }
361
362 /// Returns an iterator over all multi-property groups in the collection.
363 pub fn iter_multi_properties(&self) -> impl Iterator<Item = &MultiProperties> {
364 self.multi_properties.values()
365 }
366
367 /// Adds a 2D displacement texture resource to the collection.
368 ///
369 /// # Errors
370 ///
371 /// Returns `Lib3mfError::Validation` if a resource with the same ID already exists.
372 pub fn add_displacement_2d(&mut self, res: Displacement2D) -> Result<()> {
373 if self.exists(res.id) {
374 return Err(Lib3mfError::Validation(format!(
375 "Duplicate resource ID: {}",
376 res.id.0
377 )));
378 }
379 self.displacement_2d.insert(res.id, res);
380 Ok(())
381 }
382
383 /// Retrieves a 2D displacement texture resource by its ID.
384 ///
385 /// Returns `None` if no displacement 2D resource with the given ID exists.
386 pub fn get_displacement_2d(&self, id: ResourceId) -> Option<&Displacement2D> {
387 self.displacement_2d.get(&id)
388 }
389
390 /// Returns the number of 2D displacement texture resources in the collection.
391 pub fn displacement_2d_count(&self) -> usize {
392 self.displacement_2d.len()
393 }
394
395 /// Returns an iterator over all 2D displacement texture resources in the collection.
396 pub fn iter_displacement_2d(&self) -> impl Iterator<Item = &Displacement2D> {
397 self.displacement_2d.values()
398 }
399
400 /// Returns an iterator over all Texture2D resources in the collection.
401 pub fn iter_texture_2d(&self) -> impl Iterator<Item = &Texture2D> {
402 self.texture_2d.values()
403 }
404
405 /// Returns an iterator over all slice stacks in the collection.
406 pub fn iter_slice_stacks(&self) -> impl Iterator<Item = &SliceStack> {
407 self.slice_stacks.values()
408 }
409
410 /// Returns an iterator over all volumetric stacks in the collection.
411 pub fn iter_volumetric_stacks(&self) -> impl Iterator<Item = &VolumetricStack> {
412 self.volumetric_stacks.values()
413 }
414}