Compare commits
114 Commits
v2.3.0
...
fix-perfor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dee580c564 | ||
|
|
41fc5662e0 | ||
|
|
83e9a634e5 | ||
|
|
56ddea57d0 | ||
|
|
430a4970f5 | ||
|
|
74fbdabaee | ||
|
|
2643155e9a | ||
|
|
d45c360074 | ||
|
|
6277a14d22 | ||
|
|
c355f243e3 | ||
|
|
2c31d2659c | ||
|
|
996e06ab2c | ||
|
|
e11231ad0f | ||
|
|
9adf31121e | ||
|
|
69da18fbfb | ||
|
|
5976cc2868 | ||
|
|
a85eaed835 | ||
|
|
37c3d85672 | ||
|
|
15b698a1eb | ||
|
|
834ce7b51d | ||
|
|
93b6f5ac99 | ||
|
|
77df683deb | ||
|
|
9d791f4299 | ||
|
|
a2dff0b84b | ||
|
|
24630351b9 | ||
|
|
819b03e3b1 | ||
|
|
f315245637 | ||
|
|
16c9f3c3bf | ||
|
|
3043ca6d24 | ||
|
|
3657878f01 | ||
|
|
44cc57c3dd | ||
|
|
408caa9176 | ||
|
|
3191a3676d | ||
|
|
8fff2d2fb5 | ||
|
|
8c702a36ff | ||
|
|
9bedca071a | ||
|
|
d611837746 | ||
|
|
e06fcaf7a2 | ||
|
|
1f2772f052 | ||
|
|
82f3460043 | ||
|
|
65d6bb6c99 | ||
|
|
7ebee82982 | ||
|
|
5056e83dff | ||
|
|
3cb0b994e1 | ||
|
|
5ded3c961e | ||
|
|
893c14bff8 | ||
|
|
4f8e0020e3 | ||
|
|
456018abf7 | ||
|
|
175bdc4582 | ||
|
|
ed294a3973 | ||
|
|
6c966fc9cc | ||
|
|
ecfc6634fc | ||
|
|
a336ccd1aa | ||
|
|
f702a41af1 | ||
|
|
29c4dc6d7c | ||
|
|
f60b341eca | ||
|
|
92c5e52f46 | ||
|
|
e4d7dd15c2 | ||
|
|
cc6748115b | ||
|
|
ce9496aea5 | ||
|
|
4c87eb8141 | ||
|
|
ada475f73e | ||
|
|
e5a82ba526 | ||
|
|
309ac19b09 | ||
|
|
90df7688df | ||
|
|
5b3889a7bb | ||
|
|
67deb739e6 | ||
|
|
7c08d21f36 | ||
|
|
280acc7a8a | ||
|
|
ee59da5d9b | ||
|
|
b772cb6ab9 | ||
|
|
2646947ed0 | ||
|
|
b9976fd562 | ||
|
|
a621703da4 | ||
|
|
87efd02c74 | ||
|
|
6a6b58fd8f | ||
|
|
855c6fe846 | ||
|
|
1592951fe3 | ||
|
|
e267e04350 | ||
|
|
c2e8d5959f | ||
|
|
ffe8f865d2 | ||
|
|
4f9f4f127a | ||
|
|
014208426b | ||
|
|
6d848ef7cd | ||
|
|
1e25dfea37 | ||
|
|
903efec459 | ||
|
|
22b3969c72 | ||
|
|
0f62c4283b | ||
|
|
4299e64cba | ||
|
|
0cd386b399 | ||
|
|
2fdecbe206 | ||
|
|
75d1ab14e7 | ||
|
|
f9c18ae5f6 | ||
|
|
963fe2327b | ||
|
|
2a8ff6f641 | ||
|
|
ff3ea1093a | ||
|
|
fd401ab23f | ||
|
|
3f67e060eb | ||
|
|
858c2f4c93 | ||
|
|
e8fad0e197 | ||
|
|
7ebc66db7e | ||
|
|
28b75a22cf | ||
|
|
9ee9f0de64 | ||
|
|
5b874a5c29 | ||
|
|
eeecdc8f99 | ||
|
|
8b33e152ef | ||
|
|
ca8e50fbf1 | ||
|
|
7f4ef9130f | ||
|
|
8d183543be | ||
|
|
dd07fba15c | ||
|
|
083fbed040 | ||
|
|
4191fa4902 | ||
|
|
6c82265ee3 | ||
|
|
7d978047b4 |
2
.gitattributes
vendored
@@ -1 +1 @@
|
||||
gui-src/** linguist-vendored
|
||||
src/gui/** linguist-vendored
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,13 +11,13 @@ assignees: ''
|
||||
A clear and concise description of what the bug is and what you expected to happen.
|
||||
|
||||
**Used bbox area**
|
||||
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
||||
Please provide your input parameters (BBOX) so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
||||
|
||||
**Arnis and Minecraft version**
|
||||
Please tell us what version of Arnis and Minecraft you used.
|
||||
Please tell us what version of Arnis and Minecraft you used, as well as if you are on Windows, Linux or MacOS.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too. If you experienced any issue with the application itself like a crash, please provide the log file which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too. Please provide the log file if possible as well, which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
||||
|
||||
6
.github/workflows/ci-build.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
|
||||
8
.github/workflows/pr-benchmark.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Run benchmark command with memory tracking
|
||||
id: benchmark
|
||||
run: |
|
||||
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.101470,11.517792,48.168375,11.626968" 2> benchmark_log.txt
|
||||
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
|
||||
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
||||
peak_kb=$(cat peak_mem_kb.txt)
|
||||
peak_mb=$((peak_kb / 1024))
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
seconds=$((duration % 60))
|
||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||
|
||||
baseline_time=135
|
||||
baseline_time=69
|
||||
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))
|
||||
|
||||
14
.github/workflows/release.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -97,13 +97,13 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-13-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-latest-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
@@ -124,22 +124,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||
path: ./builds/windows
|
||||
|
||||
- name: Download Linux build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||
path: ./builds/linux
|
||||
|
||||
- name: Download macOS universal build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: ./builds/macos
|
||||
|
||||
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.envrc
|
||||
/.direnv
|
||||
|
||||
# Build artifacts
|
||||
|
||||
258
Cargo.lock
generated
@@ -2,15 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
@@ -191,8 +182,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arnis"
|
||||
version = "2.2.1"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"dirs",
|
||||
@@ -205,6 +197,7 @@ dependencies = [
|
||||
"image 0.25.6",
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
@@ -463,21 +456,6 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -810,8 +788,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
@@ -1208,9 +1188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
|
||||
checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
|
||||
dependencies = [
|
||||
"dlopen2_derive",
|
||||
"libc",
|
||||
@@ -1423,9 +1403,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastanvil"
|
||||
version = "0.31.0"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619fbd9bb48f9e54a7ff88cdd0f7048b8ea519448518c690057f9d03d3e81ebf"
|
||||
checksum = "68f773cb151aa058dd65e3a119cc0733252abf2942d4d3eed8eee72bd231d5b4"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"byteorder",
|
||||
@@ -1433,6 +1413,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"image 0.23.14",
|
||||
"log",
|
||||
"lz4-java-wrc",
|
||||
"num_enum 0.5.11",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -1442,9 +1423,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastnbt"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d4a73a95dc65551ccd98e1ecd1adb5d1ba5361146963b31f481ca42fc0520a3"
|
||||
checksum = "29e3bb1f9ba8b6dbb35d89b415216041c2cad0c3e0b329db85ef5d4fc9e80cd6"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cesu8",
|
||||
@@ -1803,9 +1784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "geo"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1"
|
||||
checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a"
|
||||
dependencies = [
|
||||
"earcutr",
|
||||
"float_next_after",
|
||||
@@ -1885,12 +1866,6 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
@@ -2262,24 +2237,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "i_float"
|
||||
version = "1.7.0"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343"
|
||||
checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i_key_sort"
|
||||
version = "0.2.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd"
|
||||
checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27"
|
||||
|
||||
[[package]]
|
||||
name = "i_overlay"
|
||||
version = "2.0.5"
|
||||
version = "4.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49"
|
||||
checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9"
|
||||
dependencies = [
|
||||
"i_float",
|
||||
"i_key_sort",
|
||||
@@ -2290,19 +2265,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "i_shape"
|
||||
version = "1.7.0"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce"
|
||||
checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082"
|
||||
dependencies = [
|
||||
"i_float",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "i_tree"
|
||||
version = "0.8.3"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139"
|
||||
checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -2590,17 +2564,6 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.10.1"
|
||||
@@ -2905,6 +2868,22 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4-java-wrc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed1c11dbd317fa5d0508cbb5ce47696bb27771e3c9604428e0e98c7c3d76d0d"
|
||||
dependencies = [
|
||||
"lz4_flex",
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -3397,6 +3376,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-javascript-core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
|
||||
dependencies = [
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-metal"
|
||||
version = "0.2.2"
|
||||
@@ -3433,6 +3422,17 @@ dependencies = [
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-security"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"objc2 0.6.1",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.1"
|
||||
@@ -3457,15 +3457,8 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4422,12 +4415,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4620,9 +4607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.24"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -4763,9 +4750,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
|
||||
checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4774,13 +4761,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript-impl"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
|
||||
checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5090,11 +5077,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.34.0"
|
||||
version = "0.34.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
|
||||
checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2 0.6.1",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
@@ -5162,12 +5150,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.7.0"
|
||||
version = "2.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
|
||||
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
@@ -5185,6 +5174,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.3.1",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
@@ -5212,9 +5202,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.3.1"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
|
||||
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -5228,15 +5218,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.3.1"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
|
||||
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -5261,9 +5251,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.3.2"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
|
||||
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -5335,9 +5325,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.7.1"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
|
||||
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -5346,20 +5336,23 @@ dependencies = [
|
||||
"jni",
|
||||
"objc2 0.6.1",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.9",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.7.2"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
|
||||
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -5384,9 +5377,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
|
||||
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
@@ -5413,7 +5406,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.9",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.2",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -5433,9 +5426,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.20.0"
|
||||
version = "3.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
@@ -5566,29 +5559,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.0"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5809,6 +5799,16 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "1.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
@@ -6367,7 +6367,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -6397,7 +6397,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
@@ -6409,7 +6409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6440,6 +6440,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
@@ -6447,7 +6453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6467,7 +6473,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6476,7 +6482,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6485,7 +6491,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6515,6 +6521,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
@@ -6766,14 +6781,15 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.52.1"
|
||||
version = "0.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
|
||||
checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.1",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
|
||||
16
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -20,34 +20,36 @@ gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs"
|
||||
tauri-build = {version = "2", optional = true}
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
fastanvil = "0.31.0"
|
||||
fastnbt = "2.5.0"
|
||||
fastanvil = "0.32.0"
|
||||
fastnbt = "2.6.0"
|
||||
flate2 = "1.1"
|
||||
fnv = "1.0.7"
|
||||
fs2 = "0.4"
|
||||
geo = "0.30.0"
|
||||
geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
lazy_static = "1.5"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
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.23"
|
||||
semver = "1.0.26"
|
||||
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.47.0", features = ["full"], optional = true }
|
||||
tokio = { version = "1.48.0", features = ["full"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
tempfile = "3.23.0"
|
||||
|
||||
16
README.md
@@ -1,4 +1,4 @@
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/banner.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/banner.png" width="100%" alt="Banner">
|
||||
|
||||
# Arnis [](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) [](https://github.com/louis-e/arnis/releases) [](https://discord.gg/mA2g69Fhxq)
|
||||
|
||||
@@ -7,11 +7,11 @@ Arnis creates complex and accurate Minecraft Java Edition worlds that reflect re
|
||||
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!
|
||||
|
||||

|
||||
<i>This Github page is the official project website. Do not download Arnis from any other website.</i>
|
||||

|
||||
<i>This Github page and [arnismc.com](https://arnismc.com) are the only official project websites. Do not download Arnis from any other website.</i>
|
||||
|
||||
## :keyboard: Usage
|
||||
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/gui.png?raw=true"><br>
|
||||
<img width="60%" src="assets/git/gui.png"><br>
|
||||
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
|
||||
|
||||
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
|
||||
@@ -19,7 +19,7 @@ Additionally, you can customize various generation settings, such as world scale
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/documentation.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/documentation.png" width="100%" alt="Banner">
|
||||
|
||||
Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/arnis/wiki/), covering topics such as technical explanations, FAQs, contribution guidelines and roadmaps.
|
||||
|
||||
@@ -34,7 +34,7 @@ Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/
|
||||
#### How to contribute
|
||||
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. Please respect the above mentioned key objectives. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
|
||||
|
||||
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
|
||||
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lat,min_lng,max_lat,max_lng"```<br>
|
||||
GUI Build: ```cargo run```<br>
|
||||
|
||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||
@@ -51,7 +51,7 @@ After your pull request was merged, I will take care of regularly creating updat
|
||||
|
||||
## :newspaper: Academic & Press Recognition
|
||||
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/recognition.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/recognition.png" width="100%" alt="Banner">
|
||||
|
||||
Arnis has been recognized in various academic and press publications after gaining a lot of attention in December 2024.
|
||||
|
||||
@@ -78,7 +78,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.[^3]
|
||||
|
||||
Download Arnis only from the official source (https://github.com/louis-e/arnis/). Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
||||
Download Arnis only from the official source https://arnismc.com or https://github.com/louis-e/arnis/. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
||||
|
||||
The logo was made by @nxfx21.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 790 KiB After Width: | Height: | Size: 790 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
60
flake.lock
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755615617,
|
||||
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
36
flake.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
flake-utils,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell.override { inherit stdenv; } {
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
99
flake/flake.lock
generated
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724653830,
|
||||
"narHash": "sha256-88f0KK8h6tGIP4Na5RJDKs0S+7WsGGaCGNkLj/bPV3g=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "9ecf5e7d800ace001320da8acadd4a3deb872a83",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1724479785,
|
||||
"narHash": "sha256-pP3Azj5d6M5nmG68Fu4JqZmdGt4S4vqI5f8te+E/FTw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d0e1602ddde669d5beb01aec49d71a51937ed7be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1724586512,
|
||||
"narHash": "sha256-mrfwk6nO8N2WtCq3sB2zhd2QN1HMKzeSESzOA6lSsQg=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "7106cd3be50b2a43c1d9f2787bf22d4369c2b25b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
inputs = {
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, fenix, flake-utils, nixpkgs }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
fenixPkgs = (fenix.packages.${system}.stable);
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell
|
||||
{
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
fenixPkgs.toolchain
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
10
src/args.rs
@@ -1,6 +1,6 @@
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use clap::Parser;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Command-line arguments parser
|
||||
@@ -21,7 +21,7 @@ pub struct Args {
|
||||
|
||||
/// Path to the Minecraft world (required)
|
||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Downloader method (requests/curl/wget) (optional)
|
||||
#[arg(long, default_value = "requests")]
|
||||
@@ -64,8 +64,8 @@ pub struct Args {
|
||||
pub spawn_point: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
||||
let mc_world_path = Path::new(path);
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
let mc_world_path = PathBuf::from(path);
|
||||
if !mc_world_path.exists() {
|
||||
return Err(format!("Path does not exist: {path}"));
|
||||
}
|
||||
@@ -76,7 +76,7 @@ fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
||||
if !region.is_dir() {
|
||||
return Err(format!("No Minecraft world found at {region:?}"));
|
||||
}
|
||||
Ok(path.to_string())
|
||||
Ok(mc_world_path)
|
||||
}
|
||||
|
||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||
|
||||
@@ -236,7 +236,7 @@ impl Block {
|
||||
155 => "chest",
|
||||
156 => "red_carpet",
|
||||
157 => "anvil",
|
||||
158 => "jukebox",
|
||||
158 => "note_block",
|
||||
159 => "oak_door",
|
||||
160 => "brewing_stand",
|
||||
161 => "red_bed", // North head
|
||||
@@ -667,7 +667,7 @@ pub const OAK_STAIRS: Block = Block::new(144);
|
||||
pub const CHEST: Block = Block::new(155);
|
||||
pub const RED_CARPET: Block = Block::new(156);
|
||||
pub const ANVIL: Block = Block::new(157);
|
||||
pub const JUKEBOX: Block = Block::new(158);
|
||||
pub const NOTE_BLOCK: Block = Block::new(158);
|
||||
pub const OAK_DOOR: Block = Block::new(159);
|
||||
pub const BREWING_STAND: Block = Block::new(160);
|
||||
pub const RED_BED_NORTH_HEAD: Block = Block::new(161);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::ground::Ground;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
@@ -14,14 +17,17 @@ pub const MIN_Y: i32 = -64;
|
||||
pub fn generate_world(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
) -> Result<(), String> {
|
||||
let region_dir: String = format!("{}/region", args.path);
|
||||
let mut editor: WorldEditor = WorldEditor::new(®ion_dir, &xzbbox);
|
||||
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
// Build highway connectivity map ONCE before processing (not for each highway!)
|
||||
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);
|
||||
|
||||
@@ -63,7 +69,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);
|
||||
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") {
|
||||
@@ -74,12 +80,19 @@ pub fn generate_world(
|
||||
leisure::generate_leisure(&mut editor, way, args);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, element);
|
||||
} else if way.tags.contains_key("waterway") {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
} 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, &xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
}
|
||||
} else if way.tags.contains_key("bridge") {
|
||||
//bridges::generate_bridges(&mut editor, way, ground_level); // TODO FIX
|
||||
} else if way.tags.contains_key("railway") {
|
||||
railways::generate_railways(&mut editor, way);
|
||||
} else if way.tags.contains_key("roller_coaster") {
|
||||
railways::generate_roller_coaster(&mut editor, way);
|
||||
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway")
|
||||
{
|
||||
highways::generate_aeroway(&mut editor, way, args);
|
||||
@@ -101,7 +114,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);
|
||||
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") {
|
||||
@@ -112,9 +125,13 @@ pub fn generate_world(
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
buildings::generate_building_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel.tags.get("natural") == Some(&"water".to_string())
|
||||
|| rel
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
water_areas::generate_water_areas(&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") {
|
||||
@@ -187,6 +204,8 @@ pub fn generate_world(
|
||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
||||
|
||||
block_counter += 1;
|
||||
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if block_counter % batch_size == 0 {
|
||||
ground_pb.inc(batch_size);
|
||||
}
|
||||
@@ -236,7 +255,10 @@ pub fn generate_world(
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
225
src/debug_logging.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use crate::osm_parser::{ProcessedMember, ProcessedMemberRole, ProcessedNode, ProcessedWay};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DEBUG_LOG: Mutex<DebugLogger> = Mutex::new(DebugLogger::new());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeSnapshot {
|
||||
pub id: u64,
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
pub tags: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl From<&ProcessedNode> for NodeSnapshot {
|
||||
fn from(node: &ProcessedNode) -> Self {
|
||||
NodeSnapshot {
|
||||
id: node.id,
|
||||
x: node.x,
|
||||
z: node.z,
|
||||
tags: node.tags.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WaySnapshot {
|
||||
pub id: u64,
|
||||
pub tags: HashMap<String, String>,
|
||||
pub nodes: Vec<NodeSnapshot>,
|
||||
pub node_count: usize,
|
||||
}
|
||||
|
||||
impl From<&ProcessedWay> for WaySnapshot {
|
||||
fn from(way: &ProcessedWay) -> Self {
|
||||
WaySnapshot {
|
||||
id: way.id,
|
||||
tags: way.tags.clone(),
|
||||
nodes: way.nodes.iter().map(NodeSnapshot::from).collect(),
|
||||
node_count: way.nodes.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MemberSnapshot {
|
||||
pub role: String,
|
||||
pub way: WaySnapshot,
|
||||
}
|
||||
|
||||
impl From<&ProcessedMember> for MemberSnapshot {
|
||||
fn from(member: &ProcessedMember) -> Self {
|
||||
MemberSnapshot {
|
||||
role: match member.role {
|
||||
ProcessedMemberRole::Outer => "outer".to_string(),
|
||||
ProcessedMemberRole::Inner => "inner".to_string(),
|
||||
},
|
||||
way: WaySnapshot::from(&member.way),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RelationSnapshot {
|
||||
pub id: u64,
|
||||
pub tags: HashMap<String, String>,
|
||||
pub members: Vec<MemberSnapshot>,
|
||||
pub member_count: usize,
|
||||
pub total_nodes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransformationStage {
|
||||
pub stage: String,
|
||||
pub timestamp: String,
|
||||
pub element_type: String,
|
||||
pub element_id: u64,
|
||||
pub way: Option<WaySnapshot>,
|
||||
pub relation: Option<RelationSnapshot>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct DebugLogger {
|
||||
enabled_elements: Vec<u64>,
|
||||
stages: Vec<TransformationStage>,
|
||||
}
|
||||
|
||||
impl DebugLogger {
|
||||
pub fn new() -> Self {
|
||||
// Read element IDs from environment variable ARNIS_DEBUG_ELEMENTS
|
||||
// Format: ARNIS_DEBUG_ELEMENTS=6553308,1470430,18254505
|
||||
let enabled_elements: Vec<u64> = std::env::var("ARNIS_DEBUG_ELEMENTS")
|
||||
.ok()
|
||||
.map(|s| {
|
||||
s.split(',')
|
||||
.filter_map(|id| id.trim().parse::<u64>().ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !enabled_elements.is_empty() {
|
||||
eprintln!(
|
||||
"DEBUG: Element tracking enabled for IDs: {:?}",
|
||||
enabled_elements
|
||||
);
|
||||
}
|
||||
|
||||
DebugLogger {
|
||||
enabled_elements,
|
||||
stages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tracking(&self, element_id: u64) -> bool {
|
||||
!self.enabled_elements.is_empty() && self.enabled_elements.contains(&element_id)
|
||||
}
|
||||
|
||||
pub fn log_way(&mut self, stage: &str, way: &ProcessedWay, notes: Vec<String>) {
|
||||
if !self.is_tracking(way.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.stages.push(TransformationStage {
|
||||
stage: stage.to_string(),
|
||||
timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(),
|
||||
element_type: "way".to_string(),
|
||||
element_id: way.id,
|
||||
way: Some(WaySnapshot::from(way)),
|
||||
relation: None,
|
||||
notes,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn log_relation(
|
||||
&mut self,
|
||||
stage: &str,
|
||||
relation_id: u64,
|
||||
tags: &HashMap<String, String>,
|
||||
members: &[ProcessedMember],
|
||||
notes: Vec<String>,
|
||||
) {
|
||||
if !self.is_tracking(relation_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let total_nodes: usize = members.iter().map(|m| m.way.nodes.len()).sum();
|
||||
|
||||
self.stages.push(TransformationStage {
|
||||
stage: stage.to_string(),
|
||||
timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(),
|
||||
element_type: "relation".to_string(),
|
||||
element_id: relation_id,
|
||||
way: None,
|
||||
relation: Some(RelationSnapshot {
|
||||
id: relation_id,
|
||||
tags: tags.clone(),
|
||||
members: members.iter().map(MemberSnapshot::from).collect(),
|
||||
member_count: members.len(),
|
||||
total_nodes,
|
||||
}),
|
||||
notes,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn write_to_file(&self) -> std::io::Result<()> {
|
||||
if self.stages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let filename = format!(
|
||||
"debug_elements_{}.json",
|
||||
chrono::Local::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
|
||||
let mut file = File::create(&filename)?;
|
||||
let json = serde_json::to_string_pretty(&self.stages)?;
|
||||
file.write_all(json.as_bytes())?;
|
||||
|
||||
eprintln!(
|
||||
"DEBUG: Wrote {} transformation stages to {}",
|
||||
self.stages.len(),
|
||||
filename
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Public API functions
|
||||
pub fn log_way_transformation(stage: &str, way: &ProcessedWay, notes: Vec<String>) {
|
||||
if let Ok(mut logger) = DEBUG_LOG.lock() {
|
||||
logger.log_way(stage, way, notes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_relation_transformation(
|
||||
stage: &str,
|
||||
relation_id: u64,
|
||||
tags: &HashMap<String, String>,
|
||||
members: &[ProcessedMember],
|
||||
notes: Vec<String>,
|
||||
) {
|
||||
if let Ok(mut logger) = DEBUG_LOG.lock() {
|
||||
logger.log_relation(stage, relation_id, tags, members, notes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_tracking_element(element_id: u64) -> bool {
|
||||
DEBUG_LOG
|
||||
.lock()
|
||||
.map(|logger| logger.is_tracking(element_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn write_debug_log() {
|
||||
if let Ok(logger) = DEBUG_LOG.lock() {
|
||||
if let Err(e) = logger.write_to_file() {
|
||||
eprintln!("ERROR: Failed to write debug log: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,13 +92,6 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
||||
}
|
||||
}
|
||||
}
|
||||
"vending" => {
|
||||
// Place vending machine blocks
|
||||
if let Some(pt) = first_node {
|
||||
editor.set_block(IRON_BLOCK, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(IRON_BLOCK, pt.x, 2, pt.z, None, None);
|
||||
}
|
||||
}
|
||||
"shelter" => {
|
||||
let roof_block: Block = STONE_BRICK_SLAB;
|
||||
|
||||
|
||||
@@ -5,8 +5,69 @@ use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||
/// 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,
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
) {
|
||||
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
/// IMPORTANT: Call this ONCE before processing elements, not for each highway!
|
||||
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
|
||||
for element in elements {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
if way.tags.contains_key("highway") {
|
||||
let layer_value = way
|
||||
.tags
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0) for connectivity
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Add connectivity for start and end nodes
|
||||
if !way.nodes.is_empty() {
|
||||
let start_node = &way.nodes[0];
|
||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||
|
||||
let start_coord = (start_node.x, start_node.z);
|
||||
let end_coord = (end_node.x, end_node.z);
|
||||
|
||||
connectivity_map
|
||||
.entry(start_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
connectivity_map
|
||||
.entry(end_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectivity_map
|
||||
}
|
||||
|
||||
/// Internal function that generates highways with connectivity context for elevation handling
|
||||
fn generate_highways_internal(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
||||
) {
|
||||
if let Some(highway_type) = element.tags().get("highway") {
|
||||
if highway_type == "street_lamp" {
|
||||
// Handle street lamps
|
||||
@@ -97,13 +158,17 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
let mut add_outline = false;
|
||||
let scale_factor = args.scale;
|
||||
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Parse the layer value for elevation calculation
|
||||
let layer_value = element
|
||||
.tags()
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0)
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Skip if 'level' is negative in the tags (indoor mapping)
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
@@ -120,10 +185,14 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
block_type = DIRT_PATH;
|
||||
block_range = 1;
|
||||
}
|
||||
"motorway" | "primary" => {
|
||||
"motorway" | "primary" | "trunk" => {
|
||||
block_range = 5;
|
||||
add_stripe = true;
|
||||
}
|
||||
"secondary" => {
|
||||
block_range = 4;
|
||||
add_stripe = true;
|
||||
}
|
||||
"tertiary" => {
|
||||
add_stripe = true;
|
||||
}
|
||||
@@ -173,7 +242,40 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
block_range = ((block_range as f64) * scale_factor).floor() as i32;
|
||||
}
|
||||
|
||||
// Calculate elevation based on layer
|
||||
const LAYER_HEIGHT_STEP: i32 = 6; // Each layer is 6 blocks higher/lower
|
||||
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||
|
||||
// Check if we need slopes at start and end
|
||||
let needs_start_slope =
|
||||
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||
let needs_end_slope = should_add_slope_at_node(
|
||||
&way.nodes[way.nodes.len() - 1],
|
||||
layer_value,
|
||||
highway_connectivity,
|
||||
);
|
||||
|
||||
// Calculate total way length for slope distribution
|
||||
let total_way_length = calculate_way_length(way);
|
||||
|
||||
// Check if this is a short isolated elevated segment - if so, treat as ground level
|
||||
let is_short_isolated_elevated =
|
||||
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||
|
||||
// Override elevation and slopes for short isolated segments
|
||||
let (effective_elevation, effective_start_slope, effective_end_slope) =
|
||||
if is_short_isolated_elevated {
|
||||
(0, false, false) // Treat as ground level
|
||||
} else {
|
||||
(base_elevation, needs_start_slope, needs_end_slope)
|
||||
};
|
||||
|
||||
let slope_length = (total_way_length as f32 * 0.35).clamp(15.0, 50.0) as usize; // 35% of way length, max 50 blocks, min 15 blocks
|
||||
|
||||
// Iterate over nodes to create the highway
|
||||
let mut segment_index = 0;
|
||||
let total_segments = way.nodes.len() - 1;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let (x1, z1) = prev;
|
||||
@@ -181,17 +283,30 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
let z2: i32 = node.z;
|
||||
|
||||
// Generate the line of coordinates between the two nodes
|
||||
// we don't care about the y because it's going to get overwritten
|
||||
// I'm not sure if we'll keep it this way
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(x1, 0, z1, x2, 0, z2);
|
||||
|
||||
// Calculate elevation for this segment
|
||||
let segment_length = bresenham_points.len();
|
||||
|
||||
// Variables to manage dashed line pattern
|
||||
let mut stripe_length: i32 = 0;
|
||||
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the solid part of the stripe
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the gap part of the stripe
|
||||
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
|
||||
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||
// Calculate Y elevation for this point based on slopes and layer
|
||||
let current_y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
|
||||
for (x, _, z) in bresenham_points {
|
||||
// Draw the road surface for the entire width
|
||||
for dx in -block_range..=block_range {
|
||||
for dz in -block_range..=block_range {
|
||||
@@ -209,7 +324,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -218,7 +333,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
@@ -228,7 +343,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -237,7 +352,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
@@ -247,12 +362,38 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
block_type,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone brick foundation underneath elevated highways for thickness
|
||||
if effective_elevation > 0 && current_y > 0 {
|
||||
// Add 1 layer of stone bricks underneath the highway surface
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Add support pillars for elevated highways
|
||||
if effective_elevation != 0 && current_y > 0 {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +406,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
0,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
@@ -278,7 +419,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
0,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
@@ -289,12 +430,12 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
// Add a dashed white line in the middle for larger roads
|
||||
if add_stripe {
|
||||
if stripe_length < dash_length {
|
||||
let stripe_x: i32 = x;
|
||||
let stripe_z: i32 = z;
|
||||
let stripe_x: i32 = *x;
|
||||
let stripe_z: i32 = *z;
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
0,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -308,6 +449,8 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segment_index += 1;
|
||||
}
|
||||
previous_node = Some((node.x, node.z));
|
||||
}
|
||||
@@ -315,6 +458,131 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to determine if a slope should be added at a specific node
|
||||
fn should_add_slope_at_node(
|
||||
node: &crate::osm_parser::ProcessedNode,
|
||||
current_layer: i32,
|
||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>,
|
||||
) -> bool {
|
||||
let node_coord = (node.x, node.z);
|
||||
|
||||
// If we don't have connectivity information, always add slopes for non-zero layers
|
||||
if highway_connectivity.is_empty() {
|
||||
return current_layer != 0;
|
||||
}
|
||||
|
||||
// Check if there are other highways at different layers connected to this node
|
||||
if let Some(connected_layers) = highway_connectivity.get(&node_coord) {
|
||||
// Count how many ways are at the same layer as current way
|
||||
let same_layer_count = connected_layers
|
||||
.iter()
|
||||
.filter(|&&layer| layer == current_layer)
|
||||
.count();
|
||||
|
||||
// If this is the only way at this layer connecting to this node, we need a slope
|
||||
// (unless we're at ground level and connecting to ground level ways)
|
||||
if same_layer_count <= 1 {
|
||||
return current_layer != 0;
|
||||
}
|
||||
|
||||
// If there are multiple ways at the same layer, don't add slope
|
||||
false
|
||||
} else {
|
||||
// No other highways connected, add slope if not at ground level
|
||||
current_layer != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to calculate the total length of a way in blocks
|
||||
fn calculate_way_length(way: &ProcessedWay) -> usize {
|
||||
let mut total_length = 0;
|
||||
let mut previous_node: Option<&crate::osm_parser::ProcessedNode> = None;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let dx = (node.x - prev.x).abs();
|
||||
let dz = (node.z - prev.z).abs();
|
||||
total_length += ((dx * dx + dz * dz) as f32).sqrt() as usize;
|
||||
}
|
||||
previous_node = Some(node);
|
||||
}
|
||||
|
||||
total_length
|
||||
}
|
||||
|
||||
/// Calculate the Y elevation for a specific point along the highway
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate_point_elevation(
|
||||
segment_index: usize,
|
||||
point_index: usize,
|
||||
segment_length: usize,
|
||||
total_segments: usize,
|
||||
base_elevation: i32,
|
||||
needs_start_slope: bool,
|
||||
needs_end_slope: bool,
|
||||
slope_length: usize,
|
||||
) -> i32 {
|
||||
// If no slopes needed, return base elevation
|
||||
if !needs_start_slope && !needs_end_slope {
|
||||
return base_elevation;
|
||||
}
|
||||
|
||||
// Calculate total distance from start
|
||||
let total_distance_from_start = segment_index * segment_length + point_index;
|
||||
let total_way_length = total_segments * segment_length;
|
||||
|
||||
// Ensure we have reasonable values
|
||||
if total_way_length == 0 || slope_length == 0 {
|
||||
return base_elevation;
|
||||
}
|
||||
|
||||
// Start slope calculation - gradual rise from ground level
|
||||
if needs_start_slope && total_distance_from_start <= slope_length {
|
||||
let slope_progress = total_distance_from_start as f32 / slope_length as f32;
|
||||
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||
return elevation_offset;
|
||||
}
|
||||
|
||||
// End slope calculation - gradual descent to ground level
|
||||
if needs_end_slope
|
||||
&& total_distance_from_start >= (total_way_length.saturating_sub(slope_length))
|
||||
{
|
||||
let distance_from_end = total_way_length - total_distance_from_start;
|
||||
let slope_progress = distance_from_end as f32 / slope_length as f32;
|
||||
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||
return elevation_offset;
|
||||
}
|
||||
|
||||
// Middle section at full elevation
|
||||
base_elevation
|
||||
}
|
||||
|
||||
/// Add support pillars for elevated highways
|
||||
fn add_highway_support_pillar(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
highway_y: i32,
|
||||
z: i32,
|
||||
dx: i32,
|
||||
dz: i32,
|
||||
_block_range: i32, // Keep for future use
|
||||
) {
|
||||
// Only add pillars at specific intervals and positions
|
||||
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
||||
// Add pillar from ground to highway level
|
||||
for y in 1..highway_y {
|
||||
editor.set_block(STONE_BRICKS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Add pillar base
|
||||
for base_dx in -1..=1 {
|
||||
for base_dz in -1..=1 {
|
||||
editor.set_block(STONE_BRICKS, x + base_dx, 0, z + base_dz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a siding using stone brick slabs
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
|
||||
@@ -18,6 +18,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
"park" | "nature_reserve" | "garden" | "disc_golf_course" | "golf_course" => {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
"schoolyard" => BLACK_CONCRETE,
|
||||
"playground" | "recreation_ground" | "pitch" | "beach_resort" | "dog_park" => {
|
||||
if let Some(surface) = element.tags.get("surface") {
|
||||
match surface.as_str() {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub fn generate_man_made(editor: &mut WorldEditor, element: &ProcessedElement, _
|
||||
"chimney" => generate_chimney(editor, element),
|
||||
"water_well" => generate_water_well(editor, element),
|
||||
"water_tower" => generate_water_tower(editor, element),
|
||||
"mast" => generate_antenna(editor, element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,6 @@ fn generate_antenna(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
Some(h) => h.parse::<i32>().unwrap_or(20).min(40), // Max 40 blocks
|
||||
None => match element.tags().get("tower:type").map(|s| s.as_str()) {
|
||||
Some("communication") => 20,
|
||||
Some("transmission") => 25,
|
||||
Some("cellular") => 15,
|
||||
_ => 20,
|
||||
},
|
||||
@@ -249,6 +249,7 @@ pub fn generate_man_made_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
"chimney" => generate_chimney(editor, &element),
|
||||
"water_well" => generate_water_well(editor, &element),
|
||||
"water_tower" => generate_water_tower(editor, &element),
|
||||
"mast" => generate_antenna(editor, &element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,3 +174,71 @@ fn determine_rail_direction(
|
||||
(None, None) => RAIL_NORTH_SOUTH,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_roller_coaster(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
if let Some(roller_coaster) = element.tags.get("roller_coaster") {
|
||||
if roller_coaster == "track" {
|
||||
// Check if it's indoor (skip if yes)
|
||||
if let Some(indoor) = element.tags.get("indoor") {
|
||||
if indoor == "yes" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if layer is negative (skip if yes)
|
||||
if let Some(layer) = element.tags.get("layer") {
|
||||
if let Ok(layer_value) = layer.parse::<i32>() {
|
||||
if layer_value < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elevation_height = 4; // 4 blocks in the air
|
||||
let pillar_interval = 6; // Support pillars every 6 blocks
|
||||
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev_node = element.nodes[i - 1].xz();
|
||||
let cur_node = element.nodes[i].xz();
|
||||
|
||||
let points = bresenham_line(prev_node.x, 0, prev_node.z, cur_node.x, 0, cur_node.z);
|
||||
let smoothed_points = smooth_diagonal_rails(&points);
|
||||
|
||||
for j in 0..smoothed_points.len() {
|
||||
let (bx, _, bz) = smoothed_points[j];
|
||||
|
||||
// Place track foundation at elevation height
|
||||
editor.set_block(IRON_BLOCK, bx, elevation_height, bz, None, None);
|
||||
|
||||
let prev = if j > 0 {
|
||||
Some(smoothed_points[j - 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next = if j < smoothed_points.len() - 1 {
|
||||
Some(smoothed_points[j + 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rail_block = determine_rail_direction(
|
||||
(bx, bz),
|
||||
prev.map(|(x, _, z)| (x, z)),
|
||||
next.map(|(x, _, z)| (x, z)),
|
||||
);
|
||||
|
||||
// Place rail on top of the foundation
|
||||
editor.set_block(rail_block, bx, elevation_height + 1, bz, None, None);
|
||||
|
||||
// Place support pillars every pillar_interval blocks
|
||||
if bx % pillar_interval == 0 && bz % pillar_interval == 0 {
|
||||
// Create a pillar from ground level up to the track
|
||||
for y in 1..elevation_height {
|
||||
editor.set_block(IRON_BLOCK, bx, y, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
'6' => Some(RED_BED_SOUTH_FOOT), // Bed South Foot
|
||||
'7' => Some(RED_BED_WEST_HEAD), // Bed West Head
|
||||
'8' => Some(RED_BED_WEST_FOOT), // Bed West Foot
|
||||
'H' => Some(CHEST), // Chest
|
||||
// 'H' => Some(CHEST), // Chest
|
||||
'L' => Some(CAULDRON), // Cauldron
|
||||
'A' => Some(ANVIL), // Anvil
|
||||
'P' => Some(OAK_PRESSURE_PLATE), // Pressure Plate
|
||||
@@ -145,7 +145,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(JUKEBOX), // Jukebox
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
|
||||
@@ -307,7 +307,7 @@ impl Tree<'_> {
|
||||
FURNACE,
|
||||
ANVIL,
|
||||
BREWING_STAND,
|
||||
JUKEBOX,
|
||||
NOTE_BLOCK,
|
||||
BOOKSHELF,
|
||||
CAULDRON,
|
||||
// Beds
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
coordinate_system::cartesian::XZPoint,
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation},
|
||||
coordinate_system::cartesian::{XZBBox, XZPoint},
|
||||
debug_logging,
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
|
||||
world_editor::WorldEditor,
|
||||
};
|
||||
|
||||
pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelation) {
|
||||
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) {
|
||||
println!("Skipping way {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &[], start_time);
|
||||
}
|
||||
|
||||
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)
|
||||
let is_water = element.tags.contains_key("water")
|
||||
|| element.tags.get("natural") == Some(&"water".to_string());
|
||||
|| element
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_water {
|
||||
return;
|
||||
@@ -36,70 +62,314 @@ pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelatio
|
||||
}
|
||||
}
|
||||
|
||||
// Process each outer polygon individually
|
||||
for (i, outer_nodes) in outers.iter().enumerate() {
|
||||
let mut individual_outers = vec![outer_nodes.clone()];
|
||||
// DON'T auto-swap outer/inner - this causes more problems than it solves
|
||||
// OSM data should already have correct roles; if it's wrong in OSM, fix it there
|
||||
// The previous heuristic was causing water to fill on land
|
||||
|
||||
merge_loopy_loops(&mut individual_outers);
|
||||
if !verify_loopy_loops(&individual_outers) {
|
||||
println!(
|
||||
"Skipping invalid outer polygon {} for relation {}",
|
||||
i + 1,
|
||||
element.id
|
||||
// However, log if we detect a suspicious configuration
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
let outer_nodes: usize = outers.iter().map(|o| o.len()).sum();
|
||||
let inner_nodes: usize = inners.iter().map(|i| i.len()).sum();
|
||||
if !inners.is_empty() && inner_nodes > outer_nodes * 2 {
|
||||
eprintln!(
|
||||
"DEBUG: Relation {} has {} inner nodes vs {} outer nodes - may be inverted",
|
||||
element.id, inner_nodes, outer_nodes
|
||||
);
|
||||
continue; // Skip this outer if it's not valid
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut inners);
|
||||
if !verify_loopy_loops(&inners) {
|
||||
// If inners are invalid, process outer without inners
|
||||
let empty_inners: Vec<Vec<ProcessedNode>> = vec![];
|
||||
let mut temp_inners = empty_inners;
|
||||
merge_loopy_loops(&mut temp_inners);
|
||||
// Log BEFORE merge_loopy_loops
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
let notes = vec![
|
||||
format!(
|
||||
"Before merge_loopy_loops: {} outer loops, {} inner loops",
|
||||
outers.len(),
|
||||
inners.len()
|
||||
),
|
||||
format!(
|
||||
"Outer loops details: {}",
|
||||
outers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, o)| format!(
|
||||
"loop{}: {} nodes (first_id={}, last_id={})",
|
||||
i,
|
||||
o.len(),
|
||||
o.first().map(|n| n.id).unwrap_or(0),
|
||||
o.last().map(|n| n.id).unwrap_or(0)
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
];
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let empty_inners_xz: Vec<Vec<XZPoint>> = vec![];
|
||||
|
||||
inverse_floodfill(
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
individual_outers_xz,
|
||||
empty_inners_xz,
|
||||
editor,
|
||||
start_time,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
||||
let fake_members: Vec<crate::osm_parser::ProcessedMember> = outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let inners_xz: Vec<Vec<XZPoint>> = inners
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.map(|nodes| crate::osm_parser::ProcessedMember {
|
||||
role: ProcessedMemberRole::Outer,
|
||||
way: ProcessedWay {
|
||||
id: 0,
|
||||
tags: std::collections::HashMap::new(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
individual_outers_xz,
|
||||
inners_xz,
|
||||
editor,
|
||||
start_time,
|
||||
debug_logging::log_relation_transformation(
|
||||
"4_before_merge_loopy_loops",
|
||||
element.id,
|
||||
&element.tags,
|
||||
&fake_members,
|
||||
notes,
|
||||
);
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut outers);
|
||||
|
||||
// Log AFTER merge_loopy_loops
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
let notes = vec![
|
||||
format!(
|
||||
"After merge_loopy_loops: {} outer loops, {} inner loops",
|
||||
outers.len(),
|
||||
inners.len()
|
||||
),
|
||||
format!(
|
||||
"Outer loops details: {}",
|
||||
outers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, o)| format!(
|
||||
"loop{}: {} nodes (first_id={}, last_id={})",
|
||||
i,
|
||||
o.len(),
|
||||
o.first().map(|n| n.id).unwrap_or(0),
|
||||
o.last().map(|n| n.id).unwrap_or(0)
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
];
|
||||
|
||||
let fake_members: Vec<crate::osm_parser::ProcessedMember> = outers
|
||||
.iter()
|
||||
.map(|nodes| crate::osm_parser::ProcessedMember {
|
||||
role: ProcessedMemberRole::Outer,
|
||||
way: ProcessedWay {
|
||||
id: 0,
|
||||
tags: std::collections::HashMap::new(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug_logging::log_relation_transformation(
|
||||
"5_after_merge_loopy_loops",
|
||||
element.id,
|
||||
&element.tags,
|
||||
&fake_members,
|
||||
notes,
|
||||
);
|
||||
}
|
||||
|
||||
// NOW clip the assembled complete rings to bbox
|
||||
// This is crucial: we merged complete rings first, THEN clip them
|
||||
outers = outers
|
||||
.into_iter()
|
||||
.filter_map(|ring| {
|
||||
let clipped = clip_polygon_ring_to_bbox(&ring, xzbbox);
|
||||
if clipped.is_none() && debug_logging::is_tracking_element(element.id) {
|
||||
eprintln!(
|
||||
"DEBUG: Relation {} outer ring failed clipping (ring had {} nodes)",
|
||||
element.id,
|
||||
ring.len()
|
||||
);
|
||||
}
|
||||
clipped
|
||||
})
|
||||
.collect();
|
||||
merge_loopy_loops(&mut inners);
|
||||
inners = inners
|
||||
.into_iter()
|
||||
.filter_map(|ring| {
|
||||
let clipped = clip_polygon_ring_to_bbox(&ring, xzbbox);
|
||||
if clipped.is_none() && debug_logging::is_tracking_element(element.id) {
|
||||
eprintln!(
|
||||
"DEBUG: Relation {} inner ring failed clipping (ring had {} nodes)",
|
||||
element.id,
|
||||
ring.len()
|
||||
);
|
||||
}
|
||||
clipped
|
||||
})
|
||||
.collect();
|
||||
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
eprintln!(
|
||||
"DEBUG: After bbox clipping: {} outer rings, {} inner rings",
|
||||
outers.len(),
|
||||
inners.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !verify_loopy_loops(&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
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
eprintln!(
|
||||
"DEBUG: Relation {} has {} outer loops before filtering",
|
||||
element.id,
|
||||
outers.len()
|
||||
);
|
||||
for (i, outer) in outers.iter().enumerate() {
|
||||
let first = &outer[0];
|
||||
let last = outer.last().unwrap();
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
let is_closed = first.id == last.id || (dx <= 1 && dz <= 1);
|
||||
eprintln!(
|
||||
" Loop {}: {} nodes, closed={}, endpoints {} blocks apart (dx={}, dz={})",
|
||||
i,
|
||||
outer.len(),
|
||||
is_closed,
|
||||
((dx * dx + dz * dz) as f64).sqrt() as i32,
|
||||
dx,
|
||||
dz
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 debug_logging::is_tracking_element(element.id) {
|
||||
eprintln!(
|
||||
"DEBUG: Relation {} has {} outer loops after filtering and closing",
|
||||
element.id,
|
||||
outers.len()
|
||||
);
|
||||
}
|
||||
|
||||
// If no valid outer loops remain, skip the relation
|
||||
if outers.is_empty() {
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
debug_logging::log_relation_transformation(
|
||||
"6_SKIPPED_no_valid_loops",
|
||||
element.id,
|
||||
&element.tags,
|
||||
&[],
|
||||
vec![
|
||||
"No properly closed loops after filtering unclosed segments".to_string(),
|
||||
"Skipping to avoid diagonal wedge artifacts".to_string(),
|
||||
],
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify again after filtering and closing
|
||||
if !verify_loopy_loops(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
|
||||
if debug_logging::is_tracking_element(element.id) {
|
||||
debug_logging::log_relation_transformation(
|
||||
"6_SKIPPED_invalid_polygon",
|
||||
element.id,
|
||||
&element.tags,
|
||||
&[],
|
||||
vec![
|
||||
"verify_loopy_loops returned false after filtering and closure".to_string(),
|
||||
format!("Number of outer loops: {}", outers.len()),
|
||||
format!(
|
||||
"Outer loop sizes: {:?}",
|
||||
outers.iter().map(|l| l.len()).collect::<Vec<_>>()
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut inners);
|
||||
if !verify_loopy_loops(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &inners, start_time);
|
||||
}
|
||||
|
||||
fn generate_water_areas(
|
||||
editor: &mut WorldEditor,
|
||||
outers: &[Vec<ProcessedNode>],
|
||||
inners: &[Vec<ProcessedNode>],
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Calculate the actual bounding box of the polygon nodes
|
||||
// This is CRITICAL for performance - we only need to scan the area covered by the polygons,
|
||||
// not the entire world!
|
||||
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<_>>())
|
||||
.collect();
|
||||
let inners_xz: Vec<Vec<XZPoint>> = inners
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(
|
||||
min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor, start_time,
|
||||
);
|
||||
}
|
||||
|
||||
// Merges ways that share nodes into full loops
|
||||
@@ -107,6 +377,18 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
// Helper function to check if two nodes match (by ID or proximity)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
// Also match if coordinates are very close (within 1 block)
|
||||
// This handles synthetic nodes created at bbox edges
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..loops.len() {
|
||||
for j in 0..loops.len() {
|
||||
if i == j {
|
||||
@@ -120,17 +402,27 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let x: &Vec<ProcessedNode> = &loops[i];
|
||||
let y: &Vec<ProcessedNode> = &loops[j];
|
||||
|
||||
// Skip empty loops (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// it's looped already
|
||||
if x[0].id == x.last().unwrap().id {
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's looped already
|
||||
if y[0].id == y.last().unwrap().id {
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if x[0].id == y[0].id {
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -138,7 +430,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);
|
||||
|
||||
@@ -146,7 +438,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);
|
||||
|
||||
@@ -154,7 +446,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);
|
||||
|
||||
@@ -185,7 +477,17 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
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 {
|
||||
let first = &l[0];
|
||||
let last = l.last().unwrap();
|
||||
|
||||
// Check if loop 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 loop");
|
||||
valid = false;
|
||||
}
|
||||
@@ -194,6 +496,226 @@ fn verify_loopy_loops(loops: &[Vec<ProcessedNode>]) -> bool {
|
||||
valid
|
||||
}
|
||||
|
||||
/// Force-close loops that have endpoints very close to each other
|
||||
/// This handles cases where clipping creates nearly-closed loops
|
||||
fn close_open_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
for loop_nodes in loops.iter_mut() {
|
||||
if loop_nodes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let first = &loop_nodes[0];
|
||||
let last = &loop_nodes[loop_nodes.len() - 1];
|
||||
|
||||
// Check if already closed
|
||||
if first.id == last.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if endpoints are very close - just duplicate first node to close
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
|
||||
if dx <= 1 && dz <= 1 {
|
||||
// Already essentially closed, just duplicate first node
|
||||
loop_nodes.push(first.clone());
|
||||
} else {
|
||||
// Endpoints are far apart - this is likely a clipped multipolygon
|
||||
// that enters/exits the bbox. Close it by connecting endpoints directly.
|
||||
// This creates a "closed polygon within bbox" representation.
|
||||
loop_nodes.push(first.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip a complete polygon ring to the bbox using Sutherland-Hodgman algorithm
|
||||
/// Returns None if the polygon is completely outside the bbox
|
||||
fn clip_polygon_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 - if so, return unchanged
|
||||
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 {
|
||||
// Ring is entirely inside bbox, no clipping needed
|
||||
return Some(ring.to_vec());
|
||||
}
|
||||
|
||||
// Check if entire ring is outside bbox
|
||||
let all_outside_left = ring.iter().all(|n| (n.x as f64) < min_x);
|
||||
let all_outside_right = ring.iter().all(|n| (n.x as f64) > max_x);
|
||||
let all_outside_top = ring.iter().all(|n| (n.z as f64) < min_z);
|
||||
let all_outside_bottom = ring.iter().all(|n| (n.z as f64) > max_z);
|
||||
|
||||
if all_outside_left || all_outside_right || all_outside_top || all_outside_bottom {
|
||||
// Ring is entirely outside bbox
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ring crosses bbox boundary, need to clip
|
||||
// Convert to f64 coordinates for clipping
|
||||
let mut polygon: Vec<(f64, f64)> = ring.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||
|
||||
// Ensure polygon is closed
|
||||
if !polygon.is_empty() && polygon.first() != polygon.last() {
|
||||
polygon.push(polygon[0]);
|
||||
}
|
||||
|
||||
// Clip against each edge of the bounding box
|
||||
// Edges are traversed COUNTER-CLOCKWISE, so "inside" (left of edge) = inside bbox
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z), // Bottom edge: left to right
|
||||
(max_x, min_z, max_x, max_z), // Right edge: bottom to top
|
||||
(max_x, max_z, min_x, max_z), // Top edge: right to left
|
||||
(min_x, max_z, min_x, min_z), // Left edge: top to bottom
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||
let mut clipped = Vec::new();
|
||||
|
||||
if polygon.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Process edges: iterate through adjacent pairs
|
||||
// For a closed polygon, we process n-1 edges (since last point == first point)
|
||||
for i in 0..(polygon.len() - 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 {
|
||||
// Entering: add intersection
|
||||
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 intersection to bbox to handle floating-point errors
|
||||
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 {
|
||||
// Exiting: add intersection
|
||||
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 intersection to bbox to handle floating-point errors
|
||||
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
polygon = clipped;
|
||||
}
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return None; // Not a valid polygon
|
||||
}
|
||||
|
||||
// Verify all points are within bbox before returning
|
||||
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_polygon_ring_to_bbox produced points outside bbox!");
|
||||
eprintln!(" Bbox: x=[{}, {}], z=[{}, {}]", min_x, max_x, min_z, max_z);
|
||||
for (i, &(x, z)) in polygon.iter().enumerate() {
|
||||
if x < min_x || x > max_x || z < min_z || z > max_z {
|
||||
eprintln!(" Point {}: ({}, {}) is OUTSIDE", i, x, z);
|
||||
}
|
||||
}
|
||||
return None; // Reject invalid result
|
||||
}
|
||||
|
||||
// Convert back to ProcessedNode with synthetic IDs
|
||||
// IMPORTANT: Clamp coordinates to bbox boundaries to handle floating-point edge cases
|
||||
let mut result: Vec<ProcessedNode> = polygon
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(x, z))| {
|
||||
let clamped_x = x.clamp(min_x, max_x);
|
||||
let clamped_z = z.clamp(min_z, max_z);
|
||||
ProcessedNode {
|
||||
id: 1_000_000_000 + i as u64, // Synthetic ID for clipped nodes
|
||||
tags: std::collections::HashMap::new(),
|
||||
x: clamped_x.round() as i32,
|
||||
z: clamped_z.round() as i32,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Ensure first and last have same ID to close the loop
|
||||
if !result.is_empty() {
|
||||
let first_id = result[0].id;
|
||||
result.last_mut().unwrap().id = first_id;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn point_inside_edge(
|
||||
point: (f64, f64),
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> bool {
|
||||
// Cross product to determine if point is on the "inside" (left) of the edge
|
||||
let dx = edge_x2 - edge_x1;
|
||||
let dz = edge_z2 - edge_z1;
|
||||
let px = point.0 - edge_x1;
|
||||
let pz = point.1 - edge_z1;
|
||||
(dx * pz - dz * px) >= 0.0
|
||||
}
|
||||
|
||||
fn line_edge_intersection(
|
||||
x1: f64,
|
||||
z1: f64,
|
||||
x2: f64,
|
||||
z2: f64,
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let dx = x2 - x1;
|
||||
let dz = z2 - z1;
|
||||
let edge_dx = edge_x2 - edge_x1;
|
||||
let edge_dz = edge_z2 - edge_z1;
|
||||
|
||||
let denominator = dx * edge_dz - dz * edge_dx;
|
||||
if denominator.abs() < 1e-10 {
|
||||
return None; // Parallel lines
|
||||
}
|
||||
|
||||
let t = ((edge_x1 - x1) * edge_dz - (edge_z1 - z1) * edge_dx) / denominator;
|
||||
if !(0.0..=1.0).contains(&t) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((x1 + t * dx, z1 + t * dz))
|
||||
}
|
||||
|
||||
// Water areas are absolutely huge. We can't easily flood fill the entire thing.
|
||||
// Instead, we'll iterate over all the blocks in our MC world, and check if each
|
||||
// one is in the river or not
|
||||
@@ -208,6 +730,10 @@ fn inverse_floodfill(
|
||||
editor: &mut WorldEditor,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Convert to geo Polygons and ORIENT them correctly
|
||||
// The geo crate expects exterior rings to be CCW and interior rings to be CW
|
||||
// Our coordinate transformation inverts the Z axis, which reverses winding order
|
||||
// Using orient(Direction::Default) normalizes this to the expected convention
|
||||
let inners: Vec<_> = inners
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
@@ -219,6 +745,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default) // Normalize winding order
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -233,6 +760,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default) // Normalize winding order
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use image::Rgb;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -44,6 +46,28 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Downloads a tile from AWS Terrain Tiles service
|
||||
fn download_tile(
|
||||
client: &reqwest::blocking::Client,
|
||||
tile_x: u32,
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
|
||||
pub fn fetch_elevation_data(
|
||||
bbox: &LLBBox,
|
||||
scale: f64,
|
||||
@@ -80,26 +104,81 @@ pub fn fetch_elevation_data(
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
|
||||
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
let img: image::DynamicImage = image::open(&tile_path)?;
|
||||
img.to_rgb8()
|
||||
} else {
|
||||
// AWS Terrain Tiles don't require an API key
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = match std::fs::metadata(&tile_path) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(&tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
img.to_rgb8()
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile appears to be too small. Refetching tile.",
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
} else {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
};
|
||||
|
||||
// Only process pixels that fall within the requested bbox
|
||||
|
||||
@@ -68,11 +68,13 @@ 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;
|
||||
|
||||
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) {
|
||||
// Fast timeout check - only every few iterations
|
||||
if filled_area.len() % 100 == 0 {
|
||||
// Check timeout more frequently for small areas
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 50 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
return filled_area;
|
||||
@@ -80,6 +82,9 @@ fn optimized_flood_fill_area(
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
|
||||
// Skip if already visited or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -93,6 +98,19 @@ 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;
|
||||
|
||||
// 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));
|
||||
|
||||
@@ -155,12 +173,14 @@ 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;
|
||||
|
||||
// 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) {
|
||||
// Reduced timeout checking frequency for better performance
|
||||
if global_visited.len() % 200 == 0 {
|
||||
// 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;
|
||||
@@ -168,6 +188,9 @@ fn original_flood_fill_area(
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
|
||||
// Skip if already processed or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -181,6 +204,19 @@ 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;
|
||||
|
||||
// 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));
|
||||
|
||||
@@ -23,12 +23,22 @@ impl Ground {
|
||||
}
|
||||
|
||||
pub fn new_enabled(bbox: &LLBBox, scale: f64, ground_level: i32) -> Self {
|
||||
let elevation_data = fetch_elevation_data(bbox, scale, ground_level)
|
||||
.expect("Failed to fetch elevation data");
|
||||
Self {
|
||||
elevation_enabled: true,
|
||||
ground_level,
|
||||
elevation_data: Some(elevation_data),
|
||||
match fetch_elevation_data(bbox, scale, ground_level) {
|
||||
Ok(elevation_data) => Self {
|
||||
elevation_enabled: true,
|
||||
ground_level,
|
||||
elevation_data: Some(elevation_data),
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch elevation data: {}", e);
|
||||
emit_gui_progress_update(15.0, "Elevation unavailable, using flat ground");
|
||||
// Graceful fallback: disable elevation and keep provided ground_level
|
||||
Self {
|
||||
elevation_enabled: false,
|
||||
ground_level,
|
||||
elevation_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
132
src/gui.rs
@@ -1,21 +1,23 @@
|
||||
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::ground::{self, Ground};
|
||||
use crate::map_transformation;
|
||||
use crate::osm_parser;
|
||||
use crate::progress;
|
||||
use crate::retrieve_data;
|
||||
use crate::telemetry::{self, send_log, LogLevel};
|
||||
use crate::version_check;
|
||||
use fastnbt::Value;
|
||||
use flate2::read::GzDecoder;
|
||||
use fs2::FileExt;
|
||||
use log::{error, LevelFilter};
|
||||
use log::LevelFilter;
|
||||
use rfd::FileDialog;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{env, fs, io::Write, panic};
|
||||
use std::{env, fs, io::Write};
|
||||
use tauri_plugin_log::{Builder as LogBuilder, Target, TargetKind};
|
||||
|
||||
/// Manages the session.lock file for a Minecraft world directory
|
||||
@@ -62,12 +64,8 @@ pub fn run_gui() {
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
|
||||
// Set a custom panic hook to log panic information
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
let message = format!("Application panicked: {panic_info:?}");
|
||||
error!("{message}");
|
||||
std::process::exit(1);
|
||||
}));
|
||||
// Install panic hook for crash reporting
|
||||
telemetry::install_panic_hook();
|
||||
|
||||
// Workaround WebKit2GTK issue with NVIDIA drivers and graphics issues
|
||||
// Source: https://github.com/tauri-apps/tauri/issues/10702
|
||||
@@ -226,13 +224,13 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||
|
||||
// Copy the region template file
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../mcassets/region.template");
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||
fs::write(®ion_path, REGION_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||
|
||||
// Add the level.dat file
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../mcassets/level.dat");
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||
|
||||
// Decompress the gzipped level.template
|
||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||
@@ -299,7 +297,7 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||
|
||||
// Add the icon.png file
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../mcassets/icon.png");
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||
|
||||
@@ -307,52 +305,45 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
}
|
||||
|
||||
/// Adds localized area name to the world name in level.dat
|
||||
fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
let world_path = PathBuf::from(world_path_str);
|
||||
|
||||
fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
// Only proceed if the path exists
|
||||
if !world_path.exists() {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Check the level.dat file first to get the current name
|
||||
let level_path = world_path.join("level.dat");
|
||||
|
||||
if !level_path.exists() {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Try to read the current world name from level.dat
|
||||
let current_name = match std::fs::read(&level_path) {
|
||||
Ok(level_data) => {
|
||||
let mut decoder = GzDecoder::new(level_data.as_slice());
|
||||
let mut decompressed_data = Vec::new();
|
||||
if decoder.read_to_end(&mut decompressed_data).is_ok() {
|
||||
if let Ok(Value::Compound(ref root)) =
|
||||
fastnbt::from_bytes::<Value>(&decompressed_data)
|
||||
{
|
||||
if let Some(Value::Compound(ref data)) = root.get("Data") {
|
||||
if let Some(Value::String(name)) = data.get("LevelName") {
|
||||
name.clone()
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
}
|
||||
Err(_) => return world_path_str.to_string(),
|
||||
let Ok(level_data) = std::fs::read(&level_path) else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let mut decoder = GzDecoder::new(level_data.as_slice());
|
||||
let mut decompressed_data = Vec::new();
|
||||
if decoder.read_to_end(&mut decompressed_data).is_err() {
|
||||
return world_path;
|
||||
}
|
||||
|
||||
let Ok(Value::Compound(ref root)) = fastnbt::from_bytes::<Value>(&decompressed_data) else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let Some(Value::Compound(ref data)) = root.get("Data") else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let Some(Value::String(current_name)) = data.get("LevelName") else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
// Only modify if it's an Arnis world and doesn't already have an area name
|
||||
if !current_name.starts_with("Arnis World ") || current_name.contains(": ") {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Calculate center coordinates of bbox
|
||||
@@ -362,7 +353,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
// Try to fetch the area name
|
||||
let area_name = match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => return world_path_str.to_string(), // Keep original name if no area name found
|
||||
_ => return world_path, // Keep original name if no area name found
|
||||
};
|
||||
|
||||
// Create new name with localized area name, ensuring total length doesn't exceed 30 characters
|
||||
@@ -378,7 +369,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
.collect::<String>()
|
||||
} else if max_area_name_len == 0 {
|
||||
// If base name is already too long, don't add area name
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
} else {
|
||||
area_name
|
||||
};
|
||||
@@ -406,6 +397,10 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
if let Ok(compressed_data) = encoder.finish() {
|
||||
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
||||
eprintln!("Failed to update level.dat with area name: {e}");
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to update level.dat with area name",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +412,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
}
|
||||
|
||||
// Return the original path since we didn't change the directory name
|
||||
world_path_str.to_string()
|
||||
world_path
|
||||
}
|
||||
|
||||
// Function to update player position in level.dat based on spawn point coordinates
|
||||
@@ -528,7 +523,7 @@ fn update_player_position(
|
||||
|
||||
// Function to update player spawn Y coordinate based on terrain height after generation
|
||||
pub fn update_player_spawn_y_after_generation(
|
||||
world_path: &str,
|
||||
world_path: &Path,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
@@ -678,15 +673,23 @@ fn gui_start_generation(
|
||||
ground_level: i32,
|
||||
floodfill_timeout: u64,
|
||||
terrain_enabled: bool,
|
||||
skip_osm_objects: bool,
|
||||
interior_enabled: bool,
|
||||
roof_enabled: bool,
|
||||
fillground_enabled: bool,
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
) -> Result<(), String> {
|
||||
use progress::emit_gui_error;
|
||||
use LLBBox;
|
||||
|
||||
// Store telemetry consent for crash reporting
|
||||
telemetry::set_telemetry_consent(telemetry_consent);
|
||||
|
||||
// Send generation click telemetry
|
||||
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() {
|
||||
// Verify the spawn point is within bounds
|
||||
@@ -744,9 +747,9 @@ 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(&selected_world, &bbox)
|
||||
add_localized_world_name(world_path, &bbox)
|
||||
} else {
|
||||
selected_world.clone()
|
||||
world_path
|
||||
};
|
||||
|
||||
// Create an Args instance with the chosen bounding box and world directory path
|
||||
@@ -767,7 +770,29 @@ fn gui_start_generation(
|
||||
spawn_point,
|
||||
};
|
||||
|
||||
// Run data fetch and world generation
|
||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||
if skip_osm_objects {
|
||||
// Generate ground data (terrain) for terrain-only mode
|
||||
let ground = ground::generate_ground_data(&args);
|
||||
|
||||
// Create empty parsed_elements and xzbbox for terrain-only mode
|
||||
let parsed_elements = Vec::new();
|
||||
let (_coord_transformer, xzbbox) =
|
||||
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
||||
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
||||
|
||||
let _ = data_processing::generate_world(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Run data fetch and world generation (standard mode: objects + terrain, or objects only)
|
||||
match retrieve_data::fetch_data_from_overpass(args.bbox, args.debug, "requests", None) {
|
||||
Ok(raw_data) => {
|
||||
let (mut parsed_elements, mut xzbbox) =
|
||||
@@ -793,8 +818,15 @@ fn gui_start_generation(
|
||||
&mut xzbbox,
|
||||
&mut ground,
|
||||
);
|
||||
send_log(LogLevel::Info, "Map transformation completed.");
|
||||
|
||||
let _ = data_processing::generate_world(parsed_elements, xzbbox, ground, &args);
|
||||
let _ = data_processing::generate_world(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 418 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
76
gui-src/css/styles.css → src/gui/css/styles.css
vendored
@@ -63,6 +63,7 @@ a:hover {
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -79,6 +80,12 @@ a:hover {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-box {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
@@ -94,6 +101,8 @@ a:hover {
|
||||
.map-container {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -249,6 +258,33 @@ button:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Modal actions/buttons */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-accent);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-accent-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.btn-secondary {
|
||||
background-color: #3a3a3a;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
#terrain-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
@@ -281,6 +317,10 @@ button:hover {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#telemetry-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
.scale-slider-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
@@ -306,7 +346,7 @@ button:hover {
|
||||
|
||||
#bbox-coords {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -353,7 +393,7 @@ button:hover {
|
||||
|
||||
.license-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.license-button {
|
||||
@@ -389,11 +429,39 @@ button:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Generation mode dropdown styling */
|
||||
.generation-mode-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
color: #0f0f0f;
|
||||
appearance: menulist;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.generation-mode-dropdown option {
|
||||
padding: 5px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.generation-mode-dropdown {
|
||||
background-color: #0f0f0f98;
|
||||
color: #ffffff;
|
||||
border: 1px solid #fecc44;
|
||||
}
|
||||
}
|
||||
|
||||
/* Language dropdown styling */
|
||||
.language-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -421,7 +489,7 @@ button:hover {
|
||||
.theme-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 487 B |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 556 B After Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
37
gui-src/index.html → src/gui/index.html
vendored
@@ -14,7 +14,7 @@
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="row">
|
||||
<a href="https://github.com/louis-e/arnis" target="_blank">
|
||||
<a href="https://arnismc.com" target="_blank">
|
||||
<img src="./images/logo.png" id="arnis-logo" class="logo arnis" alt="Arnis Logo" style="width: 35%; height: auto;">
|
||||
</a>
|
||||
</div>
|
||||
@@ -87,11 +87,15 @@
|
||||
<span class="close-button" onclick="closeSettings()">×</span>
|
||||
<h2 data-localize="customization_settings">Customization Settings</h2>
|
||||
|
||||
<!-- Terrain Toggle Button -->
|
||||
<!-- Generation Mode Dropdown -->
|
||||
<div class="settings-row">
|
||||
<label for="terrain-toggle" data-localize="terrain">Terrain</label>
|
||||
<label for="generation-mode-select" data-localize="generation_mode">Generation Mode</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="terrain-toggle" name="terrain-toggle" checked>
|
||||
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
||||
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
||||
<option value="geo-only" data-localize="mode_geo_only">Objects only</option>
|
||||
<option value="terrain-only" data-localize="mode_terrain_only">Terrain only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,6 +175,7 @@
|
||||
<option value="zh-CN">中文 (简体)</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="pl">Polski</option>
|
||||
<option value="lv">Latviešu</option>
|
||||
<option value="sv">Svenska</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="fi">Suomi</option>
|
||||
@@ -181,6 +186,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telemetry Consent Toggle -->
|
||||
<div class="settings-row">
|
||||
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License and Credits Button -->
|
||||
<div class="settings-row license-button-row">
|
||||
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
|
||||
@@ -198,6 +211,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telemetry Consent Modal (first run) -->
|
||||
<div id="telemetry-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" onclick="rejectTelemetry()">×</span>
|
||||
<h2>Help improve Arnis</h2>
|
||||
<p style="text-align:left; margin-top:6px; color:#ececec;">
|
||||
We’d like to collect anonymous usage data like crashes and performance to make Arnis more stable and faster.
|
||||
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">No personal data or world contents are collected.</a>
|
||||
</p>
|
||||
<div class="modal-actions" style="margin-top:14px;">
|
||||
<button type="button" class="btn-secondary" onclick="rejectTelemetry()">No thanks</button>
|
||||
<button type="button" class="btn-primary" onclick="acceptTelemetry()">Allow anonymous data</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License Modal -->
|
||||
<div id="license-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
0
gui-src/js/bbox.js → src/gui/js/bbox.js
vendored
@@ -3,7 +3,7 @@ export const licenseText = `
|
||||
|
||||
<p>For a full list of contributors, please refer to the <a href="https://github.com/louis-e/arnis/graphs/contributors" style="color: inherit;" target="_blank">Github contributors page</a>. Logo made by nxfx21.
|
||||
|
||||
<p style="color: #ff8686;"><b>Download Arnis only from the official source:</b> <a href="https://github.com/louis-e/arnis" style="color: inherit;" target="_blank">https://github.com/louis-e/arnis/</a>. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.</p>
|
||||
<p style="color: #ff8686;"><b>Download Arnis only from the official source:</b> <a href="https://arnismc.com" style="color: inherit;" target="_blank">https://arnismc.com</a> or <a href="https://github.com/louis-e/arnis" style="color: inherit;" target="_blank">https://github.com/louis-e/arnis/</a>. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.</p>
|
||||
|
||||
<p><b>Third-Party Map Data and Tile Services:</b></p>
|
||||
<p>This application uses map tiles from multiple providers, each with their own licensing requirements:</p>
|
||||
@@ -20,6 +20,13 @@ export const licenseText = `
|
||||
<b>Stadia Maps:</b><br> © <a href="https://www.stadiamaps.com/" style="color: inherit;" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" style="color: inherit;" target="_blank">OpenMapTiles</a> © OpenStreetMap contributors
|
||||
<p>Users of this software must comply with the respective licensing terms of these map data providers when using the application.</p>
|
||||
|
||||
<b>AWS Terrain Tiles:</b><br>
|
||||
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>
|
||||
|
||||
<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>.
|
||||
|
||||
<p><b>License:</b></p>
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||
75
gui-src/js/main.js → src/gui/js/main.js
vendored
@@ -20,6 +20,7 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
setupProgressListener();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
initTelemetryConsent();
|
||||
handleBboxInput();
|
||||
const localization = await getLocalization();
|
||||
await applyLocalization(localization);
|
||||
@@ -93,6 +94,10 @@ async function applyLocalization(localization) {
|
||||
// DEPRECATED: Ground level localization removed
|
||||
// "label[data-localize='ground_level']": "ground_level",
|
||||
"label[data-localize='language']": "language",
|
||||
"label[data-localize='generation_mode']": "generation_mode",
|
||||
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
||||
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
||||
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
||||
"label[data-localize='terrain']": "terrain",
|
||||
"label[data-localize='interior']": "interior",
|
||||
"label[data-localize='roof']": "roof",
|
||||
@@ -301,6 +306,20 @@ function initSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
// Telemetry consent toggle
|
||||
const telemetryToggle = document.getElementById("telemetry-toggle");
|
||||
const telemetryKey = 'telemetry-consent';
|
||||
|
||||
// Load saved telemetry consent
|
||||
const savedConsent = localStorage.getItem(telemetryKey);
|
||||
telemetryToggle.checked = savedConsent === 'true';
|
||||
|
||||
// Handle telemetry consent change
|
||||
telemetryToggle.addEventListener("change", () => {
|
||||
const isEnabled = telemetryToggle.checked;
|
||||
localStorage.setItem(telemetryKey, isEnabled ? 'true' : 'false');
|
||||
});
|
||||
|
||||
|
||||
/// License and Credits
|
||||
function openLicense() {
|
||||
@@ -325,6 +344,49 @@ function initSettings() {
|
||||
window.closeLicense = closeLicense;
|
||||
}
|
||||
|
||||
// Telemetry consent (first run only)
|
||||
function initTelemetryConsent() {
|
||||
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
||||
const existing = localStorage.getItem(key);
|
||||
|
||||
const modal = document.getElementById('telemetry-modal');
|
||||
if (!modal) return;
|
||||
|
||||
if (existing === null) {
|
||||
// First run: ask for consent
|
||||
modal.style.display = 'flex';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.alignItems = 'center';
|
||||
}
|
||||
|
||||
// Expose handlers
|
||||
window.acceptTelemetry = () => {
|
||||
localStorage.setItem(key, 'true');
|
||||
modal.style.display = 'none';
|
||||
// Update settings toggle to reflect the consent
|
||||
const telemetryToggle = document.getElementById('telemetry-toggle');
|
||||
if (telemetryToggle) {
|
||||
telemetryToggle.checked = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.rejectTelemetry = () => {
|
||||
localStorage.setItem(key, 'false');
|
||||
modal.style.display = 'none';
|
||||
// Update settings toggle to reflect the consent
|
||||
const telemetryToggle = document.getElementById('telemetry-toggle');
|
||||
if (telemetryToggle) {
|
||||
telemetryToggle.checked = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Utility for other scripts to read consent
|
||||
window.getTelemetryConsent = () => {
|
||||
const v = localStorage.getItem(key);
|
||||
return v === null ? null : v === 'true';
|
||||
};
|
||||
}
|
||||
|
||||
function initWorldPicker() {
|
||||
// World Picker
|
||||
const worldPickerModal = document.getElementById("world-modal");
|
||||
@@ -595,7 +657,11 @@ async function startGeneration() {
|
||||
}
|
||||
}
|
||||
|
||||
var terrain = document.getElementById("terrain-toggle").checked;
|
||||
// Get generation mode from dropdown
|
||||
var generationMode = document.getElementById("generation-mode-select").value;
|
||||
var terrain = (generationMode === "geo-terrain" || generationMode === "terrain-only");
|
||||
var skipOsmObjects = (generationMode === "terrain-only");
|
||||
|
||||
var interior = document.getElementById("interior-toggle").checked;
|
||||
var roof = document.getElementById("roof-toggle").checked;
|
||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||
@@ -609,6 +675,9 @@ async function startGeneration() {
|
||||
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
||||
|
||||
// Get telemetry consent (defaults to false if not set)
|
||||
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
||||
|
||||
// Pass the selected options to the Rust backend
|
||||
await invoke("gui_start_generation", {
|
||||
bboxText: selectedBBox,
|
||||
@@ -617,11 +686,13 @@ async function startGeneration() {
|
||||
groundLevel: ground_level,
|
||||
floodfillTimeout: floodfill_timeout,
|
||||
terrainEnabled: terrain,
|
||||
skipOsmObjects: skipOsmObjects,
|
||||
interiorEnabled: interior,
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||