Compare commits

...

195 Commits
v2.3.0 ... main

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:27:47 +00:00
Louis Erbkamm
7d86854e3c Merge pull request #640 from louis-e/reintroduce-sutherland-hodgeman
Reintroduce Sutherland-Hodgman Clipping Algorithm
2025-11-28 11:28:25 +01:00
louis-e
cddaa89d35 Fix polyline clipping for segments crossing bbox with external endpoints 2025-11-28 11:12:17 +01:00
louis-e
453845977d Fix polyline clipping for segments crossing bbox with external endpoints 2025-11-28 11:10:43 +01:00
louis-e
4e196e51bd Move sutherland hodgman into own clipping file 2025-11-28 10:57:47 +01:00
louis-e
ea4dc5dc08 Remove unused dependencies 2025-11-28 10:40:06 +01:00
louis-e
c56ff83094 Fix cargo fmt 2025-11-28 01:43:55 +01:00
louis-e
2b40a520ff Reintroduce initial Sutherland-Hodgman clipping 2025-11-28 01:43:16 +01:00
louis-e
a192be981a Fix clippy 2025-11-26 15:02:12 +01:00
louis-e
eb77bca10d Refactor code comments 2025-11-26 14:49:41 +01:00
louis-e
4a891c3603 Clean up code 2025-11-26 14:40:36 +01:00
louis-e
84adfdd931 Remove floodfill abort artifacts 2025-11-26 14:32:24 +01:00
louis-e
823b6ba052 Remove debugging changes 2025-11-26 14:22:04 +01:00
louis-e
2ba8157ec9 Remove debug logging 2025-11-26 14:15:20 +01:00
louis-e
7235ba0be9 Merge remote changes, keeping our Sutherland-Hodgman implementation 2025-11-26 13:53:53 +01:00
louis-e
dee580c564 fix: cargo fmt 2025-11-26 13:48:32 +01:00
louis-e
41fc5662e0 fix: restore performance with Sutherland-Hodgman clipping and correct water rendering
- Fix O(n*m) performance regression in highway processing by building connectivity map once
- Store unclipped ways in ways_map for proper relation member merging (merge_loopy_loops)
- Use clipped ways for standalone way processing
- Add empty vector guard in merge_loopy_loops to prevent panic
- Expose build_highway_connectivity_map as public API
- Add debug_logging module for development diagnostics
2025-11-26 13:39:29 +01:00
louis-e
ac884b8c2a Correctly clip multipolygons 2025-11-23 18:34:10 +01:00
louis-e
7a9b792bee Restore node filtering for performance without breaking water features 2025-11-23 17:34:08 +01:00
Louis Erbkamm
83e9a634e5 Update baseline time and memory in benchmark workflow 2025-11-22 14:45:51 +01:00
Louis Erbkamm
56ddea57d0 Merge pull request #638 from louis-e/telemetry-nofeatures-fix
Fix no-default-features build
2025-11-22 14:37:34 +01:00
Louis Erbkamm
430a4970f5 Merge branch 'main' into telemetry-nofeatures-fix 2025-11-22 14:30:34 +01:00
Louis Erbkamm
74fbdabaee Update benchmark command with new bounding box 2025-11-22 14:30:25 +01:00
louis-e
2643155e9a Fix no-default-features build 2025-11-22 13:50:55 +01:00
Louis Erbkamm
d45c360074 Enable pr-benchmark workflow 2025-11-22 13:44:39 +01:00
Louis Erbkamm
6277a14d22 Merge pull request #618 from louis-e/dependabot/cargo/tokio-1.48.0
build(deps): bump tokio from 1.47.0 to 1.48.0
2025-11-17 21:57:36 +01:00
Louis Erbkamm
c355f243e3 Merge pull request #619 from louis-e/dependabot/cargo/geo-0.31.0
build(deps): bump geo from 0.30.0 to 0.31.0
2025-11-17 21:57:25 +01:00
Louis Erbkamm
2c31d2659c Merge pull request #630 from louis-e/crash-telemetry
WIP: Crash telemetry collection
2025-11-17 21:56:50 +01:00
louis-e
996e06ab2c Log panic to log file 2025-11-17 21:54:48 +01:00
louis-e
e11231ad0f Fix cargo fmt 2025-11-17 21:39:38 +01:00
louis-e
9adf31121e Send error telemetry data 2025-11-17 21:30:47 +01:00
louis-e
69da18fbfb Extend telemetry by more endpoints 2025-11-17 21:28:00 +01:00
louis-e
5976cc2868 Improve responsive UI layout 2025-11-17 19:15:04 +01:00
louis-e
a85eaed835 Add telemetry toggle and link to privacy policy 2025-11-17 19:09:16 +01:00
louis-e
37c3d85672 Fix clippy errors 2025-11-17 17:01:16 +01:00
louis-e
15b698a1eb WIP: Crash telemetry collection 2025-11-17 00:10:38 +01:00
Louis Erbkamm
834ce7b51d Merge pull request #629 from louis-e/2.3.1-release-prep
Prepare release 2.3.1
2025-11-16 19:52:38 +01:00
louis-e
93b6f5ac99 Prepare release 2.3.1 2025-11-16 19:52:09 +01:00
Louis Erbkamm
77df683deb Merge pull request #628 from louis-e/terrain-only-mode
Terrain only mode
2025-11-16 19:47:08 +01:00
louis-e
9d791f4299 Fix cargo fmt 2025-11-16 19:44:20 +01:00
louis-e
a2dff0b84b Backend for terrain only mode 2025-11-16 19:40:08 +01:00
louis-e
24630351b9 UI frontend for terrain only mode 2025-11-16 19:24:10 +01:00
Louis Erbkamm
819b03e3b1 Merge pull request #627 from louis-e/floodfill-stuck-fix
Fix stuck at 70% generation
2025-11-16 18:57:43 +01:00
Louis Erbkamm
f315245637 Improve timeout checks in floodfill algorithm
Added timeout checks in inner loops for better performance.
2025-11-16 18:56:05 +01:00
Louis Erbkamm
16c9f3c3bf Merge branch 'main' into floodfill-stuck-fix 2025-11-16 18:51:58 +01:00
louis-e
3043ca6d24 Fix stuck at 70% generation 2025-11-16 18:50:59 +01:00
Louis Erbkamm
3657878f01 Merge pull request #626 from louis-e/ground_crash_fix
Fix ground crash
2025-11-16 17:44:21 +01:00
louis-e
44cc57c3dd Add arnis website references 2025-11-16 17:43:11 +01:00
louis-e
408caa9176 Improve logging 2025-11-16 17:42:57 +01:00
louis-e
3191a3676d Fix src\ground.rs:27 panic crash 2025-11-16 17:42:26 +01:00
dependabot[bot]
8fff2d2fb5 build(deps): bump geo from 0.30.0 to 0.31.0
Bumps [geo](https://github.com/georust/geo) from 0.30.0 to 0.31.0.
- [Changelog](https://github.com/georust/geo/blob/main/CHANGES.md)
- [Commits](https://github.com/georust/geo/compare/geo-0.30.0...geo-0.31.0)

---
updated-dependencies:
- dependency-name: geo
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 02:13:26 +00:00
dependabot[bot]
8c702a36ff build(deps): bump tokio from 1.47.0 to 1.48.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.47.0...tokio-1.48.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-01 02:13:08 +00:00
Louis Erbkamm
9bedca071a Revise official website links in README
Updated official project website information in README.
2025-10-24 19:57:32 +02:00
Louis Erbkamm
d611837746 Merge pull request #592 from louis-e/dependabot/cargo/fastnbt-2.6.0
build(deps): bump fastnbt from 2.5.0 to 2.6.0
2025-10-02 13:13:03 +02:00
Louis Erbkamm
e06fcaf7a2 Merge pull request #590 from louis-e/dependabot/cargo/tauri-2.8.5
build(deps): bump tauri from 2.7.0 to 2.8.5
2025-10-02 13:10:06 +02:00
Louis Erbkamm
1f2772f052 Merge pull request #589 from louis-e/dependabot/cargo/tempfile-3.23.0
build(deps): bump tempfile from 3.21.0 to 3.23.0
2025-10-02 13:08:37 +02:00
Louis Erbkamm
82f3460043 Merge pull request #586 from scd31/fix-rivers
Fix rivers
2025-10-02 13:05:28 +02:00
dependabot[bot]
65d6bb6c99 build(deps): bump fastnbt from 2.5.0 to 2.6.0
Bumps [fastnbt](https://github.com/owengage/fastnbt) from 2.5.0 to 2.6.0.
- [Commits](https://github.com/owengage/fastnbt/compare/fastnbt-2.5.0...fastnbt-2.6.0)

---
updated-dependencies:
- dependency-name: fastnbt
  dependency-version: 2.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:15:16 +00:00
dependabot[bot]
7ebee82982 build(deps): bump tauri from 2.7.0 to 2.8.5
Bumps [tauri](https://github.com/tauri-apps/tauri) from 2.7.0 to 2.8.5.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.7.0...tauri-v2.8.5)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.8.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:14:36 +00:00
dependabot[bot]
5056e83dff build(deps): bump tempfile from 3.21.0 to 3.23.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.21.0 to 3.23.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.21.0...v3.23.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 02:14:14 +00:00
Stephen D
3cb0b994e1 handle bays 2025-09-29 18:54:56 -04:00
Stephen D
5ded3c961e fix docks 2025-09-29 18:54:56 -04:00
Stephen D
893c14bff8 fix rivers 2025-09-29 18:54:56 -04:00
Louis Erbkamm
4f8e0020e3 Merge pull request #582 from louis-e/manual_is_multiple_of
Fix indentation
2025-09-27 15:14:43 +02:00
Louis Erbkamm
456018abf7 Fix indentation
Reduced timeout checking frequency for better performance.
2025-09-27 15:14:15 +02:00
Louis Erbkamm
175bdc4582 Merge pull request #581 from louis-e/manual_is_multiple_of
Use manual % check since is_multiple_of() is unstable on stable Rust
2025-09-27 15:00:20 +02:00
Louis Erbkamm
ed294a3973 Optimize timeout checking in floodfill algorithm
Reduced timeout checking frequency for better performance and added a manual check for stability.
2025-09-27 14:59:55 +02:00
Louis Erbkamm
6c966fc9cc Implement manual % check for block counter
Add manual check for block counter multiple of batch size
2025-09-27 14:59:10 +02:00
Louis Erbkamm
ecfc6634fc Add rustfmt component to Rust toolchain setup 2025-09-27 14:49:55 +02:00
Louis Erbkamm
a336ccd1aa Merge pull request #579 from louis-e/disable-benchmark
Temporarily disable benchmark workflow
2025-09-25 21:10:20 +02:00
louis-e
f702a41af1 Temporarily disable benchmark workflow 2025-09-25 21:09:57 +02:00
Louis Erbkamm
29c4dc6d7c Merge pull request #561 from louis-e/elevated-highways-layer-support 2025-09-13 20:44:42 +02:00
Louis Erbkamm
f60b341eca Merge branch 'main' into elevated-highways-layer-support 2025-09-13 20:39:34 +02:00
louis-e
92c5e52f46 Disable negative layer highways 2025-09-13 20:39:10 +02:00
Louis Erbkamm
e4d7dd15c2 Merge pull request #566 from louis-e/root-dir-cleanup
Clean up root directory
2025-09-13 16:55:58 +02:00
Louis Erbkamm
cc6748115b Merge branch 'main' into root-dir-cleanup 2025-09-13 16:55:50 +02:00
louis-e
ce9496aea5 Link images as relative paths 2025-09-13 16:54:37 +02:00
louis-e
4c87eb8141 Use include_str instead of read_to_string 2025-09-13 16:52:14 +02:00
louis-e
ada475f73e Remove asset suffix from asset/ subdirectories 2025-09-13 16:51:50 +02:00
Louis Erbkamm
e5a82ba526 Merge pull request #563 from scd31/metadata
Metadata
2025-09-13 16:46:25 +02:00
Louis Erbkamm
309ac19b09 Fix cargo fmt in save_metadata function 2025-09-13 16:42:46 +02:00
Louis Erbkamm
90df7688df Implement error handling for saving world metadata 2025-09-13 16:38:56 +02:00
Louis Erbkamm
5b3889a7bb Add Latvian language option to dropdown 2025-09-13 15:20:32 +02:00
Louis Erbkamm
67deb739e6 Merge branch 'main' into metadata 2025-09-13 15:17:24 +02:00
Louis Erbkamm
7c08d21f36 Merge pull request #533 from scd31/nix
Nix development fix/improvements
2025-09-13 15:16:57 +02:00
louis-e
280acc7a8a Move example_transformations.json to tests/ directory 2025-09-13 14:24:11 +02:00
louis-e
ee59da5d9b Clean up root dir by combining assets 2025-09-13 14:19:59 +02:00
louis-e
b772cb6ab9 Move gui-src/ to src/gui/ 2025-09-13 14:16:55 +02:00
Louis Erbkamm
2646947ed0 Merge pull request #536 from freeutka/main
Add latvian language
2025-09-13 14:12:59 +02:00
Louis Erbkamm
b9976fd562 Merge pull request #534 from scd31/dynmap-fix
fix dynmap, and maybe other things
2025-09-13 11:12:14 +02:00
Louis Erbkamm
a621703da4 Refactor data check to use is_empty method 2025-09-13 11:01:38 +02:00
Louis Erbkamm
87efd02c74 Merge branch 'main' into dynmap-fix 2025-09-13 10:58:02 +02:00
Louis Erbkamm
6a6b58fd8f Fix block property insertion in world_editor 2025-09-13 10:57:45 +02:00
Stephen D
855c6fe846 switch to mold linker for performance 2025-09-07 19:03:48 -04:00
Stephen D
1592951fe3 world metadata 2025-09-07 16:06:58 -04:00
louis-e
e267e04350 Fix cargo fmt and clippy 2025-09-07 21:39:03 +02:00
louis-e
c2e8d5959f feat: add elevated highways with layer-based elevation 2025-09-07 18:55:42 +02:00
Stephen D
ffe8f865d2 initial cleanup 2025-09-07 11:02:25 -04:00
Louis Erbkamm
4f9f4f127a Merge pull request #558 from louis-e/interior-entityblock-removal
Replace entity blocks in building interior generation for lag reduction
2025-09-03 16:26:09 +02:00
louis-e
014208426b Replace entity blocks in building interior generation for lag reduction 2025-09-03 16:25:22 +02:00
Louis Erbkamm
6d848ef7cd Merge pull request #557 from louis-e/rollercoasters
Add rollercoasters
2025-09-03 16:23:33 +02:00
louis-e
1e25dfea37 Add rollercoasters 2025-09-03 16:13:47 +02:00
Louis Erbkamm
903efec459 Merge pull request #556 from louis-e/elevation-tilecache-crash
Implement faulty elevation tile cache detection crash
2025-09-03 15:48:27 +02:00
Louis Erbkamm
22b3969c72 Merge branch 'main' into elevation-tilecache-crash 2025-09-03 15:41:15 +02:00
Louis Erbkamm
0f62c4283b Remove clear_tile_cache function and improve error logging
Removed the clear_tile_cache function and updated error handling for file removal.
2025-09-03 15:40:19 +02:00
Louis Erbkamm
4299e64cba Merge pull request #549 from louis-e/dependabot/cargo/semver-1.0.26
build(deps): bump semver from 1.0.24 to 1.0.26
2025-09-03 15:38:20 +02:00
Louis Erbkamm
0cd386b399 Merge pull request #550 from louis-e/dependabot/cargo/tempfile-3.21.0
build(deps): bump tempfile from 3.20.0 to 3.21.0
2025-09-03 15:38:12 +02:00
Louis Erbkamm
2fdecbe206 Merge pull request #551 from louis-e/dependabot/cargo/fastanvil-0.32.0
build(deps): bump fastanvil from 0.31.0 to 0.32.0
2025-09-03 15:37:54 +02:00
Louis Erbkamm
75d1ab14e7 Merge pull request #552 from louis-e/dependabot/github_actions/actions/checkout-5
build(deps): bump actions/checkout from 4 to 5
2025-09-03 15:36:58 +02:00
Louis Erbkamm
f9c18ae5f6 Merge pull request #548 from louis-e/dependabot/cargo/tauri-build-2.4.0
build(deps): bump tauri-build from 2.3.1 to 2.4.0
2025-09-03 15:36:42 +02:00
Louis Erbkamm
963fe2327b Merge pull request #553 from louis-e/dependabot/github_actions/actions/download-artifact-5
build(deps): bump actions/download-artifact from 4 to 5
2025-09-03 15:36:26 +02:00
Louis Erbkamm
2a8ff6f641 Merge branch 'main' into elevation-tilecache-crash 2025-09-03 15:34:49 +02:00
louis-e
ff3ea1093a Implement faulty elevation tile cache detection crash 2025-09-03 15:32:50 +02:00
Louis Erbkamm
fd401ab23f Merge pull request #555 from louis-e/taginfo-refinement
Refine elements based on taginfo feedback
2025-09-03 15:03:00 +02:00
louis-e
3f67e060eb Refine elements based on taginfo feedback 2025-09-03 15:01:08 +02:00
dependabot[bot]
858c2f4c93 build(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 06:22:06 +00:00
dependabot[bot]
e8fad0e197 build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 06:00:27 +00:00
dependabot[bot]
7ebc66db7e build(deps): bump fastanvil from 0.31.0 to 0.32.0
Bumps [fastanvil](https://github.com/owengage/fastnbt) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/owengage/fastnbt/compare/fastanvil-0.31.0...fastanvil-0.32.0)

---
updated-dependencies:
- dependency-name: fastanvil
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 05:29:00 +00:00
dependabot[bot]
28b75a22cf build(deps): bump tempfile from 3.20.0 to 3.21.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.20.0...v3.21.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 05:22:02 +00:00
dependabot[bot]
9ee9f0de64 build(deps): bump semver from 1.0.24 to 1.0.26
Bumps [semver](https://github.com/dtolnay/semver) from 1.0.24 to 1.0.26.
- [Release notes](https://github.com/dtolnay/semver/releases)
- [Commits](https://github.com/dtolnay/semver/compare/1.0.24...1.0.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 05:19:45 +00:00
dependabot[bot]
5b874a5c29 build(deps): bump tauri-build from 2.3.1 to 2.4.0
Bumps [tauri-build](https://github.com/tauri-apps/tauri) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v2.3.1...tauri-build-v2.4.0)

---
updated-dependencies:
- dependency-name: tauri-build
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 05:17:50 +00:00
freeutka
eeecdc8f99 Create lv.json 2025-08-25 12:20:23 +03:00
Louis Erbkamm
8b33e152ef Remove icon_url from taginfo.json 2025-08-24 19:28:05 +02:00
Louis Erbkamm
ca8e50fbf1 Merge pull request #535 from louis-e/taginfo
Add comprehensive taginfo.json for OSM tag documentation
2025-08-24 18:22:18 +02:00
louis-e
7f4ef9130f Add comprehensive taginfo.json for OSM tag documentation 2025-08-24 18:18:02 +02:00
Stephen D
8d183543be fix dynmap, and maybe other things 2025-08-24 09:01:39 -04:00
Stephen D
dd07fba15c missed a file 2025-08-24 08:58:02 -04:00
Stephen D
083fbed040 nix development fix/improvements 2025-08-24 08:57:07 -04:00
Louis Erbkamm
4191fa4902 Merge pull request #532 from sralmai/patch-1
Update README.md
2025-08-24 12:25:07 +02:00
David Samuelson
6c82265ee3 Update README.md
Fix lat and long ordering in the command line build example.
2025-08-24 05:16:05 -05:00
Louis Erbkamm
7d978047b4 Enhance bug report template with additional details
Updated bug report template for clarity and detail.
2025-08-24 02:59:43 +02:00
138 changed files with 8934 additions and 1999 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

2
.gitattributes vendored
View File

@@ -1 +1 @@
gui-src/** linguist-vendored
src/gui/** linguist-vendored

View File

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

View File

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

View File

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

View File

@@ -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
View File

@@ -1,8 +1,8 @@
/wiki
*.mcworld
# Environment files
.env
.envrc
/.direnv
# Build artifacts

592
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 790 KiB

After

Width:  |  Height:  |  Size: 790 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

60
flake.lock generated Normal file
View 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
View 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
View File

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

View File

@@ -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
];
};
});
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -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
View File

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

View File

@@ -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
View File

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

View File

@@ -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(&region_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");
}
}
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -307,7 +307,7 @@ impl Tree<'_> {
FURNACE,
ANVIL,
BREWING_STAND,
JUKEBOX,
NOTE_BLOCK,
BOOKSHELF,
CAULDRON,
// Beds

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(&region_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) => {

View File

View File

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

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 418 B

View File

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 205 B

View File

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 262 B

View File

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 348 B

View File

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View File

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 328 B

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 849 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/gui/css/maps/images/spritesheet.png vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

View File

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

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 487 B

After

Width:  |  Height:  |  Size: 487 B

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

View File

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 811 B

View File

@@ -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()">&times;</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()">&times;</span>
<h2>Help improve Arnis</h2>
<p style="text-align:left; margin-top:6px; color:#ececec;">
Wed 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">

View File

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

View File

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More