Compare commits

...

145 Commits

Author SHA1 Message Date
louis-e
996e8756d0 Mock emit_map_preview_ready 2025-12-01 18:36:29 +01:00
louis-e
0c5bd51ba4 Mock emit_map_preview_ready 2025-12-01 18:32:26 +01:00
louis-e
7965dc3737 Fix CLI build issue 2025-12-01 18:07:52 +01:00
Louis Erbkamm
c54187b43a Merge branch 'main' into benchmark-map-preview 2025-12-01 18:04:14 +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-e
beb7b73d11 Include map preview in CI benchmark 2025-12-01 18:02:59 +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
131 changed files with 5160 additions and 974 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@v5
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
@@ -43,7 +43,7 @@ jobs:
- name: Run benchmark command with memory tracking
id: benchmark
run: |
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.101470,11.517792,48.168375,11.626968" 2> benchmark_log.txt
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --generate-map --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
peak_kb=$(cat peak_mem_kb.txt)
peak_mb=$((peak_kb / 1024))
@@ -57,6 +57,25 @@ jobs:
duration=$((end_time - start_time))
echo "duration=$duration" >> $GITHUB_OUTPUT
- name: Check for map preview
id: map_check
run: |
if [ -f "./world/arnis_world_map.png" ]; then
echo "Map preview generated successfully"
echo "map_exists=true" >> $GITHUB_OUTPUT
else
echo "Map preview not found"
echo "map_exists=false" >> $GITHUB_OUTPUT
fi
- name: Upload map preview as artifact
if: steps.map_check.outputs.map_exists == 'true'
uses: actions/upload-artifact@v4
with:
name: world-map-preview
path: ./world/arnis_world_map.png
retention-days: 60
- name: Format duration and generate summary
id: comment_body
run: |
@@ -65,7 +84,7 @@ jobs:
seconds=$((duration % 60))
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
baseline_time=135
baseline_time=69
diff=$((duration - baseline_time))
abs_diff=${diff#-}
@@ -79,7 +98,7 @@ jobs:
verdict="🚨 This PR **drastically worsens generation time**."
fi
baseline_mem=5865
baseline_mem=935
mem_annotation=""
if [ "$peak_mem" -gt 2000 ]; then
mem_diff=$((peak_mem - baseline_mem))
@@ -87,17 +106,28 @@ jobs:
mem_annotation=" (↗ ${mem_percent}% more)"
fi
# Get current timestamp
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
{
echo "summary<<EOF"
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
echo "🧠 Peak memory usage: **${peak_mem} MB**${mem_annotation}"
echo "## ⏱️ Benchmark Results"
echo ""
echo "📈 Compared against baseline: **${baseline_time}s**"
echo "🧮 Delta: **${diff}s**"
echo "🔢 Commit: [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})"
echo "| Metric | Value |"
echo "|--------|-------|"
echo "| Duration | **${minutes}m ${seconds}s** |"
echo "| Peak Memory | **${peak_mem} MB**${mem_annotation} |"
echo "| Baseline | **${baseline_time}s** |"
echo "| Delta | **${diff}s** |"
echo "| Commit | [\`${GITHUB_SHA:0:7}\`](https://github.com/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA}) |"
echo ""
echo "${verdict}"
echo ""
echo "---"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time} | 📥 [Download generated world map](${run_url}#artifacts)"
echo ""
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
echo "EOF"
} >> "$GITHUB_OUTPUT"

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

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
# Environment files
.env
.envrc
/.direnv
# Build artifacts

280
Cargo.lock generated
View File

@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
@@ -191,8 +182,9 @@ dependencies = [
[[package]]
name = "arnis"
version = "2.2.1"
version = "2.3.1"
dependencies = [
"base64 0.22.1",
"clap",
"colored",
"dirs",
@@ -463,21 +455,6 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -1165,7 +1142,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1208,9 +1185,9 @@ dependencies = [
[[package]]
name = "dlopen2"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
dependencies = [
"dlopen2_derive",
"libc",
@@ -1423,9 +1400,9 @@ dependencies = [
[[package]]
name = "fastanvil"
version = "0.31.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619fbd9bb48f9e54a7ff88cdd0f7048b8ea519448518c690057f9d03d3e81ebf"
checksum = "68f773cb151aa058dd65e3a119cc0733252abf2942d4d3eed8eee72bd231d5b4"
dependencies = [
"bit_field",
"byteorder",
@@ -1433,6 +1410,7 @@ dependencies = [
"flate2",
"image 0.23.14",
"log",
"lz4-java-wrc",
"num_enum 0.5.11",
"once_cell",
"serde",
@@ -1442,9 +1420,9 @@ dependencies = [
[[package]]
name = "fastnbt"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4a73a95dc65551ccd98e1ecd1adb5d1ba5361146963b31f481ca42fc0520a3"
checksum = "29e3bb1f9ba8b6dbb35d89b415216041c2cad0c3e0b329db85ef5d4fc9e80cd6"
dependencies = [
"byteorder",
"cesu8",
@@ -1803,9 +1781,9 @@ dependencies = [
[[package]]
name = "geo"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1"
checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a"
dependencies = [
"earcutr",
"float_next_after",
@@ -1885,12 +1863,6 @@ dependencies = [
"weezl",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gio"
version = "0.18.4"
@@ -2262,24 +2234,24 @@ dependencies = [
[[package]]
name = "i_float"
version = "1.7.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343"
checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b"
dependencies = [
"serde",
"libm",
]
[[package]]
name = "i_key_sort"
version = "0.2.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd"
checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27"
[[package]]
name = "i_overlay"
version = "2.0.5"
version = "4.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49"
checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9"
dependencies = [
"i_float",
"i_key_sort",
@@ -2290,19 +2262,18 @@ dependencies = [
[[package]]
name = "i_shape"
version = "1.7.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce"
checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082"
dependencies = [
"i_float",
"serde",
]
[[package]]
name = "i_tree"
version = "0.8.3"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139"
checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915"
[[package]]
name = "iana-time-zone"
@@ -2590,17 +2561,6 @@ dependencies = [
"syn 2.0.95",
]
[[package]]
name = "io-uring"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"libc",
]
[[package]]
name = "ipnet"
version = "2.10.1"
@@ -2905,6 +2865,22 @@ dependencies = [
"imgref",
]
[[package]]
name = "lz4-java-wrc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed1c11dbd317fa5d0508cbb5ce47696bb27771e3c9604428e0e98c7c3d76d0d"
dependencies = [
"lz4_flex",
"twox-hash",
]
[[package]]
name = "lz4_flex"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
[[package]]
name = "mac"
version = "0.1.1"
@@ -3397,6 +3373,16 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-javascript-core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
dependencies = [
"objc2 0.6.1",
"objc2-core-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
@@ -3433,6 +3419,17 @@ dependencies = [
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-security"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
dependencies = [
"bitflags 2.6.0",
"objc2 0.6.1",
"objc2-core-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.1"
@@ -3457,15 +3454,8 @@ dependencies = [
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
"objc2-javascript-core",
"objc2-security",
]
[[package]]
@@ -4422,12 +4412,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -4620,19 +4604,21 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.24"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
@@ -4657,10 +4643,19 @@ dependencies = [
]
[[package]]
name = "serde_derive"
version = "1.0.217"
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -4763,9 +4758,9 @@ dependencies = [
[[package]]
name = "serialize-to-javascript"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
dependencies = [
"serde",
"serde_json",
@@ -4774,13 +4769,13 @@ dependencies = [
[[package]]
name = "serialize-to-javascript-impl"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.95",
]
[[package]]
@@ -5090,11 +5085,12 @@ dependencies = [
[[package]]
name = "tao"
version = "0.34.0"
version = "0.34.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
dependencies = [
"bitflags 2.6.0",
"block2 0.6.1",
"core-foundation 0.10.0",
"core-graphics",
"crossbeam-channel",
@@ -5162,12 +5158,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.7.0"
version = "2.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c"
dependencies = [
"anyhow",
"bytes",
"cookie",
"dirs",
"dunce",
"embed_plist",
@@ -5185,6 +5182,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"objc2-web-kit",
"percent-encoding",
"plist",
"raw-window-handle",
@@ -5212,9 +5210,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.3.1"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5228,15 +5226,15 @@ dependencies = [
"serde_json",
"tauri-utils",
"tauri-winres",
"toml 0.8.19",
"toml 0.9.2",
"walkdir",
]
[[package]]
name = "tauri-codegen"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a"
dependencies = [
"base64 0.22.1",
"brotli",
@@ -5261,9 +5259,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.3.2"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5335,9 +5333,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.7.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846"
dependencies = [
"cookie",
"dpi",
@@ -5346,20 +5344,23 @@ dependencies = [
"jni",
"objc2 0.6.1",
"objc2-ui-kit",
"objc2-web-kit",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.9",
"url",
"webkit2gtk",
"webview2-com",
"windows",
]
[[package]]
name = "tauri-runtime-wry"
version = "2.7.2"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
dependencies = [
"gtk",
"http",
@@ -5384,9 +5385,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212"
dependencies = [
"anyhow",
"brotli",
@@ -5413,7 +5414,7 @@ dependencies = [
"serde_with",
"swift-rs",
"thiserror 2.0.9",
"toml 0.8.19",
"toml 0.9.2",
"url",
"urlpattern",
"uuid",
@@ -5433,15 +5434,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.20.0"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5566,29 +5567,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.47.0"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43864ed400b6043a4757a25c7a64a8efde741aed79a056a2fb348a406701bb35"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"backtrace",
"bytes",
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2 0.6.0",
"tokio-macros",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
@@ -5809,6 +5807,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"static_assertions",
]
[[package]]
name = "typeid"
version = "1.0.2"
@@ -6367,7 +6375,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.0",
"windows-future",
"windows-link",
"windows-link 0.1.1",
"windows-numerics",
]
@@ -6397,7 +6405,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-link 0.1.1",
"windows-result",
"windows-strings 0.4.0",
]
@@ -6409,7 +6417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -6440,6 +6448,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -6447,7 +6461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -6467,7 +6481,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -6476,7 +6490,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -6485,7 +6499,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
"windows-link 0.1.1",
]
[[package]]
@@ -6515,6 +6529,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -6766,14 +6789,15 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.52.1"
version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90"
dependencies = [
"base64 0.22.1",
"block2 0.6.1",
"cookie",
"crossbeam-channel",
"dirs",
"dpi",
"dunce",
"gdkx11",

View File

@@ -1,6 +1,6 @@
[package]
name = "arnis"
version = "2.3.0"
version = "2.3.1"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -20,15 +20,16 @@ gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs"
tauri-build = {version = "2", optional = true}
[dependencies]
base64 = "0.22.1"
clap = { version = "4.5", features = ["derive", "env"] }
colored = "3.0.0"
dirs = {version = "6.0.0", optional = true }
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 +39,16 @@ 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 }
[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,4 +1,4 @@
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/banner.png?raw=true" width="100%" alt="Banner">
<img src="assets/git/banner.png" width="100%" alt="Banner">
# Arnis [![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)
@@ -7,11 +7,11 @@ Arnis creates complex and accurate Minecraft Java Edition worlds that reflect re
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
Generate your hometown, big cities, and natural landscapes with ease!
![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

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: 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")]
@@ -59,13 +59,17 @@ pub struct Args {
#[arg(long, value_parser = parse_duration)]
pub timeout: Option<Duration>,
/// Generate a top-down map preview image after world generation (optional)
#[arg(long)]
pub generate_map: bool,
/// Spawn point coordinates (lat, lng)
#[arg(skip)]
pub spawn_point: Option<(f64, f64)>,
}
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 +80,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> {

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,10 +1,14 @@
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::progress::{emit_gui_progress_update, emit_map_preview_ready};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::world_editor::WorldEditor;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
@@ -14,14 +18,17 @@ pub const MIN_Y: i32 = -64;
pub fn generate_world(
elements: Vec<ProcessedElement>,
xzbbox: XZBBox,
llbbox: LLBBox,
ground: Ground,
args: &Args,
) -> Result<(), String> {
let region_dir: String = format!("{}/region", args.path);
let mut editor: WorldEditor = WorldEditor::new(&region_dir, &xzbbox);
let mut editor: WorldEditor = WorldEditor::new(args.path.clone(), &xzbbox, llbbox);
println!("{} Processing data...", "[4/7]".bold());
// Build highway connectivity map once before processing
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 +70,7 @@ pub fn generate_world(
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(&mut editor, way, args, None);
} else if way.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, args);
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 +81,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 +115,7 @@ pub fn generate_world(
} else if node.tags.contains_key("barrier") {
barriers::generate_barrier_nodes(&mut editor, node);
} else if node.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, args);
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 +126,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 +205,8 @@ pub fn generate_world(
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
block_counter += 1;
// Use manual % check since is_multiple_of() is unstable on stable Rust
#[allow(clippy::manual_is_multiple_of)]
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
@@ -236,11 +256,64 @@ pub fn generate_world(
args.scale,
&ground,
) {
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
eprintln!("Warning: {}", warning_msg);
#[cfg(feature = "gui")]
send_log(LogLevel::Warning, &warning_msg);
}
}
emit_gui_progress_update(100.0, "Done! World generation completed.");
println!("{}", "Done! World generation completed.".green().bold());
// Generate top-down map preview:
// - Always for GUI mode (non-blocking, runs in background)
// - Only when --generate-map flag is set for CLI mode (blocking, waits for completion)
#[cfg(feature = "gui")]
let should_generate_map = true;
#[cfg(not(feature = "gui"))]
let should_generate_map = args.generate_map;
if should_generate_map {
let world_path = args.path.clone();
let bounds = (
xzbbox.min_x(),
xzbbox.max_x(),
xzbbox.min_z(),
xzbbox.max_z(),
);
let map_thread = std::thread::spawn(move || {
// Use catch_unwind to prevent any panic from affecting the application
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
map_renderer::render_world_map(&world_path, bounds.0, bounds.1, bounds.2, bounds.3)
}));
match result {
Ok(Ok(_path)) => {
// Notify the GUI that the map preview is ready
emit_map_preview_ready();
}
Ok(Err(e)) => {
eprintln!("Warning: Failed to generate map preview: {}", e);
}
Err(_) => {
eprintln!("Warning: Map preview generation panicked unexpectedly");
}
}
});
// In CLI mode, wait for map generation to complete before exiting
// In GUI mode, let it run in background to keep UI responsive
#[cfg(not(feature = "gui"))]
{
let _ = map_thread.join();
}
// In GUI mode, we don't join, let the thread run in background
#[cfg(feature = "gui")]
drop(map_thread);
}
Ok(())
}

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,81 @@ pub fn fetch_elevation_data(
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
let rgb_img: image::ImageBuffer<Rgb<u8>, Vec<u8>> = if tile_path.exists() {
println!(
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
tile_path.display()
);
let img: image::DynamicImage = image::open(&tile_path)?;
img.to_rgb8()
} else {
// AWS Terrain Tiles don't require an API key
println!("Fetching tile x={tile_x},y={tile_y},z={zoom} from AWS Terrain Tiles");
let url: String = AWS_TERRARIUM_URL
.replace("{z}", &zoom.to_string())
.replace("{x}", &tile_x.to_string())
.replace("{y}", &tile_y.to_string());
// Check if the cached file has a reasonable size (PNG files should be at least a few KB)
let file_size = match std::fs::metadata(&tile_path) {
Ok(metadata) => metadata.len(),
Err(_) => 0,
};
let response: reqwest::blocking::Response = client.get(&url).send()?;
response.error_for_status_ref()?;
let bytes = response.bytes()?;
std::fs::write(&tile_path, &bytes)?;
let img: image::DynamicImage = image::load_from_memory(&bytes)?;
img.to_rgb8()
if file_size < 1000 {
eprintln!(
"Warning: Cached tile at {} appears to be too small ({} bytes). Refetching tile.",
tile_path.display(),
file_size
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile appears to be too small. Refetching tile.",
);
// Remove the potentially corrupted file
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
eprintln!(
"Warning: Failed to remove corrupted tile file: {}",
remove_err
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during refetching.",
);
}
// Re-download the tile
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
} else {
println!(
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
tile_path.display()
);
// Try to load cached tile, but handle corruption gracefully
match image::open(&tile_path) {
Ok(img) => img.to_rgb8(),
Err(e) => {
eprintln!(
"Cached tile at {} is corrupted or invalid: {}. Re-downloading...",
tile_path.display(),
e
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Cached tile is corrupted or invalid. Re-downloading...",
);
// Remove the corrupted file
if let Err(remove_err) = std::fs::remove_file(&tile_path) {
eprintln!(
"Warning: Failed to remove corrupted tile file: {}",
remove_err
);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Failed to remove corrupted tile file during re-download.",
);
}
// Re-download the tile
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
}
}
}
} else {
// Download the tile for the first time
download_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?
};
// Only process pixels that fall within the requested bbox

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,23 @@
use crate::args::Args;
use crate::coordinate_system::cartesian::XZPoint;
use crate::coordinate_system::geographic::{LLBBox, LLPoint};
use crate::coordinate_system::transformation::CoordTransformer;
use crate::data_processing;
use crate::ground::{self, Ground};
use crate::map_transformation;
use crate::osm_parser;
use crate::progress;
use crate::retrieve_data;
use crate::telemetry::{self, send_log, LogLevel};
use crate::version_check;
use fastnbt::Value;
use flate2::read::GzDecoder;
use fs2::FileExt;
use log::{error, LevelFilter};
use log::LevelFilter;
use rfd::FileDialog;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::{env, fs, io::Write, panic};
use std::{env, fs, io::Write};
use tauri_plugin_log::{Builder as LogBuilder, Target, TargetKind};
/// Manages the session.lock file for a Minecraft world directory
@@ -62,12 +64,8 @@ pub fn run_gui() {
// Launch the UI
println!("Launching UI...");
// Set a custom panic hook to log panic information
panic::set_hook(Box::new(|panic_info| {
let message = format!("Application panicked: {panic_info:?}");
error!("{message}");
std::process::exit(1);
}));
// Install panic hook for crash reporting
telemetry::install_panic_hook();
// Workaround WebKit2GTK issue with NVIDIA drivers and graphics issues
// Source: https://github.com/tauri-apps/tauri/issues/10702
@@ -102,7 +100,8 @@ pub fn run_gui() {
gui_select_world,
gui_start_generation,
gui_get_version,
gui_check_for_updates
gui_check_for_updates,
gui_get_world_map_data
])
.setup(|app| {
let app_handle = app.handle();
@@ -226,13 +225,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 +298,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 +306,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 +354,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 +370,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 +398,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 +413,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 +524,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 +664,63 @@ fn gui_check_for_updates() -> Result<bool, String> {
}
}
/// Returns the world map image data as base64 and geo bounds for overlay display.
/// Returns None if the map image or metadata doesn't exist.
#[tauri::command]
fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, String> {
let world_dir = PathBuf::from(&world_path);
let map_path = world_dir.join("arnis_world_map.png");
let metadata_path = world_dir.join("metadata.json");
// Check if both files exist
if !map_path.exists() || !metadata_path.exists() {
return Ok(None);
}
// Read and encode the map image as base64
let image_data = fs::read(&map_path).map_err(|e| format!("Failed to read map image: {e}"))?;
let base64_image =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &image_data);
// Read metadata
let metadata_content =
fs::read_to_string(&metadata_path).map_err(|e| format!("Failed to read metadata: {e}"))?;
let metadata: serde_json::Value = serde_json::from_str(&metadata_content)
.map_err(|e| format!("Failed to parse metadata: {e}"))?;
// Extract geo bounds (metadata uses camelCase from serde)
let min_lat = metadata["minGeoLat"]
.as_f64()
.ok_or("Missing minGeoLat in metadata")?;
let max_lat = metadata["maxGeoLat"]
.as_f64()
.ok_or("Missing maxGeoLat in metadata")?;
let min_lon = metadata["minGeoLon"]
.as_f64()
.ok_or("Missing minGeoLon in metadata")?;
let max_lon = metadata["maxGeoLon"]
.as_f64()
.ok_or("Missing maxGeoLon in metadata")?;
Ok(Some(WorldMapData {
image_base64: format!("data:image/png;base64,{}", base64_image),
min_lat,
max_lat,
min_lon,
max_lon,
}))
}
/// Data structure for world map overlay
#[derive(serde::Serialize)]
struct WorldMapData {
image_base64: String,
min_lat: f64,
max_lat: f64,
min_lon: f64,
max_lon: f64,
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
#[allow(unused_variables)]
@@ -678,15 +731,23 @@ fn gui_start_generation(
ground_level: i32,
floodfill_timeout: u64,
terrain_enabled: bool,
skip_osm_objects: bool,
interior_enabled: bool,
roof_enabled: bool,
fillground_enabled: bool,
is_new_world: bool,
spawn_point: Option<(f64, f64)>,
telemetry_consent: bool,
) -> Result<(), String> {
use progress::emit_gui_error;
use LLBBox;
// Store telemetry consent for crash reporting
telemetry::set_telemetry_consent(telemetry_consent);
// Send generation click telemetry
telemetry::send_generation_click();
// If spawn point was chosen and the world is new, check and set the spawn point
if is_new_world && spawn_point.is_some() {
// Verify the spawn point is within bounds
@@ -744,9 +805,9 @@ fn gui_start_generation(
// Add localized name to the world if user generated a new world
let updated_world_path = if is_new_world {
add_localized_world_name(&selected_world, &bbox)
add_localized_world_name(world_path, &bbox)
} else {
selected_world.clone()
world_path
};
// Create an Args instance with the chosen bounding box and world directory path
@@ -764,10 +825,33 @@ fn gui_start_generation(
fillground: fillground_enabled,
debug: false,
timeout: Some(std::time::Duration::from_secs(floodfill_timeout)),
generate_map: true,
spawn_point,
};
// Run data fetch and world generation
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
if skip_osm_objects {
// Generate ground data (terrain) for terrain-only mode
let ground = ground::generate_ground_data(&args);
// Create empty parsed_elements and xzbbox for terrain-only mode
let parsed_elements = Vec::new();
let (_coord_transformer, xzbbox) =
CoordTransformer::llbbox_to_xzbbox(&args.bbox, args.scale)
.map_err(|e| format!("Failed to create coordinate transformer: {}", e))?;
let _ = data_processing::generate_world(
parsed_elements,
xzbbox,
args.bbox,
ground,
&args,
);
// Session lock will be automatically released when _session_lock goes out of scope
return Ok(());
}
// Run data fetch and world generation (standard mode: objects + terrain, or objects only)
match retrieve_data::fetch_data_from_overpass(args.bbox, args.debug, "requests", None) {
Ok(raw_data) => {
let (mut parsed_elements, mut xzbbox) =
@@ -794,7 +878,13 @@ fn gui_start_generation(
&mut ground,
);
let _ = data_processing::generate_world(parsed_elements, xzbbox, ground, &args);
let _ = data_processing::generate_world(
parsed_elements,
xzbbox,
args.bbox,
ground,
&args,
);
// Session lock will be automatically released when _session_lock goes out of scope
Ok(())
}

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

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 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

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 {
@@ -249,6 +258,33 @@ button:hover {
color: #ffffff;
}
/* Modal actions/buttons */
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn-primary {
background-color: var(--primary-accent);
color: #1a1a1a;
}
.btn-primary:hover {
background-color: var(--primary-accent-dark);
}
.btn-secondary {
background-color: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
.btn-secondary {
background-color: #3a3a3a;
color: #ffffff;
}
}
#terrain-toggle {
accent-color: #fecc44;
}
@@ -281,6 +317,10 @@ button:hover {
margin: 15px 0;
}
#telemetry-toggle {
accent-color: #fecc44;
}
.scale-slider-container label {
display: block;
margin-bottom: 5px;
@@ -306,7 +346,7 @@ button:hover {
#bbox-coords {
width: 100%;
padding: 8px;
padding: 5px;
border: 1px solid #fecc44;
border-radius: 4px;
font-size: 14px;
@@ -353,7 +393,7 @@ button:hover {
.license-button-row {
justify-content: center;
margin-top: 10px;
margin-top: 5px;
}
.license-button {
@@ -389,11 +429,39 @@ button:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Generation mode dropdown styling */
.generation-mode-dropdown {
width: 100%;
max-width: 180px;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #fecc44;
background-color: #ffffff;
color: #0f0f0f;
appearance: menulist;
cursor: pointer;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.generation-mode-dropdown option {
padding: 5px;
font-size: 15px;
}
@media (prefers-color-scheme: dark) {
.generation-mode-dropdown {
background-color: #0f0f0f98;
color: #ffffff;
border: 1px solid #fecc44;
}
}
/* Language dropdown styling */
.language-dropdown {
width: 100%;
max-width: 180px;
padding: 5px 8px;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #fecc44;
background-color: #ffffff;
@@ -421,7 +489,7 @@ button:hover {
.theme-dropdown {
width: 100%;
max-width: 180px;
padding: 5px 8px;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid #fecc44;
background-color: #ffffff;

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>
@@ -87,11 +87,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 +175,7 @@
<option value="zh-CN">中文 (简体)</option>
<option value="ko">한국어</option>
<option value="pl">Polski</option>
<option value="lv">Latviešu</option>
<option value="sv">Svenska</option>
<option value="ar">العربية</option>
<option value="fi">Suomi</option>
@@ -181,6 +186,14 @@
</div>
</div>
<!-- Telemetry Consent Toggle -->
<div class="settings-row">
<label for="telemetry-toggle">Anonymous Crash Reports</label>
<div class="settings-control">
<input type="checkbox" id="telemetry-toggle" name="telemetry-toggle">
</div>
</div>
<!-- License and Credits Button -->
<div class="settings-row license-button-row">
<button type="button" id="license-button" class="license-button" onclick="openLicense()" data-localize="license_and_credits">License and Credits</button>
@@ -198,6 +211,22 @@
</div>
</div>
<!-- Telemetry Consent Modal (first run) -->
<div id="telemetry-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="rejectTelemetry()">&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

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

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,13 @@ export const licenseText = `
<b>Stadia Maps:</b><br> © <a href="https://www.stadiamaps.com/" style="color: inherit;" target="_blank">Stadia Maps</a> © <a href="https://openmaptiles.org/" style="color: inherit;" target="_blank">OpenMapTiles</a> © OpenStreetMap contributors
<p>Users of this software must comply with the respective licensing terms of these map data providers when using the application.</p>
<b>AWS Terrain Tiles:</b><br>
Elevation data derived from the <a href="https://registry.opendata.aws/terrain-tiles/" style="color: inherit;" target="_blank">AWS Terrain Tiles</a> dataset.
<br><br>
<p><b>Privacy Policy:</b></p>
If you consent to telemetry data collection, please review our Privacy Policy at:
<a href="https://arnismc.com/privacypolicy.html" style="color: inherit;" target="_blank">https://arnismc.com/privacypolicy.html</a>.
<p><b>License:</b></p>
<pre style="white-space: pre-wrap; font-family: inherit;">

View File

@@ -20,6 +20,7 @@ window.addEventListener("DOMContentLoaded", async () => {
setupProgressListener();
initSettings();
initWorldPicker();
initTelemetryConsent();
handleBboxInput();
const localization = await getLocalization();
await applyLocalization(localization);
@@ -93,6 +94,10 @@ async function applyLocalization(localization) {
// DEPRECATED: Ground level localization removed
// "label[data-localize='ground_level']": "ground_level",
"label[data-localize='language']": "language",
"label[data-localize='generation_mode']": "generation_mode",
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
"option[data-localize='mode_geo_only']": "mode_geo_only",
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
"label[data-localize='terrain']": "terrain",
"label[data-localize='interior']": "interior",
"label[data-localize='roof']": "roof",
@@ -209,6 +214,12 @@ function setupProgressListener() {
}
}
});
// Listen for map preview ready event from backend
window.__TAURI__.event.listen("map-preview-ready", () => {
console.log("Map preview ready event received");
showWorldPreviewButton();
});
}
function initSettings() {
@@ -301,6 +312,20 @@ function initSettings() {
}
});
// Telemetry consent toggle
const telemetryToggle = document.getElementById("telemetry-toggle");
const telemetryKey = 'telemetry-consent';
// Load saved telemetry consent
const savedConsent = localStorage.getItem(telemetryKey);
telemetryToggle.checked = savedConsent === 'true';
// Handle telemetry consent change
telemetryToggle.addEventListener("change", () => {
const isEnabled = telemetryToggle.checked;
localStorage.setItem(telemetryKey, isEnabled ? 'true' : 'false');
});
/// License and Credits
function openLicense() {
@@ -325,6 +350,49 @@ function initSettings() {
window.closeLicense = closeLicense;
}
// Telemetry consent (first run only)
function initTelemetryConsent() {
const key = 'telemetry-consent'; // values: 'true' | 'false'
const existing = localStorage.getItem(key);
const modal = document.getElementById('telemetry-modal');
if (!modal) return;
if (existing === null) {
// First run: ask for consent
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
}
// Expose handlers
window.acceptTelemetry = () => {
localStorage.setItem(key, 'true');
modal.style.display = 'none';
// Update settings toggle to reflect the consent
const telemetryToggle = document.getElementById('telemetry-toggle');
if (telemetryToggle) {
telemetryToggle.checked = true;
}
};
window.rejectTelemetry = () => {
localStorage.setItem(key, 'false');
modal.style.display = 'none';
// Update settings toggle to reflect the consent
const telemetryToggle = document.getElementById('telemetry-toggle');
if (telemetryToggle) {
telemetryToggle.checked = false;
}
};
// Utility for other scripts to read consent
window.getTelemetryConsent = () => {
const v = localStorage.getItem(key);
return v === null ? null : v === 'true';
};
}
function initWorldPicker() {
// World Picker
const worldPickerModal = document.getElementById("world-modal");
@@ -529,6 +597,14 @@ async function selectWorld(generate_new_world) {
const lastSegment = worldName.split(/[\\/]/).pop();
document.getElementById('selected-world').textContent = lastSegment;
document.getElementById('selected-world').style.color = "#fecc44";
// Notify that world changed (reset preview)
notifyWorldChanged();
// If selecting an existing world, check for existing map data
if (!generate_new_world) {
await loadExistingWorldMapData();
}
}
} catch (error) {
handleWorldSelectionError(error);
@@ -537,6 +613,32 @@ async function selectWorld(generate_new_world) {
closeWorldPicker();
}
/**
* Loads existing world map data if available (for existing worlds)
* This will zoom to the location and auto-enable the preview
*/
async function loadExistingWorldMapData() {
if (!worldPath) return;
try {
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
if (mapData) {
currentWorldMapData = mapData;
// Send data to the map iframe with instruction to zoom and auto-enable
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'loadExistingWorldMap',
data: mapData
}, '*');
}
}
} catch (error) {
console.log("No existing world map data found:", error);
}
}
/**
* Handles world selection errors and displays appropriate messages
* @param {number} errorCode - Error code from the backend
@@ -583,6 +685,9 @@ async function startGeneration() {
return;
}
// Clear any existing world preview since we're generating a new one
notifyWorldChanged();
// Get the map iframe reference
const mapFrame = document.querySelector('.map-container');
// Get spawn point coordinates if marker exists
@@ -595,7 +700,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 +718,9 @@ async function startGeneration() {
floodfill_timeout = isNaN(floodfill_timeout) || floodfill_timeout < 0 ? 20 : floodfill_timeout;
ground_level = isNaN(ground_level) || ground_level < -62 ? 20 : ground_level;
// Get telemetry consent (defaults to false if not set)
const telemetryConsent = window.getTelemetryConsent ? window.getTelemetryConsent() : false;
// Pass the selected options to the Rust backend
await invoke("gui_start_generation", {
bboxText: selectedBBox,
@@ -617,11 +729,13 @@ async function startGeneration() {
groundLevel: ground_level,
floodfillTimeout: floodfill_timeout,
terrainEnabled: terrain,
skipOsmObjects: skipOsmObjects,
interiorEnabled: interior,
roofEnabled: roof,
fillgroundEnabled: fill_ground,
isNewWorld: isNewWorld,
spawnPoint: spawnPoint
spawnPoint: spawnPoint,
telemetryConsent: telemetryConsent || false
});
console.log("Generation process started.");
@@ -631,3 +745,60 @@ async function startGeneration() {
generationButtonEnabled = true;
}
}
// World preview overlay state
let worldPreviewEnabled = false;
let currentWorldMapData = null;
/**
* Notifies the map iframe that world preview data is ready
* Called when the backend emits the map-preview-ready event
*/
async function showWorldPreviewButton() {
// Try to load the world map data
await loadWorldMapData();
if (currentWorldMapData) {
// Send data to the map iframe
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'worldPreviewReady',
data: currentWorldMapData
}, '*');
console.log("World preview data sent to map iframe");
}
} else {
console.warn("Map data not available yet");
}
}
/**
* Notifies the map iframe that the world has changed (reset preview)
*/
function notifyWorldChanged() {
currentWorldMapData = null;
const mapFrame = document.querySelector('.map-container');
if (mapFrame && mapFrame.contentWindow) {
mapFrame.contentWindow.postMessage({
type: 'worldChanged'
}, '*');
}
}
/**
* Loads the world map data from the backend
*/
async function loadWorldMapData() {
if (!worldPath) return;
try {
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
if (mapData) {
currentWorldMapData = mapData;
console.log("World map data loaded successfully");
}
} catch (error) {
console.error("Failed to load world map data:", error);
}
}

View File

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