Compare commits

...

28 Commits

Author SHA1 Message Date
Louis Erbkamm
6cdebbed78 Merge pull request #714 from louis-e/revert-sequential-streaming
Revert sequential streaming saving since generation speed was affected
2026-01-11 14:36:44 +01:00
Louis Erbkamm
5291f72215 Add press assets link 2026-01-11 14:30:25 +01:00
louis-e
c24e22b790 Revert to sequential streaming saving since generation speed was affected 2026-01-11 14:19:35 +01:00
Louis Erbkamm
d4f324fd96 Migrate macOS 13 to macOS 15 Intel runner 2026-01-11 04:07:23 +01:00
Louis Erbkamm
e7e65d0e6f Merge pull request #712 from louis-e/ui-refactor
UI refactor
2026-01-11 03:58:48 +01:00
louis-e
927aaec22d Fix mixed language 2026-01-11 03:51:35 +01:00
louis-e
5ec942dbd1 Fall back to current dir if mc dir does not exist 2026-01-11 03:46:17 +01:00
louis-e
19bba3cc26 Fall back to current dir if mc dir does not exist 2026-01-11 03:40:39 +01:00
louis-e
17d6d323fc Refactor UI 2026-01-11 03:30:37 +01:00
Louis Erbkamm
236072dc42 Merge pull request #711 from louis-e/tree-building-overlap-fix
Lookup building footprint in tree generation
2026-01-11 02:07:46 +01:00
louis-e
7a8226923a Protect relation inner from tree spawning 2026-01-11 02:03:14 +01:00
louis-e
107ab70602 Fix overflow issues 2026-01-11 01:50:17 +01:00
louis-e
1364d96291 Address code review feedback 2026-01-11 01:39:36 +01:00
louis-e
b74b5c5ccb Lookup building footprint in tree generation 2026-01-11 01:20:27 +01:00
Louis Erbkamm
dd8004b159 Merge pull request #710 from louis-e/ui-enhancements
Add UI tooltips and move bbox info box
2026-01-11 00:51:44 +01:00
louis-e
b0845ce1df Merge branch 'ui-enhancements' of https://github.com/louis-e/arnis into ui-enhancements 2026-01-11 00:50:02 +01:00
louis-e
fc540db4cd Unify displayBboxSizeStatus function 2026-01-11 00:48:32 +01:00
Louis Erbkamm
1ecdffc039 Merge branch 'main' into ui-enhancements 2026-01-11 00:40:32 +01:00
louis-e
9ea34b9911 Add UI tooltips and move bbox info box 2026-01-11 00:40:04 +01:00
Louis Erbkamm
48248aad05 Merge pull request #708 from louis-e/spawn-point-improvement
Add default spawn point
2026-01-10 23:05:47 +01:00
louis-e
169545d937 Address code review feedback 2026-01-10 22:53:14 +01:00
louis-e
fba331232b Fix Bedrock spawn Y calc 2026-01-10 22:18:27 +01:00
louis-e
b02a2783c1 Address code review feedback 2026-01-10 19:19:20 +01:00
louis-e
dbc4741b78 Add minecraft prefix to blocks 2026-01-10 19:11:12 +01:00
louis-e
b52485badc Add ferns 2026-01-10 19:04:23 +01:00
Louis Erbkamm
447416f6ce Merge branch 'main' into spawn-point-improvement 2026-01-10 19:00:59 +01:00
louis-e
d26b23937e Add default spawn point 2026-01-10 18:59:35 +01:00
Louis Erbkamm
5e01abc5b6 Merge pull request #707 from louis-e/ui-improvements
UI improvements
2026-01-10 18:41:22 +01:00
36 changed files with 724 additions and 362 deletions

View File

@@ -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
@@ -99,7 +99,7 @@ jobs:
- name: Download macOS Intel build
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
@@ -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 }}

2
Cargo.lock generated
View File

@@ -182,7 +182,7 @@ dependencies = [
[[package]]
name = "arnis"
version = "2.4.0"
version = "2.4.1"
dependencies = [
"base64 0.22.1",
"bedrockrs_level",

View File

@@ -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"

View File

@@ -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)

View File

@@ -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> {

View File

@@ -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]

View File

@@ -80,7 +80,10 @@ pub fn generate_world_with_options(
// Pre-compute all flood fills in parallel for better CPU utilization
let mut flood_fill_cache = FloodFillCache::precompute(&elements, args.timeout.as_ref());
println!("Pre-computed {} flood fills", flood_fill_cache.way_count());
// 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();
@@ -127,13 +130,31 @@ pub fn generate_world_with_options(
&flood_fill_cache,
);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(&mut editor, way, args, &flood_fill_cache);
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, &flood_fill_cache);
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, &flood_fill_cache);
} else if way.tags.contains_key("leisure") {
leisure::generate_leisure(&mut editor, way, args, &flood_fill_cache);
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);
} else if let Some(val) = way.tags.get("waterway") {
@@ -166,7 +187,13 @@ 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, &flood_fill_cache);
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, &flood_fill_cache);
} else if node.tags.contains_key("barrier") {
@@ -207,6 +234,7 @@ pub fn generate_world_with_options(
rel,
args,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.contains_key("landuse") {
landuse::generate_landuse_from_relation(
@@ -214,6 +242,7 @@ pub fn generate_world_with_options(
rel,
args,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
leisure::generate_leisure_from_relation(
@@ -221,6 +250,7 @@ pub fn generate_world_with_options(
rel,
args,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, &element, args);
@@ -355,30 +385,28 @@ pub fn generate_world_with_options(
// 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;
// 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()
);
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.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);
}
// 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);
}
}

View File

@@ -2,7 +2,7 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -12,6 +12,7 @@ pub fn generate_landuse(
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
// Determine block type based on landuse tag
let binding: String = "".to_string();
@@ -37,7 +38,7 @@ pub fn generate_landuse(
"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" => {
@@ -83,6 +84,16 @@ pub fn generate_landuse(
} 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 {
block_type
};
@@ -120,7 +131,7 @@ pub fn generate_landuse(
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);
}
@@ -130,7 +141,7 @@ pub fn generate_landuse(
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,
@@ -141,7 +152,11 @@ pub fn generate_landuse(
};
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);
}
}
}
}
@@ -243,7 +258,8 @@ pub fn generate_landuse(
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),
_ => {}
}
}
@@ -252,7 +268,8 @@ pub fn generate_landuse(
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),
_ => {}
}
}
@@ -261,11 +278,13 @@ pub fn generate_landuse(
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);
}
@@ -273,11 +292,12 @@ pub fn generate_landuse(
}
"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),
_ => {}
}
}
@@ -312,12 +332,19 @@ pub fn generate_landuse_from_relation(
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, flood_fill_cache);
generate_landuse(
editor,
&member.way.clone(),
args,
flood_fill_cache,
building_footprints,
);
}
}
@@ -339,7 +366,13 @@ pub fn generate_landuse_from_relation(
};
// Generate landuse area from combined way
generate_landuse(editor, &combined_way, args, flood_fill_cache);
generate_landuse(
editor,
&combined_way,
args,
flood_fill_cache,
building_footprints,
);
}
}
}

View File

@@ -3,7 +3,7 @@ use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -13,6 +13,7 @@ pub fn generate_leisure(
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;
@@ -118,7 +119,7 @@ pub fn generate_leisure(
}
105..120 => {
// Tree
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
}
_ => {}
}
@@ -179,12 +180,19 @@ pub fn generate_leisure_from_relation(
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, flood_fill_cache);
generate_leisure(
editor,
&member.way,
args,
flood_fill_cache,
building_footprints,
);
}
}
@@ -204,6 +212,12 @@ pub fn generate_leisure_from_relation(
};
// Generate leisure area from combined way
generate_leisure(editor, &combined_way, args, flood_fill_cache);
generate_leisure(
editor,
&combined_way,
args,
flood_fill_cache,
building_footprints,
);
}
}

View File

@@ -3,7 +3,7 @@ use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill_cache::FloodFillCache;
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -13,6 +13,7 @@ pub fn generate_natural(
element: &ProcessedElement,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if let Some(natural_type) = element.tags().get("natural") {
if natural_type == "tree" {
@@ -20,7 +21,7 @@ pub fn generate_natural(
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;
@@ -134,7 +135,7 @@ pub fn generate_natural(
}
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,
@@ -163,7 +164,7 @@ pub fn generate_natural(
}
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,
@@ -222,7 +223,11 @@ pub fn generate_natural(
// 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);
}
@@ -306,6 +311,7 @@ pub fn generate_natural(
Tree::create(
editor,
(cluster_x, 1, cluster_z),
Some(building_footprints),
);
} else if vegetation_chance < 15 {
// 15% chance for grass
@@ -418,7 +424,7 @@ pub fn generate_natural(
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) {
@@ -451,6 +457,7 @@ pub fn generate_natural_from_relation(
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if rel.tags.contains_key("natural") {
// Generate individual ways with their original tags
@@ -461,6 +468,7 @@ pub fn generate_natural_from_relation(
&ProcessedElement::Way((*member.way).clone()),
args,
flood_fill_cache,
building_footprints,
);
}
}
@@ -488,6 +496,7 @@ pub fn generate_natural_from_relation(
&ProcessedElement::Way(combined_way),
args,
flood_fill_cache,
building_footprints,
);
}
}

View File

@@ -1,5 +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;
@@ -108,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());

View File

@@ -4,12 +4,119 @@
//! 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, ProcessedWay};
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
@@ -128,9 +235,51 @@ impl FloodFillCache {
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
}
/// Returns the number of cached way entries.
pub fn way_count(&self) -> usize {
self.way_cache.len()
/// 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.

View File

@@ -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};
@@ -158,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
@@ -433,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;
@@ -494,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;
}
}
}
}
@@ -540,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() {
@@ -621,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
};
@@ -635,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;
}
}
@@ -816,36 +803,49 @@ 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 {
@@ -933,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, _)) =
@@ -947,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
@@ -978,7 +984,6 @@ fn gui_start_generation(
fillground: fillground_enabled,
debug: false,
timeout: Some(std::time::Duration::from_secs(40)),
spawn_point,
};
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data

View File

@@ -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 {

168
src/gui/css/styles.css vendored
View File

@@ -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;
}
@@ -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 {

136
src/gui/index.html vendored
View File

@@ -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()" 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>
<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,7 +151,10 @@
<!-- 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>
@@ -153,7 +162,10 @@
<!-- 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>
@@ -167,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>
@@ -191,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>

98
src/gui/js/main.js vendored
View File

@@ -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,13 +97,9 @@ 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",
@@ -117,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;
}
@@ -165,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) {
@@ -188,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) => {
@@ -200,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";
}
}
});
@@ -525,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;
}
@@ -565,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;
@@ -576,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
@@ -588,8 +609,7 @@ function handleBboxInput() {
} else {
selectedBBox = mapSelectedBBox;
}
localizeElement(window.localization, { element: bboxInfo }, "invalid_format");
bboxInfo.style.color = "#fecc44";
setBboxInfo(bboxInfo, "invalid_format", "#fecc44");
}
});
}
@@ -638,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);
@@ -652,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 = "";
@@ -663,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 = "";
@@ -766,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;
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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ł",

View File

@@ -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": "Формат: широта,долгота,широта,долгота",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -215,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);

View File

@@ -155,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();

View File

@@ -1,7 +1,6 @@
//! Java Edition Anvil format world saving.
//!
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
//! Supports streaming mode for memory-efficient saving of large worlds.
use super::common::{Chunk, ChunkToModify, Section};
use super::WorldEditor;
@@ -12,9 +11,11 @@ use fastanvil::Region;
use fastnbt::Value;
use fnv::FnvHashMap;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
@@ -77,8 +78,7 @@ impl<'a> WorldEditor<'a> {
/// Saves the world in Java Edition Anvil format.
///
/// Uses streaming mode: saves regions one at a time and releases memory after each,
/// significantly reducing peak memory usage for large worlds.
/// 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...");
@@ -102,35 +102,26 @@ impl<'a> WorldEditor<'a> {
.progress_chars("█▓░"),
);
// Streaming mode: Process regions sequentially and release memory after each.
// This significantly reduces peak memory for large worlds (100+ regions).
// For small worlds, the overhead is negligible.
let mut regions_processed: u64 = 0;
let regions_processed = AtomicU64::new(0);
// Collect region keys first to allow draining
let region_keys: Vec<(i32, i32)> = self.world.regions.keys().copied().collect();
self.world
.regions
.par_iter()
.for_each(|((region_x, region_z), region_to_modify)| {
self.save_single_region(*region_x, *region_z, region_to_modify);
for (region_x, region_z) in region_keys {
// Remove region from memory - this is the key to memory savings
if let Some(region_to_modify) = self.world.regions.remove(&(region_x, region_z)) {
self.save_single_region(region_x, region_z, &region_to_modify);
// Update progress
let regions_done = regions_processed.fetch_add(1, Ordering::SeqCst) + 1;
// Region memory is freed when region_to_modify goes out of scope here
}
// 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;
emit_gui_progress_update(progress, "Saving world...");
}
regions_processed += 1;
// Update progress at regular intervals
let update_interval = (total_regions / 10).max(1);
if regions_processed.is_multiple_of(update_interval)
|| regions_processed == total_regions
{
let progress = 90.0 + (regions_processed as f64 / total_regions as f64) * 9.0;
emit_gui_progress_update(progress, "Saving world...");
}
save_pb.inc(1);
}
save_pb.inc(1);
});
save_pb.finish();
}

View File

@@ -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"