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}