From bdd1e931c331f44ced0917d3ec34f3247fa90275 Mon Sep 17 00:00:00 2001 From: grimsace Date: Mon, 23 Mar 2026 12:22:03 -0500 Subject: [PATCH] added window options --- src/exporter.rs | 109 ++++++++++++++++++- src/layout.rs | 280 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 141 +++++++++++++++++++++++- src/ui.rs | 98 +++++++++++++++++ 4 files changed, 623 insertions(+), 5 deletions(-) diff --git a/src/exporter.rs b/src/exporter.rs index 5b2895d..c2ace94 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -6,7 +6,7 @@ use image::{DynamicImage, ImageFormat, RgbaImage, imageops::FilterType}; use rfd::FileDialog; use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform}; -use crate::layout::{Door, DungeonLayout, Room, corridor_cells}; +use crate::layout::{Door, DungeonLayout, Room, Window, WindowSide, corridor_cells}; use crate::ui::{ExportFormat, MaskFormat, UiSettings}; const BG: (u8, u8, u8, u8) = (24, 24, 26, 255); @@ -16,7 +16,8 @@ const CORRIDOR: (u8, u8, u8, u8) = (210, 190, 120, 255); const BLACK: (u8, u8, u8, u8) = (0, 0, 0, 255); const OPEN_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255); const LOCKED_DOOR: (u8, u8, u8, u8) = (220, 70, 70, 255); -const ARCHWAY: (u8, u8, u8, u8) = (70, 130, 220, 255); +const ARCHWAY: (u8, u8, u8, u8) = (230, 140, 60, 255); +const WINDOW: (u8, u8, u8, u8) = (70, 130, 220, 255); const WHITE: (u8, u8, u8, u8) = (255, 255, 255, 255); pub enum ExportTarget { @@ -30,6 +31,7 @@ enum DoorStyle { LongDash, ShortDash, Dotted, + DashDot, } #[derive(Debug, Clone, Copy)] @@ -257,6 +259,15 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result String { } } + for window in &layout.windows { + let dash = settings.colorblind_mode.then_some("7,3,1,4"); + if let Some((x1, y1, x2, y2)) = window_line_points(&g, window) { + if let Some(pattern) = dash { + let _ = writeln!( + s, + "", + WINDOW.0, WINDOW.1, WINDOW.2 + ); + } else { + let _ = writeln!( + s, + "", + WINDOW.0, WINDOW.1, WINDOW.2 + ); + } + } + } + s.push_str("\n"); s } @@ -970,6 +1007,20 @@ fn draw_door( draw_line(pixmap, x1, y1, x2, y2, width, color, style); } +fn draw_window( + pixmap: &mut Pixmap, + g: &ExportGeometry, + window: &Window, + width: f32, + color: (u8, u8, u8, u8), + style: DoorStyle, +) { + let Some((x1, y1, x2, y2)) = window_line_points(g, window) else { + return; + }; + draw_line(pixmap, x1, y1, x2, y2, width, color, style); +} + // Compute the door line endpoints in export space. fn door_line_points( g: &ExportGeometry, @@ -1004,6 +1055,51 @@ fn door_line_points( } } +fn window_line_points(g: &ExportGeometry, window: &Window) -> Option<(f32, f32, f32, f32)> { + let width_cells = window_render_width(window).max(1) as isize; + let min_offset = -((width_cells - 1) / 2); + let max_offset = width_cells / 2; + + match window.side { + WindowSide::Left => { + let x = g.left() + window.cell.0 as f32 * g.cell; + let row = window.cell.1 as isize; + let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y0 = g.top() + y0_cell as f32 * g.cell; + let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell; + Some((x, y0, x, y1)) + } + WindowSide::Right => { + let x = g.left() + (window.cell.0 as f32 + 1.0) * g.cell; + let row = window.cell.1 as isize; + let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y0 = g.top() + y0_cell as f32 * g.cell; + let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell; + Some((x, y0, x, y1)) + } + WindowSide::Top => { + let y = g.top() + window.cell.1 as f32 * g.cell; + let col = window.cell.0 as isize; + let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x0 = g.left() + x0_cell as f32 * g.cell; + let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell; + Some((x0, y, x1, y)) + } + WindowSide::Bottom => { + let y = g.top() + (window.cell.1 as f32 + 1.0) * g.cell; + let col = window.cell.0 as isize; + let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x0 = g.left() + x0_cell as f32 * g.cell; + let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell; + Some((x0, y, x1, y)) + } + } +} + // Draw a styled line in the pixmap. fn draw_line( pixmap: &mut Pixmap, @@ -1020,6 +1116,7 @@ fn draw_line( DoorStyle::LongDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((14.0, 7.0))), DoorStyle::ShortDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((6.0, 4.0))), DoorStyle::Dotted => dotted_line(pixmap, x1, y1, x2, y2, width, color), + DoorStyle::DashDot => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((7.0, 3.0))), } } @@ -1170,3 +1267,11 @@ fn door_render_width(door: &Door) -> usize { 1 } } + +fn window_render_width(window: &Window) -> usize { + if window.span_width { + window.width.max(1) + } else { + 1 + } +} diff --git a/src/layout.rs b/src/layout.rs index 8867391..894fe3a 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -37,6 +37,22 @@ pub struct Door { pub archway: bool, } +#[derive(Debug, Clone)] +pub struct Window { + pub cell: (usize, usize), + pub side: WindowSide, + pub width: usize, + pub span_width: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowSide { + Left, + Right, + Top, + Bottom, +} + #[derive(Debug, Clone, Copy)] pub struct DoorSettings { pub frequency_percent: usize, @@ -45,11 +61,22 @@ pub struct DoorSettings { pub allow_middle_corridor_doors: bool, } +#[derive(Debug, Clone, Copy)] +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)] pub struct DungeonLayout { pub rooms: Vec, pub corridors: Vec, pub doors: Vec, + pub windows: Vec, pub packed_rooms: bool, } @@ -59,6 +86,7 @@ impl Default for DungeonLayout { rooms: Vec::new(), corridors: Vec::new(), doors: Vec::new(), + windows: Vec::new(), packed_rooms: false, } } @@ -99,6 +127,7 @@ pub fn generate_layout( dead_end_room_percent: usize, pack_rooms_without_corridors: bool, door_settings: DoorSettings, + window_settings: WindowSettings, ) -> DungeonLayout { let layout_salt = ((cols as u64) << 48) ^ ((rows as u64) << 32) @@ -123,6 +152,7 @@ pub fn generate_layout( rooms, corridors, doors: Vec::new(), + windows: Vec::new(), packed_rooms: pack_rooms_without_corridors, }; } @@ -144,6 +174,7 @@ pub fn generate_layout( rooms, corridors, doors: Vec::new(), + windows: Vec::new(), packed_rooms: pack_rooms_without_corridors, }; } @@ -170,9 +201,11 @@ pub fn generate_layout( rooms, corridors, doors: Vec::new(), + windows: Vec::new(), packed_rooms: true, }; apply_doors(&mut layout, seed, door_settings, cols, rows); + apply_windows(&mut layout, seed, window_settings, cols, rows); return layout; } @@ -224,6 +257,7 @@ pub fn generate_layout( rooms, corridors, doors: Vec::new(), + windows: Vec::new(), packed_rooms: false, }; } @@ -276,9 +310,11 @@ pub fn generate_layout( rooms, corridors, doors: Vec::new(), + windows: Vec::new(), packed_rooms: false, }; apply_doors(&mut layout, seed, door_settings, cols, rows); + apply_windows(&mut layout, seed, window_settings, cols, rows); layout } @@ -428,6 +464,241 @@ fn apply_packed_room_doors(layout: &mut DungeonLayout, seed: u64, settings: Door } } +pub fn apply_windows( + layout: &mut DungeonLayout, + seed: u64, + settings: WindowSettings, + cols: usize, + rows: usize, +) { + layout.windows.clear(); + + if !settings.enabled || cols == 0 || rows == 0 { + return; + } + + let mut min_width = settings.min_width.clamp(1, 5); + let max_width = settings.max_width.clamp(min_width, 5); + min_width = min_width.min(max_width); + + let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xA117_0055_u64)); + let base = (settings.frequency_percent.min(100) as f32) / 100.0; + let internal_ratio = (settings.room_hallway_percent.min(100) as f32) / 100.0; + + for segment in collect_window_segments(layout, cols, rows, settings.allow_internal_windows) { + let chance = if segment.internal { + if !settings.allow_internal_windows { + continue; + } + base * internal_ratio + } else { + base * (1.0 - internal_ratio) + }; + + if chance <= 0.0 || rng.next_f32() > chance { + continue; + } + + let max_segment_width = max_width.min(segment.cells.len()).max(1); + let width = rng.range_inclusive(min_width.min(max_segment_width), max_segment_width); + let cell = select_segment_cell(&segment.cells, width, &mut rng); + layout.windows.push(Window { + cell, + side: segment.side, + width, + span_width: width > 1, + }); + } +} + +#[derive(Debug, Clone)] +struct WindowSegment { + cells: Vec<(usize, usize)>, + side: WindowSide, + internal: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WindowTarget { + Exterior, + Corridor, + Room(usize), +} + +fn collect_window_segments( + layout: &DungeonLayout, + cols: usize, + rows: usize, + allow_internal_windows: bool, +) -> Vec { + let corridor_cells = corridor_cells(layout, cols, rows); + let mut segments = Vec::new(); + + for (room_idx, room) in layout.rooms.iter().enumerate() { + collect_room_side_segments( + room_idx, + room, + layout, + &corridor_cells, + cols, + rows, + allow_internal_windows, + WindowSide::Left, + &mut segments, + ); + collect_room_side_segments( + room_idx, + room, + layout, + &corridor_cells, + cols, + rows, + allow_internal_windows, + WindowSide::Right, + &mut segments, + ); + collect_room_side_segments( + room_idx, + room, + layout, + &corridor_cells, + cols, + rows, + allow_internal_windows, + WindowSide::Top, + &mut segments, + ); + collect_room_side_segments( + room_idx, + room, + layout, + &corridor_cells, + cols, + rows, + allow_internal_windows, + WindowSide::Bottom, + &mut segments, + ); + } + + segments +} + +fn collect_room_side_segments( + room_idx: usize, + room: &Room, + layout: &DungeonLayout, + corridor_cells: &HashSet<(usize, usize)>, + cols: usize, + rows: usize, + allow_internal_windows: bool, + side: WindowSide, + segments: &mut Vec, +) { + let cells: Vec<(usize, usize)> = match side { + WindowSide::Left => (room.y..(room.y + room.height)) + .map(|y| (room.x, y)) + .collect(), + WindowSide::Right => (room.y..(room.y + room.height)) + .map(|y| (room.x + room.width - 1, y)) + .collect(), + WindowSide::Top => (room.x..(room.x + room.width)) + .map(|x| (x, room.y)) + .collect(), + WindowSide::Bottom => (room.x..(room.x + room.width)) + .map(|x| (x, room.y + room.height - 1)) + .collect(), + }; + + let mut current_target = None; + let mut current_cells = Vec::new(); + + for cell in cells { + let Some(neighbor) = outward_neighbor(cell, side, cols, rows) else { + push_window_segment(current_target, side, &mut current_cells, segments); + current_target = Some(WindowTarget::Exterior); + current_cells.push(cell); + continue; + }; + + let target = if let Some(other_room_idx) = room_index_at_cell(&layout.rooms, neighbor) { + if other_room_idx == room_idx || !allow_internal_windows || other_room_idx < room_idx { + None + } else { + Some(WindowTarget::Room(other_room_idx)) + } + } else if corridor_cells.contains(&neighbor) { + allow_internal_windows.then_some(WindowTarget::Corridor) + } else { + Some(WindowTarget::Exterior) + }; + + if current_target != target { + push_window_segment(current_target, side, &mut current_cells, segments); + current_target = target; + } + + if target.is_some() { + current_cells.push(cell); + } + } + + push_window_segment(current_target, side, &mut current_cells, segments); +} + +fn push_window_segment( + target: Option, + side: WindowSide, + cells: &mut Vec<(usize, usize)>, + segments: &mut Vec, +) { + let Some(target) = target else { + cells.clear(); + return; + }; + if cells.is_empty() { + return; + } + + segments.push(WindowSegment { + cells: std::mem::take(cells), + side, + internal: !matches!(target, WindowTarget::Exterior), + }); +} + +fn outward_neighbor( + cell: (usize, usize), + side: WindowSide, + cols: usize, + rows: usize, +) -> Option<(usize, usize)> { + match side { + WindowSide::Left => cell.0.checked_sub(1).map(|x| (x, cell.1)), + WindowSide::Right => (cell.0 + 1 < cols).then_some((cell.0 + 1, cell.1)), + WindowSide::Top => cell.1.checked_sub(1).map(|y| (cell.0, y)), + WindowSide::Bottom => (cell.1 + 1 < rows).then_some((cell.0, cell.1 + 1)), + } +} + +fn select_segment_cell( + cells: &[(usize, usize)], + width: usize, + rng: &mut SimpleRng, +) -> (usize, usize) { + let width = width.max(1); + let min_offset = -((width as isize - 1) / 2); + let max_offset = width as isize / 2; + let low = (-min_offset) as usize; + let high = cells.len().saturating_sub(1 + max_offset.max(0) as usize); + let idx = if low <= high { + rng.range_inclusive(low, high) + } else { + cells.len() / 2 + }; + cells[idx] +} + // Find the edge where a corridor path exits a room. fn room_exit_edge( path: &[(usize, usize)], @@ -535,6 +806,15 @@ fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usi edges } +fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option { + 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(); diff --git a/src/main.rs b/src/main.rs index f613121..7d9b50b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,8 @@ use std::thread; use eframe::egui; use egui::{Color32, Stroke}; use layout::{ - DoorSettings, DungeonLayout, blocked_room_cells, corridor_cells, shortest_path_cells, + DoorSettings, DungeonLayout, WindowSettings, blocked_room_cells, corridor_cells, + shortest_path_cells, }; use ui::{AddTool, UiSettings, draw_side_panel}; @@ -57,6 +58,7 @@ impl Default for DungeonApp { settings.dead_end_rooms_percent, settings.pack_rooms_without_corridors, door_settings_from_ui(&settings), + window_settings_from_ui(&settings), ); Self { settings, @@ -96,6 +98,9 @@ impl eframe::App for DungeonApp { if self.settings.min_corridor_width > self.settings.max_corridor_width { self.settings.max_corridor_width = self.settings.min_corridor_width; } + if self.settings.min_window_width > self.settings.max_window_width { + self.settings.max_window_width = self.settings.min_window_width; + } if panel_result.clear_clicked { self.clear_layout(); @@ -160,13 +165,15 @@ impl eframe::App for DungeonApp { draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash); draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash); draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine); + draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine); } 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(70, 130, 220), "Archways"); + draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways"); + draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows"); } ui.add_space(10.0); ui.separator(); @@ -242,6 +249,7 @@ impl DungeonApp { self.settings.dead_end_rooms_percent, self.settings.pack_rooms_without_corridors, door_settings_from_ui(&self.settings), + window_settings_from_ui(&self.settings), ); } @@ -447,6 +455,13 @@ impl DungeonApp { self.settings.cols, self.settings.rows, ); + layout::apply_windows( + &mut self.layout, + self.settings.seed, + window_settings_from_ui(&self.settings), + self.settings.cols, + self.settings.rows, + ); } // Handle add-tool interactions for rooms, corridors, and doors. @@ -1371,7 +1386,7 @@ fn draw_layout( let color = if colorblind_mode { Color32::BLACK } else if door.archway { - Color32::from_rgb(70, 130, 220) + Color32::from_rgb(230, 140, 60) } else if door.locked { Color32::from_rgb(220, 70, 70) } else { @@ -1398,6 +1413,21 @@ fn draw_layout( 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, + ); + } } // Compute the rectangle for a given grid cell. @@ -1612,6 +1642,14 @@ fn door_render_width(door: &layout::Door) -> usize { } } +fn window_render_width(window: &layout::Window) -> usize { + if window.span_width { + window.width.max(1) + } else { + 1 + } +} + // Convert a pointer position to grid coordinates if inside the grid. fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> { if !geometry.rect.contains(pointer_pos) { @@ -1702,6 +1740,17 @@ fn door_settings_from_ui(settings: &UiSettings) -> DoorSettings { } } +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, + } +} + // Draw a door line segment with a selected style. fn draw_door_line( painter: &egui::Painter, @@ -1739,12 +1788,64 @@ fn draw_door_line( } } +fn draw_window_line( + painter: &egui::Painter, + geometry: &GridGeometry, + window: &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 { + 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); + } + 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); + } + 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); + } + 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); + } + } +} + #[derive(Debug, Clone, Copy)] enum DoorLineStyle { Solid, LongDash, ShortDash, Dotted, + DashDot, } // Draw a line with solid, dashed, or dotted styling. @@ -1762,6 +1863,7 @@ fn draw_styled_line( 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), } } @@ -1815,6 +1917,32 @@ fn draw_dotted_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, s } } +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; + } +} + // Compute the center position of a grid cell. fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 { egui::pos2( @@ -1864,6 +1992,7 @@ enum LegendStyle { LongDash, ShortDash, DottedLine, + DashDotLine, } // Draw a patterned legend entry for colorblind mode. @@ -1934,6 +2063,12 @@ fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendSty 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), + ), } ui.add_space(6.0); diff --git a/src/ui.rs b/src/ui.rs index 3a43e50..6570a5f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -115,6 +115,12 @@ pub struct UiSettings { pub room_hallway_door_percent: usize, pub locked_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 export_format: ExportFormat, pub mask_format: MaskFormat, @@ -146,6 +152,12 @@ impl Default for UiSettings { room_hallway_door_percent: 70, locked_door_percent: 25, allow_middle_corridor_doors: false, + windows_enabled: false, + min_window_width: 1, + max_window_width: 2, + window_frequency_percent: 30, + room_hallway_window_percent: 40, + allow_internal_windows: false, colorblind_mode: false, export_format: ExportFormat::Png, mask_format: MaskFormat::Png, @@ -576,6 +588,92 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si "Allow Middle Corridor Doors", ) .changed(); + + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); + ui.label(RichText::new("Window Settings").strong()); + ui.add_space(8.0); + + result.settings_changed |= ui + .checkbox(&mut settings.windows_enabled, "Enable Windows") + .changed(); + + if settings.windows_enabled { + ui.add_space(8.0); + ui.label("Min Window Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.min_window_width, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.min_window_width) + .speed(1.0) + .range(1..=5), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Max Window Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.max_window_width, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.max_window_width) + .speed(1.0) + .range(1..=5), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Window Frequency (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add( + egui::Slider::new(&mut settings.window_frequency_percent, 0..=100) + .show_value(false), + ) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.window_frequency_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Internal Window Chance (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add( + egui::Slider::new(&mut settings.room_hallway_window_percent, 0..=100) + .show_value(false), + ) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.room_hallway_window_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + + ui.add_space(8.0); + result.settings_changed |= ui + .checkbox( + &mut settings.allow_internal_windows, + "Allow Internal Windows", + ) + .changed(); + } } // Render the Add tab controls.