use std::fs;
use std::path::Path;
use spatial_codec_draco::{
decode_draco, encode_draco_with_config, EncodeConfig, PointCloudEncodingMethod,
};
use threecrate_core::{Error, Point3f, PointCloud, Result};
use crate::registry;
#[derive(Debug, Clone)]
pub struct DracoConfig {
pub quantization_bits: u32,
pub compression_level: u8,
pub encode_colors: bool,
}
impl Default for DracoConfig {
fn default() -> Self {
Self {
quantization_bits: 14,
compression_level: 7,
encode_colors: true,
}
}
}
pub fn draco_encode(cloud: &PointCloud<Point3f>, config: DracoConfig) -> Result<Vec<u8>> {
if cloud.is_empty() {
return Err(Error::InvalidData(
"cannot encode an empty point cloud".into(),
));
}
let coords: Vec<[f32; 3]> = cloud.points.iter().map(|p| [p.x, p.y, p.z]).collect();
let colors: Vec<[u8; 3]> = vec![[0u8; 3]; coords.len()];
let encoding_speed = 10u8.saturating_sub(config.compression_level);
let encode_config = EncodeConfig {
position_quantization_bits: config.quantization_bits,
color_quantization_bits: 8,
encoding_speed,
decoding_speed: 5,
};
encode_draco_with_config(&coords, &colors, PointCloudEncodingMethod::Sequential, &encode_config)
.map_err(|e| Error::InvalidData(format!("Draco encode failed: {e}")))
}
pub fn draco_decode(compressed: &[u8]) -> Result<PointCloud<Point3f>> {
if compressed.is_empty() {
return Err(Error::InvalidData(
"cannot decode an empty buffer".into(),
));
}
let (coords, _colors) = decode_draco(compressed)
.map_err(|e| Error::InvalidData(format!("Draco decode failed: {e}")))?;
if coords.len() % 3 != 0 || coords.is_empty() {
return Err(Error::InvalidData(
format!("Draco decode returned {} floats, expected a multiple of 3", coords.len()),
));
}
let points = coords
.chunks(3)
.map(|c| Point3f::new(c[0], c[1], c[2]))
.collect();
Ok(PointCloud::from_points(points))
}
pub struct DracoReader;
pub struct DracoWriter;
impl registry::PointCloudReader for DracoReader {
fn read_point_cloud(&self, path: &Path) -> Result<PointCloud<Point3f>> {
let bytes = fs::read(path).map_err(Error::Io)?;
draco_decode(&bytes)
}
fn can_read(&self, path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()) == Some("drc")
}
fn format_name(&self) -> &'static str {
"draco"
}
}
impl registry::PointCloudWriter for DracoWriter {
fn write_point_cloud(&self, cloud: &PointCloud<Point3f>, path: &Path) -> Result<()> {
let bytes = draco_encode(cloud, DracoConfig::default())?;
fs::write(path, bytes).map_err(Error::Io)
}
fn format_name(&self) -> &'static str {
"draco"
}
}
pub struct DracoCompressorPipeline {
config: DracoConfig,
buffer: Vec<Point3f>,
}
impl DracoCompressorPipeline {
pub fn new(config: DracoConfig) -> Self {
Self {
config,
buffer: Vec::new(),
}
}
pub fn process_chunk(&mut self, chunk: &[Point3f]) -> Result<()> {
self.buffer.extend_from_slice(chunk);
Ok(())
}
pub fn finalize(self) -> Result<Vec<u8>> {
let cloud = PointCloud::from_points(self.buffer);
draco_encode(&cloud, self.config)
}
pub fn memory_bytes(&self) -> usize {
self.buffer.len() * std::mem::size_of::<Point3f>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use threecrate_core::Point3f;
fn sample_cloud(n: usize) -> PointCloud<Point3f> {
let points = (0..n)
.map(|i| Point3f::new(i as f32 * 0.1, i as f32 * 0.2, i as f32 * 0.3))
.collect();
PointCloud::from_points(points)
}
#[test]
fn roundtrip_basic() {
let cloud = sample_cloud(100);
let config = DracoConfig::default();
let bytes = draco_encode(&cloud, config).expect("encode failed");
assert!(!bytes.is_empty());
let decoded = draco_decode(&bytes).expect("decode failed");
assert_eq!(decoded.len(), cloud.len());
for (orig, dec) in cloud.points.iter().zip(decoded.points.iter()) {
assert!((orig.x - dec.x).abs() < 0.01, "x drift too large");
assert!((orig.y - dec.y).abs() < 0.01, "y drift too large");
assert!((orig.z - dec.z).abs() < 0.01, "z drift too large");
}
}
#[test]
fn encode_empty_cloud_returns_error() {
let cloud: PointCloud<Point3f> = PointCloud::new();
assert!(draco_encode(&cloud, DracoConfig::default()).is_err());
}
#[test]
fn decode_empty_buffer_returns_error() {
assert!(draco_decode(&[]).is_err());
}
#[test]
fn streaming_compressor_roundtrip() {
let cloud = sample_cloud(50);
let mut compressor = DracoCompressorPipeline::new(DracoConfig::default());
compressor.process_chunk(&cloud.points).expect("process_chunk failed");
let bytes = compressor.finalize().expect("finalize failed");
let decoded = draco_decode(&bytes).expect("decode failed");
assert_eq!(decoded.len(), cloud.len());
}
}