cleaned up the code and added comments and changed readme

This commit is contained in:
grimsace
2026-04-14 11:47:31 -05:00
parent d30c43675f
commit f569c62137
6 changed files with 266 additions and 227 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "desktop_dungeon_generator" name = "desktop_dungeon_generator"
version = "0.5.0" version = "0.6.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+115 -63
View File
@@ -1,73 +1,128 @@
# Desktop Dungeon Generator # Desktop Dungeon Generator
**Pre-release Notice:** This program is incomplete and functionality is subject to change and it might break, you've been warned. *though releases should at least be stable enough to run* Desktop Dungeon Generator is a Rust desktop app for building and exporting simple tabletop RPG dungeon layouts.
A Rust desktop app for generating simple tabletop dungeon layouts. It combines deterministic generation with manual editing, so you can generate a baseline layout from a seed and then adjust rooms, corridors, doors, text labels, and start/end markers directly on the canvas.
## Current Features ## Current Features
- Resizable split layout: - Deterministic dungeon generation from a master seed
- Left panel: generation settings - Room generation with configurable:
- Right panel: vector-rendered grid + dungeon preview - room count
- Grid controls: - minimum and maximum room size
- Columns - square-only rooms
- Rows - packed-room mode without corridors
- Room controls: - Corridor generation with configurable:
- Room count - minimum and maximum corridor width
- Minimum room size (in grid cells) - corridor randomness
- Maximum room size (in grid cells) - dead-end room percentage
- `Square Rooms Only` toggle - Automatic door generation with configurable:
- Corridor controls: - overall frequency
- `Corridor Randomness (%)` from `0` to `100` - room/hallway chance
- `Dead-End Rooms (%)` from `0` to `50` - locked door chance
- Door controls: - secret door chance
- `Door Frequency (%)` - middle-corridor doors toggle
- `Room/Hallway Door Chance (%)` - Automatic window generation with configurable:
- `Locked Door Chance (%)` - enable/disable
- `Allow Middle Corridor Doors` toggle - minimum and maximum width
- Accessibility: - overall frequency
- `Colorblind Mode` toggle (right panel) - room/hallway chance
- Pattern/style encoding: - internal windows toggle
- Rooms: crosshatch - Generated start/end markers with configurable:
- Corridors: dotted fill - minimum and maximum size
- Walls: solid lines - minimum and maximum count
- Doors: long dashes - placement inside rooms
- Locked doors: short dashes - farthest-pair matching between start and end markers
- Archways: dotted lines - Manual editing tools for:
- Generate control: - rooms
- Master seed input (`u64`) - corridors
- `Random` seed button - archways
- `Reset` button - doors
- `Export Image` button - locked doors
- Export format selector: `.png`, `.jpeg`, `.webp`, `.svg` - secret doors
- Export uses an OS folder picker to choose save location - text labels
- start markers
- end markers
- Export support for:
- `.png`
- `.jpeg`
- `.webp`
- `.svg`
- composite folder export with masks
- Accessibility mode with alternate visual encodings for rooms, corridors, walls, doors, archways, and windows
## Generation Behavior ## Tabs
- Rooms are stored as rectangles (`x`, `y`, `width`, `height`) and placed without overlap. ### Generate
- Corridors are stored separately as vector line segments between grid cells.
- Every generated room is connected into one navigable network when possible.
- A master seed drives deterministic generation, and internal subsystems derive their own sub-seeds from it.
- Doors are generated from corridor adjacency:
- Open doors are rendered in red
- Locked doors are rendered in green
- Doors are rendered as line segments on grid boundaries
### Corridor Randomness Controls global generation and export:
- `0%` randomness: - grid columns and rows
- Uses shortest-path routing for room-to-room corridor paths. - master seed and random-seed button
- Produces cleaner, direct corridor routes. - reset and clear actions
- `100%` randomness: - export format, dimensions, and grid visibility
- Uses noisy/randomized routing while still ensuring room connectivity.
- Produces more wandering, less direct routes.
- Intermediate values blend between the two behaviors.
### Dead-End Rooms ### Layout
- Controls how many rooms should end up as dead ends (rooms with only one room-to-room connection). Controls room, corridor, door, and window generation behavior.
- `0%` targets no dead-end rooms (except tiny edge cases where topology makes this impossible, such as only 2 rooms).
- `50%` is the upper cap to avoid over-constraining connectivity. Changing values in this tab regenerates the dungeon immediately.
### Start & End
Controls generated start/end marker behavior:
- minimum and maximum start marker size
- minimum and maximum end marker size
- minimum and maximum start marker count
- minimum and maximum end marker count
Changing values in this tab regenerates the dungeon immediately.
Generated markers are placed inside rooms. Matching start/end indices are paired to rooms that are as far apart as possible.
### Add
Contains manual placement tools for editing the current layout without regenerating it.
## Canvas Interaction
- Left click and drag a room to move it
- Left click and drag a start/end marker to move it
- Right click and drag a room to resize it
- Right click without dragging to cancel an active add tool
- `Delete` or `Backspace` removes the currently hovered room, corridor, door, text label, or marker
- `Ctrl+Z` undoes
- `Ctrl+Y` redoes
## Start / End Markers
The app currently supports two marker workflows:
- Generated markers:
- created automatically during dungeon generation
- always placed inside rooms
- sized from the configured min/max ranges
- paired by distance for each matching start/end index
- Manual markers:
- added from the `Add` tab
- use the configured minimum size for their marker type
- can be repositioned by dragging on the canvas
## Folder Export Masks
Folder export writes a composite image plus separate masks for:
- floors
- walls
- doors
- archways
- locked doors
- secret doors
- windows
- start markers
- end markers
- grid, when enabled
## Run ## Run
@@ -77,8 +132,5 @@ cargo run
## Notes ## Notes
- Rendering is vector-based in-app (`egui` painter), so zooming/scaling the preview remains crisp. - Settings are persisted between runs using the local OS application data directory.
- Room and corridor vectors are stored separately to support future export workflows (for example, SVG export). - The app is still pre-release and the file layout and feature set may continue to change.
- Settings are persisted on exit and restored on startup from:
- Windows: `%LOCALAPPDATA%/desktop_dungeon_generator/settings.json`
- Linux: `~/.local/share/desktop_dungeon_generator/settings.json` (or distro-equivalent local data dir)
+14 -21
View File
@@ -331,27 +331,13 @@ fn export_composite_masks(
WHITE, WHITE,
); );
let mut doors = Pixmap::new(g.width, g.height) let mut doors = blank_mask(&g, "doors")?;
.ok_or_else(|| "Failed to allocate doors mask canvas".to_string())?; let mut archways = blank_mask(&g, "archways")?;
let mut archways = Pixmap::new(g.width, g.height) let mut locked_doors = blank_mask(&g, "locked doors")?;
.ok_or_else(|| "Failed to allocate archways mask canvas".to_string())?; let mut secret_doors = blank_mask(&g, "secret doors")?;
let mut locked_doors = Pixmap::new(g.width, g.height) let mut windows = blank_mask(&g, "windows")?;
.ok_or_else(|| "Failed to allocate locked doors mask canvas".to_string())?; let mut start_markers = blank_mask(&g, "start markers")?;
let mut secret_doors = Pixmap::new(g.width, g.height) let mut end_markers = blank_mask(&g, "end markers")?;
.ok_or_else(|| "Failed to allocate secret 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())?;
let mut start_markers = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate start markers mask canvas".to_string())?;
let mut end_markers = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate end markers mask canvas".to_string())?;
fill_bg(&mut doors, BLACK);
fill_bg(&mut archways, BLACK);
fill_bg(&mut locked_doors, BLACK);
fill_bg(&mut secret_doors, BLACK);
fill_bg(&mut windows, BLACK);
fill_bg(&mut start_markers, BLACK);
fill_bg(&mut end_markers, BLACK);
for door in &layout.doors { for door in &layout.doors {
if door.secret { if door.secret {
draw_door(&mut secret_doors, &g, door, door_w, WHITE, DoorStyle::Solid); draw_door(&mut secret_doors, &g, door, door_w, WHITE, DoorStyle::Solid);
@@ -401,6 +387,13 @@ fn export_composite_masks(
Ok(()) Ok(())
} }
fn blank_mask(g: &ExportGeometry, name: &str) -> Result<Pixmap, String> {
let mut pixmap = Pixmap::new(g.width, g.height)
.ok_or_else(|| format!("Failed to allocate {name} mask canvas"))?;
fill_bg(&mut pixmap, BLACK);
Ok(pixmap)
}
// Write a single mask image to disk. // Write a single mask image to disk.
fn save_mask_image( fn save_mask_image(
folder: &std::path::Path, folder: &std::path::Path,
+28 -24
View File
@@ -122,30 +122,7 @@ impl eframe::App for DungeonApp {
|| (i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::Z)) || (i.modifiers.command && i.modifiers.shift && i.key_pressed(egui::Key::Z))
}); });
if self.settings.min_room_size > self.settings.max_room_size { clamp_dependent_settings(&mut self.settings);
self.settings.max_room_size = self.settings.min_room_size;
}
if self.settings.min_corridor_width == 0 {
self.settings.min_corridor_width = 1;
}
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 self.settings.min_start_marker_size > self.settings.max_start_marker_size {
self.settings.max_start_marker_size = self.settings.min_start_marker_size;
}
if self.settings.min_end_marker_size > self.settings.max_end_marker_size {
self.settings.max_end_marker_size = self.settings.min_end_marker_size;
}
if self.settings.min_start_marker_count > self.settings.max_start_marker_count {
self.settings.max_start_marker_count = self.settings.min_start_marker_count;
}
if self.settings.min_end_marker_count > self.settings.max_end_marker_count {
self.settings.max_end_marker_count = self.settings.min_end_marker_count;
}
if undo_requested { if undo_requested {
self.undo(); self.undo();
@@ -280,6 +257,33 @@ impl eframe::App for DungeonApp {
} }
} }
fn clamp_dependent_settings(settings: &mut UiSettings) {
if settings.min_room_size > settings.max_room_size {
settings.max_room_size = settings.min_room_size;
}
if settings.min_corridor_width == 0 {
settings.min_corridor_width = 1;
}
if settings.min_corridor_width > settings.max_corridor_width {
settings.max_corridor_width = settings.min_corridor_width;
}
if settings.min_window_width > settings.max_window_width {
settings.max_window_width = settings.min_window_width;
}
if settings.min_start_marker_size > settings.max_start_marker_size {
settings.max_start_marker_size = settings.min_start_marker_size;
}
if settings.min_end_marker_size > settings.max_end_marker_size {
settings.max_end_marker_size = settings.min_end_marker_size;
}
if settings.min_start_marker_count > settings.max_start_marker_count {
settings.max_start_marker_count = settings.min_start_marker_count;
}
if settings.min_end_marker_count > settings.max_end_marker_count {
settings.max_end_marker_count = settings.min_end_marker_count;
}
}
impl DungeonApp { impl DungeonApp {
fn snapshot(&self) -> AppSnapshot { fn snapshot(&self) -> AppSnapshot {
AppSnapshot { AppSnapshot {
+24 -8
View File
@@ -2,6 +2,16 @@ use crate::layout::{self, DungeonLayout};
use crate::seed; use crate::seed;
use crate::ui::UiSettings; use crate::ui::UiSettings;
const START_COUNT_STREAM: u64 = 10_001;
const END_COUNT_STREAM: u64 = 10_002;
const START_ROOM_STREAM_BASE: u64 = 11_000;
const END_ROOM_STREAM_BASE: u64 = 12_000;
const EXTRA_START_ROOM_STREAM_BASE: u64 = 13_000;
const EXTRA_START_MARKER_STREAM_BASE: u64 = 14_000;
const EXTRA_END_ROOM_STREAM_BASE: u64 = 15_000;
const EXTRA_END_MARKER_STREAM_BASE: u64 = 16_000;
// Populate generated start/end markers after the core dungeon layout exists.
pub fn populate_random_markers( pub fn populate_random_markers(
mut layout: DungeonLayout, mut layout: DungeonLayout,
settings: &UiSettings, settings: &UiSettings,
@@ -16,12 +26,12 @@ pub fn populate_random_markers(
let start_count = random_range_inclusive( let start_count = random_range_inclusive(
settings.min_start_marker_count, settings.min_start_marker_count,
settings.max_start_marker_count, settings.max_start_marker_count,
seed::derive_seed(settings.seed, 10_001), seed::derive_seed(settings.seed, START_COUNT_STREAM),
); );
let end_count = random_range_inclusive( let end_count = random_range_inclusive(
settings.min_end_marker_count, settings.min_end_marker_count,
settings.max_end_marker_count, settings.max_end_marker_count,
seed::derive_seed(settings.seed, 10_002), seed::derive_seed(settings.seed, END_COUNT_STREAM),
); );
let pair_count = start_count.min(end_count); let pair_count = start_count.min(end_count);
@@ -37,13 +47,13 @@ pub fn populate_random_markers(
start_room, start_room,
settings.min_start_marker_size, settings.min_start_marker_size,
settings.max_start_marker_size, settings.max_start_marker_size,
seed::derive_seed(settings.seed, 11_000 + pair_idx as u64), seed::derive_seed(settings.seed, START_ROOM_STREAM_BASE + pair_idx as u64),
)); ));
layout.end_markers.push(marker_in_room( layout.end_markers.push(marker_in_room(
end_room, end_room,
settings.min_end_marker_size, settings.min_end_marker_size,
settings.max_end_marker_size, settings.max_end_marker_size,
seed::derive_seed(settings.seed, 12_000 + pair_idx as u64), seed::derive_seed(settings.seed, END_ROOM_STREAM_BASE + pair_idx as u64),
)); ));
consume_room(&mut available_start_rooms, start_room_idx); consume_room(&mut available_start_rooms, start_room_idx);
consume_room(&mut available_end_rooms, end_room_idx); consume_room(&mut available_end_rooms, end_room_idx);
@@ -53,14 +63,14 @@ pub fn populate_random_markers(
let room_idx = pick_room_index( let room_idx = pick_room_index(
&available_start_rooms, &available_start_rooms,
layout.rooms.len(), layout.rooms.len(),
seed::derive_seed(settings.seed, 13_000 + idx as u64), seed::derive_seed(settings.seed, EXTRA_START_ROOM_STREAM_BASE + idx as u64),
); );
let room = &layout.rooms[room_idx]; let room = &layout.rooms[room_idx];
layout.start_markers.push(marker_in_room( layout.start_markers.push(marker_in_room(
room, room,
settings.min_start_marker_size, settings.min_start_marker_size,
settings.max_start_marker_size, settings.max_start_marker_size,
seed::derive_seed(settings.seed, 14_000 + idx as u64), seed::derive_seed(settings.seed, EXTRA_START_MARKER_STREAM_BASE + idx as u64),
)); ));
consume_room(&mut available_start_rooms, room_idx); consume_room(&mut available_start_rooms, room_idx);
} }
@@ -69,14 +79,14 @@ pub fn populate_random_markers(
let room_idx = pick_room_index( let room_idx = pick_room_index(
&available_end_rooms, &available_end_rooms,
layout.rooms.len(), layout.rooms.len(),
seed::derive_seed(settings.seed, 15_000 + idx as u64), seed::derive_seed(settings.seed, EXTRA_END_ROOM_STREAM_BASE + idx as u64),
); );
let room = &layout.rooms[room_idx]; let room = &layout.rooms[room_idx];
layout.end_markers.push(marker_in_room( layout.end_markers.push(marker_in_room(
room, room,
settings.min_end_marker_size, settings.min_end_marker_size,
settings.max_end_marker_size, settings.max_end_marker_size,
seed::derive_seed(settings.seed, 16_000 + idx as u64), seed::derive_seed(settings.seed, EXTRA_END_MARKER_STREAM_BASE + idx as u64),
)); ));
consume_room(&mut available_end_rooms, room_idx); consume_room(&mut available_end_rooms, room_idx);
} }
@@ -84,6 +94,7 @@ pub fn populate_random_markers(
layout layout
} }
// Manual marker placement uses the configured minimum size for the chosen marker type.
pub fn manual_marker_size(settings: &UiSettings, is_start: bool) -> usize { pub fn manual_marker_size(settings: &UiSettings, is_start: bool) -> usize {
if is_start { if is_start {
settings settings
@@ -95,6 +106,7 @@ pub fn manual_marker_size(settings: &UiSettings, is_start: bool) -> usize {
.clamp(1, 10) .clamp(1, 10)
} }
// Pick a deterministic inclusive random value from a seed-derived stream.
fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize { fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize {
let min = min.max(1); let min = min.max(1);
let max = max.max(min); let max = max.max(min);
@@ -102,6 +114,7 @@ fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize {
min + (seed_value as usize % span) min + (seed_value as usize % span)
} }
// Fit a marker inside a room and choose a deterministic offset within that room.
fn marker_in_room( fn marker_in_room(
room: &layout::Room, room: &layout::Room,
min_size: usize, min_size: usize,
@@ -129,6 +142,7 @@ fn marker_in_room(
layout::AreaMarker { cell: (x, y), size } layout::AreaMarker { cell: (x, y), size }
} }
// Match each start/end pair to the farthest available room combination.
fn farthest_room_pair( fn farthest_room_pair(
rooms: &[layout::Room], rooms: &[layout::Room],
available_start_rooms: &[usize], available_start_rooms: &[usize],
@@ -170,12 +184,14 @@ fn room_distance_sq(a: &layout::Room, b: &layout::Room) -> usize {
dx * dx + dy * dy dx * dx + dy * dy
} }
// Remove a room from the available set once it has been consumed by a marker assignment.
fn consume_room(available_rooms: &mut Vec<usize>, room_idx: usize) { fn consume_room(available_rooms: &mut Vec<usize>, room_idx: usize) {
if let Some(pos) = available_rooms.iter().position(|&idx| idx == room_idx) { if let Some(pos) = available_rooms.iter().position(|&idx| idx == room_idx) {
available_rooms.remove(pos); available_rooms.remove(pos);
} }
} }
// Fall back to any room when all candidates for a side have been exhausted.
fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64) -> usize { fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64) -> usize {
if !available_rooms.is_empty() { if !available_rooms.is_empty() {
available_rooms[seed_value as usize % available_rooms.len()] available_rooms[seed_value as usize % available_rooms.len()]
+84 -110
View File
@@ -783,123 +783,97 @@ fn draw_start_and_end_tab(
) { ) {
ui.label(RichText::new("Start Markers").strong()); ui.label(RichText::new("Start Markers").strong());
ui.add_space(8.0); ui.add_space(8.0);
ui.label("Min Start Marker Size"); draw_usize_slider_row(
ui.horizontal(|ui| { ui,
result.settings_changed |= ui result,
.add(egui::Slider::new(&mut settings.min_start_marker_size, 1..=10).show_value(false)) "Min Start Marker Size",
.changed(); &mut settings.min_start_marker_size,
result.settings_changed |= ui 1..=10,
.add( 1..=10,
egui::DragValue::new(&mut settings.min_start_marker_size) );
.speed(1.0) draw_usize_slider_row(
.range(1..=10), ui,
) result,
.changed(); "Max Start Marker Size",
}); &mut settings.max_start_marker_size,
ui.add_space(6.0); 1..=10,
ui.label("Max Start Marker Size"); 1..=10,
ui.horizontal(|ui| { );
result.settings_changed |= ui draw_usize_slider_row(
.add(egui::Slider::new(&mut settings.max_start_marker_size, 1..=10).show_value(false)) ui,
.changed(); result,
result.settings_changed |= ui "Min Start Marker Count",
.add( &mut settings.min_start_marker_count,
egui::DragValue::new(&mut settings.max_start_marker_size) 1..=20,
.speed(1.0) 1..=100,
.range(1..=10), );
) draw_usize_slider_row(
.changed(); ui,
}); result,
ui.add_space(6.0); "Max Start Marker Count",
ui.label("Min Start Marker Count"); &mut settings.max_start_marker_count,
ui.horizontal(|ui| { 1..=20,
result.settings_changed |= ui 1..=100,
.add(egui::Slider::new(&mut settings.min_start_marker_count, 1..=20).show_value(false)) );
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.min_start_marker_count)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(6.0);
ui.label("Max Start Marker Count");
ui.horizontal(|ui| {
result.settings_changed |= ui
.add(egui::Slider::new(&mut settings.max_start_marker_count, 1..=20).show_value(false))
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.max_start_marker_count)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(12.0); ui.add_space(12.0);
ui.separator(); ui.separator();
ui.add_space(8.0); ui.add_space(8.0);
ui.label(RichText::new("End Markers").strong()); ui.label(RichText::new("End Markers").strong());
ui.add_space(8.0); ui.add_space(8.0);
ui.label("Min End Marker Size"); draw_usize_slider_row(
ui.horizontal(|ui| { ui,
result.settings_changed |= ui result,
.add(egui::Slider::new(&mut settings.min_end_marker_size, 1..=10).show_value(false)) "Min End Marker Size",
.changed(); &mut settings.min_end_marker_size,
result.settings_changed |= ui 1..=10,
.add( 1..=10,
egui::DragValue::new(&mut settings.min_end_marker_size) );
.speed(1.0) draw_usize_slider_row(
.range(1..=10), ui,
) result,
.changed(); "Max End Marker Size",
}); &mut settings.max_end_marker_size,
ui.add_space(6.0); 1..=10,
ui.label("Max End Marker Size"); 1..=10,
ui.horizontal(|ui| { );
result.settings_changed |= ui draw_usize_slider_row(
.add(egui::Slider::new(&mut settings.max_end_marker_size, 1..=10).show_value(false)) ui,
.changed(); result,
result.settings_changed |= ui "Min End Marker Count",
.add( &mut settings.min_end_marker_count,
egui::DragValue::new(&mut settings.max_end_marker_size) 1..=20,
.speed(1.0) 1..=100,
.range(1..=10), );
) draw_usize_slider_row(
.changed(); ui,
}); result,
ui.add_space(6.0); "Max End Marker Count",
ui.label("Min End Marker Count"); &mut settings.max_end_marker_count,
ui.horizontal(|ui| { 1..=20,
result.settings_changed |= ui 1..=100,
.add(egui::Slider::new(&mut settings.min_end_marker_count, 1..=20).show_value(false)) );
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.min_end_marker_count)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(6.0);
ui.label("Max End Marker Count");
ui.horizontal(|ui| {
result.settings_changed |= ui
.add(egui::Slider::new(&mut settings.max_end_marker_count, 1..=20).show_value(false))
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.max_end_marker_count)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(8.0); ui.add_space(8.0);
ui.label("Markers are generated inside rooms. Matching start/end indices are paired as far apart as possible."); ui.label("Markers are generated inside rooms. Matching start/end indices are paired as far apart as possible.");
} }
fn draw_usize_slider_row(
ui: &mut egui::Ui,
result: &mut SidePanelResult,
label: &str,
value: &mut usize,
slider_range: std::ops::RangeInclusive<usize>,
drag_range: std::ops::RangeInclusive<usize>,
) {
ui.label(label);
ui.horizontal(|ui| {
result.settings_changed |= ui
.add(egui::Slider::new(value, slider_range).show_value(false))
.changed();
result.settings_changed |= ui
.add(egui::DragValue::new(value).speed(1.0).range(drag_range))
.changed();
});
ui.add_space(6.0);
}