Files
desktop_dungeon_generator/src/rendering.rs
T
2026-05-18 10:36:52 -05:00

755 lines
25 KiB
Rust

/*
* 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<usize>,
hover_marker: Option<HoverMarker>,
hover_stair_idx: Option<usize>,
) {
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<HoverMarker>,
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);
}
}
}