278 lines
7.0 KiB
Rust
278 lines
7.0 KiB
Rust
use serde::{ Serialize, Deserialize };
|
|
|
|
use fltk::*;
|
|
use fltk::prelude::*;
|
|
use fltk::enums::*;
|
|
use fltk::image::PngImage;
|
|
|
|
extern crate image;
|
|
use image::io::Reader as ImageReader;
|
|
use image::{ DynamicImage };
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use std::io::{ Write, Read, Cursor };
|
|
use std::fs::File;
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct Node {
|
|
pub x: f32,
|
|
pub y: f32,
|
|
|
|
pub name: String,
|
|
pub edges: HashSet<usize>,
|
|
}
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
struct Board {
|
|
nodes: HashMap<usize, Node>,
|
|
}
|
|
impl Board {
|
|
pub fn new() -> Self {
|
|
let nodes = HashMap::new();
|
|
Board { nodes }
|
|
}
|
|
|
|
pub fn add_node(&mut self, x: f32, y: f32) {
|
|
self.nodes.insert(self.next_id(), Node {
|
|
x,
|
|
y,
|
|
name: "Canada".to_string(),
|
|
edges: HashSet::new(),
|
|
});
|
|
}
|
|
|
|
pub fn remove_node(&mut self, id: usize) {
|
|
self.nodes.remove(&id);
|
|
for (_, node) in &mut self.nodes {
|
|
node.edges.remove(&id);
|
|
}
|
|
}
|
|
|
|
pub fn nearest_node(&self, x: f32, y: f32) -> Option<usize> {
|
|
let f = |n: &Node| dist_sq((n.x, n.y), (x, y));
|
|
let mut iter = self.nodes.iter();
|
|
if let Some((id, node)) = iter.next() {
|
|
let mut min = *id;
|
|
let mut min_val = f(node);
|
|
for (id, node) in iter {
|
|
let val = f(node);
|
|
if val < min_val {
|
|
min = *id;
|
|
min_val = val;
|
|
}
|
|
}
|
|
Some(min)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn next_id(&self) -> usize {
|
|
for i in 0 .. {
|
|
if !self.nodes.contains_key(&i) {
|
|
return i;
|
|
}
|
|
}
|
|
unreachable!();
|
|
}
|
|
}
|
|
|
|
struct AppState {
|
|
pub board: Board,
|
|
pub image_raw: Option<DynamicImage>,
|
|
pub image: Option<PngImage>,
|
|
}
|
|
|
|
impl AppState {
|
|
fn new() -> Self {
|
|
AppState {
|
|
board: Board::new(),
|
|
image_raw: None,
|
|
image: None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn menu_cb(_m: &mut impl MenuExt) {
|
|
todo!();
|
|
}
|
|
|
|
fn encode_png(image: &DynamicImage) -> Vec<u8> {
|
|
let mut cursor = Cursor::new(Vec::new());
|
|
image.write_to(&mut cursor, image::ImageOutputFormat::Png).unwrap();
|
|
cursor.into_inner()
|
|
}
|
|
|
|
fn decode_png(buf: &[u8]) -> DynamicImage {
|
|
let cursor = Cursor::new(buf);
|
|
let mut reader = ImageReader::new(cursor);
|
|
reader.set_format(image::ImageFormat::Png);
|
|
reader.decode().unwrap()
|
|
}
|
|
|
|
fn lerp(v0: f32, v1: f32, t: f32) -> f32 {
|
|
v0 + t * (v1 - v0)
|
|
}
|
|
|
|
fn inv_lerp(v0: f32, v1: f32, a: f32) -> f32 {
|
|
(a - v0) / (v1 - v0)
|
|
}
|
|
|
|
fn dist_sq((x0, y0): (f32, f32), (x1, y1): (f32, f32)) -> f32 {
|
|
(x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)
|
|
}
|
|
|
|
trait CoordTransformer {
|
|
fn to_coords(&self, x: i32, y: i32) -> (f32, f32);
|
|
fn from_coords(&self, x: f32, y: f32) -> (i32, i32);
|
|
}
|
|
|
|
impl CoordTransformer for frame::Frame {
|
|
fn to_coords(&self, x: i32, y: i32) -> (f32, f32) {
|
|
(
|
|
inv_lerp(self.x() as f32, (self.x() + self.w()) as f32, x as f32),
|
|
inv_lerp(self.y() as f32, (self.y() + self.h()) as f32, y as f32),
|
|
)
|
|
}
|
|
|
|
fn from_coords(&self, x: f32, y: f32) -> (i32, i32) {
|
|
(
|
|
lerp(self.x() as f32, (self.x() + self.w()) as f32, x) as i32,
|
|
lerp(self.y() as f32, (self.y() + self.h()) as f32, y) as i32,
|
|
)
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let app = app::App::default()
|
|
.with_scheme(app::Scheme::Gtk);
|
|
let mut win = window::Window::default()
|
|
.with_size(400, 300)
|
|
.with_label("Board Builder");
|
|
let mut flex = group::Flex::default()
|
|
.size_of_parent();
|
|
flex.set_type(group::FlexType::Column);
|
|
|
|
// State
|
|
let state = Rc::new(RefCell::new(AppState::new()));
|
|
|
|
// Menu
|
|
let mut menubar = menu::MenuBar::default();
|
|
let state_clone = Rc::clone(&state);
|
|
menubar.add("File/New" , Shortcut::None, menu::MenuFlag::Normal, move |_| {
|
|
state_clone.replace(AppState::new());
|
|
app::redraw();
|
|
});
|
|
let state_clone = Rc::clone(&state);
|
|
menubar.add("File/Open..." , Shortcut::None, menu::MenuFlag::Normal, move |_| {
|
|
let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseFile);
|
|
fc.show();
|
|
|
|
let file = File::open(fc.filename()).unwrap();
|
|
let mut ar = zip::ZipArchive::new(file).unwrap();
|
|
let mut state = AppState::new();
|
|
|
|
let json_file = ar.by_name("graph.json").unwrap();
|
|
state.board = serde_json::from_reader(json_file).unwrap();
|
|
if let Ok(mut image_file) = ar.by_name("image.png") {
|
|
let mut buf = Vec::new();
|
|
image_file.read_to_end(&mut buf).ok();
|
|
|
|
let image = decode_png(&buf);
|
|
state.image = Some(PngImage::from_data(&buf).unwrap());
|
|
state.image_raw = Some(image);
|
|
app::redraw();
|
|
}
|
|
state_clone.replace(state);
|
|
});
|
|
let state_clone = Rc::clone(&state);
|
|
menubar.add("File/Open Image..." , Shortcut::None, menu::MenuFlag::Normal, move |_| {
|
|
let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseFile);
|
|
fc.show();
|
|
let filename = fc.filename();
|
|
|
|
match ImageReader::open(filename).map(|i| i.decode()) {
|
|
Ok(Ok(image)) => {
|
|
let mut state = state_clone.borrow_mut();
|
|
let data = encode_png(&image);
|
|
state.image = Some(PngImage::from_data(&data).unwrap());
|
|
state.image_raw = Some(image);
|
|
app::redraw();
|
|
}
|
|
_ => dialog::alert_default("Error opening file"),
|
|
}
|
|
});
|
|
let state_clone = Rc::clone(&state);
|
|
menubar.add("File/Save As ..." , Shortcut::None, menu::MenuFlag::Normal, move |_| {
|
|
let mut fc = dialog::NativeFileChooser::new(dialog::NativeFileChooserType::BrowseSaveFile);
|
|
fc.show();
|
|
|
|
let file = File::create(fc.filename()).unwrap();
|
|
let mut ar = zip::ZipWriter::new(file);
|
|
let options = zip::write::FileOptions::default();
|
|
let state = state_clone.borrow();
|
|
|
|
ar.start_file("graph.json", options).ok();
|
|
ar.write(&serde_json::to_vec(&state.board).unwrap()).ok();
|
|
if let Some(image) = state.image_raw.as_ref() {
|
|
ar.start_file("image.png", options).ok();
|
|
let data = encode_png(image);
|
|
ar.write_all(&data).unwrap();
|
|
ar.flush().unwrap();
|
|
}
|
|
ar.finish().ok();
|
|
});
|
|
menubar.add("Edit/Edit Nodes" , Shortcut::None, menu::MenuFlag::Normal, menu_cb);
|
|
menubar.add("Edit/Edit Edges" , Shortcut::None, menu::MenuFlag::Normal, menu_cb);
|
|
flex.set_size(&menubar, 40);
|
|
|
|
// Canvas
|
|
let mut frame = frame::Frame::default();
|
|
let state_clone = Rc::clone(&state);
|
|
frame.draw(move |f| {
|
|
use draw::*;
|
|
let mut state = state_clone.borrow_mut();
|
|
let image = &mut state.image;
|
|
if let Some(image) = image.as_mut() {
|
|
image.scale(f.w(), f.h(), false, true);
|
|
image.draw(f.x(), f.y(), f.w(), f.h());
|
|
}
|
|
let board = &state.board;
|
|
for (_, node) in &board.nodes {
|
|
let (x, y) = f.from_coords(node.x, node.y);
|
|
draw_text(&node.name, x, y);
|
|
}
|
|
});
|
|
let state_clone = Rc::clone(&state);
|
|
frame.handle(move |f, e| {
|
|
match e {
|
|
Event::Push => {
|
|
let mut state = state_clone.borrow_mut();
|
|
let (pos_x, pos_y) = f.to_coords(app::event_x(), app::event_y());
|
|
if app::event_button() == 1 {
|
|
state.board.add_node(pos_x, pos_y);
|
|
} else if app::event_button() == 3 {
|
|
let id = state.board.nearest_node(pos_x, pos_y);
|
|
if let Some(id) = id {
|
|
state.board.remove_node(id);
|
|
}
|
|
}
|
|
app::redraw();
|
|
true
|
|
}
|
|
_ => false,
|
|
}
|
|
});
|
|
|
|
flex.end();
|
|
|
|
win.end();
|
|
win.make_resizable(true);
|
|
win.show();
|
|
|
|
app.run().unwrap();
|
|
}
|