broke everything from mod.rs files into multiple smaller files
This commit is contained in:
+4
-485
@@ -1,495 +1,14 @@
|
|||||||
/*
|
/*
|
||||||
* Main export module.
|
* Export module facade.
|
||||||
* Orchestrates the export process, coordinating dialog selection,
|
* Re-exports export targets, events, and image/SVG export entry points.
|
||||||
* background thread execution, and multi-format (SVG/Raster) generation.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use image::ImageFormat;
|
pub mod pipeline;
|
||||||
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 raster;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use pipeline::{export_to_target, select_export_target};
|
||||||
pub use svg::build_svg;
|
pub use svg::build_svg;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
type ProgressSender = mpsc::Sender<ExportEvent>;
|
|
||||||
|
|
||||||
// Selects export target.
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports to target.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports image count.
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Counts images per level.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an export progress update to the UI channel.
|
|
||||||
fn report_progress(progress_tx: &ProgressSender, completed: usize, total: usize) {
|
|
||||||
let _ = progress_tx.send(ExportEvent::Progress(ExportProgress { completed, total }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports level images to folder.
|
|
||||||
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(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saves layout to file.
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports composite masks for levels.
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saves composite image.
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders mask pixmap.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates an empty pixmap for a named export mask.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fills marker mask.
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saves mask image.
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selects image format.
|
|
||||||
fn mask_image_format(format: MaskFormat) -> ImageFormat {
|
|
||||||
match format {
|
|
||||||
MaskFormat::Png => ImageFormat::Png,
|
|
||||||
MaskFormat::Jpeg => ImageFormat::Jpeg,
|
|
||||||
MaskFormat::Webp => ImageFormat::WebP,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,491 @@
|
|||||||
|
/*
|
||||||
|
* 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};
|
||||||
|
|
||||||
|
use super::svg::build_svg;
|
||||||
|
use super::types::*;
|
||||||
|
use super::{raster, utils};
|
||||||
|
|
||||||
|
type ProgressSender = mpsc::Sender<ExportEvent>;
|
||||||
|
|
||||||
|
// Selects export target.
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports to target.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports image count.
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Counts images per level.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an export progress update to the UI channel.
|
||||||
|
fn report_progress(progress_tx: &ProgressSender, completed: usize, total: usize) {
|
||||||
|
let _ = progress_tx.send(ExportEvent::Progress(ExportProgress { completed, total }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports level images to folder.
|
||||||
|
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(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves layout to file.
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports composite masks for levels.
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves composite image.
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders mask pixmap.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an empty pixmap for a named export mask.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills marker mask.
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves mask image.
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selects image format.
|
||||||
|
fn mask_image_format(format: MaskFormat) -> ImageFormat {
|
||||||
|
match format {
|
||||||
|
MaskFormat::Png => ImageFormat::Png,
|
||||||
|
MaskFormat::Jpeg => ImageFormat::Jpeg,
|
||||||
|
MaskFormat::Webp => ImageFormat::WebP,
|
||||||
|
}
|
||||||
|
}
|
||||||
+218
-2
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AddCorridorDrag, GridGeometry, HoverMarker, MarkerKind, centered_marker_origin,
|
AddCorridorDrag, GridGeometry, HoverMarker, MarkerKind, cell_center, centered_marker_origin,
|
||||||
corridor_drag_at_pointer, door_edge_at_pointer, door_edges_for, marker_at_pointer,
|
corridor_drag_at_pointer, door_edge_at_pointer, door_edges_for, marker_at_pointer, marker_rect,
|
||||||
normalized_edge, pointer_to_grid, room_at_pointer, stair_at_pointer, text_at_pointer,
|
normalized_edge, pointer_to_grid, room_at_pointer, stair_at_pointer, text_at_pointer,
|
||||||
};
|
};
|
||||||
use crate::app::{DungeonApp, ManualDoorKind};
|
use crate::app::{DungeonApp, ManualDoorKind};
|
||||||
@@ -16,6 +16,7 @@ use crate::layout::{
|
|||||||
};
|
};
|
||||||
use crate::ui::AddTool;
|
use crate::ui::AddTool;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
|
||||||
// Handles add tool.
|
// Handles add tool.
|
||||||
pub fn handle_add_tool(app: &mut DungeonApp, response: &egui::Response, geometry: &GridGeometry) {
|
pub fn handle_add_tool(app: &mut DungeonApp, response: &egui::Response, geometry: &GridGeometry) {
|
||||||
@@ -549,3 +550,218 @@ pub fn manual_door_edge_allowed(
|
|||||||
let b_is_feature = b_room.is_some() || b_in_corridor;
|
let b_is_feature = b_room.is_some() || b_in_corridor;
|
||||||
a_is_feature || b_is_feature
|
a_is_feature || b_is_feature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws add overlay.
|
||||||
|
pub fn draw_add_overlay(app: &DungeonApp, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||||
|
if app.settings.add_tool != AddTool::Corridor {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(drag) = &app.add_corridor_drag else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = cell_center(geometry, drag.start_cell.0, drag.start_cell.1);
|
||||||
|
let end = cell_center(geometry, drag.current_cell.0, drag.current_cell.1);
|
||||||
|
let stroke = Stroke::new(2.0, Color32::WHITE);
|
||||||
|
painter.circle_filled(start, 3.0, Color32::WHITE);
|
||||||
|
crate::rendering::draw_dashed_line(painter, start, end, stroke, 8.0, 6.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws add tool ghost.
|
||||||
|
pub fn draw_add_tool_ghost(app: &DungeonApp, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||||
|
let tool = app.settings.add_tool;
|
||||||
|
if tool == AddTool::None || tool == AddTool::Corridor {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pointer_pos) = painter.ctx().pointer_hover_pos() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cell = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||||
|
|
||||||
|
let ghost_alpha = 0.35;
|
||||||
|
|
||||||
|
match tool {
|
||||||
|
AddTool::Room => {
|
||||||
|
let width = app.settings.add_room_width.max(1);
|
||||||
|
let height = app.settings.add_room_height.max(1);
|
||||||
|
let max_x = app.settings.cols.saturating_sub(cell.0);
|
||||||
|
let max_y = app.settings.rows.saturating_sub(cell.1);
|
||||||
|
let w = width.min(max_x).max(1);
|
||||||
|
let h = height.min(max_y).max(1);
|
||||||
|
|
||||||
|
let left = geometry.rect.left() + cell.0 as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + cell.1 as f32 * geometry.cell_size;
|
||||||
|
let right = left + w as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + h as f32 * geometry.cell_size;
|
||||||
|
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||||
|
|
||||||
|
let fill = Color32::from_rgb(70, 120, 160).gamma_multiply(ghost_alpha);
|
||||||
|
painter.rect_filled(rect, 4.0, fill);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
Stroke::new(
|
||||||
|
1.5,
|
||||||
|
Color32::from_rgb(150, 200, 240).gamma_multiply(ghost_alpha + 0.3),
|
||||||
|
),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AddTool::Text => {
|
||||||
|
let text = app.settings.add_text_value.trim();
|
||||||
|
if text.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let font_size = app.settings.add_text_font_size.max(8);
|
||||||
|
let rect = crate::rendering::text_label_rect(
|
||||||
|
geometry,
|
||||||
|
&layout::TextLabel {
|
||||||
|
cell,
|
||||||
|
text: text.to_string(),
|
||||||
|
font_size,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let fill = Color32::from_rgb(50, 50, 70).gamma_multiply(ghost_alpha);
|
||||||
|
painter.rect_filled(rect.expand(4.0), 4.0, fill);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect.expand(4.0),
|
||||||
|
4.0,
|
||||||
|
Stroke::new(
|
||||||
|
1.0,
|
||||||
|
Color32::from_rgb(180, 180, 220).gamma_multiply(ghost_alpha + 0.2),
|
||||||
|
),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
|
||||||
|
let color = Color32::WHITE.gamma_multiply(ghost_alpha + 0.1);
|
||||||
|
painter.text(
|
||||||
|
rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
text,
|
||||||
|
egui::FontId::proportional(font_size as f32),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AddTool::StartMarker
|
||||||
|
| AddTool::EndMarker
|
||||||
|
| AddTool::TrapMarker
|
||||||
|
| AddTool::MonsterMarker => {
|
||||||
|
let size = app.settings.add_marker_size.max(1);
|
||||||
|
let origin = centered_marker_origin(cell, size, app.settings.cols, app.settings.rows);
|
||||||
|
|
||||||
|
let rect = marker_rect(&layout::AreaMarker { cell: origin, size }, geometry);
|
||||||
|
|
||||||
|
let base_color = match tool {
|
||||||
|
AddTool::StartMarker => Color32::from_rgb(60, 220, 200),
|
||||||
|
AddTool::EndMarker => Color32::from_rgb(240, 90, 90),
|
||||||
|
AddTool::TrapMarker => Color32::from_rgb(150, 80, 230),
|
||||||
|
AddTool::MonsterMarker => Color32::from_rgb(200, 50, 50),
|
||||||
|
_ => Color32::WHITE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fill = base_color.gamma_multiply(ghost_alpha);
|
||||||
|
painter.rect_filled(rect, 4.0, fill);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
Stroke::new(
|
||||||
|
1.5,
|
||||||
|
Color32::from_rgb(255, 255, 255).gamma_multiply(ghost_alpha + 0.2),
|
||||||
|
),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
|
||||||
|
let label = match tool {
|
||||||
|
AddTool::StartMarker => "S",
|
||||||
|
AddTool::EndMarker => "E",
|
||||||
|
AddTool::TrapMarker => "T",
|
||||||
|
AddTool::MonsterMarker => "M",
|
||||||
|
_ => "?",
|
||||||
|
};
|
||||||
|
let text_color = Color32::WHITE.gamma_multiply(ghost_alpha + 0.15);
|
||||||
|
let font_size = (geometry.cell_size * size as f32 * 0.5).clamp(14.0, 72.0);
|
||||||
|
painter.text(
|
||||||
|
rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
label,
|
||||||
|
egui::FontId::proportional(font_size),
|
||||||
|
text_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AddTool::Staircase => {
|
||||||
|
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(cell.0);
|
||||||
|
let max_y = app.settings.rows.saturating_sub(cell.1);
|
||||||
|
let w = width.min(max_x).max(1);
|
||||||
|
let h = height.min(max_y).max(1);
|
||||||
|
|
||||||
|
let left = geometry.rect.left() + cell.0 as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + cell.1 as f32 * geometry.cell_size;
|
||||||
|
let right = left + w as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + h as f32 * geometry.cell_size;
|
||||||
|
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||||
|
|
||||||
|
let fill = Color32::from_rgb(200, 150, 50).gamma_multiply(ghost_alpha);
|
||||||
|
painter.rect_filled(rect, 4.0, fill);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
Stroke::new(
|
||||||
|
1.5,
|
||||||
|
Color32::from_rgb(220, 180, 80).gamma_multiply(ghost_alpha + 0.2),
|
||||||
|
),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
|
||||||
|
let steps = (h as f32 * 2.0).round() as usize;
|
||||||
|
let step_height = (rect.height() / (steps as f32)).max(1.0);
|
||||||
|
let line_color = Color32::from_rgb(150, 100, 30).gamma_multiply(ghost_alpha + 0.15);
|
||||||
|
for i in 0..steps {
|
||||||
|
let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(rect.left() + 2.0, y),
|
||||||
|
egui::pos2(rect.right() - 2.0, y),
|
||||||
|
],
|
||||||
|
Stroke::new(1.5, line_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddTool::Archway | AddTool::Door | AddTool::LockedDoor | AddTool::SecretDoor => {
|
||||||
|
if let Some((edge_a, edge_b)) = door_edge_at_pointer(app, pointer_pos, geometry) {
|
||||||
|
let door_width_cells = 1;
|
||||||
|
let style = match tool {
|
||||||
|
AddTool::Archway => crate::rendering::DoorLineStyle::Dotted,
|
||||||
|
AddTool::Door => crate::rendering::DoorLineStyle::LongDash,
|
||||||
|
AddTool::LockedDoor => crate::rendering::DoorLineStyle::ShortDash,
|
||||||
|
AddTool::SecretDoor => crate::rendering::DoorLineStyle::DashDotDot,
|
||||||
|
_ => crate::rendering::DoorLineStyle::Solid,
|
||||||
|
};
|
||||||
|
let color = match tool {
|
||||||
|
AddTool::Archway => Color32::from_rgb(230, 140, 60),
|
||||||
|
AddTool::Door => Color32::from_rgb(80, 200, 120),
|
||||||
|
AddTool::LockedDoor => Color32::from_rgb(220, 70, 70),
|
||||||
|
AddTool::SecretDoor => Color32::from_rgb(170, 80, 170),
|
||||||
|
_ => Color32::WHITE,
|
||||||
|
};
|
||||||
|
let ghost_color = color.gamma_multiply(ghost_alpha);
|
||||||
|
crate::rendering::draw_door_line(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
edge_a,
|
||||||
|
edge_b,
|
||||||
|
door_width_cells,
|
||||||
|
Stroke::new((geometry.cell_size / 5.0).max(1.0), ghost_color),
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddTool::None | AddTool::Corridor => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+57
-3
@@ -5,14 +5,15 @@
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
CorridorDragState, DragState, GridGeometry, MarkerDragState, MarkerKind, RoomDragState,
|
CorridorDragState, DragState, GridGeometry, MarkerDragState, MarkerKind, RoomDragState,
|
||||||
StaircaseDragState, TextDragState, corridor_drag_at_pointer, marker_drag_at_pointer,
|
StaircaseDragState, TextDragState, cell_center, corridor_drag_at_pointer, door_render_width,
|
||||||
pointer_to_grid, reroute_path_through_cell, room_at_pointer, stair_drag_at_pointer,
|
marker_drag_at_pointer, pointer_to_grid, reroute_path_through_cell, room_at_pointer,
|
||||||
text_at_pointer,
|
stair_drag_at_pointer, text_at_pointer,
|
||||||
};
|
};
|
||||||
use crate::app::DungeonApp;
|
use crate::app::DungeonApp;
|
||||||
use crate::layout::{blocked_room_cells, shortest_path_cells};
|
use crate::layout::{blocked_room_cells, shortest_path_cells};
|
||||||
use crate::ui::AddTool;
|
use crate::ui::AddTool;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
|
||||||
// Handles active drag gestures for rooms, corridors, markers, stairs, and text.
|
// Handles active drag gestures for rooms, corridors, markers, stairs, and text.
|
||||||
pub fn handle_drag(
|
pub fn handle_drag(
|
||||||
@@ -327,3 +328,56 @@ pub fn drag_stair_to_pointer(
|
|||||||
d.original_cell = new_cell;
|
d.original_cell = new_cell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws corridor hover overlay.
|
||||||
|
pub fn draw_corridor_hover_overlay(
|
||||||
|
app: &DungeonApp,
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) {
|
||||||
|
let Some(idx) = app.hover_corridor_idx else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
let Some(corridor) = layout.corridors.get(idx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let color = Color32::from_gray(170);
|
||||||
|
let stroke = Stroke::new(2.0, color);
|
||||||
|
for pair in corridor.path.windows(2) {
|
||||||
|
let a = cell_center(geometry, pair[0].0, pair[0].1);
|
||||||
|
let b = cell_center(geometry, pair[1].0, pair[1].1);
|
||||||
|
crate::rendering::draw_dashed_line(painter, a, b, stroke, 6.0, 6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws door hover overlay.
|
||||||
|
pub fn draw_door_hover_overlay(app: &DungeonApp, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||||
|
let Some(idx) = app.hover_door_idx else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
let Some(door) = layout.doors.get(idx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let color = Color32::from_gray(200);
|
||||||
|
let stroke = Stroke::new(2.0, color);
|
||||||
|
crate::rendering::draw_door_line(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
door.from,
|
||||||
|
door.to,
|
||||||
|
door_render_width(door),
|
||||||
|
stroke,
|
||||||
|
crate::rendering::DoorLineStyle::LongDash,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Grid geometry and coordinate helpers.
|
||||||
|
* Owns screen/grid conversion and rectangle helpers used by interaction code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::layout;
|
||||||
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
|
||||||
|
pub struct GridGeometry {
|
||||||
|
pub rect: egui::Rect,
|
||||||
|
pub cell_size: f32,
|
||||||
|
pub cols: usize,
|
||||||
|
pub rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the editable grid and returns its screen geometry.
|
||||||
|
pub fn draw_grid(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
area: egui::Rect,
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
) -> GridGeometry {
|
||||||
|
let cols = cols.max(1);
|
||||||
|
let rows = rows.max(1);
|
||||||
|
|
||||||
|
let cell_size = (area.width() / cols as f32).min(area.height() / rows as f32);
|
||||||
|
let grid_width = cell_size * cols as f32;
|
||||||
|
let grid_height = cell_size * rows as f32;
|
||||||
|
|
||||||
|
let grid_rect = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(
|
||||||
|
area.center().x - (grid_width * 0.5),
|
||||||
|
area.center().y - (grid_height * 0.5),
|
||||||
|
),
|
||||||
|
egui::vec2(grid_width, grid_height),
|
||||||
|
);
|
||||||
|
|
||||||
|
let stroke = Stroke::new(1.0, Color32::from_gray(160));
|
||||||
|
painter.rect_stroke(grid_rect, 0.0, stroke, egui::StrokeKind::Middle);
|
||||||
|
|
||||||
|
for col in 1..cols {
|
||||||
|
let x = grid_rect.left() + (col as f32 * cell_size);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(x, grid_rect.top()),
|
||||||
|
egui::pos2(x, grid_rect.bottom()),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in 1..rows {
|
||||||
|
let y = grid_rect.top() + (row as f32 * cell_size);
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(grid_rect.left(), y),
|
||||||
|
egui::pos2(grid_rect.right(), y),
|
||||||
|
],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridGeometry {
|
||||||
|
rect: grid_rect,
|
||||||
|
cell_size,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts to grid.
|
||||||
|
pub fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> {
|
||||||
|
if !geometry.rect.contains(pointer_pos) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = (pointer_pos.x - geometry.rect.left()) / geometry.cell_size;
|
||||||
|
let y = (pointer_pos.y - geometry.rect.top()) / geometry.cell_size;
|
||||||
|
Some((x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the screen center point for a grid cell.
|
||||||
|
pub fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 {
|
||||||
|
egui::pos2(
|
||||||
|
geometry.rect.left() + (col as f32 + 0.5) * geometry.cell_size,
|
||||||
|
geometry.rect.top() + (row as f32 + 0.5) * geometry.cell_size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the screen rectangle for a grid cell.
|
||||||
|
pub fn cell_rect(geometry: &GridGeometry, col: usize, row: usize) -> egui::Rect {
|
||||||
|
let left = geometry.rect.left() + col as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + row as f32 * geometry.cell_size;
|
||||||
|
egui::Rect::from_min_size(
|
||||||
|
egui::pos2(left, top),
|
||||||
|
egui::vec2(geometry.cell_size, geometry.cell_size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the screen rectangle occupied by an area marker.
|
||||||
|
pub fn marker_rect(marker: &layout::AreaMarker, geometry: &GridGeometry) -> egui::Rect {
|
||||||
|
let left = geometry.rect.left() + marker.cell.0 as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + marker.cell.1 as f32 * geometry.cell_size;
|
||||||
|
egui::Rect::from_min_size(
|
||||||
|
egui::pos2(left, top),
|
||||||
|
egui::vec2(
|
||||||
|
marker.size as f32 * geometry.cell_size,
|
||||||
|
marker.size as f32 * geometry.cell_size,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes contains cell.
|
||||||
|
pub fn marker_contains_cell(marker: Option<&layout::AreaMarker>, cell: (usize, usize)) -> bool {
|
||||||
|
let Some(marker) = marker else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
cell.0 >= marker.cell.0
|
||||||
|
&& cell.0 < marker.cell.0 + marker.size
|
||||||
|
&& cell.1 >= marker.cell.1
|
||||||
|
&& cell.1 < marker.cell.1 + marker.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes marker origin.
|
||||||
|
pub fn centered_marker_origin(
|
||||||
|
cell: (usize, usize),
|
||||||
|
size: usize,
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
) -> (usize, usize) {
|
||||||
|
let size = size.clamp(1, 10).min(cols.max(1)).min(rows.max(1));
|
||||||
|
let half = size / 2;
|
||||||
|
let max_x = cols.saturating_sub(size);
|
||||||
|
let max_y = rows.saturating_sub(size);
|
||||||
|
(
|
||||||
|
cell.0.saturating_sub(half).min(max_x),
|
||||||
|
cell.1.saturating_sub(half).min(max_y),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
/*
|
||||||
|
* Hit-testing and hover target detection for interactive dungeon editing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
CorridorDragState, GridGeometry, HoverMarker, MarkerDragState, MarkerKind, StaircaseDragState,
|
||||||
|
door_edges_for, marker_contains_cell, normalized_edge, pointer_to_grid, resize_hit_cells,
|
||||||
|
};
|
||||||
|
use crate::app::DungeonApp;
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
// Handles update hover targets.
|
||||||
|
pub fn update_hover_targets(app: &mut DungeonApp, ctx: &egui::Context, geometry: &GridGeometry) {
|
||||||
|
if app.drag_state.is_some() || app.resize_state.is_some() || app.add_corridor_drag.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pointer_pos) = ctx.pointer_hover_pos() else {
|
||||||
|
app.clear_hover_targets();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
if let Some(edge) = door_edge_at_pointer(app, pointer_pos, geometry) {
|
||||||
|
let target = normalized_edge(edge.0, edge.1);
|
||||||
|
app.hover_door_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)
|
||||||
|
});
|
||||||
|
if app.hover_door_idx.is_some() {
|
||||||
|
app.hover_room_idx = None;
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_text_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.hover_text_idx = text_at_pointer(app, pointer_pos, geometry);
|
||||||
|
if app.hover_text_idx.is_some() {
|
||||||
|
app.hover_room_idx = None;
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.hover_marker = marker_at_pointer(app, pointer_pos, geometry);
|
||||||
|
if app.hover_marker.is_some() {
|
||||||
|
app.hover_room_idx = None;
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_stair_idx = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.hover_stair_idx = stair_at_pointer(app, pointer_pos, geometry);
|
||||||
|
if app.hover_stair_idx.is_some() {
|
||||||
|
app.hover_room_idx = None;
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((room_idx, _, _)) = room_at_pointer(app, pointer_pos, geometry) {
|
||||||
|
app.hover_room_idx = Some(room_idx);
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
} else {
|
||||||
|
app.hover_room_idx = None;
|
||||||
|
if let Some(corridor_drag) = corridor_drag_at_pointer(app, pointer_pos, geometry) {
|
||||||
|
app.hover_corridor_idx = Some(corridor_drag.corridor_index);
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
} else {
|
||||||
|
app.hover_corridor_idx = None;
|
||||||
|
app.hover_door_idx = None;
|
||||||
|
app.hover_marker = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes edge at pointer.
|
||||||
|
pub fn door_edge_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<((usize, usize), (usize, usize))> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let cell_x = grid_x.floor() as isize;
|
||||||
|
let cell_y = grid_y.floor() as isize;
|
||||||
|
if cell_x < 0
|
||||||
|
|| cell_y < 0
|
||||||
|
|| cell_x >= app.settings.cols as isize
|
||||||
|
|| cell_y >= app.settings.rows as isize
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fx = grid_x - grid_x.floor();
|
||||||
|
let fy = grid_y - grid_y.floor();
|
||||||
|
let left = fx;
|
||||||
|
let right = 1.0 - fx;
|
||||||
|
let top = fy;
|
||||||
|
let bottom = 1.0 - fy;
|
||||||
|
|
||||||
|
let mut candidates = [
|
||||||
|
(left, (cell_x - 1, cell_y), (cell_x, cell_y)),
|
||||||
|
(right, (cell_x + 1, cell_y), (cell_x, cell_y)),
|
||||||
|
(top, (cell_x, cell_y - 1), (cell_x, cell_y)),
|
||||||
|
(bottom, (cell_x, cell_y + 1), (cell_x, cell_y)),
|
||||||
|
];
|
||||||
|
candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
let (dist, a, b) = candidates[0];
|
||||||
|
if dist > 0.2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ax, ay) = a;
|
||||||
|
let (bx, by) = b;
|
||||||
|
if ax < 0
|
||||||
|
|| ay < 0
|
||||||
|
|| bx < 0
|
||||||
|
|| by < 0
|
||||||
|
|| ax >= app.settings.cols as isize
|
||||||
|
|| bx >= app.settings.cols as isize
|
||||||
|
|| ay >= app.settings.rows as isize
|
||||||
|
|| by >= app.settings.rows as isize
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(((ax as usize, ay as usize), (bx as usize, by as usize)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes at pointer.
|
||||||
|
pub fn room_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<(usize, f32, f32)> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
|
||||||
|
let room_x = room.x as f32;
|
||||||
|
let room_y = room.y as f32;
|
||||||
|
let room_right = room_x + room.width as f32;
|
||||||
|
let room_bottom = room_y + room.height as f32;
|
||||||
|
|
||||||
|
if grid_x >= room_x && grid_x < room_right && grid_y >= room_y && grid_y < room_bottom {
|
||||||
|
return Some((room_idx, grid_x - room_x, grid_y - room_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes at pointer.
|
||||||
|
pub fn text_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
layout
|
||||||
|
.text_labels
|
||||||
|
.iter()
|
||||||
|
.position(|label| label.cell == clicked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes at pointer.
|
||||||
|
pub fn marker_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<HoverMarker> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
layout
|
||||||
|
.start_markers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
|
||||||
|
.map(|(idx, _)| HoverMarker::Start(idx))
|
||||||
|
.or_else(|| {
|
||||||
|
layout
|
||||||
|
.end_markers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
|
||||||
|
.map(|(idx, _)| HoverMarker::End(idx))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
layout
|
||||||
|
.trap_markers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
|
||||||
|
.map(|(idx, _)| HoverMarker::Trap(idx))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
layout
|
||||||
|
.monster_markers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
|
||||||
|
.map(|(idx, _)| HoverMarker::Monster(idx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes drag at pointer.
|
||||||
|
pub fn marker_drag_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<MarkerDragState> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
match marker_at_pointer(app, pointer_pos, geometry)? {
|
||||||
|
HoverMarker::Start(marker_idx) => {
|
||||||
|
let marker = layout.start_markers.get(marker_idx)?;
|
||||||
|
Some(MarkerDragState {
|
||||||
|
kind: MarkerKind::Start,
|
||||||
|
marker_idx,
|
||||||
|
offset_x: grid_x - marker.cell.0 as f32,
|
||||||
|
offset_y: grid_y - marker.cell.1 as f32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HoverMarker::End(marker_idx) => {
|
||||||
|
let marker = layout.end_markers.get(marker_idx)?;
|
||||||
|
Some(MarkerDragState {
|
||||||
|
kind: MarkerKind::End,
|
||||||
|
marker_idx,
|
||||||
|
offset_x: grid_x - marker.cell.0 as f32,
|
||||||
|
offset_y: grid_y - marker.cell.1 as f32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HoverMarker::Trap(marker_idx) => {
|
||||||
|
let marker = layout.trap_markers.get(marker_idx)?;
|
||||||
|
Some(MarkerDragState {
|
||||||
|
kind: MarkerKind::Trap,
|
||||||
|
marker_idx,
|
||||||
|
offset_x: grid_x - marker.cell.0 as f32,
|
||||||
|
offset_y: grid_y - marker.cell.1 as f32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
HoverMarker::Monster(marker_idx) => {
|
||||||
|
let marker = layout.monster_markers.get(marker_idx)?;
|
||||||
|
Some(MarkerDragState {
|
||||||
|
kind: MarkerKind::Monster,
|
||||||
|
marker_idx,
|
||||||
|
offset_x: grid_x - marker.cell.0 as f32,
|
||||||
|
offset_y: grid_y - marker.cell.1 as f32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles stair at pointer.
|
||||||
|
pub fn stair_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
layout.stairs.iter().position(|stair| {
|
||||||
|
clicked.0 >= stair.cell.0
|
||||||
|
&& clicked.0 < stair.cell.0 + stair.width
|
||||||
|
&& clicked.1 >= stair.cell.1
|
||||||
|
&& clicked.1 < stair.cell.1 + stair.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles stair drag at pointer.
|
||||||
|
pub fn stair_drag_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<StaircaseDragState> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stair_idx = stair_at_pointer(app, pointer_pos, geometry)?;
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
let stair = layout.stairs.get(stair_idx)?;
|
||||||
|
|
||||||
|
Some(StaircaseDragState {
|
||||||
|
stair_idx,
|
||||||
|
offset_x: grid_x - stair.cell.0 as f32,
|
||||||
|
offset_y: grid_y - stair.cell.1 as f32,
|
||||||
|
original_cell: stair.cell,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resizes room at pointer.
|
||||||
|
pub fn resize_room_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if let Some((room_idx, _, _)) = room_at_pointer(app, pointer_pos, geometry) {
|
||||||
|
return Some(room_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let clicked = (
|
||||||
|
grid_x.floor().max(0.0) as usize,
|
||||||
|
grid_y.floor().max(0.0) as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
|
||||||
|
if resize_hit_cells(room, room_idx, &layout.rooms, geometry.cols, geometry.rows)
|
||||||
|
.contains(&clicked)
|
||||||
|
{
|
||||||
|
return Some(room_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes drag at pointer.
|
||||||
|
pub fn corridor_drag_at_pointer(
|
||||||
|
app: &DungeonApp,
|
||||||
|
pointer_pos: egui::Pos2,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
) -> Option<CorridorDragState> {
|
||||||
|
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
|
||||||
|
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
let corridor_index = layout
|
||||||
|
.corridors
|
||||||
|
.iter()
|
||||||
|
.position(|corridor| corridor.path.contains(&clicked))?;
|
||||||
|
|
||||||
|
Some(CorridorDragState {
|
||||||
|
corridor_index,
|
||||||
|
original_path: layout.corridors[corridor_index].path.clone(),
|
||||||
|
original_cell: clicked,
|
||||||
|
})
|
||||||
|
}
|
||||||
+8
-1109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Path rerouting helpers for corridor dragging and room resizing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::layout;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
// Reroutes path through cell.
|
||||||
|
pub fn reroute_path_through_cell(
|
||||||
|
original_path: &[(usize, usize)],
|
||||||
|
original_cell: (usize, usize),
|
||||||
|
target_cell: (usize, usize),
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
blocked_room_cells_set: &HashSet<(usize, usize)>,
|
||||||
|
) -> Option<Vec<(usize, usize)>> {
|
||||||
|
let (&start, &end) = (original_path.first()?, original_path.last()?);
|
||||||
|
if start == end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut blocked = blocked_room_cells_set.clone();
|
||||||
|
blocked.insert(original_cell);
|
||||||
|
blocked.remove(&start);
|
||||||
|
blocked.remove(&end);
|
||||||
|
blocked.remove(&target_cell);
|
||||||
|
|
||||||
|
let first_leg = crate::layout::shortest_path_cells(start, target_cell, cols, rows, &blocked)?;
|
||||||
|
|
||||||
|
let mut blocked_second = blocked.clone();
|
||||||
|
for &cell in first_leg
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.take(first_leg.len().saturating_sub(2))
|
||||||
|
{
|
||||||
|
if cell != target_cell && cell != end {
|
||||||
|
blocked_second.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let second_leg =
|
||||||
|
crate::layout::shortest_path_cells(target_cell, end, cols, rows, &blocked_second).or_else(
|
||||||
|
|| crate::layout::shortest_path_cells(target_cell, end, cols, rows, &blocked),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut full_path = first_leg;
|
||||||
|
for cell in second_leg.into_iter().skip(1) {
|
||||||
|
if full_path.last().copied() != Some(cell) {
|
||||||
|
full_path.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
simplify_path_loops(&mut full_path);
|
||||||
|
|
||||||
|
if full_path.len() < 2 || !full_path.contains(&target_cell) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if original_cell != start && original_cell != end && full_path.contains(&original_cell) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(full_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplifies path loops.
|
||||||
|
pub fn simplify_path_loops(path: &mut Vec<(usize, usize)>) {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for &cell in path.iter() {
|
||||||
|
if let Some(pos) = out.iter().position(|&c| c == cell) {
|
||||||
|
out.truncate(pos + 1);
|
||||||
|
} else {
|
||||||
|
out.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*path = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resizes hit cells.
|
||||||
|
pub fn resize_hit_cells(
|
||||||
|
room: &layout::Room,
|
||||||
|
room_idx: usize,
|
||||||
|
rooms: &[layout::Room],
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
) -> HashSet<(usize, usize)> {
|
||||||
|
let mut cells = HashSet::new();
|
||||||
|
let corners = [
|
||||||
|
(room.x as isize, room.y as isize, -1isize, -1isize),
|
||||||
|
(
|
||||||
|
(room.x + room.width - 1) as isize,
|
||||||
|
room.y as isize,
|
||||||
|
1isize,
|
||||||
|
-1isize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
room.x as isize,
|
||||||
|
(room.y + room.height - 1) as isize,
|
||||||
|
-1isize,
|
||||||
|
1isize,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
(room.x + room.width - 1) as isize,
|
||||||
|
(room.y + room.height - 1) as isize,
|
||||||
|
1isize,
|
||||||
|
1isize,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (cx, cy, ox, oy) in corners {
|
||||||
|
let candidates = [
|
||||||
|
(cx, cy),
|
||||||
|
(cx - ox, cy),
|
||||||
|
(cx, cy - oy),
|
||||||
|
(cx - ox, cy - oy),
|
||||||
|
(cx + ox, cy),
|
||||||
|
(cx, cy + oy),
|
||||||
|
(cx + ox, cy + oy),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (x, y) in candidates {
|
||||||
|
if x < 0 || y < 0 || x >= cols as isize || y >= rows as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = (x as usize, y as usize);
|
||||||
|
let in_other_room = rooms.iter().enumerate().any(|(idx, other_room)| {
|
||||||
|
idx != room_idx
|
||||||
|
&& cell.0 >= other_room.x
|
||||||
|
&& cell.0 < other_room.x + other_room.width
|
||||||
|
&& cell.1 >= other_room.y
|
||||||
|
&& cell.1 < other_room.y + other_room.height
|
||||||
|
});
|
||||||
|
if !in_other_room {
|
||||||
|
cells.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cells
|
||||||
|
}
|
||||||
+194
-1
@@ -9,9 +9,10 @@ use super::{
|
|||||||
stair_at_pointer, text_at_pointer,
|
stair_at_pointer, text_at_pointer,
|
||||||
};
|
};
|
||||||
use crate::app::DungeonApp;
|
use crate::app::DungeonApp;
|
||||||
use crate::layout::{rects_overlap, rooms_overlap};
|
use crate::layout::{self, rects_overlap, rooms_overlap};
|
||||||
use crate::ui::AddTool;
|
use crate::ui::AddTool;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
|
||||||
// Handles secondary-button resize gestures for resizable layout elements.
|
// Handles secondary-button resize gestures for resizable layout elements.
|
||||||
pub fn handle_resize(
|
pub fn handle_resize(
|
||||||
@@ -325,3 +326,195 @@ pub fn resize_staircase(
|
|||||||
s.original_stair.height = new_stair_height;
|
s.original_stair.height = new_stair_height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws resize overlay.
|
||||||
|
pub fn draw_resize_overlay(app: &DungeonApp, painter: &egui::Painter, geometry: &GridGeometry) {
|
||||||
|
if app.settings.active_level_index >= app.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = &app.levels[app.settings.active_level_index];
|
||||||
|
|
||||||
|
if let Some(state) = &app.resize_state {
|
||||||
|
match state {
|
||||||
|
ResizeState::Text(state) => {
|
||||||
|
if let Some(label) = layout.text_labels.get(state.text_idx) {
|
||||||
|
let rect = crate::rendering::text_label_rect(geometry, label).expand(3.0);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(2.0, Color32::from_gray(180)),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResizeState::Room(state) => {
|
||||||
|
if let Some(room) = layout.rooms.get(state.room_idx) {
|
||||||
|
draw_room_resize_visuals(painter, geometry, room, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResizeState::Staircase(state) => {
|
||||||
|
if let Some(stair) = layout.stairs.get(state.stair_idx) {
|
||||||
|
draw_staircase_resize_visuals(painter, geometry, stair, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(text_idx) = app.hover_text_idx {
|
||||||
|
if let Some(label) = layout.text_labels.get(text_idx) {
|
||||||
|
let rect = crate::rendering::text_label_rect(geometry, label).expand(3.0);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(2.0, Color32::from_gray(180)),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(room_idx) = app.hover_room_idx {
|
||||||
|
if let Some(room) = layout.rooms.get(room_idx) {
|
||||||
|
draw_room_resize_visuals(painter, geometry, room, false);
|
||||||
|
}
|
||||||
|
} else if let Some(stair_idx) = app.hover_stair_idx
|
||||||
|
&& let Some(stair) = layout.stairs.get(stair_idx)
|
||||||
|
{
|
||||||
|
draw_staircase_resize_visuals(painter, geometry, stair, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws room resize visuals.
|
||||||
|
pub fn draw_room_resize_visuals(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
room: &layout::Room,
|
||||||
|
show_handles: bool,
|
||||||
|
) {
|
||||||
|
let color = Color32::from_gray(170);
|
||||||
|
let stroke = Stroke::new(1.5, color);
|
||||||
|
|
||||||
|
let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
|
||||||
|
let right = left + room.width as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + room.height as f32 * geometry.cell_size;
|
||||||
|
|
||||||
|
let tl = egui::pos2(left, top);
|
||||||
|
let tr = egui::pos2(right, top);
|
||||||
|
let br = egui::pos2(right, bottom);
|
||||||
|
let bl = egui::pos2(left, bottom);
|
||||||
|
|
||||||
|
crate::rendering::draw_dashed_line(painter, tl, tr, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, tr, br, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, br, bl, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, bl, tl, stroke, 6.0, 6.0);
|
||||||
|
|
||||||
|
let font = egui::FontId::proportional(12.0);
|
||||||
|
let center_top = egui::pos2((left + right) * 0.5, top - 10.0);
|
||||||
|
let center_left = egui::pos2(left - 12.0, (top + bottom) * 0.5);
|
||||||
|
painter.text(
|
||||||
|
center_top,
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
"<->",
|
||||||
|
font.clone(),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
painter.text(center_left, egui::Align2::CENTER_CENTER, "^v", font, color);
|
||||||
|
|
||||||
|
if show_handles {
|
||||||
|
draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
|
||||||
|
draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
|
||||||
|
draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
|
||||||
|
draw_resize_handle(painter, bl, ResizeCorner::BottomLeft, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws staircase resize visuals.
|
||||||
|
pub fn draw_staircase_resize_visuals(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
stair: &layout::Staircase,
|
||||||
|
show_handles: bool,
|
||||||
|
) {
|
||||||
|
let color = Color32::from_gray(170);
|
||||||
|
let stroke = Stroke::new(1.5, color);
|
||||||
|
|
||||||
|
let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size;
|
||||||
|
let right = left + stair.width as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + stair.height as f32 * geometry.cell_size;
|
||||||
|
|
||||||
|
let tl = egui::pos2(left, top);
|
||||||
|
let tr = egui::pos2(right, top);
|
||||||
|
let br = egui::pos2(right, bottom);
|
||||||
|
let bl = egui::pos2(left, bottom);
|
||||||
|
|
||||||
|
crate::rendering::draw_dashed_line(painter, tl, tr, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, tr, br, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, br, bl, stroke, 6.0, 6.0);
|
||||||
|
crate::rendering::draw_dashed_line(painter, bl, tl, stroke, 6.0, 6.0);
|
||||||
|
|
||||||
|
if show_handles {
|
||||||
|
draw_resize_handle(painter, tl, ResizeCorner::TopLeft, color);
|
||||||
|
draw_resize_handle(painter, tr, ResizeCorner::TopRight, color);
|
||||||
|
draw_resize_handle(painter, br, ResizeCorner::BottomRight, color);
|
||||||
|
draw_resize_handle(painter, bl, ResizeCorner::BottomLeft, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws resize handle.
|
||||||
|
pub fn draw_resize_handle(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
corner: egui::Pos2,
|
||||||
|
which: ResizeCorner,
|
||||||
|
color: Color32,
|
||||||
|
) {
|
||||||
|
let size = 10.0;
|
||||||
|
let half = size * 0.5;
|
||||||
|
let rect = egui::Rect::from_center_size(corner, egui::vec2(size, size));
|
||||||
|
painter.rect_filled(rect, 2.0, Color32::from_gray(30));
|
||||||
|
painter.rect_stroke(rect, 2.0, Stroke::new(1.0, color), egui::StrokeKind::Middle);
|
||||||
|
|
||||||
|
let center = corner;
|
||||||
|
let stroke = Stroke::new(1.0, color);
|
||||||
|
match which {
|
||||||
|
ResizeCorner::TopLeft => {
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x - half + 2.0, center.y)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x, center.y - half + 2.0)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ResizeCorner::TopRight => {
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x + half - 2.0, center.y)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x, center.y - half + 2.0)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ResizeCorner::BottomLeft => {
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x - half + 2.0, center.y)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x, center.y + half - 2.0)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ResizeCorner::BottomRight => {
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x + half - 2.0, center.y)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[center, egui::pos2(center.x, center.y + half - 2.0)],
|
||||||
|
stroke,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,197 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* Main generation module.
|
* Procedural generation module facade.
|
||||||
* Orchestrates the procedural generation process, coordinating
|
* Re-exports room placement, passage, marker, and orchestration helpers.
|
||||||
* 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 connections;
|
||||||
pub mod markers;
|
pub mod markers;
|
||||||
|
pub mod orchestration;
|
||||||
pub mod passages;
|
pub mod passages;
|
||||||
pub mod room_placement;
|
pub mod room_placement;
|
||||||
|
|
||||||
pub use connections::*;
|
pub use connections::*;
|
||||||
pub use markers::*;
|
pub use markers::*;
|
||||||
|
pub use orchestration::*;
|
||||||
pub use passages::*;
|
pub use passages::*;
|
||||||
pub use room_placement::*;
|
pub use room_placement::*;
|
||||||
|
|
||||||
// Generates a complete dungeon layout from generation settings.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
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,191 @@
|
|||||||
|
/*
|
||||||
|
* Top-level procedural generation orchestration.
|
||||||
|
* Coordinates room placement, corridor routing, doors, and windows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::super::types::{Corridor, DoorSettings, DungeonLayout, Room, WindowSettings};
|
||||||
|
use super::super::utils::{SimpleRng, blocked_room_cells, noisy_path, shortest_path_cells};
|
||||||
|
use crate::seed;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
apply_doors, apply_windows, build_room_connection_edges, generate_room_sizes,
|
||||||
|
place_packed_rooms, random_centers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generates a complete dungeon layout from generation settings.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
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
|
||||||
|
}
|
||||||
+6
-447
@@ -1,453 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* The primary UI module. Defines the UiSettings structure and
|
* UI module facade.
|
||||||
* manages the side panel layout, legend panel, and level selection tabs.
|
* Re-exports UI settings, panel entry points, tabs, and widgets.
|
||||||
* Serves as the entry point for all UI-related rendering.
|
|
||||||
*/
|
*/
|
||||||
use eframe::egui;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
|
pub mod panels;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
pub mod types;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
pub use panels::*;
|
||||||
pub enum Tab {
|
pub use types::*;
|
||||||
Generate,
|
|
||||||
Layout,
|
|
||||||
StartAndEnd,
|
|
||||||
MonstersAndTraps,
|
|
||||||
Add,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Tab {
|
|
||||||
// Provides default settings for this type.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Generate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum ExportFormat {
|
|
||||||
Png,
|
|
||||||
Jpeg,
|
|
||||||
Webp,
|
|
||||||
Svg,
|
|
||||||
Folder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ExportFormat {
|
|
||||||
// Provides default settings for this type.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Png
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExportFormat {
|
|
||||||
// Returns the file extension for this format.
|
|
||||||
pub fn extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ExportFormat::Png => "png",
|
|
||||||
ExportFormat::Jpeg => "jpeg",
|
|
||||||
ExportFormat::Webp => "webp",
|
|
||||||
ExportFormat::Svg => "svg",
|
|
||||||
ExportFormat::Folder => "folder",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the user-facing label for this option.
|
|
||||||
pub fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ExportFormat::Png => ".png",
|
|
||||||
ExportFormat::Jpeg => ".jpeg",
|
|
||||||
ExportFormat::Webp => ".webp",
|
|
||||||
ExportFormat::Svg => ".svg",
|
|
||||||
ExportFormat::Folder => "Folder (Composite)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reports whether this format supports resolution scaling.
|
|
||||||
pub fn supports_resolution(self) -> bool {
|
|
||||||
!matches!(self, ExportFormat::Svg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum MaskFormat {
|
|
||||||
Png,
|
|
||||||
Jpeg,
|
|
||||||
Webp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MaskFormat {
|
|
||||||
// Provides default settings for this type.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Png
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MaskFormat {
|
|
||||||
// Returns the file extension for this format.
|
|
||||||
pub fn extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
MaskFormat::Png => "png",
|
|
||||||
MaskFormat::Jpeg => "jpeg",
|
|
||||||
MaskFormat::Webp => "webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the user-facing label for this option.
|
|
||||||
pub fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
MaskFormat::Png => ".png",
|
|
||||||
MaskFormat::Jpeg => ".jpeg",
|
|
||||||
MaskFormat::Webp => ".webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct UiSettings {
|
|
||||||
pub seed: u64,
|
|
||||||
pub cols: usize,
|
|
||||||
pub rows: usize,
|
|
||||||
pub room_count: usize,
|
|
||||||
pub min_room_size: usize,
|
|
||||||
pub max_room_size: usize,
|
|
||||||
pub square_rooms_only: bool,
|
|
||||||
pub min_corridor_width: usize,
|
|
||||||
pub max_corridor_width: usize,
|
|
||||||
pub corridor_randomness: usize,
|
|
||||||
pub dead_end_rooms_percent: usize,
|
|
||||||
pub pack_rooms_without_corridors: bool,
|
|
||||||
pub door_frequency_percent: usize,
|
|
||||||
pub room_hallway_door_percent: usize,
|
|
||||||
pub locked_door_percent: usize,
|
|
||||||
pub secret_door_percent: usize,
|
|
||||||
pub allow_middle_corridor_doors: bool,
|
|
||||||
pub windows_enabled: bool,
|
|
||||||
pub min_window_width: usize,
|
|
||||||
pub max_window_width: usize,
|
|
||||||
pub window_frequency_percent: usize,
|
|
||||||
pub room_hallway_window_percent: usize,
|
|
||||||
pub allow_internal_windows: bool,
|
|
||||||
pub colorblind_mode: bool,
|
|
||||||
pub composition_mode: bool,
|
|
||||||
pub export_format: ExportFormat,
|
|
||||||
pub mask_format: MaskFormat,
|
|
||||||
pub export_width: u32,
|
|
||||||
pub export_height: u32,
|
|
||||||
pub allow_export_aspect_change: bool,
|
|
||||||
pub export_show_grid: bool,
|
|
||||||
pub last_export_path: Option<String>,
|
|
||||||
pub add_tool: AddTool,
|
|
||||||
pub add_text_value: String,
|
|
||||||
pub add_text_font_size: u16,
|
|
||||||
pub add_room_width: usize,
|
|
||||||
pub add_room_height: usize,
|
|
||||||
pub add_corridor_width: usize,
|
|
||||||
pub add_stair_width: usize,
|
|
||||||
pub add_stair_height: usize,
|
|
||||||
pub add_marker_size: usize,
|
|
||||||
pub min_start_marker_size: usize,
|
|
||||||
pub max_start_marker_size: usize,
|
|
||||||
pub min_end_marker_size: usize,
|
|
||||||
pub max_end_marker_size: usize,
|
|
||||||
pub min_start_marker_count: usize,
|
|
||||||
pub max_start_marker_count: usize,
|
|
||||||
pub min_end_marker_count: usize,
|
|
||||||
pub max_end_marker_count: usize,
|
|
||||||
pub min_levels: usize,
|
|
||||||
pub max_levels: usize,
|
|
||||||
pub active_level_index: usize,
|
|
||||||
pub export_level_index: usize,
|
|
||||||
pub trap_frequency_percent: usize,
|
|
||||||
pub min_traps_per_area: usize,
|
|
||||||
pub max_traps_per_area: usize,
|
|
||||||
pub monster_frequency_percent: usize,
|
|
||||||
pub min_monsters_per_area: usize,
|
|
||||||
pub max_monsters_per_area: usize,
|
|
||||||
pub min_stair_width: usize,
|
|
||||||
pub max_stair_width: usize,
|
|
||||||
pub min_stair_height: usize,
|
|
||||||
pub max_stair_height: usize,
|
|
||||||
pub min_stairs_per_level: usize,
|
|
||||||
pub max_stairs_per_level: usize,
|
|
||||||
pub sync_stairs_across_levels: bool,
|
|
||||||
pub active_tab: Tab,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UiSettings {
|
|
||||||
// Provides default settings for this type.
|
|
||||||
fn default() -> Self {
|
|
||||||
UiSettings {
|
|
||||||
seed: 0,
|
|
||||||
cols: 32,
|
|
||||||
rows: 32,
|
|
||||||
room_count: 8,
|
|
||||||
min_room_size: 3,
|
|
||||||
max_room_size: 8,
|
|
||||||
square_rooms_only: true,
|
|
||||||
min_corridor_width: 1,
|
|
||||||
max_corridor_width: 3,
|
|
||||||
corridor_randomness: 50,
|
|
||||||
dead_end_rooms_percent: 20,
|
|
||||||
pack_rooms_without_corridors: false,
|
|
||||||
door_frequency_percent: 50,
|
|
||||||
room_hallway_door_percent: 20,
|
|
||||||
locked_door_percent: 10,
|
|
||||||
secret_door_percent: 5,
|
|
||||||
allow_middle_corridor_doors: false,
|
|
||||||
windows_enabled: false,
|
|
||||||
min_window_width: 2,
|
|
||||||
max_window_width: 4,
|
|
||||||
window_frequency_percent: 30,
|
|
||||||
room_hallway_window_percent: 10,
|
|
||||||
allow_internal_windows: false,
|
|
||||||
colorblind_mode: false,
|
|
||||||
composition_mode: false,
|
|
||||||
export_format: ExportFormat::Png,
|
|
||||||
mask_format: MaskFormat::Png,
|
|
||||||
export_width: 0,
|
|
||||||
export_height: 0,
|
|
||||||
allow_export_aspect_change: true,
|
|
||||||
export_show_grid: false,
|
|
||||||
last_export_path: None,
|
|
||||||
add_tool: AddTool::None,
|
|
||||||
add_text_value: String::from("Text"),
|
|
||||||
add_text_font_size: 18,
|
|
||||||
add_room_width: 5,
|
|
||||||
add_room_height: 5,
|
|
||||||
add_corridor_width: 2,
|
|
||||||
add_stair_width: 2,
|
|
||||||
add_stair_height: 2,
|
|
||||||
add_marker_size: 1,
|
|
||||||
min_start_marker_size: 1,
|
|
||||||
max_start_marker_size: 3,
|
|
||||||
min_end_marker_size: 1,
|
|
||||||
max_end_marker_size: 3,
|
|
||||||
min_start_marker_count: 1,
|
|
||||||
max_start_marker_count: 3,
|
|
||||||
min_end_marker_count: 1,
|
|
||||||
max_end_marker_count: 3,
|
|
||||||
min_levels: 1,
|
|
||||||
max_levels: 1,
|
|
||||||
active_level_index: 0,
|
|
||||||
trap_frequency_percent: 30,
|
|
||||||
min_traps_per_area: 1,
|
|
||||||
max_traps_per_area: 3,
|
|
||||||
monster_frequency_percent: 30,
|
|
||||||
min_monsters_per_area: 1,
|
|
||||||
max_monsters_per_area: 3,
|
|
||||||
min_stair_width: 2,
|
|
||||||
max_stair_width: 3,
|
|
||||||
min_stair_height: 2,
|
|
||||||
max_stair_height: 3,
|
|
||||||
min_stairs_per_level: 1,
|
|
||||||
max_stairs_per_level: 2,
|
|
||||||
sync_stairs_across_levels: false,
|
|
||||||
export_level_index: 0,
|
|
||||||
active_tab: Tab::Generate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum AddTool {
|
|
||||||
None,
|
|
||||||
Room,
|
|
||||||
Corridor,
|
|
||||||
Archway,
|
|
||||||
Door,
|
|
||||||
LockedDoor,
|
|
||||||
SecretDoor,
|
|
||||||
Text,
|
|
||||||
StartMarker,
|
|
||||||
EndMarker,
|
|
||||||
TrapMarker,
|
|
||||||
MonsterMarker,
|
|
||||||
Staircase,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
pub struct SidePanelResult {
|
|
||||||
pub settings_changed: bool,
|
|
||||||
pub reset_clicked: bool,
|
|
||||||
pub clear_clicked: bool,
|
|
||||||
pub export_clicked: bool,
|
|
||||||
pub save_clicked: bool,
|
|
||||||
pub load_clicked: bool,
|
|
||||||
pub new_level_clicked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws side panel.
|
|
||||||
pub fn draw_side_panel(
|
|
||||||
ctx: &egui::Context,
|
|
||||||
settings: &mut UiSettings,
|
|
||||||
export_progress: Option<(usize, usize)>,
|
|
||||||
num_levels: usize,
|
|
||||||
) -> SidePanelResult {
|
|
||||||
let mut result = SidePanelResult::default();
|
|
||||||
|
|
||||||
egui::SidePanel::left("options_panel")
|
|
||||||
.resizable(true)
|
|
||||||
.min_width(180.0)
|
|
||||||
.default_width(280.0)
|
|
||||||
.show_separator_line(true)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui
|
|
||||||
.selectable_label(settings.active_tab == Tab::Generate, "Generate")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
settings.active_tab = Tab::Generate;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.selectable_label(settings.active_tab == Tab::Layout, "Layout")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
settings.active_tab = Tab::Layout;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
settings.active_tab = Tab::StartAndEnd;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.selectable_label(
|
|
||||||
settings.active_tab == Tab::MonstersAndTraps,
|
|
||||||
"Monsters/Traps",
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
settings.active_tab = Tab::MonstersAndTraps;
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.selectable_label(settings.active_tab == Tab::Add, "Add")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
settings.active_tab = Tab::Add;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
match settings.active_tab {
|
|
||||||
Tab::Generate => tabs::draw_generate_tab(
|
|
||||||
ui,
|
|
||||||
settings,
|
|
||||||
&mut result,
|
|
||||||
export_progress,
|
|
||||||
num_levels,
|
|
||||||
),
|
|
||||||
Tab::Layout => tabs::draw_layout_tab(ui, settings, &mut result),
|
|
||||||
Tab::StartAndEnd => tabs::draw_start_and_end_tab(ui, settings, &mut result),
|
|
||||||
Tab::MonstersAndTraps => {
|
|
||||||
tabs::draw_monsters_and_traps_tab(ui, settings, &mut result)
|
|
||||||
}
|
|
||||||
Tab::Add => tabs::draw_add_tab(ui, settings, &mut result),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws legend panel.
|
|
||||||
pub fn draw_legend_panel(ctx: &egui::Context, settings: &mut UiSettings) -> bool {
|
|
||||||
let mut composition_mode_changed = false;
|
|
||||||
use egui::Color32;
|
|
||||||
use widgets::LegendStyle;
|
|
||||||
|
|
||||||
egui::SidePanel::right("legend_panel")
|
|
||||||
.resizable(false)
|
|
||||||
.min_width(180.0)
|
|
||||||
.default_width(200.0)
|
|
||||||
.show_separator_line(true)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
ui.heading("Modes");
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.checkbox(&mut settings.colorblind_mode, "Colorblind Mode");
|
|
||||||
composition_mode_changed = ui
|
|
||||||
.checkbox(&mut settings.composition_mode, "Composition Mode")
|
|
||||||
.changed();
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.heading("Legend");
|
|
||||||
ui.add_space(8.0);
|
|
||||||
if settings.colorblind_mode {
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
|
||||||
widgets::draw_legend_entry_colorblind(
|
|
||||||
ui,
|
|
||||||
"Secret Doors",
|
|
||||||
LegendStyle::DashDotDotLine,
|
|
||||||
);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
|
||||||
widgets::draw_legend_entry_colorblind(
|
|
||||||
ui,
|
|
||||||
"Stairs",
|
|
||||||
LegendStyle::LabelChar('\u{1F5CF}'),
|
|
||||||
);
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Start Marker", LegendStyle::Label("S"));
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "End Marker", LegendStyle::Label("E"));
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Trap", LegendStyle::Label("T"));
|
|
||||||
widgets::draw_legend_entry_colorblind(ui, "Monster", LegendStyle::Label("M"));
|
|
||||||
} else {
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::BLACK, "Walls");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 150, 50), "Stairs");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(60, 220, 200), "Start Marker");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(240, 90, 90), "End Marker");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(150, 80, 230), "Trap");
|
|
||||||
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 50, 50), "Monster");
|
|
||||||
}
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.heading("Keybinds");
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.label("• Delete / Backspace: Remove hovered room, corridor, door, text, or marker");
|
|
||||||
ui.label("• Ctrl+Z: Undo");
|
|
||||||
ui.label("• Ctrl+Y: Redo");
|
|
||||||
ui.label("• Left click + drag: Move hovered room");
|
|
||||||
ui.label("• Right click: Cancel add tool");
|
|
||||||
ui.label("• Right click + drag: Resize hovered room");
|
|
||||||
});
|
|
||||||
|
|
||||||
composition_mode_changed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draws level tabs.
|
|
||||||
pub fn draw_level_tabs(
|
|
||||||
ui: &mut egui::Ui,
|
|
||||||
level_count: usize,
|
|
||||||
active_level_index: &mut usize,
|
|
||||||
) -> Option<usize> {
|
|
||||||
let mut hovered_level_idx = None;
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
for level_idx in 0..level_count {
|
|
||||||
let label = format!("Level {}", level_idx + 1);
|
|
||||||
let is_active = *active_level_index == level_idx;
|
|
||||||
let response = ui.selectable_label(is_active, label);
|
|
||||||
if response.hovered() {
|
|
||||||
hovered_level_idx = Some(level_idx);
|
|
||||||
}
|
|
||||||
if response.clicked() {
|
|
||||||
*active_level_index = level_idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hovered_level_idx
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* UI panel composition.
|
||||||
|
* Draws the side panel, legend panel, and level tab strip.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::{SidePanelResult, Tab, UiSettings, tabs, widgets};
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
// Draws side panel.
|
||||||
|
pub fn draw_side_panel(
|
||||||
|
ctx: &egui::Context,
|
||||||
|
settings: &mut UiSettings,
|
||||||
|
export_progress: Option<(usize, usize)>,
|
||||||
|
num_levels: usize,
|
||||||
|
) -> SidePanelResult {
|
||||||
|
let mut result = SidePanelResult::default();
|
||||||
|
|
||||||
|
egui::SidePanel::left("options_panel")
|
||||||
|
.resizable(true)
|
||||||
|
.min_width(180.0)
|
||||||
|
.default_width(280.0)
|
||||||
|
.show_separator_line(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.selectable_label(settings.active_tab == Tab::Generate, "Generate")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
settings.active_tab = Tab::Generate;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(settings.active_tab == Tab::Layout, "Layout")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
settings.active_tab = Tab::Layout;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
settings.active_tab = Tab::StartAndEnd;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(
|
||||||
|
settings.active_tab == Tab::MonstersAndTraps,
|
||||||
|
"Monsters/Traps",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
settings.active_tab = Tab::MonstersAndTraps;
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(settings.active_tab == Tab::Add, "Add")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
settings.active_tab = Tab::Add;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
match settings.active_tab {
|
||||||
|
Tab::Generate => tabs::draw_generate_tab(
|
||||||
|
ui,
|
||||||
|
settings,
|
||||||
|
&mut result,
|
||||||
|
export_progress,
|
||||||
|
num_levels,
|
||||||
|
),
|
||||||
|
Tab::Layout => tabs::draw_layout_tab(ui, settings, &mut result),
|
||||||
|
Tab::StartAndEnd => tabs::draw_start_and_end_tab(ui, settings, &mut result),
|
||||||
|
Tab::MonstersAndTraps => {
|
||||||
|
tabs::draw_monsters_and_traps_tab(ui, settings, &mut result)
|
||||||
|
}
|
||||||
|
Tab::Add => tabs::draw_add_tab(ui, settings, &mut result),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws legend panel.
|
||||||
|
pub fn draw_legend_panel(ctx: &egui::Context, settings: &mut UiSettings) -> bool {
|
||||||
|
let mut composition_mode_changed = false;
|
||||||
|
use egui::Color32;
|
||||||
|
use widgets::LegendStyle;
|
||||||
|
|
||||||
|
egui::SidePanel::right("legend_panel")
|
||||||
|
.resizable(false)
|
||||||
|
.min_width(180.0)
|
||||||
|
.default_width(200.0)
|
||||||
|
.show_separator_line(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.heading("Modes");
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.checkbox(&mut settings.colorblind_mode, "Colorblind Mode");
|
||||||
|
composition_mode_changed = ui
|
||||||
|
.checkbox(&mut settings.composition_mode, "Composition Mode")
|
||||||
|
.changed();
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.heading("Legend");
|
||||||
|
ui.add_space(8.0);
|
||||||
|
if settings.colorblind_mode {
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
||||||
|
widgets::draw_legend_entry_colorblind(
|
||||||
|
ui,
|
||||||
|
"Secret Doors",
|
||||||
|
LegendStyle::DashDotDotLine,
|
||||||
|
);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
||||||
|
widgets::draw_legend_entry_colorblind(
|
||||||
|
ui,
|
||||||
|
"Stairs",
|
||||||
|
LegendStyle::LabelChar('\u{1F5CF}'),
|
||||||
|
);
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Start Marker", LegendStyle::Label("S"));
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "End Marker", LegendStyle::Label("E"));
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Trap", LegendStyle::Label("T"));
|
||||||
|
widgets::draw_legend_entry_colorblind(ui, "Monster", LegendStyle::Label("M"));
|
||||||
|
} else {
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::BLACK, "Walls");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 150, 50), "Stairs");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(60, 220, 200), "Start Marker");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(240, 90, 90), "End Marker");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(150, 80, 230), "Trap");
|
||||||
|
widgets::draw_legend_entry(ui, Color32::from_rgb(200, 50, 50), "Monster");
|
||||||
|
}
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.heading("Keybinds");
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label("• Delete / Backspace: Remove hovered room, corridor, door, text, or marker");
|
||||||
|
ui.label("• Ctrl+Z: Undo");
|
||||||
|
ui.label("• Ctrl+Y: Redo");
|
||||||
|
ui.label("• Left click + drag: Move hovered room");
|
||||||
|
ui.label("• Right click: Cancel add tool");
|
||||||
|
ui.label("• Right click + drag: Resize hovered room");
|
||||||
|
});
|
||||||
|
|
||||||
|
composition_mode_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws level tabs.
|
||||||
|
pub fn draw_level_tabs(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
level_count: usize,
|
||||||
|
active_level_index: &mut usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut hovered_level_idx = None;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
for level_idx in 0..level_count {
|
||||||
|
let label = format!("Level {}", level_idx + 1);
|
||||||
|
let is_active = *active_level_index == level_idx;
|
||||||
|
let response = ui.selectable_label(is_active, label);
|
||||||
|
if response.hovered() {
|
||||||
|
hovered_level_idx = Some(level_idx);
|
||||||
|
}
|
||||||
|
if response.clicked() {
|
||||||
|
*active_level_index = level_idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hovered_level_idx
|
||||||
|
}
|
||||||
+276
@@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
* UI settings and option types.
|
||||||
|
* Defines serializable configuration used by the app and UI controls.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Tab {
|
||||||
|
Generate,
|
||||||
|
Layout,
|
||||||
|
StartAndEnd,
|
||||||
|
MonstersAndTraps,
|
||||||
|
Add,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tab {
|
||||||
|
// Provides default settings for this type.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Generate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum ExportFormat {
|
||||||
|
Png,
|
||||||
|
Jpeg,
|
||||||
|
Webp,
|
||||||
|
Svg,
|
||||||
|
Folder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExportFormat {
|
||||||
|
// Provides default settings for this type.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Png
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExportFormat {
|
||||||
|
// Returns the file extension for this format.
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ExportFormat::Png => "png",
|
||||||
|
ExportFormat::Jpeg => "jpeg",
|
||||||
|
ExportFormat::Webp => "webp",
|
||||||
|
ExportFormat::Svg => "svg",
|
||||||
|
ExportFormat::Folder => "folder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the user-facing label for this option.
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ExportFormat::Png => ".png",
|
||||||
|
ExportFormat::Jpeg => ".jpeg",
|
||||||
|
ExportFormat::Webp => ".webp",
|
||||||
|
ExportFormat::Svg => ".svg",
|
||||||
|
ExportFormat::Folder => "Folder (Composite)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reports whether this format supports resolution scaling.
|
||||||
|
pub fn supports_resolution(self) -> bool {
|
||||||
|
!matches!(self, ExportFormat::Svg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum MaskFormat {
|
||||||
|
Png,
|
||||||
|
Jpeg,
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MaskFormat {
|
||||||
|
// Provides default settings for this type.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Png
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaskFormat {
|
||||||
|
// Returns the file extension for this format.
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
MaskFormat::Png => "png",
|
||||||
|
MaskFormat::Jpeg => "jpeg",
|
||||||
|
MaskFormat::Webp => "webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the user-facing label for this option.
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
MaskFormat::Png => ".png",
|
||||||
|
MaskFormat::Jpeg => ".jpeg",
|
||||||
|
MaskFormat::Webp => ".webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct UiSettings {
|
||||||
|
pub seed: u64,
|
||||||
|
pub cols: usize,
|
||||||
|
pub rows: usize,
|
||||||
|
pub room_count: usize,
|
||||||
|
pub min_room_size: usize,
|
||||||
|
pub max_room_size: usize,
|
||||||
|
pub square_rooms_only: bool,
|
||||||
|
pub min_corridor_width: usize,
|
||||||
|
pub max_corridor_width: usize,
|
||||||
|
pub corridor_randomness: usize,
|
||||||
|
pub dead_end_rooms_percent: usize,
|
||||||
|
pub pack_rooms_without_corridors: bool,
|
||||||
|
pub door_frequency_percent: usize,
|
||||||
|
pub room_hallway_door_percent: usize,
|
||||||
|
pub locked_door_percent: usize,
|
||||||
|
pub secret_door_percent: usize,
|
||||||
|
pub allow_middle_corridor_doors: bool,
|
||||||
|
pub windows_enabled: bool,
|
||||||
|
pub min_window_width: usize,
|
||||||
|
pub max_window_width: usize,
|
||||||
|
pub window_frequency_percent: usize,
|
||||||
|
pub room_hallway_window_percent: usize,
|
||||||
|
pub allow_internal_windows: bool,
|
||||||
|
pub colorblind_mode: bool,
|
||||||
|
pub composition_mode: bool,
|
||||||
|
pub export_format: ExportFormat,
|
||||||
|
pub mask_format: MaskFormat,
|
||||||
|
pub export_width: u32,
|
||||||
|
pub export_height: u32,
|
||||||
|
pub allow_export_aspect_change: bool,
|
||||||
|
pub export_show_grid: bool,
|
||||||
|
pub last_export_path: Option<String>,
|
||||||
|
pub add_tool: AddTool,
|
||||||
|
pub add_text_value: String,
|
||||||
|
pub add_text_font_size: u16,
|
||||||
|
pub add_room_width: usize,
|
||||||
|
pub add_room_height: usize,
|
||||||
|
pub add_corridor_width: usize,
|
||||||
|
pub add_stair_width: usize,
|
||||||
|
pub add_stair_height: usize,
|
||||||
|
pub add_marker_size: usize,
|
||||||
|
pub min_start_marker_size: usize,
|
||||||
|
pub max_start_marker_size: usize,
|
||||||
|
pub min_end_marker_size: usize,
|
||||||
|
pub max_end_marker_size: usize,
|
||||||
|
pub min_start_marker_count: usize,
|
||||||
|
pub max_start_marker_count: usize,
|
||||||
|
pub min_end_marker_count: usize,
|
||||||
|
pub max_end_marker_count: usize,
|
||||||
|
pub min_levels: usize,
|
||||||
|
pub max_levels: usize,
|
||||||
|
pub active_level_index: usize,
|
||||||
|
pub export_level_index: usize,
|
||||||
|
pub trap_frequency_percent: usize,
|
||||||
|
pub min_traps_per_area: usize,
|
||||||
|
pub max_traps_per_area: usize,
|
||||||
|
pub monster_frequency_percent: usize,
|
||||||
|
pub min_monsters_per_area: usize,
|
||||||
|
pub max_monsters_per_area: usize,
|
||||||
|
pub min_stair_width: usize,
|
||||||
|
pub max_stair_width: usize,
|
||||||
|
pub min_stair_height: usize,
|
||||||
|
pub max_stair_height: usize,
|
||||||
|
pub min_stairs_per_level: usize,
|
||||||
|
pub max_stairs_per_level: usize,
|
||||||
|
pub sync_stairs_across_levels: bool,
|
||||||
|
pub active_tab: Tab,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiSettings {
|
||||||
|
// Provides default settings for this type.
|
||||||
|
fn default() -> Self {
|
||||||
|
UiSettings {
|
||||||
|
seed: 0,
|
||||||
|
cols: 32,
|
||||||
|
rows: 32,
|
||||||
|
room_count: 8,
|
||||||
|
min_room_size: 3,
|
||||||
|
max_room_size: 8,
|
||||||
|
square_rooms_only: true,
|
||||||
|
min_corridor_width: 1,
|
||||||
|
max_corridor_width: 3,
|
||||||
|
corridor_randomness: 50,
|
||||||
|
dead_end_rooms_percent: 20,
|
||||||
|
pack_rooms_without_corridors: false,
|
||||||
|
door_frequency_percent: 50,
|
||||||
|
room_hallway_door_percent: 20,
|
||||||
|
locked_door_percent: 10,
|
||||||
|
secret_door_percent: 5,
|
||||||
|
allow_middle_corridor_doors: false,
|
||||||
|
windows_enabled: false,
|
||||||
|
min_window_width: 2,
|
||||||
|
max_window_width: 4,
|
||||||
|
window_frequency_percent: 30,
|
||||||
|
room_hallway_window_percent: 10,
|
||||||
|
allow_internal_windows: false,
|
||||||
|
colorblind_mode: false,
|
||||||
|
composition_mode: false,
|
||||||
|
export_format: ExportFormat::Png,
|
||||||
|
mask_format: MaskFormat::Png,
|
||||||
|
export_width: 0,
|
||||||
|
export_height: 0,
|
||||||
|
allow_export_aspect_change: true,
|
||||||
|
export_show_grid: false,
|
||||||
|
last_export_path: None,
|
||||||
|
add_tool: AddTool::None,
|
||||||
|
add_text_value: String::from("Text"),
|
||||||
|
add_text_font_size: 18,
|
||||||
|
add_room_width: 5,
|
||||||
|
add_room_height: 5,
|
||||||
|
add_corridor_width: 2,
|
||||||
|
add_stair_width: 2,
|
||||||
|
add_stair_height: 2,
|
||||||
|
add_marker_size: 1,
|
||||||
|
min_start_marker_size: 1,
|
||||||
|
max_start_marker_size: 3,
|
||||||
|
min_end_marker_size: 1,
|
||||||
|
max_end_marker_size: 3,
|
||||||
|
min_start_marker_count: 1,
|
||||||
|
max_start_marker_count: 3,
|
||||||
|
min_end_marker_count: 1,
|
||||||
|
max_end_marker_count: 3,
|
||||||
|
min_levels: 1,
|
||||||
|
max_levels: 1,
|
||||||
|
active_level_index: 0,
|
||||||
|
trap_frequency_percent: 30,
|
||||||
|
min_traps_per_area: 1,
|
||||||
|
max_traps_per_area: 3,
|
||||||
|
monster_frequency_percent: 30,
|
||||||
|
min_monsters_per_area: 1,
|
||||||
|
max_monsters_per_area: 3,
|
||||||
|
min_stair_width: 2,
|
||||||
|
max_stair_width: 3,
|
||||||
|
min_stair_height: 2,
|
||||||
|
max_stair_height: 3,
|
||||||
|
min_stairs_per_level: 1,
|
||||||
|
max_stairs_per_level: 2,
|
||||||
|
sync_stairs_across_levels: false,
|
||||||
|
export_level_index: 0,
|
||||||
|
active_tab: Tab::Generate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum AddTool {
|
||||||
|
None,
|
||||||
|
Room,
|
||||||
|
Corridor,
|
||||||
|
Archway,
|
||||||
|
Door,
|
||||||
|
LockedDoor,
|
||||||
|
SecretDoor,
|
||||||
|
Text,
|
||||||
|
StartMarker,
|
||||||
|
EndMarker,
|
||||||
|
TrapMarker,
|
||||||
|
MonsterMarker,
|
||||||
|
Staircase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct SidePanelResult {
|
||||||
|
pub settings_changed: bool,
|
||||||
|
pub reset_clicked: bool,
|
||||||
|
pub clear_clicked: bool,
|
||||||
|
pub export_clicked: bool,
|
||||||
|
pub save_clicked: bool,
|
||||||
|
pub load_clicked: bool,
|
||||||
|
pub new_level_clicked: bool,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user