diff --git a/src/data_processing.rs b/src/data_processing.rs index a5e54c8..c19dce4 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -72,6 +72,14 @@ pub fn generate_world_with_options( let building_passages = highways::collect_building_passage_coords(&elements, &xzbbox, args.scale); + // Pre-build a bitmap of every (x, z) block coordinate covered by a rendered + // road or path surface. Uses the same Bresenham + block_range geometry as + // generate_highways_internal, so the bitmap is a 1:1 match of what gets placed. + // Amenity processors use this for O(1) nearest-road-block lookups. + /// TODO Use this data to create overhanging traffic signals. + let road_mask = + highways::collect_road_surface_coords(&elements, &xzbbox, args.scale); + // Process all elements (no longer need to partition boundaries) let elements_count: usize = elements.len(); let process_pb: ProgressBar = ProgressBar::new(elements_count as u64); @@ -171,7 +179,7 @@ pub fn generate_world_with_options( &building_footprints, ); } else if way.tags.contains_key("amenity") { - amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache); + amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache, &road_mask); } else if way.tags.contains_key("leisure") { leisure::generate_leisure( &mut editor, @@ -226,7 +234,7 @@ pub fn generate_world_with_options( &building_footprints, ); } else if node.tags.contains_key("amenity") { - amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache); + amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache, &road_mask); } else if node.tags.contains_key("barrier") { barriers::generate_barrier_nodes(&mut editor, node); } else if node.tags.contains_key("highway") { @@ -312,6 +320,7 @@ pub fn generate_world_with_options( // Drop remaining caches drop(highway_connectivity); drop(flood_fill_cache); + drop(road_mask); // Generate ground layer (surface blocks, vegetation, shorelines, underground fill) ground_generation::generate_ground_layer( diff --git a/src/element_processing/amenities.rs b/src/element_processing/amenities.rs index dfa0991..35a591d 100644 --- a/src/element_processing/amenities.rs +++ b/src/element_processing/amenities.rs @@ -3,7 +3,7 @@ use crate::block_definitions::*; use crate::bresenham::bresenham_line; use crate::coordinate_system::cartesian::XZPoint; use crate::deterministic_rng::element_rng; -use crate::floodfill_cache::FloodFillCache; +use crate::floodfill_cache::{FloodFillCache, RoadMaskBitmap}; use crate::osm_parser::ProcessedElement; use crate::world_editor::WorldEditor; use fastnbt::Value; @@ -17,26 +17,22 @@ use std::collections::{HashMap, HashSet}; /// up to max_radius blocks away, and returns the (x, z) position of /// the nearest road node found. /// + /// Returns None if no road node exists within range. /// Callers can use the returned position to derive a facing direction, /// compute a distance, or do anything else they need. -fn nearest_road(x: i32, z: i32, max_radius: i32, editor: &WorldEditor) -> Option<(i32, i32)> { - let road_blocks = [ - GRAY_CONCRETE_POWDER, - CYAN_TERRACOTTA, - DIRT_PATH, - GRAY_CONCRETE, - BLACK_CONCRETE, - STONE_BRICKS, - ]; +fn get_nearest_road_block( + x: i32, + z: i32, + max_radius: i32, + road_mask: &RoadMaskBitmap, +) -> Option<(i32, i32)> { for dist in 1..=max_radius { // Cross pattern: North, South, West, East let candidates = [(x, z - dist), (x, z + dist), (x - dist, z), (x + dist, z)]; for (cx, cz) in candidates { - let surface_y = editor.get_ground_level(cx, cz); - - if editor.check_for_block_absolute(cx, surface_y, cz, Some(&road_blocks), None) { + if road_mask.contains(cx, cz) { return Some((cx, cz)); } } @@ -50,6 +46,7 @@ pub fn generate_amenities( element: &ProcessedElement, args: &Args, flood_fill_cache: &FloodFillCache, + road_mask: &RoadMaskBitmap, ) { // Skip if 'layer' or 'level' is negative in the tags if let Some(layer) = element.tags().get("layer") { @@ -164,7 +161,7 @@ pub fn generate_amenities( // Place a bench if let Some(pt) = first_node { let mut rng = element_rng(element.id()); - let road_pos = nearest_road(pt.x, pt.z, 5, editor); + let road_pos = get_nearest_road_block(pt.x, pt.z, 4, road_mask); let use_east_west = if let Some((rx, rz)) = road_pos { let dx = (rx - pt.x).abs(); diff --git a/src/element_processing/highways.rs b/src/element_processing/highways.rs index f34e883..dfbd0b8 100644 --- a/src/element_processing/highways.rs +++ b/src/element_processing/highways.rs @@ -1010,6 +1010,83 @@ pub(crate) fn highway_block_range( block_range } +/// Collect all (x, z) coordinates that are covered by any rendered road or path +/// surface. The returned bitmap has 1 for every block that the highway renderer +/// places as a road/path surface and 0 everywhere else. +/// +/// Geometry is computed identically to `generate_highways_internal`: +/// - Bresenham line between each consecutive pair of OSM nodes +/// - Expanded by `block_range` in both axes (same value as the renderer uses) +/// - `area=yes` ways, indoor ways, negative-level ways, and pure node types +/// (street_lamp, crossing, bus_stop) are excluded, matching the renderer's +/// early-return guards. +/// +/// This lets `find_nearest_road_block` in `amenities.rs` or other processors do a single O(1) bitmap lookup +/// instead of live `get_ground_level` + `check_for_block_absolute` world scans. +pub fn collect_road_surface_coords( + elements: &[ProcessedElement], + xzbbox: &XZBBox, + scale: f64, +) -> CoordinateBitmap { + let mut bitmap = CoordinateBitmap::new(xzbbox); + + for element in elements { + let ProcessedElement::Way(way) = element else { + continue; + }; + + let Some(highway_type) = way.tags.get("highway") else { + continue; + }; + + // Exclude non-surface node-only highway types + match highway_type.as_str() { + "street_lamp" | "crossing" | "bus_stop" => continue, + _ => {} + } + + // Exclude area highways (pedestrian plazas etc.) — flood-filled separately + if way.tags.get("area").is_some_and(|v| v == "yes") { + continue; + } + + // Exclude indoor ways (same guard as generate_highways_internal) + if way.tags.get("indoor").is_some_and(|v| v == "yes") { + continue; + } + + // Exclude negative-level ways (indoor mapping) + if way + .tags + .get("level") + .and_then(|l| l.parse::().ok()) + .is_some_and(|l| l < 0) + { + continue; + } + + // Use the same block_range the renderer uses for this highway type + let block_range = highway_block_range(highway_type, &way.tags, scale); + + for i in 1..way.nodes.len() { + let prev = way.nodes[i - 1].xz(); + let cur = way.nodes[i].xz(); + + let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z); + + for (bx, _, bz) in &points { + for dx in -block_range..=block_range { + for dz in -block_range..=block_range { + bitmap.set(bx + dx, bz + dz); + } + } + } + } + } + + bitmap +} + /// Collect all (x, z) coordinates covered by highways tagged /// `tunnel=building_passage`. The returned bitmap can be passed into building /// generation to cut ground-level openings through walls and floors. diff --git a/src/floodfill_cache.rs b/src/floodfill_cache.rs index b576aad..d4aac37 100644 --- a/src/floodfill_cache.rs +++ b/src/floodfill_cache.rs @@ -237,6 +237,12 @@ impl CoordinateBitmap { /// Type alias for building footprint bitmap (for backwards compatibility). pub type BuildingFootprintBitmap = CoordinateBitmap; +/// Type alias for the road surface bitmap used by amenity processors. +/// Built by `highways::collect_road_surface_coords` using the same Bresenham + +/// block_range geometry as the renderer, so every placed road/path block coordinate +/// is marked as 1 and everything else is 0. +pub type RoadMaskBitmap = CoordinateBitmap; + /// A cache of pre-computed flood fill results, keyed by element ID. pub struct FloodFillCache { /// Cached results: element_id -> filled coordinates