From b179a575f4a9b48fed0e0a9c3cd3d860beb906d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Wed, 24 Apr 2024 22:59:37 -0400 Subject: [PATCH] Brush randomization (#123) * feat: add brush density control * feat: add brush ID randomization setting * fix: fix brush undo being broken after upgrade to egui 0.26 --- Cargo.lock | 8 +++++ Cargo.toml | 1 + crates/components/Cargo.toml | 1 + crates/components/src/map_view.rs | 3 +- crates/components/src/tilepicker.rs | 55 +++++++++++++++++++++++++++-- crates/core/src/lib.rs | 16 ++++++++- crates/ui/Cargo.toml | 2 ++ crates/ui/src/tabs/map/brush.rs | 26 +++++++++++++- crates/ui/src/tabs/map/mod.rs | 45 ++++++++++++++++------- crates/ui/src/tabs/map/util.rs | 21 +++++++++++ src/app/top_bar.rs | 14 ++++++++ 11 files changed, 174 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ad9dd93..4b688b12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2933,6 +2933,7 @@ dependencies = [ "luminol-egui-wgpu", "luminol-filesystem", "luminol-graphics", + "murmur3", "parking_lot", "qp-trie", "strum", @@ -3174,6 +3175,7 @@ dependencies = [ "luminol-macros", "luminol-modals", "luminol-term", + "murmur3", "once_cell", "poll-promise", "qp-trie", @@ -3394,6 +3396,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "murmur3" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" + [[package]] name = "naga" version = "0.19.0" diff --git a/Cargo.toml b/Cargo.toml index acec6fdc..45115bbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ rfd = "0.12.0" tempfile = "3.8.1" rand = "0.8.5" +murmur3 = "0.5.2" alacritty_terminal = "0.22.0" diff --git a/crates/components/Cargo.toml b/crates/components/Cargo.toml index aa03725e..c8caf78c 100644 --- a/crates/components/Cargo.toml +++ b/crates/components/Cargo.toml @@ -48,3 +48,4 @@ fragile.workspace = true parking_lot.workspace = true fuzzy-matcher = "0.3.7" +murmur3.workspace = true diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index d06f6627..a17f215c 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -352,7 +352,8 @@ impl MapView { ); let pattern_rect = egui::Rect::from_min_size( map_rect.min + (self.cursor_pos.to_vec2() * tile_size), - if !force_show_pattern_rect && drawing_shape_pos.is_some() { + if tilepicker.brush_random || (!force_show_pattern_rect && drawing_shape_pos.is_some()) + { egui::Vec2::splat(tile_size) } else { egui::vec2( diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index 568c4dfc..4e63f9a6 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -34,6 +34,11 @@ pub struct Tilepicker { resources: Arc, viewport: Arc, ani_time: Option, + + /// When true, brush tile ID randomization is enabled. + pub brush_random: bool, + /// Seed for the PRNG used for the brush when brush tile ID randomization is enabled. + brush_seed: [u8; 16], } struct Resources { @@ -174,6 +179,18 @@ impl Tilepicker { &passages, ); + let mut brush_seed = [0u8; 16]; + brush_seed[0..8].copy_from_slice( + &update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .persistence_id + .to_le_bytes(), + ); + brush_seed[8..16].copy_from_slice(&(map_id as u64).to_le_bytes()); + Ok(Self { resources: Arc::new(Resources { tiles, @@ -189,14 +206,44 @@ impl Tilepicker { coll_enabled: false, grid_enabled: true, drag_origin: None, + brush_seed, + brush_random: false, }) } - pub fn get_tile_from_offset(&self, x: i16, y: i16) -> SelectedTile { + pub fn get_tile_from_offset( + &self, + absolute_x: i16, + absolute_y: i16, + absolute_z: i16, + relative_x: i16, + relative_y: i16, + ) -> SelectedTile { let width = self.selected_tiles_right - self.selected_tiles_left + 1; let height = self.selected_tiles_bottom - self.selected_tiles_top + 1; - let x = self.selected_tiles_left + x.rem_euclid(width); - let y = self.selected_tiles_top + y.rem_euclid(height); + + let (x, y) = if self.brush_random { + let mut preimage = [0u8; 40]; + preimage[0..16].copy_from_slice(&self.brush_seed); + preimage[16..24].copy_from_slice(&(absolute_x as u64).to_le_bytes()); + preimage[24..32].copy_from_slice(&(absolute_y as u64).to_le_bytes()); + preimage[32..40].copy_from_slice(&(absolute_z as u64).to_le_bytes()); + let image = murmur3::murmur3_32(&mut std::io::Cursor::new(preimage), 5381).unwrap(); + let x = (image & 0xffff) as i16; + let y = (image >> 16) as i16; + ( + self.selected_tiles_left + + (self.selected_tiles_left + x.rem_euclid(width)).rem_euclid(width), + self.selected_tiles_top + + (self.selected_tiles_top + y.rem_euclid(height)).rem_euclid(height), + ) + } else { + ( + self.selected_tiles_left + relative_x.rem_euclid(width), + self.selected_tiles_top + relative_y.rem_euclid(height), + ) + }; + match y { ..=0 => SelectedTile::Autotile(x), _ => SelectedTile::Tile(x + (y - 1) * 8 + 384), @@ -209,6 +256,8 @@ impl Tilepicker { ui: &mut egui::Ui, scroll_rect: egui::Rect, ) -> egui::Response { + self.brush_random = update_state.toolbar.brush_random != ui.input(|i| i.modifiers.alt); + let time = ui.ctx().input(|i| i.time); let graphics_state = update_state.graphics.clone(); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 950a1174..7f32bd48 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -147,10 +147,14 @@ impl ModifiedState { } #[allow(missing_docs)] -#[derive(Default)] pub struct ToolbarState { /// The currently selected pencil. pub pencil: Pencil, + /// Brush density between 0 and 1 inclusive; determines the proportion of randomly chosen tiles + /// the brush draws on if less than 1 + pub brush_density: f32, + /// Whether or not brush tile ID randomization is active. + pub brush_random: bool, } #[derive(Default, strum::EnumIter, strum::Display, PartialEq, Eq, Clone, Copy)] @@ -163,6 +167,16 @@ pub enum Pencil { Fill, } +impl Default for ToolbarState { + fn default() -> Self { + Self { + pencil: Default::default(), + brush_density: 1., + brush_random: false, + } + } +} + impl<'res> UpdateState<'res> { pub(crate) fn reborrow_with_edit_window<'this>( &'this mut self, diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 0ef6c542..018f1eb3 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -55,5 +55,7 @@ color-eyre.workspace = true wgpu.workspace = true +murmur3.workspace = true + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] luminol-term = { version = "0.4.0", path = "../term/" } diff --git a/crates/ui/src/tabs/map/brush.rs b/crates/ui/src/tabs/map/brush.rs index bdc7ec46..6ed35d75 100644 --- a/crates/ui/src/tabs/map/brush.rs +++ b/crates/ui/src/tabs/map/brush.rs @@ -46,13 +46,19 @@ impl super::Tab { match pencil { luminol_core::Pencil::Pen => { + let (rect_width, rect_height) = if self.tilepicker.brush_random { + (1, 1) + } else { + (width, height) + }; + let drawing_shape_pos = if let Some(drawing_shape_pos) = self.drawing_shape_pos { drawing_shape_pos } else { self.drawing_shape_pos = Some(map_pos); map_pos }; - for (y, x) in (0..height).cartesian_product(0..width) { + for (y, x) in (0..rect_height).cartesian_product(0..rect_width) { let absolute_x = map_x + x as usize; let absolute_y = map_y + y as usize; @@ -64,6 +70,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + absolute_x as i16, + absolute_y as i16, + tile_layer as i16, x + (map_x as f32 - drawing_shape_pos.x) as i16, y + (map_y as f32 - drawing_shape_pos.y) as i16, ), @@ -87,6 +96,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + position.0 as i16, + position.1 as i16, + tile_layer as i16, position.0 as i16 - drawing_shape_pos.x as i16, position.1 as i16 - drawing_shape_pos.y as i16, ), @@ -159,6 +171,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + x as i16, + y as i16, + tile_layer as i16, x as i16 - drawing_shape_pos.x as i16, y as i16 - drawing_shape_pos.y as i16, ), @@ -202,6 +217,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + map_x as i16, + map_y as i16, + tile_layer as i16, map_x as i16 - drawing_shape_pos.x as i16, map_y as i16 - drawing_shape_pos.y as i16, ), @@ -250,6 +268,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + x as i16, + y as i16, + tile_layer as i16, x as i16 - drawing_shape_pos.x as i16, y as i16 - drawing_shape_pos.y as i16, ), @@ -292,6 +313,9 @@ impl super::Tab { self.set_tile( map, self.tilepicker.get_tile_from_offset( + x as i16, + y as i16, + tile_layer as i16, x as i16 - drawing_shape_pos.x as i16, y as i16 - drawing_shape_pos.y as i16, ), diff --git a/crates/ui/src/tabs/map/mod.rs b/crates/ui/src/tabs/map/mod.rs index 4eb0cef9..7d24040a 100644 --- a/crates/ui/src/tabs/map/mod.rs +++ b/crates/ui/src/tabs/map/mod.rs @@ -85,6 +85,12 @@ pub struct Tab { /// This stores the passage values for every position on the map so that we can figure out /// which passage values have changed in the current frame passages: luminol_data::Table2, + + /// Brush density between 0 and 1 inclusive; determines the proportion of randomly chosen tiles + /// the brush draws on if less than 1 + brush_density: f32, + /// Seed for the PRNG used for the brush when brush density is less than 1 + brush_seed: [u8; 16], } // TODO: If we add support for changing event IDs, these need to be added as history entries @@ -133,6 +139,18 @@ impl Tab { |x, y, passage| passages[(x, y)] = passage, ); + let mut brush_seed = [0u8; 16]; + brush_seed[0..8].copy_from_slice( + &update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .persistence_id + .to_le_bytes(), + ); + brush_seed[8..16].copy_from_slice(&(id as u64).to_le_bytes()); + Ok(Self { id, @@ -157,6 +175,9 @@ impl Tab { tilemap_undo_cache_layer: 0, passages, + + brush_density: 1., + brush_seed, }) } } @@ -190,6 +211,8 @@ impl luminol_core::Tab for Tab { update_state: &mut luminol_core::UpdateState<'_>, is_focused: bool, ) { + self.brush_density = update_state.toolbar.brush_density; + // Display the toolbar. egui::TopBottomPanel::top(format!("map_{}_toolbar", self.id)).show_inside(ui, |ui| { ui.horizontal_wrapped(|ui| { @@ -377,7 +400,9 @@ impl luminol_core::Tab for Tab { } } - if !response.dragged_by(egui::PointerButton::Primary) { + if !response.is_pointer_button_down_on() + || ui.input(|i| !i.pointer.button_down(egui::PointerButton::Primary)) + { if self.drawing_shape { self.drawing_shape = false; } @@ -406,17 +431,6 @@ impl luminol_core::Tab for Tab { if let luminol_components::SelectedLayer::Tiles(tile_layer) = self.view.selected_layer { - // Before drawing tiles, save the state of the current layer so we can undo it - // later if we need to - if response.drag_started_by(egui::PointerButton::Primary) - && !ui.input(|i| i.modifiers.command) - { - self.tilemap_undo_cache_layer = tile_layer; - for i in 0..self.layer_cache.len() { - self.tilemap_undo_cache[i] = self.layer_cache[i]; - } - } - // Tile drawing if response.is_pointer_button_down_on() && ui.input(|i| { @@ -424,6 +438,13 @@ impl luminol_core::Tab for Tab { && !i.modifiers.command }) { + if self.drawing_shape_pos.is_none() { + // Before drawing tiles, save the state of the current layer so we can + // undo it later if we need to + self.tilemap_undo_cache_layer = tile_layer; + self.tilemap_undo_cache.copy_from_slice(&self.layer_cache); + } + self.handle_brush( map_x as usize, map_y as usize, diff --git a/crates/ui/src/tabs/map/util.rs b/crates/ui/src/tabs/map/util.rs index 07a51851..1192a876 100644 --- a/crates/ui/src/tabs/map/util.rs +++ b/crates/ui/src/tabs/map/util.rs @@ -148,6 +148,27 @@ impl super::Tab { tile: luminol_components::SelectedTile, position: (usize, usize, usize), ) { + if self.brush_density != 1. { + if self.brush_density == 0. { + return; + } + + // Pick a pseudorandom normal f32 uniformly in the interval [0, 1) + let mut preimage = [0u8; 40]; + preimage[0..16].copy_from_slice(&self.brush_seed); + preimage[16..24].copy_from_slice(&(position.0 as u64).to_le_bytes()); + preimage[24..32].copy_from_slice(&(position.1 as u64).to_le_bytes()); + preimage[32..40].copy_from_slice(&(position.2 as u64).to_le_bytes()); + let image = (murmur3::murmur3_32(&mut std::io::Cursor::new(preimage), 1729).unwrap() + & 16777215) as f32 + / 16777216f32; + + // Set the tile only if that's less than the brush density + if image >= self.brush_density { + return; + } + } + map.data[position] = tile.to_id(); for y in -1i8..=1i8 { diff --git a/src/app/top_bar.rs b/src/app/top_bar.rs index 5d18bb16..5e055229 100644 --- a/src/app/top_bar.rs +++ b/src/app/top_bar.rs @@ -424,6 +424,20 @@ impl TopBar { ui.selectable_value(&mut update_state.toolbar.pencil, brush, brush.to_string()); } + ui.add(egui::Slider::new( + &mut update_state.toolbar.brush_density, + 0.0..=1.0, + )) + .on_hover_text("The proportion of tiles the brush is able to draw on"); + + let alt_down = ui.input(|i| i.modifiers.alt); + let mut brush_random = update_state.toolbar.brush_random != alt_down; + ui.add(egui::Checkbox::new( + &mut brush_random, "Randomize ID", + )) + .on_hover_text("If enabled, the brush will randomly place tiles out of the selected tiles in the tilepicker instead of placing them in a pattern"); + update_state.toolbar.brush_random = brush_random != alt_down; + if open_project { update_state.project_manager.open_project_picker(); }