#[cfg(feature = "use_cpp")]
mod cpp_tests {
use cloudini::{
CompressionOption, CppPointcloudDecoder, CppPointcloudEncoder, EncodingInfo,
EncodingOptions, FieldType, PointField, PointcloudDecoder, PointcloudEncoder,
};
fn make_xyzi_info(n: u32, comp: CompressionOption) -> EncodingInfo {
EncodingInfo {
fields: vec![
PointField {
name: "x".into(),
offset: 0,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "y".into(),
offset: 4,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "z".into(),
offset: 8,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "intensity".into(),
offset: 12,
field_type: FieldType::Uint8,
resolution: None,
},
],
width: n,
height: 1,
point_step: 13,
encoding_opt: EncodingOptions::Lossy,
compression_opt: comp,
..EncodingInfo::default()
}
}
fn gen_cloud(n: usize) -> Vec<u8> {
let mut data = vec![0u8; n * 13];
for i in 0..n {
let t = i as f32 * 0.005;
let x = t.sin() * 20.0;
let y = t.cos() * 15.0;
let z = (t * 0.2).sin() * 8.0 + 1.0;
let base = i * 13;
data[base..base + 4].copy_from_slice(&x.to_le_bytes());
data[base + 4..base + 8].copy_from_slice(&y.to_le_bytes());
data[base + 8..base + 12].copy_from_slice(&z.to_le_bytes());
data[base + 12] = (i % 256) as u8;
}
data
}
fn decode_xyz(data: &[u8], i: usize) -> (f32, f32, f32, u8) {
let base = i * 13;
let x = f32::from_le_bytes(data[base..base + 4].try_into().unwrap());
let y = f32::from_le_bytes(data[base + 4..base + 8].try_into().unwrap());
let z = f32::from_le_bytes(data[base + 8..base + 12].try_into().unwrap());
(x, y, z, data[base + 12])
}
fn assert_lossy_close(original: &[u8], decoded: &[u8], n: usize, tol: f32) {
assert_eq!(decoded.len(), original.len());
for i in 0..n {
let (ox, oy, oz, oi) = decode_xyz(original, i);
let (dx, dy, dz, di) = decode_xyz(decoded, i);
assert!((ox - dx).abs() <= tol, "pt {i}: x: {ox} vs {dx}");
assert!((oy - dy).abs() <= tol, "pt {i}: y: {oy} vs {dy}");
assert!((oz - dz).abs() <= tol, "pt {i}: z: {oz} vs {dz}");
assert_eq!(oi, di, "pt {i}: intensity");
}
}
#[test]
fn cpp_encode_rust_decode_lz4() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_encode_rust_decode_zstd() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn rust_encode_cpp_decode_lz4() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn rust_encode_cpp_decode_zstd() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_roundtrip_lz4() {
let n = 5_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let enc = CppPointcloudEncoder::new(info.clone()).with_threads(false);
let compressed = enc.encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_roundtrip_zstd() {
let n = 5_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let enc = CppPointcloudEncoder::new(info.clone()).with_threads(false);
let compressed = enc.encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_roundtrip_multi_chunk() {
let n = 70_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let enc = CppPointcloudEncoder::new(info.clone()).with_threads(false);
let compressed = enc.encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn rust_encode_cpp_decode_multi_chunk_lz4() {
let n = 70_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn rust_encode_cpp_decode_multi_chunk_zstd() {
let n = 70_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_encode_rust_decode_multi_chunk_lz4() {
let n = 70_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_encode_rust_decode_multi_chunk_zstd() {
let n = 70_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
fn make_xyzw_info(n: u32, comp: CompressionOption) -> EncodingInfo {
EncodingInfo {
fields: vec![
PointField {
name: "x".into(),
offset: 0,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "y".into(),
offset: 4,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "z".into(),
offset: 8,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
PointField {
name: "w".into(),
offset: 12,
field_type: FieldType::Float32,
resolution: Some(0.001),
},
],
width: n,
height: 1,
point_step: 16,
encoding_opt: EncodingOptions::Lossy,
compression_opt: comp,
..EncodingInfo::default()
}
}
fn gen_xyzw_cloud(n: usize) -> Vec<u8> {
let mut data = vec![0u8; n * 16];
for i in 0..n {
let t = i as f32 * 0.01;
for j in 0..4usize {
let v = (t + j as f32 * 5.0).sin() * 10.0;
let base = i * 16 + j * 4;
data[base..base + 4].copy_from_slice(&v.to_le_bytes());
}
}
data
}
#[test]
fn rust_encode_cpp_decode_4_floats_lz4() {
let n = 5_000;
let cloud = gen_xyzw_cloud(n);
let info = make_xyzw_info(n as u32, CompressionOption::Lz4);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
let tol = 0.001 * 0.5 + 1e-5;
assert_eq!(decoded.len(), cloud.len());
for i in 0..n {
for j in 0..4usize {
let base = i * 16 + j * 4;
let o = f32::from_le_bytes(cloud[base..base + 4].try_into().unwrap());
let d = f32::from_le_bytes(decoded[base..base + 4].try_into().unwrap());
assert!((o - d).abs() <= tol, "pt {i} field {j}: {o} vs {d}");
}
}
}
#[test]
fn cpp_encode_rust_decode_4_floats_lz4() {
let n = 5_000;
let cloud = gen_xyzw_cloud(n);
let info = make_xyzw_info(n as u32, CompressionOption::Lz4);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
let tol = 0.001 * 0.5 + 1e-5;
assert_eq!(decoded.len(), cloud.len());
for i in 0..n {
for j in 0..4usize {
let base = i * 16 + j * 4;
let o = f32::from_le_bytes(cloud[base..base + 4].try_into().unwrap());
let d = f32::from_le_bytes(decoded[base..base + 4].try_into().unwrap());
assert!((o - d).abs() <= tol, "pt {i} field {j}: {o} vs {d}");
}
}
}
fn make_int_info(n: u32, comp: CompressionOption) -> (EncodingInfo, Vec<u8>) {
let point_step: u32 = 4 + 2 + 4; let info = EncodingInfo {
fields: vec![
PointField {
name: "a".into(),
offset: 0,
field_type: FieldType::Uint32,
resolution: None,
},
PointField {
name: "b".into(),
offset: 4,
field_type: FieldType::Uint16,
resolution: None,
},
PointField {
name: "c".into(),
offset: 6,
field_type: FieldType::Int32,
resolution: None,
},
],
width: n,
height: 1,
point_step,
encoding_opt: EncodingOptions::Lossy,
compression_opt: comp,
..EncodingInfo::default()
};
let mut cloud = vec![0u8; n as usize * point_step as usize];
for i in 0..n as usize {
let base = i * point_step as usize;
let a = (i * 1000) as u32;
let b = (i % 65536) as u16;
let c = -(i as i32) * 50;
cloud[base..base + 4].copy_from_slice(&a.to_le_bytes());
cloud[base + 4..base + 6].copy_from_slice(&b.to_le_bytes());
cloud[base + 6..base + 10].copy_from_slice(&c.to_le_bytes());
}
(info, cloud)
}
#[test]
fn rust_encode_cpp_decode_integer_fields() {
let n = 2_000;
let (info, cloud) = make_int_info(n as u32, CompressionOption::Zstd);
let compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_eq!(decoded, cloud, "Integer fields must round-trip exactly");
}
#[test]
fn cpp_encode_rust_decode_integer_fields() {
let n = 2_000;
let (info, cloud) = make_int_info(n as u32, CompressionOption::Zstd);
let compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_eq!(decoded, cloud, "Integer fields must round-trip exactly");
}
#[test]
fn rust_encode_cpp_decode_nan() {
let n = 100;
let mut cloud = gen_cloud(n);
cloud[0..4].copy_from_slice(&f32::NAN.to_le_bytes());
cloud[13 * 5 + 4..13 * 5 + 8].copy_from_slice(&f32::NAN.to_le_bytes());
cloud[13 * 9 + 8..13 * 9 + 12].copy_from_slice(&f32::NAN.to_le_bytes());
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = PointcloudEncoder::new(info).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
let x0 = f32::from_le_bytes(decoded[0..4].try_into().unwrap());
let y5 = f32::from_le_bytes(decoded[13 * 5 + 4..13 * 5 + 8].try_into().unwrap());
let z9 = f32::from_le_bytes(decoded[13 * 9 + 8..13 * 9 + 12].try_into().unwrap());
assert!(x0.is_nan(), "x[0] should be NaN after cross-decode");
assert!(y5.is_nan(), "y[5] should be NaN after cross-decode");
assert!(z9.is_nan(), "z[9] should be NaN after cross-decode");
}
#[test]
fn cpp_encode_rust_decode_nan() {
let n = 100;
let mut cloud = gen_cloud(n);
cloud[0..4].copy_from_slice(&f32::NAN.to_le_bytes());
cloud[13 * 5 + 4..13 * 5 + 8].copy_from_slice(&f32::NAN.to_le_bytes());
cloud[13 * 9 + 8..13 * 9 + 12].copy_from_slice(&f32::NAN.to_le_bytes());
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let compressed = CppPointcloudEncoder::new(info)
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
let x0 = f32::from_le_bytes(decoded[0..4].try_into().unwrap());
let y5 = f32::from_le_bytes(decoded[13 * 5 + 4..13 * 5 + 8].try_into().unwrap());
let z9 = f32::from_le_bytes(decoded[13 * 9 + 8..13 * 9 + 12].try_into().unwrap());
assert!(x0.is_nan(), "x[0] should be NaN after cross-decode");
assert!(y5.is_nan(), "y[5] should be NaN after cross-decode");
assert!(z9.is_nan(), "z[9] should be NaN after cross-decode");
}
#[test]
fn rust_encode_cpp_decode_no_compression() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::None);
let compressed = PointcloudEncoder::new(info).encode(&cloud).unwrap();
let (_, decoded) = CppPointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn cpp_encode_rust_decode_no_compression() {
let n = 1_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::None);
let compressed = CppPointcloudEncoder::new(info)
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, decoded) = PointcloudDecoder::new().decode(&compressed).unwrap();
assert_lossy_close(&cloud, &decoded, n, 0.001 * 0.5 + 1e-5);
}
#[test]
fn rust_and_cpp_field_encoded_bytes_identical_xyzi() {
let n = 5_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::None);
let rust_out = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let cpp_out = CppPointcloudEncoder::new(info)
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, rust_header_len) = cloudini::decode_header(&rust_out).unwrap();
let (_, cpp_header_len) = cloudini::decode_header(&cpp_out).unwrap();
assert_eq!(
&rust_out[rust_header_len..],
&cpp_out[cpp_header_len..],
"Rust and C++ must produce identical field-encoded bytes (XYZI, None compression)"
);
}
#[test]
fn rust_and_cpp_field_encoded_bytes_identical_4_floats() {
let n = 5_000;
let cloud = gen_xyzw_cloud(n);
let info = make_xyzw_info(n as u32, CompressionOption::None);
let rust_out = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let cpp_out = CppPointcloudEncoder::new(info)
.with_threads(false)
.encode(&cloud)
.unwrap();
let (_, rust_header_len) = cloudini::decode_header(&rust_out).unwrap();
let (_, cpp_header_len) = cloudini::decode_header(&cpp_out).unwrap();
assert_eq!(
&rust_out[rust_header_len..],
&cpp_out[cpp_header_len..],
"Rust and C++ must produce identical field-encoded bytes (4×Float32, None compression)"
);
}
#[test]
fn cpp_and_rust_produce_similar_sizes() {
let n = 10_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let rust_compressed = PointcloudEncoder::new(info.clone()).encode(&cloud).unwrap();
let cpp_compressed = CppPointcloudEncoder::new(info.clone())
.with_threads(false)
.encode(&cloud)
.unwrap();
let rust_ratio = rust_compressed.len() as f64 / cloud.len() as f64;
let cpp_ratio = cpp_compressed.len() as f64 / cloud.len() as f64;
println!(
"rust: {} B ({:.1}%), cpp: {} B ({:.1}%)",
rust_compressed.len(),
rust_ratio * 100.0,
cpp_compressed.len(),
cpp_ratio * 100.0,
);
assert!(rust_ratio < 0.70, "Rust ratio {rust_ratio:.3} ≥ 0.70");
assert!(cpp_ratio < 0.70, "C++ ratio {cpp_ratio:.3} ≥ 0.70");
}
#[test]
fn cpp_encode_chunks_matches_encode() {
let n = 2_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Lz4);
let enc = CppPointcloudEncoder::new(info).with_threads(false);
let full = enc.encode(&cloud).unwrap();
let mut chunks_only = Vec::new();
enc.encode_chunks(&cloud, &mut chunks_only).unwrap();
let (_, actual_header_len) = cloudini::decode_header(&full).unwrap();
assert_eq!(&full[actual_header_len..], chunks_only.as_slice());
}
#[test]
fn cpp_decode_with_info_matches_decode() {
let n = 2_000;
let cloud = gen_cloud(n);
let info = make_xyzi_info(n as u32, CompressionOption::Zstd);
let enc = CppPointcloudEncoder::new(info.clone()).with_threads(false);
let full = enc.encode(&cloud).unwrap();
let dec = CppPointcloudDecoder::new();
let (enc_info, via_decode) = dec.decode(&full).unwrap();
let (_, actual_header_len) = cloudini::decode_header(&full).unwrap();
let chunk_data = &full[actual_header_len..];
let via_decode_with_info = dec.decode_with_info(&enc_info, chunk_data).unwrap();
assert_eq!(via_decode, via_decode_with_info);
}
}