mirror of
https://github.com/louis-e/arnis.git
synced 2025-12-30 17:58:02 -05:00
Compare commits
52 Commits
v2.3.1
...
benchmark-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
996e8756d0 | ||
|
|
0c5bd51ba4 | ||
|
|
7965dc3737 | ||
|
|
c54187b43a | ||
|
|
57a4a801cf | ||
|
|
beb7b73d11 | ||
|
|
0c47e365bc | ||
|
|
dad3ab3b34 | ||
|
|
b8b63a2bc5 | ||
|
|
cab20b5e50 | ||
|
|
0e879837fa | ||
|
|
92be2ccf00 | ||
|
|
3b76d707d9 | ||
|
|
be8559dee7 | ||
|
|
94eda2fad3 | ||
|
|
7d86854e3c | ||
|
|
cddaa89d35 | ||
|
|
453845977d | ||
|
|
4e196e51bd | ||
|
|
ea4dc5dc08 | ||
|
|
c56ff83094 | ||
|
|
2b40a520ff | ||
|
|
a192be981a | ||
|
|
eb77bca10d | ||
|
|
4a891c3603 | ||
|
|
84adfdd931 | ||
|
|
823b6ba052 | ||
|
|
2ba8157ec9 | ||
|
|
7235ba0be9 | ||
|
|
dee580c564 | ||
|
|
41fc5662e0 | ||
|
|
ac884b8c2a | ||
|
|
7a9b792bee | ||
|
|
83e9a634e5 | ||
|
|
56ddea57d0 | ||
|
|
430a4970f5 | ||
|
|
74fbdabaee | ||
|
|
2643155e9a | ||
|
|
d45c360074 | ||
|
|
6277a14d22 | ||
|
|
c355f243e3 | ||
|
|
2c31d2659c | ||
|
|
996e06ab2c | ||
|
|
e11231ad0f | ||
|
|
9adf31121e | ||
|
|
69da18fbfb | ||
|
|
5976cc2868 | ||
|
|
a85eaed835 | ||
|
|
37c3d85672 | ||
|
|
15b698a1eb | ||
|
|
8fff2d2fb5 | ||
|
|
8c702a36ff |
4
.github/workflows/ci-build.yml
vendored
4
.github/workflows/ci-build.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
|
||||
@@ -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 --generate-map --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))
|
||||
@@ -57,6 +57,25 @@ jobs:
|
||||
duration=$((end_time - start_time))
|
||||
echo "duration=$duration" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for map preview
|
||||
id: map_check
|
||||
run: |
|
||||
if [ -f "./world/arnis_world_map.png" ]; then
|
||||
echo "Map preview generated successfully"
|
||||
echo "map_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Map preview not found"
|
||||
echo "map_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload map preview as artifact
|
||||
if: steps.map_check.outputs.map_exists == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: world-map-preview
|
||||
path: ./world/arnis_world_map.png
|
||||
retention-days: 60
|
||||
|
||||
- name: Format duration and generate summary
|
||||
id: comment_body
|
||||
run: |
|
||||
@@ -65,7 +84,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 +98,7 @@ jobs:
|
||||
verdict="🚨 This PR **drastically worsens generation time**."
|
||||
fi
|
||||
|
||||
baseline_mem=5865
|
||||
baseline_mem=935
|
||||
mem_annotation=""
|
||||
if [ "$peak_mem" -gt 2000 ]; then
|
||||
mem_diff=$((peak_mem - baseline_mem))
|
||||
@@ -87,17 +106,28 @@ jobs:
|
||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||
fi
|
||||
|
||||
# Get current timestamp
|
||||
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
||||
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
{
|
||||
echo "summary<<EOF"
|
||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
||||
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
|
||||
echo "## ⏱️ Benchmark Results"
|
||||
echo ""
|
||||
echo "📈 Compared against baseline: **${baseline_time}s**"
|
||||
echo "🧮 Delta: **${diff}s**"
|
||||
echo "🔢 Commit: [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})"
|
||||
echo "| Metric | Value |"
|
||||
echo "|--------|-------|"
|
||||
echo "| Duration | **${minutes}m ${seconds}s** |"
|
||||
echo "| Peak Memory | **${peak_mem} MB**${mem_annotation} |"
|
||||
echo "| Baseline | **${baseline_time}s** |"
|
||||
echo "| Delta | **${diff}s** |"
|
||||
echo "| Commit | [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}) |"
|
||||
echo ""
|
||||
echo "${verdict}"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "📅 **Last benchmark:** ${benchmark_time} | 📥 [Download generated world map](${run_url}#artifacts)"
|
||||
echo ""
|
||||
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v5
|
||||
|
||||
155
Cargo.lock
generated
155
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"
|
||||
@@ -193,6 +184,7 @@ dependencies = [
|
||||
name = "arnis"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"colored",
|
||||
"dirs",
|
||||
@@ -463,21 +455,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"
|
||||
@@ -1165,7 +1142,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1804,9 +1781,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",
|
||||
@@ -1886,12 +1863,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"
|
||||
@@ -2263,24 +2234,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",
|
||||
@@ -2291,19 +2262,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"
|
||||
@@ -2591,17 +2561,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"
|
||||
@@ -3499,15 +3458,6 @@ dependencies = [
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -4462,12 +4412,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"
|
||||
@@ -4660,19 +4604,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
@@ -4697,10 +4643,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5487,7 +5442,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5612,29 +5567,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",
|
||||
@@ -6423,7 +6375,7 @@ dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
@@ -6453,7 +6405,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-link 0.1.1",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
@@ -6465,7 +6417,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]]
|
||||
@@ -6496,6 +6448,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"
|
||||
@@ -6503,7 +6461,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]]
|
||||
@@ -6523,7 +6481,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]]
|
||||
@@ -6532,7 +6490,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]]
|
||||
@@ -6541,7 +6499,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]]
|
||||
@@ -6571,6 +6529,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"
|
||||
|
||||
@@ -20,6 +20,7 @@ gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs"
|
||||
tauri-build = {version = "2", optional = true}
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
@@ -28,7 +29,7 @@ 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"
|
||||
@@ -38,13 +39,13 @@ rand = "0.8.5"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.15.4", optional = true }
|
||||
semver = "1.0.26"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri = { version = "2", optional = true }
|
||||
tauri-plugin-log = { version = "2.6.0", optional = true }
|
||||
tauri-plugin-shell = { version = "2", optional = true }
|
||||
tokio = { version = "1.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"] }
|
||||
|
||||
@@ -59,6 +59,10 @@ pub struct Args {
|
||||
#[arg(long, value_parser = parse_duration)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Generate a top-down map preview image after world generation (optional)
|
||||
#[arg(long)]
|
||||
pub generate_map: bool,
|
||||
|
||||
/// Spawn point coordinates (lat, lng)
|
||||
#[arg(skip)]
|
||||
pub spawn_point: Option<(f64, f64)>,
|
||||
|
||||
706
src/clipping.rs
Normal file
706
src/clipping.rs
Normal file
@@ -0,0 +1,706 @@
|
||||
// Sutherland-Hodgman polygon clipping and related geometry utilities.
|
||||
//
|
||||
// Provides bbox clipping for polygons, polylines, and water rings with
|
||||
// proper corner insertion for closed shapes.
|
||||
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Clips a way to the bounding box using Sutherland-Hodgman for polygons or
|
||||
/// simple line clipping for polylines. Preserves endpoint IDs for ring assembly.
|
||||
pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||
if nodes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let is_closed = is_closed_polygon(nodes);
|
||||
|
||||
if !is_closed {
|
||||
return clip_polyline_to_bbox(nodes, xzbbox);
|
||||
}
|
||||
|
||||
// If all nodes are inside the bbox, return unchanged
|
||||
let has_nodes_outside = nodes
|
||||
.iter()
|
||||
.any(|node| !xzbbox.contains(&XZPoint::new(node.x, node.z)));
|
||||
|
||||
if !has_nodes_outside {
|
||||
return nodes.to_vec();
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
let mut polygon: Vec<(f64, f64)> = nodes.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||
|
||||
polygon = clip_polygon_sutherland_hodgman(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Final clamping for floating-point errors
|
||||
for p in &mut polygon {
|
||||
p.0 = p.0.clamp(min_x, max_x);
|
||||
p.1 = p.1.clamp(min_z, max_z);
|
||||
}
|
||||
|
||||
let polygon = remove_consecutive_duplicates(polygon);
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||
let polygon = remove_consecutive_duplicates(polygon);
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
||||
}
|
||||
|
||||
/// Clips a water polygon ring to bbox using Sutherland-Hodgman (post-ring-merge).
|
||||
pub fn clip_water_ring_to_bbox(
|
||||
ring: &[ProcessedNode],
|
||||
xzbbox: &XZBBox,
|
||||
) -> Option<Vec<ProcessedNode>> {
|
||||
if ring.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
// Check if entire ring is inside bbox
|
||||
let all_inside = ring.iter().all(|n| {
|
||||
n.x as f64 >= min_x && n.x as f64 <= max_x && n.z as f64 >= min_z && n.z as f64 <= max_z
|
||||
});
|
||||
|
||||
if all_inside {
|
||||
return Some(ring.to_vec());
|
||||
}
|
||||
|
||||
// Check if entire ring is outside bbox
|
||||
if is_ring_outside_bbox(ring, min_x, min_z, max_x, max_z) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert to f64 coordinates and ensure closed
|
||||
let mut polygon: Vec<(f64, f64)> = ring.iter().map(|n| (n.x as f64, n.z as f64)).collect();
|
||||
if !polygon.is_empty() && polygon.first() != polygon.last() {
|
||||
polygon.push(polygon[0]);
|
||||
}
|
||||
|
||||
// Clip with full-range clamping (water uses simpler approach)
|
||||
polygon = clip_polygon_sutherland_hodgman_simple(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Verify all points are within bbox
|
||||
let all_points_inside = polygon
|
||||
.iter()
|
||||
.all(|&(x, z)| x >= min_x && x <= max_x && z >= min_z && z <= max_z);
|
||||
|
||||
if !all_points_inside {
|
||||
eprintln!("ERROR: clip_water_ring_to_bbox produced points outside bbox!");
|
||||
return None;
|
||||
}
|
||||
|
||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||
if polygon.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert back to ProcessedNode with synthetic IDs
|
||||
let mut result: Vec<ProcessedNode> = polygon
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(x, z))| ProcessedNode {
|
||||
id: 1_000_000_000 + i as u64,
|
||||
tags: HashMap::new(),
|
||||
x: x.clamp(min_x, max_x).round() as i32,
|
||||
z: z.clamp(min_z, max_z).round() as i32,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Close the loop by matching first and last ID
|
||||
if !result.is_empty() {
|
||||
let first_id = result[0].id;
|
||||
result.last_mut().unwrap().id = first_id;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal helpers
|
||||
// ============================================================================
|
||||
|
||||
/// Checks if a way forms a closed polygon.
|
||||
fn is_closed_polygon(nodes: &[ProcessedNode]) -> bool {
|
||||
if nodes.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let first = nodes.first().unwrap();
|
||||
let last = nodes.last().unwrap();
|
||||
first.id == last.id || (first.x == last.x && first.z == last.z)
|
||||
}
|
||||
|
||||
/// Checks if an entire ring is outside the bbox.
|
||||
fn is_ring_outside_bbox(
|
||||
ring: &[ProcessedNode],
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> bool {
|
||||
let all_left = ring.iter().all(|n| (n.x as f64) < min_x);
|
||||
let all_right = ring.iter().all(|n| (n.x as f64) > max_x);
|
||||
let all_top = ring.iter().all(|n| (n.z as f64) < min_z);
|
||||
let all_bottom = ring.iter().all(|n| (n.z as f64) > max_z);
|
||||
all_left || all_right || all_top || all_bottom
|
||||
}
|
||||
|
||||
/// Clips a polyline (open path) to the bounding box.
|
||||
fn clip_polyline_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<ProcessedNode> {
|
||||
if nodes.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let min_x = xzbbox.min_x() as f64;
|
||||
let min_z = xzbbox.min_z() as f64;
|
||||
let max_x = xzbbox.max_x() as f64;
|
||||
let max_z = xzbbox.max_z() as f64;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..nodes.len() {
|
||||
let current = &nodes[i];
|
||||
let current_point = (current.x as f64, current.z as f64);
|
||||
let current_inside = point_in_bbox(current_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if current_inside {
|
||||
result.push(current.clone());
|
||||
}
|
||||
|
||||
if i + 1 < nodes.len() {
|
||||
let next = &nodes[i + 1];
|
||||
let next_point = (next.x as f64, next.z as f64);
|
||||
let next_inside = point_in_bbox(next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if current_inside != next_inside {
|
||||
// One endpoint inside, one outside, find single intersection
|
||||
let intersections =
|
||||
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
for intersection in intersections {
|
||||
let synthetic_id = nodes[0]
|
||||
.id
|
||||
.wrapping_mul(10000000)
|
||||
.wrapping_add(result.len() as u64);
|
||||
result.push(ProcessedNode {
|
||||
id: synthetic_id,
|
||||
x: intersection.0.round() as i32,
|
||||
z: intersection.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
});
|
||||
}
|
||||
} else if !current_inside && !next_inside {
|
||||
// Both endpoints outside, segment might still cross through bbox
|
||||
let mut intersections =
|
||||
find_bbox_intersections(current_point, next_point, min_x, min_z, max_x, max_z);
|
||||
|
||||
if intersections.len() >= 2 {
|
||||
// Sort intersections by distance from current point
|
||||
intersections.sort_by(|a, b| {
|
||||
let dist_a =
|
||||
(a.0 - current_point.0).powi(2) + (a.1 - current_point.1).powi(2);
|
||||
let dist_b =
|
||||
(b.0 - current_point.0).powi(2) + (b.1 - current_point.1).powi(2);
|
||||
dist_a
|
||||
.partial_cmp(&dist_b)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
for intersection in intersections {
|
||||
let synthetic_id = nodes[0]
|
||||
.id
|
||||
.wrapping_mul(10000000)
|
||||
.wrapping_add(result.len() as u64);
|
||||
result.push(ProcessedNode {
|
||||
id: synthetic_id,
|
||||
x: intersection.0.round() as i32,
|
||||
z: intersection.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve endpoint IDs where possible
|
||||
if result.len() >= 2 {
|
||||
let tolerance = 50.0;
|
||||
if let Some(first_orig) = nodes.first() {
|
||||
if matches_endpoint(
|
||||
(result[0].x as f64, result[0].z as f64),
|
||||
first_orig,
|
||||
tolerance,
|
||||
) {
|
||||
result[0].id = first_orig.id;
|
||||
}
|
||||
}
|
||||
if let Some(last_orig) = nodes.last() {
|
||||
let last_idx = result.len() - 1;
|
||||
if matches_endpoint(
|
||||
(result[last_idx].x as f64, result[last_idx].z as f64),
|
||||
last_orig,
|
||||
tolerance,
|
||||
) {
|
||||
result[last_idx].id = last_orig.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Sutherland-Hodgman polygon clipping with edge-specific clamping.
|
||||
fn clip_polygon_sutherland_hodgman(
|
||||
mut polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
// Edges: bottom, right, top, left (counter-clockwise traversal)
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z, 0), // Bottom: clamp z
|
||||
(max_x, min_z, max_x, max_z, 1), // Right: clamp x
|
||||
(max_x, max_z, min_x, max_z, 2), // Top: clamp z
|
||||
(min_x, max_z, min_x, min_z, 3), // Left: clamp x
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2, edge_idx) in bbox_edges {
|
||||
if polygon.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut clipped = Vec::new();
|
||||
let is_closed = !polygon.is_empty() && polygon.first() == polygon.last();
|
||||
let edge_count = if is_closed {
|
||||
polygon.len().saturating_sub(1)
|
||||
} else {
|
||||
polygon.len()
|
||||
};
|
||||
|
||||
for i in 0..edge_count {
|
||||
let current = polygon[i];
|
||||
let next = polygon.get(i + 1).copied().unwrap_or(polygon[0]);
|
||||
|
||||
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
|
||||
if next_inside {
|
||||
if !current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
// Clamp to current edge only
|
||||
match edge_idx {
|
||||
0 => intersection.1 = min_z,
|
||||
1 => intersection.0 = max_x,
|
||||
2 => intersection.1 = max_z,
|
||||
3 => intersection.0 = min_x,
|
||||
_ => {}
|
||||
}
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
clipped.push(next);
|
||||
} else if current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
match edge_idx {
|
||||
0 => intersection.1 = min_z,
|
||||
1 => intersection.0 = max_x,
|
||||
2 => intersection.1 = max_z,
|
||||
3 => intersection.0 = min_x,
|
||||
_ => {}
|
||||
}
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
polygon = clipped;
|
||||
}
|
||||
|
||||
polygon
|
||||
}
|
||||
|
||||
/// Sutherland-Hodgman with full bbox clamping (simpler, for water rings).
|
||||
fn clip_polygon_sutherland_hodgman_simple(
|
||||
mut polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z),
|
||||
(max_x, min_z, max_x, max_z),
|
||||
(max_x, max_z, min_x, max_z),
|
||||
(min_x, max_z, min_x, min_z),
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||
if polygon.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let mut clipped = Vec::new();
|
||||
|
||||
for i in 0..(polygon.len().saturating_sub(1)) {
|
||||
let current = polygon[i];
|
||||
let next = polygon[i + 1];
|
||||
|
||||
let current_inside = point_inside_edge(current, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
let next_inside = point_inside_edge(next, edge_x1, edge_z1, edge_x2, edge_z2);
|
||||
|
||||
if next_inside {
|
||||
if !current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
clipped.push(next);
|
||||
} else if current_inside {
|
||||
if let Some(mut intersection) = line_edge_intersection(
|
||||
current.0, current.1, next.0, next.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
intersection.0 = intersection.0.clamp(min_x, max_x);
|
||||
intersection.1 = intersection.1.clamp(min_z, max_z);
|
||||
clipped.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
polygon = clipped;
|
||||
}
|
||||
|
||||
polygon
|
||||
}
|
||||
|
||||
/// Checks if point is inside bbox.
|
||||
fn point_in_bbox(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> bool {
|
||||
point.0 >= min_x && point.0 <= max_x && point.1 >= min_z && point.1 <= max_z
|
||||
}
|
||||
|
||||
/// Checks if point is on the "inside" side of an edge (cross product test).
|
||||
fn point_inside_edge(
|
||||
point: (f64, f64),
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> bool {
|
||||
let edge_dx = edge_x2 - edge_x1;
|
||||
let edge_dz = edge_z2 - edge_z1;
|
||||
let point_dx = point.0 - edge_x1;
|
||||
let point_dz = point.1 - edge_z1;
|
||||
(edge_dx * point_dz - edge_dz * point_dx) >= 0.0
|
||||
}
|
||||
|
||||
/// Finds intersection between a line segment and an edge.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn line_edge_intersection(
|
||||
line_x1: f64,
|
||||
line_z1: f64,
|
||||
line_x2: f64,
|
||||
line_z2: f64,
|
||||
edge_x1: f64,
|
||||
edge_z1: f64,
|
||||
edge_x2: f64,
|
||||
edge_z2: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let line_dx = line_x2 - line_x1;
|
||||
let line_dz = line_z2 - line_z1;
|
||||
let edge_dx = edge_x2 - edge_x1;
|
||||
let edge_dz = edge_z2 - edge_z1;
|
||||
|
||||
let denom = line_dx * edge_dz - line_dz * edge_dx;
|
||||
if denom.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dx = edge_x1 - line_x1;
|
||||
let dz = edge_z1 - line_z1;
|
||||
let t = (dx * edge_dz - dz * edge_dx) / denom;
|
||||
|
||||
if (0.0..=1.0).contains(&t) {
|
||||
Some((line_x1 + t * line_dx, line_z1 + t * line_dz))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds intersections between a line segment and bbox edges.
|
||||
fn find_bbox_intersections(
|
||||
start: (f64, f64),
|
||||
end: (f64, f64),
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
let mut intersections = Vec::new();
|
||||
|
||||
let bbox_edges = [
|
||||
(min_x, min_z, max_x, min_z),
|
||||
(max_x, min_z, max_x, max_z),
|
||||
(max_x, max_z, min_x, max_z),
|
||||
(min_x, max_z, min_x, min_z),
|
||||
];
|
||||
|
||||
for (edge_x1, edge_z1, edge_x2, edge_z2) in bbox_edges {
|
||||
if let Some(intersection) = line_edge_intersection(
|
||||
start.0, start.1, end.0, end.1, edge_x1, edge_z1, edge_x2, edge_z2,
|
||||
) {
|
||||
let on_edge = point_in_bbox(intersection, min_x, min_z, max_x, max_z)
|
||||
&& ((intersection.0 == min_x || intersection.0 == max_x)
|
||||
|| (intersection.1 == min_z || intersection.1 == max_z));
|
||||
|
||||
if on_edge {
|
||||
intersections.push(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
intersections
|
||||
}
|
||||
|
||||
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
||||
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
||||
let eps = 0.5;
|
||||
|
||||
let on_left = (point.0 - min_x).abs() < eps;
|
||||
let on_right = (point.0 - max_x).abs() < eps;
|
||||
let on_bottom = (point.1 - min_z).abs() < eps;
|
||||
let on_top = (point.1 - max_z).abs() < eps;
|
||||
|
||||
// Handle corners (assign to edge in counter-clockwise order)
|
||||
if on_bottom && on_left {
|
||||
return 3;
|
||||
}
|
||||
if on_bottom && on_right {
|
||||
return 0;
|
||||
}
|
||||
if on_top && on_right {
|
||||
return 1;
|
||||
}
|
||||
if on_top && on_left {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if on_bottom {
|
||||
return 0;
|
||||
}
|
||||
if on_right {
|
||||
return 1;
|
||||
}
|
||||
if on_top {
|
||||
return 2;
|
||||
}
|
||||
if on_left {
|
||||
return 3;
|
||||
}
|
||||
|
||||
-1
|
||||
}
|
||||
|
||||
/// Returns corners to insert when traversing from edge1 to edge2 via shorter path.
|
||||
fn get_corners_between_edges(
|
||||
edge1: i32,
|
||||
edge2: i32,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
if edge1 == edge2 || edge1 < 0 || edge2 < 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let corners = [
|
||||
(max_x, min_z), // 0: bottom-right
|
||||
(max_x, max_z), // 1: top-right
|
||||
(min_x, max_z), // 2: top-left
|
||||
(min_x, min_z), // 3: bottom-left
|
||||
];
|
||||
|
||||
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||
|
||||
// Opposite edges: don't insert corners
|
||||
if ccw_dist == 2 && cw_dist == 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
if ccw_dist <= cw_dist {
|
||||
let mut current = edge1;
|
||||
for _ in 0..ccw_dist {
|
||||
result.push(corners[current as usize]);
|
||||
current = (current + 1) % 4;
|
||||
}
|
||||
} else {
|
||||
let mut current = edge1;
|
||||
for _ in 0..cw_dist {
|
||||
current = (current + 4 - 1) % 4;
|
||||
result.push(corners[current as usize]);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||
fn insert_bbox_corners(
|
||||
polygon: Vec<(f64, f64)>,
|
||||
min_x: f64,
|
||||
min_z: f64,
|
||||
max_x: f64,
|
||||
max_z: f64,
|
||||
) -> Vec<(f64, f64)> {
|
||||
if polygon.len() < 3 {
|
||||
return polygon;
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(polygon.len() + 4);
|
||||
|
||||
for i in 0..polygon.len() {
|
||||
let current = polygon[i];
|
||||
let next = polygon[(i + 1) % polygon.len()];
|
||||
|
||||
result.push(current);
|
||||
|
||||
let edge1 = get_bbox_edge(current, min_x, min_z, max_x, max_z);
|
||||
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||
|
||||
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
||||
result.push(corner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Removes consecutive duplicate points (within epsilon tolerance).
|
||||
fn remove_consecutive_duplicates(polygon: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
|
||||
if polygon.is_empty() {
|
||||
return polygon;
|
||||
}
|
||||
|
||||
let eps = 0.1;
|
||||
let mut result: Vec<(f64, f64)> = Vec::with_capacity(polygon.len());
|
||||
|
||||
for p in &polygon {
|
||||
if let Some(last) = result.last() {
|
||||
if (p.0 - last.0).abs() < eps && (p.1 - last.1).abs() < eps {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(*p);
|
||||
}
|
||||
|
||||
// Check first/last duplicates for closed polygons
|
||||
if result.len() > 1 {
|
||||
let first = result.first().unwrap();
|
||||
let last = result.last().unwrap();
|
||||
if (first.0 - last.0).abs() < eps && (first.1 - last.1).abs() < eps {
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Checks if a clipped coordinate matches an original endpoint.
|
||||
fn matches_endpoint(coord: (f64, f64), endpoint: &ProcessedNode, tolerance: f64) -> bool {
|
||||
let dx = (coord.0 - endpoint.x as f64).abs();
|
||||
let dz = (coord.1 - endpoint.z as f64).abs();
|
||||
dx * dx + dz * dz < tolerance * tolerance
|
||||
}
|
||||
|
||||
/// Assigns node IDs to clipped coordinates, preserving original endpoint IDs.
|
||||
fn assign_node_ids_preserving_endpoints(
|
||||
original_nodes: &[ProcessedNode],
|
||||
clipped_coords: Vec<(f64, f64)>,
|
||||
way_id: u64,
|
||||
) -> Vec<ProcessedNode> {
|
||||
if clipped_coords.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let original_first = original_nodes.first();
|
||||
let original_last = original_nodes.last();
|
||||
let tolerance = 50.0;
|
||||
let last_index = clipped_coords.len() - 1;
|
||||
|
||||
clipped_coords
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, coord)| {
|
||||
let is_first = i == 0;
|
||||
let is_last = i == last_index;
|
||||
|
||||
if is_first || is_last {
|
||||
if let Some(first) = original_first {
|
||||
if matches_endpoint(coord, first, tolerance) {
|
||||
return ProcessedNode {
|
||||
id: first.id,
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(last) = original_last {
|
||||
if matches_endpoint(coord, last, tolerance) {
|
||||
return ProcessedNode {
|
||||
id: last.id,
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProcessedNode {
|
||||
id: way_id.wrapping_mul(10000000).wrapping_add(i as u64),
|
||||
x: coord.0.round() as i32,
|
||||
z: coord.1.round() as i32,
|
||||
tags: HashMap::new(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -4,8 +4,11 @@ use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
@@ -23,6 +26,9 @@ pub fn generate_world(
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
// Build highway connectivity map once before processing
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(&ground);
|
||||
|
||||
@@ -64,7 +70,7 @@ pub fn generate_world(
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, args, None);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &elements);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(&mut editor, way, args);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
@@ -78,7 +84,7 @@ pub fn generate_world(
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
// docks count as water areas
|
||||
water_areas::generate_water_area_from_way(&mut editor, way);
|
||||
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
}
|
||||
@@ -109,7 +115,7 @@ pub fn generate_world(
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barrier_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args, &elements);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
@@ -126,7 +132,7 @@ pub fn generate_world(
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel);
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
||||
} else if rel.tags.contains_key("natural") {
|
||||
natural::generate_natural_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
@@ -250,11 +256,64 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Generate top-down map preview:
|
||||
// - Always for GUI mode (non-blocking, runs in background)
|
||||
// - Only when --generate-map flag is set for CLI mode (blocking, waits for completion)
|
||||
#[cfg(feature = "gui")]
|
||||
let should_generate_map = true;
|
||||
#[cfg(not(feature = "gui"))]
|
||||
let should_generate_map = args.generate_map;
|
||||
|
||||
if should_generate_map {
|
||||
let world_path = args.path.clone();
|
||||
let bounds = (
|
||||
xzbbox.min_x(),
|
||||
xzbbox.max_x(),
|
||||
xzbbox.min_z(),
|
||||
xzbbox.max_z(),
|
||||
);
|
||||
|
||||
let map_thread = std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(&world_path, bounds.0, bounds.1, bounds.2, bounds.3)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// In CLI mode, wait for map generation to complete before exiting
|
||||
// In GUI mode, let it run in background to keep UI responsive
|
||||
#[cfg(not(feature = "gui"))]
|
||||
{
|
||||
let _ = map_thread.join();
|
||||
}
|
||||
|
||||
// In GUI mode, we don't join, let the thread run in background
|
||||
#[cfg(feature = "gui")]
|
||||
drop(map_thread);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,19 +7,21 @@ use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||
pub fn generate_highways(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
all_elements: &[ProcessedElement],
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
) {
|
||||
let highway_connectivity = build_highway_connectivity_map(all_elements);
|
||||
generate_highways_internal(editor, element, args, &highway_connectivity);
|
||||
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed
|
||||
fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HashMap<(i32, i32), Vec<i32>> {
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
|
||||
for element in elements {
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
coordinate_system::cartesian::XZPoint,
|
||||
coordinate_system::cartesian::{XZBBox, XZPoint},
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
|
||||
world_editor::WorldEditor,
|
||||
};
|
||||
|
||||
pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
pub fn generate_water_area_from_way(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
_xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let outers = [element.nodes.clone()];
|
||||
if !verify_loopy_loops(&outers) {
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping way {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
@@ -20,7 +26,11 @@ pub fn generate_water_area_from_way(editor: &mut WorldEditor, element: &Processe
|
||||
generate_water_areas(editor, &outers, &[], start_time);
|
||||
}
|
||||
|
||||
pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &ProcessedRelation) {
|
||||
pub fn generate_water_areas_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedRelation,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Check if this is a water relation (either with water tag or natural=water)
|
||||
@@ -52,14 +62,63 @@ pub fn generate_water_areas_from_relation(editor: &mut WorldEditor, element: &Pr
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut outers);
|
||||
if !verify_loopy_loops(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
// Preserve OSM-defined outer/inner roles without modification
|
||||
merge_way_segments(&mut outers);
|
||||
|
||||
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
||||
outers = outers
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
.collect();
|
||||
merge_way_segments(&mut inners);
|
||||
inners = inners
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
.collect();
|
||||
|
||||
if !verify_closed_rings(&outers) {
|
||||
// For clipped multipolygons, some loops may not close perfectly
|
||||
// Instead of force-closing with straight lines (which creates wedges),
|
||||
// filter out unclosed loops and only render the properly closed ones
|
||||
|
||||
// Filter: Keep only loops that are already closed OR can be closed within 1 block
|
||||
outers.retain(|loop_nodes| {
|
||||
if loop_nodes.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
let first = &loop_nodes[0];
|
||||
let last = loop_nodes.last().unwrap();
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
|
||||
// Keep if already closed by ID or endpoints are within 1 block
|
||||
first.id == last.id || (dx <= 1 && dz <= 1)
|
||||
});
|
||||
|
||||
// Now close the remaining loops that are within 1 block tolerance
|
||||
for loop_nodes in outers.iter_mut() {
|
||||
let first = loop_nodes[0].clone();
|
||||
let last_idx = loop_nodes.len() - 1;
|
||||
if loop_nodes[0].id != loop_nodes[last_idx].id {
|
||||
// Endpoints are close (within tolerance), close the loop
|
||||
loop_nodes.push(first);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid outer loops remain, skip the relation
|
||||
if outers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify again after filtering and closing
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut inners);
|
||||
if !verify_loopy_loops(&inners) {
|
||||
merge_way_segments(&mut inners);
|
||||
if !verify_closed_rings(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
@@ -73,8 +132,34 @@ fn generate_water_areas(
|
||||
inners: &[Vec<ProcessedNode>],
|
||||
start_time: Instant,
|
||||
) {
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
// Calculate polygon bounding box to limit fill area
|
||||
let mut poly_min_x = i32::MAX;
|
||||
let mut poly_min_z = i32::MAX;
|
||||
let mut poly_max_x = i32::MIN;
|
||||
let mut poly_max_z = i32::MIN;
|
||||
|
||||
for outer in outers {
|
||||
for node in outer {
|
||||
poly_min_x = poly_min_x.min(node.x);
|
||||
poly_min_z = poly_min_z.min(node.z);
|
||||
poly_max_x = poly_max_x.max(node.x);
|
||||
poly_max_z = poly_max_z.max(node.z);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid bounds, nothing to fill
|
||||
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp to world bounds just in case
|
||||
let (world_min_x, world_min_z) = editor.get_min_coords();
|
||||
let (world_max_x, world_max_z) = editor.get_max_coords();
|
||||
let min_x = poly_min_x.max(world_min_x);
|
||||
let min_z = poly_min_z.max(world_min_z);
|
||||
let max_x = poly_max_x.min(world_max_x);
|
||||
let max_z = poly_max_z.min(world_max_z);
|
||||
|
||||
let outers_xz: Vec<Vec<XZPoint>> = outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
@@ -89,13 +174,23 @@ fn generate_water_areas(
|
||||
);
|
||||
}
|
||||
|
||||
// Merges ways that share nodes into full loops
|
||||
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
for i in 0..loops.len() {
|
||||
for j in 0..loops.len() {
|
||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..rings.len() {
|
||||
for j in 0..rings.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
@@ -104,20 +199,29 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &loops[i];
|
||||
let y: &Vec<ProcessedNode> = &loops[j];
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// it's looped already
|
||||
if x[0].id == x.last().unwrap().id {
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's looped already
|
||||
if y[0].id == y.last().unwrap().id {
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// Skip already-closed rings
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if x[0].id == y[0].id {
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -125,7 +229,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if x.last().unwrap().id == y.last().unwrap().id {
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -133,7 +237,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if x[0].id == y.last().unwrap().id {
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -141,7 +245,7 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if x.last().unwrap().id == y[0].id {
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
@@ -156,24 +260,35 @@ fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
loops.remove(*r);
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
loops.push(m);
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_loopy_loops(loops);
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_loopy_loops(loops: &[Vec<ProcessedNode>]) -> bool {
|
||||
let mut valid: bool = true;
|
||||
for l in loops {
|
||||
if l[0].id != l.last().unwrap().id {
|
||||
eprintln!("WARN: Disconnected loop");
|
||||
/// Verifies all rings are properly closed (first node matches last).
|
||||
fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||
let mut valid = true;
|
||||
for ring in rings {
|
||||
let first = &ring[0];
|
||||
let last = ring.last().unwrap();
|
||||
|
||||
// Check if ring is closed (by ID or proximity)
|
||||
let is_closed = first.id == last.id || {
|
||||
let dx = (first.x - last.x).abs();
|
||||
let dz = (first.z - last.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
if !is_closed {
|
||||
eprintln!("WARN: Disconnected ring");
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
@@ -195,6 +310,7 @@ fn inverse_floodfill(
|
||||
editor: &mut WorldEditor,
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Convert to geo Polygons with normalized winding order
|
||||
let inners: Vec<_> = inners
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
@@ -206,6 +322,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -220,6 +337,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -109,8 +111,16 @@ pub fn fetch_elevation_data(
|
||||
};
|
||||
|
||||
if file_size < 1000 {
|
||||
eprintln!("Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(), file_size);
|
||||
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) {
|
||||
@@ -118,6 +128,11 @@ pub fn fetch_elevation_data(
|
||||
"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
|
||||
@@ -132,7 +147,16 @@ pub fn fetch_elevation_data(
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Cached tile at {} is corrupted or invalid: {}. Re-downloading...", tile_path.display(), 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) {
|
||||
@@ -140,6 +164,11 @@ pub fn fetch_elevation_data(
|
||||
"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
|
||||
|
||||
@@ -68,14 +68,11 @@ fn optimized_flood_fill_area(
|
||||
|
||||
// Pre-allocate queue with reasonable capacity to avoid reallocations
|
||||
let mut queue = VecDeque::with_capacity(1024);
|
||||
let mut iterations = 0u64;
|
||||
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
|
||||
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Check timeout more frequently for small areas
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 50 == 0 {
|
||||
// Fast timeout check, only every few iterations
|
||||
if filled_area.len() % 100 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
return filled_area;
|
||||
@@ -83,16 +80,6 @@ fn optimized_flood_fill_area(
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Skip if already visited or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -106,26 +93,6 @@ fn optimized_flood_fill_area(
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Additional iteration check inside inner loop
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Timeout check in inner loop for problematic polygons
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 1000 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add current point to filled area
|
||||
filled_area.push((curr_x, curr_z));
|
||||
|
||||
@@ -188,32 +155,18 @@ fn original_flood_fill_area(
|
||||
// Pre-allocate queue and reserve space for filled_area
|
||||
let mut queue: VecDeque<(i32, i32)> = VecDeque::with_capacity(2048);
|
||||
filled_area.reserve(1000); // Reserve space to reduce reallocations
|
||||
let mut iterations = 0u64;
|
||||
const MAX_ITERATIONS: u64 = 1_000_000; // Safety limit to prevent infinite loops
|
||||
|
||||
// Scan for multiple seed points to handle U-shapes and concave polygons
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Check timeout more frequently for problematic polygons
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 50 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
// Reduced timeout checking frequency for better performance
|
||||
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check: prevent infinite loops
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Skip if already processed or not inside polygon
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
@@ -227,26 +180,6 @@ fn original_flood_fill_area(
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Additional iteration check inside inner loop
|
||||
iterations += 1;
|
||||
if iterations > MAX_ITERATIONS {
|
||||
eprintln!(
|
||||
"Warning: Flood fill exceeded max iterations ({}), aborting",
|
||||
MAX_ITERATIONS
|
||||
);
|
||||
return filled_area;
|
||||
}
|
||||
|
||||
// Timeout check in inner loop
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if iterations % 1000 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only check polygon containment once per point when adding to filled_area
|
||||
if polygon.contains(&Point::new(curr_x as f64, curr_z as f64)) {
|
||||
filled_area.push((curr_x, curr_z));
|
||||
|
||||
85
src/gui.rs
85
src/gui.rs
@@ -8,15 +8,16 @@ 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
|
||||
@@ -63,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
|
||||
@@ -103,7 +100,8 @@ pub fn run_gui() {
|
||||
gui_select_world,
|
||||
gui_start_generation,
|
||||
gui_get_version,
|
||||
gui_check_for_updates
|
||||
gui_check_for_updates,
|
||||
gui_get_world_map_data
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle();
|
||||
@@ -400,6 +398,10 @@ fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,6 +664,63 @@ fn gui_check_for_updates() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the world map image data as base64 and geo bounds for overlay display.
|
||||
/// Returns None if the map image or metadata doesn't exist.
|
||||
#[tauri::command]
|
||||
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
|
||||
let world_dir = PathBuf::from(&world_path);
|
||||
let map_path = world_dir.join("arnis_world_map.png");
|
||||
let metadata_path = world_dir.join("metadata.json");
|
||||
|
||||
// Check if both files exist
|
||||
if !map_path.exists() || !metadata_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Read and encode the map image as base64
|
||||
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
|
||||
let base64_image =
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
|
||||
|
||||
// Read metadata
|
||||
let metadata_content =
|
||||
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
|
||||
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
|
||||
|
||||
// Extract geo bounds (metadata uses camelCase from serde)
|
||||
let min_lat = metadata["minGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLat in metadata")?;
|
||||
let max_lat = metadata["maxGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLat in metadata")?;
|
||||
let min_lon = metadata["minGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLon in metadata")?;
|
||||
let max_lon = metadata["maxGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLon in metadata")?;
|
||||
|
||||
Ok(Some(WorldMapData {
|
||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||
min_lat,
|
||||
max_lat,
|
||||
min_lon,
|
||||
max_lon,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Data structure for world map overlay
|
||||
#[derive(serde::Serialize)]
|
||||
struct WorldMapData {
|
||||
image_base64: String,
|
||||
min_lat: f64,
|
||||
max_lat: f64,
|
||||
min_lon: f64,
|
||||
max_lon: f64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(unused_variables)]
|
||||
@@ -678,10 +737,17 @@ fn gui_start_generation(
|
||||
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
|
||||
@@ -759,6 +825,7 @@ fn gui_start_generation(
|
||||
fillground: fillground_enabled,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
|
||||
generate_map: true,
|
||||
spawn_point,
|
||||
};
|
||||
|
||||
|
||||
34
src/gui/css/bbox.css
vendored
34
src/gui/css/bbox.css
vendored
@@ -344,4 +344,38 @@ body,
|
||||
filter: blur(1px) sepia(1) invert(1);
|
||||
transition: all 1s ease;
|
||||
|
||||
}
|
||||
|
||||
/* World Preview Button in Edit Toolbar */
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview {
|
||||
background-position: -31px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-preview.active {
|
||||
background-color: #a0d0ff;
|
||||
}
|
||||
|
||||
.world-preview-slider-container {
|
||||
padding: 6px 8px !important;
|
||||
background: white !important;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.world-preview-slider-container a {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.world-preview-slider {
|
||||
width: 80px;
|
||||
height: 8px;
|
||||
cursor: pointer;
|
||||
accent-color: #3887BE;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
BIN
src/gui/css/maps/images/spritesheet.png
vendored
BIN
src/gui/css/maps/images/spritesheet.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
50
src/gui/css/styles.css
vendored
50
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 {
|
||||
@@ -393,7 +433,7 @@ button:hover {
|
||||
.generation-mode-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -421,7 +461,7 @@ button:hover {
|
||||
.language-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -449,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;
|
||||
|
||||
24
src/gui/index.html
vendored
24
src/gui/index.html
vendored
@@ -186,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>
|
||||
@@ -203,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">
|
||||
|
||||
202
src/gui/js/bbox.js
vendored
202
src/gui/js/bbox.js
vendored
@@ -558,11 +558,208 @@ $(document).ready(function () {
|
||||
var savedTheme = localStorage.getItem('selectedTileTheme') || 'osm';
|
||||
changeTileTheme(savedTheme);
|
||||
|
||||
// Listen for theme changes from parent window (settings modal)
|
||||
// World overlay state
|
||||
var worldOverlay = null;
|
||||
var worldOverlayData = null;
|
||||
var worldOverlayEnabled = false;
|
||||
var worldPreviewAvailable = false;
|
||||
var sliderControl = null;
|
||||
|
||||
// Create the opacity slider as a proper Leaflet control
|
||||
var SliderControl = L.Control.extend({
|
||||
options: { position: 'topleft' },
|
||||
onAdd: function(map) {
|
||||
var container = L.DomUtil.create('div', 'leaflet-bar world-preview-slider-container');
|
||||
container.id = 'world-preview-slider-container';
|
||||
container.style.display = 'none';
|
||||
|
||||
var slider = L.DomUtil.create('input', 'world-preview-slider', container);
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = '50';
|
||||
slider.id = 'world-preview-opacity';
|
||||
slider.title = 'Overlay Opacity';
|
||||
|
||||
L.DomEvent.on(slider, 'input', function(e) {
|
||||
if (worldOverlay) {
|
||||
worldOverlay.setOpacity(e.target.value / 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent all map interactions
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
L.DomEvent.disableScrollPropagation(container);
|
||||
L.DomEvent.on(container, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(container, 'touchstart', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'touchstart', L.DomEvent.stopPropagation);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to add world preview button to the draw control's edit toolbar
|
||||
function addWorldPreviewToEditToolbar() {
|
||||
// Find the edit toolbar (contains Edit layers and Delete layers buttons)
|
||||
var editToolbar = document.querySelector('.leaflet-draw-toolbar:not(.leaflet-draw-toolbar-top)');
|
||||
if (!editToolbar) {
|
||||
// Try finding by the edit/delete buttons
|
||||
var deleteBtn = document.querySelector('.leaflet-draw-edit-remove');
|
||||
if (deleteBtn) {
|
||||
editToolbar = deleteBtn.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (editToolbar) {
|
||||
// Create the preview button
|
||||
var toggleBtn = document.createElement('a');
|
||||
toggleBtn.className = 'leaflet-draw-edit-preview disabled';
|
||||
toggleBtn.href = '#';
|
||||
toggleBtn.title = 'Show World Preview (not available yet)';
|
||||
toggleBtn.id = 'world-preview-btn';
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (worldPreviewAvailable) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
editToolbar.appendChild(toggleBtn);
|
||||
|
||||
// Add the slider control to the map
|
||||
sliderControl = new SliderControl();
|
||||
map.addControl(sliderControl);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle world overlay function
|
||||
function toggleWorldOverlay() {
|
||||
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||
|
||||
worldOverlayEnabled = !worldOverlayEnabled;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
|
||||
if (worldOverlayEnabled) {
|
||||
// Show overlay
|
||||
var data = worldOverlayData;
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
}
|
||||
|
||||
var opacity = document.getElementById('world-preview-opacity');
|
||||
var opacityValue = opacity ? opacity.value / 100 : 0.5;
|
||||
|
||||
worldOverlay = L.imageOverlay(data.image_base64, bounds, {
|
||||
opacity: opacityValue,
|
||||
interactive: false,
|
||||
zIndex: 500
|
||||
});
|
||||
worldOverlay.addTo(map);
|
||||
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
btn.title = 'Hide World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// Hide overlay
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
if (btn) {
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the preview button when data is available
|
||||
function enableWorldPreview(data) {
|
||||
worldOverlayData = data;
|
||||
worldPreviewAvailable = true;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('disabled');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
}
|
||||
|
||||
// Disable and reset preview (when world changes)
|
||||
function disableWorldPreview() {
|
||||
worldPreviewAvailable = false;
|
||||
worldOverlayData = null;
|
||||
worldOverlayEnabled = false;
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
if (btn) {
|
||||
btn.classList.add('disabled');
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview (not available yet)';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
changeTileTheme(event.data.theme);
|
||||
}
|
||||
|
||||
// Handle world preview data ready (after generation completes)
|
||||
if (event.data && event.data.type === 'worldPreviewReady') {
|
||||
enableWorldPreview(event.data.data);
|
||||
|
||||
// Auto-enable the overlay when generation completes
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle existing world map load (zoom to location and auto-enable)
|
||||
if (event.data && event.data.type === 'loadExistingWorldMap') {
|
||||
var data = event.data.data;
|
||||
enableWorldPreview(data);
|
||||
|
||||
// Calculate bounds and zoom to them
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
|
||||
// Auto-enable the overlay
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle world changed (disable preview)
|
||||
if (event.data && event.data.type === 'worldChanged') {
|
||||
disableWorldPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the dropdown value in parent window if it exists
|
||||
@@ -652,6 +849,9 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
// Add world preview button to the edit toolbar after drawControl is added
|
||||
addWorldPreviewToEditToolbar();
|
||||
/*
|
||||
**
|
||||
** create bounds layer
|
||||
|
||||
4
src/gui/js/license.js
vendored
4
src/gui/js/license.js
vendored
@@ -24,6 +24,10 @@ export const licenseText = `
|
||||
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
|
||||
<br><br>
|
||||
|
||||
<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;">
|
||||
Apache License
|
||||
|
||||
164
src/gui/js/main.js
vendored
164
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);
|
||||
@@ -213,6 +214,12 @@ function setupProgressListener() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for map preview ready event from backend
|
||||
window.__TAURI__.event.listen("map-preview-ready", () => {
|
||||
console.log("Map preview ready event received");
|
||||
showWorldPreviewButton();
|
||||
});
|
||||
}
|
||||
|
||||
function initSettings() {
|
||||
@@ -305,6 +312,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() {
|
||||
@@ -329,6 +350,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");
|
||||
@@ -533,6 +597,14 @@ async function selectWorld(generate_new_world) {
|
||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
document.getElementById('selected-world').style.color = "#fecc44";
|
||||
|
||||
// Notify that world changed (reset preview)
|
||||
notifyWorldChanged();
|
||||
|
||||
// If selecting an existing world, check for existing map data
|
||||
if (!generate_new_world) {
|
||||
await loadExistingWorldMapData();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleWorldSelectionError(error);
|
||||
@@ -541,6 +613,32 @@ async function selectWorld(generate_new_world) {
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads existing world map data if available (for existing worlds)
|
||||
* This will zoom to the location and auto-enable the preview
|
||||
*/
|
||||
async function loadExistingWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
|
||||
// Send data to the map iframe with instruction to zoom and auto-enable
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'loadExistingWorldMap',
|
||||
data: mapData
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("No existing world map data found:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles world selection errors and displays appropriate messages
|
||||
* @param {number} errorCode - Error code from the backend
|
||||
@@ -587,6 +685,9 @@ async function startGeneration() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing world preview since we're generating a new one
|
||||
notifyWorldChanged();
|
||||
|
||||
// Get the map iframe reference
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
// Get spawn point coordinates if marker exists
|
||||
@@ -617,6 +718,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,
|
||||
@@ -630,7 +734,8 @@ async function startGeneration() {
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||
@@ -640,3 +745,60 @@ async function startGeneration() {
|
||||
generationButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// World preview overlay state
|
||||
let worldPreviewEnabled = false;
|
||||
let currentWorldMapData = null;
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that world preview data is ready
|
||||
* Called when the backend emits the map-preview-ready event
|
||||
*/
|
||||
async function showWorldPreviewButton() {
|
||||
// Try to load the world map data
|
||||
await loadWorldMapData();
|
||||
|
||||
if (currentWorldMapData) {
|
||||
// Send data to the map iframe
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldPreviewReady',
|
||||
data: currentWorldMapData
|
||||
}, '*');
|
||||
console.log("World preview data sent to map iframe");
|
||||
}
|
||||
} else {
|
||||
console.warn("Map data not available yet");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that the world has changed (reset preview)
|
||||
*/
|
||||
function notifyWorldChanged() {
|
||||
currentWorldMapData = null;
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldChanged'
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the world map data from the backend
|
||||
*/
|
||||
async function loadWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
console.log("World map data loaded successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load world map data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
mod args;
|
||||
mod block_definitions;
|
||||
mod bresenham;
|
||||
mod clipping;
|
||||
mod colors;
|
||||
mod coordinate_system;
|
||||
mod data_processing;
|
||||
mod element_processing;
|
||||
mod elevation_data;
|
||||
mod floodfill;
|
||||
mod ground;
|
||||
mod map_renderer;
|
||||
mod map_transformation;
|
||||
mod osm_parser;
|
||||
#[cfg(feature = "gui")]
|
||||
mod progress;
|
||||
mod retrieve_data;
|
||||
#[cfg(feature = "gui")]
|
||||
mod telemetry;
|
||||
#[cfg(test)]
|
||||
mod test_utilities;
|
||||
mod version_check;
|
||||
@@ -24,7 +29,6 @@ use clap::Parser;
|
||||
use colored::*;
|
||||
use std::{env, fs, io::Write};
|
||||
|
||||
mod elevation_data;
|
||||
#[cfg(feature = "gui")]
|
||||
mod gui;
|
||||
|
||||
@@ -33,6 +37,7 @@ mod gui;
|
||||
mod progress {
|
||||
pub fn emit_gui_error(_message: &str) {}
|
||||
pub fn emit_gui_progress_update(_progress: f64, _message: &str) {}
|
||||
pub fn emit_map_preview_ready() {}
|
||||
pub fn is_running_with_gui() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
944
src/map_renderer.rs
Normal file
944
src/map_renderer.rs
Normal file
@@ -0,0 +1,944 @@
|
||||
// Top-down world map renderer for GUI preview.
|
||||
//
|
||||
// Generates a 1:1 pixel-per-block PNG image of the generated world,
|
||||
// showing the topmost visible block at each position.
|
||||
|
||||
use fastanvil::Region;
|
||||
use fastnbt::{from_bytes, Value};
|
||||
use fnv::FnvHashMap;
|
||||
use image::{Rgb, RgbImage};
|
||||
use once_cell::sync::Lazy;
|
||||
use rayon::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Pre-computed block colors for fast lookup
|
||||
static BLOCK_COLORS: Lazy<FnvHashMap<&'static str, Rgb<u8>>> = Lazy::new(get_block_colors);
|
||||
|
||||
/// Renders a top-down view of the generated Minecraft world.
|
||||
/// Returns the path to the saved image file.
|
||||
pub fn render_world_map(
|
||||
world_dir: &Path,
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
min_z: i32,
|
||||
max_z: i32,
|
||||
) -> Result<std::path::PathBuf, String> {
|
||||
let width = (max_x - min_x + 1) as u32;
|
||||
let height = (max_z - min_z + 1) as u32;
|
||||
|
||||
if width == 0 || height == 0 {
|
||||
return Err("Invalid world bounds".to_string());
|
||||
}
|
||||
|
||||
// Use Mutex for thread-safe image access
|
||||
let img = Mutex::new(RgbImage::from_pixel(width, height, Rgb([255, 255, 255])));
|
||||
|
||||
// Calculate region range
|
||||
let min_region_x = min_x >> 9; // divide by 512 (32 chunks * 16 blocks)
|
||||
let max_region_x = max_x >> 9;
|
||||
let min_region_z = min_z >> 9;
|
||||
let max_region_z = max_z >> 9;
|
||||
|
||||
let region_dir = world_dir.join("region");
|
||||
|
||||
// Collect all region coordinates for parallel processing
|
||||
let region_coords: Vec<(i32, i32)> = (min_region_x..=max_region_x)
|
||||
.flat_map(|rx| (min_region_z..=max_region_z).map(move |rz| (rx, rz)))
|
||||
.collect();
|
||||
|
||||
// Process regions in parallel
|
||||
region_coords.par_iter().for_each(|&(region_x, region_z)| {
|
||||
let region_path = region_dir.join(format!("r.{}.{}.mca", region_x, region_z));
|
||||
|
||||
if !region_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(®ion_path) {
|
||||
if let Ok(mut region) = Region::from_stream(file) {
|
||||
// Collect all pixels from this region first
|
||||
let pixels = render_region_to_pixels(
|
||||
&mut region,
|
||||
region_x,
|
||||
region_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
);
|
||||
|
||||
// Then batch-write to image under lock
|
||||
if !pixels.is_empty() {
|
||||
let mut img_guard = img.lock().unwrap();
|
||||
for (x, z, color) in pixels {
|
||||
if x < img_guard.width() && z < img_guard.height() {
|
||||
img_guard.put_pixel(x, z, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save the image
|
||||
let output_path = world_dir.join("arnis_world_map.png");
|
||||
img.into_inner()
|
||||
.unwrap()
|
||||
.save(&output_path)
|
||||
.map_err(|e| format!("Failed to save map image: {}", e))?;
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Renders all chunks within a region and returns pixel data
|
||||
fn render_region_to_pixels(
|
||||
region: &mut Region<File>,
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
) -> Vec<(u32, u32, Rgb<u8>)> {
|
||||
let mut pixels = Vec::new();
|
||||
let region_base_x = region_x * 512;
|
||||
let region_base_z = region_z * 512;
|
||||
|
||||
for chunk_local_x in 0..32 {
|
||||
for chunk_local_z in 0..32 {
|
||||
let chunk_base_x = region_base_x + chunk_local_x * 16;
|
||||
let chunk_base_z = region_base_z + chunk_local_z * 16;
|
||||
|
||||
// Skip chunks outside our bounds
|
||||
if chunk_base_x + 15 < min_x
|
||||
|| chunk_base_x > max_x
|
||||
|| chunk_base_z + 15 < min_z
|
||||
|| chunk_base_z > max_z
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(Some(chunk_data)) =
|
||||
region.read_chunk(chunk_local_x as usize, chunk_local_z as usize)
|
||||
{
|
||||
render_chunk_to_pixels(
|
||||
&chunk_data,
|
||||
&mut pixels,
|
||||
chunk_base_x,
|
||||
chunk_base_z,
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixels
|
||||
}
|
||||
|
||||
/// Renders a single chunk and appends pixel data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_chunk_to_pixels(
|
||||
chunk_data: &[u8],
|
||||
pixels: &mut Vec<(u32, u32, Rgb<u8>)>,
|
||||
chunk_base_x: i32,
|
||||
chunk_base_z: i32,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
) {
|
||||
// Parse chunk NBT - look for Level.sections or sections depending on format
|
||||
let chunk: Value = match from_bytes(chunk_data) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Try to get sections from the chunk data
|
||||
let sections = get_sections_from_chunk(&chunk);
|
||||
if sections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-sort sections by Y (descending) once per chunk, not per column
|
||||
let sorted_sections = get_sorted_sections(§ions);
|
||||
if sorted_sections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each column in the chunk
|
||||
for local_x in 0..16 {
|
||||
for local_z in 0..16 {
|
||||
let world_x = chunk_base_x + local_x;
|
||||
let world_z = chunk_base_z + local_z;
|
||||
|
||||
// Skip if outside our bounds
|
||||
if world_x < min_x || world_x > max_x || world_z < min_z || world_z > max_z {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find topmost non-air block using pre-sorted sections
|
||||
if let Some((block_name, world_y)) =
|
||||
find_top_block_sorted(&sorted_sections, local_x as usize, local_z as usize)
|
||||
{
|
||||
// Strip minecraft: prefix for lookup
|
||||
let short_name = block_name.strip_prefix("minecraft:").unwrap_or(&block_name);
|
||||
|
||||
let base_color = BLOCK_COLORS
|
||||
.get(short_name)
|
||||
.copied()
|
||||
.unwrap_or_else(|| get_fallback_color(&block_name));
|
||||
|
||||
// Apply elevation shading
|
||||
let color = apply_elevation_shading(base_color, world_y);
|
||||
|
||||
let img_x = (world_x - min_x) as u32;
|
||||
let img_z = (world_z - min_z) as u32;
|
||||
|
||||
pixels.push((img_x, img_z, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies elevation-based shading to a color
|
||||
/// Higher elevations are brighter, lower are darker
|
||||
#[inline]
|
||||
fn apply_elevation_shading(color: Rgb<u8>, y: i32) -> Rgb<u8> {
|
||||
// Base brightness boost of 10%, plus elevation shading
|
||||
// Shading range: -20% darker to +20% brighter (asymmetric, more bright than dark)
|
||||
|
||||
// Normalize Y to a -1.0 to 1.0 range (roughly)
|
||||
// y=0 -> -0.5, y=0 -> 0, y=200 -> +1.0
|
||||
let normalized = (y as f32 / 100.0).clamp(-1.0, 1.0);
|
||||
|
||||
// Base 10% brightness boost + asymmetric elevation shading
|
||||
let elevation_adjust = if normalized >= 0.0 {
|
||||
// Above sea level: up to +20% brighter
|
||||
normalized * 0.20
|
||||
} else {
|
||||
// Below sea level: up to -20% darker
|
||||
normalized * 0.20
|
||||
};
|
||||
|
||||
let multiplier = 1.10 + elevation_adjust;
|
||||
|
||||
Rgb([
|
||||
(color.0[0] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
(color.0[1] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
(color.0[2] as f32 * multiplier).clamp(0.0, 255.0) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Extracts sections from chunk data (handles both old and new formats)
|
||||
fn get_sections_from_chunk(chunk: &Value) -> Vec<&Value> {
|
||||
let mut sections = Vec::new();
|
||||
|
||||
// Try new format (1.18+): directly in chunk
|
||||
if let Value::Compound(map) = chunk {
|
||||
if let Some(Value::List(secs)) = map.get("sections") {
|
||||
for sec in secs {
|
||||
sections.push(sec);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Try via Level wrapper (older format)
|
||||
if let Some(Value::Compound(level)) = map.get("Level") {
|
||||
if let Some(Value::List(secs)) = level.get("sections") {
|
||||
for sec in secs {
|
||||
sections.push(sec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sections
|
||||
}
|
||||
|
||||
/// Pre-sorts sections by Y coordinate (descending) - called once per chunk
|
||||
/// Returns Vec of (section_y, section_value) for Y tracking
|
||||
fn get_sorted_sections<'a>(sections: &[&'a Value]) -> Vec<(i8, &'a Value)> {
|
||||
let mut sorted: Vec<(i8, &Value)> = sections
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if let Value::Compound(map) = s {
|
||||
if let Some(Value::Byte(y)) = map.get("Y") {
|
||||
return Some((*y, *s));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
sorted.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
sorted
|
||||
}
|
||||
|
||||
/// Finds the topmost non-air block using pre-sorted sections
|
||||
/// Returns (block_name, world_y) where world_y is the actual Y coordinate
|
||||
fn find_top_block_sorted(
|
||||
sorted_sections: &[(i8, &Value)],
|
||||
local_x: usize,
|
||||
local_z: usize,
|
||||
) -> Option<(String, i32)> {
|
||||
for (section_y, section) in sorted_sections {
|
||||
if let Some((block_name, local_y)) = get_block_at_section(section, local_x, local_z) {
|
||||
if !is_transparent_block(&block_name) {
|
||||
// Calculate world Y: section_y * 16 + local_y
|
||||
let world_y = (*section_y as i32) * 16 + local_y as i32;
|
||||
return Some((block_name, world_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Gets the topmost non-air block in a section at the given x,z
|
||||
/// Returns (block_name, local_y) where local_y is 0-15 within the section
|
||||
fn get_block_at_section(
|
||||
section: &Value,
|
||||
local_x: usize,
|
||||
local_z: usize,
|
||||
) -> Option<(String, usize)> {
|
||||
let section_map = match section {
|
||||
Value::Compound(m) => m,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let block_states = match section_map.get("block_states") {
|
||||
Some(Value::Compound(bs)) => bs,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let palette = match block_states.get("palette") {
|
||||
Some(Value::List(p)) => p,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// If palette has only one block, that's the block for the entire section
|
||||
if palette.len() == 1 {
|
||||
// Return with local_y=15 (top of section) for single-block sections
|
||||
return get_block_name_from_palette(&palette[0]).map(|name| (name, 15));
|
||||
}
|
||||
|
||||
let data = match block_states.get("data") {
|
||||
Some(Value::LongArray(d)) => d,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Calculate bits per block
|
||||
let bits_per_block = std::cmp::max(4, (palette.len() as f64).log2().ceil() as usize);
|
||||
let blocks_per_long = 64 / bits_per_block;
|
||||
let mask = (1u64 << bits_per_block) - 1;
|
||||
|
||||
// Search from top (y=15) to bottom (y=0) within this section
|
||||
for local_y in (0..16).rev() {
|
||||
let block_index = local_y * 256 + local_z * 16 + local_x;
|
||||
let long_index = block_index / blocks_per_long;
|
||||
let bit_offset = (block_index % blocks_per_long) * bits_per_block;
|
||||
|
||||
if long_index >= data.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let palette_index = ((data[long_index] as u64 >> bit_offset) & mask) as usize;
|
||||
|
||||
if palette_index < palette.len() {
|
||||
if let Some(name) = get_block_name_from_palette(&palette[palette_index]) {
|
||||
if !is_transparent_block(&name) {
|
||||
return Some((name, local_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Extracts block name from a palette entry
|
||||
fn get_block_name_from_palette(entry: &Value) -> Option<String> {
|
||||
if let Value::Compound(map) = entry {
|
||||
if let Some(Value::String(name)) = map.get("Name") {
|
||||
return Some(name.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Checks if a block should be considered transparent (look through it)
|
||||
fn is_transparent_block(name: &str) -> bool {
|
||||
let short_name = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
matches!(
|
||||
short_name,
|
||||
"air"
|
||||
| "cave_air"
|
||||
| "void_air"
|
||||
| "glass"
|
||||
| "glass_pane"
|
||||
| "white_stained_glass"
|
||||
| "gray_stained_glass"
|
||||
| "light_gray_stained_glass"
|
||||
| "brown_stained_glass"
|
||||
| "tinted_glass"
|
||||
| "barrier"
|
||||
| "light"
|
||||
| "short_grass"
|
||||
| "tall_grass"
|
||||
| "dead_bush"
|
||||
| "poppy"
|
||||
| "dandelion"
|
||||
| "blue_orchid"
|
||||
| "azure_bluet"
|
||||
| "iron_bars"
|
||||
| "ladder"
|
||||
| "scaffolding"
|
||||
| "rail"
|
||||
| "powered_rail"
|
||||
| "detector_rail"
|
||||
| "activator_rail"
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a fallback color based on block name patterns
|
||||
fn get_fallback_color(name: &str) -> Rgb<u8> {
|
||||
// Try to guess color from name
|
||||
if name.contains("stone") || name.contains("cobble") || name.contains("andesite") {
|
||||
return Rgb([128, 128, 128]);
|
||||
}
|
||||
if name.contains("dirt") || name.contains("mud") {
|
||||
return Rgb([139, 90, 43]);
|
||||
}
|
||||
if name.contains("sand") {
|
||||
return Rgb([219, 211, 160]);
|
||||
}
|
||||
if name.contains("grass") {
|
||||
return Rgb([86, 125, 70]);
|
||||
}
|
||||
if name.contains("water") {
|
||||
return Rgb([59, 86, 165]);
|
||||
}
|
||||
if name.contains("log") || name.contains("wood") {
|
||||
return Rgb([101, 76, 48]);
|
||||
}
|
||||
if name.contains("leaves") {
|
||||
return Rgb([55, 95, 36]);
|
||||
}
|
||||
if name.contains("planks") {
|
||||
return Rgb([162, 130, 78]);
|
||||
}
|
||||
if name.contains("brick") {
|
||||
return Rgb([150, 97, 83]);
|
||||
}
|
||||
if name.contains("concrete") {
|
||||
return Rgb([128, 128, 128]);
|
||||
}
|
||||
if name.contains("wool") || name.contains("carpet") {
|
||||
return Rgb([220, 220, 220]);
|
||||
}
|
||||
if name.contains("terracotta") {
|
||||
return Rgb([152, 94, 67]);
|
||||
}
|
||||
if name.contains("iron") {
|
||||
return Rgb([200, 200, 200]);
|
||||
}
|
||||
if name.contains("gold") {
|
||||
return Rgb([255, 215, 0]);
|
||||
}
|
||||
if name.contains("diamond") {
|
||||
return Rgb([97, 219, 213]);
|
||||
}
|
||||
if name.contains("emerald") {
|
||||
return Rgb([17, 160, 54]);
|
||||
}
|
||||
if name.contains("lapis") {
|
||||
return Rgb([38, 67, 156]);
|
||||
}
|
||||
if name.contains("redstone") {
|
||||
return Rgb([170, 0, 0]);
|
||||
}
|
||||
if name.contains("netherrack") || name.contains("nether") {
|
||||
return Rgb([111, 54, 53]);
|
||||
}
|
||||
if name.contains("end_stone") {
|
||||
return Rgb([219, 222, 158]);
|
||||
}
|
||||
if name.contains("obsidian") {
|
||||
return Rgb([15, 10, 24]);
|
||||
}
|
||||
if name.contains("deepslate") {
|
||||
return Rgb([72, 72, 73]);
|
||||
}
|
||||
if name.contains("blackstone") {
|
||||
return Rgb([42, 36, 41]);
|
||||
}
|
||||
if name.contains("quartz") {
|
||||
return Rgb([235, 229, 222]);
|
||||
}
|
||||
if name.contains("prismarine") {
|
||||
return Rgb([76, 128, 113]);
|
||||
}
|
||||
if name.contains("copper") {
|
||||
return Rgb([192, 107, 79]);
|
||||
}
|
||||
if name.contains("amethyst") {
|
||||
return Rgb([133, 97, 191]);
|
||||
}
|
||||
if name.contains("moss") {
|
||||
return Rgb([89, 109, 45]);
|
||||
}
|
||||
if name.contains("dripstone") {
|
||||
return Rgb([134, 107, 92]);
|
||||
}
|
||||
|
||||
// Default gray for unknown blocks
|
||||
Rgb([160, 160, 160])
|
||||
}
|
||||
|
||||
/// Returns a mapping of common block names to RGB colors (without minecraft: prefix)
|
||||
fn get_block_colors() -> FnvHashMap<&'static str, Rgb<u8>> {
|
||||
FnvHashMap::from_iter([
|
||||
("grass_block", Rgb([86, 125, 70])),
|
||||
("short_grass", Rgb([86, 125, 70])),
|
||||
("tall_grass", Rgb([86, 125, 70])),
|
||||
("dirt", Rgb([139, 90, 43])),
|
||||
("coarse_dirt", Rgb([119, 85, 59])),
|
||||
("podzol", Rgb([91, 63, 24])),
|
||||
("rooted_dirt", Rgb([144, 103, 76])),
|
||||
("mud", Rgb([60, 57, 61])),
|
||||
("stone", Rgb([128, 128, 128])),
|
||||
("granite", Rgb([149, 108, 91])),
|
||||
("polished_granite", Rgb([154, 112, 98])),
|
||||
("diorite", Rgb([189, 188, 189])),
|
||||
("polished_diorite", Rgb([195, 195, 195])),
|
||||
("andesite", Rgb([136, 136, 137])),
|
||||
("polished_andesite", Rgb([132, 135, 134])),
|
||||
("deepslate", Rgb([72, 72, 73])),
|
||||
("cobbled_deepslate", Rgb([77, 77, 80])),
|
||||
("polished_deepslate", Rgb([72, 72, 73])),
|
||||
("deepslate_bricks", Rgb([70, 70, 71])),
|
||||
("deepslate_tiles", Rgb([54, 54, 55])),
|
||||
("calcite", Rgb([223, 224, 220])),
|
||||
("tuff", Rgb([108, 109, 102])),
|
||||
("dripstone_block", Rgb([134, 107, 92])),
|
||||
("sand", Rgb([219, 211, 160])),
|
||||
("red_sand", Rgb([190, 102, 33])),
|
||||
("gravel", Rgb([131, 127, 126])),
|
||||
("clay", Rgb([160, 166, 179])),
|
||||
("bedrock", Rgb([85, 85, 85])),
|
||||
("water", Rgb([59, 86, 165])),
|
||||
("ice", Rgb([145, 183, 253])),
|
||||
("packed_ice", Rgb([141, 180, 250])),
|
||||
("blue_ice", Rgb([116, 167, 253])),
|
||||
("snow", Rgb([249, 254, 254])),
|
||||
("snow_block", Rgb([249, 254, 254])),
|
||||
("powder_snow", Rgb([248, 253, 253])),
|
||||
("oak_log", Rgb([109, 85, 50])),
|
||||
("oak_planks", Rgb([162, 130, 78])),
|
||||
("oak_slab", Rgb([162, 130, 78])),
|
||||
("oak_stairs", Rgb([162, 130, 78])),
|
||||
("oak_fence", Rgb([162, 130, 78])),
|
||||
("oak_door", Rgb([162, 130, 78])),
|
||||
("spruce_log", Rgb([58, 37, 16])),
|
||||
("spruce_planks", Rgb([115, 85, 49])),
|
||||
("spruce_slab", Rgb([115, 85, 49])),
|
||||
("spruce_stairs", Rgb([115, 85, 49])),
|
||||
("spruce_fence", Rgb([115, 85, 49])),
|
||||
("spruce_door", Rgb([115, 85, 49])),
|
||||
("birch_log", Rgb([216, 215, 210])),
|
||||
("birch_planks", Rgb([196, 179, 123])),
|
||||
("birch_slab", Rgb([196, 179, 123])),
|
||||
("birch_stairs", Rgb([196, 179, 123])),
|
||||
("birch_fence", Rgb([196, 179, 123])),
|
||||
("birch_door", Rgb([196, 179, 123])),
|
||||
("jungle_log", Rgb([85, 68, 25])),
|
||||
("jungle_planks", Rgb([160, 115, 81])),
|
||||
("acacia_log", Rgb([103, 96, 86])),
|
||||
("acacia_planks", Rgb([168, 90, 50])),
|
||||
("dark_oak_log", Rgb([60, 46, 26])),
|
||||
("dark_oak_planks", Rgb([67, 43, 20])),
|
||||
("dark_oak_slab", Rgb([67, 43, 20])),
|
||||
("dark_oak_stairs", Rgb([67, 43, 20])),
|
||||
("dark_oak_fence", Rgb([67, 43, 20])),
|
||||
("dark_oak_door", Rgb([67, 43, 20])),
|
||||
("mangrove_log", Rgb([84, 66, 36])),
|
||||
("mangrove_planks", Rgb([117, 54, 48])),
|
||||
("cherry_log", Rgb([54, 33, 44])),
|
||||
("cherry_planks", Rgb([226, 178, 172])),
|
||||
("bamboo_block", Rgb([122, 129, 52])),
|
||||
("bamboo_planks", Rgb([194, 175, 93])),
|
||||
("crimson_stem", Rgb([92, 25, 29])),
|
||||
("crimson_planks", Rgb([101, 48, 70])),
|
||||
("warped_stem", Rgb([58, 58, 77])),
|
||||
("warped_planks", Rgb([43, 104, 99])),
|
||||
("oak_leaves", Rgb([55, 95, 36])),
|
||||
("spruce_leaves", Rgb([61, 99, 61])),
|
||||
("birch_leaves", Rgb([80, 106, 47])),
|
||||
("jungle_leaves", Rgb([48, 113, 20])),
|
||||
("acacia_leaves", Rgb([75, 104, 40])),
|
||||
("dark_oak_leaves", Rgb([35, 82, 11])),
|
||||
("mangrove_leaves", Rgb([69, 123, 38])),
|
||||
("cherry_leaves", Rgb([228, 177, 197])),
|
||||
("azalea_leaves", Rgb([71, 96, 37])),
|
||||
("stone_bricks", Rgb([122, 122, 122])),
|
||||
("stone_brick_slab", Rgb([122, 122, 122])),
|
||||
("stone_brick_stairs", Rgb([122, 122, 122])),
|
||||
("stone_brick_wall", Rgb([122, 122, 122])),
|
||||
("mossy_stone_bricks", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_slab", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_stairs", Rgb([115, 121, 105])),
|
||||
("mossy_stone_brick_wall", Rgb([115, 121, 105])),
|
||||
("cracked_stone_bricks", Rgb([118, 117, 118])),
|
||||
("chiseled_stone_bricks", Rgb([119, 119, 119])),
|
||||
("cobblestone", Rgb([128, 127, 127])),
|
||||
("cobblestone_slab", Rgb([128, 127, 127])),
|
||||
("cobblestone_stairs", Rgb([128, 127, 127])),
|
||||
("cobblestone_wall", Rgb([128, 127, 127])),
|
||||
("mossy_cobblestone", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_slab", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_stairs", Rgb([110, 118, 94])),
|
||||
("mossy_cobblestone_wall", Rgb([110, 118, 94])),
|
||||
("stone_slab", Rgb([128, 128, 128])),
|
||||
("stone_stairs", Rgb([128, 128, 128])),
|
||||
("smooth_stone", Rgb([158, 158, 158])),
|
||||
("smooth_stone_slab", Rgb([158, 158, 158])),
|
||||
("bricks", Rgb([150, 97, 83])),
|
||||
("brick_slab", Rgb([150, 97, 83])),
|
||||
("brick_stairs", Rgb([150, 97, 83])),
|
||||
("brick_wall", Rgb([150, 97, 83])),
|
||||
("mud_bricks", Rgb([137, 103, 79])),
|
||||
("mud_brick_slab", Rgb([137, 103, 79])),
|
||||
("mud_brick_stairs", Rgb([137, 103, 79])),
|
||||
("mud_brick_wall", Rgb([137, 103, 79])),
|
||||
("terracotta", Rgb([152, 94, 67])),
|
||||
("white_terracotta", Rgb([210, 178, 161])),
|
||||
("orange_terracotta", Rgb([162, 84, 38])),
|
||||
("magenta_terracotta", Rgb([149, 88, 109])),
|
||||
("light_blue_terracotta", Rgb([113, 109, 138])),
|
||||
("yellow_terracotta", Rgb([186, 133, 35])),
|
||||
("lime_terracotta", Rgb([104, 118, 53])),
|
||||
("pink_terracotta", Rgb([162, 78, 79])),
|
||||
("gray_terracotta", Rgb([58, 42, 36])),
|
||||
("light_gray_terracotta", Rgb([135, 107, 98])),
|
||||
("cyan_terracotta", Rgb([87, 91, 91])),
|
||||
("purple_terracotta", Rgb([118, 70, 86])),
|
||||
("blue_terracotta", Rgb([74, 60, 91])),
|
||||
("brown_terracotta", Rgb([77, 51, 36])),
|
||||
("green_terracotta", Rgb([76, 83, 42])),
|
||||
("red_terracotta", Rgb([143, 61, 47])),
|
||||
("black_terracotta", Rgb([37, 23, 16])),
|
||||
("white_concrete", Rgb([207, 213, 214])),
|
||||
("orange_concrete", Rgb([224, 97, 0])),
|
||||
("magenta_concrete", Rgb([169, 48, 159])),
|
||||
("light_blue_concrete", Rgb([35, 137, 198])),
|
||||
("yellow_concrete", Rgb([241, 175, 21])),
|
||||
("lime_concrete", Rgb([94, 169, 24])),
|
||||
("pink_concrete", Rgb([214, 101, 143])),
|
||||
("gray_concrete", Rgb([55, 58, 62])),
|
||||
("light_gray_concrete", Rgb([125, 125, 115])),
|
||||
("cyan_concrete", Rgb([21, 119, 136])),
|
||||
("purple_concrete", Rgb([100, 32, 156])),
|
||||
("blue_concrete", Rgb([45, 47, 143])),
|
||||
("brown_concrete", Rgb([96, 60, 32])),
|
||||
("green_concrete", Rgb([73, 91, 36])),
|
||||
("red_concrete", Rgb([142, 33, 33])),
|
||||
("black_concrete", Rgb([8, 10, 15])),
|
||||
("white_wool", Rgb([234, 236, 237])),
|
||||
("orange_wool", Rgb([241, 118, 20])),
|
||||
("magenta_wool", Rgb([190, 68, 179])),
|
||||
("light_blue_wool", Rgb([58, 175, 217])),
|
||||
("yellow_wool", Rgb([249, 198, 40])),
|
||||
("lime_wool", Rgb([112, 185, 26])),
|
||||
("pink_wool", Rgb([238, 141, 172])),
|
||||
("gray_wool", Rgb([63, 68, 72])),
|
||||
("light_gray_wool", Rgb([142, 142, 135])),
|
||||
("cyan_wool", Rgb([21, 138, 145])),
|
||||
("purple_wool", Rgb([122, 42, 173])),
|
||||
("blue_wool", Rgb([53, 57, 157])),
|
||||
("brown_wool", Rgb([114, 72, 41])),
|
||||
("green_wool", Rgb([85, 110, 28])),
|
||||
("red_wool", Rgb([161, 39, 35])),
|
||||
("black_wool", Rgb([21, 21, 26])),
|
||||
("sandstone", Rgb([223, 214, 170])),
|
||||
("sandstone_slab", Rgb([223, 214, 170])),
|
||||
("sandstone_stairs", Rgb([223, 214, 170])),
|
||||
("sandstone_wall", Rgb([223, 214, 170])),
|
||||
("chiseled_sandstone", Rgb([223, 214, 170])),
|
||||
("cut_sandstone", Rgb([225, 217, 171])),
|
||||
("cut_sandstone_slab", Rgb([225, 217, 171])),
|
||||
("smooth_sandstone", Rgb([223, 214, 170])),
|
||||
("smooth_sandstone_slab", Rgb([223, 214, 170])),
|
||||
("smooth_sandstone_stairs", Rgb([223, 214, 170])),
|
||||
("red_sandstone", Rgb([186, 99, 29])),
|
||||
("red_sandstone_slab", Rgb([186, 99, 29])),
|
||||
("red_sandstone_stairs", Rgb([186, 99, 29])),
|
||||
("red_sandstone_wall", Rgb([186, 99, 29])),
|
||||
("smooth_red_sandstone", Rgb([186, 99, 29])),
|
||||
("netherrack", Rgb([111, 54, 53])),
|
||||
("nether_bricks", Rgb([44, 21, 26])),
|
||||
("nether_brick_slab", Rgb([44, 21, 26])),
|
||||
("nether_brick_stairs", Rgb([44, 21, 26])),
|
||||
("nether_brick_wall", Rgb([44, 21, 26])),
|
||||
("nether_brick_fence", Rgb([44, 21, 26])),
|
||||
("red_nether_bricks", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_slab", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_stairs", Rgb([69, 7, 9])),
|
||||
("red_nether_brick_wall", Rgb([69, 7, 9])),
|
||||
("soul_sand", Rgb([81, 62, 51])),
|
||||
("soul_soil", Rgb([75, 57, 46])),
|
||||
("basalt", Rgb([73, 72, 77])),
|
||||
("polished_basalt", Rgb([88, 87, 91])),
|
||||
("smooth_basalt", Rgb([72, 72, 78])),
|
||||
("blackstone", Rgb([42, 36, 41])),
|
||||
("blackstone_slab", Rgb([42, 36, 41])),
|
||||
("blackstone_stairs", Rgb([42, 36, 41])),
|
||||
("blackstone_wall", Rgb([42, 36, 41])),
|
||||
("polished_blackstone", Rgb([53, 49, 56])),
|
||||
("polished_blackstone_bricks", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_slab", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_stairs", Rgb([48, 43, 50])),
|
||||
("polished_blackstone_brick_wall", Rgb([48, 43, 50])),
|
||||
("glowstone", Rgb([171, 131, 84])),
|
||||
("shroomlight", Rgb([240, 146, 70])),
|
||||
("crying_obsidian", Rgb([32, 10, 60])),
|
||||
("obsidian", Rgb([15, 10, 24])),
|
||||
("end_stone", Rgb([219, 222, 158])),
|
||||
("end_stone_bricks", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_slab", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_stairs", Rgb([218, 224, 162])),
|
||||
("end_stone_brick_wall", Rgb([218, 224, 162])),
|
||||
("purpur_block", Rgb([170, 126, 170])),
|
||||
("purpur_pillar", Rgb([171, 129, 171])),
|
||||
("purpur_slab", Rgb([170, 126, 170])),
|
||||
("purpur_stairs", Rgb([170, 126, 170])),
|
||||
("coal_ore", Rgb([105, 105, 105])),
|
||||
("iron_ore", Rgb([136, 130, 127])),
|
||||
("copper_ore", Rgb([124, 125, 120])),
|
||||
("gold_ore", Rgb([143, 140, 125])),
|
||||
("redstone_ore", Rgb([133, 107, 107])),
|
||||
("emerald_ore", Rgb([108, 136, 115])),
|
||||
("lapis_ore", Rgb([99, 112, 135])),
|
||||
("diamond_ore", Rgb([121, 141, 140])),
|
||||
("coal_block", Rgb([16, 15, 15])),
|
||||
("iron_block", Rgb([220, 220, 220])),
|
||||
("copper_block", Rgb([192, 107, 79])),
|
||||
("gold_block", Rgb([246, 208, 62])),
|
||||
("redstone_block", Rgb([170, 0, 0])),
|
||||
("emerald_block", Rgb([42, 203, 88])),
|
||||
("lapis_block", Rgb([38, 67, 156])),
|
||||
("diamond_block", Rgb([97, 219, 213])),
|
||||
("netherite_block", Rgb([66, 61, 63])),
|
||||
("amethyst_block", Rgb([133, 97, 191])),
|
||||
("raw_iron_block", Rgb([166, 136, 107])),
|
||||
("raw_copper_block", Rgb([154, 105, 79])),
|
||||
("raw_gold_block", Rgb([221, 169, 46])),
|
||||
("quartz_block", Rgb([235, 229, 222])),
|
||||
("quartz_slab", Rgb([235, 229, 222])),
|
||||
("quartz_stairs", Rgb([235, 229, 222])),
|
||||
("smooth_quartz", Rgb([235, 229, 222])),
|
||||
("smooth_quartz_slab", Rgb([235, 229, 222])),
|
||||
("smooth_quartz_stairs", Rgb([235, 229, 222])),
|
||||
("quartz_bricks", Rgb([234, 229, 221])),
|
||||
("quartz_pillar", Rgb([235, 230, 224])),
|
||||
("chiseled_quartz_block", Rgb([231, 226, 218])),
|
||||
("prismarine", Rgb([76, 128, 113])),
|
||||
("prismarine_slab", Rgb([76, 128, 113])),
|
||||
("prismarine_stairs", Rgb([76, 128, 113])),
|
||||
("prismarine_wall", Rgb([76, 128, 113])),
|
||||
("prismarine_bricks", Rgb([99, 172, 158])),
|
||||
("prismarine_brick_slab", Rgb([99, 172, 158])),
|
||||
("prismarine_brick_stairs", Rgb([99, 172, 158])),
|
||||
("dark_prismarine", Rgb([51, 91, 75])),
|
||||
("dark_prismarine_slab", Rgb([51, 91, 75])),
|
||||
("dark_prismarine_stairs", Rgb([51, 91, 75])),
|
||||
("sea_lantern", Rgb([172, 199, 190])),
|
||||
("exposed_copper", Rgb([161, 125, 103])),
|
||||
("weathered_copper", Rgb([109, 145, 107])),
|
||||
("oxidized_copper", Rgb([82, 162, 132])),
|
||||
("cut_copper", Rgb([191, 106, 80])),
|
||||
("cut_copper_slab", Rgb([191, 106, 80])),
|
||||
("cut_copper_stairs", Rgb([191, 106, 80])),
|
||||
("exposed_cut_copper", Rgb([154, 121, 101])),
|
||||
("exposed_cut_copper_slab", Rgb([154, 121, 101])),
|
||||
("exposed_cut_copper_stairs", Rgb([154, 121, 101])),
|
||||
("weathered_cut_copper", Rgb([109, 145, 107])),
|
||||
("weathered_cut_copper_slab", Rgb([109, 145, 107])),
|
||||
("weathered_cut_copper_stairs", Rgb([109, 145, 107])),
|
||||
("oxidized_cut_copper", Rgb([79, 153, 126])),
|
||||
("oxidized_cut_copper_slab", Rgb([79, 153, 126])),
|
||||
("oxidized_cut_copper_stairs", Rgb([79, 153, 126])),
|
||||
("glass", Rgb([200, 220, 230])),
|
||||
("glass_pane", Rgb([200, 220, 230])),
|
||||
("white_stained_glass", Rgb([255, 255, 255])),
|
||||
("white_stained_glass_pane", Rgb([255, 255, 255])),
|
||||
("orange_stained_glass", Rgb([216, 127, 51])),
|
||||
("orange_stained_glass_pane", Rgb([216, 127, 51])),
|
||||
("magenta_stained_glass", Rgb([178, 76, 216])),
|
||||
("magenta_stained_glass_pane", Rgb([178, 76, 216])),
|
||||
("light_blue_stained_glass", Rgb([102, 153, 216])),
|
||||
("light_blue_stained_glass_pane", Rgb([102, 153, 216])),
|
||||
("yellow_stained_glass", Rgb([229, 229, 51])),
|
||||
("yellow_stained_glass_pane", Rgb([229, 229, 51])),
|
||||
("lime_stained_glass", Rgb([127, 204, 25])),
|
||||
("lime_stained_glass_pane", Rgb([127, 204, 25])),
|
||||
("pink_stained_glass", Rgb([242, 127, 165])),
|
||||
("pink_stained_glass_pane", Rgb([242, 127, 165])),
|
||||
("gray_stained_glass", Rgb([76, 76, 76])),
|
||||
("gray_stained_glass_pane", Rgb([76, 76, 76])),
|
||||
("light_gray_stained_glass", Rgb([153, 153, 153])),
|
||||
("light_gray_stained_glass_pane", Rgb([153, 153, 153])),
|
||||
("cyan_stained_glass", Rgb([76, 127, 153])),
|
||||
("cyan_stained_glass_pane", Rgb([76, 127, 153])),
|
||||
("purple_stained_glass", Rgb([127, 63, 178])),
|
||||
("purple_stained_glass_pane", Rgb([127, 63, 178])),
|
||||
("blue_stained_glass", Rgb([51, 76, 178])),
|
||||
("blue_stained_glass_pane", Rgb([51, 76, 178])),
|
||||
("brown_stained_glass", Rgb([102, 76, 51])),
|
||||
("brown_stained_glass_pane", Rgb([102, 76, 51])),
|
||||
("green_stained_glass", Rgb([102, 127, 51])),
|
||||
("green_stained_glass_pane", Rgb([102, 127, 51])),
|
||||
("red_stained_glass", Rgb([153, 51, 51])),
|
||||
("red_stained_glass_pane", Rgb([153, 51, 51])),
|
||||
("black_stained_glass", Rgb([25, 25, 25])),
|
||||
("black_stained_glass_pane", Rgb([25, 25, 25])),
|
||||
("bookshelf", Rgb([116, 89, 53])),
|
||||
("hay_block", Rgb([166, 139, 12])),
|
||||
("melon", Rgb([111, 145, 31])),
|
||||
("pumpkin", Rgb([198, 118, 24])),
|
||||
("jack_o_lantern", Rgb([213, 139, 42])),
|
||||
("carved_pumpkin", Rgb([198, 118, 24])),
|
||||
("tnt", Rgb([219, 68, 52])),
|
||||
("sponge", Rgb([195, 192, 74])),
|
||||
("wet_sponge", Rgb([171, 181, 70])),
|
||||
("moss_block", Rgb([89, 109, 45])),
|
||||
("moss_carpet", Rgb([89, 109, 45])),
|
||||
("sculk", Rgb([12, 28, 36])),
|
||||
("honeycomb_block", Rgb([229, 148, 29])),
|
||||
("slime_block", Rgb([111, 192, 91])),
|
||||
("honey_block", Rgb([251, 185, 52])),
|
||||
("barrel", Rgb([140, 106, 60])),
|
||||
("chest", Rgb([155, 113, 48])),
|
||||
("trapped_chest", Rgb([155, 113, 48])),
|
||||
("crafting_table", Rgb([144, 109, 67])),
|
||||
("furnace", Rgb([110, 110, 110])),
|
||||
("blast_furnace", Rgb([80, 80, 85])),
|
||||
("smoker", Rgb([90, 80, 70])),
|
||||
("anvil", Rgb([68, 68, 68])),
|
||||
("lectern", Rgb([180, 140, 90])),
|
||||
("composter", Rgb([100, 80, 45])),
|
||||
("cauldron", Rgb([60, 60, 60])),
|
||||
("hopper", Rgb([70, 70, 70])),
|
||||
("jukebox", Rgb([130, 90, 70])),
|
||||
("note_block", Rgb([120, 80, 65])),
|
||||
("bell", Rgb([200, 170, 50])),
|
||||
("dirt_path", Rgb([148, 121, 65])),
|
||||
("farmland", Rgb([143, 88, 46])),
|
||||
("mycelium", Rgb([111, 99, 107])),
|
||||
("rail", Rgb([125, 108, 77])),
|
||||
("powered_rail", Rgb([153, 126, 55])),
|
||||
("detector_rail", Rgb([120, 97, 80])),
|
||||
("activator_rail", Rgb([117, 85, 76])),
|
||||
("redstone_wire", Rgb([170, 0, 0])),
|
||||
("redstone_torch", Rgb([170, 0, 0])),
|
||||
("redstone_lamp", Rgb([180, 130, 70])),
|
||||
("lever", Rgb([100, 80, 60])),
|
||||
("tripwire_hook", Rgb([120, 100, 80])),
|
||||
("torch", Rgb([255, 200, 100])),
|
||||
("wall_torch", Rgb([255, 200, 100])),
|
||||
("lantern", Rgb([200, 150, 80])),
|
||||
("soul_lantern", Rgb([80, 200, 200])),
|
||||
("soul_torch", Rgb([80, 200, 200])),
|
||||
("soul_wall_torch", Rgb([80, 200, 200])),
|
||||
("campfire", Rgb([200, 100, 50])),
|
||||
("soul_campfire", Rgb([80, 200, 200])),
|
||||
("candle", Rgb([200, 180, 130])),
|
||||
("dandelion", Rgb([255, 236, 85])),
|
||||
("poppy", Rgb([200, 30, 30])),
|
||||
("blue_orchid", Rgb([47, 186, 199])),
|
||||
("allium", Rgb([190, 130, 200])),
|
||||
("azure_bluet", Rgb([220, 230, 220])),
|
||||
("red_tulip", Rgb([200, 50, 50])),
|
||||
("orange_tulip", Rgb([230, 130, 50])),
|
||||
("white_tulip", Rgb([230, 230, 220])),
|
||||
("pink_tulip", Rgb([220, 150, 170])),
|
||||
("oxeye_daisy", Rgb([230, 230, 200])),
|
||||
("cornflower", Rgb([70, 90, 180])),
|
||||
("lily_of_the_valley", Rgb([230, 230, 230])),
|
||||
("wither_rose", Rgb([30, 30, 30])),
|
||||
("sunflower", Rgb([255, 200, 50])),
|
||||
("lilac", Rgb([200, 150, 200])),
|
||||
("rose_bush", Rgb([180, 40, 40])),
|
||||
("peony", Rgb([230, 180, 200])),
|
||||
("fern", Rgb([80, 120, 60])),
|
||||
("large_fern", Rgb([80, 120, 60])),
|
||||
("dead_bush", Rgb([150, 120, 80])),
|
||||
("seagrass", Rgb([40, 100, 60])),
|
||||
("tall_seagrass", Rgb([40, 100, 60])),
|
||||
("kelp", Rgb([50, 110, 60])),
|
||||
("kelp_plant", Rgb([50, 110, 60])),
|
||||
("sugar_cane", Rgb([140, 180, 100])),
|
||||
("bamboo", Rgb([90, 140, 50])),
|
||||
("vine", Rgb([50, 100, 40])),
|
||||
("lily_pad", Rgb([40, 110, 40])),
|
||||
("sweet_berry_bush", Rgb([60, 90, 50])),
|
||||
("cactus", Rgb([85, 127, 52])),
|
||||
("white_carpet", Rgb([234, 236, 237])),
|
||||
("orange_carpet", Rgb([241, 118, 20])),
|
||||
("magenta_carpet", Rgb([190, 68, 179])),
|
||||
("light_blue_carpet", Rgb([58, 175, 217])),
|
||||
("yellow_carpet", Rgb([249, 198, 40])),
|
||||
("lime_carpet", Rgb([112, 185, 26])),
|
||||
("pink_carpet", Rgb([238, 141, 172])),
|
||||
("gray_carpet", Rgb([63, 68, 72])),
|
||||
("light_gray_carpet", Rgb([142, 142, 135])),
|
||||
("cyan_carpet", Rgb([21, 138, 145])),
|
||||
("purple_carpet", Rgb([122, 42, 173])),
|
||||
("blue_carpet", Rgb([53, 57, 157])),
|
||||
("brown_carpet", Rgb([114, 72, 41])),
|
||||
("green_carpet", Rgb([85, 110, 28])),
|
||||
("red_carpet", Rgb([161, 39, 35])),
|
||||
("black_carpet", Rgb([21, 21, 26])),
|
||||
("oak_sign", Rgb([162, 130, 78])),
|
||||
("oak_wall_sign", Rgb([162, 130, 78])),
|
||||
("spruce_sign", Rgb([115, 85, 49])),
|
||||
("spruce_wall_sign", Rgb([115, 85, 49])),
|
||||
("birch_sign", Rgb([196, 179, 123])),
|
||||
("birch_wall_sign", Rgb([196, 179, 123])),
|
||||
("dark_oak_sign", Rgb([67, 43, 20])),
|
||||
("dark_oak_wall_sign", Rgb([67, 43, 20])),
|
||||
("white_bed", Rgb([234, 236, 237])),
|
||||
("orange_bed", Rgb([241, 118, 20])),
|
||||
("magenta_bed", Rgb([190, 68, 179])),
|
||||
("light_blue_bed", Rgb([58, 175, 217])),
|
||||
("yellow_bed", Rgb([249, 198, 40])),
|
||||
("lime_bed", Rgb([112, 185, 26])),
|
||||
("pink_bed", Rgb([238, 141, 172])),
|
||||
("gray_bed", Rgb([63, 68, 72])),
|
||||
("light_gray_bed", Rgb([142, 142, 135])),
|
||||
("cyan_bed", Rgb([21, 138, 145])),
|
||||
("purple_bed", Rgb([122, 42, 173])),
|
||||
("blue_bed", Rgb([53, 57, 157])),
|
||||
("brown_bed", Rgb([114, 72, 41])),
|
||||
("green_bed", Rgb([85, 110, 28])),
|
||||
("red_bed", Rgb([161, 39, 35])),
|
||||
("black_bed", Rgb([21, 21, 26])),
|
||||
("oak_trapdoor", Rgb([162, 130, 78])),
|
||||
("spruce_trapdoor", Rgb([115, 85, 49])),
|
||||
("birch_trapdoor", Rgb([196, 179, 123])),
|
||||
("dark_oak_trapdoor", Rgb([67, 43, 20])),
|
||||
("iron_trapdoor", Rgb([200, 200, 200])),
|
||||
("iron_bars", Rgb([150, 150, 150])),
|
||||
("ladder", Rgb([160, 130, 70])),
|
||||
("wheat", Rgb([200, 180, 80])),
|
||||
("carrots", Rgb([230, 140, 30])),
|
||||
("potatoes", Rgb([180, 160, 80])),
|
||||
("beetroots", Rgb([150, 50, 50])),
|
||||
("pumpkin_stem", Rgb([120, 140, 70])),
|
||||
("melon_stem", Rgb([120, 140, 70])),
|
||||
])
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::clipping::clip_way_to_bbox;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
@@ -211,7 +212,14 @@ pub fn parse_osm_data(
|
||||
|
||||
nodes_map.insert(element.id, processed.clone());
|
||||
|
||||
processed_elements.push(ProcessedElement::Node(processed));
|
||||
// Only add tagged nodes to processed_elements if they're within or near the bbox
|
||||
// This significantly improves performance by filtering out distant nodes
|
||||
if !element.tags.as_ref().map(|t| t.is_empty()).unwrap_or(true) {
|
||||
// Node has tags, check if it's in the bbox (with some margin)
|
||||
if xzbbox.contains(&xzpoint) {
|
||||
processed_elements.push(ProcessedElement::Node(processed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,13 +234,33 @@ pub fn parse_osm_data(
|
||||
}
|
||||
}
|
||||
|
||||
// Clip the way to bbox to reduce node count dramatically
|
||||
let tags = element.tags.clone().unwrap_or_default();
|
||||
|
||||
// Store unclipped way for relation assembly (clipping happens after ring merging)
|
||||
ways_map.insert(
|
||||
element.id,
|
||||
ProcessedWay {
|
||||
id: element.id,
|
||||
tags: tags.clone(),
|
||||
nodes: nodes.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Clip way nodes for standalone way processing (not relations)
|
||||
let clipped_nodes = clip_way_to_bbox(&nodes, &xzbbox);
|
||||
|
||||
// Skip ways that are completely outside the bbox (empty after clipping)
|
||||
if clipped_nodes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let processed: ProcessedWay = ProcessedWay {
|
||||
id: element.id,
|
||||
tags: element.tags.clone().unwrap_or_default(),
|
||||
nodes,
|
||||
tags: tags.clone(),
|
||||
nodes: clipped_nodes.clone(),
|
||||
};
|
||||
|
||||
ways_map.insert(element.id, processed.clone());
|
||||
processed_elements.push(ProcessedElement::Way(processed));
|
||||
}
|
||||
|
||||
@@ -247,6 +275,9 @@ pub fn parse_osm_data(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||
let is_water_relation = is_water_element(tags);
|
||||
|
||||
let members: Vec<ProcessedMember> = element
|
||||
.members
|
||||
.iter()
|
||||
@@ -271,7 +302,26 @@ pub fn parse_osm_data(
|
||||
}
|
||||
};
|
||||
|
||||
Some(ProcessedMember { role, way })
|
||||
// Water relations: keep unclipped for ring merging
|
||||
// Non-water relations: clip member ways now
|
||||
let final_way = if is_water_relation {
|
||||
way
|
||||
} else {
|
||||
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
||||
if clipped_nodes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
ProcessedWay {
|
||||
id: way.id,
|
||||
tags: way.tags,
|
||||
nodes: clipped_nodes,
|
||||
}
|
||||
};
|
||||
|
||||
Some(ProcessedMember {
|
||||
role,
|
||||
way: final_way,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -289,6 +339,30 @@ pub fn parse_osm_data(
|
||||
(processed_elements, xzbbox)
|
||||
}
|
||||
|
||||
/// Returns true if tags indicate a water element handled by water_areas.rs.
|
||||
fn is_water_element(tags: &HashMap<String, String>) -> bool {
|
||||
// Check for explicit water tag
|
||||
if tags.contains_key("water") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for natural=water or natural=bay
|
||||
if let Some(natural_val) = tags.get("natural") {
|
||||
if natural_val == "water" || natural_val == "bay" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for waterway=dock (also handled as water area)
|
||||
if let Some(waterway_val) = tags.get("waterway") {
|
||||
if waterway_val == "dock" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
const PRIORITY_ORDER: [&str; 6] = [
|
||||
"entrance", "building", "highway", "waterway", "water", "barrier",
|
||||
];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde_json::json;
|
||||
use tauri::{Emitter, WebviewWindow};
|
||||
@@ -38,7 +40,10 @@ pub fn emit_gui_progress_update(progress: f64, message: &str) {
|
||||
});
|
||||
|
||||
if let Err(e) = window.emit("progress-update", payload) {
|
||||
eprintln!("Failed to emit progress event: {e}");
|
||||
let error_msg = format!("Failed to emit progress event: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,3 +56,12 @@ pub fn emit_gui_error(message: &str) {
|
||||
};
|
||||
emit_gui_progress_update(0.0, &format!("Error! {truncated_message}"));
|
||||
}
|
||||
|
||||
/// Emits an event when the world map preview is ready
|
||||
pub fn emit_map_preview_ready() {
|
||||
if let Some(window) = get_main_window() {
|
||||
if let Err(e) = window.emit("map-preview-ready", ()) {
|
||||
eprintln!("Failed to emit map-preview-ready event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
249
src/telemetry.rs
Normal file
249
src/telemetry.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use log::error;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Serialize;
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// Telemetry endpoint URL
|
||||
const TELEMETRY_URL: &str = "https://arnismc.com/telemetry/report_telemetry.php";
|
||||
|
||||
/// Global flag to store user's telemetry consent
|
||||
static TELEMETRY_CONSENT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Sets the user's telemetry consent preference
|
||||
pub fn set_telemetry_consent(consent: bool) {
|
||||
TELEMETRY_CONSENT.store(consent, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Gets the user's telemetry consent preference
|
||||
fn get_telemetry_consent() -> bool {
|
||||
TELEMETRY_CONSENT.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Determines the current platform as a string
|
||||
fn get_platform() -> &'static str {
|
||||
match std::env::consts::OS {
|
||||
"windows" => "windows",
|
||||
"linux" => "linux",
|
||||
"macos" => "macos",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the application version from Cargo.toml
|
||||
fn get_app_version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Crash report payload structure
|
||||
#[derive(Serialize)]
|
||||
struct CrashReport<'a> {
|
||||
r#type: &'a str,
|
||||
error_message: &'a str,
|
||||
platform: &'a str,
|
||||
app_version: &'a str,
|
||||
}
|
||||
|
||||
/// Generation click payload structure
|
||||
#[derive(Serialize)]
|
||||
struct GenerationClick<'a> {
|
||||
r#type: &'a str,
|
||||
}
|
||||
|
||||
/// Log entry payload structure
|
||||
#[derive(Serialize)]
|
||||
struct LogEntry<'a> {
|
||||
r#type: &'a str,
|
||||
log_level: &'a str,
|
||||
log_message: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
platform: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
app_version: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Sends a crash report to the telemetry server
|
||||
fn send_crash_report(error_message: String, platform: &str, app_version: &str) {
|
||||
// Wrap in catch_unwind to prevent any panics during crash reporting
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = CrashReport {
|
||||
r#type: "crash",
|
||||
error_message: &error_message,
|
||||
platform,
|
||||
app_version,
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
}
|
||||
|
||||
/// Sends a generation click event to the telemetry server
|
||||
pub fn send_generation_click() {
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send in background thread to avoid blocking UI
|
||||
// Wrap in catch_unwind to prevent any panics from escaping
|
||||
let _ = std::thread::spawn(|| {
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = GenerationClick {
|
||||
r#type: "generation_click",
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// Log levels for telemetry
|
||||
#[allow(dead_code)]
|
||||
pub enum LogLevel {
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
LogLevel::Debug => "debug",
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warning => "warning",
|
||||
LogLevel::Error => "error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a log entry to the telemetry server
|
||||
pub fn send_log(level: LogLevel, message: &str) {
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate message to 1000 characters
|
||||
let truncated_message = if message.chars().count() > 1000 {
|
||||
message.chars().take(1000).collect::<String>()
|
||||
} else {
|
||||
message.to_string()
|
||||
};
|
||||
|
||||
let platform = get_platform();
|
||||
let app_version = get_app_version();
|
||||
|
||||
// Send in background thread to avoid blocking
|
||||
// Wrap in catch_unwind to prevent any panics from escaping
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let _ = (|| -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = LogEntry {
|
||||
r#type: "log",
|
||||
log_level: level.as_str(),
|
||||
log_message: &truncated_message,
|
||||
platform: Some(platform),
|
||||
app_version: Some(app_version),
|
||||
};
|
||||
|
||||
let _res = client
|
||||
.post(TELEMETRY_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload)
|
||||
.send()?;
|
||||
|
||||
Ok(())
|
||||
})();
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// Installs a panic hook that logs panics and sends crash reports
|
||||
pub fn install_panic_hook() {
|
||||
panic::set_hook(Box::new(|panic_info| {
|
||||
// Log the panic to both stderr and log file
|
||||
error!("Application panicked: {:?}", panic_info);
|
||||
|
||||
// Filter out secondary "panic in a function that cannot unwind" panics
|
||||
if let Some(location) = panic_info.location() {
|
||||
if location.file().contains("panicking.rs") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check user consent
|
||||
if !get_telemetry_consent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send crash reports in release builds
|
||||
if cfg!(debug_assertions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else wrapped in catch_unwind to prevent secondary panics
|
||||
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
// Extract panic payload
|
||||
let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Unknown panic".to_string()
|
||||
};
|
||||
|
||||
// Extract location
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
// Combine payload and location
|
||||
let mut error_message = format!("{} @ {}", payload, location);
|
||||
|
||||
// Truncate to 500 Unicode characters
|
||||
if error_message.chars().count() > 500 {
|
||||
error_message = error_message.chars().take(500).collect();
|
||||
}
|
||||
|
||||
let platform = get_platform();
|
||||
let app_version = get_app_version();
|
||||
|
||||
// Send crash report (best-effort, ignore all errors)
|
||||
send_crash_report(error_message, platform, app_version);
|
||||
}));
|
||||
}));
|
||||
}
|
||||
@@ -3,6 +3,8 @@ use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use fastanvil::Region;
|
||||
use fastnbt::{LongArray, Value};
|
||||
@@ -755,7 +757,9 @@ impl<'a> WorldEditor<'a> {
|
||||
|
||||
// Save metadata with error handling
|
||||
if let Err(e) = self.save_metadata() {
|
||||
eprintln!("Warning: Failed to save world metadata: {}", e);
|
||||
eprintln!("Failed to save world metadata: {}", e);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, "Failed to save world metadata.");
|
||||
// Continue with world saving even if metadata fails
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user