bevy_a5 0.1.2

A Bevy plugin providing A5 geospatial pentagonal cells for floating origin use and spatial queries
Documentation
//! Cell geometry extraction for rendering.
//!
//! Provides utilities to extract cell boundaries, edges, and vertices
//! as 3D coordinates suitable for rendering with Bevy's mesh or gizmo systems.

use bevy_asset::RenderAssetUsages;
use bevy_math::{DVec3, Vec3};
use bevy_mesh::{Indices, Mesh, PrimitiveTopology};

use crate::cell::GeoCell;
use crate::coord;

/// The boundary vertices of a cell as 3D positions on the planet surface.
///
/// Returns vertices in order. A5 cells are pentagons (5 vertices).
///
/// Returns `None` if the cell index is invalid.
pub fn cell_boundary_3d(cell: u64, radius: f64) -> Option<Vec<Vec3>> {
    let boundary = a5::cell_to_boundary(
        cell,
        Some(a5::core::cell::CellToBoundaryOptions {
            closed_ring: false,
            segments: Some(1),
        }),
    )
    .ok()?;
    Some(
        boundary
            .iter()
            .map(|ll| coord::lonlat_to_vec3(ll, radius))
            .collect(),
    )
}

/// The boundary vertices as double-precision 3D positions.
pub fn cell_boundary_3d_f64(cell: u64, radius: f64) -> Option<Vec<DVec3>> {
    let boundary = a5::cell_to_boundary(cell, None).ok()?;
    Some(
        boundary
            .iter()
            .map(|ll| coord::lonlat_to_dvec3(ll, radius))
            .collect(),
    )
}

/// The boundary vertices relative to the cell center (local coordinates).
///
/// Useful for constructing meshes where each cell is its own entity.
///
/// Returns `None` if the cell index is invalid.
pub fn cell_boundary_local(cell: u64, radius: f64) -> Option<Vec<Vec3>> {
    let center_ll = a5::cell_to_lonlat(cell).ok()?;
    let center = coord::lonlat_to_dvec3(&center_ll, radius);
    let boundary = a5::cell_to_boundary(cell, None).ok()?;
    Some(
        boundary
            .iter()
            .map(|ll| (coord::lonlat_to_dvec3(ll, radius) - center).as_vec3())
            .collect(),
    )
}

/// The edges of a cell as pairs of 3D positions.
///
/// Each edge connects two consecutive boundary vertices.
pub fn cell_edges_3d(cell: u64, radius: f64) -> Option<Vec<(Vec3, Vec3)>> {
    let verts = cell_boundary_3d(cell, radius)?;
    if verts.is_empty() {
        return Some(Vec::new());
    }

    let mut edges = Vec::with_capacity(verts.len());
    for i in 0..verts.len() {
        let next = (i + 1) % verts.len();
        edges.push((verts[i], verts[next]));
    }
    Some(edges)
}

/// Generate a triangle fan mesh for a cell (for simple rendering).
///
/// Returns `(vertices, indices)` where vertices are local positions (relative
/// to the cell center) and indices form triangles in a fan from the center.
///
/// Returns `None` if the cell index is invalid.
pub fn cell_triangle_fan(cell: u64, radius: f64) -> Option<(Vec<Vec3>, Vec<u32>)> {
    let local_verts = cell_boundary_local(cell, radius)?;
    let n = local_verts.len();
    if n < 3 {
        return Some((Vec::new(), Vec::new()));
    }

    // Center vertex at index 0, boundary vertices at 1..=n
    let mut vertices = Vec::with_capacity(n + 1);
    vertices.push(Vec3::ZERO); // center
    vertices.extend_from_slice(&local_verts);

    let mut indices = Vec::with_capacity(n * 3);
    for i in 0..n {
        indices.push(0); // center
        indices.push((i + 1) as u32);
        indices.push(((i + 1) % n + 1) as u32);
    }

    Some((vertices, indices))
}

/// Extract boundary vertices for a collection of cells, with connectivity info.
///
/// Returns a flat list of `(position, cell_id)` pairs for all boundary vertices,
/// useful for batch rendering of cell outlines.
pub fn batch_boundaries_3d(cells: &[u64], radius: f64) -> Vec<(Vec3, u64)> {
    let mut result = Vec::new();
    for &cell in cells {
        if let Ok(boundary) = a5::cell_to_boundary(cell, None) {
            for ll in &boundary {
                result.push((coord::lonlat_to_vec3(ll, radius), cell));
            }
        }
    }
    result
}

/// Compute the boundary of a cell relative to an origin cell.
///
/// All positions are in the local coordinate frame of the origin cell.
/// This is useful for rendering cells near the floating origin without
/// precision issues.
pub fn cell_boundary_relative(cell: u64, origin: u64, radius: f64) -> Option<Vec<Vec3>> {
    let origin_ll = a5::cell_to_lonlat(origin).ok()?;
    let origin_pos = coord::lonlat_to_dvec3(&origin_ll, radius);
    let boundary = a5::cell_to_boundary(cell, None).ok()?;
    Some(
        boundary
            .iter()
            .map(|ll| (coord::lonlat_to_dvec3(ll, radius) - origin_pos).as_vec3())
            .collect(),
    )
}

/// Build a single `LineList` [`Mesh`] containing the boundary edges of every
/// supplied cell, in world-space coordinates on a sphere of `radius`.
///
/// The returned mesh has only a `POSITION` attribute and a `U32` index
/// buffer; pair it with an unlit material to render an A5 grid wireframe in
/// one draw call.
///
/// ```rust,ignore
/// use bevy_a5::prelude::*;
/// use bevy_a5::geometry::build_grid_line_mesh;
///
/// let cells = bevy_a5::query::res0_cells();
/// let mesh = build_grid_line_mesh(&cells, 100.0);
/// commands.spawn((
///     Mesh3d(meshes.add(mesh)),
///     MeshMaterial3d(materials.add(StandardMaterial {
///         base_color: Color::WHITE,
///         unlit: true,
///         ..default()
///     })),
/// ));
/// ```
pub fn build_grid_line_mesh(cells: &[GeoCell], radius: f64) -> Mesh {
    let mut positions: Vec<[f32; 3]> = Vec::new();
    let mut indices: Vec<u32> = Vec::new();
    let mut offset: u32 = 0;

    for cell in cells {
        let Some(verts) = cell_boundary_3d(cell.raw(), radius) else {
            continue;
        };
        if verts.is_empty() {
            continue;
        }
        for v in &verts {
            positions.push([v.x, v.y, v.z]);
        }
        let n = verts.len() as u32;
        for i in 0..n {
            indices.push(offset + i);
            indices.push(offset + (i + 1) % n);
        }
        offset += n;
    }

    let mut mesh = Mesh::new(
        PrimitiveTopology::LineList,
        RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
    );
    mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
    mesh.insert_indices(Indices::U32(indices));
    mesh
}

/// Get the boundary with interpolated segments for smoother rendering on large cells.
///
/// `segments` controls how many points are interpolated along each edge.
/// Higher values produce smoother curves but more vertices.
pub fn cell_boundary_smooth(cell: u64, radius: f64, segments: i32) -> Option<Vec<Vec3>> {
    let boundary = a5::cell_to_boundary(
        cell,
        Some(a5::core::cell::CellToBoundaryOptions {
            closed_ring: true,
            segments: Some(segments),
        }),
    )
    .ok()?;
    Some(
        boundary
            .iter()
            .map(|ll| coord::lonlat_to_vec3(ll, radius))
            .collect(),
    )
}

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

    #[test]
    fn hex_has_vertices() {
        let cell = a5::lonlat_to_cell(a5::LonLat::new(2.3522, 48.8566), 5).unwrap();
        let verts = cell_boundary_3d(cell, 6_371_000.0).unwrap();
        // Cells should have at least 5 vertices (pentagon) or 6 (hexagon)
        assert!(verts.len() >= 5);
    }

    #[test]
    fn edges_match_vertices() {
        let cell = a5::lonlat_to_cell(a5::LonLat::new(2.3522, 48.8566), 5).unwrap();
        let verts = cell_boundary_3d(cell, 6_371_000.0).unwrap();
        let edges = cell_edges_3d(cell, 6_371_000.0).unwrap();
        assert_eq!(verts.len(), edges.len());
    }

    #[test]
    fn triangle_fan_valid() {
        let cell = a5::lonlat_to_cell(a5::LonLat::new(2.3522, 48.8566), 5).unwrap();
        let (verts, indices) = cell_triangle_fan(cell, 6_371_000.0).unwrap();
        assert!(!verts.is_empty());
        assert!(!indices.is_empty());
        assert_eq!(indices.len() % 3, 0);
    }
}