Compare commits

..

84 Commits

Author SHA1 Message Date
Louis Erbkamm
5d97391820 Merge pull request #664 from louis-e/single-bbox 2025-12-07 20:32:41 +01:00
louis-e
bef3cfb090 Allow only one bbox selection at a time 2025-12-07 19:37:49 +01:00
Louis Erbkamm
5a898944f7 Merge pull request #663 from louis-e/fix-world-lock-during-map-preview
Fix world lock held during map preview generation
2025-12-07 19:24:40 +01:00
louis-e
9fdd960009 Fix world lock held during map preview generation 2025-12-07 18:18:12 +01:00
Louis Erbkamm
58e4a337d9 Merge pull request #661 from louis-e/disable-transparent
Disable transparent flag
2025-12-07 15:06:33 +01:00
louis-e
236a7e5af9 Disable transparent flag 2025-12-07 15:04:02 +01:00
Louis Erbkamm
9173e5b4de Merge pull request #654 from louis-e/prepare-v2.4.0
Prepare release v2.4.0
2025-12-05 18:01:58 +01:00
louis-e
1fd02d8005 Prepare release v2.4.0 2025-12-05 17:49:58 +01:00
Louis Erbkamm
438b2beceb Merge pull request #653 from louis-e/bedrock-support
Bedrock support
2025-12-05 17:34:03 +01:00
louis-e
a62e181c16 Only modify spawn position in level.dat for Java 2025-12-05 17:26:34 +01:00
louis-e
12abba3bc8 Remove telemetry line 2025-12-05 17:25:48 +01:00
louis-e
a8e31700d8 Remove comment 2025-12-05 16:56:17 +01:00
louis-e
7a109cce0b Mock emit_open_mcworld_file 2025-12-05 16:51:30 +01:00
louis-e
86543714af Mention Bedrock Edition in README 2025-12-05 16:51:00 +01:00
louis-e
b84a565210 Restore format selection earlier in call chain 2025-12-05 16:50:44 +01:00
louis-e
93becaae7f Include world_icon via include_bytes 2025-12-05 16:39:36 +01:00
louis-e
06e377ce29 Emit done msg after snowman lock is released 2025-12-05 01:00:39 +01:00
louis-e
e22380bdd3 Fix progress bar updates on Java gen with many regions 2025-12-05 00:45:13 +01:00
louis-e
35cac44209 Release snowman lock before map preview generation 2025-12-05 00:43:51 +01:00
louis-e
61af45d2f4 Skip map preview for large areas 2025-12-05 00:39:39 +01:00
louis-e
393f1f9bd8 Increase map loading failure count 2025-12-05 00:39:20 +01:00
louis-e
e6f8466177 Adjust area size thresholds 2025-12-05 00:38:52 +01:00
louis-e
02d3a32a03 Scale gaussian blur correctly with area size 2025-12-05 00:31:51 +01:00
louis-e
f00304ff3a Fix spawn_y spawn in Bedrock 2025-12-04 23:52:44 +01:00
louis-e
a93b908104 Fix bedrock progress bar 2025-12-04 22:57:58 +01:00
louis-e
7cbc4fa263 Fix incorrectly mapped Bedrock blocks 2025-12-04 22:24:56 +01:00
louis-e
7e7f7ed476 Support spawn position marker on Bedrock 2025-12-04 19:00:14 +01:00
louis-e
3c0ba60657 Adjust time in benchmark 2025-12-04 18:59:51 +01:00
louis-e
fb438c4a0f Preserver block properties in bedrock 2025-12-04 18:38:50 +01:00
louis-e
5015c8b9b4 Fix linter issues 2025-12-04 18:15:21 +01:00
louis-e
af0ace422f Add bedrock_use_java localization 2025-12-04 18:04:59 +01:00
louis-e
0bb39b7d9e Simplify comments 2025-12-04 17:32:53 +01:00
louis-e
5b5e93b89a Refactor world_editor into modular directory structure 2025-12-04 17:26:12 +01:00
louis-e
958dc2107e Remove console log line 2025-12-04 17:25:14 +01:00
louis-e
562a3bca66 Clean up temp directory after packaging mcworld 2025-12-04 16:52:07 +01:00
louis-e
f1b37fbbb6 Add Bedrock world icon asset 2025-12-04 16:46:50 +01:00
louis-e
b34cbf4307 Add bedrock-rs license credit 2025-12-04 16:46:22 +01:00
louis-e
a03318bb98 Add format toggle logic and mcworld file opening 2025-12-04 16:45:59 +01:00
louis-e
8bb779d6cc Add format toggle button styles 2025-12-04 16:45:47 +01:00
louis-e
6d164102ad Add Java/Bedrock format toggle UI 2025-12-04 16:45:35 +01:00
louis-e
127a0e5e68 Add GUI format toggle and show_in_folder command 2025-12-04 16:44:46 +01:00
louis-e
4a326c3dad Add emit_open_mcworld_file event 2025-12-04 16:44:37 +01:00
louis-e
d4fd9b9cd3 Add GenerationOptions and format-aware world generation 2025-12-04 16:44:19 +01:00
louis-e
ee0521f232 Add Bedrock world format support with LevelDB storage 2025-12-04 16:43:59 +01:00
louis-e
8b3a41b131 Add Java to Bedrock block mapping 2025-12-04 16:43:49 +01:00
louis-e
02594b1cae Add bedrock_block_map module import 2025-12-04 16:43:38 +01:00
louis-e
06ba4db97e Add bedrockrs and dependencies for Bedrock support 2025-12-04 16:43:27 +01:00
louis-e
59d31cfbb8 Add *.mcworld to gitignore 2025-12-04 16:43:13 +01:00
Louis Erbkamm
94388e4164 Merge pull request #651 from louis-e/fix-2x-sprite
fix: correct disabled icon positions for 7-sprite 2x sheet
2025-12-01 21:49:41 +01:00
louis-e
f8c9fd8f4c fix: correct disabled icon positions for 7-sprite 2x sheet 2025-12-01 21:42:45 +01:00
Louis Erbkamm
2ee2d48f6a Merge pull request #650 from louis-e/mock-emit-mappreview
Mock map preview emit for CLI builds
2025-12-01 18:50:04 +01:00
louis-e
56c2f2e5cd Mock mep preview emit for CLI builds 2025-12-01 18:49:40 +01:00
Louis Erbkamm
9d34bc8e92 Merge pull request #649 from louis-e/benchmark-datetime
Add date time in benchmark comment
2025-12-01 18:46:45 +01:00
louis-e
c95b78fdcd Add date time to benchmark comment 2025-12-01 18:46:09 +01:00
louis-e
6e52e08b8a Remove telemetry line 2025-12-01 18:45:55 +01:00
Louis Erbkamm
57a4a801cf Modify benchmark workflow for map preview and checkout
Updated the benchmark workflow to use actions/checkout@v5 and added steps to check for and upload a map preview artifact.
2025-12-01 18:04:08 +01:00
Louis Erbkamm
0c47e365bc Merge pull request #644 from louis-e/dependabot/cargo/semver-1.0.27
build(deps): bump semver from 1.0.26 to 1.0.27
2025-12-01 17:47:50 +01:00
Louis Erbkamm
dad3ab3b34 Merge pull request #643 from louis-e/dependabot/github_actions/actions/checkout-6
build(deps): bump actions/checkout from 5 to 6
2025-12-01 17:47:40 +01:00
Louis Erbkamm
b8b63a2bc5 Merge pull request #647 from louis-e/map-preview
Display map preview in GUI
2025-12-01 17:44:05 +01:00
louis-e
cab20b5e50 Fix clippy lint 2025-12-01 17:43:43 +01:00
louis-e
0e879837fa Remove block category comments 2025-12-01 17:42:37 +01:00
louis-e
92be2ccf00 Improve map preview generation time 2025-12-01 17:38:10 +01:00
louis-e
3b76d707d9 Display map preview in GUI 2025-12-01 17:08:54 +01:00
dependabot[bot]
be8559dee7 build(deps): bump semver from 1.0.26 to 1.0.27
Bumps [semver](https://github.com/dtolnay/semver) from 1.0.26 to 1.0.27.
- [Release notes](https://github.com/dtolnay/semver/releases)
- [Commits](https://github.com/dtolnay/semver/compare/1.0.26...1.0.27)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 1.0.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:41:00 +00:00
dependabot[bot]
94eda2fad3 build(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:27:47 +00:00
Louis Erbkamm
7d86854e3c Merge pull request #640 from louis-e/reintroduce-sutherland-hodgeman
Reintroduce Sutherland-Hodgman Clipping Algorithm
2025-11-28 11:28:25 +01:00
louis-e
cddaa89d35 Fix polyline clipping for segments crossing bbox with external endpoints 2025-11-28 11:12:17 +01:00
louis-e
453845977d Fix polyline clipping for segments crossing bbox with external endpoints 2025-11-28 11:10:43 +01:00
louis-e
4e196e51bd Move sutherland hodgman into own clipping file 2025-11-28 10:57:47 +01:00
louis-e
ea4dc5dc08 Remove unused dependencies 2025-11-28 10:40:06 +01:00
louis-e
c56ff83094 Fix cargo fmt 2025-11-28 01:43:55 +01:00
louis-e
2b40a520ff Reintroduce initial Sutherland-Hodgman clipping 2025-11-28 01:43:16 +01:00
louis-e
a192be981a Fix clippy 2025-11-26 15:02:12 +01:00
louis-e
eb77bca10d Refactor code comments 2025-11-26 14:49:41 +01:00
louis-e
4a891c3603 Clean up code 2025-11-26 14:40:36 +01:00
louis-e
84adfdd931 Remove floodfill abort artifacts 2025-11-26 14:32:24 +01:00
louis-e
823b6ba052 Remove debugging changes 2025-11-26 14:22:04 +01:00
louis-e
2ba8157ec9 Remove debug logging 2025-11-26 14:15:20 +01:00
louis-e
7235ba0be9 Merge remote changes, keeping our Sutherland-Hodgman implementation 2025-11-26 13:53:53 +01:00
louis-e
dee580c564 fix: cargo fmt 2025-11-26 13:48:32 +01:00
louis-e
41fc5662e0 fix: restore performance with Sutherland-Hodgman clipping and correct water rendering
- Fix O(n*m) performance regression in highway processing by building connectivity map once
- Store unclipped ways in ways_map for proper relation member merging (merge_loopy_loops)
- Use clipped ways for standalone way processing
- Add empty vector guard in merge_loopy_loops to prevent panic
- Expose build_highway_connectivity_map as public API
- Add debug_logging module for development diagnostics
2025-11-26 13:39:29 +01:00
louis-e
ac884b8c2a Correctly clip multipolygons 2025-11-23 18:34:10 +01:00
louis-e
7a9b792bee Restore node filtering for performance without breaking water features 2025-11-23 17:34:08 +01:00
Louis Erbkamm
83e9a634e5 Update baseline time and memory in benchmark workflow 2025-11-22 14:45:51 +01:00
51 changed files with 6336 additions and 1450 deletions

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
@@ -65,7 +65,7 @@ jobs:
seconds=$((duration % 60))
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
baseline_time=135
baseline_time=30
diff=$((duration - baseline_time))
abs_diff=${diff#-}
@@ -79,7 +79,7 @@ jobs:
verdict="🚨 This PR **drastically worsens generation time**."
fi
baseline_mem=5865
baseline_mem=935
mem_annotation=""
if [ "$peak_mem" -gt 2000 ]; then
mem_diff=$((peak_mem - baseline_mem))
@@ -87,6 +87,8 @@ jobs:
mem_annotation=" (↗ ${mem_percent}% more)"
fi
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
{
echo "summary<<EOF"
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
@@ -98,6 +100,8 @@ jobs:
echo ""
echo "${verdict}"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time}"
echo ""
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -108,4 +112,4 @@ jobs:
message: ${{ steps.comment_body.outputs.summary }}
comment-tag: benchmark-report
env:
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}

View File

@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
@@ -124,7 +124,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download Windows build artifact
uses: actions/download-artifact@v5

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/wiki
*.mcworld
# Environment files
.env

346
Cargo.lock generated
View File

@@ -25,7 +25,7 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"once_cell",
"version_check",
"zerocopy",
@@ -182,8 +182,12 @@ dependencies = [
[[package]]
name = "arnis"
version = "2.3.1"
version = "2.4.0"
dependencies = [
"base64 0.22.1",
"bedrockrs_level",
"bedrockrs_shared",
"byteorder",
"clap",
"colored",
"dirs",
@@ -197,6 +201,7 @@ dependencies = [
"indicatif",
"itertools 0.14.0",
"log",
"nbtx",
"once_cell",
"rand 0.8.5",
"rayon",
@@ -211,7 +216,9 @@ dependencies = [
"tauri-plugin-shell",
"tempfile",
"tokio",
"vek",
"windows",
"zip",
]
[[package]]
@@ -297,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [
"async-lock",
"cfg-if",
"cfg-if 1.0.0",
"concurrent-queue",
"futures-io",
"futures-lite",
@@ -343,7 +350,7 @@ dependencies = [
"async-signal",
"async-task",
"blocking",
"cfg-if",
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
"rustix 0.38.42",
@@ -370,7 +377,7 @@ dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"cfg-if 1.0.0",
"futures-core",
"futures-io",
"rustix 0.38.42",
@@ -466,6 +473,73 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bedrockrs_core"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
[[package]]
name = "bedrockrs_level"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
dependencies = [
"bedrockrs_core",
"bedrockrs_shared",
"bytemuck",
"byteorder",
"concat-idents",
"len-trait",
"miniz_oxide",
"nbtx",
"rusty-leveldb",
"serde",
"thiserror 1.0.69",
"uuid",
"vek",
]
[[package]]
name = "bedrockrs_macros"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.95",
]
[[package]]
name = "bedrockrs_proto_core"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
dependencies = [
"base64 0.22.1",
"bedrockrs_macros",
"byteorder",
"jsonwebtoken",
"nbtx",
"paste",
"seq-macro",
"serde_json",
"thiserror 2.0.9",
"uuid",
"varint-rs",
"vek",
"xuid",
]
[[package]]
name = "bedrockrs_shared"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/bedrock-rs#7ef268b0a18373cf413ff53af99f85b3a327b8ec"
dependencies = [
"bedrockrs_macros",
"bedrockrs_proto_core",
"byteorder",
"log",
"varint-rs",
]
[[package]]
name = "bit_field"
version = "0.10.2"
@@ -636,9 +710,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.21.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
[[package]]
name = "byteorder"
@@ -766,6 +840,12 @@ dependencies = [
"target-lexicon",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -862,6 +942,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "concat-idents"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d"
dependencies = [
"quote",
"syn 2.0.95",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -959,13 +1049,28 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@@ -1141,7 +1246,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1295,7 +1400,7 @@ version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@@ -1351,6 +1456,17 @@ dependencies = [
"typeid",
]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno"
version = "0.3.10"
@@ -1361,6 +1477,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "event-listener"
version = "5.3.1"
@@ -1469,7 +1595,7 @@ version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"libredox",
"windows-sys 0.59.0",
@@ -1824,7 +1950,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
@@ -1835,9 +1961,11 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1846,7 +1974,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
@@ -2035,7 +2163,7 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"crunchy",
]
@@ -2549,6 +2677,12 @@ dependencies = [
"cfb",
]
[[package]]
name = "integer-encoding"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
[[package]]
name = "interpolate_name"
version = "0.2.4"
@@ -2654,7 +2788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"cfg-if 1.0.0",
"combine",
"jni-sys",
"log",
@@ -2716,6 +2850,21 @@ dependencies = [
"serde_json",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -2751,6 +2900,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "len-trait"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723558ab8acaa07cb831b424cd164b587ddc1648b34748a30953c404e9a4a65b"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -2797,7 +2955,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"winapi",
]
@@ -2923,7 +3081,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"rayon",
]
@@ -3013,6 +3171,18 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nbtx"
version = "0.1.0"
source = "git+https://github.com/bedrock-crustaceans/nbtx#551c38ac74f2e68a07d3dbdd354faac0c0ac966e"
dependencies = [
"byteorder",
"paste",
"serde",
"thiserror 1.0.69",
"varint-rs",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -3056,7 +3226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg-if 1.0.0",
"cfg_aliases",
"libc",
"memoffset",
@@ -3482,7 +3652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg-if 1.0.0",
"foreign-types 0.3.2",
"libc",
"once_cell",
@@ -3592,7 +3762,7 @@ version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
@@ -3611,6 +3781,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64 0.22.1",
"serde_core",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -3812,7 +3992,7 @@ version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
@@ -4136,7 +4316,7 @@ dependencies = [
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"cfg-if 1.0.0",
"interpolate_name",
"itertools 0.12.1",
"libc",
@@ -4342,7 +4522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"cfg-if 1.0.0",
"getrandom 0.2.15",
"libc",
"untrusted",
@@ -4427,7 +4607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.6.0",
"errno",
"errno 0.3.10",
"libc",
"linux-raw-sys 0.4.14",
"windows-sys 0.59.0",
@@ -4440,7 +4620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.6.0",
"errno",
"errno 0.3.10",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
@@ -4491,6 +4671,20 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "rusty-leveldb"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48d2f060dd1286adc9c3d179cb5af1292a9d2fcf291abcfe056023fc1977b44"
dependencies = [
"crc",
"errno 0.2.8",
"fs2",
"integer-encoding",
"rand 0.8.5",
"snap",
]
[[package]]
name = "ryu"
version = "1.0.18"
@@ -4603,19 +4797,27 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde"
version = "1.0.217"
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
@@ -4640,10 +4842,19 @@ dependencies = [
]
[[package]]
name = "serde_derive"
version = "1.0.217"
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -4782,7 +4993,7 @@ version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"cpufeatures",
"digest",
]
@@ -4833,6 +5044,18 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.9",
"time",
]
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -4854,6 +5077,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "snap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]]
name = "socket2"
version = "0.5.8"
@@ -5430,7 +5659,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5801,7 +6030,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"static_assertions",
]
@@ -5980,12 +6209,31 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
[[package]]
name = "varint-rs"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vek"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25215c4675beead435b254fc510932ff7f519cbc585b1b9fe2539ee9f20ca331"
dependencies = [
"approx",
"num-integer",
"num-traits",
"rustc_version",
"serde",
]
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -6064,7 +6312,7 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
@@ -6090,7 +6338,7 @@ version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -6750,7 +6998,7 @@ version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"windows-sys 0.59.0",
]
@@ -6860,6 +7108,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "xuid"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cc57e8e1819a2c06319a1387a6f1b0f8148a0221d17694a43ae63b60f407f0"
[[package]]
name = "yoke"
version = "0.7.5"
@@ -7017,6 +7271,18 @@ dependencies = [
"syn 2.0.95",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
"flate2",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View File

@@ -1,6 +1,6 @@
[package]
name = "arnis"
version = "2.3.1"
version = "2.4.0"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -14,12 +14,15 @@ overflow-checks = true
[features]
default = ["gui"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
[build-dependencies]
tauri-build = {version = "2", optional = true}
[dependencies]
base64 = "0.22.1"
byteorder = { version = "1.5", optional = true }
clap = { version = "4.5", features = ["derive", "env"] }
colored = "3.0.0"
dirs = {version = "6.0.0", optional = true }
@@ -38,13 +41,18 @@ rand = "0.8.5"
rayon = "1.10.0"
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
rfd = { version = "0.15.4", optional = true }
semver = "1.0.26"
semver = "1.0.27"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2", optional = true }
tauri-plugin-log = { version = "2.6.0", optional = true }
tauri-plugin-shell = { version = "2", optional = true }
tokio = { version = "1.48.0", features = ["full"], optional = true }
bedrockrs_level = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_level", optional = true }
bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_shared", optional = true }
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
vek = { version = "0.17", optional = true }
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.1", features = ["Win32_System_Console"] }

View File

@@ -2,7 +2,7 @@
# Arnis [![CI Build Status](https://github.com/louis-e/arnis/actions/workflows/ci-build.yml/badge.svg)](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [![Download here](https://img.shields.io/badge/Download-here-green)](https://github.com/louis-e/arnis/releases) [![Discord](https://img.shields.io/discord/1326192999738249267?label=Discord&color=%237289da)](https://discord.gg/mA2g69Fhxq)
Arnis creates complex and accurate Minecraft Java Edition worlds that reflect real-world geography, topography, and architecture.
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
Generate your hometown, big cities, and natural landscapes with ease!

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

849
src/bedrock_block_map.rs Normal file
View File

@@ -0,0 +1,849 @@
//! Bedrock Block Mapping
//!
//! This module provides translation between the internal Block representation
//! and Bedrock Edition block format. Bedrock uses string identifiers with
//! state properties that differ slightly from Java Edition.
use crate::block_definitions::Block;
use std::collections::HashMap;
/// Represents a Bedrock block with its identifier and state properties.
#[derive(Debug, Clone)]
pub struct BedrockBlock {
/// The Bedrock block identifier (e.g., "minecraft:stone")
pub name: String,
/// Block state properties as key-value pairs
pub states: HashMap<String, BedrockBlockStateValue>,
}
/// Bedrock block state values can be strings, booleans, or integers.
#[derive(Debug, Clone)]
pub enum BedrockBlockStateValue {
String(String),
Bool(bool),
Int(i32),
}
impl BedrockBlock {
/// Creates a simple block with no state properties.
pub fn simple(name: &str) -> Self {
Self {
name: format!("minecraft:{name}"),
states: HashMap::new(),
}
}
/// Creates a block with state properties.
pub fn with_states(name: &str, states: Vec<(&str, BedrockBlockStateValue)>) -> Self {
let mut state_map = HashMap::new();
for (key, value) in states {
state_map.insert(key.to_string(), value);
}
Self {
name: format!("minecraft:{name}"),
states: state_map,
}
}
}
/// Converts an internal Block to a BedrockBlock representation.
///
/// This function handles the mapping between Java Edition block names/properties
/// and their Bedrock Edition equivalents. Many blocks are identical, but some
/// require translation of property names or values.
pub fn to_bedrock_block(block: Block) -> BedrockBlock {
let java_name = block.name();
// Most blocks have the same name in both editions
// Handle special cases first, then fall back to direct mapping
match java_name {
// Grass block is just "grass_block" in both editions
"grass_block" => BedrockBlock::simple("grass_block"),
// Short grass is just "short_grass" in Java but "tallgrass" in Bedrock
"short_grass" => BedrockBlock::with_states(
"tallgrass",
vec![(
"tall_grass_type",
BedrockBlockStateValue::String("tall".to_string()),
)],
),
// Tall grass needs height state
"tall_grass" => BedrockBlock::with_states(
"double_plant",
vec![(
"double_plant_type",
BedrockBlockStateValue::String("grass".to_string()),
)],
),
// Oak leaves with persistence
"oak_leaves" => BedrockBlock::with_states(
"leaves",
vec![
(
"old_leaf_type",
BedrockBlockStateValue::String("oak".to_string()),
),
("persistent_bit", BedrockBlockStateValue::Bool(true)),
],
),
// Birch leaves with persistence
"birch_leaves" => BedrockBlock::with_states(
"leaves",
vec![
(
"old_leaf_type",
BedrockBlockStateValue::String("birch".to_string()),
),
("persistent_bit", BedrockBlockStateValue::Bool(true)),
],
),
// Oak log with axis (default up_down)
"oak_log" => BedrockBlock::with_states(
"oak_log",
vec![(
"pillar_axis",
BedrockBlockStateValue::String("y".to_string()),
)],
),
// Birch log with axis
"birch_log" => BedrockBlock::with_states(
"birch_log",
vec![(
"pillar_axis",
BedrockBlockStateValue::String("y".to_string()),
)],
),
// Spruce log with axis
"spruce_log" => BedrockBlock::with_states(
"spruce_log",
vec![(
"pillar_axis",
BedrockBlockStateValue::String("y".to_string()),
)],
),
// Stone slab (bottom half by default)
"stone_slab" => BedrockBlock::with_states(
"stone_block_slab",
vec![
(
"stone_slab_type",
BedrockBlockStateValue::String("smooth_stone".to_string()),
),
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
],
),
// Stone brick slab
"stone_brick_slab" => BedrockBlock::with_states(
"stone_block_slab",
vec![
(
"stone_slab_type",
BedrockBlockStateValue::String("stone_brick".to_string()),
),
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
],
),
// Oak slab
"oak_slab" => BedrockBlock::with_states(
"wooden_slab",
vec![
(
"wood_type",
BedrockBlockStateValue::String("oak".to_string()),
),
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
],
),
// Water (flowing by default)
"water" => BedrockBlock::with_states(
"water",
vec![("liquid_depth", BedrockBlockStateValue::Int(0))],
),
// Rail with shape state
"rail" => BedrockBlock::with_states(
"rail",
vec![("rail_direction", BedrockBlockStateValue::Int(0))],
),
// Farmland with moisture
"farmland" => BedrockBlock::with_states(
"farmland",
vec![("moisturized_amount", BedrockBlockStateValue::Int(7))],
),
// Snow layer
"snow" => BedrockBlock::with_states(
"snow_layer",
vec![("height", BedrockBlockStateValue::Int(0))],
),
// Cobblestone wall
"cobblestone_wall" => BedrockBlock::with_states(
"cobblestone_wall",
vec![(
"wall_block_type",
BedrockBlockStateValue::String("cobblestone".to_string()),
)],
),
// Andesite wall
"andesite_wall" => BedrockBlock::with_states(
"cobblestone_wall",
vec![(
"wall_block_type",
BedrockBlockStateValue::String("andesite".to_string()),
)],
),
// Stone brick wall
"stone_brick_wall" => BedrockBlock::with_states(
"cobblestone_wall",
vec![(
"wall_block_type",
BedrockBlockStateValue::String("stone_brick".to_string()),
)],
),
// Flowers - poppy is just "red_flower" in Bedrock
"poppy" => BedrockBlock::with_states(
"red_flower",
vec![(
"flower_type",
BedrockBlockStateValue::String("poppy".to_string()),
)],
),
// Dandelion is "yellow_flower" in Bedrock
"dandelion" => BedrockBlock::simple("yellow_flower"),
// Blue orchid
"blue_orchid" => BedrockBlock::with_states(
"red_flower",
vec![(
"flower_type",
BedrockBlockStateValue::String("orchid".to_string()),
)],
),
// Azure bluet
"azure_bluet" => BedrockBlock::with_states(
"red_flower",
vec![(
"flower_type",
BedrockBlockStateValue::String("houstonia".to_string()),
)],
),
// Concrete colors (Bedrock uses a single block with color state)
"white_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
),
"black_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
),
"gray_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
),
"light_gray_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("silver".to_string()),
)],
),
"light_blue_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("light_blue".to_string()),
)],
),
"cyan_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
),
"blue_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
),
"purple_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("purple".to_string()),
)],
),
"magenta_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("magenta".to_string()),
)],
),
"red_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
),
"orange_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("orange".to_string()),
)],
),
"yellow_concrete" => BedrockBlock::with_states(
"concrete",
vec![(
"color",
BedrockBlockStateValue::String("yellow".to_string()),
)],
),
"lime_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("lime".to_string()))],
),
"brown_concrete" => BedrockBlock::with_states(
"concrete",
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
),
// Terracotta colors
"white_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
),
"orange_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![(
"color",
BedrockBlockStateValue::String("orange".to_string()),
)],
),
"yellow_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![(
"color",
BedrockBlockStateValue::String("yellow".to_string()),
)],
),
"light_blue_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![(
"color",
BedrockBlockStateValue::String("light_blue".to_string()),
)],
),
"blue_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
),
"gray_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
),
"green_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
),
"red_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
),
"brown_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
),
"black_terracotta" => BedrockBlock::with_states(
"stained_hardened_clay",
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
),
// Plain terracotta
"terracotta" => BedrockBlock::simple("hardened_clay"),
// Wool colors
"white_wool" => BedrockBlock::with_states(
"wool",
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
),
"red_wool" => BedrockBlock::with_states(
"wool",
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
),
"green_wool" => BedrockBlock::with_states(
"wool",
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
),
"brown_wool" => BedrockBlock::with_states(
"wool",
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
),
"cyan_wool" => BedrockBlock::with_states(
"wool",
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
),
"yellow_wool" => BedrockBlock::with_states(
"wool",
vec![(
"color",
BedrockBlockStateValue::String("yellow".to_string()),
)],
),
// Carpets
"white_carpet" => BedrockBlock::with_states(
"carpet",
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
),
"red_carpet" => BedrockBlock::with_states(
"carpet",
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
),
// Stained glass
"white_stained_glass" => BedrockBlock::with_states(
"stained_glass",
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
),
"gray_stained_glass" => BedrockBlock::with_states(
"stained_glass",
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
),
"light_gray_stained_glass" => BedrockBlock::with_states(
"stained_glass",
vec![(
"color",
BedrockBlockStateValue::String("silver".to_string()),
)],
),
"brown_stained_glass" => BedrockBlock::with_states(
"stained_glass",
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
),
// Planks - Bedrock uses single "planks" block with wood_type state
"oak_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("oak".to_string()),
)],
),
"spruce_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("spruce".to_string()),
)],
),
"birch_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("birch".to_string()),
)],
),
"jungle_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("jungle".to_string()),
)],
),
"acacia_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("acacia".to_string()),
)],
),
"dark_oak_planks" => BedrockBlock::with_states(
"planks",
vec![(
"wood_type",
BedrockBlockStateValue::String("dark_oak".to_string()),
)],
),
"crimson_planks" => BedrockBlock::simple("crimson_planks"),
"warped_planks" => BedrockBlock::simple("warped_planks"),
// Stone variants
"stone" => BedrockBlock::simple("stone"),
"granite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("granite".to_string()),
)],
),
"polished_granite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("granite_smooth".to_string()),
)],
),
"diorite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("diorite".to_string()),
)],
),
"polished_diorite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("diorite_smooth".to_string()),
)],
),
"andesite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("andesite".to_string()),
)],
),
"polished_andesite" => BedrockBlock::with_states(
"stone",
vec![(
"stone_type",
BedrockBlockStateValue::String("andesite_smooth".to_string()),
)],
),
// Blocks with different names in Bedrock
"bricks" => BedrockBlock::simple("brick_block"),
"end_stone_bricks" => BedrockBlock::simple("end_bricks"),
"nether_bricks" => BedrockBlock::simple("nether_brick"),
"red_nether_bricks" => BedrockBlock::simple("red_nether_brick"),
"snow_block" => BedrockBlock::simple("snow"),
"dirt_path" => BedrockBlock::simple("grass_path"),
"dead_bush" => BedrockBlock::simple("deadbush"),
"note_block" => BedrockBlock::simple("noteblock"),
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
"oak_door" => BedrockBlock::simple("wooden_door"),
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
// Bed (Bedrock uses single "bed" block with color state)
"red_bed" => BedrockBlock::with_states(
"bed",
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
),
// Default: use the same name (works for many blocks)
// Log unmapped blocks to help identify missing mappings
_ => BedrockBlock::simple(java_name),
}
}
/// Converts an internal Block with optional Java properties to a BedrockBlock.
///
/// This function extends `to_bedrock_block` by also handling block-specific properties
/// like stair facing/shape, slab type, etc. Java property names and values are converted
/// to their Bedrock equivalents.
pub fn to_bedrock_block_with_properties(
block: Block,
java_properties: Option<&fastnbt::Value>,
) -> BedrockBlock {
let java_name = block.name();
// Extract Java properties as a map if present
let props_map = java_properties.and_then(|v| {
if let fastnbt::Value::Compound(map) = v {
Some(map)
} else {
None
}
});
// Handle stairs with facing/shape properties
if java_name.ends_with("_stairs") {
return convert_stairs(java_name, props_map);
}
// Handle slabs with type property (top/bottom/double)
if java_name.ends_with("_slab") {
return convert_slab(java_name, props_map);
}
// Handle logs with axis property
if java_name.ends_with("_log") || java_name.ends_with("_wood") {
return convert_log(java_name, props_map);
}
// Fall back to basic conversion without properties
to_bedrock_block(block)
}
/// Convert Java stair block to Bedrock format with proper orientation.
fn convert_stairs(
java_name: &str,
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
) -> BedrockBlock {
// Map Java stair names to Bedrock equivalents
let bedrock_name = match java_name {
"end_stone_brick_stairs" => "end_brick_stairs",
_ => java_name, // Most stairs have the same name
};
let mut states = HashMap::new();
// Convert facing: Java uses "north/south/east/west", Bedrock uses "weirdo_direction" (0-3)
// Bedrock: 0=east, 1=west, 2=south, 3=north
if let Some(props) = props {
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
let direction = match facing.as_str() {
"east" => 0,
"west" => 1,
"south" => 2,
"north" => 3,
_ => 0,
};
states.insert(
"weirdo_direction".to_string(),
BedrockBlockStateValue::Int(direction),
);
}
// Convert half: Java uses "top/bottom", Bedrock uses "upside_down_bit"
if let Some(fastnbt::Value::String(half)) = props.get("half") {
let upside_down = half == "top";
states.insert(
"upside_down_bit".to_string(),
BedrockBlockStateValue::Bool(upside_down),
);
}
}
// If no properties were set, use defaults
if states.is_empty() {
states.insert(
"weirdo_direction".to_string(),
BedrockBlockStateValue::Int(0),
);
states.insert(
"upside_down_bit".to_string(),
BedrockBlockStateValue::Bool(false),
);
}
BedrockBlock {
name: format!("minecraft:{bedrock_name}"),
states,
}
}
/// Convert Java slab block to Bedrock format with proper type.
fn convert_slab(
java_name: &str,
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
) -> BedrockBlock {
let mut states = HashMap::new();
// Convert type: Java uses "top/bottom/double", Bedrock uses "top_slot_bit"
if let Some(props) = props {
if let Some(fastnbt::Value::String(slab_type)) = props.get("type") {
let top_slot = slab_type == "top";
states.insert(
"top_slot_bit".to_string(),
BedrockBlockStateValue::Bool(top_slot),
);
// Note: "double" slabs in Java become full blocks in Bedrock (different block ID)
}
}
// Default to bottom if not specified
if !states.contains_key("top_slot_bit") {
states.insert(
"top_slot_bit".to_string(),
BedrockBlockStateValue::Bool(false),
);
}
// Handle special slab name mappings (same as in to_bedrock_block)
let bedrock_name = match java_name {
"stone_slab" => "stone_block_slab",
"stone_brick_slab" => "stone_block_slab",
"oak_slab" => "wooden_slab",
"spruce_slab" => "wooden_slab",
"birch_slab" => "wooden_slab",
"jungle_slab" => "wooden_slab",
"acacia_slab" => "wooden_slab",
"dark_oak_slab" => "wooden_slab",
_ => java_name,
};
// Add wood_type for wooden slabs
if bedrock_name == "wooden_slab" {
let wood_type = java_name.trim_end_matches("_slab");
states.insert(
"wood_type".to_string(),
BedrockBlockStateValue::String(wood_type.to_string()),
);
}
// Add stone_slab_type for stone slabs
if bedrock_name == "stone_block_slab" {
let slab_type = if java_name == "stone_brick_slab" {
"stone_brick"
} else {
"stone"
};
states.insert(
"stone_slab_type".to_string(),
BedrockBlockStateValue::String(slab_type.to_string()),
);
}
BedrockBlock {
name: format!("minecraft:{bedrock_name}"),
states,
}
}
/// Convert Java log/wood block to Bedrock format with proper axis.
fn convert_log(
java_name: &str,
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
) -> BedrockBlock {
let bedrock_name = java_name;
let mut states = HashMap::new();
// Convert axis: Java uses "x/y/z", Bedrock uses "pillar_axis"
if let Some(props) = props {
if let Some(fastnbt::Value::String(axis)) = props.get("axis") {
states.insert(
"pillar_axis".to_string(),
BedrockBlockStateValue::String(axis.clone()),
);
}
}
// Default to y-axis if not specified
if states.is_empty() {
states.insert(
"pillar_axis".to_string(),
BedrockBlockStateValue::String("y".to_string()),
);
}
BedrockBlock {
name: format!("minecraft:{bedrock_name}"),
states,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_definitions::{AIR, GRASS_BLOCK, STONE};
#[test]
fn test_simple_blocks() {
let bedrock = to_bedrock_block(STONE);
assert_eq!(bedrock.name, "minecraft:stone");
assert!(bedrock.states.is_empty());
let bedrock = to_bedrock_block(AIR);
assert_eq!(bedrock.name, "minecraft:air");
}
#[test]
fn test_grass_block() {
let bedrock = to_bedrock_block(GRASS_BLOCK);
assert_eq!(bedrock.name, "minecraft:grass_block");
}
#[test]
fn test_colored_blocks() {
use crate::block_definitions::WHITE_CONCRETE;
let bedrock = to_bedrock_block(WHITE_CONCRETE);
assert_eq!(bedrock.name, "minecraft:concrete");
assert!(matches!(
bedrock.states.get("color"),
Some(BedrockBlockStateValue::String(s)) if s == "white"
));
}
#[test]
fn test_stairs_with_properties() {
use crate::block_definitions::OAK_STAIRS;
use std::collections::HashMap as StdHashMap;
// Create Java properties for a south-facing stair
let mut props = StdHashMap::new();
props.insert(
"facing".to_string(),
fastnbt::Value::String("south".to_string()),
);
props.insert(
"half".to_string(),
fastnbt::Value::String("bottom".to_string()),
);
let java_props = fastnbt::Value::Compound(props);
let bedrock = to_bedrock_block_with_properties(OAK_STAIRS, Some(&java_props));
assert_eq!(bedrock.name, "minecraft:oak_stairs");
// Check weirdo_direction is set correctly (south = 2)
assert!(matches!(
bedrock.states.get("weirdo_direction"),
Some(BedrockBlockStateValue::Int(2))
));
// Check upside_down_bit is false for bottom half
assert!(matches!(
bedrock.states.get("upside_down_bit"),
Some(BedrockBlockStateValue::Bool(false))
));
}
#[test]
fn test_stairs_upside_down() {
use crate::block_definitions::STONE_BRICK_STAIRS;
use std::collections::HashMap as StdHashMap;
// Create Java properties for an upside-down north-facing stair
let mut props = StdHashMap::new();
props.insert(
"facing".to_string(),
fastnbt::Value::String("north".to_string()),
);
props.insert(
"half".to_string(),
fastnbt::Value::String("top".to_string()),
);
let java_props = fastnbt::Value::Compound(props);
let bedrock = to_bedrock_block_with_properties(STONE_BRICK_STAIRS, Some(&java_props));
// Check weirdo_direction is set correctly (north = 3)
assert!(matches!(
bedrock.states.get("weirdo_direction"),
Some(BedrockBlockStateValue::Int(3))
));
// Check upside_down_bit is true for top half
assert!(matches!(
bedrock.states.get("upside_down_bit"),
Some(BedrockBlockStateValue::Bool(true))
));
}
}

706
src/clipping.rs Normal file
View File

@@ -0,0 +1,706 @@
// Sutherland-Hodgman polygon clipping and related geometry utilities.
//
// Provides bbox clipping for polygons, polylines, and water rings with
// proper corner insertion for closed shapes.
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
use crate::osm_parser::ProcessedNode;
use std::collections::HashMap;
/// Clips a way to the bounding box using Sutherland-Hodgman for polygons or
/// simple line clipping for polylines. Preserves endpoint IDs for ring assembly.
pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
if nodes.is_empty() {
return Vec::new();
}
let is_closed = is_closed_polygon(nodes);
if !is_closed {
return clip_polyline_to_bbox(nodes, xzbbox);
}
// If all nodes are inside the bbox, return unchanged
let has_nodes_outside = nodes
.iter()
.any(|node| !xzbbox.contains(&XZPoint::new(node.x, node.z)));
if !has_nodes_outside {
return nodes.to_vec();
}
let min_x = xzbbox.min_x() as f64;
let min_z = xzbbox.min_z() as f64;
let max_x = xzbbox.max_x() as f64;
let max_z = xzbbox.max_z() as f64;
let mut polygon: Vec<(f64, f64)> = nodes.iter().map(|n| (n.x as f64, n.z as f64)).collect();
polygon = clip_polygon_sutherland_hodgman(polygon, min_x, min_z, max_x, max_z);
if polygon.len() < 3 {
return Vec::new();
}
// Final clamping for floating-point errors
for p in &mut polygon {
p.0 = p.0.clamp(min_x, max_x);
p.1 = p.1.clamp(min_z, max_z);
}
let polygon = remove_consecutive_duplicates(polygon);
if polygon.len() < 3 {
return Vec::new();
}
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
let polygon = remove_consecutive_duplicates(polygon);
if polygon.len() < 3 {
return Vec::new();
}
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
}
/// Clips a water polygon ring to bbox using Sutherland-Hodgman (post-ring-merge).
pub fn clip_water_ring_to_bbox(
ring: &[ProcessedNode],
xzbbox: &XZBBox,
) -> Option<Vec<ProcessedNode>> {
if ring.is_empty() {
return None;
}
let min_x = xzbbox.min_x() as f64;
let min_z = xzbbox.min_z() as f64;
let max_x = xzbbox.max_x() as f64;
let max_z = xzbbox.max_z() as f64;
// Check if entire ring is inside bbox
let all_inside = ring.iter().all(|n| {
n.x as f64 >= min_x && n.x as f64 <= max_x && n.z as f64 >= min_z && n.z as f64 <= max_z
});
if all_inside {
return Some(ring.to_vec());
}
// Check if entire ring is outside bbox
if is_ring_outside_bbox(ring, min_x, min_z, max_x, max_z) {
return None;
}
// Convert to f64 coordinates and ensure closed
let mut polygon: Vec<(f64, f64)> = ring.iter().map(|n| (n.x as f64, n.z as f64)).collect();
if !polygon.is_empty() && polygon.first() != polygon.last() {
polygon.push(polygon[0]);
}
// Clip with full-range clamping (water uses simpler approach)
polygon = clip_polygon_sutherland_hodgman_simple(polygon, min_x, min_z, max_x, max_z);
if polygon.len() < 3 {
return None;
}
// Verify all points are within bbox
let all_points_inside = polygon
.iter()
.all(|&(x, z)| x >= min_x && x <= max_x && z >= min_z && z <= max_z);
if !all_points_inside {
eprintln!("ERROR: clip_water_ring_to_bbox produced points outside bbox!");
return None;
}
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
if polygon.len() < 3 {
return None;
}
// Convert back to ProcessedNode with synthetic IDs
let mut result: Vec<ProcessedNode> = polygon
.iter()
.enumerate()
.map(|(i, &(x, z))| ProcessedNode {
id: 1_000_000_000 + i as u64,
tags: HashMap::new(),
x: x.clamp(min_x, max_x).round() as i32,
z: z.clamp(min_z, max_z).round() as i32,
})
.collect();
// Close the loop by matching first and last ID
if !result.is_empty() {
let first_id = result[0].id;
result.last_mut().unwrap().id = first_id;
}
Some(result)
}
// ============================================================================
// Internal helpers
// ============================================================================
/// Checks if a way forms a closed polygon.
fn is_closed_polygon(nodes: &[ProcessedNode]) -> bool {
if nodes.len() < 3 {
return false;
}
let first = nodes.first().unwrap();
let last = nodes.last().unwrap();
first.id == last.id || (first.x == last.x && first.z == last.z)
}
/// Checks if an entire ring is outside the bbox.
fn is_ring_outside_bbox(
ring: &[ProcessedNode],
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> bool {
let all_left = ring.iter().all(|n| (n.x as f64) < min_x);
let all_right = ring.iter().all(|n| (n.x as f64) > max_x);
let all_top = ring.iter().all(|n| (n.z as f64) < min_z);
let all_bottom = ring.iter().all(|n| (n.z as f64) > max_z);
all_left || all_right || all_top || all_bottom
}
/// Clips a polyline (open path) to the bounding box.
fn clip_polyline_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
if nodes.is_empty() {
return Vec::new();
}
let min_x = xzbbox.min_x() as f64;
let min_z = xzbbox.min_z() as f64;
let max_x = xzbbox.max_x() as f64;
let max_z = xzbbox.max_z() as f64;
let mut result = Vec::new();
for i in 0..nodes.len() {
let current = &nodes[i];
let current_point = (current.x as f64, current.z as f64);
let current_inside = point_in_bbox(current_point, min_x, min_z, max_x, max_z);
if current_inside {
result.push(current.clone());
}
if i + 1 < nodes.len() {
let next = &nodes[i + 1];
let next_point = (next.x as f64, next.z as f64);
let next_inside = point_in_bbox(next_point, min_x, min_z, max_x, max_z);
if current_inside != next_inside {
// One endpoint inside, one outside, find single intersection
let intersections =
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
for intersection in intersections {
let synthetic_id = nodes[0]
.id
.wrapping_mul(10000000)
.wrapping_add(result.len() as u64);
result.push(ProcessedNode {
id: synthetic_id,
x: intersection.0.round() as i32,
z: intersection.1.round() as i32,
tags: HashMap::new(),
});
}
} else if !current_inside && !next_inside {
// Both endpoints outside, segment might still cross through bbox
let mut intersections =
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
if intersections.len() >= 2 {
// Sort intersections by distance from current point
intersections.sort_by(|a, b| {
let dist_a =
(a.0 - current_point.0).powi(2) + (a.1 - current_point.1).powi(2);
let dist_b =
(b.0 - current_point.0).powi(2) + (b.1 - current_point.1).powi(2);
dist_a
.partial_cmp(&dist_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
for intersection in intersections {
let synthetic_id = nodes[0]
.id
.wrapping_mul(10000000)
.wrapping_add(result.len() as u64);
result.push(ProcessedNode {
id: synthetic_id,
x: intersection.0.round() as i32,
z: intersection.1.round() as i32,
tags: HashMap::new(),
});
}
}
}
}
}
// Preserve endpoint IDs where possible
if result.len() >= 2 {
let tolerance = 50.0;
if let Some(first_orig) = nodes.first() {
if matches_endpoint(
(result[0].x as f64, result[0].z as f64),
first_orig,
tolerance,
) {
result[0].id = first_orig.id;
}
}
if let Some(last_orig) = nodes.last() {
let last_idx = result.len() - 1;
if matches_endpoint(
(result[last_idx].x as f64, result[last_idx].z as f64),
last_orig,
tolerance,
) {
result[last_idx].id = last_orig.id;
}
}
}
result
}
/// Sutherland-Hodgman polygon clipping with edge-specific clamping.
fn clip_polygon_sutherland_hodgman(
mut polygon: Vec<(f64, f64)>,
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> Vec<(f64, f64)> {
// Edges: bottom, right, top, left (counter-clockwise traversal)
let bbox_edges = [
(min_x, min_z, max_x, min_z, 0), // Bottom: clamp z
(max_x, min_z, max_x, max_z, 1), // Right: clamp x
(max_x, max_z, min_x, max_z, 2), // Top: clamp z
(min_x, max_z, min_x, min_z, 3), // Left: clamp x
];
for (edge_x1, edge_z1, edge_x2, edge_z2, edge_idx) in bbox_edges {
if polygon.is_empty() {
break;
}
let mut clipped = Vec::new();
let is_closed = !polygon.is_empty() && polygon.first() == polygon.last();
let edge_count = if is_closed {
polygon.len().saturating_sub(1)
} else {
polygon.len()
};
for i in 0..edge_count {
let current = polygon[i];
let next = polygon.get(i + 1).copied().unwrap_or(polygon[0]);
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
if next_inside {
if !current_inside {
if let Some(mut intersection) = line_edge_intersection(
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
) {
// Clamp to current edge only
match edge_idx {
0 => intersection.1 = min_z,
1 => intersection.0 = max_x,
2 => intersection.1 = max_z,
3 => intersection.0 = min_x,
_ => {}
}
clipped.push(intersection);
}
}
clipped.push(next);
} else if current_inside {
if let Some(mut intersection) = line_edge_intersection(
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
) {
match edge_idx {
0 => intersection.1 = min_z,
1 => intersection.0 = max_x,
2 => intersection.1 = max_z,
3 => intersection.0 = min_x,
_ => {}
}
clipped.push(intersection);
}
}
}
polygon = clipped;
}
polygon
}
/// Sutherland-Hodgman with full bbox clamping (simpler, for water rings).
fn clip_polygon_sutherland_hodgman_simple(
mut polygon: Vec<(f64, f64)>,
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> Vec<(f64, f64)> {
let bbox_edges = [
(min_x, min_z, max_x, min_z),
(max_x, min_z, max_x, max_z),
(max_x, max_z, min_x, max_z),
(min_x, max_z, min_x, min_z),
];
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
if polygon.is_empty() {
break;
}
let mut clipped = Vec::new();
for i in 0..(polygon.len().saturating_sub(1)) {
let current = polygon[i];
let next = polygon[i + 1];
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
if next_inside {
if !current_inside {
if let Some(mut intersection) = line_edge_intersection(
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
) {
intersection.0 = intersection.0.clamp(min_x, max_x);
intersection.1 = intersection.1.clamp(min_z, max_z);
clipped.push(intersection);
}
}
clipped.push(next);
} else if current_inside {
if let Some(mut intersection) = line_edge_intersection(
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
) {
intersection.0 = intersection.0.clamp(min_x, max_x);
intersection.1 = intersection.1.clamp(min_z, max_z);
clipped.push(intersection);
}
}
}
polygon = clipped;
}
polygon
}
/// Checks if point is inside bbox.
fn point_in_bbox(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> bool {
point.0 >= min_x && point.0 <= max_x && point.1 >= min_z && point.1 <= max_z
}
/// Checks if point is on the "inside" side of an edge (cross product test).
fn point_inside_edge(
point: (f64, f64),
edge_x1: f64,
edge_z1: f64,
edge_x2: f64,
edge_z2: f64,
) -> bool {
let edge_dx = edge_x2 - edge_x1;
let edge_dz = edge_z2 - edge_z1;
let point_dx = point.0 - edge_x1;
let point_dz = point.1 - edge_z1;
(edge_dx * point_dz - edge_dz * point_dx) >= 0.0
}
/// Finds intersection between a line segment and an edge.
#[allow(clippy::too_many_arguments)]
fn line_edge_intersection(
line_x1: f64,
line_z1: f64,
line_x2: f64,
line_z2: f64,
edge_x1: f64,
edge_z1: f64,
edge_x2: f64,
edge_z2: f64,
) -> Option<(f64, f64)> {
let line_dx = line_x2 - line_x1;
let line_dz = line_z2 - line_z1;
let edge_dx = edge_x2 - edge_x1;
let edge_dz = edge_z2 - edge_z1;
let denom = line_dx * edge_dz - line_dz * edge_dx;
if denom.abs() < 1e-10 {
return None;
}
let dx = edge_x1 - line_x1;
let dz = edge_z1 - line_z1;
let t = (dx * edge_dz - dz * edge_dx) / denom;
if (0.0..=1.0).contains(&t) {
Some((line_x1 + t * line_dx, line_z1 + t * line_dz))
} else {
None
}
}
/// Finds intersections between a line segment and bbox edges.
fn find_bbox_intersections(
start: (f64, f64),
end: (f64, f64),
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> Vec<(f64, f64)> {
let mut intersections = Vec::new();
let bbox_edges = [
(min_x, min_z, max_x, min_z),
(max_x, min_z, max_x, max_z),
(max_x, max_z, min_x, max_z),
(min_x, max_z, min_x, min_z),
];
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
if let Some(intersection) = line_edge_intersection(
start.0, start.1, end.0, end.1, edge_x1, edge_z1, edge_x2, edge_z2,
) {
let on_edge = point_in_bbox(intersection, min_x, min_z, max_x, max_z)
&& ((intersection.0 == min_x || intersection.0 == max_x)
|| (intersection.1 == min_z || intersection.1 == max_z));
if on_edge {
intersections.push(intersection);
}
}
}
intersections
}
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
let eps = 0.5;
let on_left = (point.0 - min_x).abs() < eps;
let on_right = (point.0 - max_x).abs() < eps;
let on_bottom = (point.1 - min_z).abs() < eps;
let on_top = (point.1 - max_z).abs() < eps;
// Handle corners (assign to edge in counter-clockwise order)
if on_bottom && on_left {
return 3;
}
if on_bottom && on_right {
return 0;
}
if on_top && on_right {
return 1;
}
if on_top && on_left {
return 2;
}
if on_bottom {
return 0;
}
if on_right {
return 1;
}
if on_top {
return 2;
}
if on_left {
return 3;
}
-1
}
/// Returns corners to insert when traversing from edge1 to edge2 via shorter path.
fn get_corners_between_edges(
edge1: i32,
edge2: i32,
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> Vec<(f64, f64)> {
if edge1 == edge2 || edge1 < 0 || edge2 < 0 {
return Vec::new();
}
let corners = [
(max_x, min_z), // 0: bottom-right
(max_x, max_z), // 1: top-right
(min_x, max_z), // 2: top-left
(min_x, min_z), // 3: bottom-left
];
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
// Opposite edges: don't insert corners
if ccw_dist == 2 && cw_dist == 2 {
return Vec::new();
}
let mut result = Vec::new();
if ccw_dist <= cw_dist {
let mut current = edge1;
for _ in 0..ccw_dist {
result.push(corners[current as usize]);
current = (current + 1) % 4;
}
} else {
let mut current = edge1;
for _ in 0..cw_dist {
current = (current + 4 - 1) % 4;
result.push(corners[current as usize]);
}
}
result
}
/// Inserts bbox corners where polygon transitions between different bbox edges.
fn insert_bbox_corners(
polygon: Vec<(f64, f64)>,
min_x: f64,
min_z: f64,
max_x: f64,
max_z: f64,
) -> Vec<(f64, f64)> {
if polygon.len() < 3 {
return polygon;
}
let mut result = Vec::with_capacity(polygon.len() + 4);
for i in 0..polygon.len() {
let current = polygon[i];
let next = polygon[(i + 1) % polygon.len()];
result.push(current);
let edge1 = get_bbox_edge(current, min_x, min_z, max_x, max_z);
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
result.push(corner);
}
}
}
result
}
/// Removes consecutive duplicate points (within epsilon tolerance).
fn remove_consecutive_duplicates(polygon: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
if polygon.is_empty() {
return polygon;
}
let eps = 0.1;
let mut result: Vec<(f64, f64)> = Vec::with_capacity(polygon.len());
for p in &polygon {
if let Some(last) = result.last() {
if (p.0 - last.0).abs() < eps && (p.1 - last.1).abs() < eps {
continue;
}
}
result.push(*p);
}
// Check first/last duplicates for closed polygons
if result.len() > 1 {
let first = result.first().unwrap();
let last = result.last().unwrap();
if (first.0 - last.0).abs() < eps && (first.1 - last.1).abs() < eps {
result.pop();
}
}
result
}
/// Checks if a clipped coordinate matches an original endpoint.
fn matches_endpoint(coord: (f64, f64), endpoint: &ProcessedNode, tolerance: f64) -> bool {
let dx = (coord.0 - endpoint.x as f64).abs();
let dz = (coord.1 - endpoint.z as f64).abs();
dx * dx + dz * dz < tolerance * tolerance
}
/// Assigns node IDs to clipped coordinates, preserving original endpoint IDs.
fn assign_node_ids_preserving_endpoints(
original_nodes: &[ProcessedNode],
clipped_coords: Vec<(f64, f64)>,
way_id: u64,
) -> Vec<ProcessedNode> {
if clipped_coords.is_empty() {
return Vec::new();
}
let original_first = original_nodes.first();
let original_last = original_nodes.last();
let tolerance = 50.0;
let last_index = clipped_coords.len() - 1;
clipped_coords
.into_iter()
.enumerate()
.map(|(i, coord)| {
let is_first = i == 0;
let is_last = i == last_index;
if is_first || is_last {
if let Some(first) = original_first {
if matches_endpoint(coord, first, tolerance) {
return ProcessedNode {
id: first.id,
x: coord.0.round() as i32,
z: coord.1.round() as i32,
tags: HashMap::new(),
};
}
}
if let Some(last) = original_last {
if matches_endpoint(coord, last, tolerance) {
return ProcessedNode {
id: last.id,
x: coord.0.round() as i32,
z: coord.1.round() as i32,
tags: HashMap::new(),
};
}
}
}
ProcessedNode {
id: way_id.wrapping_mul(10000000).wrapping_add(i as u64),
x: coord.0.round() as i32,
z: coord.1.round() as i32,
tags: HashMap::new(),
}
})
.collect()
}

View File

@@ -4,16 +4,27 @@ use crate::coordinate_system::cartesian::XZBBox;
use crate::coordinate_system::geographic::LLBBox;
use crate::element_processing::*;
use crate::ground::Ground;
use crate::map_renderer;
use crate::osm_parser::ProcessedElement;
use crate::progress::emit_gui_progress_update;
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::world_editor::WorldEditor;
use crate::world_editor::{WorldEditor, WorldFormat};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;
pub const MIN_Y: i32 = -64;
/// Generation options that can be passed separately from CLI Args
#[derive(Clone)]
pub struct GenerationOptions {
pub path: PathBuf,
pub format: WorldFormat,
pub level_name: Option<String>,
pub spawn_point: Option<(i32, i32)>,
}
pub fn generate_world(
elements: Vec<ProcessedElement>,
xzbbox: XZBBox,
@@ -21,10 +32,41 @@ pub fn generate_world(
ground: Ground,
args: &Args,
) -> Result<(), String> {
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
// Default to Java format when called from CLI
let options = GenerationOptions {
path: args.path.clone(),
format: WorldFormat::JavaAnvil,
level_name: None,
spawn_point: None,
};
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options).map(|_| ())
}
/// Generate world with explicit format options (used by GUI for Bedrock support)
pub fn generate_world_with_options(
elements: Vec<ProcessedElement>,
xzbbox: XZBBox,
llbbox: LLBBox,
ground: Ground,
args: &Args,
options: GenerationOptions,
) -> Result<PathBuf, String> {
let output_path = options.path.clone();
let world_format = options.format;
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
options.path,
&xzbbox,
llbbox,
options.format,
options.level_name,
options.spawn_point,
);
println!("{} Processing data...", "[4/7]".bold());
// Build highway connectivity map once before processing
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);
@@ -66,7 +108,7 @@ pub fn generate_world(
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(&mut editor, way, args, None);
} else if way.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, args, &elements);
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(&mut editor, way, args);
} else if way.tags.contains_key("natural") {
@@ -80,7 +122,7 @@ pub fn generate_world(
} else if let Some(val) = way.tags.get("waterway") {
if val == "dock" {
// docks count as water areas
water_areas::generate_water_area_from_way(&mut editor, way);
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
} else {
waterways::generate_waterways(&mut editor, way);
}
@@ -111,7 +153,7 @@ pub fn generate_world(
} 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, &elements);
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
} else if node.tags.contains_key("tourism") {
tourisms::generate_tourisms(&mut editor, node);
} else if node.tags.contains_key("man_made") {
@@ -128,7 +170,7 @@ pub fn generate_world(
.map(|val| val == "water" || val == "bay")
.unwrap_or(false)
{
water_areas::generate_water_areas_from_relation(&mut editor, rel);
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") {
@@ -233,33 +275,109 @@ pub fn generate_world(
// 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 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()
);
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()
);
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);
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);
}
}
}
emit_gui_progress_update(100.0, "Done! World generation completed.");
println!("{}", "Done! World generation completed.".green().bold());
Ok(())
// For Bedrock format, emit event to open the mcworld file
if world_format == WorldFormat::BedrockMcWorld {
if let Some(path_str) = output_path.to_str() {
emit_open_mcworld_file(path_str);
}
}
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");
}
}
});
}

View File

@@ -7,19 +7,21 @@ use crate::osm_parser::{ProcessedElement, ProcessedWay};
use crate::world_editor::WorldEditor;
use std::collections::HashMap;
/// Type alias for highway connectivity map
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
/// Generates highways with elevation support based on layer tags and connectivity analysis
pub fn generate_highways(
editor: &mut WorldEditor,
element: &ProcessedElement,
args: &Args,
all_elements: &[ProcessedElement],
highway_connectivity: &HighwayConnectivityMap,
) {
let highway_connectivity = build_highway_connectivity_map(all_elements);
generate_highways_internal(editor, element, args, &highway_connectivity);
generate_highways_internal(editor, element, args, highway_connectivity);
}
/// Build a connectivity map for highway endpoints to determine where slopes are needed
fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HashMap<(i32, i32), Vec<i32>> {
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
for element in elements {

View File

@@ -1,18 +1,24 @@
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::{
block_definitions::WATER,
coordinate_system::cartesian::XZPoint,
coordinate_system::cartesian::{XZBBox, XZPoint},
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
world_editor::WorldEditor,
};
pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &ProcessedWay) {
pub fn generate_water_area_from_way(
editor: &mut WorldEditor,
element: &ProcessedWay,
_xzbbox: &XZBBox,
) {
let start_time = Instant::now();
let outers = [element.nodes.clone()];
if !verify_loopy_loops(&outers) {
if !verify_closed_rings(&outers) {
println!("Skipping way {} due to invalid polygon", element.id);
return;
}
@@ -20,7 +26,11 @@ pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &Processe
generate_water_areas(editor, &outers, &[], start_time);
}
pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &ProcessedRelation) {
pub fn generate_water_areas_from_relation(
editor: &mut WorldEditor,
element: &ProcessedRelation,
xzbbox: &XZBBox,
) {
let start_time = Instant::now();
// Check if this is a water relation (either with water tag or natural=water)
@@ -52,14 +62,63 @@ pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &Pr
}
}
merge_loopy_loops(&mut outers);
if !verify_loopy_loops(&outers) {
println!("Skipping relation {} due to invalid polygon", element.id);
return;
// Preserve OSM-defined outer/inner roles without modification
merge_way_segments(&mut outers);
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
outers = outers
.into_iter()
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
.collect();
merge_way_segments(&mut inners);
inners = inners
.into_iter()
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
.collect();
if !verify_closed_rings(&outers) {
// For clipped multipolygons, some loops may not close perfectly
// Instead of force-closing with straight lines (which creates wedges),
// filter out unclosed loops and only render the properly closed ones
// Filter: Keep only loops that are already closed OR can be closed within 1 block
outers.retain(|loop_nodes| {
if loop_nodes.len() < 3 {
return false;
}
let first = &loop_nodes[0];
let last = loop_nodes.last().unwrap();
let dx = (first.x - last.x).abs();
let dz = (first.z - last.z).abs();
// Keep if already closed by ID or endpoints are within 1 block
first.id == last.id || (dx <= 1 && dz <= 1)
});
// Now close the remaining loops that are within 1 block tolerance
for loop_nodes in outers.iter_mut() {
let first = loop_nodes[0].clone();
let last_idx = loop_nodes.len() - 1;
if loop_nodes[0].id != loop_nodes[last_idx].id {
// Endpoints are close (within tolerance), close the loop
loop_nodes.push(first);
}
}
// If no valid outer loops remain, skip the relation
if outers.is_empty() {
return;
}
// Verify again after filtering and closing
if !verify_closed_rings(&outers) {
println!("Skipping relation {} due to invalid polygon", element.id);
return;
}
}
merge_loopy_loops(&mut inners);
if !verify_loopy_loops(&inners) {
merge_way_segments(&mut inners);
if !verify_closed_rings(&inners) {
println!("Skipping relation {} due to invalid polygon", element.id);
return;
}
@@ -73,8 +132,34 @@ fn generate_water_areas(
inners: &[Vec<ProcessedNode>],
start_time: Instant,
) {
let (min_x, min_z) = editor.get_min_coords();
let (max_x, max_z) = editor.get_max_coords();
// Calculate polygon bounding box to limit fill area
let mut poly_min_x = i32::MAX;
let mut poly_min_z = i32::MAX;
let mut poly_max_x = i32::MIN;
let mut poly_max_z = i32::MIN;
for outer in outers {
for node in outer {
poly_min_x = poly_min_x.min(node.x);
poly_min_z = poly_min_z.min(node.z);
poly_max_x = poly_max_x.max(node.x);
poly_max_z = poly_max_z.max(node.z);
}
}
// If no valid bounds, nothing to fill
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
return;
}
// Clamp to world bounds just in case
let (world_min_x, world_min_z) = editor.get_min_coords();
let (world_max_x, world_max_z) = editor.get_max_coords();
let min_x = poly_min_x.max(world_min_x);
let min_z = poly_min_z.max(world_min_z);
let max_x = poly_max_x.min(world_max_x);
let max_z = poly_max_z.min(world_max_z);
let outers_xz: Vec<Vec<XZPoint>> = outers
.iter()
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
@@ -89,13 +174,23 @@ fn generate_water_areas(
);
}
// Merges ways that share nodes into full loops
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
/// Merges way segments that share endpoints into closed rings.
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
let mut removed: Vec<usize> = vec![];
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
for i in 0..loops.len() {
for j in 0..loops.len() {
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
if a.id == b.id {
return true;
}
let dx = (a.x - b.x).abs();
let dz = (a.z - b.z).abs();
dx <= 1 && dz <= 1
};
for i in 0..rings.len() {
for j in 0..rings.len() {
if i == j {
continue;
}
@@ -104,20 +199,29 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
continue;
}
let x: &Vec<ProcessedNode> = &loops[i];
let y: &Vec<ProcessedNode> = &loops[j];
let x: &Vec<ProcessedNode> = &rings[i];
let y: &Vec<ProcessedNode> = &rings[j];
// it's looped already
if x[0].id == x.last().unwrap().id {
// Skip empty rings (can happen after clipping)
if x.is_empty() || y.is_empty() {
continue;
}
// it's looped already
if y[0].id == y.last().unwrap().id {
let x_first = &x[0];
let x_last = x.last().unwrap();
let y_first = &y[0];
let y_last = y.last().unwrap();
// Skip already-closed rings
if nodes_match(x_first, x_last) {
continue;
}
if x[0].id == y[0].id {
if nodes_match(y_first, y_last) {
continue;
}
if nodes_match(x_first, y_first) {
removed.push(i);
removed.push(j);
@@ -125,7 +229,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
x.reverse();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
} else if x.last().unwrap().id == y.last().unwrap().id {
} else if nodes_match(x_last, y_last) {
removed.push(i);
removed.push(j);
@@ -133,7 +237,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
x.extend(y.iter().rev().skip(1).cloned());
merged.push(x);
} else if x[0].id == y.last().unwrap().id {
} else if nodes_match(x_first, y_last) {
removed.push(i);
removed.push(j);
@@ -141,7 +245,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
y.extend(x.iter().skip(1).cloned());
merged.push(y);
} else if x.last().unwrap().id == y[0].id {
} else if nodes_match(x_last, y_first) {
removed.push(i);
removed.push(j);
@@ -156,24 +260,35 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
removed.sort();
for r in removed.iter().rev() {
loops.remove(*r);
rings.remove(*r);
}
let merged_len: usize = merged.len();
for m in merged {
loops.push(m);
rings.push(m);
}
if merged_len > 0 {
merge_loopy_loops(loops);
merge_way_segments(rings);
}
}
fn verify_loopy_loops(loops: &[Vec<ProcessedNode>]) -> bool {
let mut valid: bool = true;
for l in loops {
if l[0].id != l.last().unwrap().id {
eprintln!("WARN: Disconnected loop");
/// Verifies all rings are properly closed (first node matches last).
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
let mut valid = true;
for ring in rings {
let first = &ring[0];
let last = ring.last().unwrap();
// Check if ring is closed (by ID or proximity)
let is_closed = first.id == last.id || {
let dx = (first.x - last.x).abs();
let dz = (first.z - last.z).abs();
dx <= 1 && dz <= 1
};
if !is_closed {
eprintln!("WARN: Disconnected ring");
valid = false;
}
}
@@ -195,6 +310,7 @@ fn inverse_floodfill(
editor: &mut WorldEditor,
start_time: Instant,
) {
// Convert to geo Polygons with normalized winding order
let inners: Vec<_> = inners
.into_iter()
.map(|x| {
@@ -206,6 +322,7 @@ fn inverse_floodfill(
),
vec![],
)
.orient(Direction::Default)
})
.collect();
@@ -220,6 +337,7 @@ fn inverse_floodfill(
),
vec![],
)
.orient(Direction::Default)
})
.collect();

View File

@@ -84,9 +84,8 @@ pub fn fetch_elevation_data(
let tiles: Vec<(u32, u32)> = get_tile_coordinates(bbox, zoom);
// Match grid dimensions with Minecraft world size
// Ensure minimum grid size of 1 to prevent division by zero and indexing errors
let grid_width: usize = (scale_factor_x as usize).max(1);
let grid_height: usize = (scale_factor_z as usize).max(1);
let grid_width: usize = scale_factor_x as usize;
let grid_height: usize = scale_factor_z as usize;
// Initialize height grid with proper dimensions
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
@@ -117,11 +116,6 @@ pub fn fetch_elevation_data(
tile_path.display(),
file_size
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile appears to be too small. Refetching tile.",
);
// Remove the potentially corrupted file
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
@@ -252,25 +246,26 @@ pub fn fetch_elevation_data(
filter_elevation_outliers(&mut height_grid);
// Calculate blur sigma based on grid resolution
// Reference points for tuning:
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
const LARGE_SIGMA_REF: f64 = 7.0; // Sigma for 1000x1000 grid
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
// This prevents larger generation areas from appearing noisier than smaller ones.
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
const BASE_GRID_REF: f64 = 100.0;
const BASE_SIGMA_REF: f64 = 5.0;
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
// Linear scaling for small grids
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
} else {
// Logarithmic scaling for larger grids
let ln_small: f64 = SMALL_GRID_REF.ln();
let ln_large: f64 = LARGE_GRID_REF.ln();
let log_grid_size: f64 = grid_size.ln();
let t: f64 = (log_grid_size - ln_small) / (ln_large - ln_small);
SMALL_SIGMA_REF + t * (LARGE_SIGMA_REF - SMALL_SIGMA_REF)
};
// Sqrt scaling provides a good balance:
// - 100x100: sigma = 5 (5% relative)
// - 500x500: sigma ≈ 11.2 (2.2% relative)
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
// 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!(
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
grid_width, grid_height, sigma, blur_percentage
);
/* eprintln!(
"Grid: {}x{}, Blur sigma: {:.2}",
@@ -382,11 +377,6 @@ fn get_tile_coordinates(bbox: &LLBBox, zoom: u8) -> Vec<(u32, u32)> {
}
fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
// Guard against empty input
if heights.is_empty() || heights[0].is_empty() {
return heights.to_owned();
}
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
@@ -456,11 +446,6 @@ fn create_gaussian_kernel(size: usize, sigma: f64) -> Vec<f64> {
}
fn fill_nan_values(height_grid: &mut [Vec<f64>]) {
// Guard against empty grid
if height_grid.is_empty() || height_grid[0].is_empty() {
return;
}
let height: usize = height_grid.len();
let width: usize = height_grid[0].len();
@@ -501,11 +486,6 @@ fn fill_nan_values(height_grid: &mut [Vec<f64>]) {
}
fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
// Guard against empty grid
if height_grid.is_empty() || height_grid[0].is_empty() {
return;
}
let height = height_grid.len();
let width = height_grid[0].len();
@@ -620,46 +600,4 @@ mod tests {
.unwrap()
.contains("image"));
}
#[test]
fn test_empty_grid_handling() {
// Test that empty grids don't cause panics
let empty_grid: Vec<Vec<f64>> = vec![];
let result = apply_gaussian_blur(&empty_grid, 5.0);
assert!(result.is_empty());
// Test grid with empty rows
let grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
let result = apply_gaussian_blur(&grid_with_empty_rows, 5.0);
assert_eq!(result.len(), 1);
assert!(result[0].is_empty());
}
#[test]
fn test_fill_nan_values_empty_grid() {
// Test that empty grids don't cause panics
let mut empty_grid: Vec<Vec<f64>> = vec![];
fill_nan_values(&mut empty_grid);
assert!(empty_grid.is_empty());
// Test grid with empty rows
let mut grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
fill_nan_values(&mut grid_with_empty_rows);
assert_eq!(grid_with_empty_rows.len(), 1);
assert!(grid_with_empty_rows[0].is_empty());
}
#[test]
fn test_filter_outliers_empty_grid() {
// Test that empty grids don't cause panics
let mut empty_grid: Vec<Vec<f64>> = vec![];
filter_elevation_outliers(&mut empty_grid);
assert!(empty_grid.is_empty());
// Test grid with empty rows
let mut grid_with_empty_rows: Vec<Vec<f64>> = vec![vec![]];
filter_elevation_outliers(&mut grid_with_empty_rows);
assert_eq!(grid_with_empty_rows.len(), 1);
assert!(grid_with_empty_rows[0].is_empty());
}
}

View File

@@ -68,14 +68,11 @@ fn optimized_flood_fill_area(
// Pre-allocate queue with reasonable capacity to avoid reallocations
let mut queue = VecDeque::with_capacity(1024);
let mut iterations = 0u64;
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
for z in (min_z..=max_z).step_by(step_z as usize) {
for x in (min_x..=max_x).step_by(step_x as usize) {
// Check timeout more frequently for small areas
#[allow(clippy::manual_is_multiple_of)]
if iterations % 50 == 0 {
// Fast timeout check, only every few iterations
if filled_area.len() % 100 == 0 {
if let Some(timeout) = timeout {
if start_time.elapsed() > *timeout {
return filled_area;
@@ -83,16 +80,6 @@ fn optimized_flood_fill_area(
}
}
// Safety check: prevent infinite loops
iterations += 1;
if iterations > MAX_ITERATIONS {
eprintln!(
"Warning: Flood fill exceeded max iterations ({}), aborting",
MAX_ITERATIONS
);
return filled_area;
}
// Skip if already visited or not inside polygon
if global_visited.contains(&(x, z))
|| !polygon.contains(&Point::new(x as f64, z as f64))
@@ -106,26 +93,6 @@ fn optimized_flood_fill_area(
global_visited.insert((x, z));
while let Some((curr_x, curr_z)) = queue.pop_front() {
// Additional iteration check inside inner loop
iterations += 1;
if iterations > MAX_ITERATIONS {
eprintln!(
"Warning: Flood fill exceeded max iterations ({}), aborting",
MAX_ITERATIONS
);
return filled_area;
}
// Timeout check in inner loop for problematic polygons
#[allow(clippy::manual_is_multiple_of)]
if iterations % 1000 == 0 {
if let Some(timeout) = timeout {
if start_time.elapsed() > *timeout {
return filled_area;
}
}
}
// Add current point to filled area
filled_area.push((curr_x, curr_z));
@@ -188,32 +155,18 @@ fn original_flood_fill_area(
// Pre-allocate queue and reserve space for filled_area
let mut queue: VecDeque<(i32, i32)> = VecDeque::with_capacity(2048);
filled_area.reserve(1000); // Reserve space to reduce reallocations
let mut iterations = 0u64;
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
// Scan for multiple seed points to handle U-shapes and concave polygons
for z in (min_z..=max_z).step_by(step_z as usize) {
for x in (min_x..=max_x).step_by(step_x as usize) {
// Check timeout more frequently for problematic polygons
#[allow(clippy::manual_is_multiple_of)]
if iterations % 50 == 0 {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
return filled_area;
}
// Reduced timeout checking frequency for better performance
// Use manual % check since is_multiple_of() is unstable on stable Rust
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
return filled_area;
}
}
// Safety check: prevent infinite loops
iterations += 1;
if iterations > MAX_ITERATIONS {
eprintln!(
"Warning: Flood fill exceeded max iterations ({}), aborting",
MAX_ITERATIONS
);
return filled_area;
}
// Skip if already processed or not inside polygon
if global_visited.contains(&(x, z))
|| !polygon.contains(&Point::new(x as f64, z as f64))
@@ -227,26 +180,6 @@ fn original_flood_fill_area(
global_visited.insert((x, z));
while let Some((curr_x, curr_z)) = queue.pop_front() {
// Additional iteration check inside inner loop
iterations += 1;
if iterations > MAX_ITERATIONS {
eprintln!(
"Warning: Flood fill exceeded max iterations ({}), aborting",
MAX_ITERATIONS
);
return filled_area;
}
// Timeout check in inner loop
#[allow(clippy::manual_is_multiple_of)]
if iterations % 1000 == 0 {
if let Some(timeout) = timeout {
if &start_time.elapsed() > timeout {
return filled_area;
}
}
}
// Only check polygon containment once per point when adding to filled_area
if polygon.contains(&Point::new(curr_x as f64, curr_z as f64)) {
filled_area.push((curr_x, curr_z));

View File

@@ -75,10 +75,6 @@ impl Ground {
/// Converts game coordinates to elevation data coordinates
#[inline(always)]
fn get_data_coordinates(&self, coord: XZPoint, data: &ElevationData) -> (f64, f64) {
// Guard against division by zero for edge cases
if data.width == 0 || data.height == 0 {
return (0.0, 0.0);
}
let x_ratio: f64 = coord.x as f64 / data.width as f64;
let z_ratio: f64 = coord.z as f64 / data.height as f64;
(x_ratio.clamp(0.0, 1.0), z_ratio.clamp(0.0, 1.0))
@@ -87,17 +83,8 @@ impl Ground {
/// Interpolates height value from the elevation grid
#[inline(always)]
fn interpolate_height(&self, x_ratio: f64, z_ratio: f64, data: &ElevationData) -> i32 {
// Guard against out of bounds access
if data.width == 0 || data.height == 0 || data.heights.is_empty() {
return self.ground_level;
}
let x: usize = ((x_ratio * (data.width - 1) as f64).round() as usize).min(data.width - 1);
let z: usize = ((z_ratio * (data.height - 1) as f64).round() as usize).min(data.height - 1);
// Additional safety check for row length
if z >= data.heights.len() || x >= data.heights[z].len() {
return self.ground_level;
}
data.heights[z][x]
}
@@ -151,80 +138,6 @@ impl Ground {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ground_level_with_zero_dimensions() {
// Test that zero-dimension elevation data doesn't cause panic
let elevation_data = ElevationData {
heights: vec![],
width: 0,
height: 0,
};
let ground = Ground {
elevation_enabled: true,
ground_level: 64,
elevation_data: Some(elevation_data),
};
// This should not panic and should return ground_level
let level = ground.level(XZPoint::new(10, 10));
assert_eq!(level, 64);
}
#[test]
fn test_ground_level_with_one_dimension_zero() {
// Test that partial zero dimensions don't cause panic
let elevation_data = ElevationData {
heights: vec![vec![100]],
width: 0,
height: 1,
};
let ground = Ground {
elevation_enabled: true,
ground_level: 64,
elevation_data: Some(elevation_data),
};
// This should not panic and should return ground_level
let level = ground.level(XZPoint::new(5, 5));
assert_eq!(level, 64);
}
#[test]
fn test_ground_level_normal_case() {
// Test that normal elevation data works correctly
let elevation_data = ElevationData {
heights: vec![vec![80, 85], vec![90, 95]],
width: 2,
height: 2,
};
let ground = Ground {
elevation_enabled: true,
ground_level: 64,
elevation_data: Some(elevation_data),
};
// This should work normally
let level = ground.level(XZPoint::new(0, 0));
assert!(level >= 64); // Should be one of the elevation values
}
#[test]
fn test_ground_level_disabled() {
// Test that disabled elevation returns ground_level
let ground = Ground::new_flat(70);
let level = ground.level(XZPoint::new(100, 100));
assert_eq!(level, 70);
}
}
pub fn generate_ground_data(args: &Args) -> Ground {
if args.terrain {
println!("{} Fetching elevation...", "[3/7]".bold());

View File

@@ -2,14 +2,16 @@ use crate::args::Args;
use crate::coordinate_system::cartesian::XZPoint;
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
use crate::coordinate_system::transformation::CoordTransformer;
use crate::data_processing;
use crate::data_processing::{self, GenerationOptions};
use crate::ground::{self, Ground};
use crate::map_transformation;
use crate::osm_parser;
use crate::progress;
use crate::progress::{self, emit_gui_progress_update};
use crate::retrieve_data;
use crate::telemetry::{self, send_log, LogLevel};
use crate::version_check;
use crate::world_editor::WorldFormat;
use colored::Colorize;
use fastnbt::Value;
use flate2::read::GzDecoder;
use fs2::FileExt;
@@ -60,6 +62,17 @@ impl Drop for SessionLock {
}
}
/// Gets the area name for a given bounding box using the center point
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
match retrieve_data::fetch_area_name(center_lat, center_lon) {
Ok(Some(name)) => name,
_ => "Unknown Location".to_string(),
}
}
pub fn run_gui() {
// Launch the UI
println!("Launching UI...");
@@ -100,7 +113,9 @@ pub fn run_gui() {
gui_select_world,
gui_start_generation,
gui_get_version,
gui_check_for_updates
gui_check_for_updates,
gui_get_world_map_data,
gui_show_in_folder
])
.setup(|app| {
let app_handle = app.handle();
@@ -663,6 +678,114 @@ fn gui_check_for_updates() -> Result<bool, String> {
}
}
/// Returns the world map image data as base64 and geo bounds for overlay display.
/// Returns None if the map image or metadata doesn't exist.
#[tauri::command]
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
let world_dir = PathBuf::from(&world_path);
let map_path = world_dir.join("arnis_world_map.png");
let metadata_path = world_dir.join("metadata.json");
// Check if both files exist
if !map_path.exists() || !metadata_path.exists() {
return Ok(None);
}
// Read and encode the map image as base64
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
let base64_image =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
// Read metadata
let metadata_content =
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
// Extract geo bounds (metadata uses camelCase from serde)
let min_lat = metadata["minGeoLat"]
.as_f64()
.ok_or("Missing minGeoLat in metadata")?;
let max_lat = metadata["maxGeoLat"]
.as_f64()
.ok_or("Missing maxGeoLat in metadata")?;
let min_lon = metadata["minGeoLon"]
.as_f64()
.ok_or("Missing minGeoLon in metadata")?;
let max_lon = metadata["maxGeoLon"]
.as_f64()
.ok_or("Missing maxGeoLon in metadata")?;
Ok(Some(WorldMapData {
image_base64: format!("data:image/png;base64,{}", base64_image),
min_lat,
max_lat,
min_lon,
max_lon,
}))
}
/// Data structure for world map overlay
#[derive(serde::Serialize)]
struct WorldMapData {
image_base64: String,
min_lat: f64,
max_lat: f64,
min_lon: f64,
max_lon: f64,
}
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
#[tauri::command]
fn gui_show_in_folder(path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
// On Windows, try to open with default application (Minecraft Bedrock)
// If that fails, show in Explorer
if std::process::Command::new("cmd")
.args(["/C", "start", "", &path])
.spawn()
.is_err()
{
std::process::Command::new("explorer")
.args(["/select,", &path])
.spawn()
.map_err(|e| format!("Failed to open explorer: {}", e))?;
}
}
#[cfg(target_os = "macos")]
{
// On macOS, just reveal in Finder
std::process::Command::new("open")
.args(["-R", &path])
.spawn()
.map_err(|e| format!("Failed to open Finder: {}", e))?;
}
#[cfg(target_os = "linux")]
{
// On Linux, just show in file manager
let path_parent = std::path::Path::new(&path)
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| path.clone());
// Try nautilus with select first, then fall back to xdg-open on parent
if std::process::Command::new("nautilus")
.args(["--select", &path])
.spawn()
.is_err()
{
let _ = std::process::Command::new("xdg-open")
.arg(&path_parent)
.spawn();
}
}
Ok(())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
#[allow(unused_variables)]
@@ -680,6 +803,7 @@ fn gui_start_generation(
is_new_world: bool,
spawn_point: Option<(f64, f64)>,
telemetry_consent: bool,
world_format: String,
) -> Result<(), String> {
use progress::emit_gui_error;
use LLBBox;
@@ -691,7 +815,9 @@ fn gui_start_generation(
telemetry::send_generation_click();
// If spawn point was chosen and the world is new, check and set the spawn point
if is_new_world && spawn_point.is_some() {
// 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) {
@@ -745,19 +871,73 @@ fn gui_start_generation(
}
};
// Add localized name to the world if user generated a new world
let updated_world_path = if is_new_world {
add_localized_world_name(world_path, &bbox)
// Determine world format from UI selection
let world_format = if world_format == "bedrock" {
WorldFormat::BedrockMcWorld
} else {
world_path
WorldFormat::JavaAnvil
};
// Create an Args instance with the chosen bounding box and world directory path
// Determine output path and level name based on format
let (generation_path, level_name) = match world_format {
WorldFormat::JavaAnvil => {
// Java: use the selected world path, add localized name if new
let updated_path = if is_new_world {
add_localized_world_name(world_path.clone(), &bbox)
} else {
world_path.clone()
};
(updated_path, None)
}
WorldFormat::BedrockMcWorld => {
// Bedrock: generate .mcworld in current directory with location-based name
let area_name = get_area_name_for_bedrock(&bbox);
let filename = format!("Arnis {}.mcworld", area_name);
let lvl_name = format!("Arnis World: {}", area_name);
let output_path = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(filename);
(output_path, Some(lvl_name))
}
};
// Calculate MC spawn coordinates from lat/lng if spawn point was provided
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, _)) =
CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale)
{
let xzpoint = transformer.transform_point(llpoint);
Some((xzpoint.x, xzpoint.z))
} else {
None
}
} else {
None
}
} else {
None
};
// Create generation options
let generation_options = GenerationOptions {
path: generation_path.clone(),
format: world_format,
level_name,
spawn_point: mc_spawn_point,
};
// Create an Args instance with the chosen bounding box
// Note: path is used for Java-specific features like spawn point update
let args: Args = Args {
bbox,
file: None,
save_json_file: None,
path: updated_world_path,
path: if world_format == WorldFormat::JavaAnvil {
generation_path
} else {
world_path
},
downloader: "requests".to_string(),
scale: world_scale,
ground_level,
@@ -781,14 +961,29 @@ fn gui_start_generation(
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
let _ = data_processing::generate_world(
let _ = data_processing::generate_world_with_options(
parsed_elements,
xzbbox,
xzbbox.clone(),
args.bbox,
ground,
&args,
generation_options.clone(),
);
// Session lock will be automatically released when _session_lock goes out of scope
// 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(());
}
@@ -818,16 +1013,30 @@ fn gui_start_generation(
&mut xzbbox,
&mut ground,
);
send_log(LogLevel::Info, "Map transformation completed.");
let _ = data_processing::generate_world(
let _ = data_processing::generate_world_with_options(
parsed_elements,
xzbbox,
xzbbox.clone(),
args.bbox,
ground,
&args,
generation_options.clone(),
);
// Session lock will be automatically released when _session_lock goes out of scope
// 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);
}
Ok(())
}
Err(e) => {

34
src/gui/css/bbox.css vendored
View File

@@ -344,4 +344,38 @@ body,
filter: blur(1px) sepia(1) invert(1);
transition: all 1s ease;
}
/* World Preview Button in Edit Toolbar */
.leaflet-draw-toolbar .leaflet-draw-edit-preview {
background-position: -31px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.leaflet-draw-toolbar .leaflet-draw-edit-preview.active {
background-color: #a0d0ff;
}
.world-preview-slider-container {
padding: 6px 8px !important;
background: white !important;
background-clip: padding-box;
}
.world-preview-slider-container a {
display: none !important;
}
.world-preview-slider {
width: 80px;
height: 8px;
cursor: pointer;
accent-color: #3887BE;
display: block;
margin: 0;
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -158,12 +158,13 @@
background-position: -182px -2px;
}
/* Disabled states reuse same sprites; opacity indicates disabled */
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
background-position: -212px -2px;
background-position: -152px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
background-position: -242px -2px;
background-position: -182px -2px;
}
/* ================================================================== */

View File

@@ -222,6 +222,68 @@ button:hover {
width: 100%;
}
/* World Selection Container */
.world-selection-container {
width: 100%;
}
.choose-world-btn {
padding: 10px;
line-height: 1.2;
width: 100%;
border-radius: 8px 8px 0 0 !important;
margin-bottom: 0 !important;
box-shadow: none !important;
}
/* World Format Toggle */
.format-toggle-container {
display: flex;
width: 100%;
gap: 0;
margin-top: 0;
}
.format-toggle-btn {
flex: 1;
padding: 10px;
font-size: 1em;
font-weight: 500;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.25s ease;
margin-top: 0;
border-radius: 0;
}
.format-toggle-btn:first-child {
border-radius: 0 0 0 8px;
}
.format-toggle-btn:last-child {
border-radius: 0 0 8px 0;
}
.format-toggle-btn:not(.format-active) {
background-color: #3a3a3a;
color: #b0b0b0;
}
.format-toggle-btn:not(.format-active):hover {
background-color: #4a4a4a;
color: #ffffff;
}
.format-toggle-btn.format-active {
background-color: var(--primary-accent);
color: #0f0f0f;
font-weight: 600;
}
.format-toggle-btn.format-active:hover {
background-color: var(--primary-accent-dark);
}
/* Customization Settings */
.modal {
position: fixed;

29
src/gui/index.html vendored
View File

@@ -37,15 +37,26 @@
<div class="controls-content">
<h2 data-localize="select_world">Select World</h2>
<!-- Updated Tooltip Structure -->
<div class="tooltip" style="width: 100%;">
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
<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>
<!-- 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>
<div class="button-container">

213
src/gui/js/bbox.js vendored
View File

@@ -529,7 +529,7 @@ $(document).ready(function () {
failureCount++;
// After a few failures, try HTTP fallback
if (failureCount >= 3 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
if (failureCount >= 6 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
console.log('HTTPS tile loading failed, attempting HTTP fallback for', themeKey);
this._httpFallbackAttempted = true;
@@ -558,11 +558,208 @@ $(document).ready(function () {
var savedTheme = localStorage.getItem('selectedTileTheme') || 'osm';
changeTileTheme(savedTheme);
// Listen for theme changes from parent window (settings modal)
// World overlay state
var worldOverlay = null;
var worldOverlayData = null;
var worldOverlayEnabled = false;
var worldPreviewAvailable = false;
var sliderControl = null;
// Create the opacity slider as a proper Leaflet control
var SliderControl = L.Control.extend({
options: { position: 'topleft' },
onAdd: function(map) {
var container = L.DomUtil.create('div', 'leaflet-bar world-preview-slider-container');
container.id = 'world-preview-slider-container';
container.style.display = 'none';
var slider = L.DomUtil.create('input', 'world-preview-slider', container);
slider.type = 'range';
slider.min = '0';
slider.max = '100';
slider.value = '50';
slider.id = 'world-preview-opacity';
slider.title = 'Overlay Opacity';
L.DomEvent.on(slider, 'input', function(e) {
if (worldOverlay) {
worldOverlay.setOpacity(e.target.value / 100);
}
});
// Prevent all map interactions
L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);
L.DomEvent.on(container, 'mousedown', L.DomEvent.stopPropagation);
L.DomEvent.on(container, 'touchstart', L.DomEvent.stopPropagation);
L.DomEvent.on(slider, 'mousedown', L.DomEvent.stopPropagation);
L.DomEvent.on(slider, 'touchstart', L.DomEvent.stopPropagation);
return container;
}
});
// Function to add world preview button to the draw control's edit toolbar
function addWorldPreviewToEditToolbar() {
// Find the edit toolbar (contains Edit layers and Delete layers buttons)
var editToolbar = document.querySelector('.leaflet-draw-toolbar:not(.leaflet-draw-toolbar-top)');
if (!editToolbar) {
// Try finding by the edit/delete buttons
var deleteBtn = document.querySelector('.leaflet-draw-edit-remove');
if (deleteBtn) {
editToolbar = deleteBtn.parentElement;
}
}
if (editToolbar) {
// Create the preview button
var toggleBtn = document.createElement('a');
toggleBtn.className = 'leaflet-draw-edit-preview disabled';
toggleBtn.href = '#';
toggleBtn.title = 'Show World Preview (not available yet)';
toggleBtn.id = 'world-preview-btn';
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (worldPreviewAvailable) {
toggleWorldOverlay();
}
});
editToolbar.appendChild(toggleBtn);
// Add the slider control to the map
sliderControl = new SliderControl();
map.addControl(sliderControl);
}
}
// Toggle world overlay function
function toggleWorldOverlay() {
if (!worldPreviewAvailable || !worldOverlayData) return;
worldOverlayEnabled = !worldOverlayEnabled;
var btn = document.getElementById('world-preview-btn');
var sliderContainer = document.getElementById('world-preview-slider-container');
if (worldOverlayEnabled) {
// Show overlay
var data = worldOverlayData;
var bounds = L.latLngBounds(
[data.min_lat, data.min_lon],
[data.max_lat, data.max_lon]
);
if (worldOverlay) {
map.removeLayer(worldOverlay);
}
var opacity = document.getElementById('world-preview-opacity');
var opacityValue = opacity ? opacity.value / 100 : 0.5;
worldOverlay = L.imageOverlay(data.image_base64, bounds, {
opacity: opacityValue,
interactive: false,
zIndex: 500
});
worldOverlay.addTo(map);
if (btn) {
btn.classList.add('active');
btn.title = 'Hide World Preview';
}
if (sliderContainer) {
sliderContainer.style.display = 'block';
}
} else {
// Hide overlay
if (worldOverlay) {
map.removeLayer(worldOverlay);
worldOverlay = null;
}
if (btn) {
btn.classList.remove('active');
btn.title = 'Show World Preview';
}
if (sliderContainer) {
sliderContainer.style.display = 'none';
}
}
}
// Enable the preview button when data is available
function enableWorldPreview(data) {
worldOverlayData = data;
worldPreviewAvailable = true;
var btn = document.getElementById('world-preview-btn');
if (btn) {
btn.classList.remove('disabled');
btn.title = 'Show World Preview';
}
}
// Disable and reset preview (when world changes)
function disableWorldPreview() {
worldPreviewAvailable = false;
worldOverlayData = null;
worldOverlayEnabled = false;
if (worldOverlay) {
map.removeLayer(worldOverlay);
worldOverlay = null;
}
var btn = document.getElementById('world-preview-btn');
var sliderContainer = document.getElementById('world-preview-slider-container');
if (btn) {
btn.classList.add('disabled');
btn.classList.remove('active');
btn.title = 'Show World Preview (not available yet)';
}
if (sliderContainer) {
sliderContainer.style.display = 'none';
}
}
// Listen for messages from parent window
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'changeTileTheme') {
changeTileTheme(event.data.theme);
}
// Handle world preview data ready (after generation completes)
if (event.data && event.data.type === 'worldPreviewReady') {
enableWorldPreview(event.data.data);
// Auto-enable the overlay when generation completes
if (!worldOverlayEnabled) {
toggleWorldOverlay();
}
}
// Handle existing world map load (zoom to location and auto-enable)
if (event.data && event.data.type === 'loadExistingWorldMap') {
var data = event.data.data;
enableWorldPreview(data);
// Calculate bounds and zoom to them
var bounds = L.latLngBounds(
[data.min_lat, data.min_lon],
[data.max_lat, data.max_lon]
);
map.fitBounds(bounds, { padding: [50, 50] });
// Auto-enable the overlay
if (!worldOverlayEnabled) {
toggleWorldOverlay();
}
}
// Handle world changed (disable preview)
if (event.data && event.data.type === 'worldChanged') {
disableWorldPreview();
}
});
// Set the dropdown value in parent window if it exists
@@ -652,6 +849,9 @@ $(document).ready(function () {
}
});
map.addControl(drawControl);
// Add world preview button to the edit toolbar after drawControl is added
addWorldPreviewToEditToolbar();
/*
**
** create bounds layer
@@ -699,6 +899,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({

View File

@@ -24,6 +24,10 @@ export const licenseText = `
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
<br><br>
<b>bedrock-rs:</b><br>
Bedrock Edition world format support uses the <a href="https://github.com/bedrock-crustaceans/bedrock-rs" style="color: inherit;" target="_blank">bedrock-rs</a> library, licensed under the Apache License 2.0.
<br><br>
<p><b>Privacy Policy:</b></p>
If you consent to telemetry data collection, please review our Privacy Policy at:
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.

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

@@ -24,6 +24,7 @@ window.addEventListener("DOMContentLoaded", async () => {
handleBboxInput();
const localization = await getLocalization();
await applyLocalization(localization);
updateFormatToggleUI(selectedWorldFormat);
initFooter();
await checkForUpdates();
});
@@ -214,6 +215,23 @@ function setupProgressListener() {
}
}
});
// Listen for map preview ready event from backend
window.__TAURI__.event.listen("map-preview-ready", () => {
console.log("Map preview ready event received");
showWorldPreviewButton();
});
// Listen for open-mcworld-file event to show the generated Bedrock world in file explorer
window.__TAURI__.event.listen("open-mcworld-file", async (event) => {
const filePath = event.payload;
try {
// Use our custom command to show the file in the system file explorer
await invoke("gui_show_in_folder", { path: filePath });
} catch (error) {
console.error("Failed to show mcworld file in folder:", error);
}
});
}
function initSettings() {
@@ -242,6 +260,9 @@ function initSettings() {
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
});
// World format toggle (Java/Bedrock)
initWorldFormatToggle();
// Language selector
const languageSelect = document.getElementById("language-select");
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
@@ -344,6 +365,72 @@ function initSettings() {
window.closeLicense = closeLicense;
}
// World format selection (Java/Bedrock)
let selectedWorldFormat = 'java'; // Default to Java
function initWorldFormatToggle() {
// Load saved format preference
const savedFormat = localStorage.getItem('arnis-world-format');
if (savedFormat && (savedFormat === 'java' || savedFormat === 'bedrock')) {
selectedWorldFormat = savedFormat;
}
// Apply the saved selection to UI
updateFormatToggleUI(selectedWorldFormat);
}
function setWorldFormat(format) {
if (format !== 'java' && format !== 'bedrock') return;
selectedWorldFormat = format;
localStorage.setItem('arnis-world-format', format);
updateFormatToggleUI(format);
}
function updateFormatToggleUI(format) {
const javaBtn = document.getElementById('format-java');
const bedrockBtn = document.getElementById('format-bedrock');
const chooseWorldBtn = document.getElementById('choose-world-btn');
const selectedWorldText = document.getElementById('selected-world');
if (format === 'java') {
javaBtn.classList.add('format-active');
bedrockBtn.classList.remove('format-active');
// Enable Choose World button for Java
if (chooseWorldBtn) {
chooseWorldBtn.disabled = false;
chooseWorldBtn.style.opacity = '1';
chooseWorldBtn.style.cursor = 'pointer';
}
// Show default text (world was cleared when switching to Bedrock)
if (selectedWorldText) {
const noWorldText = window.localization?.no_world_selected || 'No world selected';
selectedWorldText.textContent = noWorldText;
selectedWorldText.style.color = '#fecc44';
}
} else {
javaBtn.classList.remove('format-active');
bedrockBtn.classList.add('format-active');
// Disable Choose World button for Bedrock and clear any selected world
if (chooseWorldBtn) {
chooseWorldBtn.disabled = true;
chooseWorldBtn.style.opacity = '0.5';
chooseWorldBtn.style.cursor = 'not-allowed';
}
// Clear world selection and show Bedrock info message
worldPath = "";
isNewWorld = false;
if (selectedWorldText) {
const bedrockText = window.localization?.bedrock_use_java || 'Use Java to select worlds';
selectedWorldText.textContent = bedrockText;
selectedWorldText.style.color = '#fecc44';
}
}
}
// Expose to window for onclick handlers
window.setWorldFormat = setWorldFormat;
// Telemetry consent (first run only)
function initTelemetryConsent() {
const key = 'telemetry-consent'; // values: 'true' | 'false'
@@ -533,8 +620,8 @@ function normalizeLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
const threshold1 = 30000000.00;
const threshold2 = 45000000.00;
const threshold1 = 44000000.00; // Yellow warning threshold (~6.2km x 7km)
const threshold2 = 85000000.00; // Red error threshold (~8.7km x 9.8km)
let selectedBBox = "";
let mapSelectedBBox = ""; // Tracks bbox from map selection
let customBBoxValid = false; // Tracks if custom input is valid
@@ -591,6 +678,14 @@ async function selectWorld(generate_new_world) {
const lastSegment = worldName.split(/[\\/]/).pop();
document.getElementById('selected-world').textContent = lastSegment;
document.getElementById('selected-world').style.color = "#fecc44";
// Notify that world changed (reset preview)
notifyWorldChanged();
// If selecting an existing world, check for existing map data
if (!generate_new_world) {
await loadExistingWorldMapData();
}
}
} catch (error) {
handleWorldSelectionError(error);
@@ -599,6 +694,32 @@ async function selectWorld(generate_new_world) {
closeWorldPicker();
}
/**
* Loads existing world map data if available (for existing worlds)
* This will zoom to the location and auto-enable the preview
*/
async function loadExistingWorldMapData() {
if (!worldPath) return;
try {
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
if (mapData) {
currentWorldMapData = mapData;
// Send data to the map iframe with instruction to zoom and auto-enable
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'loadExistingWorldMap',
data: mapData
}, '*');
}
}
} catch (error) {
console.log("No existing world map data found:", error);
}
}
/**
* Handles world selection errors and displays appropriate messages
* @param {number} errorCode - Error code from the backend
@@ -638,13 +759,17 @@ async function startGeneration() {
return;
}
if (!worldPath || worldPath === "") {
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
const selectedWorld = document.getElementById('selected-world');
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
selectedWorld.style.color = "#fa7878";
return;
}
// Clear any existing world preview since we're generating a new one
notifyWorldChanged();
// Get the map iframe reference
const mapFrame = document.querySelector('.map-container');
// Get spawn point coordinates if marker exists
@@ -692,7 +817,8 @@ async function startGeneration() {
fillgroundEnabled: fill_ground,
isNewWorld: isNewWorld,
spawnPoint: spawnPoint,
telemetryConsent: telemetryConsent || false
telemetryConsent: telemetryConsent || false,
worldFormat: selectedWorldFormat
});
console.log("Generation process started.");
@@ -702,3 +828,60 @@ async function startGeneration() {
generationButtonEnabled = true;
}
}
// World preview overlay state
let worldPreviewEnabled = false;
let currentWorldMapData = null;
/**
* Notifies the map iframe that world preview data is ready
* Called when the backend emits the map-preview-ready event
*/
async function showWorldPreviewButton() {
// Try to load the world map data
await loadWorldMapData();
if (currentWorldMapData) {
// Send data to the map iframe
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'worldPreviewReady',
data: currentWorldMapData
}, '*');
console.log("World preview data sent to map iframe");
}
} else {
console.warn("Map data not available yet");
}
}
/**
* Notifies the map iframe that the world has changed (reset preview)
*/
function notifyWorldChanged() {
currentWorldMapData = null;
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'worldChanged'
}, '*');
}
}
/**
* Loads the world map data from the backend
*/
async function loadWorldMapData() {
if (!worldPath) return;
try {
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
if (mapData) {
currentWorldMapData = mapData;
console.log("World map data loaded successfully");
}
} catch (error) {
console.error("Failed to load world map data:", error);
}
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "تضاريس فقط",
"interior": "توليد الداخلية",
"roof": "توليد السقف",
"fillground": "ملء الأرض"
"fillground": "ملء الأرض",
"bedrock_use_java": "استخدم Java لاختيار العوالم"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Nur Terrain",
"interior": "Innenraum Generierung",
"roof": "Dach Generierung",
"fillground": "Boden füllen"
"fillground": "Boden füllen",
"bedrock_use_java": "Java für Weltauswahl nutzen"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Terrain only",
"interior": "Interior Generation",
"roof": "Roof Generation",
"fillground": "Fill Ground"
"fillground": "Fill Ground",
"bedrock_use_java": "Use Java to select worlds"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Solo Terreno",
"interior": "Generación Interior",
"roof": "Generación de Tejado",
"fillground": "Rellenar Suelo"
"fillground": "Rellenar Suelo",
"bedrock_use_java": "Usa Java para elegir mundos"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Vain maasto",
"interior": "Sisätilan luonti",
"roof": "Katon luonti",
"fillground": "Täytä maa"
"fillground": "Täytä maa",
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Terrain uniquement",
"interior": "Génération d'intérieur",
"roof": "Génération de toit",
"fillground": "Remplir le sol"
"fillground": "Remplir le sol",
"bedrock_use_java": "Utilisez Java pour les mondes"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Csak terep",
"interior": "Belső generálás",
"roof": "Tető generálás",
"fillground": "Talaj feltöltése"
"fillground": "Talaj feltöltése",
"bedrock_use_java": "Java világválasztáshoz"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "지형만",
"interior": "내부 생성",
"roof": "지붕 생성",
"fillground": "지면 채우기"
"fillground": "지면 채우기",
"bedrock_use_java": "Java로 세계 선택"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Tik reljefas",
"interior": "Interjero generavimas",
"roof": "Stogo generavimas",
"fillground": "Užpildyti pagrindą"
"fillground": "Užpildyti pagrindą",
"bedrock_use_java": "Naudok Java pasauliams"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Tikai reljefs",
"interior": "Interjera ģenerēšana",
"roof": "Jumta ģenerēšana",
"fillground": "Aizpildīt zemi"
"fillground": "Aizpildīt zemi",
"bedrock_use_java": "Izmanto Java pasaulēm"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Tylko teren",
"interior": "Generowanie wnętrza",
"roof": "Generowanie dachu",
"fillground": "Wypełnij podłoże"
"fillground": "Wypełnij podłoże",
"bedrock_use_java": "Użyj Java do wyboru światów"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Только Рельеф",
"interior": "Генерация Интерьера",
"roof": "Генерация Крыши",
"fillground": "Заполнить Землю"
"fillground": "Заполнить Землю",
"bedrock_use_java": "Используйте Java для миров"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Endast terräng",
"interior": "Interiörgenerering",
"roof": "Takgenerering",
"fillground": "Fyll mark"
"fillground": "Fyll mark",
"bedrock_use_java": "Använd Java för världar"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "Тільки рельєф",
"interior": "Генерація інтер'єру",
"roof": "Генерація даху",
"fillground": "Заповнити землю"
"fillground": "Заповнити землю",
"bedrock_use_java": "Використовуй Java для світів"
}

View File

@@ -44,5 +44,6 @@
"mode_terrain_only": "仅地形",
"interior": "内部生成",
"roof": "屋顶生成",
"fillground": "填充地面"
"fillground": "填充地面",
"bedrock_use_java": "使用Java选择世界"
}

View File

@@ -1,14 +1,19 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod args;
#[cfg(feature = "bedrock")]
mod bedrock_block_map;
mod block_definitions;
mod bresenham;
mod clipping;
mod colors;
mod coordinate_system;
mod data_processing;
mod element_processing;
mod elevation_data;
mod floodfill;
mod ground;
mod map_renderer;
mod map_transformation;
mod osm_parser;
#[cfg(feature = "gui")]
@@ -26,7 +31,6 @@ use clap::Parser;
use colored::*;
use std::{env, fs, io::Write};
mod elevation_data;
#[cfg(feature = "gui")]
mod gui;
@@ -35,6 +39,8 @@ mod gui;
mod progress {
pub fn emit_gui_error(_message: &str) {}
pub fn emit_gui_progress_update(_progress: f64, _message: &str) {}
pub fn emit_map_preview_ready() {}
pub fn emit_open_mcworld_file(_path: &str) {}
pub fn is_running_with_gui() -> bool {
false
}

944
src/map_renderer.rs Normal file
View File

@@ -0,0 +1,944 @@
// Top-down world map renderer for GUI preview.
//
// Generates a 1:1 pixel-per-block PNG image of the generated world,
// showing the topmost visible block at each position.
use fastanvil::Region;
use fastnbt::{from_bytes, Value};
use fnv::FnvHashMap;
use image::{Rgb, RgbImage};
use once_cell::sync::Lazy;
use rayon::prelude::*;
use std::fs::File;
use std::path::Path;
use std::sync::Mutex;
/// Pre-computed block colors for fast lookup
static BLOCK_COLORS: Lazy<FnvHashMap<&'static str, Rgb<u8>>> = Lazy::new(get_block_colors);
/// Renders a top-down view of the generated Minecraft world.
/// Returns the path to the saved image file.
pub fn render_world_map(
world_dir: &Path,
min_x: i32,
max_x: i32,
min_z: i32,
max_z: i32,
) -> Result<std::path::PathBuf, String> {
let width = (max_x - min_x + 1) as u32;
let height = (max_z - min_z + 1) as u32;
if width == 0 || height == 0 {
return Err("Invalid world bounds".to_string());
}
// Use Mutex for thread-safe image access
let img = Mutex::new(RgbImage::from_pixel(width, height, Rgb([255, 255, 255])));
// Calculate region range
let min_region_x = min_x >> 9; // divide by 512 (32 chunks * 16 blocks)
let max_region_x = max_x >> 9;
let min_region_z = min_z >> 9;
let max_region_z = max_z >> 9;
let region_dir = world_dir.join("region");
// Collect all region coordinates for parallel processing
let region_coords: Vec<(i32, i32)> = (min_region_x..=max_region_x)
.flat_map(|rx| (min_region_z..=max_region_z).map(move |rz| (rx, rz)))
.collect();
// Process regions in parallel
region_coords.par_iter().for_each(|&(region_x, region_z)| {
let region_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
if !region_path.exists() {
return;
}
if let Ok(file) = File::open(&region_path) {
if let Ok(mut region) = Region::from_stream(file) {
// Collect all pixels from this region first
let pixels = render_region_to_pixels(
&mut region,
region_x,
region_z,
min_x,
min_z,
max_x,
max_z,
);
// Then batch-write to image under lock
if !pixels.is_empty() {
let mut img_guard = img.lock().unwrap();
for (x, z, color) in pixels {
if x < img_guard.width() && z < img_guard.height() {
img_guard.put_pixel(x, z, color);
}
}
}
}
}
});
// Save the image
let output_path = world_dir.join("arnis_world_map.png");
img.into_inner()
.unwrap()
.save(&output_path)
.map_err(|e| format!("Failed to save map image: {}", e))?;
Ok(output_path)
}
/// Renders all chunks within a region and returns pixel data
fn render_region_to_pixels(
region: &mut Region<File>,
region_x: i32,
region_z: i32,
min_x: i32,
min_z: i32,
max_x: i32,
max_z: i32,
) -> Vec<(u32, u32, Rgb<u8>)> {
let mut pixels = Vec::new();
let region_base_x = region_x * 512;
let region_base_z = region_z * 512;
for chunk_local_x in 0..32 {
for chunk_local_z in 0..32 {
let chunk_base_x = region_base_x + chunk_local_x * 16;
let chunk_base_z = region_base_z + chunk_local_z * 16;
// Skip chunks outside our bounds
if chunk_base_x + 15 < min_x
|| chunk_base_x > max_x
|| chunk_base_z + 15 < min_z
|| chunk_base_z > max_z
{
continue;
}
if let Ok(Some(chunk_data)) =
region.read_chunk(chunk_local_x as usize, chunk_local_z as usize)
{
render_chunk_to_pixels(
&chunk_data,
&mut pixels,
chunk_base_x,
chunk_base_z,
min_x,
min_z,
max_x,
max_z,
);
}
}
}
pixels
}
/// Renders a single chunk and appends pixel data
#[allow(clippy::too_many_arguments)]
fn render_chunk_to_pixels(
chunk_data: &[u8],
pixels: &mut Vec<(u32, u32, Rgb<u8>)>,
chunk_base_x: i32,
chunk_base_z: i32,
min_x: i32,
min_z: i32,
max_x: i32,
max_z: i32,
) {
// Parse chunk NBT - look for Level.sections or sections depending on format
let chunk: Value = match from_bytes(chunk_data) {
Ok(v) => v,
Err(_) => return,
};
// Try to get sections from the chunk data
let sections = get_sections_from_chunk(&chunk);
if sections.is_empty() {
return;
}
// Pre-sort sections by Y (descending) once per chunk, not per column
let sorted_sections = get_sorted_sections(&sections);
if sorted_sections.is_empty() {
return;
}
// For each column in the chunk
for local_x in 0..16 {
for local_z in 0..16 {
let world_x = chunk_base_x + local_x;
let world_z = chunk_base_z + local_z;
// Skip if outside our bounds
if world_x < min_x || world_x > max_x || world_z < min_z || world_z > max_z {
continue;
}
// Find topmost non-air block using pre-sorted sections
if let Some((block_name, world_y)) =
find_top_block_sorted(&sorted_sections, local_x as usize, local_z as usize)
{
// Strip minecraft: prefix for lookup
let short_name = block_name.strip_prefix("minecraft:").unwrap_or(&block_name);
let base_color = BLOCK_COLORS
.get(short_name)
.copied()
.unwrap_or_else(|| get_fallback_color(&block_name));
// Apply elevation shading
let color = apply_elevation_shading(base_color, world_y);
let img_x = (world_x - min_x) as u32;
let img_z = (world_z - min_z) as u32;
pixels.push((img_x, img_z, color));
}
}
}
}
/// Applies elevation-based shading to a color
/// Higher elevations are brighter, lower are darker
#[inline]
fn apply_elevation_shading(color: Rgb<u8>, y: i32) -> Rgb<u8> {
// Base brightness boost of 10%, plus elevation shading
// Shading range: -20% darker to +20% brighter (asymmetric, more bright than dark)
// Normalize Y to a -1.0 to 1.0 range (roughly)
// y=0 -> -0.5, y=0 -> 0, y=200 -> +1.0
let normalized = (y as f32 / 100.0).clamp(-1.0, 1.0);
// Base 10% brightness boost + asymmetric elevation shading
let elevation_adjust = if normalized >= 0.0 {
// Above sea level: up to +20% brighter
normalized * 0.20
} else {
// Below sea level: up to -20% darker
normalized * 0.20
};
let multiplier = 1.10 + elevation_adjust;
Rgb([
(color.0[0] as f32 * multiplier).clamp(0.0, 255.0) as u8,
(color.0[1] as f32 * multiplier).clamp(0.0, 255.0) as u8,
(color.0[2] as f32 * multiplier).clamp(0.0, 255.0) as u8,
])
}
/// Extracts sections from chunk data (handles both old and new formats)
fn get_sections_from_chunk(chunk: &Value) -> Vec<&Value> {
let mut sections = Vec::new();
// Try new format (1.18+): directly in chunk
if let Value::Compound(map) = chunk {
if let Some(Value::List(secs)) = map.get("sections") {
for sec in secs {
sections.push(sec);
}
return sections;
}
// Try via Level wrapper (older format)
if let Some(Value::Compound(level)) = map.get("Level") {
if let Some(Value::List(secs)) = level.get("sections") {
for sec in secs {
sections.push(sec);
}
}
}
}
sections
}
/// Pre-sorts sections by Y coordinate (descending) - called once per chunk
/// Returns Vec of (section_y, section_value) for Y tracking
fn get_sorted_sections<'a>(sections: &[&'a Value]) -> Vec<(i8, &'a Value)> {
let mut sorted: Vec<(i8, &Value)> = sections
.iter()
.filter_map(|s| {
if let Value::Compound(map) = s {
if let Some(Value::Byte(y)) = map.get("Y") {
return Some((*y, *s));
}
}
None
})
.collect();
sorted.sort_by(|a, b| b.0.cmp(&a.0));
sorted
}
/// Finds the topmost non-air block using pre-sorted sections
/// Returns (block_name, world_y) where world_y is the actual Y coordinate
fn find_top_block_sorted(
sorted_sections: &[(i8, &Value)],
local_x: usize,
local_z: usize,
) -> Option<(String, i32)> {
for (section_y, section) in sorted_sections {
if let Some((block_name, local_y)) = get_block_at_section(section, local_x, local_z) {
if !is_transparent_block(&block_name) {
// Calculate world Y: section_y * 16 + local_y
let world_y = (*section_y as i32) * 16 + local_y as i32;
return Some((block_name, world_y));
}
}
}
None
}
/// Gets the topmost non-air block in a section at the given x,z
/// Returns (block_name, local_y) where local_y is 0-15 within the section
fn get_block_at_section(
section: &Value,
local_x: usize,
local_z: usize,
) -> Option<(String, usize)> {
let section_map = match section {
Value::Compound(m) => m,
_ => return None,
};
let block_states = match section_map.get("block_states") {
Some(Value::Compound(bs)) => bs,
_ => return None,
};
let palette = match block_states.get("palette") {
Some(Value::List(p)) => p,
_ => return None,
};
// If palette has only one block, that's the block for the entire section
if palette.len() == 1 {
// Return with local_y=15 (top of section) for single-block sections
return get_block_name_from_palette(&palette[0]).map(|name| (name, 15));
}
let data = match block_states.get("data") {
Some(Value::LongArray(d)) => d,
_ => return None,
};
// Calculate bits per block
let bits_per_block = std::cmp::max(4, (palette.len() as f64).log2().ceil() as usize);
let blocks_per_long = 64 / bits_per_block;
let mask = (1u64 << bits_per_block) - 1;
// Search from top (y=15) to bottom (y=0) within this section
for local_y in (0..16).rev() {
let block_index = local_y * 256 + local_z * 16 + local_x;
let long_index = block_index / blocks_per_long;
let bit_offset = (block_index % blocks_per_long) * bits_per_block;
if long_index >= data.len() {
continue;
}
let palette_index = ((data[long_index] as u64 >> bit_offset) & mask) as usize;
if palette_index < palette.len() {
if let Some(name) = get_block_name_from_palette(&palette[palette_index]) {
if !is_transparent_block(&name) {
return Some((name, local_y));
}
}
}
}
None
}
/// Extracts block name from a palette entry
fn get_block_name_from_palette(entry: &Value) -> Option<String> {
if let Value::Compound(map) = entry {
if let Some(Value::String(name)) = map.get("Name") {
return Some(name.clone());
}
}
None
}
/// Checks if a block should be considered transparent (look through it)
fn is_transparent_block(name: &str) -> bool {
let short_name = name.strip_prefix("minecraft:").unwrap_or(name);
matches!(
short_name,
"air"
| "cave_air"
| "void_air"
| "glass"
| "glass_pane"
| "white_stained_glass"
| "gray_stained_glass"
| "light_gray_stained_glass"
| "brown_stained_glass"
| "tinted_glass"
| "barrier"
| "light"
| "short_grass"
| "tall_grass"
| "dead_bush"
| "poppy"
| "dandelion"
| "blue_orchid"
| "azure_bluet"
| "iron_bars"
| "ladder"
| "scaffolding"
| "rail"
| "powered_rail"
| "detector_rail"
| "activator_rail"
)
}
/// Returns a fallback color based on block name patterns
fn get_fallback_color(name: &str) -> Rgb<u8> {
// Try to guess color from name
if name.contains("stone") || name.contains("cobble") || name.contains("andesite") {
return Rgb([128, 128, 128]);
}
if name.contains("dirt") || name.contains("mud") {
return Rgb([139, 90, 43]);
}
if name.contains("sand") {
return Rgb([219, 211, 160]);
}
if name.contains("grass") {
return Rgb([86, 125, 70]);
}
if name.contains("water") {
return Rgb([59, 86, 165]);
}
if name.contains("log") || name.contains("wood") {
return Rgb([101, 76, 48]);
}
if name.contains("leaves") {
return Rgb([55, 95, 36]);
}
if name.contains("planks") {
return Rgb([162, 130, 78]);
}
if name.contains("brick") {
return Rgb([150, 97, 83]);
}
if name.contains("concrete") {
return Rgb([128, 128, 128]);
}
if name.contains("wool") || name.contains("carpet") {
return Rgb([220, 220, 220]);
}
if name.contains("terracotta") {
return Rgb([152, 94, 67]);
}
if name.contains("iron") {
return Rgb([200, 200, 200]);
}
if name.contains("gold") {
return Rgb([255, 215, 0]);
}
if name.contains("diamond") {
return Rgb([97, 219, 213]);
}
if name.contains("emerald") {
return Rgb([17, 160, 54]);
}
if name.contains("lapis") {
return Rgb([38, 67, 156]);
}
if name.contains("redstone") {
return Rgb([170, 0, 0]);
}
if name.contains("netherrack") || name.contains("nether") {
return Rgb([111, 54, 53]);
}
if name.contains("end_stone") {
return Rgb([219, 222, 158]);
}
if name.contains("obsidian") {
return Rgb([15, 10, 24]);
}
if name.contains("deepslate") {
return Rgb([72, 72, 73]);
}
if name.contains("blackstone") {
return Rgb([42, 36, 41]);
}
if name.contains("quartz") {
return Rgb([235, 229, 222]);
}
if name.contains("prismarine") {
return Rgb([76, 128, 113]);
}
if name.contains("copper") {
return Rgb([192, 107, 79]);
}
if name.contains("amethyst") {
return Rgb([133, 97, 191]);
}
if name.contains("moss") {
return Rgb([89, 109, 45]);
}
if name.contains("dripstone") {
return Rgb([134, 107, 92]);
}
// Default gray for unknown blocks
Rgb([160, 160, 160])
}
/// Returns a mapping of common block names to RGB colors (without minecraft: prefix)
fn get_block_colors() -> FnvHashMap<&'static str, Rgb<u8>> {
FnvHashMap::from_iter([
("grass_block", Rgb([86, 125, 70])),
("short_grass", Rgb([86, 125, 70])),
("tall_grass", Rgb([86, 125, 70])),
("dirt", Rgb([139, 90, 43])),
("coarse_dirt", Rgb([119, 85, 59])),
("podzol", Rgb([91, 63, 24])),
("rooted_dirt", Rgb([144, 103, 76])),
("mud", Rgb([60, 57, 61])),
("stone", Rgb([128, 128, 128])),
("granite", Rgb([149, 108, 91])),
("polished_granite", Rgb([154, 112, 98])),
("diorite", Rgb([189, 188, 189])),
("polished_diorite", Rgb([195, 195, 195])),
("andesite", Rgb([136, 136, 137])),
("polished_andesite", Rgb([132, 135, 134])),
("deepslate", Rgb([72, 72, 73])),
("cobbled_deepslate", Rgb([77, 77, 80])),
("polished_deepslate", Rgb([72, 72, 73])),
("deepslate_bricks", Rgb([70, 70, 71])),
("deepslate_tiles", Rgb([54, 54, 55])),
("calcite", Rgb([223, 224, 220])),
("tuff", Rgb([108, 109, 102])),
("dripstone_block", Rgb([134, 107, 92])),
("sand", Rgb([219, 211, 160])),
("red_sand", Rgb([190, 102, 33])),
("gravel", Rgb([131, 127, 126])),
("clay", Rgb([160, 166, 179])),
("bedrock", Rgb([85, 85, 85])),
("water", Rgb([59, 86, 165])),
("ice", Rgb([145, 183, 253])),
("packed_ice", Rgb([141, 180, 250])),
("blue_ice", Rgb([116, 167, 253])),
("snow", Rgb([249, 254, 254])),
("snow_block", Rgb([249, 254, 254])),
("powder_snow", Rgb([248, 253, 253])),
("oak_log", Rgb([109, 85, 50])),
("oak_planks", Rgb([162, 130, 78])),
("oak_slab", Rgb([162, 130, 78])),
("oak_stairs", Rgb([162, 130, 78])),
("oak_fence", Rgb([162, 130, 78])),
("oak_door", Rgb([162, 130, 78])),
("spruce_log", Rgb([58, 37, 16])),
("spruce_planks", Rgb([115, 85, 49])),
("spruce_slab", Rgb([115, 85, 49])),
("spruce_stairs", Rgb([115, 85, 49])),
("spruce_fence", Rgb([115, 85, 49])),
("spruce_door", Rgb([115, 85, 49])),
("birch_log", Rgb([216, 215, 210])),
("birch_planks", Rgb([196, 179, 123])),
("birch_slab", Rgb([196, 179, 123])),
("birch_stairs", Rgb([196, 179, 123])),
("birch_fence", Rgb([196, 179, 123])),
("birch_door", Rgb([196, 179, 123])),
("jungle_log", Rgb([85, 68, 25])),
("jungle_planks", Rgb([160, 115, 81])),
("acacia_log", Rgb([103, 96, 86])),
("acacia_planks", Rgb([168, 90, 50])),
("dark_oak_log", Rgb([60, 46, 26])),
("dark_oak_planks", Rgb([67, 43, 20])),
("dark_oak_slab", Rgb([67, 43, 20])),
("dark_oak_stairs", Rgb([67, 43, 20])),
("dark_oak_fence", Rgb([67, 43, 20])),
("dark_oak_door", Rgb([67, 43, 20])),
("mangrove_log", Rgb([84, 66, 36])),
("mangrove_planks", Rgb([117, 54, 48])),
("cherry_log", Rgb([54, 33, 44])),
("cherry_planks", Rgb([226, 178, 172])),
("bamboo_block", Rgb([122, 129, 52])),
("bamboo_planks", Rgb([194, 175, 93])),
("crimson_stem", Rgb([92, 25, 29])),
("crimson_planks", Rgb([101, 48, 70])),
("warped_stem", Rgb([58, 58, 77])),
("warped_planks", Rgb([43, 104, 99])),
("oak_leaves", Rgb([55, 95, 36])),
("spruce_leaves", Rgb([61, 99, 61])),
("birch_leaves", Rgb([80, 106, 47])),
("jungle_leaves", Rgb([48, 113, 20])),
("acacia_leaves", Rgb([75, 104, 40])),
("dark_oak_leaves", Rgb([35, 82, 11])),
("mangrove_leaves", Rgb([69, 123, 38])),
("cherry_leaves", Rgb([228, 177, 197])),
("azalea_leaves", Rgb([71, 96, 37])),
("stone_bricks", Rgb([122, 122, 122])),
("stone_brick_slab", Rgb([122, 122, 122])),
("stone_brick_stairs", Rgb([122, 122, 122])),
("stone_brick_wall", Rgb([122, 122, 122])),
("mossy_stone_bricks", Rgb([115, 121, 105])),
("mossy_stone_brick_slab", Rgb([115, 121, 105])),
("mossy_stone_brick_stairs", Rgb([115, 121, 105])),
("mossy_stone_brick_wall", Rgb([115, 121, 105])),
("cracked_stone_bricks", Rgb([118, 117, 118])),
("chiseled_stone_bricks", Rgb([119, 119, 119])),
("cobblestone", Rgb([128, 127, 127])),
("cobblestone_slab", Rgb([128, 127, 127])),
("cobblestone_stairs", Rgb([128, 127, 127])),
("cobblestone_wall", Rgb([128, 127, 127])),
("mossy_cobblestone", Rgb([110, 118, 94])),
("mossy_cobblestone_slab", Rgb([110, 118, 94])),
("mossy_cobblestone_stairs", Rgb([110, 118, 94])),
("mossy_cobblestone_wall", Rgb([110, 118, 94])),
("stone_slab", Rgb([128, 128, 128])),
("stone_stairs", Rgb([128, 128, 128])),
("smooth_stone", Rgb([158, 158, 158])),
("smooth_stone_slab", Rgb([158, 158, 158])),
("bricks", Rgb([150, 97, 83])),
("brick_slab", Rgb([150, 97, 83])),
("brick_stairs", Rgb([150, 97, 83])),
("brick_wall", Rgb([150, 97, 83])),
("mud_bricks", Rgb([137, 103, 79])),
("mud_brick_slab", Rgb([137, 103, 79])),
("mud_brick_stairs", Rgb([137, 103, 79])),
("mud_brick_wall", Rgb([137, 103, 79])),
("terracotta", Rgb([152, 94, 67])),
("white_terracotta", Rgb([210, 178, 161])),
("orange_terracotta", Rgb([162, 84, 38])),
("magenta_terracotta", Rgb([149, 88, 109])),
("light_blue_terracotta", Rgb([113, 109, 138])),
("yellow_terracotta", Rgb([186, 133, 35])),
("lime_terracotta", Rgb([104, 118, 53])),
("pink_terracotta", Rgb([162, 78, 79])),
("gray_terracotta", Rgb([58, 42, 36])),
("light_gray_terracotta", Rgb([135, 107, 98])),
("cyan_terracotta", Rgb([87, 91, 91])),
("purple_terracotta", Rgb([118, 70, 86])),
("blue_terracotta", Rgb([74, 60, 91])),
("brown_terracotta", Rgb([77, 51, 36])),
("green_terracotta", Rgb([76, 83, 42])),
("red_terracotta", Rgb([143, 61, 47])),
("black_terracotta", Rgb([37, 23, 16])),
("white_concrete", Rgb([207, 213, 214])),
("orange_concrete", Rgb([224, 97, 0])),
("magenta_concrete", Rgb([169, 48, 159])),
("light_blue_concrete", Rgb([35, 137, 198])),
("yellow_concrete", Rgb([241, 175, 21])),
("lime_concrete", Rgb([94, 169, 24])),
("pink_concrete", Rgb([214, 101, 143])),
("gray_concrete", Rgb([55, 58, 62])),
("light_gray_concrete", Rgb([125, 125, 115])),
("cyan_concrete", Rgb([21, 119, 136])),
("purple_concrete", Rgb([100, 32, 156])),
("blue_concrete", Rgb([45, 47, 143])),
("brown_concrete", Rgb([96, 60, 32])),
("green_concrete", Rgb([73, 91, 36])),
("red_concrete", Rgb([142, 33, 33])),
("black_concrete", Rgb([8, 10, 15])),
("white_wool", Rgb([234, 236, 237])),
("orange_wool", Rgb([241, 118, 20])),
("magenta_wool", Rgb([190, 68, 179])),
("light_blue_wool", Rgb([58, 175, 217])),
("yellow_wool", Rgb([249, 198, 40])),
("lime_wool", Rgb([112, 185, 26])),
("pink_wool", Rgb([238, 141, 172])),
("gray_wool", Rgb([63, 68, 72])),
("light_gray_wool", Rgb([142, 142, 135])),
("cyan_wool", Rgb([21, 138, 145])),
("purple_wool", Rgb([122, 42, 173])),
("blue_wool", Rgb([53, 57, 157])),
("brown_wool", Rgb([114, 72, 41])),
("green_wool", Rgb([85, 110, 28])),
("red_wool", Rgb([161, 39, 35])),
("black_wool", Rgb([21, 21, 26])),
("sandstone", Rgb([223, 214, 170])),
("sandstone_slab", Rgb([223, 214, 170])),
("sandstone_stairs", Rgb([223, 214, 170])),
("sandstone_wall", Rgb([223, 214, 170])),
("chiseled_sandstone", Rgb([223, 214, 170])),
("cut_sandstone", Rgb([225, 217, 171])),
("cut_sandstone_slab", Rgb([225, 217, 171])),
("smooth_sandstone", Rgb([223, 214, 170])),
("smooth_sandstone_slab", Rgb([223, 214, 170])),
("smooth_sandstone_stairs", Rgb([223, 214, 170])),
("red_sandstone", Rgb([186, 99, 29])),
("red_sandstone_slab", Rgb([186, 99, 29])),
("red_sandstone_stairs", Rgb([186, 99, 29])),
("red_sandstone_wall", Rgb([186, 99, 29])),
("smooth_red_sandstone", Rgb([186, 99, 29])),
("netherrack", Rgb([111, 54, 53])),
("nether_bricks", Rgb([44, 21, 26])),
("nether_brick_slab", Rgb([44, 21, 26])),
("nether_brick_stairs", Rgb([44, 21, 26])),
("nether_brick_wall", Rgb([44, 21, 26])),
("nether_brick_fence", Rgb([44, 21, 26])),
("red_nether_bricks", Rgb([69, 7, 9])),
("red_nether_brick_slab", Rgb([69, 7, 9])),
("red_nether_brick_stairs", Rgb([69, 7, 9])),
("red_nether_brick_wall", Rgb([69, 7, 9])),
("soul_sand", Rgb([81, 62, 51])),
("soul_soil", Rgb([75, 57, 46])),
("basalt", Rgb([73, 72, 77])),
("polished_basalt", Rgb([88, 87, 91])),
("smooth_basalt", Rgb([72, 72, 78])),
("blackstone", Rgb([42, 36, 41])),
("blackstone_slab", Rgb([42, 36, 41])),
("blackstone_stairs", Rgb([42, 36, 41])),
("blackstone_wall", Rgb([42, 36, 41])),
("polished_blackstone", Rgb([53, 49, 56])),
("polished_blackstone_bricks", Rgb([48, 43, 50])),
("polished_blackstone_brick_slab", Rgb([48, 43, 50])),
("polished_blackstone_brick_stairs", Rgb([48, 43, 50])),
("polished_blackstone_brick_wall", Rgb([48, 43, 50])),
("glowstone", Rgb([171, 131, 84])),
("shroomlight", Rgb([240, 146, 70])),
("crying_obsidian", Rgb([32, 10, 60])),
("obsidian", Rgb([15, 10, 24])),
("end_stone", Rgb([219, 222, 158])),
("end_stone_bricks", Rgb([218, 224, 162])),
("end_stone_brick_slab", Rgb([218, 224, 162])),
("end_stone_brick_stairs", Rgb([218, 224, 162])),
("end_stone_brick_wall", Rgb([218, 224, 162])),
("purpur_block", Rgb([170, 126, 170])),
("purpur_pillar", Rgb([171, 129, 171])),
("purpur_slab", Rgb([170, 126, 170])),
("purpur_stairs", Rgb([170, 126, 170])),
("coal_ore", Rgb([105, 105, 105])),
("iron_ore", Rgb([136, 130, 127])),
("copper_ore", Rgb([124, 125, 120])),
("gold_ore", Rgb([143, 140, 125])),
("redstone_ore", Rgb([133, 107, 107])),
("emerald_ore", Rgb([108, 136, 115])),
("lapis_ore", Rgb([99, 112, 135])),
("diamond_ore", Rgb([121, 141, 140])),
("coal_block", Rgb([16, 15, 15])),
("iron_block", Rgb([220, 220, 220])),
("copper_block", Rgb([192, 107, 79])),
("gold_block", Rgb([246, 208, 62])),
("redstone_block", Rgb([170, 0, 0])),
("emerald_block", Rgb([42, 203, 88])),
("lapis_block", Rgb([38, 67, 156])),
("diamond_block", Rgb([97, 219, 213])),
("netherite_block", Rgb([66, 61, 63])),
("amethyst_block", Rgb([133, 97, 191])),
("raw_iron_block", Rgb([166, 136, 107])),
("raw_copper_block", Rgb([154, 105, 79])),
("raw_gold_block", Rgb([221, 169, 46])),
("quartz_block", Rgb([235, 229, 222])),
("quartz_slab", Rgb([235, 229, 222])),
("quartz_stairs", Rgb([235, 229, 222])),
("smooth_quartz", Rgb([235, 229, 222])),
("smooth_quartz_slab", Rgb([235, 229, 222])),
("smooth_quartz_stairs", Rgb([235, 229, 222])),
("quartz_bricks", Rgb([234, 229, 221])),
("quartz_pillar", Rgb([235, 230, 224])),
("chiseled_quartz_block", Rgb([231, 226, 218])),
("prismarine", Rgb([76, 128, 113])),
("prismarine_slab", Rgb([76, 128, 113])),
("prismarine_stairs", Rgb([76, 128, 113])),
("prismarine_wall", Rgb([76, 128, 113])),
("prismarine_bricks", Rgb([99, 172, 158])),
("prismarine_brick_slab", Rgb([99, 172, 158])),
("prismarine_brick_stairs", Rgb([99, 172, 158])),
("dark_prismarine", Rgb([51, 91, 75])),
("dark_prismarine_slab", Rgb([51, 91, 75])),
("dark_prismarine_stairs", Rgb([51, 91, 75])),
("sea_lantern", Rgb([172, 199, 190])),
("exposed_copper", Rgb([161, 125, 103])),
("weathered_copper", Rgb([109, 145, 107])),
("oxidized_copper", Rgb([82, 162, 132])),
("cut_copper", Rgb([191, 106, 80])),
("cut_copper_slab", Rgb([191, 106, 80])),
("cut_copper_stairs", Rgb([191, 106, 80])),
("exposed_cut_copper", Rgb([154, 121, 101])),
("exposed_cut_copper_slab", Rgb([154, 121, 101])),
("exposed_cut_copper_stairs", Rgb([154, 121, 101])),
("weathered_cut_copper", Rgb([109, 145, 107])),
("weathered_cut_copper_slab", Rgb([109, 145, 107])),
("weathered_cut_copper_stairs", Rgb([109, 145, 107])),
("oxidized_cut_copper", Rgb([79, 153, 126])),
("oxidized_cut_copper_slab", Rgb([79, 153, 126])),
("oxidized_cut_copper_stairs", Rgb([79, 153, 126])),
("glass", Rgb([200, 220, 230])),
("glass_pane", Rgb([200, 220, 230])),
("white_stained_glass", Rgb([255, 255, 255])),
("white_stained_glass_pane", Rgb([255, 255, 255])),
("orange_stained_glass", Rgb([216, 127, 51])),
("orange_stained_glass_pane", Rgb([216, 127, 51])),
("magenta_stained_glass", Rgb([178, 76, 216])),
("magenta_stained_glass_pane", Rgb([178, 76, 216])),
("light_blue_stained_glass", Rgb([102, 153, 216])),
("light_blue_stained_glass_pane", Rgb([102, 153, 216])),
("yellow_stained_glass", Rgb([229, 229, 51])),
("yellow_stained_glass_pane", Rgb([229, 229, 51])),
("lime_stained_glass", Rgb([127, 204, 25])),
("lime_stained_glass_pane", Rgb([127, 204, 25])),
("pink_stained_glass", Rgb([242, 127, 165])),
("pink_stained_glass_pane", Rgb([242, 127, 165])),
("gray_stained_glass", Rgb([76, 76, 76])),
("gray_stained_glass_pane", Rgb([76, 76, 76])),
("light_gray_stained_glass", Rgb([153, 153, 153])),
("light_gray_stained_glass_pane", Rgb([153, 153, 153])),
("cyan_stained_glass", Rgb([76, 127, 153])),
("cyan_stained_glass_pane", Rgb([76, 127, 153])),
("purple_stained_glass", Rgb([127, 63, 178])),
("purple_stained_glass_pane", Rgb([127, 63, 178])),
("blue_stained_glass", Rgb([51, 76, 178])),
("blue_stained_glass_pane", Rgb([51, 76, 178])),
("brown_stained_glass", Rgb([102, 76, 51])),
("brown_stained_glass_pane", Rgb([102, 76, 51])),
("green_stained_glass", Rgb([102, 127, 51])),
("green_stained_glass_pane", Rgb([102, 127, 51])),
("red_stained_glass", Rgb([153, 51, 51])),
("red_stained_glass_pane", Rgb([153, 51, 51])),
("black_stained_glass", Rgb([25, 25, 25])),
("black_stained_glass_pane", Rgb([25, 25, 25])),
("bookshelf", Rgb([116, 89, 53])),
("hay_block", Rgb([166, 139, 12])),
("melon", Rgb([111, 145, 31])),
("pumpkin", Rgb([198, 118, 24])),
("jack_o_lantern", Rgb([213, 139, 42])),
("carved_pumpkin", Rgb([198, 118, 24])),
("tnt", Rgb([219, 68, 52])),
("sponge", Rgb([195, 192, 74])),
("wet_sponge", Rgb([171, 181, 70])),
("moss_block", Rgb([89, 109, 45])),
("moss_carpet", Rgb([89, 109, 45])),
("sculk", Rgb([12, 28, 36])),
("honeycomb_block", Rgb([229, 148, 29])),
("slime_block", Rgb([111, 192, 91])),
("honey_block", Rgb([251, 185, 52])),
("barrel", Rgb([140, 106, 60])),
("chest", Rgb([155, 113, 48])),
("trapped_chest", Rgb([155, 113, 48])),
("crafting_table", Rgb([144, 109, 67])),
("furnace", Rgb([110, 110, 110])),
("blast_furnace", Rgb([80, 80, 85])),
("smoker", Rgb([90, 80, 70])),
("anvil", Rgb([68, 68, 68])),
("lectern", Rgb([180, 140, 90])),
("composter", Rgb([100, 80, 45])),
("cauldron", Rgb([60, 60, 60])),
("hopper", Rgb([70, 70, 70])),
("jukebox", Rgb([130, 90, 70])),
("note_block", Rgb([120, 80, 65])),
("bell", Rgb([200, 170, 50])),
("dirt_path", Rgb([148, 121, 65])),
("farmland", Rgb([143, 88, 46])),
("mycelium", Rgb([111, 99, 107])),
("rail", Rgb([125, 108, 77])),
("powered_rail", Rgb([153, 126, 55])),
("detector_rail", Rgb([120, 97, 80])),
("activator_rail", Rgb([117, 85, 76])),
("redstone_wire", Rgb([170, 0, 0])),
("redstone_torch", Rgb([170, 0, 0])),
("redstone_lamp", Rgb([180, 130, 70])),
("lever", Rgb([100, 80, 60])),
("tripwire_hook", Rgb([120, 100, 80])),
("torch", Rgb([255, 200, 100])),
("wall_torch", Rgb([255, 200, 100])),
("lantern", Rgb([200, 150, 80])),
("soul_lantern", Rgb([80, 200, 200])),
("soul_torch", Rgb([80, 200, 200])),
("soul_wall_torch", Rgb([80, 200, 200])),
("campfire", Rgb([200, 100, 50])),
("soul_campfire", Rgb([80, 200, 200])),
("candle", Rgb([200, 180, 130])),
("dandelion", Rgb([255, 236, 85])),
("poppy", Rgb([200, 30, 30])),
("blue_orchid", Rgb([47, 186, 199])),
("allium", Rgb([190, 130, 200])),
("azure_bluet", Rgb([220, 230, 220])),
("red_tulip", Rgb([200, 50, 50])),
("orange_tulip", Rgb([230, 130, 50])),
("white_tulip", Rgb([230, 230, 220])),
("pink_tulip", Rgb([220, 150, 170])),
("oxeye_daisy", Rgb([230, 230, 200])),
("cornflower", Rgb([70, 90, 180])),
("lily_of_the_valley", Rgb([230, 230, 230])),
("wither_rose", Rgb([30, 30, 30])),
("sunflower", Rgb([255, 200, 50])),
("lilac", Rgb([200, 150, 200])),
("rose_bush", Rgb([180, 40, 40])),
("peony", Rgb([230, 180, 200])),
("fern", Rgb([80, 120, 60])),
("large_fern", Rgb([80, 120, 60])),
("dead_bush", Rgb([150, 120, 80])),
("seagrass", Rgb([40, 100, 60])),
("tall_seagrass", Rgb([40, 100, 60])),
("kelp", Rgb([50, 110, 60])),
("kelp_plant", Rgb([50, 110, 60])),
("sugar_cane", Rgb([140, 180, 100])),
("bamboo", Rgb([90, 140, 50])),
("vine", Rgb([50, 100, 40])),
("lily_pad", Rgb([40, 110, 40])),
("sweet_berry_bush", Rgb([60, 90, 50])),
("cactus", Rgb([85, 127, 52])),
("white_carpet", Rgb([234, 236, 237])),
("orange_carpet", Rgb([241, 118, 20])),
("magenta_carpet", Rgb([190, 68, 179])),
("light_blue_carpet", Rgb([58, 175, 217])),
("yellow_carpet", Rgb([249, 198, 40])),
("lime_carpet", Rgb([112, 185, 26])),
("pink_carpet", Rgb([238, 141, 172])),
("gray_carpet", Rgb([63, 68, 72])),
("light_gray_carpet", Rgb([142, 142, 135])),
("cyan_carpet", Rgb([21, 138, 145])),
("purple_carpet", Rgb([122, 42, 173])),
("blue_carpet", Rgb([53, 57, 157])),
("brown_carpet", Rgb([114, 72, 41])),
("green_carpet", Rgb([85, 110, 28])),
("red_carpet", Rgb([161, 39, 35])),
("black_carpet", Rgb([21, 21, 26])),
("oak_sign", Rgb([162, 130, 78])),
("oak_wall_sign", Rgb([162, 130, 78])),
("spruce_sign", Rgb([115, 85, 49])),
("spruce_wall_sign", Rgb([115, 85, 49])),
("birch_sign", Rgb([196, 179, 123])),
("birch_wall_sign", Rgb([196, 179, 123])),
("dark_oak_sign", Rgb([67, 43, 20])),
("dark_oak_wall_sign", Rgb([67, 43, 20])),
("white_bed", Rgb([234, 236, 237])),
("orange_bed", Rgb([241, 118, 20])),
("magenta_bed", Rgb([190, 68, 179])),
("light_blue_bed", Rgb([58, 175, 217])),
("yellow_bed", Rgb([249, 198, 40])),
("lime_bed", Rgb([112, 185, 26])),
("pink_bed", Rgb([238, 141, 172])),
("gray_bed", Rgb([63, 68, 72])),
("light_gray_bed", Rgb([142, 142, 135])),
("cyan_bed", Rgb([21, 138, 145])),
("purple_bed", Rgb([122, 42, 173])),
("blue_bed", Rgb([53, 57, 157])),
("brown_bed", Rgb([114, 72, 41])),
("green_bed", Rgb([85, 110, 28])),
("red_bed", Rgb([161, 39, 35])),
("black_bed", Rgb([21, 21, 26])),
("oak_trapdoor", Rgb([162, 130, 78])),
("spruce_trapdoor", Rgb([115, 85, 49])),
("birch_trapdoor", Rgb([196, 179, 123])),
("dark_oak_trapdoor", Rgb([67, 43, 20])),
("iron_trapdoor", Rgb([200, 200, 200])),
("iron_bars", Rgb([150, 150, 150])),
("ladder", Rgb([160, 130, 70])),
("wheat", Rgb([200, 180, 80])),
("carrots", Rgb([230, 140, 30])),
("potatoes", Rgb([180, 160, 80])),
("beetroots", Rgb([150, 50, 50])),
("pumpkin_stem", Rgb([120, 140, 70])),
("melon_stem", Rgb([120, 140, 70])),
])
}

View File

@@ -1,3 +1,4 @@
use crate::clipping::clip_way_to_bbox;
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
use crate::coordinate_system::transformation::CoordTransformer;
@@ -211,7 +212,14 @@ pub fn parse_osm_data(
nodes_map.insert(element.id, processed.clone());
processed_elements.push(ProcessedElement::Node(processed));
// Only add tagged nodes to processed_elements if they're within or near the bbox
// This significantly improves performance by filtering out distant nodes
if !element.tags.as_ref().map(|t| t.is_empty()).unwrap_or(true) {
// Node has tags, check if it's in the bbox (with some margin)
if xzbbox.contains(&xzpoint) {
processed_elements.push(ProcessedElement::Node(processed));
}
}
}
}
@@ -226,13 +234,33 @@ pub fn parse_osm_data(
}
}
// Clip the way to bbox to reduce node count dramatically
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(),
},
);
// Clip way nodes for standalone way processing (not relations)
let clipped_nodes = clip_way_to_bbox(&nodes, &xzbbox);
// Skip ways that are completely outside the bbox (empty after clipping)
if clipped_nodes.is_empty() {
continue;
}
let processed: ProcessedWay = ProcessedWay {
id: element.id,
tags: element.tags.clone().unwrap_or_default(),
nodes,
tags: tags.clone(),
nodes: clipped_nodes.clone(),
};
ways_map.insert(element.id, processed.clone());
processed_elements.push(ProcessedElement::Way(processed));
}
@@ -247,6 +275,9 @@ pub fn parse_osm_data(
continue;
};
// Water relations require unclipped ways for ring merging in water_areas.rs
let is_water_relation = is_water_element(tags);
let members: Vec<ProcessedMember> = element
.members
.iter()
@@ -271,7 +302,26 @@ pub fn parse_osm_data(
}
};
Some(ProcessedMember { role, way })
// Water relations: keep unclipped for ring merging
// Non-water relations: clip member ways now
let final_way = if is_water_relation {
way
} else {
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
if clipped_nodes.is_empty() {
return None;
}
ProcessedWay {
id: way.id,
tags: way.tags,
nodes: clipped_nodes,
}
};
Some(ProcessedMember {
role,
way: final_way,
})
})
.collect();
@@ -289,6 +339,30 @@ pub fn parse_osm_data(
(processed_elements, xzbbox)
}
/// Returns true if tags indicate a water element handled by water_areas.rs.
fn is_water_element(tags: &HashMap<String, String>) -> bool {
// Check for explicit water tag
if tags.contains_key("water") {
return true;
}
// Check for natural=water or natural=bay
if let Some(natural_val) = tags.get("natural") {
if natural_val == "water" || natural_val == "bay" {
return true;
}
}
// Check for waterway=dock (also handled as water area)
if let Some(waterway_val) = tags.get("waterway") {
if waterway_val == "dock" {
return true;
}
}
false
}
const PRIORITY_ORDER: [&str; 6] = [
"entrance", "building", "highway", "waterway", "water", "barrier",
];

View File

@@ -56,3 +56,21 @@ pub fn emit_gui_error(message: &str) {
};
emit_gui_progress_update(0.0, &format!("Error! {truncated_message}"));
}
/// Emits an event when the world map preview is ready
pub fn emit_map_preview_ready() {
if let Some(window) = get_main_window() {
if let Err(e) = window.emit("map-preview-ready", ()) {
eprintln!("Failed to emit map-preview-ready event: {}", e);
}
}
}
/// Emits an event to open the generated mcworld file
pub fn emit_open_mcworld_file(path: &str) {
if let Some(window) = get_main_window() {
if let Err(e) = window.emit("open-mcworld-file", path) {
eprintln!("Failed to emit open-mcworld-file event: {}", e);
}
}
}

View File

File diff suppressed because it is too large Load Diff

1070
src/world_editor/bedrock.rs Normal file
View File

File diff suppressed because it is too large Load Diff

312
src/world_editor/common.rs Normal file
View File

@@ -0,0 +1,312 @@
//! Common data structures for world modification.
//!
//! This module contains the internal data structures used to track block changes
//! before they are written to either Java or Bedrock format.
use crate::block_definitions::*;
use fastnbt::{LongArray, Value};
use fnv::FnvHashMap;
use serde::{Deserialize, Serialize};
/// Chunk structure for Java Edition NBT format
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Chunk {
pub sections: Vec<Section>,
pub x_pos: i32,
pub z_pos: i32,
#[serde(default)]
pub is_light_on: u8,
#[serde(flatten)]
pub other: FnvHashMap<String, Value>,
}
/// Section within a chunk (16x16x16 blocks)
#[derive(Serialize, Deserialize)]
pub(crate) struct Section {
pub block_states: Blockstates,
#[serde(rename = "Y")]
pub y: i8,
#[serde(flatten)]
pub other: FnvHashMap<String, Value>,
}
/// Block states within a section
#[derive(Serialize, Deserialize)]
pub(crate) struct Blockstates {
pub palette: Vec<PaletteItem>,
pub data: Option<LongArray>,
#[serde(flatten)]
pub other: FnvHashMap<String, Value>,
}
/// Palette item for block state encoding
#[derive(Serialize, Deserialize)]
pub(crate) struct PaletteItem {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Properties")]
pub properties: Option<Value>,
}
/// A section being modified (16x16x16 blocks)
pub(crate) struct SectionToModify {
pub blocks: [Block; 4096],
/// Store properties for blocks that have them, indexed by the same index as blocks array
pub properties: FnvHashMap<usize, Value>,
}
impl SectionToModify {
#[inline]
pub fn get_block(&self, x: u8, y: u8, z: u8) -> Option<Block> {
let b = self.blocks[Self::index(x, y, z)];
if b == AIR {
return None;
}
Some(b)
}
#[inline]
pub fn set_block(&mut self, x: u8, y: u8, z: u8, block: Block) {
self.blocks[Self::index(x, y, z)] = block;
}
#[inline]
pub fn set_block_with_properties(
&mut self,
x: u8,
y: u8,
z: u8,
block_with_props: BlockWithProperties,
) {
let index = Self::index(x, y, z);
self.blocks[index] = block_with_props.block;
// Store properties if they exist
if let Some(props) = block_with_props.properties {
self.properties.insert(index, props);
} else {
// Remove any existing properties for this position
self.properties.remove(&index);
}
}
/// Calculate index from coordinates (YZX order)
#[inline(always)]
pub fn index(x: u8, y: u8, z: u8) -> usize {
usize::from(y) % 16 * 256 + usize::from(z) * 16 + usize::from(x)
}
/// Convert to Java Edition section format
pub fn to_section(&self, y: i8) -> Section {
// Create a map of unique block+properties combinations to palette indices
let mut unique_blocks: Vec<(Block, Option<Value>)> = Vec::new();
let mut palette_lookup: FnvHashMap<(Block, Option<String>), usize> = FnvHashMap::default();
// Build unique block combinations and lookup table
for (i, &block) in self.blocks.iter().enumerate() {
let properties = self.properties.get(&i).cloned();
// Create a key for the lookup (block + properties hash)
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
let lookup_key = (block, props_key);
if let std::collections::hash_map::Entry::Vacant(e) = palette_lookup.entry(lookup_key) {
let palette_index = unique_blocks.len();
e.insert(palette_index);
unique_blocks.push((block, properties));
}
}
let mut bits_per_block = 4; // minimum allowed
while (1 << bits_per_block) < unique_blocks.len() {
bits_per_block += 1;
}
let mut data = vec![];
let mut cur = 0;
let mut cur_idx = 0;
for (i, &block) in self.blocks.iter().enumerate() {
let properties = self.properties.get(&i).cloned();
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
let lookup_key = (block, props_key);
let p = palette_lookup[&lookup_key] as i64;
if cur_idx + bits_per_block > 64 {
data.push(cur);
cur = 0;
cur_idx = 0;
}
cur |= p << cur_idx;
cur_idx += bits_per_block;
}
if cur_idx > 0 {
data.push(cur);
}
let palette = unique_blocks
.iter()
.map(|(block, stored_props)| PaletteItem {
name: block.name().to_string(),
properties: stored_props.clone().or_else(|| block.properties()),
})
.collect();
Section {
block_states: Blockstates {
palette,
data: Some(LongArray::new(data)),
other: FnvHashMap::default(),
},
y,
other: FnvHashMap::default(),
}
}
}
impl Default for SectionToModify {
fn default() -> Self {
Self {
blocks: [AIR; 4096],
properties: FnvHashMap::default(),
}
}
}
/// A chunk being modified (16x384x16 blocks, divided into sections)
#[derive(Default)]
pub(crate) struct ChunkToModify {
pub sections: FnvHashMap<i8, SectionToModify>,
pub other: FnvHashMap<String, Value>,
}
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();
let section = self.sections.get(&section_idx)?;
section.get_block(x, (y & 15).try_into().unwrap(), 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();
let section = self.sections.entry(section_idx).or_default();
section.set_block(x, (y & 15).try_into().unwrap(), z, block);
}
#[inline]
pub fn set_block_with_properties(
&mut self,
x: u8,
y: i32,
z: u8,
block_with_props: BlockWithProperties,
) {
let section_idx: i8 = (y >> 4).try_into().unwrap();
let section = self.sections.entry(section_idx).or_default();
section.set_block_with_properties(x, (y & 15).try_into().unwrap(), z, block_with_props);
}
pub fn sections(&self) -> impl Iterator<Item = Section> + '_ {
self.sections.iter().map(|(y, s)| s.to_section(*y))
}
}
/// A region being modified (32x32 chunks)
#[derive(Default)]
pub(crate) struct RegionToModify {
pub chunks: FnvHashMap<(i32, i32), ChunkToModify>,
}
impl RegionToModify {
#[inline]
pub fn get_or_create_chunk(&mut self, x: i32, z: i32) -> &mut ChunkToModify {
self.chunks.entry((x, z)).or_default()
}
#[inline]
pub fn get_chunk(&self, x: i32, z: i32) -> Option<&ChunkToModify> {
self.chunks.get(&(x, z))
}
}
/// The entire world being modified
#[derive(Default)]
pub(crate) struct WorldToModify {
pub regions: FnvHashMap<(i32, i32), RegionToModify>,
}
impl WorldToModify {
#[inline]
pub fn get_or_create_region(&mut self, x: i32, z: i32) -> &mut RegionToModify {
self.regions.entry((x, z)).or_default()
}
#[inline]
pub fn get_region(&self, x: i32, z: i32) -> Option<&RegionToModify> {
self.regions.get(&(x, z))
}
#[inline]
pub fn get_block(&self, x: i32, y: i32, z: i32) -> Option<Block> {
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let region: &RegionToModify = self.get_region(region_x, region_z)?;
let chunk: &ChunkToModify = region.get_chunk(chunk_x & 31, chunk_z & 31)?;
chunk.get_block(
(x & 15).try_into().unwrap(),
y,
(z & 15).try_into().unwrap(),
)
}
#[inline]
pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: Block) {
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
chunk.set_block(
(x & 15).try_into().unwrap(),
y,
(z & 15).try_into().unwrap(),
block,
);
}
#[inline]
pub fn set_block_with_properties(
&mut self,
x: i32,
y: i32,
z: i32,
block_with_props: BlockWithProperties,
) {
let chunk_x: i32 = x >> 4;
let chunk_z: i32 = z >> 4;
let region_x: i32 = chunk_x >> 5;
let region_z: i32 = chunk_z >> 5;
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
chunk.set_block_with_properties(
(x & 15).try_into().unwrap(),
y,
(z & 15).try_into().unwrap(),
block_with_props,
);
}
}

321
src/world_editor/java.rs Normal file
View File

@@ -0,0 +1,321 @@
//! Java Edition Anvil format world saving.
//!
//! This module handles saving worlds in the Java Edition Anvil (.mca) format.
use super::common::{Chunk, ChunkToModify, Section};
use super::WorldEditor;
use crate::block_definitions::GRASS_BLOCK;
use crate::progress::emit_gui_progress_update;
use colored::Colorize;
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};
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));
const REGION_TEMPLATE: &[u8] = include_bytes!("../../assets/minecraft/region.template");
let mut region_file: File = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&out_path)
.expect("Failed to open region file");
region_file
.write_all(REGION_TEMPLATE)
.expect("Could not write region template");
Region::from_stream(region_file).expect("Failed to load region")
}
/// Helper function to create a base chunk with grass blocks at Y -62
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
let mut chunk = ChunkToModify::default();
// Fill the bottom layer with grass blocks at Y -62
for x in 0..16 {
for z in 0..16 {
chunk.set_block(x, -62, z, GRASS_BLOCK);
}
}
// Prepare chunk data
let chunk_data = Chunk {
sections: chunk.sections().collect(),
x_pos: abs_chunk_x,
z_pos: abs_chunk_z,
is_light_on: 0,
other: chunk.other,
};
// Create the Level wrapper
let level_data = create_level_wrapper(&chunk_data);
// Serialize the chunk with Level wrapper
let mut ser_buffer = Vec::with_capacity(8192);
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
(ser_buffer, true)
}
/// Saves the world in Java Edition Anvil format.
pub(super) fn save_java(&mut self) {
println!("{} Saving world...", "[7/7]".bold());
emit_gui_progress_update(90.0, "Saving world...");
// Save metadata with error handling
if let Err(e) = self.save_metadata() {
eprintln!("Failed to save world metadata: {}", e);
#[cfg(feature = "gui")]
send_log(LogLevel::Warning, "Failed to save world metadata.");
// Continue with world saving even if metadata fails
}
let total_regions = self.world.regions.len() as u64;
let save_pb = ProgressBar::new(total_regions);
save_pb.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})",
)
.unwrap()
.progress_chars("█▓░"),
);
let regions_processed = AtomicU64::new(0);
self.world
.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 &region_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();
}
}
}
// 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
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...");
}
save_pb.inc(1);
});
save_pb.finish();
}
}
/// Helper function to get entity coordinates
#[inline]
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
*x
} else {
0
};
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
*y
} else {
0
};
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
*z
} else {
0
};
(x, y, z)
}
/// Creates a Level wrapper for chunk data (Java Edition format)
#[inline]
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
HashMap::from([(
"Level".to_string(),
Value::Compound(HashMap::from([
("xPos".to_string(), Value::Int(chunk.x_pos)),
("zPos".to_string(), Value::Int(chunk.z_pos)),
(
"isLightOn".to_string(),
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
),
(
"sections".to_string(),
Value::List(
chunk
.sections
.iter()
.map(|section| {
let mut block_states = HashMap::from([(
"palette".to_string(),
Value::List(
section
.block_states
.palette
.iter()
.map(|item| {
let mut palette_item = HashMap::from([(
"Name".to_string(),
Value::String(item.name.clone()),
)]);
if let Some(props) = &item.properties {
palette_item.insert(
"Properties".to_string(),
props.clone(),
);
}
Value::Compound(palette_item)
})
.collect(),
),
)]);
// Only add the `data` attribute if it's non-empty
// to maintain compatibility with third-party tools like Dynmap
if let Some(data) = &section.block_states.data {
if !data.is_empty() {
block_states.insert(
"data".to_string(),
Value::LongArray(data.to_owned()),
);
}
}
Value::Compound(HashMap::from([
("Y".to_string(), Value::Byte(section.y)),
("block_states".to_string(), Value::Compound(block_states)),
]))
})
.collect(),
),
),
])),
)])
}

587
src/world_editor/mod.rs Normal file
View File

@@ -0,0 +1,587 @@
//! World editor module for generating Minecraft worlds.
//!
//! This module provides the `WorldEditor` struct which handles block placement
//! and world saving in both Java Edition (Anvil) and Bedrock Edition (.mcworld) formats.
//!
//! # Module Structure
//!
//! - `common` - Shared data structures for world modification
//! - `java` - Java Edition Anvil format saving
//! - `bedrock` - Bedrock Edition .mcworld format saving (behind `bedrock` feature)
mod common;
mod java;
#[cfg(feature = "bedrock")]
pub mod bedrock;
// Re-export common types used internally
pub(crate) use common::WorldToModify;
#[cfg(feature = "bedrock")]
pub(crate) use bedrock::{BedrockSaveError, BedrockWriter};
use crate::block_definitions::*;
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
use crate::coordinate_system::geographic::LLBBox;
use crate::ground::Ground;
use crate::progress::emit_gui_progress_update;
use colored::Colorize;
use fastnbt::Value;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
/// World format to generate
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code)]
pub enum WorldFormat {
/// Java Edition Anvil format (.mca region files)
JavaAnvil,
/// Bedrock Edition .mcworld format
BedrockMcWorld,
}
/// Metadata saved with the world
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WorldMetadata {
pub min_mc_x: i32,
pub max_mc_x: i32,
pub min_mc_z: i32,
pub max_mc_z: i32,
pub min_geo_lat: f64,
pub max_geo_lat: f64,
pub min_geo_lon: f64,
pub max_geo_lon: f64,
}
/// The main world editor struct for placing blocks and saving worlds.
///
/// The lifetime `'a` is tied to the `XZBBox` reference, which defines
/// the world boundaries and must outlive the WorldEditor instance.
pub struct WorldEditor<'a> {
world_dir: PathBuf,
world: WorldToModify,
xzbbox: &'a XZBBox,
llbbox: LLBBox,
ground: Option<Box<Ground>>,
format: WorldFormat,
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
bedrock_level_name: Option<String>,
/// Optional spawn point for Bedrock worlds (x, z coordinates)
bedrock_spawn_point: Option<(i32, i32)>,
}
impl<'a> WorldEditor<'a> {
/// Creates a new WorldEditor with Java Anvil format (default).
///
/// This is the default constructor used by CLI mode.
#[allow(dead_code)]
pub fn new(world_dir: PathBuf, xzbbox: &'a XZBBox, llbbox: LLBBox) -> Self {
Self {
world_dir,
world: WorldToModify::default(),
xzbbox,
llbbox,
ground: None,
format: WorldFormat::JavaAnvil,
bedrock_level_name: None,
bedrock_spawn_point: None,
}
}
/// Creates a new WorldEditor with a specific format and optional level name.
///
/// Used by GUI mode to support both Java and Bedrock formats.
#[allow(dead_code)]
pub fn new_with_format_and_name(
world_dir: PathBuf,
xzbbox: &'a XZBBox,
llbbox: LLBBox,
format: WorldFormat,
bedrock_level_name: Option<String>,
bedrock_spawn_point: Option<(i32, i32)>,
) -> Self {
Self {
world_dir,
world: WorldToModify::default(),
xzbbox,
llbbox,
ground: None,
format,
bedrock_level_name,
bedrock_spawn_point,
}
}
/// Sets the ground reference for elevation-based block placement
pub fn set_ground(&mut self, ground: &Ground) {
self.ground = Some(Box::new(ground.clone()));
}
/// 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())
}
/// Returns the current world format
#[allow(dead_code)]
pub fn format(&self) -> WorldFormat {
self.format
}
/// Calculate the absolute Y position from a ground-relative offset
#[inline(always)]
pub fn get_absolute_y(&self, x: i32, y_offset: i32, z: i32) -> i32 {
if let Some(ground) = &self.ground {
ground.level(XZPoint::new(
x - self.xzbbox.min_x(),
z - self.xzbbox.min_z(),
)) + y_offset
} else {
y_offset // If no ground reference, use y_offset as absolute Y
}
}
/// Returns the minimum world coordinates
pub fn get_min_coords(&self) -> (i32, i32) {
(self.xzbbox.min_x(), self.xzbbox.min_z())
}
/// Returns the maximum world coordinates
pub fn get_max_coords(&self) -> (i32, i32) {
(self.xzbbox.max_x(), self.xzbbox.max_z())
}
/// Checks if there's a block at the given coordinates
#[allow(unused)]
#[inline]
pub fn block_at(&self, x: i32, y: i32, z: i32) -> bool {
let absolute_y = self.get_absolute_y(x, y, z);
self.world.get_block(x, absolute_y, z).is_some()
}
/// Sets a sign at the given coordinates
#[allow(clippy::too_many_arguments, dead_code)]
pub fn set_sign(
&mut self,
line1: String,
line2: String,
line3: String,
line4: String,
x: i32,
y: i32,
z: i32,
_rotation: i8,
) {
let absolute_y = self.get_absolute_y(x, y, z);
let chunk_x = x >> 4;
let chunk_z = z >> 4;
let region_x = chunk_x >> 5;
let region_z = chunk_z >> 5;
let mut block_entities = HashMap::new();
let messages = vec![
Value::String(format!("\"{line1}\"")),
Value::String(format!("\"{line2}\"")),
Value::String(format!("\"{line3}\"")),
Value::String(format!("\"{line4}\"")),
];
let mut text_data = HashMap::new();
text_data.insert("messages".to_string(), Value::List(messages));
text_data.insert("color".to_string(), Value::String("black".to_string()));
text_data.insert("has_glowing_text".to_string(), Value::Byte(0));
block_entities.insert("front_text".to_string(), Value::Compound(text_data));
block_entities.insert(
"id".to_string(),
Value::String("minecraft:sign".to_string()),
);
block_entities.insert("is_waxed".to_string(), Value::Byte(0));
block_entities.insert("keepPacked".to_string(), Value::Byte(0));
block_entities.insert("x".to_string(), Value::Int(x));
block_entities.insert("y".to_string(), Value::Int(absolute_y));
block_entities.insert("z".to_string(), Value::Int(z));
let region = self.world.get_or_create_region(region_x, region_z);
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
if let Some(chunk_data) = chunk.other.get_mut("block_entities") {
if let Value::List(entities) = chunk_data {
entities.push(Value::Compound(block_entities));
}
} else {
chunk.other.insert(
"block_entities".to_string(),
Value::List(vec![Value::Compound(block_entities)]),
);
}
self.set_block(SIGN, x, y, z, None, None);
}
/// Sets a block of the specified type at the given coordinates.
///
/// Y value is interpreted as an offset from ground level.
#[inline]
pub fn set_block(
&mut self,
block: Block,
x: i32,
y: i32,
z: i32,
override_whitelist: Option<&[Block]>,
override_blacklist: Option<&[Block]>,
) {
// Check if coordinates are within bounds
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
// Calculate the absolute Y coordinate based on ground level
let absolute_y = self.get_absolute_y(x, y, z);
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
// Check against whitelist and blacklist
if let Some(whitelist) = override_whitelist {
whitelist
.iter()
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
} else if let Some(blacklist) = override_blacklist {
!blacklist
.iter()
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
} else {
false
}
} else {
true
};
if should_insert {
self.world.set_block(x, absolute_y, z, block);
}
}
/// Sets a block of the specified type at the given coordinates with absolute Y value.
#[inline]
pub fn set_block_absolute(
&mut self,
block: Block,
x: i32,
absolute_y: i32,
z: i32,
override_whitelist: Option<&[Block]>,
override_blacklist: Option<&[Block]>,
) {
// Check if coordinates are within bounds
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
// Check against whitelist and blacklist
if let Some(whitelist) = override_whitelist {
whitelist
.iter()
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
} else if let Some(blacklist) = override_blacklist {
!blacklist
.iter()
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
} else {
false
}
} else {
true
};
if should_insert {
self.world.set_block(x, absolute_y, z, block);
}
}
/// Sets a block with properties at the given coordinates with absolute Y value.
#[inline]
pub fn set_block_with_properties_absolute(
&mut self,
block_with_props: BlockWithProperties,
x: i32,
absolute_y: i32,
z: i32,
override_whitelist: Option<&[Block]>,
override_blacklist: Option<&[Block]>,
) {
// Check if coordinates are within bounds
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
return;
}
let should_insert = if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
// Check against whitelist and blacklist
if let Some(whitelist) = override_whitelist {
whitelist
.iter()
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
} else if let Some(blacklist) = override_blacklist {
!blacklist
.iter()
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
} else {
false
}
} else {
true
};
if should_insert {
self.world
.set_block_with_properties(x, absolute_y, z, block_with_props);
}
}
/// Fills a cuboid area with the specified block between two coordinates.
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn fill_blocks(
&mut self,
block: Block,
x1: i32,
y1: i32,
z1: i32,
x2: i32,
y2: i32,
z2: i32,
override_whitelist: Option<&[Block]>,
override_blacklist: Option<&[Block]>,
) {
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
let (min_y, max_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
for x in min_x..=max_x {
for y_offset in min_y..=max_y {
for z in min_z..=max_z {
self.set_block(
block,
x,
y_offset,
z,
override_whitelist,
override_blacklist,
);
}
}
}
}
/// Fills a cuboid area with the specified block between two coordinates using absolute Y values.
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn fill_blocks_absolute(
&mut self,
block: Block,
x1: i32,
y1_absolute: i32,
z1: i32,
x2: i32,
y2_absolute: i32,
z2: i32,
override_whitelist: Option<&[Block]>,
override_blacklist: Option<&[Block]>,
) {
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
let (min_y, max_y) = if y1_absolute < y2_absolute {
(y1_absolute, y2_absolute)
} else {
(y2_absolute, y1_absolute)
};
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
for x in min_x..=max_x {
for absolute_y in min_y..=max_y {
for z in min_z..=max_z {
self.set_block_absolute(
block,
x,
absolute_y,
z,
override_whitelist,
override_blacklist,
);
}
}
}
}
/// Checks for a block at the given coordinates.
#[inline]
pub fn check_for_block(&self, x: i32, y: i32, z: i32, whitelist: Option<&[Block]>) -> bool {
let absolute_y = self.get_absolute_y(x, y, z);
// Retrieve the chunk modification map
if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
if let Some(whitelist) = whitelist {
if whitelist
.iter()
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
{
return true; // Block is in the list
}
}
}
false
}
/// Checks for a block at the given coordinates with absolute Y value.
#[allow(unused)]
pub fn check_for_block_absolute(
&self,
x: i32,
absolute_y: i32,
z: i32,
whitelist: Option<&[Block]>,
blacklist: Option<&[Block]>,
) -> bool {
// Retrieve the chunk modification map
if let Some(existing_block) = self.world.get_block(x, absolute_y, z) {
// Check against whitelist and blacklist
if let Some(whitelist) = whitelist {
if whitelist
.iter()
.any(|whitelisted_block: &Block| whitelisted_block.id() == existing_block.id())
{
return true; // Block is in whitelist
}
return false;
}
if let Some(blacklist) = blacklist {
if blacklist
.iter()
.any(|blacklisted_block: &Block| blacklisted_block.id() == existing_block.id())
{
return true; // Block is in blacklist
}
}
return whitelist.is_none() && blacklist.is_none();
}
false
}
/// Checks if a block exists at the given coordinates with absolute Y value.
///
/// Unlike `check_for_block_absolute`, this doesn't filter by block type.
#[allow(unused)]
pub fn block_at_absolute(&self, x: i32, absolute_y: i32, z: i32) -> bool {
self.world.get_block(x, absolute_y, z).is_some()
}
/// Saves all changes made to the world by writing to the appropriate format.
pub fn save(&mut self) {
println!(
"Generating world for: {}",
match self.format {
WorldFormat::JavaAnvil => "Java Edition (Anvil)",
WorldFormat::BedrockMcWorld => "Bedrock Edition (.mcworld)",
}
);
match self.format {
WorldFormat::JavaAnvil => self.save_java(),
WorldFormat::BedrockMcWorld => self.save_bedrock(),
}
}
#[allow(unreachable_code)]
fn save_bedrock(&mut self) {
println!("{} Saving Bedrock world...", "[7/7]".bold());
emit_gui_progress_update(90.0, "Saving Bedrock world...");
#[cfg(feature = "bedrock")]
{
if let Err(error) = self.save_bedrock_internal() {
eprintln!("Failed to save Bedrock world: {error}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Error,
&format!("Failed to save Bedrock world: {error}"),
);
}
}
#[cfg(not(feature = "bedrock"))]
{
eprintln!(
"Bedrock output requested but the 'bedrock' feature is not enabled at build time."
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Error,
"Bedrock output requested but the 'bedrock' feature is not enabled at build time.",
);
}
}
#[cfg(feature = "bedrock")]
fn save_bedrock_internal(&mut self) -> Result<(), BedrockSaveError> {
// Use the stored level name if available, otherwise extract from path
let level_name = self.bedrock_level_name.clone().unwrap_or_else(|| {
self.world_dir
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Arnis World")
.to_string()
});
BedrockWriter::new(
self.world_dir.clone(),
level_name,
self.bedrock_spawn_point,
self.ground.clone(),
)
.write_world(&self.world, self.xzbbox, &self.llbbox)
}
/// Saves world metadata to a JSON file
pub(crate) fn save_metadata(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let metadata_path = self.world_dir.join("metadata.json");
let mut file = File::create(&metadata_path).map_err(|e| {
format!(
"Failed to create metadata file at {}: {}",
metadata_path.display(),
e
)
})?;
let metadata = WorldMetadata {
min_mc_x: self.xzbbox.min_x(),
max_mc_x: self.xzbbox.max_x(),
min_mc_z: self.xzbbox.min_z(),
max_mc_z: self.xzbbox.max_z(),
min_geo_lat: self.llbbox.min().lat(),
max_geo_lat: self.llbbox.max().lat(),
min_geo_lon: self.llbbox.min().lng(),
max_geo_lon: self.llbbox.max().lng(),
};
let contents = serde_json::to_string(&metadata)
.map_err(|e| format!("Failed to serialize metadata to JSON: {}", e))?;
write!(&mut file, "{}", contents)
.map_err(|e| format!("Failed to write metadata to file: {}", e))?;
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Arnis",
"version": "2.3.1",
"version": "2.4.0",
"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"