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 /// Minimum beam length threshold (beams shorter than this may be ignored).
300 #[serde(default)]
301 pub min_length: f32,
302 /// Precision for beam representation (implementation-defined).
303 #[serde(default)]
304 pub precision: f32,
305 /// How beams should be clipped by the mesh boundary.
306 #[serde(default)]
307 pub clipping_mode: ClippingMode,
308 /// The list of beams in this lattice.
309 pub beams: Vec<Beam>,
310 /// Named groups of beams for organization and material assignment.
311 pub beam_sets: Vec<BeamSet>,
312}
313
314/// Clipping mode for beam lattice geometry.
315///
316/// Controls how beams interact with the mesh boundary.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
318#[serde(rename_all = "lowercase")]
319pub enum ClippingMode {
320 /// No clipping applied (default).
321 #[default]
322 None,
323 /// Clip beams to inside the mesh boundary.
324 Inside,
325 /// Clip beams to outside the mesh boundary.
326 Outside,
327}
328
329/// A cylindrical beam connecting two vertices.
330///
331/// Beams are defined by two vertex indices and a radius at each end,
332/// allowing for tapered beams. They can have different materials at
333/// each endpoint via property indices.
334#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
335pub struct Beam {
336 /// Index of the first vertex.
337 pub v1: u32,
338 /// Index of the second vertex.
339 pub v2: u32,
340 /// Radius at the first vertex.
341 pub r1: f32,
342 /// Radius at the second vertex.
343 pub r2: f32,
344 /// Property index at the first vertex (optional).
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub p1: Option<u32>,
347 /// Property index at the second vertex (optional).
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub p2: Option<u32>,
350 /// Cap style for the beam ends.
351 #[serde(default)]
352 pub cap_mode: CapMode,
353}
354
355/// End cap style for beams.
356///
357/// Determines how the ends of beams are terminated.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
359#[serde(rename_all = "lowercase")]
360pub enum CapMode {
361 /// Spherical cap (default) - fully rounded.
362 #[default]
363 Sphere,
364 /// Hemispherical cap - half sphere.
365 Hemisphere,
366 /// Flat cap - no rounding.
367 Butt,
368}
369
370/// A named group of beams.
371///
372/// Beam sets allow organizing beams and applying properties to groups.
373#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
374pub struct BeamSet {
375 /// Human-readable name for this beam set (optional).
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub name: Option<String>,
378 /// Machine-readable identifier (optional).
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub identifier: Option<String>,
381 /// Indices of beams in this set (references into the beams array).
382 pub refs: Vec<u32>,
383}
384
385impl Mesh {
386 /// Creates a new empty mesh.
387 pub fn new() -> Self {
388 Self::default()
389 }
390
391 /// Adds a vertex to the mesh and returns its index.
392 ///
393 /// # Arguments
394 ///
395 /// * `x` - X coordinate in model units
396 /// * `y` - Y coordinate in model units
397 /// * `z` - Z coordinate in model units
398 ///
399 /// # Returns
400 ///
401 /// The index of the newly added vertex, which can be used to reference this vertex in triangles.
402 pub fn add_vertex(&mut self, x: f32, y: f32, z: f32) -> u32 {
403 self.vertices.push(Vertex { x, y, z });
404 (self.vertices.len() - 1) as u32
405 }
406
407 /// Adds a triangle to the mesh connecting three vertices.
408 ///
409 /// # Arguments
410 ///
411 /// * `v1` - Index of the first vertex
412 /// * `v2` - Index of the second vertex
413 /// * `v3` - Index of the third vertex
414 ///
415 /// The vertex indices should be in counter-clockwise order when viewed from outside
416 /// the mesh for correct normal orientation.
417 pub fn add_triangle(&mut self, v1: u32, v2: u32, v3: u32) {
418 self.triangles.push(Triangle {
419 v1,
420 v2,
421 v3,
422 ..Default::default()
423 });
424 }
425
426 /// Computes the axis-aligned bounding box (AABB) of the mesh.
427 ///
428 /// Returns `None` if the mesh has no vertices.
429 pub fn compute_aabb(&self) -> Option<crate::model::stats::BoundingBox> {
430 if self.vertices.is_empty() {
431 return None;
432 }
433
434 let initial = (
435 f32::INFINITY,
436 f32::INFINITY,
437 f32::INFINITY,
438 f32::NEG_INFINITY,
439 f32::NEG_INFINITY,
440 f32::NEG_INFINITY,
441 );
442
443 #[cfg(feature = "parallel")]
444 let (min_x, min_y, min_z, max_x, max_y, max_z) = {
445 use rayon::prelude::*;
446 self.vertices
447 .par_iter()
448 .fold(
449 || initial,
450 |acc, v| {
451 (
452 acc.0.min(v.x),
453 acc.1.min(v.y),
454 acc.2.min(v.z),
455 acc.3.max(v.x),
456 acc.4.max(v.y),
457 acc.5.max(v.z),
458 )
459 },
460 )
461 .reduce(
462 || initial,
463 |a, b| {
464 (
465 a.0.min(b.0),
466 a.1.min(b.1),
467 a.2.min(b.2),
468 a.3.max(b.3),
469 a.4.max(b.4),
470 a.5.max(b.5),
471 )
472 },
473 )
474 };
475
476 #[cfg(not(feature = "parallel"))]
477 let (min_x, min_y, min_z, max_x, max_y, max_z) =
478 self.vertices.iter().fold(initial, |acc, v| {
479 (
480 acc.0.min(v.x),
481 acc.1.min(v.y),
482 acc.2.min(v.z),
483 acc.3.max(v.x),
484 acc.4.max(v.y),
485 acc.5.max(v.z),
486 )
487 });
488
489 Some(crate::model::stats::BoundingBox {
490 min: [min_x, min_y, min_z],
491 max: [max_x, max_y, max_z],
492 })
493 }
494
495 /// Computes the total surface area and volume of the mesh.
496 ///
497 /// Uses triangle area calculation and signed tetrahedron volumes.
498 /// Returns (0.0, 0.0) if the mesh has no triangles.
499 ///
500 /// # Returns
501 ///
502 /// A tuple of (surface_area, volume) in square and cubic model units respectively.
503 pub fn compute_area_and_volume(&self) -> (f64, f64) {
504 if self.triangles.is_empty() {
505 return (0.0, 0.0);
506 }
507
508 #[cfg(feature = "parallel")]
509 let (area, volume) = {
510 use rayon::prelude::*;
511 self.triangles
512 .par_iter()
513 .fold(
514 || (0.0f64, 0.0f64),
515 |acc, t| {
516 let (area, volume) = self.compute_triangle_stats(t);
517 (acc.0 + area, acc.1 + volume)
518 },
519 )
520 .reduce(|| (0.0, 0.0), |a, b| (a.0 + b.0, a.1 + b.1))
521 };
522
523 #[cfg(not(feature = "parallel"))]
524 let (area, volume) = self.triangles.iter().fold((0.0f64, 0.0f64), |acc, t| {
525 let (area, volume) = self.compute_triangle_stats(t);
526 (acc.0 + area, acc.1 + volume)
527 });
528
529 (area, volume)
530 }
531
532 fn compute_triangle_stats(&self, t: &Triangle) -> (f64, f64) {
533 let v1 = glam::Vec3::new(
534 self.vertices[t.v1 as usize].x,
535 self.vertices[t.v1 as usize].y,
536 self.vertices[t.v1 as usize].z,
537 );
538 let v2 = glam::Vec3::new(
539 self.vertices[t.v2 as usize].x,
540 self.vertices[t.v2 as usize].y,
541 self.vertices[t.v2 as usize].z,
542 );
543 let v3 = glam::Vec3::new(
544 self.vertices[t.v3 as usize].x,
545 self.vertices[t.v3 as usize].y,
546 self.vertices[t.v3 as usize].z,
547 );
548
549 // Area using cross product
550 let edge1 = v2 - v1;
551 let edge2 = v3 - v1;
552 let cross = edge1.cross(edge2);
553 let triangle_area = 0.5 * cross.length() as f64;
554
555 // Signed volume of tetrahedron from origin
556 let triangle_volume = (v1.dot(v2.cross(v3)) / 6.0) as f64;
557
558 (triangle_area, triangle_volume)
559 }
560
561 /// Computes the area of a single triangle.
562 ///
563 /// # Arguments
564 ///
565 /// * `triangle` - Reference to the triangle whose area to compute
566 ///
567 /// # Returns
568 ///
569 /// The area of the triangle in square model units.
570 pub fn compute_triangle_area(&self, triangle: &Triangle) -> f64 {
571 let v1 = glam::Vec3::new(
572 self.vertices[triangle.v1 as usize].x,
573 self.vertices[triangle.v1 as usize].y,
574 self.vertices[triangle.v1 as usize].z,
575 );
576 let v2 = glam::Vec3::new(
577 self.vertices[triangle.v2 as usize].x,
578 self.vertices[triangle.v2 as usize].y,
579 self.vertices[triangle.v2 as usize].z,
580 );
581 let v3 = glam::Vec3::new(
582 self.vertices[triangle.v3 as usize].x,
583 self.vertices[triangle.v3 as usize].y,
584 self.vertices[triangle.v3 as usize].z,
585 );
586
587 let edge1 = v2 - v1;
588 let edge2 = v3 - v1;
589 let cross = edge1.cross(edge2);
590 0.5 * cross.length() as f64
591 }
592}
593
594/// A single point in 3D space.
595///
596/// Represents a vertex position in the mesh coordinate system.
597/// Coordinates are in model units (see [`Unit`](crate::model::Unit)).
598#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
599pub struct Vertex {
600 /// X coordinate in model units
601 pub x: f32,
602 /// Y coordinate in model units
603 pub y: f32,
604 /// Z coordinate in model units
605 pub z: f32,
606}
607
608impl From<Vec3> for Vertex {
609 fn from(v: Vec3) -> Self {
610 Self {
611 x: v.x,
612 y: v.y,
613 z: v.z,
614 }
615 }
616}
617
618/// A triangle face defined by three vertex indices.
619///
620/// Triangles are the fundamental building blocks of 3MF meshes. They reference
621/// vertices by index and can optionally specify material properties per-vertex
622/// or per-triangle.
623///
624/// # Material Properties
625///
626/// The property system allows materials/colors to be assigned at different levels:
627/// - **Triangle-level**: Use `pid` to assign a property resource to the entire triangle
628/// - **Vertex-level**: Use `p1`, `p2`, `p3` to assign different properties to each vertex
629/// - **Object-level**: If no triangle properties are set, the object's default `pid`/`pindex` apply
630///
631/// The property resolution hierarchy is: Vertex → Triangle → Object → None
632#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
633pub struct Triangle {
634 /// Index of the first vertex (counter-clockwise winding).
635 pub v1: u32,
636 /// Index of the second vertex (counter-clockwise winding).
637 pub v2: u32,
638 /// Index of the third vertex (counter-clockwise winding).
639 pub v3: u32,
640
641 /// Property index for v1 (optional, for per-vertex material assignment).
642 #[serde(skip_serializing_if = "Option::is_none")]
643 pub p1: Option<u32>,
644 /// Property index for v2 (optional, for per-vertex material assignment).
645 #[serde(skip_serializing_if = "Option::is_none")]
646 pub p2: Option<u32>,
647 /// Property index for v3 (optional, for per-vertex material assignment).
648 #[serde(skip_serializing_if = "Option::is_none")]
649 pub p3: Option<u32>,
650
651 /// Property ID resource for the entire triangle (optional, for per-triangle material assignment).
652 #[serde(skip_serializing_if = "Option::is_none")]
653 pub pid: Option<u32>,
654}
655
656/// A normal vector for displacement mesh vertices.
657#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
658pub struct NormalVector {
659 pub nx: f32,
660 pub ny: f32,
661 pub nz: f32,
662}
663
664/// A gradient vector for displacement texture mapping.
665#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
666pub struct GradientVector {
667 pub gu: f32,
668 pub gv: f32,
669}
670
671/// A triangle in a displacement mesh with displacement coordinate indices.
672#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
673pub struct DisplacementTriangle {
674 /// Index of the first vertex.
675 pub v1: u32,
676 /// Index of the second vertex.
677 pub v2: u32,
678 /// Index of the third vertex.
679 pub v3: u32,
680
681 /// Displacement coordinate index for v1 (optional).
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub d1: Option<u32>,
684 /// Displacement coordinate index for v2 (optional).
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub d2: Option<u32>,
687 /// Displacement coordinate index for v3 (optional).
688 #[serde(skip_serializing_if = "Option::is_none")]
689 pub d3: Option<u32>,
690
691 /// Property index for v1 (optional).
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub p1: Option<u32>,
694 /// Property index for v2 (optional).
695 #[serde(skip_serializing_if = "Option::is_none")]
696 pub p2: Option<u32>,
697 /// Property index for v3 (optional).
698 #[serde(skip_serializing_if = "Option::is_none")]
699 pub p3: Option<u32>,
700
701 /// Property ID for the entire triangle (optional).
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub pid: Option<u32>,
704}
705
706/// A mesh with displacement mapping support.
707#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
708pub struct DisplacementMesh {
709 /// List of vertices (points in 3D space).
710 pub vertices: Vec<Vertex>,
711 /// List of triangles connecting vertices.
712 pub triangles: Vec<DisplacementTriangle>,
713 /// Per-vertex normal vectors (must match vertex count).
714 pub normals: Vec<NormalVector>,
715 /// Per-vertex gradient vectors (optional).
716 #[serde(skip_serializing_if = "Option::is_none")]
717 pub gradients: Option<Vec<GradientVector>>,
718}