added window options
This commit is contained in:
+107
-2
@@ -6,7 +6,7 @@ use image::{DynamicImage, ImageFormat, RgbaImage, imageops::FilterType};
|
|||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform};
|
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};
|
use crate::ui::{ExportFormat, MaskFormat, UiSettings};
|
||||||
|
|
||||||
const BG: (u8, u8, u8, u8) = (24, 24, 26, 255);
|
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 BLACK: (u8, u8, u8, u8) = (0, 0, 0, 255);
|
||||||
const OPEN_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255);
|
const OPEN_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255);
|
||||||
const LOCKED_DOOR: (u8, u8, u8, u8) = (220, 70, 70, 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);
|
const WHITE: (u8, u8, u8, u8) = (255, 255, 255, 255);
|
||||||
|
|
||||||
pub enum ExportTarget {
|
pub enum ExportTarget {
|
||||||
@@ -30,6 +31,7 @@ enum DoorStyle {
|
|||||||
LongDash,
|
LongDash,
|
||||||
ShortDash,
|
ShortDash,
|
||||||
Dotted,
|
Dotted,
|
||||||
|
DashDot,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[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);
|
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)
|
Ok(pixmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,9 +322,12 @@ fn export_composite_masks(
|
|||||||
.ok_or_else(|| "Failed to allocate archways mask canvas".to_string())?;
|
.ok_or_else(|| "Failed to allocate archways mask canvas".to_string())?;
|
||||||
let mut locked_doors = Pixmap::new(g.width, g.height)
|
let mut locked_doors = Pixmap::new(g.width, g.height)
|
||||||
.ok_or_else(|| "Failed to allocate locked doors mask canvas".to_string())?;
|
.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 doors, BLACK);
|
||||||
fill_bg(&mut archways, BLACK);
|
fill_bg(&mut archways, BLACK);
|
||||||
fill_bg(&mut locked_doors, BLACK);
|
fill_bg(&mut locked_doors, BLACK);
|
||||||
|
fill_bg(&mut windows, BLACK);
|
||||||
for door in &layout.doors {
|
for door in &layout.doors {
|
||||||
if door.archway {
|
if door.archway {
|
||||||
draw_door(&mut archways, &g, door, door_w, WHITE, DoorStyle::Solid);
|
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);
|
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 = render_pixmap(layout, settings)?;
|
||||||
let composite_img = raster_image_from_pixmap(&composite, 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, "doors_mask", &doors, settings)?;
|
||||||
save_mask_image(folder, "archways_mask", &archways, settings)?;
|
save_mask_image(folder, "archways_mask", &archways, settings)?;
|
||||||
save_mask_image(folder, "locked_doors_mask", &locked_doors, settings)?;
|
save_mask_image(folder, "locked_doors_mask", &locked_doors, settings)?;
|
||||||
|
save_mask_image(folder, "windows_mask", &windows, settings)?;
|
||||||
if settings.export_show_grid {
|
if settings.export_show_grid {
|
||||||
let mut grid = Pixmap::new(g.width, g.height)
|
let mut grid = Pixmap::new(g.width, g.height)
|
||||||
.ok_or_else(|| "Failed to allocate grid mask canvas".to_string())?;
|
.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.push_str("</svg>\n");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -970,6 +1007,20 @@ fn draw_door(
|
|||||||
draw_line(pixmap, x1, y1, x2, y2, width, color, style);
|
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.
|
// Compute the door line endpoints in export space.
|
||||||
fn door_line_points(
|
fn door_line_points(
|
||||||
g: &ExportGeometry,
|
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.
|
// Draw a styled line in the pixmap.
|
||||||
fn draw_line(
|
fn draw_line(
|
||||||
pixmap: &mut Pixmap,
|
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::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::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::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
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn window_render_width(window: &Window) -> usize {
|
||||||
|
if window.span_width {
|
||||||
|
window.width.max(1)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+280
@@ -37,6 +37,22 @@ pub struct Door {
|
|||||||
pub archway: bool,
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct DoorSettings {
|
pub struct DoorSettings {
|
||||||
pub frequency_percent: usize,
|
pub frequency_percent: usize,
|
||||||
@@ -45,11 +61,22 @@ pub struct DoorSettings {
|
|||||||
pub allow_middle_corridor_doors: bool,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DungeonLayout {
|
pub struct DungeonLayout {
|
||||||
pub rooms: Vec<Room>,
|
pub rooms: Vec<Room>,
|
||||||
pub corridors: Vec<Corridor>,
|
pub corridors: Vec<Corridor>,
|
||||||
pub doors: Vec<Door>,
|
pub doors: Vec<Door>,
|
||||||
|
pub windows: Vec<Window>,
|
||||||
pub packed_rooms: bool,
|
pub packed_rooms: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +86,7 @@ impl Default for DungeonLayout {
|
|||||||
rooms: Vec::new(),
|
rooms: Vec::new(),
|
||||||
corridors: Vec::new(),
|
corridors: Vec::new(),
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: false,
|
packed_rooms: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +127,7 @@ pub fn generate_layout(
|
|||||||
dead_end_room_percent: usize,
|
dead_end_room_percent: usize,
|
||||||
pack_rooms_without_corridors: bool,
|
pack_rooms_without_corridors: bool,
|
||||||
door_settings: DoorSettings,
|
door_settings: DoorSettings,
|
||||||
|
window_settings: WindowSettings,
|
||||||
) -> DungeonLayout {
|
) -> DungeonLayout {
|
||||||
let layout_salt = ((cols as u64) << 48)
|
let layout_salt = ((cols as u64) << 48)
|
||||||
^ ((rows as u64) << 32)
|
^ ((rows as u64) << 32)
|
||||||
@@ -123,6 +152,7 @@ pub fn generate_layout(
|
|||||||
rooms,
|
rooms,
|
||||||
corridors,
|
corridors,
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: pack_rooms_without_corridors,
|
packed_rooms: pack_rooms_without_corridors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -144,6 +174,7 @@ pub fn generate_layout(
|
|||||||
rooms,
|
rooms,
|
||||||
corridors,
|
corridors,
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: pack_rooms_without_corridors,
|
packed_rooms: pack_rooms_without_corridors,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -170,9 +201,11 @@ pub fn generate_layout(
|
|||||||
rooms,
|
rooms,
|
||||||
corridors,
|
corridors,
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: true,
|
packed_rooms: true,
|
||||||
};
|
};
|
||||||
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
||||||
|
apply_windows(&mut layout, seed, window_settings, cols, rows);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +257,7 @@ pub fn generate_layout(
|
|||||||
rooms,
|
rooms,
|
||||||
corridors,
|
corridors,
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: false,
|
packed_rooms: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -276,9 +310,11 @@ pub fn generate_layout(
|
|||||||
rooms,
|
rooms,
|
||||||
corridors,
|
corridors,
|
||||||
doors: Vec::new(),
|
doors: Vec::new(),
|
||||||
|
windows: Vec::new(),
|
||||||
packed_rooms: false,
|
packed_rooms: false,
|
||||||
};
|
};
|
||||||
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
apply_doors(&mut layout, seed, door_settings, cols, rows);
|
||||||
|
apply_windows(&mut layout, seed, window_settings, cols, rows);
|
||||||
layout
|
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.
|
// Find the edge where a corridor path exits a room.
|
||||||
fn room_exit_edge(
|
fn room_exit_edge(
|
||||||
path: &[(usize, usize)],
|
path: &[(usize, usize)],
|
||||||
@@ -535,6 +806,15 @@ fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usi
|
|||||||
edges
|
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.
|
// Compute corridor cells while excluding room cells.
|
||||||
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> {
|
||||||
let mut cells = HashSet::new();
|
let mut cells = HashSet::new();
|
||||||
|
|||||||
+138
-3
@@ -11,7 +11,8 @@ use std::thread;
|
|||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use egui::{Color32, Stroke};
|
use egui::{Color32, Stroke};
|
||||||
use layout::{
|
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};
|
use ui::{AddTool, UiSettings, draw_side_panel};
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ impl Default for DungeonApp {
|
|||||||
settings.dead_end_rooms_percent,
|
settings.dead_end_rooms_percent,
|
||||||
settings.pack_rooms_without_corridors,
|
settings.pack_rooms_without_corridors,
|
||||||
door_settings_from_ui(&settings),
|
door_settings_from_ui(&settings),
|
||||||
|
window_settings_from_ui(&settings),
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
settings,
|
settings,
|
||||||
@@ -96,6 +98,9 @@ impl eframe::App for DungeonApp {
|
|||||||
if self.settings.min_corridor_width > self.settings.max_corridor_width {
|
if self.settings.min_corridor_width > self.settings.max_corridor_width {
|
||||||
self.settings.max_corridor_width = self.settings.min_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 {
|
if panel_result.clear_clicked {
|
||||||
self.clear_layout();
|
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, "Doors", LegendStyle::LongDash);
|
||||||
draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash);
|
||||||
draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine);
|
||||||
|
draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine);
|
||||||
} else {
|
} else {
|
||||||
draw_legend_entry(ui, Color32::from_rgb(70, 120, 160), "Rooms");
|
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::from_rgb(210, 190, 120), "Corridors");
|
||||||
draw_legend_entry(ui, Color32::BLACK, "Walls");
|
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(80, 200, 120), "Doors");
|
||||||
draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked 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.add_space(10.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
@@ -242,6 +249,7 @@ impl DungeonApp {
|
|||||||
self.settings.dead_end_rooms_percent,
|
self.settings.dead_end_rooms_percent,
|
||||||
self.settings.pack_rooms_without_corridors,
|
self.settings.pack_rooms_without_corridors,
|
||||||
door_settings_from_ui(&self.settings),
|
door_settings_from_ui(&self.settings),
|
||||||
|
window_settings_from_ui(&self.settings),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +455,13 @@ impl DungeonApp {
|
|||||||
self.settings.cols,
|
self.settings.cols,
|
||||||
self.settings.rows,
|
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.
|
// Handle add-tool interactions for rooms, corridors, and doors.
|
||||||
@@ -1371,7 +1386,7 @@ fn draw_layout(
|
|||||||
let color = if colorblind_mode {
|
let color = if colorblind_mode {
|
||||||
Color32::BLACK
|
Color32::BLACK
|
||||||
} else if door.archway {
|
} else if door.archway {
|
||||||
Color32::from_rgb(70, 130, 220)
|
Color32::from_rgb(230, 140, 60)
|
||||||
} else if door.locked {
|
} else if door.locked {
|
||||||
Color32::from_rgb(220, 70, 70)
|
Color32::from_rgb(220, 70, 70)
|
||||||
} else {
|
} else {
|
||||||
@@ -1398,6 +1413,21 @@ fn draw_layout(
|
|||||||
style,
|
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.
|
// 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.
|
// Convert a pointer position to grid coordinates if inside the grid.
|
||||||
fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> {
|
fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> {
|
||||||
if !geometry.rect.contains(pointer_pos) {
|
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.
|
// Draw a door line segment with a selected style.
|
||||||
fn draw_door_line(
|
fn draw_door_line(
|
||||||
painter: &egui::Painter,
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum DoorLineStyle {
|
enum DoorLineStyle {
|
||||||
Solid,
|
Solid,
|
||||||
LongDash,
|
LongDash,
|
||||||
ShortDash,
|
ShortDash,
|
||||||
Dotted,
|
Dotted,
|
||||||
|
DashDot,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a line with solid, dashed, or dotted styling.
|
// 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::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::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0),
|
||||||
DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke),
|
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.
|
// Compute the center position of a grid cell.
|
||||||
fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 {
|
fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 {
|
||||||
egui::pos2(
|
egui::pos2(
|
||||||
@@ -1864,6 +1992,7 @@ enum LegendStyle {
|
|||||||
LongDash,
|
LongDash,
|
||||||
ShortDash,
|
ShortDash,
|
||||||
DottedLine,
|
DottedLine,
|
||||||
|
DashDotLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a patterned legend entry for colorblind mode.
|
// 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),
|
egui::pos2(rect.right() - 1.0, rect.center().y),
|
||||||
Stroke::new(2.0, Color32::BLACK),
|
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);
|
ui.add_space(6.0);
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ pub struct UiSettings {
|
|||||||
pub room_hallway_door_percent: usize,
|
pub room_hallway_door_percent: usize,
|
||||||
pub locked_door_percent: usize,
|
pub locked_door_percent: usize,
|
||||||
pub allow_middle_corridor_doors: bool,
|
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 colorblind_mode: bool,
|
||||||
pub export_format: ExportFormat,
|
pub export_format: ExportFormat,
|
||||||
pub mask_format: MaskFormat,
|
pub mask_format: MaskFormat,
|
||||||
@@ -146,6 +152,12 @@ impl Default for UiSettings {
|
|||||||
room_hallway_door_percent: 70,
|
room_hallway_door_percent: 70,
|
||||||
locked_door_percent: 25,
|
locked_door_percent: 25,
|
||||||
allow_middle_corridor_doors: false,
|
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,
|
colorblind_mode: false,
|
||||||
export_format: ExportFormat::Png,
|
export_format: ExportFormat::Png,
|
||||||
mask_format: MaskFormat::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",
|
"Allow Middle Corridor Doors",
|
||||||
)
|
)
|
||||||
.changed();
|
.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.
|
// Render the Add tab controls.
|
||||||
|
|||||||
Reference in New Issue
Block a user