Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion src/data_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -26,6 +28,69 @@ pub struct GenerationOptions {
pub spawn_point: Option<(i32, i32)>,
}

static SMALLEST_AREA_TAGS: once_cell::sync::Lazy<Vec<String>> = 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<ProcessedNode>> = 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reinstantiate the variable before calling it in the var_func

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<ProcessedElement>,
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/element_processing/landuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions src/element_processing/leisure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
_ => {}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/element_processing/natural.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 16 additions & 3 deletions src/ground_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,16 +729,27 @@ 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),
Some(building_footprints),
);
} else if ground_is_natural {
// Undergrowth only on natural surfaces
if choice == 1 {
if choice <= 3 {
let flower = [
RED_FLOWER,
BLUE_FLOWER,
Expand All @@ -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,
Expand Down Expand Up @@ -1029,6 +1040,8 @@ pub fn generate_ground_layer(
LIGHT_GRAY_CONCRETE,
WHITE_CONCRETE,
DIRT_PATH,
SAND,
DIRT,
]),
None,
) && editor.check_for_block_absolute(
Expand Down
35 changes: 35 additions & 0 deletions src/world_editor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>,
Expand All @@ -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")]
Expand Down Expand Up @@ -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")]
Expand All @@ -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<Ground>) {
self.ground = Some(ground);
Expand Down