415 lines
14 KiB
Rust
415 lines
14 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, Path};
|
|
use coordinates::{hex_map::parse_data, AxialAddr};
|
|
use gio::resources_lookup_data;
|
|
use glib::{subclass::InitializingObject, Object};
|
|
use gtk::{
|
|
gdk_pixbuf::Pixbuf, gio, prelude::*, subclass::prelude::*, Application, CompositeTemplate,
|
|
DrawingArea, Label,
|
|
};
|
|
use image::{io::Reader as ImageReader, DynamicImage};
|
|
use std::{cell::RefCell, io::Cursor, rc::Rc};
|
|
|
|
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.);
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum Terrain {
|
|
Empty,
|
|
Mountain,
|
|
Grasslands,
|
|
ShallowWater,
|
|
DeepWater,
|
|
Badlands,
|
|
Desert,
|
|
Swamp,
|
|
}
|
|
|
|
impl Default for Terrain {
|
|
fn default() -> Self {
|
|
Self::Empty
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Terrain {
|
|
fn from(s: &str) -> Self {
|
|
match s {
|
|
"m" => Self::Mountain,
|
|
"g" => Self::Grasslands,
|
|
"sw" => Self::ShallowWater,
|
|
"dw" => Self::DeepWater,
|
|
"b" => Self::Badlands,
|
|
"d" => Self::Desert,
|
|
"s" => Self::Swamp,
|
|
_ => Self::Empty,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<String> for Terrain {
|
|
fn from(s: String) -> Self {
|
|
Self::from(s.as_ref())
|
|
}
|
|
}
|
|
|
|
struct Tile {
|
|
terrain: Terrain,
|
|
image: Pixbuf,
|
|
}
|
|
|
|
impl Tile {
|
|
fn new(source: &DynamicImage, terrain: Terrain) -> Tile {
|
|
let image = match terrain {
|
|
Terrain::DeepWater => pixbuf_from_image_tile(source.clone().crop(0, 0, 100, 88)),
|
|
Terrain::ShallowWater => pixbuf_from_image_tile(source.clone().crop(100, 0, 100, 88)),
|
|
Terrain::Grasslands => pixbuf_from_image_tile(source.clone().crop(200, 0, 100, 88)),
|
|
Terrain::Desert => pixbuf_from_image_tile(source.clone().crop(300, 0, 100, 88)),
|
|
Terrain::Mountain => pixbuf_from_image_tile(source.clone().crop(0, 88, 100, 88)),
|
|
Terrain::Badlands => pixbuf_from_image_tile(source.clone().crop(100, 88, 100, 88)),
|
|
Terrain::Swamp => pixbuf_from_image_tile(source.clone().crop(0, 176, 100, 88)),
|
|
Terrain::Empty => pixbuf_from_image_tile(source.clone().crop(300, 176, 100, 88)),
|
|
};
|
|
|
|
Tile { terrain, image }
|
|
}
|
|
|
|
fn render_on_context(&self, context: &Context, translate_x: f64, translate_y: f64) {
|
|
context.save().unwrap();
|
|
context.append_path(&hexagon_path(context, translate_x, translate_y, 100., 88.));
|
|
context.clip();
|
|
context.set_source_pixbuf(&self.image, translate_x, translate_y);
|
|
context.paint().expect("paint should succeed");
|
|
context.restore().unwrap();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct HexGridWindowPrivate {
|
|
layout: gtk::Box,
|
|
|
|
drawing_area: DrawingArea,
|
|
hex_address: Label,
|
|
canvas_address: Label,
|
|
|
|
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 {
|
|
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_row = gtk::Box::builder()
|
|
.hexpand(true)
|
|
.spacing(8)
|
|
.homogeneous(true)
|
|
.build();
|
|
|
|
canvas_address_row.append(>k::Label::builder().label("Canvas Address").build());
|
|
|
|
let canvas_address = gtk::Label::builder()
|
|
.label("-----")
|
|
.margin_start(4)
|
|
.margin_end(4)
|
|
.margin_top(4)
|
|
.margin_bottom(4)
|
|
.build();
|
|
|
|
canvas_address_row.append(&canvas_address);
|
|
|
|
let hex_address_row = gtk::Box::builder()
|
|
.hexpand(true)
|
|
.spacing(8)
|
|
.homogeneous(true)
|
|
.build();
|
|
|
|
hex_address_row.append(>k::Label::builder().label("Hex Address").build());
|
|
|
|
let hex_address = gtk::Label::builder()
|
|
.label("-----")
|
|
.margin_start(4)
|
|
.margin_end(4)
|
|
.margin_top(4)
|
|
.margin_bottom(4)
|
|
.build();
|
|
|
|
hex_address_row.append(&hex_address);
|
|
|
|
sidebar.append(&canvas_address_row);
|
|
sidebar.append(&hex_address_row);
|
|
|
|
layout.append(&drawing_area);
|
|
layout.append(&sidebar);
|
|
|
|
layout.show();
|
|
|
|
Self {
|
|
drawing_area,
|
|
hex_address,
|
|
canvas_address,
|
|
current_coordinate,
|
|
layout,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for HexGridWindowPrivate {
|
|
fn constructed(&self) {
|
|
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::<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 deep_water = Tile::new(&image, Terrain::DeepWater);
|
|
let shallow_water = Tile::new(&image, Terrain::ShallowWater);
|
|
let grasslands = Tile::new(&image, Terrain::Grasslands);
|
|
let desert = Tile::new(&image, Terrain::Desert);
|
|
let mountain = Tile::new(&image, Terrain::Mountain);
|
|
let badlands = Tile::new(&image, Terrain::Badlands);
|
|
let swamp = Tile::new(&image, Terrain::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_label(&format!("{:.0} {:.0}", x, y));
|
|
|
|
if coordinate.distance(&AxialAddr::origin()) > MAP_RADIUS {
|
|
hex_address.set_label(&format!("-----"));
|
|
*c.borrow_mut() = None;
|
|
} else {
|
|
hex_address.set_label(&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() {
|
|
Terrain::Mountain => &mountain,
|
|
Terrain::Grasslands => &grasslands,
|
|
Terrain::ShallowWater => &shallow_water,
|
|
Terrain::DeepWater => &deep_water,
|
|
Terrain::Badlands => &badlands,
|
|
Terrain::Desert => &desert,
|
|
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)> = 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 hexagon_path(
|
|
context: &Context,
|
|
translate_x: f64,
|
|
translate_y: f64,
|
|
width: f64,
|
|
height: f64,
|
|
) -> Path {
|
|
context.new_path();
|
|
let points = hexagon(width, height);
|
|
context.move_to(translate_x + points[0].0, translate_y + points[0].1);
|
|
context.line_to(translate_x + points[1].0, translate_y + points[1].1);
|
|
context.line_to(translate_x + points[2].0, translate_y + points[2].1);
|
|
context.line_to(translate_x + points[3].0, translate_y + points[3].1);
|
|
context.line_to(translate_x + points[4].0, translate_y + points[4].1);
|
|
context.line_to(translate_x + points[5].0, translate_y + points[5].1);
|
|
context.copy_path().expect("to successfully copy a path")
|
|
}
|
|
|
|
fn hexagon(width: f64, height: f64) -> Vec<(f64, f64)> {
|
|
let center_x = width / 2.;
|
|
let center_y = height / 2.;
|
|
let radius = width / 2.;
|
|
|
|
vec![
|
|
(center_x + radius, center_y),
|
|
(
|
|
center_x + radius / 2.,
|
|
center_y + (3. as f64).sqrt() * radius / 2.,
|
|
),
|
|
(
|
|
center_x - radius / 2.,
|
|
center_y + (3. as f64).sqrt() * radius / 2.,
|
|
),
|
|
(center_x - radius, center_y),
|
|
(
|
|
center_x - radius / 2.,
|
|
center_y - (3. as f64).sqrt() * radius / 2.,
|
|
),
|
|
(
|
|
center_x + radius / 2.,
|
|
center_y - (3. as f64).sqrt() * radius / 2.,
|
|
),
|
|
]
|
|
}
|
|
|
|
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>()) }
|
|
}
|
|
|
|
fn pixbuf_from_image_tile(image: image::DynamicImage) -> Pixbuf {
|
|
Pixbuf::from_bytes(
|
|
&glib::Bytes::from(image.as_bytes()),
|
|
gtk::gdk_pixbuf::Colorspace::Rgb,
|
|
false,
|
|
8,
|
|
image.width() as i32,
|
|
image.height() as i32,
|
|
image.to_rgb8().sample_layout().height_stride as i32,
|
|
)
|
|
}
|