1use std::collections::VecDeque;
10use std::fmt;
11
12use glam::Vec2;
13use rand::Rng;
14use rand_pcg::Pcg64;
15use serde::{Deserialize, Serialize};
16use symbios_ground::HeightMap;
17
18use crate::graph::{RoadGraph, RoadType};
19use crate::spatial::{SpatialHash, TraceResult, resolve_trace_step};
20use crate::tensor::{TensorField, TensorFieldConfig};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum GenerationStage {
25 Config,
27 Tracer,
29 Tensor,
31 Rationalize,
33 Polygons,
35 Lots,
37 Carve,
39 Prune,
41 Roads3d,
43}
44
45impl fmt::Display for GenerationStage {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 let s = match self {
48 Self::Config => "config",
49 Self::Tracer => "tracer",
50 Self::Tensor => "tensor",
51 Self::Rationalize => "rationalize",
52 Self::Polygons => "polygons",
53 Self::Lots => "lots",
54 Self::Carve => "carve",
55 Self::Prune => "prune",
56 Self::Roads3d => "roads_3d",
57 };
58 f.write_str(s)
59 }
60}
61
62#[derive(Debug, Clone)]
69pub enum GenerationError {
70 InvalidConfig {
73 stage: GenerationStage,
75 message: String,
77 },
78 DegenerateInput {
81 stage: GenerationStage,
83 message: String,
85 },
86 Numerical {
88 stage: GenerationStage,
90 message: String,
92 },
93}
94
95impl fmt::Display for GenerationError {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 Self::InvalidConfig { stage, message } => {
99 write!(f, "[{stage}] invalid config: {message}")
100 }
101 Self::DegenerateInput { stage, message } => {
102 write!(f, "[{stage}] degenerate input: {message}")
103 }
104 Self::Numerical { stage, message } => {
105 write!(f, "[{stage}] numerical failure: {message}")
106 }
107 }
108 }
109}
110
111impl std::error::Error for GenerationError {}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct TensorConfig {
116 pub seed: u64,
118 pub step_size: f32,
120 pub major_road_dist: f32,
122 pub minor_road_dist: f32,
124 pub snap_radius: f32,
126 pub max_trace_steps: u32,
128 pub tracer_inertia: f32,
132 pub water_level: f32,
136 pub field: TensorFieldConfig,
138}
139
140impl Default for TensorConfig {
141 fn default() -> Self {
142 Self {
143 seed: 42,
144 step_size: 2.0,
145 major_road_dist: 40.0,
146 minor_road_dist: 15.0,
147 snap_radius: 4.0,
148 max_trace_steps: 300,
149 tracer_inertia: 0.8,
150 water_level: f32::NEG_INFINITY,
151 field: TensorFieldConfig::default(),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy)]
157struct Seed {
158 position: Vec2,
159 direction: Vec2,
160 road_type: RoadType,
161 branch_accum: f32,
163 existing_node: Option<u32>,
165}
166
167pub fn generate_roads(
176 heightmap: &HeightMap,
177 config: &TensorConfig,
178) -> Result<RoadGraph, GenerationError> {
179 let cfg_err = |message: String| GenerationError::InvalidConfig {
180 stage: GenerationStage::Config,
181 message,
182 };
183 if !config.step_size.is_finite() || config.step_size <= 0.0 {
184 return Err(cfg_err(format!(
185 "step_size must be finite and positive, got {}",
186 config.step_size
187 )));
188 }
189 if !config.major_road_dist.is_finite() || config.major_road_dist <= 0.0 {
190 return Err(cfg_err(format!(
191 "major_road_dist must be finite and positive, got {}",
192 config.major_road_dist
193 )));
194 }
195 if !config.minor_road_dist.is_finite() || config.minor_road_dist <= 0.0 {
196 return Err(cfg_err(format!(
197 "minor_road_dist must be finite and positive, got {}",
198 config.minor_road_dist
199 )));
200 }
201 if !config.snap_radius.is_finite() || config.snap_radius <= 0.0 {
202 return Err(cfg_err(format!(
203 "snap_radius must be finite and positive, got {}",
204 config.snap_radius
205 )));
206 }
207
208 let field = TensorField::with_config(heightmap, config.field.clone());
209 let mut graph = RoadGraph::default();
210
211 let world_w = heightmap.world_width();
212 let world_d = heightmap.world_depth();
213 let cell_size = config.snap_radius * 2.0;
214 let mut spatial = SpatialHash::new(world_w, world_d, cell_size);
215
216 let mut rng = Pcg64::new(config.seed.into(), 0xa02bdbf7bb3c0a7_u128);
217
218 let mut active: VecDeque<Seed> = VecDeque::new();
221
222 let margin = config.major_road_dist * 0.5;
223 let mut x = margin;
224 while x < world_w - margin {
225 let mut z = margin;
226 while z < world_d - margin {
227 let jitter_x: f32 = rng.random_range(-config.step_size..config.step_size);
228 let jitter_z: f32 = rng.random_range(-config.step_size..config.step_size);
229 let pos = Vec2::new(x + jitter_x, z + jitter_z);
230
231 if heightmap.get_height_at(pos.x, pos.y) <= config.water_level {
233 z += config.major_road_dist;
234 continue;
235 }
236
237 let (major, minor) = field.sample(pos.x, pos.y);
238
239 let elev = heightmap.get_height_at(pos.x, pos.y);
241 let shared_node = graph.add_node_with_elevation(pos, elev);
242 spatial.insert_node(shared_node, pos);
243
244 for &dir in &[major, -major] {
246 active.push_back(Seed {
247 position: pos,
248 direction: dir,
249 road_type: RoadType::Major,
250 branch_accum: 0.0,
251 existing_node: Some(shared_node),
252 });
253 }
254 for &dir in &[minor, -minor] {
255 active.push_back(Seed {
256 position: pos,
257 direction: dir,
258 road_type: RoadType::Minor,
259 branch_accum: 0.0,
260 existing_node: Some(shared_node),
261 });
262 }
263
264 z += config.major_road_dist;
265 }
266 x += config.major_road_dist;
267 }
268
269 let bounds = Vec2::new(world_w, world_d);
271 let area_based = ((world_w * world_d) / config.minor_road_dist) as usize;
275 let max_traces = (active.len() * 50).max(area_based);
276 let mut trace_count = 0_usize;
277 while let Some(seed) = active.pop_front() {
278 trace_count += 1;
279 if trace_count > max_traces {
280 break;
281 }
282 trace_streamline(
283 &field,
284 &mut graph,
285 &mut spatial,
286 &mut active,
287 seed,
288 config,
289 bounds,
290 );
291 }
292
293 Ok(graph)
294}
295
296fn trace_streamline(
297 field: &TensorField<'_>,
298 graph: &mut RoadGraph,
299 spatial: &mut SpatialHash,
300 active: &mut VecDeque<Seed>,
301 seed: Seed,
302 config: &TensorConfig,
303 bounds: Vec2,
304) {
305 let start_node = match seed.existing_node {
306 Some(id) => id,
307 None => {
308 let elev = field
309 .heightmap
310 .get_height_at(seed.position.x, seed.position.y);
311 let id = graph.add_node_with_elevation(seed.position, elev);
312 spatial.insert_node(id, seed.position);
313 id
314 }
315 };
316
317 let mut current_node = start_node;
318 let mut dir = seed.direction;
319 let mut branch_accum = seed.branch_accum;
320
321 for _ in 0..config.max_trace_steps {
322 let current_pos = graph.node_pos(current_node);
323
324 let (k1_major, k1_minor) = field.sample(current_pos.x, current_pos.y);
326 let k1 = match seed.road_type {
327 RoadType::Major => k1_major,
328 RoadType::Minor => k1_minor,
329 };
330 let k1 = if k1.dot(dir) < 0.0 { -k1 } else { k1 };
331
332 let mid = current_pos + k1 * (config.step_size * 0.5);
336 let k2 = if mid.x >= 0.0 && mid.x < bounds.x && mid.y >= 0.0 && mid.y < bounds.y {
337 let (k2_major, k2_minor) = field.sample(mid.x, mid.y);
338 let k2 = match seed.road_type {
339 RoadType::Major => k2_major,
340 RoadType::Minor => k2_minor,
341 };
342 if k2.dot(k1) < 0.0 { -k2 } else { k2 }
343 } else {
344 k1
345 };
346
347 let inertia = config.tracer_inertia.clamp(0.0, 0.99);
349 dir = (dir * inertia + k2 * (1.0 - inertia)).normalize_or_zero();
350 if dir.length_squared() < 1e-12 {
351 break;
352 }
353 let proposed = current_pos + dir * config.step_size;
354
355 if !proposed.x.is_finite()
357 || !proposed.y.is_finite()
358 || proposed.x < 0.0
359 || proposed.x >= bounds.x
360 || proposed.y < 0.0
361 || proposed.y >= bounds.y
362 {
363 break;
364 }
365
366 if field.heightmap.get_height_at(proposed.x, proposed.y) <= config.water_level {
368 break;
369 }
370
371 match resolve_trace_step(
372 graph,
373 spatial,
374 current_pos,
375 proposed,
376 config.snap_radius,
377 current_node,
378 ) {
379 TraceResult::Clear(pos) => {
380 let elev = field.heightmap.get_height_at(pos.x, pos.y);
381 let new_node = graph.add_node_with_elevation(pos, elev);
382 spatial.insert_node(new_node, pos);
383 let edge_id = graph.add_edge(current_node, new_node, seed.road_type);
384 spatial.insert_edge(edge_id, current_pos, pos);
385 current_node = new_node;
386 }
387 TraceResult::SnappedToNode(n_id) => {
388 let already_connected =
390 graph.nodes[current_node as usize].edges.iter().any(|&eid| {
391 let e = &graph.edges[eid as usize];
392 e.active && (e.start == n_id || e.end == n_id)
393 });
394 if !already_connected {
395 let n_pos = graph.node_pos(n_id);
396
397 let crossing =
402 find_crossing(graph, spatial, current_pos, n_pos, current_node, n_id);
403 if let Some((cross_eid, cross_pt)) = crossing {
404 let mid_node = split_or_snap_edge(graph, spatial, cross_eid, cross_pt);
405 let already = graph.nodes[current_node as usize].edges.iter().any(|&eid| {
408 let e = &graph.edges[eid as usize];
409 e.active && (e.start == mid_node || e.end == mid_node)
410 });
411 if !already {
412 let mid_pos = graph.node_pos(mid_node);
413 let connecting = graph.add_edge(current_node, mid_node, seed.road_type);
414 spatial.insert_edge(connecting, current_pos, mid_pos);
415 }
416 } else {
417 let edge_id = graph.add_edge(current_node, n_id, seed.road_type);
418 spatial.insert_edge(edge_id, current_pos, n_pos);
419 }
420 }
421 break;
422 }
423 TraceResult::SnappedToEdge {
424 edge_id,
425 intersection_pos,
426 } => {
427 let split_edge = &graph.edges[edge_id as usize];
428 let old_start_pos_pre = graph.node_pos(split_edge.start);
429 let old_end_pos_pre = graph.node_pos(split_edge.end);
430 let edge_dir = (old_end_pos_pre - old_start_pos_pre).normalize_or_zero();
431
432 let mid_node = split_or_snap_edge(graph, spatial, edge_id, intersection_pos);
433 let mid_pos = graph.node_pos(mid_node);
434
435 let crossing =
440 find_crossing(graph, spatial, current_pos, mid_pos, current_node, mid_node);
441 if let Some((cross_eid, cross_pt)) = crossing {
442 let cross_mid = split_or_snap_edge(graph, spatial, cross_eid, cross_pt);
443 let already = graph.nodes[current_node as usize].edges.iter().any(|&eid| {
446 let e = &graph.edges[eid as usize];
447 e.active && (e.start == cross_mid || e.end == cross_mid)
448 });
449 if !already {
450 let cross_pos = graph.node_pos(cross_mid);
451 let connecting_edge =
452 graph.add_edge(current_node, cross_mid, seed.road_type);
453 spatial.insert_edge(connecting_edge, current_pos, cross_pos);
454 }
455 break;
459 }
460
461 let connecting_edge = graph.add_edge(current_node, mid_node, seed.road_type);
462 spatial.insert_edge(connecting_edge, graph.node_pos(current_node), mid_pos);
463
464 let alignment = dir.dot(edge_dir).abs();
469 if alignment > 0.9 {
470 break;
471 }
472
473 current_node = mid_node;
477 }
478 }
479
480 branch_accum += config.step_size;
482 let branch_dist = match seed.road_type {
483 RoadType::Major => config.minor_road_dist,
484 RoadType::Minor => config.major_road_dist,
485 };
486 if branch_accum >= branch_dist {
487 branch_accum -= branch_dist;
488 let (field_major, field_minor) = field.sample(proposed.x, proposed.y);
489 let branch_dir = match seed.road_type {
490 RoadType::Major => field_minor,
491 RoadType::Minor => field_major,
492 };
493 let branch_type = match seed.road_type {
494 RoadType::Major => RoadType::Minor,
495 RoadType::Minor => RoadType::Major,
496 };
497 for &dir_sign in &[1.0_f32, -1.0] {
498 active.push_back(Seed {
499 position: graph.node_pos(current_node),
500 direction: branch_dir * dir_sign,
501 road_type: branch_type,
502 branch_accum: 0.0,
503 existing_node: Some(current_node),
504 });
505 }
506 }
507 }
508}
509
510const SPLIT_SNAP_DIST_SQ: f32 = 1.0;
514
515fn split_or_snap_edge(
520 graph: &mut RoadGraph,
521 spatial: &mut SpatialHash,
522 edge_id: u32,
523 split_pos: Vec2,
524) -> u32 {
525 let edge = &graph.edges[edge_id as usize];
526 let start = edge.start;
527 let end = edge.end;
528 let start_pos = graph.node_pos(start);
529 let end_pos = graph.node_pos(end);
530
531 if split_pos.distance_squared(start_pos) < SPLIT_SNAP_DIST_SQ {
533 return start;
534 }
535 if split_pos.distance_squared(end_pos) < SPLIT_SNAP_DIST_SQ {
536 return end;
537 }
538
539 let (mid_node, ea, eb) = graph.split_edge(edge_id, split_pos);
540 spatial.remove_edge(edge_id, start_pos, end_pos);
541 spatial.insert_node(mid_node, split_pos);
542 spatial.insert_edge(ea, start_pos, split_pos);
543 spatial.insert_edge(eb, split_pos, end_pos);
544 mid_node
545}
546
547fn find_crossing(
551 graph: &RoadGraph,
552 spatial: &SpatialHash,
553 from: Vec2,
554 to: Vec2,
555 from_node: u32,
556 to_node: u32,
557) -> Option<(u32, Vec2)> {
558 use crate::geometry::segment_intersection;
559
560 let edge_ids = spatial.edges_in_region(from, to, 0.0);
561 let mut best: Option<(u32, Vec2)> = None;
562 let mut best_dist = f32::MAX;
563
564 for e_id in edge_ids {
565 let edge = &graph.edges[e_id as usize];
566 if !edge.active {
567 continue;
568 }
569 if edge.start == from_node || edge.end == from_node {
570 continue;
571 }
572 if edge.start == to_node || edge.end == to_node {
573 continue;
574 }
575 let e_start = graph.nodes[edge.start as usize].position;
576 let e_end = graph.nodes[edge.end as usize].position;
577 if let Some(pt) = segment_intersection(from, to, e_start, e_end) {
578 let d = from.distance_squared(pt);
579 if d < best_dist {
580 best_dist = d;
581 best = Some((e_id, pt));
582 }
583 }
584 }
585 best
586}