diff --git a/src/exporter.rs b/src/exporter.rs index 43b5270..01d6296 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -587,7 +587,9 @@ fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String { ("rgb(220,70,70)", None) }; - if let Some((x1, y1, x2, y2)) = door_line_points(&g, door.from, door.to, door.width) { + if let Some((x1, y1, x2, y2)) = + door_line_points(&g, door.from, door.to, door_render_width(door)) + { if let Some(pattern) = dash { let _ = writeln!( s, @@ -938,7 +940,8 @@ fn draw_door( color: (u8, u8, u8, u8), style: DoorStyle, ) { - let Some((x1, y1, x2, y2)) = door_line_points(g, door.from, door.to, door.width) else { + let Some((x1, y1, x2, y2)) = door_line_points(g, door.from, door.to, door_render_width(door)) + else { return; }; draw_line(pixmap, x1, y1, x2, y2, width, color, style); @@ -1097,7 +1100,7 @@ fn door_edges_for(door: &Door, cols: usize, rows: usize) -> Vec<((usize, usize), return edges; } - let width = door.width.max(1) as isize; + let width = door_render_width(door).max(1) as isize; let min_offset = -((width - 1) / 2); let max_offset = width / 2; @@ -1127,3 +1130,11 @@ fn door_edges_for(door: &Door, cols: usize, rows: usize) -> Vec<((usize, usize), edges } + +fn door_render_width(door: &Door) -> usize { + if door.span_width { + door.width.max(1) + } else { + 1 + } +} diff --git a/src/layout.rs b/src/layout.rs index 5384142..8d6e743 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -31,6 +31,7 @@ pub struct Door { pub from: (usize, usize), pub to: (usize, usize), pub width: usize, + pub span_width: bool, pub locked: bool, pub archway: bool, } @@ -245,6 +246,7 @@ pub fn apply_doors( from: edge.0, to: edge.1, width: corridor.width.max(1), + span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, }); @@ -258,6 +260,7 @@ pub fn apply_doors( from: edge.0, to: edge.1, width: corridor.width.max(1), + span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, }); @@ -276,6 +279,7 @@ pub fn apply_doors( from: edge.0, to: edge.1, width: 1, + span_width: false, locked: rng.next_f32() <= locked, archway: false, }); @@ -288,6 +292,7 @@ pub fn apply_doors( from: edge.0, to: edge.1, width: 1, + span_width: false, locked: rng.next_f32() <= locked, archway: false, }); diff --git a/src/main.rs b/src/main.rs index db9464f..f9b96d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ struct DungeonApp { resize_state: Option, hover_room_idx: Option, hover_corridor_idx: Option, + hover_door_idx: Option, } impl Default for DungeonApp { @@ -62,6 +63,7 @@ impl Default for DungeonApp { resize_state: None, hover_room_idx: None, hover_corridor_idx: None, + hover_door_idx: None, } } } @@ -193,6 +195,7 @@ impl eframe::App for DungeonApp { self.draw_add_overlay(&painter, &geometry); self.draw_resize_overlay(&painter, &geometry); self.draw_corridor_hover_overlay(&painter, &geometry); + self.draw_door_hover_overlay(&painter, &geometry); }); } } @@ -445,6 +448,16 @@ impl DungeonApp { self.settings.add_tool = AddTool::None; } } + AddTool::Door | AddTool::LockedDoor => { + self.add_corridor_drag = None; + if response.clicked() + && let Some(pointer_pos) = response.interact_pointer_pos() + { + let locked = self.settings.add_tool == AddTool::LockedDoor; + self.add_door_at_pointer(pointer_pos, geometry, locked); + self.settings.add_tool = AddTool::None; + } + } AddTool::None => { self.add_corridor_drag = None; } @@ -552,17 +565,35 @@ impl DungeonApp { let Some(pointer_pos) = ctx.pointer_hover_pos() else { self.hover_room_idx = None; self.hover_corridor_idx = None; + self.hover_door_idx = None; return; }; + if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) { + let target = normalized_edge(edge.0, edge.1); + self.hover_door_idx = self.layout.doors.iter().position(|d| { + door_edges_for(d, self.settings.cols, self.settings.rows) + .iter() + .any(|e| normalized_edge(e.0, e.1) == target) + }); + if self.hover_door_idx.is_some() { + self.hover_room_idx = None; + self.hover_corridor_idx = None; + return; + } + } + if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) { self.hover_room_idx = Some(room_idx); self.hover_corridor_idx = None; + self.hover_door_idx = None; } else { self.hover_room_idx = None; if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) { self.hover_corridor_idx = Some(corridor_drag.corridor_index); + self.hover_door_idx = None; } else { self.hover_corridor_idx = None; + self.hover_door_idx = None; } } } @@ -777,7 +808,71 @@ impl DungeonApp { } } + fn draw_door_hover_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) { + let Some(idx) = self.hover_door_idx else { + return; + }; + let Some(door) = self.layout.doors.get(idx) else { + return; + }; + let color = Color32::from_gray(200); + let stroke = Stroke::new(2.0, color); + draw_door_line( + painter, + geometry, + door.from, + door.to, + door_render_width(door), + stroke, + DoorLineStyle::LongDash, + ); + } + fn delete_at_pointer(&mut self, pointer_pos: egui::Pos2, geometry: &GridGeometry) { + if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) { + let target = normalized_edge(edge.0, edge.1); + if let Some(idx) = self.layout.doors.iter().position(|d| { + door_edges_for(d, self.settings.cols, self.settings.rows) + .iter() + .any(|e| normalized_edge(e.0, e.1) == target) + }) { + let edges = door_edges_for( + &self.layout.doors[idx], + self.settings.cols, + self.settings.rows, + ); + let mut spawned = Vec::new(); + for edge in edges { + if normalized_edge(edge.0, edge.1) == target { + continue; + } + if self + .layout + .doors + .iter() + .any(|d| normalized_edge(d.from, d.to) == normalized_edge(edge.0, edge.1)) + { + continue; + } + spawned.push(layout::Door { + from: edge.0, + to: edge.1, + width: 1, + span_width: false, + locked: false, + archway: true, + }); + } + let door = &mut self.layout.doors[idx]; + door.locked = false; + door.archway = true; + door.width = 1; + door.span_width = false; + self.layout.doors.extend(spawned); + return; + } + } + if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) { self.delete_room(room_idx); return; @@ -811,6 +906,132 @@ impl DungeonApp { self.drag_state = None; self.refresh_doors(); } + + fn add_door_at_pointer( + &mut self, + pointer_pos: egui::Pos2, + geometry: &GridGeometry, + locked: bool, + ) { + let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else { + return; + }; + let target = normalized_edge(edge.0, edge.1); + if let Some(idx) = self.layout.doors.iter().position(|d| { + door_edges_for(d, self.settings.cols, self.settings.rows) + .iter() + .any(|e| normalized_edge(e.0, e.1) == target) + }) { + let edges = door_edges_for( + &self.layout.doors[idx], + self.settings.cols, + self.settings.rows, + ); + let mut spawned = Vec::new(); + for edge in edges { + if normalized_edge(edge.0, edge.1) == target { + continue; + } + if self + .layout + .doors + .iter() + .any(|d| normalized_edge(d.from, d.to) == normalized_edge(edge.0, edge.1)) + { + continue; + } + spawned.push(layout::Door { + from: edge.0, + to: edge.1, + width: 1, + span_width: false, + locked: false, + archway: true, + }); + } + let door = &mut self.layout.doors[idx]; + door.locked = locked; + door.archway = false; + door.width = 1; + door.span_width = false; + self.layout.doors.extend(spawned); + return; + } + + let corridor_cells = corridor_cells(&self.layout, self.settings.cols, self.settings.rows); + let a_in_room = room_index_at_cell(&self.layout.rooms, edge.0).is_some(); + let b_in_room = room_index_at_cell(&self.layout.rooms, edge.1).is_some(); + let a_in_corridor = corridor_cells.contains(&edge.0); + let b_in_corridor = corridor_cells.contains(&edge.1); + + if !(a_in_room ^ b_in_room) { + return; + } + if !(a_in_corridor ^ b_in_corridor) { + return; + } + + self.layout.doors.push(layout::Door { + from: edge.0, + to: edge.1, + width: 1, + span_width: false, + locked, + archway: false, + }); + } + + fn door_edge_at_pointer( + &self, + pointer_pos: egui::Pos2, + geometry: &GridGeometry, + ) -> Option<((usize, usize), (usize, usize))> { + let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; + let cell_x = grid_x.floor() as isize; + let cell_y = grid_y.floor() as isize; + if cell_x < 0 + || cell_y < 0 + || cell_x >= self.settings.cols as isize + || cell_y >= self.settings.rows as isize + { + return None; + } + + let fx = grid_x - grid_x.floor(); + let fy = grid_y - grid_y.floor(); + let left = fx; + let right = 1.0 - fx; + let top = fy; + let bottom = 1.0 - fy; + + let mut candidates = [ + (left, (cell_x - 1, cell_y), (cell_x, cell_y)), + (right, (cell_x + 1, cell_y), (cell_x, cell_y)), + (top, (cell_x, cell_y - 1), (cell_x, cell_y)), + (bottom, (cell_x, cell_y + 1), (cell_x, cell_y)), + ]; + candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + let (dist, a, b) = candidates[0]; + if dist > 0.2 { + return None; + } + + let (ax, ay) = a; + let (bx, by) = b; + if ax < 0 + || ay < 0 + || bx < 0 + || by < 0 + || ax >= self.settings.cols as isize + || bx >= self.settings.cols as isize + || ay >= self.settings.rows as isize + || by >= self.settings.rows as isize + { + return None; + } + + Some(((ax as usize, ay as usize), (bx as usize, by as usize))) + } } #[derive(Debug, Clone)] @@ -1117,7 +1338,7 @@ fn draw_layout( geometry, door.from, door.to, - door.width, + door_render_width(door), Stroke::new(door_width_px, color), style, ); @@ -1287,7 +1508,7 @@ fn door_edges_for( return edges; } - let width = door.width.max(1) as isize; + let width = door_render_width(door).max(1) as isize; let min_offset = -((width - 1) / 2); let max_offset = width / 2; @@ -1318,6 +1539,14 @@ fn door_edges_for( edges } +fn door_render_width(door: &layout::Door) -> usize { + if door.span_width { + door.width.max(1) + } else { + 1 + } +} + fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> { if !geometry.rect.contains(pointer_pos) { return None; diff --git a/src/ui.rs b/src/ui.rs index 76010e9..176f0d1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -153,6 +153,8 @@ pub enum AddTool { None, Room, Corridor, + Door, + LockedDoor, } #[derive(Debug, Clone, Copy, Default)] @@ -557,12 +559,20 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings) { ui.horizontal(|ui| { ui.selectable_value(&mut settings.add_tool, AddTool::Room, "Add Room"); ui.selectable_value(&mut settings.add_tool, AddTool::Corridor, "Add Corridor"); + ui.selectable_value(&mut settings.add_tool, AddTool::Door, "Add Door"); + ui.selectable_value( + &mut settings.add_tool, + AddTool::LockedDoor, + "Add Locked Door", + ); }); ui.add_space(6.0); match settings.add_tool { AddTool::Room => ui.label("Click to place a room at the grid cell."), AddTool::Corridor => ui.label("Click-drag to place a corridor between two cells."), + AddTool::Door => ui.label("Click a room/corridor edge to place a door."), + AddTool::LockedDoor => ui.label("Click a room/corridor edge to place a locked door."), AddTool::None => ui.label("Select a tool to add rooms or corridors."), }; }