From 433b6466769c5166136e064a2569770b626a652b Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Sun, 26 Apr 2026 09:05:52 +0200 Subject: [PATCH 1/7] Use only smallest landuse area --- src/data_processing.rs | 27 ++++++++++++ src/element_processing/landuse.rs | 72 ++++++++++++++++++++++++++++++- src/element_processing/leisure.rs | 7 +-- src/ground_generation.rs | 2 + src/world_editor/mod.rs | 8 ++++ 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/data_processing.rs b/src/data_processing.rs index ccb0a5a2d..4b64ecf7e 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -129,6 +129,33 @@ pub fn generate_world_with_options( outlines }; + // Check for smallest landuse areas per (x,z) + for element in &elements { + match element { + ProcessedElement::Way(way) => { + if way.tags.contains_key("landuse") { + landuse::accumulate_landuse( + &mut editor, + way, + args, + &flood_fill_cache, + ); + } + } + ProcessedElement::Relation(rel) => { + if rel.tags.contains_key("landuse") { + landuse::accumulate_landuse_from_relation( + &mut editor, + rel, + args, + &flood_fill_cache, + ); + } + } + _ => {} + } + } + // 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..4d48ac1b2 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -5,10 +5,70 @@ use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; -use crate::world_editor::WorldEditor; +use crate::world_editor::{WorldEditor, LanduseCell}; use rand::prelude::IndexedRandom; use rand::Rng; +pub fn accumulate_landuse( + editor: &mut WorldEditor, + element: &ProcessedWay, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + let binding = String::new(); + let landuse_tag = element.tags.get("landuse").unwrap_or(&binding); + if landuse_tag.is_empty() { + return; + } + + let floor_area = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); + let area = floor_area.len() as f64; + + for &(x, z) in floor_area.iter() { + let key = (x, z); + if let Some(existing) = editor.landuse_map.get(&key) { + if existing.area <= area { + continue; + } + } + editor.landuse_map.insert( + key, + LanduseCell { + area, + element_id: element.id, + }, + ); + } +} + +pub fn accumulate_landuse_from_relation( + editor: &mut WorldEditor, + rel: &ProcessedRelation, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + if !rel.tags.contains_key("landuse") { + return; + } + + for member in &rel.members { + if member.role == ProcessedMemberRole::Outer { + let way_with_rel_tags = ProcessedWay { + id: member.way.id, + nodes: member.way.nodes.clone(), + tags: rel.tags.clone(), + }; + accumulate_landuse( + editor, + &way_with_rel_tags, + args, + flood_fill_cache, + ); + } + } +} + + pub fn generate_landuse( editor: &mut WorldEditor, element: &ProcessedWay, @@ -80,6 +140,16 @@ 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.landuse_map.get(&(x, z)) { + if cell.element_id != element.id { + continue; + } + } else { + // For this coordinate there was no landuse element at all + 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..9c3635c51 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; @@ -115,10 +114,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/ground_generation.rs b/src/ground_generation.rs index 130731766..3c07777e7 100644 --- a/src/ground_generation.rs +++ b/src/ground_generation.rs @@ -1029,6 +1029,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..b9a43ca93 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -109,6 +109,11 @@ pub(crate) struct WorldMetadata { pub max_geo_lon: f64, } +pub struct LanduseCell { + pub area: f64, + pub element_id: u64, +} + /// The main world editor struct for placing blocks and saving worlds. /// /// The lifetime `'a` is tied to the `XZBBox` reference, which defines @@ -138,6 +143,7 @@ pub struct WorldEditor<'a> { bedrock_spawn_point: Option<(i32, i32)>, #[cfg(feature = "bedrock")] bedrock_extend_height: bool, + pub landuse_map: HashMap<(i32, i32), LanduseCell>, } impl<'a> WorldEditor<'a> { @@ -160,6 +166,7 @@ impl<'a> WorldEditor<'a> { bedrock_spawn_point: None, #[cfg(feature = "bedrock")] bedrock_extend_height: false, + landuse_map: HashMap::new(), } } @@ -194,6 +201,7 @@ impl<'a> WorldEditor<'a> { bedrock_spawn_point, #[cfg(feature = "bedrock")] bedrock_extend_height, + landuse_map: HashMap::new(), } } From e1cd04824942cd0eb09109353bfda7f67d00b8c4 Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:49:49 +0200 Subject: [PATCH 2/7] Extend smallest landuse area to landuse and leisure, improve random tree generation --- src/data_processing.rs | 18 ++++-------- src/element_processing/landuse.rs | 42 +++++++-------------------- src/element_processing/leisure.rs | 48 +++++++++++++++++++++++++++++++ src/ground_generation.rs | 16 +++++++++-- src/world_editor/mod.rs | 28 +++++++++++++++--- 5 files changed, 101 insertions(+), 51 deletions(-) diff --git a/src/data_processing.rs b/src/data_processing.rs index 4b64ecf7e..915459689 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -134,22 +134,16 @@ pub fn generate_world_with_options( match element { ProcessedElement::Way(way) => { if way.tags.contains_key("landuse") { - landuse::accumulate_landuse( - &mut editor, - way, - args, - &flood_fill_cache, - ); + landuse::accumulate_landuse(&mut editor, way, args, &flood_fill_cache); + } else if way.tags.contains_key("leisure") { + leisure::accumulate_leisure(&mut editor, way, args, &flood_fill_cache); } } ProcessedElement::Relation(rel) => { if rel.tags.contains_key("landuse") { - landuse::accumulate_landuse_from_relation( - &mut editor, - rel, - args, - &flood_fill_cache, - ); + landuse::accumulate_landuse_from_relation(&mut editor, rel, args, &flood_fill_cache); + } else if rel.tags.contains_key("leisure") { + leisure::accumulate_leisure_from_relation(&mut editor, rel, args, &flood_fill_cache); } } _ => {} diff --git a/src/element_processing/landuse.rs b/src/element_processing/landuse.rs index 4d48ac1b2..3d958acd6 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -5,7 +5,7 @@ use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; -use crate::world_editor::{WorldEditor, LanduseCell}; +use crate::world_editor::WorldEditor; use rand::prelude::IndexedRandom; use rand::Rng; @@ -15,29 +15,14 @@ pub fn accumulate_landuse( args: &Args, flood_fill_cache: &FloodFillCache, ) { - let binding = String::new(); - let landuse_tag = element.tags.get("landuse").unwrap_or(&binding); - if landuse_tag.is_empty() { - return; - } + let Some(tag) = element.tags.get("landuse") else { return; }; - let floor_area = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); - let area = floor_area.len() as f64; + let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); + let area = filled.len() as f64; + let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); - for &(x, z) in floor_area.iter() { - let key = (x, z); - if let Some(existing) = editor.landuse_map.get(&key) { - if existing.area <= area { - continue; - } - } - editor.landuse_map.insert( - key, - LanduseCell { - area, - element_id: element.id, - }, - ); + for &(x, z) in filled.iter() { + editor.set_area_if_smaller(x, z, area, element.id, tag_static); } } @@ -53,22 +38,16 @@ pub fn accumulate_landuse_from_relation( for member in &rel.members { if member.role == ProcessedMemberRole::Outer { - let way_with_rel_tags = ProcessedWay { + let way = ProcessedWay { id: member.way.id, nodes: member.way.nodes.clone(), tags: rel.tags.clone(), }; - accumulate_landuse( - editor, - &way_with_rel_tags, - args, - flood_fill_cache, - ); + accumulate_landuse(editor, &way, args, flood_fill_cache); } } } - pub fn generate_landuse( editor: &mut WorldEditor, element: &ProcessedWay, @@ -141,12 +120,11 @@ 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.landuse_map.get(&(x, z)) { + if let Some(cell) = editor.area_map.get(&(x, z)) { if cell.element_id != element.id { continue; } } else { - // For this coordinate there was no landuse element at all continue; } diff --git a/src/element_processing/leisure.rs b/src/element_processing/leisure.rs index 9c3635c51..95007cbcf 100644 --- a/src/element_processing/leisure.rs +++ b/src/element_processing/leisure.rs @@ -8,6 +8,45 @@ use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; use rand::Rng; +pub fn accumulate_leisure( + editor: &mut WorldEditor, + element: &ProcessedWay, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + let Some(tag) = element.tags.get("leisure") else { return; }; + + let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); + let area = filled.len() as f64; + let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); + + for &(x, z) in filled.iter() { + editor.set_area_if_smaller(x, z, area, element.id, tag_static); + } +} + +pub fn accumulate_leisure_from_relation( + editor: &mut WorldEditor, + rel: &ProcessedRelation, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + if !rel.tags.contains_key("leisure") { + return; + } + + for member in &rel.members { + if member.role == ProcessedMemberRole::Outer { + let way = ProcessedWay { + id: member.way.id, + nodes: member.way.nodes.clone(), + tags: rel.tags.clone(), + }; + accumulate_leisure(editor, &way, args, flood_fill_cache); + } + } +} + pub fn generate_leisure( editor: &mut WorldEditor, element: &ProcessedWay, @@ -85,6 +124,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 diff --git a/src/ground_generation.rs b/src/ground_generation.rs index 3c07777e7..13a9004e2 100644 --- a/src/ground_generation.rs +++ b/src/ground_generation.rs @@ -729,8 +729,18 @@ 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 +748,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 +763,7 @@ pub fn generate_ground_layer( None, None, ); - } else if choice <= 13 { + } else if choice <= 30 { editor.set_block_absolute( GRASS, x, diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index b9a43ca93..b62d02eba 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -109,9 +109,10 @@ pub(crate) struct WorldMetadata { pub max_geo_lon: f64, } -pub struct LanduseCell { +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. @@ -135,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, @@ -143,7 +146,6 @@ pub struct WorldEditor<'a> { bedrock_spawn_point: Option<(i32, i32)>, #[cfg(feature = "bedrock")] bedrock_extend_height: bool, - pub landuse_map: HashMap<(i32, i32), LanduseCell>, } impl<'a> WorldEditor<'a> { @@ -160,13 +162,13 @@ 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")] bedrock_spawn_point: None, #[cfg(feature = "bedrock")] bedrock_extend_height: false, - landuse_map: HashMap::new(), } } @@ -195,16 +197,34 @@ 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")] bedrock_spawn_point, #[cfg(feature = "bedrock")] bedrock_extend_height, - landuse_map: HashMap::new(), } } + /// 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); From dfdadc560bad456c80a36ab4cd5c9004b35c89dc Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:29:18 +0200 Subject: [PATCH 3/7] Add natural elements --- src/data_processing.rs | 4 +++ src/element_processing/natural.rs | 48 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/data_processing.rs b/src/data_processing.rs index 915459689..e475cb219 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -137,6 +137,8 @@ pub fn generate_world_with_options( landuse::accumulate_landuse(&mut editor, way, args, &flood_fill_cache); } else if way.tags.contains_key("leisure") { leisure::accumulate_leisure(&mut editor, way, args, &flood_fill_cache); + } else if way.tags.contains_key("natural") { + natural::accumulate_natural(&mut editor, way, args, &flood_fill_cache); } } ProcessedElement::Relation(rel) => { @@ -144,6 +146,8 @@ pub fn generate_world_with_options( landuse::accumulate_landuse_from_relation(&mut editor, rel, args, &flood_fill_cache); } else if rel.tags.contains_key("leisure") { leisure::accumulate_leisure_from_relation(&mut editor, rel, args, &flood_fill_cache); + } else if rel.tags.contains_key("natural") { + natural::accumulate_natural_from_relation(&mut editor, rel, args, &flood_fill_cache); } } _ => {} diff --git a/src/element_processing/natural.rs b/src/element_processing/natural.rs index 361cdfbbb..33c66351a 100644 --- a/src/element_processing/natural.rs +++ b/src/element_processing/natural.rs @@ -8,6 +8,45 @@ use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation use crate::world_editor::WorldEditor; use rand::{prelude::IndexedRandom, Rng}; +pub fn accumulate_natural( + editor: &mut WorldEditor, + element: &ProcessedWay, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + let Some(tag) = element.tags.get("natural") else { return; }; + + let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); + let area = filled.len() as f64; + let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); + + for &(x, z) in filled.iter() { + editor.set_area_if_smaller(x, z, area, element.id, tag_static); + } +} + +pub fn accumulate_natural_from_relation( + editor: &mut WorldEditor, + rel: &ProcessedRelation, + args: &Args, + flood_fill_cache: &FloodFillCache, +) { + if !rel.tags.contains_key("natural") { + return; + } + + for member in &rel.members { + if member.role == ProcessedMemberRole::Outer { + let way = ProcessedWay { + id: member.way.id, + nodes: member.way.nodes.clone(), + tags: rel.tags.clone(), + }; + accumulate_natural(editor, &way, args, flood_fill_cache); + } + } +} + pub fn generate_natural( editor: &mut WorldEditor, element: &ProcessedElement, @@ -214,6 +253,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 { From 16be480fcae2f8175940cfde31147ee26dd74116 Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:48:23 +0200 Subject: [PATCH 4/7] Fix handling of relations with segmented outer line --- src/element_processing/landuse.rs | 26 +++++++++++++++++++------- src/element_processing/leisure.rs | 26 +++++++++++++++++++------- src/element_processing/natural.rs | 26 +++++++++++++++++++------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/element_processing/landuse.rs b/src/element_processing/landuse.rs index 3d958acd6..68547d153 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -4,7 +4,7 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; +use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; use crate::world_editor::WorldEditor; use rand::prelude::IndexedRandom; use rand::Rng; @@ -36,16 +36,28 @@ pub fn accumulate_landuse_from_relation( return; } + // Collect outer ways as node list + let mut outers: Vec> = Vec::new(); + for member in &rel.members { if member.role == ProcessedMemberRole::Outer { - let way = ProcessedWay { - id: member.way.id, - nodes: member.way.nodes.clone(), - tags: rel.tags.clone(), - }; - accumulate_landuse(editor, &way, args, flood_fill_cache); + outers.push(member.way.nodes.clone()); } } + + // Merge outer ways to polygon + super::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_landuse(editor, &way, args, flood_fill_cache); + } } pub fn generate_landuse( diff --git a/src/element_processing/leisure.rs b/src/element_processing/leisure.rs index 95007cbcf..ffb151219 100644 --- a/src/element_processing/leisure.rs +++ b/src/element_processing/leisure.rs @@ -4,7 +4,7 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::surfaces::get_blocks_for_surface; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; +use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; use crate::world_editor::WorldEditor; use rand::Rng; @@ -35,16 +35,28 @@ pub fn accumulate_leisure_from_relation( return; } + // Collect outer ways as node list + let mut outers: Vec> = Vec::new(); + for member in &rel.members { if member.role == ProcessedMemberRole::Outer { - let way = ProcessedWay { - id: member.way.id, - nodes: member.way.nodes.clone(), - tags: rel.tags.clone(), - }; - accumulate_leisure(editor, &way, args, flood_fill_cache); + outers.push(member.way.nodes.clone()); } } + + // Merge outer ways to polygon + super::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_leisure(editor, &way, args, flood_fill_cache); + } } pub fn generate_leisure( diff --git a/src/element_processing/natural.rs b/src/element_processing/natural.rs index 33c66351a..b35796fbb 100644 --- a/src/element_processing/natural.rs +++ b/src/element_processing/natural.rs @@ -4,7 +4,7 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay}; +use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; use crate::world_editor::WorldEditor; use rand::{prelude::IndexedRandom, Rng}; @@ -35,16 +35,28 @@ pub fn accumulate_natural_from_relation( return; } + // Collect outer ways as node list + let mut outers: Vec> = Vec::new(); + for member in &rel.members { if member.role == ProcessedMemberRole::Outer { - let way = ProcessedWay { - id: member.way.id, - nodes: member.way.nodes.clone(), - tags: rel.tags.clone(), - }; - accumulate_natural(editor, &way, args, flood_fill_cache); + outers.push(member.way.nodes.clone()); } } + + // Merge outer ways to polygon + super::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_natural(editor, &way, args, flood_fill_cache); + } } pub fn generate_natural( From 2ce318b3584b59d5c38064f8090f1fe934927e91 Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:15:25 +0200 Subject: [PATCH 5/7] Move area accumulation to generic functions --- src/data_processing.rs | 93 ++++++++++++++++++++++++++----- src/element_processing/landuse.rs | 53 +----------------- src/element_processing/leisure.rs | 53 +----------------- src/element_processing/natural.rs | 53 +----------------- 4 files changed, 82 insertions(+), 170 deletions(-) diff --git a/src/data_processing.rs b/src/data_processing.rs index e475cb219..571764308 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -6,7 +6,7 @@ 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, ProcessedWay, ProcessedRelation}; 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 +26,63 @@ pub struct GenerationOptions { pub spawn_point: Option<(i32, i32)>, } +const SMALLEST_AREA_TAGS: &[&str] = &["landuse", "leisure", "natural"]; + +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.to_string()) 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.to_string()) { + 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,25 +186,33 @@ pub fn generate_world_with_options( outlines }; - // Check for smallest landuse areas per (x,z) + // Check for smallest areas per (x,z) for element in &elements { match element { ProcessedElement::Way(way) => { - if way.tags.contains_key("landuse") { - landuse::accumulate_landuse(&mut editor, way, args, &flood_fill_cache); - } else if way.tags.contains_key("leisure") { - leisure::accumulate_leisure(&mut editor, way, args, &flood_fill_cache); - } else if way.tags.contains_key("natural") { - natural::accumulate_natural(&mut editor, way, args, &flood_fill_cache); + for tag_key in SMALLEST_AREA_TAGS { + if way.tags.contains_key(&tag_key.to_string()) { + accumulate_smallest_area( + &mut editor, + way, + tag_key, + &flood_fill_cache, + args, + ); + } } } ProcessedElement::Relation(rel) => { - if rel.tags.contains_key("landuse") { - landuse::accumulate_landuse_from_relation(&mut editor, rel, args, &flood_fill_cache); - } else if rel.tags.contains_key("leisure") { - leisure::accumulate_leisure_from_relation(&mut editor, rel, args, &flood_fill_cache); - } else if rel.tags.contains_key("natural") { - natural::accumulate_natural_from_relation(&mut editor, rel, args, &flood_fill_cache); + for tag_key in SMALLEST_AREA_TAGS { + if rel.tags.contains_key(&tag_key.to_string()) { + accumulate_smallest_area_from_relation( + &mut editor, + rel, + tag_key, + &flood_fill_cache, + args, + ); + } } } _ => {} diff --git a/src/element_processing/landuse.rs b/src/element_processing/landuse.rs index 68547d153..a1e6ccd62 100644 --- a/src/element_processing/landuse.rs +++ b/src/element_processing/landuse.rs @@ -4,62 +4,11 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; +use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; use rand::prelude::IndexedRandom; use rand::Rng; -pub fn accumulate_landuse( - editor: &mut WorldEditor, - element: &ProcessedWay, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - let Some(tag) = element.tags.get("landuse") else { return; }; - - let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); - let area = filled.len() as f64; - let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); - - for &(x, z) in filled.iter() { - editor.set_area_if_smaller(x, z, area, element.id, tag_static); - } -} - -pub fn accumulate_landuse_from_relation( - editor: &mut WorldEditor, - rel: &ProcessedRelation, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - if !rel.tags.contains_key("landuse") { - 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 - super::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_landuse(editor, &way, args, flood_fill_cache); - } -} - pub fn generate_landuse( editor: &mut WorldEditor, element: &ProcessedWay, diff --git a/src/element_processing/leisure.rs b/src/element_processing/leisure.rs index ffb151219..328ffd366 100644 --- a/src/element_processing/leisure.rs +++ b/src/element_processing/leisure.rs @@ -4,61 +4,10 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::surfaces::get_blocks_for_surface; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; +use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; use rand::Rng; -pub fn accumulate_leisure( - editor: &mut WorldEditor, - element: &ProcessedWay, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - let Some(tag) = element.tags.get("leisure") else { return; }; - - let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); - let area = filled.len() as f64; - let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); - - for &(x, z) in filled.iter() { - editor.set_area_if_smaller(x, z, area, element.id, tag_static); - } -} - -pub fn accumulate_leisure_from_relation( - editor: &mut WorldEditor, - rel: &ProcessedRelation, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - if !rel.tags.contains_key("leisure") { - 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 - super::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_leisure(editor, &way, args, flood_fill_cache); - } -} - pub fn generate_leisure( editor: &mut WorldEditor, element: &ProcessedWay, diff --git a/src/element_processing/natural.rs b/src/element_processing/natural.rs index b35796fbb..d0434e0f3 100644 --- a/src/element_processing/natural.rs +++ b/src/element_processing/natural.rs @@ -4,61 +4,10 @@ use crate::bresenham::bresenham_line; use crate::deterministic_rng::element_rng; use crate::element_processing::tree::{Tree, TreeType}; use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache}; -use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay, ProcessedNode}; +use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay}; use crate::world_editor::WorldEditor; use rand::{prelude::IndexedRandom, Rng}; -pub fn accumulate_natural( - editor: &mut WorldEditor, - element: &ProcessedWay, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - let Some(tag) = element.tags.get("natural") else { return; }; - - let filled = flood_fill_cache.get_or_compute(element, args.timeout.as_ref()); - let area = filled.len() as f64; - let tag_static: &'static str = Box::leak(tag.clone().into_boxed_str()); - - for &(x, z) in filled.iter() { - editor.set_area_if_smaller(x, z, area, element.id, tag_static); - } -} - -pub fn accumulate_natural_from_relation( - editor: &mut WorldEditor, - rel: &ProcessedRelation, - args: &Args, - flood_fill_cache: &FloodFillCache, -) { - if !rel.tags.contains_key("natural") { - 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 - super::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_natural(editor, &way, args, flood_fill_cache); - } -} - pub fn generate_natural( editor: &mut WorldEditor, element: &ProcessedElement, From fab742caa9347c73058fc5837470e97853064821 Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:56:24 +0200 Subject: [PATCH 6/7] Fix formatting --- src/data_processing.rs | 10 ++++++---- src/ground_generation.rs | 5 +++-- src/world_editor/mod.rs | 9 ++++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/data_processing.rs b/src/data_processing.rs index 571764308..772c5c7df 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, ProcessedNode, ProcessedWay, ProcessedRelation}; +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}; @@ -35,7 +37,9 @@ fn accumulate_smallest_area( flood_fill_cache: &FloodFillCache, args: &Args, ) { - let Some(tag_value) = way.tags.get(&tag_key.to_string()) else { return; }; + let Some(tag_value) = way.tags.get(&tag_key.to_string()) else { + return; + }; let filled = flood_fill_cache.get_or_compute(way, args.timeout.as_ref()); let area = filled.len() as f64; @@ -81,8 +85,6 @@ fn accumulate_smallest_area_from_relation( 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, diff --git a/src/ground_generation.rs b/src/ground_generation.rs index 13a9004e2..d948f2dbc 100644 --- a/src/ground_generation.rs +++ b/src/ground_generation.rs @@ -734,8 +734,9 @@ pub fn generate_ground_layer( 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" => { + "park" | "garden" | "playground" + | "recreation_ground" | "pitch" + | "golf_course" | "dog_park" => { continue; } _ => {} diff --git a/src/world_editor/mod.rs b/src/world_editor/mod.rs index b62d02eba..9f14af35f 100644 --- a/src/world_editor/mod.rs +++ b/src/world_editor/mod.rs @@ -222,7 +222,14 @@ impl<'a> WorldEditor<'a> { return; } } - self.area_map.insert(key, AreaCell { area, element_id, tag }); + self.area_map.insert( + key, + AreaCell { + area, + element_id, + tag, + }, + ); } /// Sets the ground reference for elevation-based block placement From 7ae32924b76a446873dc6794d42e6b8d91d186b2 Mon Sep 17 00:00:00 2001 From: eengnr <31403422+eengnr@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:11:26 +0200 Subject: [PATCH 7/7] Fix clippy lints --- src/data_processing.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/data_processing.rs b/src/data_processing.rs index 772c5c7df..bc62cff8f 100644 --- a/src/data_processing.rs +++ b/src/data_processing.rs @@ -28,7 +28,13 @@ pub struct GenerationOptions { pub spawn_point: Option<(i32, i32)>, } -const SMALLEST_AREA_TAGS: &[&str] = &["landuse", "leisure", "natural"]; +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, @@ -37,7 +43,7 @@ fn accumulate_smallest_area( flood_fill_cache: &FloodFillCache, args: &Args, ) { - let Some(tag_value) = way.tags.get(&tag_key.to_string()) else { + let Some(tag_value) = way.tags.get(tag_key) else { return; }; @@ -58,7 +64,7 @@ fn accumulate_smallest_area_from_relation( flood_fill_cache: &FloodFillCache, args: &Args, ) { - if !rel.tags.contains_key(&tag_key.to_string()) { + if !rel.tags.contains_key(tag_key) { return; } @@ -192,8 +198,8 @@ pub fn generate_world_with_options( for element in &elements { match element { ProcessedElement::Way(way) => { - for tag_key in SMALLEST_AREA_TAGS { - if way.tags.contains_key(&tag_key.to_string()) { + for tag_key in SMALLEST_AREA_TAGS.iter() { + if way.tags.contains_key(tag_key) { accumulate_smallest_area( &mut editor, way, @@ -205,8 +211,8 @@ pub fn generate_world_with_options( } } ProcessedElement::Relation(rel) => { - for tag_key in SMALLEST_AREA_TAGS { - if rel.tags.contains_key(&tag_key.to_string()) { + for tag_key in SMALLEST_AREA_TAGS.iter() { + if rel.tags.contains_key(tag_key) { accumulate_smallest_area_from_relation( &mut editor, rel,