1use std::collections::BTreeSet;
4
5#[cfg(feature = "draco-decode")]
6use draco_core::draco_types::DataType;
7#[cfg(feature = "draco-decode")]
8use draco_core::mesh::Mesh;
9use thiserror::Error as ThisError;
10
11use crate::{ComponentType, ValidationProfile};
12#[cfg(feature = "draco-decode")]
13use crate::{Error, Result};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17#[repr(u32)]
18pub enum PrimitiveMode {
19 Points = 0,
21 Lines = 1,
23 LineLoop = 2,
25 LineStrip = 3,
27 #[default]
29 Triangles = 4,
30 TriangleStrip = 5,
32 TriangleFan = 6,
34}
35
36impl PrimitiveMode {
37 pub fn from_gltf(value: u32) -> Option<Self> {
39 Some(match value {
40 0 => Self::Points,
41 1 => Self::Lines,
42 2 => Self::LineLoop,
43 3 => Self::LineStrip,
44 4 => Self::Triangles,
45 5 => Self::TriangleStrip,
46 6 => Self::TriangleFan,
47 _ => return None,
48 })
49 }
50
51 pub const fn to_gltf(self) -> u32 {
53 self as u32
54 }
55}
56
57#[derive(Clone, Debug, ThisError, PartialEq, Eq)]
59pub enum GeometryError {
60 #[error("packed geometry byte size overflow")]
62 ByteSizeOverflow,
63 #[error("packed {kind} has {actual} bytes, expected {expected}")]
65 ByteLength {
66 kind: &'static str,
68 actual: usize,
70 expected: usize,
72 },
73 #[error("duplicate packed attribute semantic {0:?}")]
75 DuplicateSemantic(String),
76 #[error("attribute {semantic:?} has count {actual}, expected {expected}")]
78 AttributeCount {
79 semantic: String,
81 actual: usize,
83 expected: usize,
85 },
86 #[error("packed geometry is missing POSITION")]
88 MissingPosition,
89 #[error("packed geometry has no vertices")]
91 EmptyGeometry,
92 #[error("{semantic:?} has {actual} components; expected {expected}")]
94 AttributeComponents {
95 semantic: String,
97 actual: u8,
99 expected: &'static str,
101 },
102 #[error("invalid {component_type:?}/normalized={normalized} for {semantic:?} in {profile:?}")]
104 AttributeComponentType {
105 semantic: String,
107 component_type: ComponentType,
109 normalized: bool,
111 profile: ValidationProfile,
113 },
114 #[cfg(feature = "write")]
116 #[error("POSITION contains a non-finite floating-point value")]
117 NonFinitePosition,
118 #[error("invalid {mode:?} element count {count}")]
120 InvalidElementCount {
121 mode: PrimitiveMode,
123 count: usize,
125 },
126 #[error("primitive mode {0} is not supported")]
128 InvalidPrimitiveMode(u32),
129 #[error("packed attribute component count {0} is not supported")]
131 InvalidComponents(u8),
132 #[error("index component type {0:?} is not permitted")]
134 InvalidIndexType(ComponentType),
135 #[error("index {index} is outside vertex count {vertex_count}")]
137 IndexOutOfRange {
138 index: u64,
140 vertex_count: usize,
142 },
143 #[error("decoded Draco {semantic} count {decoded} does not match accessor count {declared}")]
145 DracoAccessorCount {
146 semantic: String,
148 decoded: u64,
150 declared: u64,
152 },
153 #[error("component type {component_type:?} is not permitted by {profile:?}")]
155 ComponentTypeProfile {
156 component_type: ComponentType,
158 profile: ValidationProfile,
160 },
161 #[error("Draco encoding does not support {0}")]
163 UnsupportedDraco(String),
164 #[cfg(feature = "write")]
166 #[error("replacement vertex count {actual} does not match morph target count {expected}")]
167 MorphTargetCount {
168 expected: usize,
170 actual: usize,
172 },
173}
174
175#[derive(Clone, Debug, Eq)]
192pub struct PackedAttribute {
193 semantic: String,
194 count: usize,
195 components: u8,
196 component_type: ComponentType,
197 normalized: bool,
198 bytes: Vec<u8>,
199 source_accessor: Option<usize>,
200}
201
202impl PartialEq for PackedAttribute {
208 fn eq(&self, other: &Self) -> bool {
209 self.semantic == other.semantic
210 && self.count == other.count
211 && self.components == other.components
212 && self.component_type == other.component_type
213 && self.normalized == other.normalized
214 && self.bytes == other.bytes
215 }
216}
217
218impl PackedAttribute {
219 pub fn new(
221 semantic: impl Into<String>,
222 count: usize,
223 components: u8,
224 component_type: ComponentType,
225 normalized: bool,
226 bytes: Vec<u8>,
227 ) -> std::result::Result<Self, GeometryError> {
228 if !(1..=4).contains(&components) {
229 return Err(GeometryError::InvalidComponents(components));
230 }
231 validate_byte_len("attribute", count, components, component_type, bytes.len())?;
232 Ok(Self {
233 semantic: semantic.into(),
234 count,
235 components,
236 component_type,
237 normalized,
238 bytes,
239 source_accessor: None,
240 })
241 }
242
243 #[must_use]
251 pub fn with_source_accessor(mut self, accessor: usize) -> Self {
252 self.source_accessor = Some(accessor);
253 self
254 }
255
256 pub const fn source_accessor(&self) -> Option<usize> {
258 self.source_accessor
259 }
260
261 pub fn semantic(&self) -> &str {
263 &self.semantic
264 }
265
266 pub const fn count(&self) -> usize {
268 self.count
269 }
270
271 pub const fn components(&self) -> u8 {
273 self.components
274 }
275
276 pub const fn component_type(&self) -> ComponentType {
278 self.component_type
279 }
280
281 pub const fn normalized(&self) -> bool {
283 self.normalized
284 }
285
286 pub fn bytes(&self) -> &[u8] {
288 &self.bytes
289 }
290}
291
292#[derive(Clone, Debug, Eq)]
294pub struct PackedIndices {
295 count: usize,
296 component_type: ComponentType,
297 bytes: Vec<u8>,
298 source_accessor: Option<usize>,
299}
300
301impl PartialEq for PackedIndices {
303 fn eq(&self, other: &Self) -> bool {
304 self.count == other.count
305 && self.component_type == other.component_type
306 && self.bytes == other.bytes
307 }
308}
309
310impl PackedIndices {
311 pub fn new(
313 count: usize,
314 component_type: ComponentType,
315 bytes: Vec<u8>,
316 ) -> std::result::Result<Self, GeometryError> {
317 if !matches!(
318 component_type,
319 ComponentType::U8 | ComponentType::U16 | ComponentType::U32
320 ) {
321 return Err(GeometryError::InvalidIndexType(component_type));
322 }
323 validate_byte_len("indices", count, 1, component_type, bytes.len())?;
324 Ok(Self {
325 count,
326 component_type,
327 bytes,
328 source_accessor: None,
329 })
330 }
331
332 #[must_use]
336 pub fn with_source_accessor(mut self, accessor: usize) -> Self {
337 self.source_accessor = Some(accessor);
338 self
339 }
340
341 pub const fn source_accessor(&self) -> Option<usize> {
343 self.source_accessor
344 }
345
346 pub const fn count(&self) -> usize {
348 self.count
349 }
350
351 pub const fn component_type(&self) -> ComponentType {
353 self.component_type
354 }
355
356 pub fn bytes(&self) -> &[u8] {
358 &self.bytes
359 }
360}
361
362#[derive(Clone, Debug, PartialEq, Eq)]
375pub struct PackedGeometry {
376 mode: PrimitiveMode,
377 indices: Option<PackedIndices>,
378 attributes: Vec<PackedAttribute>,
379}
380
381impl PackedGeometry {
382 pub fn new(
384 mode: PrimitiveMode,
385 attributes: Vec<PackedAttribute>,
386 indices: Option<PackedIndices>,
387 ) -> std::result::Result<Self, GeometryError> {
388 let geometry = Self {
389 mode,
390 indices,
391 attributes,
392 };
393 geometry.validate(ValidationProfile::Gltf21Draft)?;
394 Ok(geometry)
395 }
396
397 pub const fn mode(&self) -> PrimitiveMode {
399 self.mode
400 }
401
402 pub fn vertex_count(&self) -> usize {
404 self.attributes.first().map_or(0, PackedAttribute::count)
405 }
406
407 pub fn attributes(&self) -> &[PackedAttribute] {
409 &self.attributes
410 }
411
412 pub fn indices(&self) -> Option<&PackedIndices> {
414 self.indices.as_ref()
415 }
416
417 pub fn validate(&self, profile: ValidationProfile) -> std::result::Result<(), GeometryError> {
419 let mut semantics = BTreeSet::new();
420 let mut vertex_count = None;
421 for attribute in &self.attributes {
422 validate_component_profile(attribute.component_type, profile)?;
423 validate_attribute_components(attribute)?;
424 validate_attribute_profile(attribute, profile)?;
425 if !semantics.insert(attribute.semantic.as_str()) {
426 return Err(GeometryError::DuplicateSemantic(attribute.semantic.clone()));
427 }
428 match vertex_count {
429 None => vertex_count = Some(attribute.count),
430 Some(expected) if expected != attribute.count => {
431 return Err(GeometryError::AttributeCount {
432 semantic: attribute.semantic.clone(),
433 actual: attribute.count,
434 expected,
435 })
436 }
437 _ => {}
438 }
439 }
440 if !semantics.contains("POSITION") {
441 return Err(GeometryError::MissingPosition);
442 }
443 let vertex_count = vertex_count.unwrap_or(0);
444 if vertex_count == 0 {
445 return Err(GeometryError::EmptyGeometry);
446 }
447 if let Some(indices) = &self.indices {
448 validate_component_profile(indices.component_type, profile)?;
449 for index in index_values(indices) {
450 let index = index?;
451 if index >= vertex_count as u64 {
452 return Err(GeometryError::IndexOutOfRange {
453 index,
454 vertex_count,
455 });
456 }
457 }
458 }
459 validate_element_count(
460 self.mode,
461 self.indices
462 .as_ref()
463 .map_or(vertex_count, PackedIndices::count),
464 )?;
465 Ok(())
466 }
467
468 #[cfg(feature = "draco-decode")]
469 pub(crate) fn from_draco_mesh(
470 mode: PrimitiveMode,
471 mesh: &Mesh,
472 attributes: &[(String, u32)],
473 normalized: &std::collections::BTreeMap<String, bool>,
474 ) -> Result<Self> {
475 let attributes = attributes
476 .iter()
477 .map(|(semantic, unique_id)| {
478 let attribute = mesh.attribute_by_unique_id(*unique_id).ok_or_else(|| {
479 Error::Geometry(GeometryError::UnsupportedDraco(format!(
480 "decoded attribute {unique_id} is missing"
481 )))
482 })?;
483 PackedAttribute::new(
484 semantic.clone(),
485 mesh.num_points(),
486 attribute.num_components(),
487 component_type_for_data_type(attribute.data_type())?,
488 normalized
492 .get(semantic.as_str())
493 .copied()
494 .unwrap_or_else(|| attribute.normalized()),
495 packed_draco_attribute_bytes(mesh, *unique_id)?,
496 )
497 .map_err(Error::Geometry)
498 })
499 .collect::<Result<Vec<_>>>()?;
500 let count = mesh
501 .num_faces()
502 .checked_mul(3)
503 .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
504 let indices =
505 PackedIndices::new(count, ComponentType::U32, packed_draco_index_bytes(mesh)?)
506 .map_err(Error::Geometry)?;
507 Self::new(mode, attributes, Some(indices)).map_err(Error::Geometry)
508 }
509}
510
511fn validate_element_count(
512 mode: PrimitiveMode,
513 count: usize,
514) -> std::result::Result<(), GeometryError> {
515 let valid = match mode {
516 PrimitiveMode::Points => count >= 1,
517 PrimitiveMode::Lines => count >= 2 && count.is_multiple_of(2),
518 PrimitiveMode::LineLoop | PrimitiveMode::LineStrip => count >= 2,
519 PrimitiveMode::Triangles => count >= 3 && count.is_multiple_of(3),
520 PrimitiveMode::TriangleStrip | PrimitiveMode::TriangleFan => count >= 3,
521 };
522 if !valid {
523 return Err(GeometryError::InvalidElementCount { mode, count });
524 }
525 Ok(())
526}
527
528fn validate_attribute_components(
529 attribute: &PackedAttribute,
530) -> std::result::Result<(), GeometryError> {
531 let expected = if attribute.semantic == "POSITION" || attribute.semantic == "NORMAL" {
532 Some("3")
533 } else if attribute.semantic == "TANGENT"
534 || attribute.semantic.starts_with("JOINTS_")
535 || attribute.semantic.starts_with("WEIGHTS_")
536 {
537 Some("4")
538 } else if attribute.semantic.starts_with("TEXCOORD_") {
539 Some("2")
540 } else if attribute.semantic.starts_with("COLOR_") && !matches!(attribute.components, 3 | 4) {
541 Some("3 or 4")
542 } else {
543 None
544 };
545 if let Some(expected) = expected {
546 let valid = match expected {
547 "2" => attribute.components == 2,
548 "3" => attribute.components == 3,
549 "4" => attribute.components == 4,
550 "3 or 4" => matches!(attribute.components, 3 | 4),
551 _ => unreachable!("known component requirement"),
552 };
553 if !valid {
554 return Err(GeometryError::AttributeComponents {
555 semantic: attribute.semantic.clone(),
556 actual: attribute.components,
557 expected,
558 });
559 }
560 }
561 Ok(())
562}
563
564fn validate_attribute_profile(
565 attribute: &PackedAttribute,
566 profile: ValidationProfile,
567) -> std::result::Result<(), GeometryError> {
568 if profile != ValidationProfile::Gltf20 {
569 return Ok(());
570 }
571 let float = attribute.component_type == ComponentType::F32 && !attribute.normalized;
572 let normalized_unsigned = matches!(
573 attribute.component_type,
574 ComponentType::U8 | ComponentType::U16
575 ) && attribute.normalized;
576 let valid = if matches!(
577 attribute.semantic.as_str(),
578 "POSITION" | "NORMAL" | "TANGENT"
579 ) {
580 float
581 } else if attribute.semantic.starts_with("TEXCOORD_")
582 || attribute.semantic.starts_with("COLOR_")
583 || attribute.semantic.starts_with("WEIGHTS_")
584 {
585 float || normalized_unsigned
586 } else if attribute.semantic.starts_with("JOINTS_") {
587 matches!(
588 attribute.component_type,
589 ComponentType::U8 | ComponentType::U16
590 ) && !attribute.normalized
591 } else {
592 true
593 };
594 if !valid {
595 return Err(GeometryError::AttributeComponentType {
596 semantic: attribute.semantic.clone(),
597 component_type: attribute.component_type,
598 normalized: attribute.normalized,
599 profile,
600 });
601 }
602 Ok(())
603}
604
605fn validate_component_profile(
606 component_type: ComponentType,
607 profile: ValidationProfile,
608) -> std::result::Result<(), GeometryError> {
609 if profile == ValidationProfile::Gltf20
610 && !matches!(
611 component_type,
612 ComponentType::I8
613 | ComponentType::U8
614 | ComponentType::I16
615 | ComponentType::U16
616 | ComponentType::U32
617 | ComponentType::F32
618 )
619 {
620 return Err(GeometryError::ComponentTypeProfile {
621 component_type,
622 profile,
623 });
624 }
625 Ok(())
626}
627
628fn validate_byte_len(
629 kind: &'static str,
630 count: usize,
631 components: u8,
632 component_type: ComponentType,
633 actual: usize,
634) -> std::result::Result<(), GeometryError> {
635 let expected = count
636 .checked_mul(components as usize)
637 .and_then(|value| value.checked_mul(component_type.byte_width()))
638 .ok_or(GeometryError::ByteSizeOverflow)?;
639 if actual != expected {
640 return Err(GeometryError::ByteLength {
641 kind,
642 actual,
643 expected,
644 });
645 }
646 Ok(())
647}
648
649fn index_values(
650 indices: &PackedIndices,
651) -> impl Iterator<Item = std::result::Result<u64, GeometryError>> + '_ {
652 let width = indices.component_type.byte_width();
653 indices.bytes.chunks_exact(width).map(move |bytes| {
654 Ok(match indices.component_type {
655 ComponentType::U8 => bytes[0] as u64,
656 ComponentType::U16 => u16::from_le_bytes(bytes.try_into().unwrap()) as u64,
657 ComponentType::U32 => u32::from_le_bytes(bytes.try_into().unwrap()) as u64,
658 _ => return Err(GeometryError::InvalidIndexType(indices.component_type)),
659 })
660 })
661}
662
663#[cfg(feature = "draco-decode")]
664fn component_type_for_data_type(data_type: DataType) -> Result<ComponentType> {
665 match data_type {
666 DataType::Int8 => Ok(ComponentType::I8),
667 DataType::Uint8 => Ok(ComponentType::U8),
668 DataType::Int16 => Ok(ComponentType::I16),
669 DataType::Uint16 => Ok(ComponentType::U16),
670 DataType::Int32 => Ok(ComponentType::I32),
671 DataType::Uint32 => Ok(ComponentType::U32),
672 DataType::Float32 => Ok(ComponentType::F32),
673 DataType::Int64 => Ok(ComponentType::I64),
674 DataType::Uint64 => Ok(ComponentType::U64),
675 DataType::Float64 => Ok(ComponentType::F64),
676 other => Err(Error::Geometry(GeometryError::UnsupportedDraco(format!(
677 "component type {other:?}"
678 )))),
679 }
680}
681
682#[cfg(feature = "draco-decode")]
683fn packed_draco_attribute_bytes(mesh: &Mesh, unique_id: u32) -> Result<Vec<u8>> {
684 let attribute = mesh.attribute_by_unique_id(unique_id).ok_or_else(|| {
685 Error::Geometry(GeometryError::UnsupportedDraco(format!(
686 "decoded attribute {unique_id} is missing"
687 )))
688 })?;
689 let stride = usize::try_from(attribute.byte_stride()).map_err(|_| {
690 Error::Geometry(GeometryError::UnsupportedDraco(
691 "decoded attribute stride is invalid".into(),
692 ))
693 })?;
694 let byte_len = mesh
695 .num_points()
696 .checked_mul(stride)
697 .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
698 let mut out = vec![0; byte_len];
699 let mut row = vec![0; stride];
700 for point in 0..mesh.num_points() {
701 let index = attribute.mapped_index(draco_core::PointIndex(point as u32));
702 if !attribute
703 .buffer()
704 .try_read(index.0 as usize * stride, &mut row)
705 {
706 return Err(Error::Geometry(GeometryError::UnsupportedDraco(
707 "decoded attribute is out of bounds".into(),
708 )));
709 }
710 out[point * stride..(point + 1) * stride].copy_from_slice(&row);
711 }
712 Ok(out)
713}
714
715#[cfg(feature = "draco-decode")]
716fn packed_draco_index_bytes(mesh: &Mesh) -> Result<Vec<u8>> {
717 let byte_len = mesh
718 .num_faces()
719 .checked_mul(3)
720 .and_then(|value| value.checked_mul(4))
721 .ok_or(Error::Geometry(GeometryError::ByteSizeOverflow))?;
722 let mut out = Vec::with_capacity(byte_len);
723 for face in 0..mesh.num_faces() {
724 for index in mesh.face(draco_core::FaceIndex(face as u32)) {
725 out.extend_from_slice(&index.0.to_le_bytes());
726 }
727 }
728 Ok(out)
729}