split up more files
This commit is contained in:
-2078
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(¢ers, 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(¢ers, 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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
@@ -1,10 +1,10 @@
|
||||
pub mod generation;
|
||||
/*
|
||||
* Public entry point for the layout module.
|
||||
* Exports types, utilities, and generation algorithms used to build
|
||||
* the dungeon structure.
|
||||
*/
|
||||
|
||||
pub mod generation;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
use rfd::FileDialog;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/*
|
||||
* Persistence logic for dungeon state.
|
||||
* Handles saving and loading the complete dungeon state
|
||||
* (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::ui::UiSettings;
|
||||
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
|
||||
/*
|
||||
* Individual tab rendering logic for the side panel.
|
||||
* Contains functions for drawing the "Generate", "Layout", "Add",
|
||||
* "Start & End", and "Monsters & Traps" control panels.
|
||||
*/
|
||||
|
||||
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
|
||||
|
||||
use crate::seed;
|
||||
use eframe::egui;
|
||||
use egui::RichText;
|
||||
|
||||
Reference in New Issue
Block a user