diff --git a/src/exporter.rs b/src/exporter.rs
index 432ae51..7332823 100644
--- a/src/exporter.rs
+++ b/src/exporter.rs
@@ -889,8 +889,8 @@ pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
for stair in &layout.stairs {
let x = g.left() + stair.cell.0 as f32 * g.cell;
let y = g.top() + stair.cell.1 as f32 * g.cell;
- let w = stair.size as f32 * g.cell;
- let h = stair.size as f32 * g.cell;
+ let w = stair.width as f32 * g.cell;
+ let h = stair.height as f32 * g.cell;
let fill = if settings.colorblind_mode {
"rgb(180,180,180)"
@@ -901,7 +901,7 @@ pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
s,
""
);
- let steps = (stair.size as f32 * 2.0).round() as usize;
+ let steps = (stair.height as f32 * 2.0).round() as usize;
let step_h = h / steps as f32;
for i in 0..steps {
let sy = y + (i as f32 * step_h) + step_h * 0.5;
@@ -1155,8 +1155,8 @@ fn fill_rect_staircase(
let Some(rect) = Rect::from_xywh(
g.left() + staircase.cell.0 as f32 * g.cell,
g.top() + staircase.cell.1 as f32 * g.cell,
- staircase.size as f32 * g.cell,
- staircase.size as f32 * g.cell,
+ staircase.width as f32 * g.cell,
+ staircase.height as f32 * g.cell,
) else {
return;
};
@@ -1181,8 +1181,8 @@ fn draw_staircase(
let Some(rect) = Rect::from_xywh(
g.left() + staircase.cell.0 as f32 * g.cell,
g.top() + staircase.cell.1 as f32 * g.cell,
- staircase.size as f32 * g.cell,
- staircase.size as f32 * g.cell,
+ staircase.width as f32 * g.cell,
+ staircase.height as f32 * g.cell,
) else {
return;
};
@@ -1206,7 +1206,7 @@ fn draw_staircase(
draw_wall_segment(pixmap, x, y, x, y + h, 2.0, stroke_color);
draw_wall_segment(pixmap, x + w, y, x + w, y + h, 2.0, stroke_color);
- let steps = (staircase.size as f32 * 2.0).round() as usize;
+ let steps = (staircase.height as f32 * 2.0).round() as usize;
let step_h = h / steps as f32;
for i in 0..steps {
let sy = (y + (i as f32 * step_h) + step_h * 0.5).round();
diff --git a/src/layout.rs b/src/layout.rs
index f441256..df99884 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -64,7 +64,8 @@ pub struct AreaMarker {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Staircase {
pub cell: (usize, usize),
- pub size: usize,
+ pub width: usize,
+ pub height: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -1384,13 +1385,26 @@ fn shuffle_rooms(rooms: &mut [Room], rng: &mut SimpleRng) {
}
}
-fn rooms_overlap(a: &Room, b: &Room) -> bool {
- let a_right = a.x + a.width;
- let a_bottom = a.y + a.height;
- let b_right = b.x + b.width;
- let b_bottom = b.y + b.height;
+fn rects_overlap(
+ ax: usize,
+ ay: usize,
+ aw: usize,
+ ah: usize,
+ bx: usize,
+ by: usize,
+ bw: usize,
+ bh: usize,
+) -> bool {
+ let a_right = ax + aw;
+ let a_bottom = ay + ah;
+ let b_right = bx + bw;
+ let b_bottom = by + bh;
- a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
+ ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
+}
+
+fn rooms_overlap(a: &Room, b: &Room) -> bool {
+ rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
}
fn rooms_touch(a: &Room, b: &Room) -> bool {
diff --git a/src/main.rs b/src/main.rs
index bfacc89..6bc8d13 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1194,7 +1194,8 @@ impl DungeonApp {
let stair = layout::Staircase {
cell: (origin_x, origin_y),
- size,
+ width: size,
+ height: size,
};
if self.settings.sync_stairs_across_levels {
@@ -1414,9 +1415,9 @@ impl DungeonApp {
layout.stairs.iter().position(|stair| {
clicked.0 >= stair.cell.0
- && clicked.0 < stair.cell.0 + stair.size
+ && clicked.0 < stair.cell.0 + stair.width
&& clicked.1 >= stair.cell.1
- && clicked.1 < stair.cell.1 + stair.size
+ && clicked.1 < stair.cell.1 + stair.height
})
}
@@ -1488,16 +1489,16 @@ impl DungeonApp {
return;
}
- let stair_size = {
+ let (stair_width, stair_height) = {
let layout = &self.levels[self.settings.active_level_index];
let Some(stair) = layout.stairs.get(drag.stair_idx) else {
return;
};
- stair.size
+ (stair.width, stair.height)
};
- let max_x = self.settings.cols.saturating_sub(stair_size);
- let max_y = self.settings.rows.saturating_sub(stair_size);
+ let max_x = self.settings.cols.saturating_sub(stair_width);
+ let max_y = self.settings.rows.saturating_sub(stair_height);
let target_x = (grid_x - drag.offset_x).floor().max(0.0) as usize;
let target_y = (grid_y - drag.offset_y).floor().max(0.0) as usize;
let new_cell = (target_x.min(max_x), target_y.min(max_y));
@@ -1565,6 +1566,14 @@ impl DungeonApp {
self.resize_state = Some(ResizeState::Room(state));
return true;
}
+ } else if let Some(stair_idx) = self.stair_at_pointer(pointer_pos, geometry) {
+ if let Some(state) =
+ self.start_staircase_resize(stair_idx, pointer_pos, geometry)
+ {
+ self.push_undo_snapshot();
+ self.resize_state = Some(ResizeState::Staircase(state));
+ return true;
+ }
}
}
}
@@ -1579,6 +1588,7 @@ impl DungeonApp {
match state {
ResizeState::Room(state) => self.resize_room(&state, cell),
ResizeState::Text(state) => self.resize_text(&state, cell),
+ ResizeState::Staircase(state) => self.resize_staircase(&state, cell),
}
}
}
@@ -1671,6 +1681,38 @@ impl DungeonApp {
})
}
+ fn start_staircase_resize(
+ &self,
+ stair_idx: usize,
+ pointer_pos: egui::Pos2,
+ geometry: &GridGeometry,
+ ) -> Option {
+ if self.settings.active_level_index >= self.levels.len() {
+ return None;
+ }
+ let layout = &self.levels[self.settings.active_level_index];
+
+ let original_stair = layout.stairs.get(stair_idx)?.clone();
+ let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
+ let center_x = original_stair.cell.0 as f32 + original_stair.width as f32 * 0.5;
+ let center_y = original_stair.cell.1 as f32 + original_stair.height as f32 * 0.5;
+ let pointer_x = grid_x;
+ let pointer_y = grid_y;
+
+ let resize_corner = match (pointer_x >= center_x, pointer_y >= center_y) {
+ (false, false) => ResizeCorner::TopLeft,
+ (true, false) => ResizeCorner::TopRight,
+ (false, true) => ResizeCorner::BottomLeft,
+ (true, true) => ResizeCorner::BottomRight,
+ };
+
+ Some(StaircaseResizeState {
+ stair_idx,
+ original_stair,
+ resize_corner,
+ })
+ }
+
// Resize only the corner chosen at drag start while keeping the opposite corner fixed.
fn resize_room(&mut self, state: &RoomResizeState, target_cell: (usize, usize)) {
if self.settings.active_level_index >= self.levels.len() {
@@ -1748,6 +1790,112 @@ impl DungeonApp {
label.font_size = new_size as u16;
}
+ fn resize_staircase(&mut self, state: &StaircaseResizeState, target_cell: (usize, usize)) {
+ if self.settings.active_level_index >= self.levels.len() {
+ return;
+ }
+
+ let cols = self.settings.cols.max(1);
+ let rows = self.settings.rows.max(1);
+
+ let tx = target_cell.0.min(cols - 1) as isize;
+ let ty = target_cell.1.min(rows - 1) as isize;
+ let original = &state.original_stair;
+
+ let left = original.cell.0 as isize;
+ let top = original.cell.1 as isize;
+ let right = (original.cell.0 + original.width - 1) as isize;
+ let bottom = (original.cell.1 + original.height - 1) as isize;
+
+ let (x0, x1, y0, y1) = match state.resize_corner {
+ ResizeCorner::TopLeft => (tx.min(right), right, ty.min(bottom), bottom),
+ ResizeCorner::TopRight => (left, tx.max(left), ty.min(bottom), bottom),
+ ResizeCorner::BottomLeft => (tx.min(right), right, top, ty.max(top)),
+ ResizeCorner::BottomRight => (left, tx.max(left), top, ty.max(top)),
+ };
+
+ let new_stair_cell = (x0 as usize, y0 as usize);
+ let new_stair_width = (x1 - x0 + 1) as usize;
+ let new_stair_height = (y1 - y0 + 1) as usize;
+
+ if new_stair_width == 0 || new_stair_height == 0 {
+ return;
+ }
+
+ // Check for collisions with other stairs or rooms on the current level.
+ let active_layout = &self.levels[self.settings.active_level_index];
+ if active_layout.rooms.iter().any(|room| {
+ rects_overlap(
+ new_stair_cell.0,
+ new_stair_cell.1,
+ new_stair_width,
+ new_stair_height,
+ room.x,
+ room.y,
+ room.width,
+ room.height,
+ )
+ }) {
+ return;
+ }
+ if active_layout.stairs.iter().enumerate().any(|(idx, stair)| {
+ idx != state.stair_idx
+ && rects_overlap(
+ new_stair_cell.0,
+ new_stair_cell.1,
+ new_stair_width,
+ new_stair_height,
+ stair.cell.0,
+ stair.cell.1,
+ stair.width,
+ stair.height,
+ )
+ }) {
+ return;
+ }
+
+ if self.settings.sync_stairs_across_levels {
+ for layout in &mut self.levels {
+ for stair in &mut layout.stairs {
+ if stair.cell == state.original_stair.cell {
+ stair.cell = new_stair_cell;
+ stair.width = new_stair_width;
+ stair.height = new_stair_height;
+ }
+ }
+ }
+ } else {
+ let current_idx = self.settings.active_level_index;
+ let level_indices = vec![
+ Some(current_idx),
+ current_idx.checked_sub(1),
+ if current_idx + 1 < self.levels.len() {
+ Some(current_idx + 1)
+ } else {
+ None
+ },
+ ];
+
+ for idx in level_indices.into_iter().flatten() {
+ let layout = &mut self.levels[idx];
+ for stair in &mut layout.stairs {
+ if stair.cell == state.original_stair.cell {
+ stair.cell = new_stair_cell;
+ stair.width = new_stair_width;
+ stair.height = new_stair_height;
+ }
+ }
+ }
+ }
+
+ // Update the resize state with the new dimensions to allow continuous resizing.
+ if let Some(ResizeState::Staircase(ref mut s)) = self.resize_state {
+ s.original_stair.cell = new_stair_cell;
+ s.original_stair.width = new_stair_width;
+ s.original_stair.height = new_stair_height;
+ }
+ }
+
// Draw dashed overlays for room resizing interactions.
fn draw_resize_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) {
if self.settings.active_level_index >= self.levels.len() {
@@ -1760,7 +1908,7 @@ impl DungeonApp {
.as_ref()
.and_then(|s| match s {
ResizeState::Text(state) => Some(state.text_idx),
- ResizeState::Room(_) => None,
+ _ => None,
})
.or(self.hover_text_idx)
{
@@ -1781,50 +1929,88 @@ impl DungeonApp {
.as_ref()
.and_then(|s| match s {
ResizeState::Room(state) => Some(state.room_idx),
- ResizeState::Text(_) => None,
+ _ => None,
})
.or(self.hover_room_idx);
- let Some(room_idx) = room_idx else {
- return;
- };
- let Some(room) = layout.rooms.get(room_idx) else {
- return;
- };
- let color = Color32::from_gray(170);
- let stroke = Stroke::new(1.5, color);
- let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
- let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
- let right = left + room.width as f32 * geometry.cell_size;
- let bottom = top + room.height as f32 * geometry.cell_size;
+ if let Some(room_idx) = room_idx {
+ if let Some(room) = layout.rooms.get(room_idx) {
+ let color = Color32::from_gray(170);
+ let stroke = Stroke::new(1.5, color);
- let tl = egui::pos2(left, top);
- let tr = egui::pos2(right, top);
- let br = egui::pos2(right, bottom);
- let bl = egui::pos2(left, bottom);
+ let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
+ let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
+ let right = left + room.width as f32 * geometry.cell_size;
+ let bottom = top + room.height as f32 * geometry.cell_size;
- draw_dashed_line(painter, tl, tr, stroke, 6.0, 6.0);
- draw_dashed_line(painter, tr, br, stroke, 6.0, 6.0);
- draw_dashed_line(painter, br, bl, stroke, 6.0, 6.0);
- draw_dashed_line(painter, bl, tl, stroke, 6.0, 6.0);
+ let tl = egui::pos2(left, top);
+ let tr = egui::pos2(right, top);
+ let br = egui::pos2(right, bottom);
+ let bl = egui::pos2(left, bottom);
- let font = egui::FontId::proportional(12.0);
- let center_top = egui::pos2((left + right) * 0.5, top - 10.0);
- let center_left = egui::pos2(left - 12.0, (top + bottom) * 0.5);
- painter.text(
- center_top,
- egui::Align2::CENTER_CENTER,
- "<->",
- font.clone(),
- color,
- );
- painter.text(center_left, egui::Align2::CENTER_CENTER, "^v", font, color);
+ draw_dashed_line(painter, tl, tr, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, tr, br, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, br, bl, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, bl, tl, stroke, 6.0, 6.0);
- if matches!(self.resize_state, Some(ResizeState::Room(_))) {
- draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
- draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
- draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
- draw_resize_handle(painter, bl, ResizeCorner::BottomLeft, color);
+ let font = egui::FontId::proportional(12.0);
+ let center_top = egui::pos2((left + right) * 0.5, top - 10.0);
+ let center_left = egui::pos2(left - 12.0, (top + bottom) * 0.5);
+ painter.text(
+ center_top,
+ egui::Align2::CENTER_CENTER,
+ "<->",
+ font.clone(),
+ color,
+ );
+ painter.text(center_left, egui::Align2::CENTER_CENTER, "^v", font, color);
+
+ if matches!(self.resize_state, Some(ResizeState::Room(_))) {
+ draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
+ draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
+ draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
+ draw_resize_handle(painter, bl, ResizeCorner::BottomLeft, color);
+ }
+ return;
+ }
+ }
+
+ let stair_idx = self
+ .resize_state
+ .as_ref()
+ .and_then(|s| match s {
+ ResizeState::Staircase(state) => Some(state.stair_idx),
+ _ => None,
+ })
+ .or(self.hover_stair_idx);
+
+ if let Some(stair_idx) = stair_idx {
+ if let Some(stair) = layout.stairs.get(stair_idx) {
+ let color = Color32::from_gray(170);
+ let stroke = Stroke::new(1.5, color);
+
+ let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size;
+ let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size;
+ let right = left + stair.width as f32 * geometry.cell_size;
+ let bottom = top + stair.height as f32 * geometry.cell_size;
+
+ let tl = egui::pos2(left, top);
+ let tr = egui::pos2(right, top);
+ let br = egui::pos2(right, bottom);
+ let bl = egui::pos2(left, bottom);
+
+ draw_dashed_line(painter, tl, tr, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, tr, br, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, br, bl, stroke, 6.0, 6.0);
+ draw_dashed_line(painter, bl, tl, stroke, 6.0, 6.0);
+
+ if matches!(self.resize_state, Some(ResizeState::Staircase(_))) {
+ draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
+ draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
+ draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
+ draw_resize_handle(painter, bl, ResizeCorner::BottomLeft, color);
+ }
+ }
}
}
@@ -2248,10 +2434,18 @@ struct TextResizeState {
original_font_size: u16,
}
+#[derive(Debug, Clone)]
+struct StaircaseResizeState {
+ stair_idx: usize,
+ original_stair: layout::Staircase,
+ resize_corner: ResizeCorner,
+}
+
#[derive(Debug, Clone)]
enum ResizeState {
Room(RoomResizeState),
Text(TextResizeState),
+ Staircase(StaircaseResizeState),
}
struct GridGeometry {
@@ -2635,8 +2829,8 @@ fn draw_staircase(
) {
let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size;
let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size;
- let right = left + stair.size as f32 * geometry.cell_size;
- let bottom = top + stair.size as f32 * geometry.cell_size;
+ let right = left + stair.width as f32 * geometry.cell_size;
+ let bottom = top + stair.height as f32 * geometry.cell_size;
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
// Fill with a distinctive color for stairs.
@@ -2665,7 +2859,7 @@ fn draw_staircase(
);
// Draw stair lines (horizontal steps).
- let steps = (stair.size as f32 * 2.0).round() as usize;
+ let steps = (stair.height as f32 * 2.0).round() as usize;
let step_height = (rect.height() / (steps as f32)).max(1.0);
let line_color = if colorblind_mode {
Color32::BLACK
@@ -2886,13 +3080,27 @@ fn room_index_at_cell(rooms: &[layout::Room], cell: (usize, usize)) -> Option bool {
+ let a_right = ax + aw;
+ let a_bottom = ay + ah;
+ let b_right = bx + bw;
+ let b_bottom = by + bh;
+ ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
+}
+
// Check whether two rooms overlap by area.
fn rooms_overlap(a: &layout::Room, b: &layout::Room) -> bool {
- let a_right = a.x + a.width;
- let a_bottom = a.y + a.height;
- let b_right = b.x + b.width;
- let b_bottom = b.y + b.height;
- a.x < b_right && a_right > b.x && a.y < b_bottom && a_bottom > b.y
+ rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
}
fn manual_door_edge_allowed(
diff --git a/src/startend.rs b/src/startend.rs
index 0620c1c..763bba6 100644
--- a/src/startend.rs
+++ b/src/startend.rs
@@ -448,14 +448,15 @@ fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircase) -> bool {
let stair_x = stair.cell.0;
let stair_y = stair.cell.1;
- let stair_size = stair.size;
+ let stair_width = stair.width;
+ let stair_height = stair.height;
// Check if any room already contains the staircase.
for room in &layout.rooms {
if stair_x >= room.x
&& stair_y >= room.y
- && (stair_x + stair_size) <= (room.x + room.width)
- && (stair_y + stair_size) <= (room.y + room.height)
+ && (stair_x + stair_width) <= (room.x + room.width)
+ && (stair_y + stair_height) <= (room.y + room.height)
{
return false;
}
@@ -480,8 +481,8 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
let room = &mut layout.rooms[idx];
let new_x = room.x.min(stair_x);
let new_y = room.y.min(stair_y);
- let new_right = (room.x + room.width).max(stair_x + stair_size);
- let new_bottom = (room.y + room.height).max(stair_y + stair_size);
+ let new_right = (room.x + room.width).max(stair_x + stair_width);
+ let new_bottom = (room.y + room.height).max(stair_y + stair_height);
room.x = new_x;
room.y = new_y;
@@ -493,8 +494,8 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
layout.rooms.push(layout::Room {
x: stair_x,
y: stair_y,
- width: stair_size,
- height: stair_size,
+ width: stair_width,
+ height: stair_height,
});
true
}
@@ -502,16 +503,16 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
// Compute the minimum Manhattan distance between a room and a staircase.
fn room_to_stair_min_dist(room: &layout::Room, stair: &layout::Staircase) -> usize {
- let dx = if stair.cell.0 + stair.size <= room.x {
- room.x - (stair.cell.0 + stair.size)
+ let dx = if stair.cell.0 + stair.width <= room.x {
+ room.x - (stair.cell.0 + stair.width)
} else if stair.cell.0 >= room.x + room.width {
stair.cell.0 - (room.x + room.width)
} else {
0
};
- let dy = if stair.cell.1 + stair.size <= room.y {
- room.y - (stair.cell.1 + stair.size)
+ let dy = if stair.cell.1 + stair.height <= room.y {
+ room.y - (stair.cell.1 + stair.height)
} else if stair.cell.1 >= room.y + room.height {
stair.cell.1 - (room.y + room.height)
} else {
@@ -595,15 +596,19 @@ fn pick_stair_positions(
stair_cells
.into_iter()
.enumerate()
- .map(|(i, cell)| layout::Staircase {
- cell,
- size: random_stair_size(
+ .map(|(i, cell)| {
+ let (width, height) = random_stair_size(
settings,
seed::derive_seed(
settings.seed,
STAIR_MARKER_STREAM_BASE + i as u64 + seed_offset * 1000,
),
- ),
+ );
+ layout::Staircase {
+ cell,
+ width,
+ height,
+ }
})
.collect()
}
@@ -626,11 +631,18 @@ fn has_start_or_end_on_bottom_row(
}
// Pick a random stair size within settings range.
-fn random_stair_size(settings: &UiSettings, seed_value: u64) -> usize {
- let min_size = settings.min_stair_width.min(settings.min_stair_height);
- let max_size = settings.max_stair_width.max(settings.max_stair_height);
- let span = max_size - min_size + 1;
- min_size + (seed_value as usize % span)
+fn random_stair_size(settings: &UiSettings, seed_value: u64) -> (usize, usize) {
+ let w_span = settings
+ .max_stair_width
+ .saturating_sub(settings.min_stair_width)
+ + 1;
+ let h_span = settings
+ .max_stair_height
+ .saturating_sub(settings.min_stair_height)
+ + 1;
+ let width = settings.min_stair_width + (seed_value as usize % w_span);
+ let height = settings.min_stair_height + (seed_value.rotate_left(17) as usize % h_span);
+ (width, height)
}
// Shuffle a vector deterministically using a seed.