cloudini 0.3.1

The cloudini point cloud compression library for Rust.
Documentation
//! Core types shared across the cloudini encoder and decoder.

/// Magic bytes that prefix every Cloudini buffer (`"CLOUDINI_V"`).
pub const MAGIC_HEADER: &[u8; 10] = b"CLOUDINI_V";

/// Current encoding format version written into new buffers.
pub const ENCODING_VERSION: u8 = 3;

/// Number of points packed into one compressed chunk.
///
/// Each chunk is independently compressed, allowing streaming decode and
/// limiting peak memory usage for very large clouds.
pub const POINTS_PER_CHUNK: usize = 32 * 1024;

/// Data type of a single point field.
///
/// The numeric values match the Cloudini wire format so they can be stored
/// directly in headers without a separate mapping table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FieldType {
    Unknown = 0,
    Int8 = 1,
    Uint8 = 2,
    Int16 = 3,
    Uint16 = 4,
    Int32 = 5,
    Uint32 = 6,
    /// 32-bit IEEE 754 float. With `resolution` set, encoded lossy; otherwise copied verbatim.
    Float32 = 7,
    /// 64-bit IEEE 754 float. With `resolution` set, encoded lossy; otherwise XOR-encoded (lossless).
    Float64 = 8,
    Int64 = 9,
    Uint64 = 10,
}

impl FieldType {
    /// Returns the byte width of this field type (0 for [`FieldType::Unknown`]).
    pub fn size_of(self) -> usize {
        match self {
            FieldType::Int8 | FieldType::Uint8 => 1,
            FieldType::Int16 | FieldType::Uint16 => 2,
            FieldType::Int32 | FieldType::Uint32 | FieldType::Float32 => 4,
            FieldType::Float64 | FieldType::Int64 | FieldType::Uint64 => 8,
            FieldType::Unknown => 0,
        }
    }

    /// Parse from the upper-case string name used in YAML headers (e.g. `"FLOAT32"`)
    /// or from a decimal integer string (e.g. `"7"`).
    pub fn from_yaml(s: &str) -> Option<Self> {
        match s {
            "INT8" => Some(FieldType::Int8),
            "UINT8" => Some(FieldType::Uint8),
            "INT16" => Some(FieldType::Int16),
            "UINT16" => Some(FieldType::Uint16),
            "INT32" => Some(FieldType::Int32),
            "UINT32" => Some(FieldType::Uint32),
            "FLOAT32" => Some(FieldType::Float32),
            "FLOAT64" => Some(FieldType::Float64),
            "INT64" => Some(FieldType::Int64),
            "UINT64" => Some(FieldType::Uint64),
            _ => {
                if let Ok(v) = s.parse::<u8>() {
                    return match v {
                        0 => Some(FieldType::Unknown),
                        1 => Some(FieldType::Int8),
                        2 => Some(FieldType::Uint8),
                        3 => Some(FieldType::Int16),
                        4 => Some(FieldType::Uint16),
                        5 => Some(FieldType::Int32),
                        6 => Some(FieldType::Uint32),
                        7 => Some(FieldType::Float32),
                        8 => Some(FieldType::Float64),
                        9 => Some(FieldType::Int64),
                        10 => Some(FieldType::Uint64),
                        _ => None,
                    };
                }
                None
            }
        }
    }

    /// Returns the upper-case string name used in YAML headers (e.g. `"FLOAT32"`).
    pub fn as_str(self) -> &'static str {
        match self {
            FieldType::Int8 => "INT8",
            FieldType::Uint8 => "UINT8",
            FieldType::Int16 => "INT16",
            FieldType::Uint16 => "UINT16",
            FieldType::Int32 => "INT32",
            FieldType::Uint32 => "UINT32",
            FieldType::Float32 => "FLOAT32",
            FieldType::Float64 => "FLOAT64",
            FieldType::Int64 => "INT64",
            FieldType::Uint64 => "UINT64",
            FieldType::Unknown => "UNKNOWN",
        }
    }
}

/// Description of a single named field within a point.
///
/// Fields map directly to the fields listed in the Cloudini YAML header and
/// to the fields in a ROS `sensor_msgs/PointCloud2` message.
#[derive(Debug, Clone, PartialEq)]
pub struct PointField {
    /// Field name (e.g. `"x"`, `"intensity"`, `"ring"`).
    pub name: String,
    /// Byte offset of this field within one point (0-based).
    pub offset: u32,
    /// Data type stored at `offset`.
    pub field_type: FieldType,
    /// Quantisation step for lossy float encoding.
    ///
    /// - `Some(r)` — `Float32`/`Float64` values are rounded to multiples of `r`;
    ///   maximum reconstruction error is `r / 2`.
    /// - `None` — the field is copied verbatim (no lossy compression).
    pub resolution: Option<f32>,
}

/// Controls the field-level encoding algorithm.
///
/// This is the first stage of compression (before optional LZ4/ZSTD).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum EncodingOptions {
    /// No field encoding; raw bytes are passed through unchanged.
    None = 0,
    /// Lossy float quantisation + delta-varint for integers.
    ///
    /// Floats with `resolution` set are quantised to `round(v / resolution)` then
    /// delta-encoded across consecutive points. Integers use delta + zigzag varint.
    Lossy = 1,
    /// Lossless encoding: XOR for `Float64`, delta-varint for integers.
    ///
    /// `Float32` without resolution is copied verbatim. `Float64` without resolution
    /// is XOR-encoded against the previous point's bits.
    Lossless = 2,
}

impl EncodingOptions {
    /// Parse from the upper-case string used in YAML headers (`"NONE"`, `"LOSSY"`, `"LOSSLESS"`)
    /// or from a decimal digit string.
    pub fn from_yaml(s: &str) -> Option<Self> {
        match s {
            "NONE" => Some(EncodingOptions::None),
            "LOSSY" => Some(EncodingOptions::Lossy),
            "LOSSLESS" => Some(EncodingOptions::Lossless),
            _ => {
                if let Ok(v) = s.parse::<u8>() {
                    return match v {
                        0 => Some(EncodingOptions::None),
                        1 => Some(EncodingOptions::Lossy),
                        2 => Some(EncodingOptions::Lossless),
                        _ => None,
                    };
                }
                None
            }
        }
    }

    /// Returns the upper-case string used in YAML headers.
    pub fn as_str(self) -> &'static str {
        match self {
            EncodingOptions::None => "NONE",
            EncodingOptions::Lossy => "LOSSY",
            EncodingOptions::Lossless => "LOSSLESS",
        }
    }
}

/// Block-compression algorithm applied after field encoding.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CompressionOption {
    /// No block compression (field encoding only).
    None = 0,
    /// LZ4 block compression — very fast, moderate ratio.
    Lz4 = 1,
    /// ZSTD compression at level 1 — slower than LZ4, higher ratio.
    Zstd = 2,
}

impl CompressionOption {
    /// Parse from the upper-case string used in YAML headers (`"NONE"`, `"LZ4"`, `"ZSTD"`)
    /// or from a decimal digit string.
    pub fn from_yaml(s: &str) -> Option<Self> {
        match s {
            "NONE" => Some(CompressionOption::None),
            "LZ4" => Some(CompressionOption::Lz4),
            "ZSTD" => Some(CompressionOption::Zstd),
            _ => {
                if let Ok(v) = s.parse::<u8>() {
                    return match v {
                        0 => Some(CompressionOption::None),
                        1 => Some(CompressionOption::Lz4),
                        2 => Some(CompressionOption::Zstd),
                        _ => None,
                    };
                }
                None
            }
        }
    }

    /// Returns the upper-case string used in YAML headers.
    pub fn as_str(self) -> &'static str {
        match self {
            CompressionOption::None => "NONE",
            CompressionOption::Lz4 => "LZ4",
            CompressionOption::Zstd => "ZSTD",
        }
    }
}

/// Full description of a point cloud's layout, encoding, and compression settings.
///
/// This is both the encoder configuration and the metadata stored in every
/// Cloudini buffer header so decoders need no out-of-band information.
///
/// ## Example
///
/// ```rust
/// use cloudini::{EncodingInfo, EncodingOptions, CompressionOption, FieldType, PointField};
///
/// // Typical Lidar cloud: XYZ float32 at 1 mm resolution + uint8 intensity
/// let info = 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: 1000,
///     height: 1,
///     point_step: 13,
///     encoding_opt: EncodingOptions::Lossy,
///     compression_opt: CompressionOption::Zstd,
///     ..EncodingInfo::default()
/// };
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct EncodingInfo {
    /// Per-field encoding descriptors, in order of appearance in a point.
    pub fields: Vec<PointField>,
    /// Number of points per row (equals total points for unorganised clouds).
    pub width: u32,
    /// Number of rows (1 for unorganised clouds).
    pub height: u32,
    /// Byte stride between consecutive points.
    pub point_step: u32,
    /// Field-level encoding algorithm.
    pub encoding_opt: EncodingOptions,
    /// Block compression applied after field encoding.
    pub compression_opt: CompressionOption,
    /// Optional free-form string stored in the header (ignored by the decoder).
    pub encoding_config: String,
    /// Format version read from or written to the header.
    pub version: u8,
}

impl Default for EncodingInfo {
    fn default() -> Self {
        Self {
            fields: Vec::new(),
            width: 0,
            height: 1,
            point_step: 0,
            encoding_opt: EncodingOptions::Lossy,
            compression_opt: CompressionOption::Zstd,
            encoding_config: String::new(),
            version: ENCODING_VERSION,
        }
    }
}