Skip to main content

terrain_forge/algorithms/
glass_seam.rs

1use 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)]
8/// Configuration for glass seam bridging connectivity.
9pub struct GlassSeamConfig {
10    /// Target connectivity coverage (0.0–1.0). Default: 0.8.
11    pub coverage_threshold: f64,
12    /// Points that must be connected. Default: empty.
13    pub required_points: Vec<(usize, usize)>,
14    /// Radius of carved tunnels. Default: 1.
15    pub carve_radius: usize,
16    /// Use MST to link required terminals. Default: false.
17    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)]
32/// Glass seam bridging algorithm for connecting disconnected regions.
33pub struct GlassSeam {
34    config: GlassSeamConfig,
35}
36
37impl GlassSeam {
38    /// Creates a new glass seam generator with the given config.
39    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        // Glass Seam Bridging should only connect existing regions, not create new patterns
49        // The grid should already have floor tiles from a previous algorithm
50
51        // Find spawn point (first required point or first floor tile)
52        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 between existing regions
65        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(&regions, &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, &regions);
112            for (a, b) in edges {
113                connect_regions(grid, &regions[a], &regions[b], config.carve_radius);
114                connected.insert(a);
115                connected.insert(b);
116            }
117            coverage = coverage_for_regions(&regions, &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(&regions[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                &regions[source],
142                &regions[target],
143                config.carve_radius,
144            );
145            connected.insert(target);
146            coverage = coverage_for_regions(&regions, &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(&regions[a], &regions[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}