cloudini 0.3.2

The cloudini point cloud compression library for Rust.
Documentation
//! C++ encoder bridge (feature `use_cpp`).
//!
//! [`CppPointcloudEncoder`] wraps the cloudini C++ implementation via FFI and
//! exposes the same interface as the native Rust [`crate::PointcloudEncoder`].

use std::ffi::CString;

use crate::ffi;
use crate::header::{encode_header, encoding_info_to_yaml};
use crate::types::EncodingInfo;
use crate::{Error, Result};

/// Encodes raw point cloud bytes using the C++ cloudini implementation.
///
/// The API is intentionally identical to [`crate::PointcloudEncoder`] so the
/// two types can be used interchangeably — swap the type name and everything
/// else compiles unchanged.
///
/// # Threading
///
/// By default the C++ encoder overlaps field encoding with block compression
/// on a background thread (`use_threads = true`). Call
/// [`with_threads(false)`](Self::with_threads) to force single-threaded
/// operation, which is useful for fair benchmarking against the Rust
/// implementation.
pub struct CppPointcloudEncoder {
    info: EncodingInfo,
    header: Vec<u8>,
    yaml_header: CString,
    use_threads: bool,
}

impl CppPointcloudEncoder {
    /// Create a new encoder for the given point cloud layout and compression settings.
    ///
    /// Mirrors [`crate::PointcloudEncoder::new`] exactly — takes ownership of `info`.
    pub fn new(info: EncodingInfo) -> Self {
        let yaml = encoding_info_to_yaml(&info);
        let header = encode_header(&info);
        Self {
            yaml_header: CString::new(yaml).expect("YAML header contained a null byte"),
            info,
            header,
            use_threads: true,
        }
    }

    /// Control whether the C++ background compression thread is used.
    pub fn with_threads(mut self, use_threads: bool) -> Self {
        self.use_threads = use_threads;
        self
    }

    /// Returns the [`EncodingInfo`] this encoder was constructed with.
    pub fn encoding_info(&self) -> &EncodingInfo {
        &self.info
    }

    /// Returns the pre-built header bytes (`CLOUDINI_V03\n…\0`).
    pub fn header_bytes(&self) -> &[u8] {
        &self.header
    }

    /// Encode `cloud_data` and return a complete Cloudini buffer (header + chunks).
    ///
    /// `cloud_data` must be exactly `width * height * point_step` bytes.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Cpp`] if the C++ side returns 0 (any internal error).
    pub fn encode(&self, cloud_data: &[u8]) -> Result<Vec<u8>> {
        // Worst case: field encoding can expand incompressible data slightly;
        // the header adds a few hundred bytes. 2× headroom is generous but safe.
        let max_out = cloud_data.len().saturating_mul(2).max(65_536);
        let mut output = vec![0u8; max_out];

        let written = unsafe {
            ffi::cloudini_c_encode(
                self.yaml_header.as_ptr(),
                cloud_data.as_ptr(),
                cloud_data.len() as u32,
                output.as_mut_ptr(),
                max_out as u32,
                self.use_threads as i32,
            )
        };

        if written == 0 {
            return Err(Error::Cpp("encode returned 0".into()));
        }
        output.truncate(written as usize);
        Ok(output)
    }

    /// Encode `cloud_data` and append the raw chunk data (without header) to `out`.
    ///
    /// Mirrors [`crate::PointcloudEncoder::encode_chunks`] — useful when you
    /// manage the header separately (e.g. streaming scenarios).
    pub fn encode_chunks(&self, cloud_data: &[u8], out: &mut Vec<u8>) -> Result<()> {
        let full = self.encode(cloud_data)?;
        // The C++ encoder writes its own YAML header, which may differ in byte
        // length from the Rust header stored in self.header.  Parse it to find
        // the true boundary.
        let (_, header_len) = crate::header::decode_header(&full)?;
        out.extend_from_slice(&full[header_len..]);
        Ok(())
    }
}