diff --git a/src/interact/add_delete.rs b/src/interact/add_delete.rs index 2ddd565..c320e10 100644 --- a/src/interact/add_delete.rs +++ b/src/interact/add_delete.rs @@ -81,6 +81,10 @@ pub fn handle_add_tool(app: &mut DungeonApp, response: &egui::Response, geometry app.add_corridor_drag = None; handle_add_door_tool(app, response, geometry); } + AddTool::Window => { + app.add_corridor_drag = None; + handle_add_window_tool(app, response, geometry); + } AddTool::Text => { app.add_corridor_drag = None; app.add_door_drag = None; @@ -149,6 +153,178 @@ pub fn handle_add_door_tool( } } +// Handles click-and-drag painting for manual window tools. +pub fn handle_add_window_tool( + app: &mut DungeonApp, + response: &egui::Response, + geometry: &GridGeometry, +) { + if response.drag_started() { + app.add_door_drag = Some(AddDoorDrag { + painted_edges: Default::default(), + snapshot_taken: false, + }); + } + + if (response.drag_started() || response.dragged() || response.clicked()) + && let Some(pointer_pos) = response.interact_pointer_pos() + { + paint_window_at_pointer(app, pointer_pos, geometry); + } + + if response.drag_stopped() { + app.add_door_drag = None; + } +} + +// Paints one window edge during a manual window drag. +pub fn paint_window_at_pointer( + app: &mut DungeonApp, + pointer_pos: egui::Pos2, + geometry: &GridGeometry, +) { + let Some(edge) = door_edge_at_pointer(app, pointer_pos, geometry) else { + return; + }; + let target = normalized_edge(edge.0, edge.1); + + if let Some(drag) = &app.add_door_drag + && drag.painted_edges.contains(&target) + { + return; + } + + let needs_snapshot = app + .add_door_drag + .as_ref() + .map(|drag| !drag.snapshot_taken) + .unwrap_or(true); + if needs_snapshot { + app.push_undo_snapshot(); + if let Some(drag) = &mut app.add_door_drag { + drag.snapshot_taken = true; + } + } + + if add_window_at_edge(app, edge) + && let Some(drag) = &mut app.add_door_drag + { + drag.painted_edges.insert(target); + } +} + +// Adds or updates a manual window on a specific cell edge. +pub fn add_window_at_edge(app: &mut DungeonApp, edge: ((usize, usize), (usize, usize))) -> bool { + if app.settings.active_level_index >= app.levels.len() { + return false; + } + + let layout = &mut app.levels[app.settings.active_level_index]; + + let target = normalized_edge(edge.0, edge.1); + + // Check if a door already exists at this edge + if layout.doors.iter().any(|d| { + door_edges_for(d, app.settings.cols, app.settings.rows) + .iter() + .any(|e| normalized_edge(e.0, e.1) == target) + }) { + return false; + } + + // Check if a window already exists at this edge + if layout.windows.iter().any(|w| { + let win_edge = match w.side { + layout::WindowSide::Left => { + ((w.cell.0, w.cell.1), (w.cell.0.saturating_sub(1), w.cell.1)) + } + layout::WindowSide::Right => ((w.cell.0, w.cell.1), (w.cell.0 + 1, w.cell.1)), + layout::WindowSide::Top => { + ((w.cell.0, w.cell.1), (w.cell.0, w.cell.1.saturating_sub(1))) + } + layout::WindowSide::Bottom => ((w.cell.0, w.cell.1), (w.cell.0, w.cell.1 + 1)), + }; + normalized_edge(win_edge.0, win_edge.1) == target + }) { + return true; + } + + let is_boundary = layout::is_room_boundary_edge(&layout.rooms, edge.0, edge.1); + + // Only allow windows on room boundaries (no corridors for windows usually) + if !is_boundary { + return false; + } + + // Determine cell and side for the window + let (cell, side) = if edge.0.0 == edge.1.0 { + // Vertical edge + let top = edge.0.1.min(edge.1.1); + // We need to know which cell is the room cell. + let a_in = layout.rooms.iter().any(|r| { + edge.0.0 >= r.x + && edge.0.0 < r.x + r.width + && edge.0.1 >= r.y + && edge.0.1 < r.y + r.height + }); + if a_in { + ( + edge.0, + if edge.0.1 == top { + layout::WindowSide::Bottom + } else { + layout::WindowSide::Top + }, + ) + } else { + ( + edge.1, + if edge.1.1 == top { + layout::WindowSide::Bottom + } else { + layout::WindowSide::Top + }, + ) + } + } else { + // Horizontal edge + let left = edge.0.0.min(edge.1.0); + let a_in = layout.rooms.iter().any(|r| { + edge.0.0 >= r.x + && edge.0.0 < r.x + r.width + && edge.0.1 >= r.y + && edge.0.1 < r.y + r.height + }); + if a_in { + ( + edge.0, + if edge.0.0 == left { + layout::WindowSide::Right + } else { + layout::WindowSide::Left + }, + ) + } else { + ( + edge.1, + if edge.1.0 == left { + layout::WindowSide::Right + } else { + layout::WindowSide::Left + }, + ) + } + }; + + layout.windows.push(layout::Window { + cell, + side, + width: 1, + span_width: false, + }); + true +} + // Converts the active add tool into a manual door kind. pub fn manual_door_kind_from_tool(tool: AddTool) -> Option { match tool { @@ -243,6 +419,8 @@ pub fn delete_at_pointer(app: &mut DungeonApp, pointer_pos: egui::Pos2, geometry if let Some(edge) = door_edge_at_pointer(app, pointer_pos, geometry) { let target = normalized_edge(edge.0, edge.1); let active_index = app.settings.active_level_index; + + // Try deleting a door first if let Some(idx) = app.levels[active_index].doors.iter().position(|d| { door_edges_for(d, app.settings.cols, app.settings.rows) .iter() @@ -262,6 +440,25 @@ pub fn delete_at_pointer(app: &mut DungeonApp, pointer_pos: egui::Pos2, geometry layout.doors.remove(idx); return; } + + // Try deleting a window if no door was found at this edge + if let Some(idx) = app.levels[active_index].windows.iter().position(|w| { + let win_edge = match w.side { + layout::WindowSide::Left => { + ((w.cell.0, w.cell.1), (w.cell.0.saturating_sub(1), w.cell.1)) + } + layout::WindowSide::Right => ((w.cell.0, w.cell.1), (w.cell.0 + 1, w.cell.1)), + layout::WindowSide::Top => { + ((w.cell.0, w.cell.1), (w.cell.0, w.cell.1.saturating_sub(1))) + } + layout::WindowSide::Bottom => ((w.cell.0, w.cell.1), (w.cell.0, w.cell.1 + 1)), + }; + normalized_edge(win_edge.0, win_edge.1) == target + }) { + app.push_undo_snapshot(); + app.levels[active_index].windows.remove(idx); + return; + } } if let Some(stair_idx) = stair_at_pointer(app, pointer_pos, geometry) { @@ -814,6 +1011,21 @@ pub fn draw_add_tool_ghost(app: &DungeonApp, painter: &egui::Painter, geometry: ); } } + AddTool::Window => { + if let Some((edge_a, edge_b)) = door_edge_at_pointer(app, pointer_pos, geometry) { + let door_width_px = (geometry.cell_size / 5.0).max(1.0); + let ghost_color = Color32::from_rgb(70, 130, 220).gamma_multiply(ghost_alpha); + crate::rendering::draw_door_line( + painter, + geometry, + edge_a, + edge_b, + 1, + Stroke::new(door_width_px, ghost_color), + crate::rendering::DoorLineStyle::Solid, + ); + } + } AddTool::None | AddTool::Corridor => {} } } diff --git a/src/ui/tabs.rs b/src/ui/tabs.rs index d85555f..8a45236 100644 --- a/src/ui/tabs.rs +++ b/src/ui/tabs.rs @@ -689,6 +689,7 @@ pub fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut S ui.selectable_value(&mut settings.add_tool, AddTool::Door, "Door"); ui.selectable_value(&mut settings.add_tool, AddTool::LockedDoor, "Locked"); ui.selectable_value(&mut settings.add_tool, AddTool::SecretDoor, "Secret"); + ui.selectable_value(&mut settings.add_tool, AddTool::Window, "Window"); }); ui.add_space(8.0); @@ -769,6 +770,9 @@ pub fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut S AddTool::SecretDoor => { ui.label("Left-click or drag along room/corridor edges to place secret doors."); } + AddTool::Window => { + ui.label("Left-click or drag along room edges to place windows."); + } AddTool::Text => { ui.label("Left-click to place the text label (edit text in options above)."); } diff --git a/src/ui/types.rs b/src/ui/types.rs index cd39ba2..14766b3 100644 --- a/src/ui/types.rs +++ b/src/ui/types.rs @@ -259,6 +259,7 @@ pub enum AddTool { Door, LockedDoor, SecretDoor, + Window, Text, StartMarker, EndMarker,