use std::collections::HashMap;
use glam::Vec2;
use symbios_ground::HeightMap;
use crate::graph::RoadGraph;
use crate::lots::{BuildingLot, LotConfig, extract_lots};
use crate::polygons::extract_blocks;
use crate::rationalize::{RationalizeConfig, rationalize_graph};
use crate::tracer::{GenerationError, TensorConfig, generate_roads};
#[derive(Debug, Clone)]
pub struct CityStreamerConfig {
pub tile_size: f32,
pub base_seed: u64,
pub tensor: TensorConfig,
pub rationalize: RationalizeConfig,
pub lots: LotConfig,
}
#[derive(Debug, Clone)]
pub struct CityTile {
pub tile_x: i32,
pub tile_z: i32,
pub origin: Vec2,
pub size: f32,
pub graph: RoadGraph,
pub lots: Vec<BuildingLot>,
pub heightmap: HeightMap,
}
pub struct CityStreamer<P>
where
P: FnMut(i32, i32) -> HeightMap,
{
config: CityStreamerConfig,
provider: P,
cache: HashMap<(i32, i32), CityTile>,
}
impl<P> CityStreamer<P>
where
P: FnMut(i32, i32) -> HeightMap,
{
pub fn new(config: CityStreamerConfig, provider: P) -> Self {
assert!(
config.tile_size.is_finite() && config.tile_size > 0.0,
"tile_size must be positive and finite, got {}",
config.tile_size
);
Self {
config,
provider,
cache: HashMap::new(),
}
}
pub fn tile_size(&self) -> f32 {
self.config.tile_size
}
pub fn cached_tile_count(&self) -> usize {
self.cache.len()
}
pub fn tile_coord_for(&self, world_pos: Vec2) -> (i32, i32) {
let s = self.config.tile_size;
(
(world_pos.x / s).floor() as i32,
(world_pos.y / s).floor() as i32,
)
}
pub fn ensure_tile(&mut self, tile_x: i32, tile_z: i32) -> Result<&CityTile, GenerationError> {
if !self.cache.contains_key(&(tile_x, tile_z)) {
let tile = self.generate_tile(tile_x, tile_z)?;
self.cache.insert((tile_x, tile_z), tile);
}
Ok(&self.cache[&(tile_x, tile_z)])
}
pub fn query_region(
&mut self,
min: Vec2,
max: Vec2,
) -> Result<Vec<&CityTile>, GenerationError> {
let s = self.config.tile_size;
if !min.x.is_finite() || !min.y.is_finite() || !max.x.is_finite() || !max.y.is_finite() {
return Ok(Vec::new());
}
let tx_min = (min.x / s).floor() as i32;
let tz_min = (min.y / s).floor() as i32;
let tx_max = ((max.x / s).floor() as i32).max(tx_min);
let tz_max = ((max.y / s).floor() as i32).max(tz_min);
for tz in tz_min..=tz_max {
for tx in tx_min..=tx_max {
self.ensure_tile(tx, tz)?;
}
}
let mut out = Vec::new();
for tz in tz_min..=tz_max {
for tx in tx_min..=tx_max {
if let Some(t) = self.cache.get(&(tx, tz)) {
out.push(t);
}
}
}
Ok(out)
}
pub fn evict_outside(&mut self, center: Vec2, max_distance: f32) -> usize {
let s = self.config.tile_size;
let max_sq = max_distance * max_distance;
let to_remove: Vec<(i32, i32)> = self
.cache
.keys()
.filter(|&&(tx, tz)| {
let tile_center = Vec2::new((tx as f32 + 0.5) * s, (tz as f32 + 0.5) * s);
tile_center.distance_squared(center) > max_sq
})
.copied()
.collect();
let n = to_remove.len();
for k in to_remove {
self.cache.remove(&k);
}
n
}
fn generate_tile(&mut self, tile_x: i32, tile_z: i32) -> Result<CityTile, GenerationError> {
let mut heightmap = (self.provider)(tile_x, tile_z);
let s = self.config.tile_size;
let provided_w = heightmap.world_width();
let provided_d = heightmap.world_depth();
debug_assert!(
(provided_w - s).abs() < 1e-3 && (provided_d - s).abs() < 1e-3,
"heightmap_provider returned heightmap of {provided_w}x{provided_d} world units, expected {s}x{s}",
);
let mut tensor_config = self.config.tensor.clone();
tensor_config.seed = mix_seed(self.config.base_seed, tile_x, tile_z);
let mut graph = generate_roads(&heightmap, &tensor_config)?;
rationalize_graph(&mut graph, &heightmap, &self.config.rationalize);
extract_blocks(&mut graph);
let lots = extract_lots(&graph, &mut heightmap, &self.config.lots);
let origin = Vec2::new(tile_x as f32 * s, tile_z as f32 * s);
for node in &mut graph.nodes {
node.position += origin;
}
let mut lots = lots;
for lot in &mut lots {
lot.position += origin;
lot.frontage_center += origin;
}
Ok(CityTile {
tile_x,
tile_z,
origin,
size: s,
graph,
lots,
heightmap,
})
}
}
fn mix_seed(base: u64, tile_x: i32, tile_z: i32) -> u64 {
let x = tile_x as i64 as u64;
let z = tile_z as i64 as u64;
let mut h = base ^ x.wrapping_mul(0x9E3779B97F4A7C15);
h = h.wrapping_add(z.wrapping_mul(0xBF58476D1CE4E5B9));
h ^= h >> 30;
h = h.wrapping_mul(0xBF58476D1CE4E5B9);
h ^= h >> 27;
h = h.wrapping_mul(0x94D049BB133111EB);
h ^ (h >> 31)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_streamer() -> CityStreamer<impl FnMut(i32, i32) -> HeightMap> {
let cfg = CityStreamerConfig {
tile_size: 32.0,
base_seed: 7,
tensor: TensorConfig {
step_size: 1.0,
major_road_dist: 12.0,
minor_road_dist: 6.0,
snap_radius: 2.0,
max_trace_steps: 80,
..Default::default()
},
rationalize: RationalizeConfig::default(),
lots: LotConfig::default(),
};
let provider = move |tile_x: i32, tile_z: i32| {
let mut hm = HeightMap::new(32, 32, 1.0);
for z in 0..32 {
for x in 0..32 {
let global_x = tile_x as f32 * 32.0 + x as f32;
let global_z = tile_z as f32 * 32.0 + z as f32;
let h = (global_x * 0.05).sin() * 2.0 + (global_z * 0.07).cos();
hm.set(x, z, h);
}
}
hm
};
CityStreamer::new(cfg, provider)
}
#[test]
fn ensure_tile_caches() {
let mut s = make_streamer();
assert_eq!(s.cached_tile_count(), 0);
s.ensure_tile(0, 0).expect("generate (0,0)");
assert_eq!(s.cached_tile_count(), 1);
s.ensure_tile(0, 0).expect("re-request (0,0)");
assert_eq!(s.cached_tile_count(), 1);
}
#[test]
fn tile_nodes_in_world_coordinates() {
let mut s = make_streamer();
let tile = s.ensure_tile(2, 3).expect("generate").clone();
let origin = tile.origin;
let size = tile.size;
for node in &tile.graph.nodes {
assert!(
node.position.x >= origin.x - 1e-3
&& node.position.x <= origin.x + size + 1e-3
&& node.position.y >= origin.y - 1e-3
&& node.position.y <= origin.y + size + 1e-3,
"node at {:?} outside tile (origin={origin:?}, size={size})",
node.position
);
}
}
#[test]
fn query_region_spans_multiple_tiles() {
let mut s = make_streamer();
let tiles = s
.query_region(Vec2::new(-10.0, -10.0), Vec2::new(50.0, 50.0))
.expect("query_region");
assert_eq!(tiles.len(), 9, "expected 9 tiles, got {}", tiles.len());
}
#[test]
fn evict_outside_drops_far_tiles() {
let mut s = make_streamer();
s.ensure_tile(0, 0).expect("(0,0)");
s.ensure_tile(5, 5).expect("(5,5)");
assert_eq!(s.cached_tile_count(), 2);
let evicted = s.evict_outside(Vec2::new(16.0, 16.0), 100.0);
assert_eq!(evicted, 1);
assert_eq!(s.cached_tile_count(), 1);
}
#[test]
fn deterministic_seeds_per_tile() {
let mut s1 = make_streamer();
let mut s2 = make_streamer();
let t1 = s1.ensure_tile(3, 4).unwrap().clone();
let t2 = s2.ensure_tile(3, 4).unwrap().clone();
assert_eq!(t1.graph.nodes.len(), t2.graph.nodes.len());
for (a, b) in t1.graph.nodes.iter().zip(t2.graph.nodes.iter()) {
assert!((a.position - b.position).length() < 1e-5);
}
}
}