/* * Canvas drawing logic for the dungeon layout. * Provides functions to render rooms, corridors, walls, doors, windows, * and various markers using egui's Painter API. */ use crate::exporter::utils::{corridor_edges_from_cells, room_edges_from_rooms}; use crate::interact::{ GridGeometry, HoverMarker, MarkerKind, cell_center, cell_rect, door_edges_for, door_render_width, marker_rect, normalized_edge, window_render_width, }; use crate::layout::{DungeonLayout, corridor_cells}; use eframe::egui; use egui::{Color32, Stroke}; use std::collections::HashSet; // Draws the full dungeon layout into the interactive canvas. pub fn draw_layout( painter: &egui::Painter, geometry: &GridGeometry, layout: &DungeonLayout, colorblind_mode: bool, hover_text_idx: Option, hover_marker: Option, hover_stair_idx: Option, ) { let wall_width_px = (geometry.cell_size / 2.5).max(1.0); let door_width_px = (geometry.cell_size / 5.0).max(1.0); let room_fill = Color32::from_rgb(70, 120, 160); let corridor_fill = Color32::from_rgb(210, 190, 120); let wall_color = Color32::BLACK; let corridor_cells_set = corridor_cells(layout, geometry.cols, geometry.rows); let corridor_edges_set = corridor_edges_from_cells(&corridor_cells_set); let mut room_cells_set = HashSet::new(); let room_edges_set = room_edges_from_rooms(&layout.rooms); let mut door_edges_set = HashSet::new(); for door in &layout.doors { for edge in door_edges_for(door, geometry.cols, geometry.rows) { door_edges_set.insert(edge); } } for &(col, row) in &corridor_cells_set { painter.rect_filled(cell_rect(geometry, col, row), 0.0, corridor_fill); if colorblind_mode { painter.circle_filled( cell_center(geometry, col, row), geometry.cell_size * 0.14, Color32::BLACK, ); } } for &(col, row) in &corridor_cells_set { let rect = cell_rect(geometry, col, row); let right = (col + 1, row); let bottom = (col, row + 1); let left = col.checked_sub(1).map(|x| (x, row)); let top = row.checked_sub(1).map(|y| (col, y)); if col == 0 || (!corridor_cells_set.contains(&(col - 1, row)) && left .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true)) { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), egui::pos2(rect.left(), rect.bottom()), wall_width_px, wall_color, ); } if (!corridor_cells_set.contains(&right) || !corridor_edges_set.contains(&normalized_edge((col, row), right))) && !door_edges_set.contains(&normalized_edge((col, row), right)) { draw_wall_segment( painter, egui::pos2(rect.right(), rect.top()), egui::pos2(rect.right(), rect.bottom()), wall_width_px, wall_color, ); } if row == 0 || (!corridor_cells_set.contains(&(col, row - 1)) && top .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true)) { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), egui::pos2(rect.right(), rect.top()), wall_width_px, wall_color, ); } if (!corridor_cells_set.contains(&bottom) || !corridor_edges_set.contains(&normalized_edge((col, row), bottom))) && !door_edges_set.contains(&normalized_edge((col, row), bottom)) { draw_wall_segment( painter, egui::pos2(rect.left(), rect.bottom()), egui::pos2(rect.right(), rect.bottom()), wall_width_px, wall_color, ); } } for room in &layout.rooms { for x in room.x..(room.x + room.width) { for y in room.y..(room.y + room.height) { room_cells_set.insert((x, y)); } } 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; let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom)); painter.rect_filled(room_rect, 0.0, room_fill); if colorblind_mode { draw_room_crosshatch(painter, geometry, room, Stroke::new(1.5, Color32::BLACK)); } } for &(col, row) in &room_cells_set { let rect = cell_rect(geometry, col, row); let right = (col + 1, row); let bottom = (col, row + 1); let left = col.checked_sub(1).map(|x| (x, row)); let top = row.checked_sub(1).map(|y| (col, y)); let left_edge = col == 0 || (!room_cells_set.contains(&(col - 1, row)) || left .map(|n| !room_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true)) && left .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true); if left_edge { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), egui::pos2(rect.left(), rect.bottom()), wall_width_px, wall_color, ); } if (!room_cells_set.contains(&right) || !room_edges_set.contains(&normalized_edge((col, row), right))) && !door_edges_set.contains(&normalized_edge((col, row), right)) { draw_wall_segment( painter, egui::pos2(rect.right(), rect.top()), egui::pos2(rect.right(), rect.bottom()), wall_width_px, wall_color, ); } let top_edge = row == 0 || (!room_cells_set.contains(&(col, row - 1)) || top .map(|n| !room_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true)) && top .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) .unwrap_or(true); if top_edge { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), egui::pos2(rect.right(), rect.top()), wall_width_px, wall_color, ); } if (!room_cells_set.contains(&bottom) || !room_edges_set.contains(&normalized_edge((col, row), bottom))) && !door_edges_set.contains(&normalized_edge((col, row), bottom)) { draw_wall_segment( painter, egui::pos2(rect.left(), rect.bottom()), egui::pos2(rect.right(), rect.bottom()), wall_width_px, wall_color, ); } } for door in &layout.doors { let color = if colorblind_mode { Color32::BLACK } else if door.secret { Color32::from_rgb(170, 80, 170) } else if door.archway { Color32::from_rgb(230, 140, 60) } else if door.locked { Color32::from_rgb(220, 70, 70) } else { Color32::from_rgb(80, 200, 120) }; let style = if colorblind_mode { if door.secret { DoorLineStyle::DashDotDot } else if door.archway { DoorLineStyle::Dotted } else if door.locked { DoorLineStyle::ShortDash } else { DoorLineStyle::LongDash } } else { DoorLineStyle::Solid }; draw_door_line( painter, geometry, door.from, door.to, door_render_width(door), Stroke::new(door_width_px, color), style, ); } for window in &layout.windows { let style = if colorblind_mode { DoorLineStyle::DashDot } else { DoorLineStyle::Solid }; draw_window_line( painter, geometry, window, Stroke::new(door_width_px, Color32::from_rgb(70, 130, 220)), style, ); } for (idx, label) in layout.text_labels.iter().enumerate() { if hover_text_idx == Some(idx) { painter.rect_stroke( text_label_rect(geometry, label).expand(2.0), 0.0, Stroke::new(2.0, Color32::from_rgb(255, 220, 120)), egui::StrokeKind::Middle, ); } let pos = cell_center(geometry, label.cell.0, label.cell.1); let color = if colorblind_mode { Color32::BLACK } else { Color32::WHITE }; painter.text( pos, egui::Align2::CENTER_CENTER, &label.text, egui::FontId::proportional(label.font_size as f32), color, ); } draw_area_marker_group( painter, geometry, &layout.start_markers, "S", Color32::from_rgb(60, 220, 200), colorblind_mode, hover_marker, MarkerKind::Start, ); draw_area_marker_group( painter, geometry, &layout.end_markers, "E", Color32::from_rgb(240, 90, 90), colorblind_mode, hover_marker, MarkerKind::End, ); draw_area_marker_group( painter, geometry, &layout.trap_markers, "T", Color32::from_rgb(150, 80, 230), colorblind_mode, hover_marker, MarkerKind::Trap, ); draw_area_marker_group( painter, geometry, &layout.monster_markers, "M", Color32::from_rgb(200, 50, 50), colorblind_mode, hover_marker, MarkerKind::Monster, ); for (idx, stair) in layout.stairs.iter().enumerate() { draw_staircase( painter, geometry, stair, colorblind_mode, hover_stair_idx == Some(idx), ); } } // Draws a staircase marker into the interactive canvas. pub fn draw_staircase( painter: &egui::Painter, geometry: &GridGeometry, stair: &crate::layout::Staircase, colorblind_mode: bool, is_hovered: bool, ) { 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 rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom)); let fill = if is_hovered { Color32::from_rgb(220, 180, 80).gamma_multiply(0.5) } else if colorblind_mode { Color32::from_rgb(180, 180, 180).gamma_multiply(0.4) } else { Color32::from_rgb(200, 150, 50).gamma_multiply(0.35) }; painter.rect_filled(rect, 4.0, fill); let stroke_color = if is_hovered { Color32::WHITE } else if colorblind_mode { Color32::BLACK } else { Color32::from_rgb(200, 150, 50) }; painter.rect_stroke( rect, 4.0, Stroke::new(if is_hovered { 3.0 } else { 2.0 }, stroke_color), egui::StrokeKind::Middle, ); 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 } else { Color32::from_rgb(150, 100, 30) }; for i in 0..steps { let y = rect.top() + (i as f32 * step_height) + step_height * 0.5; painter.line_segment( [ egui::pos2(rect.left() + 2.0, y), egui::pos2(rect.right() - 2.0, y), ], Stroke::new(1.5, line_color), ); } } // Computes label rect. pub fn text_label_rect(geometry: &GridGeometry, label: &crate::layout::TextLabel) -> egui::Rect { let center = cell_center(geometry, label.cell.0, label.cell.1); let font_size = label.font_size as f32; let width = (label.text.chars().count().max(1) as f32 * font_size * 0.6).max(geometry.cell_size * 0.5); let height = font_size.max(geometry.cell_size * 0.35); egui::Rect::from_center_size(center, egui::vec2(width, height)) } // Draws area marker. pub fn draw_area_marker( painter: &egui::Painter, geometry: &GridGeometry, marker: &crate::layout::AreaMarker, label: &str, base_color: Color32, colorblind_mode: bool, hovered: bool, ) { let rect = marker_rect(marker, geometry); let fill = base_color.gamma_multiply(if colorblind_mode { 0.22 } else { 0.35 }); let stroke_color = if hovered { Color32::from_rgb(255, 220, 120) } else if colorblind_mode { Color32::BLACK } else { base_color }; let text_color = if colorblind_mode { Color32::BLACK } else { Color32::WHITE }; let font_size = (geometry.cell_size * marker.size as f32 * 0.5).clamp(14.0, 72.0); painter.rect_filled(rect, 4.0, fill); painter.rect_stroke( rect, 4.0, Stroke::new(if hovered { 3.0 } else { 2.0 }, stroke_color), egui::StrokeKind::Middle, ); painter.text( rect.center(), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(font_size), text_color, ); } // Draws area marker group. #[allow(clippy::too_many_arguments)] pub fn draw_area_marker_group( painter: &egui::Painter, geometry: &GridGeometry, markers: &[crate::layout::AreaMarker], label: &str, base_color: Color32, colorblind_mode: bool, hover_marker: Option, kind: MarkerKind, ) { for (idx, marker) in markers.iter().enumerate() { let hovered = match (kind, hover_marker) { (MarkerKind::Start, Some(HoverMarker::Start(hover_idx))) => hover_idx == idx, (MarkerKind::End, Some(HoverMarker::End(hover_idx))) => hover_idx == idx, (MarkerKind::Trap, Some(HoverMarker::Trap(hover_idx))) => hover_idx == idx, (MarkerKind::Monster, Some(HoverMarker::Monster(hover_idx))) => hover_idx == idx, _ => false, }; draw_area_marker( painter, geometry, marker, label, base_color, colorblind_mode, hovered, ); } } // Draws wall segment. pub fn draw_wall_segment( painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, width: f32, color: Color32, ) { let half = width * 0.5; if (from.x - to.x).abs() <= f32::EPSILON { let x = from.x.min(to.x); let y0 = from.y.min(to.y); let y1 = from.y.max(to.y); let rect = egui::Rect::from_min_max( egui::pos2(x - half, y0 - half), egui::pos2(x + half, y1 + half), ); painter.rect_filled(rect, 0.0, color); } else { let y = from.y.min(to.y); let x0 = from.x.min(to.x); let x1 = from.x.max(to.x); let rect = egui::Rect::from_min_max( egui::pos2(x0 - half, y - half), egui::pos2(x1 + half, y + half), ); painter.rect_filled(rect, 0.0, color); } } #[derive(Debug, Clone, Copy)] pub enum DoorLineStyle { Solid, LongDash, ShortDash, Dotted, DashDot, DashDotDot, } // Draws door line. pub fn draw_door_line( painter: &egui::Painter, geometry: &GridGeometry, a: (usize, usize), b: (usize, usize), width_cells: usize, stroke: Stroke, style: DoorLineStyle, ) { if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 { return; } let width_cells = width_cells.max(1); let min_offset = -((width_cells as isize - 1) / 2); let max_offset = width_cells as isize / 2; if a.0 != b.0 { let x = geometry.rect.left() + (a.0.max(b.0) as f32) * geometry.cell_size; let row = a.1 as isize; let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size; let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style); } else { let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size; let col = a.0 as isize; let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size; let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style); } } // Draws window line. pub fn draw_window_line( painter: &egui::Painter, geometry: &GridGeometry, window: &crate::layout::Window, stroke: Stroke, style: DoorLineStyle, ) { let width_cells = window_render_width(window).max(1) as isize; let min_offset = -((width_cells - 1) / 2); let max_offset = width_cells / 2; match window.side { crate::layout::WindowSide::Left => { let x = geometry.rect.left() + window.cell.0 as f32 * geometry.cell_size; let row = window.cell.1 as isize; let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size; let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style); } crate::layout::WindowSide::Right => { let x = geometry.rect.left() + (window.cell.0 as f32 + 1.0) * geometry.cell_size; let row = window.cell.1 as isize; let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size; let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style); } crate::layout::WindowSide::Top => { let y = geometry.rect.top() + window.cell.1 as f32 * geometry.cell_size; let col = window.cell.0 as isize; let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size; let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style); } crate::layout::WindowSide::Bottom => { let y = geometry.rect.top() + (window.cell.1 as f32 + 1.0) * geometry.cell_size; let col = window.cell.0 as isize; let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size; let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style); } } } // Draws styled line. pub fn draw_styled_line( painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke, style: DoorLineStyle, ) { match style { DoorLineStyle::Solid => { painter.line_segment([from, to], stroke); } DoorLineStyle::LongDash => draw_dashed_line(painter, from, to, stroke, 14.0, 7.0), DoorLineStyle::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0), DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke), DoorLineStyle::DashDot => draw_dash_dot_line(painter, from, to, stroke), DoorLineStyle::DashDotDot => draw_dash_dot_dot_line(painter, from, to, stroke), } } // Draws dashed line. pub fn draw_dashed_line( painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke, dash_len: f32, gap_len: f32, ) { let dx = to.x - from.x; let dy = to.y - from.y; let len = (dx * dx + dy * dy).sqrt(); if len <= 0.0 { return; } let ux = dx / len; let uy = dy / len; let mut dist = 0.0_f32; while dist < len { let seg_start = dist; let seg_end = (dist + dash_len).min(len); let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start); let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end); painter.line_segment([p0, p1], stroke); dist += dash_len + gap_len; } } // Draws dotted line. pub fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) { let dx = to.x - from.x; let dy = to.y - from.y; let len = (dx * dx + dy * dy).sqrt(); if len <= 0.0 { return; } let ux = dx / len; let uy = dy / len; let step = 5.0_f32; let radius = (stroke.width * 0.35).max(1.0); let mut dist = 0.0_f32; while dist <= len { let p = egui::pos2(from.x + ux * dist, from.y + uy * dist); painter.circle_filled(p, radius, stroke.color); dist += step; } } // Draws dash dot line. pub fn draw_dash_dot_line( painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke, ) { let dx = to.x - from.x; let dy = to.y - from.y; let len = (dx * dx + dy * dy).sqrt(); if len <= 0.0 { return; } let ux = dx / len; let uy = dy / len; let mut dist = 0.0_f32; while dist < len { let dash_end = (dist + 7.0).min(len); let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist); let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end); painter.line_segment([p0, p1], stroke); dist = dash_end + 3.0; if dist >= len { break; } let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist); painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color); dist += 4.0; } } // Draws dash dot dot line. pub fn draw_dash_dot_dot_line( painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke, ) { let dx = to.x - from.x; let dy = to.y - from.y; let len = (dx * dx + dy * dy).sqrt(); if len <= 0.0 { return; } let ux = dx / len; let uy = dy / len; let radius = (stroke.width * 0.35).max(1.0); let mut dist = 0.0_f32; while dist < len { let dash_end = (dist + 7.0).min(len); let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist); let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end); painter.line_segment([p0, p1], stroke); dist = dash_end + 3.0; if dist >= len { break; } let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist); painter.circle_filled(dot1, radius, stroke.color); dist += 3.0; if dist >= len { break; } let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist); painter.circle_filled(dot2, radius, stroke.color); dist += 4.0; } } // Draws room crosshatch. pub fn draw_room_crosshatch( painter: &egui::Painter, geometry: &GridGeometry, room: &crate::layout::Room, stroke: Stroke, ) { for x in room.x..(room.x + room.width) { for y in room.y..(room.y + room.height) { let rect = cell_rect(geometry, x, y).shrink(2.0); painter.line_segment([rect.left_top(), rect.right_bottom()], stroke); painter.line_segment([rect.right_top(), rect.left_bottom()], stroke); } } }