pub(crate) mod attribute;
pub(crate) mod config_spec;
pub(crate) mod connectivity;
pub(crate) mod ds;
pub(crate) mod entropy;
pub(crate) mod header;
pub(crate) mod metadata;
pub mod point_cloud;
use draco_oxide_core::bit_coder::ByteWriter;
use draco_oxide_core::debug_write;
use draco_oxide_core::mesh::Mesh;
use draco_oxide_core::point_cloud::PointCloud;
use draco_oxide_core::types::ConfigType;
use thiserror::Error;
pub use attribute::{AttributeConfig, NormalEncoding, Quantization};
pub use connectivity::edgebreaker::Config as EdgebreakerConfig;
pub use connectivity::sequential::Config as SequentialConfig;
pub use connectivity::Config as ConnectivityConfig;
pub use point_cloud::Config as PointCloudConfig;
use config_spec::ConfigSpec;
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(from = "ConfigSpec")]
pub struct Config {
connectivity: connectivity::Config,
attribute: attribute::Config,
geometry_type: header::EncodedGeometryType,
metadata: bool,
}
impl ConfigType for Config {
fn default() -> Self {
Self {
connectivity: connectivity::Config::default(),
attribute: attribute::Config::default(),
geometry_type: header::EncodedGeometryType::TrianglarMesh,
metadata: false,
}
}
}
impl Config {
pub fn with_normals(mut self, enc: NormalEncoding) -> Self {
self.attribute.set_normal_encoding(enc);
self
}
pub fn with_attribute(
mut self,
ty: draco_oxide_core::attribute::AttributeType,
cfg: AttributeConfig,
) -> Self {
self.attribute.set(ty, cfg);
self
}
pub fn with_connectivity(mut self, cfg: ConnectivityConfig) -> Self {
self.connectivity = cfg;
self
}
pub fn with_edgebreaker(mut self, cfg: EdgebreakerConfig) -> Self {
self.connectivity = ConnectivityConfig::Edgebreaker(cfg);
self
}
pub fn with_sequential(mut self, cfg: SequentialConfig) -> Self {
self.connectivity = ConnectivityConfig::Sequential(cfg);
self
}
pub fn with_metadata(mut self, metadata: bool) -> Self {
self.metadata = metadata;
self
}
pub fn attribute_config(
&self,
ty: draco_oxide_core::attribute::AttributeType,
) -> AttributeConfig {
self.attribute.get(ty)
}
pub fn connectivity(&self) -> &ConnectivityConfig {
&self.connectivity
}
pub fn validate(&self) -> Result<(), ConfigError> {
use draco_oxide_core::attribute::AttributeType;
use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
if let ConnectivityConfig::Edgebreaker(eb) = &self.connectivity {
if eb.traversal == EdgebreakerKind::Predictive {
return Err(ConfigError::UnsupportedTraversal);
}
}
let sequential = matches!(self.connectivity, ConnectivityConfig::Sequential(_));
for (&ty, over) in self.attribute.overrides() {
if sequential {
if let Some(scheme) = &over.prediction {
if !matches!(
scheme,
PredictionSchemeType::DeltaPrediction | PredictionSchemeType::NoPrediction
) {
return Err(ConfigError::MeshPredictionUnderSequential(format!(
"{scheme:?}"
)));
}
}
if over.normal_encoding == Some(NormalEncoding::PredictedOnly) {
return Err(ConfigError::PredictedNormalsUnderSequential);
}
if over.traversal
== Some(draco_oxide_core::codec::connectivity::edgebreaker::TraversalType::PredictionDegree)
{
return Err(ConfigError::PredictionDegreeUnderSequential);
}
}
if over.normal_encoding.is_some() && ty != AttributeType::Normal {
return Err(ConfigError::NormalEncodingOnNonNormal(ty));
}
if let Some(scheme) = &over.prediction {
if !allowed_schemes(ty).iter().any(|s| s == scheme) {
return Err(ConfigError::PredictionSchemeForType {
ty,
scheme: format!("{scheme:?}"),
});
}
}
if let Some(transform) = over.transform {
if !allowed_transforms(ty).contains(&transform) {
return Err(ConfigError::TransformForType {
ty,
transform: format!("{transform:?}"),
});
}
if over.prediction == Some(PredictionSchemeType::NoPrediction)
&& transform != attribute::PredictionTransformType::NoTransform
{
return Err(ConfigError::TransformWithNoPrediction);
}
}
if let Some(quant) = over.quantization {
if ty == AttributeType::Normal && !matches!(quant, Quantization::Bits(_)) {
return Err(ConfigError::NonBitsQuantizationForNormal);
}
if let Quantization::Bits(n) = quant {
if !(1..=30).contains(&n) {
return Err(ConfigError::QuantizationBitsOutOfRange(n));
}
}
}
}
Ok(())
}
}
fn allowed_schemes(
ty: draco_oxide_core::attribute::AttributeType,
) -> Vec<draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType> {
use draco_oxide_core::attribute::AttributeType::*;
use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType as S;
match ty {
Position => vec![
S::MeshParallelogramPrediction,
S::MeshConstrainedMultiParallelogramPrediction,
S::DeltaPrediction,
S::NoPrediction,
],
Normal => vec![S::MeshNormalPrediction],
TextureCoordinate => vec![
S::MeshParallelogramPrediction,
S::MeshConstrainedMultiParallelogramPrediction,
S::MeshPredictionForTextureCoordinates,
S::DeltaPrediction,
S::NoPrediction,
],
_ => vec![
S::MeshConstrainedMultiParallelogramPrediction,
S::DeltaPrediction,
S::NoPrediction,
],
}
}
fn allowed_transforms(
ty: draco_oxide_core::attribute::AttributeType,
) -> Vec<attribute::PredictionTransformType> {
use attribute::PredictionTransformType as T;
use draco_oxide_core::attribute::AttributeType::*;
match ty {
Normal => vec![T::OctahedralOrthogonal],
_ => vec![T::Difference, T::WrappedDifference, T::NoTransform],
}
}
#[remain::sorted]
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("prediction scheme {0} needs mesh connectivity, which sequential encoding omits")]
MeshPredictionUnderSequential(String),
#[error("normals accept only an explicit bit count (octahedral error is angular)")]
NonBitsQuantizationForNormal,
#[error("normal encoding was set on a non-normal attribute ({0:?})")]
NormalEncodingOnNonNormal(draco_oxide_core::attribute::AttributeType),
#[error("geometry-predicted normals need mesh connectivity, which sequential encoding omits")]
PredictedNormalsUnderSequential,
#[error(
"prediction-degree traversal needs mesh connectivity, which sequential encoding omits"
)]
PredictionDegreeUnderSequential,
#[error("prediction scheme {scheme} is not valid for attribute type {ty:?}")]
PredictionSchemeForType {
ty: draco_oxide_core::attribute::AttributeType,
scheme: String,
},
#[error("quantization bits {0} out of range (must be 1..=30)")]
QuantizationBitsOutOfRange(u8),
#[error("prediction transform {transform} is not valid for attribute type {ty:?}")]
TransformForType {
ty: draco_oxide_core::attribute::AttributeType,
transform: String,
},
#[error("NoPrediction carries no transform on the wire; only NoTransform can accompany it")]
TransformWithNoPrediction,
#[error("the selected edgebreaker traversal is not implemented")]
UnsupportedTraversal,
}
#[remain::sorted]
#[derive(Error, Debug)]
pub enum Err {
#[error("Attribute encoding error: {0}")]
AttributeError(#[from] attribute::Err),
#[error("Invalid encoder configuration: {0}")]
ConfigError(#[from] ConfigError),
#[error("Connectivity encoding error: {0}")]
ConnectivityError(#[from] connectivity::Err),
#[error("Header encoding error: {0}")]
HeaderError(#[from] header::Err),
#[error("Metadata encoding error: {0}")]
MetadataError(#[from] metadata::Err),
#[error("Point cloud encoding error: {0}")]
PointCloudError(#[from] point_cloud::Err),
#[error("the mesh has no faces; encode it as a point cloud instead")]
PointCloudInput,
}
#[derive(Default)]
pub struct Encoder {}
impl Encoder {
pub fn new() -> Self {
Self {}
}
pub fn encode_mesh<W>(&mut self, mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
where
W: ByteWriter,
{
encode_impl(mesh, writer, cfg)
}
pub fn encode_point_cloud<W>(
&mut self,
pc: PointCloud,
writer: &mut W,
cfg: PointCloudConfig,
) -> Result<(), Err>
where
W: ByteWriter,
{
point_cloud::encode_impl(pc, writer, cfg)?;
Ok(())
}
}
pub fn encode_mesh<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
where
W: ByteWriter,
{
Encoder::new().encode_mesh(mesh, writer, cfg)
}
pub fn encode_point_cloud<W>(
pc: PointCloud,
writer: &mut W,
cfg: PointCloudConfig,
) -> Result<(), Err>
where
W: ByteWriter,
{
Encoder::new().encode_point_cloud(pc, writer, cfg)
}
fn encode_impl<W>(mesh: Mesh, writer: &mut W, cfg: Config) -> Result<(), Err>
where
W: ByteWriter,
{
cfg.validate()?;
if mesh.faces.is_empty() {
return Err(Err::PointCloudInput);
}
header::encode_header(writer, &cfg)?;
debug_write!("Header done, now starting metadata.", writer);
if cfg.metadata {
metadata::encode_metadata(&mesh, writer)?;
}
debug_write!("Metadata done, now starting connectivity.", writer);
let Mesh {
mut attributes,
faces,
..
} = mesh;
if !attributes
.iter()
.any(|att| att.get_attribute_type() == draco_oxide_core::attribute::AttributeType::Position)
{
return Err(Err::ConnectivityError(
connectivity::Err::PositionAttributeTypeError,
));
}
let (ds, pos_corner_table) = ds::build_global_ds(faces, &mut attributes);
let mut adss = ds::build_attribute_ds(&ds, &pos_corner_table, attributes);
let corners_of_edgebreaker = connectivity::encode_connectivity(&mut adss, writer, &cfg)?;
debug_write!("Connectivity done, now starting attributes.", writer);
attribute::encode_attributes(adss, corners_of_edgebreaker, writer, &cfg)?;
debug_write!("All done", writer);
Ok(())
}
#[cfg(test)]
mod config_tests {
use super::*;
use draco_oxide_core::attribute::AttributeType;
use draco_oxide_core::codec::attribute::prediction_scheme::PredictionSchemeType;
use draco_oxide_core::codec::connectivity::edgebreaker::EdgebreakerKind;
#[test]
fn default_config_is_valid() {
assert!(<Config as ConfigType>::default().validate().is_ok());
}
#[test]
fn faceless_mesh_is_rejected_as_point_cloud() {
use draco_oxide_core::attribute::{Attribute, AttributeDomain, AttributeType};
use draco_oxide_core::types::NdVector;
let mut mesh = Mesh::new();
mesh.attributes = vec![Attribute::new::<NdVector<3, f32>, 3>(
vec![[0.0, 0.0, 0.0].into(), [1.0, 0.0, 0.0].into()],
AttributeType::Position,
AttributeDomain::Position,
Vec::new(),
)];
let mut out = Vec::new();
assert!(matches!(
encode_mesh(mesh, &mut out, <Config as ConfigType>::default()),
Err(Err::PointCloudInput)
));
assert!(out.is_empty(), "nothing must be written before the check");
}
#[test]
fn position_quantization_override_validates() {
let cfg = Config::default().with_attribute(
AttributeType::Position,
AttributeConfig {
quantization: Some(Quantization::Bits(14)),
..Default::default()
},
);
assert!(cfg.validate().is_ok());
}
#[test]
fn texture_predictor_on_normal_is_rejected() {
let cfg = Config::default().with_attribute(
AttributeType::Normal,
AttributeConfig {
prediction: Some(PredictionSchemeType::MeshPredictionForTextureCoordinates),
..Default::default()
},
);
assert!(matches!(
cfg.validate(),
Err(ConfigError::PredictionSchemeForType { .. })
));
}
#[test]
fn max_error_quantization_on_normal_is_rejected() {
let cfg = Config::default().with_attribute(
AttributeType::Normal,
AttributeConfig {
quantization: Some(Quantization::MaxError(0.01)),
..Default::default()
},
);
assert!(matches!(
cfg.validate(),
Err(ConfigError::NonBitsQuantizationForNormal)
));
}
#[test]
fn normal_encoding_on_position_is_rejected() {
let cfg = Config::default().with_attribute(
AttributeType::Position,
AttributeConfig {
normal_encoding: Some(NormalEncoding::PredictedOnly),
..Default::default()
},
);
assert!(matches!(
cfg.validate(),
Err(ConfigError::NormalEncodingOnNonNormal(
AttributeType::Position
))
));
}
#[test]
fn out_of_range_bits_is_rejected() {
let cfg = Config::default().with_attribute(
AttributeType::Position,
AttributeConfig {
quantization: Some(Quantization::Bits(40)),
..Default::default()
},
);
assert!(matches!(
cfg.validate(),
Err(ConfigError::QuantizationBitsOutOfRange(40))
));
}
#[test]
fn predictive_edgebreaker_is_rejected() {
let cfg = Config::default().with_edgebreaker(EdgebreakerConfig {
traversal: EdgebreakerKind::Predictive,
});
assert!(matches!(
cfg.validate(),
Err(ConfigError::UnsupportedTraversal)
));
}
#[test]
fn sequential_selects_sequential_encoder_method() {
use draco_oxide_core::codec::header::EncoderMethod;
let cfg = Config::default().with_sequential(SequentialConfig::default());
assert_eq!(cfg.connectivity.encoder_method(), EncoderMethod::Sequential);
}
}