1use std::collections::HashMap;
33
34use glam::Vec2;
35use symbios_ground::HeightMap;
36
37use crate::graph::RoadGraph;
38use crate::lots::{BuildingLot, LotConfig, extract_lots};
39use crate::polygons::extract_blocks;
40use crate::rationalize::{RationalizeConfig, rationalize_graph};
41use crate::tracer::{GenerationError, TensorConfig, generate_roads};
42
43#[derive(Debug, Clone)]
45pub struct CityStreamerConfig {
46 pub tile_size: f32,
49 pub base_seed: u64,
53 pub tensor: TensorConfig,
56 pub rationalize: RationalizeConfig,
58 pub lots: LotConfig,
60}
61
62#[derive(Debug, Clone)]
66pub struct CityTile {
67 pub tile_x: i32,
69 pub tile_z: i32,
71 pub origin: Vec2,
73 pub size: f32,
75 pub graph: RoadGraph,
77 pub lots: Vec<BuildingLot>,
79 pub heightmap: HeightMap,
82}
83
84pub struct CityStreamer<P>
91where
92 P: FnMut(i32, i32) -> HeightMap,
93{
94 config: CityStreamerConfig,
95 provider: P,
96 cache: HashMap<(i32, i32), CityTile>,
97}
98
99impl<P> CityStreamer<P>
100where
101 P: FnMut(i32, i32) -> HeightMap,
102{
103 pub fn new(config: CityStreamerConfig, provider: P) -> Self {
109 assert!(
110 config.tile_size.is_finite() && config.tile_size > 0.0,
111 "tile_size must be positive and finite, got {}",
112 config.tile_size
113 );
114 Self {
115 config,
116 provider,
117 cache: HashMap::new(),
118 }
119 }
120
121 pub fn tile_size(&self) -> f32 {
123 self.config.tile_size
124 }
125
126 pub fn cached_tile_count(&self) -> usize {
128 self.cache.len()
129 }
130
131 pub fn tile_coord_for(&self, world_pos: Vec2) -> (i32, i32) {
133 let s = self.config.tile_size;
134 (
135 (world_pos.x / s).floor() as i32,
136 (world_pos.y / s).floor() as i32,
137 )
138 }
139
140 pub fn ensure_tile(&mut self, tile_x: i32, tile_z: i32) -> Result<&CityTile, GenerationError> {
143 if !self.cache.contains_key(&(tile_x, tile_z)) {
144 let tile = self.generate_tile(tile_x, tile_z)?;
145 self.cache.insert((tile_x, tile_z), tile);
146 }
147 Ok(&self.cache[&(tile_x, tile_z)])
148 }
149
150 pub fn query_region(
153 &mut self,
154 min: Vec2,
155 max: Vec2,
156 ) -> Result<Vec<&CityTile>, GenerationError> {
157 let s = self.config.tile_size;
158 if !min.x.is_finite() || !min.y.is_finite() || !max.x.is_finite() || !max.y.is_finite() {
159 return Ok(Vec::new());
160 }
161 let tx_min = (min.x / s).floor() as i32;
162 let tz_min = (min.y / s).floor() as i32;
163 let tx_max = ((max.x / s).floor() as i32).max(tx_min);
164 let tz_max = ((max.y / s).floor() as i32).max(tz_min);
165
166 for tz in tz_min..=tz_max {
167 for tx in tx_min..=tx_max {
168 self.ensure_tile(tx, tz)?;
169 }
170 }
171
172 let mut out = Vec::new();
173 for tz in tz_min..=tz_max {
174 for tx in tx_min..=tx_max {
175 if let Some(t) = self.cache.get(&(tx, tz)) {
176 out.push(t);
177 }
178 }
179 }
180 Ok(out)
181 }
182
183 pub fn evict_outside(&mut self, center: Vec2, max_distance: f32) -> usize {
186 let s = self.config.tile_size;
187 let max_sq = max_distance * max_distance;
188 let to_remove: Vec<(i32, i32)> = self
189 .cache
190 .keys()
191 .filter(|&&(tx, tz)| {
192 let tile_center = Vec2::new((tx as f32 + 0.5) * s, (tz as f32 + 0.5) * s);
193 tile_center.distance_squared(center) > max_sq
194 })
195 .copied()
196 .collect();
197 let n = to_remove.len();
198 for k in to_remove {
199 self.cache.remove(&k);
200 }
201 n
202 }
203
204 fn generate_tile(&mut self, tile_x: i32, tile_z: i32) -> Result<CityTile, GenerationError> {
205 let mut heightmap = (self.provider)(tile_x, tile_z);
206 let s = self.config.tile_size;
207 let provided_w = heightmap.world_width();
208 let provided_d = heightmap.world_depth();
209 debug_assert!(
210 (provided_w - s).abs() < 1e-3 && (provided_d - s).abs() < 1e-3,
211 "heightmap_provider returned heightmap of {provided_w}x{provided_d} world units, expected {s}x{s}",
212 );
213
214 let mut tensor_config = self.config.tensor.clone();
215 tensor_config.seed = mix_seed(self.config.base_seed, tile_x, tile_z);
216
217 let mut graph = generate_roads(&heightmap, &tensor_config)?;
218 rationalize_graph(&mut graph, &heightmap, &self.config.rationalize);
219 extract_blocks(&mut graph);
220 let lots = extract_lots(&graph, &mut heightmap, &self.config.lots);
221
222 let origin = Vec2::new(tile_x as f32 * s, tile_z as f32 * s);
224 for node in &mut graph.nodes {
225 node.position += origin;
226 }
227 let mut lots = lots;
228 for lot in &mut lots {
229 lot.position += origin;
230 lot.frontage_center += origin;
231 }
232
233 Ok(CityTile {
234 tile_x,
235 tile_z,
236 origin,
237 size: s,
238 graph,
239 lots,
240 heightmap,
241 })
242 }
243}
244
245fn mix_seed(base: u64, tile_x: i32, tile_z: i32) -> u64 {
249 let x = tile_x as i64 as u64;
250 let z = tile_z as i64 as u64;
251 let mut h = base ^ x.wrapping_mul(0x9E3779B97F4A7C15);
252 h = h.wrapping_add(z.wrapping_mul(0xBF58476D1CE4E5B9));
253 h ^= h >> 30;
254 h = h.wrapping_mul(0xBF58476D1CE4E5B9);
255 h ^= h >> 27;
256 h = h.wrapping_mul(0x94D049BB133111EB);
257 h ^ (h >> 31)
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn make_streamer() -> CityStreamer<impl FnMut(i32, i32) -> HeightMap> {
265 let cfg = CityStreamerConfig {
266 tile_size: 32.0,
267 base_seed: 7,
268 tensor: TensorConfig {
269 step_size: 1.0,
270 major_road_dist: 12.0,
271 minor_road_dist: 6.0,
272 snap_radius: 2.0,
273 max_trace_steps: 80,
274 ..Default::default()
275 },
276 rationalize: RationalizeConfig::default(),
277 lots: LotConfig::default(),
278 };
279
280 let provider = move |tile_x: i32, tile_z: i32| {
283 let mut hm = HeightMap::new(32, 32, 1.0);
284 for z in 0..32 {
285 for x in 0..32 {
286 let global_x = tile_x as f32 * 32.0 + x as f32;
287 let global_z = tile_z as f32 * 32.0 + z as f32;
288 let h = (global_x * 0.05).sin() * 2.0 + (global_z * 0.07).cos();
289 hm.set(x, z, h);
290 }
291 }
292 hm
293 };
294
295 CityStreamer::new(cfg, provider)
296 }
297
298 #[test]
299 fn ensure_tile_caches() {
300 let mut s = make_streamer();
301 assert_eq!(s.cached_tile_count(), 0);
302 s.ensure_tile(0, 0).expect("generate (0,0)");
303 assert_eq!(s.cached_tile_count(), 1);
304 s.ensure_tile(0, 0).expect("re-request (0,0)");
306 assert_eq!(s.cached_tile_count(), 1);
307 }
308
309 #[test]
310 fn tile_nodes_in_world_coordinates() {
311 let mut s = make_streamer();
312 let tile = s.ensure_tile(2, 3).expect("generate").clone();
313 let origin = tile.origin;
314 let size = tile.size;
315 for node in &tile.graph.nodes {
316 assert!(
317 node.position.x >= origin.x - 1e-3
318 && node.position.x <= origin.x + size + 1e-3
319 && node.position.y >= origin.y - 1e-3
320 && node.position.y <= origin.y + size + 1e-3,
321 "node at {:?} outside tile (origin={origin:?}, size={size})",
322 node.position
323 );
324 }
325 }
326
327 #[test]
328 fn query_region_spans_multiple_tiles() {
329 let mut s = make_streamer();
330 let tiles = s
331 .query_region(Vec2::new(-10.0, -10.0), Vec2::new(50.0, 50.0))
332 .expect("query_region");
333 assert_eq!(tiles.len(), 9, "expected 9 tiles, got {}", tiles.len());
336 }
337
338 #[test]
339 fn evict_outside_drops_far_tiles() {
340 let mut s = make_streamer();
341 s.ensure_tile(0, 0).expect("(0,0)");
342 s.ensure_tile(5, 5).expect("(5,5)");
343 assert_eq!(s.cached_tile_count(), 2);
344
345 let evicted = s.evict_outside(Vec2::new(16.0, 16.0), 100.0);
348 assert_eq!(evicted, 1);
349 assert_eq!(s.cached_tile_count(), 1);
350 }
351
352 #[test]
353 fn deterministic_seeds_per_tile() {
354 let mut s1 = make_streamer();
355 let mut s2 = make_streamer();
356 let t1 = s1.ensure_tile(3, 4).unwrap().clone();
357 let t2 = s2.ensure_tile(3, 4).unwrap().clone();
358 assert_eq!(t1.graph.nodes.len(), t2.graph.nodes.len());
359 for (a, b) in t1.graph.nodes.iter().zip(t2.graph.nodes.iter()) {
360 assert!((a.position - b.position).length() < 1e-5);
361 }
362 }
363}