major refactor/rework for better code organization
This commit is contained in:
+591
@@ -0,0 +1,591 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use crate::exporter;
|
||||||
|
use crate::interact::{AddCorridorDrag, DragState, HoverMarker, ResizeState, draw_grid};
|
||||||
|
use crate::layout::{
|
||||||
|
self, DoorSettings, DungeonLayout, WindowSettings, populate_random_markers, populate_stairs,
|
||||||
|
};
|
||||||
|
use crate::rendering::draw_layout;
|
||||||
|
use crate::saveandload;
|
||||||
|
use crate::settings;
|
||||||
|
use crate::ui::{AddTool, UiSettings, draw_legend_panel, draw_level_tabs, draw_side_panel};
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ManualDoorKind {
|
||||||
|
Archway,
|
||||||
|
Regular,
|
||||||
|
Locked,
|
||||||
|
Secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AppSnapshot {
|
||||||
|
pub settings: UiSettings,
|
||||||
|
pub levels: Vec<DungeonLayout>,
|
||||||
|
pub suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DungeonApp {
|
||||||
|
pub settings: UiSettings,
|
||||||
|
pub levels: Vec<DungeonLayout>,
|
||||||
|
pub suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
|
||||||
|
pub undo_stack: Vec<AppSnapshot>,
|
||||||
|
pub redo_stack: Vec<AppSnapshot>,
|
||||||
|
pub drag_state: Option<DragState>,
|
||||||
|
pub export_rx: Option<mpsc::Receiver<exporter::ExportEvent>>,
|
||||||
|
pub export_progress: Option<exporter::ExportProgress>,
|
||||||
|
pub pending_delete: bool,
|
||||||
|
pub add_corridor_drag: Option<AddCorridorDrag>,
|
||||||
|
pub resize_state: Option<ResizeState>,
|
||||||
|
pub hover_room_idx: Option<usize>,
|
||||||
|
pub hover_corridor_idx: Option<usize>,
|
||||||
|
pub hover_door_idx: Option<usize>,
|
||||||
|
pub hover_text_idx: Option<usize>,
|
||||||
|
pub hover_marker: Option<HoverMarker>,
|
||||||
|
pub hover_level_idx: Option<usize>,
|
||||||
|
pub hover_stair_idx: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DungeonApp {
|
||||||
|
fn default() -> Self {
|
||||||
|
let settings = settings::load_settings().unwrap_or_default();
|
||||||
|
let mut app = Self {
|
||||||
|
settings,
|
||||||
|
levels: Vec::new(),
|
||||||
|
suppressed_auto_door_edges: HashSet::new(),
|
||||||
|
undo_stack: Vec::new(),
|
||||||
|
redo_stack: Vec::new(),
|
||||||
|
drag_state: None,
|
||||||
|
export_rx: None,
|
||||||
|
export_progress: None,
|
||||||
|
pending_delete: false,
|
||||||
|
add_corridor_drag: None,
|
||||||
|
resize_state: None,
|
||||||
|
hover_room_idx: None,
|
||||||
|
hover_corridor_idx: None,
|
||||||
|
hover_door_idx: None,
|
||||||
|
hover_text_idx: None,
|
||||||
|
hover_marker: None,
|
||||||
|
hover_level_idx: None,
|
||||||
|
hover_stair_idx: None,
|
||||||
|
};
|
||||||
|
if app.settings.composition_mode {
|
||||||
|
app.enter_composition_layout();
|
||||||
|
} else {
|
||||||
|
app.levels = generate_all_levels(&app.settings);
|
||||||
|
app.refresh_stairs();
|
||||||
|
}
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DungeonApp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = settings::save_settings(&self.settings) {
|
||||||
|
eprintln!("Failed to save settings: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for DungeonApp {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
let panel_result = draw_side_panel(
|
||||||
|
ctx,
|
||||||
|
&mut self.settings,
|
||||||
|
self.export_progress
|
||||||
|
.map(|progress| (progress.completed, progress.total)),
|
||||||
|
self.levels.len(),
|
||||||
|
);
|
||||||
|
let undo_requested = ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::Z));
|
||||||
|
let redo_requested = ctx.input(|i| {
|
||||||
|
(i.modifiers.command && i.key_pressed(egui::Key::Y))
|
||||||
|
|| (i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::Z))
|
||||||
|
});
|
||||||
|
let escape_pressed = ctx.input(|i| i.key_pressed(egui::Key::Escape));
|
||||||
|
|
||||||
|
if escape_pressed || panel_result.settings_changed {
|
||||||
|
self.settings.add_tool = AddTool::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
clamp_dependent_settings(&mut self.settings);
|
||||||
|
|
||||||
|
if undo_requested {
|
||||||
|
self.undo();
|
||||||
|
} else if redo_requested {
|
||||||
|
self.redo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if panel_result.clear_clicked {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.clear_layout();
|
||||||
|
} else if panel_result.reset_clicked {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
if self.settings.composition_mode {
|
||||||
|
self.enter_composition_layout();
|
||||||
|
} else {
|
||||||
|
self.regenerate_layout();
|
||||||
|
}
|
||||||
|
} else if panel_result.settings_changed && !self.settings.composition_mode {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.regenerate_layout();
|
||||||
|
} else if panel_result.save_clicked {
|
||||||
|
let mut layouts = Vec::new();
|
||||||
|
let mut svgs = Vec::new();
|
||||||
|
for layout in &self.levels {
|
||||||
|
layouts.push(layout.clone());
|
||||||
|
svgs.push(exporter::build_svg(layout, &self.settings));
|
||||||
|
}
|
||||||
|
if let Err(err) = saveandload::save_dungeon(&self.settings, layouts, svgs) {
|
||||||
|
eprintln!("{err}");
|
||||||
|
}
|
||||||
|
} else if panel_result.load_clicked {
|
||||||
|
match saveandload::load_dungeon() {
|
||||||
|
Ok(data) => {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.settings = data.settings;
|
||||||
|
self.levels = data.layouts;
|
||||||
|
self.settings.active_level_index = 0;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if panel_result.new_level_clicked {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
if self.settings.composition_mode {
|
||||||
|
self.levels.push(DungeonLayout::empty(
|
||||||
|
self.settings.pack_rooms_without_corridors,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.levels
|
||||||
|
.push(generate_level(&self.settings, self.levels.len()));
|
||||||
|
self.refresh_stairs();
|
||||||
|
}
|
||||||
|
self.settings.active_level_index = self.levels.len() - 1;
|
||||||
|
}
|
||||||
|
if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
|
||||||
|
self.pending_delete = true;
|
||||||
|
}
|
||||||
|
if panel_result.export_clicked && self.export_rx.is_none() {
|
||||||
|
let target = match exporter::select_export_target(&self.settings) {
|
||||||
|
Ok(target) => target,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match &target {
|
||||||
|
exporter::ExportTarget::File(path) => {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
self.settings.last_export_path = Some(parent.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exporter::ExportTarget::Folder(path) => {
|
||||||
|
self.settings.last_export_path = Some(path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layouts = self.levels.clone();
|
||||||
|
let settings = self.settings.clone();
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let result = exporter::export_to_target(&layouts, &settings, target, &tx);
|
||||||
|
let _ = tx.send(exporter::ExportEvent::Finished(result));
|
||||||
|
});
|
||||||
|
self.export_progress = Some(exporter::ExportProgress {
|
||||||
|
completed: 0,
|
||||||
|
total: 1,
|
||||||
|
});
|
||||||
|
self.export_rx = Some(rx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut export_finished = false;
|
||||||
|
if let Some(rx) = &self.export_rx {
|
||||||
|
loop {
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(exporter::ExportEvent::Progress(progress)) => {
|
||||||
|
self.export_progress = Some(progress);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
Ok(exporter::ExportEvent::Finished(Ok(_))) => {
|
||||||
|
export_finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(exporter::ExportEvent::Finished(Err(err))) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
export_finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(mpsc::TryRecvError::Empty) => break,
|
||||||
|
Err(mpsc::TryRecvError::Disconnected) => {
|
||||||
|
export_finished = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if export_finished {
|
||||||
|
self.export_rx = None;
|
||||||
|
self.export_progress = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if draw_legend_panel(ctx, &mut self.settings) {
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
if self.settings.composition_mode {
|
||||||
|
self.enter_composition_layout();
|
||||||
|
} else {
|
||||||
|
self.regenerate_layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
self.hover_level_idx =
|
||||||
|
draw_level_tabs(ui, self.levels.len(), &mut self.settings.active_level_index);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
let available = ui.available_size_before_wrap();
|
||||||
|
let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag());
|
||||||
|
let canvas = response.rect.shrink(12.0);
|
||||||
|
let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows);
|
||||||
|
crate::interact::update_hover_targets(self, ctx, &geometry);
|
||||||
|
|
||||||
|
let level_delete_requested = ctx.input(|i| {
|
||||||
|
(i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
|
||||||
|
&& self.hover_level_idx.is_some()
|
||||||
|
});
|
||||||
|
if level_delete_requested {
|
||||||
|
if let Some(level_idx) = self.hover_level_idx {
|
||||||
|
self.delete_level(level_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizing = crate::interact::handle_resize(self, ctx, &response, &geometry);
|
||||||
|
if !resizing {
|
||||||
|
crate::interact::handle_drag(self, ctx, &response, &geometry);
|
||||||
|
}
|
||||||
|
crate::interact::handle_add_tool(self, &response, &geometry);
|
||||||
|
if self.pending_delete {
|
||||||
|
if let Some(pointer_pos) = ctx.pointer_interact_pos() {
|
||||||
|
crate::interact::delete_at_pointer(self, pointer_pos, &geometry);
|
||||||
|
}
|
||||||
|
self.pending_delete = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.settings.active_level_index < self.levels.len() {
|
||||||
|
draw_layout(
|
||||||
|
&painter,
|
||||||
|
&geometry,
|
||||||
|
&self.levels[self.settings.active_level_index],
|
||||||
|
self.settings.colorblind_mode,
|
||||||
|
self.hover_text_idx,
|
||||||
|
self.hover_marker,
|
||||||
|
self.hover_stair_idx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
crate::interact::draw_add_overlay(self, &painter, &geometry);
|
||||||
|
crate::interact::draw_add_tool_ghost(self, &painter, &geometry);
|
||||||
|
crate::interact::draw_resize_overlay(self, &painter, &geometry);
|
||||||
|
crate::interact::draw_corridor_hover_overlay(self, &painter, &geometry);
|
||||||
|
crate::interact::draw_door_hover_overlay(self, &painter, &geometry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_all_levels(settings: &UiSettings) -> Vec<DungeonLayout> {
|
||||||
|
let level_count = if settings.min_levels == settings.max_levels {
|
||||||
|
settings.min_levels
|
||||||
|
} else {
|
||||||
|
let range_seed = crate::seed::derive_seed(settings.seed, 0x1E_7E_1_u64);
|
||||||
|
(range_seed as usize % (settings.max_levels - settings.min_levels + 1))
|
||||||
|
+ settings.min_levels
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut levels = Vec::with_capacity(level_count);
|
||||||
|
for i in 0..level_count {
|
||||||
|
levels.push(generate_level(settings, i));
|
||||||
|
}
|
||||||
|
levels
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_level(settings: &UiSettings, level_index: usize) -> DungeonLayout {
|
||||||
|
let level_seed = crate::seed::derive_seed(settings.seed, level_index as u64);
|
||||||
|
let layout = layout::generate_layout(
|
||||||
|
settings.cols,
|
||||||
|
settings.rows,
|
||||||
|
settings.room_count,
|
||||||
|
level_seed,
|
||||||
|
settings.min_room_size,
|
||||||
|
settings.max_room_size,
|
||||||
|
settings.square_rooms_only,
|
||||||
|
settings.min_corridor_width,
|
||||||
|
settings.max_corridor_width,
|
||||||
|
settings.corridor_randomness,
|
||||||
|
settings.dead_end_rooms_percent,
|
||||||
|
settings.pack_rooms_without_corridors,
|
||||||
|
door_settings_from_ui(settings),
|
||||||
|
window_settings_from_ui(settings),
|
||||||
|
);
|
||||||
|
|
||||||
|
populate_random_markers(layout, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp_dependent_settings(settings: &mut UiSettings) {
|
||||||
|
if settings.min_room_size > settings.max_room_size {
|
||||||
|
settings.max_room_size = settings.min_room_size;
|
||||||
|
}
|
||||||
|
if settings.min_corridor_width == 0 {
|
||||||
|
settings.min_corridor_width = 1;
|
||||||
|
}
|
||||||
|
if settings.min_corridor_width > settings.max_corridor_width {
|
||||||
|
settings.max_corridor_width = settings.min_corridor_width;
|
||||||
|
}
|
||||||
|
if settings.min_window_width > settings.max_window_width {
|
||||||
|
settings.max_window_width = settings.min_window_width;
|
||||||
|
}
|
||||||
|
if settings.min_start_marker_size > settings.max_start_marker_size {
|
||||||
|
settings.max_start_marker_size = settings.min_start_marker_size;
|
||||||
|
}
|
||||||
|
if settings.min_end_marker_size > settings.max_end_marker_size {
|
||||||
|
settings.max_end_marker_size = settings.min_end_marker_size;
|
||||||
|
}
|
||||||
|
if settings.min_start_marker_count > settings.max_start_marker_count {
|
||||||
|
settings.max_start_marker_count = settings.min_start_marker_count;
|
||||||
|
}
|
||||||
|
if settings.min_end_marker_count > settings.max_end_marker_count {
|
||||||
|
settings.max_end_marker_count = settings.min_end_marker_count;
|
||||||
|
}
|
||||||
|
if settings.min_traps_per_area > settings.max_traps_per_area {
|
||||||
|
settings.max_traps_per_area = settings.min_traps_per_area;
|
||||||
|
}
|
||||||
|
if settings.min_monsters_per_area > settings.max_monsters_per_area {
|
||||||
|
settings.max_monsters_per_area = settings.min_monsters_per_area;
|
||||||
|
}
|
||||||
|
if settings.min_levels == 0 {
|
||||||
|
settings.min_levels = 1;
|
||||||
|
}
|
||||||
|
if settings.min_levels > settings.max_levels {
|
||||||
|
settings.max_levels = settings.min_levels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DungeonApp {
|
||||||
|
pub fn clear_hover_targets(&mut self) {
|
||||||
|
self.hover_room_idx = None;
|
||||||
|
self.hover_corridor_idx = None;
|
||||||
|
self.hover_door_idx = None;
|
||||||
|
self.hover_text_idx = None;
|
||||||
|
self.hover_marker = None;
|
||||||
|
self.hover_level_idx = None;
|
||||||
|
self.hover_stair_idx = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_transient_state(&mut self) {
|
||||||
|
self.drag_state = None;
|
||||||
|
self.add_corridor_drag = None;
|
||||||
|
self.resize_state = None;
|
||||||
|
self.clear_hover_targets();
|
||||||
|
self.pending_delete = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self) -> AppSnapshot {
|
||||||
|
AppSnapshot {
|
||||||
|
settings: self.settings.clone(),
|
||||||
|
levels: self.levels.clone(),
|
||||||
|
suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_undo_snapshot(&mut self) {
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
if self.undo_stack.last() == Some(&snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.undo_stack.push(snapshot);
|
||||||
|
if self.undo_stack.len() > 100 {
|
||||||
|
self.undo_stack.remove(0);
|
||||||
|
}
|
||||||
|
self.redo_stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_snapshot(&mut self, snapshot: AppSnapshot) {
|
||||||
|
self.settings = snapshot.settings;
|
||||||
|
self.levels = snapshot.levels;
|
||||||
|
self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges;
|
||||||
|
self.reset_transient_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self) {
|
||||||
|
let Some(snapshot) = self.undo_stack.pop() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.redo_stack.push(self.snapshot());
|
||||||
|
self.restore_snapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redo(&mut self) {
|
||||||
|
let Some(snapshot) = self.redo_stack.pop() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.undo_stack.push(self.snapshot());
|
||||||
|
self.restore_snapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_layout(&mut self) {
|
||||||
|
if self.settings.active_level_index >= self.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.levels[self.settings.active_level_index] =
|
||||||
|
DungeonLayout::empty(self.settings.pack_rooms_without_corridors);
|
||||||
|
self.suppressed_auto_door_edges.clear();
|
||||||
|
self.reset_transient_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter_composition_layout(&mut self) {
|
||||||
|
self.levels = vec![DungeonLayout::empty(
|
||||||
|
self.settings.pack_rooms_without_corridors,
|
||||||
|
)];
|
||||||
|
self.settings.active_level_index = 0;
|
||||||
|
self.settings.export_level_index = 0;
|
||||||
|
self.suppressed_auto_door_edges.clear();
|
||||||
|
self.reset_transient_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regenerate_layout(&mut self) {
|
||||||
|
self.drag_state = None;
|
||||||
|
self.suppressed_auto_door_edges.clear();
|
||||||
|
self.levels = generate_all_levels(&self.settings);
|
||||||
|
self.refresh_stairs();
|
||||||
|
if self.settings.active_level_index >= self.levels.len() {
|
||||||
|
self.settings.active_level_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_level(&mut self, level_idx: usize) {
|
||||||
|
if self.levels.len() <= 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if level_idx >= self.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot();
|
||||||
|
self.levels.remove(level_idx);
|
||||||
|
if self.settings.active_level_index >= self.levels.len() {
|
||||||
|
self.settings.active_level_index = self.levels.len() - 1;
|
||||||
|
}
|
||||||
|
self.reset_transient_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_level(&mut self, level_idx: usize) {
|
||||||
|
if level_idx >= self.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = &mut self.levels[level_idx];
|
||||||
|
if !layout.packed_rooms {
|
||||||
|
let rooms = layout.rooms.clone();
|
||||||
|
for corridor in &mut layout.corridors {
|
||||||
|
let start = rooms[corridor.start_room_id].center_cell();
|
||||||
|
let end = rooms[corridor.end_room_id].center_cell();
|
||||||
|
let blocked = layout::blocked_room_cells(
|
||||||
|
&rooms,
|
||||||
|
&[corridor.start_room_id, corridor.end_room_id],
|
||||||
|
);
|
||||||
|
if let Some(path) = layout::shortest_path_cells(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
self.settings.cols,
|
||||||
|
self.settings.rows,
|
||||||
|
&blocked,
|
||||||
|
) {
|
||||||
|
corridor.path = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let door_settings = door_settings_from_ui(&self.settings);
|
||||||
|
layout::apply_doors(
|
||||||
|
layout,
|
||||||
|
self.settings.seed,
|
||||||
|
door_settings,
|
||||||
|
self.settings.cols,
|
||||||
|
self.settings.rows,
|
||||||
|
);
|
||||||
|
layout.doors.retain(|door| {
|
||||||
|
!crate::interact::door_edges_for(door, self.settings.cols, self.settings.rows)
|
||||||
|
.iter()
|
||||||
|
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
|
||||||
|
});
|
||||||
|
|
||||||
|
let window_settings = window_settings_from_ui(&self.settings);
|
||||||
|
layout::apply_windows(
|
||||||
|
layout,
|
||||||
|
self.settings.seed,
|
||||||
|
window_settings,
|
||||||
|
self.settings.cols,
|
||||||
|
self.settings.rows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_stairs(&mut self) {
|
||||||
|
let results = populate_stairs(self.levels.clone(), &self.settings);
|
||||||
|
self.levels = results.iter().map(|(layout, _)| layout.clone()).collect();
|
||||||
|
|
||||||
|
for (level_idx, (_, modified)) in results.into_iter().enumerate() {
|
||||||
|
if modified {
|
||||||
|
self.refresh_level(level_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_doors(&mut self) {
|
||||||
|
if self.settings.active_level_index >= self.levels.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let layout = &mut self.levels[self.settings.active_level_index];
|
||||||
|
|
||||||
|
layout::apply_doors(
|
||||||
|
layout,
|
||||||
|
self.settings.seed,
|
||||||
|
door_settings_from_ui(&self.settings),
|
||||||
|
self.settings.cols,
|
||||||
|
self.settings.rows,
|
||||||
|
);
|
||||||
|
layout.doors.retain(|door| {
|
||||||
|
!crate::interact::door_edges_for(door, self.settings.cols, self.settings.rows)
|
||||||
|
.iter()
|
||||||
|
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
|
||||||
|
});
|
||||||
|
layout::apply_windows(
|
||||||
|
layout,
|
||||||
|
self.settings.seed,
|
||||||
|
window_settings_from_ui(&self.settings),
|
||||||
|
self.settings.cols,
|
||||||
|
self.settings.rows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn door_settings_from_ui(settings: &UiSettings) -> DoorSettings {
|
||||||
|
DoorSettings {
|
||||||
|
frequency_percent: settings.door_frequency_percent,
|
||||||
|
room_hallway_percent: settings.room_hallway_door_percent,
|
||||||
|
locked_percent: settings.locked_door_percent,
|
||||||
|
secret_percent: settings.secret_door_percent,
|
||||||
|
allow_middle_corridor_doors: settings.allow_middle_corridor_doors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_settings_from_ui(settings: &UiSettings) -> WindowSettings {
|
||||||
|
WindowSettings {
|
||||||
|
enabled: settings.windows_enabled,
|
||||||
|
min_width: settings.min_window_width,
|
||||||
|
max_width: settings.max_window_width,
|
||||||
|
frequency_percent: settings.window_frequency_percent,
|
||||||
|
room_hallway_percent: settings.room_hallway_window_percent,
|
||||||
|
allow_internal_windows: settings.allow_internal_windows,
|
||||||
|
}
|
||||||
|
}
|
||||||
+2378
File diff suppressed because it is too large
Load Diff
@@ -1,177 +1,15 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use super::types::{
|
||||||
use std::collections::{HashSet, VecDeque};
|
AreaMarker, Corridor, Door, DoorSettings, DungeonLayout, Room, Staircase, Window,
|
||||||
|
WindowSettings, WindowSide,
|
||||||
|
};
|
||||||
|
use super::utils::{
|
||||||
|
SimpleRng, blocked_room_cells, corridor_cells, manhattan_distance, noisy_path,
|
||||||
|
normalized_cell_edge, overlaps_with_padding, room_index_at_cell, rooms_overlap, rooms_touch,
|
||||||
|
shared_boundary_edges, shared_opening_width, shortest_path_cells, shuffle_indices,
|
||||||
|
};
|
||||||
use crate::seed;
|
use crate::seed;
|
||||||
use crate::ui::UiSettings;
|
use crate::ui::UiSettings;
|
||||||
|
use std::collections::{HashSet, VecDeque};
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Room {
|
|
||||||
pub x: usize,
|
|
||||||
pub y: usize,
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Room {
|
|
||||||
// Return the center cell of the room.
|
|
||||||
pub fn center_cell(&self) -> (usize, usize) {
|
|
||||||
(self.x + (self.width / 2), self.y + (self.height / 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Corridor {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub id: u64,
|
|
||||||
pub start_room_id: usize,
|
|
||||||
pub end_room_id: usize,
|
|
||||||
pub path: Vec<(usize, usize)>,
|
|
||||||
pub width: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Door {
|
|
||||||
pub from: (usize, usize),
|
|
||||||
pub to: (usize, usize),
|
|
||||||
pub width: usize,
|
|
||||||
pub span_width: bool,
|
|
||||||
pub locked: bool,
|
|
||||||
pub archway: bool,
|
|
||||||
pub secret: bool,
|
|
||||||
pub manual: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Window {
|
|
||||||
pub cell: (usize, usize),
|
|
||||||
pub side: WindowSide,
|
|
||||||
pub width: usize,
|
|
||||||
pub span_width: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct TextLabel {
|
|
||||||
pub cell: (usize, usize),
|
|
||||||
pub text: String,
|
|
||||||
pub font_size: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct AreaMarker {
|
|
||||||
pub cell: (usize, usize),
|
|
||||||
pub size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Staircase {
|
|
||||||
pub cell: (usize, usize),
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum WindowSide {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct DoorSettings {
|
|
||||||
pub frequency_percent: usize,
|
|
||||||
pub room_hallway_percent: usize,
|
|
||||||
pub locked_percent: usize,
|
|
||||||
pub secret_percent: usize,
|
|
||||||
pub allow_middle_corridor_doors: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct WindowSettings {
|
|
||||||
pub enabled: bool,
|
|
||||||
pub min_width: usize,
|
|
||||||
pub max_width: usize,
|
|
||||||
pub frequency_percent: usize,
|
|
||||||
pub room_hallway_percent: usize,
|
|
||||||
pub allow_internal_windows: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct DungeonLayout {
|
|
||||||
pub rooms: Vec<Room>,
|
|
||||||
pub corridors: Vec<Corridor>,
|
|
||||||
pub doors: Vec<Door>,
|
|
||||||
pub windows: Vec<Window>,
|
|
||||||
pub text_labels: Vec<TextLabel>,
|
|
||||||
pub start_markers: Vec<AreaMarker>,
|
|
||||||
pub end_markers: Vec<AreaMarker>,
|
|
||||||
pub trap_markers: Vec<AreaMarker>,
|
|
||||||
pub monster_markers: Vec<AreaMarker>,
|
|
||||||
pub packed_rooms: bool,
|
|
||||||
pub stairs: Vec<Staircase>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DungeonLayout {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
rooms: Vec::new(),
|
|
||||||
corridors: Vec::new(),
|
|
||||||
doors: Vec::new(),
|
|
||||||
windows: Vec::new(),
|
|
||||||
text_labels: Vec::new(),
|
|
||||||
start_markers: Vec::new(),
|
|
||||||
end_markers: Vec::new(),
|
|
||||||
trap_markers: Vec::new(),
|
|
||||||
monster_markers: Vec::new(),
|
|
||||||
packed_rooms: false,
|
|
||||||
stairs: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DungeonLayout {
|
|
||||||
// Build an empty layout for the current packing mode.
|
|
||||||
pub(crate) fn empty(packed_rooms: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
packed_rooms,
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a layout from generated rooms and corridors.
|
|
||||||
fn from_generated_parts(
|
|
||||||
rooms: Vec<Room>,
|
|
||||||
corridors: Vec<Corridor>,
|
|
||||||
packed_rooms: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
rooms,
|
|
||||||
corridors,
|
|
||||||
packed_rooms,
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all room cells except those belonging to excluded room ids.
|
|
||||||
pub fn blocked_room_cells(rooms: &[Room], excluded_room_ids: &[usize]) -> HashSet<(usize, usize)> {
|
|
||||||
let excluded: HashSet<usize> = excluded_room_ids.iter().copied().collect();
|
|
||||||
let mut blocked = HashSet::new();
|
|
||||||
|
|
||||||
for (room_idx, room) in rooms.iter().enumerate() {
|
|
||||||
if excluded.contains(&room_idx) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for x in room.x..(room.x + room.width) {
|
|
||||||
for y in room.y..(room.y + room.height) {
|
|
||||||
blocked.insert((x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a layout based on settings and a derived deterministic seed.
|
// Build a layout based on settings and a derived deterministic seed.
|
||||||
pub fn generate_layout(
|
pub fn generate_layout(
|
||||||
@@ -206,7 +44,7 @@ pub fn generate_layout(
|
|||||||
let mut corridor_rng = SimpleRng::new(seed::derive_seed(base_seed, 4));
|
let mut corridor_rng = SimpleRng::new(seed::derive_seed(base_seed, 4));
|
||||||
|
|
||||||
let mut rooms = Vec::new();
|
let mut rooms = Vec::new();
|
||||||
let mut corridors = Vec::new();
|
let corridors = Vec::new();
|
||||||
|
|
||||||
if cols < 2 || rows < 2 || target_room_count == 0 {
|
if cols < 2 || rows < 2 || target_room_count == 0 {
|
||||||
return DungeonLayout::empty(pack_rooms_without_corridors);
|
return DungeonLayout::empty(pack_rooms_without_corridors);
|
||||||
@@ -308,6 +146,7 @@ pub fn generate_layout(
|
|||||||
let room_edges =
|
let room_edges =
|
||||||
build_room_connection_edges(¢ers, randomness, target_dead_end_rooms, &mut graph_rng);
|
build_room_connection_edges(¢ers, randomness, target_dead_end_rooms, &mut graph_rng);
|
||||||
|
|
||||||
|
let mut corridors = Vec::new();
|
||||||
let mut next_corridor_id = 1u64;
|
let mut next_corridor_id = 1u64;
|
||||||
for (start_room_id, end_room_id) in room_edges {
|
for (start_room_id, end_room_id) in room_edges {
|
||||||
let start = centers[start_room_id];
|
let start = centers[start_room_id];
|
||||||
@@ -930,163 +769,6 @@ fn shared_room_boundaries(
|
|||||||
boundaries
|
boundaries
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usize))> {
|
|
||||||
let mut edges = Vec::new();
|
|
||||||
|
|
||||||
if a.x + a.width == b.x || b.x + b.width == a.x {
|
|
||||||
let left = if a.x < b.x { a } else { b };
|
|
||||||
let right = if a.x < b.x { b } else { a };
|
|
||||||
let y0 = left.y.max(right.y);
|
|
||||||
let y1 = (left.y + left.height).min(right.y + right.height);
|
|
||||||
for y in y0..y1 {
|
|
||||||
edges.push(normalized_cell_edge(
|
|
||||||
(left.x + left.width - 1, y),
|
|
||||||
(right.x, y),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.y + a.height == b.y || b.y + b.height == a.y {
|
|
||||||
let top = if a.y < b.y { a } else { b };
|
|
||||||
let bottom = if a.y < b.y { b } else { a };
|
|
||||||
let x0 = top.x.max(bottom.x);
|
|
||||||
let x1 = (top.x + top.width).min(bottom.x + bottom.width);
|
|
||||||
for x in x0..x1 {
|
|
||||||
edges.push(normalized_cell_edge(
|
|
||||||
(x, top.y + top.height - 1),
|
|
||||||
(x, bottom.y),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
edges
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option<usize> {
|
|
||||||
rooms.iter().position(|room| {
|
|
||||||
cell.0 >= room.x
|
|
||||||
&& cell.0 < room.x + room.width
|
|
||||||
&& cell.1 >= room.y
|
|
||||||
&& cell.1 < room.y + room.height
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute corridor cells while excluding room cells.
|
|
||||||
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
|
||||||
let mut cells = HashSet::new();
|
|
||||||
if cols == 0 || rows == 0 {
|
|
||||||
return cells;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut room_cells = HashSet::new();
|
|
||||||
for room in &layout.rooms {
|
|
||||||
for x in room.x..(room.x + room.width) {
|
|
||||||
for y in room.y..(room.y + room.height) {
|
|
||||||
room_cells.insert((x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for corridor in &layout.corridors {
|
|
||||||
let width = corridor.width.max(1);
|
|
||||||
let min_offset = -((width as isize - 1) / 2);
|
|
||||||
let max_offset = width as isize / 2;
|
|
||||||
if corridor.path.len() == 1 {
|
|
||||||
let (x, y) = corridor.path[0];
|
|
||||||
for dy in min_offset..=max_offset {
|
|
||||||
let ny = y as isize + dy;
|
|
||||||
if ny < 0 || ny >= rows as isize {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let cell = (x, ny as usize);
|
|
||||||
if !room_cells.contains(&cell) {
|
|
||||||
cells.insert(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for pair in corridor.path.windows(2) {
|
|
||||||
let a = pair[0];
|
|
||||||
let b = pair[1];
|
|
||||||
if a == b {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.0 != b.0 {
|
|
||||||
let x0 = a.0.min(b.0);
|
|
||||||
let x1 = a.0.max(b.0);
|
|
||||||
let y = a.1 as isize;
|
|
||||||
for x in x0..=x1 {
|
|
||||||
for dy in min_offset..=max_offset {
|
|
||||||
let ny = y + dy;
|
|
||||||
if ny < 0 || ny >= rows as isize {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let cell = (x, ny as usize);
|
|
||||||
if !room_cells.contains(&cell) {
|
|
||||||
cells.insert(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let y0 = a.1.min(b.1);
|
|
||||||
let y1 = a.1.max(b.1);
|
|
||||||
let x = a.0 as isize;
|
|
||||||
for y in y0..=y1 {
|
|
||||||
for dx in min_offset..=max_offset {
|
|
||||||
let nx = x + dx;
|
|
||||||
if nx < 0 || nx >= cols as isize {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let cell = (nx as usize, y);
|
|
||||||
if !room_cells.contains(&cell) {
|
|
||||||
cells.insert(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for turn in corridor.path.windows(3) {
|
|
||||||
let prev = turn[0];
|
|
||||||
let corner = turn[1];
|
|
||||||
let next = turn[2];
|
|
||||||
let incoming = (
|
|
||||||
corner.0 as isize - prev.0 as isize,
|
|
||||||
corner.1 as isize - prev.1 as isize,
|
|
||||||
);
|
|
||||||
let outgoing = (
|
|
||||||
next.0 as isize - corner.0 as isize,
|
|
||||||
next.1 as isize - corner.1 as isize,
|
|
||||||
);
|
|
||||||
if incoming == outgoing {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for dx in min_offset..=max_offset {
|
|
||||||
let nx = corner.0 as isize + dx;
|
|
||||||
if nx < 0 || nx >= cols as isize {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for dy in min_offset..=max_offset {
|
|
||||||
let ny = corner.1 as isize + dy;
|
|
||||||
if ny < 0 || ny >= rows as isize {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cell = (nx as usize, ny as usize);
|
|
||||||
if !room_cells.contains(&cell) {
|
|
||||||
cells.insert(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cells
|
|
||||||
}
|
|
||||||
|
|
||||||
const START_COUNT_STREAM: u64 = 10_001;
|
const START_COUNT_STREAM: u64 = 10_001;
|
||||||
const END_COUNT_STREAM: u64 = 10_002;
|
const END_COUNT_STREAM: u64 = 10_002;
|
||||||
const START_ROOM_STREAM_BASE: u64 = 11_000;
|
const START_ROOM_STREAM_BASE: u64 = 11_000;
|
||||||
@@ -1982,41 +1664,6 @@ fn shuffle_rooms(rooms: &mut [Room], rng: &mut SimpleRng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rects_overlap(
|
|
||||||
ax: usize,
|
|
||||||
ay: usize,
|
|
||||||
aw: usize,
|
|
||||||
ah: usize,
|
|
||||||
bx: usize,
|
|
||||||
by: usize,
|
|
||||||
bw: usize,
|
|
||||||
bh: usize,
|
|
||||||
) -> bool {
|
|
||||||
let a_right = ax + aw;
|
|
||||||
let a_bottom = ay + ah;
|
|
||||||
let b_right = bx + bw;
|
|
||||||
let b_bottom = by + bh;
|
|
||||||
|
|
||||||
ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rooms_overlap(a: &Room, b: &Room) -> bool {
|
|
||||||
rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rooms_touch(a: &Room, b: &Room) -> bool {
|
|
||||||
!shared_boundary_edges(a, b).is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shared_opening_width(a: &Room, b: &Room, span: usize, default_width: usize) -> usize {
|
|
||||||
let max_width = if a.x + a.width == b.x || b.x + b.width == a.x {
|
|
||||||
a.height.min(b.height)
|
|
||||||
} else {
|
|
||||||
a.width.min(b.width)
|
|
||||||
};
|
|
||||||
default_width.max(1).min(span).min(max_width.max(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a connected graph of room-to-room edges with optional dead ends.
|
// Create a connected graph of room-to-room edges with optional dead ends.
|
||||||
fn build_room_connection_edges(
|
fn build_room_connection_edges(
|
||||||
centers: &[(usize, usize)],
|
centers: &[(usize, usize)],
|
||||||
@@ -2127,253 +1774,3 @@ fn push_unique_room_edge(
|
|||||||
edges.push((a, b));
|
edges.push((a, b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shuffle indices in place using the provided RNG.
|
|
||||||
fn shuffle_indices(indices: &mut [usize], rng: &mut SimpleRng) {
|
|
||||||
if indices.len() <= 1 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for i in (1..indices.len()).rev() {
|
|
||||||
let j = rng.range_inclusive(0, i);
|
|
||||||
indices.swap(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the shortest grid path between two cells using BFS.
|
|
||||||
pub fn shortest_path_cells(
|
|
||||||
start: (usize, usize),
|
|
||||||
end: (usize, usize),
|
|
||||||
cols: usize,
|
|
||||||
rows: usize,
|
|
||||||
blocked: &HashSet<(usize, usize)>,
|
|
||||||
) -> Option<Vec<(usize, usize)>> {
|
|
||||||
if start == end {
|
|
||||||
return Some(vec![start]);
|
|
||||||
}
|
|
||||||
if blocked.contains(&start) || blocked.contains(&end) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total = cols.saturating_mul(rows);
|
|
||||||
if total == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = |p: (usize, usize)| -> usize { p.1 * cols + p.0 };
|
|
||||||
let coord = |idx: usize| -> (usize, usize) { (idx % cols, idx / cols) };
|
|
||||||
|
|
||||||
let start_idx = index(start);
|
|
||||||
let end_idx = index(end);
|
|
||||||
|
|
||||||
let mut queue = VecDeque::new();
|
|
||||||
let mut visited = vec![false; total];
|
|
||||||
let mut parent: Vec<Option<usize>> = vec![None; total];
|
|
||||||
|
|
||||||
visited[start_idx] = true;
|
|
||||||
queue.push_back(start_idx);
|
|
||||||
|
|
||||||
while let Some(current) = queue.pop_front() {
|
|
||||||
if current == end_idx {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (x, y) = coord(current);
|
|
||||||
let neighbors = [
|
|
||||||
x.checked_sub(1).map(|nx| (nx, y)),
|
|
||||||
(x + 1 < cols).then_some((x + 1, y)),
|
|
||||||
y.checked_sub(1).map(|ny| (x, ny)),
|
|
||||||
(y + 1 < rows).then_some((x, y + 1)),
|
|
||||||
];
|
|
||||||
|
|
||||||
for neighbor in neighbors.into_iter().flatten() {
|
|
||||||
if blocked.contains(&neighbor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let n_idx = index(neighbor);
|
|
||||||
if !visited[n_idx] {
|
|
||||||
visited[n_idx] = true;
|
|
||||||
parent[n_idx] = Some(current);
|
|
||||||
queue.push_back(n_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visited[end_idx] {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut path = Vec::new();
|
|
||||||
let mut current = end_idx;
|
|
||||||
path.push(coord(current));
|
|
||||||
|
|
||||||
while let Some(prev) = parent[current] {
|
|
||||||
current = prev;
|
|
||||||
path.push(coord(current));
|
|
||||||
}
|
|
||||||
|
|
||||||
path.reverse();
|
|
||||||
Some(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a noisy path biased toward the target cell.
|
|
||||||
fn noisy_path(
|
|
||||||
start: (usize, usize),
|
|
||||||
end: (usize, usize),
|
|
||||||
cols: usize,
|
|
||||||
rows: usize,
|
|
||||||
randomness: f32,
|
|
||||||
blocked: &HashSet<(usize, usize)>,
|
|
||||||
rng: &mut SimpleRng,
|
|
||||||
) -> Vec<(usize, usize)> {
|
|
||||||
if start == end {
|
|
||||||
return vec![start];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut path = vec![start];
|
|
||||||
let mut visited = HashSet::new();
|
|
||||||
visited.insert(start);
|
|
||||||
|
|
||||||
let mut current = start;
|
|
||||||
let mut prev_dir = (0isize, 0isize);
|
|
||||||
let max_steps = cols.saturating_mul(rows).max(32);
|
|
||||||
|
|
||||||
for _ in 0..max_steps {
|
|
||||||
if current == end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut neighbors = Vec::with_capacity(4);
|
|
||||||
let (x, y) = current;
|
|
||||||
if x > 0 {
|
|
||||||
neighbors.push((x - 1, y));
|
|
||||||
}
|
|
||||||
if x + 1 < cols {
|
|
||||||
neighbors.push((x + 1, y));
|
|
||||||
}
|
|
||||||
if y > 0 {
|
|
||||||
neighbors.push((x, y - 1));
|
|
||||||
}
|
|
||||||
if y + 1 < rows {
|
|
||||||
neighbors.push((x, y + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if neighbors.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut best = neighbors[0];
|
|
||||||
let mut best_score = f32::INFINITY;
|
|
||||||
|
|
||||||
for &candidate in &neighbors {
|
|
||||||
if blocked.contains(&candidate) && candidate != end {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let step_dir = (
|
|
||||||
candidate.0 as isize - current.0 as isize,
|
|
||||||
candidate.1 as isize - current.1 as isize,
|
|
||||||
);
|
|
||||||
|
|
||||||
let dist = manhattan_distance(candidate, end) as f32;
|
|
||||||
let progress_weight = 1.0 - (0.85 * randomness);
|
|
||||||
let revisit_penalty = if visited.contains(&candidate) {
|
|
||||||
2.5 + (2.0 * randomness)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let turn_penalty = if prev_dir == (0, 0) || prev_dir == step_dir {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
0.6 - (0.35 * randomness)
|
|
||||||
};
|
|
||||||
let noise = rng.next_f32() * 8.0 * randomness;
|
|
||||||
let score = (dist * progress_weight) + revisit_penalty + turn_penalty + noise;
|
|
||||||
|
|
||||||
if score < best_score {
|
|
||||||
best_score = score;
|
|
||||||
best = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_dir = (
|
|
||||||
best.0 as isize - current.0 as isize,
|
|
||||||
best.1 as isize - current.1 as isize,
|
|
||||||
);
|
|
||||||
current = best;
|
|
||||||
path.push(current);
|
|
||||||
visited.insert(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if current != end
|
|
||||||
&& let Some(tail) = shortest_path_cells(current, end, cols, rows, blocked)
|
|
||||||
{
|
|
||||||
for &cell in tail.iter().skip(1) {
|
|
||||||
path.push(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test whether two rooms overlap with extra padding.
|
|
||||||
fn overlaps_with_padding(a: &Room, b: &Room, padding: usize) -> bool {
|
|
||||||
let a_left = a.x.saturating_sub(padding);
|
|
||||||
let a_top = a.y.saturating_sub(padding);
|
|
||||||
let a_right = a.x + a.width + padding;
|
|
||||||
let a_bottom = a.y + a.height + padding;
|
|
||||||
|
|
||||||
let b_left = b.x;
|
|
||||||
let b_top = b.y;
|
|
||||||
let b_right = b.x + b.width;
|
|
||||||
let b_bottom = b.y + b.height;
|
|
||||||
|
|
||||||
a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute Manhattan distance between two grid cells.
|
|
||||||
fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize {
|
|
||||||
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize a cell edge ordering.
|
|
||||||
fn normalized_cell_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
|
|
||||||
if a <= b { (a, b) } else { (b, a) }
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SimpleRng {
|
|
||||||
state: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleRng {
|
|
||||||
// Create a small deterministic RNG with a fallback seed.
|
|
||||||
fn new(seed: u64) -> Self {
|
|
||||||
let state = if seed == 0 {
|
|
||||||
0xA5A5_A5A5_1234_5678
|
|
||||||
} else {
|
|
||||||
seed
|
|
||||||
};
|
|
||||||
Self { state }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the next random u32.
|
|
||||||
fn next_u32(&mut self) -> u32 {
|
|
||||||
self.state ^= self.state >> 12;
|
|
||||||
self.state ^= self.state << 25;
|
|
||||||
self.state ^= self.state >> 27;
|
|
||||||
(self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 32) as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the next random f32 in [0,1].
|
|
||||||
fn next_f32(&mut self) -> f32 {
|
|
||||||
self.next_u32() as f32 / u32::MAX as f32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a random usize between min and max inclusive.
|
|
||||||
fn range_inclusive(&mut self, min: usize, max: usize) -> usize {
|
|
||||||
if min >= max {
|
|
||||||
return min;
|
|
||||||
}
|
|
||||||
let width = max - min + 1;
|
|
||||||
min + (self.next_u32() as usize % width)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod generation;
|
||||||
|
pub mod types;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use generation::*;
|
||||||
|
pub use types::*;
|
||||||
|
pub use utils::*;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Room {
|
||||||
|
pub x: usize,
|
||||||
|
pub y: usize,
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
// Return the center cell of the room.
|
||||||
|
pub fn center_cell(&self) -> (usize, usize) {
|
||||||
|
(self.x + (self.width / 2), self.y + (self.height / 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Corridor {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub id: u64,
|
||||||
|
pub start_room_id: usize,
|
||||||
|
pub end_room_id: usize,
|
||||||
|
pub path: Vec<(usize, usize)>,
|
||||||
|
pub width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Door {
|
||||||
|
pub from: (usize, usize),
|
||||||
|
pub to: (usize, usize),
|
||||||
|
pub width: usize,
|
||||||
|
pub span_width: bool,
|
||||||
|
pub locked: bool,
|
||||||
|
pub archway: bool,
|
||||||
|
pub secret: bool,
|
||||||
|
pub manual: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Window {
|
||||||
|
pub cell: (usize, usize),
|
||||||
|
pub side: WindowSide,
|
||||||
|
pub width: usize,
|
||||||
|
pub span_width: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct TextLabel {
|
||||||
|
pub cell: (usize, usize),
|
||||||
|
pub text: String,
|
||||||
|
pub font_size: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AreaMarker {
|
||||||
|
pub cell: (usize, usize),
|
||||||
|
pub size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Staircase {
|
||||||
|
pub cell: (usize, usize),
|
||||||
|
pub width: usize,
|
||||||
|
pub height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum WindowSide {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct DoorSettings {
|
||||||
|
pub frequency_percent: usize,
|
||||||
|
pub room_hallway_percent: usize,
|
||||||
|
pub locked_percent: usize,
|
||||||
|
pub secret_percent: usize,
|
||||||
|
pub allow_middle_corridor_doors: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct WindowSettings {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub min_width: usize,
|
||||||
|
pub max_width: usize,
|
||||||
|
pub frequency_percent: usize,
|
||||||
|
pub room_hallway_percent: usize,
|
||||||
|
pub allow_internal_windows: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct DungeonLayout {
|
||||||
|
pub rooms: Vec<Room>,
|
||||||
|
pub corridors: Vec<Corridor>,
|
||||||
|
pub doors: Vec<Door>,
|
||||||
|
pub windows: Vec<Window>,
|
||||||
|
pub text_labels: Vec<TextLabel>,
|
||||||
|
pub start_markers: Vec<AreaMarker>,
|
||||||
|
pub end_markers: Vec<AreaMarker>,
|
||||||
|
pub trap_markers: Vec<AreaMarker>,
|
||||||
|
pub monster_markers: Vec<AreaMarker>,
|
||||||
|
pub packed_rooms: bool,
|
||||||
|
pub stairs: Vec<Staircase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DungeonLayout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
rooms: Vec::new(),
|
||||||
|
corridors: Vec::new(),
|
||||||
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
|
text_labels: Vec::new(),
|
||||||
|
start_markers: Vec::new(),
|
||||||
|
end_markers: Vec::new(),
|
||||||
|
trap_markers: Vec::new(),
|
||||||
|
monster_markers: Vec::new(),
|
||||||
|
packed_rooms: false,
|
||||||
|
stairs: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DungeonLayout {
|
||||||
|
// Build an empty layout for the current packing mode.
|
||||||
|
pub(crate) fn empty(packed_rooms: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
packed_rooms,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a layout from generated rooms and corridors.
|
||||||
|
pub(crate) fn from_generated_parts(
|
||||||
|
rooms: Vec<Room>,
|
||||||
|
corridors: Vec<Corridor>,
|
||||||
|
packed_rooms: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
rooms,
|
||||||
|
corridors,
|
||||||
|
packed_rooms,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
use super::types::{DungeonLayout, Room};
|
||||||
|
use std::collections::{HashSet, VecDeque};
|
||||||
|
|
||||||
|
pub struct SimpleRng {
|
||||||
|
pub state: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleRng {
|
||||||
|
// Create a small deterministic RNG with a fallback seed.
|
||||||
|
pub fn new(seed: u64) -> Self {
|
||||||
|
let state = if seed == 0 {
|
||||||
|
0xA5A5_A5A5_1234_5678
|
||||||
|
} else {
|
||||||
|
seed
|
||||||
|
};
|
||||||
|
Self { state }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the next random u32.
|
||||||
|
pub fn next_u32(&mut self) -> u32 {
|
||||||
|
self.state ^= self.state >> 12;
|
||||||
|
self.state ^= self.state << 25;
|
||||||
|
self.state ^= self.state >> 27;
|
||||||
|
(self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 32) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the next random f32 in [0,1].
|
||||||
|
pub fn next_f32(&mut self) -> f32 {
|
||||||
|
self.next_u32() as f32 / u32::MAX as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random usize between min and max inclusive.
|
||||||
|
pub fn range_inclusive(&mut self, min: usize, max: usize) -> usize {
|
||||||
|
if min >= max {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
let width = max - min + 1;
|
||||||
|
min + (self.next_u32() as usize % width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all room cells except those belonging to excluded room ids.
|
||||||
|
pub fn blocked_room_cells(rooms: &[Room], excluded_room_ids: &[usize]) -> HashSet<(usize, usize)> {
|
||||||
|
let excluded: HashSet<usize> = excluded_room_ids.iter().copied().collect();
|
||||||
|
let mut blocked = HashSet::new();
|
||||||
|
|
||||||
|
for (room_idx, room) in rooms.iter().enumerate() {
|
||||||
|
if excluded.contains(&room_idx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for x in room.x..(room.x + room.width) {
|
||||||
|
for y in room.y..(room.y + room.height) {
|
||||||
|
blocked.insert((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute corridor cells while excluding room cells.
|
||||||
|
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
||||||
|
let mut cells = HashSet::new();
|
||||||
|
if cols == 0 || rows == 0 {
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut room_cells = HashSet::new();
|
||||||
|
for room in &layout.rooms {
|
||||||
|
for x in room.x..(room.x + room.width) {
|
||||||
|
for y in room.y..(room.y + room.height) {
|
||||||
|
room_cells.insert((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for corridor in &layout.corridors {
|
||||||
|
let width = corridor.width.max(1);
|
||||||
|
let min_offset = -((width as isize - 1) / 2);
|
||||||
|
let max_offset = width as isize / 2;
|
||||||
|
if corridor.path.len() == 1 {
|
||||||
|
let (x, y) = corridor.path[0];
|
||||||
|
for dy in min_offset..=max_offset {
|
||||||
|
let ny = y as isize + dy;
|
||||||
|
if ny < 0 || ny >= rows as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell = (x, ny as usize);
|
||||||
|
if !room_cells.contains(&cell) {
|
||||||
|
cells.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for pair in corridor.path.windows(2) {
|
||||||
|
let a = pair[0];
|
||||||
|
let b = pair[1];
|
||||||
|
if a == b {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.0 != b.0 {
|
||||||
|
let x0 = a.0.min(b.0);
|
||||||
|
let x1 = a.0.max(b.0);
|
||||||
|
let y = a.1 as isize;
|
||||||
|
for x in x0..=x1 {
|
||||||
|
for dy in min_offset..=max_offset {
|
||||||
|
let ny = y + dy;
|
||||||
|
if ny < 0 || ny >= rows as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell = (x, ny as usize);
|
||||||
|
if !room_cells.contains(&cell) {
|
||||||
|
cells.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let y0 = a.1.min(b.1);
|
||||||
|
let y1 = a.1.max(b.1);
|
||||||
|
let x = a.0 as isize;
|
||||||
|
for y in y0..=y1 {
|
||||||
|
for dx in min_offset..=max_offset {
|
||||||
|
let nx = x + dx;
|
||||||
|
if nx < 0 || nx >= cols as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cell = (nx as usize, y);
|
||||||
|
if !room_cells.contains(&cell) {
|
||||||
|
cells.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for turn in corridor.path.windows(3) {
|
||||||
|
let prev = turn[0];
|
||||||
|
let corner = turn[1];
|
||||||
|
let next = turn[2];
|
||||||
|
let incoming = (
|
||||||
|
corner.0 as isize - prev.0 as isize,
|
||||||
|
corner.1 as isize - prev.1 as isize,
|
||||||
|
);
|
||||||
|
let outgoing = (
|
||||||
|
next.0 as isize - corner.0 as isize,
|
||||||
|
next.1 as isize - corner.1 as isize,
|
||||||
|
);
|
||||||
|
if incoming == outgoing {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for dx in min_offset..=max_offset {
|
||||||
|
let nx = corner.0 as isize + dx;
|
||||||
|
if nx < 0 || nx >= cols as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for dy in min_offset..=max_offset {
|
||||||
|
let ny = corner.1 as isize + dy;
|
||||||
|
if ny < 0 || ny >= rows as isize {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = (nx as usize, ny as usize);
|
||||||
|
if !room_cells.contains(&cell) {
|
||||||
|
cells.insert(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cells
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option<usize> {
|
||||||
|
rooms.iter().position(|room| {
|
||||||
|
cell.0 >= room.x
|
||||||
|
&& cell.0 < room.x + room.width
|
||||||
|
&& cell.1 >= room.y
|
||||||
|
&& cell.1 < room.y + room.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a cell edge ordering.
|
||||||
|
pub fn normalized_cell_edge(
|
||||||
|
a: (usize, usize),
|
||||||
|
b: (usize, usize),
|
||||||
|
) -> ((usize, usize), (usize, usize)) {
|
||||||
|
if a <= b { (a, b) } else { (b, a) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute Manhattan distance between two grid cells.
|
||||||
|
pub fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize {
|
||||||
|
a.0.abs_diff(b.0) + a.1.abs_diff(b.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test whether two rooms overlap with extra padding.
|
||||||
|
pub fn overlaps_with_padding(a: &Room, b: &Room, padding: usize) -> bool {
|
||||||
|
let a_left = a.x.saturating_sub(padding);
|
||||||
|
let a_top = a.y.saturating_sub(padding);
|
||||||
|
let a_right = a.x + a.width + padding;
|
||||||
|
let a_bottom = a.y + a.height + padding;
|
||||||
|
|
||||||
|
let b_left = b.x;
|
||||||
|
let b_top = b.y;
|
||||||
|
let b_right = b.x + b.width;
|
||||||
|
let b_bottom = b.y + b.height;
|
||||||
|
|
||||||
|
a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rects_overlap(
|
||||||
|
ax: usize,
|
||||||
|
ay: usize,
|
||||||
|
aw: usize,
|
||||||
|
ah: usize,
|
||||||
|
bx: usize,
|
||||||
|
by: usize,
|
||||||
|
bw: usize,
|
||||||
|
bh: usize,
|
||||||
|
) -> bool {
|
||||||
|
let a_right = ax + aw;
|
||||||
|
let a_bottom = ay + ah;
|
||||||
|
let b_right = bx + bw;
|
||||||
|
let b_bottom = by + bh;
|
||||||
|
|
||||||
|
ax < b_right && a_right > bx && ay < b_bottom && a_bottom > by
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rooms_overlap(a: &Room, b: &Room) -> bool {
|
||||||
|
rects_overlap(a.x, a.y, a.width, a.height, b.x, b.y, b.width, b.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rooms_touch(a: &Room, b: &Room) -> bool {
|
||||||
|
!shared_boundary_edges(a, b).is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usize))> {
|
||||||
|
let mut edges = Vec::new();
|
||||||
|
|
||||||
|
if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||||
|
let left = if a.x < b.x { a } else { b };
|
||||||
|
let right = if a.x < b.x { b } else { a };
|
||||||
|
let y0 = left.y.max(right.y);
|
||||||
|
let y1 = (left.y + left.height).min(right.y + right.height);
|
||||||
|
for y in y0..y1 {
|
||||||
|
edges.push(normalized_cell_edge(
|
||||||
|
(left.x + left.width - 1, y),
|
||||||
|
(right.x, y),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.y + a.height == b.y || b.y + b.height == a.y {
|
||||||
|
let top = if a.y < b.y { a } else { b };
|
||||||
|
let bottom = if a.y < b.y { b } else { a };
|
||||||
|
let x0 = top.x.max(bottom.x);
|
||||||
|
let x1 = (top.x + top.width).min(bottom.x + bottom.width);
|
||||||
|
for x in x0..x1 {
|
||||||
|
edges.push(normalized_cell_edge(
|
||||||
|
(x, top.y + top.height - 1),
|
||||||
|
(x, bottom.y),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shared_opening_width(a: &Room, b: &Room, span: usize, default_width: usize) -> usize {
|
||||||
|
let max_width = if a.x + a.width == b.x || b.x + b.width == a.x {
|
||||||
|
a.height.min(b.height)
|
||||||
|
} else {
|
||||||
|
a.width.min(b.width)
|
||||||
|
};
|
||||||
|
default_width.max(1).min(span).min(max_width.max(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the shortest grid path between two cells using BFS.
|
||||||
|
pub fn shortest_path_cells(
|
||||||
|
start: (usize, usize),
|
||||||
|
end: (usize, usize),
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
blocked: &HashSet<(usize, usize)>,
|
||||||
|
) -> Option<Vec<(usize, usize)>> {
|
||||||
|
if start == end {
|
||||||
|
return Some(vec![start]);
|
||||||
|
}
|
||||||
|
if blocked.contains(&start) || blocked.contains(&end) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = cols.saturating_mul(rows);
|
||||||
|
if total == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = |p: (usize, usize)| -> usize { p.1 * cols + p.0 };
|
||||||
|
let coord = |idx: usize| -> (usize, usize) { (idx % cols, idx / cols) };
|
||||||
|
|
||||||
|
let start_idx = index(start);
|
||||||
|
let end_idx = index(end);
|
||||||
|
|
||||||
|
let mut queue = VecDeque::new();
|
||||||
|
let mut visited = vec![false; total];
|
||||||
|
let mut parent: Vec<Option<usize>> = vec![None; total];
|
||||||
|
|
||||||
|
visited[start_idx] = true;
|
||||||
|
queue.push_back(start_idx);
|
||||||
|
|
||||||
|
while let Some(current) = queue.pop_front() {
|
||||||
|
if current == end_idx {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = coord(current);
|
||||||
|
let neighbors = [
|
||||||
|
x.checked_sub(1).map(|nx| (nx, y)),
|
||||||
|
(x + 1 < cols).then_some((x + 1, y)),
|
||||||
|
y.checked_sub(1).map(|ny| (x, ny)),
|
||||||
|
(y + 1 < rows).then_some((x, y + 1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for neighbor in neighbors.into_iter().flatten() {
|
||||||
|
if blocked.contains(&neighbor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let n_idx = index(neighbor);
|
||||||
|
if !visited[n_idx] {
|
||||||
|
visited[n_idx] = true;
|
||||||
|
parent[n_idx] = Some(current);
|
||||||
|
queue.push_back(n_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visited[end_idx] {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = Vec::new();
|
||||||
|
let mut current = end_idx;
|
||||||
|
path.push(coord(current));
|
||||||
|
|
||||||
|
while let Some(prev) = parent[current] {
|
||||||
|
current = prev;
|
||||||
|
path.push(coord(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
path.reverse();
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a noisy path biased toward the target cell.
|
||||||
|
pub fn noisy_path(
|
||||||
|
start: (usize, usize),
|
||||||
|
end: (usize, usize),
|
||||||
|
cols: usize,
|
||||||
|
rows: usize,
|
||||||
|
randomness: f32,
|
||||||
|
blocked: &HashSet<(usize, usize)>,
|
||||||
|
rng: &mut SimpleRng,
|
||||||
|
) -> Vec<(usize, usize)> {
|
||||||
|
if start == end {
|
||||||
|
return vec![start];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path = vec![start];
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
visited.insert(start);
|
||||||
|
|
||||||
|
let mut current = start;
|
||||||
|
let mut prev_dir = (0isize, 0isize);
|
||||||
|
let max_steps = cols.saturating_mul(rows).max(32);
|
||||||
|
|
||||||
|
for _ in 0..max_steps {
|
||||||
|
if current == end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut neighbors = Vec::with_capacity(4);
|
||||||
|
let (x, y) = current;
|
||||||
|
if x > 0 {
|
||||||
|
neighbors.push((x - 1, y));
|
||||||
|
}
|
||||||
|
if x + 1 < cols {
|
||||||
|
neighbors.push((x + 1, y));
|
||||||
|
}
|
||||||
|
if y > 0 {
|
||||||
|
neighbors.push((x, y - 1));
|
||||||
|
}
|
||||||
|
if y + 1 < rows {
|
||||||
|
neighbors.push((x, y + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if neighbors.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best = neighbors[0];
|
||||||
|
let mut best_score = f32::INFINITY;
|
||||||
|
|
||||||
|
for &candidate in &neighbors {
|
||||||
|
if blocked.contains(&candidate) && candidate != end {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let step_dir = (
|
||||||
|
candidate.0 as isize - current.0 as isize,
|
||||||
|
candidate.1 as isize - current.1 as isize,
|
||||||
|
);
|
||||||
|
|
||||||
|
let dist = manhattan_distance(candidate, end) as f32;
|
||||||
|
let progress_weight = 1.0 - (0.85 * randomness);
|
||||||
|
let revisit_penalty = if visited.contains(&candidate) {
|
||||||
|
2.5 + (2.0 * randomness)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let turn_penalty = if prev_dir == (0, 0) || prev_dir == step_dir {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
0.6 - (0.35 * randomness)
|
||||||
|
};
|
||||||
|
let noise = rng.next_f32() * 8.0 * randomness;
|
||||||
|
let score = (dist * progress_weight) + revisit_penalty + turn_penalty + noise;
|
||||||
|
|
||||||
|
if score < best_score {
|
||||||
|
best_score = score;
|
||||||
|
best = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_dir = (
|
||||||
|
best.0 as isize - current.0 as isize,
|
||||||
|
best.1 as isize - current.1 as isize,
|
||||||
|
);
|
||||||
|
current = best;
|
||||||
|
path.push(current);
|
||||||
|
visited.insert(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if current != end
|
||||||
|
&& let Some(tail) = shortest_path_cells(current, end, cols, rows, blocked)
|
||||||
|
{
|
||||||
|
for &cell in tail.iter().skip(1) {
|
||||||
|
path.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle indices in place using the provided RNG.
|
||||||
|
pub fn shuffle_indices(indices: &mut [usize], rng: &mut SimpleRng) {
|
||||||
|
if indices.len() <= 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for i in (1..indices.len()).rev() {
|
||||||
|
let j = rng.range_inclusive(0, i);
|
||||||
|
indices.swap(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-3821
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,770 @@
|
|||||||
|
use crate::interact::{
|
||||||
|
GridGeometry, HoverMarker, MarkerKind, cell_center, cell_rect, door_edges_for,
|
||||||
|
door_render_width, marker_rect, normalized_edge, window_render_width,
|
||||||
|
};
|
||||||
|
use crate::layout::{DungeonLayout, corridor_cells};
|
||||||
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
pub fn draw_layout(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
layout: &DungeonLayout,
|
||||||
|
colorblind_mode: bool,
|
||||||
|
hover_text_idx: Option<usize>,
|
||||||
|
hover_marker: Option<HoverMarker>,
|
||||||
|
hover_stair_idx: Option<usize>,
|
||||||
|
) {
|
||||||
|
let wall_width_px = (geometry.cell_size / 2.5).max(1.0);
|
||||||
|
let door_width_px = (geometry.cell_size / 5.0).max(1.0);
|
||||||
|
|
||||||
|
let room_fill = Color32::from_rgb(70, 120, 160);
|
||||||
|
let corridor_fill = Color32::from_rgb(210, 190, 120);
|
||||||
|
let wall_color = Color32::BLACK;
|
||||||
|
let corridor_cells_set = corridor_cells(layout, geometry.cols, geometry.rows);
|
||||||
|
let corridor_edges_set = corridor_edges_from_cells(&corridor_cells_set);
|
||||||
|
let mut room_cells_set = HashSet::new();
|
||||||
|
let room_edges_set = room_edges_from_rooms(&layout.rooms);
|
||||||
|
let mut door_edges_set = HashSet::new();
|
||||||
|
|
||||||
|
for door in &layout.doors {
|
||||||
|
for edge in door_edges_for(door, geometry.cols, geometry.rows) {
|
||||||
|
door_edges_set.insert(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &(col, row) in &corridor_cells_set {
|
||||||
|
painter.rect_filled(cell_rect(geometry, col, row), 0.0, corridor_fill);
|
||||||
|
if colorblind_mode {
|
||||||
|
painter.circle_filled(
|
||||||
|
cell_center(geometry, col, row),
|
||||||
|
geometry.cell_size * 0.14,
|
||||||
|
Color32::BLACK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &(col, row) in &corridor_cells_set {
|
||||||
|
let rect = cell_rect(geometry, col, row);
|
||||||
|
let right = (col + 1, row);
|
||||||
|
let bottom = (col, row + 1);
|
||||||
|
let left = col.checked_sub(1).map(|x| (x, row));
|
||||||
|
let top = row.checked_sub(1).map(|y| (col, y));
|
||||||
|
|
||||||
|
if col == 0
|
||||||
|
|| (!corridor_cells_set.contains(&(col - 1, row))
|
||||||
|
&& left
|
||||||
|
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.top()),
|
||||||
|
egui::pos2(rect.left(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!corridor_cells_set.contains(&right)
|
||||||
|
|| !corridor_edges_set.contains(&normalized_edge((col, row), right)))
|
||||||
|
&& !door_edges_set.contains(&normalized_edge((col, row), right))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.right(), rect.top()),
|
||||||
|
egui::pos2(rect.right(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if row == 0
|
||||||
|
|| (!corridor_cells_set.contains(&(col, row - 1))
|
||||||
|
&& top
|
||||||
|
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.top()),
|
||||||
|
egui::pos2(rect.right(), rect.top()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!corridor_cells_set.contains(&bottom)
|
||||||
|
|| !corridor_edges_set.contains(&normalized_edge((col, row), bottom)))
|
||||||
|
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.bottom()),
|
||||||
|
egui::pos2(rect.right(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for room in &layout.rooms {
|
||||||
|
for x in room.x..(room.x + room.width) {
|
||||||
|
for y in room.y..(room.y + room.height) {
|
||||||
|
room_cells_set.insert((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = geometry.rect.left() + room.x as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + room.y as f32 * geometry.cell_size;
|
||||||
|
let right = left + room.width as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + room.height as f32 * geometry.cell_size;
|
||||||
|
let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||||
|
|
||||||
|
painter.rect_filled(room_rect, 0.0, room_fill);
|
||||||
|
if colorblind_mode {
|
||||||
|
draw_room_crosshatch(painter, geometry, room, Stroke::new(1.5, Color32::BLACK));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &(col, row) in &room_cells_set {
|
||||||
|
let rect = cell_rect(geometry, col, row);
|
||||||
|
let right = (col + 1, row);
|
||||||
|
let bottom = (col, row + 1);
|
||||||
|
let left = col.checked_sub(1).map(|x| (x, row));
|
||||||
|
let top = row.checked_sub(1).map(|y| (col, y));
|
||||||
|
|
||||||
|
let left_edge = col == 0
|
||||||
|
|| (!room_cells_set.contains(&(col - 1, row))
|
||||||
|
|| left
|
||||||
|
.map(|n| !room_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true))
|
||||||
|
&& left
|
||||||
|
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true);
|
||||||
|
if left_edge {
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.top()),
|
||||||
|
egui::pos2(rect.left(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!room_cells_set.contains(&right)
|
||||||
|
|| !room_edges_set.contains(&normalized_edge((col, row), right)))
|
||||||
|
&& !door_edges_set.contains(&normalized_edge((col, row), right))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.right(), rect.top()),
|
||||||
|
egui::pos2(rect.right(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let top_edge = row == 0
|
||||||
|
|| (!room_cells_set.contains(&(col, row - 1))
|
||||||
|
|| top
|
||||||
|
.map(|n| !room_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true))
|
||||||
|
&& top
|
||||||
|
.map(|n| !door_edges_set.contains(&normalized_edge((col, row), n)))
|
||||||
|
.unwrap_or(true);
|
||||||
|
if top_edge {
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.top()),
|
||||||
|
egui::pos2(rect.right(), rect.top()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!room_cells_set.contains(&bottom)
|
||||||
|
|| !room_edges_set.contains(&normalized_edge((col, row), bottom)))
|
||||||
|
&& !door_edges_set.contains(&normalized_edge((col, row), bottom))
|
||||||
|
{
|
||||||
|
draw_wall_segment(
|
||||||
|
painter,
|
||||||
|
egui::pos2(rect.left(), rect.bottom()),
|
||||||
|
egui::pos2(rect.right(), rect.bottom()),
|
||||||
|
wall_width_px,
|
||||||
|
wall_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for door in &layout.doors {
|
||||||
|
let color = if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else if door.secret {
|
||||||
|
Color32::from_rgb(170, 80, 170)
|
||||||
|
} else if door.archway {
|
||||||
|
Color32::from_rgb(230, 140, 60)
|
||||||
|
} else if door.locked {
|
||||||
|
Color32::from_rgb(220, 70, 70)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(80, 200, 120)
|
||||||
|
};
|
||||||
|
let style = if colorblind_mode {
|
||||||
|
if door.secret {
|
||||||
|
DoorLineStyle::DashDotDot
|
||||||
|
} else if door.archway {
|
||||||
|
DoorLineStyle::Dotted
|
||||||
|
} else if door.locked {
|
||||||
|
DoorLineStyle::ShortDash
|
||||||
|
} else {
|
||||||
|
DoorLineStyle::LongDash
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DoorLineStyle::Solid
|
||||||
|
};
|
||||||
|
draw_door_line(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
door.from,
|
||||||
|
door.to,
|
||||||
|
door_render_width(door),
|
||||||
|
Stroke::new(door_width_px, color),
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for window in &layout.windows {
|
||||||
|
let style = if colorblind_mode {
|
||||||
|
DoorLineStyle::DashDot
|
||||||
|
} else {
|
||||||
|
DoorLineStyle::Solid
|
||||||
|
};
|
||||||
|
draw_window_line(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
window,
|
||||||
|
Stroke::new(door_width_px, Color32::from_rgb(70, 130, 220)),
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, label) in layout.text_labels.iter().enumerate() {
|
||||||
|
if hover_text_idx == Some(idx) {
|
||||||
|
painter.rect_stroke(
|
||||||
|
text_label_rect(geometry, label).expand(2.0),
|
||||||
|
0.0,
|
||||||
|
Stroke::new(2.0, Color32::from_rgb(255, 220, 120)),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let pos = cell_center(geometry, label.cell.0, label.cell.1);
|
||||||
|
let color = if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else {
|
||||||
|
Color32::WHITE
|
||||||
|
};
|
||||||
|
painter.text(
|
||||||
|
pos,
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
&label.text,
|
||||||
|
egui::FontId::proportional(label.font_size as f32),
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_area_marker_group(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
&layout.start_markers,
|
||||||
|
"S",
|
||||||
|
Color32::from_rgb(60, 220, 200),
|
||||||
|
colorblind_mode,
|
||||||
|
hover_marker,
|
||||||
|
MarkerKind::Start,
|
||||||
|
);
|
||||||
|
draw_area_marker_group(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
&layout.end_markers,
|
||||||
|
"E",
|
||||||
|
Color32::from_rgb(240, 90, 90),
|
||||||
|
colorblind_mode,
|
||||||
|
hover_marker,
|
||||||
|
MarkerKind::End,
|
||||||
|
);
|
||||||
|
draw_area_marker_group(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
&layout.trap_markers,
|
||||||
|
"T",
|
||||||
|
Color32::from_rgb(150, 80, 230),
|
||||||
|
colorblind_mode,
|
||||||
|
hover_marker,
|
||||||
|
MarkerKind::Trap,
|
||||||
|
);
|
||||||
|
draw_area_marker_group(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
&layout.monster_markers,
|
||||||
|
"M",
|
||||||
|
Color32::from_rgb(200, 50, 50),
|
||||||
|
colorblind_mode,
|
||||||
|
hover_marker,
|
||||||
|
MarkerKind::Monster,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (idx, stair) in layout.stairs.iter().enumerate() {
|
||||||
|
draw_staircase(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
stair,
|
||||||
|
colorblind_mode,
|
||||||
|
hover_stair_idx == Some(idx),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_staircase(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
stair: &crate::layout::Staircase,
|
||||||
|
colorblind_mode: bool,
|
||||||
|
is_hovered: bool,
|
||||||
|
) {
|
||||||
|
let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size;
|
||||||
|
let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size;
|
||||||
|
let right = left + stair.width as f32 * geometry.cell_size;
|
||||||
|
let bottom = top + stair.height as f32 * geometry.cell_size;
|
||||||
|
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
|
||||||
|
|
||||||
|
let fill = if is_hovered {
|
||||||
|
Color32::from_rgb(220, 180, 80).gamma_multiply(0.5)
|
||||||
|
} else if colorblind_mode {
|
||||||
|
Color32::from_rgb(180, 180, 180).gamma_multiply(0.4)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(200, 150, 50).gamma_multiply(0.35)
|
||||||
|
};
|
||||||
|
painter.rect_filled(rect, 4.0, fill);
|
||||||
|
|
||||||
|
let stroke_color = if is_hovered {
|
||||||
|
Color32::WHITE
|
||||||
|
} else if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(200, 150, 50)
|
||||||
|
};
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
Stroke::new(if is_hovered { 3.0 } else { 2.0 }, stroke_color),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
|
||||||
|
let steps = (stair.height as f32 * 2.0).round() as usize;
|
||||||
|
let step_height = (rect.height() / (steps as f32)).max(1.0);
|
||||||
|
let line_color = if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(150, 100, 30)
|
||||||
|
};
|
||||||
|
for i in 0..steps {
|
||||||
|
let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(rect.left() + 2.0, y),
|
||||||
|
egui::pos2(rect.right() - 2.0, y),
|
||||||
|
],
|
||||||
|
Stroke::new(1.5, line_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_label_rect(geometry: &GridGeometry, label: &crate::layout::TextLabel) -> egui::Rect {
|
||||||
|
let center = cell_center(geometry, label.cell.0, label.cell.1);
|
||||||
|
let font_size = label.font_size as f32;
|
||||||
|
let width =
|
||||||
|
(label.text.chars().count().max(1) as f32 * font_size * 0.6).max(geometry.cell_size * 0.5);
|
||||||
|
let height = font_size.max(geometry.cell_size * 0.35);
|
||||||
|
egui::Rect::from_center_size(center, egui::vec2(width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_area_marker(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
marker: &crate::layout::AreaMarker,
|
||||||
|
label: &str,
|
||||||
|
base_color: Color32,
|
||||||
|
colorblind_mode: bool,
|
||||||
|
hovered: bool,
|
||||||
|
) {
|
||||||
|
let rect = marker_rect(marker, geometry);
|
||||||
|
let fill = base_color.gamma_multiply(if colorblind_mode { 0.22 } else { 0.35 });
|
||||||
|
let stroke_color = if hovered {
|
||||||
|
Color32::from_rgb(255, 220, 120)
|
||||||
|
} else if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else {
|
||||||
|
base_color
|
||||||
|
};
|
||||||
|
let text_color = if colorblind_mode {
|
||||||
|
Color32::BLACK
|
||||||
|
} else {
|
||||||
|
Color32::WHITE
|
||||||
|
};
|
||||||
|
let font_size = (geometry.cell_size * marker.size as f32 * 0.5).clamp(14.0, 72.0);
|
||||||
|
|
||||||
|
painter.rect_filled(rect, 4.0, fill);
|
||||||
|
painter.rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
Stroke::new(if hovered { 3.0 } else { 2.0 }, stroke_color),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
painter.text(
|
||||||
|
rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
label,
|
||||||
|
egui::FontId::proportional(font_size),
|
||||||
|
text_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_area_marker_group(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
markers: &[crate::layout::AreaMarker],
|
||||||
|
label: &str,
|
||||||
|
base_color: Color32,
|
||||||
|
colorblind_mode: bool,
|
||||||
|
hover_marker: Option<HoverMarker>,
|
||||||
|
kind: MarkerKind,
|
||||||
|
) {
|
||||||
|
for (idx, marker) in markers.iter().enumerate() {
|
||||||
|
let hovered = match (kind, hover_marker) {
|
||||||
|
(MarkerKind::Start, Some(HoverMarker::Start(hover_idx))) => hover_idx == idx,
|
||||||
|
(MarkerKind::End, Some(HoverMarker::End(hover_idx))) => hover_idx == idx,
|
||||||
|
(MarkerKind::Trap, Some(HoverMarker::Trap(hover_idx))) => hover_idx == idx,
|
||||||
|
(MarkerKind::Monster, Some(HoverMarker::Monster(hover_idx))) => hover_idx == idx,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
draw_area_marker(
|
||||||
|
painter,
|
||||||
|
geometry,
|
||||||
|
marker,
|
||||||
|
label,
|
||||||
|
base_color,
|
||||||
|
colorblind_mode,
|
||||||
|
hovered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_wall_segment(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
width: f32,
|
||||||
|
color: Color32,
|
||||||
|
) {
|
||||||
|
let half = width * 0.5;
|
||||||
|
if (from.x - to.x).abs() <= f32::EPSILON {
|
||||||
|
let x = from.x.min(to.x);
|
||||||
|
let y0 = from.y.min(to.y);
|
||||||
|
let y1 = from.y.max(to.y);
|
||||||
|
let rect = egui::Rect::from_min_max(
|
||||||
|
egui::pos2(x - half, y0 - half),
|
||||||
|
egui::pos2(x + half, y1 + half),
|
||||||
|
);
|
||||||
|
painter.rect_filled(rect, 0.0, color);
|
||||||
|
} else {
|
||||||
|
let y = from.y.min(to.y);
|
||||||
|
let x0 = from.x.min(to.x);
|
||||||
|
let x1 = from.x.max(to.x);
|
||||||
|
let rect = egui::Rect::from_min_max(
|
||||||
|
egui::pos2(x0 - half, y - half),
|
||||||
|
egui::pos2(x1 + half, y + half),
|
||||||
|
);
|
||||||
|
painter.rect_filled(rect, 0.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn corridor_edges_from_cells(
|
||||||
|
corridor_cells: &HashSet<(usize, usize)>,
|
||||||
|
) -> HashSet<((usize, usize), (usize, usize))> {
|
||||||
|
let mut edges = HashSet::new();
|
||||||
|
for &(col, row) in corridor_cells {
|
||||||
|
let right = (col + 1, row);
|
||||||
|
let bottom = (col, row + 1);
|
||||||
|
if corridor_cells.contains(&right) {
|
||||||
|
edges.insert(normalized_edge((col, row), right));
|
||||||
|
}
|
||||||
|
if corridor_cells.contains(&bottom) {
|
||||||
|
edges.insert(normalized_edge((col, row), bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn room_edges_from_rooms(
|
||||||
|
rooms: &[crate::layout::Room],
|
||||||
|
) -> HashSet<((usize, usize), (usize, usize))> {
|
||||||
|
let mut edges = HashSet::new();
|
||||||
|
for room in rooms {
|
||||||
|
let x_end = room.x + room.width;
|
||||||
|
let y_end = room.y + room.height;
|
||||||
|
for x in room.x..x_end {
|
||||||
|
for y in room.y..y_end {
|
||||||
|
if x + 1 < x_end {
|
||||||
|
edges.insert(normalized_edge((x, y), (x + 1, y)));
|
||||||
|
}
|
||||||
|
if y + 1 < y_end {
|
||||||
|
edges.insert(normalized_edge((x, y), (x, y + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DoorLineStyle {
|
||||||
|
Solid,
|
||||||
|
LongDash,
|
||||||
|
ShortDash,
|
||||||
|
Dotted,
|
||||||
|
DashDot,
|
||||||
|
DashDotDot,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_door_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
a: (usize, usize),
|
||||||
|
b: (usize, usize),
|
||||||
|
width_cells: usize,
|
||||||
|
stroke: Stroke,
|
||||||
|
style: DoorLineStyle,
|
||||||
|
) {
|
||||||
|
if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width_cells = width_cells.max(1);
|
||||||
|
let min_offset = -((width_cells as isize - 1) / 2);
|
||||||
|
let max_offset = width_cells as isize / 2;
|
||||||
|
|
||||||
|
if a.0 != b.0 {
|
||||||
|
let x = geometry.rect.left() + (a.0.max(b.0) as f32) * geometry.cell_size;
|
||||||
|
let row = a.1 as isize;
|
||||||
|
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||||
|
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||||
|
} else {
|
||||||
|
let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size;
|
||||||
|
let col = a.0 as isize;
|
||||||
|
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||||
|
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_window_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
window: &crate::layout::Window,
|
||||||
|
stroke: Stroke,
|
||||||
|
style: DoorLineStyle,
|
||||||
|
) {
|
||||||
|
let width_cells = window_render_width(window).max(1) as isize;
|
||||||
|
let min_offset = -((width_cells - 1) / 2);
|
||||||
|
let max_offset = width_cells / 2;
|
||||||
|
|
||||||
|
match window.side {
|
||||||
|
crate::layout::WindowSide::Left => {
|
||||||
|
let x = geometry.rect.left() + window.cell.0 as f32 * geometry.cell_size;
|
||||||
|
let row = window.cell.1 as isize;
|
||||||
|
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||||
|
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||||
|
}
|
||||||
|
crate::layout::WindowSide::Right => {
|
||||||
|
let x = geometry.rect.left() + (window.cell.0 as f32 + 1.0) * geometry.cell_size;
|
||||||
|
let row = window.cell.1 as isize;
|
||||||
|
let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize);
|
||||||
|
let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size;
|
||||||
|
let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style);
|
||||||
|
}
|
||||||
|
crate::layout::WindowSide::Top => {
|
||||||
|
let y = geometry.rect.top() + window.cell.1 as f32 * geometry.cell_size;
|
||||||
|
let col = window.cell.0 as isize;
|
||||||
|
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||||
|
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||||
|
}
|
||||||
|
crate::layout::WindowSide::Bottom => {
|
||||||
|
let y = geometry.rect.top() + (window.cell.1 as f32 + 1.0) * geometry.cell_size;
|
||||||
|
let col = window.cell.0 as isize;
|
||||||
|
let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize);
|
||||||
|
let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size;
|
||||||
|
let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size;
|
||||||
|
draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_styled_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
style: DoorLineStyle,
|
||||||
|
) {
|
||||||
|
match style {
|
||||||
|
DoorLineStyle::Solid => {
|
||||||
|
painter.line_segment([from, to], stroke);
|
||||||
|
}
|
||||||
|
DoorLineStyle::LongDash => draw_dashed_line(painter, from, to, stroke, 14.0, 7.0),
|
||||||
|
DoorLineStyle::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0),
|
||||||
|
DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke),
|
||||||
|
DoorLineStyle::DashDot => draw_dash_dot_line(painter, from, to, stroke),
|
||||||
|
DoorLineStyle::DashDotDot => draw_dash_dot_dot_line(painter, from, to, stroke),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dashed_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
dash_len: f32,
|
||||||
|
gap_len: f32,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let seg_start = dist;
|
||||||
|
let seg_end = (dist + dash_len).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
||||||
|
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist += dash_len + gap_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let step = 5.0_f32;
|
||||||
|
let radius = (stroke.width * 0.35).max(1.0);
|
||||||
|
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist <= len {
|
||||||
|
let p = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(p, radius, stroke.color);
|
||||||
|
dist += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dash_dot_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let dash_end = (dist + 7.0).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist = dash_end + 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
||||||
|
dist += 4.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dash_dot_dot_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let radius = (stroke.width * 0.35).max(1.0);
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let dash_end = (dist + 7.0).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist = dash_end + 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot1, radius, stroke.color);
|
||||||
|
dist += 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot2, radius, stroke.color);
|
||||||
|
dist += 4.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_room_crosshatch(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
geometry: &GridGeometry,
|
||||||
|
room: &crate::layout::Room,
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
for x in room.x..(room.x + room.width) {
|
||||||
|
for y in room.y..(room.y + room.height) {
|
||||||
|
let rect = cell_rect(geometry, x, y).shrink(2.0);
|
||||||
|
painter.line_segment([rect.left_top(), rect.right_bottom()], stroke);
|
||||||
|
painter.line_segment([rect.right_top(), rect.left_bottom()], stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+436
@@ -0,0 +1,436 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod tabs;
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Tab {
|
||||||
|
Generate,
|
||||||
|
Layout,
|
||||||
|
StartAndEnd,
|
||||||
|
MonstersAndTraps,
|
||||||
|
Add,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tab {
|
||||||
|
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 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Png
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExportFormat {
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ExportFormat::Png => "png",
|
||||||
|
ExportFormat::Jpeg => "jpeg",
|
||||||
|
ExportFormat::Webp => "webp",
|
||||||
|
ExportFormat::Svg => "svg",
|
||||||
|
ExportFormat::Folder => "folder",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ExportFormat::Png => ".png",
|
||||||
|
ExportFormat::Jpeg => ".jpeg",
|
||||||
|
ExportFormat::Webp => ".webp",
|
||||||
|
ExportFormat::Svg => ".svg",
|
||||||
|
ExportFormat::Folder => "Folder (Composite)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Png
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaskFormat {
|
||||||
|
pub fn extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
MaskFormat::Png => "png",
|
||||||
|
MaskFormat::Jpeg => "jpeg",
|
||||||
|
MaskFormat::Webp => "webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
+8
-706
@@ -1,433 +1,10 @@
|
|||||||
|
use super::{AddTool, ExportFormat, MaskFormat, SidePanelResult, UiSettings};
|
||||||
use crate::seed;
|
use crate::seed;
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{Color32, RichText, Stroke};
|
use egui::RichText;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
enum Tab {
|
|
||||||
Generate,
|
|
||||||
Layout,
|
|
||||||
StartAndEnd,
|
|
||||||
MonstersAndTraps,
|
|
||||||
Add,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Tab {
|
|
||||||
// Start with the Generate tab selected.
|
|
||||||
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 {
|
|
||||||
// Default export format is PNG.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Png
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExportFormat {
|
|
||||||
// Return the file extension for this export format.
|
|
||||||
pub fn extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ExportFormat::Png => "png",
|
|
||||||
ExportFormat::Jpeg => "jpeg",
|
|
||||||
ExportFormat::Webp => "webp",
|
|
||||||
ExportFormat::Svg => "svg",
|
|
||||||
ExportFormat::Folder => "folder",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the UI label for this export format.
|
|
||||||
pub fn label(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ExportFormat::Png => ".png",
|
|
||||||
ExportFormat::Jpeg => ".jpeg",
|
|
||||||
ExportFormat::Webp => ".webp",
|
|
||||||
ExportFormat::Svg => ".svg",
|
|
||||||
ExportFormat::Folder => "Folder (Composite)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report whether this export format supports a raster resolution.
|
|
||||||
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 {
|
|
||||||
// Default mask format is PNG.
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Png
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MaskFormat {
|
|
||||||
// Return the file extension for this mask format.
|
|
||||||
pub fn extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
MaskFormat::Png => "png",
|
|
||||||
MaskFormat::Jpeg => "jpeg",
|
|
||||||
MaskFormat::Webp => "webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the UI label for this mask format.
|
|
||||||
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,
|
|
||||||
active_tab: Tab,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UiSettings {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_side_panel(
|
|
||||||
ctx: &egui::Context,
|
|
||||||
settings: &mut UiSettings,
|
|
||||||
export_progress: Option<(usize, usize)>,
|
|
||||||
num_levels: usize,
|
|
||||||
) -> SidePanelResult {
|
|
||||||
// Render the left side panel and return user interaction results.
|
|
||||||
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| {
|
|
||||||
let is_generate = settings.active_tab == Tab::Generate;
|
|
||||||
let is_layout = settings.active_tab == Tab::Layout;
|
|
||||||
|
|
||||||
if ui.selectable_label(is_generate, "Generate").clicked() {
|
|
||||||
settings.active_tab = Tab::Generate;
|
|
||||||
}
|
|
||||||
if ui.selectable_label(is_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 => {
|
|
||||||
draw_generate_tab(ui, settings, &mut result, export_progress, num_levels)
|
|
||||||
}
|
|
||||||
Tab::Layout => draw_layout_tab(ui, settings, &mut result),
|
|
||||||
Tab::StartAndEnd => draw_start_and_end_tab(ui, settings, &mut result),
|
|
||||||
Tab::MonstersAndTraps => draw_monsters_and_traps_tab(ui, settings, &mut result),
|
|
||||||
Tab::Add => draw_add_tab(ui, settings, &mut result),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw mode controls and the legend panel.
|
|
||||||
pub fn draw_legend_panel(ctx: &egui::Context, settings: &mut UiSettings) -> bool {
|
|
||||||
let mut composition_mode_changed = false;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
draw_legend_entry_colorblind(ui, "Rooms", LegendStyle::Crosshatch);
|
|
||||||
draw_legend_entry_colorblind(ui, "Corridors", LegendStyle::Dots);
|
|
||||||
draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine);
|
|
||||||
draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash);
|
|
||||||
draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
|
||||||
draw_legend_entry_colorblind(ui, "Secret Doors", LegendStyle::DashDotDotLine);
|
|
||||||
draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
|
||||||
draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
|
||||||
draw_legend_entry_colorblind(ui, "Stairs", LegendStyle::LabelChar('\u{1F5CF}'));
|
|
||||||
draw_legend_entry_colorblind(ui, "Start Marker", LegendStyle::Label("S"));
|
|
||||||
draw_legend_entry_colorblind(ui, "End Marker", LegendStyle::Label("E"));
|
|
||||||
draw_legend_entry_colorblind(ui, "Trap", LegendStyle::Label("T"));
|
|
||||||
draw_legend_entry_colorblind(ui, "Monster", LegendStyle::Label("M"));
|
|
||||||
} else {
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(210, 190, 120), "Corridors");
|
|
||||||
draw_legend_entry(ui, Color32::BLACK, "Walls");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(200, 150, 50), "Stairs");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(60, 220, 200), "Start Marker");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(240, 90, 90), "End Marker");
|
|
||||||
draw_legend_entry(ui, Color32::from_rgb(150, 80, 230), "Trap");
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw level tabs and return the hovered tab index.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the Generate tab controls.
|
// Render the Generate tab controls.
|
||||||
fn draw_generate_tab(
|
pub fn draw_generate_tab(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
settings: &mut UiSettings,
|
settings: &mut UiSettings,
|
||||||
result: &mut SidePanelResult,
|
result: &mut SidePanelResult,
|
||||||
@@ -649,7 +226,7 @@ fn draw_generate_tab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the Layout tab controls.
|
// Render the Layout tab controls.
|
||||||
fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
pub fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||||
ui.label(RichText::new("Levels").strong());
|
ui.label(RichText::new("Levels").strong());
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
@@ -1075,7 +652,7 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the Add tab controls.
|
// Render the Add tab controls.
|
||||||
fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
pub fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
|
||||||
ui.label(RichText::new("Add Tools").strong());
|
ui.label(RichText::new("Add Tools").strong());
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
@@ -1203,282 +780,7 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SideP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a colored legend entry.
|
pub fn draw_start_and_end_tab(
|
||||||
fn draw_legend_entry(ui: &mut egui::Ui, color: Color32, label: &str) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
|
||||||
ui.painter().rect_filled(rect, 0.0, color);
|
|
||||||
ui.painter().rect_stroke(
|
|
||||||
rect,
|
|
||||||
0.0,
|
|
||||||
Stroke::new(1.0, Color32::WHITE),
|
|
||||||
egui::StrokeKind::Middle,
|
|
||||||
);
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.label(label);
|
|
||||||
});
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum LegendStyle {
|
|
||||||
Crosshatch,
|
|
||||||
Dots,
|
|
||||||
SolidLine,
|
|
||||||
LongDash,
|
|
||||||
ShortDash,
|
|
||||||
DottedLine,
|
|
||||||
DashDotLine,
|
|
||||||
DashDotDotLine,
|
|
||||||
Label(&'static str),
|
|
||||||
LabelChar(char),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a patterned legend entry.
|
|
||||||
fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendStyle) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
|
||||||
ui.painter().rect_filled(rect, 0.0, Color32::from_gray(220));
|
|
||||||
ui.painter().rect_stroke(
|
|
||||||
rect,
|
|
||||||
0.0,
|
|
||||||
Stroke::new(1.0, Color32::BLACK),
|
|
||||||
egui::StrokeKind::Middle,
|
|
||||||
);
|
|
||||||
|
|
||||||
match style {
|
|
||||||
LegendStyle::Crosshatch => {
|
|
||||||
ui.painter().line_segment(
|
|
||||||
[rect.left_top(), rect.right_bottom()],
|
|
||||||
Stroke::new(1.2, Color32::BLACK),
|
|
||||||
);
|
|
||||||
ui.painter().line_segment(
|
|
||||||
[rect.right_top(), rect.left_bottom()],
|
|
||||||
Stroke::new(1.2, Color32::BLACK),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LegendStyle::Dots => {
|
|
||||||
ui.painter()
|
|
||||||
.circle_filled(rect.center(), 2.0, Color32::BLACK);
|
|
||||||
ui.painter().circle_filled(
|
|
||||||
egui::pos2(rect.center().x - 4.0, rect.center().y),
|
|
||||||
1.4,
|
|
||||||
Color32::BLACK,
|
|
||||||
);
|
|
||||||
ui.painter().circle_filled(
|
|
||||||
egui::pos2(rect.center().x + 4.0, rect.center().y),
|
|
||||||
1.4,
|
|
||||||
Color32::BLACK,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LegendStyle::SolidLine => {
|
|
||||||
ui.painter().line_segment(
|
|
||||||
[
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
],
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LegendStyle::Label(prefix) => {
|
|
||||||
ui.painter()
|
|
||||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
|
||||||
ui.painter()
|
|
||||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
|
||||||
let font_size = 10.0;
|
|
||||||
let font_id = egui::FontId::monospace(font_size);
|
|
||||||
let text_width = prefix.len() as f32 * font_size * 0.6;
|
|
||||||
let text_height = font_size * 0.8;
|
|
||||||
let text_pos = egui::pos2(
|
|
||||||
rect.center().x - text_width / 2.0,
|
|
||||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
|
||||||
);
|
|
||||||
ui.painter().text(
|
|
||||||
text_pos,
|
|
||||||
egui::Align2::LEFT_TOP,
|
|
||||||
prefix,
|
|
||||||
font_id,
|
|
||||||
Color32::BLACK,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LegendStyle::LabelChar(character) => {
|
|
||||||
ui.painter()
|
|
||||||
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
|
||||||
ui.painter()
|
|
||||||
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
|
||||||
let font_size = 10.0;
|
|
||||||
let font_id = egui::FontId::monospace(font_size);
|
|
||||||
let text_width = font_size * 0.6;
|
|
||||||
let text_height = font_size * 0.8;
|
|
||||||
let text_pos = egui::pos2(
|
|
||||||
rect.center().x - text_width / 2.0,
|
|
||||||
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
|
||||||
);
|
|
||||||
ui.painter().text(
|
|
||||||
text_pos,
|
|
||||||
egui::Align2::LEFT_TOP,
|
|
||||||
character,
|
|
||||||
font_id,
|
|
||||||
Color32::BLACK,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LegendStyle::LongDash => draw_dashed_line(
|
|
||||||
ui.painter(),
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
6.0,
|
|
||||||
3.0,
|
|
||||||
),
|
|
||||||
LegendStyle::ShortDash => draw_dashed_line(
|
|
||||||
ui.painter(),
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
3.0,
|
|
||||||
2.0,
|
|
||||||
),
|
|
||||||
LegendStyle::DottedLine => draw_dotted_line(
|
|
||||||
ui.painter(),
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
),
|
|
||||||
LegendStyle::DashDotLine => draw_dash_dot_line(
|
|
||||||
ui.painter(),
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
),
|
|
||||||
LegendStyle::DashDotDotLine => draw_dash_dot_dot_line(
|
|
||||||
ui.painter(),
|
|
||||||
egui::pos2(rect.left() + 1.0, rect.center().y),
|
|
||||||
egui::pos2(rect.right() - 1.0, rect.center().y),
|
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(6.0);
|
|
||||||
ui.label(label);
|
|
||||||
});
|
|
||||||
ui.add_space(4.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a dashed line.
|
|
||||||
fn draw_dashed_line(
|
|
||||||
painter: &egui::Painter,
|
|
||||||
from: egui::Pos2,
|
|
||||||
to: egui::Pos2,
|
|
||||||
stroke: Stroke,
|
|
||||||
dash_len: f32,
|
|
||||||
gap_len: f32,
|
|
||||||
) {
|
|
||||||
let dx = to.x - from.x;
|
|
||||||
let dy = to.y - from.y;
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len <= 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ux = dx / len;
|
|
||||||
let uy = dy / len;
|
|
||||||
|
|
||||||
let mut dist = 0.0_f32;
|
|
||||||
while dist < len {
|
|
||||||
let seg_start = dist;
|
|
||||||
let seg_end = (dist + dash_len).min(len);
|
|
||||||
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
|
||||||
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
|
||||||
painter.line_segment([p0, p1], stroke);
|
|
||||||
dist += dash_len + gap_len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a dotted line.
|
|
||||||
fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
|
||||||
let dx = to.x - from.x;
|
|
||||||
let dy = to.y - from.y;
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len <= 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ux = dx / len;
|
|
||||||
let uy = dy / len;
|
|
||||||
let step = 5.0_f32;
|
|
||||||
let radius = (stroke.width * 0.35).max(1.0);
|
|
||||||
|
|
||||||
let mut dist = 0.0_f32;
|
|
||||||
while dist <= len {
|
|
||||||
let point = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
painter.circle_filled(point, radius, stroke.color);
|
|
||||||
dist += step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_dash_dot_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
|
||||||
let dx = to.x - from.x;
|
|
||||||
let dy = to.y - from.y;
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len <= 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ux = dx / len;
|
|
||||||
let uy = dy / len;
|
|
||||||
let mut dist = 0.0_f32;
|
|
||||||
while dist < len {
|
|
||||||
let dash_end = (dist + 7.0).min(len);
|
|
||||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
|
||||||
painter.line_segment([p0, p1], stroke);
|
|
||||||
dist = dash_end + 3.0;
|
|
||||||
if dist >= len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
|
||||||
dist += 4.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_dash_dot_dot_line(
|
|
||||||
painter: &egui::Painter,
|
|
||||||
from: egui::Pos2,
|
|
||||||
to: egui::Pos2,
|
|
||||||
stroke: Stroke,
|
|
||||||
) {
|
|
||||||
let dx = to.x - from.x;
|
|
||||||
let dy = to.y - from.y;
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len <= 0.0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ux = dx / len;
|
|
||||||
let uy = dy / len;
|
|
||||||
let radius = (stroke.width * 0.35).max(1.0);
|
|
||||||
let mut dist = 0.0_f32;
|
|
||||||
while dist < len {
|
|
||||||
let dash_end = (dist + 7.0).min(len);
|
|
||||||
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
|
||||||
painter.line_segment([p0, p1], stroke);
|
|
||||||
dist = dash_end + 3.0;
|
|
||||||
if dist >= len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
painter.circle_filled(dot1, radius, stroke.color);
|
|
||||||
dist += 3.0;
|
|
||||||
if dist >= len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
|
||||||
painter.circle_filled(dot2, radius, stroke.color);
|
|
||||||
dist += 4.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_start_and_end_tab(
|
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
settings: &mut UiSettings,
|
settings: &mut UiSettings,
|
||||||
result: &mut SidePanelResult,
|
result: &mut SidePanelResult,
|
||||||
@@ -1560,7 +862,7 @@ fn draw_start_and_end_tab(
|
|||||||
ui.label("Markers are generated inside rooms. Matching start/end indices are paired as far apart as possible.");
|
ui.label("Markers are generated inside rooms. Matching start/end indices are paired as far apart as possible.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_monsters_and_traps_tab(
|
pub fn draw_monsters_and_traps_tab(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
settings: &mut UiSettings,
|
settings: &mut UiSettings,
|
||||||
result: &mut SidePanelResult,
|
result: &mut SidePanelResult,
|
||||||
@@ -1623,7 +925,7 @@ fn draw_monsters_and_traps_tab(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_usize_slider_row(
|
pub fn draw_usize_slider_row(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
result: &mut SidePanelResult,
|
result: &mut SidePanelResult,
|
||||||
label: &str,
|
label: &str,
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use egui::{Color32, Stroke};
|
||||||
|
|
||||||
|
// Draw a colored legend entry.
|
||||||
|
pub fn draw_legend_entry(ui: &mut egui::Ui, color: Color32, label: &str) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||||
|
ui.painter().rect_filled(rect, 0.0, color);
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(1.0, Color32::WHITE),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label(label);
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum LegendStyle {
|
||||||
|
Crosshatch,
|
||||||
|
Dots,
|
||||||
|
SolidLine,
|
||||||
|
LongDash,
|
||||||
|
ShortDash,
|
||||||
|
DottedLine,
|
||||||
|
DashDotLine,
|
||||||
|
DashDotDotLine,
|
||||||
|
Label(&'static str),
|
||||||
|
LabelChar(char),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a patterned legend entry.
|
||||||
|
pub fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendStyle) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let (rect, _resp) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||||
|
ui.painter().rect_filled(rect, 0.0, Color32::from_gray(220));
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
0.0,
|
||||||
|
Stroke::new(1.0, Color32::BLACK),
|
||||||
|
egui::StrokeKind::Middle,
|
||||||
|
);
|
||||||
|
|
||||||
|
match style {
|
||||||
|
LegendStyle::Crosshatch => {
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[rect.left_top(), rect.right_bottom()],
|
||||||
|
Stroke::new(1.2, Color32::BLACK),
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[rect.right_top(), rect.left_bottom()],
|
||||||
|
Stroke::new(1.2, Color32::BLACK),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LegendStyle::Dots => {
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(rect.center(), 2.0, Color32::BLACK);
|
||||||
|
ui.painter().circle_filled(
|
||||||
|
egui::pos2(rect.center().x - 4.0, rect.center().y),
|
||||||
|
1.4,
|
||||||
|
Color32::BLACK,
|
||||||
|
);
|
||||||
|
ui.painter().circle_filled(
|
||||||
|
egui::pos2(rect.center().x + 4.0, rect.center().y),
|
||||||
|
1.4,
|
||||||
|
Color32::BLACK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LegendStyle::SolidLine => {
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
],
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LegendStyle::Label(prefix) => {
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||||
|
ui.painter()
|
||||||
|
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||||
|
let font_size = 10.0;
|
||||||
|
let font_id = egui::FontId::monospace(font_size);
|
||||||
|
let text_width = prefix.len() as f32 * font_size * 0.6;
|
||||||
|
let text_height = font_size * 0.8;
|
||||||
|
let text_pos = egui::pos2(
|
||||||
|
rect.center().x - text_width / 2.0,
|
||||||
|
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
text_pos,
|
||||||
|
egui::Align2::LEFT_TOP,
|
||||||
|
prefix,
|
||||||
|
font_id,
|
||||||
|
Color32::BLACK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LegendStyle::LabelChar(character) => {
|
||||||
|
ui.painter()
|
||||||
|
.circle_filled(rect.center(), 6.0, Color32::from_gray(180));
|
||||||
|
ui.painter()
|
||||||
|
.circle_stroke(rect.center(), 6.0, Stroke::new(1.0, Color32::BLACK));
|
||||||
|
let font_size = 10.0;
|
||||||
|
let font_id = egui::FontId::monospace(font_size);
|
||||||
|
let text_width = font_size * 0.6;
|
||||||
|
let text_height = font_size * 0.8;
|
||||||
|
let text_pos = egui::pos2(
|
||||||
|
rect.center().x - text_width / 2.0,
|
||||||
|
rect.center().y - text_height / 2.0 + font_size / 5.0,
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
text_pos,
|
||||||
|
egui::Align2::LEFT_TOP,
|
||||||
|
character,
|
||||||
|
font_id,
|
||||||
|
Color32::BLACK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LegendStyle::LongDash => draw_dashed_line(
|
||||||
|
ui.painter(),
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
6.0,
|
||||||
|
3.0,
|
||||||
|
),
|
||||||
|
LegendStyle::ShortDash => draw_dashed_line(
|
||||||
|
ui.painter(),
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
3.0,
|
||||||
|
2.0,
|
||||||
|
),
|
||||||
|
LegendStyle::DottedLine => draw_dotted_line(
|
||||||
|
ui.painter(),
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
),
|
||||||
|
LegendStyle::DashDotLine => draw_dash_dot_line(
|
||||||
|
ui.painter(),
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
),
|
||||||
|
LegendStyle::DashDotDotLine => draw_dash_dot_dot_line(
|
||||||
|
ui.painter(),
|
||||||
|
egui::pos2(rect.left() + 1.0, rect.center().y),
|
||||||
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
|
Stroke::new(2.0, Color32::BLACK),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(6.0);
|
||||||
|
ui.label(label);
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a dashed line.
|
||||||
|
pub fn draw_dashed_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
dash_len: f32,
|
||||||
|
gap_len: f32,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let seg_start = dist;
|
||||||
|
let seg_end = (dist + dash_len).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * seg_start, from.y + uy * seg_start);
|
||||||
|
let p1 = egui::pos2(from.x + ux * seg_end, from.y + uy * seg_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist += dash_len + gap_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a dotted line.
|
||||||
|
pub fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, stroke: Stroke) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let step = 5.0_f32;
|
||||||
|
let radius = (stroke.width * 0.35).max(1.0);
|
||||||
|
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist <= len {
|
||||||
|
let point = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(point, radius, stroke.color);
|
||||||
|
dist += step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dash_dot_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let dash_end = (dist + 7.0).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist = dash_end + 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot, (stroke.width * 0.35).max(1.0), stroke.color);
|
||||||
|
dist += 4.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_dash_dot_dot_line(
|
||||||
|
painter: &egui::Painter,
|
||||||
|
from: egui::Pos2,
|
||||||
|
to: egui::Pos2,
|
||||||
|
stroke: Stroke,
|
||||||
|
) {
|
||||||
|
let dx = to.x - from.x;
|
||||||
|
let dy = to.y - from.y;
|
||||||
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
|
if len <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ux = dx / len;
|
||||||
|
let uy = dy / len;
|
||||||
|
let radius = (stroke.width * 0.35).max(1.0);
|
||||||
|
let mut dist = 0.0_f32;
|
||||||
|
while dist < len {
|
||||||
|
let dash_end = (dist + 7.0).min(len);
|
||||||
|
let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end);
|
||||||
|
painter.line_segment([p0, p1], stroke);
|
||||||
|
dist = dash_end + 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot1, radius, stroke.color);
|
||||||
|
dist += 3.0;
|
||||||
|
if dist >= len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist);
|
||||||
|
painter.circle_filled(dot2, radius, stroke.color);
|
||||||
|
dist += 4.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user