Skip to main content

lib3mf_core/model/
mesh.rs

1use crate::model::ResourceId;
2use glam::Vec3;
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Type of 3MF object determining validation requirements and build behavior.
7///
8/// Per 3MF Core Specification:
9/// - Model/SolidSupport: Must be manifold, closed volumes
10/// - Support/Surface/Other: Can be non-manifold, open meshes
11///
12/// # Examples
13///
14/// ```
15/// use lib3mf_core::model::ObjectType;
16///
17/// let obj_type = ObjectType::default();
18/// assert_eq!(obj_type, ObjectType::Model);
19///
20/// // Check validation requirements
21/// assert!(ObjectType::Model.requires_manifold());
22/// assert!(!ObjectType::Support.requires_manifold());
23///
24/// // Check build constraints
25/// assert!(ObjectType::Model.can_be_in_build());
26/// assert!(!ObjectType::Other.can_be_in_build());
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
29#[serde(rename_all = "lowercase")]
30pub enum ObjectType {
31    /// Printable part - requires manifold mesh (default)
32    #[default]
33    Model,
34    /// Support structure - non-manifold allowed, can be ignored by consumer
35    Support,
36    /// Solid support structure - manifold required, filled like model
37    #[serde(rename = "solidsupport")]
38    SolidSupport,
39    /// Surface geometry - non-manifold allowed
40    Surface,
41    /// Other geometry - non-manifold allowed, cannot be referenced in build
42    Other,
43}
44
45impl std::fmt::Display for ObjectType {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            ObjectType::Model => write!(f, "model"),
49            ObjectType::Support => write!(f, "support"),
50            ObjectType::SolidSupport => write!(f, "solidsupport"),
51            ObjectType::Surface => write!(f, "surface"),
52            ObjectType::Other => write!(f, "other"),
53        }
54    }
55}
56
57impl ObjectType {
58    /// Returns true if this type requires manifold mesh validation
59    pub fn requires_manifold(&self) -> bool {
60        matches!(self, ObjectType::Model | ObjectType::SolidSupport)
61    }
62
63    /// Returns true if this type can be referenced in build items
64    pub fn can_be_in_build(&self) -> bool {
65        !matches!(self, ObjectType::Other)
66    }
67}
68
69/// A 3D object resource containing geometry and metadata.
70///
71/// Objects are the primary reusable resources in a 3MF model. They define geometry
72/// (meshes, components, boolean shapes, etc.) and can be referenced by build items
73/// or composed into other objects via components.
74///
75/// Each object has an [`ObjectType`] that determines its validation requirements and
76/// whether it can appear in the build. For example, `Model` and `SolidSupport` types
77/// must be manifold closed volumes, while `Support` and `Surface` types can be
78/// non-manifold.
79///
80/// # Examples
81///
82/// See [`Mesh`] for examples of creating geometry to place in an object.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Object {
85    /// Unique identifier for this resource within the model.
86    /// See [`ResourceId`] for details on the global namespace.
87    pub id: ResourceId,
88    /// Object type determining validation rules and build behavior.
89    #[serde(default)]
90    pub object_type: ObjectType,
91    /// Human-readable name for the object (optional).
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub name: Option<String>,
94    /// Part number for inventory/manufacturing tracking (optional).
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub part_number: Option<String>,
97    /// Production Extension UUID for unique identification (optional).
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub uuid: Option<Uuid>,
100    /// Default property resource ID (material/color) for this object (optional).
101    /// Used when triangles don't specify their own properties.
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub pid: Option<ResourceId>,
104    /// Default property index within the property resource (optional).
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub pindex: Option<u32>,
107    /// Path to the thumbnail image in the 3MF package (optional).
108    /// Used for object-level thumbnails (distinct from package thumbnail).
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub thumbnail: Option<String>,
111    /// The actual geometric content of the object.
112    pub geometry: Geometry,
113}
114
115/// The geometric data contained in an object.
116///
117/// Represents the different types of geometry that can be stored in a 3MF object.
118/// The basic types are meshes and component assemblies, with various extensions
119/// adding support for slices, voxels, boolean operations, and displacement mapping.
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub enum Geometry {
122    /// A triangle mesh (the most common geometry type).
123    Mesh(Mesh),
124    /// A hierarchical assembly of other objects via components.
125    Components(Components),
126    /// A stack of 2D slices for layer-based printing (Slice Extension).
127    /// References a slice stack resource by ID.
128    SliceStack(ResourceId),
129    /// Voxel-based volumetric data (Volumetric Extension).
130    /// References a volumetric stack resource by ID.
131    VolumetricStack(ResourceId),
132    /// Constructive solid geometry from boolean operations (Boolean Operations Extension).
133    BooleanShape(BooleanShape),
134    /// A mesh with displacement mapping for fine surface detail (Displacement Extension).
135    DisplacementMesh(DisplacementMesh),
136}
137
138impl Geometry {
139    /// Returns true if this geometry contains actual content (non-empty mesh,
140    /// components, boolean shapes, or displacement meshes).
141    ///
142    /// A default-constructed `Geometry::Mesh(Mesh::default())` has no content
143    /// (no vertices, no triangles). This is the default return from
144    /// `parse_object_geometry` when no `<mesh>` or `<components>` child element
145    /// is present.
146    pub fn has_content(&self) -> bool {
147        match self {
148            Geometry::Mesh(mesh) => !mesh.vertices.is_empty() || !mesh.triangles.is_empty(),
149            Geometry::Components(c) => !c.components.is_empty(),
150            Geometry::BooleanShape(_) => true,
151            Geometry::DisplacementMesh(_) => true,
152            // SliceStack and VolumetricStack are references, not inline content
153            Geometry::SliceStack(_) | Geometry::VolumetricStack(_) => false,
154        }
155    }
156}
157
158/// A collection of components forming a hierarchical assembly.
159///
160/// Components allow building complex objects by composing and transforming
161/// other objects. This enables reuse and efficient representation of
162/// assemblies with repeated parts.
163#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
164pub struct Components {
165    /// The list of component instances in this assembly.
166    pub components: Vec<Component>,
167}
168
169/// A reference to another object with optional transformation.
170///
171/// Components enable hierarchical object composition by referencing other
172/// objects (which can themselves contain meshes or more components). Each
173/// component can apply a transformation matrix to position/rotate/scale
174/// the referenced object.
175///
176/// # Examples
177///
178/// Components are commonly used to:
179/// - Create assemblies from multiple parts
180/// - Reuse the same object in different positions (instances)
181/// - Apply transformations (rotation, scaling, translation) to objects
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct Component {
184    /// ID of the object being referenced.
185    pub object_id: ResourceId,
186    /// External reference path for production tracking (optional).
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub path: Option<String>,
189    /// Production Extension UUID for this component instance (optional).
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub uuid: Option<Uuid>,
192    /// Transformation matrix applied to the referenced object.
193    /// Defaults to identity (no transformation).
194    #[serde(default = "default_transform", skip_serializing_if = "is_identity")]
195    pub transform: glam::Mat4,
196}
197
198fn default_transform() -> glam::Mat4 {
199    glam::Mat4::IDENTITY
200}
201
202fn is_identity(transform: &glam::Mat4) -> bool {
203    *transform == glam::Mat4::IDENTITY
204}
205
206/// Type of boolean operation to apply between shapes.
207///
208/// Per 3MF Boolean Operations Extension v1.1.1:
209/// - Union: Combines both shapes (default)
210/// - Difference: Subtracts operation shape from base
211/// - Intersection: Only keeps overlapping volume
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
213#[serde(rename_all = "lowercase")]
214pub enum BooleanOperationType {
215    /// Combine both shapes (default)
216    #[default]
217    Union,
218    /// Subtract operation shape from base
219    Difference,
220    /// Keep only overlapping volume
221    Intersection,
222}
223
224/// A single boolean operation applied to a shape.
225///
226/// Represents one `<boolean>` element within a `<booleanshape>`.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct BooleanOperation {
229    /// Type of boolean operation (union, difference, intersection)
230    #[serde(default)]
231    pub operation_type: BooleanOperationType,
232    /// Reference to the object to apply
233    pub object_id: ResourceId,
234    /// Transformation matrix applied to the operation object
235    #[serde(default = "default_transform", skip_serializing_if = "is_identity")]
236    pub transform: glam::Mat4,
237    /// Optional external reference path (p:path)
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub path: Option<String>,
240}
241
242/// A boolean shape combining multiple objects with CSG operations.
243///
244/// Represents a `<booleanshape>` resource that defines geometry through
245/// constructive solid geometry (CSG) operations.
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct BooleanShape {
248    /// Base object to start with
249    pub base_object_id: ResourceId,
250    /// Transformation applied to base object
251    #[serde(default = "default_transform", skip_serializing_if = "is_identity")]
252    pub base_transform: glam::Mat4,
253    /// Optional external reference path for base (p:path)
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub base_path: Option<String>,
256    /// Ordered list of boolean operations to apply
257    pub operations: Vec<BooleanOperation>,
258}
259
260/// A triangle mesh representing 3D geometry.
261///
262/// A mesh is the fundamental geometry container in 3MF, consisting of vertices
263/// (3D points) and triangles that connect those vertices. Meshes can optionally
264/// include beam lattice structures for lightweight, high-strength geometry.
265///
266/// # Examples
267///
268/// ```
269/// use lib3mf_core::model::Mesh;
270///
271/// let mut mesh = Mesh::new();
272/// let v1 = mesh.add_vertex(0.0, 0.0, 0.0);
273/// let v2 = mesh.add_vertex(1.0, 0.0, 0.0);
274/// let v3 = mesh.add_vertex(0.0, 1.0, 0.0);
275/// mesh.add_triangle(v1, v2, v3);
276/// assert_eq!(mesh.triangles.len(), 1);
277/// ```
278#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
279pub struct Mesh {
280    /// List of vertices (points in 3D space).
281    pub vertices: Vec<Vertex>,
282    /// List of triangles connecting vertices by their indices.
283    pub triangles: Vec<Triangle>,
284    /// Beam Lattice extension data for structural lattice geometry (optional).
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub beam_lattice: Option<BeamLattice>,
287}
288
289/// Beam lattice structure for lightweight, high-strength geometry.
290///
291/// The Beam Lattice extension allows representing cylindrical beams between
292/// vertices as an alternative to solid triangle meshes. This is particularly
293/// useful for lightweight structures, scaffolding, and lattice infill patterns.
294///
295/// Each beam is a cylinder connecting two vertices with potentially different
296/// radii at each end (creating tapered beams).
297#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
298pub struct BeamLattice {
299    /// Default radius for beams that don't specify r1 (from beamlattice@radius).
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub radius: Option<f32>,
302    /// Minimum beam length threshold (beams shorter than this may be ignored).
303    #[serde(default)]
304    pub min_length: f32,
305    /// Precision for beam representation (implementation-defined).
306    #[serde(default)]
307    pub precision: f32,
308    /// How beams should be clipped by the mesh boundary.
309    #[serde(default)]
310    pub clipping_mode: ClippingMode,
311    /// The list of beams in this lattice.
312    pub beams: Vec<Beam>,
313    /// Named groups of beams for organization and material assignment.
314    pub beam_sets: Vec<BeamSet>,
315}
316
317/// Clipping mode for beam lattice geometry.
318///
319/// Controls how beams interact with the mesh boundary.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
321#[serde(rename_all = "lowercase")]
322pub enum ClippingMode {
323    /// No clipping applied (default).
324    #[default]
325    None,
326    /// Clip beams to inside the mesh boundary.
327    Inside,
328    /// Clip beams to outside the mesh boundary.
329    Outside,
330}
331
332/// A cylindrical beam connecting two vertices.
333///
334/// Beams are defined by two vertex indices and a radius at each end,
335/// allowing for tapered beams. They can have different materials at
336/// each endpoint via property indices.
337#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
338pub struct Beam {
339    /// Index of the first vertex.
340    pub v1: u32,
341    /// Index of the second vertex.
342    pub v2: u32,
343    /// Radius at the first vertex.
344    pub r1: f32,
345    /// Radius at the second vertex.
346    pub r2: f32,
347    /// Property index at the first vertex (optional).
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub p1: Option<u32>,
350    /// Property index at the second vertex (optional).
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub p2: Option<u32>,
353    /// Cap style for the beam ends.
354    #[serde(default)]
355    pub cap_mode: CapMode,
356}
357
358/// End cap style for beams.
359///
360/// Determines how the ends of beams are terminated.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
362#[serde(rename_all = "lowercase")]
363pub enum CapMode {
364    /// Spherical cap (default) - fully rounded.
365    #[default]
366    Sphere,
367    /// Hemispherical cap - half sphere.
368    Hemisphere,
369    /// Flat cap - no rounding.
370    Butt,
371}
372
373/// A named group of beams.
374///
375/// Beam sets allow organizing beams and applying properties to groups.
376#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
377pub struct BeamSet {
378    /// Human-readable name for this beam set (optional).
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub name: Option<String>,
381    /// Machine-readable identifier (optional).
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub identifier: Option<String>,
384    /// Indices of beams in this set (references into the beams array).
385    pub refs: Vec<u32>,
386}
387
388impl Mesh {
389    /// Creates a new empty mesh.
390    pub fn new() -> Self {
391        Self::default()
392    }
393
394    /// Adds a vertex to the mesh and returns its index.
395    ///
396    /// # Arguments
397    ///
398    /// * `x` - X coordinate in model units
399    /// * `y` - Y coordinate in model units
400    /// * `z` - Z coordinate in model units
401    ///
402    /// # Returns
403    ///
404    /// The index of the newly added vertex, which can be used to reference this vertex in triangles.
405    pub fn add_vertex(&mut self, x: f32, y: f32, z: f32) -> u32 {
406        self.vertices.push(Vertex { x, y, z });
407        (self.vertices.len() - 1) as u32
408    }
409
410    /// Adds a triangle to the mesh connecting three vertices.
411    ///
412    /// # Arguments
413    ///
414    /// * `v1` - Index of the first vertex
415    /// * `v2` - Index of the second vertex
416    /// * `v3` - Index of the third vertex
417    ///
418    /// The vertex indices should be in counter-clockwise order when viewed from outside
419    /// the mesh for correct normal orientation.
420    pub fn add_triangle(&mut self, v1: u32, v2: u32, v3: u32) {
421        self.triangles.push(Triangle {
422            v1,
423            v2,
424            v3,
425            ..Default::default()
426        });
427    }
428
429    /// Computes the axis-aligned bounding box (AABB) of the mesh.
430    ///
431    /// Returns `None` if the mesh has no vertices.
432    pub fn compute_aabb(&self) -> Option<crate::model::stats::BoundingBox> {
433        if self.vertices.is_empty() {
434            return None;
435        }
436
437        let initial = (
438            f32::INFINITY,
439            f32::INFINITY,
440            f32::INFINITY,
441            f32::NEG_INFINITY,
442            f32::NEG_INFINITY,
443            f32::NEG_INFINITY,
444        );
445
446        #[cfg(feature = "parallel")]
447        let (min_x, min_y, min_z, max_x, max_y, max_z) = {
448            use rayon::prelude::*;
449            self.vertices
450                .par_iter()
451                .fold(
452                    || initial,
453                    |acc, v| {
454                        (
455                            acc.0.min(v.x),
456                            acc.1.min(v.y),
457                            acc.2.min(v.z),
458                            acc.3.max(v.x),
459                            acc.4.max(v.y),
460                            acc.5.max(v.z),
461                        )
462                    },
463                )
464                .reduce(
465                    || initial,
466                    |a, b| {
467                        (
468                            a.0.min(b.0),
469                            a.1.min(b.1),
470                            a.2.min(b.2),
471                            a.3.max(b.3),
472                            a.4.max(b.4),
473                            a.5.max(b.5),
474                        )
475                    },
476                )
477        };
478
479        #[cfg(not(feature = "parallel"))]
480        let (min_x, min_y, min_z, max_x, max_y, max_z) =
481            self.vertices.iter().fold(initial, |acc, v| {
482                (
483                    acc.0.min(v.x),
484                    acc.1.min(v.y),
485                    acc.2.min(v.z),
486                    acc.3.max(v.x),
487                    acc.4.max(v.y),
488                    acc.5.max(v.z),
489                )
490            });
491
492        Some(crate::model::stats::BoundingBox {
493            min: [min_x, min_y, min_z],
494            max: [max_x, max_y, max_z],
495        })
496    }
497
498    /// Computes the total surface area and volume of the mesh.
499    ///
500    /// Uses triangle area calculation and signed tetrahedron volumes.
501    /// Returns (0.0, 0.0) if the mesh has no triangles.
502    ///
503    /// # Returns
504    ///
505    /// A tuple of (surface_area, volume) in square and cubic model units respectively.
506    pub fn compute_area_and_volume(&self) -> (f64, f64) {
507        if self.triangles.is_empty() {
508            return (0.0, 0.0);
509        }
510
511        #[cfg(feature = "parallel")]
512        let (area, volume) = {
513            use rayon::prelude::*;
514            self.triangles
515                .par_iter()
516                .fold(
517                    || (0.0f64, 0.0f64),
518                    |acc, t| {
519                        let (area, volume) = self.compute_triangle_stats(t);
520                        (acc.0 + area, acc.1 + volume)
521                    },
522                )
523                .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
524        };
525
526        #[cfg(not(feature = "parallel"))]
527        let (area, volume) = self.triangles.iter().fold((0.0f64, 0.0f64), |acc, t| {
528            let (area, volume) = self.compute_triangle_stats(t);
529            (acc.0 + area, acc.1 + volume)
530        });
531
532        (area, volume)
533    }
534
535    fn compute_triangle_stats(&self, t: &Triangle) -> (f64, f64) {
536        let v1 = glam::Vec3::new(
537            self.vertices[t.v1 as usize].x,
538            self.vertices[t.v1 as usize].y,
539            self.vertices[t.v1 as usize].z,
540        );
541        let v2 = glam::Vec3::new(
542            self.vertices[t.v2 as usize].x,
543            self.vertices[t.v2 as usize].y,
544            self.vertices[t.v2 as usize].z,
545        );
546        let v3 = glam::Vec3::new(
547            self.vertices[t.v3 as usize].x,
548            self.vertices[t.v3 as usize].y,
549            self.vertices[t.v3 as usize].z,
550        );
551
552        // Area using cross product
553        let edge1 = v2 - v1;
554        let edge2 = v3 - v1;
555        let cross = edge1.cross(edge2);
556        let triangle_area = 0.5 * cross.length() as f64;
557
558        // Signed volume of tetrahedron from origin
559        let triangle_volume = (v1.dot(v2.cross(v3)) / 6.0) as f64;
560
561        (triangle_area, triangle_volume)
562    }
563
564    /// Computes the area of a single triangle.
565    ///
566    /// # Arguments
567    ///
568    /// * `triangle` - Reference to the triangle whose area to compute
569    ///
570    /// # Returns
571    ///
572    /// The area of the triangle in square model units.
573    pub fn compute_triangle_area(&self, triangle: &Triangle) -> f64 {
574        let v1 = glam::Vec3::new(
575            self.vertices[triangle.v1 as usize].x,
576            self.vertices[triangle.v1 as usize].y,
577            self.vertices[triangle.v1 as usize].z,
578        );
579        let v2 = glam::Vec3::new(
580            self.vertices[triangle.v2 as usize].x,
581            self.vertices[triangle.v2 as usize].y,
582            self.vertices[triangle.v2 as usize].z,
583        );
584        let v3 = glam::Vec3::new(
585            self.vertices[triangle.v3 as usize].x,
586            self.vertices[triangle.v3 as usize].y,
587            self.vertices[triangle.v3 as usize].z,
588        );
589
590        let edge1 = v2 - v1;
591        let edge2 = v3 - v1;
592        let cross = edge1.cross(edge2);
593        0.5 * cross.length() as f64
594    }
595}
596
597/// A single point in 3D space.
598///
599/// Represents a vertex position in the mesh coordinate system.
600/// Coordinates are in model units (see [`Unit`](crate::model::Unit)).
601#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
602pub struct Vertex {
603    /// X coordinate in model units
604    pub x: f32,
605    /// Y coordinate in model units
606    pub y: f32,
607    /// Z coordinate in model units
608    pub z: f32,
609}
610
611impl From<Vec3> for Vertex {
612    fn from(v: Vec3) -> Self {
613        Self {
614            x: v.x,
615            y: v.y,
616            z: v.z,
617        }
618    }
619}
620
621/// A triangle face defined by three vertex indices.
622///
623/// Triangles are the fundamental building blocks of 3MF meshes. They reference
624/// vertices by index and can optionally specify material properties per-vertex
625/// or per-triangle.
626///
627/// # Material Properties
628///
629/// The property system allows materials/colors to be assigned at different levels:
630/// - **Triangle-level**: Use `pid` to assign a property resource to the entire triangle
631/// - **Vertex-level**: Use `p1`, `p2`, `p3` to assign different properties to each vertex
632/// - **Object-level**: If no triangle properties are set, the object's default `pid`/`pindex` apply
633///
634/// The property resolution hierarchy is: Vertex → Triangle → Object → None
635#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
636pub struct Triangle {
637    /// Index of the first vertex (counter-clockwise winding).
638    pub v1: u32,
639    /// Index of the second vertex (counter-clockwise winding).
640    pub v2: u32,
641    /// Index of the third vertex (counter-clockwise winding).
642    pub v3: u32,
643
644    /// Property index for v1 (optional, for per-vertex material assignment).
645    #[serde(skip_serializing_if = "Option::is_none")]
646    pub p1: Option<u32>,
647    /// Property index for v2 (optional, for per-vertex material assignment).
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub p2: Option<u32>,
650    /// Property index for v3 (optional, for per-vertex material assignment).
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub p3: Option<u32>,
653
654    /// Property ID resource for the entire triangle (optional, for per-triangle material assignment).
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub pid: Option<u32>,
657}
658
659/// A normal vector for displacement mesh vertices.
660#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
661pub struct NormalVector {
662    /// X component of the normal.
663    pub nx: f32,
664    /// Y component of the normal.
665    pub ny: f32,
666    /// Z component of the normal.
667    pub nz: f32,
668}
669
670/// A gradient vector for displacement texture mapping.
671#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
672pub struct GradientVector {
673    /// U (horizontal) gradient component.
674    pub gu: f32,
675    /// V (vertical) gradient component.
676    pub gv: f32,
677}
678
679/// A triangle in a displacement mesh with displacement coordinate indices.
680#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
681pub struct DisplacementTriangle {
682    /// Index of the first vertex.
683    pub v1: u32,
684    /// Index of the second vertex.
685    pub v2: u32,
686    /// Index of the third vertex.
687    pub v3: u32,
688
689    /// Displacement coordinate index for v1 (optional).
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub d1: Option<u32>,
692    /// Displacement coordinate index for v2 (optional).
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub d2: Option<u32>,
695    /// Displacement coordinate index for v3 (optional).
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub d3: Option<u32>,
698
699    /// Property index for v1 (optional).
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub p1: Option<u32>,
702    /// Property index for v2 (optional).
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub p2: Option<u32>,
705    /// Property index for v3 (optional).
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub p3: Option<u32>,
708
709    /// Property ID for the entire triangle (optional).
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub pid: Option<u32>,
712}
713
714/// A mesh with displacement mapping support.
715#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
716pub struct DisplacementMesh {
717    /// List of vertices (points in 3D space).
718    pub vertices: Vec<Vertex>,
719    /// List of triangles connecting vertices.
720    pub triangles: Vec<DisplacementTriangle>,
721    /// Per-vertex normal vectors (must match vertex count).
722    pub normals: Vec<NormalVector>,
723    /// Per-vertex gradient vectors (optional).
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub gradients: Option<Vec<GradientVector>>,
726}