bevy_a5 0.1.2

A Bevy plugin providing A5 geospatial pentagonal cells for floating origin use and spatial queries
Documentation
//! Example: visualize the entire A5 pentagonal tiling on a planet.
//!
//! Builds a single `LineList` mesh from every A5 cell at the chosen resolution
//! (using [`cell_boundary_3d`] from the `bevy_a5` plugin) and projects them
//! onto a sphere. Fly around with WASD + mouse look.
//!
//! Run with:
//!   cargo run --example planet_grid

use bevy::{
    input::mouse::AccumulatedMouseMotion,
    prelude::*,
    window::{CursorGrabMode, CursorOptions, PrimaryWindow},
};
use bevy_a5::geometry::build_grid_line_mesh;
use bevy_a5::prelude::*;
use bevy_a5::WORLD_CELL;

/// Planet radius in world units (scaled down so it fits on screen).
const PLANET_RADIUS: f64 = 100.0;
/// A5 resolution for the grid. Cell counts: r3=192, r4=768, r5=3072, r6=12288.
const RESOLUTION: i32 = 5;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(BevyA5Plugin)
        .insert_resource(
            PlanetSettings::earth().with_radius(PLANET_RADIUS),
        )
        .add_systems(Startup, setup)
        .add_systems(Update, (camera_controller, cursor_grab))
        .run();
}

#[derive(Component)]
struct CameraController {
    speed: f32,
    sensitivity: f32,
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut cursor_query: Query<&mut CursorOptions, With<PrimaryWindow>>,
) {
    // Planet sphere (slightly smaller than the grid radius so the lines sit on top).
    commands.spawn((
        Mesh3d(meshes.add(Sphere::new(PLANET_RADIUS as f32 * 0.99))),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::linear_rgb(0.05, 0.10, 0.20),
            ..default()
        })),
        Transform::default(),
    ));

    // Fly camera.
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 0.0, PLANET_RADIUS as f32 * 2.5)
            .looking_at(Vec3::ZERO, Vec3::Y),
        CameraController {
            speed: 80.0,
            sensitivity: 0.003,
        },
    ));

    commands.spawn((
        DirectionalLight {
            illuminance: 18_000.0,
            shadows_enabled: false,
            ..default()
        },
        Transform::from_xyz(300.0, 500.0, 300.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    // Build one LineList mesh containing every cell boundary on the planet.
    let world = GeoCell::new(WORLD_CELL);
    let cells = world.children(RESOLUTION).unwrap_or_default();
    let grid = build_grid_line_mesh(&cells, PLANET_RADIUS);

    commands.spawn((
        Mesh3d(meshes.add(grid)),
        MeshMaterial3d(materials.add(StandardMaterial {
            base_color: Color::srgb(0.0, 1.0, 0.75),
            emissive: LinearRgba::new(0.0, 1.8, 1.2, 1.0),
            unlit: true,
            ..default()
        })),
    ));

    info!(
        "A5 planet grid ready — {} cells at resolution {} on a sphere of radius {}",
        cells.len(),
        RESOLUTION,
        PLANET_RADIUS,
    );
    info!("Controls: WASD to fly, Q/E down/up, Shift to boost, mouse to look (Esc releases cursor)");

    if let Ok(mut cursor_opts) = cursor_query.single_mut() {
        cursor_opts.grab_mode = CursorGrabMode::Locked;
        cursor_opts.visible = false;
    }
}

fn camera_controller(
    time: Res<Time>,
    keyboard: Res<ButtonInput<KeyCode>>,
    mouse: Res<AccumulatedMouseMotion>,
    mut query: Query<(&mut Transform, &CameraController), With<Camera3d>>,
) {
    let dt = time.delta_secs();
    let Ok((mut transform, controller)) = query.single_mut() else {
        return;
    };

    let forward = Vec3::from(transform.forward());
    let right = Vec3::from(transform.right());

    let mut velocity = Vec3::ZERO;
    if keyboard.pressed(KeyCode::KeyW) {
        velocity += forward;
    }
    if keyboard.pressed(KeyCode::KeyS) {
        velocity -= forward;
    }
    if keyboard.pressed(KeyCode::KeyA) {
        velocity -= right;
    }
    if keyboard.pressed(KeyCode::KeyD) {
        velocity += right;
    }
    if keyboard.pressed(KeyCode::KeyQ) {
        velocity.y -= 1.0;
    }
    if keyboard.pressed(KeyCode::KeyE) {
        velocity.y += 1.0;
    }

    if velocity != Vec3::ZERO {
        velocity = velocity.normalize();
    }

    let boost = if keyboard.pressed(KeyCode::ShiftLeft) || keyboard.pressed(KeyCode::ShiftRight) {
        4.0
    } else {
        1.0
    };
    transform.translation += velocity * controller.speed * boost * dt;

    if mouse.delta != Vec2::ZERO {
        let yaw = -mouse.delta.x * controller.sensitivity;
        let pitch = -mouse.delta.y * controller.sensitivity;
        transform.rotate_y(yaw);
        transform.rotate_local_x(pitch);
    }
}

fn cursor_grab(
    mut cursor_query: Query<&mut CursorOptions, With<PrimaryWindow>>,
    mouse: Res<ButtonInput<MouseButton>>,
    key: Res<ButtonInput<KeyCode>>,
) {
    let Ok(mut cursor_opts) = cursor_query.single_mut() else {
        return;
    };

    if mouse.just_pressed(MouseButton::Left) {
        cursor_opts.grab_mode = CursorGrabMode::Locked;
        cursor_opts.visible = false;
    }
    if key.just_pressed(KeyCode::Escape) {
        cursor_opts.grab_mode = CursorGrabMode::None;
        cursor_opts.visible = true;
    }
}