diff --git a/src/data_processing.rs b/src/data_processing.rs index ccb0a5a2d..bc62cff8f 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -6,7 +6,9 @@ use crate::floodfill_cache::FloodFillCache; use crate::ground::Ground; use crate::ground_generation; use crate::map_renderer; -use crate::osm_parser::{ProcessedElement, ProcessedMemberRole}; +use crate::osm_parser::{ + ProcessedElement, ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay, +}; use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_show_in_folder}; #[cfg(feature = "gui")] use crate::telemetry::{send_log, LogLevel}; @@ -26,6 +28,69 @@ pub struct GenerationOptions { pub spawn_point: Option<(i32, i32)>, } +static SMALLEST_AREA_TAGS: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { + vec![ + "landuse".to_string(), + "leisure".to_string(), + "natural".to_string(), + ] +}); + +fn accumulate_smallest_area( + editor: &mut WorldEditor, + way: &ProcessedWay, + tag_key: &str, + flood_fill_cache: &FloodFillCache, + args: &Args, +) { + let Some(tag_value) = way.tags.get(tag_key) else { + return; + }; + + let filled = flood_fill_cache.get_or_compute(way, args.timeout.as_ref()); + let area = filled.len() as f64; + + let tag_static: &'static str = Box::leak(tag_value.clone().into_boxed_str()); + + for &(x, z) in filled.iter() { + editor.set_area_if_smaller(x, z, area, way.id, tag_static); + } +} + +fn accumulate_smallest_area_from_relation( + editor: &mut WorldEditor, + rel: &ProcessedRelation, + tag_key: &str, + flood_fill_cache: &FloodFillCache, + args: &Args, +) { + if !rel.tags.contains_key(tag_key) { + return; + } + + // Collect outer ways as node list + let mut outers: Vec> = Vec::new(); + + for member in &rel.members { + if member.role == ProcessedMemberRole::Outer { + outers.push(member.way.nodes.clone()); + } + } + + // Merge outer ways to polygon + merge_way_segments(&mut outers); + + // Fill every merged ring as one way + for ring in outers { + let way = ProcessedWay { + id: rel.id, + nodes: ring, + tags: rel.tags.clone(), + }; + + accumulate_smallest_area(editor, &way, tag_key, flood_fill_cache, args); + } +} /// Generate world with explicit format options (used by GUI for Bedrock support) pub fn generate_world_with_options( elements: Vec, @@ -129,6 +194,39 @@ pub fn generate_world_with_options( outlines }; + // Check for smallest areas per (x,z) + for element in &elements { + match element { + ProcessedElement::Way(way) => { + for tag_key in SMALLEST_AREA_TAGS.iter() { + if way.tags.contains_key(tag_key) { + accumulate_smallest_area( + &mut editor, + way, + tag_key, + &flood_fill_cache, + args, + ); + } + } + } + ProcessedElement::Relation(rel) => { + for tag_key in SMALLEST_AREA_TAGS.iter() { + if rel.tags.contains_key(tag_key) { + accumulate_smallest_area_from_relation( + &mut editor, + rel, + tag_key, + &flood_fill_cache, + args, + ); + } + } + } + _ => {} + } + } + // Process all elements for element in elements.into_iter() { element_counter += 1; diff --git a/src/element_processing/landuse.rs b/src/element_processing/landuse.rs index b67c63be8..a1e6ccd62 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -80,6 +80,15 @@ pub fn generate_landuse( }; for &(x, z) in floor_area.iter() { + // Only the element that has the smallest area for (x,z) is allowed to render here + if let Some(cell) = editor.area_map.get(&(x, z)) { + if cell.element_id != element.id { + continue; + } + } else { + continue; + } + // Apply per-block randomness for certain landuse types let actual_block = if landuse_tag == "industrial" { // Industrial: primarily stone, with some stone bricks and smooth stone diff --git a/src/element_processing/leisure.rs b/src/element_processing/leisure.rs index 6369cbc26..328ffd366 100644 --- a/src/element_processing/leisure.rs +++ b/src/element_processing/leisure.rs @@ -3,7 +3,6 @@ use crate::block_definitions::*; use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::surfaces::get_blocks_for_surface; -use crate::element_processing::tree::Tree; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; @@ -14,7 +13,7 @@ pub fn generate_leisure( element: &ProcessedWay, args: &Args, flood_fill_cache: &FloodFillCache, - building_footprints: &BuildingFootprintBitmap, + _building_footprints: &BuildingFootprintBitmap, ) { if let Some(leisure_type) = element.tags.get("leisure") { let mut previous_node: Option<(i32, i32)> = None; @@ -86,6 +85,15 @@ pub fn generate_leisure( let mut rng = element_rng(element.id); for &(x, z) in filled_area.iter() { + // Only the element that has the smallest area for (x,z) is allowed to render here + if let Some(cell) = editor.area_map.get(&(x, z)) { + if cell.element_id != element.id { + continue; + } + } else { + continue; + } + editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None); // Add decorative elements for parks and gardens @@ -115,10 +123,6 @@ pub fn generate_leisure( // Oak leaves editor.set_block(OAK_LEAVES, x, 1, z, None, None); } - 105..120 => { - // Tree - Tree::create(editor, (x, 1, z), Some(building_footprints)); - } _ => {} } } diff --git a/src/element_processing/natural.rs b/src/element_processing/natural.rs index 361cdfbbb..d0434e0f3 100644 --- a/src/element_processing/natural.rs +++ b/src/element_processing/natural.rs @@ -214,6 +214,15 @@ pub fn generate_natural( ]; for &(x, z) in filled_area.iter() { + // Only the element that has the smallest area for (x,z) is allowed to render here + if let Some(cell) = editor.area_map.get(&(x, z)) { + if cell.element_id != way.id { + continue; + } + } else { + continue; + } + // Don't overwrite road/path blocks with natural ground if !editor.check_for_block(x, 0, z, Some(protected_blocks)) { let b = if rock_variation { diff --git a/src/ground_generation.rs b/src/ground_generation.rs index 130731766..d948f2dbc 100644 --- a/src/ground_generation.rs +++ b/src/ground_generation.rs @@ -729,8 +729,19 @@ pub fn generate_ground_layer( land_cover::LC_TREE_COVER if slope <= 4 && ground_allows_trees => { - let choice = rng.random_range(0..30); + let choice = rng.random_range(0..80); if choice == 0 { + if let Some(cell) = editor.area_map.get(&(x, z)) { + // Check the tag and don't create a random tree if it matches + match cell.tag { + "park" | "garden" | "playground" + | "recreation_ground" | "pitch" + | "golf_course" | "dog_park" => { + continue; + } + _ => {} + } + } tree::Tree::create( editor, (x, 1, z), @@ -738,7 +749,7 @@ pub fn generate_ground_layer( ); } else if ground_is_natural { // Undergrowth only on natural surfaces - if choice == 1 { + if choice <= 3 { let flower = [ RED_FLOWER, BLUE_FLOWER, @@ -753,7 +764,7 @@ pub fn generate_ground_layer( None, None, ); - } else if choice <= 13 { + } else if choice <= 30 { editor.set_block_absolute( GRASS, x, @@ -1029,6 +1040,8 @@ pub fn generate_ground_layer( LIGHT_GRAY_CONCRETE, WHITE_CONCRETE, DIRT_PATH, + SAND, + DIRT, ]), None, ) && editor.check_for_block_absolute( diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index 6a8d62a75..9f14af35f 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -109,6 +109,12 @@ pub(crate) struct WorldMetadata { pub max_geo_lon: f64, } +pub struct AreaCell { + pub area: f64, + pub element_id: u64, + pub tag: &'static str, +} + /// The main world editor struct for placing blocks and saving worlds. /// /// The lifetime `'a` is tied to the `XZBBox` reference, which defines @@ -130,6 +136,8 @@ pub struct WorldEditor<'a> { /// Uses FNV hashing (not SipHash): `get_ground_level` sits on a hot /// path (called per-block during placement), so the hash cost matters. road_surface_overrides: FnvHashMap<(i32, i32), i32>, + /// Area map to persist area sizes + pub area_map: HashMap<(i32, i32), AreaCell>, /// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City") #[cfg(feature = "bedrock")] bedrock_level_name: Option, @@ -154,6 +162,7 @@ impl<'a> WorldEditor<'a> { ground: None, format: WorldFormat::JavaAnvil, road_surface_overrides: FnvHashMap::default(), + area_map: HashMap::new(), #[cfg(feature = "bedrock")] bedrock_level_name: None, #[cfg(feature = "bedrock")] @@ -188,6 +197,7 @@ impl<'a> WorldEditor<'a> { ground: None, format, road_surface_overrides: FnvHashMap::default(), + area_map: HashMap::new(), #[cfg(feature = "bedrock")] bedrock_level_name, #[cfg(feature = "bedrock")] @@ -197,6 +207,31 @@ impl<'a> WorldEditor<'a> { } } + /// Sets the area into the area_map if it has the smallest size + pub fn set_area_if_smaller( + &mut self, + x: i32, + z: i32, + area: f64, + element_id: u64, + tag: &'static str, + ) { + let key = (x, z); + if let Some(existing) = self.area_map.get(&key) { + if existing.area <= area { + return; + } + } + self.area_map.insert( + key, + AreaCell { + area, + element_id, + tag, + }, + ); + } + /// Sets the ground reference for elevation-based block placement pub fn set_ground(&mut self, ground: Arc) { self.ground = Some(ground);