Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d97391820 | ||
|
|
bef3cfb090 | ||
|
|
5a898944f7 | ||
|
|
9fdd960009 | ||
|
|
58e4a337d9 | ||
|
|
236a7e5af9 | ||
|
|
9173e5b4de | ||
|
|
1fd02d8005 | ||
|
|
438b2beceb | ||
|
|
a62e181c16 | ||
|
|
12abba3bc8 | ||
|
|
a8e31700d8 | ||
|
|
7a109cce0b | ||
|
|
86543714af | ||
|
|
b84a565210 | ||
|
|
93becaae7f | ||
|
|
06e377ce29 | ||
|
|
e22380bdd3 | ||
|
|
35cac44209 | ||
|
|
61af45d2f4 | ||
|
|
393f1f9bd8 | ||
|
|
e6f8466177 | ||
|
|
02d3a32a03 | ||
|
|
f00304ff3a | ||
|
|
a93b908104 | ||
|
|
7cbc4fa263 | ||
|
|
7e7f7ed476 | ||
|
|
3c0ba60657 | ||
|
|
fb438c4a0f | ||
|
|
5015c8b9b4 | ||
|
|
af0ace422f | ||
|
|
0bb39b7d9e | ||
|
|
5b5e93b89a | ||
|
|
958dc2107e | ||
|
|
562a3bca66 | ||
|
|
f1b37fbbb6 | ||
|
|
b34cbf4307 | ||
|
|
a03318bb98 | ||
|
|
8bb779d6cc | ||
|
|
6d164102ad | ||
|
|
127a0e5e68 | ||
|
|
4a326c3dad | ||
|
|
d4fd9b9cd3 | ||
|
|
ee0521f232 | ||
|
|
8b3a41b131 | ||
|
|
02594b1cae | ||
|
|
06ba4db97e | ||
|
|
59d31cfbb8 | ||
|
|
94388e4164 | ||
|
|
f8c9fd8f4c | ||
|
|
2ee2d48f6a | ||
|
|
56c2f2e5cd | ||
|
|
9d34bc8e92 | ||
|
|
c95b78fdcd | ||
|
|
6e52e08b8a | ||
|
|
57a4a801cf | ||
|
|
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 | ||
|
|
834ce7b51d | ||
|
|
93b6f5ac99 | ||
|
|
77df683deb | ||
|
|
9d791f4299 | ||
|
|
a2dff0b84b | ||
|
|
24630351b9 | ||
|
|
819b03e3b1 | ||
|
|
f315245637 | ||
|
|
16c9f3c3bf | ||
|
|
3043ca6d24 | ||
|
|
3657878f01 | ||
|
|
44cc57c3dd | ||
|
|
408caa9176 | ||
|
|
3191a3676d | ||
|
|
8fff2d2fb5 | ||
|
|
8c702a36ff | ||
|
|
9bedca071a | ||
|
|
d611837746 | ||
|
|
e06fcaf7a2 | ||
|
|
1f2772f052 | ||
|
|
82f3460043 | ||
|
|
65d6bb6c99 | ||
|
|
7ebee82982 | ||
|
|
5056e83dff | ||
|
|
3cb0b994e1 | ||
|
|
5ded3c961e | ||
|
|
893c14bff8 | ||
|
|
4f8e0020e3 | ||
|
|
456018abf7 | ||
|
|
175bdc4582 | ||
|
|
ed294a3973 | ||
|
|
6c966fc9cc | ||
|
|
ecfc6634fc | ||
|
|
a336ccd1aa | ||
|
|
f702a41af1 | ||
|
|
29c4dc6d7c | ||
|
|
f60b341eca | ||
|
|
92c5e52f46 | ||
|
|
e4d7dd15c2 | ||
|
|
cc6748115b | ||
|
|
ce9496aea5 | ||
|
|
4c87eb8141 | ||
|
|
ada475f73e | ||
|
|
e5a82ba526 | ||
|
|
309ac19b09 | ||
|
|
90df7688df | ||
|
|
5b3889a7bb | ||
|
|
67deb739e6 | ||
|
|
7c08d21f36 | ||
|
|
280acc7a8a | ||
|
|
ee59da5d9b | ||
|
|
b772cb6ab9 | ||
|
|
2646947ed0 | ||
|
|
b9976fd562 | ||
|
|
a621703da4 | ||
|
|
87efd02c74 | ||
|
|
6a6b58fd8f | ||
|
|
855c6fe846 | ||
|
|
1592951fe3 | ||
|
|
e267e04350 | ||
|
|
c2e8d5959f | ||
|
|
ffe8f865d2 | ||
|
|
4f9f4f127a | ||
|
|
014208426b | ||
|
|
6d848ef7cd | ||
|
|
1e25dfea37 | ||
|
|
903efec459 | ||
|
|
22b3969c72 | ||
|
|
0f62c4283b | ||
|
|
4299e64cba | ||
|
|
0cd386b399 | ||
|
|
2fdecbe206 | ||
|
|
75d1ab14e7 | ||
|
|
f9c18ae5f6 | ||
|
|
963fe2327b | ||
|
|
2a8ff6f641 | ||
|
|
ff3ea1093a | ||
|
|
fd401ab23f | ||
|
|
3f67e060eb | ||
|
|
858c2f4c93 | ||
|
|
e8fad0e197 | ||
|
|
7ebc66db7e | ||
|
|
28b75a22cf | ||
|
|
9ee9f0de64 | ||
|
|
5b874a5c29 | ||
|
|
eeecdc8f99 | ||
|
|
8b33e152ef | ||
|
|
ca8e50fbf1 | ||
|
|
7f4ef9130f | ||
|
|
8d183543be | ||
|
|
dd07fba15c | ||
|
|
083fbed040 | ||
|
|
4191fa4902 | ||
|
|
6c82265ee3 | ||
|
|
7d978047b4 |
2
.gitattributes
vendored
@@ -1 +1 @@
|
||||
gui-src/** linguist-vendored
|
||||
src/gui/** linguist-vendored
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -11,13 +11,13 @@ assignees: ''
|
||||
A clear and concise description of what the bug is and what you expected to happen.
|
||||
|
||||
**Used bbox area**
|
||||
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
||||
Please provide your input parameters (BBOX) so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
|
||||
|
||||
**Arnis and Minecraft version**
|
||||
Please tell us what version of Arnis and Minecraft you used.
|
||||
Please tell us what version of Arnis and Minecraft you used, as well as if you are on Windows, Linux or MacOS.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too. If you experienced any issue with the application itself like a crash, please provide the log file which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
||||
Add any other context about the problem here. If you used any more custom settings, please provide them here too. Please provide the log file if possible as well, which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
|
||||
|
||||
6
.github/workflows/ci-build.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
|
||||
14
.github/workflows/pr-benchmark.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@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 --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
|
||||
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
|
||||
peak_kb=$(cat peak_mem_kb.txt)
|
||||
peak_mb=$((peak_kb / 1024))
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
seconds=$((duration % 60))
|
||||
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
|
||||
|
||||
baseline_time=135
|
||||
baseline_time=30
|
||||
diff=$((duration - baseline_time))
|
||||
abs_diff=${diff#-}
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
verdict="🚨 This PR **drastically worsens generation time**."
|
||||
fi
|
||||
|
||||
baseline_mem=5865
|
||||
baseline_mem=935
|
||||
mem_annotation=""
|
||||
if [ "$peak_mem" -gt 2000 ]; then
|
||||
mem_diff=$((peak_mem - baseline_mem))
|
||||
@@ -87,6 +87,8 @@ jobs:
|
||||
mem_annotation=" (↗ ${mem_percent}% more)"
|
||||
fi
|
||||
|
||||
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
{
|
||||
echo "summary<<EOF"
|
||||
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
|
||||
@@ -98,6 +100,8 @@ jobs:
|
||||
echo ""
|
||||
echo "${verdict}"
|
||||
echo ""
|
||||
echo "📅 **Last benchmark:** ${benchmark_time}"
|
||||
echo ""
|
||||
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
@@ -108,4 +112,4 @@ jobs:
|
||||
message: ${{ steps.comment_body.outputs.summary }}
|
||||
comment-tag: benchmark-report
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
|
||||
14
.github/workflows/release.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@v1
|
||||
@@ -97,13 +97,13 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Download macOS Intel build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-13-x86_64-apple-darwin-build
|
||||
path: ./intel
|
||||
|
||||
- name: Download macOS ARM64 build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-latest-aarch64-apple-darwin-build
|
||||
path: ./arm64
|
||||
@@ -124,22 +124,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Windows build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: windows-latest-x86_64-pc-windows-msvc-build
|
||||
path: ./builds/windows
|
||||
|
||||
- name: Download Linux build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: ubuntu-latest-x86_64-unknown-linux-gnu-build
|
||||
path: ./builds/linux
|
||||
|
||||
- name: Download macOS universal build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: macos-universal-build
|
||||
path: ./builds/macos
|
||||
|
||||
2
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
/wiki
|
||||
*.mcworld
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.envrc
|
||||
/.direnv
|
||||
|
||||
# Build artifacts
|
||||
|
||||
592
Cargo.lock
generated
24
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -14,21 +14,24 @@ overflow-checks = true
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {version = "2", optional = true}
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
fastanvil = "0.31.0"
|
||||
fastnbt = "2.5.0"
|
||||
fastanvil = "0.32.0"
|
||||
fastnbt = "2.6.0"
|
||||
flate2 = "1.1"
|
||||
fnv = "1.0.7"
|
||||
fs2 = "0.4"
|
||||
geo = "0.30.0"
|
||||
geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
@@ -38,16 +41,21 @@ rand = "0.8.5"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.15.4", optional = true }
|
||||
semver = "1.0.23"
|
||||
semver = "1.0.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 }
|
||||
bedrockrs_level = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_level", optional = true }
|
||||
bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_shared", optional = true }
|
||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||
vek = { version = "0.17", optional = true }
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
tempfile = "3.23.0"
|
||||
|
||||
18
README.md
@@ -1,17 +1,17 @@
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/banner.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/banner.png" width="100%" alt="Banner">
|
||||
|
||||
# Arnis [](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [](https://github.com/louis-e/arnis/releases) [](https://discord.gg/mA2g69Fhxq)
|
||||
|
||||
Arnis creates complex and accurate Minecraft Java Edition worlds that reflect real-world geography, topography, and architecture.
|
||||
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
|
||||
|
||||
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
|
||||
Generate your hometown, big cities, and natural landscapes with ease!
|
||||
|
||||

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

|
||||
<i>This Github page and [arnismc.com](https://arnismc.com) are the only official project websites. Do not download Arnis from any other website.</i>
|
||||
|
||||
## :keyboard: Usage
|
||||
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/gui.png?raw=true"><br>
|
||||
<img width="60%" src="assets/git/gui.png"><br>
|
||||
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
|
||||
|
||||
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
|
||||
@@ -19,7 +19,7 @@ Additionally, you can customize various generation settings, such as world scale
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/documentation.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/documentation.png" width="100%" alt="Banner">
|
||||
|
||||
Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/arnis/wiki/), covering topics such as technical explanations, FAQs, contribution guidelines and roadmaps.
|
||||
|
||||
@@ -34,7 +34,7 @@ Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/
|
||||
#### How to contribute
|
||||
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. Please respect the above mentioned key objectives. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
|
||||
|
||||
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
|
||||
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lat,min_lng,max_lat,max_lng"```<br>
|
||||
GUI Build: ```cargo run```<br>
|
||||
|
||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||
@@ -51,7 +51,7 @@ After your pull request was merged, I will take care of regularly creating updat
|
||||
|
||||
## :newspaper: Academic & Press Recognition
|
||||
|
||||
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/recognition.png?raw=true" width="100%" alt="Banner">
|
||||
<img src="assets/git/recognition.png" width="100%" alt="Banner">
|
||||
|
||||
Arnis has been recognized in various academic and press publications after gaining a lot of attention in December 2024.
|
||||
|
||||
@@ -78,7 +78,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.[^3]
|
||||
|
||||
Download Arnis only from the official source (https://github.com/louis-e/arnis/). Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
||||
Download Arnis only from the official source https://arnismc.com or https://github.com/louis-e/arnis/. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.
|
||||
|
||||
The logo was made by @nxfx21.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 790 KiB After Width: | Height: | Size: 790 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
assets/minecraft/world_icon.jpeg
Normal file
|
After Width: | Height: | Size: 54 KiB |
60
flake.lock
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755615617,
|
||||
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
36
flake.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
flake-utils,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell.override { inherit stdenv; } {
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
99
flake/flake.lock
generated
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1724653830,
|
||||
"narHash": "sha256-88f0KK8h6tGIP4Na5RJDKs0S+7WsGGaCGNkLj/bPV3g=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "9ecf5e7d800ace001320da8acadd4a3deb872a83",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1724479785,
|
||||
"narHash": "sha256-pP3Azj5d6M5nmG68Fu4JqZmdGt4S4vqI5f8te+E/FTw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d0e1602ddde669d5beb01aec49d71a51937ed7be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1724586512,
|
||||
"narHash": "sha256-mrfwk6nO8N2WtCq3sB2zhd2QN1HMKzeSESzOA6lSsQg=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "7106cd3be50b2a43c1d9f2787bf22d4369c2b25b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
inputs = {
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, fenix, flake-utils, nixpkgs }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
fenixPkgs = (fenix.packages.${system}.stable);
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell
|
||||
{
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
fenixPkgs.toolchain
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
10
src/args.rs
@@ -1,6 +1,6 @@
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use clap::Parser;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Command-line arguments parser
|
||||
@@ -21,7 +21,7 @@ pub struct Args {
|
||||
|
||||
/// Path to the Minecraft world (required)
|
||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Downloader method (requests/curl/wget) (optional)
|
||||
#[arg(long, default_value = "requests")]
|
||||
@@ -64,8 +64,8 @@ pub struct Args {
|
||||
pub spawn_point: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
||||
let mc_world_path = Path::new(path);
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
let mc_world_path = PathBuf::from(path);
|
||||
if !mc_world_path.exists() {
|
||||
return Err(format!("Path does not exist: {path}"));
|
||||
}
|
||||
@@ -76,7 +76,7 @@ fn validate_minecraft_world_path(path: &str) -> Result<String, String> {
|
||||
if !region.is_dir() {
|
||||
return Err(format!("No Minecraft world found at {region:?}"));
|
||||
}
|
||||
Ok(path.to_string())
|
||||
Ok(mc_world_path)
|
||||
}
|
||||
|
||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||
|
||||
849
src/bedrock_block_map.rs
Normal file
@@ -0,0 +1,849 @@
|
||||
//! Bedrock Block Mapping
|
||||
//!
|
||||
//! This module provides translation between the internal Block representation
|
||||
//! and Bedrock Edition block format. Bedrock uses string identifiers with
|
||||
//! state properties that differ slightly from Java Edition.
|
||||
|
||||
use crate::block_definitions::Block;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents a Bedrock block with its identifier and state properties.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BedrockBlock {
|
||||
/// The Bedrock block identifier (e.g., "minecraft:stone")
|
||||
pub name: String,
|
||||
/// Block state properties as key-value pairs
|
||||
pub states: HashMap<String, BedrockBlockStateValue>,
|
||||
}
|
||||
|
||||
/// Bedrock block state values can be strings, booleans, or integers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BedrockBlockStateValue {
|
||||
String(String),
|
||||
Bool(bool),
|
||||
Int(i32),
|
||||
}
|
||||
|
||||
impl BedrockBlock {
|
||||
/// Creates a simple block with no state properties.
|
||||
pub fn simple(name: &str) -> Self {
|
||||
Self {
|
||||
name: format!("minecraft:{name}"),
|
||||
states: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a block with state properties.
|
||||
pub fn with_states(name: &str, states: Vec<(&str, BedrockBlockStateValue)>) -> Self {
|
||||
let mut state_map = HashMap::new();
|
||||
for (key, value) in states {
|
||||
state_map.insert(key.to_string(), value);
|
||||
}
|
||||
Self {
|
||||
name: format!("minecraft:{name}"),
|
||||
states: state_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an internal Block to a BedrockBlock representation.
|
||||
///
|
||||
/// This function handles the mapping between Java Edition block names/properties
|
||||
/// and their Bedrock Edition equivalents. Many blocks are identical, but some
|
||||
/// require translation of property names or values.
|
||||
pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
let java_name = block.name();
|
||||
|
||||
// Most blocks have the same name in both editions
|
||||
// Handle special cases first, then fall back to direct mapping
|
||||
match java_name {
|
||||
// Grass block is just "grass_block" in both editions
|
||||
"grass_block" => BedrockBlock::simple("grass_block"),
|
||||
|
||||
// Short grass is just "short_grass" in Java but "tallgrass" in Bedrock
|
||||
"short_grass" => BedrockBlock::with_states(
|
||||
"tallgrass",
|
||||
vec![(
|
||||
"tall_grass_type",
|
||||
BedrockBlockStateValue::String("tall".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Tall grass needs height state
|
||||
"tall_grass" => BedrockBlock::with_states(
|
||||
"double_plant",
|
||||
vec![(
|
||||
"double_plant_type",
|
||||
BedrockBlockStateValue::String("grass".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Oak leaves with persistence
|
||||
"oak_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Birch leaves with persistence
|
||||
"birch_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("birch".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Oak log with axis (default up_down)
|
||||
"oak_log" => BedrockBlock::with_states(
|
||||
"oak_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Birch log with axis
|
||||
"birch_log" => BedrockBlock::with_states(
|
||||
"birch_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Spruce log with axis
|
||||
"spruce_log" => BedrockBlock::with_states(
|
||||
"spruce_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Stone slab (bottom half by default)
|
||||
"stone_slab" => BedrockBlock::with_states(
|
||||
"stone_block_slab",
|
||||
vec![
|
||||
(
|
||||
"stone_slab_type",
|
||||
BedrockBlockStateValue::String("smooth_stone".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Stone brick slab
|
||||
"stone_brick_slab" => BedrockBlock::with_states(
|
||||
"stone_block_slab",
|
||||
vec![
|
||||
(
|
||||
"stone_slab_type",
|
||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Oak slab
|
||||
"oak_slab" => BedrockBlock::with_states(
|
||||
"wooden_slab",
|
||||
vec![
|
||||
(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
),
|
||||
("top_slot_bit", BedrockBlockStateValue::Bool(false)),
|
||||
],
|
||||
),
|
||||
|
||||
// Water (flowing by default)
|
||||
"water" => BedrockBlock::with_states(
|
||||
"water",
|
||||
vec![("liquid_depth", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Rail with shape state
|
||||
"rail" => BedrockBlock::with_states(
|
||||
"rail",
|
||||
vec![("rail_direction", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Farmland with moisture
|
||||
"farmland" => BedrockBlock::with_states(
|
||||
"farmland",
|
||||
vec![("moisturized_amount", BedrockBlockStateValue::Int(7))],
|
||||
),
|
||||
|
||||
// Snow layer
|
||||
"snow" => BedrockBlock::with_states(
|
||||
"snow_layer",
|
||||
vec![("height", BedrockBlockStateValue::Int(0))],
|
||||
),
|
||||
|
||||
// Cobblestone wall
|
||||
"cobblestone_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("cobblestone".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Andesite wall
|
||||
"andesite_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("andesite".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Stone brick wall
|
||||
"stone_brick_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Flowers - poppy is just "red_flower" in Bedrock
|
||||
"poppy" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("poppy".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Dandelion is "yellow_flower" in Bedrock
|
||||
"dandelion" => BedrockBlock::simple("yellow_flower"),
|
||||
|
||||
// Blue orchid
|
||||
"blue_orchid" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("orchid".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Azure bluet
|
||||
"azure_bluet" => BedrockBlock::with_states(
|
||||
"red_flower",
|
||||
vec![(
|
||||
"flower_type",
|
||||
BedrockBlockStateValue::String("houstonia".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Concrete colors (Bedrock uses a single block with color state)
|
||||
"white_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"black_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||
),
|
||||
"gray_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"light_gray_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("silver".to_string()),
|
||||
)],
|
||||
),
|
||||
"light_blue_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||
)],
|
||||
),
|
||||
"cyan_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||
),
|
||||
"blue_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
"purple_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("purple".to_string()),
|
||||
)],
|
||||
),
|
||||
"magenta_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("magenta".to_string()),
|
||||
)],
|
||||
),
|
||||
"red_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"orange_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"yellow_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"lime_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("lime".to_string()))],
|
||||
),
|
||||
"brown_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
|
||||
// Terracotta colors
|
||||
"white_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"orange_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"yellow_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"light_blue_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||
)],
|
||||
),
|
||||
"blue_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
"gray_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"green_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||
),
|
||||
"red_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"brown_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"black_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||
),
|
||||
// Plain terracotta
|
||||
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
||||
|
||||
// Wool colors
|
||||
"white_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"red_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"green_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||
),
|
||||
"brown_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"cyan_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||
),
|
||||
"yellow_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Carpets
|
||||
"white_carpet" => BedrockBlock::with_states(
|
||||
"carpet",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"red_carpet" => BedrockBlock::with_states(
|
||||
"carpet",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
|
||||
// Stained glass
|
||||
"white_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("white".to_string()))],
|
||||
),
|
||||
"gray_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("gray".to_string()))],
|
||||
),
|
||||
"light_gray_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("silver".to_string()),
|
||||
)],
|
||||
),
|
||||
"brown_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
|
||||
// Planks - Bedrock uses single "planks" block with wood_type state
|
||||
"oak_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("oak".to_string()),
|
||||
)],
|
||||
),
|
||||
"spruce_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("spruce".to_string()),
|
||||
)],
|
||||
),
|
||||
"birch_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("birch".to_string()),
|
||||
)],
|
||||
),
|
||||
"jungle_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("jungle".to_string()),
|
||||
)],
|
||||
),
|
||||
"acacia_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("acacia".to_string()),
|
||||
)],
|
||||
),
|
||||
"dark_oak_planks" => BedrockBlock::with_states(
|
||||
"planks",
|
||||
vec![(
|
||||
"wood_type",
|
||||
BedrockBlockStateValue::String("dark_oak".to_string()),
|
||||
)],
|
||||
),
|
||||
"crimson_planks" => BedrockBlock::simple("crimson_planks"),
|
||||
"warped_planks" => BedrockBlock::simple("warped_planks"),
|
||||
|
||||
// Stone variants
|
||||
"stone" => BedrockBlock::simple("stone"),
|
||||
"granite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("granite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_granite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("granite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
"diorite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("diorite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_diorite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("diorite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
"andesite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("andesite".to_string()),
|
||||
)],
|
||||
),
|
||||
"polished_andesite" => BedrockBlock::with_states(
|
||||
"stone",
|
||||
vec![(
|
||||
"stone_type",
|
||||
BedrockBlockStateValue::String("andesite_smooth".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Blocks with different names in Bedrock
|
||||
"bricks" => BedrockBlock::simple("brick_block"),
|
||||
"end_stone_bricks" => BedrockBlock::simple("end_bricks"),
|
||||
"nether_bricks" => BedrockBlock::simple("nether_brick"),
|
||||
"red_nether_bricks" => BedrockBlock::simple("red_nether_brick"),
|
||||
"snow_block" => BedrockBlock::simple("snow"),
|
||||
"dirt_path" => BedrockBlock::simple("grass_path"),
|
||||
"dead_bush" => BedrockBlock::simple("deadbush"),
|
||||
"note_block" => BedrockBlock::simple("noteblock"),
|
||||
|
||||
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
||||
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
||||
"oak_door" => BedrockBlock::simple("wooden_door"),
|
||||
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
||||
|
||||
// Bed (Bedrock uses single "bed" block with color state)
|
||||
"red_bed" => BedrockBlock::with_states(
|
||||
"bed",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
|
||||
// Default: use the same name (works for many blocks)
|
||||
// Log unmapped blocks to help identify missing mappings
|
||||
_ => BedrockBlock::simple(java_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an internal Block with optional Java properties to a BedrockBlock.
|
||||
///
|
||||
/// This function extends `to_bedrock_block` by also handling block-specific properties
|
||||
/// like stair facing/shape, slab type, etc. Java property names and values are converted
|
||||
/// to their Bedrock equivalents.
|
||||
pub fn to_bedrock_block_with_properties(
|
||||
block: Block,
|
||||
java_properties: Option<&fastnbt::Value>,
|
||||
) -> BedrockBlock {
|
||||
let java_name = block.name();
|
||||
|
||||
// Extract Java properties as a map if present
|
||||
let props_map = java_properties.and_then(|v| {
|
||||
if let fastnbt::Value::Compound(map) = v {
|
||||
Some(map)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Handle stairs with facing/shape properties
|
||||
if java_name.ends_with("_stairs") {
|
||||
return convert_stairs(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle slabs with type property (top/bottom/double)
|
||||
if java_name.ends_with("_slab") {
|
||||
return convert_slab(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle logs with axis property
|
||||
if java_name.ends_with("_log") || java_name.ends_with("_wood") {
|
||||
return convert_log(java_name, props_map);
|
||||
}
|
||||
|
||||
// Fall back to basic conversion without properties
|
||||
to_bedrock_block(block)
|
||||
}
|
||||
|
||||
/// Convert Java stair block to Bedrock format with proper orientation.
|
||||
fn convert_stairs(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
// Map Java stair names to Bedrock equivalents
|
||||
let bedrock_name = match java_name {
|
||||
"end_stone_brick_stairs" => "end_brick_stairs",
|
||||
_ => java_name, // Most stairs have the same name
|
||||
};
|
||||
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert facing: Java uses "north/south/east/west", Bedrock uses "weirdo_direction" (0-3)
|
||||
// Bedrock: 0=east, 1=west, 2=south, 3=north
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let direction = match facing.as_str() {
|
||||
"east" => 0,
|
||||
"west" => 1,
|
||||
"south" => 2,
|
||||
"north" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
states.insert(
|
||||
"weirdo_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(direction),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert half: Java uses "top/bottom", Bedrock uses "upside_down_bit"
|
||||
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||
let upside_down = half == "top";
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(upside_down),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no properties were set, use defaults
|
||||
if states.is_empty() {
|
||||
states.insert(
|
||||
"weirdo_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(0),
|
||||
);
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java slab block to Bedrock format with proper type.
|
||||
fn convert_slab(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert type: Java uses "top/bottom/double", Bedrock uses "top_slot_bit"
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(slab_type)) = props.get("type") {
|
||||
let top_slot = slab_type == "top";
|
||||
states.insert(
|
||||
"top_slot_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(top_slot),
|
||||
);
|
||||
// Note: "double" slabs in Java become full blocks in Bedrock (different block ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to bottom if not specified
|
||||
if !states.contains_key("top_slot_bit") {
|
||||
states.insert(
|
||||
"top_slot_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
|
||||
// Handle special slab name mappings (same as in to_bedrock_block)
|
||||
let bedrock_name = match java_name {
|
||||
"stone_slab" => "stone_block_slab",
|
||||
"stone_brick_slab" => "stone_block_slab",
|
||||
"oak_slab" => "wooden_slab",
|
||||
"spruce_slab" => "wooden_slab",
|
||||
"birch_slab" => "wooden_slab",
|
||||
"jungle_slab" => "wooden_slab",
|
||||
"acacia_slab" => "wooden_slab",
|
||||
"dark_oak_slab" => "wooden_slab",
|
||||
_ => java_name,
|
||||
};
|
||||
|
||||
// Add wood_type for wooden slabs
|
||||
if bedrock_name == "wooden_slab" {
|
||||
let wood_type = java_name.trim_end_matches("_slab");
|
||||
states.insert(
|
||||
"wood_type".to_string(),
|
||||
BedrockBlockStateValue::String(wood_type.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone_slab_type for stone slabs
|
||||
if bedrock_name == "stone_block_slab" {
|
||||
let slab_type = if java_name == "stone_brick_slab" {
|
||||
"stone_brick"
|
||||
} else {
|
||||
"stone"
|
||||
};
|
||||
states.insert(
|
||||
"stone_slab_type".to_string(),
|
||||
BedrockBlockStateValue::String(slab_type.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java log/wood block to Bedrock format with proper axis.
|
||||
fn convert_log(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let bedrock_name = java_name;
|
||||
let mut states = HashMap::new();
|
||||
|
||||
// Convert axis: Java uses "x/y/z", Bedrock uses "pillar_axis"
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(axis)) = props.get("axis") {
|
||||
states.insert(
|
||||
"pillar_axis".to_string(),
|
||||
BedrockBlockStateValue::String(axis.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to y-axis if not specified
|
||||
if states.is_empty() {
|
||||
states.insert(
|
||||
"pillar_axis".to_string(),
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::block_definitions::{AIR, GRASS_BLOCK, STONE};
|
||||
|
||||
#[test]
|
||||
fn test_simple_blocks() {
|
||||
let bedrock = to_bedrock_block(STONE);
|
||||
assert_eq!(bedrock.name, "minecraft:stone");
|
||||
assert!(bedrock.states.is_empty());
|
||||
|
||||
let bedrock = to_bedrock_block(AIR);
|
||||
assert_eq!(bedrock.name, "minecraft:air");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grass_block() {
|
||||
let bedrock = to_bedrock_block(GRASS_BLOCK);
|
||||
assert_eq!(bedrock.name, "minecraft:grass_block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_colored_blocks() {
|
||||
use crate::block_definitions::WHITE_CONCRETE;
|
||||
let bedrock = to_bedrock_block(WHITE_CONCRETE);
|
||||
assert_eq!(bedrock.name, "minecraft:concrete");
|
||||
assert!(matches!(
|
||||
bedrock.states.get("color"),
|
||||
Some(BedrockBlockStateValue::String(s)) if s == "white"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stairs_with_properties() {
|
||||
use crate::block_definitions::OAK_STAIRS;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
|
||||
// Create Java properties for a south-facing stair
|
||||
let mut props = StdHashMap::new();
|
||||
props.insert(
|
||||
"facing".to_string(),
|
||||
fastnbt::Value::String("south".to_string()),
|
||||
);
|
||||
props.insert(
|
||||
"half".to_string(),
|
||||
fastnbt::Value::String("bottom".to_string()),
|
||||
);
|
||||
let java_props = fastnbt::Value::Compound(props);
|
||||
|
||||
let bedrock = to_bedrock_block_with_properties(OAK_STAIRS, Some(&java_props));
|
||||
assert_eq!(bedrock.name, "minecraft:oak_stairs");
|
||||
|
||||
// Check weirdo_direction is set correctly (south = 2)
|
||||
assert!(matches!(
|
||||
bedrock.states.get("weirdo_direction"),
|
||||
Some(BedrockBlockStateValue::Int(2))
|
||||
));
|
||||
|
||||
// Check upside_down_bit is false for bottom half
|
||||
assert!(matches!(
|
||||
bedrock.states.get("upside_down_bit"),
|
||||
Some(BedrockBlockStateValue::Bool(false))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stairs_upside_down() {
|
||||
use crate::block_definitions::STONE_BRICK_STAIRS;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
|
||||
// Create Java properties for an upside-down north-facing stair
|
||||
let mut props = StdHashMap::new();
|
||||
props.insert(
|
||||
"facing".to_string(),
|
||||
fastnbt::Value::String("north".to_string()),
|
||||
);
|
||||
props.insert(
|
||||
"half".to_string(),
|
||||
fastnbt::Value::String("top".to_string()),
|
||||
);
|
||||
let java_props = fastnbt::Value::Compound(props);
|
||||
|
||||
let bedrock = to_bedrock_block_with_properties(STONE_BRICK_STAIRS, Some(&java_props));
|
||||
|
||||
// Check weirdo_direction is set correctly (north = 3)
|
||||
assert!(matches!(
|
||||
bedrock.states.get("weirdo_direction"),
|
||||
Some(BedrockBlockStateValue::Int(3))
|
||||
));
|
||||
|
||||
// Check upside_down_bit is true for top half
|
||||
assert!(matches!(
|
||||
bedrock.states.get("upside_down_bit"),
|
||||
Some(BedrockBlockStateValue::Bool(true))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ impl Block {
|
||||
155 => "chest",
|
||||
156 => "red_carpet",
|
||||
157 => "anvil",
|
||||
158 => "jukebox",
|
||||
158 => "note_block",
|
||||
159 => "oak_door",
|
||||
160 => "brewing_stand",
|
||||
161 => "red_bed", // North head
|
||||
@@ -667,7 +667,7 @@ pub const OAK_STAIRS: Block = Block::new(144);
|
||||
pub const CHEST: Block = Block::new(155);
|
||||
pub const RED_CARPET: Block = Block::new(156);
|
||||
pub const ANVIL: Block = Block::new(157);
|
||||
pub const JUKEBOX: Block = Block::new(158);
|
||||
pub const NOTE_BLOCK: Block = Block::new(158);
|
||||
pub const OAK_DOOR: Block = Block::new(159);
|
||||
pub const BREWING_STAND: Block = Block::new(160);
|
||||
pub const RED_BED_NORTH_HEAD: Block = Block::new(161);
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -1,27 +1,72 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const MIN_Y: i32 = -64;
|
||||
|
||||
/// Generation options that can be passed separately from CLI Args
|
||||
#[derive(Clone)]
|
||||
pub struct GenerationOptions {
|
||||
pub path: PathBuf,
|
||||
pub format: WorldFormat,
|
||||
pub level_name: Option<String>,
|
||||
pub spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
pub fn generate_world(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
) -> Result<(), String> {
|
||||
let region_dir: String = format!("{}/region", args.path);
|
||||
let mut editor: WorldEditor = WorldEditor::new(®ion_dir, &xzbbox);
|
||||
// Default to Java format when called from CLI
|
||||
let options = GenerationOptions {
|
||||
path: args.path.clone(),
|
||||
format: WorldFormat::JavaAnvil,
|
||||
level_name: None,
|
||||
spawn_point: None,
|
||||
};
|
||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options).map(|_| ())
|
||||
}
|
||||
|
||||
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
||||
pub fn generate_world_with_options(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
options: GenerationOptions,
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = options.path.clone();
|
||||
let world_format = options.format;
|
||||
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
|
||||
options.path,
|
||||
&xzbbox,
|
||||
llbbox,
|
||||
options.format,
|
||||
options.level_name,
|
||||
options.spawn_point,
|
||||
);
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
// Build highway connectivity map once before processing
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(&ground);
|
||||
|
||||
@@ -63,7 +108,7 @@ pub fn generate_world(
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(&mut editor, way, args, None);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(&mut editor, way, args);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
@@ -74,12 +119,19 @@ pub fn generate_world(
|
||||
leisure::generate_leisure(&mut editor, way, args);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(&mut editor, element);
|
||||
} else if way.tags.contains_key("waterway") {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
// docks count as water areas
|
||||
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(&mut editor, way);
|
||||
}
|
||||
} else if way.tags.contains_key("bridge") {
|
||||
//bridges::generate_bridges(&mut editor, way, ground_level); // TODO FIX
|
||||
} else if way.tags.contains_key("railway") {
|
||||
railways::generate_railways(&mut editor, way);
|
||||
} else if way.tags.contains_key("roller_coaster") {
|
||||
railways::generate_roller_coaster(&mut editor, way);
|
||||
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway")
|
||||
{
|
||||
highways::generate_aeroway(&mut editor, way, args);
|
||||
@@ -101,7 +153,7 @@ pub fn generate_world(
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barrier_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(&mut editor, element, args);
|
||||
highways::generate_highways(&mut editor, element, args, &highway_connectivity);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
@@ -112,9 +164,13 @@ pub fn generate_world(
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
buildings::generate_building_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel.tags.get("natural") == Some(&"water".to_string())
|
||||
|| rel
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
water_areas::generate_water_areas(&mut editor, rel);
|
||||
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
|
||||
} else if rel.tags.contains_key("natural") {
|
||||
natural::generate_natural_from_relation(&mut editor, rel, args);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
@@ -187,6 +243,8 @@ pub fn generate_world(
|
||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
||||
|
||||
block_counter += 1;
|
||||
// Use manual % check since is_multiple_of() is unstable on stable Rust
|
||||
#[allow(clippy::manual_is_multiple_of)]
|
||||
if block_counter % batch_size == 0 {
|
||||
ground_pb.inc(batch_size);
|
||||
}
|
||||
@@ -217,30 +275,109 @@ pub fn generate_world(
|
||||
// Save world
|
||||
editor.save();
|
||||
|
||||
emit_gui_progress_update(99.0, "Finalizing world...");
|
||||
|
||||
// Update player spawn Y coordinate based on terrain height after generation
|
||||
#[cfg(feature = "gui")]
|
||||
if let Some(spawn_coords) = &args.spawn_point {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.max().lng(),
|
||||
args.bbox.max().lat()
|
||||
);
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
if let Some(spawn_coords) = &args.spawn_point {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.max().lng(),
|
||||
args.bbox.max().lat()
|
||||
);
|
||||
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
Some(*spawn_coords),
|
||||
bbox_string,
|
||||
args.scale,
|
||||
&ground,
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
Ok(())
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
emit_open_mcworld_file(path_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Information needed to generate a map preview after world generation is complete
|
||||
#[derive(Clone)]
|
||||
pub struct MapPreviewInfo {
|
||||
pub world_path: PathBuf,
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
pub world_area: i64,
|
||||
}
|
||||
|
||||
impl MapPreviewInfo {
|
||||
/// Create MapPreviewInfo from world bounds
|
||||
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
|
||||
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
|
||||
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
|
||||
Self {
|
||||
world_path,
|
||||
min_x: xzbbox.min_x(),
|
||||
max_x: xzbbox.max_x(),
|
||||
min_z: xzbbox.min_z(),
|
||||
max_z: xzbbox.max_z(),
|
||||
world_area: world_width * world_height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
|
||||
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
|
||||
|
||||
/// Start map preview generation in a background thread.
|
||||
/// This should be called AFTER the world generation is complete, the session lock is released,
|
||||
/// and the GUI has been notified of 100% completion.
|
||||
///
|
||||
/// For Java worlds only, and only if the world area is within limits.
|
||||
pub fn start_map_preview_generation(info: MapPreviewInfo) {
|
||||
if info.world_area > MAX_MAP_PREVIEW_AREA {
|
||||
return;
|
||||
}
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// Use catch_unwind to prevent any panic from affecting the application
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
map_renderer::render_world_map(
|
||||
&info.world_path,
|
||||
info.min_x,
|
||||
info.max_x,
|
||||
info.min_z,
|
||||
info.max_z,
|
||||
)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(_path)) => {
|
||||
// Notify the GUI that the map preview is ready
|
||||
emit_map_preview_ready();
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
eprintln!("Warning: Failed to generate map preview: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
eprintln!("Warning: Map preview generation panicked unexpectedly");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,13 +92,6 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
|
||||
}
|
||||
}
|
||||
}
|
||||
"vending" => {
|
||||
// Place vending machine blocks
|
||||
if let Some(pt) = first_node {
|
||||
editor.set_block(IRON_BLOCK, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(IRON_BLOCK, pt.x, 2, pt.z, None, None);
|
||||
}
|
||||
}
|
||||
"shelter" => {
|
||||
let roof_block: Block = STONE_BRICK_SLAB;
|
||||
|
||||
|
||||
@@ -5,8 +5,68 @@ use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
|
||||
/// Type alias for highway connectivity map
|
||||
pub type HighwayConnectivityMap = HashMap<(i32, i32), Vec<i32>>;
|
||||
|
||||
/// Generates highways with elevation support based on layer tags and connectivity analysis
|
||||
pub fn generate_highways(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
) {
|
||||
generate_highways_internal(editor, element, args, highway_connectivity);
|
||||
}
|
||||
|
||||
/// Build a connectivity map for highway endpoints to determine where slopes are needed.
|
||||
pub fn build_highway_connectivity_map(elements: &[ProcessedElement]) -> HighwayConnectivityMap {
|
||||
let mut connectivity_map: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
|
||||
|
||||
for element in elements {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
if way.tags.contains_key("highway") {
|
||||
let layer_value = way
|
||||
.tags
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0) for connectivity
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Add connectivity for start and end nodes
|
||||
if !way.nodes.is_empty() {
|
||||
let start_node = &way.nodes[0];
|
||||
let end_node = &way.nodes[way.nodes.len() - 1];
|
||||
|
||||
let start_coord = (start_node.x, start_node.z);
|
||||
let end_coord = (end_node.x, end_node.z);
|
||||
|
||||
connectivity_map
|
||||
.entry(start_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
connectivity_map
|
||||
.entry(end_coord)
|
||||
.or_default()
|
||||
.push(layer_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectivity_map
|
||||
}
|
||||
|
||||
/// Internal function that generates highways with connectivity context for elevation handling
|
||||
fn generate_highways_internal(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
args: &Args,
|
||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>, // Maps node coordinates to list of layers that connect to this node
|
||||
) {
|
||||
if let Some(highway_type) = element.tags().get("highway") {
|
||||
if highway_type == "street_lamp" {
|
||||
// Handle street lamps
|
||||
@@ -97,13 +157,17 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
let mut add_outline = false;
|
||||
let scale_factor = args.scale;
|
||||
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Parse the layer value for elevation calculation
|
||||
let layer_value = element
|
||||
.tags()
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0)
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Skip if 'level' is negative in the tags (indoor mapping)
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
@@ -120,10 +184,14 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
block_type = DIRT_PATH;
|
||||
block_range = 1;
|
||||
}
|
||||
"motorway" | "primary" => {
|
||||
"motorway" | "primary" | "trunk" => {
|
||||
block_range = 5;
|
||||
add_stripe = true;
|
||||
}
|
||||
"secondary" => {
|
||||
block_range = 4;
|
||||
add_stripe = true;
|
||||
}
|
||||
"tertiary" => {
|
||||
add_stripe = true;
|
||||
}
|
||||
@@ -173,7 +241,40 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
block_range = ((block_range as f64) * scale_factor).floor() as i32;
|
||||
}
|
||||
|
||||
// Calculate elevation based on layer
|
||||
const LAYER_HEIGHT_STEP: i32 = 6; // Each layer is 6 blocks higher/lower
|
||||
let base_elevation = layer_value * LAYER_HEIGHT_STEP;
|
||||
|
||||
// Check if we need slopes at start and end
|
||||
let needs_start_slope =
|
||||
should_add_slope_at_node(&way.nodes[0], layer_value, highway_connectivity);
|
||||
let needs_end_slope = should_add_slope_at_node(
|
||||
&way.nodes[way.nodes.len() - 1],
|
||||
layer_value,
|
||||
highway_connectivity,
|
||||
);
|
||||
|
||||
// Calculate total way length for slope distribution
|
||||
let total_way_length = calculate_way_length(way);
|
||||
|
||||
// Check if this is a short isolated elevated segment - if so, treat as ground level
|
||||
let is_short_isolated_elevated =
|
||||
needs_start_slope && needs_end_slope && layer_value > 0 && total_way_length <= 35;
|
||||
|
||||
// Override elevation and slopes for short isolated segments
|
||||
let (effective_elevation, effective_start_slope, effective_end_slope) =
|
||||
if is_short_isolated_elevated {
|
||||
(0, false, false) // Treat as ground level
|
||||
} else {
|
||||
(base_elevation, needs_start_slope, needs_end_slope)
|
||||
};
|
||||
|
||||
let slope_length = (total_way_length as f32 * 0.35).clamp(15.0, 50.0) as usize; // 35% of way length, max 50 blocks, min 15 blocks
|
||||
|
||||
// Iterate over nodes to create the highway
|
||||
let mut segment_index = 0;
|
||||
let total_segments = way.nodes.len() - 1;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let (x1, z1) = prev;
|
||||
@@ -181,17 +282,30 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
let z2: i32 = node.z;
|
||||
|
||||
// Generate the line of coordinates between the two nodes
|
||||
// we don't care about the y because it's going to get overwritten
|
||||
// I'm not sure if we'll keep it this way
|
||||
let bresenham_points: Vec<(i32, i32, i32)> =
|
||||
bresenham_line(x1, 0, z1, x2, 0, z2);
|
||||
|
||||
// Calculate elevation for this segment
|
||||
let segment_length = bresenham_points.len();
|
||||
|
||||
// Variables to manage dashed line pattern
|
||||
let mut stripe_length: i32 = 0;
|
||||
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the solid part of the stripe
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32; // Length of the gap part of the stripe
|
||||
let dash_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
let gap_length: i32 = (5.0 * scale_factor).ceil() as i32;
|
||||
|
||||
for (point_index, (x, _, z)) in bresenham_points.iter().enumerate() {
|
||||
// Calculate Y elevation for this point based on slopes and layer
|
||||
let current_y = calculate_point_elevation(
|
||||
segment_index,
|
||||
point_index,
|
||||
segment_length,
|
||||
total_segments,
|
||||
effective_elevation,
|
||||
effective_start_slope,
|
||||
effective_end_slope,
|
||||
slope_length,
|
||||
);
|
||||
|
||||
for (x, _, z) in bresenham_points {
|
||||
// Draw the road surface for the entire width
|
||||
for dx in -block_range..=block_range {
|
||||
for dz in -block_range..=block_range {
|
||||
@@ -209,7 +323,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -218,7 +332,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
@@ -228,7 +342,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -237,7 +351,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
BLACK_CONCRETE,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
@@ -247,12 +361,38 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
block_type,
|
||||
set_x,
|
||||
0,
|
||||
current_y,
|
||||
set_z,
|
||||
None,
|
||||
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
|
||||
);
|
||||
}
|
||||
|
||||
// Add stone brick foundation underneath elevated highways for thickness
|
||||
if effective_elevation > 0 && current_y > 0 {
|
||||
// Add 1 layer of stone bricks underneath the highway surface
|
||||
editor.set_block(
|
||||
STONE_BRICKS,
|
||||
set_x,
|
||||
current_y - 1,
|
||||
set_z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Add support pillars for elevated highways
|
||||
if effective_elevation != 0 && current_y > 0 {
|
||||
add_highway_support_pillar(
|
||||
editor,
|
||||
set_x,
|
||||
current_y,
|
||||
set_z,
|
||||
dx,
|
||||
dz,
|
||||
block_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +405,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
0,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
@@ -278,7 +418,7 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
editor.set_block(
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
outline_x,
|
||||
0,
|
||||
current_y,
|
||||
outline_z,
|
||||
None,
|
||||
None,
|
||||
@@ -289,12 +429,12 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
// Add a dashed white line in the middle for larger roads
|
||||
if add_stripe {
|
||||
if stripe_length < dash_length {
|
||||
let stripe_x: i32 = x;
|
||||
let stripe_z: i32 = z;
|
||||
let stripe_x: i32 = *x;
|
||||
let stripe_z: i32 = *z;
|
||||
editor.set_block(
|
||||
WHITE_CONCRETE,
|
||||
stripe_x,
|
||||
0,
|
||||
current_y,
|
||||
stripe_z,
|
||||
Some(&[BLACK_CONCRETE]),
|
||||
None,
|
||||
@@ -308,6 +448,8 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segment_index += 1;
|
||||
}
|
||||
previous_node = Some((node.x, node.z));
|
||||
}
|
||||
@@ -315,6 +457,131 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to determine if a slope should be added at a specific node
|
||||
fn should_add_slope_at_node(
|
||||
node: &crate::osm_parser::ProcessedNode,
|
||||
current_layer: i32,
|
||||
highway_connectivity: &HashMap<(i32, i32), Vec<i32>>,
|
||||
) -> bool {
|
||||
let node_coord = (node.x, node.z);
|
||||
|
||||
// If we don't have connectivity information, always add slopes for non-zero layers
|
||||
if highway_connectivity.is_empty() {
|
||||
return current_layer != 0;
|
||||
}
|
||||
|
||||
// Check if there are other highways at different layers connected to this node
|
||||
if let Some(connected_layers) = highway_connectivity.get(&node_coord) {
|
||||
// Count how many ways are at the same layer as current way
|
||||
let same_layer_count = connected_layers
|
||||
.iter()
|
||||
.filter(|&&layer| layer == current_layer)
|
||||
.count();
|
||||
|
||||
// If this is the only way at this layer connecting to this node, we need a slope
|
||||
// (unless we're at ground level and connecting to ground level ways)
|
||||
if same_layer_count <= 1 {
|
||||
return current_layer != 0;
|
||||
}
|
||||
|
||||
// If there are multiple ways at the same layer, don't add slope
|
||||
false
|
||||
} else {
|
||||
// No other highways connected, add slope if not at ground level
|
||||
current_layer != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to calculate the total length of a way in blocks
|
||||
fn calculate_way_length(way: &ProcessedWay) -> usize {
|
||||
let mut total_length = 0;
|
||||
let mut previous_node: Option<&crate::osm_parser::ProcessedNode> = None;
|
||||
|
||||
for node in &way.nodes {
|
||||
if let Some(prev) = previous_node {
|
||||
let dx = (node.x - prev.x).abs();
|
||||
let dz = (node.z - prev.z).abs();
|
||||
total_length += ((dx * dx + dz * dz) as f32).sqrt() as usize;
|
||||
}
|
||||
previous_node = Some(node);
|
||||
}
|
||||
|
||||
total_length
|
||||
}
|
||||
|
||||
/// Calculate the Y elevation for a specific point along the highway
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate_point_elevation(
|
||||
segment_index: usize,
|
||||
point_index: usize,
|
||||
segment_length: usize,
|
||||
total_segments: usize,
|
||||
base_elevation: i32,
|
||||
needs_start_slope: bool,
|
||||
needs_end_slope: bool,
|
||||
slope_length: usize,
|
||||
) -> i32 {
|
||||
// If no slopes needed, return base elevation
|
||||
if !needs_start_slope && !needs_end_slope {
|
||||
return base_elevation;
|
||||
}
|
||||
|
||||
// Calculate total distance from start
|
||||
let total_distance_from_start = segment_index * segment_length + point_index;
|
||||
let total_way_length = total_segments * segment_length;
|
||||
|
||||
// Ensure we have reasonable values
|
||||
if total_way_length == 0 || slope_length == 0 {
|
||||
return base_elevation;
|
||||
}
|
||||
|
||||
// Start slope calculation - gradual rise from ground level
|
||||
if needs_start_slope && total_distance_from_start <= slope_length {
|
||||
let slope_progress = total_distance_from_start as f32 / slope_length as f32;
|
||||
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||
return elevation_offset;
|
||||
}
|
||||
|
||||
// End slope calculation - gradual descent to ground level
|
||||
if needs_end_slope
|
||||
&& total_distance_from_start >= (total_way_length.saturating_sub(slope_length))
|
||||
{
|
||||
let distance_from_end = total_way_length - total_distance_from_start;
|
||||
let slope_progress = distance_from_end as f32 / slope_length as f32;
|
||||
let elevation_offset = (base_elevation as f32 * slope_progress) as i32;
|
||||
return elevation_offset;
|
||||
}
|
||||
|
||||
// Middle section at full elevation
|
||||
base_elevation
|
||||
}
|
||||
|
||||
/// Add support pillars for elevated highways
|
||||
fn add_highway_support_pillar(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
highway_y: i32,
|
||||
z: i32,
|
||||
dx: i32,
|
||||
dz: i32,
|
||||
_block_range: i32, // Keep for future use
|
||||
) {
|
||||
// Only add pillars at specific intervals and positions
|
||||
if dx == 0 && dz == 0 && (x + z) % 8 == 0 {
|
||||
// Add pillar from ground to highway level
|
||||
for y in 1..highway_y {
|
||||
editor.set_block(STONE_BRICKS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Add pillar base
|
||||
for base_dx in -1..=1 {
|
||||
for base_dz in -1..=1 {
|
||||
editor.set_block(STONE_BRICKS, x + base_dx, 0, z + base_dz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a siding using stone brick slabs
|
||||
pub fn generate_siding(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
let mut previous_node: Option<XZPoint> = None;
|
||||
|
||||
@@ -18,6 +18,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
|
||||
"park" | "nature_reserve" | "garden" | "disc_golf_course" | "golf_course" => {
|
||||
GRASS_BLOCK
|
||||
}
|
||||
"schoolyard" => BLACK_CONCRETE,
|
||||
"playground" | "recreation_ground" | "pitch" | "beach_resort" | "dog_park" => {
|
||||
if let Some(surface) = element.tags.get("surface") {
|
||||
match surface.as_str() {
|
||||
|
||||
@@ -25,6 +25,7 @@ pub fn generate_man_made(editor: &mut WorldEditor, element: &ProcessedElement, _
|
||||
"chimney" => generate_chimney(editor, element),
|
||||
"water_well" => generate_water_well(editor, element),
|
||||
"water_tower" => generate_water_tower(editor, element),
|
||||
"mast" => generate_antenna(editor, element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
@@ -96,7 +97,6 @@ fn generate_antenna(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
Some(h) => h.parse::<i32>().unwrap_or(20).min(40), // Max 40 blocks
|
||||
None => match element.tags().get("tower:type").map(|s| s.as_str()) {
|
||||
Some("communication") => 20,
|
||||
Some("transmission") => 25,
|
||||
Some("cellular") => 15,
|
||||
_ => 20,
|
||||
},
|
||||
@@ -249,6 +249,7 @@ pub fn generate_man_made_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
"chimney" => generate_chimney(editor, &element),
|
||||
"water_well" => generate_water_well(editor, &element),
|
||||
"water_tower" => generate_water_tower(editor, &element),
|
||||
"mast" => generate_antenna(editor, &element),
|
||||
_ => {} // Unknown man_made type, ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,3 +174,71 @@ fn determine_rail_direction(
|
||||
(None, None) => RAIL_NORTH_SOUTH,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_roller_coaster(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
if let Some(roller_coaster) = element.tags.get("roller_coaster") {
|
||||
if roller_coaster == "track" {
|
||||
// Check if it's indoor (skip if yes)
|
||||
if let Some(indoor) = element.tags.get("indoor") {
|
||||
if indoor == "yes" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if layer is negative (skip if yes)
|
||||
if let Some(layer) = element.tags.get("layer") {
|
||||
if let Ok(layer_value) = layer.parse::<i32>() {
|
||||
if layer_value < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elevation_height = 4; // 4 blocks in the air
|
||||
let pillar_interval = 6; // Support pillars every 6 blocks
|
||||
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev_node = element.nodes[i - 1].xz();
|
||||
let cur_node = element.nodes[i].xz();
|
||||
|
||||
let points = bresenham_line(prev_node.x, 0, prev_node.z, cur_node.x, 0, cur_node.z);
|
||||
let smoothed_points = smooth_diagonal_rails(&points);
|
||||
|
||||
for j in 0..smoothed_points.len() {
|
||||
let (bx, _, bz) = smoothed_points[j];
|
||||
|
||||
// Place track foundation at elevation height
|
||||
editor.set_block(IRON_BLOCK, bx, elevation_height, bz, None, None);
|
||||
|
||||
let prev = if j > 0 {
|
||||
Some(smoothed_points[j - 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next = if j < smoothed_points.len() - 1 {
|
||||
Some(smoothed_points[j + 1])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rail_block = determine_rail_direction(
|
||||
(bx, bz),
|
||||
prev.map(|(x, _, z)| (x, z)),
|
||||
next.map(|(x, _, z)| (x, z)),
|
||||
);
|
||||
|
||||
// Place rail on top of the foundation
|
||||
editor.set_block(rail_block, bx, elevation_height + 1, bz, None, None);
|
||||
|
||||
// Place support pillars every pillar_interval blocks
|
||||
if bx % pillar_interval == 0 && bz % pillar_interval == 0 {
|
||||
// Create a pillar from ground level up to the track
|
||||
for y in 1..elevation_height {
|
||||
editor.set_block(IRON_BLOCK, bx, y, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
'6' => Some(RED_BED_SOUTH_FOOT), // Bed South Foot
|
||||
'7' => Some(RED_BED_WEST_HEAD), // Bed West Head
|
||||
'8' => Some(RED_BED_WEST_FOOT), // Bed West Foot
|
||||
'H' => Some(CHEST), // Chest
|
||||
// 'H' => Some(CHEST), // Chest
|
||||
'L' => Some(CAULDRON), // Cauldron
|
||||
'A' => Some(ANVIL), // Anvil
|
||||
'P' => Some(OAK_PRESSURE_PLATE), // Pressure Plate
|
||||
@@ -145,7 +145,7 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(JUKEBOX), // Jukebox
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
|
||||
@@ -307,7 +307,7 @@ impl Tree<'_> {
|
||||
FURNACE,
|
||||
ANVIL,
|
||||
BREWING_STAND,
|
||||
JUKEBOX,
|
||||
NOTE_BLOCK,
|
||||
BOOKSHELF,
|
||||
CAULDRON,
|
||||
// Beds
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
coordinate_system::cartesian::XZPoint,
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation},
|
||||
coordinate_system::cartesian::{XZBBox, XZPoint},
|
||||
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
|
||||
world_editor::WorldEditor,
|
||||
};
|
||||
|
||||
pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelation) {
|
||||
pub fn generate_water_area_from_way(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
_xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let outers = [element.nodes.clone()];
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping way {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &[], start_time);
|
||||
}
|
||||
|
||||
pub fn generate_water_areas_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedRelation,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Check if this is a water relation (either with water tag or natural=water)
|
||||
let is_water = element.tags.contains_key("water")
|
||||
|| element.tags.get("natural") == Some(&"water".to_string());
|
||||
|| element
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_water {
|
||||
return;
|
||||
@@ -36,79 +62,135 @@ pub fn generate_water_areas(editor: &mut WorldEditor, element: &ProcessedRelatio
|
||||
}
|
||||
}
|
||||
|
||||
// Process each outer polygon individually
|
||||
for (i, outer_nodes) in outers.iter().enumerate() {
|
||||
let mut individual_outers = vec![outer_nodes.clone()];
|
||||
// Preserve OSM-defined outer/inner roles without modification
|
||||
merge_way_segments(&mut outers);
|
||||
|
||||
merge_loopy_loops(&mut individual_outers);
|
||||
if !verify_loopy_loops(&individual_outers) {
|
||||
println!(
|
||||
"Skipping invalid outer polygon {} for relation {}",
|
||||
i + 1,
|
||||
element.id
|
||||
);
|
||||
continue; // Skip this outer if it's not valid
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
merge_loopy_loops(&mut inners);
|
||||
if !verify_loopy_loops(&inners) {
|
||||
// If inners are invalid, process outer without inners
|
||||
let empty_inners: Vec<Vec<ProcessedNode>> = vec![];
|
||||
let mut temp_inners = empty_inners;
|
||||
merge_loopy_loops(&mut temp_inners);
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let empty_inners_xz: Vec<Vec<XZPoint>> = vec![];
|
||||
|
||||
inverse_floodfill(
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
individual_outers_xz,
|
||||
empty_inners_xz,
|
||||
editor,
|
||||
start_time,
|
||||
);
|
||||
continue;
|
||||
// If no valid outer loops remain, skip the relation
|
||||
if outers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
let individual_outers_xz: Vec<Vec<XZPoint>> = individual_outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let inners_xz: Vec<Vec<XZPoint>> = inners
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(
|
||||
min_x,
|
||||
min_z,
|
||||
max_x,
|
||||
max_z,
|
||||
individual_outers_xz,
|
||||
inners_xz,
|
||||
editor,
|
||||
start_time,
|
||||
);
|
||||
// Verify again after filtering and closing
|
||||
if !verify_closed_rings(&outers) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
merge_way_segments(&mut inners);
|
||||
if !verify_closed_rings(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
}
|
||||
|
||||
generate_water_areas(editor, &outers, &inners, start_time);
|
||||
}
|
||||
|
||||
// Merges ways that share nodes into full loops
|
||||
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
|
||||
fn generate_water_areas(
|
||||
editor: &mut WorldEditor,
|
||||
outers: &[Vec<ProcessedNode>],
|
||||
inners: &[Vec<ProcessedNode>],
|
||||
start_time: Instant,
|
||||
) {
|
||||
// Calculate polygon bounding box to limit fill area
|
||||
let mut poly_min_x = i32::MAX;
|
||||
let mut poly_min_z = i32::MAX;
|
||||
let mut poly_max_x = i32::MIN;
|
||||
let mut poly_max_z = i32::MIN;
|
||||
|
||||
for outer in outers {
|
||||
for node in outer {
|
||||
poly_min_x = poly_min_x.min(node.x);
|
||||
poly_min_z = poly_min_z.min(node.z);
|
||||
poly_max_x = poly_max_x.max(node.x);
|
||||
poly_max_z = poly_max_z.max(node.z);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid bounds, nothing to fill
|
||||
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp to world bounds just in case
|
||||
let (world_min_x, world_min_z) = editor.get_min_coords();
|
||||
let (world_max_x, world_max_z) = editor.get_max_coords();
|
||||
let min_x = poly_min_x.max(world_min_x);
|
||||
let min_z = poly_min_z.max(world_min_z);
|
||||
let max_x = poly_max_x.min(world_max_x);
|
||||
let max_z = poly_max_z.min(world_max_z);
|
||||
|
||||
let outers_xz: Vec<Vec<XZPoint>> = outers
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
let inners_xz: Vec<Vec<XZPoint>> = inners
|
||||
.iter()
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
inverse_floodfill(
|
||||
min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor, start_time,
|
||||
);
|
||||
}
|
||||
|
||||
/// Merges 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;
|
||||
}
|
||||
@@ -117,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);
|
||||
|
||||
@@ -138,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);
|
||||
|
||||
@@ -146,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);
|
||||
|
||||
@@ -154,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);
|
||||
|
||||
@@ -169,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;
|
||||
}
|
||||
}
|
||||
@@ -208,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| {
|
||||
@@ -219,6 +322,7 @@ fn inverse_floodfill(
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -233,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;
|
||||
|
||||
@@ -44,6 +46,28 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
|
||||
(x, y)
|
||||
}
|
||||
|
||||
/// Downloads a tile from AWS Terrain Tiles service
|
||||
fn download_tile(
|
||||
client: &reqwest::blocking::Client,
|
||||
tile_x: u32,
|
||||
tile_y: u32,
|
||||
zoom: u8,
|
||||
tile_path: &Path,
|
||||
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
Ok(img.to_rgb8())
|
||||
}
|
||||
|
||||
pub fn fetch_elevation_data(
|
||||
bbox: &LLBBox,
|
||||
scale: f64,
|
||||
@@ -80,26 +104,76 @@ pub fn fetch_elevation_data(
|
||||
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
|
||||
|
||||
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
let img: image::DynamicImage = image::open(&tile_path)?;
|
||||
img.to_rgb8()
|
||||
} else {
|
||||
// AWS Terrain Tiles don't require an API key
|
||||
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
|
||||
let url: String = AWS_TERRARIUM_URL
|
||||
.replace("{z}", &zoom.to_string())
|
||||
.replace("{x}", &tile_x.to_string())
|
||||
.replace("{y}", &tile_y.to_string());
|
||||
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
|
||||
let file_size = match std::fs::metadata(&tile_path) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
|
||||
let response: reqwest::blocking::Response = client.get(&url).send()?;
|
||||
response.error_for_status_ref()?;
|
||||
let bytes = response.bytes()?;
|
||||
std::fs::write(&tile_path, &bytes)?;
|
||||
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
|
||||
img.to_rgb8()
|
||||
if file_size < 1000 {
|
||||
eprintln!(
|
||||
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
|
||||
tile_path.display(),
|
||||
file_size
|
||||
);
|
||||
|
||||
// Remove the potentially corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during refetching.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
} else {
|
||||
println!(
|
||||
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
|
||||
tile_path.display()
|
||||
);
|
||||
|
||||
// Try to load cached tile, but handle corruption gracefully
|
||||
match image::open(&tile_path) {
|
||||
Ok(img) => img.to_rgb8(),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
|
||||
tile_path.display(),
|
||||
e
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Cached tile is corrupted or invalid. Re-downloading...",
|
||||
);
|
||||
|
||||
// Remove the corrupted file
|
||||
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
|
||||
eprintln!(
|
||||
"Warning: Failed to remove corrupted tile file: {}",
|
||||
remove_err
|
||||
);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to remove corrupted tile file during re-download.",
|
||||
);
|
||||
}
|
||||
|
||||
// Re-download the tile
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Download the tile for the first time
|
||||
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
|
||||
};
|
||||
|
||||
// Only process pixels that fall within the requested bbox
|
||||
@@ -172,25 +246,26 @@ pub fn fetch_elevation_data(
|
||||
filter_elevation_outliers(&mut height_grid);
|
||||
|
||||
// Calculate blur sigma based on grid resolution
|
||||
// Reference points for tuning:
|
||||
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
|
||||
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
|
||||
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
|
||||
const LARGE_SIGMA_REF: f64 = 7.0; // Sigma for 1000x1000 grid
|
||||
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
|
||||
// This prevents larger generation areas from appearing noisier than smaller ones.
|
||||
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
|
||||
const BASE_GRID_REF: f64 = 100.0;
|
||||
const BASE_SIGMA_REF: f64 = 5.0;
|
||||
|
||||
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
|
||||
|
||||
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
|
||||
// Linear scaling for small grids
|
||||
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
|
||||
} else {
|
||||
// Logarithmic scaling for larger grids
|
||||
let ln_small: f64 = SMALL_GRID_REF.ln();
|
||||
let ln_large: f64 = LARGE_GRID_REF.ln();
|
||||
let log_grid_size: f64 = grid_size.ln();
|
||||
let t: f64 = (log_grid_size - ln_small) / (ln_large - ln_small);
|
||||
SMALL_SIGMA_REF + t * (LARGE_SIGMA_REF - SMALL_SIGMA_REF)
|
||||
};
|
||||
// Sqrt scaling provides a good balance:
|
||||
// - 100x100: sigma = 5 (5% relative)
|
||||
// - 500x500: sigma ≈ 11.2 (2.2% relative)
|
||||
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
|
||||
// This smooths terrain proportionally while preserving more detail.
|
||||
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
|
||||
|
||||
let blur_percentage: f64 = (sigma / grid_size) * 100.0;
|
||||
eprintln!(
|
||||
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
|
||||
grid_width, grid_height, sigma, blur_percentage
|
||||
);
|
||||
|
||||
/* eprintln!(
|
||||
"Grid: {}x{}, Blur sigma: {:.2}",
|
||||
|
||||
@@ -71,7 +71,7 @@ fn optimized_flood_fill_area(
|
||||
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Fast timeout check - only every few iterations
|
||||
// Fast timeout check, only every few iterations
|
||||
if filled_area.len() % 100 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if start_time.elapsed() > *timeout {
|
||||
@@ -160,11 +160,10 @@ fn original_flood_fill_area(
|
||||
for z in (min_z..=max_z).step_by(step_z as usize) {
|
||||
for x in (min_x..=max_x).step_by(step_x as usize) {
|
||||
// Reduced timeout checking frequency for better performance
|
||||
if global_visited.len() % 200 == 0 {
|
||||
if let Some(timeout) = timeout {
|
||||
if &start_time.elapsed() > timeout {
|
||||
return filled_area;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,22 @@ impl Ground {
|
||||
}
|
||||
|
||||
pub fn new_enabled(bbox: &LLBBox, scale: f64, ground_level: i32) -> Self {
|
||||
let elevation_data = fetch_elevation_data(bbox, scale, ground_level)
|
||||
.expect("Failed to fetch elevation data");
|
||||
Self {
|
||||
elevation_enabled: true,
|
||||
ground_level,
|
||||
elevation_data: Some(elevation_data),
|
||||
match fetch_elevation_data(bbox, scale, ground_level) {
|
||||
Ok(elevation_data) => Self {
|
||||
elevation_enabled: true,
|
||||
ground_level,
|
||||
elevation_data: Some(elevation_data),
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch elevation data: {}", e);
|
||||
emit_gui_progress_update(15.0, "Elevation unavailable, using flat ground");
|
||||
// Graceful fallback: disable elevation and keep provided ground_level
|
||||
Self {
|
||||
elevation_enabled: false,
|
||||
ground_level,
|
||||
elevation_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
359
src/gui.rs
@@ -1,21 +1,25 @@
|
||||
use crate::args::Args;
|
||||
use crate::coordinate_system::cartesian::XZPoint;
|
||||
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
|
||||
use crate::data_processing;
|
||||
use crate::coordinate_system::transformation::CoordTransformer;
|
||||
use crate::data_processing::{self, GenerationOptions};
|
||||
use crate::ground::{self, Ground};
|
||||
use crate::map_transformation;
|
||||
use crate::osm_parser;
|
||||
use crate::progress;
|
||||
use crate::progress::{self, emit_gui_progress_update};
|
||||
use crate::retrieve_data;
|
||||
use crate::telemetry::{self, send_log, LogLevel};
|
||||
use crate::version_check;
|
||||
use crate::world_editor::WorldFormat;
|
||||
use colored::Colorize;
|
||||
use fastnbt::Value;
|
||||
use flate2::read::GzDecoder;
|
||||
use fs2::FileExt;
|
||||
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
|
||||
@@ -58,16 +62,23 @@ impl Drop for SessionLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the area name for a given bounding box using the center point
|
||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
||||
|
||||
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => "Unknown Location".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_gui() {
|
||||
// Launch the UI
|
||||
println!("Launching UI...");
|
||||
|
||||
// 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
|
||||
@@ -102,7 +113,9 @@ pub fn run_gui() {
|
||||
gui_select_world,
|
||||
gui_start_generation,
|
||||
gui_get_version,
|
||||
gui_check_for_updates
|
||||
gui_check_for_updates,
|
||||
gui_get_world_map_data,
|
||||
gui_show_in_folder
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle();
|
||||
@@ -226,13 +239,13 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||
|
||||
// Copy the region template file
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../mcassets/region.template");
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||
fs::write(®ion_path, REGION_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||
|
||||
// Add the level.dat file
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../mcassets/level.dat");
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||
|
||||
// Decompress the gzipped level.template
|
||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||
@@ -299,7 +312,7 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||
|
||||
// Add the icon.png file
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../mcassets/icon.png");
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||
|
||||
@@ -307,52 +320,45 @@ fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
}
|
||||
|
||||
/// Adds localized area name to the world name in level.dat
|
||||
fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
let world_path = PathBuf::from(world_path_str);
|
||||
|
||||
fn add_localized_world_name(world_path: PathBuf, bbox: &LLBBox) -> PathBuf {
|
||||
// Only proceed if the path exists
|
||||
if !world_path.exists() {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Check the level.dat file first to get the current name
|
||||
let level_path = world_path.join("level.dat");
|
||||
|
||||
if !level_path.exists() {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Try to read the current world name from level.dat
|
||||
let current_name = match std::fs::read(&level_path) {
|
||||
Ok(level_data) => {
|
||||
let mut decoder = GzDecoder::new(level_data.as_slice());
|
||||
let mut decompressed_data = Vec::new();
|
||||
if decoder.read_to_end(&mut decompressed_data).is_ok() {
|
||||
if let Ok(Value::Compound(ref root)) =
|
||||
fastnbt::from_bytes::<Value>(&decompressed_data)
|
||||
{
|
||||
if let Some(Value::Compound(ref data)) = root.get("Data") {
|
||||
if let Some(Value::String(name)) = data.get("LevelName") {
|
||||
name.clone()
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
} else {
|
||||
return world_path_str.to_string();
|
||||
}
|
||||
}
|
||||
Err(_) => return world_path_str.to_string(),
|
||||
let Ok(level_data) = std::fs::read(&level_path) else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let mut decoder = GzDecoder::new(level_data.as_slice());
|
||||
let mut decompressed_data = Vec::new();
|
||||
if decoder.read_to_end(&mut decompressed_data).is_err() {
|
||||
return world_path;
|
||||
}
|
||||
|
||||
let Ok(Value::Compound(ref root)) = fastnbt::from_bytes::<Value>(&decompressed_data) else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let Some(Value::Compound(ref data)) = root.get("Data") else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
let Some(Value::String(current_name)) = data.get("LevelName") else {
|
||||
return world_path;
|
||||
};
|
||||
|
||||
// Only modify if it's an Arnis world and doesn't already have an area name
|
||||
if !current_name.starts_with("Arnis World ") || current_name.contains(": ") {
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
}
|
||||
|
||||
// Calculate center coordinates of bbox
|
||||
@@ -362,7 +368,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
// Try to fetch the area name
|
||||
let area_name = match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => return world_path_str.to_string(), // Keep original name if no area name found
|
||||
_ => return world_path, // Keep original name if no area name found
|
||||
};
|
||||
|
||||
// Create new name with localized area name, ensuring total length doesn't exceed 30 characters
|
||||
@@ -378,7 +384,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
.collect::<String>()
|
||||
} else if max_area_name_len == 0 {
|
||||
// If base name is already too long, don't add area name
|
||||
return world_path_str.to_string();
|
||||
return world_path;
|
||||
} else {
|
||||
area_name
|
||||
};
|
||||
@@ -406,6 +412,10 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
if let Ok(compressed_data) = encoder.finish() {
|
||||
if let Err(e) = std::fs::write(&level_path, compressed_data) {
|
||||
eprintln!("Failed to update level.dat with area name: {e}");
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
"Failed to update level.dat with area name",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,7 +427,7 @@ fn add_localized_world_name(world_path_str: &str, bbox: &LLBBox) -> String {
|
||||
}
|
||||
|
||||
// Return the original path since we didn't change the directory name
|
||||
world_path_str.to_string()
|
||||
world_path
|
||||
}
|
||||
|
||||
// Function to update player position in level.dat based on spawn point coordinates
|
||||
@@ -528,7 +538,7 @@ fn update_player_position(
|
||||
|
||||
// Function to update player spawn Y coordinate based on terrain height after generation
|
||||
pub fn update_player_spawn_y_after_generation(
|
||||
world_path: &str,
|
||||
world_path: &Path,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
bbox_text: String,
|
||||
scale: f64,
|
||||
@@ -668,6 +678,114 @@ fn gui_check_for_updates() -> Result<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the world map image data as base64 and geo bounds for overlay display.
|
||||
/// Returns None if the map image or metadata doesn't exist.
|
||||
#[tauri::command]
|
||||
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
|
||||
let world_dir = PathBuf::from(&world_path);
|
||||
let map_path = world_dir.join("arnis_world_map.png");
|
||||
let metadata_path = world_dir.join("metadata.json");
|
||||
|
||||
// Check if both files exist
|
||||
if !map_path.exists() || !metadata_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Read and encode the map image as base64
|
||||
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
|
||||
let base64_image =
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
|
||||
|
||||
// Read metadata
|
||||
let metadata_content =
|
||||
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
|
||||
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
|
||||
|
||||
// Extract geo bounds (metadata uses camelCase from serde)
|
||||
let min_lat = metadata["minGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLat in metadata")?;
|
||||
let max_lat = metadata["maxGeoLat"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLat in metadata")?;
|
||||
let min_lon = metadata["minGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing minGeoLon in metadata")?;
|
||||
let max_lon = metadata["maxGeoLon"]
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLon in metadata")?;
|
||||
|
||||
Ok(Some(WorldMapData {
|
||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||
min_lat,
|
||||
max_lat,
|
||||
min_lon,
|
||||
max_lon,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Data structure for world map overlay
|
||||
#[derive(serde::Serialize)]
|
||||
struct WorldMapData {
|
||||
image_base64: String,
|
||||
min_lat: f64,
|
||||
max_lat: f64,
|
||||
min_lon: f64,
|
||||
max_lon: f64,
|
||||
}
|
||||
|
||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||
#[tauri::command]
|
||||
fn gui_show_in_folder(path: String) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, try to open with default application (Minecraft Bedrock)
|
||||
// If that fails, show in Explorer
|
||||
if std::process::Command::new("cmd")
|
||||
.args(["/C", "start", "", &path])
|
||||
.spawn()
|
||||
.is_err()
|
||||
{
|
||||
std::process::Command::new("explorer")
|
||||
.args(["/select,", &path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open explorer: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// On macOS, just reveal in Finder
|
||||
std::process::Command::new("open")
|
||||
.args(["-R", &path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open Finder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// On Linux, just show in file manager
|
||||
let path_parent = std::path::Path::new(&path)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
|
||||
// Try nautilus with select first, then fall back to xdg-open on parent
|
||||
if std::process::Command::new("nautilus")
|
||||
.args(["--select", &path])
|
||||
.spawn()
|
||||
.is_err()
|
||||
{
|
||||
let _ = std::process::Command::new("xdg-open")
|
||||
.arg(&path_parent)
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(unused_variables)]
|
||||
@@ -678,17 +796,28 @@ fn gui_start_generation(
|
||||
ground_level: i32,
|
||||
floodfill_timeout: u64,
|
||||
terrain_enabled: bool,
|
||||
skip_osm_objects: bool,
|
||||
interior_enabled: bool,
|
||||
roof_enabled: bool,
|
||||
fillground_enabled: bool,
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
world_format: String,
|
||||
) -> 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() {
|
||||
// Only update player position for Java worlds - Bedrock worlds don't have a pre-existing
|
||||
// level.dat to modify (the spawn point will be set when the .mcworld is created)
|
||||
if is_new_world && spawn_point.is_some() && world_format != "bedrock" {
|
||||
// Verify the spawn point is within bounds
|
||||
if let Some(coords) = spawn_point {
|
||||
let llbbox = match LLBBox::from_str(&bbox_text) {
|
||||
@@ -742,19 +871,73 @@ fn gui_start_generation(
|
||||
}
|
||||
};
|
||||
|
||||
// Add localized name to the world if user generated a new world
|
||||
let updated_world_path = if is_new_world {
|
||||
add_localized_world_name(&selected_world, &bbox)
|
||||
// Determine world format from UI selection
|
||||
let world_format = if world_format == "bedrock" {
|
||||
WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
selected_world.clone()
|
||||
WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Create an Args instance with the chosen bounding box and world directory path
|
||||
// Determine output path and level name based on format
|
||||
let (generation_path, level_name) = match world_format {
|
||||
WorldFormat::JavaAnvil => {
|
||||
// Java: use the selected world path, add localized name if new
|
||||
let updated_path = if is_new_world {
|
||||
add_localized_world_name(world_path.clone(), &bbox)
|
||||
} else {
|
||||
world_path.clone()
|
||||
};
|
||||
(updated_path, None)
|
||||
}
|
||||
WorldFormat::BedrockMcWorld => {
|
||||
// Bedrock: generate .mcworld in current directory with location-based name
|
||||
let area_name = get_area_name_for_bedrock(&bbox);
|
||||
let filename = format!("Arnis {}.mcworld", area_name);
|
||||
let lvl_name = format!("Arnis World: {}", area_name);
|
||||
let output_path = std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join(filename);
|
||||
(output_path, Some(lvl_name))
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate MC spawn coordinates from lat/lng if spawn point was provided
|
||||
let mc_spawn_point: Option<(i32, i32)> = if let Some((lat, lng)) = spawn_point {
|
||||
if let Ok(llpoint) = LLPoint::new(lat, lng) {
|
||||
if let Ok((transformer, _)) =
|
||||
CoordTransformer::llbbox_to_xzbbox(&bbox, world_scale)
|
||||
{
|
||||
let xzpoint = transformer.transform_point(llpoint);
|
||||
Some((xzpoint.x, xzpoint.z))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Create generation options
|
||||
let generation_options = GenerationOptions {
|
||||
path: generation_path.clone(),
|
||||
format: world_format,
|
||||
level_name,
|
||||
spawn_point: mc_spawn_point,
|
||||
};
|
||||
|
||||
// Create an Args instance with the chosen bounding box
|
||||
// Note: path is used for Java-specific features like spawn point update
|
||||
let args: Args = Args {
|
||||
bbox,
|
||||
file: None,
|
||||
save_json_file: None,
|
||||
path: updated_world_path,
|
||||
path: if world_format == WorldFormat::JavaAnvil {
|
||||
generation_path
|
||||
} else {
|
||||
world_path
|
||||
},
|
||||
downloader: "requests".to_string(),
|
||||
scale: world_scale,
|
||||
ground_level,
|
||||
@@ -767,7 +950,44 @@ fn gui_start_generation(
|
||||
spawn_point,
|
||||
};
|
||||
|
||||
// Run data fetch and world generation
|
||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||
if skip_osm_objects {
|
||||
// Generate ground data (terrain) for terrain-only mode
|
||||
let ground = ground::generate_ground_data(&args);
|
||||
|
||||
// Create empty parsed_elements and xzbbox for terrain-only mode
|
||||
let parsed_elements = Vec::new();
|
||||
let (_coord_transformer, xzbbox) =
|
||||
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
|
||||
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
|
||||
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Run data fetch and world generation (standard mode: objects + terrain, or objects only)
|
||||
match retrieve_data::fetch_data_from_overpass(args.bbox, args.debug, "requests", None) {
|
||||
Ok(raw_data) => {
|
||||
let (mut parsed_elements, mut xzbbox) =
|
||||
@@ -794,8 +1014,29 @@ fn gui_start_generation(
|
||||
&mut ground,
|
||||
);
|
||||
|
||||
let _ = data_processing::generate_world(parsed_elements, xzbbox, ground, &args);
|
||||
// Session lock will be automatically released when _session_lock goes out of scope
|
||||
let _ = data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox.clone(),
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
drop(_session_lock);
|
||||
emit_gui_progress_update(100.0, "Done! World generation completed.");
|
||||
println!("{}", "Done! World generation completed.".green().bold());
|
||||
|
||||
// Start map preview generation silently in background (Java only)
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
let preview_info = data_processing::MapPreviewInfo::new(
|
||||
generation_options.path.clone(),
|
||||
&xzbbox,
|
||||
);
|
||||
data_processing::start_map_preview_generation(preview_info);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
34
gui-src/css/bbox.css → 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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 418 B |
|
Before Width: | Height: | Size: 312 B After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 847 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
BIN
src/gui/css/maps/images/spritesheet-2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/gui/css/maps/images/spritesheet.png
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -158,12 +158,13 @@
|
||||
background-position: -182px -2px;
|
||||
}
|
||||
|
||||
/* Disabled states reuse same sprites; opacity indicates disabled */
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
|
||||
background-position: -212px -2px;
|
||||
background-position: -152px -2px;
|
||||
}
|
||||
|
||||
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
|
||||
background-position: -242px -2px;
|
||||
background-position: -182px -2px;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
138
gui-src/css/styles.css → src/gui/css/styles.css
vendored
@@ -63,6 +63,7 @@ a:hover {
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 5px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -79,6 +80,12 @@ a:hover {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-box {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.controls-content {
|
||||
@@ -94,6 +101,8 @@ a:hover {
|
||||
.map-container {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -213,6 +222,68 @@ button:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* World Selection Container */
|
||||
.world-selection-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.choose-world-btn {
|
||||
padding: 10px;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* World Format Toggle */
|
||||
.format-toggle-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:first-child {
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
.format-toggle-btn:last-child {
|
||||
border-radius: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:not(.format-active) {
|
||||
background-color: #3a3a3a;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.format-toggle-btn:not(.format-active):hover {
|
||||
background-color: #4a4a4a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.format-toggle-btn.format-active {
|
||||
background-color: var(--primary-accent);
|
||||
color: #0f0f0f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.format-toggle-btn.format-active:hover {
|
||||
background-color: var(--primary-accent-dark);
|
||||
}
|
||||
|
||||
/* Customization Settings */
|
||||
.modal {
|
||||
position: fixed;
|
||||
@@ -249,6 +320,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 +379,10 @@ button:hover {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#telemetry-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
.scale-slider-container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
@@ -306,7 +408,7 @@ button:hover {
|
||||
|
||||
#bbox-coords {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
@@ -353,7 +455,7 @@ button:hover {
|
||||
|
||||
.license-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.license-button {
|
||||
@@ -389,11 +491,39 @@ button:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Generation mode dropdown styling */
|
||||
.generation-mode-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
color: #0f0f0f;
|
||||
appearance: menulist;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.generation-mode-dropdown option {
|
||||
padding: 5px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.generation-mode-dropdown {
|
||||
background-color: #0f0f0f98;
|
||||
color: #ffffff;
|
||||
border: 1px solid #fecc44;
|
||||
}
|
||||
}
|
||||
|
||||
/* Language dropdown styling */
|
||||
.language-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
@@ -421,7 +551,7 @@ button:hover {
|
||||
.theme-dropdown {
|
||||
width: 100%;
|
||||
max-width: 180px;
|
||||
padding: 5px 8px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #fecc44;
|
||||
background-color: #ffffff;
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 487 B |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 556 B After Width: | Height: | Size: 556 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
66
gui-src/index.html → src/gui/index.html
vendored
@@ -14,7 +14,7 @@
|
||||
<body>
|
||||
<main class="container">
|
||||
<div class="row">
|
||||
<a href="https://github.com/louis-e/arnis" target="_blank">
|
||||
<a href="https://arnismc.com" target="_blank">
|
||||
<img src="./images/logo.png" id="arnis-logo" class="logo arnis" alt="Arnis Logo" style="width: 35%; height: auto;">
|
||||
</a>
|
||||
</div>
|
||||
@@ -37,15 +37,26 @@
|
||||
<div class="controls-content">
|
||||
<h2 data-localize="select_world">Select World</h2>
|
||||
|
||||
<!-- Updated Tooltip Structure -->
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- World Format Toggle -->
|
||||
<div class="format-toggle-container">
|
||||
<button type="button" id="format-java" class="format-toggle-btn format-active" onclick="setWorldFormat('java')">
|
||||
Java
|
||||
</button>
|
||||
<button type="button" id="format-bedrock" class="format-toggle-btn" onclick="setWorldFormat('bedrock')">
|
||||
Bedrock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
@@ -87,11 +98,15 @@
|
||||
<span class="close-button" onclick="closeSettings()">×</span>
|
||||
<h2 data-localize="customization_settings">Customization Settings</h2>
|
||||
|
||||
<!-- Terrain Toggle Button -->
|
||||
<!-- Generation Mode Dropdown -->
|
||||
<div class="settings-row">
|
||||
<label for="terrain-toggle" data-localize="terrain">Terrain</label>
|
||||
<label for="generation-mode-select" data-localize="generation_mode">Generation Mode</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="terrain-toggle" name="terrain-toggle" checked>
|
||||
<select id="generation-mode-select" name="generation-mode-select" class="generation-mode-dropdown">
|
||||
<option value="geo-terrain" data-localize="mode_geo_terrain">Objects + Terrain</option>
|
||||
<option value="geo-only" data-localize="mode_geo_only">Objects only</option>
|
||||
<option value="terrain-only" data-localize="mode_terrain_only">Terrain only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,6 +186,7 @@
|
||||
<option value="zh-CN">中文 (简体)</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="pl">Polski</option>
|
||||
<option value="lv">Latviešu</option>
|
||||
<option value="sv">Svenska</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="fi">Suomi</option>
|
||||
@@ -181,6 +197,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telemetry Consent Toggle -->
|
||||
<div class="settings-row">
|
||||
<label for="telemetry-toggle">Anonymous Crash Reports</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- License and Credits Button -->
|
||||
<div class="settings-row license-button-row">
|
||||
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
|
||||
@@ -198,6 +222,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">
|
||||
213
gui-src/js/bbox.js → src/gui/js/bbox.js
vendored
@@ -529,7 +529,7 @@ $(document).ready(function () {
|
||||
failureCount++;
|
||||
|
||||
// After a few failures, try HTTP fallback
|
||||
if (failureCount >= 3 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
|
||||
if (failureCount >= 6 && !this._httpFallbackAttempted && theme.url.startsWith('https://')) {
|
||||
console.log('HTTPS tile loading failed, attempting HTTP fallback for', themeKey);
|
||||
this._httpFallbackAttempted = true;
|
||||
|
||||
@@ -558,11 +558,208 @@ $(document).ready(function () {
|
||||
var savedTheme = localStorage.getItem('selectedTileTheme') || 'osm';
|
||||
changeTileTheme(savedTheme);
|
||||
|
||||
// Listen for theme changes from parent window (settings modal)
|
||||
// World overlay state
|
||||
var worldOverlay = null;
|
||||
var worldOverlayData = null;
|
||||
var worldOverlayEnabled = false;
|
||||
var worldPreviewAvailable = false;
|
||||
var sliderControl = null;
|
||||
|
||||
// Create the opacity slider as a proper Leaflet control
|
||||
var SliderControl = L.Control.extend({
|
||||
options: { position: 'topleft' },
|
||||
onAdd: function(map) {
|
||||
var container = L.DomUtil.create('div', 'leaflet-bar world-preview-slider-container');
|
||||
container.id = 'world-preview-slider-container';
|
||||
container.style.display = 'none';
|
||||
|
||||
var slider = L.DomUtil.create('input', 'world-preview-slider', container);
|
||||
slider.type = 'range';
|
||||
slider.min = '0';
|
||||
slider.max = '100';
|
||||
slider.value = '50';
|
||||
slider.id = 'world-preview-opacity';
|
||||
slider.title = 'Overlay Opacity';
|
||||
|
||||
L.DomEvent.on(slider, 'input', function(e) {
|
||||
if (worldOverlay) {
|
||||
worldOverlay.setOpacity(e.target.value / 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent all map interactions
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
L.DomEvent.disableScrollPropagation(container);
|
||||
L.DomEvent.on(container, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(container, 'touchstart', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'mousedown', L.DomEvent.stopPropagation);
|
||||
L.DomEvent.on(slider, 'touchstart', L.DomEvent.stopPropagation);
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to add world preview button to the draw control's edit toolbar
|
||||
function addWorldPreviewToEditToolbar() {
|
||||
// Find the edit toolbar (contains Edit layers and Delete layers buttons)
|
||||
var editToolbar = document.querySelector('.leaflet-draw-toolbar:not(.leaflet-draw-toolbar-top)');
|
||||
if (!editToolbar) {
|
||||
// Try finding by the edit/delete buttons
|
||||
var deleteBtn = document.querySelector('.leaflet-draw-edit-remove');
|
||||
if (deleteBtn) {
|
||||
editToolbar = deleteBtn.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (editToolbar) {
|
||||
// Create the preview button
|
||||
var toggleBtn = document.createElement('a');
|
||||
toggleBtn.className = 'leaflet-draw-edit-preview disabled';
|
||||
toggleBtn.href = '#';
|
||||
toggleBtn.title = 'Show World Preview (not available yet)';
|
||||
toggleBtn.id = 'world-preview-btn';
|
||||
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (worldPreviewAvailable) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
editToolbar.appendChild(toggleBtn);
|
||||
|
||||
// Add the slider control to the map
|
||||
sliderControl = new SliderControl();
|
||||
map.addControl(sliderControl);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle world overlay function
|
||||
function toggleWorldOverlay() {
|
||||
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||
|
||||
worldOverlayEnabled = !worldOverlayEnabled;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
|
||||
if (worldOverlayEnabled) {
|
||||
// Show overlay
|
||||
var data = worldOverlayData;
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
}
|
||||
|
||||
var opacity = document.getElementById('world-preview-opacity');
|
||||
var opacityValue = opacity ? opacity.value / 100 : 0.5;
|
||||
|
||||
worldOverlay = L.imageOverlay(data.image_base64, bounds, {
|
||||
opacity: opacityValue,
|
||||
interactive: false,
|
||||
zIndex: 500
|
||||
});
|
||||
worldOverlay.addTo(map);
|
||||
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
btn.title = 'Hide World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
// Hide overlay
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
if (btn) {
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the preview button when data is available
|
||||
function enableWorldPreview(data) {
|
||||
worldOverlayData = data;
|
||||
worldPreviewAvailable = true;
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
if (btn) {
|
||||
btn.classList.remove('disabled');
|
||||
btn.title = 'Show World Preview';
|
||||
}
|
||||
}
|
||||
|
||||
// Disable and reset preview (when world changes)
|
||||
function disableWorldPreview() {
|
||||
worldPreviewAvailable = false;
|
||||
worldOverlayData = null;
|
||||
worldOverlayEnabled = false;
|
||||
|
||||
if (worldOverlay) {
|
||||
map.removeLayer(worldOverlay);
|
||||
worldOverlay = null;
|
||||
}
|
||||
|
||||
var btn = document.getElementById('world-preview-btn');
|
||||
var sliderContainer = document.getElementById('world-preview-slider-container');
|
||||
if (btn) {
|
||||
btn.classList.add('disabled');
|
||||
btn.classList.remove('active');
|
||||
btn.title = 'Show World Preview (not available yet)';
|
||||
}
|
||||
if (sliderContainer) {
|
||||
sliderContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
changeTileTheme(event.data.theme);
|
||||
}
|
||||
|
||||
// Handle world preview data ready (after generation completes)
|
||||
if (event.data && event.data.type === 'worldPreviewReady') {
|
||||
enableWorldPreview(event.data.data);
|
||||
|
||||
// Auto-enable the overlay when generation completes
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle existing world map load (zoom to location and auto-enable)
|
||||
if (event.data && event.data.type === 'loadExistingWorldMap') {
|
||||
var data = event.data.data;
|
||||
enableWorldPreview(data);
|
||||
|
||||
// Calculate bounds and zoom to them
|
||||
var bounds = L.latLngBounds(
|
||||
[data.min_lat, data.min_lon],
|
||||
[data.max_lat, data.max_lon]
|
||||
);
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
|
||||
// Auto-enable the overlay
|
||||
if (!worldOverlayEnabled) {
|
||||
toggleWorldOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle world changed (disable preview)
|
||||
if (event.data && event.data.type === 'worldChanged') {
|
||||
disableWorldPreview();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the dropdown value in parent window if it exists
|
||||
@@ -652,6 +849,9 @@ $(document).ready(function () {
|
||||
}
|
||||
});
|
||||
map.addControl(drawControl);
|
||||
|
||||
// Add world preview button to the edit toolbar after drawControl is added
|
||||
addWorldPreviewToEditToolbar();
|
||||
/*
|
||||
**
|
||||
** create bounds layer
|
||||
@@ -699,6 +899,15 @@ $(document).ready(function () {
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a rectangle, remove any existing rectangles first
|
||||
if (e.layerType === 'rectangle') {
|
||||
drawnItems.eachLayer(function(layer) {
|
||||
if (layer instanceof L.Rectangle) {
|
||||
drawnItems.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a rectangle and set proper styles before adding it to the layer
|
||||
if (e.layerType === 'rectangle') {
|
||||
e.layer.setStyle({
|
||||
13
gui-src/js/license.js → src/gui/js/license.js
vendored
@@ -3,7 +3,7 @@ export const licenseText = `
|
||||
|
||||
<p>For a full list of contributors, please refer to the <a href="https://github.com/louis-e/arnis/graphs/contributors" style="color: inherit;" target="_blank">Github contributors page</a>. Logo made by nxfx21.
|
||||
|
||||
<p style="color: #ff8686;"><b>Download Arnis only from the official source:</b> <a href="https://github.com/louis-e/arnis" style="color: inherit;" target="_blank">https://github.com/louis-e/arnis/</a>. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.</p>
|
||||
<p style="color: #ff8686;"><b>Download Arnis only from the official source:</b> <a href="https://arnismc.com" style="color: inherit;" target="_blank">https://arnismc.com</a> or <a href="https://github.com/louis-e/arnis" style="color: inherit;" target="_blank">https://github.com/louis-e/arnis/</a>. Every other website providing a download and claiming to be affiliated with the project is unofficial and may be malicious.</p>
|
||||
|
||||
<p><b>Third-Party Map Data and Tile Services:</b></p>
|
||||
<p>This application uses map tiles from multiple providers, each with their own licensing requirements:</p>
|
||||
@@ -20,6 +20,17 @@ export const licenseText = `
|
||||
<b>Stadia Maps:</b><br> © <a href="https://www.stadiamaps.com/" style="color: inherit;" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" style="color: inherit;" target="_blank">OpenMapTiles</a> © OpenStreetMap contributors
|
||||
<p>Users of this software must comply with the respective licensing terms of these map data providers when using the application.</p>
|
||||
|
||||
<b>AWS Terrain Tiles:</b><br>
|
||||
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
|
||||
<br><br>
|
||||
|
||||
<b>bedrock-rs:</b><br>
|
||||
Bedrock Edition world format support uses the <a href="https://github.com/bedrock-crustaceans/bedrock-rs" style="color: inherit;" target="_blank">bedrock-rs</a> library, licensed under the Apache License 2.0.
|
||||
<br><br>
|
||||
|
||||
<p><b>Privacy Policy:</b></p>
|
||||
If you consent to telemetry data collection, please review our Privacy Policy at:
|
||||
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.
|
||||
|
||||
<p><b>License:</b></p>
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">
|
||||
264
gui-src/js/main.js → src/gui/js/main.js
vendored
@@ -20,9 +20,11 @@ window.addEventListener("DOMContentLoaded", async () => {
|
||||
setupProgressListener();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
initTelemetryConsent();
|
||||
handleBboxInput();
|
||||
const localization = await getLocalization();
|
||||
await applyLocalization(localization);
|
||||
updateFormatToggleUI(selectedWorldFormat);
|
||||
initFooter();
|
||||
await checkForUpdates();
|
||||
});
|
||||
@@ -93,6 +95,10 @@ async function applyLocalization(localization) {
|
||||
// DEPRECATED: Ground level localization removed
|
||||
// "label[data-localize='ground_level']": "ground_level",
|
||||
"label[data-localize='language']": "language",
|
||||
"label[data-localize='generation_mode']": "generation_mode",
|
||||
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
||||
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
||||
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
||||
"label[data-localize='terrain']": "terrain",
|
||||
"label[data-localize='interior']": "interior",
|
||||
"label[data-localize='roof']": "roof",
|
||||
@@ -209,6 +215,23 @@ function setupProgressListener() {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for map preview ready event from backend
|
||||
window.__TAURI__.event.listen("map-preview-ready", () => {
|
||||
console.log("Map preview ready event received");
|
||||
showWorldPreviewButton();
|
||||
});
|
||||
|
||||
// Listen for open-mcworld-file event to show the generated Bedrock world in file explorer
|
||||
window.__TAURI__.event.listen("open-mcworld-file", async (event) => {
|
||||
const filePath = event.payload;
|
||||
try {
|
||||
// Use our custom command to show the file in the system file explorer
|
||||
await invoke("gui_show_in_folder", { path: filePath });
|
||||
} catch (error) {
|
||||
console.error("Failed to show mcworld file in folder:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSettings() {
|
||||
@@ -237,6 +260,9 @@ function initSettings() {
|
||||
sliderValue.textContent = parseFloat(slider.value).toFixed(2);
|
||||
});
|
||||
|
||||
// World format toggle (Java/Bedrock)
|
||||
initWorldFormatToggle();
|
||||
|
||||
// Language selector
|
||||
const languageSelect = document.getElementById("language-select");
|
||||
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
||||
@@ -301,6 +327,20 @@ function initSettings() {
|
||||
}
|
||||
});
|
||||
|
||||
// Telemetry consent toggle
|
||||
const telemetryToggle = document.getElementById("telemetry-toggle");
|
||||
const telemetryKey = 'telemetry-consent';
|
||||
|
||||
// Load saved telemetry consent
|
||||
const savedConsent = localStorage.getItem(telemetryKey);
|
||||
telemetryToggle.checked = savedConsent === 'true';
|
||||
|
||||
// Handle telemetry consent change
|
||||
telemetryToggle.addEventListener("change", () => {
|
||||
const isEnabled = telemetryToggle.checked;
|
||||
localStorage.setItem(telemetryKey, isEnabled ? 'true' : 'false');
|
||||
});
|
||||
|
||||
|
||||
/// License and Credits
|
||||
function openLicense() {
|
||||
@@ -325,6 +365,115 @@ function initSettings() {
|
||||
window.closeLicense = closeLicense;
|
||||
}
|
||||
|
||||
// World format selection (Java/Bedrock)
|
||||
let selectedWorldFormat = 'java'; // Default to Java
|
||||
|
||||
function initWorldFormatToggle() {
|
||||
// Load saved format preference
|
||||
const savedFormat = localStorage.getItem('arnis-world-format');
|
||||
if (savedFormat && (savedFormat === 'java' || savedFormat === 'bedrock')) {
|
||||
selectedWorldFormat = savedFormat;
|
||||
}
|
||||
|
||||
// Apply the saved selection to UI
|
||||
updateFormatToggleUI(selectedWorldFormat);
|
||||
}
|
||||
|
||||
function setWorldFormat(format) {
|
||||
if (format !== 'java' && format !== 'bedrock') return;
|
||||
|
||||
selectedWorldFormat = format;
|
||||
localStorage.setItem('arnis-world-format', format);
|
||||
updateFormatToggleUI(format);
|
||||
}
|
||||
|
||||
function updateFormatToggleUI(format) {
|
||||
const javaBtn = document.getElementById('format-java');
|
||||
const bedrockBtn = document.getElementById('format-bedrock');
|
||||
const chooseWorldBtn = document.getElementById('choose-world-btn');
|
||||
const selectedWorldText = document.getElementById('selected-world');
|
||||
|
||||
if (format === 'java') {
|
||||
javaBtn.classList.add('format-active');
|
||||
bedrockBtn.classList.remove('format-active');
|
||||
// Enable Choose World button for Java
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = false;
|
||||
chooseWorldBtn.style.opacity = '1';
|
||||
chooseWorldBtn.style.cursor = 'pointer';
|
||||
}
|
||||
// Show default text (world was cleared when switching to Bedrock)
|
||||
if (selectedWorldText) {
|
||||
const noWorldText = window.localization?.no_world_selected || 'No world selected';
|
||||
selectedWorldText.textContent = noWorldText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
} else {
|
||||
javaBtn.classList.remove('format-active');
|
||||
bedrockBtn.classList.add('format-active');
|
||||
// Disable Choose World button for Bedrock and clear any selected world
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = true;
|
||||
chooseWorldBtn.style.opacity = '0.5';
|
||||
chooseWorldBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
// Clear world selection and show Bedrock info message
|
||||
worldPath = "";
|
||||
isNewWorld = false;
|
||||
if (selectedWorldText) {
|
||||
const bedrockText = window.localization?.bedrock_use_java || 'Use Java to select worlds';
|
||||
selectedWorldText.textContent = bedrockText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose to window for onclick handlers
|
||||
window.setWorldFormat = setWorldFormat;
|
||||
|
||||
// Telemetry consent (first run only)
|
||||
function initTelemetryConsent() {
|
||||
const key = 'telemetry-consent'; // values: 'true' | 'false'
|
||||
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");
|
||||
@@ -471,8 +620,8 @@ function normalizeLongitude(lon) {
|
||||
return ((lon + 180) % 360 + 360) % 360 - 180;
|
||||
}
|
||||
|
||||
const threshold1 = 30000000.00;
|
||||
const threshold2 = 45000000.00;
|
||||
const threshold1 = 44000000.00; // Yellow warning threshold (~6.2km x 7km)
|
||||
const threshold2 = 85000000.00; // Red error threshold (~8.7km x 9.8km)
|
||||
let selectedBBox = "";
|
||||
let mapSelectedBBox = ""; // Tracks bbox from map selection
|
||||
let customBBoxValid = false; // Tracks if custom input is valid
|
||||
@@ -529,6 +678,14 @@ async function selectWorld(generate_new_world) {
|
||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
document.getElementById('selected-world').style.color = "#fecc44";
|
||||
|
||||
// Notify that world changed (reset preview)
|
||||
notifyWorldChanged();
|
||||
|
||||
// If selecting an existing world, check for existing map data
|
||||
if (!generate_new_world) {
|
||||
await loadExistingWorldMapData();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleWorldSelectionError(error);
|
||||
@@ -537,6 +694,32 @@ async function selectWorld(generate_new_world) {
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads existing world map data if available (for existing worlds)
|
||||
* This will zoom to the location and auto-enable the preview
|
||||
*/
|
||||
async function loadExistingWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
|
||||
// Send data to the map iframe with instruction to zoom and auto-enable
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'loadExistingWorldMap',
|
||||
data: mapData
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("No existing world map data found:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles world selection errors and displays appropriate messages
|
||||
* @param {number} errorCode - Error code from the backend
|
||||
@@ -576,13 +759,17 @@ async function startGeneration() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!worldPath || worldPath === "") {
|
||||
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
|
||||
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
||||
const selectedWorld = document.getElementById('selected-world');
|
||||
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
||||
selectedWorld.style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing world preview since we're generating a new one
|
||||
notifyWorldChanged();
|
||||
|
||||
// Get the map iframe reference
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
// Get spawn point coordinates if marker exists
|
||||
@@ -595,7 +782,11 @@ async function startGeneration() {
|
||||
}
|
||||
}
|
||||
|
||||
var terrain = document.getElementById("terrain-toggle").checked;
|
||||
// Get generation mode from dropdown
|
||||
var generationMode = document.getElementById("generation-mode-select").value;
|
||||
var terrain = (generationMode === "geo-terrain" || generationMode === "terrain-only");
|
||||
var skipOsmObjects = (generationMode === "terrain-only");
|
||||
|
||||
var interior = document.getElementById("interior-toggle").checked;
|
||||
var roof = document.getElementById("roof-toggle").checked;
|
||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||
@@ -609,6 +800,9 @@ async function startGeneration() {
|
||||
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
|
||||
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
|
||||
|
||||
// Get telemetry consent (defaults to false if not set)
|
||||
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
|
||||
|
||||
// Pass the selected options to the Rust backend
|
||||
await invoke("gui_start_generation", {
|
||||
bboxText: selectedBBox,
|
||||
@@ -617,11 +811,14 @@ async function startGeneration() {
|
||||
groundLevel: ground_level,
|
||||
floodfillTimeout: floodfill_timeout,
|
||||
terrainEnabled: terrain,
|
||||
skipOsmObjects: skipOsmObjects,
|
||||
interiorEnabled: interior,
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false,
|
||||
worldFormat: selectedWorldFormat
|
||||
});
|
||||
|
||||
console.log("Generation process started.");
|
||||
@@ -631,3 +828,60 @@ async function startGeneration() {
|
||||
generationButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// World preview overlay state
|
||||
let worldPreviewEnabled = false;
|
||||
let currentWorldMapData = null;
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that world preview data is ready
|
||||
* Called when the backend emits the map-preview-ready event
|
||||
*/
|
||||
async function showWorldPreviewButton() {
|
||||
// Try to load the world map data
|
||||
await loadWorldMapData();
|
||||
|
||||
if (currentWorldMapData) {
|
||||
// Send data to the map iframe
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldPreviewReady',
|
||||
data: currentWorldMapData
|
||||
}, '*');
|
||||
console.log("World preview data sent to map iframe");
|
||||
}
|
||||
} else {
|
||||
console.warn("Map data not available yet");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the map iframe that the world has changed (reset preview)
|
||||
*/
|
||||
function notifyWorldChanged() {
|
||||
currentWorldMapData = null;
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'worldChanged'
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the world map data from the backend
|
||||
*/
|
||||
async function loadWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
console.log("World map data loaded successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load world map data:", error);
|
||||
}
|
||||
}
|
||||