n3gb-rs 0.2.2

A Rust implementation of a hierarchical hex-based spatial indexing system based on the OSGB National Grid.
Documentation
use crate::error::N3gbError;

/// All dimensions of a regular hexagon computed from a single input measurement.
///
/// A regular hexagon has relationships between its various measurements.
/// This struct contains all derived dimensions once you provide any one of them.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HexagonDims {
    /// Side length (same as circumradius for regular hexagons)
    pub a: f64,
    /// Circumradius (center to vertex distance)
    pub r_circum: f64,
    /// Apothem (center to edge midpoint distance)
    pub r_apothem: f64,
    /// Distance across corners (vertex to opposite vertex)
    pub d_corners: f64,
    /// Distance across flats (edge to opposite edge)
    pub d_flats: f64,
    /// Total perimeter (6 * side length)
    pub perimeter: f64,
    /// Total area
    pub area: f64,
}

/// Computes all hexagon dimensions from the side length.
///
/// # Arguments
///
/// * `a` - The side length of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the side length.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `a` is zero or negative.
pub fn from_side(a: f64) -> Result<HexagonDims, N3gbError> {
    if a <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Side length must be positive".to_string(),
        ));
    }

    let sqrt3 = 3.0_f64.sqrt();
    let r_circum = a;
    let r_apothem = (sqrt3 / 2.0) * a;
    let d_corners = 2.0 * a;
    let d_flats = sqrt3 * a;
    let perimeter = 6.0 * a;
    let area = (3.0 * sqrt3 / 2.0) * a * a;

    Ok(HexagonDims {
        a,
        r_circum,
        r_apothem,
        d_corners,
        d_flats,
        perimeter,
        area,
    })
}

/// Computes all hexagon dimensions from the circumradius.
///
/// For regular hexagons, circumradius equals side length.
///
/// # Arguments
///
/// * `r` - The circumradius (center to vertex distance) of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the circumradius.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `r` is zero or negative.
pub fn from_circumradius(r: f64) -> Result<HexagonDims, N3gbError> {
    from_side(r)
}

/// Computes all hexagon dimensions from the apothem (inradius).
///
/// # Arguments
///
/// * `r` - The apothem (center to edge midpoint distance) of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the apothem.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `r` is zero or negative.
pub fn from_apothem(r: f64) -> Result<HexagonDims, N3gbError> {
    if r <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Apothem must be positive".to_string(),
        ));
    }

    let sqrt3 = 3.0_f64.sqrt();
    let a = 2.0 * r / sqrt3;
    from_side(a)
}

/// Computes all hexagon dimensions from the across-flats distance.
///
/// # Arguments
///
/// * `df` - The distance across flats (edge to opposite edge) of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the across-flats distance.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `df` is zero or negative.
pub fn from_across_flats(df: f64) -> Result<HexagonDims, N3gbError> {
    if df <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Across-flats must be positive".to_string(),
        ));
    }

    let sqrt3 = 3.0_f64.sqrt();
    let a = df / sqrt3;
    from_side(a)
}

/// Computes all hexagon dimensions from the across-corners distance.
///
/// # Arguments
///
/// * `dc` - The distance across corners (vertex to opposite vertex) of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the across-corners distance.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `dc` is zero or negative.
pub fn from_across_corners(dc: f64) -> Result<HexagonDims, N3gbError> {
    if dc <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Across-corners must be positive".to_string(),
        ));
    }

    let a = dc / 2.0;
    from_side(a)
}

/// Computes all hexagon dimensions from the area.
///
/// # Arguments
///
/// * `area` - The total area of the hexagon (must be positive).
///
/// # Returns
///
/// A [`HexagonDims`] with all derived measurements computed from the area.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `area` is zero or negative.
pub fn from_area(area: f64) -> Result<HexagonDims, N3gbError> {
    if area <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Area must be positive".to_string(),
        ));
    }

    let sqrt3 = 3.0_f64.sqrt();
    let a = ((2.0 * area) / (3.0 * sqrt3)).sqrt();
    from_side(a)
}

/// Returns the bounding box dimensions (width, height) for a hexagon.
///
/// - `pointy_top`: If true, hexagon has vertices at top/bottom; if false, flat edges at top/bottom.
///
/// # Arguments
///
/// * `a` - The side length of the hexagon (must be positive).
/// * `pointy_top` - If true, the hexagon has vertices at top/bottom; if false, flat edges at top/bottom.
///
/// # Returns
///
/// A `(width, height)` tuple giving the bounding box dimensions.
///
/// # Errors
///
/// Returns [`N3gbError::InvalidDimension`] if `a` is zero or negative.
pub fn bounding_box(a: f64, pointy_top: bool) -> Result<(f64, f64), N3gbError> {
    if a <= 0.0 {
        return Err(N3gbError::InvalidDimension(
            "Side length must be positive".to_string(),
        ));
    }

    let sqrt3 = 3.0_f64.sqrt();
    let dc = 2.0 * a;
    let df = sqrt3 * a;

    if pointy_top {
        Ok((df, dc))
    } else {
        Ok((dc, df))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hexagon_dimensions() -> Result<(), N3gbError> {
        let dims = from_side(10.0)?;

        assert!((dims.a - 10.0).abs() < 0.001);
        assert!((dims.r_circum - 10.0).abs() < 0.001);
        assert!((dims.d_corners - 20.0).abs() < 0.001);
        assert!((dims.perimeter - 60.0).abs() < 0.001);

        let dims2 = from_across_flats(dims.d_flats)?;
        assert!((dims2.a - 10.0).abs() < 0.001);
        Ok(())
    }

    #[test]
    fn test_bounding_box() -> Result<(), N3gbError> {
        let (w, h) = bounding_box(10.0, true)?;
        assert!((w - 17.320508).abs() < 0.001); // sqrt(3) * 10
        assert!((h - 20.0).abs() < 0.001); // 2 * 10
        Ok(())
    }
}