added window options

This commit is contained in:
grimsace
2026-03-23 12:22:03 -05:00
parent 68d45956ac
commit bdd1e931c3
4 changed files with 623 additions and 5 deletions
+107 -2
View File
@@ -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<Pixmap
draw_door(&mut pixmap, &g, door, door_w, color, style);
}
for window in &layout.windows {
let style = if settings.colorblind_mode {
DoorStyle::DashDot
} else {
DoorStyle::Solid
};
draw_window(&mut pixmap, &g, window, door_w, WINDOW, style);
}
Ok(pixmap)
}
@@ -311,9 +322,12 @@ fn export_composite_masks(
.ok_or_else(|| "Failed to allocate archways mask canvas".to_string())?;
let mut locked_doors = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate locked doors mask canvas".to_string())?;
let mut windows = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate windows mask canvas".to_string())?;
fill_bg(&mut doors, BLACK);
fill_bg(&mut archways, BLACK);
fill_bg(&mut locked_doors, BLACK);
fill_bg(&mut windows, BLACK);
for door in &layout.doors {
if door.archway {
draw_door(&mut archways, &g, door, door_w, WHITE, DoorStyle::Solid);
@@ -323,6 +337,9 @@ fn export_composite_masks(
draw_door(&mut doors, &g, door, door_w, WHITE, DoorStyle::Solid);
}
}
for window in &layout.windows {
draw_window(&mut windows, &g, window, door_w, WHITE, DoorStyle::Solid);
}
let composite = render_pixmap(layout, settings)?;
let composite_img = raster_image_from_pixmap(&composite, settings)?;
@@ -337,6 +354,7 @@ fn export_composite_masks(
save_mask_image(folder, "doors_mask", &doors, settings)?;
save_mask_image(folder, "archways_mask", &archways, settings)?;
save_mask_image(folder, "locked_doors_mask", &locked_doors, settings)?;
save_mask_image(folder, "windows_mask", &windows, settings)?;
if settings.export_show_grid {
let mut grid = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate grid mask canvas".to_string())?;
@@ -615,6 +633,25 @@ fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> 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,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='rgb({},{},{})' stroke-width='{door_w}' stroke-dasharray='{pattern}'/>",
WINDOW.0, WINDOW.1, WINDOW.2
);
} else {
let _ = writeln!(
s,
"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='rgb({},{},{})' stroke-width='{door_w}'/>",
WINDOW.0, WINDOW.1, WINDOW.2
);
}
}
}
s.push_str("</svg>\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
}
}
+280
View File
@@ -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<Room>,
pub corridors: Vec<Corridor>,
pub doors: Vec<Door>,
pub windows: Vec<Window>,
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<WindowSegment> {
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<WindowSegment>,
) {
let cells: Vec<(usize, usize)> = match side {
WindowSide::Left => (room.y..(room.y + room.height))
.map(|y| (room.x, y))
.collect(),
WindowSide::Right => (room.y..(room.y + room.height))
.map(|y| (room.x + room.width - 1, y))
.collect(),
WindowSide::Top => (room.x..(room.x + room.width))
.map(|x| (x, room.y))
.collect(),
WindowSide::Bottom => (room.x..(room.x + room.width))
.map(|x| (x, room.y + room.height - 1))
.collect(),
};
let mut current_target = None;
let mut current_cells = Vec::new();
for cell in cells {
let Some(neighbor) = outward_neighbor(cell, side, cols, rows) else {
push_window_segment(current_target, side, &mut current_cells, segments);
current_target = Some(WindowTarget::Exterior);
current_cells.push(cell);
continue;
};
let target = if let Some(other_room_idx) = room_index_at_cell(&layout.rooms, neighbor) {
if other_room_idx == room_idx || !allow_internal_windows || other_room_idx < room_idx {
None
} else {
Some(WindowTarget::Room(other_room_idx))
}
} else if corridor_cells.contains(&neighbor) {
allow_internal_windows.then_some(WindowTarget::Corridor)
} else {
Some(WindowTarget::Exterior)
};
if current_target != target {
push_window_segment(current_target, side, &mut current_cells, segments);
current_target = target;
}
if target.is_some() {
current_cells.push(cell);
}
}
push_window_segment(current_target, side, &mut current_cells, segments);
}
fn push_window_segment(
target: Option<WindowTarget>,
side: WindowSide,
cells: &mut Vec<(usize, usize)>,
segments: &mut Vec<WindowSegment>,
) {
let Some(target) = target else {
cells.clear();
return;
};
if cells.is_empty() {
return;
}
segments.push(WindowSegment {
cells: std::mem::take(cells),
side,
internal: !matches!(target, WindowTarget::Exterior),
});
}
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<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();
+138 -3
View File
@@ -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);
+98
View File
@@ -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.