mirror of
https://github.com/louis-e/arnis.git
synced 2026-01-13 08:37:45 -05:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cdebbed78 | ||
|
|
5291f72215 | ||
|
|
c24e22b790 | ||
|
|
d4f324fd96 | ||
|
|
e7e65d0e6f | ||
|
|
927aaec22d | ||
|
|
5ec942dbd1 | ||
|
|
19bba3cc26 | ||
|
|
17d6d323fc | ||
|
|
236072dc42 | ||
|
|
7a8226923a | ||
|
|
107ab70602 | ||
|
|
1364d96291 | ||
|
|
b74b5c5ccb | ||
|
|
dd8004b159 | ||
|
|
b0845ce1df | ||
|
|
fc540db4cd | ||
|
|
1ecdffc039 | ||
|
|
9ea34b9911 | ||
|
|
48248aad05 | ||
|
|
169545d937 | ||
|
|
fba331232b | ||
|
|
b02a2783c1 | ||
|
|
dbc4741b78 | ||
|
|
b52485badc | ||
|
|
447416f6ce | ||
|
|
d26b23937e | ||
|
|
5e01abc5b6 | ||
|
|
7c808ec352 | ||
|
|
b757c5acf4 | ||
|
|
ced5fc274e | ||
|
|
295ca415d7 | ||
|
|
e2b4ca8bdb | ||
|
|
07105f0208 | ||
|
|
ad57fdbc3a | ||
|
|
550870d9e0 | ||
|
|
bd693ea007 | ||
|
|
ce8f343414 | ||
|
|
f882145780 | ||
|
|
b52d750935 | ||
|
|
4d30899909 | ||
|
|
311610a717 | ||
|
|
b4902ebc9e | ||
|
|
e5bbb3e4a0 | ||
|
|
0238cfe2d0 | ||
|
|
2d9892fe7f | ||
|
|
b858ce4691 | ||
|
|
e031e53492 | ||
|
|
6fb9b8943d | ||
|
|
18266dd459 | ||
|
|
b1940fa412 | ||
|
|
d57a732055 | ||
|
|
4e52b38f5a | ||
|
|
feb4317086 | ||
|
|
d02cbed997 | ||
|
|
99d1f8e117 | ||
|
|
6fa76bc381 | ||
|
|
0fef27e6af | ||
|
|
fa3384cf86 | ||
|
|
ffbc5e5788 | ||
|
|
4215e7644c | ||
|
|
118335bad4 | ||
|
|
7bbee28279 | ||
|
|
9cb35a3b13 | ||
|
|
4fecf98c54 | ||
|
|
47a7b81f99 | ||
|
|
7ec90b4fef | ||
|
|
f1f3fb287a | ||
|
|
b23658d5ef | ||
|
|
cc89576828 | ||
|
|
809fa23941 | ||
|
|
51ad1fef3f | ||
|
|
8e8d8e0567 | ||
|
|
da6f23c0a2 | ||
|
|
d4a872989c | ||
|
|
2a5a5230c5 | ||
|
|
9018584b1d | ||
|
|
9eda39846c | ||
|
|
5e9d6795df | ||
|
|
54a7a4f2a9 | ||
|
|
d0d65643f5 | ||
|
|
946fd43a5e | ||
|
|
05e5ffdd2a | ||
|
|
0b7e27df7f | ||
|
|
613a410c93 | ||
|
|
faefd29e30 | ||
|
|
9ad6c75440 | ||
|
|
e51f28f067 | ||
|
|
47ddb9b211 | ||
|
|
46415bb002 | ||
|
|
0683dd3343 | ||
|
|
4d304dc978 | ||
|
|
5d97391820 | ||
|
|
bef3cfb090 | ||
|
|
5a898944f7 | ||
|
|
9fdd960009 | ||
|
|
0a51b302ee | ||
|
|
93dc9f446c | ||
|
|
e6430f2a04 | ||
|
|
58e4a337d9 | ||
|
|
236a7e5af9 | ||
|
|
5962decf44 |
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
binary_name: arnis
|
||||
asset_name: arnis-linux
|
||||
- os: macos-13 # Intel runner for x86_64 builds
|
||||
- os: macos-15-intel # Intel runner for x86_64 builds
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: arnis
|
||||
asset_name: arnis-mac-intel
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.target }}-build
|
||||
path: target/release/${{ matrix.asset_name }}
|
||||
@@ -97,13 +97,13 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-13-x86_64-apple-darwin-build
|
||||
name: macos-15-intel-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-latest-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
chmod +x arnis-mac-universal
|
||||
|
||||
- name: Upload universal binary
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: arnis-mac-universal
|
||||
@@ -127,19 +127,19 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||
path: ./builds/windows
|
||||
|
||||
- name: Download Linux build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||
path: ./builds/linux
|
||||
|
||||
- name: Download macOS universal build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: ./builds/macos
|
||||
@@ -157,4 +157,4 @@ jobs:
|
||||
builds/linux/arnis-linux
|
||||
builds/macos/arnis-mac-universal
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -182,7 +182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arnis"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bedrockrs_level",
|
||||
@@ -204,6 +204,7 @@ dependencies = [
|
||||
"nbtx",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
@@ -4487,9 +4488,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.4"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.1",
|
||||
@@ -4506,7 +4507,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6765,6 +6766,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -38,9 +38,10 @@ itertools = "0.14.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.15.4", optional = true }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -63,6 +63,8 @@ Arnis has been recognized in various academic and press publications after gaini
|
||||
|
||||
[XDA Developers: Hometown Minecraft Map: Arnis](https://www.xda-developers.com/hometown-minecraft-map-arnis/)
|
||||
|
||||
Free to use assets, including screenshots and logos, can be found [here](https://drive.google.com/file/d/1T1IsZSyT8oa6qAO_40hVF5KR8eEVCJjo/view?usp=sharing).
|
||||
|
||||
## :copyright: License Information
|
||||
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
|
||||
|
||||
|
||||
@@ -58,10 +58,6 @@ pub struct Args {
|
||||
/// Set floodfill timeout (seconds) (optional)
|
||||
#[arg(long, value_parser = parse_duration)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Spawn point coordinates (lat, lng)
|
||||
#[arg(skip)]
|
||||
pub spawn_point: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
|
||||
@@ -266,6 +266,7 @@ impl Block {
|
||||
185 => "quartz_stairs",
|
||||
186 => "polished_andesite_stairs",
|
||||
187 => "nether_brick_stairs",
|
||||
188 => "fern",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -697,6 +698,7 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||
pub const FERN: Block = Block::new(188);
|
||||
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
@@ -13,6 +14,7 @@ use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const MIN_Y: i32 = -64;
|
||||
|
||||
@@ -53,14 +55,17 @@ pub fn generate_world_with_options(
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = options.path.clone();
|
||||
let world_format = options.format;
|
||||
|
||||
// Create editor with appropriate format
|
||||
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
|
||||
options.path,
|
||||
&xzbbox,
|
||||
llbbox,
|
||||
options.format,
|
||||
options.level_name,
|
||||
options.level_name.clone(),
|
||||
options.spawn_point,
|
||||
);
|
||||
let ground = Arc::new(ground);
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
@@ -68,13 +73,21 @@ pub fn generate_world_with_options(
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(&ground);
|
||||
editor.set_ground(Arc::clone(&ground));
|
||||
|
||||
println!("{} Processing terrain...", "[5/7]".bold());
|
||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||
|
||||
// Pre-compute all flood fills in parallel for better CPU utilization
|
||||
let mut flood_fill_cache = FloodFillCache::precompute(&elements, args.timeout.as_ref());
|
||||
|
||||
// Collect building footprints to prevent trees from spawning inside buildings
|
||||
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
|
||||
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
|
||||
|
||||
// Process data
|
||||
let elements_count: usize = elements.len();
|
||||
let mut elements = elements; // Take ownership for consuming
|
||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||
process_pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||
@@ -85,7 +98,8 @@ pub fn generate_world_with_options(
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||
|
||||
for element in &elements {
|
||||
// Process elements by draining in insertion order
|
||||
for element in elements.drain(..) {
|
||||
process_pb.inc(1);
|
||||
current_progress_prcs += progress_increment_prcs;
|
||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||
@@ -103,22 +117,46 @@ pub fn generate_world_with_options(
|
||||
process_pb.set_message("");
|
||||
}
|
||||
|
||||
match element {
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, args, None);
|
||||
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&highway_connectivity,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(&mut editor, way, args);
|
||||
landuse::generate_landuse(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
natural::generate_natural(&mut editor, element, args);
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, element, args);
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("leisure") {
|
||||
leisure::generate_leisure(&mut editor, way, args);
|
||||
leisure::generate_leisure(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, element);
|
||||
barriers::generate_barriers(&mut editor, &element);
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
// docks count as water areas
|
||||
@@ -138,8 +176,10 @@ pub fn generate_world_with_options(
|
||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||
highways::generate_siding(&mut editor, way);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, element, args);
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
}
|
||||
// Release flood fill cache entry for this way
|
||||
flood_fill_cache.remove_way(way.id);
|
||||
}
|
||||
ProcessedElement::Node(node) => {
|
||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
||||
@@ -147,13 +187,25 @@ pub fn generate_world_with_options(
|
||||
} else if node.tags.contains_key("natural")
|
||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
||||
{
|
||||
natural::generate_natural(&mut editor, element, args);
|
||||
natural::generate_natural(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if node.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(&mut editor, element, args);
|
||||
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barrier_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
&element,
|
||||
args,
|
||||
&highway_connectivity,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
@@ -162,7 +214,12 @@ pub fn generate_world_with_options(
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
buildings::generate_building_from_relation(&mut editor, rel, args);
|
||||
buildings::generate_building_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel
|
||||
.tags
|
||||
@@ -172,24 +229,46 @@ pub fn generate_world_with_options(
|
||||
{
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
||||
} else if rel.tags.contains_key("natural") {
|
||||
natural::generate_natural_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
leisure::generate_leisure_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(
|
||||
natural::generate_natural_from_relation(
|
||||
&mut editor,
|
||||
&ProcessedElement::Relation(rel.clone()),
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
leisure::generate_leisure_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
}
|
||||
// Release flood fill cache entries for all ways in this relation
|
||||
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
|
||||
flood_fill_cache.remove_relation_ways(&way_ids);
|
||||
}
|
||||
}
|
||||
// Element is dropped here, freeing its memory immediately
|
||||
}
|
||||
|
||||
process_pb.finish();
|
||||
|
||||
// Drop remaining caches
|
||||
drop(highway_connectivity);
|
||||
drop(flood_fill_cache);
|
||||
|
||||
// Generate ground layer
|
||||
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
|
||||
let desired_updates: u64 = 1500;
|
||||
@@ -213,46 +292,72 @@ pub fn generate_world_with_options(
|
||||
let total_iterations_grnd: f64 = total_blocks as f64;
|
||||
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
|
||||
|
||||
let groundlayer_block = GRASS_BLOCK;
|
||||
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
|
||||
let terrain_enabled = ground.elevation_enabled;
|
||||
|
||||
for x in xzbbox.min_x()..=xzbbox.max_x() {
|
||||
for z in xzbbox.min_z()..=xzbbox.max_z() {
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
|
||||
editor.set_block(groundlayer_block, x, 0, z, None, None);
|
||||
editor.set_block(DIRT, x, -1, z, None, None);
|
||||
editor.set_block(DIRT, x, -2, z, None, None);
|
||||
}
|
||||
// Process ground generation chunk-by-chunk for better cache locality.
|
||||
// This keeps the same region/chunk HashMap entries hot in CPU cache,
|
||||
// rather than jumping between regions on every Z iteration.
|
||||
let min_chunk_x = xzbbox.min_x() >> 4;
|
||||
let max_chunk_x = xzbbox.max_x() >> 4;
|
||||
let min_chunk_z = xzbbox.min_z() >> 4;
|
||||
let max_chunk_z = xzbbox.max_z() >> 4;
|
||||
|
||||
// Fill underground with stone
|
||||
if args.fillground {
|
||||
// Fill from bedrock+1 to 3 blocks below ground with stone
|
||||
editor.fill_blocks_absolute(
|
||||
STONE,
|
||||
x,
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
editor.get_absolute_y(x, -3, z),
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Generate a bedrock level at MIN_Y
|
||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
||||
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||
// Calculate the block range for this chunk, clamped to bbox
|
||||
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
|
||||
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
|
||||
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
|
||||
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
|
||||
|
||||
block_counter += 1;
|
||||
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if block_counter % batch_size == 0 {
|
||||
ground_pb.inc(batch_size);
|
||||
}
|
||||
for x in chunk_min_x..=chunk_max_x {
|
||||
for z in chunk_min_z..=chunk_max_z {
|
||||
// Get ground level, when terrain is enabled, look it up once per block
|
||||
// When disabled, use constant ground_level (no function call overhead)
|
||||
let ground_y = if terrain_enabled {
|
||||
editor.get_ground_level(x, z)
|
||||
} else {
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
gui_progress_grnd += progress_increment_grnd;
|
||||
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
|
||||
emit_gui_progress_update(gui_progress_grnd, "");
|
||||
last_emitted_progress = gui_progress_grnd;
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||
}
|
||||
|
||||
// Fill underground with stone
|
||||
if args.fillground {
|
||||
// Fill from bedrock+1 to 3 blocks below ground with stone
|
||||
editor.fill_blocks_absolute(
|
||||
STONE,
|
||||
x,
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
ground_y - 3,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Generate a bedrock level at MIN_Y
|
||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
||||
|
||||
block_counter += 1;
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if block_counter % batch_size == 0 {
|
||||
ground_pb.inc(batch_size);
|
||||
}
|
||||
|
||||
gui_progress_grnd += progress_increment_grnd;
|
||||
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
|
||||
emit_gui_progress_update(gui_progress_grnd, "");
|
||||
last_emitted_progress = gui_progress_grnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,36 +380,36 @@ pub fn generate_world_with_options(
|
||||
// Save world
|
||||
editor.save();
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
if let Some(spawn_coords) = &args.spawn_point {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.max().lng(),
|
||||
args.bbox.max().lat()
|
||||
);
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
// Reconstruct bbox string to match the format that GUI originally provided.
|
||||
// This ensures LLBBox::from_str() can parse it correctly.
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.max().lat(),
|
||||
args.bbox.max().lng()
|
||||
);
|
||||
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
@@ -312,41 +417,72 @@ pub fn generate_world_with_options(
|
||||
}
|
||||
}
|
||||
|
||||
// Generate top-down map preview silently in background after completion (Java only)
|
||||
// Skip map preview for very large areas to avoid memory issues
|
||||
const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
let world_area = world_width * world_height;
|
||||
|
||||
if world_format == WorldFormat::JavaAnvil && world_area <= MAX_MAP_PREVIEW_AREA {
|
||||
let world_path = args.path.clone();
|
||||
let bounds = (
|
||||
xzbbox.min_x(),
|
||||
xzbbox.max_x(),
|
||||
xzbbox.min_z(),
|
||||
xzbbox.max_z(),
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(&world_path, bounds.0, bounds.1, bounds.2, bounds.3)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Information needed to generate a map preview after world generation is complete
|
||||
#[derive(Clone)]
|
||||
pub struct MapPreviewInfo {
|
||||
pub world_path: PathBuf,
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
pub world_area: i64,
|
||||
}
|
||||
|
||||
impl MapPreviewInfo {
|
||||
/// Create MapPreviewInfo from world bounds
|
||||
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
Self {
|
||||
world_path,
|
||||
min_x: xzbbox.min_x(),
|
||||
max_x: xzbbox.max_x(),
|
||||
min_z: xzbbox.min_z(),
|
||||
max_z: xzbbox.max_z(),
|
||||
world_area: world_width * world_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
|
||||
/// Start map preview generation in a background thread.
|
||||
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||
/// and the GUI has been notified of 100% completion.
|
||||
///
|
||||
/// For Java worlds only, and only if the world area is within limits.
|
||||
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(
|
||||
&info.world_path,
|
||||
info.min_x,
|
||||
info.max_x,
|
||||
info.min_z,
|
||||
info.max_z,
|
||||
)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
127
src/deterministic_rng.rs
Normal file
127
src/deterministic_rng.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
//! Deterministic random number generation for consistent element processing.
|
||||
//!
|
||||
//! This module provides seeded RNG that ensures the same element always produces
|
||||
//! the same random values, regardless of processing order. This is essential for
|
||||
//! region-by-region streaming where the same element may be processed multiple times
|
||||
//! (once for each region it touches).
|
||||
//!
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! let mut rng = element_rng(element_id);
|
||||
//! let color = rng.gen_bool(0.5); // Always same result for same element_id
|
||||
//! ```
|
||||
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
|
||||
/// Creates a deterministic RNG seeded from an element ID.
|
||||
///
|
||||
/// The same element ID will always produce the same sequence of random values,
|
||||
/// ensuring consistent results when an element is processed multiple times
|
||||
/// (e.g., once per region it touches during streaming).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `element_id` - The unique OSM element ID (way ID, node ID, or relation ID)
|
||||
///
|
||||
/// # Returns
|
||||
/// A seeded ChaCha8Rng that will produce deterministic random values
|
||||
#[inline]
|
||||
pub fn element_rng(element_id: u64) -> ChaCha8Rng {
|
||||
ChaCha8Rng::seed_from_u64(element_id)
|
||||
}
|
||||
|
||||
/// Creates a deterministic RNG seeded from an element ID with an additional salt.
|
||||
///
|
||||
/// Use this when you need multiple independent random sequences for the same element.
|
||||
/// For example, one sequence for wall colors and another for roof style.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `element_id` - The unique OSM element ID
|
||||
/// * `salt` - Additional value to create a different sequence (e.g., use different
|
||||
/// salt values for different purposes within the same element)
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn element_rng_salted(element_id: u64, salt: u64) -> ChaCha8Rng {
|
||||
// Combine element_id and salt using XOR and bit rotation to avoid collisions
|
||||
let combined = element_id ^ salt.rotate_left(32);
|
||||
ChaCha8Rng::seed_from_u64(combined)
|
||||
}
|
||||
|
||||
/// Creates a deterministic RNG seeded from coordinates.
|
||||
///
|
||||
/// Use this for per-block randomness that needs to be consistent regardless
|
||||
/// of processing order (e.g., random flower placement within a natural area).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - X coordinate
|
||||
/// * `z` - Z coordinate
|
||||
/// * `element_id` - The element ID for additional uniqueness
|
||||
#[inline]
|
||||
pub fn coord_rng(x: i32, z: i32, element_id: u64) -> ChaCha8Rng {
|
||||
// Combine coordinates and element_id into a seed.
|
||||
// Cast through u32 to handle negative coordinates consistently.
|
||||
let coord_part = ((x as u32 as i64) << 32) | (z as u32 as i64);
|
||||
let seed = (coord_part as u64) ^ element_id;
|
||||
ChaCha8Rng::seed_from_u64(seed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::Rng;
|
||||
|
||||
#[test]
|
||||
fn test_element_rng_deterministic() {
|
||||
let mut rng1 = element_rng(12345);
|
||||
let mut rng2 = element_rng(12345);
|
||||
|
||||
// Same seed should produce same sequence
|
||||
for _ in 0..100 {
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_elements_different_values() {
|
||||
let mut rng1 = element_rng(12345);
|
||||
let mut rng2 = element_rng(12346);
|
||||
|
||||
// Different seeds should (almost certainly) produce different values
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_salted_rng_different_from_base() {
|
||||
let mut rng1 = element_rng(12345);
|
||||
let mut rng2 = element_rng_salted(12345, 1);
|
||||
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_rng_deterministic() {
|
||||
let mut rng1 = coord_rng(100, 200, 12345);
|
||||
let mut rng2 = coord_rng(100, 200, 12345);
|
||||
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_rng_negative_coordinates() {
|
||||
// Negative coordinates are common in Minecraft worlds
|
||||
let mut rng1 = coord_rng(-100, -200, 12345);
|
||||
let mut rng2 = coord_rng(-100, -200, 12345);
|
||||
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
|
||||
// Ensure different negative coords produce different seeds
|
||||
let mut rng3 = coord_rng(-100, -200, 12345);
|
||||
let mut rng4 = coord_rng(-101, -200, 12345);
|
||||
|
||||
assert_ne!(rng3.gen::<u64>(), rng4.gen::<u64>());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,19 @@ use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||
pub fn generate_amenities(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
@@ -42,18 +50,14 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
||||
let ground_block: Block = OAK_PLANKS;
|
||||
let roof_block: Block = STONE_BLOCK_SLAB;
|
||||
|
||||
let polygon_coords: Vec<(i32, i32)> = element
|
||||
.nodes()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
// Use pre-computed flood fill from cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
|
||||
|
||||
if polygon_coords.is_empty() {
|
||||
if floor_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
|
||||
// Fill the floor area
|
||||
for (x, z) in floor_area.iter() {
|
||||
editor.set_block(ground_block, *x, 0, *z, None, None);
|
||||
@@ -80,8 +84,10 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
||||
"bench" => {
|
||||
// Place a bench
|
||||
if let Some(pt) = first_node {
|
||||
// 50% chance to 90 degrees rotate the bench using if
|
||||
if rand::random::<bool>() {
|
||||
// Use deterministic RNG for consistent bench orientation across region boundaries
|
||||
let mut rng = element_rng(element.id());
|
||||
// 50% chance to 90 degrees rotate the bench
|
||||
if rng.gen_bool(0.5) {
|
||||
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x + 1, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x - 1, 1, pt.z, None, None);
|
||||
@@ -95,12 +101,9 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
||||
"shelter" => {
|
||||
let roof_block: Block = STONE_BRICK_SLAB;
|
||||
|
||||
let polygon_coords: Vec<(i32, i32)> = element
|
||||
.nodes()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
// Use pre-computed flood fill from cache
|
||||
let roof_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
|
||||
|
||||
// Place fences and roof slabs at each corner node directly
|
||||
for node in element.nodes() {
|
||||
|
||||
@@ -3,37 +3,97 @@ use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::ProcessedWay;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
// TODO FIX
|
||||
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
|
||||
#[allow(dead_code)]
|
||||
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
if let Some(_bridge_type) = element.tags.get("bridge") {
|
||||
let bridge_height = 3; // Fixed height
|
||||
let bridge_height = 3; // Height above the ground level
|
||||
|
||||
// Get start and end node elevations and use MAX for level bridge deck
|
||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
||||
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
|
||||
let start_node = &element.nodes[0];
|
||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
start_y.max(end_y)
|
||||
} else {
|
||||
return; // Need at least 2 nodes for a bridge
|
||||
};
|
||||
|
||||
// Calculate total bridge length for ramp positioning
|
||||
let total_length: f64 = element
|
||||
.nodes
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
let dx = (pair[1].x - pair[0].x) as f64;
|
||||
let dz = (pair[1].z - pair[0].z) as f64;
|
||||
(dx * dx + dz * dz).sqrt()
|
||||
})
|
||||
.sum();
|
||||
|
||||
if total_length == 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut accumulated_length: f64 = 0.0;
|
||||
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev = &element.nodes[i - 1];
|
||||
let cur = &element.nodes[i];
|
||||
|
||||
let segment_dx = (cur.x - prev.x) as f64;
|
||||
let segment_dz = (cur.z - prev.z) as f64;
|
||||
let segment_length = (segment_dx * segment_dx + segment_dz * segment_dz).sqrt();
|
||||
|
||||
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
||||
|
||||
let total_length = points.len();
|
||||
let ramp_length = 6; // Length of ramp at each end
|
||||
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
|
||||
|
||||
for (idx, (x, _, z)) in points.iter().enumerate() {
|
||||
let height = if idx < ramp_length {
|
||||
// Calculate progress along this segment
|
||||
let segment_progress = if points.len() > 1 {
|
||||
idx as f64 / (points.len() - 1) as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Calculate overall progress along the entire bridge
|
||||
let point_distance = accumulated_length + segment_progress * segment_length;
|
||||
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
|
||||
let total_len_usize = total_length as usize;
|
||||
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
|
||||
|
||||
// Calculate ramp height offset
|
||||
let ramp_offset = if overall_idx < ramp_length {
|
||||
// Start ramp (rising)
|
||||
(idx * bridge_height) / ramp_length
|
||||
} else if idx >= total_length - ramp_length {
|
||||
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
|
||||
// End ramp (descending)
|
||||
((total_length - idx) * bridge_height) / ramp_length
|
||||
let dist_from_end = total_len_usize - overall_idx;
|
||||
(dist_from_end as f64 * bridge_height as f64 / ramp_length as f64) as i32
|
||||
} else {
|
||||
// Middle section (constant height)
|
||||
bridge_height
|
||||
};
|
||||
|
||||
// Use fixed bridge deck height (max of endpoints) plus ramp offset
|
||||
let bridge_y = bridge_deck_ground_y + ramp_offset;
|
||||
|
||||
// Place bridge blocks
|
||||
for dx in -2..=2 {
|
||||
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
*x + dx,
|
||||
bridge_y,
|
||||
*z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
accumulated_length += segment_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::colors::color_text_to_rgb_tuple;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::subprocessor::buildings_interior::generate_building_interior;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
@@ -28,6 +29,7 @@ pub fn generate_buildings(
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
relation_levels: Option<i32>,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Get min_level first so we can use it both for start_level and building height calculations
|
||||
let min_level = if let Some(min_level_str) = element.tags.get("building:min_level") {
|
||||
@@ -43,10 +45,9 @@ pub fn generate_buildings(
|
||||
let scale_factor = args.scale;
|
||||
let min_level_offset = multiply_scale(min_level * 4, scale_factor);
|
||||
|
||||
// Cache floodfill result: compute once and reuse throughout
|
||||
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
// Use pre-computed flood fill from cache
|
||||
let cached_floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
let cached_footprint_size = cached_floor_area.len();
|
||||
|
||||
// Use fixed starting Y coordinate based on maximum ground level when terrain is enabled
|
||||
@@ -121,7 +122,8 @@ pub fn generate_buildings(
|
||||
let mut processed_points: HashSet<(i32, i32)> = HashSet::new();
|
||||
let mut building_height: i32 = ((6.0 * scale_factor) as i32).max(3); // Default building height with scale and minimum
|
||||
let mut is_tall_building = false;
|
||||
let mut rng = rand::thread_rng();
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(element.id);
|
||||
let use_vertical_windows = rng.gen_bool(0.7);
|
||||
let use_accent_roof_line = rng.gen_bool(0.25);
|
||||
|
||||
@@ -386,7 +388,7 @@ pub fn generate_buildings(
|
||||
building_height = ((23.0 * scale_factor) as i32).max(3);
|
||||
}
|
||||
} else if building_type == "bridge" {
|
||||
generate_bridge(editor, element, args.timeout.as_ref());
|
||||
generate_bridge(editor, element, flood_fill_cache, args.timeout.as_ref());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1484,6 +1486,7 @@ pub fn generate_building_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
relation: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Extract levels from relation tags
|
||||
let relation_levels = relation
|
||||
@@ -1495,7 +1498,13 @@ pub fn generate_building_from_relation(
|
||||
// Process the outer way to create the building walls
|
||||
for member in &relation.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_buildings(editor, &member.way, args, Some(relation_levels));
|
||||
generate_buildings(
|
||||
editor,
|
||||
&member.way,
|
||||
args,
|
||||
Some(relation_levels),
|
||||
flood_fill_cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,52 +1525,18 @@ pub fn generate_building_from_relation(
|
||||
}
|
||||
|
||||
/// Generates a bridge structure, paying attention to the "level" tag.
|
||||
/// Bridge deck is interpolated between start and end point elevations to avoid
|
||||
/// being dragged down by valleys underneath.
|
||||
fn generate_bridge(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
floodfill_timeout: Option<&Duration>,
|
||||
) {
|
||||
let floor_block: Block = STONE;
|
||||
let railing_block: Block = STONE_BRICKS;
|
||||
|
||||
// Process the nodes to create bridge pathways and railings
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
for node in &element.nodes {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
// Calculate bridge level based on the "level" tag
|
||||
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
|
||||
if let Ok(level) = level_str.parse::<i32>() {
|
||||
(level * 3) + 1
|
||||
} else {
|
||||
1 // Default elevation
|
||||
}
|
||||
} else {
|
||||
1 // Default elevation
|
||||
};
|
||||
|
||||
// Create bridge path using Bresenham's line
|
||||
if let Some(prev) = previous_node {
|
||||
let bridge_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(prev.0, bridge_y_offset, prev.1, x, bridge_y_offset, z);
|
||||
|
||||
for (bx, by, bz) in bridge_points {
|
||||
// Place railing blocks
|
||||
editor.set_block(railing_block, bx, by + 1, bz, None, None);
|
||||
editor.set_block(railing_block, bx, by, bz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
// Flood fill the area between the bridge path nodes
|
||||
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
|
||||
let bridge_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, floodfill_timeout);
|
||||
|
||||
// Calculate bridge level based on the "level" tag
|
||||
// Calculate bridge level offset based on the "level" tag
|
||||
let bridge_y_offset = if let Some(level_str) = element.tags.get("level") {
|
||||
if let Ok(level) = level_str.parse::<i32>() {
|
||||
(level * 3) + 1
|
||||
@@ -1572,8 +1547,51 @@ fn generate_bridge(
|
||||
1 // Default elevation
|
||||
};
|
||||
|
||||
// Need at least 2 nodes to form a bridge
|
||||
if element.nodes.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get start and end node elevations and use MAX for level bridge deck
|
||||
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
|
||||
let start_node = &element.nodes[0];
|
||||
let end_node = &element.nodes[element.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
let bridge_deck_ground_y = start_y.max(end_y);
|
||||
|
||||
// Process the nodes to create bridge pathways and railings
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
|
||||
for node in &element.nodes {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
// Create bridge path using Bresenham's line
|
||||
if let Some(prev) = previous_node {
|
||||
let bridge_points: Vec<(i32, i32, i32)> = bresenham_line(prev.0, 0, prev.1, x, 0, z);
|
||||
|
||||
for (bx, _, bz) in bridge_points.iter() {
|
||||
// Use fixed bridge deck height (max of endpoints)
|
||||
let bridge_y = bridge_deck_ground_y + bridge_y_offset;
|
||||
|
||||
// Place railing blocks
|
||||
editor.set_block_absolute(railing_block, *bx, bridge_y + 1, *bz, None, None);
|
||||
editor.set_block_absolute(railing_block, *bx, bridge_y, *bz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
// Flood fill the area between the bridge path nodes (uses cache)
|
||||
let bridge_area: Vec<(i32, i32)> = flood_fill_cache.get_or_compute(element, floodfill_timeout);
|
||||
|
||||
// Use the same level bridge deck height for filled areas
|
||||
let floor_y = bridge_deck_ground_y + bridge_y_offset;
|
||||
|
||||
// Place floor blocks
|
||||
for (x, z) in bridge_area {
|
||||
editor.set_block(floor_block, x, bridge_y_offset, z, None, None);
|
||||
editor.set_block_absolute(floor_block, x, floor_y, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
@@ -10,14 +10,24 @@ use std::collections::HashMap;
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
|
||||
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
|
||||
|
||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||
pub fn generate_highways(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||
generate_highways_internal(
|
||||
editor,
|
||||
element,
|
||||
args,
|
||||
highway_connectivity,
|
||||
flood_fill_cache,
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
@@ -66,6 +76,7 @@ fn generate_highways_internal(
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
if let Some(highway_type) = element.tags().get("highway") {
|
||||
if highway_type == "street_lamp" {
|
||||
@@ -137,14 +148,9 @@ fn generate_highways_internal(
|
||||
};
|
||||
}
|
||||
|
||||
// Fill the area using flood fill or by iterating through the nodes
|
||||
let polygon_coords: Vec<(i32, i32)> = way
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
// Fill the area using flood fill cache
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||
|
||||
for (x, z) in filled_area {
|
||||
editor.set_block(surface_block, x, 0, z, None, None);
|
||||
@@ -157,6 +163,11 @@ fn generate_highways_internal(
|
||||
let mut add_outline = false;
|
||||
let scale_factor = args.scale;
|
||||
|
||||
// Check if this is a bridge - bridges need special elevation handling
|
||||
// to span across valleys instead of following terrain
|
||||
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
||||
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
|
||||
// Parse the layer value for elevation calculation
|
||||
let layer_value = element
|
||||
.tags()
|
||||
@@ -246,6 +257,7 @@ fn generate_highways_internal(
|
||||
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||
|
||||
// Check if we need slopes at start and end
|
||||
// This is used for overpasses that need ramps to ground-level roads
|
||||
let needs_start_slope =
|
||||
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||
let needs_end_slope = should_add_slope_at_node(
|
||||
@@ -254,10 +266,67 @@ fn generate_highways_internal(
|
||||
highway_connectivity,
|
||||
);
|
||||
|
||||
// Calculate total way length for slope distribution
|
||||
// Calculate total way length for slope distribution (needed before valley bridge check)
|
||||
let total_way_length = calculate_way_length(way);
|
||||
|
||||
// Check if this is a short isolated elevated segment - if so, treat as ground level
|
||||
// For bridges: detect if this spans a valley by checking terrain profile
|
||||
// A valley bridge has terrain that dips significantly below the endpoints
|
||||
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
|
||||
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
|
||||
let terrain_enabled = editor
|
||||
.get_ground()
|
||||
.map(|g| g.elevation_enabled)
|
||||
.unwrap_or(false);
|
||||
|
||||
let (is_valley_bridge, bridge_deck_y) =
|
||||
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
|
||||
let start_node = &way.nodes[0];
|
||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||
let start_y = editor.get_ground_level(start_node.x, start_node.z);
|
||||
let end_y = editor.get_ground_level(end_node.x, end_node.z);
|
||||
let max_endpoint_y = start_y.max(end_y);
|
||||
|
||||
// Sample terrain at middle nodes only (excluding endpoints we already have)
|
||||
// This avoids redundant get_ground_level() calls
|
||||
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
|
||||
let sampled_min = if middle_nodes.is_empty() {
|
||||
// No middle nodes, just use endpoints
|
||||
start_y.min(end_y)
|
||||
} else {
|
||||
// Sample up to 3 middle points (5 total with endpoints) for performance
|
||||
// Valleys are wide terrain features, so sparse sampling is sufficient
|
||||
let sample_count = middle_nodes.len().min(3);
|
||||
let step = if sample_count > 1 {
|
||||
(middle_nodes.len() - 1) / (sample_count - 1)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
middle_nodes
|
||||
.iter()
|
||||
.step_by(step.max(1))
|
||||
.map(|node| editor.get_ground_level(node.x, node.z))
|
||||
.min()
|
||||
.unwrap_or(max_endpoint_y)
|
||||
};
|
||||
|
||||
// Include endpoint elevations in the minimum calculation
|
||||
let min_terrain_y = sampled_min.min(start_y).min(end_y);
|
||||
|
||||
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
|
||||
// treat as valley bridge
|
||||
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
|
||||
|
||||
if is_valley {
|
||||
(true, max_endpoint_y)
|
||||
} else {
|
||||
(false, 0)
|
||||
}
|
||||
} else {
|
||||
(false, 0)
|
||||
};
|
||||
|
||||
// Check if this is a short isolated elevated segment (layer > 0), if so, treat as ground level
|
||||
let is_short_isolated_elevated =
|
||||
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||
|
||||
@@ -294,17 +363,28 @@ fn generate_highways_internal(
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
|
||||
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||
// Calculate Y elevation for this point based on slopes and layer
|
||||
let current_y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
// Calculate Y elevation for this point
|
||||
// For valley bridges: use fixed deck height (max of endpoints) to stay level
|
||||
// For overpasses and regular roads: use terrain-relative elevation with slopes
|
||||
let (current_y, use_absolute_y) = if is_valley_bridge {
|
||||
// Valley bridge deck is level at the maximum endpoint elevation
|
||||
// Don't add base_elevation - the layer tag indicates it's above water/road,
|
||||
// not that it should be higher than the terrain endpoints
|
||||
(bridge_deck_y, true)
|
||||
} else {
|
||||
// Regular road or overpass: use terrain-relative calculation with ramps
|
||||
let y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
(y, false)
|
||||
};
|
||||
|
||||
// Draw the road surface for the entire width
|
||||
for dx in -block_range..=block_range {
|
||||
@@ -320,12 +400,32 @@ fn generate_highways_internal(
|
||||
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
|
||||
if is_horizontal {
|
||||
if set_x % 2 < 1 {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
@@ -339,12 +439,32 @@ fn generate_highways_internal(
|
||||
);
|
||||
}
|
||||
} else if set_z % 2 < 1 {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
@@ -357,6 +477,15 @@ fn generate_highways_internal(
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
block_type,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
block_type,
|
||||
@@ -368,30 +497,53 @@ fn generate_highways_internal(
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone brick foundation underneath elevated highways for thickness
|
||||
if effective_elevation > 0 && current_y > 0 {
|
||||
// Add stone brick foundation underneath elevated highways/bridges for thickness
|
||||
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
|
||||
// Add 1 layer of stone bricks underneath the highway surface
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add support pillars for elevated highways
|
||||
if effective_elevation != 0 && current_y > 0 {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
// Add support pillars for elevated highways/bridges
|
||||
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
|
||||
if use_absolute_y {
|
||||
add_highway_support_pillar_absolute(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
} else {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,27 +554,49 @@ fn generate_highways_internal(
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x - block_range - 1;
|
||||
let outline_z = z + dz;
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Right outline
|
||||
for dz in -block_range..=block_range {
|
||||
let outline_x = x + block_range + 1;
|
||||
let outline_z = z + dz;
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +605,25 @@ fn generate_highways_internal(
|
||||
if stripe_length < dash_length {
|
||||
let stripe_x: i32 = *x;
|
||||
let stripe_z: i32 = *z;
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
if use_absolute_y {
|
||||
editor.set_block_absolute(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Increment stripe_length and reset after completing a dash and gap
|
||||
@@ -582,6 +767,46 @@ fn add_highway_support_pillar(
|
||||
}
|
||||
}
|
||||
|
||||
/// Add support pillars for bridges using absolute Y coordinates
|
||||
/// Pillars extend from ground level up to the bridge deck
|
||||
fn add_highway_support_pillar_absolute(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
bridge_deck_y: i32,
|
||||
z: i32,
|
||||
dx: i32,
|
||||
dz: i32,
|
||||
_block_range: i32, // Keep for future use
|
||||
) {
|
||||
// Only add pillars at specific intervals and positions
|
||||
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
||||
// Get the actual ground level at this position
|
||||
let ground_y = editor.get_ground_level(x, z);
|
||||
|
||||
// Add pillar from ground up to bridge deck
|
||||
// Only if the bridge is actually above the ground
|
||||
if bridge_deck_y > ground_y {
|
||||
for y in (ground_y + 1)..bridge_deck_y {
|
||||
editor.set_block_absolute(STONE_BRICKS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Add pillar base at ground level
|
||||
for base_dx in -1..=1 {
|
||||
for base_dz in -1..=1 {
|
||||
editor.set_block_absolute(
|
||||
STONE_BRICKS,
|
||||
x + base_dx,
|
||||
ground_y,
|
||||
z + base_dz,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a siding using stone brick slabs
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
||||
pub fn generate_landuse(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
// Determine block type based on landuse tag
|
||||
let binding: String = "".to_string();
|
||||
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(element.id);
|
||||
|
||||
let block_type = match landuse_tag.as_str() {
|
||||
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
|
||||
"farmland" => FARMLAND,
|
||||
@@ -22,13 +32,13 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if residential_tag == "rural" {
|
||||
GRASS_BLOCK
|
||||
} else {
|
||||
STONE_BRICKS
|
||||
STONE_BRICKS // Placeholder, will be randomized per-block
|
||||
}
|
||||
}
|
||||
"commercial" => SMOOTH_STONE,
|
||||
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
|
||||
"education" => POLISHED_ANDESITE,
|
||||
"religious" => POLISHED_ANDESITE,
|
||||
"industrial" => COBBLESTONE,
|
||||
"industrial" => STONE, // Placeholder, will be randomized per-block
|
||||
"military" => GRAY_CONCRETE,
|
||||
"railway" => GRAVEL,
|
||||
"landfill" => {
|
||||
@@ -44,19 +54,56 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
_ => GRASS_BLOCK,
|
||||
};
|
||||
|
||||
// Get the area of the landuse element
|
||||
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
// Get the area of the landuse element using cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
for (x, z) in floor_area {
|
||||
if landuse_tag == "traffic_island" {
|
||||
editor.set_block(block_type, x, 1, z, None, None);
|
||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
|
||||
// Apply per-block randomness for certain landuse types
|
||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 72 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 87 {
|
||||
CRACKED_STONE_BRICKS
|
||||
} else if random_value < 92 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "commercial" {
|
||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 40 {
|
||||
SMOOTH_STONE
|
||||
} else if random_value < 70 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 90 {
|
||||
STONE
|
||||
} else {
|
||||
COBBLESTONE
|
||||
}
|
||||
} else if landuse_tag == "industrial" {
|
||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 70 {
|
||||
STONE
|
||||
} else if random_value < 90 {
|
||||
STONE_BRICKS
|
||||
} else {
|
||||
SMOOTH_STONE
|
||||
}
|
||||
} else {
|
||||
editor.set_block(block_type, x, 0, z, None, None);
|
||||
block_type
|
||||
};
|
||||
|
||||
if landuse_tag == "traffic_island" {
|
||||
editor.set_block(actual_block, x, 1, z, None, None);
|
||||
} else if landuse_tag == "construction" || landuse_tag == "railway" {
|
||||
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
editor.set_block(actual_block, x, 0, z, None, None);
|
||||
}
|
||||
|
||||
// Add specific features for different landuse types
|
||||
@@ -84,7 +131,7 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
}
|
||||
} else if random_choice < 33 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
}
|
||||
@@ -94,7 +141,7 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 20 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||
1 => OAK_LEAVES,
|
||||
@@ -105,7 +152,11 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
if rng.gen_range(0..100) < 12 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +258,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +268,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -225,11 +278,13 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 6 {
|
||||
editor.set_block(RED_FLOWER, x, 1, z, None, None);
|
||||
} else if random_choice < 9 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else if random_choice < 40 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else if random_choice < 800 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
@@ -237,11 +292,12 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
}
|
||||
"orchard" => {
|
||||
if x % 18 == 0 && z % 10 == 0 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.gen_range(0..100) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -275,12 +331,20 @@ pub fn generate_landuse_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("landuse") {
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_landuse(editor, &member.way.clone(), args);
|
||||
generate_landuse(
|
||||
editor,
|
||||
&member.way.clone(),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +366,13 @@ pub fn generate_landuse_from_relation(
|
||||
};
|
||||
|
||||
// Generate landuse area from combined way
|
||||
generate_landuse(editor, &combined_way, args);
|
||||
generate_landuse(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
|
||||
pub fn generate_leisure(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(leisure_type) = element.tags.get("leisure") {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
|
||||
@@ -74,15 +81,13 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
previous_node = Some((node.x, node.z));
|
||||
}
|
||||
|
||||
// Flood-fill the interior of the leisure area
|
||||
// Flood-fill the interior of the leisure area using cache
|
||||
if corner_addup != (0, 0, 0) {
|
||||
let polygon_coords: Vec<(i32, i32)> = element
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(element.id);
|
||||
|
||||
for (x, z) in filled_area {
|
||||
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
|
||||
@@ -91,7 +96,6 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
||||
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
|
||||
{
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
let random_choice: i32 = rng.gen_range(0..1000);
|
||||
|
||||
match random_choice {
|
||||
@@ -115,7 +119,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
}
|
||||
105..120 => {
|
||||
// Tree
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -123,7 +127,6 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
|
||||
// Add playground or recreation ground features
|
||||
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
let random_choice: i32 = rng.gen_range(0..5000);
|
||||
|
||||
match random_choice {
|
||||
@@ -176,12 +179,20 @@ pub fn generate_leisure_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
// First generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_leisure(editor, &member.way, args);
|
||||
generate_leisure(
|
||||
editor,
|
||||
&member.way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +212,12 @@ pub fn generate_leisure_from_relation(
|
||||
};
|
||||
|
||||
// Generate leisure area from combined way
|
||||
generate_leisure(editor, &combined_way, args);
|
||||
generate_leisure(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||
pub fn generate_natural(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if let Some(natural_type) = element.tags().get("natural") {
|
||||
if natural_type == "tree" {
|
||||
if let ProcessedElement::Node(node) = element {
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -69,17 +76,13 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
previous_node = Some((x, z));
|
||||
}
|
||||
|
||||
// If there are natural nodes, flood-fill the area
|
||||
// If there are natural nodes, flood-fill the area using cache
|
||||
if corner_addup != (0, 0, 0) {
|
||||
let polygon_coords: Vec<(i32, i32)> = way
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
|
||||
.collect();
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_area(&polygon_coords, args.timeout.as_ref());
|
||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||
|
||||
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(way.id);
|
||||
|
||||
for (x, z) in filled_area {
|
||||
editor.set_block(block_type, x, 0, z, None, None);
|
||||
@@ -132,7 +135,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
}
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
@@ -161,7 +164,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
}
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
@@ -220,7 +223,11 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
// TODO implement mangrove
|
||||
let random_choice: i32 = rng.gen_range(0..40);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
@@ -304,6 +311,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
Tree::create(
|
||||
editor,
|
||||
(cluster_x, 1, cluster_z),
|
||||
Some(building_footprints),
|
||||
);
|
||||
} else if vegetation_chance < 15 {
|
||||
// 15% chance for grass
|
||||
@@ -416,7 +424,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
|
||||
let hill_chance = rng.gen_range(0..1000);
|
||||
if hill_chance == 0 {
|
||||
// 0.1% chance for rare trees
|
||||
Tree::create(editor, (x, 1, z));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if hill_chance < 50 {
|
||||
// 5% chance for flowers
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
@@ -448,12 +456,20 @@ pub fn generate_natural_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("natural") {
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
generate_natural(editor, &ProcessedElement::Way(member.way.clone()), args);
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way((*member.way).clone()),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +491,13 @@ pub fn generate_natural_from_relation(
|
||||
};
|
||||
|
||||
// Generate natural area from combined way
|
||||
generate_natural(editor, &ProcessedElement::Way(combined_way), args);
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way(combined_way),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::coord_rng;
|
||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
@@ -107,7 +109,25 @@ pub struct Tree<'a> {
|
||||
}
|
||||
|
||||
impl Tree<'_> {
|
||||
pub fn create(editor: &mut WorldEditor, (x, y, z): Coord) {
|
||||
/// Creates a tree at the specified coordinates.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `editor` - The world editor to place blocks
|
||||
/// * `(x, y, z)` - The base coordinates for the tree
|
||||
/// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings.
|
||||
/// If provided, trees will not be placed at coordinates within this bitmap.
|
||||
pub fn create(
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Skip if this coordinate is inside a building
|
||||
if let Some(footprints) = building_footprints {
|
||||
if footprints.contains(x, z) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut blacklist: Vec<Block> = Vec::new();
|
||||
blacklist.extend(Self::get_building_wall_blocks());
|
||||
blacklist.extend(Self::get_building_floor_blocks());
|
||||
@@ -115,7 +135,9 @@ impl Tree<'_> {
|
||||
blacklist.extend(Self::get_functional_blocks());
|
||||
blacklist.push(WATER);
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
@@ -15,15 +14,13 @@ pub fn generate_water_area_from_way(
|
||||
element: &ProcessedWay,
|
||||
_xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let outers = [element.nodes.clone()];
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping way {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &[], start_time);
|
||||
generate_water_areas(editor, &outers, &[]);
|
||||
}
|
||||
|
||||
pub fn generate_water_areas_from_relation(
|
||||
@@ -31,8 +28,6 @@ pub fn generate_water_areas_from_relation(
|
||||
element: &ProcessedRelation,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Check if this is a water relation (either with water tag or natural=water)
|
||||
let is_water = element.tags.contains_key("water")
|
||||
|| element
|
||||
@@ -123,14 +118,13 @@ pub fn generate_water_areas_from_relation(
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &inners, start_time);
|
||||
generate_water_areas(editor, &outers, &inners);
|
||||
}
|
||||
|
||||
fn generate_water_areas(
|
||||
editor: &mut WorldEditor,
|
||||
outers: &[Vec<ProcessedNode>],
|
||||
inners: &[Vec<ProcessedNode>],
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Calculate polygon bounding box to limit fill area
|
||||
let mut poly_min_x = i32::MAX;
|
||||
@@ -169,9 +163,7 @@ fn generate_water_areas(
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(
|
||||
min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor, start_time,
|
||||
);
|
||||
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||
}
|
||||
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
@@ -308,7 +300,6 @@ fn inverse_floodfill(
|
||||
outers: Vec<Vec<XZPoint>>,
|
||||
inners: Vec<Vec<XZPoint>>,
|
||||
editor: &mut WorldEditor,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Convert to geo Polygons with normalized winding order
|
||||
let inners: Vec<_> = inners
|
||||
@@ -341,14 +332,7 @@ fn inverse_floodfill(
|
||||
})
|
||||
.collect();
|
||||
|
||||
inverse_floodfill_recursive(
|
||||
(min_x, min_z),
|
||||
(max_x, max_z),
|
||||
&outers,
|
||||
&inners,
|
||||
editor,
|
||||
start_time,
|
||||
);
|
||||
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
|
||||
}
|
||||
|
||||
fn inverse_floodfill_recursive(
|
||||
@@ -357,12 +341,11 @@ fn inverse_floodfill_recursive(
|
||||
outers: &[Polygon],
|
||||
inners: &[Polygon],
|
||||
editor: &mut WorldEditor,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Check if we've exceeded 25 seconds
|
||||
if start_time.elapsed().as_secs() > 25 {
|
||||
println!("Water area generation exceeded 25 seconds, continuing anyway");
|
||||
}
|
||||
// Check if we've exceeded 40 seconds
|
||||
// if start_time.elapsed().as_secs() > 40 {
|
||||
// println!("Water area generation exceeded 40 seconds, continuing anyway");
|
||||
// }
|
||||
|
||||
const ITERATIVE_THRES: i64 = 10_000;
|
||||
|
||||
@@ -417,7 +400,6 @@ fn inverse_floodfill_recursive(
|
||||
&outers_intersects,
|
||||
&inners_intersects,
|
||||
editor,
|
||||
start_time,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance}
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use image::Rgb;
|
||||
use std::path::Path;
|
||||
use rayon::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Maximum Y coordinate in Minecraft (build height limit)
|
||||
const MAX_Y: i32 = 319;
|
||||
/// Scale factor for converting real elevation to Minecraft heights
|
||||
const BASE_HEIGHT_SCALE: f64 = 0.7;
|
||||
/// AWS S3 Terrarium tiles endpoint (no API key required)
|
||||
const AWS_TERRARIUM_URL: &str =
|
||||
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
|
||||
@@ -17,6 +16,8 @@ const TERRARIUM_OFFSET: f64 = 32768.0;
|
||||
const MIN_ZOOM: u8 = 10;
|
||||
/// Maximum zoom level for terrain tiles
|
||||
const MAX_ZOOM: u8 = 15;
|
||||
/// Maximum concurrent tile downloads to be respectful to AWS
|
||||
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
|
||||
|
||||
/// Holds processed elevation data and metadata
|
||||
#[derive(Clone)]
|
||||
@@ -29,6 +30,11 @@ pub struct ElevationData {
|
||||
pub(crate) height: usize,
|
||||
}
|
||||
|
||||
/// RGB image buffer type for elevation tiles
|
||||
type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
|
||||
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
|
||||
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
|
||||
|
||||
/// Calculates appropriate zoom level for the given bounding box
|
||||
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
|
||||
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
|
||||
@@ -53,21 +59,103 @@ fn download_tile(
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
let response = client.get(&url).send().map_err(|e| e.to_string())?;
|
||||
response.error_for_status_ref().map_err(|e| e.to_string())?;
|
||||
let bytes = response.bytes().map_err(|e| e.to_string())?;
|
||||
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
|
||||
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
|
||||
/// Fetches a tile from cache or downloads it if not available
|
||||
/// Note: In parallel execution, multiple threads may attempt to download the same tile
|
||||
/// if it's missing or corrupted. This is harmless (just wastes some bandwidth) as
|
||||
/// file writes are atomic at the OS level.
|
||||
fn fetch_or_load_tile(
|
||||
client: &reqwest::blocking::Client,
|
||||
tile_x: u32,
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
|
||||
if tile_path.exists() {
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = std::fs::metadata(tile_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile appears too small, refetching.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(e) = std::fs::remove_file(tile_path) {
|
||||
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
return download_tile(client, tile_x, tile_y, zoom, tile_path);
|
||||
}
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(tile_path) {
|
||||
Ok(img) => {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(e) = std::fs::remove_file(tile_path) {
|
||||
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(client, tile_x, tile_y, zoom, tile_path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(client, tile_x, tile_y, zoom, tile_path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_elevation_data(
|
||||
bbox: &LLBBox,
|
||||
scale: f64,
|
||||
@@ -91,101 +179,68 @@ pub fn fetch_elevation_data(
|
||||
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
|
||||
let mut extreme_values_found = Vec::new(); // Track extreme values for debugging
|
||||
|
||||
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
|
||||
|
||||
let tile_cache_dir = Path::new("./arnis-tile-cache");
|
||||
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
|
||||
if !tile_cache_dir.exists() {
|
||||
std::fs::create_dir_all(tile_cache_dir)?;
|
||||
std::fs::create_dir_all(&tile_cache_dir)?;
|
||||
}
|
||||
|
||||
// Fetch and process each tile
|
||||
for (tile_x, tile_y) in &tiles {
|
||||
// Check if tile is already cached
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
// Create a shared HTTP client for connection pooling
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = match std::fs::metadata(&tile_path) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
// Download tiles in parallel with limited concurrency to be respectful to AWS
|
||||
let num_tiles = tiles.len();
|
||||
println!(
|
||||
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
|
||||
);
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
// Use a custom thread pool to limit concurrent downloads
|
||||
let thread_pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(MAX_CONCURRENT_DOWNLOADS)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
|
||||
|
||||
let downloaded_tiles: Vec<TileDownloadResult> = thread_pool.install(|| {
|
||||
tiles
|
||||
.par_iter()
|
||||
.map(|(tile_x, tile_y)| {
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
|
||||
let rgb_img = fetch_or_load_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?;
|
||||
Ok(((*tile_x, *tile_y), rgb_img))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
// Check for any download errors
|
||||
let mut successful_tiles = Vec::new();
|
||||
for result in downloaded_tiles {
|
||||
match result {
|
||||
Ok(tile_data) => successful_tiles.push(tile_data),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to download tile: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
&format!("Failed to download elevation tile: {e}"),
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
} else {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
println!("Processing {} elevation tiles...", successful_tiles.len());
|
||||
|
||||
// Process tiles sequentially (writes to shared height_grid)
|
||||
for ((tile_x, tile_y), rgb_img) in successful_tiles {
|
||||
// Only process pixels that fall within the requested bbox
|
||||
for (y, row) in rgb_img.rows().enumerate() {
|
||||
for (x, pixel) in row.enumerate() {
|
||||
// Convert tile pixel coordinates back to geographic coordinates
|
||||
let pixel_lng = ((*tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
|
||||
let pixel_lng = ((tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
|
||||
* 360.0
|
||||
- 180.0;
|
||||
let pixel_lat_rad = std::f64::consts::PI
|
||||
* (1.0
|
||||
- 2.0 * (*tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
|
||||
- 2.0 * (tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
|
||||
let pixel_lat = pixel_lat_rad.sinh().atan().to_degrees();
|
||||
|
||||
// Skip pixels outside the requested bounding box
|
||||
@@ -261,20 +316,18 @@ pub fn fetch_elevation_data(
|
||||
// This smooths terrain proportionally while preserving more detail.
|
||||
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
|
||||
|
||||
let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||
eprintln!(
|
||||
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||
/*eprintln!(
|
||||
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
||||
grid_width, grid_height, sigma, blur_percentage
|
||||
);
|
||||
|
||||
/* eprintln!(
|
||||
"Grid: {}x{}, Blur sigma: {:.2}",
|
||||
grid_width, grid_height, sigma
|
||||
); */
|
||||
);*/
|
||||
|
||||
// Continue with the existing blur and conversion to Minecraft heights...
|
||||
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
|
||||
|
||||
// Release raw height grid
|
||||
drop(height_grid);
|
||||
|
||||
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
|
||||
|
||||
// Find min/max in raw data
|
||||
@@ -298,7 +351,7 @@ pub fn fetch_elevation_data(
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Height data range: {min_height} to {max_height} m");
|
||||
//eprintln!("Height data range: {min_height} to {max_height} m");
|
||||
if extreme_low_count > 0 {
|
||||
eprintln!(
|
||||
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
|
||||
@@ -311,35 +364,58 @@ pub fn fetch_elevation_data(
|
||||
}
|
||||
|
||||
let height_range: f64 = max_height - min_height;
|
||||
// Apply scale factor to height scaling
|
||||
let mut height_scale: f64 = BASE_HEIGHT_SCALE * scale.sqrt(); // sqrt to make height scaling less extreme
|
||||
let mut scaled_range: f64 = height_range * height_scale;
|
||||
|
||||
// Adaptive scaling: ensure we don't exceed reasonable Y range
|
||||
let available_y_range = (MAX_Y - ground_level) as f64;
|
||||
let safety_margin = 0.9; // Use 90% of available range
|
||||
let max_allowed_range = available_y_range * safety_margin;
|
||||
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
|
||||
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
|
||||
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
|
||||
let ideal_scaled_range: f64 = height_range * scale;
|
||||
|
||||
if scaled_range > max_allowed_range {
|
||||
let adjustment_factor = max_allowed_range / scaled_range;
|
||||
height_scale *= adjustment_factor;
|
||||
scaled_range = height_range * height_scale;
|
||||
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
|
||||
// Leave a buffer at the top for buildings, trees, and other structures
|
||||
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
|
||||
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
|
||||
|
||||
// Determine final height scale:
|
||||
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
|
||||
// - Only compress if the terrain would exceed the build height
|
||||
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
|
||||
// Terrain fits! Use realistic scaling
|
||||
eprintln!(
|
||||
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
|
||||
"Realistic elevation: {:.1}m range fits in {} available blocks",
|
||||
height_range, available_y_range as i32
|
||||
);
|
||||
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
|
||||
}
|
||||
ideal_scaled_range
|
||||
} else {
|
||||
// Terrain too tall, compress to fit within Minecraft limits
|
||||
let compression_factor: f64 = available_y_range / height_range;
|
||||
let compressed_range: f64 = height_range * compression_factor;
|
||||
eprintln!(
|
||||
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
|
||||
height_range,
|
||||
compressed_range,
|
||||
height_range / compressed_range,
|
||||
compressed_range / height_range
|
||||
);
|
||||
compressed_range
|
||||
};
|
||||
|
||||
// Convert to scaled Minecraft Y coordinates
|
||||
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
|
||||
for row in blurred_heights {
|
||||
let mc_row: Vec<i32> = row
|
||||
.iter()
|
||||
.map(|&h| {
|
||||
// Scale the height differences
|
||||
let relative_height: f64 = (h - min_height) / height_range;
|
||||
// Calculate relative position within the elevation range (0.0 to 1.0)
|
||||
let relative_height: f64 = if height_range > 0.0 {
|
||||
(h - min_height) / height_range
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Scale to Minecraft blocks and add to ground level
|
||||
let scaled_height: f64 = relative_height * scaled_range;
|
||||
// With terrain enabled, ground_level is used as the MIN_Y for terrain
|
||||
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
|
||||
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
|
||||
((ground_level as f64 + scaled_height).round() as i32)
|
||||
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
|
||||
})
|
||||
.collect();
|
||||
mc_heights.push(mc_row);
|
||||
@@ -353,7 +429,7 @@ pub fn fetch_elevation_data(
|
||||
max_block_height = max_block_height.max(height);
|
||||
}
|
||||
}
|
||||
eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
||||
//eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
|
||||
|
||||
Ok(ElevationData {
|
||||
heights: mc_heights,
|
||||
@@ -513,7 +589,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
||||
let min_reasonable = all_heights[p1_idx];
|
||||
let max_reasonable = all_heights[p99_idx];
|
||||
|
||||
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
|
||||
|
||||
let mut outliers_filtered = 0;
|
||||
|
||||
@@ -528,7 +604,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
|
||||
}
|
||||
|
||||
if outliers_filtered > 0 {
|
||||
eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
||||
//eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
|
||||
// Re-run the NaN filling to interpolate the filtered values
|
||||
fill_nan_values(height_grid);
|
||||
}
|
||||
|
||||
338
src/floodfill_cache.rs
Normal file
338
src/floodfill_cache.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
//! Pre-computed flood fill cache for parallel polygon filling.
|
||||
//!
|
||||
//! This module provides a way to pre-compute all flood fill operations in parallel
|
||||
//! before the main element processing loop, then retrieve cached results during
|
||||
//! sequential processing.
|
||||
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedWay};
|
||||
use fnv::FnvHashMap;
|
||||
use rayon::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
||||
///
|
||||
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
||||
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
||||
///
|
||||
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
||||
pub struct BuildingFootprintBitmap {
|
||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
||||
bits: Vec<u8>,
|
||||
/// Minimum x coordinate (offset for indexing)
|
||||
min_x: i32,
|
||||
/// Minimum z coordinate (offset for indexing)
|
||||
min_z: i32,
|
||||
/// Width of the world (max_x - min_x + 1)
|
||||
width: usize,
|
||||
/// Height of the world (max_z - min_z + 1)
|
||||
height: usize,
|
||||
/// Number of coordinates marked as building footprints
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl BuildingFootprintBitmap {
|
||||
/// Creates a new empty bitmap covering the given world bounds.
|
||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
||||
let min_x = xzbbox.min_x();
|
||||
let min_z = xzbbox.min_z();
|
||||
// Use i64 to avoid overflow when world spans more than i32::MAX in either dimension
|
||||
let width = (i64::from(xzbbox.max_x()) - i64::from(min_x) + 1) as usize;
|
||||
let height = (i64::from(xzbbox.max_z()) - i64::from(min_z) + 1) as usize;
|
||||
|
||||
// Calculate number of bytes needed (round up to nearest byte)
|
||||
let total_bits = width
|
||||
.checked_mul(height)
|
||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
||||
let num_bytes = total_bits.div_ceil(8);
|
||||
|
||||
Self {
|
||||
bits: vec![0u8; num_bytes],
|
||||
min_x,
|
||||
min_z,
|
||||
width,
|
||||
height,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts (x, z) coordinate to bit index, returning None if out of bounds.
|
||||
#[inline]
|
||||
fn coord_to_index(&self, x: i32, z: i32) -> Option<usize> {
|
||||
// Use i64 arithmetic to avoid overflow when coordinates span large ranges
|
||||
let local_x = i64::from(x) - i64::from(self.min_x);
|
||||
let local_z = i64::from(z) - i64::from(self.min_z);
|
||||
|
||||
if local_x < 0 || local_z < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let local_x = local_x as usize;
|
||||
let local_z = local_z as usize;
|
||||
|
||||
if local_x >= self.width || local_z >= self.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Safe: bounds checks above ensure this won't overflow (max = total_bits - 1)
|
||||
Some(local_z * self.width + local_x)
|
||||
}
|
||||
|
||||
/// Sets a coordinate as part of a building footprint.
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: i32, z: i32) {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
let byte_index = bit_index / 8;
|
||||
let bit_offset = bit_index % 8;
|
||||
|
||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
||||
let mask = 1u8 << bit_offset;
|
||||
// Only increment count if bit wasn't already set
|
||||
if self.bits[byte_index] & mask == 0 {
|
||||
self.bits[byte_index] |= mask;
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a coordinate is part of a building footprint.
|
||||
#[inline]
|
||||
pub fn contains(&self, x: i32, z: i32) -> bool {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
let byte_index = bit_index / 8;
|
||||
let bit_offset = bit_index % 8;
|
||||
|
||||
// Safety: coord_to_index already validates bounds, so byte_index is always valid
|
||||
return (self.bits[byte_index] >> bit_offset) & 1 == 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if no coordinates are marked.
|
||||
#[must_use]
|
||||
#[allow(dead_code)] // Standard API method for collection-like types
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.count == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache of pre-computed flood fill results, keyed by element ID.
|
||||
pub struct FloodFillCache {
|
||||
/// Cached results: element_id -> filled coordinates
|
||||
way_cache: FnvHashMap<u64, Vec<(i32, i32)>>,
|
||||
}
|
||||
|
||||
impl FloodFillCache {
|
||||
/// Creates an empty cache.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
way_cache: FnvHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-computes flood fills for all elements that need them.
|
||||
///
|
||||
/// This runs in parallel using Rayon, taking advantage of multiple CPU cores.
|
||||
pub fn precompute(elements: &[ProcessedElement], timeout: Option<&Duration>) -> Self {
|
||||
// Collect all ways that need flood fill
|
||||
let ways_needing_fill: Vec<&ProcessedWay> = elements
|
||||
.iter()
|
||||
.filter_map(|el| match el {
|
||||
ProcessedElement::Way(way) => {
|
||||
if Self::way_needs_flood_fill(way) {
|
||||
Some(way)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Compute all way flood fills in parallel
|
||||
let way_results: Vec<(u64, Vec<(i32, i32)>)> = ways_needing_fill
|
||||
.par_iter()
|
||||
.map(|way| {
|
||||
let polygon_coords: Vec<(i32, i32)> =
|
||||
way.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
let filled = flood_fill_area(&polygon_coords, timeout);
|
||||
(way.id, filled)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build the cache
|
||||
let mut cache = Self::new();
|
||||
for (id, filled) in way_results {
|
||||
cache.way_cache.insert(id, filled);
|
||||
}
|
||||
|
||||
cache
|
||||
}
|
||||
|
||||
/// Gets cached flood fill result for a way, or computes it if not cached.
|
||||
///
|
||||
/// Note: Combined ways created from relations (e.g., in `generate_natural_from_relation`)
|
||||
/// will miss the cache and fall back to on-demand computation. This is by design,
|
||||
/// these synthetic ways don't exist in the original element list and have relation IDs
|
||||
/// rather than way IDs. The individual member ways are still cached.
|
||||
pub fn get_or_compute(
|
||||
&self,
|
||||
way: &ProcessedWay,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
if let Some(cached) = self.way_cache.get(&way.id) {
|
||||
// Clone is intentional: each result is typically accessed once during
|
||||
// sequential processing, so the cost is acceptable vs Arc complexity
|
||||
cached.clone()
|
||||
} else {
|
||||
// Fallback: compute on demand for synthetic/combined ways from relations
|
||||
let polygon_coords: Vec<(i32, i32)> = way.nodes.iter().map(|n| (n.x, n.z)).collect();
|
||||
flood_fill_area(&polygon_coords, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets cached flood fill result for a ProcessedElement (Way only).
|
||||
/// For Nodes/Relations, returns empty vec.
|
||||
pub fn get_or_compute_element(
|
||||
&self,
|
||||
element: &ProcessedElement,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
match element {
|
||||
ProcessedElement::Way(way) => self.get_or_compute(way, timeout),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if a way element needs flood fill based on its tags.
|
||||
///
|
||||
/// This checks for tag presence (not specific values) because:
|
||||
/// - Only some values within each tag type actually use flood fill
|
||||
/// - But caching extra results is harmless (small memory overhead)
|
||||
/// - And avoids duplicating value-checking logic from processors
|
||||
///
|
||||
/// Covered cases:
|
||||
/// - building/building:part -> buildings::generate_buildings (includes bridge)
|
||||
/// - landuse -> landuse::generate_landuse
|
||||
/// - leisure -> leisure::generate_leisure
|
||||
/// - amenity -> amenities::generate_amenities
|
||||
/// - natural (except tree) -> natural::generate_natural
|
||||
/// - highway with area=yes -> highways::generate_highways (area fill)
|
||||
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
|
||||
way.tags.contains_key("building")
|
||||
|| way.tags.contains_key("building:part")
|
||||
|| way.tags.contains_key("landuse")
|
||||
|| way.tags.contains_key("leisure")
|
||||
|| way.tags.contains_key("amenity")
|
||||
|| way
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|v| v != "tree")
|
||||
.unwrap_or(false)
|
||||
// Highway areas (like pedestrian plazas) use flood fill when area=yes
|
||||
|| (way.tags.contains_key("highway")
|
||||
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Collects all building footprint coordinates from the pre-computed cache.
|
||||
///
|
||||
/// This should be called after precompute() and before elements are processed.
|
||||
/// Returns a memory-efficient bitmap of all (x, z) coordinates that are part of buildings.
|
||||
///
|
||||
/// The bitmap uses only 1 bit per coordinate in the world bounds, compared to ~24 bytes
|
||||
/// per entry in a HashSet, reducing memory usage by ~200x for large worlds.
|
||||
pub fn collect_building_footprints(
|
||||
&self,
|
||||
elements: &[ProcessedElement],
|
||||
xzbbox: &XZBBox,
|
||||
) -> BuildingFootprintBitmap {
|
||||
let mut footprints = BuildingFootprintBitmap::new(xzbbox);
|
||||
|
||||
for element in elements {
|
||||
match element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
if let Some(cached) = self.way_cache.get(&way.id) {
|
||||
for &(x, z) in cached {
|
||||
footprints.set(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
for member in &rel.members {
|
||||
// Only treat outer members as building footprints.
|
||||
// Inner members represent courtyards/holes where trees can spawn.
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
if let Some(cached) = self.way_cache.get(&member.way.id) {
|
||||
for &(x, z) in cached {
|
||||
footprints.set(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
footprints
|
||||
}
|
||||
|
||||
/// Removes a way's cached flood fill result, freeing memory.
|
||||
///
|
||||
/// Call this after processing an element to release its cached data.
|
||||
pub fn remove_way(&mut self, way_id: u64) {
|
||||
self.way_cache.remove(&way_id);
|
||||
}
|
||||
|
||||
/// Removes all cached flood fill results for ways in a relation.
|
||||
///
|
||||
/// Relations contain multiple ways, so we need to remove all of them.
|
||||
pub fn remove_relation_ways(&mut self, way_ids: &[u64]) {
|
||||
for &id in way_ids {
|
||||
self.way_cache.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FloodFillCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the global Rayon thread pool with a CPU usage cap.
|
||||
///
|
||||
/// Call this once at startup before any parallel operations.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `cpu_fraction` - Fraction of available cores to use (e.g., 0.9 for 90%).
|
||||
/// Values are clamped to the range [0.1, 1.0].
|
||||
pub fn configure_rayon_thread_pool(cpu_fraction: f64) {
|
||||
// Clamp cpu_fraction to valid range
|
||||
let cpu_fraction = cpu_fraction.clamp(0.1, 1.0);
|
||||
|
||||
let available_cores = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4);
|
||||
|
||||
let target_threads = ((available_cores as f64) * cpu_fraction).floor() as usize;
|
||||
let target_threads = target_threads.max(1); // At least 1 thread
|
||||
|
||||
// Only configure if we haven't already (this can only be called once)
|
||||
match rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(target_threads)
|
||||
.build_global()
|
||||
{
|
||||
Ok(()) => {
|
||||
// Successfully configured (silent to avoid cluttering output)
|
||||
}
|
||||
Err(_) => {
|
||||
// Thread pool already configured
|
||||
}
|
||||
}
|
||||
}
|
||||
251
src/gui.rs
251
src/gui.rs
@@ -1,5 +1,5 @@
|
||||
use crate::args::Args;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
use crate::data_processing::{self, GenerationOptions};
|
||||
@@ -74,6 +74,9 @@ fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
}
|
||||
|
||||
pub fn run_gui() {
|
||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
|
||||
@@ -155,16 +158,20 @@ fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
||||
|
||||
if generate_new {
|
||||
// Handle new world generation
|
||||
if let Some(default_path) = &default_dir {
|
||||
// Try Minecraft saves directory first, fall back to current directory
|
||||
let target_path = if let Some(default_path) = &default_dir {
|
||||
if default_path.exists() {
|
||||
// Call create_new_world and return the result
|
||||
create_new_world(default_path).map_err(|_| 1) // Error code 1: Minecraft directory not found
|
||||
default_path.clone()
|
||||
} else {
|
||||
Err(1) // Error code 1: Minecraft directory not found
|
||||
// Minecraft directory doesn't exist, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
}
|
||||
} else {
|
||||
Err(1) // Error code 1: Minecraft directory not found
|
||||
}
|
||||
// No default directory configured, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
|
||||
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
||||
} else {
|
||||
// Handle existing world selection
|
||||
// Open the directory picker dialog
|
||||
@@ -430,36 +437,20 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
world_path
|
||||
}
|
||||
|
||||
// Function to update player position in level.dat based on spawn point coordinates
|
||||
fn update_player_position(
|
||||
/// Calculates the default spawn point at X=1, Z=1 relative to the world origin.
|
||||
/// This is used when no spawn point is explicitly selected by the user.
|
||||
fn calculate_default_spawn(xzbbox: &XZBBox) -> (i32, i32) {
|
||||
(xzbbox.min_x() + 1, xzbbox.min_z() + 1)
|
||||
}
|
||||
|
||||
/// Sets the player spawn point in level.dat using Minecraft XZ coordinates.
|
||||
/// The Y coordinate is set to a temporary value (150) and will be updated
|
||||
/// after terrain generation by `update_player_spawn_y_after_generation`.
|
||||
fn set_player_spawn_in_level_dat(
|
||||
world_path: &str,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
spawn_x: i32,
|
||||
spawn_z: i32,
|
||||
) -> Result<(), String> {
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
|
||||
let Some((lat, lng)) = spawn_point else {
|
||||
return Ok(()); // No spawn point selected, exit early
|
||||
};
|
||||
|
||||
// Parse geometrical point and bounding box
|
||||
let llpoint =
|
||||
LLPoint::new(lat, lng).map_err(|e| format!("Failed to parse spawn point:\n{e}"))?;
|
||||
let llbbox = LLBBox::from_str(&bbox_text)
|
||||
.map_err(|e| format!("Failed to parse bounding box for spawn point:\n{e}"))?;
|
||||
|
||||
// Check if spawn point is within the bbox
|
||||
if !llbbox.contains(&llpoint) {
|
||||
return Err("Spawn point is outside the selected area".to_string());
|
||||
}
|
||||
|
||||
// Convert lat/lng to Minecraft coordinates
|
||||
let (transformer, _) = CoordTransformer::llbbox_to_xzbbox(&llbbox, scale)
|
||||
.map_err(|e| format!("Failed to build transformation on coordinate systems:\n{e}"))?;
|
||||
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
|
||||
// Default y spawn position since terrain elevation cannot be determined yet
|
||||
let y = 150.0;
|
||||
|
||||
@@ -491,21 +482,24 @@ fn update_player_position(
|
||||
if let Value::Compound(ref mut root) = nbt_data {
|
||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||
// Set world spawn point
|
||||
data.insert("SpawnX".to_string(), Value::Int(xzpoint.x));
|
||||
data.insert("SpawnX".to_string(), Value::Int(spawn_x));
|
||||
data.insert("SpawnY".to_string(), Value::Int(y as i32));
|
||||
data.insert("SpawnZ".to_string(), Value::Int(xzpoint.z));
|
||||
data.insert("SpawnZ".to_string(), Value::Int(spawn_z));
|
||||
|
||||
// Update player position
|
||||
// Update player position if Player compound exists
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
if let Value::Double(ref mut pos_x) = pos.get_mut(0).unwrap() {
|
||||
*pos_x = xzpoint.x as f64;
|
||||
}
|
||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||
*pos_y = y;
|
||||
}
|
||||
if let Value::Double(ref mut pos_z) = pos.get_mut(2).unwrap() {
|
||||
*pos_z = xzpoint.z as f64;
|
||||
// Safely update position values with bounds checking
|
||||
if pos.len() >= 3 {
|
||||
if let Some(Value::Double(ref mut pos_x)) = pos.get_mut(0) {
|
||||
*pos_x = spawn_x as f64;
|
||||
}
|
||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||
*pos_y = y;
|
||||
}
|
||||
if let Some(Value::Double(ref mut pos_z)) = pos.get_mut(2) {
|
||||
*pos_z = spawn_z as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,19 +531,15 @@ fn update_player_position(
|
||||
}
|
||||
|
||||
// Function to update player spawn Y coordinate based on terrain height after generation
|
||||
// This updates the spawn Y coordinate to be at terrain height + 3 blocks
|
||||
pub fn update_player_spawn_y_after_generation(
|
||||
world_path: &Path,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
ground: &Ground,
|
||||
) -> Result<(), String> {
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
|
||||
let Some((_lat, _lng)) = spawn_point else {
|
||||
return Ok(()); // No spawn point selected, exit early
|
||||
};
|
||||
|
||||
// Read the current level.dat file to get existing spawn coordinates
|
||||
let level_path = PathBuf::from(world_path).join("level.dat");
|
||||
if !level_path.exists() {
|
||||
@@ -618,7 +608,7 @@ pub fn update_player_spawn_y_after_generation(
|
||||
let relative_z = existing_spawn_z - xzbbox.min_z();
|
||||
let terrain_point = XZPoint::new(relative_x, relative_z);
|
||||
|
||||
ground.level(terrain_point) + 2
|
||||
ground.level(terrain_point) + 3 // Add 3 blocks above terrain for safety
|
||||
} else {
|
||||
-61 // Default Y if no terrain
|
||||
};
|
||||
@@ -632,8 +622,8 @@ pub fn update_player_spawn_y_after_generation(
|
||||
// Update player position - only Y coordinate
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
// Keep existing X and Z, only update Y
|
||||
if let Value::Double(ref mut pos_y) = pos.get_mut(1).unwrap() {
|
||||
// Safely update Y position with bounds checking
|
||||
if let Some(Value::Double(ref mut pos_y)) = pos.get_mut(1) {
|
||||
*pos_y = spawn_y as f64;
|
||||
}
|
||||
}
|
||||
@@ -794,7 +784,6 @@ fn gui_start_generation(
|
||||
selected_world: String,
|
||||
world_scale: f64,
|
||||
ground_level: i32,
|
||||
floodfill_timeout: u64,
|
||||
terrain_enabled: bool,
|
||||
skip_osm_objects: bool,
|
||||
interior_enabled: bool,
|
||||
@@ -814,50 +803,99 @@ fn gui_start_generation(
|
||||
// Send generation click telemetry
|
||||
telemetry::send_generation_click();
|
||||
|
||||
// If spawn point was chosen and the world is new, check and set the spawn point
|
||||
// For new Java worlds, set the spawn point in level.dat
|
||||
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
|
||||
// level.dat to modify (the spawn point will be set when the .mcworld is created)
|
||||
if is_new_world && spawn_point.is_some() && world_format != "bedrock" {
|
||||
// Verify the spawn point is within bounds
|
||||
if let Some(coords) = spawn_point {
|
||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||
Ok(bbox) => bbox,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
if is_new_world && world_format != "bedrock" {
|
||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||
Ok(bbox) => bbox,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to parse bounding box: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
|
||||
let (transformer, xzbbox) = match CoordTransformer::llbbox_to_xzbbox(&llbbox, world_scale) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to create coordinate transformer: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
|
||||
let (spawn_x, spawn_z) = if let Some(coords) = spawn_point {
|
||||
// User selected a spawn point - verify it's within bounds and convert to XZ
|
||||
let llpoint = LLPoint::new(coords.0, coords.1)
|
||||
.map_err(|e| format!("Failed to parse spawn point: {e}"))?;
|
||||
|
||||
if llbbox.contains(&llpoint) {
|
||||
// Spawn point is valid, update the player position
|
||||
update_player_position(
|
||||
&selected_world,
|
||||
spawn_point,
|
||||
bbox_text.clone(),
|
||||
world_scale,
|
||||
)
|
||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
(xzpoint.x, xzpoint.z)
|
||||
} else {
|
||||
// Spawn point outside bounds, use default
|
||||
calculate_default_spawn(&xzbbox)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No user-selected spawn point - use default at X=1, Z=1 relative to world origin
|
||||
calculate_default_spawn(&xzbbox)
|
||||
};
|
||||
|
||||
set_player_spawn_in_level_dat(&selected_world, spawn_x, spawn_z)
|
||||
.map_err(|e| format!("Failed to set spawn point: {e}"))?;
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = tokio::task::spawn_blocking(move || {
|
||||
// Acquire session lock for the world directory before starting generation
|
||||
let world_path = PathBuf::from(&selected_world);
|
||||
let _session_lock = match SessionLock::acquire(&world_path) {
|
||||
Ok(lock) => lock,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
||||
|
||||
// Determine world format from UI selection first (needed for session lock decision)
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Check available disk space before starting generation (minimum 3GB required)
|
||||
const MIN_DISK_SPACE_BYTES: u64 = 3 * 1024 * 1024 * 1024; // 3 GB
|
||||
let check_path = if world_format == WorldFormat::JavaAnvil {
|
||||
world_path.clone()
|
||||
} else {
|
||||
// For Bedrock, check current directory where .mcworld will be created
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
match fs2::available_space(&check_path) {
|
||||
Ok(available) if available < MIN_DISK_SPACE_BYTES => {
|
||||
let error_msg = "Not enough disk space available.".to_string();
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
Err(e) => {
|
||||
// Log warning but don't block generation if we can't check space
|
||||
eprintln!("Warning: Could not check disk space: {e}");
|
||||
}
|
||||
_ => {} // Sufficient space available
|
||||
}
|
||||
|
||||
// Acquire session lock for Java worlds only
|
||||
// Session lock prevents Minecraft from having the world open during generation
|
||||
// Bedrock worlds are generated as .mcworld files and don't need this lock
|
||||
let _session_lock: Option<SessionLock> = if world_format == WorldFormat::JavaAnvil {
|
||||
match SessionLock::acquire(&world_path) {
|
||||
Ok(lock) => Some(lock),
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to acquire session lock: {e}");
|
||||
eprintln!("{error_msg}");
|
||||
emit_gui_error(&error_msg);
|
||||
return Err(error_msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse the bounding box from the text with proper error handling
|
||||
@@ -871,13 +909,6 @@ fn gui_start_generation(
|
||||
}
|
||||
};
|
||||
|
||||
// Determine world format from UI selection
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Determine output path and level name based on format
|
||||
let (generation_path, level_name) = match world_format {
|
||||
WorldFormat::JavaAnvil => {
|
||||
@@ -902,6 +933,7 @@ fn gui_start_generation(
|
||||
};
|
||||
|
||||
// Calculate MC spawn coordinates from lat/lng if spawn point was provided
|
||||
// Otherwise, default to X=1, Z=1 (relative to xzbbox min coordinates)
|
||||
let mc_spawn_point: Option<(i32, i32)> = if let Some((lat, lng)) = spawn_point {
|
||||
if let Ok(llpoint) = LLPoint::new(lat, lng) {
|
||||
if let Ok((transformer, _)) =
|
||||
@@ -916,7 +948,12 @@ fn gui_start_generation(
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
// Default spawn point: X=1, Z=1 relative to world origin
|
||||
if let Ok((_, xzbbox)) = CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale) {
|
||||
Some(calculate_default_spawn(&xzbbox))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Create generation options
|
||||
@@ -946,8 +983,7 @@ fn gui_start_generation(
|
||||
roof: roof_enabled,
|
||||
fillground: fillground_enabled,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
|
||||
spawn_point,
|
||||
timeout: Some(std::time::Duration::from_secs(40)),
|
||||
};
|
||||
|
||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||
@@ -963,17 +999,27 @@ fn gui_start_generation(
|
||||
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1006,7 +1052,7 @@ fn gui_start_generation(
|
||||
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
@@ -1017,13 +1063,22 @@ fn gui_start_generation(
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to fetch data: {e}");
|
||||
emit_gui_error(&error_msg);
|
||||
emit_gui_error(&e.to_string());
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
Err(error_msg)
|
||||
Err(e.to_string())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
11
src/gui/css/bbox.css
vendored
11
src/gui/css/bbox.css
vendored
@@ -8,13 +8,9 @@ body,
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
/* Hide the BBOX coordinates display at bottom of map */
|
||||
#info-box {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
bottom: 0;
|
||||
border: 0 0 7px 0;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#coord-format {
|
||||
@@ -351,7 +347,8 @@ body,
|
||||
background-position: -31px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled,
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
179
src/gui/css/styles.css
vendored
179
src/gui/css/styles.css
vendored
@@ -32,9 +32,12 @@ p {
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
padding-top: 0.4em;
|
||||
padding-bottom: 0.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
max-width: 950px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.logo.arnis:hover {
|
||||
@@ -59,11 +62,11 @@ a:hover {
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
min-height: 60vh;
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -75,34 +78,60 @@ a:hover {
|
||||
|
||||
.map-box,
|
||||
.controls-box {
|
||||
width: 45%;
|
||||
background: #575757;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-box {
|
||||
min-height: 400px;
|
||||
width: 63%;
|
||||
min-height: 420px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #575757;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.controls-box {
|
||||
width: 32%;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.controls-box .progress-section {
|
||||
margin-top: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.controls-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bbox-info-text {
|
||||
font-size: 0.9em;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
min-height: 2.5em;
|
||||
line-height: 1.25em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
border: 2px solid #e0e0e0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
min-height: 300px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -142,18 +171,26 @@ button:hover {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.progress-section h2 {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
.progress-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
max-width: 80%;
|
||||
height: 20px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#progress-detail {
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -163,15 +200,6 @@ button:hover {
|
||||
transition: width 0.4s;
|
||||
}
|
||||
|
||||
/* Left and right alignment for "Saving world..." text */
|
||||
.progress-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9em;
|
||||
margin-top: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
@@ -190,7 +218,7 @@ button:hover {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -233,6 +261,7 @@ button:hover {
|
||||
width: 100%;
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@@ -287,7 +316,7 @@ button:hover {
|
||||
/* Customization Settings */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
z-index: 20001;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
@@ -300,7 +329,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #797979;
|
||||
background-color: #717171;
|
||||
padding: 20px;
|
||||
border: 1px solid #797979;
|
||||
border-radius: 10px;
|
||||
@@ -420,6 +449,20 @@ button:hover {
|
||||
box-shadow: 0 0 5px #fecc44;
|
||||
}
|
||||
|
||||
#save-path {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#save-path:focus {
|
||||
outline: none;
|
||||
border-color: #fecc44;
|
||||
box-shadow: 0 0 5px #fecc44;
|
||||
}
|
||||
|
||||
/* Settings Modal Layout */
|
||||
.settings-row {
|
||||
display: flex;
|
||||
@@ -431,6 +474,75 @@ button:hover {
|
||||
.settings-row label {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Tooltip icon (question mark in circle) */
|
||||
.tooltip-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(254, 204, 68, 0.3);
|
||||
color: #fecc44;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
cursor: help;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tooltip-icon:hover {
|
||||
background-color: rgba(254, 204, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Arnis-styled tooltip box */
|
||||
.tooltip-icon::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 8px);
|
||||
transform: translateX(-50%);
|
||||
background-color: #2a2a2a;
|
||||
color: #fecc44;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid #fecc44;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tooltip arrow */
|
||||
.tooltip-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 2px);
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #fecc44;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 1001;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-icon:hover::after,
|
||||
.tooltip-icon:hover::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.settings-control {
|
||||
@@ -606,9 +718,12 @@ button:hover {
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.settings-button .gear-icon::before {
|
||||
content: "⚙️";
|
||||
font-size: 18px;
|
||||
.settings-button svg {
|
||||
stroke: white;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
min-width: 22px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
/* Logo Animation */
|
||||
|
||||
144
src/gui/index.html
vendored
144
src/gui/index.html
vendored
@@ -20,60 +20,51 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<!-- Left Box: Map and BBox Input -->
|
||||
<section class="section map-box" style="margin-bottom: 0; padding-bottom: 0;">
|
||||
<h2 data-localize="select_location">Select Location</h2>
|
||||
<span id="bbox-text" style="font-size: 1.0em; display: block; margin-top: -8px; margin-bottom: 3px;" data-localize="zoom_in_and_choose">
|
||||
Zoom in and choose your area using the rectangle tool
|
||||
</span>
|
||||
<iframe src="maps.html" width="100%" height="300" class="map-container" title="Map Picker"></iframe>
|
||||
|
||||
<span id="bbox-info"
|
||||
style="font-size: 0.75em; color: #7bd864; display: block; margin-bottom: 4px; font-weight: bold; min-height: 2em;"></span>
|
||||
<!-- Left Box: Map -->
|
||||
<section class="section map-box">
|
||||
<iframe src="maps.html" width="100%" height="100%" class="map-container" title="Map Picker"></iframe>
|
||||
</section>
|
||||
|
||||
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
|
||||
<section class="section controls-box">
|
||||
<div class="controls-content">
|
||||
<h2 data-localize="select_world">Select World</h2>
|
||||
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
<div class="controls-top">
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- World Format Toggle -->
|
||||
<div class="format-toggle-container">
|
||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
||||
Java
|
||||
</button>
|
||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
||||
Bedrock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- World Format Toggle -->
|
||||
<div class="format-toggle-container">
|
||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
||||
Java
|
||||
</button>
|
||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
||||
Bedrock
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
||||
<button type="button" class="settings-button" onclick="openSettings()" aria-label="Settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
|
||||
<button type="button" class="settings-button" onclick="openSettings()">
|
||||
<i class="gear-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<div class="progress-section">
|
||||
<h2 data-localize="progress">Progress</h2>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<div class="progress-status">
|
||||
<span id="progress-message"></span>
|
||||
<span id="bbox-info" class="bbox-info-text" data-localize="select_area_prompt">Select an area on the map using the tools.</span>
|
||||
<div class="progress-row">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" id="progress-bar"></div>
|
||||
</div>
|
||||
<span id="progress-detail">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,7 +91,10 @@
|
||||
|
||||
<!-- Generation Mode Dropdown -->
|
||||
<div class="settings-row">
|
||||
<label for="generation-mode-select" data-localize="generation_mode">Generation Mode</label>
|
||||
<label for="generation-mode-select">
|
||||
<span data-localize="generation_mode">Generation Mode</span>
|
||||
<span class="tooltip-icon" data-tooltip="Choose what to generate: buildings/roads with terrain, just objects, or terrain only">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
||||
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
||||
@@ -110,25 +104,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interior Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="interior-toggle" data-localize="interior">Interior Generation</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="interior-toggle" name="interior-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roof Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="roof-toggle" data-localize="roof">Roof Generation</label>
|
||||
<label for="roof-toggle">
|
||||
<span data-localize="roof">Roof Generation</span>
|
||||
<span class="tooltip-icon" data-tooltip="Generate roofs on buildings">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interior Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="interior-toggle">
|
||||
<span data-localize="interior">Interior Generation</span>
|
||||
<span class="tooltip-icon" data-tooltip="Generate interior details inside buildings">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="interior-toggle" name="interior-toggle">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fill ground Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="fillground-toggle" data-localize="fillground">Fill Ground</label>
|
||||
<label for="fillground-toggle">
|
||||
<span data-localize="fillground">Fill Ground</span>
|
||||
<span class="tooltip-icon" data-tooltip="Fill the ground below the surface">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
|
||||
</div>
|
||||
@@ -136,7 +139,10 @@
|
||||
|
||||
<!-- World Scale Slider -->
|
||||
<div class="settings-row">
|
||||
<label for="scale-value-slider" data-localize="world_scale">World Scale</label>
|
||||
<label for="scale-value-slider">
|
||||
<span data-localize="world_scale">World Scale</span>
|
||||
<span class="tooltip-icon" data-tooltip="Scale factor for the generated world (1.0 = real-world scale)">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
|
||||
<span id="slider-value">1.00</span>
|
||||
@@ -145,23 +151,21 @@
|
||||
|
||||
<!-- Bounding Box Input -->
|
||||
<div class="settings-row">
|
||||
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box</label>
|
||||
<label for="bbox-coords">
|
||||
<span data-localize="custom_bounding_box">Custom Bounding Box</span>
|
||||
<span class="tooltip-icon" data-tooltip="Manually enter coordinates (lat,lng,lat,lng) or use map selection">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floodfill Timeout Input -->
|
||||
<div class="settings-row">
|
||||
<label for="floodfill-timeout" data-localize="floodfill_timeout">Floodfill Timeout (sec)</label>
|
||||
<div class="settings-control">
|
||||
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" placeholder="Seconds">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Theme Selector -->
|
||||
<div class="settings-row">
|
||||
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>
|
||||
<label for="tile-theme-select">
|
||||
<span data-localize="map_theme">Map Theme</span>
|
||||
<span class="tooltip-icon" data-tooltip="Visual style of the map picker">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
|
||||
<option value="osm">Standard</option>
|
||||
@@ -175,7 +179,10 @@
|
||||
|
||||
<!-- Language Selector -->
|
||||
<div class="settings-row">
|
||||
<label for="language-select" data-localize="language">Language</label>
|
||||
<label for="language-select">
|
||||
<span data-localize="language">Language</span>
|
||||
<span class="tooltip-icon" data-tooltip="Interface language">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<select id="language-select" name="language-select" class="language-dropdown">
|
||||
<option value="en">English</option>
|
||||
@@ -199,7 +206,10 @@
|
||||
|
||||
<!-- Telemetry Consent Toggle -->
|
||||
<div class="settings-row">
|
||||
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||
<label for="telemetry-toggle" style="white-space: nowrap;">
|
||||
<span>Anonymous Crash Reports</span>
|
||||
<span class="tooltip-icon" data-tooltip="Send anonymous crash data to help improve Arnis">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||
</div>
|
||||
|
||||
53
src/gui/js/bbox.js
vendored
53
src/gui/js/bbox.js
vendored
@@ -564,6 +564,7 @@ $(document).ready(function () {
|
||||
var worldOverlayEnabled = false;
|
||||
var worldPreviewAvailable = false;
|
||||
var sliderControl = null;
|
||||
var worldOverlayHiddenForEdit = false; // Track if we hid the overlay for edit/delete mode
|
||||
|
||||
// Create the opacity slider as a proper Leaflet control
|
||||
var SliderControl = L.Control.extend({
|
||||
@@ -722,6 +723,32 @@ $(document).ready(function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily hide the overlay (for edit/delete mode)
|
||||
function hideWorldOverlayTemporarily() {
|
||||
if (worldOverlay && worldOverlayEnabled) {
|
||||
worldOverlayHiddenForEdit = true;
|
||||
map.removeLayer(worldOverlay);
|
||||
}
|
||||
// Also visually disable the preview button during edit/delete mode
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.add('editing-mode');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the overlay after edit/delete mode ends
|
||||
function restoreWorldOverlay() {
|
||||
if (worldOverlayHiddenForEdit && worldOverlay && worldOverlayEnabled) {
|
||||
worldOverlay.addTo(map);
|
||||
worldOverlayHiddenForEdit = false;
|
||||
}
|
||||
// Re-enable the preview button
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('editing-mode');
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
@@ -899,6 +926,15 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a rectangle, remove any existing rectangles first
|
||||
if (e.layerType === 'rectangle') {
|
||||
drawnItems.eachLayer(function(layer) {
|
||||
if (layer instanceof L.Rectangle) {
|
||||
drawnItems.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a rectangle and set proper styles before adding it to the layer
|
||||
if (e.layerType === 'rectangle') {
|
||||
e.layer.setStyle({
|
||||
@@ -990,6 +1026,23 @@ $(document).ready(function () {
|
||||
map.fitBounds(bounds.getBounds());
|
||||
});
|
||||
|
||||
// Hide world preview overlay when entering edit or delete mode
|
||||
map.on('draw:editstart', function() {
|
||||
hideWorldOverlayTemporarily();
|
||||
});
|
||||
|
||||
map.on('draw:deletestart', function() {
|
||||
hideWorldOverlayTemporarily();
|
||||
});
|
||||
|
||||
// Restore world preview overlay when exiting edit or delete mode
|
||||
map.on('draw:editstop', function() {
|
||||
restoreWorldOverlay();
|
||||
});
|
||||
|
||||
map.on('draw:deletestop', function() {
|
||||
restoreWorldOverlay();
|
||||
});
|
||||
function display() {
|
||||
$('#boxbounds').text(formatBounds(bounds.getBounds(), '4326'));
|
||||
$('#boxboundsmerc').text(formatBounds(bounds.getBounds(), currentproj));
|
||||
|
||||
121
src/gui/js/main.js
vendored
121
src/gui/js/main.js
vendored
@@ -12,6 +12,25 @@ if (window.__TAURI__) {
|
||||
|
||||
const DEFAULT_LOCALE_PATH = `./locales/en.json`;
|
||||
|
||||
// Track current bbox-info localization key for language changes
|
||||
let currentBboxInfoKey = "select_area_prompt";
|
||||
let currentBboxInfoColor = "#ffffff";
|
||||
|
||||
// Helper function to set bbox-info text and track it for language changes
|
||||
async function setBboxInfo(bboxInfoElement, localizationKey, color) {
|
||||
currentBboxInfoKey = localizationKey;
|
||||
currentBboxInfoColor = color;
|
||||
|
||||
// Ensure localization is available
|
||||
let localization = window.localization;
|
||||
if (!localization) {
|
||||
localization = await getLocalization();
|
||||
}
|
||||
|
||||
localizeElement(localization, { element: bboxInfoElement }, localizationKey);
|
||||
bboxInfoElement.style.color = color;
|
||||
}
|
||||
|
||||
// Initialize elements and start the demo progress
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
registerMessageEvent();
|
||||
@@ -66,7 +85,7 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
||||
const attribute = localizedStringKey.startsWith("placeholder_") ? "placeholder" : "textContent";
|
||||
|
||||
if (element) {
|
||||
if (localizedStringKey in json) {
|
||||
if (json && localizedStringKey in json) {
|
||||
element[attribute] = json[localizedStringKey];
|
||||
} else {
|
||||
// Fallback to default (English) string
|
||||
@@ -78,20 +97,15 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
||||
|
||||
async function applyLocalization(localization) {
|
||||
const localizationElements = {
|
||||
"h2[data-localize='select_location']": "select_location",
|
||||
"#bbox-text": "zoom_in_and_choose",
|
||||
"h2[data-localize='select_world']": "select_world",
|
||||
"span[id='choose_world']": "choose_world",
|
||||
"#selected-world": "no_world_selected",
|
||||
"#start-button": "start_generation",
|
||||
"h2[data-localize='progress']": "progress",
|
||||
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
||||
"h2[data-localize='customization_settings']": "customization_settings",
|
||||
"label[data-localize='world_scale']": "world_scale",
|
||||
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||
"label[data-localize='floodfill_timeout']": "floodfill_timeout",
|
||||
// DEPRECATED: Ground level localization removed
|
||||
// "label[data-localize='ground_level']": "ground_level",
|
||||
"label[data-localize='language']": "language",
|
||||
@@ -110,7 +124,6 @@ async function applyLocalization(localization) {
|
||||
|
||||
// Placeholder strings
|
||||
"input[id='bbox-coords']": "placeholder_bbox",
|
||||
"input[id='floodfill-timeout']": "placeholder_floodfill",
|
||||
// DEPRECATED: Ground level placeholder removed
|
||||
// "input[id='ground-level']": "placeholder_ground"
|
||||
};
|
||||
@@ -119,6 +132,13 @@ async function applyLocalization(localization) {
|
||||
localizeElement(localization, { selector: selector }, localizationElements[selector]);
|
||||
}
|
||||
|
||||
// Re-apply current bbox-info text with new language
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
if (bboxInfo && currentBboxInfoKey) {
|
||||
localizeElement(localization, { element: bboxInfo }, currentBboxInfoKey);
|
||||
bboxInfo.style.color = currentBboxInfoColor;
|
||||
}
|
||||
|
||||
// Update error messages
|
||||
window.localization = localization;
|
||||
}
|
||||
@@ -167,7 +187,7 @@ async function checkForUpdates() {
|
||||
updateMessage.style.textDecoration = "none";
|
||||
|
||||
localizeElement(window.localization, { element: updateMessage }, "new_version_available");
|
||||
footer.style.marginTop = "15px";
|
||||
footer.style.marginTop = "10px";
|
||||
footer.appendChild(updateMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -190,7 +210,7 @@ function registerMessageEvent() {
|
||||
// Function to set up the progress bar listener
|
||||
function setupProgressListener() {
|
||||
const progressBar = document.getElementById("progress-bar");
|
||||
const progressMessage = document.getElementById("progress-message");
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
const progressDetail = document.getElementById("progress-detail");
|
||||
|
||||
window.__TAURI__.event.listen("progress-update", (event) => {
|
||||
@@ -202,16 +222,16 @@ function setupProgressListener() {
|
||||
}
|
||||
|
||||
if (message != "") {
|
||||
progressMessage.textContent = message;
|
||||
bboxInfo.textContent = message;
|
||||
|
||||
if (message.startsWith("Error!")) {
|
||||
progressMessage.style.color = "#fa7878";
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
generationButtonEnabled = true;
|
||||
} else if (message.startsWith("Done!")) {
|
||||
progressMessage.style.color = "#7bd864";
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
generationButtonEnabled = true;
|
||||
} else {
|
||||
progressMessage.style.color = "";
|
||||
bboxInfo.style.color = "#ececec";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -252,6 +272,20 @@ function initSettings() {
|
||||
settingsModal.style.display = "none";
|
||||
}
|
||||
|
||||
// Close settings and license modals on escape key
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
if (settingsModal.style.display === "flex") {
|
||||
closeSettings();
|
||||
}
|
||||
|
||||
const licenseModal = document.getElementById("license-modal");
|
||||
if (licenseModal && licenseModal.style.display === "flex") {
|
||||
closeLicense();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.openSettings = openSettings;
|
||||
window.closeSettings = closeSettings;
|
||||
|
||||
@@ -513,11 +547,12 @@ function handleBboxInput() {
|
||||
|
||||
// Clear the info text only if no map selection exists
|
||||
if (!mapSelectedBBox) {
|
||||
bboxInfo.textContent = "";
|
||||
bboxInfo.style.color = "";
|
||||
setBboxInfo(bboxInfo, "select_area_prompt", "#ffffff");
|
||||
} else {
|
||||
// Restore map selection display
|
||||
displayBboxInfoText(mapSelectedBBox);
|
||||
// Restore map selection info display but don't update input field
|
||||
const [lng1, lat1, lng2, lat2] = mapSelectedBBox.split(" ").map(Number);
|
||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||
displayBboxSizeStatus(bboxInfo, selectedSize);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -553,8 +588,7 @@ function handleBboxInput() {
|
||||
// Update the info text and mark custom input as valid
|
||||
customBBoxValid = true;
|
||||
selectedBBox = bboxText.replace(/,/g, ' '); // Convert to space format for consistency
|
||||
localizeElement(window.localization, { element: bboxInfo }, "custom_selection_confirmed");
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
setBboxInfo(bboxInfo, "custom_selection_confirmed", "#7bd864");
|
||||
} else {
|
||||
// Valid numbers but invalid order or range
|
||||
customBBoxValid = false;
|
||||
@@ -564,8 +598,7 @@ function handleBboxInput() {
|
||||
} else {
|
||||
selectedBBox = mapSelectedBBox;
|
||||
}
|
||||
localizeElement(window.localization, { element: bboxInfo }, "error_coordinates_out_of_range");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
setBboxInfo(bboxInfo, "error_coordinates_out_of_range", "#fecc44");
|
||||
}
|
||||
} else {
|
||||
// Input doesn't match the required format
|
||||
@@ -576,8 +609,7 @@ function handleBboxInput() {
|
||||
} else {
|
||||
selectedBBox = mapSelectedBBox;
|
||||
}
|
||||
localizeElement(window.localization, { element: bboxInfo }, "invalid_format");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
setBboxInfo(bboxInfo, "invalid_format", "#fecc44");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -626,6 +658,21 @@ let selectedBBox = "";
|
||||
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
||||
let customBBoxValid = false; // Tracks if custom input is valid
|
||||
|
||||
/**
|
||||
* Displays the appropriate bbox size status message based on area thresholds
|
||||
* @param {HTMLElement} bboxInfo - The element to display the message in
|
||||
* @param {number} selectedSize - The calculated bbox area in square meters
|
||||
*/
|
||||
function displayBboxSizeStatus(bboxInfo, selectedSize) {
|
||||
if (selectedSize > threshold2) {
|
||||
setBboxInfo(bboxInfo, "area_too_large", "#fa7878");
|
||||
} else if (selectedSize > threshold1) {
|
||||
setBboxInfo(bboxInfo, "area_extensive", "#fecc44");
|
||||
} else {
|
||||
setBboxInfo(bboxInfo, "selection_confirmed", "#7bd864");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle incoming bbox data
|
||||
function displayBboxInfoText(bboxText) {
|
||||
let [lng1, lat1, lng2, lat2] = bboxText.split(" ").map(Number);
|
||||
@@ -640,10 +687,12 @@ function displayBboxInfoText(bboxText) {
|
||||
customBBoxValid = false;
|
||||
|
||||
const bboxInfo = document.getElementById("bbox-info");
|
||||
const bboxCoordsInput = document.getElementById("bbox-coords");
|
||||
|
||||
// Reset the info text if the bbox is 0,0,0,0
|
||||
if (lng1 === 0 && lat1 === 0 && lng2 === 0 && lat2 === 0) {
|
||||
bboxInfo.textContent = "";
|
||||
setBboxInfo(bboxInfo, "select_area_prompt", "#ffffff");
|
||||
bboxCoordsInput.value = "";
|
||||
mapSelectedBBox = "";
|
||||
if (!customBBoxValid) {
|
||||
selectedBBox = "";
|
||||
@@ -651,19 +700,13 @@ function displayBboxInfoText(bboxText) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the custom bbox input with the map selection (comma-separated format)
|
||||
bboxCoordsInput.value = `${lng1},${lat1},${lng2},${lat2}`;
|
||||
|
||||
// Calculate the size of the selected bbox
|
||||
const selectedSize = calculateBBoxSize(lng1, lat1, lng2, lat2);
|
||||
|
||||
if (selectedSize > threshold2) {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "area_too_large");
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
} else if (selectedSize > threshold1) {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "area_extensive");
|
||||
bboxInfo.style.color = "#fecc44";
|
||||
} else {
|
||||
localizeElement(window.localization, { element: bboxInfo }, "selection_confirmed");
|
||||
bboxInfo.style.color = "#7bd864";
|
||||
}
|
||||
displayBboxSizeStatus(bboxInfo, selectedSize);
|
||||
}
|
||||
|
||||
let worldPath = "";
|
||||
@@ -754,8 +797,7 @@ async function startGeneration() {
|
||||
|
||||
if (!selectedBBox || selectedBBox == "0.000000 0.000000 0.000000 0.000000") {
|
||||
const bboxInfo = document.getElementById('bbox-info');
|
||||
localizeElement(window.localization, { element: bboxInfo }, "select_location_first");
|
||||
bboxInfo.style.color = "#fa7878";
|
||||
setBboxInfo(bboxInfo, "select_location_first", "#fa7878");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -791,14 +833,12 @@ async function startGeneration() {
|
||||
var roof = document.getElementById("roof-toggle").checked;
|
||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
||||
var floodfill_timeout = parseInt(document.getElementById("floodfill-timeout").value, 10);
|
||||
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||
// DEPRECATED: Ground level input removed from UI
|
||||
var ground_level = -62;
|
||||
|
||||
// Validate floodfill_timeout and ground_level
|
||||
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
||||
// Validate ground_level
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? -62 : ground_level;
|
||||
|
||||
// Get telemetry consent (defaults to false if not set)
|
||||
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
||||
@@ -809,7 +849,6 @@ async function startGeneration() {
|
||||
selectedWorld: worldPath,
|
||||
worldScale: scale,
|
||||
groundLevel: ground_level,
|
||||
floodfillTimeout: floodfill_timeout,
|
||||
terrainEnabled: terrain,
|
||||
skipOsmObjects: skipOsmObjects,
|
||||
interiorEnabled: interior,
|
||||
|
||||
5
src/gui/locales/ar.json
vendored
5
src/gui/locales/ar.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "اختيار موقع",
|
||||
"zoom_in_and_choose": "قم بالتكبير واختر منطقتك باستخدام أداة المستطيل",
|
||||
"select_world": "تحديد عالم",
|
||||
"choose_world": "اختيار عالم",
|
||||
"no_world_selected": "لم يتم تحديد عالم",
|
||||
"start_generation": "بدء البناء",
|
||||
"progress": "التقدم",
|
||||
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
||||
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
||||
"invalid_format": "تنسيق غير صالح. استخدم 'lat,lng,lat,lng' أو 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
||||
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
||||
"selection_confirmed": "تم تأكيد التحديد!",
|
||||
"select_area_prompt": "حدد منطقة على الخريطة باستخدام الأدوات.",
|
||||
"unknown_error": "خطأ غير معروف",
|
||||
"license_and_credits": "الرخصة والمساهمون",
|
||||
"placeholder_bbox": "الصيغة: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/de.json
vendored
5
src/gui/locales/de.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Standort auswählen",
|
||||
"zoom_in_and_choose": "Zoome hinein und wähle dein Gebiet aus",
|
||||
"select_world": "Welt auswählen",
|
||||
"choose_world": "Welt wählen",
|
||||
"no_world_selected": "Keine Welt ausgewählt",
|
||||
"start_generation": "Generierung starten",
|
||||
"progress": "Fortschritt",
|
||||
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
||||
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
||||
"invalid_format": "Ungültiges Format. Bitte verwende 'lat,lng,lat,lng' oder 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
||||
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
||||
"selection_confirmed": "Auswahl bestätigt!",
|
||||
"select_area_prompt": "Wähle einen Bereich auf der Karte aus.",
|
||||
"unknown_error": "Unbekannter Fehler",
|
||||
"license_and_credits": "Lizenz und Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/en-US.json
vendored
5
src/gui/locales/en-US.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Select Location",
|
||||
"zoom_in_and_choose": "Zoom in and choose your area using the rectangle tool",
|
||||
"select_world": "Select World",
|
||||
"choose_world": "Choose World",
|
||||
"no_world_selected": "No world selected",
|
||||
"start_generation": "Start Generation",
|
||||
"progress": "Progress",
|
||||
"custom_selection_confirmed": "Custom selection confirmed!",
|
||||
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
||||
"invalid_format": "Invalid format. Please use 'lat,lng,lat,lng' or 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
||||
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
||||
"selection_confirmed": "Selection confirmed!",
|
||||
"select_area_prompt": "Select an area on the map using the tools.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/es.json
vendored
5
src/gui/locales/es.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Seleccionar ubicación",
|
||||
"zoom_in_and_choose": "Acércate y elige tu área usando la herramienta de rectángulo",
|
||||
"select_world": "Seleccionar mundo",
|
||||
"choose_world": "Elegir mundo",
|
||||
"no_world_selected": "Ningún mundo seleccionado",
|
||||
"start_generation": "Iniciar generación",
|
||||
"progress": "Progreso",
|
||||
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
||||
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
||||
"invalid_format": "Formato inválido. Por favor, use 'lat,lng,lat,lng' o 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
||||
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
||||
"selection_confirmed": "¡Selección confirmada!",
|
||||
"select_area_prompt": "Selecciona un área en el mapa usando las herramientas.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/fi.json
vendored
5
src/gui/locales/fi.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Valitse paikka",
|
||||
"zoom_in_and_choose": "Zoomaa ja valitse paikka käyttämällä suorakulmatyökalua.",
|
||||
"select_world": "Valitse maailma",
|
||||
"choose_world": "Valitse maailma",
|
||||
"no_world_selected": "Maailmaa ei valittu",
|
||||
"start_generation": "Aloita generointi",
|
||||
"progress": "Edistys",
|
||||
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
||||
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
||||
"invalid_format": "Väärä formaatti. Käytä 'lat,lng,lat,lng' tai 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
||||
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
||||
"selection_confirmed": "Valinta vahvistettu!",
|
||||
"select_area_prompt": "Valitse alue kartalta työkaluilla.",
|
||||
"unknown_error": "Tuntematon virhe",
|
||||
"license_and_credits": "Lisenssi ja krediitit",
|
||||
"placeholder_bbox": "Formaatti: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/fr-FR.json
vendored
5
src/gui/locales/fr-FR.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Sélectionner une localisation",
|
||||
"zoom_in_and_choose": "Zoomez et choisissez votre zone avec l'outil rectangle",
|
||||
"select_world": "Sélectionner un monde",
|
||||
"choose_world": "Choisir un monde",
|
||||
"no_world_selected": "Aucun monde sélectionné",
|
||||
"start_generation": "Commencer la génération",
|
||||
"progress": "Progrès",
|
||||
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
||||
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
||||
"invalid_format": "Format invalide. Utilisez 'lat,lng,lat,lng' ou 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
||||
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
||||
"selection_confirmed": "Sélection confirmée !",
|
||||
"select_area_prompt": "Sélectionnez une zone sur la carte avec les outils.",
|
||||
"unknown_error": "Erreur inconnue",
|
||||
"license_and_credits": "Licence et crédits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/hu.json
vendored
5
src/gui/locales/hu.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Hely kiválasztása",
|
||||
"zoom_in_and_choose": "Nagyíts és jelöld ki a területet a kijelölő eszközzel",
|
||||
"select_world": "Világ kijelölése",
|
||||
"choose_world": "Világ kiválasztása",
|
||||
"no_world_selected": "Nincs világ kiválasztva",
|
||||
"start_generation": "Generálás indítása",
|
||||
"progress": "Haladás",
|
||||
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
||||
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
||||
"invalid_format": "Érvénytelen formátum. Kérjük, használja a 'lat,lng,lat,lng' vagy a 'lat lng lat lng' formátumot.'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
||||
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
||||
"selection_confirmed": "Kiválasztás megerősítve",
|
||||
"select_area_prompt": "Jelölj ki egy területet a térképen az eszközökkel.",
|
||||
"unknown_error": "Ismeretlen hiba",
|
||||
"license_and_credits": "Licenc és elismerés.",
|
||||
"placeholder_bbox": "Formátum: lat,lng,lat,lng",
|
||||
|
||||
7
src/gui/locales/ko.json
vendored
7
src/gui/locales/ko.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "장소 선택",
|
||||
"zoom_in_and_choose": "줌 인하고 직사각형 도구를 사용하여 영역을 선택하세요.",
|
||||
"select_world": "세계 선택",
|
||||
"choose_world": "세계 선택",
|
||||
"no_world_selected": "선택된 세계 없음",
|
||||
"start_generation": "생성 시작",
|
||||
"progress": "진행",
|
||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||
"invalid_format": "잘못된 형식입니다. 'lat,lng,lat,lng' 또는 'lat lng lat lng' 형식을 사용하세요.",
|
||||
@@ -28,8 +24,9 @@
|
||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||
"select_location_first": "먼저 위치를 선택하세요!",
|
||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 значитель한 시간과 자원이 필요할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
||||
"selection_confirmed": "선택이 확인되었습니다!",
|
||||
"select_area_prompt": "도구를 사용하여 지도에서 영역을 선택하세요.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/lt.json
vendored
5
src/gui/locales/lt.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Vietos pasirinkimas",
|
||||
"zoom_in_and_choose": "Pasididinkite žemėlapį ir pasirinkite plotą su kvadrato įrankiu",
|
||||
"select_world": "Pasaulio pasirinkimas",
|
||||
"choose_world": "Pasirinkti pasaulį",
|
||||
"no_world_selected": "Pasaulis nepasirinktas",
|
||||
"start_generation": "Pradėti generaciją",
|
||||
"progress": "Progresas",
|
||||
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
||||
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
||||
"invalid_format": "Neteisingas formatas. Prašome naudoti 'plat,ilg,plat,ilg' arba 'plat ilg plat ilg'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
||||
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
||||
"selection_confirmed": "Pasirinkimas patvirtintas!",
|
||||
"select_area_prompt": "Pasirinkite plotą žemėlapyje naudodami įrankius.",
|
||||
"unknown_error": "Nežinoma klaida",
|
||||
"license_and_credits": "Licencija ir padėkos",
|
||||
"placeholder_bbox": "Formatas: plat,lyg,plat,lyg",
|
||||
|
||||
5
src/gui/locales/lv.json
vendored
5
src/gui/locales/lv.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Izvēlēties atrašanās vietu",
|
||||
"zoom_in_and_choose": "Pietuviniet un izvēlieties apgabalu",
|
||||
"select_world": "Izvēlēties pasauli",
|
||||
"choose_world": "Izvēlēties pasauli",
|
||||
"no_world_selected": "Pasaulē nav izvēlēta",
|
||||
"start_generation": "Sākt ģenerēšanu",
|
||||
"progress": "Progress",
|
||||
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
||||
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
||||
"invalid_format": "Nederīgs formāts. Izmantojiet 'platums,garums,platums,garums' vai 'platums garums platums garums'",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
||||
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
||||
"selection_confirmed": "Izvēle apstiprināta!",
|
||||
"select_area_prompt": "Izvēlieties apgabalu kartē, izmantojot rīkus.",
|
||||
"unknown_error": "Nezināma kļūda",
|
||||
"license_and_credits": "Licence un autori",
|
||||
"placeholder_bbox": "Formāts: platums,garums,platums,garums",
|
||||
|
||||
5
src/gui/locales/pl.json
vendored
5
src/gui/locales/pl.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Wybierz lokalizację",
|
||||
"zoom_in_and_choose": "Przybliż i zaznacz obszar za pomocą prostokąta",
|
||||
"select_world": "Wybierz świat",
|
||||
"choose_world": "Wybierz świat",
|
||||
"no_world_selected": "Nie wybrano świata",
|
||||
"start_generation": "Rozpocznij generowanie",
|
||||
"progress": "Postęp",
|
||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
||||
"invalid_format": "Nieprawidłowy format. Użyj 'szer.,dł.,szer.,dł.' lub 'szer. dł. szer. dł.'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
||||
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
||||
"selection_confirmed": "Wybór potwierdzony!",
|
||||
"select_area_prompt": "Zaznacz obszar na mapie za pomocą narzędzi.",
|
||||
"unknown_error": "Nieznany błąd",
|
||||
"license_and_credits": "Licencja i autorzy",
|
||||
"placeholder_bbox": "Format: szer,dł,szer,dł",
|
||||
|
||||
5
src/gui/locales/ru.json
vendored
5
src/gui/locales/ru.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Выбрать местоположение",
|
||||
"zoom_in_and_choose": "Приблизьте и выберите область",
|
||||
"select_world": "Выбрать мир",
|
||||
"choose_world": "Выбрать мир",
|
||||
"no_world_selected": "Мир не выбран",
|
||||
"start_generation": "Начать генерацию",
|
||||
"progress": "Прогресс",
|
||||
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
||||
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
||||
"invalid_format": "Неверный формат. Используйте 'широта,долгота,широта,долгота' или 'широта долгота широта долгота'",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
||||
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
||||
"selection_confirmed": "Выбор подтвержден!",
|
||||
"select_area_prompt": "Выберите область на карте с помощью инструментов.",
|
||||
"unknown_error": "Неизвестная ошибка",
|
||||
"license_and_credits": "Лицензия и авторы",
|
||||
"placeholder_bbox": "Формат: широта,долгота,широта,долгота",
|
||||
|
||||
5
src/gui/locales/sv.json
vendored
5
src/gui/locales/sv.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Välj plats",
|
||||
"zoom_in_and_choose": "Zooma in och välj ditt område med rektangulärt verktyg",
|
||||
"select_world": "Välj värld",
|
||||
"choose_world": "Välj värld",
|
||||
"no_world_selected": "Ingen värld vald",
|
||||
"start_generation": "Starta generering",
|
||||
"progress": "Framsteg",
|
||||
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
||||
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
||||
"invalid_format": "Ogiltigt format. Använd 'lat,lng,lat,lng' eller 'lat lng lat lng'.",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
||||
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
||||
"selection_confirmed": "Val bekräftat!",
|
||||
"select_area_prompt": "Välj ett område på kartan med verktygen.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/ua.json
vendored
5
src/gui/locales/ua.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "Обрати локацію",
|
||||
"zoom_in_and_choose": "Збільште і оберіть область за допомогою прямокутника",
|
||||
"select_world": "Обрати світ",
|
||||
"choose_world": "Обрати світ",
|
||||
"no_world_selected": "Світ не обрано",
|
||||
"start_generation": "Почати генерацію",
|
||||
"progress": "Прогрес",
|
||||
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
||||
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
||||
"invalid_format": "Неправильний формат. Будь ласка, використовуйте 'широта,довгота,широта,довгота' або 'широта довгота широта довгота'",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
||||
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
||||
"selection_confirmed": "Вибір підтверджено!",
|
||||
"select_area_prompt": "Виберіть область на карті за допомогою інструментів.",
|
||||
"unknown_error": "Unknown error",
|
||||
"license_and_credits": "License and Credits",
|
||||
"placeholder_bbox": "Format: lat,lng,lat,lng",
|
||||
|
||||
5
src/gui/locales/zh-CN.json
vendored
5
src/gui/locales/zh-CN.json
vendored
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"select_location": "选择位置",
|
||||
"zoom_in_and_choose": "放大并使用矩形工具选择您的区域",
|
||||
"select_world": "选择世界",
|
||||
"choose_world": "选择世界",
|
||||
"no_world_selected": "未选择世界",
|
||||
"start_generation": "开始生成",
|
||||
"progress": "进度",
|
||||
"custom_selection_confirmed": "自定义选择已确认!",
|
||||
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
||||
"invalid_format": "格式无效。请使用 'lat,lng,lat,lng' 或 'lat lng lat lng'。",
|
||||
@@ -30,6 +26,7 @@
|
||||
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
||||
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
||||
"selection_confirmed": "选择已确认!",
|
||||
"select_area_prompt": "使用工具在地图上选择一个区域。",
|
||||
"unknown_error": "未知错误",
|
||||
"license_and_credits": "许可证和致谢",
|
||||
"placeholder_bbox": "格式: lat,lng,lat,lng",
|
||||
|
||||
2
src/gui/maps.html
vendored
2
src/gui/maps.html
vendored
@@ -26,7 +26,7 @@
|
||||
<div id="search-container">
|
||||
<div id="search-box">
|
||||
<input type="text" id="city-search" placeholder="Search for a city..." autocomplete="off" />
|
||||
<button id="search-btn">🔍</button>
|
||||
<button id="search-btn" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></button>
|
||||
</div>
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,11 @@ mod clipping;
|
||||
mod colors;
|
||||
mod coordinate_system;
|
||||
mod data_processing;
|
||||
mod deterministic_rng;
|
||||
mod element_processing;
|
||||
mod elevation_data;
|
||||
mod floodfill;
|
||||
mod floodfill_cache;
|
||||
mod ground;
|
||||
mod map_renderer;
|
||||
mod map_transformation;
|
||||
@@ -49,6 +51,9 @@ mod progress {
|
||||
use windows::Win32::System::Console::{AttachConsole, FreeConsole, ATTACH_PARENT_PROCESS};
|
||||
|
||||
fn run_cli() {
|
||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||
floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||
|
||||
let version: &str = env!("CARGO_PKG_VERSION");
|
||||
let repository: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
println!(
|
||||
|
||||
@@ -5,8 +5,8 @@ use crate::coordinate_system::transformation::CoordTransformer;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Raw data from OSM
|
||||
|
||||
@@ -29,9 +29,18 @@ struct OsmElement {
|
||||
pub members: Vec<OsmMember>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OsmData {
|
||||
pub elements: Vec<OsmElement>,
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OsmData {
|
||||
elements: Vec<OsmElement>,
|
||||
#[serde(default)]
|
||||
pub remark: Option<String>,
|
||||
}
|
||||
|
||||
impl OsmData {
|
||||
/// Returns true if there are no elements in the OSM data
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.elements.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
struct SplitOsmData {
|
||||
@@ -68,11 +77,6 @@ impl SplitOsmData {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_raw_osm_data(json_data: Value) -> Result<SplitOsmData, serde_json::Error> {
|
||||
let osm_data: OsmData = serde_json::from_value(json_data)?;
|
||||
Ok(SplitOsmData::from_raw_osm_data(osm_data))
|
||||
}
|
||||
|
||||
// End raw data
|
||||
|
||||
// Normalized data that we can use
|
||||
@@ -112,7 +116,7 @@ pub enum ProcessedMemberRole {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProcessedMember {
|
||||
pub role: ProcessedMemberRole,
|
||||
pub way: ProcessedWay,
|
||||
pub way: Arc<ProcessedWay>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -164,7 +168,7 @@ impl ProcessedElement {
|
||||
}
|
||||
|
||||
pub fn parse_osm_data(
|
||||
json_data: Value,
|
||||
osm_data: OsmData,
|
||||
bbox: LLBBox,
|
||||
scale: f64,
|
||||
debug: bool,
|
||||
@@ -174,7 +178,7 @@ pub fn parse_osm_data(
|
||||
emit_gui_progress_update(5.0, "Parsing data...");
|
||||
|
||||
// Deserialize the JSON data into the OSMData structure
|
||||
let data = parse_raw_osm_data(json_data).expect("Failed to parse OSM data");
|
||||
let data = SplitOsmData::from_raw_osm_data(osm_data);
|
||||
|
||||
let (coord_transformer, xzbbox) = CoordTransformer::llbbox_to_xzbbox(&bbox, scale)
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -189,7 +193,7 @@ pub fn parse_osm_data(
|
||||
}
|
||||
|
||||
let mut nodes_map: HashMap<u64, ProcessedNode> = HashMap::new();
|
||||
let mut ways_map: HashMap<u64, ProcessedWay> = HashMap::new();
|
||||
let mut ways_map: HashMap<u64, Arc<ProcessedWay>> = HashMap::new();
|
||||
|
||||
let mut processed_elements: Vec<ProcessedElement> = Vec::new();
|
||||
|
||||
@@ -238,17 +242,15 @@ pub fn parse_osm_data(
|
||||
let tags = element.tags.clone().unwrap_or_default();
|
||||
|
||||
// Store unclipped way for relation assembly (clipping happens after ring merging)
|
||||
ways_map.insert(
|
||||
element.id,
|
||||
ProcessedWay {
|
||||
id: element.id,
|
||||
tags: tags.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
);
|
||||
let way = Arc::new(ProcessedWay {
|
||||
id: element.id,
|
||||
tags,
|
||||
nodes,
|
||||
});
|
||||
ways_map.insert(element.id, Arc::clone(&way));
|
||||
|
||||
// Clip way nodes for standalone way processing (not relations)
|
||||
let clipped_nodes = clip_way_to_bbox(&nodes, &xzbbox);
|
||||
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
||||
|
||||
// Skip ways that are completely outside the bbox (empty after clipping)
|
||||
if clipped_nodes.is_empty() {
|
||||
@@ -257,8 +259,8 @@ pub fn parse_osm_data(
|
||||
|
||||
let processed: ProcessedWay = ProcessedWay {
|
||||
id: element.id,
|
||||
tags: tags.clone(),
|
||||
nodes: clipped_nodes.clone(),
|
||||
tags: way.tags.clone(),
|
||||
nodes: clipped_nodes,
|
||||
};
|
||||
|
||||
processed_elements.push(ProcessedElement::Way(processed));
|
||||
@@ -294,8 +296,8 @@ pub fn parse_osm_data(
|
||||
};
|
||||
|
||||
// Check if the way exists in ways_map
|
||||
let way: ProcessedWay = match ways_map.get(&mem.r#ref) {
|
||||
Some(w) => w.clone(),
|
||||
let way = match ways_map.get(&mem.r#ref) {
|
||||
Some(w) => Arc::clone(w),
|
||||
None => {
|
||||
// Way was likely filtered out because it was completely outside the bbox
|
||||
return None;
|
||||
@@ -311,11 +313,11 @@ pub fn parse_osm_data(
|
||||
if clipped_nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
ProcessedWay {
|
||||
Arc::new(ProcessedWay {
|
||||
id: way.id,
|
||||
tags: way.tags,
|
||||
tags: way.tags.clone(),
|
||||
nodes: clipped_nodes,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(ProcessedMember {
|
||||
@@ -336,6 +338,9 @@ pub fn parse_osm_data(
|
||||
|
||||
emit_gui_progress_update(15.0, "");
|
||||
|
||||
drop(nodes_map);
|
||||
drop(ways_map);
|
||||
|
||||
(processed_elements, xzbbox)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::osm_parser::OsmData;
|
||||
use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_gui};
|
||||
use colored::Colorize;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::blocking::ClientBuilder;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, Write};
|
||||
use std::io::{self, BufReader, Cursor, Write};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -34,19 +36,17 @@ fn download_with_reqwest(url: &str, query: &str) -> Result<String, Box<dyn std::
|
||||
}
|
||||
Err(e) => {
|
||||
if e.is_timeout() {
|
||||
eprintln!(
|
||||
"{}",
|
||||
"Error! Request timed out. Try selecting a smaller area."
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
emit_gui_error("Request timed out. Try selecting a smaller area.");
|
||||
let msg = "Request timed out. Try selecting a smaller area.";
|
||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
||||
Err(msg.into())
|
||||
} else if e.is_connect() {
|
||||
let msg = "No internet connection.";
|
||||
eprintln!("{}", format!("Error! {msg}").red().bold());
|
||||
Err(msg.into())
|
||||
} else {
|
||||
eprintln!("{}", format!("Error! {e:.52}").red().bold());
|
||||
emit_gui_error(&format!("{:.52}", e.to_string()));
|
||||
Err(format!("{e:.52}").into())
|
||||
}
|
||||
// Always propagate errors
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,13 +79,14 @@ fn download_with_wget(url: &str, query: &str) -> io::Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_data_from_file(file: &str) -> Result<Value, Box<dyn std::error::Error>> {
|
||||
pub fn fetch_data_from_file(file: &str) -> Result<OsmData, Box<dyn std::error::Error>> {
|
||||
println!("{} Loading data from file...", "[1/7]".bold());
|
||||
emit_gui_progress_update(1.0, "Loading data from file...");
|
||||
|
||||
let file: File = File::open(file)?;
|
||||
let reader: BufReader<File> = BufReader::new(file);
|
||||
let data: Value = serde_json::from_reader(reader)?;
|
||||
let mut deserializer = serde_json::Deserializer::from_reader(reader);
|
||||
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ pub fn fetch_data_from_overpass(
|
||||
debug: bool,
|
||||
download_method: &str,
|
||||
save_file: Option<&str>,
|
||||
) -> Result<Value, Box<dyn std::error::Error>> {
|
||||
) -> Result<OsmData, Box<dyn std::error::Error>> {
|
||||
println!("{} Fetching data...", "[1/7]".bold());
|
||||
emit_gui_progress_update(1.0, "Fetching data...");
|
||||
|
||||
@@ -182,14 +183,12 @@ pub fn fetch_data_from_overpass(
|
||||
println!("API response saved to: {save_file}");
|
||||
}
|
||||
|
||||
let data: Value = serde_json::from_str(&response)?;
|
||||
let mut deserializer =
|
||||
serde_json::Deserializer::from_reader(Cursor::new(response.as_bytes()));
|
||||
let data: OsmData = OsmData::deserialize(&mut deserializer)?;
|
||||
|
||||
if data["elements"]
|
||||
.as_array()
|
||||
.map_or(0, |elements: &Vec<Value>| elements.len())
|
||||
== 0
|
||||
{
|
||||
if let Some(remark) = data["remark"].as_str() {
|
||||
if data.is_empty() {
|
||||
if let Some(remark) = data.remark.as_deref() {
|
||||
// Check if the remark mentions memory or other runtime errors
|
||||
if remark.contains("runtime error") && remark.contains("out of memory") {
|
||||
eprintln!("{}", "Error! The query ran out of memory on the Overpass API server. Try using a smaller area.".red().bold());
|
||||
@@ -211,7 +210,7 @@ pub fn fetch_data_from_overpass(
|
||||
}
|
||||
|
||||
if debug {
|
||||
println!("Additional debug information: {data}");
|
||||
println!("Additional debug information: {data:?}");
|
||||
}
|
||||
|
||||
if !is_running_with_gui() {
|
||||
|
||||
@@ -7,9 +7,8 @@ use crate::retrieve_data;
|
||||
// this is copied from main.rs
|
||||
pub fn generate_example(llbbox: LLBBox) -> (XZBBox, Vec<ProcessedElement>) {
|
||||
// Fetch data
|
||||
let raw_data: serde_json::Value =
|
||||
retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
|
||||
.expect("Failed to fetch data");
|
||||
let raw_data = retrieve_data::fetch_data_from_overpass(llbbox, false, "requests", None)
|
||||
.expect("Failed to fetch data");
|
||||
|
||||
// Parse raw data
|
||||
let (mut parsed_elements, xzbbox) = osm_parser::parse_osm_data(raw_data, llbbox, 1.0, false);
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::collections::HashMap as StdHashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Cursor, Write as IoWrite};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use vek::Vec2;
|
||||
use zip::write::FileOptions;
|
||||
use zip::CompressionMethod;
|
||||
@@ -122,7 +123,7 @@ pub struct BedrockWriter {
|
||||
output_dir: PathBuf,
|
||||
level_name: String,
|
||||
spawn_point: Option<(i32, i32)>,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
}
|
||||
|
||||
impl BedrockWriter {
|
||||
@@ -131,7 +132,7 @@ impl BedrockWriter {
|
||||
output_path: PathBuf,
|
||||
level_name: String,
|
||||
spawn_point: Option<(i32, i32)>,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
) -> Self {
|
||||
// If the path ends with .mcworld, use it as the final archive path
|
||||
// and create a temp directory without that extension for working files
|
||||
@@ -214,8 +215,11 @@ impl BedrockWriter {
|
||||
.ground
|
||||
.as_ref()
|
||||
.map(|ground| {
|
||||
let coord = crate::coordinate_system::cartesian::XZPoint::new(spawn_x, spawn_z);
|
||||
ground.level(coord) + 2 // Add 2 blocks above ground for safety
|
||||
// Ground elevation data expects coordinates relative to the XZ bbox origin
|
||||
let rel_x = spawn_x - xzbbox.min_x();
|
||||
let rel_z = spawn_z - xzbbox.min_z();
|
||||
let coord = crate::coordinate_system::cartesian::XZPoint::new(rel_x, rel_z);
|
||||
ground.level(coord) + 3 // Add 3 blocks above ground for safety
|
||||
})
|
||||
.unwrap_or(64);
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! before they are written to either Java or Bedrock format.
|
||||
|
||||
use crate::block_definitions::*;
|
||||
|
||||
/// Minimum Y coordinate in Minecraft (1.18+)
|
||||
const MIN_Y: i32 = -64;
|
||||
/// Maximum Y coordinate in Minecraft (1.18+)
|
||||
const MAX_Y: i32 = 319;
|
||||
use fastnbt::{LongArray, Value};
|
||||
use fnv::FnvHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -150,7 +155,7 @@ impl SectionToModify {
|
||||
let palette = unique_blocks
|
||||
.iter()
|
||||
.map(|(block, stored_props)| PaletteItem {
|
||||
name: block.name().to_string(),
|
||||
name: format!("{}:{}", block.namespace(), block.name()),
|
||||
properties: stored_props.clone().or_else(|| block.properties()),
|
||||
})
|
||||
.collect();
|
||||
@@ -186,16 +191,20 @@ pub(crate) struct ChunkToModify {
|
||||
impl ChunkToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: i32, z: u8) -> Option<Block> {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.get(§ion_idx)?;
|
||||
section.get_block(x, (y & 15).try_into().unwrap(), z)
|
||||
section.get_block(x, (y & 15) as u8, z)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: i32, z: u8, block: Block) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block(x, (y & 15).try_into().unwrap(), z, block);
|
||||
section.set_block(x, (y & 15) as u8, z, block);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -206,9 +215,11 @@ impl ChunkToModify {
|
||||
z: u8,
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let section_idx: i8 = (y >> 4).try_into().unwrap();
|
||||
// Clamp Y to valid Minecraft range to prevent TryFromIntError
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = self.sections.entry(section_idx).or_default();
|
||||
section.set_block_with_properties(x, (y & 15).try_into().unwrap(), z, block_with_props);
|
||||
section.set_block_with_properties(x, (y & 15) as u8, z, block_with_props);
|
||||
}
|
||||
|
||||
pub fn sections(&self) -> impl Iterator<Item = Section> + '_ {
|
||||
|
||||
@@ -23,9 +23,11 @@ use crate::telemetry::{send_log, LogLevel};
|
||||
impl<'a> WorldEditor<'a> {
|
||||
/// Creates a region file for the given region coordinates.
|
||||
pub(super) fn create_region(&self, region_x: i32, region_z: i32) -> Region<File> {
|
||||
let out_path = self
|
||||
.world_dir
|
||||
.join(format!("region/r.{}.{}.mca", region_x, region_z));
|
||||
let region_dir = self.world_dir.join("region");
|
||||
let out_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
// Ensure region directory exists before creating region files
|
||||
std::fs::create_dir_all(®ion_dir).expect("Failed to create region directory");
|
||||
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
|
||||
|
||||
@@ -75,6 +77,8 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
|
||||
/// Saves the world in Java Edition Anvil format.
|
||||
///
|
||||
/// Uses parallel processing with rayon for fast region saving.
|
||||
pub(super) fn save_java(&mut self) {
|
||||
println!("{} Saving world...", "[7/7]".bold());
|
||||
emit_gui_progress_update(90.0, "Saving world...");
|
||||
@@ -104,122 +108,12 @@ impl<'a> WorldEditor<'a> {
|
||||
.regions
|
||||
.par_iter()
|
||||
.for_each(|((region_x, region_z), region_to_modify)| {
|
||||
let mut region = self.create_region(*region_x, *region_z);
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
|
||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||
// Read existing chunk data if it exists
|
||||
let existing_data = region
|
||||
.read_chunk(chunk_x as usize, chunk_z as usize)
|
||||
.unwrap()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse existing chunk or create new one
|
||||
let mut chunk: Chunk = if !existing_data.is_empty() {
|
||||
fastnbt::from_bytes(&existing_data).unwrap()
|
||||
} else {
|
||||
Chunk {
|
||||
sections: Vec::new(),
|
||||
x_pos: chunk_x + (region_x * 32),
|
||||
z_pos: chunk_z + (region_z * 32),
|
||||
is_light_on: 0,
|
||||
other: FnvHashMap::default(),
|
||||
}
|
||||
};
|
||||
|
||||
// Update sections while preserving existing data
|
||||
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
|
||||
for new_section in new_sections {
|
||||
if let Some(existing_section) =
|
||||
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
|
||||
{
|
||||
// Merge block states
|
||||
existing_section.block_states.palette =
|
||||
new_section.block_states.palette;
|
||||
existing_section.block_states.data = new_section.block_states.data;
|
||||
} else {
|
||||
// Add new section if it doesn't exist
|
||||
chunk.sections.push(new_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing block entities and merge with new ones
|
||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities")
|
||||
{
|
||||
if let (Value::List(existing), Value::List(new)) =
|
||||
(existing_entities, new_entities)
|
||||
{
|
||||
// Remove old entities that are replaced by new ones
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
let (x, y, z) = get_entity_coords(map);
|
||||
!new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
||||
x == nx && y == ny && z == nz
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Add new entities
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no existing entities, just add the new ones
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities")
|
||||
{
|
||||
chunk
|
||||
.other
|
||||
.insert("block_entities".to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Update chunk coordinates and flags
|
||||
chunk.x_pos = chunk_x + (region_x * 32);
|
||||
chunk.z_pos = chunk_z + (region_z * 32);
|
||||
|
||||
// Create Level wrapper and save
|
||||
let level_data = create_level_wrapper(&chunk);
|
||||
ser_buffer.clear();
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: ensure all chunks exist
|
||||
for chunk_x in 0..32 {
|
||||
for chunk_z in 0..32 {
|
||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||
let abs_chunk_z = chunk_z + (region_z * 32);
|
||||
|
||||
// Check if chunk exists in our modifications
|
||||
let chunk_exists =
|
||||
region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
||||
|
||||
// If chunk doesn't exist, create it with base layer
|
||||
if !chunk_exists {
|
||||
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.save_single_region(*region_x, *region_z, region_to_modify);
|
||||
|
||||
// Update progress
|
||||
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
||||
// Update progress at regular intervals (every ~1% or at least every 10 regions)
|
||||
// This ensures progress is visible even with many regions
|
||||
// Update progress at regular intervals (every ~10% or at least every 10 regions)
|
||||
let update_interval = (total_regions / 10).max(1);
|
||||
if regions_done.is_multiple_of(update_interval) || regions_done == total_regions {
|
||||
let progress = 90.0 + (regions_done as f64 / total_regions as f64) * 9.0;
|
||||
@@ -231,6 +125,123 @@ impl<'a> WorldEditor<'a> {
|
||||
|
||||
save_pb.finish();
|
||||
}
|
||||
|
||||
/// Saves a single region to disk.
|
||||
///
|
||||
/// This is extracted to allow streaming mode to save and release regions one at a time.
|
||||
fn save_single_region(
|
||||
&self,
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
region_to_modify: &super::common::RegionToModify,
|
||||
) {
|
||||
let mut region = self.create_region(region_x, region_z);
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
|
||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||
// Read existing chunk data if it exists
|
||||
let existing_data = region
|
||||
.read_chunk(chunk_x as usize, chunk_z as usize)
|
||||
.unwrap()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Parse existing chunk or create new one
|
||||
let mut chunk: Chunk = if !existing_data.is_empty() {
|
||||
fastnbt::from_bytes(&existing_data).unwrap()
|
||||
} else {
|
||||
Chunk {
|
||||
sections: Vec::new(),
|
||||
x_pos: chunk_x + (region_x * 32),
|
||||
z_pos: chunk_z + (region_z * 32),
|
||||
is_light_on: 0,
|
||||
other: FnvHashMap::default(),
|
||||
}
|
||||
};
|
||||
|
||||
// Update sections while preserving existing data
|
||||
let new_sections: Vec<Section> = chunk_to_modify.sections().collect();
|
||||
for new_section in new_sections {
|
||||
if let Some(existing_section) =
|
||||
chunk.sections.iter_mut().find(|s| s.y == new_section.y)
|
||||
{
|
||||
// Merge block states
|
||||
existing_section.block_states.palette = new_section.block_states.palette;
|
||||
existing_section.block_states.data = new_section.block_states.data;
|
||||
} else {
|
||||
// Add new section if it doesn't exist
|
||||
chunk.sections.push(new_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing block entities and merge with new ones
|
||||
if let Some(existing_entities) = chunk.other.get_mut("block_entities") {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
if let (Value::List(existing), Value::List(new)) =
|
||||
(existing_entities, new_entities)
|
||||
{
|
||||
// Remove old entities that are replaced by new ones
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
let (x, y, z) = get_entity_coords(map);
|
||||
!new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
let (nx, ny, nz) = get_entity_coords(new_map);
|
||||
x == nx && y == ny && z == nz
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Add new entities
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no existing entities, just add the new ones
|
||||
if let Some(new_entities) = chunk_to_modify.other.get("block_entities") {
|
||||
chunk
|
||||
.other
|
||||
.insert("block_entities".to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Update chunk coordinates and flags
|
||||
chunk.x_pos = chunk_x + (region_x * 32);
|
||||
chunk.z_pos = chunk_z + (region_z * 32);
|
||||
|
||||
// Create Level wrapper and save
|
||||
let level_data = create_level_wrapper(&chunk);
|
||||
ser_buffer.clear();
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: ensure all chunks exist
|
||||
for chunk_x in 0..32 {
|
||||
for chunk_z in 0..32 {
|
||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||
let abs_chunk_z = chunk_z + (region_z * 32);
|
||||
|
||||
// Check if chunk exists in our modifications
|
||||
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
||||
|
||||
// If chunk doesn't exist, create it with base layer
|
||||
if !chunk_exists {
|
||||
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
||||
region
|
||||
.write_chunk(chunk_x as usize, chunk_z as usize, &ser_buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to get entity coordinates
|
||||
|
||||
@@ -33,6 +33,7 @@ use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
@@ -71,7 +72,7 @@ pub struct WorldEditor<'a> {
|
||||
world: WorldToModify,
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Option<Box<Ground>>,
|
||||
ground: Option<Arc<Ground>>,
|
||||
format: WorldFormat,
|
||||
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
|
||||
bedrock_level_name: Option<String>,
|
||||
@@ -122,13 +123,13 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
|
||||
/// Sets the ground reference for elevation-based block placement
|
||||
pub fn set_ground(&mut self, ground: &Ground) {
|
||||
self.ground = Some(Box::new(ground.clone()));
|
||||
pub fn set_ground(&mut self, ground: Arc<Ground>) {
|
||||
self.ground = Some(ground);
|
||||
}
|
||||
|
||||
/// Gets a reference to the ground data if available
|
||||
pub fn get_ground(&self) -> Option<&Ground> {
|
||||
self.ground.as_ref().map(|g| g.as_ref())
|
||||
self.ground.as_deref()
|
||||
}
|
||||
|
||||
/// Returns the current world format
|
||||
@@ -150,6 +151,19 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ground level at a specific world coordinate (without any offset)
|
||||
#[inline(always)]
|
||||
pub fn get_ground_level(&self, x: i32, z: i32) -> i32 {
|
||||
if let Some(ground) = &self.ground {
|
||||
ground.level(XZPoint::new(
|
||||
x - self.xzbbox.min_x(),
|
||||
z - self.xzbbox.min_z(),
|
||||
))
|
||||
} else {
|
||||
0 // Default ground level if no terrain data
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the minimum world coordinates
|
||||
pub fn get_min_coords(&self) -> (i32, i32) {
|
||||
(self.xzbbox.min_x(), self.xzbbox.min_z())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "src/gui"
|
||||
@@ -16,7 +16,7 @@
|
||||
"minWidth": 1000,
|
||||
"minHeight": 650,
|
||||
"resizable": true,
|
||||
"transparent": true,
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"theme": "Dark",
|
||||
"additionalBrowserArgs": "--disable-features=VizDisplayCompositor"
|
||||
|
||||
Reference in New Issue
Block a user