use alice_sdf::mesh::{detect_primitive, primitives_to_csg, FittedPrimitive, FittingConfig};
use alice_sdf::prelude::Vec3;
use alice_sdf::svo::{SvoBuildConfig, SvoNode};
#[derive(Debug, Clone)]
pub enum CompressedSdf {
Primitives {
primitives: Vec<SerializedPrimitive>,
asdf_data: Vec<u8>,
},
SvoChunks {
chunks: Vec<SvoChunkData>,
total_nodes: u32,
},
Hybrid {
primitives: Vec<SerializedPrimitive>,
svo_chunks: Vec<SvoChunkData>,
asdf_data: Vec<u8>,
},
}
#[derive(Debug, Clone)]
pub struct SerializedPrimitive {
pub kind: PrimitiveKind,
pub params: [f32; 8], pub mse: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PrimitiveKind {
Sphere = 0, Box = 1, Cylinder = 2, Plane = 3, }
#[derive(Debug, Clone)]
pub struct SvoChunkData {
pub chunk_id: u32,
pub data: Vec<u8>,
pub node_count: u32,
pub bounds_min: [f32; 3],
pub bounds_max: [f32; 3],
}
#[derive(Debug, Clone)]
pub struct CompressConfig {
pub fitting: FittingConfig,
pub svo_depth: u32,
pub primitive_mse_threshold: f32,
pub min_inlier_ratio: f32,
pub svo_distance_threshold: f32,
}
impl Default for CompressConfig {
fn default() -> Self {
Self {
fitting: FittingConfig::default(),
svo_depth: 6, primitive_mse_threshold: 0.01,
min_inlier_ratio: 0.8,
svo_distance_threshold: 1.5,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CompressStats {
pub input_points: usize,
pub primitives_detected: usize,
pub primitive_inliers: usize,
pub residual_points: usize,
pub output_bytes: usize,
pub compression_ratio: f32,
}
#[must_use]
pub fn compress_point_cloud(
points: &[[f32; 3]],
config: &CompressConfig,
) -> (CompressedSdf, CompressStats) {
let mut stats = CompressStats {
input_points: points.len(),
..Default::default()
};
if points.is_empty() {
return (
CompressedSdf::Primitives {
primitives: Vec::new(),
asdf_data: Vec::new(),
},
stats,
);
}
let vec3_points: Vec<Vec3> = points.iter().map(|p| Vec3::new(p[0], p[1], p[2])).collect();
let fitting_result = detect_primitive(&vec3_points, &config.fitting);
if let Some(ref result) = fitting_result {
if result.mse <= config.primitive_mse_threshold {
let primitive = serialize_fitted_primitive(&result.primitive, result.mse);
let primitives = vec![primitive];
let fitted_prims: Vec<FittedPrimitive> = vec![result.primitive.clone()];
let asdf_data = if let Some(csg_node) = primitives_to_csg(&fitted_prims) {
let tree = alice_sdf::types::SdfTree {
version: String::from("1.0"),
root: csg_node,
metadata: None,
};
let tmp_path = std::env::temp_dir().join("alice_edge_asdf.tmp");
if alice_sdf::io::save_asdf(&tree, &tmp_path).is_ok() {
std::fs::read(&tmp_path).unwrap_or_default()
} else {
Vec::new()
}
} else {
Vec::new()
};
stats.primitives_detected = 1;
stats.primitive_inliers = result.inlier_count;
stats.output_bytes = asdf_data.len() + std::mem::size_of::<SerializedPrimitive>();
stats.compression_ratio = if stats.output_bytes > 0 {
(points.len() as u64).saturating_mul(12) as f32 / stats.output_bytes as f32
} else {
0.0
};
return (
CompressedSdf::Primitives {
primitives,
asdf_data,
},
stats,
);
}
}
let svo_config = SvoBuildConfig {
max_depth: config.svo_depth,
distance_threshold: config.svo_distance_threshold,
..Default::default()
};
let (bounds_min, bounds_max) = compute_bounds(points);
let svo_data = build_svo_from_bounds(&svo_config, bounds_min, bounds_max);
let chunk = SvoChunkData {
chunk_id: 0,
data: svo_data.clone(),
node_count: (svo_data.len() / std::mem::size_of::<SvoNode>()) as u32,
bounds_min,
bounds_max,
};
stats.residual_points = points.len();
stats.output_bytes = svo_data.len();
stats.compression_ratio = if stats.output_bytes > 0 {
(points.len() as u64).saturating_mul(12) as f32 / stats.output_bytes as f32
} else {
0.0
};
(
CompressedSdf::SvoChunks {
chunks: vec![chunk],
total_nodes: (stats.output_bytes as u32) >> 5, },
stats,
)
}
fn serialize_fitted_primitive(prim: &FittedPrimitive, mse: f32) -> SerializedPrimitive {
let mut params = [0.0f32; 8];
let kind = match prim {
FittedPrimitive::Sphere { center, radius } => {
params[0] = center.x;
params[1] = center.y;
params[2] = center.z;
params[3] = *radius;
PrimitiveKind::Sphere
}
FittedPrimitive::Box {
center,
half_extents,
} => {
params[0] = center.x;
params[1] = center.y;
params[2] = center.z;
params[3] = half_extents.x;
params[4] = half_extents.y;
params[5] = half_extents.z;
PrimitiveKind::Box
}
FittedPrimitive::Cylinder {
center,
axis,
radius,
half_height,
} => {
params[0] = center.x;
params[1] = center.y;
params[2] = center.z;
params[3] = axis.x;
params[4] = axis.y;
params[5] = axis.z;
params[6] = *radius;
params[7] = *half_height;
PrimitiveKind::Cylinder
}
FittedPrimitive::Plane { normal, distance } => {
params[0] = normal.x;
params[1] = normal.y;
params[2] = normal.z;
params[3] = *distance;
PrimitiveKind::Plane
}
FittedPrimitive::Capsule { .. } => PrimitiveKind::Box,
};
SerializedPrimitive { kind, params, mse }
}
#[inline(always)]
fn compute_bounds(points: &[[f32; 3]]) -> ([f32; 3], [f32; 3]) {
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for p in points {
min[0] = min[0].min(p[0]);
min[1] = min[1].min(p[1]);
min[2] = min[2].min(p[2]);
max[0] = max[0].max(p[0]);
max[1] = max[1].max(p[1]);
max[2] = max[2].max(p[2]);
}
(min, max)
}
fn build_svo_from_bounds(config: &SvoBuildConfig, min: [f32; 3], max: [f32; 3]) -> Vec<u8> {
let mut data = Vec::with_capacity(256);
data.extend_from_slice(b"SVO\0");
data.extend_from_slice(&config.max_depth.to_le_bytes());
for v in &min {
data.extend_from_slice(&v.to_le_bytes());
}
for v in &max {
data.extend_from_slice(&v.to_le_bytes());
}
build_svo_node_recursive(&mut data, min, max, 0, config.max_depth);
data
}
fn build_svo_node_recursive(
data: &mut Vec<u8>,
min: [f32; 3],
max: [f32; 3],
depth: u32,
max_depth: u32,
) {
if depth >= max_depth {
data.extend_from_slice(&[0u8; 4]);
return;
}
data.extend_from_slice(&[0xFF, 0, 0, 0]);
let mid = [
(min[0] + max[0]) * 0.5,
(min[1] + max[1]) * 0.5,
(min[2] + max[2]) * 0.5,
];
for octant in 0..8u8 {
let child_min = [
if octant & 1 != 0 { mid[0] } else { min[0] },
if octant & 2 != 0 { mid[1] } else { min[1] },
if octant & 4 != 0 { mid[2] } else { min[2] },
];
let child_max = [
if octant & 1 != 0 { max[0] } else { mid[0] },
if octant & 2 != 0 { max[1] } else { mid[1] },
if octant & 4 != 0 { max[2] } else { mid[2] },
];
build_svo_node_recursive(data, child_min, child_max, depth + 1, max_depth);
}
}
#[inline(always)]
#[must_use]
pub fn svo_diff_hash(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let chunks = data.chunks_exact(8);
let remainder = chunks.remainder();
for chunk in chunks {
let word = u64::from_le_bytes(chunk.try_into().unwrap());
hash ^= word;
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
for &byte in remainder {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_bounds() {
let points = vec![[0.0, 1.0, 2.0], [-1.0, 0.0, 0.0], [3.0, 2.0, 1.0]];
let (min, max) = compute_bounds(&points);
assert_eq!(min, [-1.0, 0.0, 0.0]);
assert_eq!(max, [3.0, 2.0, 2.0]);
}
#[test]
fn test_svo_diff_hash() {
let data1 = b"test data 1";
let data2 = b"test data 2";
let hash1 = svo_diff_hash(data1);
let hash2 = svo_diff_hash(data2);
assert_ne!(hash1, hash2);
assert_eq!(hash1, svo_diff_hash(data1)); }
#[test]
fn test_build_svo_from_bounds() {
let config = SvoBuildConfig::default();
let data = build_svo_from_bounds(&config, [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]);
assert!(data.starts_with(b"SVO\0"));
assert!(data.len() >= 32);
}
#[test]
fn test_svo_node_count_depth1() {
let mut config = SvoBuildConfig::default();
config.max_depth = 1;
let data = build_svo_from_bounds(&config, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let node_bytes = data.len() - 32; assert_eq!(node_bytes, 9 * 4); }
#[test]
fn test_serialize_primitive_sphere() {
let prim = FittedPrimitive::Sphere {
center: Vec3::new(1.0, 2.0, 3.0),
radius: 0.5,
};
let s = serialize_fitted_primitive(&prim, 0.001);
assert_eq!(s.kind, PrimitiveKind::Sphere);
assert!((s.params[0] - 1.0).abs() < 1e-6);
assert!((s.params[3] - 0.5).abs() < 1e-6);
}
#[test]
fn test_compress_empty_point_cloud() {
let config = CompressConfig::default();
let (result, stats) = compress_point_cloud(&[], &config);
assert!(matches!(result, CompressedSdf::Primitives { .. }));
assert_eq!(stats.input_points, 0);
}
#[test]
fn test_svo_diff_hash_empty() {
let hash = svo_diff_hash(&[]);
assert_eq!(hash, svo_diff_hash(&[]));
}
#[test]
fn test_svo_node_count_depth0() {
let mut config = SvoBuildConfig::default();
config.max_depth = 0;
let data = build_svo_from_bounds(&config, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let node_bytes = data.len() - 32;
assert_eq!(node_bytes, 4); }
}