From 8740b98001a752d25a1e6f4813d9573aa868dfc5 Mon Sep 17 00:00:00 2001 From: Meshiest Date: Tue, 27 Dec 2022 15:36:00 -0600 Subject: [PATCH] allow interrupting the generation and removing images --- .gitignore | 4 +- Cargo.lock | 2 +- Cargo.toml | 2 +- src/gui/app.rs | 123 ++++++-- src/main.rs | 281 ++++++++--------- src/quad.rs | 802 +++++++++++++++++++++++++------------------------ src/util.rs | 171 +++++------ 7 files changed, 729 insertions(+), 656 deletions(-) diff --git a/.gitignore b/.gitignore index be69b5b..4740bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /target **/*.rs.bk -out.brs +*.brs maps - + diff --git a/Cargo.lock b/Cargo.lock index f819b40..c4f37b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1018,7 +1018,7 @@ dependencies = [ [[package]] name = "heightmap" -version = "0.6.0" +version = "0.6.1" dependencies = [ "brickadia", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 803623b..5af5171 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "heightmap" -version = "0.6.0" +version = "0.6.1" authors = ["Meshiest "] edition = "2018" diff --git a/src/gui/app.rs b/src/gui/app.rs index ea4cd6c..5e58f5f 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1,8 +1,8 @@ #![allow(dead_code, unused_variables)] use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, sync::mpsc::{self, Receiver, Sender}, - thread, + thread::{self}, time::Duration, }; @@ -40,6 +40,7 @@ pub struct HeightmapApp { out_file: String, vertical_scale: u32, horizontal_size: u32, + opt_quad: bool, opt_cull: bool, opt_nocollide: bool, opt_lrgb: bool, @@ -51,6 +52,7 @@ pub struct HeightmapApp { progress_channel: (Sender, Receiver), promise: Option>>, texture_handles: HashMap, + gen_interrupt: Option>, } impl Default for HeightmapApp { @@ -64,6 +66,7 @@ impl Default for HeightmapApp { out_file: "out.brs".to_string(), vertical_scale: 1, horizontal_size: 1, + opt_quad: true, opt_cull: false, opt_nocollide: false, opt_lrgb: false, @@ -75,6 +78,7 @@ impl Default for HeightmapApp { progress: ("Pending", 0.), progress_channel: mpsc::channel(), texture_handles: HashMap::new(), + gen_interrupt: None, } } } @@ -96,6 +100,7 @@ impl HeightmapApp { hdmap: self.opt_hdmap, lrgb: self.opt_lrgb, nocollide: self.opt_nocollide, + quadtree: self.opt_quad, }; if options.tile { @@ -118,14 +123,31 @@ impl HeightmapApp { let options = self.options(); let heightmap_files = self.heightmaps.clone(); let colormap_file = self.colormap.clone(); - let progress = self.progress_channel.0.clone(); + + let progress_tx = self.progress_channel.0.clone(); + let progress = move |status, p| progress_tx.send((status, p)).unwrap(); + + // handle interrupts + let (tx, rx) = mpsc::channel::<()>(); + self.gen_interrupt = Some(tx); + let is_stopped = move || rx.try_recv().is_ok(); self.promise.get_or_insert_with(|| { info!("Preparing converter..."); let (sender, promise) = Promise::new(); - progress.send(("Reading", 0.)).unwrap(); + + progress("Reading", 0.); thread::spawn(move || { + macro_rules! stop_if_stopped { + () => { + if is_stopped() { + sender.send(Err("Stopped by user".to_string())); + return; + } + }; + } + info!("Reading image files..."); let (heightmap, colormap) = match maps_from_files(&options, heightmap_files, colormap_file) { @@ -136,10 +158,12 @@ impl HeightmapApp { } }; - progress.send(("Generating", 0.10)).unwrap(); + stop_if_stopped!(); + progress("Generating", 0.10); let bricks = match gen_opt_heightmap(&*heightmap, &*colormap, options, |p| { - progress.send(("Generating", 0.1 + 0.85 * p)).unwrap(); + progress("Generating", 0.1 + 0.85 * p); + !is_stopped() }) { Ok(b) => b, Err(err) => { @@ -147,22 +171,25 @@ impl HeightmapApp { return sender.send(Err(err)); } }; + stop_if_stopped!(); info!("Writing Save to {}", out_file); - progress.send(("Writing", 0.95)).unwrap(); + progress("Writing", 0.95); let data = bricks_to_save(bricks, owner_id, owner_name); if let Err(e) = SaveWriter::new(File::create(&out_file).unwrap(), data).write() { let err = format!("failed to write file: {e}"); error!("{err}"); return sender.send(Err(err)); } - progress.send(("Finshed", 1.0)).unwrap(); + stop_if_stopped!(); + progress("Finished", 1.0); info!("Done!"); sender.send(Ok(())); thread::sleep(Duration::from_millis(500)); - progress.send(("", 2.0)).unwrap(); + progress("", 2.0); }); + // thread::self.gen_thread.unwrap().thread(). promise }); } @@ -230,6 +257,9 @@ impl HeightmapApp { .on_hover_text("Using a high detail rgb color encoded heightmap"); ui.checkbox(&mut self.opt_glow, "Glow") .on_hover_text("Glow bricks at lowest intensity"); + ui.checkbox(&mut self.opt_quad, "Quadtree").on_hover_text( + "Run quadtree optimization (looks much better but has a few more bricks)", + ); }); ui.end_row(); @@ -255,7 +285,7 @@ impl HeightmapApp { ui.label("Select image files to use for save generation."); // handle heightmap multiple file selection - if ui.button("Select images").clicked() { + if ui.button("Select heightmaps").clicked() { let result = nfd::dialog_multiple() .filter("png") .open() @@ -277,23 +307,31 @@ impl HeightmapApp { egui::Grid::new("heightmap_grid") .striped(true) - .spacing([4.0, 4.0]) + .spacing([8.0, 4.0]) + .min_col_width(4.0) .show(ui, |ui| { - for img in &self.heightmaps.clone() { - self.thumb(ui, img); + let mut to_remove = HashSet::new(); + for img in self.heightmaps.clone() { + if ui.add(Button::new("✖")).clicked() { + to_remove.insert(img.clone()); + } + self.thumb(ui, &img); ui.label(Path::new(&img).file_name().unwrap().to_str().unwrap()); ui.end_row(); } + self.heightmaps.retain(|i| !to_remove.contains(i)); }); ui.separator(); - ui.add_space(4.0); ui.heading("Colormap Image"); ui.label("Select image file to use for heightmap coloring. Select only a colormap for img2brick mode."); // handle colormap single file selection - if ui.button("Select colormap image").clicked() { + if ui + .add(Button::new("Select colormap").fill(Color32::from_rgb(60, 60, 120))) + .clicked() + { let result = nfd::dialog().filter("png").open().unwrap_or_else(|e| { panic!("{}", e); }); @@ -311,20 +349,28 @@ impl HeightmapApp { } if let Some(path) = self.colormap.clone() { - ui.horizontal(|ui| { - self.thumb(ui, &path); - ui.label(Path::new(&path).file_name().unwrap().to_str().unwrap()); - }); + egui::Grid::new("colormap_grid") + .striped(true) + .spacing([8.0, 4.0]) + .min_col_width(4.0) + .show(ui, |ui| { + if ui.button("✖").clicked() { + self.colormap = None; + } + self.thumb(ui, &path); + ui.label(Path::new(&path).file_name().unwrap().to_str().unwrap()); + }); } } - fn draw_progress(&mut self, ctx: &Context, ui: &mut Ui) { + fn draw_progress(&mut self, ctx: &Context, ui: &mut Ui) -> bool { while let Ok(p) = self.progress_channel.1.try_recv() { self.progress = p; } let (progress_text, progress) = self.progress; let mut clear_promise = progress > 1.0; + let mut rendered = false; if let Some(p) = &self.promise { match p.ready() { @@ -347,22 +393,34 @@ impl HeightmapApp { }); } None => { - ui.add( - ProgressBar::new(ctx.animate_value_with_time( - Id::new("progress"), - progress, - 0.1, - )) - .text(progress_text) - .animate(true), - ); + ui.horizontal(|ui| { + let stop_btn = ui.button("Stop"); + ui.add( + ProgressBar::new(ctx.animate_value_with_time( + Id::new("progress"), + progress, + 0.1, + )) + .text(progress_text) + .animate(true), + ); + if let (true, Some(tx)) = (stop_btn.clicked(), &self.gen_interrupt) { + info!("Sending interrupt..."); + if let Err(e) = tx.send(()) { + error!("error sending interrupt {e}"); + } + } + }); } } + rendered = true; } if clear_promise { self.promise = None } + + rendered } fn draw_submit(&mut self, ui: &mut Ui) { @@ -383,7 +441,7 @@ impl HeightmapApp { (false, true) => "Generate image2brick save", (false, false) => unreachable!(), }) - .fill(Color32::DARK_GREEN), + .fill(Color32::from_rgb(50, 90, 50)), ) .clicked() { @@ -418,8 +476,9 @@ impl App for HeightmapApp { ui.separator(); self.draw_settings(ui); ui.separator(); - self.draw_progress(ctx, ui); - self.draw_submit(ui); + if !self.draw_progress(ctx, ui) { + self.draw_submit(ui); + } }); TopBottomPanel::bottom(Id::new("logs")) diff --git a/src/main.rs b/src/main.rs index 248af2f..f14aae1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,140 +1,141 @@ -pub mod map; -pub mod quad; -pub mod util; - -use crate::{map::*, quad::*, util::*}; -use brickadia::write::SaveWriter; -use clap::clap_app; -use env_logger::Builder; -use log::{error, info, LevelFilter}; -use std::{boxed::Box, fs::File, io::Write}; - -fn main() { - Builder::new() - .format(|buf, record| writeln!(buf, "{}", record.args())) - .filter(None, LevelFilter::Info) - .init(); - - let matches = clap_app!(heightmap => - (version: env!("CARGO_PKG_VERSION")) - (author: "github.com/Meshiest") - (about: "Converts heightmap png files to Brickadia save files") - (@arg INPUT: +required +multiple "Input heightmap PNG images") - (@arg output: -o --output +takes_value "Output BRS file") - (@arg colormap: -c --colormap +takes_value "Input colormap PNG image") - (@arg vertical: -v --vertical +takes_value "Vertical scale multiplier (default 1)") - (@arg size: -s --size +takes_value "Brick stud size (default 1)") - (@arg cull: --cull "Automatically remove bottom level bricks and fully transparent bricks") - (@arg tile: --tile "Render bricks as tiles") - (@arg micro: --micro "Render bricks as micro bricks") - (@arg stud: --stud "Render bricks as stud cubes") - (@arg snap: --snap "Snap bricks to the brick grid") - (@arg lrgb: --lrgb "Use linear rgb input color instead of sRGB") - (@arg img: -i --img "Make the heightmap flat and render an image") - (@arg glow: --glow "Make the heightmap glow at 0 intensity") - (@arg hdmap: --hdmap "Using a high detail rgb color encoded heightmap") - (@arg nocollide: --nocollide "Disable brick collision") - (@arg owner_id: --owner_id +takes_value "Set the owner id (default a1b16aca-9627-4a16-a160-67fa9adbb7b6)") - (@arg owner: --owner +takes_value "Set the owner name (default Generator)") - ) - .get_matches(); - - // get files from matches - let heightmap_files = matches.values_of("INPUT").unwrap().collect::>(); - let colormap_file = matches - .value_of("colormap") - .unwrap_or(heightmap_files[0]) - .to_string(); - let out_file = matches - .value_of("output") - .unwrap_or("./out.brs") - .to_string(); - - // owner values - let owner_id = matches - .value_of("owner_id") - .unwrap_or("a1b16aca-9627-4a16-a160-67fa9adbb7b6") - .to_string(); - let owner_name = matches.value_of("owner").unwrap_or("Generator").to_string(); - - // output options - let mut options = GenOptions { - size: matches - .value_of("size") - .unwrap_or("1") - .parse::() - .expect("Size must be integer") - * 5, - scale: matches - .value_of("vertical") - .unwrap_or("1") - .parse::() - .expect("Scale must be integer"), - cull: matches.is_present("cull"), - asset: 0, - tile: matches.is_present("tile"), - micro: matches.is_present("micro"), - stud: matches.is_present("stud"), - snap: matches.is_present("snap"), - img: matches.is_present("img"), - glow: matches.is_present("glow"), - hdmap: matches.is_present("hdmap"), - lrgb: matches.is_present("lrgb"), - nocollide: matches.is_present("nocollide"), - }; - - if options.tile { - options.asset = 1 - } else if options.micro { - options.size /= 5; - options.asset = 2; - } - if options.stud { - options.asset = 3 - } - - info!("Reading image files"); - - // colormap file parsing - let colormap = match file_ext(&colormap_file.to_lowercase()) { - Some("png") => match ColormapPNG::new(&colormap_file, options.lrgb) { - Ok(map) => map, - Err(err) => { - return error!("Error reading colormap: {:?}", err); - } - }, - Some(ext) => { - return error!("Unsupported colormap format '{}'", ext); - } - None => { - return error!("Missing colormap format for '{}'", colormap_file); - } - }; - - // heightmap file parsing - let heightmap: Box = - if heightmap_files.iter().all(|f| file_ext(f) == Some("png")) { - if options.img { - Box::new(HeightmapFlat::new(colormap.size()).unwrap()) - } else { - match HeightmapPNG::new(heightmap_files, options.hdmap) { - Ok(map) => Box::new(map), - Err(error) => { - return error!("Error reading heightmap: {:?}", error); - } - } - } - } else { - return error!("Unsupported heightmap format"); - }; - - let bricks = gen_opt_heightmap(&*heightmap, &colormap, options, |_| {}) - .expect("error during generation"); - - info!("Writing Save to {}", out_file); - let data = bricks_to_save(bricks, owner_id, owner_name); - SaveWriter::new(File::create(out_file).unwrap(), data) - .write() - .expect("Failed to write file!"); - info!("Done!"); -} +pub mod map; +pub mod quad; +pub mod util; + +use crate::{map::*, quad::*, util::*}; +use brickadia::write::SaveWriter; +use clap::clap_app; +use env_logger::Builder; +use log::{error, info, LevelFilter}; +use std::{boxed::Box, fs::File, io::Write}; + +fn main() { + Builder::new() + .format(|buf, record| writeln!(buf, "{}", record.args())) + .filter(None, LevelFilter::Info) + .init(); + + let matches = clap_app!(heightmap => + (version: env!("CARGO_PKG_VERSION")) + (author: "github.com/Meshiest") + (about: "Converts heightmap png files to Brickadia save files") + (@arg INPUT: +required +multiple "Input heightmap PNG images") + (@arg output: -o --output +takes_value "Output BRS file") + (@arg colormap: -c --colormap +takes_value "Input colormap PNG image") + (@arg vertical: -v --vertical +takes_value "Vertical scale multiplier (default 1)") + (@arg size: -s --size +takes_value "Brick stud size (default 1)") + (@arg cull: --cull "Automatically remove bottom level bricks and fully transparent bricks") + (@arg tile: --tile "Render bricks as tiles") + (@arg micro: --micro "Render bricks as micro bricks") + (@arg stud: --stud "Render bricks as stud cubes") + (@arg snap: --snap "Snap bricks to the brick grid") + (@arg lrgb: --lrgb "Use linear rgb input color instead of sRGB") + (@arg img: -i --img "Make the heightmap flat and render an image") + (@arg glow: --glow "Make the heightmap glow at 0 intensity") + (@arg hdmap: --hdmap "Using a high detail rgb color encoded heightmap") + (@arg nocollide: --nocollide "Disable brick collision") + (@arg owner_id: --owner_id +takes_value "Set the owner id (default a1b16aca-9627-4a16-a160-67fa9adbb7b6)") + (@arg owner: --owner +takes_value "Set the owner name (default Generator)") + ) + .get_matches(); + + // get files from matches + let heightmap_files = matches.values_of("INPUT").unwrap().collect::>(); + let colormap_file = matches + .value_of("colormap") + .unwrap_or(heightmap_files[0]) + .to_string(); + let out_file = matches + .value_of("output") + .unwrap_or("./out.brs") + .to_string(); + + // owner values + let owner_id = matches + .value_of("owner_id") + .unwrap_or("a1b16aca-9627-4a16-a160-67fa9adbb7b6") + .to_string(); + let owner_name = matches.value_of("owner").unwrap_or("Generator").to_string(); + + // output options + let mut options = GenOptions { + size: matches + .value_of("size") + .unwrap_or("1") + .parse::() + .expect("Size must be integer") + * 5, + scale: matches + .value_of("vertical") + .unwrap_or("1") + .parse::() + .expect("Scale must be integer"), + cull: matches.is_present("cull"), + asset: 0, + tile: matches.is_present("tile"), + micro: matches.is_present("micro"), + stud: matches.is_present("stud"), + snap: matches.is_present("snap"), + img: matches.is_present("img"), + glow: matches.is_present("glow"), + hdmap: matches.is_present("hdmap"), + lrgb: matches.is_present("lrgb"), + nocollide: matches.is_present("nocollide"), + quadtree: true, + }; + + if options.tile { + options.asset = 1 + } else if options.micro { + options.size /= 5; + options.asset = 2; + } + if options.stud { + options.asset = 3 + } + + info!("Reading image files"); + + // colormap file parsing + let colormap = match file_ext(&colormap_file.to_lowercase()) { + Some("png") => match ColormapPNG::new(&colormap_file, options.lrgb) { + Ok(map) => map, + Err(err) => { + return error!("Error reading colormap: {:?}", err); + } + }, + Some(ext) => { + return error!("Unsupported colormap format '{}'", ext); + } + None => { + return error!("Missing colormap format for '{}'", colormap_file); + } + }; + + // heightmap file parsing + let heightmap: Box = + if heightmap_files.iter().all(|f| file_ext(f) == Some("png")) { + if options.img { + Box::new(HeightmapFlat::new(colormap.size()).unwrap()) + } else { + match HeightmapPNG::new(heightmap_files, options.hdmap) { + Ok(map) => Box::new(map), + Err(error) => { + return error!("Error reading heightmap: {:?}", error); + } + } + } + } else { + return error!("Unsupported heightmap format"); + }; + + let bricks = gen_opt_heightmap(&*heightmap, &colormap, options, |_| true) + .expect("error during generation"); + + info!("Writing Save to {}", out_file); + let data = bricks_to_save(bricks, owner_id, owner_name); + SaveWriter::new(File::create(out_file).unwrap(), data) + .write() + .expect("Failed to write file!"); + info!("Done!"); +} diff --git a/src/quad.rs b/src/quad.rs index fa6e20d..16238cc 100644 --- a/src/quad.rs +++ b/src/quad.rs @@ -1,395 +1,407 @@ -use crate::map::*; -use crate::util::*; -use brickadia::save::{Brick, BrickColor, Collision, Color, Size}; -use log::info; -use std::{ - cmp::{max, min}, - collections::HashSet, -}; - -#[derive(Debug, Default)] -struct Tile { - index: usize, - center: (u32, u32), - size: (u32, u32), - color: [u8; 4], - height: u32, - neighbors: HashSet, - parent: Option, -} - -pub struct QuadTree { - tiles: Box<[Tile]>, - width: u32, - height: u32, -} - -impl Tile { - // determine if another tile is similar in all properties - fn similar_quad(&self, other: &Self) -> bool { - self.size == other.size - && self.color == other.color - && self.height == other.height - && self.parent.is_none() - && other.parent.is_none() - } - - // determine if another tile is similar in all properties except potentially width or height as long as they are in a line - fn similar_line(&self, other: &Self) -> bool { - let is_vertical = self.center.0 == other.center.0; - let is_horizontal = self.center.1 == other.center.1; - - (is_vertical && self.size.0 == other.size.0 || is_horizontal && self.size.1 == other.size.1) - && self.color == other.color - && self.height == other.height - && self.parent.is_none() - && other.parent.is_none() - } - - // merge a few tiles with this one - fn merge_quad( - &mut self, - top_right: &mut Self, - bottom_left: &mut Self, - bottom_right: &mut Self, - ) { - // update size - self.size = (self.size.0 * 2, self.size.1 * 2); - - self.neighbors.extend(&top_right.neighbors); - self.neighbors.extend(&bottom_left.neighbors); - self.neighbors.extend(&bottom_right.neighbors); - - // update parents of merged nodes - top_right.parent = Some(self.index); - bottom_left.parent = Some(self.index); - bottom_right.parent = Some(self.index); - } -} - -impl QuadTree { - // create a heightmap grid from two images - pub fn new(heightmap: &dyn Heightmap, colormap: &dyn Colormap) -> Result { - let (width, height) = heightmap.size(); - - if colormap.size() != heightmap.size() { - return Err("Heightmap and colormap must have same dimensions".to_string()); - } - - let mut tiles = Vec::with_capacity((width * height) as usize); - - // add all the tiles to the heightmap - for x in 0..width as i32 { - for y in 0..height as i32 { - tiles.push(Tile { - index: (x + y * height as i32) as usize, - center: (x as u32, y as u32), - // store a set of the neighbor's heights with each tile - // they will be joined when the tiles merge - neighbors: vec![(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] - .into_iter() - .filter(|(x, y)| { - *x >= 0 && *x < width as i32 && *y >= 0 && *y < height as i32 - }) - .map(|(x, y)| heightmap.at(x as u32, y as u32)) - .fold(HashSet::new(), |mut set, height| { - set.insert(height); - set - }), - size: (1, 1), - color: colormap.at(x as u32, y as u32), - height: heightmap.at(x as u32, y as u32), - parent: None, - }) - } - } - - Ok(QuadTree { - tiles: tiles.into_boxed_slice(), - width, - height, - }) - } - - fn index(&self, x: u32, y: u32) -> usize { - (y + x * self.height) as usize - } - - // optimize bricks with size (level+1) - pub fn quad_optimize_level(&mut self, level: u32) -> usize { - let mut count = 0; - - // step amounts - let space = 2_u32.pow(level); - let step_amt = space as usize * 2; - - for x in (0..self.width - space).step_by(step_amt) { - for y in (0..self.height - space).step_by(step_amt) { - // split vertically (left/right columns) - let (left, right) = self - .tiles - .split_at_mut(((x + space) * self.height) as usize); - - // split the columns horizontally - let (top_left, bottom_left) = - left.split_at_mut((y + space + x * self.height) as usize); - let (top_right, bottom_right) = right.split_at_mut((y + space) as usize); - - // first of each slice is the target cell - let top_left = &mut top_left[(y + x * self.height) as usize]; - let bottom_left = &mut bottom_left[0]; - let top_right = &mut top_right[y as usize]; - let bottom_right = &mut bottom_right[0]; - - // if these are not similar tiles, skip them - if top_left.size.0 != space - || !top_left.similar_quad(top_right) - || !top_left.similar_quad(bottom_left) - || !top_left.similar_quad(bottom_right) - { - continue; - } - - count += 3; - - // merge the tiles into the first one - top_left.merge_quad(top_right, bottom_left, bottom_right); - } - } - - count - } - - // merge tiles that are arranged in a line - fn merge_line(&mut self, start_i: usize, children: Vec) { - // there is nothing to merge, return - if children.is_empty() { - return; - } - - let mut new_neighbors = vec![]; - - // determine direction of this merge - let is_vertical = self.tiles[children[0]].center.0 == self.tiles[start_i].center.0; - - // determine the new size of the parent tile, make children point at the parent - let new_size = children.iter().fold(0, |sum, &i| { - let mut t = &mut self.tiles[i]; - // assign parent, extend parent's neighbors - t.parent = Some(start_i); - new_neighbors.push(t.neighbors.clone()); - - // sum size depending on merge direction - sum + if is_vertical { t.size.1 } else { t.size.0 } - }); - - let mut start = &mut self.tiles[start_i]; - - for n in new_neighbors { - start.neighbors.extend(&n); - } - - // add the size to its respective dimension - if is_vertical { - start.size.1 += new_size - } else { - start.size.0 += new_size - } - } - - // optimize by nearby bricks in line - pub fn line_optimize(&mut self, tile_scale: u32) -> usize { - let mut count = 0; - for x in 0..self.width { - for y in 0..self.height { - let start_i = self.index(x, y); - let start = &self.tiles[start_i]; - if start.parent.is_some() { - continue; - } - - let shift = start.size; - let mut sx = shift.0; - let mut horiz_tiles = vec![]; - let mut sy = shift.1; - let mut vert_tiles = vec![]; - - // determine longest horizontal merge - while x + sx < self.width { - let i = self.index(x + sx, y); - let t = &self.tiles[i]; - if (sx + t.size.0) * tile_scale > 500 || !start.similar_line(t) { - break; - } - horiz_tiles.push(i); - sx += t.size.0; - } - - // determine longest vertical merge - while y + sy < self.height { - let i = self.index(x, y + sy); - let t = &self.tiles[i]; - if (sy + t.size.1) * tile_scale > 500 || !start.similar_line(t) { - break; - } - vert_tiles.push(i); - sy += t.size.1; - } - - count += max(horiz_tiles.len(), vert_tiles.len()); - - // merge whichever is largest - self.merge_line( - start_i, - if horiz_tiles.len() > vert_tiles.len() { - horiz_tiles - } else { - vert_tiles - }, - ); - } - } - - count - } - - // convert quadtree state into bricks - pub fn into_bricks(&self, options: GenOptions) -> Vec { - self.tiles - .iter() - .flat_map(|t| { - if t.parent.is_some() || options.cull && (t.height == 0 || t.color[3] == 0) { - return vec![]; - } - - let mut z = (options.scale * t.height) as i32; - - // determine the height of this brick (difference of self and smallest neighbor) - let raw_height = max( - t.height as i32 - t.neighbors.iter().cloned().min().unwrap_or(0) as i32 + 1, - 2, - ); - let mut desired_height = max(raw_height * options.scale as i32 / 2, 2); - - // snap bricks to grid - if options.snap { - z += 4 - z % 4; - desired_height += 4 - desired_height % 4; - } - - let mut bricks = vec![]; - // until we've made enough bricks to fill the height - // add a brick with a max height of 250 - while desired_height > 0 { - // pick height for this brick - - let height = - min(max(desired_height, if options.stud { 5 } else { 2 }), 250) as u32; - let height = height + height % (if options.stud { 5 } else { 2 }); - - bricks.push(Brick { - asset_name_index: options.asset, - size: Size::Procedural( - t.size.0 * options.size, - t.size.1 * options.size, - // if it's a microbrick image, just use the block size so it's cubes - if options.img && options.micro { - options.size - } else { - height - }, - ), - position: ( - ((t.center.0 * 2 + t.size.0) * options.size) as i32, - ((t.center.1 * 2 + t.size.1) * options.size) as i32, - z - height as i32 + 2, - ), - collision: Collision { - player: !options.nocollide, - weapon: !options.nocollide, - interaction: !options.nocollide, - tool: true, - }, - color: BrickColor::Unique(Color { - r: t.color[0], - g: t.color[1], - b: t.color[2], - a: t.color[3], - }), - owner_index: 1, - material_intensity: 0, - material_index: u32::from(options.glow), - ..Default::default() - }); - - // update Z and remaining height - desired_height -= height as i32; - z -= height as i32 * 2; - } - bricks - }) - .collect() - } -} - -// Generate a heightmap with brick conservation optimizations -pub fn gen_opt_heightmap( - heightmap: &dyn Heightmap, - colormap: &dyn Colormap, - options: GenOptions, - progress: F, -) -> Result, String> { - progress(0.0); - - info!("Building initial quadtree"); - let (width, height) = heightmap.size(); - let area = width * height; - let mut quad = QuadTree::new(heightmap, colormap)?; - progress(0.2); - - info!("Optimizing quadtree"); - let mut scale = 0; - - // loop until the bricks would be too wide or we stop optimizing bricks - while 2_i32.pow(scale + 1) * (options.size as i32) < 500 { - progress(0.2 + 0.5 * (scale as f32 / (500.0 / (options.size as f32)).log2())); - let count = quad.quad_optimize_level(scale); - if count == 0 { - break; - } else { - info!(" Removed {:?} {}x bricks", count, 2_i32.pow(scale)); - } - scale += 1; - } - - progress(0.7); - - info!("Optimizing linear"); - let mut i = 0; - loop { - i += 1; - - let count = quad.line_optimize(options.size); - progress(0.7 + 0.25 * (i as f32 / 5.0).min(1.0)); - - if count == 0 { - break; - } - info!(" Removed {} bricks", count); - } - - progress(0.95); - - let bricks = quad.into_bricks(options); - let brick_count = bricks.len(); - info!( - "Reduced {} to {} ({}%; -{} bricks)", - area, - brick_count, - (100. - brick_count as f64 / area as f64 * 100.).floor(), - area as i32 - brick_count as i32, - ); - - progress(1.0); - Ok(bricks) -} +use crate::map::*; +use crate::util::*; +use brickadia::save::{Brick, BrickColor, Collision, Color, Size}; +use log::info; +use std::{ + cmp::{max, min}, + collections::HashSet, +}; + +#[derive(Debug, Default)] +struct Tile { + index: usize, + center: (u32, u32), + size: (u32, u32), + color: [u8; 4], + height: u32, + neighbors: HashSet, + parent: Option, +} + +pub struct QuadTree { + tiles: Box<[Tile]>, + width: u32, + height: u32, +} + +impl Tile { + // determine if another tile is similar in all properties + fn similar_quad(&self, other: &Self) -> bool { + self.size == other.size + && self.color == other.color + && self.height == other.height + && self.parent.is_none() + && other.parent.is_none() + } + + // determine if another tile is similar in all properties except potentially width or height as long as they are in a line + fn similar_line(&self, other: &Self) -> bool { + let is_vertical = self.center.0 == other.center.0; + let is_horizontal = self.center.1 == other.center.1; + + (is_vertical && self.size.0 == other.size.0 || is_horizontal && self.size.1 == other.size.1) + && self.color == other.color + && self.height == other.height + && self.parent.is_none() + && other.parent.is_none() + } + + // merge a few tiles with this one + fn merge_quad( + &mut self, + top_right: &mut Self, + bottom_left: &mut Self, + bottom_right: &mut Self, + ) { + // update size + self.size = (self.size.0 * 2, self.size.1 * 2); + + self.neighbors.extend(&top_right.neighbors); + self.neighbors.extend(&bottom_left.neighbors); + self.neighbors.extend(&bottom_right.neighbors); + + // update parents of merged nodes + top_right.parent = Some(self.index); + bottom_left.parent = Some(self.index); + bottom_right.parent = Some(self.index); + } +} + +impl QuadTree { + // create a heightmap grid from two images + pub fn new(heightmap: &dyn Heightmap, colormap: &dyn Colormap) -> Result { + let (width, height) = heightmap.size(); + + if colormap.size() != heightmap.size() { + return Err("Heightmap and colormap must have same dimensions".to_string()); + } + + let mut tiles = Vec::with_capacity((width * height) as usize); + + // add all the tiles to the heightmap + for x in 0..width as i32 { + for y in 0..height as i32 { + tiles.push(Tile { + index: (x + y * height as i32) as usize, + center: (x as u32, y as u32), + // store a set of the neighbor's heights with each tile + // they will be joined when the tiles merge + neighbors: vec![(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)] + .into_iter() + .filter(|(x, y)| { + *x >= 0 && *x < width as i32 && *y >= 0 && *y < height as i32 + }) + .map(|(x, y)| heightmap.at(x as u32, y as u32)) + .fold(HashSet::new(), |mut set, height| { + set.insert(height); + set + }), + size: (1, 1), + color: colormap.at(x as u32, y as u32), + height: heightmap.at(x as u32, y as u32), + parent: None, + }) + } + } + + Ok(QuadTree { + tiles: tiles.into_boxed_slice(), + width, + height, + }) + } + + fn index(&self, x: u32, y: u32) -> usize { + (y + x * self.height) as usize + } + + // optimize bricks with size (level+1) + pub fn quad_optimize_level(&mut self, level: u32) -> usize { + let mut count = 0; + + // step amounts + let space = 2_u32.pow(level); + let step_amt = space as usize * 2; + + for x in (0..self.width - space).step_by(step_amt) { + for y in (0..self.height - space).step_by(step_amt) { + // split vertically (left/right columns) + let (left, right) = self + .tiles + .split_at_mut(((x + space) * self.height) as usize); + + // split the columns horizontally + let (top_left, bottom_left) = + left.split_at_mut((y + space + x * self.height) as usize); + let (top_right, bottom_right) = right.split_at_mut((y + space) as usize); + + // first of each slice is the target cell + let top_left = &mut top_left[(y + x * self.height) as usize]; + let bottom_left = &mut bottom_left[0]; + let top_right = &mut top_right[y as usize]; + let bottom_right = &mut bottom_right[0]; + + // if these are not similar tiles, skip them + if top_left.size.0 != space + || !top_left.similar_quad(top_right) + || !top_left.similar_quad(bottom_left) + || !top_left.similar_quad(bottom_right) + { + continue; + } + + count += 3; + + // merge the tiles into the first one + top_left.merge_quad(top_right, bottom_left, bottom_right); + } + } + + count + } + + // merge tiles that are arranged in a line + fn merge_line(&mut self, start_i: usize, children: Vec) { + // there is nothing to merge, return + if children.is_empty() { + return; + } + + let mut new_neighbors = vec![]; + + // determine direction of this merge + let is_vertical = self.tiles[children[0]].center.0 == self.tiles[start_i].center.0; + + // determine the new size of the parent tile, make children point at the parent + let new_size = children.iter().fold(0, |sum, &i| { + let mut t = &mut self.tiles[i]; + // assign parent, extend parent's neighbors + t.parent = Some(start_i); + new_neighbors.push(t.neighbors.clone()); + + // sum size depending on merge direction + sum + if is_vertical { t.size.1 } else { t.size.0 } + }); + + let mut start = &mut self.tiles[start_i]; + + for n in new_neighbors { + start.neighbors.extend(&n); + } + + // add the size to its respective dimension + if is_vertical { + start.size.1 += new_size + } else { + start.size.0 += new_size + } + } + + // optimize by nearby bricks in line + pub fn line_optimize(&mut self, tile_scale: u32) -> usize { + let mut count = 0; + for x in 0..self.width { + for y in 0..self.height { + let start_i = self.index(x, y); + let start = &self.tiles[start_i]; + if start.parent.is_some() { + continue; + } + + let shift = start.size; + let mut sx = shift.0; + let mut horiz_tiles = vec![]; + let mut sy = shift.1; + let mut vert_tiles = vec![]; + + // determine longest horizontal merge + while x + sx < self.width { + let i = self.index(x + sx, y); + let t = &self.tiles[i]; + if (sx + t.size.0) * tile_scale > 500 || !start.similar_line(t) { + break; + } + horiz_tiles.push(i); + sx += t.size.0; + } + + // determine longest vertical merge + while y + sy < self.height { + let i = self.index(x, y + sy); + let t = &self.tiles[i]; + if (sy + t.size.1) * tile_scale > 500 || !start.similar_line(t) { + break; + } + vert_tiles.push(i); + sy += t.size.1; + } + + count += max(horiz_tiles.len(), vert_tiles.len()); + + // merge whichever is largest + self.merge_line( + start_i, + if horiz_tiles.len() > vert_tiles.len() { + horiz_tiles + } else { + vert_tiles + }, + ); + } + } + + count + } + + // convert quadtree state into bricks + pub fn into_bricks(&self, options: GenOptions) -> Vec { + self.tiles + .iter() + .flat_map(|t| { + if t.parent.is_some() || options.cull && (t.height == 0 || t.color[3] == 0) { + return vec![]; + } + + let mut z = (options.scale * t.height) as i32; + + // determine the height of this brick (difference of self and smallest neighbor) + let raw_height = max( + t.height as i32 - t.neighbors.iter().cloned().min().unwrap_or(0) as i32 + 1, + 2, + ); + let mut desired_height = max(raw_height * options.scale as i32 / 2, 2); + + // snap bricks to grid + if options.snap { + z += 4 - z % 4; + desired_height += 4 - desired_height % 4; + } + + let mut bricks = vec![]; + // until we've made enough bricks to fill the height + // add a brick with a max height of 250 + while desired_height > 0 { + // pick height for this brick + + let height = + min(max(desired_height, if options.stud { 5 } else { 2 }), 250) as u32; + let height = height + height % (if options.stud { 5 } else { 2 }); + + bricks.push(Brick { + asset_name_index: options.asset, + size: Size::Procedural( + t.size.0 * options.size, + t.size.1 * options.size, + // if it's a microbrick image, just use the block size so it's cubes + if options.img && options.micro { + options.size + } else { + height + }, + ), + position: ( + ((t.center.0 * 2 + t.size.0) * options.size) as i32, + ((t.center.1 * 2 + t.size.1) * options.size) as i32, + z - height as i32 + 2, + ), + collision: Collision { + player: !options.nocollide, + weapon: !options.nocollide, + interaction: !options.nocollide, + tool: true, + }, + color: BrickColor::Unique(Color { + r: t.color[0], + g: t.color[1], + b: t.color[2], + a: t.color[3], + }), + owner_index: 1, + material_intensity: 0, + material_index: u32::from(options.glow), + ..Default::default() + }); + + // update Z and remaining height + desired_height -= height as i32; + z -= height as i32 * 2; + } + bricks + }) + .collect() + } +} + +// Generate a heightmap with brick conservation optimizations +pub fn gen_opt_heightmap bool>( + heightmap: &dyn Heightmap, + colormap: &dyn Colormap, + options: GenOptions, + progress_f: F, +) -> Result, String> { + macro_rules! progress { + ($e:expr) => { + if !progress_f($e) { + return Err("Stopped by user".to_string()); + } + }; + } + progress!(0.0); + + info!("Building initial quadtree"); + let (width, height) = heightmap.size(); + let area = width * height; + let mut quad = QuadTree::new(heightmap, colormap)?; + progress!(0.2); + + let (prog_offset, prog_scale) = if options.quadtree { + info!("Optimizing quadtree"); + let mut scale = 0; + + // loop until the bricks would be too wide or we stop optimizing bricks + while 2_i32.pow(scale + 1) * (options.size as i32) < 500 { + progress!(0.2 + 0.5 * (scale as f32 / (500.0 / (options.size as f32)).log2())); + let count = quad.quad_optimize_level(scale); + if count == 0 { + break; + } else { + info!(" Removed {:?} {}x bricks", count, 2_i32.pow(scale)); + } + scale += 1; + } + progress!(0.7); + + (0.7, 0.25) + } else { + (0.2, 0.75) + }; + + info!("Optimizing linear"); + let mut i = 0; + loop { + i += 1; + + let count = quad.line_optimize(options.size); + progress!(prog_offset + prog_scale * (i as f32 / 5.0).min(1.0)); + + if count == 0 { + break; + } + info!(" Removed {} bricks", count); + } + + progress!(0.95); + + let bricks = quad.into_bricks(options); + let brick_count = bricks.len(); + info!( + "Reduced {} to {} ({}%; -{} bricks)", + area, + brick_count, + (100. - brick_count as f64 / area as f64 * 100.).floor(), + area as i32 - brick_count as i32, + ); + + progress!(1.0); + Ok(bricks) +} diff --git a/src/util.rs b/src/util.rs index 293af0c..36b80d5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,85 +1,86 @@ -use brickadia::save::{Brick, BrickOwner, Header1, Header2, SaveData, User}; -use std::ffi::OsStr; -use std::path::Path; -use uuid::Uuid; - -pub struct GenOptions { - pub size: u32, - pub scale: u32, - pub asset: u32, - pub cull: bool, - pub tile: bool, - pub micro: bool, - pub stud: bool, - pub snap: bool, - pub img: bool, - pub glow: bool, - pub hdmap: bool, - pub lrgb: bool, - pub nocollide: bool, -} - -// convert gamma to linear gamma -pub fn to_linear_gamma(c: u8) -> u8 { - let cf = (c as f64) / 255.0; - (if cf > 0.04045 { - (cf / 1.055 + 0.0521327).powf(2.4) * 255.0 - } else { - cf / 12.192 * 255.0 - }) as u8 -} - -// convert sRGB to linear rgb -pub fn to_linear_rgb(rgb: [u8; 4]) -> [u8; 4] { - [ - to_linear_gamma(rgb[0]), - to_linear_gamma(rgb[1]), - to_linear_gamma(rgb[2]), - rgb[3], - ] -} - -// given an array of bricks, create a save -#[allow(unused)] -pub fn bricks_to_save(bricks: Vec, owner_id: String, owner_name: String) -> SaveData { - let default_id = Uuid::parse_str("a1b16aca-9627-4a16-a160-67fa9adbb7b6").unwrap(); - - let author = User { - id: Uuid::parse_str(&owner_id).unwrap_or(default_id), - name: owner_name.clone(), - }; - - let brick_owners = vec![BrickOwner { - id: Uuid::parse_str(&owner_id).unwrap_or(default_id), - name: owner_name, - bricks: bricks.len() as u32, - }]; - - SaveData { - header1: Header1 { - map: String::from("https://github.com/brickadia-community"), - author, - description: String::from("Save generated from heightmap file"), - ..Default::default() - }, - header2: Header2 { - brick_assets: vec![ - String::from("PB_DefaultBrick"), - String::from("PB_DefaultTile"), - String::from("PB_DefaultMicroBrick"), - String::from("PB_DefaultStudded"), - ], - materials: vec!["BMC_Plastic".into(), "BMC_Glow".into()], - brick_owners, - ..Default::default() - }, - bricks, - ..Default::default() - } -} - -// get extension from filename -#[allow(unused)] -pub fn file_ext(filename: &str) -> Option<&str> { - Path::new(filename).extension().and_then(OsStr::to_str) -} +use brickadia::save::{Brick, BrickOwner, Header1, Header2, SaveData, User}; +use std::ffi::OsStr; +use std::path::Path; +use uuid::Uuid; + +pub struct GenOptions { + pub size: u32, + pub scale: u32, + pub asset: u32, + pub cull: bool, + pub tile: bool, + pub micro: bool, + pub stud: bool, + pub snap: bool, + pub img: bool, + pub glow: bool, + pub hdmap: bool, + pub lrgb: bool, + pub nocollide: bool, + pub quadtree: bool, +} + +// convert gamma to linear gamma +pub fn to_linear_gamma(c: u8) -> u8 { + let cf = (c as f64) / 255.0; + (if cf > 0.04045 { + (cf / 1.055 + 0.0521327).powf(2.4) * 255.0 + } else { + cf / 12.192 * 255.0 + }) as u8 +} + +// convert sRGB to linear rgb +pub fn to_linear_rgb(rgb: [u8; 4]) -> [u8; 4] { + [ + to_linear_gamma(rgb[0]), + to_linear_gamma(rgb[1]), + to_linear_gamma(rgb[2]), + rgb[3], + ] +} + +// given an array of bricks, create a save +#[allow(unused)] +pub fn bricks_to_save(bricks: Vec, owner_id: String, owner_name: String) -> SaveData { + let default_id = Uuid::parse_str("a1b16aca-9627-4a16-a160-67fa9adbb7b6").unwrap(); + + let author = User { + id: Uuid::parse_str(&owner_id).unwrap_or(default_id), + name: owner_name.clone(), + }; + + let brick_owners = vec![BrickOwner { + id: Uuid::parse_str(&owner_id).unwrap_or(default_id), + name: owner_name, + bricks: bricks.len() as u32, + }]; + + SaveData { + header1: Header1 { + map: String::from("https://github.com/brickadia-community"), + author, + description: String::from("Save generated from heightmap file"), + ..Default::default() + }, + header2: Header2 { + brick_assets: vec![ + String::from("PB_DefaultBrick"), + String::from("PB_DefaultTile"), + String::from("PB_DefaultMicroBrick"), + String::from("PB_DefaultStudded"), + ], + materials: vec!["BMC_Plastic".into(), "BMC_Glow".into()], + brick_owners, + ..Default::default() + }, + bricks, + ..Default::default() + } +} + +// get extension from filename +#[allow(unused)] +pub fn file_ext(filename: &str) -> Option<&str> { + Path::new(filename).extension().and_then(OsStr::to_str) +}