use std::collections::VecDeque;
use std::fmt;
use glam::Vec2;
use rand::Rng;
use rand_pcg::Pcg64;
use serde::{Deserialize, Serialize};
use symbios_ground::HeightMap;
use crate::graph::{RoadGraph, RoadType};
use crate::spatial::{SpatialHash, TraceResult, resolve_trace_step};
use crate::tensor::{TensorField, TensorFieldConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenerationStage {
Config,
Tracer,
Tensor,
Rationalize,
Polygons,
Lots,
Carve,
Prune,
Roads3d,
}
impl fmt::Display for GenerationStage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Config => "config",
Self::Tracer => "tracer",
Self::Tensor => "tensor",
Self::Rationalize => "rationalize",
Self::Polygons => "polygons",
Self::Lots => "lots",
Self::Carve => "carve",
Self::Prune => "prune",
Self::Roads3d => "roads_3d",
};
f.write_str(s)
}
}
#[derive(Debug, Clone)]
pub enum GenerationError {
InvalidConfig {
stage: GenerationStage,
message: String,
},
DegenerateInput {
stage: GenerationStage,
message: String,
},
Numerical {
stage: GenerationStage,
message: String,
},
}
impl fmt::Display for GenerationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfig { stage, message } => {
write!(f, "[{stage}] invalid config: {message}")
}
Self::DegenerateInput { stage, message } => {
write!(f, "[{stage}] degenerate input: {message}")
}
Self::Numerical { stage, message } => {
write!(f, "[{stage}] numerical failure: {message}")
}
}
}
}
impl std::error::Error for GenerationError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TensorConfig {
pub seed: u64,
pub step_size: f32,
pub major_road_dist: f32,
pub minor_road_dist: f32,
pub snap_radius: f32,
pub max_trace_steps: u32,
pub tracer_inertia: f32,
pub water_level: f32,
pub field: TensorFieldConfig,
}
impl Default for TensorConfig {
fn default() -> Self {
Self {
seed: 42,
step_size: 2.0,
major_road_dist: 40.0,
minor_road_dist: 15.0,
snap_radius: 4.0,
max_trace_steps: 300,
tracer_inertia: 0.8,
water_level: f32::NEG_INFINITY,
field: TensorFieldConfig::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Seed {
position: Vec2,
direction: Vec2,
road_type: RoadType,
branch_accum: f32,
existing_node: Option<u32>,
}
pub fn generate_roads(
heightmap: &HeightMap,
config: &TensorConfig,
) -> Result<RoadGraph, GenerationError> {
let cfg_err = |message: String| GenerationError::InvalidConfig {
stage: GenerationStage::Config,
message,
};
if !config.step_size.is_finite() || config.step_size <= 0.0 {
return Err(cfg_err(format!(
"step_size must be finite and positive, got {}",
config.step_size
)));
}
if !config.major_road_dist.is_finite() || config.major_road_dist <= 0.0 {
return Err(cfg_err(format!(
"major_road_dist must be finite and positive, got {}",
config.major_road_dist
)));
}
if !config.minor_road_dist.is_finite() || config.minor_road_dist <= 0.0 {
return Err(cfg_err(format!(
"minor_road_dist must be finite and positive, got {}",
config.minor_road_dist
)));
}
if !config.snap_radius.is_finite() || config.snap_radius <= 0.0 {
return Err(cfg_err(format!(
"snap_radius must be finite and positive, got {}",
config.snap_radius
)));
}
let field = TensorField::with_config(heightmap, config.field.clone());
let mut graph = RoadGraph::default();
let world_w = heightmap.world_width();
let world_d = heightmap.world_depth();
let cell_size = config.snap_radius * 2.0;
let mut spatial = SpatialHash::new(world_w, world_d, cell_size);
let mut rng = Pcg64::new(config.seed.into(), 0xa02bdbf7bb3c0a7_u128);
let mut active: VecDeque<Seed> = VecDeque::new();
let margin = config.major_road_dist * 0.5;
let mut x = margin;
while x < world_w - margin {
let mut z = margin;
while z < world_d - margin {
let jitter_x: f32 = rng.random_range(-config.step_size..config.step_size);
let jitter_z: f32 = rng.random_range(-config.step_size..config.step_size);
let pos = Vec2::new(x + jitter_x, z + jitter_z);
if heightmap.get_height_at(pos.x, pos.y) <= config.water_level {
z += config.major_road_dist;
continue;
}
let (major, minor) = field.sample(pos.x, pos.y);
let elev = heightmap.get_height_at(pos.x, pos.y);
let shared_node = graph.add_node_with_elevation(pos, elev);
spatial.insert_node(shared_node, pos);
for &dir in &[major, -major] {
active.push_back(Seed {
position: pos,
direction: dir,
road_type: RoadType::Major,
branch_accum: 0.0,
existing_node: Some(shared_node),
});
}
for &dir in &[minor, -minor] {
active.push_back(Seed {
position: pos,
direction: dir,
road_type: RoadType::Minor,
branch_accum: 0.0,
existing_node: Some(shared_node),
});
}
z += config.major_road_dist;
}
x += config.major_road_dist;
}
let bounds = Vec2::new(world_w, world_d);
let area_based = ((world_w * world_d) / config.minor_road_dist) as usize;
let max_traces = (active.len() * 50).max(area_based);
let mut trace_count = 0_usize;
while let Some(seed) = active.pop_front() {
trace_count += 1;
if trace_count > max_traces {
break;
}
trace_streamline(
&field,
&mut graph,
&mut spatial,
&mut active,
seed,
config,
bounds,
);
}
Ok(graph)
}
fn trace_streamline(
field: &TensorField<'_>,
graph: &mut RoadGraph,
spatial: &mut SpatialHash,
active: &mut VecDeque<Seed>,
seed: Seed,
config: &TensorConfig,
bounds: Vec2,
) {
let start_node = match seed.existing_node {
Some(id) => id,
None => {
let elev = field
.heightmap
.get_height_at(seed.position.x, seed.position.y);
let id = graph.add_node_with_elevation(seed.position, elev);
spatial.insert_node(id, seed.position);
id
}
};
let mut current_node = start_node;
let mut dir = seed.direction;
let mut branch_accum = seed.branch_accum;
for _ in 0..config.max_trace_steps {
let current_pos = graph.node_pos(current_node);
let (k1_major, k1_minor) = field.sample(current_pos.x, current_pos.y);
let k1 = match seed.road_type {
RoadType::Major => k1_major,
RoadType::Minor => k1_minor,
};
let k1 = if k1.dot(dir) < 0.0 { -k1 } else { k1 };
let mid = current_pos + k1 * (config.step_size * 0.5);
let k2 = if mid.x >= 0.0 && mid.x < bounds.x && mid.y >= 0.0 && mid.y < bounds.y {
let (k2_major, k2_minor) = field.sample(mid.x, mid.y);
let k2 = match seed.road_type {
RoadType::Major => k2_major,
RoadType::Minor => k2_minor,
};
if k2.dot(k1) < 0.0 { -k2 } else { k2 }
} else {
k1
};
let inertia = config.tracer_inertia.clamp(0.0, 0.99);
dir = (dir * inertia + k2 * (1.0 - inertia)).normalize_or_zero();
if dir.length_squared() < 1e-12 {
break;
}
let proposed = current_pos + dir * config.step_size;
if !proposed.x.is_finite()
|| !proposed.y.is_finite()
|| proposed.x < 0.0
|| proposed.x >= bounds.x
|| proposed.y < 0.0
|| proposed.y >= bounds.y
{
break;
}
if field.heightmap.get_height_at(proposed.x, proposed.y) <= config.water_level {
break;
}
match resolve_trace_step(
graph,
spatial,
current_pos,
proposed,
config.snap_radius,
current_node,
) {
TraceResult::Clear(pos) => {
let elev = field.heightmap.get_height_at(pos.x, pos.y);
let new_node = graph.add_node_with_elevation(pos, elev);
spatial.insert_node(new_node, pos);
let edge_id = graph.add_edge(current_node, new_node, seed.road_type);
spatial.insert_edge(edge_id, current_pos, pos);
current_node = new_node;
}
TraceResult::SnappedToNode(n_id) => {
let already_connected =
graph.nodes[current_node as usize].edges.iter().any(|&eid| {
let e = &graph.edges[eid as usize];
e.active && (e.start == n_id || e.end == n_id)
});
if !already_connected {
let n_pos = graph.node_pos(n_id);
let crossing =
find_crossing(graph, spatial, current_pos, n_pos, current_node, n_id);
if let Some((cross_eid, cross_pt)) = crossing {
let mid_node = split_or_snap_edge(graph, spatial, cross_eid, cross_pt);
let already = graph.nodes[current_node as usize].edges.iter().any(|&eid| {
let e = &graph.edges[eid as usize];
e.active && (e.start == mid_node || e.end == mid_node)
});
if !already {
let mid_pos = graph.node_pos(mid_node);
let connecting = graph.add_edge(current_node, mid_node, seed.road_type);
spatial.insert_edge(connecting, current_pos, mid_pos);
}
} else {
let edge_id = graph.add_edge(current_node, n_id, seed.road_type);
spatial.insert_edge(edge_id, current_pos, n_pos);
}
}
break;
}
TraceResult::SnappedToEdge {
edge_id,
intersection_pos,
} => {
let split_edge = &graph.edges[edge_id as usize];
let old_start_pos_pre = graph.node_pos(split_edge.start);
let old_end_pos_pre = graph.node_pos(split_edge.end);
let edge_dir = (old_end_pos_pre - old_start_pos_pre).normalize_or_zero();
let mid_node = split_or_snap_edge(graph, spatial, edge_id, intersection_pos);
let mid_pos = graph.node_pos(mid_node);
let crossing =
find_crossing(graph, spatial, current_pos, mid_pos, current_node, mid_node);
if let Some((cross_eid, cross_pt)) = crossing {
let cross_mid = split_or_snap_edge(graph, spatial, cross_eid, cross_pt);
let already = graph.nodes[current_node as usize].edges.iter().any(|&eid| {
let e = &graph.edges[eid as usize];
e.active && (e.start == cross_mid || e.end == cross_mid)
});
if !already {
let cross_pos = graph.node_pos(cross_mid);
let connecting_edge =
graph.add_edge(current_node, cross_mid, seed.road_type);
spatial.insert_edge(connecting_edge, current_pos, cross_pos);
}
break;
}
let connecting_edge = graph.add_edge(current_node, mid_node, seed.road_type);
spatial.insert_edge(connecting_edge, graph.node_pos(current_node), mid_pos);
let alignment = dir.dot(edge_dir).abs();
if alignment > 0.9 {
break;
}
current_node = mid_node;
}
}
branch_accum += config.step_size;
let branch_dist = match seed.road_type {
RoadType::Major => config.minor_road_dist,
RoadType::Minor => config.major_road_dist,
};
if branch_accum >= branch_dist {
branch_accum -= branch_dist;
let (field_major, field_minor) = field.sample(proposed.x, proposed.y);
let branch_dir = match seed.road_type {
RoadType::Major => field_minor,
RoadType::Minor => field_major,
};
let branch_type = match seed.road_type {
RoadType::Major => RoadType::Minor,
RoadType::Minor => RoadType::Major,
};
for &dir_sign in &[1.0_f32, -1.0] {
active.push_back(Seed {
position: graph.node_pos(current_node),
direction: branch_dir * dir_sign,
road_type: branch_type,
branch_accum: 0.0,
existing_node: Some(current_node),
});
}
}
}
}
const SPLIT_SNAP_DIST_SQ: f32 = 1.0;
fn split_or_snap_edge(
graph: &mut RoadGraph,
spatial: &mut SpatialHash,
edge_id: u32,
split_pos: Vec2,
) -> u32 {
let edge = &graph.edges[edge_id as usize];
let start = edge.start;
let end = edge.end;
let start_pos = graph.node_pos(start);
let end_pos = graph.node_pos(end);
if split_pos.distance_squared(start_pos) < SPLIT_SNAP_DIST_SQ {
return start;
}
if split_pos.distance_squared(end_pos) < SPLIT_SNAP_DIST_SQ {
return end;
}
let (mid_node, ea, eb) = graph.split_edge(edge_id, split_pos);
spatial.remove_edge(edge_id, start_pos, end_pos);
spatial.insert_node(mid_node, split_pos);
spatial.insert_edge(ea, start_pos, split_pos);
spatial.insert_edge(eb, split_pos, end_pos);
mid_node
}
fn find_crossing(
graph: &RoadGraph,
spatial: &SpatialHash,
from: Vec2,
to: Vec2,
from_node: u32,
to_node: u32,
) -> Option<(u32, Vec2)> {
use crate::geometry::segment_intersection;
let edge_ids = spatial.edges_in_region(from, to, 0.0);
let mut best: Option<(u32, Vec2)> = None;
let mut best_dist = f32::MAX;
for e_id in edge_ids {
let edge = &graph.edges[e_id as usize];
if !edge.active {
continue;
}
if edge.start == from_node || edge.end == from_node {
continue;
}
if edge.start == to_node || edge.end == to_node {
continue;
}
let e_start = graph.nodes[edge.start as usize].position;
let e_end = graph.nodes[edge.end as usize].position;
if let Some(pt) = segment_intersection(from, to, e_start, e_end) {
let d = from.distance_squared(pt);
if d < best_dist {
best_dist = d;
best = Some((e_id, pt));
}
}
}
best
}