monorepo/hex-grid/src/main.rs

283 lines
10 KiB
Rust

/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of the Luminescent Dreams Tools.
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/
use cairo::Context;
use coordinates::{hex_map::parse_data, AxialAddr};
use gio::resources_lookup_data;
use glib::{subclass::InitializingObject, Object};
use gtk::{gio, prelude::*, subclass::prelude::*, Application, DrawingArea};
use image::io::Reader as ImageReader;
use std::{cell::RefCell, io::Cursor, rc::Rc};
mod labeled_field;
mod palette_entry;
mod tile;
mod utilities;
const APP_ID: &'static str = "com.luminescent-dreams.hex-grid";
const HEX_RADIUS: f64 = 50.;
const MAP_RADIUS: usize = 3;
const DRAWING_ORIGIN: (f64, f64) = (1024. / 2., 768. / 2.);
fn main() {
gio::resources_register_include!("com.luminescent-dreams.hex-grid.gresource")
.expect("Failed to register resources");
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(|app| {
let window = HexGridWindow::new(app);
window.present();
});
app.run();
}
pub struct HexGridWindowPrivate {
layout: gtk::Box,
palette: gtk::Box,
drawing_area: DrawingArea,
hex_address: labeled_field::LabeledField,
canvas_address: labeled_field::LabeledField,
current_coordinate: Rc<RefCell<Option<AxialAddr>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for HexGridWindowPrivate {
const NAME: &'static str = "HexGridWindow";
type Type = HexGridWindow;
type ParentType = gtk::ApplicationWindow;
fn new() -> Self {
println!("hexGridWindowPrivate::new()");
let current_coordinate = Rc::new(RefCell::new(None));
let drawing_area = DrawingArea::builder()
.width_request(1024)
.height_request(768)
.margin_start(8)
.margin_end(8)
.margin_top(8)
.margin_bottom(8)
.hexpand(true)
.build();
let layout = gtk::Box::builder()
.homogeneous(false)
.spacing(8)
.can_focus(false)
.visible(true)
.build();
let sidebar = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.width_request(100)
.visible(true)
.can_focus(false)
.margin_start(4)
.margin_end(4)
.margin_top(4)
.margin_bottom(4)
.spacing(8)
.build();
let canvas_address = labeled_field::LabeledField::new("Canvas Address", "-----");
let hex_address = labeled_field::LabeledField::new("Hex Address", "-----");
let palette = gtk::Box::builder()
.spacing(8)
.orientation(gtk::Orientation::Vertical)
.hexpand(true)
.build();
sidebar.append(&canvas_address);
sidebar.append(&hex_address);
sidebar.append(&palette);
layout.append(&drawing_area);
layout.append(&sidebar);
layout.show();
Self {
drawing_area,
hex_address,
canvas_address,
current_coordinate,
layout,
palette,
}
}
}
impl ObjectImpl for HexGridWindowPrivate {
fn constructed(&self) {
println!("HexGridWindowPrivate::constructed()");
self.parent_constructed();
let map_text_resource = resources_lookup_data(
"/com/luminescent-dreams/hex-grid/map.txt",
gio::ResourceLookupFlags::NONE,
)
.expect("map should be in the bundle")
.to_vec();
let hex_map =
parse_data::<tile::Terrain>(String::from_utf8(map_text_resource).unwrap().lines());
let terrain_data = resources_lookup_data(
"/com/luminescent-dreams/hex-grid/terrain.ppm",
gio::ResourceLookupFlags::NONE,
)
.unwrap();
let reader = ImageReader::new(Cursor::new(terrain_data))
.with_guessed_format()
.unwrap();
let image = reader.decode().unwrap();
let badlands = tile::Tile::new(&image, tile::Terrain::Badlands);
let deep_water = tile::Tile::new(&image, tile::Terrain::DeepWater);
let desert = tile::Tile::new(&image, tile::Terrain::Desert);
let grasslands = tile::Tile::new(&image, tile::Terrain::Grasslands);
let mountain = tile::Tile::new(&image, tile::Terrain::Mountain);
let shallow_water = tile::Tile::new(&image, tile::Terrain::ShallowWater);
let swamp = tile::Tile::new(&image, tile::Terrain::Swamp);
self.palette
.append(&palette_entry::PaletteEntry::new(&badlands));
self.palette
.append(&palette_entry::PaletteEntry::new(&deep_water));
self.palette
.append(&palette_entry::PaletteEntry::new(&desert));
self.palette
.append(&palette_entry::PaletteEntry::new(&grasslands));
self.palette
.append(&palette_entry::PaletteEntry::new(&mountain));
self.palette
.append(&palette_entry::PaletteEntry::new(&shallow_water));
self.palette
.append(&palette_entry::PaletteEntry::new(&swamp));
let motion_controller = gtk::EventControllerMotion::new();
{
let canvas_address = self.canvas_address.clone();
let hex_address = self.hex_address.clone();
let c = self.current_coordinate.clone();
motion_controller.connect_motion(move |_, x, y| {
let norm_x = x - DRAWING_ORIGIN.0;
let norm_y = y - DRAWING_ORIGIN.1;
let q = (2. / 3. * norm_x) / HEX_RADIUS;
let r = (-1. / 3. * norm_x + (3. as f64).sqrt() / 3. * norm_y) / HEX_RADIUS;
let (q, r) = axial_round(q, r);
let coordinate = AxialAddr::new(q, r);
canvas_address.set_value(&format!("{:.0} {:.0}", x, y));
if coordinate.distance(&AxialAddr::origin()) > MAP_RADIUS {
hex_address.set_value(&format!("-----"));
*c.borrow_mut() = None;
} else {
hex_address.set_value(&format!("{:.0} {:.0}", coordinate.q(), coordinate.r()));
*c.borrow_mut() = Some(coordinate);
}
});
}
{
self.drawing_area.set_draw_func(move |_, context, _, _| {
context.set_source_rgb(0., 0., 0.);
let _ = context.paint();
context.set_line_width(2.);
for coordinate in vec![AxialAddr::origin()]
.into_iter()
.chain(AxialAddr::origin().addresses(MAP_RADIUS))
{
let center_x =
DRAWING_ORIGIN.0 + HEX_RADIUS * (3. / 2. * (coordinate.q() as f64));
let center_y = DRAWING_ORIGIN.1
+ HEX_RADIUS
* ((3. as f64).sqrt() / 2. * (coordinate.q() as f64)
+ (3. as f64).sqrt() * (coordinate.r() as f64));
let translate_x = center_x - HEX_RADIUS;
let translate_y = center_y - (3. as f64).sqrt() * HEX_RADIUS / 2.;
let tile = match hex_map.get(&coordinate).unwrap() {
tile::Terrain::Mountain => &mountain,
tile::Terrain::Grasslands => &grasslands,
tile::Terrain::ShallowWater => &shallow_water,
tile::Terrain::DeepWater => &deep_water,
tile::Terrain::Badlands => &badlands,
tile::Terrain::Desert => &desert,
tile::Terrain::Swamp => &swamp,
_ => panic!("unhandled terrain type"),
};
tile.render_on_context(context, translate_x, translate_y);
}
});
}
self.drawing_area.add_controller(&motion_controller);
}
}
impl WidgetImpl for HexGridWindowPrivate {}
impl WindowImpl for HexGridWindowPrivate {}
impl ApplicationWindowImpl for HexGridWindowPrivate {}
glib::wrapper! {
pub struct HexGridWindow(ObjectSubclass<HexGridWindowPrivate>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl HexGridWindow {
pub fn new(app: &Application) -> Self {
let window: Self = Object::builder().property("application", app).build();
window.set_child(Some(&window.imp().layout));
window
}
}
fn draw_hexagon(context: &Context, center_x: f64, center_y: f64, radius: f64) {
let ul_x = center_x - radius;
let ul_y = center_y - (3. as f64).sqrt() * radius / 2.;
let points: Vec<(f64, f64)> = utilities::hexagon(radius * 2., (3. as f64).sqrt() * radius);
context.new_path();
context.move_to(ul_x + points[0].0, ul_y + points[0].1);
context.line_to(ul_x + points[1].0, ul_y + points[1].1);
context.line_to(ul_x + points[2].0, ul_y + points[2].1);
context.line_to(ul_x + points[3].0, ul_y + points[3].1);
context.line_to(ul_x + points[4].0, ul_y + points[4].1);
context.line_to(ul_x + points[5].0, ul_y + points[5].1);
context.close_path();
}
fn axial_round(q_f64: f64, r_f64: f64) -> (i32, i32) {
let s_f64 = -q_f64 - r_f64;
let mut q = q_f64.round();
let mut r = r_f64.round();
let s = s_f64.round();
let q_diff = (q - q_f64).abs();
let r_diff = (r - r_f64).abs();
let s_diff = (s - s_f64).abs();
if q_diff > r_diff && q_diff > s_diff {
q = -r - s;
} else if r_diff > s_diff {
r = -q - s;
}
unsafe { (q.to_int_unchecked::<i32>(), r.to_int_unchecked::<i32>()) }
}