split up more files

This commit is contained in:
grimsace
2026-05-18 10:16:47 -05:00
parent 49ebe351d6
commit 001d583f27
21 changed files with 6324 additions and 6255 deletions
-2078
View File
File diff suppressed because it is too large Load Diff
+481
View File
@@ -0,0 +1,481 @@
/*
* Main export module.
* Orchestrates the export process, coordinating dialog selection,
* background thread execution, and multi-format (SVG/Raster) generation.
*/
use image::ImageFormat;
use rayon::prelude::*;
use rfd::FileDialog;
use std::path::PathBuf;
use std::sync::mpsc;
use tiny_skia::Pixmap;
use crate::layout::DungeonLayout;
use crate::ui::{ExportFormat, MaskFormat, UiSettings};
pub mod raster;
pub mod svg;
pub mod types;
pub mod utils;
pub use svg::build_svg;
pub use types::*;
type ProgressSender = mpsc::Sender<ExportEvent>;
pub fn select_export_target(settings: &UiSettings) -> Result<ExportTarget, String> {
if settings.export_format == ExportFormat::Folder || settings.export_level_index == 999 {
let title = if settings.export_level_index == 999 {
"Select export folder for all levels"
} else {
"Export composite masks folder"
};
let mut dialog = FileDialog::new().set_title(title);
if let Some(ref last_path) = settings.last_export_path {
dialog = dialog.set_directory(last_path);
}
let folder = dialog
.pick_folder()
.ok_or_else(|| "Export canceled".to_string())?;
return Ok(ExportTarget::Folder(folder));
}
let ext = settings.export_format.extension();
let default_name = format!("dungeon_export.{ext}");
let mut dialog = FileDialog::new();
dialog = dialog
.set_title("Export image")
.set_file_name(&default_name)
.add_filter(settings.export_format.label(), &[ext]);
if let Some(ref last_path) = settings.last_export_path {
dialog = dialog.set_directory(last_path);
}
let mut path = dialog
.save_file()
.ok_or_else(|| "Export canceled".to_string())?;
if path.extension().is_none() {
path.set_extension(ext);
}
Ok(ExportTarget::File(path))
}
pub fn export_to_target(
layouts: &[DungeonLayout],
settings: &UiSettings,
target: ExportTarget,
progress_tx: &ProgressSender,
) -> Result<PathBuf, String> {
let export_indices: Vec<usize> = if settings.export_level_index == 999 {
(0..layouts.len()).collect()
} else {
let idx = settings
.export_level_index
.min(layouts.len().saturating_sub(1));
vec![idx]
};
let total = export_image_count(settings, &target, export_indices.len());
report_progress(progress_tx, 0, total);
match target {
ExportTarget::Folder(folder) => {
if settings.export_format == ExportFormat::Folder {
export_composite_masks_for_levels(
layouts,
settings,
&folder,
&export_indices,
progress_tx,
)?;
} else {
export_level_images_to_folder(
layouts,
settings,
&folder,
&export_indices,
progress_tx,
)?;
}
Ok(folder)
}
ExportTarget::File(path) => {
let idx = export_indices[0];
save_layout_to_file(&layouts[idx], settings, &path)?;
report_progress(progress_tx, 1, total);
Ok(path)
}
}
}
fn export_image_count(settings: &UiSettings, target: &ExportTarget, level_count: usize) -> usize {
match target {
ExportTarget::File(_) => 1,
ExportTarget::Folder(_) if settings.export_format == ExportFormat::Folder => {
level_count * composite_images_per_level(settings)
}
ExportTarget::Folder(_) => level_count,
}
}
fn composite_images_per_level(settings: &UiSettings) -> usize {
let base_masks = 12;
let grid_mask = usize::from(settings.export_show_grid);
let composite = 1;
base_masks + grid_mask + composite
}
fn report_progress(progress_tx: &ProgressSender, completed: usize, total: usize) {
let _ = progress_tx.send(ExportEvent::Progress(ExportProgress { completed, total }));
}
fn export_level_images_to_folder(
layouts: &[DungeonLayout],
settings: &UiSettings,
folder: &std::path::Path,
export_indices: &[usize],
progress_tx: &ProgressSender,
) -> Result<(), String> {
std::fs::create_dir_all(folder).map_err(|err| format!("Failed creating folder: {err}"))?;
let completed = std::sync::atomic::AtomicUsize::new(0);
let total = export_indices.len();
let ext = settings.export_format.extension();
export_indices.par_iter().try_for_each(|&idx| {
let filename = format!("level{}.{ext}", idx + 1);
let path = folder.join(filename);
save_layout_to_file(&layouts[idx], settings, &path)?;
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
report_progress(progress_tx, done, total);
Ok(())
})
}
fn save_layout_to_file(
layout: &DungeonLayout,
settings: &UiSettings,
path: &std::path::Path,
) -> Result<(), String> {
match settings.export_format {
ExportFormat::Svg => {
let svg = build_svg(layout, settings);
std::fs::write(path, svg).map_err(|e| format!("Failed writing SVG: {e}"))?;
}
ExportFormat::Png | ExportFormat::Jpeg | ExportFormat::Webp => {
let pixmap = raster::render_pixmap(layout, settings)?;
let img = raster::raster_image_from_pixmap(&pixmap, settings)?;
let format = match settings.export_format {
ExportFormat::Png => ImageFormat::Png,
ExportFormat::Jpeg => ImageFormat::Jpeg,
ExportFormat::Webp => ImageFormat::WebP,
ExportFormat::Svg | ExportFormat::Folder => unreachable!(),
};
img.save_with_format(path, format)
.map_err(|e| format!("Failed writing image: {e}"))?;
}
ExportFormat::Folder => unreachable!(),
}
Ok(())
}
fn export_composite_masks_for_levels(
layouts: &[DungeonLayout],
settings: &UiSettings,
folder: &std::path::Path,
export_indices: &[usize],
progress_tx: &ProgressSender,
) -> Result<(), String> {
let contexts = export_indices
.iter()
.map(|&idx| {
let subfolder = if settings.export_level_index == 999 {
folder.join(format!("level {}", idx + 1))
} else {
folder.to_path_buf()
};
std::fs::create_dir_all(&subfolder)
.map_err(|err| format!("Failed creating folder: {err}"))?;
let geometry = ExportGeometry::new(settings.cols, settings.rows);
let scene = utils::collect_scene_data(&layouts[idx], settings);
Ok(CompositeExportContext {
layout: &layouts[idx],
folder: subfolder,
geometry,
scene,
wall_width: (geometry.cell / 2.5).max(1.0),
door_width: (geometry.cell / 5.0).max(1.0),
})
})
.collect::<Result<Vec<_>, String>>()?;
let mut tasks = vec![
MaskTask::Floors,
MaskTask::Walls,
MaskTask::Doors,
MaskTask::Archways,
MaskTask::LockedDoors,
MaskTask::SecretDoors,
MaskTask::Windows,
MaskTask::StartMarkers,
MaskTask::EndMarkers,
MaskTask::TrapMarkers,
MaskTask::MonsterMarkers,
MaskTask::Stairs,
];
if settings.export_show_grid {
tasks.push(MaskTask::Grid);
}
let export_tasks = contexts
.iter()
.enumerate()
.flat_map(|(context_idx, _)| {
tasks
.iter()
.copied()
.map(move |task| (context_idx, CompositeTask::Mask(task)))
.chain(std::iter::once((context_idx, CompositeTask::Composite)))
})
.collect::<Vec<_>>();
let completed = std::sync::atomic::AtomicUsize::new(0);
let total = export_tasks.len();
export_tasks
.into_par_iter()
.try_for_each(|(context_idx, task)| {
let context = &contexts[context_idx];
match task {
CompositeTask::Mask(mask_task) => {
let (stem, pixmap) = render_mask_pixmap(context, mask_task)?;
save_mask_image(&context.folder, stem, &pixmap, settings)
}
CompositeTask::Composite => save_composite_image(context, settings),
}?;
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
report_progress(progress_tx, done, total);
Ok::<(), String>(())
})?;
Ok(())
}
fn save_composite_image(
context: &CompositeExportContext<'_>,
settings: &UiSettings,
) -> Result<(), String> {
let composite = raster::render_pixmap_with_scene(
context.layout,
settings,
&context.geometry,
&context.scene,
)?;
let composite_img = raster::raster_image_from_pixmap(&composite, settings)?;
let composite_ext = settings.mask_format.extension();
let composite_path = context.folder.join(format!("composite.{composite_ext}"));
composite_img
.save_with_format(&composite_path, mask_image_format(settings.mask_format))
.map_err(|e| format!("Failed writing {}: {e}", composite_path.display()))?;
Ok(())
}
fn render_mask_pixmap(
context: &CompositeExportContext<'_>,
task: MaskTask,
) -> Result<(&'static str, Pixmap), String> {
let g = &context.geometry;
let scene = &context.scene;
let layout = context.layout;
let result = match task {
MaskTask::Floors => {
let mut pixmap = blank_mask(g, "floors")?;
for &(x, y) in &scene.corridor_cells {
raster::fill_rect_cell(&mut pixmap, g, x, y, WHITE);
}
for room in &layout.rooms {
raster::fill_rect_room(&mut pixmap, g, room, WHITE);
}
("floors_mask", pixmap)
}
MaskTask::Walls => {
let mut pixmap = blank_mask(g, "walls")?;
raster::draw_cell_walls(
&mut pixmap,
g,
&scene.corridor_cells,
Some(&scene.corridor_edges),
&scene.door_edges,
context.wall_width,
WHITE,
);
raster::draw_cell_walls(
&mut pixmap,
g,
&scene.room_cells,
Some(&scene.room_edges),
&scene.door_edges,
context.wall_width,
WHITE,
);
("walls_mask", pixmap)
}
MaskTask::Doors => {
let mut pixmap = blank_mask(g, "doors")?;
for door in &layout.doors {
if !door.secret && !door.archway && !door.locked {
raster::draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("doors_mask", pixmap)
}
MaskTask::Archways => {
let mut pixmap = blank_mask(g, "archways")?;
for door in &layout.doors {
if door.archway {
raster::draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("archways_mask", pixmap)
}
MaskTask::LockedDoors => {
let mut pixmap = blank_mask(g, "locked doors")?;
for door in &layout.doors {
if door.locked {
raster::draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("locked_doors_mask", pixmap)
}
MaskTask::SecretDoors => {
let mut pixmap = blank_mask(g, "secret doors")?;
for door in &layout.doors {
if door.secret {
raster::draw_door(
&mut pixmap,
g,
door,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
}
("secret_doors_mask", pixmap)
}
MaskTask::Windows => {
let mut pixmap = blank_mask(g, "windows")?;
for window in &layout.windows {
raster::draw_window(
&mut pixmap,
g,
window,
context.door_width,
WHITE,
DoorStyle::Solid,
);
}
("windows_mask", pixmap)
}
MaskTask::StartMarkers => {
let mut pixmap = blank_mask(g, "start markers")?;
fill_marker_mask(&mut pixmap, g, &layout.start_markers);
("start_markers_mask", pixmap)
}
MaskTask::EndMarkers => {
let mut pixmap = blank_mask(g, "end markers")?;
fill_marker_mask(&mut pixmap, g, &layout.end_markers);
("end_markers_mask", pixmap)
}
MaskTask::TrapMarkers => {
let mut pixmap = blank_mask(g, "trap markers")?;
fill_marker_mask(&mut pixmap, g, &layout.trap_markers);
("trap_markers_mask", pixmap)
}
MaskTask::MonsterMarkers => {
let mut pixmap = blank_mask(g, "monster markers")?;
fill_marker_mask(&mut pixmap, g, &layout.monster_markers);
("monster_markers_mask", pixmap)
}
MaskTask::Stairs => {
let mut pixmap = blank_mask(g, "stairs")?;
for staircase in &layout.stairs {
raster::fill_rect_staircase(&mut pixmap, g, staircase, WHITE);
}
("stairs_mask", pixmap)
}
MaskTask::Grid => {
let mut pixmap = blank_mask(g, "grid")?;
raster::draw_grid_color(&mut pixmap, g, WHITE);
("grid_mask", pixmap)
}
};
Ok(result)
}
fn blank_mask(g: &ExportGeometry, name: &str) -> Result<Pixmap, String> {
let mut pixmap = Pixmap::new(g.width, g.height)
.ok_or_else(|| format!("Failed to allocate {name} mask canvas"))?;
raster::fill_bg(&mut pixmap, BLACK);
Ok(pixmap)
}
fn fill_marker_mask(
pixmap: &mut Pixmap,
g: &ExportGeometry,
markers: &[crate::layout::AreaMarker],
) {
for marker in markers {
raster::fill_rect_marker(pixmap, g, marker, WHITE);
}
}
fn save_mask_image(
folder: &std::path::Path,
stem: &str,
pixmap: &Pixmap,
settings: &UiSettings,
) -> Result<(), String> {
let ext = settings.mask_format.extension();
let path = folder.join(format!("{stem}.{ext}"));
let img = raster::raster_mask_image_from_pixmap(pixmap, settings)?;
img.save_with_format(&path, mask_image_format(settings.mask_format))
.map_err(|e| format!("Failed writing {}: {e}", path.display()))?;
Ok(())
}
fn mask_image_format(format: MaskFormat) -> ImageFormat {
match format {
MaskFormat::Png => ImageFormat::Png,
MaskFormat::Jpeg => ImageFormat::Jpeg,
MaskFormat::Webp => ImageFormat::WebP,
}
}
+816
View File
@@ -0,0 +1,816 @@
/*
* Raster image export logic.
* Provides functions to render dungeon layouts into pixel buffers (Pixmaps)
* and encode them as PNG, JPEG, or WebP images.
*/
use super::types::{
BG, BLACK, CORRIDOR, DoorStyle, END_MARKER, ExportGeometry, GRID, MONSTER_MARKER, ROOM, STAIRS,
START_MARKER, TRAP_MARKER, WHITE, WINDOW_COLOR, door_render_width, norm_edge,
};
use super::utils::{collect_scene_data, door_line_points, window_line_points};
use crate::layout::{AreaMarker, DungeonLayout, Room, Staircase, Window};
use crate::ui::UiSettings;
use image::{DynamicImage, RgbaImage, imageops::FilterType};
use std::collections::HashSet;
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform};
pub fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap, String> {
let g = ExportGeometry::new(settings.cols, settings.rows);
let scene = collect_scene_data(layout, settings);
render_pixmap_with_scene(layout, settings, &g, &scene)
}
pub fn render_pixmap_with_scene(
layout: &DungeonLayout,
settings: &UiSettings,
g: &ExportGeometry,
scene: &super::types::SceneData,
) -> Result<Pixmap, String> {
let mut pixmap =
Pixmap::new(g.width, g.height).ok_or_else(|| "Failed to allocate canvas".to_string())?;
fill_bg(&mut pixmap, BG);
let wall_w = snap_even_width((g.cell / 2.5).max(1.0));
let door_w = (g.cell / 5.0).max(1.0).round();
if settings.export_show_grid {
draw_grid_color(&mut pixmap, g, GRID);
}
for &(x, y) in &scene.corridor_cells {
fill_rect_cell(&mut pixmap, g, x, y, CORRIDOR);
if settings.colorblind_mode {
let (cx, cy) = g.cell_center(x, y);
draw_dot(&mut pixmap, cx, cy, g.cell * 0.14, BLACK);
}
}
for room in &layout.rooms {
fill_rect_room(&mut pixmap, g, room, ROOM);
if settings.colorblind_mode {
draw_room_crosshatch(&mut pixmap, g, room, 1.5, BLACK);
}
}
draw_cell_walls(
&mut pixmap,
g,
&scene.corridor_cells,
Some(&scene.corridor_edges),
&scene.door_edges,
wall_w,
BLACK,
);
draw_cell_walls(
&mut pixmap,
g,
&scene.room_cells,
Some(&scene.room_edges),
&scene.door_edges,
wall_w,
BLACK,
);
for door in &layout.doors {
let (color, style) = if settings.colorblind_mode {
let s = if door.secret {
DoorStyle::DashDotDot
} else if door.archway {
DoorStyle::Dotted
} else if door.locked {
DoorStyle::ShortDash
} else {
DoorStyle::LongDash
};
(BLACK, s)
} else if door.secret {
(super::types::SECRET_DOOR, DoorStyle::Solid)
} else if door.archway {
(super::types::ARCHWAY, DoorStyle::Solid)
} else if door.locked {
(super::types::LOCKED_DOOR, DoorStyle::Solid)
} else {
(super::types::OPEN_DOOR, DoorStyle::Solid)
};
draw_door(&mut pixmap, g, door, door_w, color, style);
}
for window in &layout.windows {
let style = if settings.colorblind_mode {
DoorStyle::DashDot
} else {
DoorStyle::Solid
};
draw_window(&mut pixmap, g, window, door_w, WINDOW_COLOR, style);
}
draw_area_marker_group(
&mut pixmap,
g,
&layout.start_markers,
"S",
START_MARKER,
settings.colorblind_mode,
);
draw_area_marker_group(
&mut pixmap,
g,
&layout.end_markers,
"E",
END_MARKER,
settings.colorblind_mode,
);
draw_area_marker_group(
&mut pixmap,
g,
&layout.trap_markers,
"T",
TRAP_MARKER,
settings.colorblind_mode,
);
draw_area_marker_group(
&mut pixmap,
g,
&layout.monster_markers,
"M",
MONSTER_MARKER,
settings.colorblind_mode,
);
draw_staircase_group(
&mut pixmap,
g,
&layout.stairs,
STAIRS,
settings.colorblind_mode,
);
Ok(pixmap)
}
pub fn fill_bg(pixmap: &mut Pixmap, color: (u8, u8, u8, u8)) {
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
let Some(rect) = Rect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32)
else {
return;
};
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn draw_grid_color(pixmap: &mut Pixmap, g: &ExportGeometry, color: (u8, u8, u8, u8)) {
let left = g.left();
let top = g.top();
let width = g.cols as f32 * g.cell;
let height = g.rows as f32 * g.cell;
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
for c in 0..=g.cols {
let x = (left + c as f32 * g.cell).round();
let Some(rect) = Rect::from_xywh(x, top, 1.0, height) else {
continue;
};
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
for r in 0..=g.rows {
let y = (top + r as f32 * g.cell).round();
let Some(rect) = Rect::from_xywh(left, y, width, 1.0) else {
continue;
};
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
}
pub fn fill_rect_cell(
pixmap: &mut Pixmap,
g: &ExportGeometry,
col: usize,
row: usize,
color: (u8, u8, u8, u8),
) {
let Some(rect) = g.cell_rect(col, row) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn fill_rect_room(
pixmap: &mut Pixmap,
g: &ExportGeometry,
room: &Room,
color: (u8, u8, u8, u8),
) {
let Some(rect) = Rect::from_xywh(
g.left() + room.x as f32 * g.cell,
g.top() + room.y as f32 * g.cell,
room.width as f32 * g.cell,
room.height as f32 * g.cell,
) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn fill_rect_marker(
pixmap: &mut Pixmap,
g: &ExportGeometry,
marker: &AreaMarker,
color: (u8, u8, u8, u8),
) {
let Some(rect) = Rect::from_xywh(
g.left() + marker.cell.0 as f32 * g.cell,
g.top() + marker.cell.1 as f32 * g.cell,
marker.size as f32 * g.cell,
marker.size as f32 * g.cell,
) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn draw_area_marker(
pixmap: &mut Pixmap,
g: &ExportGeometry,
marker: &AreaMarker,
label: &str,
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
let overlay = (
color.0,
color.1,
color.2,
if colorblind_mode { 56 } else { 90 },
);
fill_rect_marker(pixmap, g, marker, overlay);
let x = g.left() + marker.cell.0 as f32 * g.cell;
let y = g.top() + marker.cell.1 as f32 * g.cell;
let w = marker.size as f32 * g.cell;
let h = marker.size as f32 * g.cell;
draw_wall_segment(pixmap, x, y, x + w, y, 3.0, color);
draw_wall_segment(pixmap, x, y + h, x + w, y + h, 3.0, color);
draw_wall_segment(pixmap, x, y, x, y + h, 3.0, color);
draw_wall_segment(pixmap, x + w, y, x + w, y + h, 3.0, color);
let symbol = if colorblind_mode { BLACK } else { WHITE };
if label == "S" {
draw_line(
pixmap,
x + w * 0.25,
y + h * 0.75,
x + w * 0.5,
y + h * 0.25,
4.0,
symbol,
DoorStyle::Solid,
);
draw_line(
pixmap,
x + w * 0.5,
y + h * 0.25,
x + w * 0.75,
y + h * 0.75,
4.0,
symbol,
DoorStyle::Solid,
);
} else if label == "E" {
draw_line(
pixmap,
x + w * 0.25,
y + h * 0.25,
x + w * 0.75,
y + h * 0.75,
4.0,
symbol,
DoorStyle::Solid,
);
draw_line(
pixmap,
x + w * 0.75,
y + h * 0.25,
x + w * 0.25,
y + h * 0.75,
4.0,
symbol,
DoorStyle::Solid,
);
} else if label == "T" {
// Horizontal bar
draw_line(
pixmap,
x + w * 0.2,
y + h * 0.25,
x + w * 0.8,
y + h * 0.25,
4.0,
symbol,
DoorStyle::Solid,
);
// Vertical bar
draw_line(
pixmap,
x + w * 0.5,
y + h * 0.25,
x + w * 0.5,
y + h * 0.75,
4.0,
symbol,
DoorStyle::Solid,
);
} else if label == "M" {
draw_line(
pixmap,
x + w * 0.2,
y + h * 0.75,
x + w * 0.2,
y + h * 0.25,
4.0,
symbol,
DoorStyle::Solid,
);
draw_line(
pixmap,
x + w * 0.2,
y + h * 0.25,
x + w * 0.5,
y + h * 0.5,
4.0,
symbol,
DoorStyle::Solid,
);
draw_line(
pixmap,
x + w * 0.5,
y + h * 0.5,
x + w * 0.8,
y + h * 0.25,
4.0,
symbol,
DoorStyle::Solid,
);
draw_line(
pixmap,
x + w * 0.8,
y + h * 0.25,
x + w * 0.8,
y + h * 0.75,
4.0,
symbol,
DoorStyle::Solid,
);
}
}
pub fn fill_rect_staircase(
pixmap: &mut Pixmap,
g: &ExportGeometry,
staircase: &Staircase,
color: (u8, u8, u8, u8),
) {
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.width as f32 * g.cell,
staircase.height as f32 * g.cell,
) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn draw_staircase(
pixmap: &mut Pixmap,
g: &ExportGeometry,
staircase: &Staircase,
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
let fill_color = if colorblind_mode {
(180, 180, 180, 90)
} else {
(color.0, color.1, color.2, 90)
};
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.width as f32 * g.cell,
staircase.height as f32 * g.cell,
) else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(fill_color.0, fill_color.1, fill_color.2, fill_color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
let stroke_color = if colorblind_mode {
BLACK
} else {
(150, 100, 30, 255)
};
let x = rect.left();
let y = rect.top();
let w = rect.width();
let h = rect.height();
draw_wall_segment(pixmap, x, y, x + w, y, 2.0, stroke_color);
draw_wall_segment(pixmap, x, y + h, x + w, y + h, 2.0, stroke_color);
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.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();
draw_line(
pixmap,
x + 2.0,
sy,
x + w - 2.0,
sy,
1.5,
stroke_color,
DoorStyle::Solid,
);
}
}
pub fn draw_staircase_group(
pixmap: &mut Pixmap,
g: &ExportGeometry,
stairs: &[Staircase],
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
for staircase in stairs {
draw_staircase(pixmap, g, staircase, color, colorblind_mode);
}
}
pub fn draw_area_marker_group(
pixmap: &mut Pixmap,
g: &ExportGeometry,
markers: &[AreaMarker],
label: &str,
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
for marker in markers {
draw_area_marker(pixmap, g, marker, label, color, colorblind_mode);
}
}
pub fn draw_dot(pixmap: &mut Pixmap, cx: f32, cy: f32, radius: f32, color: (u8, u8, u8, u8)) {
let mut pb = PathBuilder::new();
pb.push_circle(cx, cy, radius);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
pub fn draw_room_crosshatch(
pixmap: &mut Pixmap,
g: &ExportGeometry,
room: &Room,
width: f32,
color: (u8, u8, u8, u8),
) {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
if let Some(rect) = g.cell_rect(x, y) {
let pad = 2.0;
draw_line(
pixmap,
rect.left() + pad,
rect.top() + pad,
rect.right() - pad,
rect.bottom() - pad,
width,
color,
DoorStyle::Solid,
);
draw_line(
pixmap,
rect.right() - pad,
rect.top() + pad,
rect.left() + pad,
rect.bottom() - pad,
width,
color,
DoorStyle::Solid,
);
}
}
}
}
pub fn draw_cell_walls(
pixmap: &mut Pixmap,
g: &ExportGeometry,
cells: &HashSet<(usize, usize)>,
connected_edges: Option<&HashSet<((usize, usize), (usize, usize))>>,
door_edges: &HashSet<((usize, usize), (usize, usize))>,
width: f32,
color: (u8, u8, u8, u8),
) {
for &(col, row) in cells {
if let Some(rect) = g.cell_rect(col, row) {
let right = (col + 1, row);
let bottom = (col, row + 1);
let should_left = col == 0
|| (!cells.contains(&(col - 1, row))
&& !door_edges.contains(&norm_edge((col, row), (col - 1, row))));
let right_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), right)))
.unwrap_or(true);
let should_right = (!cells.contains(&right) || !right_connected)
&& !door_edges.contains(&norm_edge((col, row), right));
let should_top = row == 0
|| (!cells.contains(&(col, row - 1))
&& !door_edges.contains(&norm_edge((col, row), (col, row - 1))));
let bottom_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), bottom)))
.unwrap_or(true);
let should_bottom = (!cells.contains(&bottom) || !bottom_connected)
&& !door_edges.contains(&norm_edge((col, row), bottom));
if should_left {
draw_wall_segment(
pixmap,
rect.left(),
rect.top(),
rect.left(),
rect.bottom(),
width,
color,
);
}
if should_right {
draw_wall_segment(
pixmap,
rect.right(),
rect.top(),
rect.right(),
rect.bottom(),
width,
color,
);
}
if should_top {
draw_wall_segment(
pixmap,
rect.left(),
rect.top(),
rect.right(),
rect.top(),
width,
color,
);
}
if should_bottom {
draw_wall_segment(
pixmap,
rect.left(),
rect.bottom(),
rect.right(),
rect.bottom(),
width,
color,
);
}
}
}
}
pub fn draw_wall_segment(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
) {
let width = width.round().max(1.0);
let half = width * 0.5;
let x1 = x1.round();
let y1 = y1.round();
let x2 = x2.round();
let y2 = y2.round();
let rect = if (x1 - x2).abs() <= f32::EPSILON {
let x = x1.min(x2);
let y0 = y1.min(y2);
let y1 = y1.max(y2);
Rect::from_xywh(x - half, y0 - half, width, (y1 - y0) + width)
} else {
let y = y1.min(y2);
let x0 = x1.min(x2);
let x1 = x1.max(x2);
Rect::from_xywh(x0 - half, y - half, (x1 - x0) + width, width)
};
let Some(rect) = rect else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
pixmap.fill_rect(rect, &paint, Transform::identity(), None);
}
pub fn snap_even_width(value: f32) -> f32 {
let mut w = value.round().max(1.0);
if (w as u32) % 2 == 1 {
w += 1.0;
}
w
}
pub fn draw_door(
pixmap: &mut Pixmap,
g: &ExportGeometry,
door: &crate::layout::Door,
width: f32,
color: (u8, u8, u8, u8),
style: DoorStyle,
) {
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);
}
pub fn draw_window(
pixmap: &mut Pixmap,
g: &ExportGeometry,
window: &Window,
width: f32,
color: (u8, u8, u8, u8),
style: DoorStyle,
) {
let Some((x1, y1, x2, y2)) = window_line_points(g, window) else {
return;
};
draw_line(pixmap, x1, y1, x2, y2, width, color, style);
}
pub fn draw_line(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
style: DoorStyle,
) {
match style {
DoorStyle::Solid => stroke_path(pixmap, x1, y1, x2, y2, width, color, None),
DoorStyle::LongDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((14.0, 7.0))),
DoorStyle::ShortDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((6.0, 4.0))),
DoorStyle::Dotted => dotted_line(pixmap, x1, y1, x2, y2, width, color),
DoorStyle::DashDot => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((7.0, 3.0))),
DoorStyle::DashDotDot => {
stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((7.0, 3.0)))
}
}
}
pub fn stroke_path(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
dash: Option<(f32, f32)>,
) {
let mut pb = PathBuilder::new();
pb.move_to(x1, y1);
pb.line_to(x2, y2);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.0, color.1, color.2, color.3);
let mut stroke = Stroke::default();
stroke.width = width;
if let Some((dash_len, gap_len)) = dash {
stroke.dash = StrokeDash::new(vec![dash_len, gap_len], 0.0);
}
pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
pub fn dotted_line(
pixmap: &mut Pixmap,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
width: f32,
color: (u8, u8, u8, u8),
) {
let dx = x2 - x1;
let dy = y2 - y1;
let len = (dx * dx + dy * dy).sqrt();
if len <= 0.0 {
return;
}
let ux = dx / len;
let uy = dy / len;
let mut d = 0.0_f32;
let step = 6.0;
let radius = (width * 0.35).max(1.0);
while d <= len {
draw_dot(pixmap, x1 + ux * d, y1 + uy * d, radius, color);
d += step;
}
}
pub fn raster_image_from_pixmap(
pixmap: &Pixmap,
settings: &UiSettings,
) -> Result<DynamicImage, String> {
let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or_else(|| "Failed to build image buffer".to_string())?;
let mut img = DynamicImage::ImageRgba8(rgba);
let target = raster_target_size(settings);
if img.width() != target.0 || img.height() != target.1 {
img = img.resize_exact(target.0, target.1, FilterType::Lanczos3);
}
Ok(img)
}
pub fn raster_mask_image_from_pixmap(
pixmap: &Pixmap,
settings: &UiSettings,
) -> Result<DynamicImage, String> {
let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec())
.ok_or_else(|| "Failed to build image buffer".to_string())?;
let mut img = DynamicImage::ImageRgba8(rgba);
let target = raster_target_size(settings);
if img.width() != target.0 || img.height() != target.1 {
img = img.resize_exact(target.0, target.1, FilterType::Nearest);
}
let mut out = img.to_rgba8();
for pixel in out.pixels_mut() {
let on = pixel[0] > 127 || pixel[1] > 127 || pixel[2] > 127;
let v = if on { 255 } else { 0 };
*pixel = image::Rgba([v, v, v, 255]);
}
Ok(DynamicImage::ImageRgba8(out))
}
pub fn raster_target_size(settings: &UiSettings) -> (u32, u32) {
let width = settings.export_width.clamp(1, 10_000);
let mut height = settings.export_height.clamp(1, 10_000);
if !settings.allow_export_aspect_change {
let cols = settings.cols.max(1) as f32;
let rows = settings.rows.max(1) as f32;
let ratio = cols / rows;
height = ((width as f32) / ratio).round() as u32;
height = height.clamp(1, 10_000);
}
(width, height)
}
+433
View File
@@ -0,0 +1,433 @@
/*
* SVG vector export logic.
* Handles building SVG representations of dungeon levels, including
* rooms, corridors, walls, doors, and markers.
*/
use super::types::{
BG, CORRIDOR, END_MARKER, ExportGeometry, GRID, MONSTER_MARKER, ROOM, START_MARKER,
TRAP_MARKER, WINDOW_COLOR, door_render_width, norm_edge,
};
use super::utils::{collect_scene_data, door_line_points, window_line_points};
use crate::layout::DungeonLayout;
use crate::ui::UiSettings;
use std::collections::HashSet;
use std::fmt::Write as _;
pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
let g = ExportGeometry::new(settings.cols, settings.rows);
let wall_w = (g.cell / 2.5).max(1.0);
let door_w = (g.cell / 5.0).max(1.0);
let mut s = String::new();
let _ = writeln!(
s,
"<svg xmlns='http://www.w3.org/2000/svg' width='{}' height='{}' viewBox='0 0 {} {}'>",
g.width, g.height, g.width, g.height
);
let _ = writeln!(
s,
"<rect width='100%' height='100%' fill='rgb({},{},{})'/>",
BG.0, BG.1, BG.2
);
if settings.export_show_grid {
for c in 0..=g.cols {
let x = g.left() + c as f32 * g.cell;
let _ = writeln!(
s,
"<line x1='{x}' y1='{}' x2='{x}' y2='{}' stroke='rgb({},{},{})' stroke-width='1'/>",
g.top(),
g.top() + g.rows as f32 * g.cell,
GRID.0,
GRID.1,
GRID.2
);
}
for r in 0..=g.rows {
let y = g.top() + r as f32 * g.cell;
let _ = writeln!(
s,
"<line x1='{}' y1='{y}' x2='{}' y2='{y}' stroke='rgb({},{},{})' stroke-width='1'/>",
g.left(),
g.left() + g.cols as f32 * g.cell,
GRID.0,
GRID.1,
GRID.2
);
}
}
let scene = collect_scene_data(layout, settings);
for &(x, y) in &scene.corridor_cells {
let px = g.left() + x as f32 * g.cell;
let py = g.top() + y as f32 * g.cell;
let _ = writeln!(
s,
"<rect x='{px}' y='{py}' width='{}' height='{}' fill='rgb({},{},{})'/>",
g.cell, g.cell, CORRIDOR.0, CORRIDOR.1, CORRIDOR.2
);
if settings.colorblind_mode {
let (cx, cy) = g.cell_center(x, y);
let _ = writeln!(
s,
"<circle cx='{cx}' cy='{cy}' r='{}' fill='black'/>",
g.cell * 0.14
);
}
}
for room in &layout.rooms {
let px = g.left() + room.x as f32 * g.cell;
let py = g.top() + room.y as f32 * g.cell;
let _ = writeln!(
s,
"<rect x='{px}' y='{py}' width='{}' height='{}' fill='rgb({},{},{})'/>",
room.width as f32 * g.cell,
room.height as f32 * g.cell,
ROOM.0,
ROOM.1,
ROOM.2
);
if settings.colorblind_mode {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
let rx = g.left() + x as f32 * g.cell + 2.0;
let ry = g.top() + y as f32 * g.cell + 2.0;
let rr = g.cell - 4.0;
let _ = writeln!(
s,
"<line x1='{rx}' y1='{ry}' x2='{}' y2='{}' stroke='black' stroke-width='1.5'/>",
rx + rr,
ry + rr
);
let _ = writeln!(
s,
"<line x1='{}' y1='{ry}' x2='{rx}' y2='{}' stroke='black' stroke-width='1.5'/>",
rx + rr,
ry + rr
);
}
}
}
}
append_svg_walls(
&mut s,
&g,
&scene.corridor_cells,
Some(&scene.corridor_edges),
&scene.door_edges,
wall_w,
);
append_svg_walls(
&mut s,
&g,
&scene.room_cells,
Some(&scene.room_edges),
&scene.door_edges,
wall_w,
);
for door in &layout.doors {
let (color, dash) = if settings.colorblind_mode {
if door.secret {
("black", Some("7,3,1,3,1,4"))
} else if door.archway {
("black", Some("2,4"))
} else if door.locked {
("black", Some("6,4"))
} else {
("black", Some("14,7"))
}
} else if door.secret {
("rgb(170,80,170)", None)
} else if door.archway {
("rgb(230,140,60)", None)
} else if door.locked {
("rgb(220,70,70)", None)
} else {
("rgb(80,200,120)", None)
};
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,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='{color}' stroke-width='{door_w}' stroke-dasharray='{pattern}'/>"
);
} else {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='{color}' stroke-width='{door_w}'/>"
);
}
}
}
for window in &layout.windows {
let dash = settings.colorblind_mode.then_some("7,3,1,4");
if let Some((x1, y1, x2, y2)) = window_line_points(&g, window) {
if let Some(pattern) = dash {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='rgb({},{},{})' stroke-width='{door_w}' stroke-dasharray='{pattern}'/>",
WINDOW_COLOR.0, WINDOW_COLOR.1, WINDOW_COLOR.2
);
} else {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='rgb({},{},{})' stroke-width='{door_w}'/>",
WINDOW_COLOR.0, WINDOW_COLOR.1, WINDOW_COLOR.2
);
}
}
}
append_svg_area_marker_group(
&mut s,
&g,
&layout.start_markers,
"S",
START_MARKER,
settings.colorblind_mode,
);
append_svg_area_marker_group(
&mut s,
&g,
&layout.end_markers,
"E",
END_MARKER,
settings.colorblind_mode,
);
append_svg_area_marker_group(
&mut s,
&g,
&layout.trap_markers,
"T",
TRAP_MARKER,
settings.colorblind_mode,
);
append_svg_area_marker_group(
&mut s,
&g,
&layout.monster_markers,
"M",
MONSTER_MARKER,
settings.colorblind_mode,
);
// Draw stairs.
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.width as f32 * g.cell;
let h = stair.height as f32 * g.cell;
let fill = if settings.colorblind_mode {
"rgb(180,180,180)"
} else {
"rgb(200,150,50)"
};
let _ = writeln!(
s,
"<rect x='{x}' y='{y}' width='{w}' height='{h}' fill='{fill}' fill-opacity='0.35' stroke='rgb(150,100,30)' stroke-width='2'/>"
);
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;
let _ = writeln!(
s,
"<line x1='{x1}' y1='{sy}' x2='{x2}' y2='{sy}' stroke='rgb(150,100,30)' stroke-width='1.5'/>",
x1 = x + 2.0,
x2 = x + w - 2.0
);
}
}
s.push_str("</svg>\n");
s
}
pub fn append_svg_walls(
out: &mut String,
g: &ExportGeometry,
cells: &HashSet<(usize, usize)>,
connected_edges: Option<&HashSet<((usize, usize), (usize, usize))>>,
door_edges: &HashSet<((usize, usize), (usize, usize))>,
width: f32,
) {
for &(col, row) in cells {
let left = g.left() + col as f32 * g.cell;
let top = g.top() + row as f32 * g.cell;
let right = left + g.cell;
let bottom = top + g.cell;
let right_cell = (col + 1, row);
let bottom_cell = (col, row + 1);
let left_edge = col == 0
|| (!cells.contains(&(col - 1, row))
&& !door_edges.contains(&norm_edge((col, row), (col - 1, row))));
let right_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), right_cell)))
.unwrap_or(true);
let right_edge = (!cells.contains(&right_cell) || !right_connected)
&& !door_edges.contains(&norm_edge((col, row), right_cell));
let top_edge = row == 0
|| (!cells.contains(&(col, row - 1))
&& !door_edges.contains(&norm_edge((col, row), (col, row - 1))));
let bottom_connected = connected_edges
.map(|set| set.contains(&norm_edge((col, row), bottom_cell)))
.unwrap_or(true);
let bottom_edge = (!cells.contains(&bottom_cell) || !bottom_connected)
&& !door_edges.contains(&norm_edge((col, row), bottom_cell));
if left_edge {
let _ = writeln!(
out,
"<rect x='{}' y='{}' width='{width}' height='{}' fill='black'/>",
left - (width * 0.5),
top - (width * 0.5),
(bottom - top) + width
);
}
if right_edge {
let _ = writeln!(
out,
"<rect x='{}' y='{}' width='{width}' height='{}' fill='black'/>",
right - (width * 0.5),
top - (width * 0.5),
(bottom - top) + width
);
}
if top_edge {
let _ = writeln!(
out,
"<rect x='{}' y='{}' width='{}' height='{width}' fill='black'/>",
left - (width * 0.5),
top - (width * 0.5),
(right - left) + width
);
}
if bottom_edge {
let _ = writeln!(
out,
"<rect x='{}' y='{}' width='{}' height='{width}' fill='black'/>",
left - (width * 0.5),
bottom - (width * 0.5),
(right - left) + width
);
}
}
}
pub fn append_svg_area_marker(
out: &mut String,
g: &ExportGeometry,
marker: &crate::layout::AreaMarker,
label: &str,
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
let x = g.left() + marker.cell.0 as f32 * g.cell;
let y = g.top() + marker.cell.1 as f32 * g.cell;
let w = marker.size as f32 * g.cell;
let h = marker.size as f32 * g.cell;
let alpha = if colorblind_mode { 0.22 } else { 0.35 };
let _ = writeln!(
out,
"<rect x='{x}' y='{y}' width='{w}' height='{h}' fill='rgb({},{},{})' fill-opacity='{alpha}' stroke='rgb({},{},{})' stroke-width='3'/>",
color.0, color.1, color.2, color.0, color.1, color.2
);
let symbol = if colorblind_mode { "black" } else { "white" };
if label == "S" {
let _ = writeln!(
out,
"<polyline points='{},{} {},{} {},{}' fill='none' stroke='{}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/>",
x + w * 0.25,
y + h * 0.75,
x + w * 0.5,
y + h * 0.25,
x + w * 0.75,
y + h * 0.75,
symbol
);
} else if label == "E" {
let _ = writeln!(
out,
"<line x1='{}' y1='{}' x2='{}' y2='{}' stroke='{}' stroke-width='4' stroke-linecap='round'/>",
x + w * 0.25,
y + h * 0.25,
x + w * 0.75,
y + h * 0.75,
symbol
);
let _ = writeln!(
out,
"<line x1='{}' y1='{}' x2='{}' y2='{}' stroke='{}' stroke-width='4' stroke-linecap='round'/>",
x + w * 0.75,
y + h * 0.25,
x + w * 0.25,
y + h * 0.75,
symbol
);
} else if label == "T" {
// Horizontal bar
let _ = writeln!(
out,
"<line x1='{}' y1='{}' x2='{}' y2='{}' stroke='{}' stroke-width='4' stroke-linecap='round'/>",
x + w * 0.2,
y + h * 0.25,
x + w * 0.8,
y + h * 0.25,
symbol
);
// Vertical bar
let _ = writeln!(
out,
"<line x1='{}' y1='{}' x2='{}' y2='{}' stroke='{}' stroke-width='4' stroke-linecap='round'/>",
x + w * 0.5,
y + h * 0.25,
x + w * 0.5,
y + h * 0.75,
symbol
);
} else if label == "M" {
let _ = writeln!(
out,
"<polyline points='{},{} {},{} {},{} {},{} {},{}' fill='none' stroke='{}' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/>",
x + w * 0.2,
y + h * 0.75,
x + w * 0.2,
y + h * 0.25,
x + w * 0.5,
y + h * 0.5,
x + w * 0.8,
y + h * 0.25,
x + w * 0.8,
y + h * 0.75,
symbol
);
}
}
pub fn append_svg_area_marker_group(
out: &mut String,
g: &ExportGeometry,
markers: &[crate::layout::AreaMarker],
label: &str,
color: (u8, u8, u8, u8),
colorblind_mode: bool,
) {
for marker in markers {
append_svg_area_marker(out, g, marker, label, color, colorblind_mode);
}
}
+206
View File
@@ -0,0 +1,206 @@
/*
* Core types and constants for the export system.
* Defines shared color constants, geometry helpers, and data structures
* used by both SVG and raster exporters.
*/
use crate::layout::{Door, DungeonLayout, Window};
use std::collections::HashSet;
use std::path::PathBuf;
use tiny_skia::Rect;
pub const BG: (u8, u8, u8, u8) = (24, 24, 26, 255);
pub const GRID: (u8, u8, u8, u8) = (130, 130, 130, 255);
pub const ROOM: (u8, u8, u8, u8) = (70, 120, 160, 255);
pub const CORRIDOR: (u8, u8, u8, u8) = (210, 190, 120, 255);
pub const BLACK: (u8, u8, u8, u8) = (0, 0, 0, 255);
pub const OPEN_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255);
pub const LOCKED_DOOR: (u8, u8, u8, u8) = (220, 70, 70, 255);
pub const SECRET_DOOR: (u8, u8, u8, u8) = (170, 80, 170, 255);
pub const ARCHWAY: (u8, u8, u8, u8) = (230, 140, 60, 255);
pub const WINDOW_COLOR: (u8, u8, u8, u8) = (70, 130, 220, 255);
pub const WHITE: (u8, u8, u8, u8) = (255, 255, 255, 255);
pub const START_MARKER: (u8, u8, u8, u8) = (60, 220, 200, 255);
pub const END_MARKER: (u8, u8, u8, u8) = (240, 90, 90, 255);
pub const TRAP_MARKER: (u8, u8, u8, u8) = (150, 80, 230, 255);
pub const MONSTER_MARKER: (u8, u8, u8, u8) = (200, 50, 50, 255);
pub const STAIRS: (u8, u8, u8, u8) = (200, 150, 50, 255);
pub enum ExportTarget {
File(PathBuf),
Folder(PathBuf),
}
#[derive(Debug, Clone, Copy)]
pub struct ExportProgress {
pub completed: usize,
pub total: usize,
}
pub enum ExportEvent {
Progress(ExportProgress),
Finished(Result<PathBuf, String>),
}
#[derive(Debug, Clone, Copy)]
pub enum DoorStyle {
Solid,
LongDash,
ShortDash,
Dotted,
DashDot,
DashDotDot,
}
#[derive(Debug, Clone, Copy)]
pub struct ExportGeometry {
pub cols: usize,
pub rows: usize,
pub cell: f32,
pub pad: f32,
pub width: u32,
pub height: u32,
}
impl ExportGeometry {
pub fn new(cols: usize, rows: usize) -> Self {
let cell = 64.0;
let pad = 0.0;
let width = (cols as f32 * cell).round() as u32;
let height = (rows as f32 * cell).round() as u32;
Self {
cols,
rows,
cell,
pad,
width,
height,
}
}
pub fn left(&self) -> f32 {
self.pad
}
pub fn top(&self) -> f32 {
self.pad
}
pub fn cell_rect(&self, col: usize, row: usize) -> Option<Rect> {
Rect::from_xywh(
self.left() + col as f32 * self.cell,
self.top() + row as f32 * self.cell,
self.cell,
self.cell,
)
}
pub fn cell_center(&self, col: usize, row: usize) -> (f32, f32) {
(
self.left() + (col as f32 + 0.5) * self.cell,
self.top() + (row as f32 + 0.5) * self.cell,
)
}
}
#[derive(Default)]
pub struct SceneData {
pub corridor_cells: HashSet<(usize, usize)>,
pub corridor_edges: HashSet<((usize, usize), (usize, usize))>,
pub room_cells: HashSet<(usize, usize)>,
pub room_edges: HashSet<((usize, usize), (usize, usize))>,
pub door_edges: HashSet<((usize, usize), (usize, usize))>,
}
#[derive(Debug, Clone, Copy)]
pub enum MaskTask {
Floors,
Walls,
Doors,
Archways,
LockedDoors,
SecretDoors,
Windows,
StartMarkers,
EndMarkers,
TrapMarkers,
MonsterMarkers,
Stairs,
Grid,
}
#[derive(Debug, Clone, Copy)]
pub enum CompositeTask {
Mask(MaskTask),
Composite,
}
pub struct CompositeExportContext<'a> {
pub layout: &'a DungeonLayout,
pub folder: PathBuf,
pub geometry: ExportGeometry,
pub scene: SceneData,
pub wall_width: f32,
pub door_width: f32,
}
pub fn norm_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
if a <= b { (a, b) } else { (b, a) }
}
pub fn door_edges_for(
door: &Door,
cols: usize,
rows: usize,
) -> Vec<((usize, usize), (usize, usize))> {
let mut edges = Vec::new();
if door.from.0.abs_diff(door.to.0) + door.from.1.abs_diff(door.to.1) != 1 {
return edges;
}
let width = door_render_width(door).max(1) as isize;
let min_offset = -((width - 1) / 2);
let max_offset = width / 2;
if door.from.0 != door.to.0 {
let y = door.from.1 as isize;
for dy in min_offset..=max_offset {
let ny = y + dy;
if ny < 0 || ny >= rows as isize {
continue;
}
let a = (door.from.0, ny as usize);
let b = (door.to.0, ny as usize);
edges.push(norm_edge(a, b));
}
} else {
let x = door.from.0 as isize;
for dx in min_offset..=max_offset {
let nx = x + dx;
if nx < 0 || nx >= cols as isize {
continue;
}
let a = (nx as usize, door.from.1);
let b = (nx as usize, door.to.1);
edges.push(norm_edge(a, b));
}
}
edges
}
pub fn door_render_width(door: &Door) -> usize {
if door.span_width {
door.width.max(1)
} else {
1
}
}
pub fn window_render_width(window: &Window) -> usize {
if window.span_width {
window.width.max(1)
} else {
1
}
}
+148
View File
@@ -0,0 +1,148 @@
/*
* Shared utilities for the export module.
* Provides functions for scene data collection and common geometric
* calculations used by both SVG and raster rendering paths.
*/
use super::types::{ExportGeometry, SceneData, door_edges_for, norm_edge, window_render_width};
use crate::layout::{DungeonLayout, Room, Window, WindowSide, corridor_cells};
use crate::ui::UiSettings;
use std::collections::HashSet;
pub fn collect_scene_data(layout: &DungeonLayout, settings: &UiSettings) -> SceneData {
let mut scene = SceneData::default();
for door in &layout.doors {
for edge in door_edges_for(door, settings.cols, settings.rows) {
scene.door_edges.insert(edge);
}
}
scene.corridor_cells = corridor_cells(layout, settings.cols, settings.rows);
scene.corridor_edges = corridor_edges_from_cells(&scene.corridor_cells);
for room in &layout.rooms {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
scene.room_cells.insert((x, y));
}
}
}
scene.room_edges = room_edges_from_rooms(&layout.rooms);
scene
}
pub fn corridor_edges_from_cells(
corridor_cells: &HashSet<(usize, usize)>,
) -> HashSet<((usize, usize), (usize, usize))> {
let mut edges = HashSet::new();
for &(col, row) in corridor_cells {
let right = (col + 1, row);
let bottom = (col, row + 1);
if corridor_cells.contains(&right) {
edges.insert(norm_edge((col, row), right));
}
if corridor_cells.contains(&bottom) {
edges.insert(norm_edge((col, row), bottom));
}
}
edges
}
pub fn room_edges_from_rooms(rooms: &[Room]) -> HashSet<((usize, usize), (usize, usize))> {
let mut edges = HashSet::new();
for room in rooms {
let x_end = room.x + room.width;
let y_end = room.y + room.height;
for x in room.x..x_end {
for y in room.y..y_end {
if x + 1 < x_end {
edges.insert(norm_edge((x, y), (x + 1, y)));
}
if y + 1 < y_end {
edges.insert(norm_edge((x, y), (x, y + 1)));
}
}
}
}
edges
}
pub fn door_line_points(
g: &ExportGeometry,
a: (usize, usize),
b: (usize, usize),
width_cells: usize,
) -> Option<(f32, f32, f32, f32)> {
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
return None;
}
let width_cells = width_cells.max(1) as isize;
let min_offset = -((width_cells - 1) / 2);
let max_offset = width_cells / 2;
if a.0 != b.0 {
let x = g.left() + (a.0.max(b.0) as f32) * g.cell;
let row = a.1 as isize;
let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y0 = g.top() + y0_cell as f32 * g.cell;
let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell;
Some((x, y0, x, y1))
} else {
let y = g.top() + (a.1.max(b.1) as f32) * g.cell;
let col = a.0 as isize;
let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x0 = g.left() + x0_cell as f32 * g.cell;
let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell;
Some((x0, y, x1, y))
}
}
pub fn window_line_points(g: &ExportGeometry, window: &Window) -> Option<(f32, f32, f32, f32)> {
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 {
WindowSide::Left => {
let x = g.left() + window.cell.0 as f32 * g.cell;
let row = window.cell.1 as isize;
let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y0 = g.top() + y0_cell as f32 * g.cell;
let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell;
Some((x, y0, x, y1))
}
WindowSide::Right => {
let x = g.left() + (window.cell.0 as f32 + 1.0) * g.cell;
let row = window.cell.1 as isize;
let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize);
let y0 = g.top() + y0_cell as f32 * g.cell;
let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell;
Some((x, y0, x, y1))
}
WindowSide::Top => {
let y = g.top() + window.cell.1 as f32 * g.cell;
let col = window.cell.0 as isize;
let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x0 = g.left() + x0_cell as f32 * g.cell;
let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell;
Some((x0, y, x1, y))
}
WindowSide::Bottom => {
let y = g.top() + (window.cell.1 as f32 + 1.0) * g.cell;
let col = window.cell.0 as isize;
let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize);
let x0 = g.left() + x0_cell as f32 * g.cell;
let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell;
Some((x0, y, x1, y))
}
}
}
-2387
View File
File diff suppressed because it is too large Load Diff
+541
View File
@@ -0,0 +1,541 @@
/*
* Manual addition and deletion tools.
* Handles placing new rooms, corridors, doors, markers, and text labels,
* as well as deleting existing elements via pointer interaction.
*/
use super::{
AddCorridorDrag, GridGeometry, HoverMarker, MarkerKind, centered_marker_origin,
corridor_drag_at_pointer, door_edge_at_pointer, door_edges_for, marker_at_pointer,
normalized_edge, pointer_to_grid, room_at_pointer, stair_at_pointer, text_at_pointer,
};
use crate::app::{DungeonApp, ManualDoorKind};
use crate::layout::{
self, blocked_room_cells, corridor_cells, room_index_at_cell, rooms_overlap,
shortest_path_cells,
};
use crate::ui::AddTool;
use eframe::egui;
pub fn handle_add_tool(app: &mut DungeonApp, response: &egui::Response, geometry: &GridGeometry) {
if app.settings.add_tool != AddTool::None && response.secondary_clicked() {
app.settings.add_tool = AddTool::None;
app.add_corridor_drag = None;
return;
}
match app.settings.add_tool {
AddTool::Room => {
app.add_corridor_drag = None;
if response.clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
{
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
add_room_at_cell(app, cell);
}
}
AddTool::Corridor => {
if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
{
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
app.add_corridor_drag = Some(AddCorridorDrag {
start_cell: cell,
current_cell: cell,
});
}
if response.dragged()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
&& let Some(drag) = &mut app.add_corridor_drag
{
drag.current_cell = (grid_x.floor() as usize, grid_y.floor() as usize);
}
if response.drag_stopped() {
if let Some(drag) = app.add_corridor_drag.take() {
add_corridor_between(app, drag.start_cell, drag.current_cell);
}
}
}
AddTool::Staircase => {
app.add_corridor_drag = None;
if response.clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
{
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
add_staircase_at_cell(app, cell);
}
}
AddTool::Archway | AddTool::Door | AddTool::LockedDoor | AddTool::SecretDoor => {
app.add_corridor_drag = None;
if response.clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
let kind = match app.settings.add_tool {
AddTool::Archway => ManualDoorKind::Archway,
AddTool::Door => ManualDoorKind::Regular,
AddTool::LockedDoor => ManualDoorKind::Locked,
AddTool::SecretDoor => ManualDoorKind::Secret,
_ => unreachable!(),
};
add_door_at_pointer(app, pointer_pos, geometry, kind);
}
}
AddTool::Text => {
app.add_corridor_drag = None;
if response.clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
{
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
add_text_at_cell(app, cell);
}
}
AddTool::StartMarker
| AddTool::EndMarker
| AddTool::TrapMarker
| AddTool::MonsterMarker => {
app.add_corridor_drag = None;
if response.clicked()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry)
{
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
let kind = match app.settings.add_tool {
AddTool::StartMarker => MarkerKind::Start,
AddTool::EndMarker => MarkerKind::End,
AddTool::TrapMarker => MarkerKind::Trap,
AddTool::MonsterMarker => MarkerKind::Monster,
_ => unreachable!(),
};
add_marker_at_cell(app, cell, kind);
}
}
AddTool::None => {
app.add_corridor_drag = None;
}
}
}
pub fn delete_at_pointer(app: &mut DungeonApp, pointer_pos: egui::Pos2, geometry: &GridGeometry) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
if let Some(idx) = text_at_pointer(app, pointer_pos, geometry) {
app.push_undo_snapshot();
let layout = &mut app.levels[app.settings.active_level_index];
if idx < layout.text_labels.len() {
layout.text_labels.remove(idx);
}
return;
}
if let Some(marker) = marker_at_pointer(app, pointer_pos, geometry) {
app.push_undo_snapshot();
let layout = &mut app.levels[app.settings.active_level_index];
match marker {
HoverMarker::Start(idx) => {
if idx < layout.start_markers.len() {
layout.start_markers.remove(idx);
}
}
HoverMarker::End(idx) => {
if idx < layout.end_markers.len() {
layout.end_markers.remove(idx);
}
}
HoverMarker::Trap(idx) => {
if idx < layout.trap_markers.len() {
layout.trap_markers.remove(idx);
}
}
HoverMarker::Monster(idx) => {
if idx < layout.monster_markers.len() {
layout.monster_markers.remove(idx);
}
}
}
return;
}
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;
if let Some(idx) = app.levels[active_index].doors.iter().position(|d| {
door_edges_for(d, app.settings.cols, app.settings.rows)
.iter()
.any(|e| normalized_edge(e.0, e.1) == target)
}) {
app.push_undo_snapshot();
let layout = &mut app.levels[app.settings.active_level_index];
if !layout.doors[idx].manual {
for edge in door_edges_for(&layout.doors[idx], app.settings.cols, app.settings.rows)
{
app.suppressed_auto_door_edges.insert(edge);
}
layout.doors.remove(idx);
return;
}
layout.doors.remove(idx);
return;
}
}
if let Some(stair_idx) = stair_at_pointer(app, pointer_pos, geometry) {
let original_cell = {
let layout = &app.levels[app.settings.active_level_index];
layout.stairs[stair_idx].cell
};
app.push_undo_snapshot();
if app.settings.sync_stairs_across_levels {
for layout in &mut app.levels {
layout.stairs.retain(|s| s.cell != original_cell);
}
} else {
let current_idx = app.settings.active_level_index;
let level_indices = vec![
Some(current_idx),
current_idx.checked_sub(1),
if current_idx + 1 < app.levels.len() {
Some(current_idx + 1)
} else {
None
},
];
for idx in level_indices.into_iter().flatten() {
app.levels[idx].stairs.retain(|s| s.cell != original_cell);
}
}
return;
}
if let Some((room_idx, _, _)) = room_at_pointer(app, pointer_pos, geometry) {
app.push_undo_snapshot();
delete_room(app, room_idx);
return;
}
if let Some(corridor_drag) = corridor_drag_at_pointer(app, pointer_pos, geometry) {
let active_index = app.settings.active_level_index;
if corridor_drag.corridor_index < app.levels[active_index].corridors.len() {
app.push_undo_snapshot();
let layout = &mut app.levels[app.settings.active_level_index];
layout.corridors.remove(corridor_drag.corridor_index);
app.drag_state = None;
app.refresh_doors();
}
}
}
pub fn add_room_at_cell(app: &mut DungeonApp, cell: (usize, usize)) {
app.push_undo_snapshot();
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
let width = app.settings.add_room_width.max(1);
let height = app.settings.add_room_height.max(1);
if cell.0 >= app.settings.cols || cell.1 >= app.settings.rows {
return;
}
let max_w = app.settings.cols.saturating_sub(cell.0);
let max_h = app.settings.rows.saturating_sub(cell.1);
let width = width.min(max_w);
let height = height.min(max_h);
if width == 0 || height == 0 {
return;
}
let new_room = layout::Room {
x: cell.0,
y: cell.1,
width,
height,
};
if layout
.rooms
.iter()
.any(|room| rooms_overlap(&new_room, room))
{
return;
}
layout.rooms.push(new_room);
app.refresh_doors();
}
pub fn add_corridor_between(app: &mut DungeonApp, start: (usize, usize), end: (usize, usize)) {
app.push_undo_snapshot();
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if start == end {
return;
}
let start_room = room_index_at_cell(&layout.rooms, start);
let end_room = room_index_at_cell(&layout.rooms, end);
let (Some(start_room), Some(end_room)) = (start_room, end_room) else {
return;
};
let blocked = blocked_room_cells(&layout.rooms, &[start_room, end_room]);
let Some(path) =
shortest_path_cells(start, end, app.settings.cols, app.settings.rows, &blocked)
else {
return;
};
let next_id = layout
.corridors
.iter()
.map(|c| c.id)
.max()
.unwrap_or(0)
.wrapping_add(1);
let width = app.settings.add_corridor_width.max(1);
layout.corridors.push(layout::Corridor {
id: next_id,
start_room_id: start_room,
end_room_id: end_room,
path,
width,
});
app.refresh_doors();
}
pub fn add_text_at_cell(app: &mut DungeonApp, cell: (usize, usize)) {
app.push_undo_snapshot();
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if cell.0 >= app.settings.cols || cell.1 >= app.settings.rows {
return;
}
let text = app.settings.add_text_value.trim().to_string();
if text.is_empty() {
return;
}
if let Some(existing) = layout
.text_labels
.iter_mut()
.find(|label| label.cell == cell)
{
existing.text = text;
return;
}
layout.text_labels.push(layout::TextLabel {
cell,
text,
font_size: app.settings.add_text_font_size,
});
}
pub fn add_marker_at_cell(app: &mut DungeonApp, cell: (usize, usize), kind: MarkerKind) {
app.push_undo_snapshot();
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if cell.0 >= app.settings.cols || cell.1 >= app.settings.rows {
return;
}
let size = app.settings.add_marker_size.max(1);
let origin = centered_marker_origin(cell, size, app.settings.cols, app.settings.rows);
let marker = layout::AreaMarker { cell: origin, size };
match kind {
MarkerKind::Start => layout.start_markers.push(marker),
MarkerKind::End => layout.end_markers.push(marker),
MarkerKind::Trap => layout.trap_markers.push(marker),
MarkerKind::Monster => layout.monster_markers.push(marker),
}
}
pub fn add_staircase_at_cell(app: &mut DungeonApp, cell: (usize, usize)) {
app.push_undo_snapshot();
if app.settings.active_level_index >= app.levels.len() {
return;
}
if cell.0 >= app.settings.cols || cell.1 >= app.settings.rows {
return;
}
let width = app.settings.add_stair_width.max(1);
let height = app.settings.add_stair_height.max(1);
let max_x = app.settings.cols.saturating_sub(width);
let max_y = app.settings.rows.saturating_sub(height);
let origin_x = cell.0.min(max_x);
let origin_y = cell.1.min(max_y);
let stair = layout::Staircase {
cell: (origin_x, origin_y),
width,
height,
};
if app.settings.sync_stairs_across_levels {
for layout in &mut app.levels {
layout.stairs.push(stair.clone());
}
} else {
let active_idx = app.settings.active_level_index;
let layout = &mut app.levels[active_idx];
layout.stairs.push(stair.clone());
if active_idx + 1 < app.levels.len() {
let above_layout = &mut app.levels[active_idx + 1];
above_layout.stairs.push(stair);
} else if active_idx > 0 {
let below_layout = &mut app.levels[active_idx - 1];
below_layout.stairs.push(stair);
}
}
}
pub fn delete_room(app: &mut DungeonApp, room_idx: usize) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if room_idx >= layout.rooms.len() {
return;
}
layout.rooms.remove(room_idx);
layout
.corridors
.retain(|c| c.start_room_id != room_idx && c.end_room_id != room_idx);
for corridor in &mut layout.corridors {
if corridor.start_room_id > room_idx {
corridor.start_room_id -= 1;
}
if corridor.end_room_id > room_idx {
corridor.end_room_id -= 1;
}
}
app.drag_state = None;
app.refresh_doors();
}
pub fn add_door_at_pointer(
app: &mut DungeonApp,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
kind: ManualDoorKind,
) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let Some(edge) = door_edge_at_pointer(app, pointer_pos, geometry) else {
return;
};
app.push_undo_snapshot();
let layout = &mut app.levels[app.settings.active_level_index];
let target = normalized_edge(edge.0, edge.1);
if let Some(idx) = layout.doors.iter().position(|d| {
door_edges_for(d, app.settings.cols, app.settings.rows)
.iter()
.any(|e| normalized_edge(e.0, e.1) == target)
}) {
let edges = door_edges_for(&layout.doors[idx], app.settings.cols, app.settings.rows);
let mut spawned = Vec::new();
for edge in edges {
if normalized_edge(edge.0, edge.1) == target {
continue;
}
if 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,
secret: false,
manual: true,
});
}
let door = &mut layout.doors[idx];
door.locked = kind == ManualDoorKind::Locked;
door.archway = kind == ManualDoorKind::Archway;
door.secret = kind == ManualDoorKind::Secret;
door.width = 1;
door.span_width = false;
door.manual = true;
for edge in door_edges_for(door, app.settings.cols, app.settings.rows) {
app.suppressed_auto_door_edges.remove(&edge);
}
layout.doors.extend(spawned);
return;
}
let corridor_cells = corridor_cells(layout, app.settings.cols, app.settings.rows);
let a_room = room_index_at_cell(&layout.rooms, edge.0);
let b_room = room_index_at_cell(&layout.rooms, edge.1);
let a_in_corridor = corridor_cells.contains(&edge.0);
let b_in_corridor = corridor_cells.contains(&edge.1);
if !manual_door_edge_allowed(a_room, b_room, a_in_corridor, b_in_corridor) {
return;
}
layout.doors.push(layout::Door {
from: edge.0,
to: edge.1,
width: 1,
span_width: false,
locked: kind == ManualDoorKind::Locked,
archway: kind == ManualDoorKind::Archway,
secret: kind == ManualDoorKind::Secret,
manual: true,
});
app.suppressed_auto_door_edges.remove(&target);
}
pub fn manual_door_edge_allowed(
a_room: Option<usize>,
b_room: Option<usize>,
a_in_corridor: bool,
b_in_corridor: bool,
) -> bool {
if matches!((a_room, b_room), (Some(a), Some(b)) if a == b) {
return false;
}
let a_is_feature = a_room.is_some() || a_in_corridor;
let b_is_feature = b_room.is_some() || b_in_corridor;
a_is_feature || b_is_feature
}
+322
View File
@@ -0,0 +1,322 @@
/*
* Drag-and-drop interaction logic.
* Handles the movement of rooms, text labels, markers, corridors, and staircases.
*/
use super::{
CorridorDragState, DragState, GridGeometry, MarkerDragState, MarkerKind, RoomDragState,
StaircaseDragState, TextDragState, corridor_drag_at_pointer, marker_drag_at_pointer,
pointer_to_grid, reroute_path_through_cell, room_at_pointer, stair_drag_at_pointer,
text_at_pointer,
};
use crate::app::DungeonApp;
use crate::layout::{blocked_room_cells, shortest_path_cells};
use crate::ui::AddTool;
use eframe::egui;
pub fn handle_drag(
app: &mut DungeonApp,
ctx: &egui::Context,
response: &egui::Response,
geometry: &GridGeometry,
) {
if !ctx.input(|i| i.pointer.primary_down()) && !response.drag_started() {
return;
}
if app.settings.add_tool != AddTool::None {
return;
}
if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(text_idx) = text_at_pointer(app, pointer_pos, geometry)
{
app.push_undo_snapshot();
app.drag_state = Some(DragState::Text(TextDragState { text_idx }));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(marker_drag) = marker_drag_at_pointer(app, pointer_pos, geometry)
{
app.push_undo_snapshot();
app.drag_state = Some(DragState::Marker(marker_drag));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(stair_drag) = stair_drag_at_pointer(app, pointer_pos, geometry)
{
app.push_undo_snapshot();
app.drag_state = Some(DragState::Staircase(stair_drag));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(corridor_drag) = corridor_drag_at_pointer(app, pointer_pos, geometry)
{
app.push_undo_snapshot();
app.drag_state = Some(DragState::Corridor(corridor_drag));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((room_idx, offset_x, offset_y)) = room_at_pointer(app, pointer_pos, geometry)
{
app.push_undo_snapshot();
app.drag_state = Some(DragState::Room(RoomDragState {
room_idx,
offset_x,
offset_y,
}));
}
if response.dragged()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(drag_state) = app.drag_state.clone()
{
match drag_state {
DragState::Room(drag) => drag_room_to_pointer(app, drag, pointer_pos, geometry),
DragState::Text(drag) => drag_text_to_pointer(app, drag, pointer_pos, geometry),
DragState::Marker(drag) => drag_marker_to_pointer(app, drag, pointer_pos, geometry),
DragState::Corridor(drag) => drag_corridor_to_pointer(app, drag, pointer_pos, geometry),
DragState::Staircase(drag) => drag_stair_to_pointer(app, drag, pointer_pos, geometry),
}
}
if response.drag_stopped() || !response.ctx.input(|i| i.pointer.primary_down()) {
app.drag_state = None;
}
}
pub fn drag_room_to_pointer(
app: &mut DungeonApp,
drag: RoomDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if app.settings.active_level_index >= app.levels.len() {
return;
}
if drag.room_idx >= app.levels[app.settings.active_level_index].rooms.len() {
app.drag_state = None;
return;
}
let (width, height, old_x, old_y) = {
let room = &app.levels[app.settings.active_level_index].rooms[drag.room_idx];
(room.width, room.height, room.x, room.y)
};
let max_x = app.settings.cols.saturating_sub(width);
let max_y = app.settings.rows.saturating_sub(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_x = target_x.min(max_x);
let new_y = target_y.min(max_y);
if new_x == old_x && new_y == old_y {
return;
}
app.levels[app.settings.active_level_index].rooms[drag.room_idx].x = new_x;
app.levels[app.settings.active_level_index].rooms[drag.room_idx].y = new_y;
reroute_corridors_for_room(app, drag.room_idx);
app.refresh_doors();
}
pub fn drag_text_to_pointer(
app: &mut DungeonApp,
drag: TextDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if app.settings.active_level_index >= app.levels.len() {
return;
}
if drag.text_idx
>= app.levels[app.settings.active_level_index]
.text_labels
.len()
{
app.drag_state = None;
return;
}
let target = (
(grid_x.floor().max(0.0) as usize).min(app.settings.cols.saturating_sub(1)),
(grid_y.floor().max(0.0) as usize).min(app.settings.rows.saturating_sub(1)),
);
app.levels[app.settings.active_level_index].text_labels[drag.text_idx].cell = target;
}
pub fn reroute_corridors_for_room(app: &mut DungeonApp, room_idx: usize) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if layout.packed_rooms {
return;
}
for corridor in &mut layout.corridors {
if corridor.start_room_id != room_idx && corridor.end_room_id != room_idx {
continue;
}
let start = layout.rooms[corridor.start_room_id].center_cell();
let end = layout.rooms[corridor.end_room_id].center_cell();
let blocked = blocked_room_cells(
&layout.rooms,
&[corridor.start_room_id, corridor.end_room_id],
);
if let Some(path) =
shortest_path_cells(start, end, app.settings.cols, app.settings.rows, &blocked)
{
corridor.path = path;
}
}
}
pub fn drag_corridor_to_pointer(
app: &mut DungeonApp,
drag: CorridorDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
if drag.corridor_index >= layout.corridors.len() {
app.drag_state = None;
return;
}
let corridor = &layout.corridors[drag.corridor_index];
let target = (
(grid_x.floor().max(0.0) as usize).min(app.settings.cols.saturating_sub(1)),
(grid_y.floor().max(0.0) as usize).min(app.settings.rows.saturating_sub(1)),
);
let Some(new_path) = reroute_path_through_cell(
&drag.original_path,
drag.original_cell,
target,
app.settings.cols,
app.settings.rows,
&blocked_room_cells(
&layout.rooms,
&[corridor.start_room_id, corridor.end_room_id],
),
) else {
return;
};
layout.corridors[drag.corridor_index].path = new_path;
app.refresh_doors();
}
pub fn drag_marker_to_pointer(
app: &mut DungeonApp,
drag: MarkerDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
let markers = match drag.kind {
MarkerKind::Start => &mut layout.start_markers,
MarkerKind::End => &mut layout.end_markers,
MarkerKind::Trap => &mut layout.trap_markers,
MarkerKind::Monster => &mut layout.monster_markers,
};
let Some(marker) = markers.get_mut(drag.marker_idx) else {
app.drag_state = None;
return;
};
let max_x = app.settings.cols.saturating_sub(marker.size);
let max_y = app.settings.rows.saturating_sub(marker.size);
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;
marker.cell = (target_x.min(max_x), target_y.min(max_y));
}
pub fn drag_stair_to_pointer(
app: &mut DungeonApp,
drag: StaircaseDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if app.settings.active_level_index >= app.levels.len() {
return;
}
let (stair_width, stair_height) = {
let layout = &app.levels[app.settings.active_level_index];
let Some(stair) = layout.stairs.get(drag.stair_idx) else {
return;
};
(stair.width, stair.height)
};
let max_x = app.settings.cols.saturating_sub(stair_width);
let max_y = app.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));
if app.settings.sync_stairs_across_levels {
for layout in &mut app.levels {
for stair in &mut layout.stairs {
if stair.cell == drag.original_cell {
stair.cell = new_cell;
}
}
}
} else {
let current_idx = app.settings.active_level_index;
let level_indices = vec![
Some(current_idx),
current_idx.checked_sub(1),
if current_idx + 1 < app.levels.len() {
Some(current_idx + 1)
} else {
None
},
];
for idx in level_indices.into_iter().flatten() {
let layout = &mut app.levels[idx];
for stair in &mut layout.stairs {
if stair.cell == drag.original_cell {
stair.cell = new_cell;
}
}
}
}
if let Some(DragState::Staircase(ref mut d)) = app.drag_state {
d.original_cell = new_cell;
}
}
+1155
View File
File diff suppressed because it is too large Load Diff
+320
View File
@@ -0,0 +1,320 @@
/*
* Resize interaction logic.
* Handles resizing of rooms, text labels, and staircases using secondary click + drag.
*/
use super::{
GridGeometry, ResizeCorner, ResizeState, RoomResizeState, StaircaseResizeState,
TextResizeState, pointer_to_grid, reroute_corridors_for_room, resize_room_at_pointer,
stair_at_pointer, text_at_pointer,
};
use crate::app::DungeonApp;
use crate::layout::{rects_overlap, rooms_overlap};
use crate::ui::AddTool;
use eframe::egui;
pub fn handle_resize(
app: &mut DungeonApp,
ctx: &egui::Context,
response: &egui::Response,
geometry: &GridGeometry,
) -> bool {
if app.settings.add_tool != AddTool::None {
return false;
}
if ctx.input(|i| i.pointer.secondary_pressed()) && app.resize_state.is_none() {
if let Some(pointer_pos) = response.interact_pointer_pos() {
if let Some(text_idx) = text_at_pointer(app, pointer_pos, geometry) {
if let Some(state) = start_text_resize(app, text_idx) {
app.push_undo_snapshot();
app.resize_state = Some(ResizeState::Text(state));
return true;
}
} else if let Some(stair_idx) = stair_at_pointer(app, pointer_pos, geometry) {
if let Some(state) = start_staircase_resize(app, stair_idx, pointer_pos, geometry) {
app.push_undo_snapshot();
app.resize_state = Some(ResizeState::Staircase(state));
return true;
}
} else if let Some(room_idx) = resize_room_at_pointer(app, pointer_pos, geometry) {
if let Some(state) = start_resize(app, room_idx, pointer_pos, geometry) {
app.push_undo_snapshot();
app.resize_state = Some(ResizeState::Room(state));
return true;
}
}
}
}
if let Some(state) = app.resize_state.clone() {
if response.dragged_by(egui::PointerButton::Secondary)
&& ctx.input(|i| i.pointer.secondary_down())
&& let Some(pointer_pos) = response.interact_pointer_pos()
{
if let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) {
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
match state {
ResizeState::Room(state) => resize_room(app, &state, cell),
ResizeState::Text(state) => resize_text(app, &state, cell),
ResizeState::Staircase(state) => resize_staircase(app, &state, cell),
}
}
}
if ctx.input(|i| i.pointer.button_released(egui::PointerButton::Secondary)) {
app.resize_state = None;
}
return true;
}
false
}
pub fn start_text_resize(app: &DungeonApp, text_idx: usize) -> Option<TextResizeState> {
if app.settings.active_level_index >= app.levels.len() {
return None;
}
let layout = &app.levels[app.settings.active_level_index];
let label = layout.text_labels.get(text_idx)?;
Some(TextResizeState {
text_idx,
origin_cell: label.cell,
original_font_size: label.font_size,
})
}
pub fn start_resize(
app: &DungeonApp,
room_idx: usize,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) -> Option<RoomResizeState> {
if app.settings.active_level_index >= app.levels.len() {
return None;
}
let layout = &app.levels[app.settings.active_level_index];
let original_room = layout.rooms.get(room_idx)?.clone();
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let center_x = original_room.x as f32 + original_room.width as f32 * 0.5;
let center_y = original_room.y as f32 + original_room.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(RoomResizeState {
room_idx,
original_room,
resize_corner,
})
}
pub fn start_staircase_resize(
app: &DungeonApp,
stair_idx: usize,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) -> Option<StaircaseResizeState> {
if app.settings.active_level_index >= app.levels.len() {
return None;
}
let layout = &app.levels[app.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,
})
}
pub fn resize_room(app: &mut DungeonApp, state: &RoomResizeState, target_cell: (usize, usize)) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
let cols = app.settings.cols.max(1);
let rows = app.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_room;
let left = original.x as isize;
let top = original.y as isize;
let right = (original.x + original.width - 1) as isize;
let bottom = (original.y + 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_room = crate::layout::Room {
x: x0 as usize,
y: y0 as usize,
width: (x1 - x0 + 1) as usize,
height: (y1 - y0 + 1) as usize,
};
if new_room.width == 0 || new_room.height == 0 {
return;
}
if new_room.x + new_room.width > cols || new_room.y + new_room.height > rows {
return;
}
if layout
.rooms
.iter()
.enumerate()
.any(|(idx, room)| idx != state.room_idx && rooms_overlap(&new_room, room))
{
return;
}
if let Some(room) = layout.rooms.get_mut(state.room_idx) {
room.x = new_room.x;
room.y = new_room.y;
room.width = new_room.width;
room.height = new_room.height;
reroute_corridors_for_room(app, state.room_idx);
app.refresh_doors();
}
}
pub fn resize_text(app: &mut DungeonApp, state: &TextResizeState, target_cell: (usize, usize)) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let layout = &mut app.levels[app.settings.active_level_index];
let Some(label) = layout.text_labels.get_mut(state.text_idx) else {
app.resize_state = None;
return;
};
let dx = target_cell.0 as isize - state.origin_cell.0 as isize;
let dy = target_cell.1 as isize - state.origin_cell.1 as isize;
let delta_cells = if dx.abs() >= dy.abs() { dx } else { -dy };
let new_size = (state.original_font_size as isize + (delta_cells * 4)).clamp(8, 96);
label.font_size = new_size as u16;
}
pub fn resize_staircase(
app: &mut DungeonApp,
state: &StaircaseResizeState,
target_cell: (usize, usize),
) {
if app.settings.active_level_index >= app.levels.len() {
return;
}
let cols = app.settings.cols.max(1);
let rows = app.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;
}
let active_layout = &app.levels[app.settings.active_level_index];
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 app.settings.sync_stairs_across_levels {
for layout in &mut app.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 = app.settings.active_level_index;
let level_indices = vec![
Some(current_idx),
current_idx.checked_sub(1),
if current_idx + 1 < app.levels.len() {
Some(current_idx + 1)
} else {
None
},
];
for idx in level_indices.into_iter().flatten() {
let layout = &mut app.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;
}
}
}
}
if let Some(ResizeState::Staircase(ref mut s)) = app.resize_state {
s.original_stair.cell = new_stair_cell;
s.original_stair.width = new_stair_width;
s.original_stair.height = new_stair_height;
}
}
+108
View File
@@ -0,0 +1,108 @@
/*
* Core interaction state types and enums.
* Defines the various states for dragging and resizing dungeon elements.
*/
use crate::layout;
#[derive(Debug, Clone)]
pub struct RoomDragState {
pub room_idx: usize,
pub offset_x: f32,
pub offset_y: f32,
}
#[derive(Debug, Clone)]
pub struct TextDragState {
pub text_idx: usize,
}
#[derive(Debug, Clone)]
pub struct MarkerDragState {
pub kind: MarkerKind,
pub marker_idx: usize,
pub offset_x: f32,
pub offset_y: f32,
}
#[derive(Debug, Clone)]
pub struct CorridorDragState {
pub corridor_index: usize,
pub original_path: Vec<(usize, usize)>,
pub original_cell: (usize, usize),
}
#[derive(Debug, Clone)]
pub struct AddCorridorDrag {
pub start_cell: (usize, usize),
pub current_cell: (usize, usize),
}
#[derive(Debug, Clone, Copy)]
pub enum ResizeCorner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkerKind {
Start,
End,
Trap,
Monster,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HoverMarker {
Start(usize),
End(usize),
Trap(usize),
Monster(usize),
}
#[derive(Debug, Clone)]
pub struct RoomResizeState {
pub room_idx: usize,
pub original_room: layout::Room,
pub resize_corner: ResizeCorner,
}
#[derive(Debug, Clone)]
pub enum DragState {
Room(RoomDragState),
Text(TextDragState),
Marker(MarkerDragState),
Corridor(CorridorDragState),
Staircase(StaircaseDragState),
}
#[derive(Debug, Clone)]
pub struct StaircaseDragState {
pub stair_idx: usize,
pub offset_x: f32,
pub offset_y: f32,
pub original_cell: (usize, usize),
}
#[derive(Debug, Clone)]
pub struct TextResizeState {
pub text_idx: usize,
pub origin_cell: (usize, usize),
pub original_font_size: u16,
}
#[derive(Debug, Clone)]
pub struct StaircaseResizeState {
pub stair_idx: usize,
pub original_stair: layout::Staircase,
pub resize_corner: ResizeCorner,
}
#[derive(Debug, Clone)]
pub enum ResizeState {
Room(RoomResizeState),
Text(TextResizeState),
Staircase(StaircaseResizeState),
}
File diff suppressed because it is too large Load Diff
+192
View File
@@ -0,0 +1,192 @@
/*
* Room connection graph and corridor pathing.
* Builds the logical connections between rooms and generates the
* physical grid paths for corridors.
*/
use super::super::types::Room;
use super::super::utils::{
SimpleRng, manhattan_distance, normalized_cell_edge, shared_boundary_edges, shuffle_indices,
};
use std::collections::HashSet;
pub fn build_room_connection_edges(
centers: &[(usize, usize)],
randomness: f32,
target_dead_end_rooms: usize,
rng: &mut SimpleRng,
) -> Vec<(usize, usize)> {
if centers.len() < 2 {
return Vec::new();
}
let room_count = centers.len();
let max_dead_ends = room_count / 2;
let desired_dead_ends = target_dead_end_rooms.min(max_dead_ends);
let core_count = (room_count - desired_dead_ends).max(1);
let mut room_indices: Vec<usize> = (0..room_count).collect();
shuffle_indices(&mut room_indices, rng);
let mut core_rooms = room_indices[..core_count].to_vec();
let leaf_rooms = room_indices[core_count..].to_vec();
core_rooms = ordered_core_rooms(&core_rooms, centers, randomness, rng);
let mut edges = Vec::new();
let mut edge_set = HashSet::new();
if core_rooms.len() >= 2 {
for pair in core_rooms.windows(2) {
push_unique_room_edge(pair[0], pair[1], &mut edges, &mut edge_set);
}
if core_rooms.len() >= 3 {
push_unique_room_edge(
core_rooms[core_rooms.len() - 1],
core_rooms[0],
&mut edges,
&mut edge_set,
);
}
}
for leaf in leaf_rooms {
let mut best_anchor = core_rooms[0];
let mut best_score = f32::INFINITY;
for &core in &core_rooms {
let dist = manhattan_distance(centers[leaf], centers[core]) as f32;
let score = (dist * (1.0 - 0.8 * randomness)) + (rng.next_f32() * 30.0 * randomness);
if score < best_score {
best_score = score;
best_anchor = core;
}
}
push_unique_room_edge(leaf, best_anchor, &mut edges, &mut edge_set);
}
edges
}
pub fn ordered_core_rooms(
core_rooms: &[usize],
centers: &[(usize, usize)],
randomness: f32,
rng: &mut SimpleRng,
) -> Vec<usize> {
if core_rooms.len() <= 2 {
return core_rooms.to_vec();
}
let mut remaining = core_rooms.to_vec();
let start_idx = rng.range_inclusive(0, remaining.len() - 1);
let mut ordered = vec![remaining.swap_remove(start_idx)];
while !remaining.is_empty() {
let last = *ordered.last().unwrap_or(&remaining[0]);
let mut best_idx = 0usize;
let mut best_score = f32::INFINITY;
for (idx, candidate) in remaining.iter().enumerate() {
let dist = manhattan_distance(centers[last], centers[*candidate]) as f32;
let score = (dist * (1.0 - 0.85 * randomness)) + (rng.next_f32() * 20.0 * randomness);
if score < best_score {
best_score = score;
best_idx = idx;
}
}
ordered.push(remaining.swap_remove(best_idx));
}
ordered
}
pub fn push_unique_room_edge(
a: usize,
b: usize,
edges: &mut Vec<(usize, usize)>,
edge_set: &mut HashSet<(usize, usize)>,
) {
if a == b {
return;
}
let normalized = if a < b { (a, b) } else { (b, a) };
if edge_set.insert(normalized) {
edges.push((a, b));
}
}
pub fn room_exit_edge(
path: &[(usize, usize)],
room: &Room,
from_start: bool,
) -> Option<((usize, usize), (usize, usize))> {
let in_room = |cell: (usize, usize)| {
cell.0 >= room.x
&& cell.0 < room.x + room.width
&& cell.1 >= room.y
&& cell.1 < room.y + room.height
};
if from_start {
for pair in path.windows(2) {
if in_room(pair[0]) && !in_room(pair[1]) {
return Some(normalized_cell_edge(pair[0], pair[1]));
}
}
} else {
for pair in path.windows(2).rev() {
if in_room(pair[1]) && !in_room(pair[0]) {
return Some(normalized_cell_edge(pair[0], pair[1]));
}
}
}
None
}
pub fn room_collision_edges(
path: &[(usize, usize)],
room: &Room,
) -> Vec<((usize, usize), (usize, usize))> {
let in_room = |cell: (usize, usize)| {
cell.0 >= room.x
&& cell.0 < room.x + room.width
&& cell.1 >= room.y
&& cell.1 < room.y + room.height
};
let mut edges = Vec::new();
let mut seen = HashSet::new();
for pair in path.windows(2) {
let a_in_room = in_room(pair[0]);
let b_in_room = in_room(pair[1]);
if a_in_room == b_in_room {
continue;
}
let edge = normalized_cell_edge(pair[0], pair[1]);
if seen.insert(edge) {
edges.push(edge);
}
}
edges
}
pub fn shared_room_boundaries(
rooms: &[Room],
) -> Vec<(usize, usize, Vec<((usize, usize), (usize, usize))>)> {
let mut boundaries = Vec::new();
for a_idx in 0..rooms.len() {
for b_idx in (a_idx + 1)..rooms.len() {
let shared_edges = shared_boundary_edges(&rooms[a_idx], &rooms[b_idx]);
if !shared_edges.is_empty() {
boundaries.push((a_idx, b_idx, shared_edges));
}
}
}
boundaries
}
+607
View File
@@ -0,0 +1,607 @@
/*
* Marker and transition population.
* Handles the placement of start/end points, traps, monsters,
* and staircase transitions between dungeon levels.
*/
use super::super::types::{AreaMarker, Corridor, DungeonLayout, Room, Staircase};
use super::super::utils::room_index_at_cell;
use crate::seed;
use crate::ui::UiSettings;
use std::collections::HashSet;
pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout {
layout.start_markers.clear();
layout.end_markers.clear();
layout.trap_markers.clear();
layout.monster_markers.clear();
if layout.rooms.is_empty() {
return layout;
}
let start_count = random_range_inclusive(
settings.min_start_marker_count,
settings.max_start_marker_count,
seed::derive_seed(settings.seed, 10_001),
);
let end_count = random_range_inclusive(
settings.min_end_marker_count,
settings.max_end_marker_count,
seed::derive_seed(settings.seed, 10_002),
);
let pair_count = start_count.min(end_count);
let mut available_start_rooms: Vec<usize> = (0..layout.rooms.len()).collect();
let mut available_end_rooms: Vec<usize> = (0..layout.rooms.len()).collect();
for pair_idx in 0..pair_count {
let (start_room_idx, end_room_idx) =
farthest_room_pair(&layout.rooms, &available_start_rooms, &available_end_rooms);
let start_room = &layout.rooms[start_room_idx];
let end_room = &layout.rooms[end_room_idx];
layout.start_markers.push(marker_in_room(
start_room,
settings.min_start_marker_size,
settings.max_start_marker_size,
seed::derive_seed(settings.seed, 11_000 + pair_idx as u64),
));
layout.end_markers.push(marker_in_room(
end_room,
settings.min_end_marker_size,
settings.max_end_marker_size,
seed::derive_seed(settings.seed, 12_000 + pair_idx as u64),
));
consume_room(&mut available_start_rooms, start_room_idx);
consume_room(&mut available_end_rooms, end_room_idx);
}
assign_extra_markers(
&mut layout.start_markers,
&layout.rooms,
&mut available_start_rooms,
pair_count,
start_count,
settings.min_start_marker_size,
settings.max_start_marker_size,
settings.seed,
13_000,
14_000,
);
assign_extra_markers(
&mut layout.end_markers,
&layout.rooms,
&mut available_end_rooms,
pair_count,
end_count,
settings.min_end_marker_size,
settings.max_end_marker_size,
settings.seed,
15_000,
16_000,
);
layout = populate_random_traps(layout, settings);
layout = populate_random_monsters(layout, settings);
layout
}
pub fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize {
let min = min.max(1);
let max = max.max(min);
let span = max - min + 1;
min + (seed_value as usize % span)
}
pub fn marker_in_room(
room: &Room,
min_size: usize,
max_size: usize,
seed_value: u64,
) -> AreaMarker {
let room_limit = room.width.min(room.height).max(1);
let min_size = min_size.clamp(1, 10).min(room_limit);
let max_size = max_size.clamp(min_size, 10).min(room_limit);
let size = random_range_inclusive(min_size, max_size, seed_value.rotate_left(7));
let x_span = room.width.saturating_sub(size);
let y_span = room.height.saturating_sub(size);
let x = room.x
+ if x_span == 0 {
0
} else {
seed_value as usize % (x_span + 1)
};
let y = room.y
+ if y_span == 0 {
0
} else {
seed_value.rotate_left(19) as usize % (y_span + 1)
};
AreaMarker { cell: (x, y), size }
}
pub fn assign_extra_markers(
markers: &mut Vec<AreaMarker>,
rooms: &[Room],
available_rooms: &mut Vec<usize>,
start_idx: usize,
end_idx: usize,
min_size: usize,
max_size: usize,
seed_base: u64,
room_stream_base: u64,
marker_stream_base: u64,
) {
for idx in start_idx..end_idx {
let room_idx = pick_room_index(
available_rooms,
rooms.len(),
seed::derive_seed(seed_base, room_stream_base + idx as u64),
);
markers.push(marker_in_room(
&rooms[room_idx],
min_size,
max_size,
seed::derive_seed(seed_base, marker_stream_base + idx as u64),
));
consume_room(available_rooms, room_idx);
}
}
pub fn farthest_room_pair(
rooms: &[Room],
available_start_rooms: &[usize],
available_end_rooms: &[usize],
) -> (usize, usize) {
let start_rooms = if available_start_rooms.is_empty() {
(0..rooms.len()).collect::<Vec<_>>()
} else {
available_start_rooms.to_vec()
};
let end_rooms = if available_end_rooms.is_empty() {
(0..rooms.len()).collect::<Vec<_>>()
} else {
available_end_rooms.to_vec()
};
let mut best = (start_rooms[0], end_rooms[0]);
let mut best_dist = 0usize;
for &start_idx in &start_rooms {
for &end_idx in &end_rooms {
if rooms.len() > 1 && start_idx == end_idx {
continue;
}
let dist = room_distance_sq(&rooms[start_idx], &rooms[end_idx]);
if dist > best_dist {
best = (start_idx, end_idx);
best_dist = dist;
}
}
}
best
}
pub fn room_distance_sq(a: &Room, b: &Room) -> usize {
let ac = a.center_cell();
let bc = b.center_cell();
let dx = ac.0.abs_diff(bc.0);
let dy = ac.1.abs_diff(bc.1);
dx * dx + dy * dy
}
pub fn consume_room(available_rooms: &mut Vec<usize>, room_idx: usize) {
if let Some(pos) = available_rooms.iter().position(|&idx| idx == room_idx) {
available_rooms.remove(pos);
}
}
pub fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64) -> usize {
if !available_rooms.is_empty() {
available_rooms[seed_value as usize % available_rooms.len()]
} else {
seed_value as usize % room_count.max(1)
}
}
pub fn populate_random_traps(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout {
layout.trap_markers = populate_random_area_markers(
&layout,
settings.seed,
settings.trap_frequency_percent,
settings.min_traps_per_area,
settings.max_traps_per_area,
17_000,
18_000,
);
layout
}
pub fn populate_random_monsters(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout {
layout.monster_markers = populate_random_area_markers(
&layout,
settings.seed,
settings.monster_frequency_percent,
settings.min_monsters_per_area,
settings.max_monsters_per_area,
19_000,
20_000,
);
layout
}
pub fn populate_random_area_markers(
layout: &DungeonLayout,
master_seed: u64,
frequency_percent: usize,
min_count: usize,
max_count: usize,
room_stream_base: u64,
corridor_stream_base: u64,
) -> Vec<AreaMarker> {
if frequency_percent == 0 {
return Vec::new();
}
let mut markers = Vec::new();
for (idx, room) in layout.rooms.iter().enumerate() {
let area_seed = seed::derive_seed(master_seed, room_stream_base + idx as u64);
append_room_markers(
&mut markers,
room,
area_seed,
frequency_percent,
min_count,
max_count,
);
}
for (idx, corridor) in layout.corridors.iter().enumerate() {
let area_seed = seed::derive_seed(master_seed, corridor_stream_base + idx as u64);
append_corridor_markers(
&mut markers,
corridor,
area_seed,
frequency_percent,
min_count,
max_count,
);
}
markers
}
pub fn append_room_markers(
markers: &mut Vec<AreaMarker>,
room: &Room,
area_seed: u64,
frequency_percent: usize,
min_count: usize,
max_count: usize,
) {
if !passes_frequency_roll(area_seed, frequency_percent) {
return;
}
let count = random_range_inclusive(min_count, max_count, area_seed.rotate_left(13));
for offset in 0..count {
markers.push(marker_in_room(
room,
1,
1,
area_seed.wrapping_add(offset as u64).rotate_right(7),
));
}
}
pub fn append_corridor_markers(
markers: &mut Vec<AreaMarker>,
corridor: &Corridor,
area_seed: u64,
frequency_percent: usize,
min_count: usize,
max_count: usize,
) {
if !passes_frequency_roll(area_seed, frequency_percent) {
return;
}
let count = random_range_inclusive(min_count, max_count, area_seed.rotate_left(13));
for offset in 0..count {
if let Some(cell) =
pick_random_corridor_cell(corridor, area_seed.wrapping_add(offset as u64))
{
markers.push(AreaMarker { cell, size: 1 });
}
}
}
pub fn passes_frequency_roll(area_seed: u64, frequency_percent: usize) -> bool {
(area_seed % 100) < frequency_percent as u64
}
pub fn pick_random_corridor_cell(corridor: &Corridor, seed_value: u64) -> Option<(usize, usize)> {
if corridor.path.is_empty() {
return None;
}
let idx = seed_value as usize % corridor.path.len();
Some(corridor.path[idx])
}
pub fn populate_stairs(
mut layouts: Vec<DungeonLayout>,
settings: &UiSettings,
) -> Vec<(DungeonLayout, bool)> {
if layouts.is_empty() {
return Vec::new();
}
for layout in &mut layouts {
layout.stairs.clear();
}
if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 {
return layouts.into_iter().map(|layout| (layout, false)).collect();
}
let num_levels = layouts.len();
let num_gaps = num_levels.saturating_sub(1);
let sets_to_gen = if num_gaps == 0 { 1 } else { num_gaps };
let mut all_stair_sets: Vec<Vec<Staircase>> = Vec::new();
if settings.sync_stairs_across_levels {
let stair_count = get_stair_count(settings, 0);
let synced_stairs = pick_stair_positions(&layouts[0], settings, stair_count, 0);
for _ in 0..sets_to_gen {
all_stair_sets.push(synced_stairs.clone());
}
} else {
for gap_idx in 0..sets_to_gen {
let stair_count = get_stair_count(settings, gap_idx);
let layout_idx = gap_idx.min(num_levels - 1);
let stairs =
pick_stair_positions(&layouts[layout_idx], settings, stair_count, gap_idx as u64);
all_stair_sets.push(stairs);
}
}
if num_levels == 1 {
layouts[0].stairs.extend(all_stair_sets[0].clone());
} else {
for gap_idx in 0..num_gaps {
let stairs = &all_stair_sets[gap_idx];
layouts[gap_idx].stairs.extend(stairs.clone());
layouts[gap_idx + 1].stairs.extend(stairs.clone());
}
}
for layout in &mut layouts {
let mut seen = HashSet::new();
layout.stairs.retain(|stair| seen.insert(stair.cell));
}
let mut result = Vec::new();
for mut layout in layouts {
let mut modified = false;
let stairs = layout.stairs.clone();
for stair in &stairs {
if ensure_stair_in_room(&mut layout, stair) {
modified = true;
}
}
result.push((layout, modified));
}
result
}
pub fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
if settings.min_stairs_per_level == settings.max_stairs_per_level {
settings.min_stairs_per_level
} else {
let range_seed = seed::derive_seed(settings.seed, 0x2A_3B_4_u64 + gap_idx as u64);
(range_seed as usize % (settings.max_stairs_per_level - settings.min_stairs_per_level + 1))
+ settings.min_stairs_per_level
}
}
pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &Staircase) -> bool {
let stair_x = stair.cell.0;
let stair_y = stair.cell.1;
let stair_width = stair.width;
let stair_height = stair.height;
for room in &layout.rooms {
if stair_x >= room.x
&& stair_y >= room.y
&& (stair_x + stair_width) <= room.x + room.width
&& (stair_y + stair_height) <= room.y + room.height
{
return false;
}
}
let mut nearest_room_idx = None;
let mut min_dist = usize::MAX;
for (idx, room) in layout.rooms.iter().enumerate() {
let dist = room_to_stair_min_dist(room, stair);
if dist < min_dist {
min_dist = dist;
nearest_room_idx = Some(idx);
}
}
if let Some(idx) = nearest_room_idx
&& min_dist <= 5
{
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_width);
let new_bottom = (room.y + room.height).max(stair_y + stair_height);
room.x = new_x;
room.y = new_y;
room.width = new_right - new_x;
room.height = new_bottom - new_y;
true
} else {
layout.rooms.push(Room {
x: stair_x,
y: stair_y,
width: stair_width,
height: stair_height,
});
true
}
}
pub fn room_to_stair_min_dist(room: &Room, stair: &Staircase) -> usize {
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.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 {
0
};
dx + dy
}
pub fn pick_stair_positions(
layout: &DungeonLayout,
settings: &UiSettings,
count: usize,
seed_offset: u64,
) -> Vec<Staircase> {
if layout.rooms.is_empty() || count == 0 {
return Vec::new();
}
let mut valid_cells = Vec::new();
for room in &layout.rooms {
let blocked_bottom_row =
has_start_or_end_on_bottom_row(room, &layout.start_markers, &layout.end_markers);
if !blocked_bottom_row {
for x in room.x..(room.x + room.width) {
for y in room.y..(room.y + room.height) {
valid_cells.push((x, y));
}
}
}
}
if valid_cells.is_empty() {
return Vec::new();
}
let mut shuffled = valid_cells.clone();
shuffle_with_seed(
&mut shuffled,
settings.seed.wrapping_add(21_000).wrapping_add(seed_offset),
);
let max_size = settings.max_stair_width.max(settings.max_stair_height);
let mut stair_cells = Vec::new();
let mut used_cells = HashSet::new();
for cell in shuffled {
if stair_cells.len() >= count || used_cells.contains(&cell) {
continue;
}
if let Some(room_idx) = room_index_at_cell(&layout.rooms, cell) {
let room = &layout.rooms[room_idx];
let available_w = room.x + room.width - cell.0;
let available_h = room.y + room.height - cell.1;
if available_w >= settings.min_stair_width && available_h >= settings.min_stair_height {
stair_cells.push(cell);
for dx in 0..max_size {
for dy in 0..max_size {
used_cells.insert((cell.0 + dx, cell.1 + dy));
}
}
}
}
}
stair_cells
.into_iter()
.enumerate()
.map(|(idx, cell)| {
let (width, height) = random_stair_size(
settings,
seed::derive_seed(settings.seed, 21_000 + idx as u64 + seed_offset * 1000),
);
Staircase {
cell,
width,
height,
}
})
.collect()
}
pub fn has_start_or_end_on_bottom_row(
room: &Room,
start_markers: &[AreaMarker],
end_markers: &[AreaMarker],
) -> bool {
let bottom_row = room.y + room.height - 1;
for marker in start_markers.iter().chain(end_markers.iter()) {
let marker_bottom = marker.cell.1 + marker.size;
if marker.cell.1 <= bottom_row && marker_bottom > bottom_row {
return true;
}
}
false
}
pub 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)
}
pub fn shuffle_with_seed<T>(items: &mut [T], seed: u64) {
let mut rng = StairShuffleRng::new(seed);
for idx in (1..items.len()).rev() {
let swap_idx = rng.next_u32() as usize % (idx + 1);
items.swap(idx, swap_idx);
}
}
pub struct StairShuffleRng {
pub state: u64,
}
impl StairShuffleRng {
pub fn new(seed: u64) -> Self {
Self { state: seed }
}
pub fn next_u32(&mut self) -> u32 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(self.state >> 32) as u32
}
}
+195
View File
@@ -0,0 +1,195 @@
/*
* Main generation module.
* Orchestrates the procedural generation process, coordinating
* room placement, connection graph building, and passage/marker population.
*/
use super::types::{Corridor, DoorSettings, DungeonLayout, Room, WindowSettings};
use super::utils::{SimpleRng, blocked_room_cells, noisy_path, shortest_path_cells};
use crate::seed;
pub mod connections;
pub mod markers;
pub mod passages;
pub mod room_placement;
pub use connections::*;
pub use markers::*;
pub use passages::*;
pub use room_placement::*;
pub fn generate_layout(
cols: usize,
rows: usize,
target_room_count: usize,
seed: u64,
min_room_size: usize,
max_room_size: usize,
square_rooms_only: bool,
min_corridor_width: usize,
max_corridor_width: usize,
corridor_randomness_percent: usize,
dead_end_room_percent: usize,
pack_rooms_without_corridors: bool,
door_settings: DoorSettings,
window_settings: WindowSettings,
) -> DungeonLayout {
let layout_salt = ((cols as u64) << 48)
^ ((rows as u64) << 32)
^ ((target_room_count as u64) << 16)
^ (min_room_size as u64)
^ ((max_room_size as u64) << 8)
^ ((min_corridor_width as u64) << 24)
^ ((max_corridor_width as u64) << 28)
^ ((corridor_randomness_percent as u64) << 56)
^ ((dead_end_room_percent as u64) << 40);
let base_seed = seed::derive_seed(seed, layout_salt);
let mut room_rng = SimpleRng::new(seed::derive_seed(base_seed, 1));
let mut graph_rng = SimpleRng::new(seed::derive_seed(base_seed, 2));
let mut path_rng = SimpleRng::new(seed::derive_seed(base_seed, 3));
let mut corridor_rng = SimpleRng::new(seed::derive_seed(base_seed, 4));
let mut rooms = Vec::new();
let corridors = Vec::new();
if cols < 2 || rows < 2 || target_room_count == 0 {
return DungeonLayout::empty(pack_rooms_without_corridors);
}
let mut min_size = min_room_size.max(2);
let mut max_size = max_room_size.max(min_size);
if square_rooms_only {
let hard_max = cols.min(rows);
min_size = min_size.min(hard_max);
max_size = max_size.min(hard_max);
} else {
min_size = min_size.min(cols.min(rows));
max_size = max_size.min(cols.max(rows));
}
if min_size == 0 || max_size < min_size {
return DungeonLayout::empty(pack_rooms_without_corridors);
}
let room_sizes = generate_room_sizes(
target_room_count,
cols,
rows,
min_size,
max_size,
square_rooms_only,
&mut room_rng,
);
let room_count = room_sizes.len();
let centers = random_centers(room_count, cols, rows, &mut graph_rng);
let randomness = (corridor_randomness_percent.min(100) as f32) / 100.0;
let target_dead_end_rooms = ((room_count * dead_end_room_percent.min(50)) + 50) / 100;
let room_edges =
build_room_connection_edges(&centers, randomness, target_dead_end_rooms, &mut graph_rng);
if pack_rooms_without_corridors {
rooms = place_packed_rooms(&room_sizes, &room_edges, cols, rows, &mut room_rng);
let mut layout = DungeonLayout::from_generated_parts(rooms, corridors, true);
apply_doors(&mut layout, seed, door_settings, cols, rows);
apply_windows(&mut layout, seed, window_settings, cols, rows);
return layout;
}
let max_attempts = target_room_count.saturating_mul(40).max(50);
for _ in 0..max_attempts {
if rooms.len() >= target_room_count {
break;
}
let (width, height) = if square_rooms_only {
let side = room_rng.range_inclusive(min_size, max_size.min(cols.min(rows)));
(side, side)
} else {
let width_max = max_size.min(cols);
let height_max = max_size.min(rows);
if min_size > width_max || min_size > height_max {
continue;
}
(
room_rng.range_inclusive(min_size, width_max),
room_rng.range_inclusive(min_size, height_max),
)
};
if width > cols || height > rows {
continue;
}
let x = room_rng.range_inclusive(0, cols - width);
let y = room_rng.range_inclusive(0, rows - height);
let candidate = Room {
x,
y,
width,
height,
};
if rooms
.iter()
.all(|existing| !crate::layout::utils::overlaps_with_padding(&candidate, existing, 1))
{
rooms.push(candidate);
}
}
if rooms.len() < 2 {
return DungeonLayout::from_generated_parts(rooms, corridors, false);
}
let mut min_width = min_corridor_width.max(1);
let max_grid_width = cols.max(1).min(rows.max(1));
let max_width = max_corridor_width.max(min_width).min(max_grid_width);
min_width = min_width.min(max_width);
let centers: Vec<(usize, usize)> = rooms.iter().map(Room::center_cell).collect();
let target_dead_end_rooms = ((rooms.len() * dead_end_room_percent.min(50)) + 50) / 100;
let room_edges =
build_room_connection_edges(&centers, randomness, target_dead_end_rooms, &mut graph_rng);
let mut corridors = Vec::new();
let mut next_corridor_id = 1u64;
for (start_room_id, end_room_id) in room_edges {
let start = centers[start_room_id];
let end = centers[end_room_id];
let blocked = blocked_room_cells(&rooms, &[start_room_id, end_room_id]);
let path = if randomness <= 0.001 {
shortest_path_cells(start, end, cols, rows, &blocked)
} else {
Some(noisy_path(
start,
end,
cols,
rows,
randomness,
&blocked,
&mut path_rng,
))
}
.or_else(|| shortest_path_cells(start, end, cols, rows, &blocked));
if let Some(path) = path
&& path.len() >= 2
{
corridors.push(Corridor {
id: next_corridor_id,
start_room_id,
end_room_id,
path,
width: corridor_rng.range_inclusive(min_width, max_width),
});
next_corridor_id = next_corridor_id.wrapping_add(1);
}
}
let mut layout = DungeonLayout::from_generated_parts(rooms, corridors, false);
apply_doors(&mut layout, seed, door_settings, cols, rows);
apply_windows(&mut layout, seed, window_settings, cols, rows);
layout
}
+520
View File
@@ -0,0 +1,520 @@
/*
* Door and window placement logic.
* Handles the automatic generation and arrangement of doors and windows
* between rooms and corridors.
*/
use super::super::types::{
Door, DoorSettings, DungeonLayout, Room, Window, WindowSettings, WindowSide,
};
use super::super::utils::{
SimpleRng, corridor_cells, normalized_cell_edge, room_index_at_cell, shared_opening_width,
};
use super::connections::{room_collision_edges, room_exit_edge, shared_room_boundaries};
use crate::seed;
use std::collections::HashSet;
pub fn apply_doors(
layout: &mut DungeonLayout,
seed: u64,
settings: DoorSettings,
cols: usize,
rows: usize,
) {
layout.doors.clear();
if layout.packed_rooms {
apply_packed_room_doors(layout, seed, settings);
return;
}
let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64));
let base = (settings.frequency_percent.min(100) as f32) / 100.0;
let room_hall = (settings.room_hallway_percent.min(100) as f32) / 100.0;
let locked = (settings.locked_percent.min(100) as f32) / 100.0;
let corridor_cells = corridor_cells(layout, cols, rows);
let mut seen_edges = HashSet::new();
let door_chance = base * room_hall;
for corridor in &layout.corridors {
if corridor.path.len() < 2 {
continue;
}
let start_room = &layout.rooms[corridor.start_room_id];
let end_room = &layout.rooms[corridor.end_room_id];
if let Some(edge) = room_exit_edge(&corridor.path, start_room, true) {
if seen_edges.insert(edge) {
let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance;
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width: corridor.width.max(1),
span_width: true,
locked: place_door && rng.next_f32() <= locked,
archway: !place_door,
secret: false,
manual: false,
});
}
}
if let Some(edge) = room_exit_edge(&corridor.path, end_room, false) {
if seen_edges.insert(edge) {
let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance;
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width: corridor.width.max(1),
span_width: true,
locked: place_door && rng.next_f32() <= locked,
archway: !place_door,
secret: false,
manual: false,
});
}
}
for (room_idx, room) in layout.rooms.iter().enumerate() {
if room_idx == corridor.start_room_id || room_idx == corridor.end_room_id {
continue;
}
for edge in room_collision_edges(&corridor.path, room) {
if seen_edges.insert(edge) {
let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance;
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width: corridor.width.max(1),
span_width: true,
locked: place_door && rng.next_f32() <= locked,
archway: !place_door,
secret: false,
manual: false,
});
}
}
}
}
if settings.allow_middle_corridor_doors {
for &(x, y) in &corridor_cells {
let right = (x + 1, y);
let bottom = (x, y + 1);
if x + 1 < cols && corridor_cells.contains(&right) {
let edge = normalized_cell_edge((x, y), right);
if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base {
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width: 1,
span_width: false,
locked: rng.next_f32() <= locked,
archway: false,
secret: false,
manual: false,
});
}
}
if y + 1 < rows && corridor_cells.contains(&bottom) {
let edge = normalized_cell_edge((x, y), bottom);
if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base {
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width: 1,
span_width: false,
locked: rng.next_f32() <= locked,
archway: false,
secret: false,
manual: false,
});
}
}
}
}
apply_secret_doors(layout, seed, settings);
}
pub fn apply_packed_room_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) {
let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64));
let door_chance = ((settings.frequency_percent.min(100) as f32) / 100.0)
* ((settings.room_hallway_percent.min(100) as f32) / 100.0);
let locked = (settings.locked_percent.min(100) as f32) / 100.0;
for (a_idx, b_idx, shared_edges) in shared_room_boundaries(&layout.rooms) {
let edge_idx = if shared_edges.len() <= 1 {
0
} else {
rng.range_inclusive(0, shared_edges.len() - 1)
};
let edge = shared_edges[edge_idx];
let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance;
let width = shared_opening_width(
&layout.rooms[a_idx],
&layout.rooms[b_idx],
shared_edges.len(),
1,
);
layout.doors.push(Door {
from: edge.0,
to: edge.1,
width,
span_width: width > 1,
locked: place_door && rng.next_f32() <= locked,
archway: !place_door,
secret: false,
manual: false,
});
}
apply_secret_doors(layout, seed, settings);
}
pub fn apply_secret_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) {
let secret_percent = settings.secret_percent.min(100);
let chance = (secret_percent as f32) / 100.0;
if chance <= 0.0 || layout.rooms.is_empty() || layout.doors.is_empty() {
return;
}
let mut rng = SimpleRng::new(seed::derive_seed(seed, 0x5EC2_E700_u64));
let room_door_map = room_to_door_indices(layout);
let eligible_secret_doors = (0..layout.doors.len())
.filter(|&door_idx| {
let door = &layout.doors[door_idx];
!door.archway
&& !door.locked
&& door_is_secret_eligible_for_rooms(layout, &room_door_map, door_idx)
})
.collect::<HashSet<_>>();
for (door_idx, door) in layout.doors.iter_mut().enumerate() {
if !eligible_secret_doors.contains(&door_idx) {
continue;
}
if rng.next_f32() <= chance {
door.locked = false;
door.archway = false;
door.secret = true;
}
}
}
pub fn door_is_secret_eligible_for_rooms(
layout: &DungeonLayout,
room_door_map: &[Vec<usize>],
door_idx: usize,
) -> bool {
for door_indices in room_door_map {
if !door_indices.contains(&door_idx) {
continue;
}
for &other_door_idx in door_indices {
if other_door_idx == door_idx {
continue;
}
if !door_directly_connects_rooms(layout, other_door_idx) {
return false;
}
}
}
true
}
pub fn room_to_door_indices(layout: &DungeonLayout) -> Vec<Vec<usize>> {
let mut room_door_map = vec![Vec::new(); layout.rooms.len()];
for (door_idx, door) in layout.doors.iter().enumerate() {
for (room_idx, room) in layout.rooms.iter().enumerate() {
if door_touches_room(door, room) {
room_door_map[room_idx].push(door_idx);
}
}
}
room_door_map
}
pub fn secret_room_ids(layout: &DungeonLayout) -> HashSet<usize> {
let mut secret_rooms = HashSet::new();
for door in layout.doors.iter().filter(|door| door.secret) {
for (room_idx, room) in layout.rooms.iter().enumerate() {
if door_touches_room(door, room) {
secret_rooms.insert(room_idx);
}
}
}
secret_rooms
}
pub fn door_touches_room(door: &Door, room: &Room) -> bool {
cell_in_room(door.from, room) || cell_in_room(door.to, room)
}
pub fn door_directly_connects_rooms(layout: &DungeonLayout, door_idx: usize) -> bool {
let Some(door) = layout.doors.get(door_idx) else {
return false;
};
let touching_rooms = layout
.rooms
.iter()
.enumerate()
.filter_map(|(room_idx, room)| door_touches_room(door, room).then_some(room_idx))
.collect::<Vec<_>>();
touching_rooms.len() >= 2
}
pub fn cell_in_room(cell: (usize, usize), room: &Room) -> bool {
cell.0 >= room.x
&& cell.0 < room.x + room.width
&& cell.1 >= room.y
&& cell.1 < room.y + room.height
}
pub fn apply_windows(
layout: &mut DungeonLayout,
seed: u64,
settings: WindowSettings,
cols: usize,
rows: usize,
) {
layout.windows.clear();
if !settings.enabled || cols == 0 || rows == 0 {
return;
}
let mut min_width = settings.min_width.clamp(1, 5);
let max_width = settings.max_width.clamp(min_width, 5);
min_width = min_width.min(max_width);
let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xA117_0055_u64));
let base = (settings.frequency_percent.min(100) as f32) / 100.0;
let internal_ratio = (settings.room_hallway_percent.min(100) as f32) / 100.0;
for segment in collect_window_segments(layout, cols, rows, settings.allow_internal_windows) {
let chance = if segment.internal {
if !settings.allow_internal_windows {
continue;
}
base * internal_ratio
} else {
base * (1.0 - internal_ratio)
};
if chance <= 0.0 || rng.next_f32() > chance {
continue;
}
let max_segment_width = max_width.min(segment.cells.len()).max(1);
let width = rng.range_inclusive(min_width.min(max_segment_width), max_segment_width);
let cell = select_segment_cell(&segment.cells, width, &mut rng);
layout.windows.push(Window {
cell,
side: segment.side,
width,
span_width: width > 1,
});
}
}
#[derive(Debug, Clone)]
pub struct WindowSegment {
pub cells: Vec<(usize, usize)>,
pub side: WindowSide,
pub internal: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowTarget {
Exterior,
Corridor,
Room(usize),
}
pub fn collect_window_segments(
layout: &DungeonLayout,
cols: usize,
rows: usize,
allow_internal_windows: bool,
) -> Vec<WindowSegment> {
let corridor_cells = corridor_cells(layout, cols, rows);
let secret_rooms = secret_room_ids(layout);
let mut segments = Vec::new();
for (room_idx, room) in layout.rooms.iter().enumerate() {
if secret_rooms.contains(&room_idx) {
continue;
}
collect_room_side_segments(
room_idx,
room,
layout,
&corridor_cells,
cols,
rows,
allow_internal_windows,
WindowSide::Left,
&mut segments,
);
collect_room_side_segments(
room_idx,
room,
layout,
&corridor_cells,
cols,
rows,
allow_internal_windows,
WindowSide::Right,
&mut segments,
);
collect_room_side_segments(
room_idx,
room,
layout,
&corridor_cells,
cols,
rows,
allow_internal_windows,
WindowSide::Top,
&mut segments,
);
collect_room_side_segments(
room_idx,
room,
layout,
&corridor_cells,
cols,
rows,
allow_internal_windows,
WindowSide::Bottom,
&mut segments,
);
}
segments
}
pub fn collect_room_side_segments(
room_idx: usize,
room: &Room,
layout: &DungeonLayout,
corridor_cells: &HashSet<(usize, usize)>,
cols: usize,
rows: usize,
allow_internal_windows: bool,
side: WindowSide,
segments: &mut Vec<WindowSegment>,
) {
let cells: Vec<(usize, usize)> = match side {
WindowSide::Left => (room.y..(room.y + room.height))
.map(|y| (room.x, y))
.collect(),
WindowSide::Right => (room.y..(room.y + room.height))
.map(|y| (room.x + room.width - 1, y))
.collect(),
WindowSide::Top => (room.x..(room.x + room.width))
.map(|x| (x, room.y))
.collect(),
WindowSide::Bottom => (room.x..(room.x + room.width))
.map(|x| (x, room.y + room.height - 1))
.collect(),
};
let mut current_target = None;
let mut current_cells = Vec::new();
for cell in cells {
let Some(neighbor) = outward_neighbor(cell, side, cols, rows) else {
push_window_segment(current_target, side, &mut current_cells, segments);
current_target = Some(WindowTarget::Exterior);
current_cells.push(cell);
continue;
};
let target = if let Some(other_room_idx) = room_index_at_cell(&layout.rooms, neighbor) {
if other_room_idx == room_idx || !allow_internal_windows || other_room_idx < room_idx {
None
} else {
Some(WindowTarget::Room(other_room_idx))
}
} else if corridor_cells.contains(&neighbor) {
allow_internal_windows.then_some(WindowTarget::Corridor)
} else {
Some(WindowTarget::Exterior)
};
if current_target != target {
push_window_segment(current_target, side, &mut current_cells, segments);
current_target = target;
}
if target.is_some() {
current_cells.push(cell);
}
}
push_window_segment(current_target, side, &mut current_cells, segments);
}
pub fn push_window_segment(
target: Option<WindowTarget>,
side: WindowSide,
cells: &mut Vec<(usize, usize)>,
segments: &mut Vec<WindowSegment>,
) {
let Some(target) = target else {
cells.clear();
return;
};
if cells.is_empty() {
return;
}
segments.push(WindowSegment {
cells: std::mem::take(cells),
side,
internal: !matches!(target, WindowTarget::Exterior),
});
}
pub fn outward_neighbor(
cell: (usize, usize),
side: WindowSide,
cols: usize,
rows: usize,
) -> Option<(usize, usize)> {
match side {
WindowSide::Left => cell.0.checked_sub(1).map(|x| (x, cell.1)),
WindowSide::Right => (cell.0 + 1 < cols).then_some((cell.0 + 1, cell.1)),
WindowSide::Top => cell.1.checked_sub(1).map(|y| (cell.0, y)),
WindowSide::Bottom => (cell.1 + 1 < rows).then_some((cell.0, cell.1 + 1)),
}
}
pub fn select_segment_cell(
cells: &[(usize, usize)],
width: usize,
rng: &mut SimpleRng,
) -> (usize, usize) {
let width = width.max(1);
let min_offset = -((width as isize - 1) / 2);
let max_offset = width as isize / 2;
let low = (-min_offset) as usize;
let high = cells.len().saturating_sub(1 + max_offset.max(0) as usize);
let idx = if low <= high {
rng.range_inclusive(low, high)
} else {
cells.len() / 2
};
cells[idx]
}
+272
View File
@@ -0,0 +1,272 @@
/*
* Room placement and sizing algorithms.
* Handles the generation of room dimensions and their spatial arrangement,
* including packed room placement.
*/
use super::super::types::Room;
use super::super::utils::{SimpleRng, rooms_overlap, rooms_touch, shuffle_indices};
use std::collections::VecDeque;
pub fn generate_room_sizes(
target_room_count: usize,
cols: usize,
rows: usize,
min_size: usize,
max_size: usize,
square_rooms_only: bool,
rng: &mut SimpleRng,
) -> Vec<(usize, usize)> {
let mut sizes = Vec::new();
let max_attempts = target_room_count.saturating_mul(40).max(50);
for _ in 0..max_attempts {
if sizes.len() >= target_room_count {
break;
}
let (width, height) = if square_rooms_only {
let side = rng.range_inclusive(min_size, max_size.min(cols.min(rows)));
(side, side)
} else {
let width_max = max_size.min(cols);
let height_max = max_size.min(rows);
if min_size > width_max || min_size > height_max {
continue;
}
(
rng.range_inclusive(min_size, width_max),
rng.range_inclusive(min_size, height_max),
)
};
if width <= cols && height <= rows {
sizes.push((width, height));
}
}
sizes
}
pub fn random_centers(
count: usize,
cols: usize,
rows: usize,
rng: &mut SimpleRng,
) -> Vec<(usize, usize)> {
let mut centers = Vec::with_capacity(count);
for _ in 0..count {
centers.push((
rng.range_inclusive(0, cols.saturating_sub(1)),
rng.range_inclusive(0, rows.saturating_sub(1)),
));
}
centers
}
pub fn place_packed_rooms(
room_sizes: &[(usize, usize)],
room_edges: &[(usize, usize)],
cols: usize,
rows: usize,
rng: &mut SimpleRng,
) -> Vec<Room> {
if room_sizes.is_empty() || cols == 0 || rows == 0 {
return Vec::new();
}
let mut placed: Vec<Option<Room>> = vec![None; room_sizes.len()];
let (first_w, first_h) = room_sizes[0];
if first_w > cols || first_h > rows {
return Vec::new();
}
placed[0] = Some(Room {
x: (cols.saturating_sub(first_w)) / 2,
y: (rows.saturating_sub(first_h)) / 2,
width: first_w,
height: first_h,
});
let mut order = placement_order(room_sizes.len(), room_edges);
if !order.contains(&0) {
order.insert(0, 0);
}
for room_idx in order.into_iter().skip(1) {
let Some(room) =
try_place_packed_room(room_idx, room_sizes, room_edges, &placed, cols, rows, rng)
else {
continue;
};
placed[room_idx] = Some(room);
}
for room_idx in 0..room_sizes.len() {
if placed[room_idx].is_some() {
continue;
}
if let Some(room) =
try_place_packed_room(room_idx, room_sizes, room_edges, &placed, cols, rows, rng)
{
placed[room_idx] = Some(room);
}
}
placed.into_iter().flatten().collect()
}
pub fn placement_order(room_count: usize, room_edges: &[(usize, usize)]) -> Vec<usize> {
if room_count == 0 {
return Vec::new();
}
let mut adjacency = vec![Vec::new(); room_count];
for &(a, b) in room_edges {
adjacency[a].push(b);
adjacency[b].push(a);
}
let mut visited = vec![false; room_count];
let mut queue = VecDeque::new();
let mut order = Vec::with_capacity(room_count);
queue.push_back(0);
visited[0] = true;
while let Some(idx) = queue.pop_front() {
order.push(idx);
for &next in &adjacency[idx] {
if !visited[next] {
visited[next] = true;
queue.push_back(next);
}
}
}
for idx in 0..room_count {
if !visited[idx] {
order.push(idx);
}
}
order
}
pub fn try_place_packed_room(
room_idx: usize,
room_sizes: &[(usize, usize)],
room_edges: &[(usize, usize)],
placed: &[Option<Room>],
cols: usize,
rows: usize,
rng: &mut SimpleRng,
) -> Option<Room> {
let (width, height) = *room_sizes.get(room_idx)?;
if width > cols || height > rows {
return None;
}
let mut anchors: Vec<usize> = room_edges
.iter()
.filter_map(|&(a, b)| {
if a == room_idx && placed.get(b)?.is_some() {
Some(b)
} else if b == room_idx && placed.get(a)?.is_some() {
Some(a)
} else {
None
}
})
.collect();
if anchors.is_empty() {
anchors = placed
.iter()
.enumerate()
.filter_map(|(idx, room)| room.as_ref().map(|_| idx))
.collect();
}
shuffle_indices(&mut anchors, rng);
for anchor_idx in anchors {
let Some(anchor) = placed.get(anchor_idx).and_then(|room| room.as_ref()) else {
continue;
};
let mut candidates = packed_room_candidates(anchor, width, height, cols, rows);
shuffle_rooms(&mut candidates, rng);
for candidate in candidates {
if placed
.iter()
.flatten()
.all(|existing| !rooms_overlap(&candidate, existing))
&& placed
.iter()
.flatten()
.any(|existing| rooms_touch(&candidate, existing))
{
return Some(candidate);
}
}
}
None
}
pub fn packed_room_candidates(
anchor: &Room,
width: usize,
height: usize,
cols: usize,
rows: usize,
) -> Vec<Room> {
let mut candidates = Vec::new();
let min_y = anchor.y.saturating_sub(height.saturating_sub(1));
let max_y = (anchor.y + anchor.height).saturating_sub(1);
for y in min_y..=max_y {
candidates.push(Room {
x: anchor.x.saturating_sub(width),
y,
width,
height,
});
candidates.push(Room {
x: anchor.x + anchor.width,
y,
width,
height,
});
}
let min_x = anchor.x.saturating_sub(width.saturating_sub(1));
let max_x = (anchor.x + anchor.width).saturating_sub(1);
for x in min_x..=max_x {
candidates.push(Room {
x,
y: anchor.y.saturating_sub(height),
width,
height,
});
candidates.push(Room {
x,
y: anchor.y + anchor.height,
width,
height,
});
}
candidates.retain(|room| room.x + room.width <= cols && room.y + room.height <= rows);
candidates
}
pub fn shuffle_rooms(rooms: &mut [Room], rng: &mut SimpleRng) {
if rooms.len() <= 1 {
return;
}
for i in (1..rooms.len()).rev() {
let j = rng.range_inclusive(0, i);
rooms.swap(i, j);
}
}
+1 -1
View File
@@ -1,10 +1,10 @@
pub mod generation;
/* /*
* Public entry point for the layout module. * Public entry point for the layout module.
* Exports types, utilities, and generation algorithms used to build * Exports types, utilities, and generation algorithms used to build
* the dungeon structure. * the dungeon structure.
*/ */
pub mod generation;
pub mod types; pub mod types;
pub mod utils; pub mod utils;
+5 -5
View File
@@ -1,14 +1,14 @@
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
/* /*
* Persistence logic for dungeon state. * Persistence logic for dungeon state.
* Handles saving and loading the complete dungeon state * Handles saving and loading the complete dungeon state
* (settings and layout data) to and from disk using JSON. * (settings and layout data) to and from disk using JSON.
*/ */
use rfd::FileDialog;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use crate::layout::DungeonLayout; use crate::layout::DungeonLayout;
use crate::ui::UiSettings; use crate::ui::UiSettings;
+2 -1
View File
@@ -1,10 +1,11 @@
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
/* /*
* Individual tab rendering logic for the side panel. * Individual tab rendering logic for the side panel.
* Contains functions for drawing the "Generate", "Layout", "Add", * Contains functions for drawing the "Generate", "Layout", "Add",
* "Start & End", and "Monsters & Traps" control panels. * "Start & End", and "Monsters & Traps" control panels.
*/ */
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
use crate::seed; use crate::seed;
use eframe::egui; use eframe::egui;
use egui::RichText; use egui::RichText;