From bd0d64b4af39d520a36398d1e5d04a1cfc429498 Mon Sep 17 00:00:00 2001 From: grimsace Date: Tue, 28 Apr 2026 14:09:51 -0500 Subject: [PATCH] added stair resizing --- src/exporter.rs | 16 +-- src/layout.rs | 28 +++-- src/main.rs | 314 ++++++++++++++++++++++++++++++++++++++++-------- src/startend.rs | 52 +++++--- 4 files changed, 322 insertions(+), 88 deletions(-) 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.