388 Commits

Author SHA1 Message Date
Louis Erbkamm
489e571a42 Merge pull request #745 from louis-e/security-updates-dependabot
security: Update time to 0.3.47 and reqwest to 0.13.2 (Dependabot fixes)
2026-02-09 14:05:18 +01:00
louis-e
67e22a574a security: Update time to 0.3.47 and reqwest to 0.13.2
- Update time from 0.3.37 to 0.3.47
  - Fixes stack exhaustion DoS vulnerability in RFC 2822 parsing (CVE-2026-XXXX)
  - Adds recursion depth tracking to prevent unbounded recursion

- Update reqwest from 0.12.15 to 0.13.2
  - Switch TLS backend from native-tls to rustls with aws-lc crypto provider
  - Improves TLS security and platform compatibility
  - Add 'query' feature to maintain URL query parameter functionality

- Update related dependencies:
  - time-core 0.1.2 -> 0.1.8
  - time-macros 0.2.19 -> 0.2.27
  - deranged 0.3.11 -> 0.5.5
  - num-conv 0.1.0 -> 0.2.0
  - hyper 1.5.2 -> 1.8.1
  - hyper-util 0.1.10 -> 0.1.19
  - rustls 0.23.20 -> 0.23.36

Resolves Dependabot PRs #743 and #733
Tested: cargo check and cargo check --release pass successfully
2026-02-09 14:03:31 +01:00
Louis Erbkamm
c094db8464 Merge pull request #744 from louis-e/logic-changes-and-fixes
Logic changes and fixes
2026-02-09 12:41:40 +01:00
louis-e
9be9104c8d Address code review feedback 2026-02-09 12:38:10 +01:00
louis-e
8c0f0cc366 Address code review feedback 2026-02-09 11:55:53 +01:00
louis-e
403469dcb5 Address code review feedback 2026-02-09 11:44:40 +01:00
louis-e
e0674823fd Address code review feedback 2026-02-09 00:19:23 +01:00
louis-e
438328ec28 Address code review feedback 2026-02-08 23:09:23 +01:00
louis-e
d2a8f09487 Refactor world saving logic 2026-02-08 22:59:16 +01:00
louis-e
318ab1e26c Add floodfill limit 2026-02-08 21:56:12 +01:00
Louis Erbkamm
6adb8d050e Merge pull request #742 from louis-e/buildings-improvement
Buildings improvement
2026-02-08 19:43:17 +01:00
louis-e
0cc32e70b9 Fix minor details 2026-02-08 19:43:04 +01:00
louis-e
ed07de68a6 Fix balcony chair facing direction 2026-02-08 19:35:48 +01:00
louis-e
0fda04f2be Address code review feedback 2026-02-08 18:29:22 +01:00
louis-e
8598a1847b Address code review feedback 2026-02-08 18:19:03 +01:00
Louis Erbkamm
2422786607 Merge pull request #738 from louis-e/dependabot/cargo/cargo-f6ecf5c85a
Bump bytes from 1.9.0 to 1.11.1 in the cargo group across 1 directory
2026-02-08 18:17:32 +01:00
louis-e
4e2d886077 Address code review feedback 2026-02-08 17:22:53 +01:00
louis-e
4299863410 Address code review feedback 2026-02-08 16:57:54 +01:00
louis-e
b316f95030 Add balconies 2026-02-08 16:37:18 +01:00
louis-e
7cc53434a7 Window sills and shutters 2026-02-08 15:20:56 +01:00
louis-e
6bd17c937d Add more building structs and flat roof variety 2026-02-07 20:42:16 +01:00
louis-e
cf198f9e93 Add more tree variations 2026-02-07 17:20:17 +01:00
louis-e
bedf2b763a Address code review feedback 2026-02-07 15:50:43 +01:00
louis-e
02823134df Fix cargo fmt and clippy 2026-02-07 14:18:32 +01:00
louis-e
36e1c04e6f Add more predefined building structs 2026-02-07 14:11:02 +01:00
louis-e
333ed52e28 Add place tag handling 2026-02-07 01:02:11 +01:00
louis-e
e9c8f203a7 Don't use oak planks for building material 2026-02-07 00:56:57 +01:00
louis-e
87069665fe Improve coverage of roof generation 2026-02-07 00:40:29 +01:00
louis-e
5bfb8606e2 Initial changes for improving buildings 2026-02-06 23:12:01 +01:00
Louis Erbkamm
44023f99e2 Merge pull request #734 from louis-e/dependabot/cargo/tauri-plugin-shell-2.3.0 2026-02-04 20:49:44 +01:00
dependabot[bot]
3aad272c20 Bump bytes from 1.9.0 to 1.11.1 in the cargo group across 1 directory
Bumps the cargo group with 1 update in the / directory: [bytes](https://github.com/tokio-rs/bytes).


Updates `bytes` from 1.9.0 to 1.11.1
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.9.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 21:17:25 +00:00
louis-e
9f47b0269b Add chimneys 2026-02-02 19:51:38 +01:00
Louis Erbkamm
c7e1fec02c Merge pull request #736 from louis-e/more-processors
More processors
2026-02-01 20:24:49 +01:00
louis-e
fb05e2f2b8 Implement UrbanGroundLookup 2026-02-01 19:42:01 +01:00
louis-e
7015cfff5f Correct comments 2026-02-01 19:30:10 +01:00
louis-e
78ca5a49ce Address code review feedback 2026-02-01 19:19:51 +01:00
louis-e
e265f8fa7e Address code review feedback 2026-02-01 19:07:33 +01:00
louis-e
552f4ab013 Abandon boundary element and add rubber band detection 2026-02-01 19:01:15 +01:00
louis-e
319eb656ee GUI option to disable boundary element 2026-02-01 16:40:47 +01:00
louis-e
11a756ab06 Fix overflow 2026-02-01 16:21:39 +01:00
louis-e
0f93853dcb Enhance urban densitiy calculation speed 2026-02-01 16:13:25 +01:00
louis-e
b4c47f559c Address code review feedback 2026-02-01 16:03:26 +01:00
louis-e
a86e23129b Address code review feedback 2026-02-01 15:52:15 +01:00
louis-e
69b30ef59f Fix underground building:part and cargo fmt 2026-02-01 15:51:21 +01:00
louis-e
1733f5d664 Enhance urban density grid 2026-02-01 15:39:01 +01:00
louis-e
e6b6de27ff Determine boundary validity 2026-02-01 14:12:33 +01:00
louis-e
ac0fc275dc Enhance OSM query 2026-02-01 13:46:52 +01:00
louis-e
de1f52bfaf Fix power base 2026-02-01 13:15:13 +01:00
dependabot[bot]
851aec71d0 Bump tauri-plugin-shell from 2.2.1 to 2.3.0
Bumps [tauri-plugin-shell](https://github.com/tauri-apps/plugins-workspace) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/tauri-apps/plugins-workspace/releases)
- [Commits](https://github.com/tauri-apps/plugins-workspace/compare/os-v2.2.1...os-v2.3.0)

---
updated-dependencies:
- dependency-name: tauri-plugin-shell
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 02:48:30 +00:00
louis-e
03cc86f3e2 Add more element processors 2026-02-01 03:14:45 +01:00
Louis Erbkamm
d4af5ce7ef Merge pull request #732 from louis-e/copy-coords
Copy coordinates on right click
2026-02-01 01:09:08 +01:00
louis-e
382ab19a0d Remove keyframe animation 2026-02-01 01:08:51 +01:00
louis-e
38678deefc Copy coordinates on right click 2026-02-01 01:04:37 +01:00
Louis Erbkamm
674a2d9656 Merge pull request #731 from louis-e/add-boundaries
Add boundaries for city ground blocks
2026-02-01 00:23:16 +01:00
louis-e
c4ad3dd61a Fix cargo fmt 2026-02-01 00:21:11 +01:00
louis-e
e42bc121fa Address code review feedback 2026-02-01 00:18:14 +01:00
louis-e
19da1fe55d Address code review feedback 2026-02-01 00:02:11 +01:00
louis-e
663394f3b5 Remove debug files 2026-01-31 23:55:03 +01:00
louis-e
bef7bd3965 Fix wrong clipping of boundaries 2026-01-31 23:54:24 +01:00
Louis Erbkamm
d8e1b29146 Merge branch 'main' into add-boundaries 2026-01-31 18:56:46 +01:00
louis-e
a9f53b2cd6 Add boundaries for city ground blocks 2026-01-31 18:56:25 +01:00
Louis Erbkamm
516b9ecf33 Merge pull request #730 from louis-e/save-faster
Improve saving
2026-01-31 18:42:14 +01:00
louis-e
babe610cca Remove unused vars 2026-01-31 18:21:36 +01:00
louis-e
20922a3be6 Improve saving 2026-01-31 18:18:20 +01:00
Louis Erbkamm
f1dc3b8ffb Merge pull request #675 from wielandb/entities
Add recycling containers (and ability to add entities during world generation)
2026-01-27 18:23:14 +01:00
Louis Erbkamm
602767c1d1 Remove unreachable wheat seeds in amenities 2026-01-27 17:41:49 +01:00
Louis Erbkamm
96e6d9e129 Fix Bedrock LevelDB lock error 2026-01-27 17:32:37 +01:00
Louis Erbkamm
c722ea689f Fix typos in interior comments 2026-01-27 17:31:51 +01:00
Louis Erbkamm
f473e980a2 Merge pull request #662 from amir16yp/enable_structures
set generate_features to 1 in level.dat nbt template
2026-01-27 16:46:43 +01:00
Wieland
1901c21049 Merge branch 'wielandb-modified' into entities 2026-01-26 00:07:30 +01:00
Wieland
bc41838671 Resolve merge conflicts 2026-01-25 23:57:20 +01:00
Wieland
11de6cfd85 Resolve merge conflict in wkt.parser.js 2026-01-25 23:45:27 +01:00
Wieland
92f629fc96 Merge pull request #2 from wielandb/buildings-enhancements
[to modified] Buildings enhancements
2026-01-25 23:37:18 +01:00
Wieland
880d86971d Refactor abandoned building checks and comments
Replaces map_or with is_some_and for abandoned tag check in generate_buildings for clarity. Also updates a doc comment to a regular comment in buildings_interior.rs for consistency.
2026-01-25 23:31:04 +01:00
Wieland
1421247ea4 run cargo fmt 2026-01-25 23:24:59 +01:00
Wieland
1b21dec366 Merge origin/main into buildings-enhancements 2026-01-25 23:20:18 +01:00
Wieland
c9a9d55f76 Merge pull request #1 from wielandb/trees
[to modified] Change what trees are generated based on tag content
2026-01-25 23:10:40 +01:00
Wieland
53846a7b5a cargo fmt check 2026-01-25 22:58:45 +01:00
Wieland
0593615909 Merge branch 'wielandb-modified' into trees 2026-01-25 22:52:29 +01:00
Louis Erbkamm
f79b610c0d Merge pull request #721 from louis-e/minor-improvements
Minor fixes and improvements
2026-01-20 20:57:25 +01:00
louis-e
c62600e972 Fix jumping progress bar 2026-01-20 20:56:46 +01:00
louis-e
225cb79381 Feature gate send_log 2026-01-20 20:35:48 +01:00
louis-e
9fd1868d41 Parallelize terrain processing 2026-01-20 19:47:16 +01:00
louis-e
ceb0c80fba Log unexpected request errors to telemetry 2026-01-20 19:33:34 +01:00
louis-e
6444a4498a Save Bedrock .mcworld to Desktop, enable Info-level logging 2026-01-20 19:33:23 +01:00
louis-e
6ef8169d45 Add tile download retry, optimize outlier filtering, add cache cleanup 2026-01-20 19:33:04 +01:00
louis-e
568a6063f7 UI improvements and tile cache max age 2026-01-20 19:06:50 +01:00
Louis Erbkamm
6cdebbed78 Merge pull request #714 from louis-e/revert-sequential-streaming
Revert sequential streaming saving since generation speed was affected
2026-01-11 14:36:44 +01:00
Louis Erbkamm
5291f72215 Add press assets link 2026-01-11 14:30:25 +01:00
louis-e
c24e22b790 Revert to sequential streaming saving since generation speed was affected 2026-01-11 14:19:35 +01:00
Louis Erbkamm
d4f324fd96 Migrate macOS 13 to macOS 15 Intel runner 2026-01-11 04:07:23 +01:00
Louis Erbkamm
e7e65d0e6f Merge pull request #712 from louis-e/ui-refactor
UI refactor
2026-01-11 03:58:48 +01:00
louis-e
927aaec22d Fix mixed language 2026-01-11 03:51:35 +01:00
louis-e
5ec942dbd1 Fall back to current dir if mc dir does not exist 2026-01-11 03:46:17 +01:00
louis-e
19bba3cc26 Fall back to current dir if mc dir does not exist 2026-01-11 03:40:39 +01:00
louis-e
17d6d323fc Refactor UI 2026-01-11 03:30:37 +01:00
Louis Erbkamm
236072dc42 Merge pull request #711 from louis-e/tree-building-overlap-fix
Lookup building footprint in tree generation
2026-01-11 02:07:46 +01:00
louis-e
7a8226923a Protect relation inner from tree spawning 2026-01-11 02:03:14 +01:00
louis-e
107ab70602 Fix overflow issues 2026-01-11 01:50:17 +01:00
louis-e
1364d96291 Address code review feedback 2026-01-11 01:39:36 +01:00
louis-e
b74b5c5ccb Lookup building footprint in tree generation 2026-01-11 01:20:27 +01:00
Louis Erbkamm
dd8004b159 Merge pull request #710 from louis-e/ui-enhancements
Add UI tooltips and move bbox info box
2026-01-11 00:51:44 +01:00
louis-e
b0845ce1df Merge branch 'ui-enhancements' of https://github.com/louis-e/arnis into ui-enhancements 2026-01-11 00:50:02 +01:00
louis-e
fc540db4cd Unify displayBboxSizeStatus function 2026-01-11 00:48:32 +01:00
Louis Erbkamm
1ecdffc039 Merge branch 'main' into ui-enhancements 2026-01-11 00:40:32 +01:00
louis-e
9ea34b9911 Add UI tooltips and move bbox info box 2026-01-11 00:40:04 +01:00
Louis Erbkamm
48248aad05 Merge pull request #708 from louis-e/spawn-point-improvement
Add default spawn point
2026-01-10 23:05:47 +01:00
louis-e
169545d937 Address code review feedback 2026-01-10 22:53:14 +01:00
louis-e
fba331232b Fix Bedrock spawn Y calc 2026-01-10 22:18:27 +01:00
louis-e
b02a2783c1 Address code review feedback 2026-01-10 19:19:20 +01:00
louis-e
dbc4741b78 Add minecraft prefix to blocks 2026-01-10 19:11:12 +01:00
louis-e
b52485badc Add ferns 2026-01-10 19:04:23 +01:00
Louis Erbkamm
447416f6ce Merge branch 'main' into spawn-point-improvement 2026-01-10 19:00:59 +01:00
louis-e
d26b23937e Add default spawn point 2026-01-10 18:59:35 +01:00
Louis Erbkamm
5e01abc5b6 Merge pull request #707 from louis-e/ui-improvements
UI improvements
2026-01-10 18:41:22 +01:00
louis-e
7c808ec352 Address code review feedback 2026-01-10 18:36:43 +01:00
louis-e
b757c5acf4 Better icons and map overlay fix 2026-01-10 17:56:37 +01:00
louis-e
ced5fc274e More landuse variations 2026-01-10 17:27:10 +01:00
Louis Erbkamm
295ca415d7 Merge pull request #660 from louis-e/copilot/fix-spawn-point-y-coordinate
Fix spawn point Y coordinate update for longitude values outside ±90°
2026-01-10 13:20:46 +01:00
Louis Erbkamm
e2b4ca8bdb Merge branch 'main' into copilot/fix-spawn-point-y-coordinate 2026-01-10 13:15:04 +01:00
Louis Erbkamm
07105f0208 Merge pull request #703 from louis-e/no-internet-warning
Send proper error for no internet
2026-01-08 23:36:36 +01:00
louis-e
ad57fdbc3a Send proper error for no internet 2026-01-08 23:32:39 +01:00
Louis Erbkamm
550870d9e0 Merge pull request #702 from louis-e/better-elevation
Implement more realistic elevation
2026-01-08 23:14:22 +01:00
louis-e
bd693ea007 Reduce log lines 2026-01-08 23:13:46 +01:00
louis-e
ce8f343414 Sample less points in bridge calc 2026-01-08 23:01:12 +01:00
louis-e
f882145780 Improve efficiency of ground generation 2026-01-08 22:42:06 +01:00
louis-e
b52d750935 Address code review feedback 2026-01-08 22:20:17 +01:00
louis-e
4d30899909 Remove snow again 2026-01-08 21:23:18 +01:00
louis-e
311610a717 Use less operations for better efficiency 2026-01-08 20:53:00 +01:00
louis-e
b4902ebc9e Add snow on top of mountains and address code review feedback 2026-01-08 20:39:56 +01:00
louis-e
e5bbb3e4a0 Address code review feedback 2026-01-08 20:03:15 +01:00
louis-e
0238cfe2d0 Implement more realistic elevation 2026-01-08 19:52:03 +01:00
Louis Erbkamm
2d9892fe7f Merge pull request #699 from louis-e/streaming-save-and-memory-reduction
Streaming save and memory reduction
2026-01-06 23:54:04 +01:00
louis-e
b858ce4691 Address code review feedback 2026-01-06 23:14:54 +01:00
louis-e
e031e53492 Address code review feedback 2026-01-06 23:05:20 +01:00
louis-e
6fb9b8943d Address code review feedback 2026-01-06 22:56:30 +01:00
louis-e
18266dd459 Comment out water area timeout warning 2026-01-06 22:39:32 +01:00
louis-e
b1940fa412 Add deterministic RNG for consistent element generation 2026-01-06 22:39:17 +01:00
louis-e
d57a732055 Drop elements and flood fill cache entries after processing 2026-01-06 22:38:55 +01:00
louis-e
4e52b38f5a stream regions during save to reduce peak memory 2026-01-06 22:38:40 +01:00
Louis Erbkamm
feb4317086 Merge pull request #698 from louis-e/floodfill-precompute-afterfix
Afterfixes for floodfill precomputation
2026-01-06 20:04:04 +01:00
louis-e
d02cbed997 Afterfixes for floodfill precomputation 2026-01-06 20:03:42 +01:00
Louis Erbkamm
99d1f8e117 Merge pull request #696 from louis-e/floodfill-precompute
Add multithreaded precomputation of floodfill
2026-01-06 20:00:13 +01:00
louis-e
6fa76bc381 Add clarifying comment 2026-01-06 19:43:52 +01:00
louis-e
0fef27e6af Address code review feedback 2026-01-06 19:36:33 +01:00
louis-e
fa3384cf86 Address code review feedback 2026-01-06 19:25:13 +01:00
louis-e
ffbc5e5788 Remove floodfill timeout parameter 2026-01-06 18:21:21 +01:00
louis-e
4215e7644c Remove floodfill timeout parameter and refine minor changes 2026-01-06 18:18:56 +01:00
louis-e
118335bad4 Add multithreaded precomputation of floodfill 2026-01-06 18:11:59 +01:00
Louis Erbkamm
7bbee28279 Merge pull request #695 from louis-e/ground-gen-cache-locality
perf: improve ground generation cache locality
2026-01-06 17:15:23 +01:00
louis-e
9cb35a3b13 perf: improve ground generation cache locality 2026-01-06 17:11:47 +01:00
Louis Erbkamm
4fecf98c54 Merge pull request #694 from louis-e/tiny-release-memory
Release memory on a few occasions
2026-01-06 17:01:48 +01:00
louis-e
47a7b81f99 Release memory on a few occasions 2026-01-06 17:01:27 +01:00
Louis Erbkamm
7ec90b4fef Merge pull request #692 from louis-e/codex/refactor-data-parsing-and-memory-usage
Stream OSM parsing and reduce cloning
2026-01-06 16:46:20 +01:00
louis-e
f1f3fb287a optimize tags cloning and fix Arc usage in OSM parser 2026-01-06 16:40:44 +01:00
Louis Erbkamm
b23658d5ef Merge branch 'main' into codex/refactor-data-parsing-and-memory-usage 2026-01-06 16:29:30 +01:00
Louis Erbkamm
cc89576828 Merge pull request #690 from louis-e/codex/change-ground-field-to-arcground
Use Arc<Ground> for WorldEditor ground reference
2026-01-06 16:09:31 +01:00
louis-e
809fa23941 Use Arc<Ground> insteaf of Box<Ground> in BedrockWriter 2026-01-06 16:00:32 +01:00
Louis Erbkamm
51ad1fef3f Stream OSM parsing and reduce cloning 2026-01-06 15:42:40 +01:00
Louis Erbkamm
8e8d8e0567 Use Arc for world editor ground 2026-01-06 15:42:34 +01:00
Louis Erbkamm
da6f23c0a2 Merge pull request #688 from louis-e/parallel-tile-download
Prallelize AWS terrain tile downloads
2026-01-02 14:15:24 +01:00
louis-e
d4a872989c Fix cargo fmt and clippy 2026-01-02 13:48:02 +01:00
louis-e
2a5a5230c5 Apply code review feedback 2026-01-02 13:43:38 +01:00
louis-e
9018584b1d Prallelize AWS terrain tile downloads 2026-01-02 13:33:03 +01:00
Louis Erbkamm
9eda39846c Merge pull request #687 from louis-e/disk-space-check
Disk space check
2026-01-01 22:18:36 +01:00
Louis Erbkamm
5e9d6795df Merge branch 'main' into disk-space-check 2026-01-01 22:14:31 +01:00
Louis Erbkamm
54a7a4f2a9 Merge pull request #686 from louis-e/world-editor-crash-robustness
fix: clamp Y coords and ensure region dir exists
2026-01-01 22:14:17 +01:00
Louis Erbkamm
d0d65643f5 Use idempotent create_dir_all() instead of exists() call
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-01 22:13:51 +01:00
louis-e
946fd43a5e fix: add 3GB disk space check before generation 2026-01-01 22:00:33 +01:00
Louis Erbkamm
05e5ffdd2a Merge branch 'main' into world-editor-crash-robustness 2026-01-01 17:43:52 +01:00
Louis Erbkamm
0b7e27df7f Merge pull request #685 from louis-e/bedrock-session-lock
fix: skip session lock for bedrock world generation
2026-01-01 17:42:21 +01:00
louis-e
613a410c93 fix: clamp Y coords and ensure region dir exists 2026-01-01 17:42:08 +01:00
louis-e
faefd29e30 fix: skip session lock for bedrock world generation 2026-01-01 17:32:03 +01:00
Louis Erbkamm
9ad6c75440 Merge pull request #682 from louis-e/dependabot/github_actions/actions/upload-artifact-6
build(deps): bump actions/upload-artifact from 4 to 6
2026-01-01 17:24:09 +01:00
Louis Erbkamm
e51f28f067 Merge pull request #683 from louis-e/dependabot/github_actions/actions/download-artifact-7
build(deps): bump actions/download-artifact from 5 to 7
2026-01-01 17:23:37 +01:00
Louis Erbkamm
47ddb9b211 Merge pull request #684 from louis-e/dependabot/cargo/rfd-0.16.0
build(deps): bump rfd from 0.15.4 to 0.16.0
2026-01-01 17:23:18 +01:00
dependabot[bot]
46415bb002 build(deps): bump rfd from 0.15.4 to 0.16.0
Bumps [rfd](https://github.com/PolyMeilex/rfd) from 0.15.4 to 0.16.0.
- [Release notes](https://github.com/PolyMeilex/rfd/releases)
- [Changelog](https://github.com/PolyMeilex/rfd/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PolyMeilex/rfd/compare/0.15.4...0.16.0)

---
updated-dependencies:
- dependency-name: rfd
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 02:10:17 +00:00
dependabot[bot]
0683dd3343 build(deps): bump actions/download-artifact from 5 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 02:09:53 +00:00
dependabot[bot]
4d304dc978 build(deps): bump actions/upload-artifact from 4 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1
.envrc Normal file
View File

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

2
.gitattributes vendored
View File

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

View File

@@ -11,13 +11,13 @@ assignees: ''
A clear and concise description of what the bug is and what you expected to happen.
**Used bbox area**
Please provide your input parameters so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
Please provide your input parameters (BBOX) so we can reproduce the issue. *(For example: 48.133444 11.569462 48.142609 11.584740)*
**Arnis and Minecraft version**
Please tell us what version of Arnis and Minecraft you used.
Please tell us what version of Arnis and Minecraft you used, as well as if you are on Windows, Linux or MacOS.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here. If you used any more custom settings, please provide them here too. If you experienced any issue with the application itself like a crash, please provide the log file which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs
Add any other context about the problem here. If you used any more custom settings, please provide them here too. Please provide the log file if possible as well, which can be found at C:\Users\USERNAME\AppData\Local\com.louisdev.arnis\logs

View File

@@ -18,13 +18,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: clippy
components: clippy, rustfmt
- name: Install Linux dependencies
run: |
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@v1
@@ -43,7 +43,7 @@ jobs:
- name: Run benchmark command with memory tracking
id: benchmark
run: |
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.101470,11.517792,48.168375,11.626968" 2> benchmark_log.txt
/usr/bin/time -v ./target/release/arnis --path="./world" --terrain --bbox="48.125768 11.552296 48.148565 11.593838" 2> benchmark_log.txt
grep "Maximum resident set size" benchmark_log.txt | awk '{print $6}' > peak_mem_kb.txt
peak_kb=$(cat peak_mem_kb.txt)
peak_mb=$((peak_kb / 1024))
@@ -65,7 +65,7 @@ jobs:
seconds=$((duration % 60))
peak_mem=${{ steps.benchmark.outputs.peak_memory }}
baseline_time=135
baseline_time=30
diff=$((duration - baseline_time))
abs_diff=${diff#-}
@@ -79,7 +79,7 @@ jobs:
verdict="🚨 This PR **drastically worsens generation time**."
fi
baseline_mem=5865
baseline_mem=935
mem_annotation=""
if [ "$peak_mem" -gt 2000 ]; then
mem_diff=$((peak_mem - baseline_mem))
@@ -87,6 +87,8 @@ jobs:
mem_annotation=" (↗ ${mem_percent}% more)"
fi
benchmark_time=$(date -u "+%Y-%m-%d %H:%M:%S UTC")
{
echo "summary<<EOF"
echo "⏱️ Benchmark run finished in **${minutes}m ${seconds}s**"
@@ -98,6 +100,8 @@ jobs:
echo ""
echo "${verdict}"
echo ""
echo "📅 **Last benchmark:** ${benchmark_time}"
echo ""
echo "_You can retrigger the benchmark by commenting \`retrigger-benchmark\`._"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -108,4 +112,4 @@ jobs:
message: ${{ steps.comment_body.outputs.summary }}
comment-tag: benchmark-report
env:
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}
GITHUB_TOKEN: ${{ secrets.BENCHMARK_TOKEN }}

View File

@@ -17,7 +17,7 @@ jobs:
target: x86_64-unknown-linux-gnu
binary_name: arnis
asset_name: arnis-linux
- os: macos-13 # Intel runner for x86_64 builds
- os: macos-15-intel # Intel runner for x86_64 builds
target: x86_64-apple-darwin
binary_name: arnis
asset_name: arnis-mac-intel
@@ -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
@@ -87,7 +87,7 @@ jobs:
shell: powershell
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.os }}-${{ matrix.target }}-build
path: target/release/${{ matrix.asset_name }}
@@ -97,13 +97,13 @@ jobs:
runs-on: macos-latest
steps:
- name: Download macOS Intel build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: macos-13-x86_64-apple-darwin-build
name: macos-15-intel-x86_64-apple-darwin-build
path: ./intel
- name: Download macOS ARM64 build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: macos-latest-aarch64-apple-darwin-build
path: ./arm64
@@ -114,7 +114,7 @@ jobs:
chmod +x arnis-mac-universal
- name: Upload universal binary
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: macos-universal-build
path: arnis-mac-universal
@@ -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@v7
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@v7
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@v7
with:
name: macos-universal-build
path: ./builds/macos
@@ -157,4 +157,4 @@ jobs:
builds/linux/arnis-linux
builds/macos/arnis-mac-universal
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

2
.gitignore vendored
View File

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

3442
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "arnis"
version = "2.3.0"
version = "2.4.1"
edition = "2021"
description = "Arnis - Generate real life cities in Minecraft"
homepage = "https://github.com/louis-e/arnis"
@@ -14,40 +14,50 @@ overflow-checks = true
[features]
default = ["gui"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build"]
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "tauri-build", "bedrock"]
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
[build-dependencies]
tauri-build = {version = "2", optional = true}
[dependencies]
base64 = "0.22.1"
byteorder = { version = "1.5", optional = true }
clap = { version = "4.5", features = ["derive", "env"] }
colored = "3.0.0"
dirs = {version = "6.0.0", optional = true }
fastanvil = "0.31.0"
fastnbt = "2.5.0"
dirs = "6.0.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"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.8.5"
rand_chacha = "0.3"
rayon = "1.10.0"
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
rfd = { version = "0.15.4", optional = true }
semver = "1.0.23"
reqwest = { version = "0.13.1", features = ["blocking", "json", "query"] }
rfd = { version = "0.16.0", optional = true }
semver = "1.0.27"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2", optional = true }
tauri-plugin-log = { version = "2.6.0", optional = true }
tauri-plugin-shell = { version = "2", optional = true }
tokio = { version = "1.47.0", features = ["full"], optional = true }
tokio = { version = "1.48.0", features = ["full"], optional = true }
bedrockrs_level = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_level", optional = true }
bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs", package = "bedrockrs_shared", optional = true }
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
vek = { version = "0.17", optional = true }
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
rusty-leveldb = { version = "3", optional = true }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
[dev-dependencies]
tempfile = "3.20.0"
tempfile = "3.23.0"

View File

@@ -1,17 +1,17 @@
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/banner.png?raw=true" width="100%" alt="Banner">
<img src="assets/git/banner.png" width="100%" alt="Banner">
# Arnis [![CI Build Status](https://github.com/louis-e/arnis/actions/workflows/ci-build.yml/badge.svg)](https://github.com/louis-e/arnis/actions) [<img alt="GitHub Release" src="https://img.shields.io/github/v/release/louis-e/arnis" />](https://github.com/louis-e/arnis/releases) [<img alt="GitHub Downloads (all assets, all releases" src="https://img.shields.io/github/downloads/louis-e/arnis/total" />](https://github.com/louis-e/arnis/releases) [![Download here](https://img.shields.io/badge/Download-here-green)](https://github.com/louis-e/arnis/releases) [![Discord](https://img.shields.io/discord/1326192999738249267?label=Discord&color=%237289da)](https://discord.gg/mA2g69Fhxq)
Arnis creates complex and accurate Minecraft Java Edition worlds that reflect real-world geography, topography, and architecture.
Arnis creates complex and accurate Minecraft Java Edition (1.17+) and Bedrock Edition worlds that reflect real-world geography, topography, and architecture.
This free and open source project is designed to handle large-scale geographic data from the real world and generate detailed Minecraft worlds. The algorithm processes geospatial data from OpenStreetMap as well as elevation data to create an accurate Minecraft representation of terrain and architecture.
Generate your hometown, big cities, and natural landscapes with ease!
![Minecraft Preview](https://raw.githubusercontent.com/louis-e/arnis/refs/heads/main/gitassets/preview.jpg)
<i>This Github page is the official project website. Do not download Arnis from any other website.</i>
![Minecraft Preview](assets/git/preview.jpg)
<i>This Github page and [arnismc.com](https://arnismc.com) are the only official project websites. Do not download Arnis from any other website.</i>
## :keyboard: Usage
<img width="60%" src="https://github.com/louis-e/arnis/blob/main/gitassets/gui.png?raw=true"><br>
<img width="60%" src="assets/git/gui.png"><br>
Download the [latest release](https://github.com/louis-e/arnis/releases/) or [compile](#trophy-open-source) the project on your own.
Choose your area on the map using the rectangle tool and select your Minecraft world - then simply click on <i>Start Generation</i>!
@@ -19,7 +19,7 @@ Additionally, you can customize various generation settings, such as world scale
## 📚 Documentation
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/documentation.png?raw=true" width="100%" alt="Banner">
<img src="assets/git/documentation.png" width="100%" alt="Banner">
Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/arnis/wiki/), covering topics such as technical explanations, FAQs, contribution guidelines and roadmaps.
@@ -34,7 +34,7 @@ Full documentation is available in the [GitHub Wiki](https://github.com/louis-e/
#### How to contribute
This project is open source and welcomes contributions from everyone! Whether you're interested in fixing bugs, improving performance, adding new features, or enhancing documentation, your input is valuable. Simply fork the repository, make your changes, and submit a pull request. Please respect the above mentioned key objectives. Contributions of all levels are appreciated, and your efforts help improve this tool for everyone.
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lng,min_lat,max_lng,max_lat"```<br>
Command line Build: ```cargo run --no-default-features -- --terrain --path="C:/YOUR_PATH/.minecraft/saves/worldname" --bbox="min_lat,min_lng,max_lat,max_lng"```<br>
GUI Build: ```cargo run```<br>
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
@@ -51,7 +51,7 @@ After your pull request was merged, I will take care of regularly creating updat
## :newspaper: Academic & Press Recognition
<img src="https://github.com/louis-e/arnis/blob/main/gitassets/recognition.png?raw=true" width="100%" alt="Banner">
<img src="assets/git/recognition.png" width="100%" alt="Banner">
Arnis has been recognized in various academic and press publications after gaining a lot of attention in December 2024.
@@ -63,6 +63,8 @@ Arnis has been recognized in various academic and press publications after gaini
[XDA Developers: Hometown Minecraft Map: Arnis](https://www.xda-developers.com/hometown-minecraft-map-arnis/)
Free to use assets, including screenshots and logos, can be found [here](https://drive.google.com/file/d/1T1IsZSyT8oa6qAO_40hVF5KR8eEVCJjo/view?usp=sharing).
## :copyright: License Information
Copyright (c) 2022-2025 Louis Erbkamm (louis-e)
@@ -78,7 +80,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

BIN
assets/minecraft/level.dat Normal file
View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

60
flake.lock generated Normal file
View File

@@ -0,0 +1,60 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1755615617,
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

36
flake.nix Normal file
View File

@@ -0,0 +1,36 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs/nixos-unstable";
};
outputs =
{
flake-utils,
nixpkgs,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
in
{
devShell = pkgs.mkShell.override { inherit stdenv; } {
buildInputs = with pkgs; [
openssl.dev
pkg-config
wayland
glib
gdk-pixbuf
pango
gtk3
libsoup_3.dev
webkitgtk_4_1.dev
];
};
}
);
}

99
flake/flake.lock generated
View File

@@ -1,99 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1724653830,
"narHash": "sha256-88f0KK8h6tGIP4Na5RJDKs0S+7WsGGaCGNkLj/bPV3g=",
"owner": "nix-community",
"repo": "fenix",
"rev": "9ecf5e7d800ace001320da8acadd4a3deb872a83",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1724479785,
"narHash": "sha256-pP3Azj5d6M5nmG68Fu4JqZmdGt4S4vqI5f8te+E/FTw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d0e1602ddde669d5beb01aec49d71a51937ed7be",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1724586512,
"narHash": "sha256-mrfwk6nO8N2WtCq3sB2zhd2QN1HMKzeSESzOA6lSsQg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "7106cd3be50b2a43c1d9f2787bf22d4369c2b25b",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,34 +0,0 @@
{
inputs = {
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs/nixos-unstable";
};
outputs = { self, fenix, flake-utils, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
fenixPkgs = (fenix.packages.${system}.stable);
in
{
devShell = pkgs.mkShell
{
buildInputs = with pkgs; [
openssl.dev
pkg-config
fenixPkgs.toolchain
wayland
glib
gdk-pixbuf
pango
gtk3
libsoup_3.dev
webkitgtk_4_1.dev
];
};
});
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,221 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="./css/styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arnis</title>
<script type="module" src="./js/main.js" defer></script>
<script type="module" src="./js/license.js" defer></script>
<script type="module" src="./js/language-selector.js" defer></script>
</head>
<body>
<main class="container">
<div class="row">
<a href="https://github.com/louis-e/arnis" target="_blank">
<img src="./images/logo.png" id="arnis-logo" class="logo arnis" alt="Arnis Logo" style="width: 35%; height: auto;">
</a>
</div>
<div class="flex-container">
<!-- Left Box: Map and BBox Input -->
<section class="section map-box" style="margin-bottom: 0; padding-bottom: 0;">
<h2 data-localize="select_location">Select Location</h2>
<span id="bbox-text" style="font-size: 1.0em; display: block; margin-top: -8px; margin-bottom: 3px;" data-localize="zoom_in_and_choose">
Zoom in and choose your area using the rectangle tool
</span>
<iframe src="maps.html" width="100%" height="300" class="map-container" title="Map Picker"></iframe>
<span id="bbox-info"
style="font-size: 0.75em; color: #7bd864; display: block; margin-bottom: 4px; font-weight: bold; min-height: 2em;"></span>
</section>
<!-- Right Box: Directory Selection, Start Button, and Progress Bar -->
<section class="section controls-box">
<div class="controls-content">
<h2 data-localize="select_world">Select World</h2>
<!-- Updated Tooltip Structure -->
<div class="tooltip" style="width: 100%;">
<button type="button" onclick="openWorldPicker()" style="padding: 10px; line-height: 1.2; width: 100%;">
<span id="choose_world">Choose World</span>
<br>
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
No world selected
</span>
</button>
</div>
<div class="button-container">
<button type="button" id="start-button" class="start-button" onclick="startGeneration()" data-localize="start_generation">Start Generation</button>
<button type="button" class="settings-button" onclick="openSettings()">
<i class="gear-icon"></i>
</button>
</div>
<br><br>
<div class="progress-section">
<h2 data-localize="progress">Progress</h2>
<div class="progress-bar-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="progress-status">
<span id="progress-message"></span>
<span id="progress-detail">0%</span>
</div>
</div>
</div>
</section>
</div>
<!-- World Picker Modal -->
<div id="world-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeWorldPicker()">&times;</span>
<h2 data-localize="choose_world_modal_title">Choose World</h2>
<button type="button" id="select-world-button" class="select-world-button" onclick="selectWorld(false)" data-localize="select_existing_world">Select existing world</button>
<button type="button" id="generate-world-button" class="generate-world-button" onclick="selectWorld(true)" data-localize="generate_new_world">Generate new world</button>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeSettings()">&times;</span>
<h2 data-localize="customization_settings">Customization Settings</h2>
<!-- Terrain Toggle Button -->
<div class="settings-row">
<label for="terrain-toggle" data-localize="terrain">Terrain</label>
<div class="settings-control">
<input type="checkbox" id="terrain-toggle" name="terrain-toggle" checked>
</div>
</div>
<!-- Interior Toggle Button -->
<div class="settings-row">
<label for="interior-toggle" data-localize="interior">Interior Generation</label>
<div class="settings-control">
<input type="checkbox" id="interior-toggle" name="interior-toggle" checked>
</div>
</div>
<!-- Roof Toggle Button -->
<div class="settings-row">
<label for="roof-toggle" data-localize="roof">Roof Generation</label>
<div class="settings-control">
<input type="checkbox" id="roof-toggle" name="roof-toggle" checked>
</div>
</div>
<!-- Fill ground Toggle Button -->
<div class="settings-row">
<label for="fillground-toggle" data-localize="fillground">Fill Ground</label>
<div class="settings-control">
<input type="checkbox" id="fillground-toggle" name="fillground-toggle">
</div>
</div>
<!-- World Scale Slider -->
<div class="settings-row">
<label for="scale-value-slider" data-localize="world_scale">World Scale</label>
<div class="settings-control">
<input type="range" id="scale-value-slider" name="scale-value-slider" min="0.30" max="2.5" step="0.1" value="1">
<span id="slider-value">1.00</span>
</div>
</div>
<!-- Bounding Box Input -->
<div class="settings-row">
<label for="bbox-coords" data-localize="custom_bounding_box">Custom Bounding Box</label>
<div class="settings-control">
<input type="text" id="bbox-coords" name="bbox-coords" maxlength="55" placeholder="Format: lat,lng,lat,lng">
</div>
</div>
<!-- Floodfill Timeout Input -->
<div class="settings-row">
<label for="floodfill-timeout" data-localize="floodfill_timeout">Floodfill Timeout (sec)</label>
<div class="settings-control">
<input type="number" id="floodfill-timeout" name="floodfill-timeout" min="0" step="1" value="20" placeholder="Seconds">
</div>
</div>
<!-- Map Theme Selector -->
<div class="settings-row">
<label for="tile-theme-select" data-localize="map_theme">Map Theme</label>
<div class="settings-control">
<select id="tile-theme-select" name="tile-theme-select" class="theme-dropdown">
<option value="osm">Standard</option>
<option value="esri-imagery">Satellite</option>
<option value="opentopomap">Topographic</option>
<option value="stadia-bright">Smooth Bright</option>
<option value="stadia-dark">Smooth Dark</option>
</select>
</div>
</div>
<!-- Language Selector -->
<div class="settings-row">
<label for="language-select" data-localize="language">Language</label>
<div class="settings-control">
<select id="language-select" name="language-select" class="language-dropdown">
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
<option value="fr-FR">Français</option>
<option value="ru">Русский</option>
<option value="zh-CN">中文 (简体)</option>
<option value="ko">한국어</option>
<option value="pl">Polski</option>
<option value="sv">Svenska</option>
<option value="ar">العربية</option>
<option value="fi">Suomi</option>
<option value="hu">Magyar</option>
<option value="lt">Lietuvių</option>
<option value="ua">Українська</option>
</select>
</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>
</div>
<!-- Buy Me a Coffee and Discord -->
<div class="settings-row buymeacoffee-row">
<a href="https://buymeacoffee.com/louisdev" target="_blank" class="buymeacoffee-link">
<img src="./images/buymeacoffee.png" alt="Buy Me a Coffee" class="buymeacoffee-logo">
</a>
<a href="https://discord.gg/mA2g69Fhxq" target="_blank" class="discord-link">
<img src="./images/discord.png" alt="Discord" class="discord-logo">
</a>
</div>
</div>
</div>
<!-- License Modal -->
<div id="license-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close-button" onclick="closeLicense()">&times;</span>
<h2 data-localize="license_and_credits">License and Credits</h2>
<div id="license-content" style="overflow-y: auto; max-height: 300px; font-size: 0.85em; line-height: 1.3; padding: 10px; border: 1px solid #ccc; border-radius: 4px;">
Loading...
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<a href="https://github.com/louis-e/arnis" target="_blank" class="footer-link" data-localize="footer_text">
© {year} Arnis v{version} by louis-e
</a>
</footer>
</main>
</body>
</html>

View File

Binary file not shown.

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
@@ -19,9 +19,14 @@ pub struct Args {
#[arg(long, group = "location")]
pub save_json_file: Option<String>,
/// Path to the Minecraft world (required)
#[arg(long, value_parser = validate_minecraft_world_path)]
pub path: String,
/// Output directory for the generated world (required for Java, optional for Bedrock).
/// Use --output-dir (or the deprecated --path alias) to specify where the world is created.
#[arg(long = "output-dir", alias = "path")]
pub path: Option<PathBuf>,
/// Generate a Bedrock Edition world (.mcworld) instead of Java Edition
#[arg(long)]
pub bedrock: bool,
/// Downloader method (requests/curl/wget) (optional)
#[arg(long, default_value = "requests")]
@@ -40,17 +45,23 @@ pub struct Args {
pub terrain: bool,
/// Enable interior generation (optional)
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
#[arg(long, default_value_t = true)]
pub interior: bool,
/// Enable roof generation (optional)
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
#[arg(long, default_value_t = true)]
pub roof: bool,
/// Enable filling ground (optional)
#[arg(long, default_value_t = false, action = clap::ArgAction::SetFalse)]
#[arg(long, default_value_t = false)]
pub fillground: bool,
/// Enable city boundary ground generation (optional)
/// When enabled, detects building clusters and places stone ground in urban areas.
/// Isolated buildings in rural areas will keep grass around them.
#[arg(long, default_value_t = true)]
pub city_boundaries: bool,
/// Enable debug mode (optional)
#[arg(long)]
pub debug: bool,
@@ -58,25 +69,43 @@ pub struct Args {
/// Set floodfill timeout (seconds) (optional)
#[arg(long, value_parser = parse_duration)]
pub timeout: Option<Duration>,
/// 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);
if !mc_world_path.exists() {
return Err(format!("Path does not exist: {path}"));
/// Validates CLI arguments after parsing.
/// For Java Edition: `--path` is required and must point to an existing directory
/// where a new world will be created automatically.
/// For Bedrock Edition (`--bedrock`): `--path` is optional (defaults to Desktop output).
pub fn validate_args(args: &Args) -> Result<(), String> {
if args.bedrock {
// Bedrock: path is optional; if provided, it must be an existing directory
if let Some(ref path) = args.path {
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
if !path.is_dir() {
return Err(format!("Path is not a directory: {}", path.display()));
}
}
} else {
// Java: path is required and must be an existing directory
match &args.path {
None => {
return Err(
"The --output-dir argument is required for Java Edition. Provide the directory where the world should be created. Use --bedrock for Bedrock Edition output."
.to_string(),
);
}
Some(ref path) => {
if !path.exists() {
return Err(format!("Path does not exist: {}", path.display()));
}
if !path.is_dir() {
return Err(format!("Path is not a directory: {}", path.display()));
}
}
}
}
if !mc_world_path.is_dir() {
return Err(format!("Path is not a directory: {path}"));
}
let region = mc_world_path.join("region");
if !region.is_dir() {
return Err(format!("No Minecraft world found at {region:?}"));
}
Ok(path.to_string())
Ok(())
}
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
@@ -88,22 +117,15 @@ fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntEr
mod tests {
use super::*;
fn minecraft_tmpdir() -> tempfile::TempDir {
let tmpdir = tempfile::tempdir().unwrap();
// create a `region` directory in the tempdir
let region_path = tmpdir.path().join("region");
std::fs::create_dir(&region_path).unwrap();
tmpdir
}
#[test]
fn test_flags() {
let tmpdir = minecraft_tmpdir();
let tmpdir = tempfile::tempdir().unwrap();
let tmp_path = tmpdir.path().to_str().unwrap();
// Test that terrain/debug are SetTrue
let cmd = [
"arnis",
"--path",
"--output-dir",
tmp_path,
"--bbox",
"1,2,3,4",
@@ -114,24 +136,81 @@ mod tests {
assert!(args.debug);
assert!(args.terrain);
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
let args = Args::parse_from(cmd.iter());
assert!(!args.debug);
assert!(!args.terrain);
assert!(!args.bedrock);
}
#[test]
fn test_bedrock_flag() {
// Bedrock mode doesn't require --output-dir
let cmd = ["arnis", "--bedrock", "--bbox", "1,2,3,4"];
let args = Args::parse_from(cmd.iter());
assert!(args.bedrock);
assert!(args.path.is_none());
assert!(validate_args(&args).is_ok());
}
#[test]
fn test_java_requires_path() {
let cmd = ["arnis", "--bbox", "1,2,3,4"];
let args = Args::parse_from(cmd.iter());
assert!(!args.bedrock);
assert!(args.path.is_none());
assert!(validate_args(&args).is_err());
}
#[test]
fn test_java_path_must_exist() {
let cmd = [
"arnis",
"--output-dir",
"/nonexistent/path",
"--bbox",
"1,2,3,4",
];
let args = Args::parse_from(cmd.iter());
let result = validate_args(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn test_bedrock_path_must_exist() {
let cmd = [
"arnis",
"--bedrock",
"--output-dir",
"/nonexistent/path",
"--bbox",
"1,2,3,4",
];
let args = Args::parse_from(cmd.iter());
let result = validate_args(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn test_required_options() {
let tmpdir = minecraft_tmpdir();
let tmpdir = tempfile::tempdir().unwrap();
let tmp_path = tmpdir.path().to_str().unwrap();
let cmd = ["arnis"];
assert!(Args::try_parse_from(cmd.iter()).is_err());
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
assert!(Args::try_parse_from(cmd.iter()).is_ok());
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
let args = Args::try_parse_from(cmd.iter()).unwrap();
assert!(validate_args(&args).is_ok());
let cmd = ["arnis", "--path", tmp_path, "--file", ""];
// Verify --path still works as a deprecated alias
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
let args = Args::try_parse_from(cmd.iter()).unwrap();
assert!(validate_args(&args).is_ok());
let cmd = ["arnis", "--output-dir", tmp_path, "--file", ""];
assert!(Args::try_parse_from(cmd.iter()).is_err());
// The --gui flag isn't used here, ugh. TODO clean up main.rs and its argparse usage.

1234
src/bedrock_block_map.rs Normal file
View File

File diff suppressed because it is too large Load Diff

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
@@ -266,6 +266,67 @@ impl Block {
185 => "quartz_stairs",
186 => "polished_andesite_stairs",
187 => "nether_brick_stairs",
188 => "barrel",
189 => "fern",
190 => "cobweb",
191 => "chiseled_bookshelf",
192 => "chiseled_bookshelf",
193 => "chiseled_bookshelf",
194 => "chiseled_bookshelf",
195 => "chipped_anvil",
196 => "damaged_anvil",
197 => "large_fern",
198 => "large_fern",
199 => "chain",
200 => "end_rod",
201 => "lightning_rod",
202 => "gold_block",
203 => "sea_lantern",
204 => "orange_concrete",
205 => "orange_wool",
206 => "blue_wool",
207 => "green_concrete",
208 => "brick_wall",
209 => "redstone_block",
210 => "chain",
211 => "chain",
212 => "spruce_door",
213 => "spruce_door",
214 => "smooth_stone_slab",
215 => "glass_pane",
216 => "light_gray_terracotta",
217 => "oak_slab",
218 => "oak_door",
219 => "dark_oak_log",
220 => "dark_oak_leaves",
221 => "jungle_log",
222 => "jungle_leaves",
223 => "acacia_log",
224 => "acacia_leaves",
225 => "spruce_leaves",
226 => "cyan_stained_glass",
227 => "blue_stained_glass",
228 => "light_blue_stained_glass",
229 => "daylight_detector",
230 => "red_stained_glass",
231 => "yellow_stained_glass",
232 => "purple_stained_glass",
233 => "orange_stained_glass",
234 => "magenta_stained_glass",
235 => "potted_poppy",
236 => "oak_trapdoor",
237 => "oak_trapdoor",
238 => "oak_trapdoor",
239 => "oak_trapdoor",
240 => "quartz_slab",
241 => "dark_oak_trapdoor",
242 => "spruce_trapdoor",
243 => "birch_trapdoor",
244 => "mud_brick_slab",
245 => "brick_slab",
246 => "potted_red_tulip",
247 => "potted_dandelion",
248 => "potted_blue_orchid",
_ => panic!("Invalid id"),
}
}
@@ -324,6 +385,13 @@ impl Block {
map
})),
// Oak door lower
159 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("lower".to_string()));
map
})),
116 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert(
@@ -463,6 +531,140 @@ impl Block {
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
191 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("north".to_string()));
map
})),
192 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("east".to_string()));
map
})),
193 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("south".to_string()));
map
})),
194 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("west".to_string()));
map
})),
197 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("lower".to_string()));
map
})),
198 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
210 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("axis".to_string(), Value::String("x".to_string()));
map
})),
211 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("axis".to_string(), Value::String("z".to_string()));
map
})),
// Spruce door lower
212 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("lower".to_string()));
map
})),
// Spruce door upper
213 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
// Smooth stone slab (bottom by default)
214 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("type".to_string(), Value::String("bottom".to_string()));
map
})),
// Oak slab top
217 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("type".to_string(), Value::String("top".to_string()));
map
})),
// Oak door upper
218 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("half".to_string(), Value::String("upper".to_string()));
map
})),
// Dark oak leaves
220 => Some(Value::Compound({
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("persistent".to_string(), Value::String("true".to_string()));
map
})),
// Jungle leaves
222 => Some(Value::Compound({
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("persistent".to_string(), Value::String("true".to_string()));
map
})),
// Acacia leaves
224 => Some(Value::Compound({
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("persistent".to_string(), Value::String("true".to_string()));
map
})),
// Spruce leaves
225 => Some(Value::Compound({
let mut map: HashMap<String, Value> = HashMap::new();
map.insert("persistent".to_string(), Value::String("true".to_string()));
map
})),
// Quartz slab (top half) used as window sill
240 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("type".to_string(), Value::String("top".to_string()));
map
})),
// Open oak trapdoor facing north (hangs flat against wall, looks like shutter)
236 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("north".to_string()));
map.insert("open".to_string(), Value::String("true".to_string()));
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
// Open oak trapdoor facing south
237 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("south".to_string()));
map.insert("open".to_string(), Value::String("true".to_string()));
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
// Open oak trapdoor facing east
238 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("east".to_string()));
map.insert("open".to_string(), Value::String("true".to_string()));
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
// Open oak trapdoor facing west
239 => Some(Value::Compound({
let mut map = HashMap::new();
map.insert("facing".to_string(), Value::String("west".to_string()));
map.insert("open".to_string(), Value::String("true".to_string()));
map.insert("half".to_string(), Value::String("top".to_string()));
map
})),
_ => None,
}
}
@@ -667,7 +869,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);
@@ -697,6 +899,69 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
pub const QUARTZ_STAIRS: Block = Block::new(185);
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
pub const BARREL: Block = Block::new(188);
pub const FERN: Block = Block::new(189);
pub const COBWEB: Block = Block::new(190);
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
// Backwards-compatible alias (defaults to north-facing)
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
pub const CHIPPED_ANVIL: Block = Block::new(195);
pub const DAMAGED_ANVIL: Block = Block::new(196);
pub const LARGE_FERN_LOWER: Block = Block::new(197);
pub const LARGE_FERN_UPPER: Block = Block::new(198);
pub const CHAIN: Block = Block::new(199);
pub const END_ROD: Block = Block::new(200);
pub const LIGHTNING_ROD: Block = Block::new(201);
pub const GOLD_BLOCK: Block = Block::new(202);
pub const SEA_LANTERN: Block = Block::new(203);
pub const ORANGE_CONCRETE: Block = Block::new(204);
pub const ORANGE_WOOL: Block = Block::new(205);
pub const BLUE_WOOL: Block = Block::new(206);
pub const GREEN_CONCRETE: Block = Block::new(207);
pub const BRICK_WALL: Block = Block::new(208);
pub const REDSTONE_BLOCK: Block = Block::new(209);
pub const CHAIN_X: Block = Block::new(210);
pub const CHAIN_Z: Block = Block::new(211);
pub const SPRUCE_DOOR_LOWER: Block = Block::new(212);
pub const SPRUCE_DOOR_UPPER: Block = Block::new(213);
pub const SMOOTH_STONE_SLAB: Block = Block::new(214);
pub const GLASS_PANE: Block = Block::new(215);
pub const LIGHT_GRAY_TERRACOTTA: Block = Block::new(216);
pub const OAK_SLAB_TOP: Block = Block::new(217);
pub const OAK_DOOR_UPPER: Block = Block::new(218);
pub const DARK_OAK_LOG: Block = Block::new(219);
pub const DARK_OAK_LEAVES: Block = Block::new(220);
pub const JUNGLE_LOG: Block = Block::new(221);
pub const JUNGLE_LEAVES: Block = Block::new(222);
pub const ACACIA_LOG: Block = Block::new(223);
pub const ACACIA_LEAVES: Block = Block::new(224);
pub const SPRUCE_LEAVES: Block = Block::new(225);
pub const CYAN_STAINED_GLASS: Block = Block::new(226);
pub const BLUE_STAINED_GLASS: Block = Block::new(227);
pub const LIGHT_BLUE_STAINED_GLASS: Block = Block::new(228);
pub const DAYLIGHT_DETECTOR: Block = Block::new(229);
pub const RED_STAINED_GLASS: Block = Block::new(230);
pub const YELLOW_STAINED_GLASS: Block = Block::new(231);
pub const PURPLE_STAINED_GLASS: Block = Block::new(232);
pub const ORANGE_STAINED_GLASS: Block = Block::new(233);
pub const MAGENTA_STAINED_GLASS: Block = Block::new(234);
pub const FLOWER_POT: Block = Block::new(235);
pub const OAK_TRAPDOOR_OPEN_NORTH: Block = Block::new(236);
pub const OAK_TRAPDOOR_OPEN_SOUTH: Block = Block::new(237);
pub const OAK_TRAPDOOR_OPEN_EAST: Block = Block::new(238);
pub const OAK_TRAPDOOR_OPEN_WEST: Block = Block::new(239);
pub const QUARTZ_SLAB_TOP: Block = Block::new(240);
pub const DARK_OAK_TRAPDOOR: Block = Block::new(241);
pub const SPRUCE_TRAPDOOR: Block = Block::new(242);
pub const BIRCH_TRAPDOOR: Block = Block::new(243);
pub const MUD_BRICK_SLAB: Block = Block::new(244);
pub const BRICK_SLAB: Block = Block::new(245);
pub const POTTED_RED_TULIP: Block = Block::new(246);
pub const POTTED_DANDELION: Block = Block::new(247);
pub const POTTED_BLUE_ORCHID: Block = Block::new(248);
/// Maps a block to its corresponding stair variant
#[inline]
@@ -748,58 +1013,80 @@ pub static WINDOW_VARIATIONS: [Block; 7] = [
TINTED_GLASS,
];
// Window types for different building styles
// Residential window options
pub static RESIDENTIAL_WINDOW_OPTIONS: [Block; 4] = [
GLASS,
WHITE_STAINED_GLASS,
LIGHT_GRAY_STAINED_GLASS,
BROWN_STAINED_GLASS,
];
// Institutional window options (hospital, school, etc.)
pub static INSTITUTIONAL_WINDOW_OPTIONS: [Block; 3] =
[GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
// Hospitality window options (hotel, restaurant)
pub static HOSPITALITY_WINDOW_OPTIONS: [Block; 2] = [GLASS, WHITE_STAINED_GLASS];
// Industrial window options
pub static INDUSTRIAL_WINDOW_OPTIONS: [Block; 4] = [
GLASS,
GRAY_STAINED_GLASS,
LIGHT_GRAY_STAINED_GLASS,
BROWN_STAINED_GLASS,
];
// Window types for different building styles (non-deterministic, for backwards compatibility)
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
use rand::Rng;
let mut rng = rand::thread_rng();
get_window_block_for_building_type_with_rng(building_type, &mut rng)
}
/// Deterministic window block selection using provided RNG
pub fn get_window_block_for_building_type_with_rng(
building_type: &str,
rng: &mut impl rand::Rng,
) -> Block {
match building_type {
"residential" | "house" | "apartment" => {
let residential_windows = [
GLASS,
WHITE_STAINED_GLASS,
LIGHT_GRAY_STAINED_GLASS,
BROWN_STAINED_GLASS,
];
residential_windows[rng.gen_range(0..residential_windows.len())]
"residential" | "house" | "apartment" | "apartments" => {
RESIDENTIAL_WINDOW_OPTIONS[rng.gen_range(0..RESIDENTIAL_WINDOW_OPTIONS.len())]
}
"hospital" | "school" | "university" => {
let institutional_windows = [GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
institutional_windows[rng.gen_range(0..institutional_windows.len())]
INSTITUTIONAL_WINDOW_OPTIONS[rng.gen_range(0..INSTITUTIONAL_WINDOW_OPTIONS.len())]
}
"hotel" | "restaurant" => {
let hospitality_windows = [GLASS, WHITE_STAINED_GLASS];
hospitality_windows[rng.gen_range(0..hospitality_windows.len())]
HOSPITALITY_WINDOW_OPTIONS[rng.gen_range(0..HOSPITALITY_WINDOW_OPTIONS.len())]
}
"industrial" | "warehouse" => {
let industrial_windows = [
GLASS,
GRAY_STAINED_GLASS,
LIGHT_GRAY_STAINED_GLASS,
BROWN_STAINED_GLASS,
];
industrial_windows[rng.gen_range(0..industrial_windows.len())]
INDUSTRIAL_WINDOW_OPTIONS[rng.gen_range(0..INDUSTRIAL_WINDOW_OPTIONS.len())]
}
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
}
}
// Random floor block selection
// Floor block options for buildings
pub static FLOOR_BLOCK_OPTIONS: [Block; 8] = [
WHITE_CONCRETE,
GRAY_CONCRETE,
LIGHT_GRAY_CONCRETE,
POLISHED_ANDESITE,
SMOOTH_STONE,
STONE_BRICKS,
MUD_BRICKS,
OAK_PLANKS,
];
// Random floor block selection (non-deterministic, for backwards compatibility)
pub fn get_random_floor_block() -> Block {
use rand::Rng;
let mut rng = rand::thread_rng();
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
}
let floor_options = [
WHITE_CONCRETE,
GRAY_CONCRETE,
LIGHT_GRAY_CONCRETE,
POLISHED_ANDESITE,
SMOOTH_STONE,
STONE_BRICKS,
MUD_BRICKS,
OAK_PLANKS,
];
floor_options[rng.gen_range(0..floor_options.len())]
/// Deterministic floor block selection using provided RNG
pub fn get_floor_block_with_rng(rng: &mut impl rand::Rng) -> Block {
FLOOR_BLOCK_OPTIONS[rng.gen_range(0..FLOOR_BLOCK_OPTIONS.len())]
}
// Define all predefined colors with their blocks
@@ -981,7 +1268,6 @@ pub fn get_fallback_building_block() -> Block {
STONE_BRICKS,
WHITE_CONCRETE,
WHITE_TERRACOTTA,
OAK_PLANKS,
];
fallback_options[rng.gen_range(0..fallback_options.len())]
}

725
src/clipping.rs Normal file
View File

@@ -0,0 +1,725 @@
// 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();
}
// Get way ID for ID generation
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
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();
}
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 {
// Use a slightly larger epsilon to handle floating-point errors from Sutherland-Hodgman.
// Points should be clamped to bbox before this function is called, so any point
// at or very near the boundary should be considered ON that edge.
let eps = 1.0;
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;
// For opposite edges (distance = 2), we need to pick a direction.
// Use counter-clockwise by default to ensure corners are inserted.
// This prevents diagonal lines when polygon spans opposite bbox edges.
let mut result = Vec::new();
if ccw_dist <= cw_dist {
// Go counter-clockwise
let mut current = edge1;
for _ in 0..ccw_dist {
result.push(corners[current as usize]);
current = (current + 1) % 4;
}
} else {
// Go clockwise
let mut current = edge1;
for _ in 0..cw_dist {
current = (current + 4 - 1) % 4;
result.push(corners[current as usize]);
}
}
result
}
/// Checks if two points are approximately equal (within epsilon tolerance).
fn points_approx_equal(p1: (f64, f64), p2: (f64, f64)) -> bool {
let eps = 1.0;
(p1.0 - p2.0).abs() <= eps && (p1.1 - p2.1).abs() <= eps
}
/// 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 {
let corners = get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z);
// Filter out corners that match the current point or the next point
for corner in corners {
if !points_approx_equal(corner, current) && !points_approx_equal(corner, next) {
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,34 +1,83 @@
use crate::args::Args;
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, SMOOTH_STONE, STONE};
use crate::coordinate_system::cartesian::XZBBox;
use crate::coordinate_system::geographic::LLBBox;
use crate::element_processing::*;
use crate::floodfill_cache::FloodFillCache;
use crate::ground::Ground;
use crate::map_renderer;
use crate::osm_parser::ProcessedElement;
use crate::progress::emit_gui_progress_update;
use crate::world_editor::WorldEditor;
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::urban_ground;
use crate::world_editor::{WorldEditor, WorldFormat};
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;
use std::sync::Arc;
pub const MIN_Y: i32 = -64;
pub fn generate_world(
/// Generation options that can be passed separately from CLI Args
#[derive(Clone)]
pub struct GenerationOptions {
pub path: PathBuf,
pub format: WorldFormat,
pub level_name: Option<String>,
pub spawn_point: Option<(i32, i32)>,
}
/// Generate world with explicit format options (used by GUI for Bedrock support)
pub fn generate_world_with_options(
elements: Vec<ProcessedElement>,
xzbbox: XZBBox,
llbbox: LLBBox,
ground: Ground,
args: &Args,
) -> Result<(), String> {
let region_dir: String = format!("{}/region", args.path);
let mut editor: WorldEditor = WorldEditor::new(&region_dir, &xzbbox);
options: GenerationOptions,
) -> Result<PathBuf, String> {
let output_path = options.path.clone();
let world_format = options.format;
// Create editor with appropriate format
let mut editor: WorldEditor = WorldEditor::new_with_format_and_name(
options.path,
&xzbbox,
llbbox,
options.format,
options.level_name.clone(),
options.spawn_point,
);
let ground = Arc::new(ground);
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);
editor.set_ground(Arc::clone(&ground));
println!("{} Processing terrain...", "[5/7]".bold());
emit_gui_progress_update(25.0, "Processing terrain...");
// Process data
// Pre-compute all flood fills in parallel for better CPU utilization
let mut flood_fill_cache = FloodFillCache::precompute(&elements, args.timeout.as_ref());
// Collect building footprints to prevent trees from spawning inside buildings
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
// Collect building centroids for urban ground generation (only if enabled)
// This must be done before the processing loop clears the flood fill cache
let building_centroids = if args.city_boundaries {
flood_fill_cache.collect_building_centroids(&elements)
} else {
Vec::new()
};
// Process all elements (no longer need to partition boundaries)
let elements_count: usize = elements.len();
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
process_pb.set_style(ProgressStyle::default_bar()
@@ -40,7 +89,8 @@ pub fn generate_world(
let mut current_progress_prcs: f64 = 25.0;
let mut last_emitted_progress: f64 = current_progress_prcs;
for element in &elements {
// Process all elements
for element in elements.into_iter() {
process_pb.inc(1);
current_progress_prcs += progress_increment_prcs;
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
@@ -58,36 +108,73 @@ pub fn generate_world(
process_pb.set_message("");
}
match element {
match &element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
buildings::generate_buildings(&mut editor, way, args, None);
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
} else if way.tags.contains_key("highway") {
highways::generate_highways(&mut editor, element, args);
highways::generate_highways(
&mut editor,
&element,
args,
&highway_connectivity,
&flood_fill_cache,
);
} else if way.tags.contains_key("landuse") {
landuse::generate_landuse(&mut editor, way, args);
landuse::generate_landuse(
&mut editor,
way,
args,
&flood_fill_cache,
&building_footprints,
);
} else if way.tags.contains_key("natural") {
natural::generate_natural(&mut editor, element, args);
natural::generate_natural(
&mut editor,
&element,
args,
&flood_fill_cache,
&building_footprints,
);
} else if way.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, element, args);
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
} else if way.tags.contains_key("leisure") {
leisure::generate_leisure(&mut editor, way, args);
leisure::generate_leisure(
&mut editor,
way,
args,
&flood_fill_cache,
&building_footprints,
);
} 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);
barriers::generate_barriers(&mut editor, &element);
} 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);
} else if way.tags.get("service") == Some(&"siding".to_string()) {
highways::generate_siding(&mut editor, way);
} else if way.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, element, args);
man_made::generate_man_made(&mut editor, &element, args);
} else if way.tags.contains_key("power") {
power::generate_power(&mut editor, &element);
} else if way.tags.contains_key("place") {
landuse::generate_place(&mut editor, way, args, &flood_fill_cache);
}
// Release flood fill cache entry for this way
flood_fill_cache.remove_way(way.id);
}
ProcessedElement::Node(node) => {
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
@@ -95,45 +182,106 @@ pub fn generate_world(
} else if node.tags.contains_key("natural")
&& node.tags.get("natural") == Some(&"tree".to_string())
{
natural::generate_natural(&mut editor, element, args);
natural::generate_natural(
&mut editor,
&element,
args,
&flood_fill_cache,
&building_footprints,
);
} else if node.tags.contains_key("amenity") {
amenities::generate_amenities(&mut editor, element, args);
amenities::generate_amenities(&mut editor, &element, args, &flood_fill_cache);
} 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,
&flood_fill_cache,
);
} else if node.tags.contains_key("tourism") {
tourisms::generate_tourisms(&mut editor, node);
} else if node.tags.contains_key("man_made") {
man_made::generate_man_made_nodes(&mut editor, node);
} else if node.tags.contains_key("power") {
power::generate_power_nodes(&mut editor, node);
} else if node.tags.contains_key("historic") {
historic::generate_historic(&mut editor, node);
} else if node.tags.contains_key("emergency") {
emergency::generate_emergency(&mut editor, node);
} else if node.tags.contains_key("advertising") {
advertising::generate_advertising(&mut editor, node);
}
}
ProcessedElement::Relation(rel) => {
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())
{
water_areas::generate_water_areas(&mut editor, rel);
} else if rel.tags.contains_key("natural") {
natural::generate_natural_from_relation(&mut editor, rel, args);
} else if rel.tags.contains_key("landuse") {
landuse::generate_landuse_from_relation(&mut editor, rel, args);
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
leisure::generate_leisure_from_relation(&mut editor, rel, args);
} else if rel.tags.contains_key("man_made") {
man_made::generate_man_made(
buildings::generate_building_from_relation(
&mut editor,
&ProcessedElement::Relation(rel.clone()),
rel,
args,
&flood_fill_cache,
);
} else if rel.tags.contains_key("water")
|| rel
.tags
.get("natural")
.map(|val| val == "water" || val == "bay")
.unwrap_or(false)
{
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,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.contains_key("landuse") {
landuse::generate_landuse_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
leisure::generate_leisure_from_relation(
&mut editor,
rel,
args,
&flood_fill_cache,
&building_footprints,
);
} else if rel.tags.contains_key("man_made") {
man_made::generate_man_made(&mut editor, &element, args);
}
// Release flood fill cache entries for all ways in this relation
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
flood_fill_cache.remove_relation_ways(&way_ids);
}
}
// Element is dropped here, freeing its memory immediately
}
process_pb.finish();
// Compute urban ground lookup (if enabled)
// Uses a compact cell-based representation instead of storing all coordinates.
// Memory usage: ~270 KB vs ~560 MB for coordinate-based approach.
let urban_lookup = if args.city_boundaries && !building_centroids.is_empty() {
urban_ground::compute_urban_ground_lookup(building_centroids, &xzbbox)
} else {
urban_ground::UrbanGroundLookup::empty()
};
let has_urban_ground = !urban_lookup.is_empty();
// Drop remaining caches
drop(highway_connectivity);
drop(flood_fill_cache);
// Generate ground layer
let total_blocks: u64 = xzbbox.bounding_rect().total_blocks();
let desired_updates: u64 = 1500;
@@ -157,44 +305,81 @@ pub fn generate_world(
let total_iterations_grnd: f64 = total_blocks as f64;
let progress_increment_grnd: f64 = 20.0 / total_iterations_grnd;
let groundlayer_block = GRASS_BLOCK;
// Check if terrain elevation is enabled; when disabled, we can skip ground level lookups entirely
let terrain_enabled = ground.elevation_enabled;
for x in xzbbox.min_x()..=xzbbox.max_x() {
for z in xzbbox.min_z()..=xzbbox.max_z() {
// Add default dirt and grass layer if there isn't a stone layer already
if !editor.check_for_block(x, 0, z, Some(&[STONE])) {
editor.set_block(groundlayer_block, x, 0, z, None, None);
editor.set_block(DIRT, x, -1, z, None, None);
editor.set_block(DIRT, x, -2, z, None, None);
}
// Process ground generation chunk-by-chunk for better cache locality.
// This keeps the same region/chunk HashMap entries hot in CPU cache,
// rather than jumping between regions on every Z iteration.
let min_chunk_x = xzbbox.min_x() >> 4;
let max_chunk_x = xzbbox.max_x() >> 4;
let min_chunk_z = xzbbox.min_z() >> 4;
let max_chunk_z = xzbbox.max_z() >> 4;
// Fill underground with stone
if args.fillground {
// Fill from bedrock+1 to 3 blocks below ground with stone
editor.fill_blocks_absolute(
STONE,
x,
MIN_Y + 1,
z,
x,
editor.get_absolute_y(x, -3, z),
z,
None,
None,
);
}
// Generate a bedrock level at MIN_Y
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
for chunk_x in min_chunk_x..=max_chunk_x {
for chunk_z in min_chunk_z..=max_chunk_z {
// Calculate the block range for this chunk, clamped to bbox
let chunk_min_x = (chunk_x << 4).max(xzbbox.min_x());
let chunk_max_x = ((chunk_x << 4) + 15).min(xzbbox.max_x());
let chunk_min_z = (chunk_z << 4).max(xzbbox.min_z());
let chunk_max_z = ((chunk_z << 4) + 15).min(xzbbox.max_z());
block_counter += 1;
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
for x in chunk_min_x..=chunk_max_x {
for z in chunk_min_z..=chunk_max_z {
// Get ground level, when terrain is enabled, look it up once per block
// When disabled, use constant ground_level (no function call overhead)
let ground_y = if terrain_enabled {
editor.get_ground_level(x, z)
} else {
args.ground_level
};
gui_progress_grnd += progress_increment_grnd;
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
emit_gui_progress_update(gui_progress_grnd, "");
last_emitted_progress = gui_progress_grnd;
// Check if this coordinate is in an urban area (O(1) lookup)
let is_urban = has_urban_ground && urban_lookup.is_urban(x, z);
// Add default dirt and grass layer if there isn't a stone layer already
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
if is_urban {
// Urban area: smooth stone ground
editor.set_block_absolute(SMOOTH_STONE, x, ground_y, z, None, None);
} else {
// Rural/natural area: grass and dirt
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
}
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
}
// Fill underground with stone
if args.fillground {
// Fill from bedrock+1 to 3 blocks below ground with stone
editor.fill_blocks_absolute(
STONE,
x,
MIN_Y + 1,
z,
x,
ground_y - 3,
z,
None,
None,
);
}
// Generate a bedrock level at MIN_Y
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
block_counter += 1;
#[allow(clippy::manual_is_multiple_of)]
if block_counter % batch_size == 0 {
ground_pb.inc(batch_size);
}
gui_progress_grnd += progress_increment_grnd;
if (gui_progress_grnd - last_emitted_progress).abs() > 0.25 {
emit_gui_progress_update(gui_progress_grnd, "");
last_emitted_progress = gui_progress_grnd;
}
}
}
}
}
@@ -217,30 +402,111 @@ pub fn generate_world(
// Save world
editor.save();
emit_gui_progress_update(99.0, "Finalizing world...");
// Update player spawn Y coordinate based on terrain height after generation
#[cfg(feature = "gui")]
if let Some(spawn_coords) = &args.spawn_point {
if world_format == WorldFormat::JavaAnvil {
use crate::gui::update_player_spawn_y_after_generation;
// Reconstruct bbox string to match the format that GUI originally provided.
// This ensures LLBBox::from_str() can parse it correctly.
let bbox_string = format!(
"{},{},{},{}",
args.bbox.min().lng(),
args.bbox.min().lat(),
args.bbox.max().lng(),
args.bbox.max().lat()
args.bbox.min().lng(),
args.bbox.max().lat(),
args.bbox.max().lng()
);
if let Err(e) = update_player_spawn_y_after_generation(
&args.path,
Some(*spawn_coords),
bbox_string,
args.scale,
&ground,
) {
eprintln!("Warning: Failed to update spawn point Y coordinate: {e}");
// Always update spawn Y since we now always set a spawn point (user-selected or default)
if let Some(ref world_path) = args.path {
if let Err(e) = update_player_spawn_y_after_generation(
world_path,
bbox_string,
args.scale,
ground.as_ref(),
) {
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
eprintln!("Warning: {}", warning_msg);
#[cfg(feature = "gui")]
send_log(LogLevel::Warning, &warning_msg);
}
}
}
emit_gui_progress_update(100.0, "Done! World generation completed.");
println!("{}", "Done! World generation completed.".green().bold());
Ok(())
// For Bedrock format, emit event to open the mcworld file
if world_format == WorldFormat::BedrockMcWorld {
if let Some(path_str) = output_path.to_str() {
emit_open_mcworld_file(path_str);
}
}
Ok(output_path)
}
/// Information needed to generate a map preview after world generation is complete
#[derive(Clone)]
pub struct MapPreviewInfo {
pub world_path: PathBuf,
pub min_x: i32,
pub max_x: i32,
pub min_z: i32,
pub max_z: i32,
pub world_area: i64,
}
impl MapPreviewInfo {
/// Create MapPreviewInfo from world bounds
pub fn new(world_path: PathBuf, xzbbox: &XZBBox) -> Self {
let world_width = (xzbbox.max_x() - xzbbox.min_x()) as i64;
let world_height = (xzbbox.max_z() - xzbbox.min_z()) as i64;
Self {
world_path,
min_x: xzbbox.min_x(),
max_x: xzbbox.max_x(),
min_z: xzbbox.min_z(),
max_z: xzbbox.max_z(),
world_area: world_width * world_height,
}
}
}
/// Maximum area for which map preview generation is allowed (to avoid memory issues)
pub const MAX_MAP_PREVIEW_AREA: i64 = 6400 * 6900;
/// Start map preview generation in a background thread.
/// This should be called AFTER the world generation is complete, the session lock is released,
/// and the GUI has been notified of 100% completion.
///
/// For Java worlds only, and only if the world area is within limits.
pub fn start_map_preview_generation(info: MapPreviewInfo) {
if info.world_area > MAX_MAP_PREVIEW_AREA {
return;
}
std::thread::spawn(move || {
// Use catch_unwind to prevent any panic from affecting the application
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
map_renderer::render_world_map(
&info.world_path,
info.min_x,
info.max_x,
info.min_z,
info.max_z,
)
}));
match result {
Ok(Ok(_path)) => {
// Notify the GUI that the map preview is ready
emit_map_preview_ready();
}
Ok(Err(e)) => {
eprintln!("Warning: Failed to generate map preview: {}", e);
}
Err(_) => {
eprintln!("Warning: Map preview generation panicked unexpectedly");
}
}
});
}

127
src/deterministic_rng.rs Normal file
View File

@@ -0,0 +1,127 @@
//! Deterministic random number generation for consistent element processing.
//!
//! This module provides seeded RNG that ensures the same element always produces
//! the same random values, regardless of processing order. This is essential for
//! region-by-region streaming where the same element may be processed multiple times
//! (once for each region it touches).
//!
//! # Example
//! ```ignore
//! let mut rng = element_rng(element_id);
//! let color = rng.gen_bool(0.5); // Always same result for same element_id
//! ```
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Creates a deterministic RNG seeded from an element ID.
///
/// The same element ID will always produce the same sequence of random values,
/// ensuring consistent results when an element is processed multiple times
/// (e.g., once per region it touches during streaming).
///
/// # Arguments
/// * `element_id` - The unique OSM element ID (way ID, node ID, or relation ID)
///
/// # Returns
/// A seeded ChaCha8Rng that will produce deterministic random values
#[inline]
pub fn element_rng(element_id: u64) -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(element_id)
}
/// Creates a deterministic RNG seeded from an element ID with an additional salt.
///
/// Use this when you need multiple independent random sequences for the same element.
/// For example, one sequence for wall colors and another for roof style.
///
/// # Arguments
/// * `element_id` - The unique OSM element ID
/// * `salt` - Additional value to create a different sequence (e.g., use different
/// salt values for different purposes within the same element)
#[inline]
#[allow(dead_code)]
pub fn element_rng_salted(element_id: u64, salt: u64) -> ChaCha8Rng {
// Combine element_id and salt using XOR and bit rotation to avoid collisions
let combined = element_id ^ salt.rotate_left(32);
ChaCha8Rng::seed_from_u64(combined)
}
/// Creates a deterministic RNG seeded from coordinates.
///
/// Use this for per-block randomness that needs to be consistent regardless
/// of processing order (e.g., random flower placement within a natural area).
///
/// # Arguments
/// * `x` - X coordinate
/// * `z` - Z coordinate
/// * `element_id` - The element ID for additional uniqueness
#[inline]
pub fn coord_rng(x: i32, z: i32, element_id: u64) -> ChaCha8Rng {
// Combine coordinates and element_id into a seed.
// Cast through u32 to handle negative coordinates consistently.
let coord_part = ((x as u32 as i64) << 32) | (z as u32 as i64);
let seed = (coord_part as u64) ^ element_id;
ChaCha8Rng::seed_from_u64(seed)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
#[test]
fn test_element_rng_deterministic() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng(12345);
// Same seed should produce same sequence
for _ in 0..100 {
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
}
}
#[test]
fn test_different_elements_different_values() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng(12346);
// Different seeds should (almost certainly) produce different values
let v1: u64 = rng1.gen();
let v2: u64 = rng2.gen();
assert_ne!(v1, v2);
}
#[test]
fn test_salted_rng_different_from_base() {
let mut rng1 = element_rng(12345);
let mut rng2 = element_rng_salted(12345, 1);
let v1: u64 = rng1.gen();
let v2: u64 = rng2.gen();
assert_ne!(v1, v2);
}
#[test]
fn test_coord_rng_deterministic() {
let mut rng1 = coord_rng(100, 200, 12345);
let mut rng2 = coord_rng(100, 200, 12345);
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
}
#[test]
fn test_coord_rng_negative_coordinates() {
// Negative coordinates are common in Minecraft worlds
let mut rng1 = coord_rng(-100, -200, 12345);
let mut rng2 = coord_rng(-100, -200, 12345);
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
// Ensure different negative coords produce different seeds
let mut rng3 = coord_rng(-100, -200, 12345);
let mut rng4 = coord_rng(-101, -200, 12345);
assert_ne!(rng3.gen::<u64>(), rng4.gen::<u64>());
}
}

View File

@@ -0,0 +1,120 @@
//! Processing of advertising elements.
//!
//! This module handles advertising-related OSM elements including:
//! - `advertising=column` - Cylindrical advertising columns (Litfaßsäule)
//! - `advertising=flag` - Advertising flags on poles
//! - `advertising=poster_box` - Illuminated poster display boxes
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
use rand::Rng;
/// Generate advertising structures from node elements
pub fn generate_advertising(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(advertising_type) = node.tags.get("advertising") {
match advertising_type.as_str() {
"column" => generate_advertising_column(editor, node),
"flag" => generate_advertising_flag(editor, node),
"poster_box" => generate_poster_box(editor, node),
_ => {}
}
}
}
/// Generate an advertising column (Litfaßsäule)
///
/// Creates a simple advertising column.
fn generate_advertising_column(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Two green concrete blocks stacked
editor.set_block(GREEN_CONCRETE, x, 1, z, None, None);
editor.set_block(GREEN_CONCRETE, x, 2, z, None, None);
// Stone brick slab on top
editor.set_block(STONE_BRICK_SLAB, x, 3, z, None, None);
}
/// Generate an advertising flag
///
/// Creates a flagpole with a banner/flag for advertising.
fn generate_advertising_flag(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Use deterministic RNG for flag color
let mut rng = element_rng(node.id);
// Get height from tags or default
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(6)
.clamp(4, 12);
// Flagpole
for y in 1..=height {
editor.set_block(IRON_BARS, x, y, z, None, None);
}
// Flag/banner at top (using colored wool)
// Random bright advertising colors
let flag_colors = [
RED_WOOL,
YELLOW_WOOL,
BLUE_WOOL,
GREEN_WOOL,
ORANGE_WOOL,
WHITE_WOOL,
];
let flag_block = flag_colors[rng.gen_range(0..flag_colors.len())];
// Flag extends to one side (2-3 blocks)
let flag_length = 3;
for dx in 1..=flag_length {
editor.set_block(flag_block, x + dx, height, z, None, None);
editor.set_block(flag_block, x + dx, height - 1, z, None, None);
}
// Finial at top
editor.set_block(IRON_BLOCK, x, height + 1, z, None, None);
}
/// Generate a poster box (city light / lollipop display)
///
/// Creates an illuminated poster display box on a pole.
fn generate_poster_box(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Y=1: Two iron bars next to each other
editor.set_block(IRON_BARS, x, 1, z, None, None);
editor.set_block(IRON_BARS, x + 1, 1, z, None, None);
// Y=2 and Y=3: Two sea lanterns
editor.set_block(SEA_LANTERN, x, 2, z, None, None);
editor.set_block(SEA_LANTERN, x + 1, 2, z, None, None);
editor.set_block(SEA_LANTERN, x, 3, z, None, None);
editor.set_block(SEA_LANTERN, x + 1, 3, z, None, None);
// Y=4: Two polished stone brick slabs
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 4, z, None, None);
}

View File

@@ -2,11 +2,21 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::coordinate_system::cartesian::XZPoint;
use crate::floodfill::flood_fill_area;
use crate::deterministic_rng::element_rng;
use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
use crate::floodfill_cache::FloodFillCache;
use crate::osm_parser::ProcessedElement;
use crate::world_editor::WorldEditor;
use fastnbt::Value;
use rand::{seq::SliceRandom, Rng};
use std::collections::{HashMap, HashSet};
pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
pub fn generate_amenities(
editor: &mut WorldEditor,
element: &ProcessedElement,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
// 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 {
@@ -26,6 +36,49 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
.next();
match amenity_type.as_str() {
"recycling" => {
let is_container = element
.tags()
.get("recycling_type")
.is_some_and(|value| value == "container");
if !is_container {
return;
}
if let Some(pt) = first_node {
let mut rng = rand::thread_rng();
let loot_pool = build_recycling_loot_pool(element.tags());
let items = build_recycling_items(&loot_pool, &mut rng);
let properties = Value::Compound(recycling_barrel_properties());
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
editor.set_block_entity_with_items(
barrel_block,
pt.x,
1,
pt.z,
"minecraft:barrel",
items,
);
if let Some(category) = single_loot_category(&loot_pool) {
if let Some(display_item) =
build_display_item_for_category(category, &mut rng)
{
place_item_frame_on_random_side(
editor,
pt.x,
absolute_y,
pt.z,
display_item,
);
}
}
}
}
"waste_disposal" | "waste_basket" => {
// Place a cauldron for waste disposal or waste basket
if let Some(pt) = first_node {
@@ -42,18 +95,14 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
let ground_block: Block = OAK_PLANKS;
let roof_block: Block = STONE_BLOCK_SLAB;
let polygon_coords: Vec<(i32, i32)> = element
.nodes()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
// Use pre-computed flood fill from cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
if polygon_coords.is_empty() {
if floor_area.is_empty() {
return;
}
let floor_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Fill the floor area
for (x, z) in floor_area.iter() {
editor.set_block(ground_block, *x, 0, *z, None, None);
@@ -80,8 +129,10 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
"bench" => {
// Place a bench
if let Some(pt) = first_node {
// 50% chance to 90 degrees rotate the bench using if
if rand::random::<bool>() {
// Use deterministic RNG for consistent bench orientation across region boundaries
let mut rng = element_rng(element.id());
// 50% chance to 90 degrees rotate the bench
if rng.gen_bool(0.5) {
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
editor.set_block(OAK_LOG, pt.x + 1, 1, pt.z, None, None);
editor.set_block(OAK_LOG, pt.x - 1, 1, pt.z, None, None);
@@ -92,22 +143,12 @@ 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;
let polygon_coords: Vec<(i32, i32)> = element
.nodes()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
// Use pre-computed flood fill from cache
let roof_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
flood_fill_cache.get_or_compute_element(element, args.timeout.as_ref());
// Place fences and roof slabs at each corner node directly
for node in element.nodes() {
@@ -267,3 +308,420 @@ pub fn generate_amenities(editor: &mut WorldEditor, element: &ProcessedElement,
}
}
}
#[derive(Clone, Copy)]
enum RecyclingLootKind {
GlassBottle,
Paper,
GlassBlock,
GlassPane,
LeatherArmor,
EmptyBucket,
LeatherBoots,
ScrapMetal,
GreenWaste,
}
#[derive(Clone, Copy)]
enum LeatherPiece {
Helmet,
Chestplate,
Leggings,
Boots,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum LootCategory {
GlassBottle,
Paper,
Glass,
Leather,
EmptyBucket,
ScrapMetal,
GreenWaste,
}
fn recycling_barrel_properties() -> HashMap<String, Value> {
let mut props = HashMap::new();
props.insert("facing".to_string(), Value::String("up".to_string()));
props
}
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
if tag_enabled(tags, "recycling:glass_bottles") {
loot_pool.push(RecyclingLootKind::GlassBottle);
}
if tag_enabled(tags, "recycling:paper") {
loot_pool.push(RecyclingLootKind::Paper);
}
if tag_enabled(tags, "recycling:glass") {
loot_pool.push(RecyclingLootKind::GlassBlock);
loot_pool.push(RecyclingLootKind::GlassPane);
}
if tag_enabled(tags, "recycling:clothes") {
loot_pool.push(RecyclingLootKind::LeatherArmor);
}
if tag_enabled(tags, "recycling:cans") {
loot_pool.push(RecyclingLootKind::EmptyBucket);
}
if tag_enabled(tags, "recycling:shoes") {
loot_pool.push(RecyclingLootKind::LeatherBoots);
}
if tag_enabled(tags, "recycling:scrap_metal") {
loot_pool.push(RecyclingLootKind::ScrapMetal);
}
if tag_enabled(tags, "recycling:green_waste") {
loot_pool.push(RecyclingLootKind::GreenWaste);
}
loot_pool
}
fn build_recycling_items(
loot_pool: &[RecyclingLootKind],
rng: &mut impl Rng,
) -> Vec<HashMap<String, Value>> {
if loot_pool.is_empty() {
return Vec::new();
}
let mut items = Vec::new();
for slot in 0..27 {
if rng.gen_bool(0.2) {
let kind = loot_pool[rng.gen_range(0..loot_pool.len())];
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
items.push(item);
}
}
}
items
}
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
match kind {
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
RecyclingLootKind::Paper => LootCategory::Paper,
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
}
}
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
let mut categories: HashSet<LootCategory> = HashSet::new();
for kind in loot_pool {
categories.insert(kind_to_category(*kind));
if categories.len() > 1 {
return None;
}
}
categories.iter().next().copied()
}
fn build_display_item_for_category(
category: LootCategory,
rng: &mut impl Rng,
) -> Option<HashMap<String, Value>> {
match category {
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
LootCategory::Paper => Some(make_display_item("minecraft:paper", rng.gen_range(1..=4))),
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
LootCategory::Leather => Some(build_leather_display_item(rng)),
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
LootCategory::ScrapMetal => {
let metals = [
"minecraft:copper_ingot",
"minecraft:iron_ingot",
"minecraft:gold_ingot",
];
let metal = metals.choose(rng)?;
Some(make_display_item(metal, rng.gen_range(1..=2)))
}
LootCategory::GreenWaste => {
let options = [
"minecraft:oak_sapling",
"minecraft:birch_sapling",
"minecraft:tall_grass",
"minecraft:sweet_berries",
"minecraft:wheat_seeds",
];
let choice = options.choose(rng)?;
Some(make_display_item(choice, rng.gen_range(1..=3)))
}
}
}
fn place_item_frame_on_random_side(
editor: &mut WorldEditor,
x: i32,
barrel_absolute_y: i32,
z: i32,
item: HashMap<String, Value>,
) {
let mut rng = rand::thread_rng();
let mut directions = [
((0, 0, -1), 2), // North
((0, 0, 1), 3), // South
((-1, 0, 0), 4), // West
((1, 0, 0), 5), // East
];
directions.shuffle(&mut rng);
let (min_x, min_z) = editor.get_min_coords();
let (max_x, max_z) = editor.get_max_coords();
let ((dx, _dy, dz), facing) = directions
.into_iter()
.find(|((dx, _dy, dz), _)| {
let target_x = x + dx;
let target_z = z + dz;
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
})
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
let target_x = x + dx;
let target_y = barrel_absolute_y;
let target_z = z + dz;
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
let mut extra = HashMap::new();
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
extra.insert("ItemRotation".to_string(), Value::Byte(0));
extra.insert("Item".to_string(), Value::Compound(item));
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
extra.insert(
"block_pos".to_string(),
Value::List(vec![
Value::Int(target_x),
Value::Int(target_y),
Value::Int(target_z),
]),
);
extra.insert("TileX".to_string(), Value::Int(target_x));
extra.insert("TileY".to_string(), Value::Int(target_y));
extra.insert("TileZ".to_string(), Value::Int(target_z));
extra.insert("Fixed".to_string(), Value::Byte(1));
let relative_y = target_y - ground_y;
editor.add_entity(
"minecraft:item_frame",
target_x,
relative_y,
target_z,
Some(extra),
);
}
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
let mut item = HashMap::new();
item.insert("id".to_string(), Value::String(id.to_string()));
item.insert("Count".to_string(), Value::Byte(count));
item
}
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
let mut item = make_display_item("minecraft:leather_chestplate", 1);
let damage = biased_damage(80, rng);
let mut tag = HashMap::new();
tag.insert("Damage".to_string(), Value::Int(damage));
if let Some(color) = maybe_leather_color(rng) {
let mut display = HashMap::new();
display.insert("color".to_string(), Value::Int(color));
tag.insert("display".to_string(), Value::Compound(display));
}
item.insert("tag".to_string(), Value::Compound(tag));
let mut components = HashMap::new();
components.insert("minecraft:damage".to_string(), Value::Int(damage));
item.insert("components".to_string(), Value::Compound(components));
item
}
fn build_item_for_kind(
kind: RecyclingLootKind,
slot: i8,
rng: &mut impl Rng,
) -> Option<HashMap<String, Value>> {
match kind {
RecyclingLootKind::GlassBottle => Some(make_basic_item(
"minecraft:glass_bottle",
slot,
rng.gen_range(1..=4),
)),
RecyclingLootKind::Paper => Some(make_basic_item(
"minecraft:paper",
slot,
rng.gen_range(1..=10),
)),
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
RecyclingLootKind::LeatherArmor => {
Some(build_leather_item(random_leather_piece(rng), slot, rng))
}
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
}
}
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
let count = rng.gen_range(1..=3);
make_basic_item(&format!("minecraft:{metal}"), slot, count)
}
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
#[allow(clippy::match_same_arms)]
let (id, count) = match rng.gen_range(0..8) {
0 => ("minecraft:tall_grass", rng.gen_range(1..=4)),
1 => ("minecraft:sweet_berries", rng.gen_range(2..=6)),
2 => ("minecraft:oak_sapling", rng.gen_range(1..=2)),
3 => ("minecraft:birch_sapling", rng.gen_range(1..=2)),
4 => ("minecraft:spruce_sapling", rng.gen_range(1..=2)),
5 => ("minecraft:jungle_sapling", rng.gen_range(1..=2)),
6 => ("minecraft:acacia_sapling", rng.gen_range(1..=2)),
_ => ("minecraft:dark_oak_sapling", rng.gen_range(1..=2)),
};
// 25% chance to replace with seeds instead
let id = if rng.gen_bool(0.25) {
match rng.gen_range(0..4) {
0 => "minecraft:wheat_seeds",
1 => "minecraft:pumpkin_seeds",
2 => "minecraft:melon_seeds",
_ => "minecraft:beetroot_seeds",
}
} else {
id
};
make_basic_item(id, slot, count)
}
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
const GLASS_COLORS: &[&str] = &[
"white",
"orange",
"magenta",
"light_blue",
"yellow",
"lime",
"pink",
"gray",
"light_gray",
"cyan",
"purple",
"blue",
"brown",
"green",
"red",
"black",
];
let use_colorless = rng.gen_bool(0.7);
let id = if use_colorless {
if is_pane {
"minecraft:glass_pane".to_string()
} else {
"minecraft:glass".to_string()
}
} else {
let color = GLASS_COLORS
.choose(rng)
.expect("glass color array is non-empty");
if is_pane {
format!("minecraft:{color}_stained_glass_pane")
} else {
format!("minecraft:{color}_stained_glass")
}
};
let count = if is_pane {
rng.gen_range(4..=16)
} else {
rng.gen_range(1..=6)
};
make_basic_item(&id, slot, count)
}
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
let (id, max_damage) = match piece {
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
};
let mut item = make_basic_item(id, slot, 1);
let damage = biased_damage(max_damage, rng);
let mut tag = HashMap::new();
tag.insert("Damage".to_string(), Value::Int(damage));
if let Some(color) = maybe_leather_color(rng) {
let mut display = HashMap::new();
display.insert("color".to_string(), Value::Int(color));
tag.insert("display".to_string(), Value::Compound(display));
}
item.insert("tag".to_string(), Value::Compound(tag));
let mut components = HashMap::new();
components.insert("minecraft:damage".to_string(), Value::Int(damage));
item.insert("components".to_string(), Value::Compound(components));
item
}
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
let safe_max = max_damage.max(1);
let upper = safe_max.saturating_sub(1);
let lower = (safe_max / 2).min(upper);
let heavy_wear = rng.gen_range(lower..=upper);
let random_wear = rng.gen_range(0..=upper);
heavy_wear.max(random_wear)
}
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
if rng.gen_bool(0.3) {
Some(rng.gen_range(0..=0x00FF_FFFF))
} else {
None
}
}
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
match rng.gen_range(0..4) {
0 => LeatherPiece::Helmet,
1 => LeatherPiece::Chestplate,
2 => LeatherPiece::Leggings,
_ => LeatherPiece::Boots,
}
}
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
let mut item = HashMap::new();
item.insert("id".to_string(), Value::String(id.to_string()));
item.insert("Slot".to_string(), Value::Byte(slot));
item.insert("Count".to_string(), Value::Byte(count));
item
}
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
tags.get(key).is_some_and(|value| value == "yes")
}

View File

@@ -69,7 +69,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
barrier_material = LIGHT_GRAY_CONCRETE;
}
if barrier_mat == "metal" {
barrier_material = STONE_BRICK_WALL; // IRON_BARS
barrier_material = STONE_BRICK_WALL;
}
}
@@ -80,7 +80,8 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
.get("height")
.and_then(|height: &String| height.parse::<f32>().ok())
.map(|height: f32| height.round() as i32)
.unwrap_or(barrier_height);
.unwrap_or(barrier_height)
.max(2); // Minimum height of 2
// Process nodes to create the barrier wall
for i in 1..way.nodes.len() {

View File

@@ -3,37 +3,97 @@ use crate::bresenham::bresenham_line;
use crate::osm_parser::ProcessedWay;
use crate::world_editor::WorldEditor;
// TODO FIX
// TODO FIX - This handles ways with bridge=yes tag (e.g., highway bridges)
#[allow(dead_code)]
pub fn generate_bridges(editor: &mut WorldEditor, element: &ProcessedWay) {
if let Some(_bridge_type) = element.tags.get("bridge") {
let bridge_height = 3; // Fixed height
let bridge_height = 3; // Height above the ground level
// Get start and end node elevations and use MAX for level bridge deck
// Using MAX ensures bridges don't dip when multiple bridge ways meet in a valley
let bridge_deck_ground_y = if element.nodes.len() >= 2 {
let start_node = &element.nodes[0];
let end_node = &element.nodes[element.nodes.len() - 1];
let start_y = editor.get_ground_level(start_node.x, start_node.z);
let end_y = editor.get_ground_level(end_node.x, end_node.z);
start_y.max(end_y)
} else {
return; // Need at least 2 nodes for a bridge
};
// Calculate total bridge length for ramp positioning
let total_length: f64 = element
.nodes
.windows(2)
.map(|pair| {
let dx = (pair[1].x - pair[0].x) as f64;
let dz = (pair[1].z - pair[0].z) as f64;
(dx * dx + dz * dz).sqrt()
})
.sum();
if total_length == 0.0 {
return;
}
let mut accumulated_length: f64 = 0.0;
for i in 1..element.nodes.len() {
let prev = &element.nodes[i - 1];
let cur = &element.nodes[i];
let segment_dx = (cur.x - prev.x) as f64;
let segment_dz = (cur.z - prev.z) as f64;
let segment_length = (segment_dx * segment_dx + segment_dz * segment_dz).sqrt();
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
let total_length = points.len();
let ramp_length = 6; // Length of ramp at each end
let ramp_length = (total_length * 0.15).clamp(6.0, 20.0) as usize; // 15% of bridge, min 6, max 20 blocks
for (idx, (x, _, z)) in points.iter().enumerate() {
let height = if idx < ramp_length {
// Calculate progress along this segment
let segment_progress = if points.len() > 1 {
idx as f64 / (points.len() - 1) as f64
} else {
0.0
};
// Calculate overall progress along the entire bridge
let point_distance = accumulated_length + segment_progress * segment_length;
let overall_progress = (point_distance / total_length).clamp(0.0, 1.0);
let total_len_usize = total_length as usize;
let overall_idx = (overall_progress * total_len_usize as f64) as usize;
// Calculate ramp height offset
let ramp_offset = if overall_idx < ramp_length {
// Start ramp (rising)
(idx * bridge_height) / ramp_length
} else if idx >= total_length - ramp_length {
(overall_idx as f64 * bridge_height as f64 / ramp_length as f64) as i32
} else if overall_idx >= total_len_usize.saturating_sub(ramp_length) {
// End ramp (descending)
((total_length - idx) * bridge_height) / ramp_length
let dist_from_end = total_len_usize - overall_idx;
(dist_from_end as f64 * bridge_height as f64 / ramp_length as f64) as i32
} else {
// Middle section (constant height)
bridge_height
};
// Use fixed bridge deck height (max of endpoints) plus ramp offset
let bridge_y = bridge_deck_ground_y + ramp_offset;
// Place bridge blocks
for dx in -2..=2 {
editor.set_block(LIGHT_GRAY_CONCRETE, *x + dx, height as i32, *z, None, None);
editor.set_block_absolute(
LIGHT_GRAY_CONCRETE,
*x + dx,
bridge_y,
*z,
None,
None,
);
}
}
accumulated_length += segment_length;
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
//! Processing of emergency infrastructure elements.
//!
//! This module handles emergency-related OSM elements including:
//! - `emergency=fire_hydrant` - Fire hydrants
use crate::block_definitions::*;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
/// Generate emergency infrastructure from node elements
pub fn generate_emergency(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(emergency_type) = node.tags.get("emergency") {
if emergency_type.as_str() == "fire_hydrant" {
generate_fire_hydrant(editor, node)
}
}
}
/// Generate a fire hydrant
///
/// Creates a simple fire hydrant structure using brick wall with redstone block on top.
/// Skips underground, wall-mounted, and pond hydrant types.
fn generate_fire_hydrant(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Get hydrant type - skip underground, wall, and pond types
let hydrant_type = node
.tags
.get("fire_hydrant:type")
.map(|s| s.as_str())
.unwrap_or("pillar");
// Skip non-visible hydrant types
if matches!(hydrant_type, "underground" | "wall" | "pond") {
return;
}
// Simple hydrant: brick wall with redstone block on top
editor.set_block(BRICK_WALL, x, 1, z, None, None);
editor.set_block(REDSTONE_BLOCK, x, 2, z, None, None);
}

View File

@@ -2,11 +2,82 @@ use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::coordinate_system::cartesian::XZPoint;
use crate::floodfill::flood_fill_area;
use crate::floodfill_cache::FloodFillCache;
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>>;
/// Minimum terrain dip (in blocks) below max endpoint elevation to classify a bridge as valley-spanning
const VALLEY_BRIDGE_THRESHOLD: i32 = 7;
/// 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,
flood_fill_cache: &FloodFillCache,
) {
generate_highways_internal(
editor,
element,
args,
highway_connectivity,
flood_fill_cache,
);
}
/// 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
flood_fill_cache: &FloodFillCache,
) {
if let Some(highway_type) = element.tags().get("highway") {
if highway_type == "street_lamp" {
// Handle street lamps
@@ -77,14 +148,9 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
};
}
// Fill the area using flood fill or by iterating through the nodes
let polygon_coords: Vec<(i32, i32)> = way
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
// Fill the area using flood fill cache
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
for (x, z) in filled_area {
editor.set_block(surface_block, x, 0, z, None, None);
@@ -97,13 +163,22 @@ 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;
}
}
// Check if this is a bridge - bridges need special elevation handling
// to span across valleys instead of following terrain
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
// 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 +195,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 +252,98 @@ 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
// This is used for overpasses that need ramps to ground-level roads
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 (needed before valley bridge check)
let total_way_length = calculate_way_length(way);
// For bridges: detect if this spans a valley by checking terrain profile
// A valley bridge has terrain that dips significantly below the endpoints
// Skip valley detection entirely if terrain is disabled (no valleys in flat terrain)
// Skip very short bridges (< 25 blocks) as they're unlikely to span significant valleys
let terrain_enabled = editor
.get_ground()
.map(|g| g.elevation_enabled)
.unwrap_or(false);
let (is_valley_bridge, bridge_deck_y) =
if is_bridge && terrain_enabled && way.nodes.len() >= 2 && total_way_length >= 25 {
let start_node = &way.nodes[0];
let end_node = &way.nodes[way.nodes.len() - 1];
let start_y = editor.get_ground_level(start_node.x, start_node.z);
let end_y = editor.get_ground_level(end_node.x, end_node.z);
let max_endpoint_y = start_y.max(end_y);
// Sample terrain at middle nodes only (excluding endpoints we already have)
// This avoids redundant get_ground_level() calls
let middle_nodes = &way.nodes[1..way.nodes.len().saturating_sub(1)];
let sampled_min = if middle_nodes.is_empty() {
// No middle nodes, just use endpoints
start_y.min(end_y)
} else {
// Sample up to 3 middle points (5 total with endpoints) for performance
// Valleys are wide terrain features, so sparse sampling is sufficient
let sample_count = middle_nodes.len().min(3);
let step = if sample_count > 1 {
(middle_nodes.len() - 1) / (sample_count - 1)
} else {
1
};
middle_nodes
.iter()
.step_by(step.max(1))
.map(|node| editor.get_ground_level(node.x, node.z))
.min()
.unwrap_or(max_endpoint_y)
};
// Include endpoint elevations in the minimum calculation
let min_terrain_y = sampled_min.min(start_y).min(end_y);
// If ANY sampled point along the bridge is significantly lower than the max endpoint,
// treat as valley bridge
let is_valley = min_terrain_y < max_endpoint_y - VALLEY_BRIDGE_THRESHOLD;
if is_valley {
(true, max_endpoint_y)
} else {
(false, 0)
}
} else {
(false, 0)
};
// Check if this is a short isolated elevated segment (layer > 0), 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 +351,41 @@ 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
// For valley bridges: use fixed deck height (max of endpoints) to stay level
// For overpasses and regular roads: use terrain-relative elevation with slopes
let (current_y, use_absolute_y) = if is_valley_bridge {
// Valley bridge deck is level at the maximum endpoint elevation
// Don't add base_elevation - the layer tag indicates it's above water/road,
// not that it should be higher than the terrain endpoints
(bridge_deck_y, true)
} else {
// Regular road or overpass: use terrain-relative calculation with ramps
let y = calculate_point_elevation(
segment_index,
point_index,
segment_length,
total_segments,
effective_elevation,
effective_start_slope,
effective_end_slope,
slope_length,
);
(y, false)
};
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 {
@@ -206,52 +400,150 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
let is_horizontal: bool = (x2 - x1).abs() >= (z2 - z1).abs();
if is_horizontal {
if set_x % 2 < 1 {
editor.set_block(
WHITE_CONCRETE,
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
} else if use_absolute_y {
editor.set_block_absolute(
BLACK_CONCRETE,
set_x,
0,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
None,
);
} else {
editor.set_block(
BLACK_CONCRETE,
set_x,
0,
current_y,
set_z,
None,
None,
);
}
} else if set_z % 2 < 1 {
editor.set_block(
WHITE_CONCRETE,
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
set_x,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
} else if use_absolute_y {
editor.set_block_absolute(
BLACK_CONCRETE,
set_x,
0,
current_y,
set_z,
Some(&[BLACK_CONCRETE]),
None,
None,
);
} else {
editor.set_block(
BLACK_CONCRETE,
set_x,
0,
current_y,
set_z,
None,
None,
);
}
} else {
editor.set_block(
} else if use_absolute_y {
editor.set_block_absolute(
block_type,
set_x,
0,
current_y,
set_z,
None,
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
);
} else {
editor.set_block(
block_type,
set_x,
current_y,
set_z,
None,
Some(&[BLACK_CONCRETE, WHITE_CONCRETE]),
);
}
// Add stone brick foundation underneath elevated highways/bridges for thickness
if (effective_elevation > 0 || use_absolute_y) && current_y > 0 {
// Add 1 layer of stone bricks underneath the highway surface
if use_absolute_y {
editor.set_block_absolute(
STONE_BRICKS,
set_x,
current_y - 1,
set_z,
None,
None,
);
} else {
editor.set_block(
STONE_BRICKS,
set_x,
current_y - 1,
set_z,
None,
None,
);
}
}
// Add support pillars for elevated highways/bridges
if (effective_elevation != 0 || use_absolute_y) && current_y > 0 {
if use_absolute_y {
add_highway_support_pillar_absolute(
editor,
set_x,
current_y,
set_z,
dx,
dz,
block_range,
);
} else {
add_highway_support_pillar(
editor,
set_x,
current_y,
set_z,
dx,
dz,
block_range,
);
}
}
}
}
@@ -262,43 +554,76 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
for dz in -block_range..=block_range {
let outline_x = x - block_range - 1;
let outline_z = z + dz;
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
0,
outline_z,
None,
None,
);
if use_absolute_y {
editor.set_block_absolute(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
} else {
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
}
}
// Right outline
for dz in -block_range..=block_range {
let outline_x = x + block_range + 1;
let outline_z = z + dz;
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
0,
outline_z,
None,
None,
);
if use_absolute_y {
editor.set_block_absolute(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
} else {
editor.set_block(
LIGHT_GRAY_CONCRETE,
outline_x,
current_y,
outline_z,
None,
None,
);
}
}
}
// 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;
editor.set_block(
WHITE_CONCRETE,
stripe_x,
0,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
let stripe_x: i32 = *x;
let stripe_z: i32 = *z;
if use_absolute_y {
editor.set_block_absolute(
WHITE_CONCRETE,
stripe_x,
current_y,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
} else {
editor.set_block(
WHITE_CONCRETE,
stripe_x,
current_y,
stripe_z,
Some(&[BLACK_CONCRETE]),
None,
);
}
}
// Increment stripe_length and reset after completing a dash and gap
@@ -308,6 +633,8 @@ pub fn generate_highways(editor: &mut WorldEditor, element: &ProcessedElement, a
}
}
}
segment_index += 1;
}
previous_node = Some((node.x, node.z));
}
@@ -315,6 +642,171 @@ 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);
}
}
}
}
/// Add support pillars for bridges using absolute Y coordinates
/// Pillars extend from ground level up to the bridge deck
fn add_highway_support_pillar_absolute(
editor: &mut WorldEditor,
x: i32,
bridge_deck_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 {
// Get the actual ground level at this position
let ground_y = editor.get_ground_level(x, z);
// Add pillar from ground up to bridge deck
// Only if the bridge is actually above the ground
if bridge_deck_y > ground_y {
for y in (ground_y + 1)..bridge_deck_y {
editor.set_block_absolute(STONE_BRICKS, x, y, z, None, None);
}
// Add pillar base at ground level
for base_dx in -1..=1 {
for base_dz in -1..=1 {
editor.set_block_absolute(
STONE_BRICKS,
x + base_dx,
ground_y,
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

@@ -0,0 +1,207 @@
//! Processing of historic elements.
//!
//! This module handles historic OSM elements including:
//! - `historic=memorial` - Memorials, monuments, and commemorative structures
use crate::block_definitions::*;
use crate::deterministic_rng::element_rng;
use crate::osm_parser::ProcessedNode;
use crate::world_editor::WorldEditor;
use rand::Rng;
/// Generate historic structures from node elements
pub fn generate_historic(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(historic_type) = node.tags.get("historic") {
match historic_type.as_str() {
"memorial" => generate_memorial(editor, node),
"monument" => generate_monument(editor, node),
"wayside_cross" => generate_wayside_cross(editor, node),
_ => {}
}
}
}
/// Generate a memorial structure
///
/// Memorials come in many forms. We determine the type from the `memorial` tag:
/// - plaque: Simple wall-mounted or standing plaque
/// - statue: A statue on a pedestal
/// - sculpture: Artistic sculpture
/// - stone/stolperstein: Memorial stone
/// - bench: Memorial bench (already handled by amenity=bench typically)
/// - cross: Memorial cross
/// - obelisk: Tall pointed pillar
/// - stele: Upright stone slab
/// - bust: Bust on a pedestal
/// - Default: A general monument/pillar
fn generate_memorial(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Use deterministic RNG for consistent results
let mut rng = element_rng(node.id);
// Get memorial subtype
let memorial_type = node
.tags
.get("memorial")
.map(|s| s.as_str())
.unwrap_or("yes");
match memorial_type {
"plaque" => {
// Simple plaque on a small stand
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x, 2, z, None, None);
}
"statue" | "sculpture" | "bust" => {
// Statue on a pedestal
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
editor.set_block(CHISELED_STONE_BRICKS, x, 2, z, None, None);
// Use polished andesite for bronze/metal statue appearance
let statue_block = if rng.gen_bool(0.5) {
POLISHED_ANDESITE
} else {
POLISHED_DIORITE
};
editor.set_block(statue_block, x, 3, z, None, None);
editor.set_block(statue_block, x, 4, z, None, None);
editor.set_block(STONE_BRICK_WALL, x, 5, z, None, None);
}
"stone" | "stolperstein" => {
// Simple memorial stone embedded in ground
let stone_block = if memorial_type == "stolperstein" {
GOLD_BLOCK // Stolpersteine are brass/gold colored
} else {
STONE
};
editor.set_block(stone_block, x, 0, z, None, None);
}
"cross" | "war_memorial" => {
// Memorial cross
generate_cross(editor, x, z, 5);
}
"obelisk" => {
// Tall pointed pillar with fixed height
// Base layer at Y=1
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
}
}
// Second base layer at Y=2
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
}
}
// Stone brick slabs on the 4 corners at Y=3 (on top of corner blocks)
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z - 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z - 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z + 1, None, None);
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z + 1, None, None);
// Main shaft, fixed height of 4 blocks (Y=3 to Y=6)
for y in 3..=6 {
editor.set_block(SMOOTH_QUARTZ, x, y, z, None, None);
}
editor.set_block(STONE_BRICK_SLAB, x, 7, z, None, None);
}
"stele" => {
// Upright stone slab
// Base
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
// Upright slab (using wall blocks for thin appearance)
for y in 2..=4 {
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
}
editor.set_block(STONE_BRICK_SLAB, x, 5, z, None, None);
}
_ => {
// Default: simple stone pillar monument
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
editor.set_block(STONE_BRICKS, x, 2, z, None, None);
editor.set_block(CHISELED_STONE_BRICKS, x, 3, z, None, None);
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
}
}
}
/// Generate a monument (larger than memorial)
fn generate_monument(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Monuments are typically larger structures
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(10)
.clamp(5, 20);
// Large base platform
for dx in -2..=2 {
for dz in -2..=2 {
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
}
}
for dx in -1..=1 {
for dz in -1..=1 {
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
}
}
// Main structure
for y in 3..height {
editor.set_block(POLISHED_ANDESITE, x, y, z, None, None);
}
// Decorative top
editor.set_block(CHISELED_STONE_BRICKS, x, height, z, None, None);
}
/// Generate a wayside cross
fn generate_wayside_cross(editor: &mut WorldEditor, node: &ProcessedNode) {
let x = node.x;
let z = node.z;
// Simple roadside cross
generate_cross(editor, x, z, 4);
}
/// Helper function to generate a cross structure
fn generate_cross(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
// Base
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
// Vertical beam
for y in 2..=height {
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
}
// Horizontal beam (cross arm) at approximately 2/3 height, but at least 2 and at most height-1
let arm_y = ((height * 2 + 2) / 3).clamp(2, height - 1);
// Only place horizontal arms if height allows for them (height >= 3)
if height >= 3 {
editor.set_block(STONE_BRICK_WALL, x - 1, arm_y, z, None, None);
editor.set_block(STONE_BRICK_WALL, x + 1, arm_y, z, None, None);
}
}

View File

@@ -1,16 +1,27 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::element_processing::tree::Tree;
use crate::floodfill::flood_fill_area;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::{Tree, TreeType};
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::prelude::SliceRandom;
use rand::Rng;
pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
pub fn generate_landuse(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
// Determine block type based on landuse tag
let binding: String = "".to_string();
let landuse_tag: &String = element.tags.get("landuse").unwrap_or(&binding);
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
let block_type = match landuse_tag.as_str() {
"greenfield" | "meadow" | "grass" | "orchard" | "forest" => GRASS_BLOCK,
"farmland" => FARMLAND,
@@ -22,13 +33,13 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
if residential_tag == "rural" {
GRASS_BLOCK
} else {
STONE_BRICKS
STONE_BRICKS // Placeholder, will be randomized per-block
}
}
"commercial" => SMOOTH_STONE,
"commercial" => SMOOTH_STONE, // Placeholder, will be randomized per-block
"education" => POLISHED_ANDESITE,
"religious" => POLISHED_ANDESITE,
"industrial" => COBBLESTONE,
"industrial" => STONE, // Placeholder, will be randomized per-block
"military" => GRAY_CONCRETE,
"railway" => GRAVEL,
"landfill" => {
@@ -44,19 +55,79 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
_ => GRASS_BLOCK,
};
// Get the area of the landuse element
let polygon_coords: Vec<(i32, i32)> = element.nodes.iter().map(|n| (n.x, n.z)).collect();
let floor_area: Vec<(i32, i32)> = flood_fill_area(&polygon_coords, args.timeout.as_ref());
// Get the area of the landuse element using cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let trees_ok_to_generate: Vec<TreeType> = {
let mut trees: Vec<TreeType> = vec![];
if let Some(leaf_type) = element.tags.get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees.push(TreeType::Oak);
trees.push(TreeType::Birch);
}
"needleleaved" => trees.push(TreeType::Spruce),
_ => {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
}
} else {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
trees
};
for (x, z) in floor_area {
if landuse_tag == "traffic_island" {
editor.set_block(block_type, x, 1, z, None, None);
} else if landuse_tag == "construction" || landuse_tag == "railway" {
editor.set_block(block_type, x, 0, z, None, Some(&[SPONGE]));
// Apply per-block randomness for certain landuse types
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
let random_value = rng.gen_range(0..100);
if random_value < 72 {
STONE_BRICKS
} else if random_value < 87 {
CRACKED_STONE_BRICKS
} else if random_value < 92 {
STONE
} else {
COBBLESTONE
}
} else if landuse_tag == "commercial" {
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
let random_value = rng.gen_range(0..100);
if random_value < 40 {
SMOOTH_STONE
} else if random_value < 70 {
STONE_BRICKS
} else if random_value < 90 {
STONE
} else {
COBBLESTONE
}
} else if landuse_tag == "industrial" {
// Industrial: primarily stone, with some stone bricks and smooth stone
let random_value = rng.gen_range(0..100);
if random_value < 70 {
STONE
} else if random_value < 90 {
STONE_BRICKS
} else {
SMOOTH_STONE
}
} else {
editor.set_block(block_type, x, 0, z, None, None);
block_type
};
if landuse_tag == "traffic_island" {
editor.set_block(actual_block, x, 1, z, None, None);
} else if landuse_tag == "construction" || landuse_tag == "railway" {
editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));
} else {
editor.set_block(actual_block, x, 0, z, None, None);
}
// Add specific features for different landuse types
@@ -84,9 +155,14 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
editor.set_block(RED_FLOWER, x, 1, z, None, None);
}
} else if random_choice < 33 {
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if random_choice < 35 {
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
} else if random_choice < 37 {
editor.set_block(FERN, x, 1, z, None, None);
} else if random_choice < 41 {
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
}
}
}
@@ -94,18 +170,31 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
let random_choice: i32 = rng.gen_range(0..30);
if random_choice == 20 {
Tree::create(editor, (x, 1, z));
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(
editor,
(x, 1, z),
tree_type,
Some(building_footprints),
);
} else if random_choice == 2 {
let flower_block: Block = match rng.gen_range(1..=5) {
let flower_block: Block = match rng.gen_range(1..=6) {
1 => OAK_LEAVES,
2 => RED_FLOWER,
3 => BLUE_FLOWER,
4 => YELLOW_FLOWER,
5 => FERN,
_ => WHITE_FLOWER,
};
editor.set_block(flower_block, x, 1, z, None, None);
} else if random_choice <= 12 {
editor.set_block(GRASS, x, 1, z, None, None);
if rng.gen_range(0..100) < 12 {
editor.set_block(FERN, x, 1, z, None, None);
} else {
editor.set_block(GRASS, x, 1, z, None, None);
}
}
}
}
@@ -207,7 +296,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
match rng.gen_range(0..200) {
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
1..=170 => editor.set_block(GRASS, x, 1, z, None, None),
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
_ => {}
}
}
@@ -216,7 +306,8 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
match rng.gen_range(0..200) {
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
1..=17 => editor.set_block(GRASS, x, 1, z, None, None),
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
3..=16 => editor.set_block(GRASS, x, 1, z, None, None),
_ => {}
}
}
@@ -225,23 +316,29 @@ pub fn generate_landuse(editor: &mut WorldEditor, element: &ProcessedWay, args:
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
let random_choice: i32 = rng.gen_range(0..1001);
if random_choice < 5 {
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if random_choice < 6 {
editor.set_block(RED_FLOWER, x, 1, z, None, None);
} else if random_choice < 9 {
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
} else if random_choice < 800 {
} else if random_choice < 40 {
editor.set_block(FERN, x, 1, z, None, None);
} else if random_choice < 65 {
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
} else if random_choice < 825 {
editor.set_block(GRASS, x, 1, z, None, None);
}
}
}
"orchard" => {
if x % 18 == 0 && z % 10 == 0 {
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
match rng.gen_range(0..100) {
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
1..=20 => editor.set_block(GRASS, x, 1, z, None, None),
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
_ => {}
}
}
@@ -275,34 +372,57 @@ pub fn generate_landuse_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if rel.tags.contains_key("landuse") {
// Generate individual ways with their original tags
// Process each outer member way individually using cached flood fill.
// We intentionally do not combine all outer nodes into one mega-way,
// because that creates a nonsensical polygon spanning the whole relation
// extent, misses the flood fill cache, and can cause multi-GB allocations.
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_landuse(editor, &member.way.clone(), args);
// Use relation tags so the member inherits the relation's landuse=* type
let way_with_rel_tags = ProcessedWay {
id: member.way.id,
nodes: member.way.nodes.clone(),
tags: rel.tags.clone(),
};
generate_landuse(
editor,
&way_with_rel_tags,
args,
flood_fill_cache,
building_footprints,
);
}
}
// Combine all outer ways into one with relation tags
let mut combined_nodes = Vec::new();
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
combined_nodes.extend(member.way.nodes.clone());
}
}
// Only process if we have nodes
if !combined_nodes.is_empty() {
// Create combined way with relation tags
let combined_way = ProcessedWay {
id: rel.id,
nodes: combined_nodes,
tags: rel.tags.clone(),
};
// Generate landuse area from combined way
generate_landuse(editor, &combined_way, args);
}
}
}
/// Generates ground blocks for place=* areas (squares, neighbourhoods, etc.)
pub fn generate_place(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
) {
let binding = String::new();
let place_tag = element.tags.get("place").unwrap_or(&binding);
// Determine block type based on place tag
let block_type = match place_tag.as_str() {
"square" => STONE_BRICKS,
"neighbourhood" | "city_block" | "quarter" | "suburb" => SMOOTH_STONE,
_ => return,
};
// Get the area using flood fill cache
let floor_area: Vec<(i32, i32)> =
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Place ground blocks
for (x, z) in floor_area {
editor.set_block(block_type, x, 0, z, None, None);
}
}

View File

@@ -1,13 +1,20 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::Tree;
use crate::floodfill::flood_fill_area;
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::Rng;
pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args: &Args) {
pub fn generate_leisure(
editor: &mut WorldEditor,
element: &ProcessedWay,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if let Some(leisure_type) = element.tags.get("leisure") {
let mut previous_node: Option<(i32, i32)> = None;
let mut corner_addup: (i32, i32, i32) = (0, 0, 0);
@@ -18,6 +25,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() {
@@ -73,15 +81,13 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
previous_node = Some((node.x, node.z));
}
// Flood-fill the interior of the leisure area
// Flood-fill the interior of the leisure area using cache
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = element
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(element.id);
for (x, z) in filled_area {
editor.set_block(block_type, x, 0, z, Some(&[GRASS_BLOCK]), None);
@@ -90,19 +96,20 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
{
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..1000);
match random_choice {
0..30 => {
// Flowers
let flower_choice = match random_choice {
0..10 => RED_FLOWER,
10..20 => YELLOW_FLOWER,
20..30 => BLUE_FLOWER,
_ => WHITE_FLOWER,
// Plants
let plant_choice = match random_choice {
0..5 => RED_FLOWER,
5..10 => YELLOW_FLOWER,
10..16 => BLUE_FLOWER,
16..22 => WHITE_FLOWER,
22..30 => FERN,
_ => unreachable!(),
};
editor.set_block(flower_choice, x, 1, z, None, None);
editor.set_block(plant_choice, x, 1, z, None, None);
}
30..90 => {
// Grass
@@ -114,7 +121,7 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
}
105..120 => {
// Tree
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
}
_ => {}
}
@@ -122,7 +129,6 @@ pub fn generate_leisure(editor: &mut WorldEditor, element: &ProcessedWay, args:
// Add playground or recreation ground features
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let random_choice: i32 = rng.gen_range(0..5000);
match random_choice {
@@ -175,31 +181,30 @@ pub fn generate_leisure_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if rel.tags.get("leisure") == Some(&"park".to_string()) {
// First generate individual ways with their original tags
// Process each outer member way individually using cached flood fill.
// We intentionally do not combine all outer nodes into one mega-way,
// because that creates a nonsensical polygon spanning the whole relation
// extent, misses the flood fill cache, and can cause multi-GB allocations.
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_leisure(editor, &member.way, args);
// Use relation tags so the member inherits the relation's leisure=* type
let way_with_rel_tags = ProcessedWay {
id: member.way.id,
nodes: member.way.nodes.clone(),
tags: rel.tags.clone(),
};
generate_leisure(
editor,
&way_with_rel_tags,
args,
flood_fill_cache,
building_footprints,
);
}
}
// Then combine all outer ways into one
let mut combined_nodes = Vec::new();
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
combined_nodes.extend(member.way.nodes.clone());
}
}
// Create combined way with relation tags
let combined_way = ProcessedWay {
id: rel.id,
nodes: combined_nodes,
tags: rel.tags.clone(),
};
// Generate leisure area from combined way
generate_leisure(editor, &combined_way, args);
}
}

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

@@ -1,16 +1,122 @@
pub mod advertising;
pub mod amenities;
pub mod barriers;
pub mod bridges;
pub mod buildings;
pub mod doors;
pub mod emergency;
pub mod highways;
pub mod historic;
pub mod landuse;
pub mod leisure;
pub mod man_made;
pub mod natural;
pub mod power;
pub mod railways;
pub mod subprocessor;
pub mod tourisms;
pub mod tree;
pub mod water_areas;
pub mod waterways;
use crate::osm_parser::ProcessedNode;
/// Merges way segments that share endpoints into closed rings.
/// Used by water_areas.rs and boundaries.rs for assembling relation members.
pub fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
let mut removed: Vec<usize> = vec![];
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
// 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;
}
if removed.contains(&i) || removed.contains(&j) {
continue;
}
let x: &Vec<ProcessedNode> = &rings[i];
let y: &Vec<ProcessedNode> = &rings[j];
// Skip empty rings (can happen after clipping)
if x.is_empty() || y.is_empty() {
continue;
}
let x_first = &x[0];
let x_last = x.last().unwrap();
let y_first = &y[0];
let y_last = y.last().unwrap();
// Skip already-closed rings
if nodes_match(x_first, x_last) {
continue;
}
if nodes_match(y_first, y_last) {
continue;
}
if nodes_match(x_first, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.reverse();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_last, y_last) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().rev().skip(1).cloned());
merged.push(x);
} else if nodes_match(x_first, y_last) {
removed.push(i);
removed.push(j);
let mut y: Vec<ProcessedNode> = y.clone();
y.extend(x.iter().skip(1).cloned());
merged.push(y);
} else if nodes_match(x_last, y_first) {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
}
}
}
removed.sort();
for r in removed.iter().rev() {
rings.remove(*r);
}
let merged_len: usize = merged.len();
for m in merged {
rings.push(m);
}
if merged_len > 0 {
merge_way_segments(rings);
}
}

View File

@@ -1,20 +1,87 @@
use crate::args::Args;
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::element_processing::tree::Tree;
use crate::floodfill::flood_fill_area;
use crate::deterministic_rng::element_rng;
use crate::element_processing::tree::{Tree, TreeType};
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
use crate::world_editor::WorldEditor;
use rand::prelude::SliceRandom;
use rand::Rng;
pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, args: &Args) {
pub fn generate_natural(
editor: &mut WorldEditor,
element: &ProcessedElement,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if let Some(natural_type) = element.tags().get("natural") {
if natural_type == "tree" {
if let ProcessedElement::Node(node) = element {
let x: i32 = node.x;
let z: i32 = node.z;
Tree::create(editor, (x, 1, z));
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
if let Some(species) = element.tags().get("species") {
if species.contains("Betula") {
trees_ok_to_generate.push(TreeType::Birch);
}
if species.contains("Quercus") {
trees_ok_to_generate.push(TreeType::Oak);
}
if species.contains("Picea") {
trees_ok_to_generate.push(TreeType::Spruce);
}
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
match genus_wikidata.as_str() {
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
_ => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
}
} else if let Some(genus) = element.tags().get("genus") {
match genus.as_str() {
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
_ => trees_ok_to_generate.push(TreeType::Oak),
}
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Birch);
}
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
_ => {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
}
} else {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
if trees_ok_to_generate.is_empty() {
trees_ok_to_generate.push(TreeType::Oak);
trees_ok_to_generate.push(TreeType::Spruce);
trees_ok_to_generate.push(TreeType::Birch);
}
let mut rng = element_rng(element.id());
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
}
} else {
let mut previous_node: Option<(i32, i32)> = None;
@@ -69,17 +136,36 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
previous_node = Some((x, z));
}
// If there are natural nodes, flood-fill the area
// If there are natural nodes, flood-fill the area using cache
if corner_addup != (0, 0, 0) {
let polygon_coords: Vec<(i32, i32)> = way
.nodes
.iter()
.map(|n: &crate::osm_parser::ProcessedNode| (n.x, n.z))
.collect();
let filled_area: Vec<(i32, i32)> =
flood_fill_area(&polygon_coords, args.timeout.as_ref());
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
let mut rng: rand::prelude::ThreadRng = rand::thread_rng();
let trees_ok_to_generate: Vec<TreeType> = {
let mut trees: Vec<TreeType> = vec![];
if let Some(leaf_type) = element.tags().get("leaf_type") {
match leaf_type.as_str() {
"broadleaved" => {
trees.push(TreeType::Oak);
trees.push(TreeType::Birch);
}
"needleleaved" => trees.push(TreeType::Spruce),
_ => {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
}
} else {
trees.push(TreeType::Oak);
trees.push(TreeType::Spruce);
trees.push(TreeType::Birch);
}
trees
};
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
let mut rng = element_rng(way.id);
for (x, z) in filled_area {
editor.set_block(block_type, x, 0, z, None, None);
@@ -132,7 +218,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
}
let random_choice = rng.gen_range(0..500);
if random_choice == 0 {
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if random_choice == 1 {
let flower_block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,
@@ -161,7 +247,15 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
}
let random_choice: i32 = rng.gen_range(0..30);
if random_choice == 0 {
Tree::create(editor, (x, 1, z));
let tree_type = *trees_ok_to_generate
.choose(&mut rng)
.unwrap_or(&TreeType::Oak);
Tree::create_of_type(
editor,
(x, 1, z),
tree_type,
Some(building_footprints),
);
} else if random_choice == 1 {
let flower_block = match rng.gen_range(1..=4) {
1 => RED_FLOWER,
@@ -220,7 +314,11 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
// TODO implement mangrove
let random_choice: i32 = rng.gen_range(0..40);
if random_choice == 0 {
Tree::create(editor, (x, 1, z));
Tree::create(
editor,
(x, 1, z),
Some(building_footprints),
);
} else if random_choice < 35 {
editor.set_block(GRASS, x, 1, z, None, None);
}
@@ -304,6 +402,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
Tree::create(
editor,
(cluster_x, 1, cluster_z),
Some(building_footprints),
);
} else if vegetation_chance < 15 {
// 15% chance for grass
@@ -416,7 +515,7 @@ pub fn generate_natural(editor: &mut WorldEditor, element: &ProcessedElement, ar
let hill_chance = rng.gen_range(0..1000);
if hill_chance == 0 {
// 0.1% chance for rare trees
Tree::create(editor, (x, 1, z));
Tree::create(editor, (x, 1, z), Some(building_footprints));
} else if hill_chance < 50 {
// 5% chance for flowers
let flower_block = match rng.gen_range(1..=4) {
@@ -448,34 +547,30 @@ pub fn generate_natural_from_relation(
editor: &mut WorldEditor,
rel: &ProcessedRelation,
args: &Args,
flood_fill_cache: &FloodFillCache,
building_footprints: &BuildingFootprintBitmap,
) {
if rel.tags.contains_key("natural") {
// Generate individual ways with their original tags
// Process each outer member way individually using cached flood fill.
// We intentionally do not combine all outer nodes into one mega-way,
// because that creates a nonsensical polygon spanning the whole relation
// extent, misses the flood fill cache, and can cause multi-GB allocations.
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
generate_natural(editor, &ProcessedElement::Way(member.way.clone()), args);
// Use relation tags so the member inherits the relation's natural=* type
let way_with_rel_tags = ProcessedWay {
id: member.way.id,
nodes: member.way.nodes.clone(),
tags: rel.tags.clone(),
};
generate_natural(
editor,
&ProcessedElement::Way(way_with_rel_tags),
args,
flood_fill_cache,
building_footprints,
);
}
}
// Combine all outer ways into one with relation tags
let mut combined_nodes = Vec::new();
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
combined_nodes.extend(member.way.nodes.clone());
}
}
// Only process if we have nodes
if !combined_nodes.is_empty() {
// Create combined way with relation tags
let combined_way = ProcessedWay {
id: rel.id,
nodes: combined_nodes,
tags: rel.tags.clone(),
};
// Generate natural area from combined way
generate_natural(editor, &ProcessedElement::Way(combined_way), args);
}
}
}

View File

@@ -0,0 +1,385 @@
//! Processing of power infrastructure elements.
//!
//! This module handles power-related OSM elements including:
//! - `power=tower` - Large electricity pylons
//! - `power=pole` - Smaller wooden/concrete poles
//! - `power=line` - Power lines connecting towers/poles
use crate::block_definitions::*;
use crate::bresenham::bresenham_line;
use crate::osm_parser::{ProcessedElement, ProcessedNode, ProcessedWay};
use crate::world_editor::WorldEditor;
/// Generate power infrastructure from way elements (power lines)
pub fn generate_power(editor: &mut WorldEditor, element: &ProcessedElement) {
// 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;
}
}
if let Some(level) = element.tags().get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Skip underground power infrastructure
if element
.tags()
.get("location")
.map(|v| v == "underground" || v == "underwater")
.unwrap_or(false)
{
return;
}
if element
.tags()
.get("tunnel")
.map(|v| v == "yes")
.unwrap_or(false)
{
return;
}
if let Some(power_type) = element.tags().get("power") {
match power_type.as_str() {
"line" | "minor_line" => {
if let ProcessedElement::Way(way) = element {
generate_power_line(editor, way);
}
}
"tower" => generate_power_tower(editor, element),
"pole" => generate_power_pole(editor, element),
_ => {}
}
}
}
/// Generate power infrastructure from node elements
pub fn generate_power_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
// Skip if 'layer' or 'level' is negative in the tags
if let Some(layer) = node.tags.get("layer") {
if layer.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
if let Some(level) = node.tags.get("level") {
if level.parse::<i32>().unwrap_or(0) < 0 {
return;
}
}
// Skip underground power infrastructure
if node
.tags
.get("location")
.map(|v| v == "underground" || v == "underwater")
.unwrap_or(false)
{
return;
}
if node.tags.get("tunnel").map(|v| v == "yes").unwrap_or(false) {
return;
}
if let Some(power_type) = node.tags.get("power") {
match power_type.as_str() {
"tower" => generate_power_tower_from_node(editor, node),
"pole" => generate_power_pole_from_node(editor, node),
_ => {}
}
}
}
/// Generate a high-voltage transmission tower (pylon) from a ProcessedElement
fn generate_power_tower(editor: &mut WorldEditor, element: &ProcessedElement) {
let Some(first_node) = element.nodes().next() else {
return;
};
let height = element
.tags()
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(25)
.clamp(15, 40);
generate_power_tower_impl(editor, first_node.x, first_node.z, height);
}
/// Generate a high-voltage transmission tower (pylon) from a ProcessedNode
fn generate_power_tower_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(25)
.clamp(15, 40);
generate_power_tower_impl(editor, node.x, node.z, height);
}
/// Generate a high-voltage transmission tower (pylon)
///
/// Creates a realistic lattice tower structure using iron bars and iron blocks.
/// The design is a tapered lattice tower with cross-bracing and insulators.
fn generate_power_tower_impl(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
// Tower design constants
let base_width = 3; // Half-width at base (so 7x7 footprint)
let top_width = 1; // Half-width at top (so 3x3)
let arm_height = height - 4; // Height where arms extend
let arm_length = 5; // How far arms extend horizontally
// Build the four corner legs with tapering
for y in 1..=height {
// Calculate taper: legs get closer together as we go up
let progress = y as f32 / height as f32;
let current_width = base_width - ((base_width - top_width) as f32 * progress) as i32;
// Four corner positions
let corners = [
(x - current_width, z - current_width),
(x + current_width, z - current_width),
(x - current_width, z + current_width),
(x + current_width, z + current_width),
];
for (cx, cz) in corners {
editor.set_block(IRON_BLOCK, cx, y, cz, None, None);
}
// Add horizontal cross-bracing every 5 blocks
if y % 5 == 0 && y < height - 2 {
// Connect corners horizontally
for dx in -current_width..=current_width {
editor.set_block(IRON_BLOCK, x + dx, y, z - current_width, None, None);
editor.set_block(IRON_BLOCK, x + dx, y, z + current_width, None, None);
}
for dz in -current_width..=current_width {
editor.set_block(IRON_BLOCK, x - current_width, y, z + dz, None, None);
editor.set_block(IRON_BLOCK, x + current_width, y, z + dz, None, None);
}
}
// Add diagonal bracing between cross-brace levels
if y % 5 >= 1 && y % 5 <= 4 && y > 1 && y < height - 2 {
let prev_width = base_width
- ((base_width - top_width) as f32 * ((y - 1) as f32 / height as f32)) as i32;
// Only add center vertical support if the width changed
if current_width != prev_width || y % 5 == 2 {
editor.set_block(IRON_BARS, x, y, z, None, None);
}
}
}
// Create the cross-arms at arm_height for holding power lines
// These extend outward in two directions (perpendicular to typical line direction)
for arm_offset in [-arm_length, arm_length] {
// Main arm beam (iron blocks for strength)
for dx in 0..=arm_length {
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
editor.set_block(IRON_BLOCK, arm_x, arm_height, z, None, None);
// Add second arm perpendicular
editor.set_block(
IRON_BLOCK,
x,
arm_height,
z + if arm_offset < 0 { -dx } else { dx },
None,
None,
);
}
// Insulators hanging from arm ends (end rods to simulate ceramic insulators)
let end_x = if arm_offset < 0 {
x - arm_length
} else {
x + arm_length
};
editor.set_block(END_ROD, end_x, arm_height - 1, z, None, None);
editor.set_block(END_ROD, x, arm_height - 1, z + arm_offset, None, None);
}
// Add a second, smaller arm set lower for additional circuits
let lower_arm_height = arm_height - 6;
if lower_arm_height > 5 {
let lower_arm_length = arm_length - 1;
for arm_offset in [-lower_arm_length, lower_arm_length] {
for dx in 0..=lower_arm_length {
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
editor.set_block(IRON_BLOCK, arm_x, lower_arm_height, z, None, None);
}
let end_x = if arm_offset < 0 {
x - lower_arm_length
} else {
x + lower_arm_length
};
editor.set_block(END_ROD, end_x, lower_arm_height - 1, z, None, None);
}
}
// Top finial/lightning rod
editor.set_block(IRON_BLOCK, x, height, z, None, None);
editor.set_block(LIGHTNING_ROD, x, height + 1, z, None, None);
// Concrete foundation at base
for dx in -3..=3 {
for dz in -3..=3 {
editor.set_block(GRAY_CONCRETE, x + dx, 0, z + dz, None, None);
}
}
}
/// Generate a wooden/concrete power pole from a ProcessedElement
fn generate_power_pole(editor: &mut WorldEditor, element: &ProcessedElement) {
let Some(first_node) = element.nodes().next() else {
return;
};
let height = element
.tags()
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(10)
.clamp(6, 15);
let pole_material = element
.tags()
.get("material")
.map(|m| m.as_str())
.unwrap_or("wood");
generate_power_pole_impl(editor, first_node.x, first_node.z, height, pole_material);
}
/// Generate a wooden/concrete power pole from a ProcessedNode
fn generate_power_pole_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
let height = node
.tags
.get("height")
.and_then(|h| h.parse::<i32>().ok())
.unwrap_or(10)
.clamp(6, 15);
let pole_material = node
.tags
.get("material")
.map(|m| m.as_str())
.unwrap_or("wood");
generate_power_pole_impl(editor, node.x, node.z, height, pole_material);
}
/// Generate a wooden/concrete power pole
///
/// Creates a simpler single-pole structure for lower voltage distribution lines.
fn generate_power_pole_impl(
editor: &mut WorldEditor,
x: i32,
z: i32,
height: i32,
pole_material: &str,
) {
let pole_block = match pole_material {
"concrete" => LIGHT_GRAY_CONCRETE,
"steel" | "metal" => IRON_BLOCK,
_ => OAK_LOG, // Default to wood
};
// Build the main pole
for y in 1..=height {
editor.set_block(pole_block, x, y, z, None, None);
}
// Cross-arm at top (perpendicular beam for wires)
let arm_length = 2;
for dx in -arm_length..=arm_length {
editor.set_block(OAK_FENCE, x + dx, height, z, None, None);
}
// Insulators at arm ends
editor.set_block(END_ROD, x - arm_length, height + 1, z, None, None);
editor.set_block(END_ROD, x + arm_length, height + 1, z, None, None);
editor.set_block(END_ROD, x, height + 1, z, None, None); // Center insulator
}
/// Generate power lines connecting towers/poles
///
/// Creates a catenary-like curve (simplified) between nodes to simulate
/// the natural sag of power cables.
fn generate_power_line(editor: &mut WorldEditor, way: &ProcessedWay) {
if way.nodes.len() < 2 {
return;
}
// Determine line height based on voltage (higher voltage = taller structures)
let base_height = way
.tags
.get("voltage")
.and_then(|v| v.parse::<i32>().ok())
.map(|voltage| {
if voltage >= 220000 {
22 // High voltage transmission
} else if voltage >= 110000 {
18
} else if voltage >= 33000 {
14
} else {
10 // Distribution lines
}
})
.unwrap_or(15);
// Process consecutive node pairs
for i in 1..way.nodes.len() {
let start = &way.nodes[i - 1];
let end = &way.nodes[i];
// Calculate distance between nodes
let dx = (end.x - start.x) as f64;
let dz = (end.z - start.z) as f64;
let distance = (dx * dx + dz * dz).sqrt();
// Calculate sag based on span length (longer spans = more sag)
let max_sag = (distance / 15.0).clamp(1.0, 6.0) as i32;
// Determine chain orientation based on line direction
// If the line runs more along X-axis, use CHAIN_X; if more along Z-axis, use CHAIN_Z
let chain_block = if dx.abs() >= dz.abs() {
CHAIN_X // Line runs primarily along X-axis
} else {
CHAIN_Z // Line runs primarily along Z-axis
};
// Generate points along the line using Bresenham
let line_points = bresenham_line(start.x, 0, start.z, end.x, 0, end.z);
for (idx, (lx, _, lz)) in line_points.iter().enumerate() {
// Calculate position along the span (0.0 to 1.0)
// Use len-1 as denominator so last point reaches t=1.0
let denom = (line_points.len().saturating_sub(1)).max(1) as f64;
let t = idx as f64 / denom;
// Catenary approximation: sag is maximum at center, zero at ends
// Using parabola: sag = 4 * max_sag * t * (1 - t)
let sag = (4.0 * max_sag as f64 * t * (1.0 - t)) as i32;
// Ensure wire doesn't go underground (minimum height of 3 blocks above ground)
let wire_y = (base_height - sag).max(3);
// Place the wire block (chain aligned with line direction)
editor.set_block(chain_block, *lx, wire_y, *lz, None, None);
// For high voltage lines, add parallel wires offset to sides
if base_height >= 18 {
// Three-phase power: 3 parallel lines
// Offset perpendicular to the line direction
if dx.abs() >= dz.abs() {
// Line runs along X, offset in Z
editor.set_block(chain_block, *lx, wire_y, *lz + 1, None, None);
editor.set_block(chain_block, *lx, wire_y, *lz - 1, None, None);
} else {
// Line runs along Z, offset in X
editor.set_block(chain_block, *lx + 1, wire_y, *lz, None, None);
editor.set_block(chain_block, *lx - 1, wire_y, *lz, None, None);
}
}
}
}
}

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

@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building level floors (1nd layer above floor)
/// Interior layout for building level floors (1st layer above floor)
#[rustfmt::skip]
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
@@ -114,6 +114,119 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
];
// Generic Abandoned Building Interiors
/// Interior layout for building ground floors (1st layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building ground floors (2nd layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
];
/// Interior layout for building level floors (1st layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
];
/// Interior layout for building level floors (2nd layer above floor)
#[rustfmt::skip]
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
];
/// Maps interior layout characters to actual block types for different floor layers
#[inline(always)]
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
@@ -133,7 +246,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,12 +258,19 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
Some(DARK_OAK_DOOR_LOWER)
}
}
'J' => Some(JUKEBOX), // Jukebox
'G' => Some(GLOWSTONE), // Glowstone
'N' => Some(BREWING_STAND), // Brewing Stand
'T' => Some(WHITE_CARPET), // White Carpet
'E' => Some(OAK_LEAVES), // Oak Leaves
_ => None, // Default case for unknown characters
'J' => Some(NOTE_BLOCK), // Note block
'G' => Some(GLOWSTONE), // Glowstone
'N' => Some(BREWING_STAND), // Brewing Stand
'T' => Some(WHITE_CARPET), // White Carpet
'E' => Some(OAK_LEAVES), // Oak Leaves
'O' => Some(COBWEB), // Cobweb
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
'Q' => Some(SCAFFOLDING), // Scaffolding
_ => None, // Default case for unknown characters
}
}
@@ -170,6 +290,7 @@ pub fn generate_building_interior(
args: &crate::args::Args,
element: &crate::osm_parser::ProcessedWay,
abs_terrain_offset: i32,
is_abandoned_building: bool,
) {
// Skip interior generation for very small buildings
let width = max_x - min_x + 1;
@@ -214,7 +335,13 @@ pub fn generate_building_interior(
};
// Choose the appropriate interior pattern based on floor number
let (layer1, layer2) = if floor_index == 0 {
let (layer1, layer2) = if is_abandoned_building {
if floor_index == 0 {
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
} else {
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
}
} else if floor_index == 0 {
// Ground floor uses INTERIOR1 patterns
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
} else {

View File

@@ -1,4 +1,6 @@
use crate::block_definitions::*;
use crate::deterministic_rng::coord_rng;
use crate::floodfill_cache::BuildingFootprintBitmap;
use crate::world_editor::WorldEditor;
use rand::Rng;
@@ -81,6 +83,33 @@ const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [
((0, 7, 0), (0, 8, 0)),
];
/// Dark oak: short but wide canopy, leaves start at y=3 up to y=6 with a cap
const DARK_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [
((-1, 3, 0), (-1, 6, 0)),
((1, 3, 0), (1, 6, 0)),
((0, 3, -1), (0, 6, -1)),
((0, 3, 1), (0, 6, 1)),
((0, 6, 0), (0, 7, 0)),
];
/// Jungle: tall tree with canopy only near the top, leaves from y=7 to y=11
const JUNGLE_LEAVES_FILL: [(Coord, Coord); 5] = [
((-1, 7, 0), (-1, 11, 0)),
((1, 7, 0), (1, 11, 0)),
((0, 7, -1), (0, 11, -1)),
((0, 7, 1), (0, 11, 1)),
((0, 11, 0), (0, 12, 0)),
];
/// Acacia: umbrella-shaped canopy with a gentle dome, leaves from y=5 to y=8
const ACACIA_LEAVES_FILL: [(Coord, Coord); 5] = [
((-1, 5, 0), (-1, 8, 0)),
((1, 5, 0), (1, 8, 0)),
((0, 5, -1), (0, 8, -1)),
((0, 5, 1), (0, 8, 1)),
((0, 8, 0), (0, 9, 0)),
];
//////////////////////////////////////////////////
/// Helper function to set blocks in various patterns.
@@ -90,10 +119,14 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
}
}
#[derive(Clone, Copy)]
pub enum TreeType {
Oak,
Spruce,
Birch,
DarkOak,
Jungle,
Acacia,
}
// TODO what should be moved in, and what should be referenced?
@@ -107,7 +140,49 @@ pub struct Tree<'a> {
}
impl Tree<'_> {
pub fn create(editor: &mut WorldEditor, (x, y, z): Coord) {
/// Creates a tree at the specified coordinates.
///
/// # Arguments
/// * `editor` - The world editor to place blocks
/// * `(x, y, z)` - The base coordinates for the tree
/// * `building_footprints` - Optional bitmap of (x, z) coordinates that are inside buildings.
/// If provided, trees will not be placed at coordinates within this bitmap.
pub fn create(
editor: &mut WorldEditor,
(x, y, z): Coord,
building_footprints: Option<&BuildingFootprintBitmap>,
) {
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
// The element_id of 0 is used as a salt for tree-specific randomness
let mut rng = coord_rng(x, z, 0);
let tree_type = match rng.gen_range(1..=10) {
1..=3 => TreeType::Oak,
4..=5 => TreeType::Spruce,
6..=7 => TreeType::Birch,
8 => TreeType::DarkOak,
9 => TreeType::Jungle,
10 => TreeType::Acacia,
_ => unreachable!(),
};
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
}
/// Creates a tree of a specific type at the specified coordinates.
pub fn create_of_type(
editor: &mut WorldEditor,
(x, y, z): Coord,
tree_type: TreeType,
building_footprints: Option<&BuildingFootprintBitmap>,
) {
// Skip if this coordinate is inside a building
if let Some(footprints) = building_footprints {
if footprints.contains(x, z) {
return;
}
}
let mut blacklist: Vec<Block> = Vec::new();
blacklist.extend(Self::get_building_wall_blocks());
blacklist.extend(Self::get_building_floor_blocks());
@@ -115,14 +190,7 @@ impl Tree<'_> {
blacklist.extend(Self::get_functional_blocks());
blacklist.push(WATER);
let mut rng = rand::thread_rng();
let tree = Self::get_tree(match rng.gen_range(1..=3) {
1 => TreeType::Oak,
2 => TreeType::Spruce,
3 => TreeType::Birch,
_ => unreachable!(),
});
let tree = Self::get_tree(tree_type);
// Build the logs
editor.fill_blocks(
@@ -179,9 +247,9 @@ impl Tree<'_> {
// kind,
log_block: SPRUCE_LOG,
log_height: 9,
leaves_block: BIRCH_LEAVES, // TODO Is this correct?
leaves_block: SPRUCE_LEAVES,
leaves_fill: &SPRUCE_LEAVES_FILL,
// TODO can I omit the third empty vec? May cause issues with iter zip
// Conical shape: wide at bottom, narrow at top
round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]],
},
@@ -193,6 +261,44 @@ impl Tree<'_> {
leaves_fill: &BIRCH_LEAVES_FILL,
round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]],
},
TreeType::DarkOak => Self {
// Short trunk with a very wide, bushy canopy
log_block: DARK_OAK_LOG,
log_height: 5,
leaves_block: DARK_OAK_LEAVES,
leaves_fill: &DARK_OAK_LEAVES_FILL,
// All 3 round patterns used for maximum width
round_ranges: [
(3..=6).rev().collect(),
(3..=5).rev().collect(),
(4..=5).rev().collect(),
],
},
TreeType::Jungle => Self {
// Tall trunk, canopy clustered at the top
log_block: JUNGLE_LOG,
log_height: 10,
leaves_block: JUNGLE_LEAVES,
leaves_fill: &JUNGLE_LEAVES_FILL,
// Canopy only near the top of the tree
round_ranges: [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]],
},
TreeType::Acacia => Self {
// Medium trunk with umbrella-shaped canopy, domed center
log_block: ACACIA_LOG,
log_height: 6,
leaves_block: ACACIA_LEAVES,
leaves_fill: &ACACIA_LEAVES_FILL,
// Inner rounds reach higher → gentle dome, outer stays low → wide brim
round_ranges: [
(5..=8).rev().collect(),
(5..=7).rev().collect(),
(6..=7).rev().collect(),
],
},
} // match
} // fn get_tree
@@ -307,7 +413,7 @@ impl Tree<'_> {
FURNACE,
ANVIL,
BREWING_STAND,
JUKEBOX,
NOTE_BLOCK,
BOOKSHELF,
CAULDRON,
// Beds
@@ -328,6 +434,9 @@ impl Tree<'_> {
GRAY_STAINED_GLASS,
LIGHT_GRAY_STAINED_GLASS,
BROWN_STAINED_GLASS,
CYAN_STAINED_GLASS,
BLUE_STAINED_GLASS,
LIGHT_BLUE_STAINED_GLASS,
TINTED_GLASS,
// Carpets
WHITE_CARPET,

View File

@@ -1,19 +1,40 @@
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) {
let start_time = Instant::now();
pub fn generate_water_area_from_way(
editor: &mut WorldEditor,
element: &ProcessedWay,
_xzbbox: &XZBBox,
) {
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, &[]);
}
pub fn generate_water_areas_from_relation(
editor: &mut WorldEditor,
element: &ProcessedRelation,
xzbbox: &XZBBox,
) {
// 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,157 +57,131 @@ 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
super::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();
super::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;
}
}
super::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);
}
// Merges ways that share nodes into full loops
fn merge_loopy_loops(loops: &mut Vec<Vec<ProcessedNode>>) {
let mut removed: Vec<usize> = vec![];
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
fn generate_water_areas(
editor: &mut WorldEditor,
outers: &[Vec<ProcessedNode>],
inners: &[Vec<ProcessedNode>],
) {
// 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 i in 0..loops.len() {
for j in 0..loops.len() {
if i == j {
continue;
}
if removed.contains(&i) || removed.contains(&j) {
continue;
}
let x: &Vec<ProcessedNode> = &loops[i];
let y: &Vec<ProcessedNode> = &loops[j];
// it's looped already
if x[0].id == x.last().unwrap().id {
continue;
}
// it's looped already
if y[0].id == y.last().unwrap().id {
continue;
}
if x[0].id == y[0].id {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.reverse();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
} else if x.last().unwrap().id == y.last().unwrap().id {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().rev().skip(1).cloned());
merged.push(x);
} else if x[0].id == y.last().unwrap().id {
removed.push(i);
removed.push(j);
let mut y: Vec<ProcessedNode> = y.clone();
y.extend(x.iter().skip(1).cloned());
merged.push(y);
} else if x.last().unwrap().id == y[0].id {
removed.push(i);
removed.push(j);
let mut x: Vec<ProcessedNode> = x.clone();
x.extend(y.iter().skip(1).cloned());
merged.push(x);
}
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);
}
}
removed.sort();
for r in removed.iter().rev() {
loops.remove(*r);
// If no valid bounds, nothing to fill
if poly_min_x == i32::MAX || poly_max_x == i32::MIN {
return;
}
let merged_len: usize = merged.len();
for m in merged {
loops.push(m);
}
// 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);
if merged_len > 0 {
merge_loopy_loops(loops);
}
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);
}
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;
}
}
@@ -206,8 +201,8 @@ fn inverse_floodfill(
outers: Vec<Vec<XZPoint>>,
inners: Vec<Vec<XZPoint>>,
editor: &mut WorldEditor,
start_time: Instant,
) {
// Convert to geo Polygons with normalized winding order
let inners: Vec<_> = inners
.into_iter()
.map(|x| {
@@ -219,6 +214,7 @@ fn inverse_floodfill(
),
vec![],
)
.orient(Direction::Default)
})
.collect();
@@ -233,17 +229,11 @@ fn inverse_floodfill(
),
vec![],
)
.orient(Direction::Default)
})
.collect();
inverse_floodfill_recursive(
(min_x, min_z),
(max_x, max_z),
&outers,
&inners,
editor,
start_time,
);
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
}
fn inverse_floodfill_recursive(
@@ -252,12 +242,11 @@ fn inverse_floodfill_recursive(
outers: &[Polygon],
inners: &[Polygon],
editor: &mut WorldEditor,
start_time: Instant,
) {
// Check if we've exceeded 25 seconds
if start_time.elapsed().as_secs() > 25 {
println!("Water area generation exceeded 25 seconds, continuing anyway");
}
// Check if we've exceeded 40 seconds
// if start_time.elapsed().as_secs() > 40 {
// println!("Water area generation exceeded 40 seconds, continuing anyway");
// }
const ITERATIVE_THRES: i64 = 10_000;
@@ -312,7 +301,6 @@ fn inverse_floodfill_recursive(
&outers_intersects,
&inners_intersects,
editor,
start_time,
);
}
}

View File

@@ -1,11 +1,15 @@
use crate::coordinate_system::{geographic::LLBBox, transformation::geo_distance};
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use crate::{
coordinate_system::{geographic::LLBBox, transformation::geo_distance},
progress::emit_gui_progress_update,
};
use image::Rgb;
use std::path::Path;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
/// Maximum Y coordinate in Minecraft (build height limit)
const MAX_Y: i32 = 319;
/// Scale factor for converting real elevation to Minecraft heights
const BASE_HEIGHT_SCALE: f64 = 0.7;
/// AWS S3 Terrarium tiles endpoint (no API key required)
const AWS_TERRARIUM_URL: &str =
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
@@ -15,6 +19,10 @@ const TERRARIUM_OFFSET: f64 = 32768.0;
const MIN_ZOOM: u8 = 10;
/// Maximum zoom level for terrain tiles
const MAX_ZOOM: u8 = 15;
/// Maximum concurrent tile downloads to be respectful to AWS
const MAX_CONCURRENT_DOWNLOADS: usize = 8;
/// Maximum age for cached tiles in days before they are cleaned up
const TILE_CACHE_MAX_AGE_DAYS: u64 = 7;
/// Holds processed elevation data and metadata
#[derive(Clone)]
@@ -27,6 +35,93 @@ pub struct ElevationData {
pub(crate) height: usize,
}
/// RGB image buffer type for elevation tiles
type TileImage = image::ImageBuffer<Rgb<u8>, Vec<u8>>;
/// Result type for tile download operations: ((tile_x, tile_y), image) or error
type TileDownloadResult = Result<((u32, u32), TileImage), String>;
/// Cleans up old cached tiles from the tile cache directory.
/// Only deletes .png files within the arnis-tile-cache directory that are older than TILE_CACHE_MAX_AGE_DAYS.
/// This function is safe and will not delete files outside the cache directory or fail on errors.
pub fn cleanup_old_cached_tiles() {
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
if !tile_cache_dir.exists() || !tile_cache_dir.is_dir() {
return; // Nothing to clean up
}
let max_age = std::time::Duration::from_secs(TILE_CACHE_MAX_AGE_DAYS * 24 * 60 * 60);
let now = std::time::SystemTime::now();
let mut deleted_count = 0;
let mut error_count = 0;
// Read directory entries
let entries = match std::fs::read_dir(&tile_cache_dir) {
Ok(entries) => entries,
Err(_) => {
return;
}
};
for entry in entries.flatten() {
let path = entry.path();
// Safety check: only process .png files within the cache directory
if !path.is_file() {
continue;
}
// Verify the file is a .png and follows our naming pattern (z{zoom}_x{x}_y{y}.png)
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(name) => name,
None => continue,
};
if !file_name.ends_with(".png") || !file_name.starts_with('z') {
continue; // Skip files that don't match our tile naming pattern
}
// Check file age
let metadata = match std::fs::metadata(&path) {
Ok(m) => m,
Err(_) => continue,
};
let modified = match metadata.modified() {
Ok(time) => time,
Err(_) => continue,
};
let age = match now.duration_since(modified) {
Ok(duration) => duration,
Err(_) => continue, // File modified in the future? Skip it.
};
if age > max_age {
match std::fs::remove_file(&path) {
Ok(()) => deleted_count += 1,
Err(e) => {
// Log but don't fail, this is a best-effort cleanup
if error_count == 0 {
eprintln!(
"Warning: Failed to delete old cached tile {}: {e}",
path.display()
);
}
error_count += 1;
}
}
}
}
if deleted_count > 0 {
println!("Cleaned up {deleted_count} old cached elevation tiles (older than {TILE_CACHE_MAX_AGE_DAYS} days)");
}
if error_count > 1 {
eprintln!("Warning: Failed to delete {error_count} old cached tiles");
}
}
/// Calculates appropriate zoom level for the given bounding box
fn calculate_zoom_level(bbox: &LLBBox) -> u8 {
let lat_diff: f64 = (bbox.max().lat() - bbox.min().lat()).abs();
@@ -44,6 +139,131 @@ fn lat_lng_to_tile(lat: f64, lng: f64, zoom: u8) -> (u32, u32) {
(x, y)
}
/// Maximum number of retry attempts for tile downloads
const TILE_DOWNLOAD_MAX_RETRIES: u32 = 3;
/// Base delay in milliseconds for exponential backoff between retries
const TILE_DOWNLOAD_RETRY_BASE_DELAY_MS: u64 = 500;
/// Downloads a tile from AWS Terrain Tiles service with retry logic
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>>, String> {
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 mut last_error: String = String::new();
for attempt in 0..TILE_DOWNLOAD_MAX_RETRIES {
if attempt > 0 {
// Exponential backoff: 500ms, 1000ms, 2000ms...
let delay_ms = TILE_DOWNLOAD_RETRY_BASE_DELAY_MS * (1 << (attempt - 1));
eprintln!(
"Retry attempt {}/{} for tile x={},y={},z={} after {}ms delay",
attempt,
TILE_DOWNLOAD_MAX_RETRIES - 1,
tile_x,
tile_y,
zoom,
delay_ms
);
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
match download_tile_once(client, &url, tile_path) {
Ok(img) => return Ok(img),
Err(e) => {
last_error = e;
if attempt < TILE_DOWNLOAD_MAX_RETRIES - 1 {
eprintln!(
"Tile download failed for x={},y={},z={}: {}",
tile_x, tile_y, zoom, last_error
);
}
}
}
}
Err(format!(
"Failed to download tile x={},y={},z={} after {} attempts: {}",
tile_x, tile_y, zoom, TILE_DOWNLOAD_MAX_RETRIES, last_error
))
}
/// Single download attempt for a tile (no retries)
fn download_tile_once(
client: &reqwest::blocking::Client,
url: &str,
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
let response = client.get(url).send().map_err(|e| e.to_string())?;
response.error_for_status_ref().map_err(|e| e.to_string())?;
let bytes = response.bytes().map_err(|e| e.to_string())?;
std::fs::write(tile_path, &bytes).map_err(|e| e.to_string())?;
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
Ok(img.to_rgb8())
}
/// Fetches a tile from cache or downloads it if not available
/// Note: In parallel execution, multiple threads may attempt to download the same tile
/// if it's missing or corrupted. This is harmless (just wastes some bandwidth) as
/// file writes are atomic at the OS level.
fn fetch_or_load_tile(
client: &reqwest::blocking::Client,
tile_x: u32,
tile_y: u32,
zoom: u8,
tile_path: &Path,
) -> Result<image::ImageBuffer<Rgb<u8>, Vec<u8>>, String> {
if tile_path.exists() {
// Try to load cached tile, but handle corruption gracefully
match image::open(tile_path) {
Ok(img) => {
println!(
"Loading cached tile x={tile_x},y={tile_y},z={zoom} from {}",
tile_path.display()
);
Ok(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(e) = std::fs::remove_file(tile_path) {
eprintln!("Warning: Failed to remove corrupted tile file: {e}");
#[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)
}
}
pub fn fetch_elevation_data(
bbox: &LLBBox,
scale: f64,
@@ -67,51 +287,69 @@ pub fn fetch_elevation_data(
let mut height_grid: Vec<Vec<f64>> = vec![vec![f64::NAN; grid_width]; grid_height];
let mut extreme_values_found = Vec::new(); // Track extreme values for debugging
let client: reqwest::blocking::Client = reqwest::blocking::Client::new();
let tile_cache_dir = Path::new("./arnis-tile-cache");
let tile_cache_dir = PathBuf::from("./arnis-tile-cache");
if !tile_cache_dir.exists() {
std::fs::create_dir_all(tile_cache_dir)?;
std::fs::create_dir_all(&tile_cache_dir)?;
}
// Fetch and process each tile
for (tile_x, tile_y) in &tiles {
// Check if tile is already cached
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
// Create a shared HTTP client for connection pooling
let client = reqwest::blocking::Client::new();
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());
// Download tiles in parallel with limited concurrency to be respectful to AWS
let num_tiles = tiles.len();
println!(
"Downloading {num_tiles} elevation tiles (up to {MAX_CONCURRENT_DOWNLOADS} concurrent)..."
);
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()
};
// Use a custom thread pool to limit concurrent downloads
let thread_pool = rayon::ThreadPoolBuilder::new()
.num_threads(MAX_CONCURRENT_DOWNLOADS)
.build()
.map_err(|e| format!("Failed to create thread pool: {e}"))?;
let downloaded_tiles: Vec<TileDownloadResult> = thread_pool.install(|| {
tiles
.par_iter()
.map(|(tile_x, tile_y)| {
let tile_path = tile_cache_dir.join(format!("z{zoom}_x{tile_x}_y{tile_y}.png"));
let rgb_img = fetch_or_load_tile(&client, *tile_x, *tile_y, zoom, &tile_path)?;
Ok(((*tile_x, *tile_y), rgb_img))
})
.collect()
});
// Check for any download errors
let mut successful_tiles = Vec::new();
for result in downloaded_tiles {
match result {
Ok(tile_data) => successful_tiles.push(tile_data),
Err(e) => {
eprintln!("Warning: Failed to download tile: {e}");
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
&format!("Failed to download elevation tile: {e}"),
);
}
}
}
println!("Processing {} elevation tiles...", successful_tiles.len());
emit_gui_progress_update(15.0, "Processing elevation...");
// Process tiles sequentially (writes to shared height_grid)
for ((tile_x, tile_y), rgb_img) in successful_tiles {
// Only process pixels that fall within the requested bbox
for (y, row) in rgb_img.rows().enumerate() {
for (x, pixel) in row.enumerate() {
// Convert tile pixel coordinates back to geographic coordinates
let pixel_lng = ((*tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
let pixel_lng = ((tile_x as f64 + x as f64 / 256.0) / (2.0_f64.powi(zoom as i32)))
* 360.0
- 180.0;
let pixel_lat_rad = std::f64::consts::PI
* (1.0
- 2.0 * (*tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
- 2.0 * (tile_y as f64 + y as f64 / 256.0) / (2.0_f64.powi(zoom as i32)));
let pixel_lat = pixel_lat_rad.sinh().atan().to_degrees();
// Skip pixels outside the requested bounding box
@@ -172,58 +410,61 @@ pub fn fetch_elevation_data(
filter_elevation_outliers(&mut height_grid);
// Calculate blur sigma based on grid resolution
// Reference points for tuning:
const SMALL_GRID_REF: f64 = 100.0; // Reference grid size
const SMALL_SIGMA_REF: f64 = 15.0; // Sigma for 100x100 grid
const LARGE_GRID_REF: f64 = 1000.0; // Reference grid size
const LARGE_SIGMA_REF: f64 = 7.0; // Sigma for 1000x1000 grid
// Use sqrt scaling to maintain consistent relative smoothing across different area sizes.
// This prevents larger generation areas from appearing noisier than smaller ones.
// Reference: 100x100 grid uses sigma=5 (5% relative blur)
const BASE_GRID_REF: f64 = 100.0;
const BASE_SIGMA_REF: f64 = 5.0;
let grid_size: f64 = (grid_width.min(grid_height) as f64).max(1.0);
let sigma: f64 = if grid_size <= SMALL_GRID_REF {
// Linear scaling for small grids
SMALL_SIGMA_REF * (grid_size / SMALL_GRID_REF)
} else {
// Logarithmic scaling for larger grids
let ln_small: f64 = SMALL_GRID_REF.ln();
let ln_large: f64 = LARGE_GRID_REF.ln();
let log_grid_size: f64 = grid_size.ln();
let t: f64 = (log_grid_size - ln_small) / (ln_large - ln_small);
SMALL_SIGMA_REF + t * (LARGE_SIGMA_REF - SMALL_SIGMA_REF)
};
// Sqrt scaling provides a good balance:
// - 100x100: sigma = 5 (5% relative)
// - 500x500: sigma ≈ 11.2 (2.2% relative)
// - 1000x1000: sigma ≈ 15.8 (1.6% relative)
// This smooths terrain proportionally while preserving more detail.
let sigma: f64 = BASE_SIGMA_REF * (grid_size / BASE_GRID_REF).sqrt();
/* eprintln!(
"Grid: {}x{}, Blur sigma: {:.2}",
grid_width, grid_height, sigma
); */
//let blur_percentage: f64 = (sigma / grid_size) * 100.0;
/*eprintln!(
"Elevation blur: grid={}x{}, sigma={:.2}, blur_percentage={:.2}%",
grid_width, grid_height, sigma, blur_percentage
);*/
// Continue with the existing blur and conversion to Minecraft heights...
let blurred_heights: Vec<Vec<f64>> = apply_gaussian_blur(&height_grid, sigma);
let mut mc_heights: Vec<Vec<i32>> = Vec::with_capacity(blurred_heights.len());
// Release raw height grid
drop(height_grid);
// Find min/max in raw data
let mut min_height: f64 = f64::MAX;
let mut max_height: f64 = f64::MIN;
let mut extreme_low_count = 0;
let mut extreme_high_count = 0;
for row in &blurred_heights {
for &height in row {
min_height = min_height.min(height);
max_height = max_height.max(height);
// Count extreme values that might indicate data issues
if height < -1000.0 {
extreme_low_count += 1;
// Find min/max in raw data using parallel reduction
let (min_height, max_height, extreme_low_count, extreme_high_count) = blurred_heights
.par_iter()
.map(|row| {
let mut local_min = f64::MAX;
let mut local_max = f64::MIN;
let mut local_low = 0usize;
let mut local_high = 0usize;
for &height in row {
local_min = local_min.min(height);
local_max = local_max.max(height);
if height < -1000.0 {
local_low += 1;
}
if height > 10000.0 {
local_high += 1;
}
}
if height > 10000.0 {
extreme_high_count += 1;
}
}
}
(local_min, local_max, local_low, local_high)
})
.reduce(
|| (f64::MAX, f64::MIN, 0usize, 0usize),
|(min1, max1, low1, high1), (min2, max2, low2, high2)| {
(min1.min(min2), max1.max(max2), low1 + low2, high1 + high2)
},
);
eprintln!("Height data range: {min_height} to {max_height} m");
//eprintln!("Height data range: {min_height} to {max_height} m");
if extreme_low_count > 0 {
eprintln!(
"WARNING: Found {extreme_low_count} pixels with extremely low elevations (< -1000m)"
@@ -236,39 +477,63 @@ pub fn fetch_elevation_data(
}
let height_range: f64 = max_height - min_height;
// Apply scale factor to height scaling
let mut height_scale: f64 = BASE_HEIGHT_SCALE * scale.sqrt(); // sqrt to make height scaling less extreme
let mut scaled_range: f64 = height_range * height_scale;
// Adaptive scaling: ensure we don't exceed reasonable Y range
let available_y_range = (MAX_Y - ground_level) as f64;
let safety_margin = 0.9; // Use 90% of available range
let max_allowed_range = available_y_range * safety_margin;
// Realistic height scaling: 1 meter of real elevation = scale blocks in Minecraft
// At scale=1.0, 1 meter = 1 block (realistic 1:1 mapping)
// At scale=2.0, 1 meter = 2 blocks (exaggerated for larger worlds)
let ideal_scaled_range: f64 = height_range * scale;
if scaled_range > max_allowed_range {
let adjustment_factor = max_allowed_range / scaled_range;
height_scale *= adjustment_factor;
scaled_range = height_range * height_scale;
// Calculate available Y range in Minecraft (from ground_level to MAX_Y)
// Leave a buffer at the top for buildings, trees, and other structures
const TERRAIN_HEIGHT_BUFFER: i32 = 15;
let available_y_range: f64 = (MAX_Y - TERRAIN_HEIGHT_BUFFER - ground_level) as f64;
// Determine final height scale:
// - Use realistic 1:1 (times scale) if terrain fits within Minecraft limits
// - Only compress if the terrain would exceed the build height
let scaled_range: f64 = if ideal_scaled_range <= available_y_range {
// Terrain fits! Use realistic scaling
eprintln!(
"Height range too large, applying scaling adjustment factor: {adjustment_factor:.3}"
"Realistic elevation: {:.1}m range fits in {} available blocks",
height_range, available_y_range as i32
);
eprintln!("Adjusted scaled range: {scaled_range:.1} blocks");
}
ideal_scaled_range
} else {
// Terrain too tall, compress to fit within Minecraft limits
let compression_factor: f64 = available_y_range / height_range;
let compressed_range: f64 = height_range * compression_factor;
eprintln!(
"Elevation compressed: {:.1}m range -> {:.0} blocks ({:.2}:1 ratio, 1 block = {:.2}m)",
height_range,
compressed_range,
height_range / compressed_range,
compressed_range / height_range
);
compressed_range
};
// Convert to scaled Minecraft Y coordinates
for row in blurred_heights {
let mc_row: Vec<i32> = row
.iter()
.map(|&h| {
// Scale the height differences
let relative_height: f64 = (h - min_height) / height_range;
let scaled_height: f64 = relative_height * scaled_range;
// With terrain enabled, ground_level is used as the MIN_Y for terrain
((ground_level as f64 + scaled_height).round() as i32).clamp(ground_level, MAX_Y)
})
.collect();
mc_heights.push(mc_row);
}
// Convert to scaled Minecraft Y coordinates (parallelized across rows)
// Lowest real elevation maps to ground_level, highest maps to ground_level + scaled_range
let mc_heights: Vec<Vec<i32>> = blurred_heights
.par_iter()
.map(|row| {
row.iter()
.map(|&h| {
// Calculate relative position within the elevation range (0.0 to 1.0)
let relative_height: f64 = if height_range > 0.0 {
(h - min_height) / height_range
} else {
0.0
};
// Scale to Minecraft blocks and add to ground level
let scaled_height: f64 = relative_height * scaled_range;
// Clamp to valid Minecraft Y range (leave buffer at top for structures)
((ground_level as f64 + scaled_height).round() as i32)
.clamp(ground_level, MAX_Y - TERRAIN_HEIGHT_BUFFER)
})
.collect()
})
.collect();
let mut min_block_height: i32 = i32::MAX;
let mut max_block_height: i32 = i32::MIN;
@@ -278,7 +543,7 @@ pub fn fetch_elevation_data(
max_block_height = max_block_height.max(height);
}
}
eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
//eprintln!("Minecraft height data range: {min_block_height} to {max_block_height} blocks");
Ok(ElevationData {
heights: mc_heights,
@@ -305,48 +570,61 @@ fn apply_gaussian_blur(heights: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
let kernel_size: usize = (sigma * 3.0).ceil() as usize * 2 + 1;
let kernel: Vec<f64> = create_gaussian_kernel(kernel_size, sigma);
// Apply blur
let mut blurred: Vec<Vec<f64>> = heights.to_owned();
let height_len = heights.len();
let width = heights[0].len();
// Horizontal pass
for row in blurred.iter_mut() {
let mut temp: Vec<f64> = row.clone();
for (i, val) in temp.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < row.len() as i32 {
sum += row[idx as usize] * k;
weight_sum += k;
// Horizontal pass - parallelize across rows (each row is independent)
let after_horizontal: Vec<Vec<f64>> = heights
.par_iter()
.map(|row| {
let mut temp: Vec<f64> = vec![0.0; row.len()];
for (i, val) in temp.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = i as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < row.len() as i32 {
sum += row[idx as usize] * k;
weight_sum += k;
}
}
*val = sum / weight_sum;
}
*val = sum / weight_sum;
}
*row = temp;
}
temp
})
.collect();
// Vertical pass
let height: usize = blurred.len();
let width: usize = blurred[0].len();
for x in 0..width {
let temp: Vec<_> = blurred
.iter()
.take(height)
.map(|row: &Vec<f64>| row[x])
.collect();
// Vertical pass - parallelize across columns (each column is independent)
// Process each column in parallel and collect results as column vectors
let blurred_columns: Vec<Vec<f64>> = (0..width)
.into_par_iter()
.map(|x| {
// Extract column from after_horizontal
let column: Vec<f64> = after_horizontal.iter().map(|row| row[x]).collect();
for (y, row) in blurred.iter_mut().enumerate().take(height) {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < height as i32 {
sum += temp[idx as usize] * k;
weight_sum += k;
// Apply vertical blur to this column
let mut blurred_column: Vec<f64> = vec![0.0; height_len];
for (y, val) in blurred_column.iter_mut().enumerate() {
let mut sum: f64 = 0.0;
let mut weight_sum: f64 = 0.0;
for (j, k) in kernel.iter().enumerate() {
let idx: i32 = y as i32 + j as i32 - kernel_size as i32 / 2;
if idx >= 0 && idx < height_len as i32 {
sum += column[idx as usize] * k;
weight_sum += k;
}
}
*val = sum / weight_sum;
}
row[x] = sum / weight_sum;
blurred_column
})
.collect();
// Transpose columns back to row-major format
let mut blurred: Vec<Vec<f64>> = vec![vec![0.0; width]; height_len];
for (x, column) in blurred_columns.into_iter().enumerate() {
for (y, val) in column.into_iter().enumerate() {
blurred[y][x] = val;
}
}
@@ -428,17 +706,24 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
return;
}
// Sort to find percentiles
all_heights.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = all_heights.len();
// Use 1st and 99th percentiles to define reasonable bounds
// Using quickselect (select_nth_unstable) instead of full sort: O(n) vs O(n log n)
let p1_idx = (len as f64 * 0.01) as usize;
let p99_idx = (len as f64 * 0.99) as usize;
let min_reasonable = all_heights[p1_idx];
let max_reasonable = all_heights[p99_idx];
let p99_idx = ((len as f64 * 0.99) as usize).min(len - 1);
eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
// Find p1 (1st percentile) - all elements before p1_idx will be <= p1
let (_, p1_val, _) =
all_heights.select_nth_unstable_by(p1_idx, |a, b| a.partial_cmp(b).unwrap());
let min_reasonable = *p1_val;
// Find p99 (99th percentile) - need to search in remaining slice or use separate call
let (_, p99_val, _) =
all_heights.select_nth_unstable_by(p99_idx, |a, b| a.partial_cmp(b).unwrap());
let max_reasonable = *p99_val;
//eprintln!("Filtering outliers outside range: {min_reasonable:.1}m to {max_reasonable:.1}m");
let mut outliers_filtered = 0;
@@ -453,7 +738,7 @@ fn filter_elevation_outliers(height_grid: &mut [Vec<f64>]) {
}
if outliers_filtered > 0 {
eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
//eprintln!("Filtered {outliers_filtered} elevation outliers, interpolating replacements...");
// Re-run the NaN filling to interpolate the filtered values
fill_nan_values(height_grid);
}

View File

@@ -3,6 +3,11 @@ use itertools::Itertools;
use std::collections::{HashSet, VecDeque};
use std::time::{Duration, Instant};
/// Maximum bounding box area (in blocks) for flood fill.
/// Polygons exceeding this are skipped to prevent multi-GB memory allocations.
/// 25 million blocks ≈ 5000×5000; HashSet would use ~700 MB at this size.
const MAX_FLOOD_FILL_AREA: i64 = 25_000_000;
/// Main flood fill function with automatic algorithm selection
/// Chooses the best algorithm based on polygon size and complexity
pub fn flood_fill_area(
@@ -29,6 +34,13 @@ pub fn flood_fill_area(
let area = (max_x - min_x + 1) as i64 * (max_z - min_z + 1) as i64;
// Safety cap: reject polygons whose bounding box is too large.
// This prevents multi-GB memory allocations when ocean-adjacent elements
// (e.g. natural=water, large landuse) produce huge clipped polygons.
if area > MAX_FLOOD_FILL_AREA {
return vec![];
}
// For small and medium areas, use optimized flood fill with span filling
if area < 50000 {
optimized_flood_fill_area(polygon_coords, timeout, min_x, max_x, min_z, max_z)
@@ -71,7 +83,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 +172,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;
}
}

502
src/floodfill_cache.rs Normal file
View File

@@ -0,0 +1,502 @@
//! Pre-computed flood fill cache for parallel polygon filling.
//!
//! This module provides a way to pre-compute all flood fill operations in parallel
//! before the main element processing loop, then retrieve cached results during
//! sequential processing.
use crate::coordinate_system::cartesian::XZBBox;
use crate::floodfill::flood_fill_area;
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedWay};
use fnv::FnvHashMap;
use rayon::prelude::*;
use std::time::Duration;
/// A memory-efficient bitmap for storing coordinates.
///
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
///
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
pub struct CoordinateBitmap {
/// The bitmap data, where each bit represents one (x, z) coordinate
bits: Vec<u8>,
/// Minimum x coordinate (offset for indexing)
min_x: i32,
/// Minimum z coordinate (offset for indexing)
min_z: i32,
/// Width of the world (max_x - min_x + 1)
width: usize,
/// Height of the world (max_z - min_z + 1)
#[allow(dead_code)]
height: usize,
/// Number of coordinates marked
count: usize,
}
impl CoordinateBitmap {
/// Creates a new empty bitmap covering the given world bounds.
pub fn new(xzbbox: &XZBBox) -> Self {
let min_x = xzbbox.min_x();
let min_z = xzbbox.min_z();
// Use i64 to avoid overflow when world spans more than i32::MAX in either dimension
let width = (i64::from(xzbbox.max_x()) - i64::from(min_x) + 1) as usize;
let height = (i64::from(xzbbox.max_z()) - i64::from(min_z) + 1) as usize;
// Calculate number of bytes needed (round up to nearest byte)
let total_bits = width
.checked_mul(height)
.expect("CoordinateBitmap: world size too large (width * height overflowed)");
let num_bytes = total_bits.div_ceil(8);
Self {
bits: vec![0u8; num_bytes],
min_x,
min_z,
width,
height,
count: 0,
}
}
/// Converts (x, z) coordinate to bit index, returning None if out of bounds.
#[inline]
fn coord_to_index(&self, x: i32, z: i32) -> Option<usize> {
// Use i64 arithmetic to avoid overflow when coordinates span large ranges
let local_x = i64::from(x) - i64::from(self.min_x);
let local_z = i64::from(z) - i64::from(self.min_z);
if local_x < 0 || local_z < 0 {
return None;
}
let local_x = local_x as usize;
let local_z = local_z as usize;
if local_x >= self.width || local_z >= self.height {
return None;
}
// Safe: bounds checks above ensure this won't overflow (max = total_bits - 1)
Some(local_z * self.width + local_x)
}
/// Sets a coordinate.
#[inline]
pub fn set(&mut self, x: i32, z: i32) {
if let Some(bit_index) = self.coord_to_index(x, z) {
let byte_index = bit_index / 8;
let bit_offset = bit_index % 8;
// Safety: coord_to_index already validates bounds, so byte_index is always valid
let mask = 1u8 << bit_offset;
// Only increment count if bit wasn't already set
if self.bits[byte_index] & mask == 0 {
self.bits[byte_index] |= mask;
self.count += 1;
}
}
}
/// Checks if a coordinate is set.
#[inline]
pub fn contains(&self, x: i32, z: i32) -> bool {
if let Some(bit_index) = self.coord_to_index(x, z) {
let byte_index = bit_index / 8;
let bit_offset = bit_index % 8;
// Safety: coord_to_index already validates bounds, so byte_index is always valid
return (self.bits[byte_index] >> bit_offset) & 1 == 1;
}
false
}
/// Returns true if no coordinates are marked.
#[must_use]
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.count == 0
}
/// Returns the number of coordinates that are set.
#[inline]
#[allow(dead_code)]
pub fn count(&self) -> usize {
self.count
}
/// Counts how many coordinates from the given iterator are set in this bitmap.
#[inline]
#[allow(dead_code)]
pub fn count_contained<'a, I>(&self, coords: I) -> usize
where
I: Iterator<Item = &'a (i32, i32)>,
{
coords.filter(|(x, z)| self.contains(*x, *z)).count()
}
/// Counts the number of set bits in a rectangular range.
///
/// This is optimized to iterate row-by-row and use `count_ones()` on bytes
/// where possible, which is much faster than checking individual coordinates.
///
/// Returns `(urban_count, total_count)` for the given range.
#[inline]
#[allow(dead_code)]
pub fn count_in_range(&self, min_x: i32, min_z: i32, max_x: i32, max_z: i32) -> (usize, usize) {
let mut urban_count = 0usize;
let mut total_count = 0usize;
for z in min_z..=max_z {
// Calculate local z coordinate
let local_z = i64::from(z) - i64::from(self.min_z);
if local_z < 0 || local_z >= self.height as i64 {
// Row is out of bounds, still counts toward total
total_count += (i64::from(max_x) - i64::from(min_x) + 1) as usize;
continue;
}
let local_z = local_z as usize;
// Calculate x range in local coordinates
let local_min_x = (i64::from(min_x) - i64::from(self.min_x)).max(0) as usize;
let local_max_x =
((i64::from(max_x) - i64::from(self.min_x)) as usize).min(self.width - 1);
// Count out-of-bounds x coordinates toward total
let x_start_offset = (i64::from(self.min_x) - i64::from(min_x)).max(0) as usize;
let x_end_offset = (i64::from(max_x) - i64::from(self.min_x) - (self.width as i64 - 1))
.max(0) as usize;
total_count += x_start_offset + x_end_offset;
if local_min_x > local_max_x {
continue;
}
// Process this row
let row_start_bit = local_z * self.width + local_min_x;
let row_end_bit = local_z * self.width + local_max_x;
let num_bits = row_end_bit - row_start_bit + 1;
total_count += num_bits;
// Count set bits using byte-wise popcount where possible
let start_byte = row_start_bit / 8;
let end_byte = row_end_bit / 8;
let start_bit_in_byte = row_start_bit % 8;
let end_bit_in_byte = row_end_bit % 8;
if start_byte == end_byte {
// All bits are in the same byte
let byte = self.bits[start_byte];
// Create mask for bits from start_bit to end_bit (inclusive)
let num_bits_in_mask = end_bit_in_byte - start_bit_in_byte + 1;
let mask = if num_bits_in_mask >= 8 {
0xFFu8
} else {
((1u16 << num_bits_in_mask) - 1) as u8
};
let masked = (byte >> start_bit_in_byte) & mask;
urban_count += masked.count_ones() as usize;
} else {
// First partial byte
let first_byte = self.bits[start_byte];
let first_mask = !((1u8 << start_bit_in_byte) - 1); // bits from start_bit to 7
urban_count += (first_byte & first_mask).count_ones() as usize;
// Full bytes in between
for byte_idx in (start_byte + 1)..end_byte {
urban_count += self.bits[byte_idx].count_ones() as usize;
}
// Last partial byte
let last_byte = self.bits[end_byte];
// Handle case where end_bit_in_byte is 7 (would overflow 1u8 << 8)
let last_mask = if end_bit_in_byte >= 7 {
0xFFu8
} else {
(1u8 << (end_bit_in_byte + 1)) - 1
};
urban_count += (last_byte & last_mask).count_ones() as usize;
}
}
(urban_count, total_count)
}
}
/// Type alias for building footprint bitmap (for backwards compatibility).
pub type BuildingFootprintBitmap = CoordinateBitmap;
/// A cache of pre-computed flood fill results, keyed by element ID.
pub struct FloodFillCache {
/// Cached results: element_id -> filled coordinates
way_cache: FnvHashMap<u64, Vec<(i32, i32)>>,
}
impl FloodFillCache {
/// Creates an empty cache.
pub fn new() -> Self {
Self {
way_cache: FnvHashMap::default(),
}
}
/// Pre-computes flood fills for all elements that need them.
///
/// This runs in parallel using Rayon, taking advantage of multiple CPU cores.
pub fn precompute(elements: &[ProcessedElement], timeout: Option<&Duration>) -> Self {
// Collect all ways that need flood fill
let ways_needing_fill: Vec<&ProcessedWay> = elements
.iter()
.filter_map(|el| match el {
ProcessedElement::Way(way) => {
if Self::way_needs_flood_fill(way) {
Some(way)
} else {
None
}
}
_ => None,
})
.collect();
// Compute all way flood fills in parallel
let way_results: Vec<(u64, Vec<(i32, i32)>)> = ways_needing_fill
.par_iter()
.map(|way| {
let polygon_coords: Vec<(i32, i32)> =
way.nodes.iter().map(|n| (n.x, n.z)).collect();
let filled = flood_fill_area(&polygon_coords, timeout);
(way.id, filled)
})
.collect();
// Build the cache
let mut cache = Self::new();
for (id, filled) in way_results {
cache.way_cache.insert(id, filled);
}
cache
}
/// Gets cached flood fill result for a way, or computes it if not cached.
///
/// Note: Combined ways created from relations (e.g., in `generate_natural_from_relation`)
/// will miss the cache and fall back to on-demand computation. This is by design,
/// these synthetic ways don't exist in the original element list and have relation IDs
/// rather than way IDs. The individual member ways are still cached.
pub fn get_or_compute(
&self,
way: &ProcessedWay,
timeout: Option<&Duration>,
) -> Vec<(i32, i32)> {
if let Some(cached) = self.way_cache.get(&way.id) {
// Clone is intentional: each result is typically accessed once during
// sequential processing, so the cost is acceptable vs Arc complexity
cached.clone()
} else {
// Fallback: compute on demand for synthetic/combined ways from relations
let polygon_coords: Vec<(i32, i32)> = way.nodes.iter().map(|n| (n.x, n.z)).collect();
flood_fill_area(&polygon_coords, timeout)
}
}
/// Gets cached flood fill result for a ProcessedElement (Way only).
/// For Nodes/Relations, returns empty vec.
pub fn get_or_compute_element(
&self,
element: &ProcessedElement,
timeout: Option<&Duration>,
) -> Vec<(i32, i32)> {
match element {
ProcessedElement::Way(way) => self.get_or_compute(way, timeout),
_ => Vec::new(),
}
}
/// Determines if a way element needs flood fill based on its tags.
///
/// This checks for tag presence (not specific values) because:
/// - Only some values within each tag type actually use flood fill
/// - But caching extra results is harmless (small memory overhead)
/// - And avoids duplicating value-checking logic from processors
///
/// Covered cases:
/// - building/building:part -> buildings::generate_buildings (includes bridge)
/// - landuse -> landuse::generate_landuse
/// - leisure -> leisure::generate_leisure
/// - amenity -> amenities::generate_amenities
/// - natural (except tree) -> natural::generate_natural
/// - highway with area=yes -> highways::generate_highways (area fill)
fn way_needs_flood_fill(way: &ProcessedWay) -> bool {
way.tags.contains_key("building")
|| way.tags.contains_key("building:part")
|| way.tags.contains_key("boundary")
|| way.tags.contains_key("landuse")
|| way.tags.contains_key("leisure")
|| way.tags.contains_key("amenity")
|| way
.tags
.get("natural")
.map(|v| v != "tree")
.unwrap_or(false)
// Highway areas (like pedestrian plazas) use flood fill when area=yes
|| (way.tags.contains_key("highway")
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
}
/// Collects all building footprint coordinates from the pre-computed cache.
///
/// This should be called after precompute() and before elements are processed.
/// Returns a memory-efficient bitmap of all (x, z) coordinates that are part of buildings.
///
/// The bitmap uses only 1 bit per coordinate in the world bounds, compared to ~24 bytes
/// per entry in a HashSet, reducing memory usage by ~200x for large worlds.
pub fn collect_building_footprints(
&self,
elements: &[ProcessedElement],
xzbbox: &XZBBox,
) -> BuildingFootprintBitmap {
let mut footprints = BuildingFootprintBitmap::new(xzbbox);
for element in elements {
match element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
if let Some(cached) = self.way_cache.get(&way.id) {
for &(x, z) in cached {
footprints.set(x, z);
}
}
}
}
ProcessedElement::Relation(rel) => {
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
for member in &rel.members {
// Only treat outer members as building footprints.
// Inner members represent courtyards/holes where trees can spawn.
if member.role == ProcessedMemberRole::Outer {
if let Some(cached) = self.way_cache.get(&member.way.id) {
for &(x, z) in cached {
footprints.set(x, z);
}
}
}
}
}
}
_ => {}
}
}
footprints
}
/// Collects centroids of all buildings from the pre-computed cache.
///
/// This is used for urban ground detection - building clusters are identified
/// using their centroids, and a concave hull is computed around dense clusters
/// to determine where city ground (smooth stone) should be placed.
///
/// Returns a vector of (x, z) centroid coordinates for all buildings.
pub fn collect_building_centroids(&self, elements: &[ProcessedElement]) -> Vec<(i32, i32)> {
let mut centroids = Vec::new();
for element in elements {
match element {
ProcessedElement::Way(way) => {
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
if let Some(cached) = self.way_cache.get(&way.id) {
if let Some(centroid) = Self::compute_centroid(cached) {
centroids.push(centroid);
}
}
}
}
ProcessedElement::Relation(rel) => {
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
// For building relations, compute centroid from outer ways
let mut all_coords = Vec::new();
for member in &rel.members {
if member.role == ProcessedMemberRole::Outer {
if let Some(cached) = self.way_cache.get(&member.way.id) {
all_coords.extend(cached.iter().copied());
}
}
}
if let Some(centroid) = Self::compute_centroid(&all_coords) {
centroids.push(centroid);
}
}
}
_ => {}
}
}
centroids
}
/// Computes the centroid of a set of coordinates.
fn compute_centroid(coords: &[(i32, i32)]) -> Option<(i32, i32)> {
if coords.is_empty() {
return None;
}
let sum_x: i64 = coords.iter().map(|(x, _)| i64::from(*x)).sum();
let sum_z: i64 = coords.iter().map(|(_, z)| i64::from(*z)).sum();
let len = coords.len() as i64;
Some(((sum_x / len) as i32, (sum_z / len) as i32))
}
/// Removes a way's cached flood fill result, freeing memory.
///
/// Call this after processing an element to release its cached data.
pub fn remove_way(&mut self, way_id: u64) {
self.way_cache.remove(&way_id);
}
/// Removes all cached flood fill results for ways in a relation.
///
/// Relations contain multiple ways, so we need to remove all of them.
pub fn remove_relation_ways(&mut self, way_ids: &[u64]) {
for &id in way_ids {
self.way_cache.remove(&id);
}
}
}
impl Default for FloodFillCache {
fn default() -> Self {
Self::new()
}
}
/// Configures the global Rayon thread pool with a CPU usage cap.
///
/// Call this once at startup before any parallel operations.
///
/// # Arguments
/// * `cpu_fraction` - Fraction of available cores to use (e.g., 0.9 for 90%).
/// Values are clamped to the range [0.1, 1.0].
pub fn configure_rayon_thread_pool(cpu_fraction: f64) {
// Clamp cpu_fraction to valid range
let cpu_fraction = cpu_fraction.clamp(0.1, 1.0);
let available_cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
let target_threads = ((available_cores as f64) * cpu_fraction).floor() as usize;
let target_threads = target_threads.max(1); // At least 1 thread
// Only configure if we haven't already (this can only be called once)
match rayon::ThreadPoolBuilder::new()
.num_threads(target_threads)
.build_global()
{
Ok(()) => {
// Successfully configured (silent to avoid cluttering output)
}
Err(_) => {
// Thread pool already configured
}
}
}

View File

@@ -2,6 +2,8 @@ use crate::args::Args;
use crate::coordinate_system::{cartesian::XZPoint, geographic::LLBBox};
use crate::elevation_data::{fetch_elevation_data, ElevationData};
use crate::progress::emit_gui_progress_update;
#[cfg(feature = "gui")]
use crate::telemetry::{send_log, LogLevel};
use colored::Colorize;
use image::{Rgb, RgbImage};
@@ -23,12 +25,26 @@ 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);
#[cfg(feature = "gui")]
send_log(
LogLevel::Warning,
"Elevation unavailable, using flat ground",
);
// Graceful fallback: disable elevation and keep provided ground_level
Self {
elevation_enabled: false,
ground_level,
elevation_data: None,
}
}
}
}
@@ -131,7 +147,7 @@ impl Ground {
pub fn generate_ground_data(args: &Args) -> Ground {
if args.terrain {
println!("{} Fetching elevation...", "[3/7]".bold());
emit_gui_progress_update(15.0, "Fetching elevation...");
emit_gui_progress_update(14.0, "Fetching elevation...");
let ground = Ground::new_enabled(&args.bbox, args.scale, args.ground_level);
if args.debug {
ground.save_debug_image("elevation_debug");

View File

File diff suppressed because it is too large Load Diff

View File

View File

@@ -8,13 +8,9 @@ body,
font-family: "Courier New", Courier, monospace;
}
/* Hide the BBOX coordinates display at bottom of map */
#info-box {
position: absolute;
width: 100%;
height: auto;
bottom: 0;
border: 0 0 7px 0;
z-index: 10000;
display: none;
}
#coord-format {
@@ -344,4 +340,77 @@ 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,
.leaflet-draw-toolbar .leaflet-draw-edit-preview.editing-mode {
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;
}
/* Context menu for coordinate copying */
.coordinate-context-menu {
position: fixed;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10000;
min-width: 160px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 13px;
}
.coordinate-context-menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: #333;
}
.coordinate-context-menu-item:hover {
background: #f0f0f0;
}
.coordinate-context-menu-item svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.coordinate-context-menu-separator {
height: 1px;
background: #e0e0e0;
margin: 4px 0;
}

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 418 B

View File

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 205 B

View File

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 262 B

View File

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 348 B

View File

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

View File

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 328 B

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 849 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 847 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

View File

@@ -32,9 +32,12 @@ p {
.logo {
height: 6em;
padding: 1.5em;
padding-top: 0.4em;
padding-bottom: 0.5em;
will-change: filter;
transition: 0.75s;
max-width: 950px;
max-height: 600px;
}
.logo.arnis:hover {
@@ -59,10 +62,11 @@ a:hover {
.flex-container {
display: flex;
gap: 20px;
gap: 15px;
justify-content: center;
align-items: stretch;
margin-top: 5px;
min-height: 70vh;
}
.section {
@@ -74,26 +78,70 @@ a:hover {
.map-box,
.controls-box {
width: 45%;
display: flex;
flex-direction: column;
}
.map-box {
width: 63%;
min-height: 420px;
padding: 0;
overflow: hidden;
background: #575757;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.controls-box {
width: 32%;
background: transparent;
padding: 0;
border-radius: 0;
box-shadow: none;
}
.controls-content {
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
}
.controls-box .progress-section {
margin-top: auto;
margin-top: 0;
}
.controls-top {
display: flex;
flex-direction: column;
}
.bbox-selection-text {
font-size: 0.9em;
color: #ffffff;
display: block;
font-weight: bold;
min-height: 2.5em;
line-height: 1.25em;
margin-top: 8px;
}
.progress-info-text {
font-size: 0.9em;
color: #ececec;
display: block;
font-weight: bold;
min-height: 1.5em;
line-height: 1.25em;
margin-bottom: 5px;
}
.map-container {
border: 2px solid #e0e0e0;
border: none;
border-radius: 8px;
flex-grow: 1;
width: 100%;
height: 100%;
}
.section h2 {
@@ -133,18 +181,25 @@ button:hover {
margin-top: auto;
}
.progress-section h2 {
margin-bottom: 8px;
text-align: center;
.progress-row {
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar-container {
width: 100%;
flex: 1;
height: 20px;
background-color: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin-top: 8px;
}
#progress-detail {
min-width: 40px;
text-align: right;
font-size: 0.9em;
color: #fff;
}
.progress-bar {
@@ -154,15 +209,6 @@ button:hover {
transition: width 0.4s;
}
/* Left and right alignment for "Saving world..." text */
.progress-status {
display: flex;
justify-content: space-between;
font-size: 0.9em;
margin-top: 8px;
color: #fff;
}
.footer {
margin-top: 20px;
text-align: center;
@@ -181,7 +227,7 @@ button:hover {
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
background-color: #333333;
}
p {
@@ -213,10 +259,73 @@ button:hover {
width: 100%;
}
/* World Selection Container */
.world-selection-container {
width: 100%;
}
.choose-world-btn {
padding: 10px;
line-height: 1.2;
width: 100%;
border-radius: 8px 8px 0 0 !important;
margin-bottom: 0 !important;
margin-top: 0 !important;
box-shadow: none !important;
}
/* World Format Toggle */
.format-toggle-container {
display: flex;
width: 100%;
gap: 0;
margin-top: 0;
}
.format-toggle-btn {
flex: 1;
padding: 10px;
font-size: 1em;
font-weight: 500;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.25s ease;
margin-top: 0;
border-radius: 0;
}
.format-toggle-btn:first-child {
border-radius: 0 0 0 8px;
}
.format-toggle-btn:last-child {
border-radius: 0 0 8px 0;
}
.format-toggle-btn:not(.format-active) {
background-color: #3a3a3a;
color: #b0b0b0;
}
.format-toggle-btn:not(.format-active):hover {
background-color: #4a4a4a;
color: #ffffff;
}
.format-toggle-btn.format-active {
background-color: var(--primary-accent);
color: #0f0f0f;
font-weight: 600;
}
.format-toggle-btn.format-active:hover {
background-color: var(--primary-accent-dark);
}
/* Customization Settings */
.modal {
position: fixed;
z-index: 1000;
z-index: 20001;
left: 0;
top: 0;
width: 100%;
@@ -229,7 +338,7 @@ button:hover {
}
.modal-content {
background-color: #797979;
background-color: #717171;
padding: 20px;
border: 1px solid #797979;
border-radius: 10px;
@@ -249,6 +358,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 +417,14 @@ button:hover {
margin: 15px 0;
}
#city-boundaries-toggle {
accent-color: #fecc44;
}
#telemetry-toggle {
accent-color: #fecc44;
}
.scale-slider-container label {
display: block;
margin-bottom: 5px;
@@ -306,7 +450,7 @@ button:hover {
#bbox-coords {
width: 100%;
padding: 8px;
padding: 5px;
border: 1px solid #fecc44;
border-radius: 4px;
font-size: 14px;
@@ -318,6 +462,20 @@ button:hover {
box-shadow: 0 0 5px #fecc44;
}
#save-path {
width: 100%;
padding: 5px;
border: 1px solid #fecc44;
border-radius: 4px;
font-size: 14px;
}
#save-path:focus {
outline: none;
border-color: #fecc44;
box-shadow: 0 0 5px #fecc44;
}
/* Settings Modal Layout */
.settings-row {
display: flex;
@@ -329,6 +487,75 @@ button:hover {
.settings-row label {
text-align: left;
flex: 1;
display: flex;
align-items: center;
gap: 6px;
}
/* Tooltip icon (question mark in circle) */
.tooltip-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: rgba(254, 204, 68, 0.3);
color: #fecc44;
font-size: 11px;
font-weight: bold;
cursor: help;
position: relative;
flex-shrink: 0;
}
.tooltip-icon:hover {
background-color: rgba(254, 204, 68, 0.5);
}
/* Arnis-styled tooltip box */
.tooltip-icon::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 8px);
transform: translateX(-50%);
background-color: #2a2a2a;
color: #fecc44;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: normal;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid #fecc44;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 1000;
pointer-events: none;
}
/* Tooltip arrow */
.tooltip-icon::before {
content: '';
position: absolute;
left: 50%;
bottom: calc(100% + 2px);
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #fecc44;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 1001;
pointer-events: none;
}
.tooltip-icon:hover::after,
.tooltip-icon:hover::before {
opacity: 1;
visibility: visible;
}
.settings-control {
@@ -351,9 +578,47 @@ button:hover {
border: 1px solid #fecc44;
}
/* Save Path Setting */
.save-path-control {
gap: 0;
}
.save-path-input {
max-width: 200px !important;
font-size: 0.85em;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-right: none !important;
}
.save-path-browse {
background: none;
border: 1px solid #fecc44;
border-radius: 0 4px 4px 0;
padding: 0 6px;
margin-top: 0;
box-shadow: none;
cursor: pointer;
display: flex;
align-items: center;
align-self: stretch;
justify-content: center;
transition: background 0.2s ease;
}
.save-path-browse:hover {
background: rgba(254, 204, 68, 0.15);
}
.save-path-browse svg {
width: 14px;
height: 14px;
fill: #fecc44;
}
.license-button-row {
justify-content: center;
margin-top: 10px;
margin-top: 5px;
}
.license-button {
@@ -389,11 +654,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 +714,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;
@@ -476,9 +769,12 @@ button:hover {
transition: background-color 0.3s, border-color 0.3s;
}
.settings-button .gear-icon::before {
content: "⚙️";
font-size: 18px;
.settings-button svg {
stroke: white;
width: 22px;
height: 22px;
min-width: 22px;
min-height: 22px;
}
/* Logo Animation */

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

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