cloudini 0.3.2

The cloudini point cloud compression library for Rust.
Documentation
//! Integration tests for the `ros` feature: compress/decompress `PointCloud2Msg` roundtrips.

#[cfg(feature = "ros")]
mod ros_tests {
    use cloudini::ros::{CompressExt, CompressedPointCloud2, CompressionConfig};
    use ros_pointcloud2::{PointCloud2Msg, points::PointXYZI};

    fn make_cloud(n: usize, _resolution: f32) -> PointCloud2Msg {
        let points: Vec<PointXYZI> = (0..n)
            .map(|i| {
                let t = i as f32 * 0.01;
                PointXYZI::new(t.sin() * 5.0, t.cos() * 5.0, t * 0.1, (i % 256) as f32)
            })
            .collect();
        PointCloud2Msg::try_from_iter(&points).unwrap()
    }

    fn approx_eq_f32(a: f32, b: f32, tol: f32) -> bool {
        (a - b).abs() <= tol
    }

    fn check_roundtrip(original: &PointCloud2Msg, restored: &PointCloud2Msg, tol: f32) {
        assert_eq!(original.dimensions.width, restored.dimensions.width);
        assert_eq!(original.dimensions.height, restored.dimensions.height);
        assert_eq!(original.point_step, restored.point_step);
        assert_eq!(original.data.len(), restored.data.len());

        // Compare per-point floats within tolerance
        let pts_orig: Vec<PointXYZI> = original.clone().try_into_iter().unwrap().collect();
        let pts_rest: Vec<PointXYZI> = restored.clone().try_into_iter().unwrap().collect();

        for (a, b) in pts_orig.iter().zip(pts_rest.iter()) {
            assert!(
                approx_eq_f32(a.x, b.x, tol),
                "x mismatch: {} vs {}",
                a.x,
                b.x
            );
            assert!(
                approx_eq_f32(a.y, b.y, tol),
                "y mismatch: {} vs {}",
                a.y,
                b.y
            );
            assert!(
                approx_eq_f32(a.z, b.z, tol),
                "z mismatch: {} vs {}",
                a.z,
                b.z
            );
            assert!(
                approx_eq_f32(a.intensity, b.intensity, tol),
                "intensity mismatch: {} vs {}",
                a.intensity,
                b.intensity
            );
        }
    }

    #[test]
    fn compress_ext_lossy_zstd_roundtrip() {
        let resolution = 0.001_f32;
        let cloud = make_cloud(500, resolution);
        let compressed = cloud
            .clone()
            .compress(CompressionConfig::lossy_zstd(resolution))
            .unwrap();
        assert_eq!(compressed.format, "cloudini/lossy/zstd");
        let restored = compressed.decompress().unwrap();
        check_roundtrip(&cloud, &restored, resolution * 0.5 + 1e-5);
    }

    #[test]
    fn compress_ext_lossy_lz4_roundtrip() {
        let resolution = 0.01_f32;
        let cloud = make_cloud(500, resolution);
        let compressed = cloud
            .clone()
            .compress(CompressionConfig::lossy_lz4(resolution))
            .unwrap();
        assert_eq!(compressed.format, "cloudini/lossy/lz4");
        let restored = compressed.decompress().unwrap();
        check_roundtrip(&cloud, &restored, resolution * 0.5 + 1e-5);
    }

    #[test]
    fn compressed_pointcloud2_lossless_zstd() {
        let cloud = make_cloud(500, 0.001);
        let compressed =
            CompressedPointCloud2::compress(cloud.clone(), CompressionConfig::lossless_zstd())
                .unwrap();
        assert_eq!(compressed.format, "cloudini/lossless/zstd");
        let restored = compressed.decompress().unwrap();
        // Lossless: data must be bit-exact
        assert_eq!(cloud.data, restored.data);
    }

    #[test]
    fn compressed_pointcloud2_lossless_lz4() {
        let cloud = make_cloud(200, 0.001);
        let compressed =
            CompressedPointCloud2::compress(cloud.clone(), CompressionConfig::lossless_lz4())
                .unwrap();
        assert_eq!(compressed.format, "cloudini/lossless/lz4");
        let restored = compressed.decompress().unwrap();
        assert_eq!(cloud.data, restored.data);
    }

    #[test]
    fn metadata_preserved_through_roundtrip() {
        let cloud = make_cloud(100, 0.001);
        let compressed = cloud
            .clone()
            .compress(CompressionConfig::default())
            .unwrap();

        // Header, width, height, is_bigendian, is_dense should be carried verbatim
        assert_eq!(compressed.height, cloud.dimensions.height);
        assert_eq!(compressed.width, cloud.dimensions.width);
        assert!(!compressed.is_bigendian);
        assert!(compressed.is_dense); // try_from_iter produces Denseness::Dense

        let restored = compressed.decompress().unwrap();
        assert_eq!(restored.dimensions.width, cloud.dimensions.width);
        assert_eq!(restored.dimensions.height, cloud.dimensions.height);
        assert_eq!(restored.point_step, cloud.point_step);
    }

    #[test]
    fn large_cloud_multi_chunk_roundtrip() {
        // > 32768 points to exercise multi-chunk path
        let resolution = 0.001_f32;
        let cloud = make_cloud(40_000, resolution);
        let compressed = cloud
            .clone()
            .compress(CompressionConfig::lossy_zstd(resolution))
            .unwrap();
        let restored = compressed.decompress().unwrap();
        check_roundtrip(&cloud, &restored, resolution * 0.5 + 1e-5);
    }

    // ── serde ──────────────────────────────────────────────────────────────────

    #[cfg(feature = "serde")]
    mod serde_tests {
        use super::*;

        #[test]
        fn serde_json_roundtrip() {
            let cloud = make_cloud(100, 0.001);
            let compressed = cloud
                .compress(CompressionConfig::lossy_zstd(0.001))
                .unwrap();

            let json = serde_json::to_string(&compressed).unwrap();
            let restored: CompressedPointCloud2 = serde_json::from_str(&json).unwrap();

            assert_eq!(compressed.width, restored.width);
            assert_eq!(compressed.height, restored.height);
            assert_eq!(compressed.compressed_data, restored.compressed_data);
            assert_eq!(compressed.format, restored.format);
            assert_eq!(compressed.point_step, restored.point_step);
        }

        #[test]
        fn serde_bincode_roundtrip() {
            let cloud = make_cloud(100, 0.001);
            let compressed = cloud.compress(CompressionConfig::lossless_zstd()).unwrap();

            let bytes = bincode::serialize(&compressed).unwrap();
            let restored: CompressedPointCloud2 = bincode::deserialize(&bytes).unwrap();

            assert_eq!(compressed.compressed_data, restored.compressed_data);
            assert_eq!(compressed.format, restored.format);
        }
    }

    // ── rkyv ───────────────────────────────────────────────────────────────────

    #[cfg(feature = "rkyv")]
    mod rkyv_tests {
        use super::*;
        use rkyv::rancor::Error;

        #[test]
        fn rkyv_zero_copy_access() {
            let cloud = make_cloud(100, 0.001);
            let compressed = cloud
                .compress(CompressionConfig::lossy_zstd(0.001))
                .unwrap();

            // Serialize to bytes
            let bytes = rkyv::to_bytes::<Error>(&compressed).unwrap();

            // Zero-copy access — the primary rkyv use case
            let archived =
                rkyv::access::<rkyv::Archived<CompressedPointCloud2>, Error>(&bytes).unwrap();

            assert_eq!(u32::from(archived.width), compressed.width);
            assert_eq!(u32::from(archived.height), compressed.height);
            assert_eq!(archived.format.as_str(), compressed.format.as_str());
            assert_eq!(
                archived.compressed_data.as_slice(),
                compressed.compressed_data.as_slice()
            );

            // The compressed_data bytes are still valid cloudini — decompress straight from
            // the archived slice without ever copying to an owned Vec first.
            let decoder = cloudini::PointcloudDecoder::new();
            let (_info, _raw) = decoder.decode(archived.compressed_data.as_slice()).unwrap();
        }
    }
}