hexx 0.22.0

Hexagonal utilities
Documentation
use std::ops::DerefMut;

use bevy::{
    asset::RenderAssetUsages,
    ecs::system::RunSystemOnce,
    mesh::{Indices, PrimitiveTopology},
    prelude::*,
};
use bevy_egui::{EguiContext, EguiPlugin, egui};
use bevy_inspector_egui::bevy_inspector;
use hexx::{shapes, *};

/// World size of the hexagons (outer radius)
const HEX_SIZE: Vec2 = Vec2::splat(13.0);

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                resolution: (1_000, 1_000).into(),
                ..default()
            }),
            ..default()
        }))
        .add_plugins(EguiPlugin::default())
        .add_plugins(bevy_inspector_egui::DefaultInspectorConfigPlugin)
        .add_systems(Startup, (setup, generate).chain())
        .add_systems(Update, show_ui)
        .run();
}

#[derive(Resource)]
struct HexMap {
    layout: HexLayout,
    entity: Entity,
    mat: Handle<ColorMaterial>,
}

#[derive(Resource)]
enum Shape {
    Hexagon(shapes::Hexagon),
    Rombus(shapes::Rombus),
    Triangle(shapes::Triangle),
    FlatRectangle(shapes::FlatRectangle),
    PointyRectangle(shapes::PointyRectangle),
    Parallelogram(shapes::Parallelogram),
}

impl Shape {
    fn all_values() -> [Self; 6] {
        [
            Self::Hexagon(Default::default()),
            Self::Rombus(Default::default()),
            Self::Triangle(Default::default()),
            Self::FlatRectangle(Default::default()),
            Self::PointyRectangle(Default::default()),
            Self::Parallelogram(Default::default()),
        ]
    }
    fn label(&self) -> &'static str {
        match self {
            Shape::Hexagon(_) => "Hexagon",
            Shape::Rombus(_) => "Rombus",
            Shape::Triangle(_) => "Triangle",
            Shape::FlatRectangle(_) => "FlatRectangle",
            Shape::PointyRectangle(_) => "PointyRectangle",
            Shape::Parallelogram(_) => "Parallelogram",
        }
    }
    fn coords(&self) -> Vec<Hex> {
        match self {
            Self::Hexagon(v) => v.coords().collect(),
            Self::Rombus(v) => v.coords().collect(),
            Self::Triangle(v) => v.coords().collect(),
            Self::FlatRectangle(v) => v.coords().collect(),
            Self::PointyRectangle(v) => v.coords().collect(),
            Self::Parallelogram(v) => v.coords().collect(),
        }
    }
}

pub fn setup(mut commands: Commands, mut mats: ResMut<Assets<ColorMaterial>>) {
    commands.spawn(Camera2d);
    let layout = HexLayout {
        scale: HEX_SIZE,
        ..default()
    };
    let mat = mats.add(Color::WHITE);
    let entity = commands
        .spawn((Transform::default(), Visibility::default()))
        .id();
    commands.insert_resource(HexMap {
        layout,
        mat,
        entity,
    });
    commands.insert_resource(Shape::Hexagon(Default::default()));
}

fn show_ui(world: &mut World) {
    let mut regenerate = false;

    let Ok(egui_context) = world.query::<&mut EguiContext>().single(world) else {
        return;
    };
    let mut egui_context = egui_context.clone();
    egui::Window::new("Options").show(egui_context.get_mut(), |ui| {
        world.resource_scope(|world, mut map: Mut<HexMap>| {
            ui.heading("Layout");
            ui.horizontal(|ui| {
                ui.label("Orientation");
                bevy_inspector::ui_for_value(&mut map.layout.orientation, ui, world);
            });
            ui.horizontal(|ui| {
                ui.label("scale");
                bevy_inspector::ui_for_value(&mut map.layout.scale, ui, world);
            });
        });

        ui.separator();

        world.resource_scope(|world, mut shape: Mut<Shape>| {
            ui.horizontal(|ui| {
                ui.heading("Shape");
                egui::ComboBox::from_id_salt("Shape")
                    .selected_text(shape.label())
                    .show_ui(ui, |ui| {
                        for option in Shape::all_values() {
                            if ui.selectable_label(false, option.label()).clicked() {
                                *shape = option;
                            };
                        }
                    });
            });
            match shape.deref_mut() {
                Shape::Hexagon(v) => bevy_inspector::ui_for_value(v, ui, world),
                Shape::Rombus(v) => bevy_inspector::ui_for_value(v, ui, world),
                Shape::Triangle(v) => bevy_inspector::ui_for_value(v, ui, world),
                Shape::FlatRectangle(v) => bevy_inspector::ui_for_value(v, ui, world),
                Shape::PointyRectangle(v) => bevy_inspector::ui_for_value(v, ui, world),
                Shape::Parallelogram(v) => bevy_inspector::ui_for_value(v, ui, world),
            };

            ui.add_space(10.0);
            ui.vertical_centered_justified(|ui| {
                regenerate = ui.button("Generate").clicked();
            });
        });
    });
    if regenerate {
        world.run_system_once(generate).unwrap();
    }
}

fn generate(
    mut commands: Commands,
    map: Res<HexMap>,
    shape: Res<Shape>,
    mut meshes: ResMut<Assets<Mesh>>,
) {
    commands.entity(map.entity).despawn_related::<Children>();
    let mesh = meshes.add(hexagonal_plane(&map.layout));
    for coord in shape.coords() {
        let pos = map.layout.hex_to_world_pos(coord);
        commands.spawn((
            Mesh2d(mesh.clone()),
            MeshMaterial2d(map.mat.clone()),
            Transform::from_xyz(pos.x, pos.y, 0.0),
            ChildOf(map.entity),
            children![(
                Text2d(format!("{},{}", coord.x, coord.y)),
                TextColor(Color::BLACK),
                TextFont {
                    font_size: 7.0,
                    ..default()
                },
                Transform::from_xyz(0.0, 0.0, 10.0),
            )],
        ));
    }
}

/// Compute a bevy mesh from the layout
fn hexagonal_plane(hex_layout: &HexLayout) -> Mesh {
    let mesh_info = PlaneMeshBuilder::new(hex_layout)
        .facing(Vec3::Z)
        .with_scale(Vec3::splat(0.98))
        .center_aligned()
        .build();
    Mesh::new(
        PrimitiveTopology::TriangleList,
        RenderAssetUsages::RENDER_WORLD,
    )
    .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, mesh_info.vertices)
    .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_info.normals)
    .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, mesh_info.uvs)
    .with_inserted_indices(Indices::U16(mesh_info.indices))
}