bevy_a5 0.1.2

A Bevy plugin providing A5 geospatial pentagonal cells for floating origin use and spatial queries
Documentation
//! Cell orientation computation.
//!
//! Each A5 cell has a deterministic orientation derived from its position on the
//! planet surface. The A5 system is based on a dodecahedron with 12 origin faces,
//! each with a known quaternion rotation. This module computes orientations for
//! cells at any resolution.
//!
//! Two conventions are available:
//!
//! - [`cell_vertex_orientation`] — **the canonical convention used by
//!   `propagate_geo_transforms`.** The cell centre is the local origin, +Y is
//!   radial-up, and -Z (Bevy "forward") points from the cell centre to the
//!   cell's first boundary vertex. This makes the local frame line up with the
//!   pentagon's geometry: a unit vector in -Z lands on the cell's first
//!   vertex direction.
//! - [`cell_orientation`] — the geographic convention. -Z points toward
//!   geographic north (higher latitude). Useful when you want every cell to
//!   share a global compass alignment.

use bevy_math::{DQuat, Quat};

use crate::coord;

/// Compute the **geographic** orientation quaternion for a cell.
///
/// The returned quaternion transforms from a local frame where:
/// - +Y is "up" (radial, away from planet center)
/// - -Z is "forward" (toward geographic north, projected onto the tangent plane)
/// - +X is "right" (toward geographic east)
///
/// This matches Bevy's default camera/entity orientation conventions.
///
/// Note: [`propagate_geo_transforms`](crate::systems::propagate_geo_transforms)
/// uses [`cell_vertex_orientation`] instead. Use this function if you want a
/// consistent geographic frame independent of which cell you're in.
///
/// Returns `None` if the cell index is invalid.
pub fn cell_orientation(cell: u64) -> Option<Quat> {
    let ll = a5::cell_to_lonlat(cell).ok()?;
    Some(orientation_at(&ll))
}

/// Compute an orientation quaternion for a specific longitude/latitude on the planet.
///
/// See [`cell_orientation`] for the coordinate frame convention.
pub fn orientation_at(ll: &a5::LonLat) -> Quat {
    let (east, up, north) = coord::tangent_frame(ll);

    // Bevy: +X right, +Y up, -Z forward
    let mat = bevy_math::DMat3::from_cols(east, up, -north);
    DQuat::from_mat3(&mat).as_quat().normalize()
}

/// Compute the **vertex-aligned** orientation quaternion for a cell.
///
/// The returned quaternion transforms from a local frame where:
/// - the cell centre is the local origin (0, 0, 0)
/// - +Y is radial-up (away from planet centre)
/// - -Z (Bevy "forward") points from the cell centre to the cell's first
///   boundary vertex, projected onto the tangent plane
/// - +X is "right" (cross of forward and up)
///
/// This is the canonical convention used by
/// [`propagate_geo_transforms`](crate::systems::propagate_geo_transforms): the
/// local frame is anchored to the cell's pentagonal geometry. A child entity
/// at `Transform::from_xyz(0.0, 0.0, -boundary_distance)` sits exactly on the
/// cell's first boundary vertex.
///
/// Note that adjacent cells have different vertex-aligned frames, so a Vec3
/// in one cell's frame doesn't correspond to the same direction in a
/// neighbour's frame — that's the price of the cell-local convention.
///
/// Returns `None` if the cell index is invalid.
pub fn cell_vertex_orientation(cell: u64, radius: f64) -> Option<Quat> {
    let ll = a5::cell_to_lonlat(cell).ok()?;
    let center_pos = coord::lonlat_to_dvec3(&ll, radius);
    let up = center_pos.normalize();

    // Get the first boundary vertex to define "forward"
    let boundary = a5::cell_to_boundary(cell, None).ok()?;
    if boundary.is_empty() {
        return cell_orientation(cell);
    }

    let first_vert_pos = coord::lonlat_to_dvec3(&boundary[0], radius);
    let to_vert = (first_vert_pos - center_pos).normalize();

    // Project onto tangent plane
    let forward_on_tangent = (to_vert - up * to_vert.dot(up)).normalize();
    let right = forward_on_tangent.cross(up).normalize();

    // Bevy: +X right, +Y up, -Z forward
    let mat = bevy_math::DMat3::from_cols(right, up, -forward_on_tangent);
    Some(DQuat::from_mat3(&mat).as_quat().normalize())
}