added ctrl+z
This commit is contained in:
+5
-5
@@ -2,7 +2,7 @@ use std::collections::{HashSet, VecDeque};
|
|||||||
|
|
||||||
use crate::seed;
|
use crate::seed;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
pub x: usize,
|
pub x: usize,
|
||||||
pub y: usize,
|
pub y: usize,
|
||||||
@@ -17,7 +17,7 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Corridor {
|
pub struct Corridor {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@@ -27,7 +27,7 @@ pub struct Corridor {
|
|||||||
pub width: usize,
|
pub width: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Door {
|
pub struct Door {
|
||||||
pub from: (usize, usize),
|
pub from: (usize, usize),
|
||||||
pub to: (usize, usize),
|
pub to: (usize, usize),
|
||||||
@@ -39,7 +39,7 @@ pub struct Door {
|
|||||||
pub manual: bool,
|
pub manual: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub cell: (usize, usize),
|
pub cell: (usize, usize),
|
||||||
pub side: WindowSide,
|
pub side: WindowSide,
|
||||||
@@ -74,7 +74,7 @@ pub struct WindowSettings {
|
|||||||
pub allow_internal_windows: bool,
|
pub allow_internal_windows: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct DungeonLayout {
|
pub struct DungeonLayout {
|
||||||
pub rooms: Vec<Room>,
|
pub rooms: Vec<Room>,
|
||||||
pub corridors: Vec<Corridor>,
|
pub corridors: Vec<Corridor>,
|
||||||
|
|||||||
+84
@@ -23,6 +23,13 @@ enum ManualDoorKind {
|
|||||||
Secret,
|
Secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct AppSnapshot {
|
||||||
|
settings: UiSettings,
|
||||||
|
layout: DungeonLayout,
|
||||||
|
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||||
|
}
|
||||||
|
|
||||||
// Boot the native app and start the egui event loop.
|
// Boot the native app and start the egui event loop.
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
let options = eframe::NativeOptions::default();
|
let options = eframe::NativeOptions::default();
|
||||||
@@ -38,6 +45,8 @@ struct DungeonApp {
|
|||||||
settings: UiSettings,
|
settings: UiSettings,
|
||||||
layout: DungeonLayout,
|
layout: DungeonLayout,
|
||||||
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||||
|
undo_stack: Vec<AppSnapshot>,
|
||||||
|
redo_stack: Vec<AppSnapshot>,
|
||||||
drag_state: Option<DragState>,
|
drag_state: Option<DragState>,
|
||||||
export_rx: Option<mpsc::Receiver<Result<std::path::PathBuf, String>>>,
|
export_rx: Option<mpsc::Receiver<Result<std::path::PathBuf, String>>>,
|
||||||
pending_delete: bool,
|
pending_delete: bool,
|
||||||
@@ -72,6 +81,8 @@ impl Default for DungeonApp {
|
|||||||
settings,
|
settings,
|
||||||
layout,
|
layout,
|
||||||
suppressed_auto_door_edges: HashSet::new(),
|
suppressed_auto_door_edges: HashSet::new(),
|
||||||
|
undo_stack: Vec::new(),
|
||||||
|
redo_stack: Vec::new(),
|
||||||
drag_state: None,
|
drag_state: None,
|
||||||
export_rx: None,
|
export_rx: None,
|
||||||
pending_delete: false,
|
pending_delete: false,
|
||||||
@@ -97,6 +108,11 @@ impl eframe::App for DungeonApp {
|
|||||||
// Render UI, handle input, and update app state each frame.
|
// Render UI, handle input, and update app state each frame.
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
let panel_result = draw_side_panel(ctx, &mut self.settings, self.export_rx.is_some());
|
let panel_result = draw_side_panel(ctx, &mut self.settings, self.export_rx.is_some());
|
||||||
|
let undo_requested = ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Z));
|
||||||
|
let redo_requested = ctx.input(|i| {
|
||||||
|
(i.modifiers.command && i.key_pressed(egui::Key::Y))
|
||||||
|
|| (i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::Z))
|
||||||
|
});
|
||||||
|
|
||||||
if self.settings.min_room_size > self.settings.max_room_size {
|
if self.settings.min_room_size > self.settings.max_room_size {
|
||||||
self.settings.max_room_size = self.settings.min_room_size;
|
self.settings.max_room_size = self.settings.min_room_size;
|
||||||
@@ -111,9 +127,17 @@ impl eframe::App for DungeonApp {
|
|||||||
self.settings.max_window_width = self.settings.min_window_width;
|
self.settings.max_window_width = self.settings.min_window_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if undo_requested {
|
||||||
|
self.undo();
|
||||||
|
} else if redo_requested {
|
||||||
|
self.redo();
|
||||||
|
}
|
||||||
|
|
||||||
if panel_result.clear_clicked {
|
if panel_result.clear_clicked {
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.clear_layout();
|
self.clear_layout();
|
||||||
} else if panel_result.reset_clicked || panel_result.settings_changed {
|
} else if panel_result.reset_clicked || panel_result.settings_changed {
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.regenerate_layout();
|
self.regenerate_layout();
|
||||||
}
|
}
|
||||||
if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
|
if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
|
||||||
@@ -192,6 +216,8 @@ impl eframe::App for DungeonApp {
|
|||||||
ui.heading("Keybinds");
|
ui.heading("Keybinds");
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.label("Delete / Backspace: Remove hovered room or corridor");
|
ui.label("Delete / Backspace: Remove hovered room or corridor");
|
||||||
|
ui.label("Ctrl+Z: Undo");
|
||||||
|
ui.label("Ctrl+Y: Redo");
|
||||||
ui.label("Right click: Cancel add tool");
|
ui.label("Right click: Cancel add tool");
|
||||||
ui.label("Right click + drag: Resize hovered room");
|
ui.label("Right click + drag: Resize hovered room");
|
||||||
});
|
});
|
||||||
@@ -228,6 +254,55 @@ impl eframe::App for DungeonApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DungeonApp {
|
impl DungeonApp {
|
||||||
|
fn snapshot(&self) -> AppSnapshot {
|
||||||
|
AppSnapshot {
|
||||||
|
settings: self.settings.clone(),
|
||||||
|
layout: self.layout.clone(),
|
||||||
|
suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_undo_snapshot(&mut self) {
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
if self.undo_stack.last() == Some(&snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.undo_stack.push(snapshot);
|
||||||
|
if self.undo_stack.len() > 100 {
|
||||||
|
self.undo_stack.remove(0);
|
||||||
|
}
|
||||||
|
self.redo_stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_snapshot(&mut self, snapshot: AppSnapshot) {
|
||||||
|
self.settings = snapshot.settings;
|
||||||
|
self.layout = snapshot.layout;
|
||||||
|
self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges;
|
||||||
|
self.drag_state = None;
|
||||||
|
self.add_corridor_drag = None;
|
||||||
|
self.resize_state = None;
|
||||||
|
self.hover_room_idx = None;
|
||||||
|
self.hover_corridor_idx = None;
|
||||||
|
self.hover_door_idx = None;
|
||||||
|
self.pending_delete = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo(&mut self) {
|
||||||
|
let Some(snapshot) = self.undo_stack.pop() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.redo_stack.push(self.snapshot());
|
||||||
|
self.restore_snapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redo(&mut self) {
|
||||||
|
let Some(snapshot) = self.redo_stack.pop() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.undo_stack.push(self.snapshot());
|
||||||
|
self.restore_snapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the current layout and reset transient interaction state.
|
// Clear the current layout and reset transient interaction state.
|
||||||
fn clear_layout(&mut self) {
|
fn clear_layout(&mut self) {
|
||||||
self.layout = DungeonLayout {
|
self.layout = DungeonLayout {
|
||||||
@@ -284,6 +359,7 @@ impl DungeonApp {
|
|||||||
&& let Some((room_idx, offset_x, offset_y)) =
|
&& let Some((room_idx, offset_x, offset_y)) =
|
||||||
self.room_at_pointer(pointer_pos, geometry)
|
self.room_at_pointer(pointer_pos, geometry)
|
||||||
{
|
{
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.drag_state = Some(DragState::Room(RoomDragState {
|
self.drag_state = Some(DragState::Room(RoomDragState {
|
||||||
room_idx,
|
room_idx,
|
||||||
offset_x,
|
offset_x,
|
||||||
@@ -293,6 +369,7 @@ impl DungeonApp {
|
|||||||
&& let Some(pointer_pos) = response.interact_pointer_pos()
|
&& let Some(pointer_pos) = response.interact_pointer_pos()
|
||||||
&& let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry)
|
&& let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry)
|
||||||
{
|
{
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.drag_state = Some(DragState::Corridor(corridor_drag));
|
self.drag_state = Some(DragState::Corridor(corridor_drag));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,6 +645,7 @@ impl DungeonApp {
|
|||||||
|
|
||||||
// Add a room anchored to the given grid cell if space allows.
|
// Add a room anchored to the given grid cell if space allows.
|
||||||
fn add_room_at_cell(&mut self, cell: (usize, usize)) {
|
fn add_room_at_cell(&mut self, cell: (usize, usize)) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
let width = self.settings.min_room_size.max(1);
|
let width = self.settings.min_room_size.max(1);
|
||||||
let height = self.settings.min_room_size.max(1);
|
let height = self.settings.min_room_size.max(1);
|
||||||
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
|
||||||
@@ -603,6 +681,7 @@ impl DungeonApp {
|
|||||||
|
|
||||||
// Add a corridor between two room cells using a shortest path.
|
// Add a corridor between two room cells using a shortest path.
|
||||||
fn add_corridor_between(&mut self, start: (usize, usize), end: (usize, usize)) {
|
fn add_corridor_between(&mut self, start: (usize, usize), end: (usize, usize)) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
if start == end {
|
if start == end {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -694,6 +773,7 @@ impl DungeonApp {
|
|||||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||||
if let Some(room_idx) = self.resize_room_at_pointer(pointer_pos, geometry) {
|
if let Some(room_idx) = self.resize_room_at_pointer(pointer_pos, geometry) {
|
||||||
if let Some(state) = self.start_resize(room_idx, pointer_pos, geometry) {
|
if let Some(state) = self.start_resize(room_idx, pointer_pos, geometry) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.resize_state = Some(state);
|
self.resize_state = Some(state);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -956,6 +1036,7 @@ impl DungeonApp {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|e| normalized_edge(e.0, e.1) == target)
|
.any(|e| normalized_edge(e.0, e.1) == target)
|
||||||
}) {
|
}) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
if !self.layout.doors[idx].manual {
|
if !self.layout.doors[idx].manual {
|
||||||
for edge in door_edges_for(
|
for edge in door_edges_for(
|
||||||
&self.layout.doors[idx],
|
&self.layout.doors[idx],
|
||||||
@@ -1010,12 +1091,14 @@ impl DungeonApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) {
|
if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.delete_room(room_idx);
|
self.delete_room(room_idx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) {
|
if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) {
|
||||||
if corridor_drag.corridor_index < self.layout.corridors.len() {
|
if corridor_drag.corridor_index < self.layout.corridors.len() {
|
||||||
|
self.push_undo_snapshot();
|
||||||
self.layout.corridors.remove(corridor_drag.corridor_index);
|
self.layout.corridors.remove(corridor_drag.corridor_index);
|
||||||
self.drag_state = None;
|
self.drag_state = None;
|
||||||
self.refresh_doors();
|
self.refresh_doors();
|
||||||
@@ -1051,6 +1134,7 @@ impl DungeonApp {
|
|||||||
geometry: &GridGeometry,
|
geometry: &GridGeometry,
|
||||||
kind: ManualDoorKind,
|
kind: ManualDoorKind,
|
||||||
) {
|
) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else {
|
let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user