terrain_forge/algorithms/
glass_seam.rs1use crate::effects::carve_path;
2use crate::grid::line_points;
3use crate::{Algorithm, Grid, Tile};
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct GlassSeamConfig {
10 pub coverage_threshold: f64,
12 pub required_points: Vec<(usize, usize)>,
14 pub carve_radius: usize,
16 pub use_mst_terminals: bool,
18}
19
20impl Default for GlassSeamConfig {
21 fn default() -> Self {
22 Self {
23 coverage_threshold: 0.75,
24 required_points: Vec::new(),
25 carve_radius: 0,
26 use_mst_terminals: true,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct GlassSeam {
34 config: GlassSeamConfig,
35}
36
37impl GlassSeam {
38 pub fn new(config: GlassSeamConfig) -> Self {
40 Self { config }
41 }
42}
43
44impl Algorithm<Tile> for GlassSeam {
45 fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
46 let _seed = seed;
47
48 let spawn = self
53 .config
54 .required_points
55 .iter()
56 .copied()
57 .find(|&(x, y)| {
58 grid.get(x as i32, y as i32)
59 .is_some_and(|tile| tile.is_floor())
60 })
61 .or_else(|| find_spawn_point(grid))
62 .unwrap_or((5, 5));
63
64 ensure_connectivity(grid, spawn, &self.config);
66 }
67
68 fn name(&self) -> &'static str {
69 "GlassSeam"
70 }
71}
72
73fn find_spawn_point(grid: &Grid<Tile>) -> Option<(usize, usize)> {
74 for y in 0..grid.height() {
75 for x in 0..grid.width() {
76 if grid[(x, y)].is_floor() {
77 return Some((x, y));
78 }
79 }
80 }
81 None
82}
83
84fn ensure_connectivity(grid: &mut Grid<Tile>, spawn: (usize, usize), config: &GlassSeamConfig) {
85 let RegionData {
86 regions,
87 labels,
88 width,
89 } = identify_regions(grid);
90 if regions.len() <= 1 {
91 return;
92 }
93
94 let spawn_region = match region_for_point(&labels, width, spawn) {
95 Some(region) => region,
96 None => return,
97 };
98 let total_floor: usize = regions.iter().map(|r| r.len()).sum();
99 let mut connected: HashSet<usize> = HashSet::new();
100 connected.insert(spawn_region);
101 let mut coverage = coverage_for_regions(®ions, &connected, total_floor);
102
103 if coverage >= config.coverage_threshold {
104 return;
105 }
106
107 if config.use_mst_terminals {
108 let required_regions =
109 required_regions(&labels, width, &config.required_points, spawn_region);
110 if required_regions.len() > 1 {
111 let edges = mst_edges(&required_regions, ®ions);
112 for (a, b) in edges {
113 connect_regions(grid, ®ions[a], ®ions[b], config.carve_radius);
114 connected.insert(a);
115 connected.insert(b);
116 }
117 coverage = coverage_for_regions(®ions, &connected, total_floor);
118 }
119 }
120
121 while coverage < config.coverage_threshold && connected.len() < regions.len() {
122 let mut best = None;
123 let mut best_cost = usize::MAX;
124
125 for (i, region) in regions.iter().enumerate() {
126 if connected.contains(&i) {
127 continue;
128 }
129 for &ci in &connected {
130 let cost = connection_cost(®ions[ci], region);
131 if cost < best_cost {
132 best_cost = cost;
133 best = Some((i, ci));
134 }
135 }
136 }
137
138 if let Some((target, source)) = best {
139 connect_regions(
140 grid,
141 ®ions[source],
142 ®ions[target],
143 config.carve_radius,
144 );
145 connected.insert(target);
146 coverage = coverage_for_regions(®ions, &connected, total_floor);
147 } else {
148 break;
149 }
150 }
151}
152
153struct RegionData {
154 regions: Vec<Vec<(usize, usize)>>,
155 labels: Vec<u32>,
156 width: usize,
157}
158
159fn identify_regions(grid: &Grid<Tile>) -> RegionData {
160 let w = grid.width();
161 let regions = grid.flood_regions();
162 let mut labels = vec![0u32; w * grid.height()];
163 for (i, region) in regions.iter().enumerate() {
164 let label = (i + 1) as u32;
165 for &(x, y) in region {
166 labels[y * w + x] = label;
167 }
168 }
169 RegionData {
170 regions,
171 labels,
172 width: w,
173 }
174}
175
176fn region_for_point(labels: &[u32], width: usize, point: (usize, usize)) -> Option<usize> {
177 if width == 0 {
178 return None;
179 }
180 let height = labels.len() / width;
181 if point.0 >= width || point.1 >= height {
182 return None;
183 }
184 let idx = point.1 * width + point.0;
185 let label = *labels.get(idx)?;
186 if label == 0 {
187 None
188 } else {
189 Some((label - 1) as usize)
190 }
191}
192
193fn required_regions(
194 labels: &[u32],
195 width: usize,
196 points: &[(usize, usize)],
197 spawn_region: usize,
198) -> Vec<usize> {
199 let mut set = HashSet::new();
200 set.insert(spawn_region);
201 for &point in points {
202 if let Some(region) = region_for_point(labels, width, point) {
203 set.insert(region);
204 }
205 }
206 set.into_iter().collect()
207}
208
209fn coverage_for_regions(
210 regions: &[Vec<(usize, usize)>],
211 connected: &HashSet<usize>,
212 total: usize,
213) -> f64 {
214 if total == 0 {
215 return 0.0;
216 }
217 let connected_cells: usize = connected.iter().map(|&idx| regions[idx].len()).sum();
218 connected_cells as f64 / total as f64
219}
220
221fn mst_edges(required: &[usize], regions: &[Vec<(usize, usize)>]) -> Vec<(usize, usize)> {
222 if required.len() < 2 {
223 return Vec::new();
224 }
225
226 let mut in_tree = HashSet::new();
227 in_tree.insert(required[0]);
228 let mut edges = Vec::new();
229
230 while in_tree.len() < required.len() {
231 let mut best = None;
232 let mut best_cost = usize::MAX;
233
234 for &a in &in_tree {
235 for &b in required {
236 if in_tree.contains(&b) {
237 continue;
238 }
239 let cost = connection_cost(®ions[a], ®ions[b]);
240 if cost < best_cost {
241 best_cost = cost;
242 best = Some((a, b));
243 }
244 }
245 }
246
247 if let Some((a, b)) = best {
248 edges.push((a, b));
249 in_tree.insert(b);
250 } else {
251 break;
252 }
253 }
254
255 edges
256}
257
258fn connection_cost(a: &[(usize, usize)], b: &[(usize, usize)]) -> usize {
259 let ca = centroid(a);
260 let cb = centroid(b);
261 ((ca.0 as i32 - cb.0 as i32).abs() + (ca.1 as i32 - cb.1 as i32).abs()) as usize
262}
263
264fn centroid(region: &[(usize, usize)]) -> (usize, usize) {
265 if region.is_empty() {
266 return (0, 0);
267 }
268 let sx: usize = region.iter().map(|p| p.0).sum();
269 let sy: usize = region.iter().map(|p| p.1).sum();
270 (sx / region.len(), sy / region.len())
271}
272
273fn connect_regions(
274 grid: &mut Grid<Tile>,
275 source: &[(usize, usize)],
276 target: &[(usize, usize)],
277 radius: usize,
278) {
279 let from = centroid(source);
280 let to = centroid(target);
281
282 let path = line_points(from, to);
283 carve_path(grid, &path, radius);
284}