1pub(crate) mod attribute;
2pub(crate) mod config_spec;
3pub(crate) mod connectivity;
4pub(crate) mod ds;
5pub(crate) mod entropy;
6pub(crate) mod header;
7pub(crate) mod metadata;
8pub mod point_cloud;
10
11use draco_oxide_core::bit_coder::ByteWriter;
12use draco_oxide_core::debug_write;
13use draco_oxide_core::mesh::Mesh;
14use draco_oxide_core::point_cloud::PointCloud;
15use draco_oxide_core::types::ConfigType;
16use thiserror::Error;
17
18pub use attribute::{AttributeConfig, NormalEncoding, Quantization};
20pub use connectivity::edgebreaker::Config as EdgebreakerConfig;
22pub use connectivity::sequential::Config as SequentialConfig;
24pub use connectivity::Config as ConnectivityConfig;
26pub use point_cloud::Config as PointCloudConfig;
28
29use config_spec::ConfigSpec;
30
31#[derive(Debug, Clone, serde::Deserialize)]
35#[serde(from = "ConfigSpec")]
36pub struct Config {
37 connectivity: connectivity::Config,
40 attribute: attribute::Config,
42 geometry_type: header::EncodedGeometryType,
43 metadata: bool,
44}
45
46impl ConfigType for Config {
47 fn default() -> Self {
48 Self {
49 connectivity: connectivity::Config::default(),
50 attribute: attribute::Config::default(),
51 geometry_type: header::EncodedGeometryType::TrianglarMesh,
52 metadata: false,
53 }
54 }
55}
56
57impl Config {
58 pub fn with_normals(mut self, enc: NormalEncoding) -> Self {
71 self.attribute.set_normal_encoding(enc);
72 self
73 }
74
75 pub fn with_attribute(
79 mut self,
80 ty: draco_oxide_core::attribute::AttributeType,
81 cfg: AttributeConfig,
82 ) -> Self {
83 self.attribute.set(ty, cfg);
84 self
85 }
86
87 pub fn with_connectivity(mut self, cfg: ConnectivityConfig) -> Self {
89 self.connectivity = cfg;
90 self
91 }
92
93 pub fn with_edgebreaker(mut self, cfg: EdgebreakerConfig) -> Self {
95 self.connectivity = ConnectivityConfig::Edgebreaker(cfg);
96 self
97 }
98
99 pub fn with_sequential(mut self, cfg: SequentialConfig) -> Self {
101 self.connectivity = ConnectivityConfig::Sequential(cfg);
102 self
103 }
104
105 pub fn with_metadata(mut self, metadata: bool) -> Self {
107 self.metadata = metadata;
108 self
109 }
110
111 pub fn attribute_config(
115 &self,
116 ty: draco_oxide_core::attribute::AttributeType,
117 ) -> AttributeConfig {
118 self.attribute.get(ty)
119 }
120
121 pub fn connectivity(&self) -> &ConnectivityConfig {
123 &self.connectivity
124 }
125
126 pub fn validate(&self) -> Result<(), ConfigError> {
132 use draco_oxide_core::attribute::AttributeType;
133 use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
134 use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
135
136 if let ConnectivityConfig::Edgebreaker(eb) = &self.connectivity {
138 if eb.traversal == EdgebreakerKind::Predictive {
139 return Err(ConfigError::UnsupportedTraversal);
140 }
141 }
142 let sequential = matches!(self.connectivity, ConnectivityConfig::Sequential(_));
143
144 for (&ty, over) in self.attribute.overrides() {
145 if sequential {
150 if let Some(scheme) = &over.prediction {
151 if !matches!(
152 scheme,
153 PredictionSchemeType::DeltaPrediction | PredictionSchemeType::NoPrediction
154 ) {
155 return Err(ConfigError::MeshPredictionUnderSequential(format!(
156 "{scheme:?}"
157 )));
158 }
159 }
160 if over.normal_encoding == Some(NormalEncoding::PredictedOnly) {
161 return Err(ConfigError::PredictedNormalsUnderSequential);
162 }
163 if over.traversal
164 == Some(draco_oxide_core::codec::connectivity::edgebreaker::TraversalType::PredictionDegree)
165 {
166 return Err(ConfigError::PredictionDegreeUnderSequential);
167 }
168 }
169
170 if over.normal_encoding.is_some() && ty != AttributeType::Normal {
171 return Err(ConfigError::NormalEncodingOnNonNormal(ty));
172 }
173
174 if let Some(scheme) = &over.prediction {
175 if !allowed_schemes(ty).iter().any(|s| s == scheme) {
176 return Err(ConfigError::PredictionSchemeForType {
177 ty,
178 scheme: format!("{scheme:?}"),
179 });
180 }
181 }
182
183 if let Some(transform) = over.transform {
184 if !allowed_transforms(ty).contains(&transform) {
185 return Err(ConfigError::TransformForType {
186 ty,
187 transform: format!("{transform:?}"),
188 });
189 }
190 if over.prediction == Some(PredictionSchemeType::NoPrediction)
193 && transform != attribute::PredictionTransformType::NoTransform
194 {
195 return Err(ConfigError::TransformWithNoPrediction);
196 }
197 }
198
199 if let Some(quant) = over.quantization {
200 if ty == AttributeType::Normal && !matches!(quant, Quantization::Bits(_)) {
203 return Err(ConfigError::NonBitsQuantizationForNormal);
204 }
205 if let Quantization::Bits(n) = quant {
206 if !(1..=30).contains(&n) {
207 return Err(ConfigError::QuantizationBitsOutOfRange(n));
208 }
209 }
210 }
211 }
212
213 Ok(())
214 }
215}
216
217fn allowed_schemes(
219 ty: draco_oxide_core::attribute::AttributeType,
220) -> Vec<draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType> {
221 use draco_oxide_core::attribute::AttributeType::*;
222 use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType as S;
223 match ty {
224 Position => vec![
225 S::MeshParallelogramPrediction,
226 S::MeshConstrainedMultiParallelogramPrediction,
227 S::DeltaPrediction,
228 S::NoPrediction,
229 ],
230 Normal => vec![S::MeshNormalPrediction],
231 TextureCoordinate => vec![
232 S::MeshParallelogramPrediction,
233 S::MeshConstrainedMultiParallelogramPrediction,
234 S::MeshPredictionForTextureCoordinates,
235 S::DeltaPrediction,
236 S::NoPrediction,
237 ],
238 _ => vec![
242 S::MeshConstrainedMultiParallelogramPrediction,
243 S::DeltaPrediction,
244 S::NoPrediction,
245 ],
246 }
247}
248
249fn allowed_transforms(
251 ty: draco_oxide_core::attribute::AttributeType,
252) -> Vec<attribute::PredictionTransformType> {
253 use attribute::PredictionTransformType as T;
254 use draco_oxide_core::attribute::AttributeType::*;
255 match ty {
256 Normal => vec![T::OctahedralOrthogonal],
258 _ => vec![T::Difference, T::WrappedDifference, T::NoTransform],
259 }
260}
261
262#[remain::sorted]
264#[derive(Error, Debug)]
265pub enum ConfigError {
266 #[error("prediction scheme {0} needs mesh connectivity, which sequential encoding omits")]
269 MeshPredictionUnderSequential(String),
270 #[error("normals accept only an explicit bit count (octahedral error is angular)")]
273 NonBitsQuantizationForNormal,
274 #[error("normal encoding was set on a non-normal attribute ({0:?})")]
276 NormalEncodingOnNonNormal(draco_oxide_core::attribute::AttributeType),
277 #[error("geometry-predicted normals need mesh connectivity, which sequential encoding omits")]
280 PredictedNormalsUnderSequential,
281 #[error(
284 "prediction-degree traversal needs mesh connectivity, which sequential encoding omits"
285 )]
286 PredictionDegreeUnderSequential,
287 #[error("prediction scheme {scheme} is not valid for attribute type {ty:?}")]
289 PredictionSchemeForType {
290 ty: draco_oxide_core::attribute::AttributeType,
291 scheme: String,
292 },
293 #[error("quantization bits {0} out of range (must be 1..=30)")]
295 QuantizationBitsOutOfRange(u8),
296 #[error("prediction transform {transform} is not valid for attribute type {ty:?}")]
298 TransformForType {
299 ty: draco_oxide_core::attribute::AttributeType,
300 transform: String,
301 },
302 #[error("NoPrediction carries no transform on the wire; only NoTransform can accompany it")]
305 TransformWithNoPrediction,
306 #[error("the selected edgebreaker traversal is not implemented")]
308 UnsupportedTraversal,
309}
310
311#[remain::sorted]
313#[derive(Error, Debug)]
314pub enum Err {
315 #[error("Attribute encoding error: {0}")]
317 AttributeError(#[from] attribute::Err),
318 #[error("Invalid encoder configuration: {0}")]
320 ConfigError(#[from] ConfigError),
321 #[error("Connectivity encoding error: {0}")]
323 ConnectivityError(#[from] connectivity::Err),
324 #[error("Header encoding error: {0}")]
326 HeaderError(#[from] header::Err),
327 #[error("Metadata encoding error: {0}")]
329 MetadataError(#[from] metadata::Err),
330 #[error("Point cloud encoding error: {0}")]
332 PointCloudError(#[from] point_cloud::Err),
333 #[error("the mesh has no faces; encode it as a point cloud instead")]
336 PointCloudInput,
337}
338
339#[derive(Default)]
342pub struct Encoder {}
343
344impl Encoder {
345 pub fn new() -> Self {
347 Self {}
348 }
349
350 pub fn encode_mesh<W>(&mut self, mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
352 where
353 W: ByteWriter,
354 {
355 encode_impl(mesh, writer, cfg)
356 }
357
358 pub fn encode_point_cloud<W>(
361 &mut self,
362 pc: PointCloud,
363 writer: &mut W,
364 cfg: PointCloudConfig,
365 ) -> Result<(), Err>
366 where
367 W: ByteWriter,
368 {
369 point_cloud::encode_impl(pc, writer, cfg)?;
370 Ok(())
371 }
372}
373
374pub fn encode_mesh<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
377where
378 W: ByteWriter,
379{
380 Encoder::new().encode_mesh(mesh, writer, cfg)
381}
382
383pub fn encode_point_cloud<W>(
386 pc: PointCloud,
387 writer: &mut W,
388 cfg: PointCloudConfig,
389) -> Result<(), Err>
390where
391 W: ByteWriter,
392{
393 Encoder::new().encode_point_cloud(pc, writer, cfg)
394}
395
396fn encode_impl<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
397where
398 W: ByteWriter,
399{
400 cfg.validate()?;
402
403 if mesh.faces.is_empty() {
406 return Err(Err::PointCloudInput);
407 }
408
409 header::encode_header(writer, &cfg)?;
411
412 debug_write!("Header done, now starting metadata.", writer);
413
414 if cfg.metadata {
416 metadata::encode_metadata(&mesh, writer)?;
417 }
418
419 debug_write!("Metadata done, now starting connectivity.", writer);
420
421 let Mesh {
423 mut attributes,
424 faces,
425 ..
426 } = mesh;
427
428 if !attributes
429 .iter()
430 .any(|att| att.get_attribute_type() == draco_oxide_core::attribute::AttributeType::Position)
431 {
432 return Err(Err::ConnectivityError(
433 connectivity::Err::PositionAttributeTypeError,
434 ));
435 }
436
437 let (ds, pos_corner_table) = ds::build_global_ds(faces, &mut attributes);
438 let mut adss = ds::build_attribute_ds(&ds, &pos_corner_table, attributes);
439
440 let corners_of_edgebreaker = connectivity::encode_connectivity(&mut adss, writer, &cfg)?;
442 debug_write!("Connectivity done, now starting attributes.", writer);
443
444 attribute::encode_attributes(adss, corners_of_edgebreaker, writer, &cfg)?;
446
447 debug_write!("All done", writer);
448
449 Ok(())
450}
451
452#[cfg(test)]
453mod config_tests {
454 use super::*;
455 use draco_oxide_core::attribute::AttributeType;
456 use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
457 use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
458
459 #[test]
460 fn default_config_is_valid() {
461 assert!(<Config as ConfigType>::default().validate().is_ok());
462 }
463
464 #[test]
465 fn faceless_mesh_is_rejected_as_point_cloud() {
466 use draco_oxide_core::attribute::{Attribute, AttributeDomain, AttributeType};
467 use draco_oxide_core::types::NdVector;
468 let mut mesh = Mesh::new();
469 mesh.attributes = vec![Attribute::new::<NdVector<3, f32>, 3>(
470 vec![[0.0, 0.0, 0.0].into(), [1.0, 0.0, 0.0].into()],
471 AttributeType::Position,
472 AttributeDomain::Position,
473 Vec::new(),
474 )];
475 let mut out = Vec::new();
476 assert!(matches!(
477 encode_mesh(mesh, &mut out, <Config as ConfigType>::default()),
478 Err(Err::PointCloudInput)
479 ));
480 assert!(out.is_empty(), "nothing must be written before the check");
481 }
482
483 #[test]
484 fn position_quantization_override_validates() {
485 let cfg = Config::default().with_attribute(
486 AttributeType::Position,
487 AttributeConfig {
488 quantization: Some(Quantization::Bits(14)),
489 ..Default::default()
490 },
491 );
492 assert!(cfg.validate().is_ok());
493 }
494
495 #[test]
496 fn texture_predictor_on_normal_is_rejected() {
497 let cfg = Config::default().with_attribute(
498 AttributeType::Normal,
499 AttributeConfig {
500 prediction: Some(PredictionSchemeType::MeshPredictionForTextureCoordinates),
501 ..Default::default()
502 },
503 );
504 assert!(matches!(
505 cfg.validate(),
506 Err(ConfigError::PredictionSchemeForType { .. })
507 ));
508 }
509
510 #[test]
511 fn max_error_quantization_on_normal_is_rejected() {
512 let cfg = Config::default().with_attribute(
513 AttributeType::Normal,
514 AttributeConfig {
515 quantization: Some(Quantization::MaxError(0.01)),
516 ..Default::default()
517 },
518 );
519 assert!(matches!(
520 cfg.validate(),
521 Err(ConfigError::NonBitsQuantizationForNormal)
522 ));
523 }
524
525 #[test]
526 fn normal_encoding_on_position_is_rejected() {
527 let cfg = Config::default().with_attribute(
528 AttributeType::Position,
529 AttributeConfig {
530 normal_encoding: Some(NormalEncoding::PredictedOnly),
531 ..Default::default()
532 },
533 );
534 assert!(matches!(
535 cfg.validate(),
536 Err(ConfigError::NormalEncodingOnNonNormal(
537 AttributeType::Position
538 ))
539 ));
540 }
541
542 #[test]
543 fn out_of_range_bits_is_rejected() {
544 let cfg = Config::default().with_attribute(
545 AttributeType::Position,
546 AttributeConfig {
547 quantization: Some(Quantization::Bits(40)),
548 ..Default::default()
549 },
550 );
551 assert!(matches!(
552 cfg.validate(),
553 Err(ConfigError::QuantizationBitsOutOfRange(40))
554 ));
555 }
556
557 #[test]
558 fn predictive_edgebreaker_is_rejected() {
559 let cfg = Config::default().with_edgebreaker(EdgebreakerConfig {
560 traversal: EdgebreakerKind::Predictive,
561 });
562 assert!(matches!(
563 cfg.validate(),
564 Err(ConfigError::UnsupportedTraversal)
565 ));
566 }
567
568 #[test]
569 fn sequential_selects_sequential_encoder_method() {
570 use draco_oxide_core::codec::header::EncoderMethod;
571 let cfg = Config::default().with_sequential(SequentialConfig::default());
572 assert_eq!(cfg.connectivity.encoder_method(), EncoderMethod::Sequential);
573 }
574}