Skip to content

Commit

Permalink
Brush randomization (#123)
Browse files Browse the repository at this point in the history
* feat: add brush density control

* feat: add brush ID randomization setting

* fix: fix brush undo being broken after upgrade to egui 0.26
  • Loading branch information
white-axe authored Apr 25, 2024
1 parent fd24f72 commit 392a9cb
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 18 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ rfd = "0.12.0"
tempfile = "3.8.1"

rand = "0.8.5"
murmur3 = "0.5.2"

alacritty_terminal = "0.22.0"

Expand Down
1 change: 1 addition & 0 deletions crates/components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ fragile.workspace = true
parking_lot.workspace = true

fuzzy-matcher = "0.3.7"
murmur3.workspace = true
3 changes: 2 additions & 1 deletion crates/components/src/map_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
55 changes: 52 additions & 3 deletions crates/components/src/tilepicker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ pub struct Tilepicker {
resources: Arc<Resources>,
viewport: Arc<luminol_graphics::viewport::Viewport>,
ani_time: Option<f64>,

/// 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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -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();

Expand Down
16 changes: 15 additions & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/" }
26 changes: 25 additions & 1 deletion crates/ui/src/tabs/map/brush.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
),
Expand All @@ -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,
),
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -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,
),
Expand Down
45 changes: 33 additions & 12 deletions crates/ui/src/tabs/map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,

Expand All @@ -157,6 +175,9 @@ impl Tab {
tilemap_undo_cache_layer: 0,

passages,

brush_density: 1.,
brush_seed,
})
}
}
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -406,24 +431,20 @@ 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| {
i.pointer.button_down(egui::PointerButton::Primary)
&& !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,
Expand Down
21 changes: 21 additions & 0 deletions crates/ui/src/tabs/map/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 392a9cb

Please sign in to comment.