mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-01 18:03:19 -05:00
Compare commits
17 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7e1fec02c | ||
|
|
fb05e2f2b8 | ||
|
|
7015cfff5f | ||
|
|
78ca5a49ce | ||
|
|
e265f8fa7e | ||
|
|
552f4ab013 | ||
|
|
319eb656ee | ||
|
|
11a756ab06 | ||
|
|
0f93853dcb | ||
|
|
b4c47f559c | ||
|
|
a86e23129b | ||
|
|
69b30ef59f | ||
|
|
1733f5d664 | ||
|
|
e6b6de27ff | ||
|
|
ac0fc275dc | ||
|
|
de1f52bfaf | ||
|
|
03cc86f3e2 |
428
Cargo.lock
generated
428
Cargo.lock
generated
@@ -206,7 +206,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"reqwest 0.13.1",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"rusty-leveldb",
|
||||
"semver",
|
||||
@@ -463,28 +463,6 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -828,11 +806,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
@@ -936,15 +913,6 @@ version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -1059,7 +1027,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1635,12 +1603,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.2"
|
||||
@@ -1663,6 +1625,15 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1670,7 +1641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1684,6 +1655,12 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -1709,12 +1686,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
@@ -2006,11 +1977,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2322,21 +2291,19 @@ checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
@@ -2360,29 +2327,38 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.19"
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"socket2 0.5.8",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2726,16 +2702,6 @@ version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
@@ -3058,12 +3024,6 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lz4-java-wrc"
|
||||
version = "0.2.0"
|
||||
@@ -3196,6 +3156,23 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nbtx"
|
||||
version = "0.1.0"
|
||||
@@ -3671,10 +3648,48 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
name = "openssl"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
@@ -4160,62 +4175,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.9",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.9",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
@@ -4483,44 +4442,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -4534,28 +4458,33 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4664,12 +4593,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -4707,11 +4630,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.36"
|
||||
version = "0.23.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
@@ -4720,61 +4642,26 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -4871,12 +4758,12 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -4884,9 +4771,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -5198,6 +5085,16 @@ version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
@@ -5217,7 +5114,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -5508,7 +5405,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.12.28",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -5899,7 +5796,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.6.0",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -5915,6 +5812,16 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.1"
|
||||
@@ -6048,24 +5955,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -6328,6 +6217,12 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vek"
|
||||
version = "0.17.2"
|
||||
@@ -6621,15 +6516,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ once_cell = "1.21.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.13.1", features = ["blocking", "json"] }
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
12
src/args.rs
12
src/args.rs
@@ -40,17 +40,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,
|
||||
|
||||
@@ -277,6 +277,19 @@ impl Block {
|
||||
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",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -505,6 +518,17 @@ impl Block {
|
||||
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
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -752,6 +776,19 @@ 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);
|
||||
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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::*;
|
||||
@@ -10,6 +10,7 @@ use crate::osm_parser::ProcessedElement;
|
||||
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};
|
||||
@@ -85,14 +86,16 @@ pub fn generate_world_with_options(
|
||||
// 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);
|
||||
|
||||
// Partition elements: separate boundary elements for deferred processing
|
||||
// This avoids cloning by moving elements instead of copying them
|
||||
let (boundary_elements, other_elements): (Vec<_>, Vec<_>) = elements
|
||||
.into_iter()
|
||||
.partition(|element| element.tags().contains_key("boundary"));
|
||||
// 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 data
|
||||
let elements_count: usize = other_elements.len() + boundary_elements.len();
|
||||
// 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()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||
@@ -103,8 +106,8 @@ pub fn generate_world_with_options(
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||
|
||||
// Process non-boundary elements first
|
||||
for element in other_elements.into_iter() {
|
||||
// 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 {
|
||||
@@ -182,6 +185,8 @@ pub fn generate_world_with_options(
|
||||
highways::generate_siding(&mut editor, way);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
} else if way.tags.contains_key("power") {
|
||||
power::generate_power(&mut editor, &element);
|
||||
}
|
||||
// Release flood fill cache entry for this way
|
||||
flood_fill_cache.remove_way(way.id);
|
||||
@@ -215,6 +220,14 @@ pub fn generate_world_with_options(
|
||||
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) => {
|
||||
@@ -270,32 +283,15 @@ pub fn generate_world_with_options(
|
||||
|
||||
process_pb.finish();
|
||||
|
||||
// Process deferred boundary elements after all other elements
|
||||
// This ensures boundaries only fill empty areas, they won't overwrite
|
||||
// any ground blocks set by landuse, leisure, natural, etc.
|
||||
for element in boundary_elements.into_iter() {
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
boundaries::generate_boundary(&mut editor, way, args, &flood_fill_cache);
|
||||
// Clean up cache entry for consistency with other element processing
|
||||
flood_fill_cache.remove_way(way.id);
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
boundaries::generate_boundary_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&xzbbox,
|
||||
);
|
||||
// Clean up cache entries for consistency with other element processing
|
||||
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
|
||||
}
|
||||
// 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);
|
||||
@@ -353,9 +349,18 @@ pub fn generate_world_with_options(
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
// 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) {
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, 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);
|
||||
}
|
||||
|
||||
120
src/element_processing/advertising.rs
Normal file
120
src/element_processing/advertising.rs
Normal 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);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
//! Processing of administrative and urban boundaries.
|
||||
//!
|
||||
//! This module handles boundary elements that define urban areas (cities, boroughs, etc.)
|
||||
//! and sets appropriate ground blocks for them.
|
||||
//!
|
||||
//! Boundaries are processed last but only fill empty areas, allowing more specific
|
||||
//! landuse areas (parks, residential, etc.) to take priority over the general
|
||||
//! urban ground.
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::clipping::clip_way_to_bbox;
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
/// Checks if a boundary element represents an urban area that should have stone ground.
|
||||
///
|
||||
/// Returns true for:
|
||||
/// - `boundary=administrative` with `admin_level >= 8` (city/borough level or smaller)
|
||||
/// - `boundary=low_emission_zone` (urban traffic zones)
|
||||
/// - `boundary=limited_traffic_zone` (urban traffic zones)
|
||||
/// - `boundary=special_economic_zone` (developed industrial/commercial zones)
|
||||
/// - `boundary=political` (electoral districts, usually urban)
|
||||
fn is_urban_boundary(tags: &std::collections::HashMap<String, String>) -> bool {
|
||||
let Some(boundary_value) = tags.get("boundary") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match boundary_value.as_str() {
|
||||
"administrative" => {
|
||||
// Only consider city-level or smaller (admin_level >= 8)
|
||||
// admin_level 2 = country, 4 = state, 6 = county, 8 = city/municipality
|
||||
if let Some(admin_level_str) = tags.get("admin_level") {
|
||||
if let Ok(admin_level) = admin_level_str.parse::<u8>() {
|
||||
return admin_level >= 8;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
// Urban zones that should have stone ground
|
||||
"low_emission_zone" | "limited_traffic_zone" | "special_economic_zone" | "political" => {
|
||||
true
|
||||
}
|
||||
// Natural/protected areas should keep grass - don't process these
|
||||
// "national_park" | "protected_area" | "forest" | "forest_compartment" | "aboriginal_lands"
|
||||
// Statistical/administrative-only boundaries - don't affect ground
|
||||
// "census" | "statistical" | "postal_code" | "timezone" | "disputed" | "maritime" | etc.
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ground blocks for an urban boundary way.
|
||||
pub fn generate_boundary(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Check if this is an urban boundary
|
||||
if !is_urban_boundary(&element.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the area of the boundary element using cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
// Fill the area with smooth stone as ground block
|
||||
// Use None, None to only set where no block exists yet - don't overwrite anything
|
||||
for (x, z) in floor_area {
|
||||
editor.set_block(SMOOTH_STONE, x, 0, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ground blocks for an urban boundary relation.
|
||||
pub fn generate_boundary_from_relation(
|
||||
editor: &mut WorldEditor,
|
||||
rel: &ProcessedRelation,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
xzbbox: &XZBBox,
|
||||
) {
|
||||
// Check if this is an urban boundary
|
||||
if !is_urban_boundary(&rel.tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect outer ways (unclipped) for merging
|
||||
let mut outers: Vec<Vec<ProcessedNode>> = rel
|
||||
.members
|
||||
.iter()
|
||||
.filter(|m| m.role == ProcessedMemberRole::Outer)
|
||||
.map(|m| m.way.nodes.clone())
|
||||
.collect();
|
||||
|
||||
if outers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge way segments into closed rings
|
||||
super::merge_way_segments(&mut outers);
|
||||
|
||||
// Clip each merged ring to bbox and process
|
||||
for ring in outers {
|
||||
if ring.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clip the merged ring to bbox
|
||||
let clipped_nodes = clip_way_to_bbox(&ring, xzbbox);
|
||||
if clipped_nodes.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a ProcessedWay for the clipped ring
|
||||
let clipped_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: clipped_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate boundary area from clipped way
|
||||
generate_boundary(editor, &clipped_way, args, flood_fill_cache);
|
||||
}
|
||||
}
|
||||
@@ -138,16 +138,17 @@ pub fn generate_buildings(
|
||||
];
|
||||
let accent_block = accent_blocks[rng.gen_range(0..accent_blocks.len())];
|
||||
|
||||
// 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;
|
||||
// Skip building:part if 'layer' or 'level' is negative (underground parts)
|
||||
if element.tags.contains_key("building:part") {
|
||||
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;
|
||||
if let Some(level) = element.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,6 +1517,20 @@ pub fn generate_building_from_relation(
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
// Skip building:part relations if layer or level is negative (underground parts)
|
||||
if relation.tags.contains_key("building:part") {
|
||||
if let Some(layer) = relation.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(level) = relation.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract levels from relation tags
|
||||
let relation_levels = relation
|
||||
.tags
|
||||
|
||||
55
src/element_processing/emergency.rs
Normal file
55
src/element_processing/emergency.rs
Normal 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);
|
||||
}
|
||||
207
src/element_processing/historic.rs
Normal file
207
src/element_processing/historic.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
pub mod advertising;
|
||||
pub mod amenities;
|
||||
pub mod barriers;
|
||||
pub mod boundaries;
|
||||
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;
|
||||
|
||||
385
src/element_processing/power.rs
Normal file
385
src/element_processing/power.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ use fnv::FnvHashMap;
|
||||
use rayon::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
||||
/// 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 BuildingFootprintBitmap {
|
||||
pub struct CoordinateBitmap {
|
||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
||||
bits: Vec<u8>,
|
||||
/// Minimum x coordinate (offset for indexing)
|
||||
@@ -27,12 +27,13 @@ pub struct BuildingFootprintBitmap {
|
||||
/// 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 as building footprints
|
||||
/// Number of coordinates marked
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl BuildingFootprintBitmap {
|
||||
impl CoordinateBitmap {
|
||||
/// Creates a new empty bitmap covering the given world bounds.
|
||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
||||
let min_x = xzbbox.min_x();
|
||||
@@ -44,7 +45,7 @@ impl BuildingFootprintBitmap {
|
||||
// Calculate number of bytes needed (round up to nearest byte)
|
||||
let total_bits = width
|
||||
.checked_mul(height)
|
||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
||||
.expect("CoordinateBitmap: world size too large (width * height overflowed)");
|
||||
let num_bytes = total_bits.div_ceil(8);
|
||||
|
||||
Self {
|
||||
@@ -79,7 +80,7 @@ impl BuildingFootprintBitmap {
|
||||
Some(local_z * self.width + local_x)
|
||||
}
|
||||
|
||||
/// Sets a coordinate as part of a building footprint.
|
||||
/// Sets a coordinate.
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: i32, z: i32) {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
@@ -96,7 +97,7 @@ impl BuildingFootprintBitmap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a coordinate is part of a building footprint.
|
||||
/// 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) {
|
||||
@@ -111,12 +112,119 @@ impl BuildingFootprintBitmap {
|
||||
|
||||
/// Returns true if no coordinates are marked.
|
||||
#[must_use]
|
||||
#[allow(dead_code)] // Standard API method for collection-like types
|
||||
#[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
|
||||
@@ -283,6 +391,61 @@ impl FloodFillCache {
|
||||
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.
|
||||
|
||||
@@ -815,6 +815,7 @@ fn gui_start_generation(
|
||||
interior_enabled: bool,
|
||||
roof_enabled: bool,
|
||||
fillground_enabled: bool,
|
||||
city_boundaries_enabled: bool,
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
@@ -1007,6 +1008,7 @@ fn gui_start_generation(
|
||||
interior: interior_enabled,
|
||||
roof: roof_enabled,
|
||||
fillground: fillground_enabled,
|
||||
city_boundaries: city_boundaries_enabled,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(40)),
|
||||
};
|
||||
|
||||
4
src/gui/css/styles.css
vendored
4
src/gui/css/styles.css
vendored
@@ -417,6 +417,10 @@ button:hover {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#city-boundaries-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
#telemetry-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
11
src/gui/index.html
vendored
11
src/gui/index.html
vendored
@@ -138,6 +138,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- City Ground Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="city-boundaries-toggle">
|
||||
<span data-localize="city_boundaries">City Ground</span>
|
||||
<span class="tooltip-icon" data-tooltip="Detect urban areas and place smooth stone ground where cities are located.">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="city-boundaries-toggle" name="city-boundaries-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- World Scale Slider -->
|
||||
<div class="settings-row">
|
||||
<label for="scale-value-slider">
|
||||
|
||||
21
src/gui/js/main.js
vendored
21
src/gui/js/main.js
vendored
@@ -104,20 +104,21 @@ async function applyLocalization(localization) {
|
||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
||||
"h2[data-localize='customization_settings']": "customization_settings",
|
||||
"label[data-localize='world_scale']": "world_scale",
|
||||
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||
"span[data-localize='world_scale']": "world_scale",
|
||||
"span[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||
// DEPRECATED: Ground level localization removed
|
||||
// "label[data-localize='ground_level']": "ground_level",
|
||||
"label[data-localize='language']": "language",
|
||||
"label[data-localize='generation_mode']": "generation_mode",
|
||||
"span[data-localize='language']": "language",
|
||||
"span[data-localize='generation_mode']": "generation_mode",
|
||||
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
||||
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
||||
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
||||
"label[data-localize='terrain']": "terrain",
|
||||
"label[data-localize='interior']": "interior",
|
||||
"label[data-localize='roof']": "roof",
|
||||
"label[data-localize='fillground']": "fillground",
|
||||
"label[data-localize='map_theme']": "map_theme",
|
||||
"span[data-localize='terrain']": "terrain",
|
||||
"span[data-localize='interior']": "interior",
|
||||
"span[data-localize='roof']": "roof",
|
||||
"span[data-localize='fillground']": "fillground",
|
||||
"span[data-localize='city_boundaries']": "city_boundaries",
|
||||
"span[data-localize='map_theme']": "map_theme",
|
||||
".footer-link": "footer_text",
|
||||
"button[data-localize='license_and_credits']": "license_and_credits",
|
||||
"h2[data-localize='license_and_credits']": "license_and_credits",
|
||||
@@ -832,6 +833,7 @@ async function startGeneration() {
|
||||
var interior = document.getElementById("interior-toggle").checked;
|
||||
var roof = document.getElementById("roof-toggle").checked;
|
||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||
var city_boundaries = document.getElementById("city-boundaries-toggle").checked;
|
||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
||||
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||
// DEPRECATED: Ground level input removed from UI
|
||||
@@ -854,6 +856,7 @@ async function startGeneration() {
|
||||
interiorEnabled: interior,
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
cityBoundariesEnabled: city_boundaries,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false,
|
||||
|
||||
1
src/gui/locales/ar.json
vendored
1
src/gui/locales/ar.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "توليد الداخلية",
|
||||
"roof": "توليد السقف",
|
||||
"fillground": "ملء الأرض",
|
||||
"city_boundaries": "أرضية المدينة",
|
||||
"bedrock_use_java": "استخدم Java لاختيار العوالم"
|
||||
}
|
||||
|
||||
1
src/gui/locales/de.json
vendored
1
src/gui/locales/de.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Innenraum Generierung",
|
||||
"roof": "Dach Generierung",
|
||||
"fillground": "Boden füllen",
|
||||
"city_boundaries": "Stadtboden",
|
||||
"bedrock_use_java": "Java für Weltauswahl nutzen"
|
||||
}
|
||||
1
src/gui/locales/en-US.json
vendored
1
src/gui/locales/en-US.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Interior Generation",
|
||||
"roof": "Roof Generation",
|
||||
"fillground": "Fill Ground",
|
||||
"city_boundaries": "City Ground",
|
||||
"bedrock_use_java": "Use Java to select worlds"
|
||||
}
|
||||
1
src/gui/locales/es.json
vendored
1
src/gui/locales/es.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Generación Interior",
|
||||
"roof": "Generación de Tejado",
|
||||
"fillground": "Rellenar Suelo",
|
||||
"city_boundaries": "Suelo Urbano",
|
||||
"bedrock_use_java": "Usa Java para elegir mundos"
|
||||
}
|
||||
1
src/gui/locales/fi.json
vendored
1
src/gui/locales/fi.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Sisätilan luonti",
|
||||
"roof": "Katon luonti",
|
||||
"fillground": "Täytä maa",
|
||||
"city_boundaries": "Kaupungin maa",
|
||||
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
|
||||
}
|
||||
|
||||
1
src/gui/locales/fr-FR.json
vendored
1
src/gui/locales/fr-FR.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Génération d'intérieur",
|
||||
"roof": "Génération de toit",
|
||||
"fillground": "Remplir le sol",
|
||||
"city_boundaries": "Sol urbain",
|
||||
"bedrock_use_java": "Utilisez Java pour les mondes"
|
||||
}
|
||||
|
||||
1
src/gui/locales/hu.json
vendored
1
src/gui/locales/hu.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Belső generálás",
|
||||
"roof": "Tető generálás",
|
||||
"fillground": "Talaj feltöltése",
|
||||
"city_boundaries": "Városi talaj",
|
||||
"bedrock_use_java": "Java világválasztáshoz"
|
||||
}
|
||||
1
src/gui/locales/ko.json
vendored
1
src/gui/locales/ko.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "내부 생성",
|
||||
"roof": "지붕 생성",
|
||||
"fillground": "지면 채우기",
|
||||
"city_boundaries": "도시 지면",
|
||||
"bedrock_use_java": "Java로 세계 선택"
|
||||
}
|
||||
1
src/gui/locales/lt.json
vendored
1
src/gui/locales/lt.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Interjero generavimas",
|
||||
"roof": "Stogo generavimas",
|
||||
"fillground": "Užpildyti pagrindą",
|
||||
"city_boundaries": "Miesto žemė",
|
||||
"bedrock_use_java": "Naudok Java pasauliams"
|
||||
}
|
||||
1
src/gui/locales/lv.json
vendored
1
src/gui/locales/lv.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Interjera ģenerēšana",
|
||||
"roof": "Jumta ģenerēšana",
|
||||
"fillground": "Aizpildīt zemi",
|
||||
"city_boundaries": "Pilsētas zeme",
|
||||
"bedrock_use_java": "Izmanto Java pasaulēm"
|
||||
}
|
||||
1
src/gui/locales/pl.json
vendored
1
src/gui/locales/pl.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Generowanie wnętrza",
|
||||
"roof": "Generowanie dachu",
|
||||
"fillground": "Wypełnij podłoże",
|
||||
"city_boundaries": "Podłoże miejskie",
|
||||
"bedrock_use_java": "Użyj Java do wyboru światów"
|
||||
}
|
||||
1
src/gui/locales/ru.json
vendored
1
src/gui/locales/ru.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Генерация Интерьера",
|
||||
"roof": "Генерация Крыши",
|
||||
"fillground": "Заполнить Землю",
|
||||
"city_boundaries": "Городской грунт",
|
||||
"bedrock_use_java": "Используйте Java для миров"
|
||||
}
|
||||
|
||||
1
src/gui/locales/sv.json
vendored
1
src/gui/locales/sv.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Interiörgenerering",
|
||||
"roof": "Takgenerering",
|
||||
"fillground": "Fyll mark",
|
||||
"city_boundaries": "Stadsmark",
|
||||
"bedrock_use_java": "Använd Java för världar"
|
||||
}
|
||||
1
src/gui/locales/ua.json
vendored
1
src/gui/locales/ua.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "Генерація інтер'єру",
|
||||
"roof": "Генерація даху",
|
||||
"fillground": "Заповнити землю",
|
||||
"city_boundaries": "Міська земля",
|
||||
"bedrock_use_java": "Використовуй Java для світів"
|
||||
}
|
||||
1
src/gui/locales/zh-CN.json
vendored
1
src/gui/locales/zh-CN.json
vendored
@@ -42,5 +42,6 @@
|
||||
"interior": "内部生成",
|
||||
"roof": "屋顶生成",
|
||||
"fillground": "填充地面",
|
||||
"city_boundaries": "城市地面",
|
||||
"bedrock_use_java": "使用Java选择世界"
|
||||
}
|
||||
@@ -25,6 +25,7 @@ mod retrieve_data;
|
||||
mod telemetry;
|
||||
#[cfg(test)]
|
||||
mod test_utilities;
|
||||
mod urban_ground;
|
||||
mod version_check;
|
||||
mod world_editor;
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ pub fn fetch_data_from_overpass(
|
||||
r#"[out:json][timeout:360][bbox:{},{},{},{}];
|
||||
(
|
||||
nwr["building"];
|
||||
nwr["building:part"];
|
||||
nwr["highway"];
|
||||
nwr["landuse"];
|
||||
nwr["natural"];
|
||||
@@ -134,10 +135,17 @@ pub fn fetch_data_from_overpass(
|
||||
nwr["tourism"];
|
||||
nwr["bridge"];
|
||||
nwr["railway"];
|
||||
nwr["roller_coaster"];
|
||||
nwr["barrier"];
|
||||
nwr["entrance"];
|
||||
nwr["door"];
|
||||
nwr["boundary"];
|
||||
nwr["power"];
|
||||
nwr["historic"];
|
||||
nwr["emergency"];
|
||||
nwr["advertising"];
|
||||
nwr["man_made"];
|
||||
nwr["aeroway"];
|
||||
way;
|
||||
)->.relsinbbox;
|
||||
(
|
||||
|
||||
848
src/urban_ground.rs
Normal file
848
src/urban_ground.rs
Normal file
@@ -0,0 +1,848 @@
|
||||
//! Urban ground detection and generation based on building clusters.
|
||||
//!
|
||||
//! This module computes urban areas by analyzing building density and clustering,
|
||||
//! then generates appropriate ground blocks (smooth stone) for those areas.
|
||||
//!
|
||||
//! # Algorithm Overview
|
||||
//!
|
||||
//! 1. **Grid-based density analysis**: Divide the world into cells and count buildings per cell
|
||||
//! 2. **Connected component detection**: Find clusters of dense cells using flood fill
|
||||
//! 3. **Cluster filtering**: Only keep clusters with enough buildings to be considered "urban"
|
||||
//! 4. **Concave hull computation**: Compute a tight-fitting boundary around each cluster
|
||||
//! 5. **Ground filling**: Fill the hull area with stone blocks
|
||||
//!
|
||||
//! This approach handles various scenarios:
|
||||
//! - Full city coverage: Large connected cluster
|
||||
//! - Multiple cities: Separate clusters, each gets its own hull
|
||||
//! - Rural areas: No clusters meet threshold, no stone placed
|
||||
//! - Isolated buildings: Don't meet cluster threshold, remain on grass
|
||||
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use geo::{ConcaveHull, ConvexHull, MultiPoint, Point, Polygon, Simplify};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for urban ground detection.
|
||||
///
|
||||
/// These parameters control how building clusters are identified and
|
||||
/// how the urban ground boundary is computed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UrbanGroundConfig {
|
||||
/// Grid cell size for density analysis (in blocks).
|
||||
/// Smaller = more precise but slower. Default: 64 blocks (4 chunks).
|
||||
pub cell_size: i32,
|
||||
|
||||
/// Minimum buildings per cell to consider it potentially urban.
|
||||
/// Cells below this threshold are ignored. Default: 1.
|
||||
pub min_buildings_per_cell: usize,
|
||||
|
||||
/// Minimum total buildings in a connected cluster to be considered urban.
|
||||
/// Small clusters (villages, isolated buildings) won't get stone ground. Default: 5.
|
||||
pub min_buildings_for_cluster: usize,
|
||||
|
||||
/// Concavity parameter for hull computation (used in legacy hull-based method).
|
||||
/// Lower = tighter fit to buildings (more concave), Higher = smoother (more convex).
|
||||
/// Range: 1.0 (very tight) to 10.0 (almost convex). Default: 2.0.
|
||||
pub concavity: f64,
|
||||
|
||||
/// Whether to expand the hull slightly beyond building boundaries (used in legacy method).
|
||||
/// This creates a small buffer zone around the urban area. Default: true.
|
||||
pub expand_hull: bool,
|
||||
|
||||
/// Base number of cells to expand the urban region.
|
||||
/// This helps fill small gaps between buildings. Adaptive expansion may increase this.
|
||||
/// Default: 2.
|
||||
pub cell_expansion: i32,
|
||||
}
|
||||
|
||||
impl Default for UrbanGroundConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cell_size: 64, // Smaller cells for better granularity (4 chunks instead of 6)
|
||||
min_buildings_per_cell: 1,
|
||||
min_buildings_for_cluster: 5,
|
||||
concavity: 2.0,
|
||||
expand_hull: true,
|
||||
cell_expansion: 2, // Larger expansion to connect spread-out buildings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a detected urban cluster with its buildings and computed boundary.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct UrbanCluster {
|
||||
/// Grid cells that belong to this cluster
|
||||
cells: Vec<(i32, i32)>,
|
||||
/// Building centroids within this cluster
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
/// Total number of buildings in the cluster
|
||||
building_count: usize,
|
||||
}
|
||||
|
||||
/// A compact lookup structure for checking if a coordinate is in an urban area.
|
||||
///
|
||||
/// Instead of storing millions of individual coordinates, this stores only
|
||||
/// the cell indices (thousands) and performs O(1) lookups. This reduces
|
||||
/// memory usage by ~4000x compared to storing all coordinates.
|
||||
///
|
||||
/// # Memory Usage
|
||||
/// - 7.8 km² area: ~17K cells × 16 bytes = ~270 KB (vs ~560 MB for coordinates)
|
||||
/// - 100 km² area: ~220K cells × 16 bytes = ~3.5 MB (vs ~7 GB for coordinates)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UrbanGroundLookup {
|
||||
/// Set of cell indices (cx, cz) that are urban
|
||||
urban_cells: HashSet<(i32, i32)>,
|
||||
/// Cell size used for coordinate-to-cell conversion
|
||||
cell_size: i32,
|
||||
/// Bounding box origin for coordinate conversion
|
||||
bbox_min_x: i32,
|
||||
bbox_min_z: i32,
|
||||
}
|
||||
|
||||
impl UrbanGroundLookup {
|
||||
/// Creates an empty lookup (no urban areas).
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
urban_cells: HashSet::new(),
|
||||
cell_size: 64,
|
||||
bbox_min_x: 0,
|
||||
bbox_min_z: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given world coordinate is in an urban area.
|
||||
#[inline]
|
||||
pub fn is_urban(&self, x: i32, z: i32) -> bool {
|
||||
if self.urban_cells.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let cx = (x - self.bbox_min_x) / self.cell_size;
|
||||
let cz = (z - self.bbox_min_z) / self.cell_size;
|
||||
self.urban_cells.contains(&(cx, cz))
|
||||
}
|
||||
|
||||
/// Returns the number of urban cells.
|
||||
#[allow(dead_code)]
|
||||
pub fn cell_count(&self) -> usize {
|
||||
self.urban_cells.len()
|
||||
}
|
||||
|
||||
/// Returns true if there are no urban areas.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.urban_cells.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes urban ground areas from building locations.
|
||||
pub struct UrbanGroundComputer {
|
||||
config: UrbanGroundConfig,
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: XZBBox,
|
||||
}
|
||||
|
||||
impl UrbanGroundComputer {
|
||||
/// Creates a new urban ground computer with the given world bounds and configuration.
|
||||
pub fn new(xzbbox: XZBBox, config: UrbanGroundConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
building_centroids: Vec::new(),
|
||||
xzbbox,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new urban ground computer with default configuration.
|
||||
pub fn with_defaults(xzbbox: XZBBox) -> Self {
|
||||
Self::new(xzbbox, UrbanGroundConfig::default())
|
||||
}
|
||||
|
||||
/// Adds a building centroid to be considered for urban area detection.
|
||||
#[inline]
|
||||
pub fn add_building_centroid(&mut self, x: i32, z: i32) {
|
||||
// Only add if within bounds
|
||||
if x >= self.xzbbox.min_x()
|
||||
&& x <= self.xzbbox.max_x()
|
||||
&& z >= self.xzbbox.min_z()
|
||||
&& z <= self.xzbbox.max_z()
|
||||
{
|
||||
self.building_centroids.push((x, z));
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds multiple building centroids from an iterator.
|
||||
pub fn add_building_centroids<I>(&mut self, centroids: I)
|
||||
where
|
||||
I: IntoIterator<Item = (i32, i32)>,
|
||||
{
|
||||
for (x, z) in centroids {
|
||||
self.add_building_centroid(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of buildings added.
|
||||
#[allow(dead_code)]
|
||||
pub fn building_count(&self) -> usize {
|
||||
self.building_centroids.len()
|
||||
}
|
||||
|
||||
/// Computes all urban ground coordinates.
|
||||
///
|
||||
/// Returns a list of (x, z) coordinates that should have stone ground.
|
||||
/// The coordinates are clipped to the world bounding box.
|
||||
///
|
||||
/// Performance: Uses cell-based filling for O(cells) complexity instead of
|
||||
/// flood-filling complex hulls which would be O(area). For a city with 1000
|
||||
/// buildings in 100 cells, this is ~100x faster than flood fill.
|
||||
///
|
||||
/// NOTE: For better performance and memory usage, prefer `compute_lookup()`.
|
||||
#[allow(dead_code)]
|
||||
pub fn compute(&self, _timeout: Option<&Duration>) -> Vec<(i32, i32)> {
|
||||
// Not enough buildings for any urban area
|
||||
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||
let grid = self.create_density_grid();
|
||||
|
||||
// Step 2: Find connected urban regions and get their expanded cells
|
||||
let clusters = self.find_urban_clusters(&grid);
|
||||
|
||||
if clusters.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 3: Fill cells directly instead of using expensive flood fill on hulls
|
||||
// This is much faster: O(cells × cell_size²) vs O(hull_area) for flood fill
|
||||
let mut all_coords = Vec::new();
|
||||
for cluster in clusters {
|
||||
let coords = self.fill_cluster_cells(&cluster);
|
||||
all_coords.extend(coords);
|
||||
}
|
||||
|
||||
all_coords
|
||||
}
|
||||
|
||||
/// Computes urban ground and returns a compact lookup structure.
|
||||
///
|
||||
/// This is the preferred method for production use. Instead of returning
|
||||
/// millions of coordinates (high memory), it returns a lookup structure
|
||||
/// that stores only cell indices (~4000x less memory) and provides O(1)
|
||||
/// coordinate lookups.
|
||||
///
|
||||
/// # Memory Comparison
|
||||
/// - `compute()`: ~560 MB for 7.8 km² area
|
||||
/// - `compute_lookup()`: ~270 KB for same area
|
||||
pub fn compute_lookup(&self) -> UrbanGroundLookup {
|
||||
// Not enough buildings for any urban area
|
||||
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||
return UrbanGroundLookup::empty();
|
||||
}
|
||||
|
||||
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||
let grid = self.create_density_grid();
|
||||
|
||||
// Step 2: Find connected urban regions and get their expanded cells
|
||||
let clusters = self.find_urban_clusters(&grid);
|
||||
|
||||
if clusters.is_empty() {
|
||||
return UrbanGroundLookup::empty();
|
||||
}
|
||||
|
||||
// Step 3: Collect all expanded cells from all clusters into a HashSet
|
||||
let mut urban_cells = HashSet::new();
|
||||
for cluster in clusters {
|
||||
urban_cells.extend(cluster.cells.iter().copied());
|
||||
}
|
||||
|
||||
UrbanGroundLookup {
|
||||
urban_cells,
|
||||
cell_size: self.config.cell_size,
|
||||
bbox_min_x: self.xzbbox.min_x(),
|
||||
bbox_min_z: self.xzbbox.min_z(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills all cells in a cluster directly, returning coordinates.
|
||||
/// This is much faster than computing a hull and flood-filling it.
|
||||
fn fill_cluster_cells(&self, cluster: &UrbanCluster) -> Vec<(i32, i32)> {
|
||||
let mut coords = Vec::new();
|
||||
let cell_size = self.config.cell_size;
|
||||
|
||||
// Pre-calculate bounds once
|
||||
let bbox_min_x = self.xzbbox.min_x();
|
||||
let bbox_max_x = self.xzbbox.max_x();
|
||||
let bbox_min_z = self.xzbbox.min_z();
|
||||
let bbox_max_z = self.xzbbox.max_z();
|
||||
|
||||
for &(cx, cz) in &cluster.cells {
|
||||
// Calculate cell bounds in world coordinates
|
||||
let cell_min_x = (bbox_min_x + cx * cell_size).max(bbox_min_x);
|
||||
let cell_max_x = (bbox_min_x + (cx + 1) * cell_size - 1).min(bbox_max_x);
|
||||
let cell_min_z = (bbox_min_z + cz * cell_size).max(bbox_min_z);
|
||||
let cell_max_z = (bbox_min_z + (cz + 1) * cell_size - 1).min(bbox_max_z);
|
||||
|
||||
// Skip if cell is entirely outside bbox
|
||||
if cell_min_x > bbox_max_x
|
||||
|| cell_max_x < bbox_min_x
|
||||
|| cell_min_z > bbox_max_z
|
||||
|| cell_max_z < bbox_min_z
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fill all coordinates in this cell
|
||||
for x in cell_min_x..=cell_max_x {
|
||||
for z in cell_min_z..=cell_max_z {
|
||||
coords.push((x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coords
|
||||
}
|
||||
|
||||
/// Creates a density grid mapping cell coordinates to buildings in that cell.
|
||||
fn create_density_grid(&self) -> HashMap<(i32, i32), Vec<(i32, i32)>> {
|
||||
let mut grid: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new();
|
||||
|
||||
for &(x, z) in &self.building_centroids {
|
||||
let cell_x = (x - self.xzbbox.min_x()) / self.config.cell_size;
|
||||
let cell_z = (z - self.xzbbox.min_z()) / self.config.cell_size;
|
||||
grid.entry((cell_x, cell_z)).or_default().push((x, z));
|
||||
}
|
||||
|
||||
grid
|
||||
}
|
||||
|
||||
/// Finds connected clusters of urban cells.
|
||||
fn find_urban_clusters(
|
||||
&self,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
) -> Vec<UrbanCluster> {
|
||||
// Step 1: Identify cells that meet minimum density threshold
|
||||
let dense_cells: HashSet<(i32, i32)> = grid
|
||||
.iter()
|
||||
.filter(|(_, buildings)| buildings.len() >= self.config.min_buildings_per_cell)
|
||||
.map(|(&cell, _)| cell)
|
||||
.collect();
|
||||
|
||||
if dense_cells.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 2: Calculate adaptive expansion based on building density
|
||||
// For spread-out cities, we need more expansion to connect buildings
|
||||
let adaptive_expansion = self.calculate_adaptive_expansion(&dense_cells, grid);
|
||||
|
||||
// Step 3: Expand dense cells to connect nearby clusters
|
||||
let expanded_cells = self.expand_cells_adaptive(&dense_cells, adaptive_expansion);
|
||||
|
||||
// Step 4: Find connected components using flood fill
|
||||
let mut visited = HashSet::new();
|
||||
let mut clusters = Vec::new();
|
||||
|
||||
for &cell in &expanded_cells {
|
||||
if visited.contains(&cell) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BFS to find connected component
|
||||
let mut component_cells = Vec::new();
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(cell);
|
||||
visited.insert(cell);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
component_cells.push(current);
|
||||
|
||||
// Check 8-connected neighbors (including diagonals for better connectivity)
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
if dx == 0 && dz == 0 {
|
||||
continue;
|
||||
}
|
||||
let neighbor = (current.0 + dx, current.1 + dz);
|
||||
if expanded_cells.contains(&neighbor) && !visited.contains(&neighbor) {
|
||||
visited.insert(neighbor);
|
||||
queue.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect buildings from the original dense cells only (not expanded empty cells)
|
||||
let mut cluster_buildings = Vec::new();
|
||||
for &cell in &component_cells {
|
||||
if let Some(buildings) = grid.get(&cell) {
|
||||
cluster_buildings.extend(buildings.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
let building_count = cluster_buildings.len();
|
||||
|
||||
// Only keep clusters with enough buildings
|
||||
if building_count >= self.config.min_buildings_for_cluster {
|
||||
clusters.push(UrbanCluster {
|
||||
cells: component_cells,
|
||||
building_centroids: cluster_buildings,
|
||||
building_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clusters
|
||||
}
|
||||
|
||||
/// Calculates adaptive expansion based on building density.
|
||||
///
|
||||
/// For spread-out cities (low density), we need more expansion to connect
|
||||
/// buildings that are farther apart. For dense cities, less expansion is needed.
|
||||
fn calculate_adaptive_expansion(
|
||||
&self,
|
||||
dense_cells: &HashSet<(i32, i32)>,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
) -> i32 {
|
||||
if dense_cells.is_empty() {
|
||||
return self.config.cell_expansion;
|
||||
}
|
||||
|
||||
// Calculate total buildings and average per occupied cell
|
||||
let total_buildings: usize = dense_cells
|
||||
.iter()
|
||||
.filter_map(|cell| grid.get(cell))
|
||||
.map(|buildings| buildings.len())
|
||||
.sum();
|
||||
|
||||
let avg_buildings_per_cell = total_buildings as f64 / dense_cells.len() as f64;
|
||||
|
||||
// Calculate the "spread" of cells - how far apart are occupied cells?
|
||||
// Find bounding box of occupied cells
|
||||
if dense_cells.len() < 2 {
|
||||
return self.config.cell_expansion;
|
||||
}
|
||||
|
||||
let min_x = dense_cells.iter().map(|(x, _)| x).min().unwrap();
|
||||
let max_x = dense_cells.iter().map(|(x, _)| x).max().unwrap();
|
||||
let min_z = dense_cells.iter().map(|(_, z)| z).min().unwrap();
|
||||
let max_z = dense_cells.iter().map(|(_, z)| z).max().unwrap();
|
||||
|
||||
let grid_span_x = (max_x - min_x + 1) as f64;
|
||||
let grid_span_z = (max_z - min_z + 1) as f64;
|
||||
let total_possible_cells = grid_span_x * grid_span_z;
|
||||
|
||||
// Cell occupancy ratio: what fraction of the bounding box has buildings?
|
||||
let occupancy = dense_cells.len() as f64 / total_possible_cells;
|
||||
|
||||
// Adaptive expansion logic:
|
||||
// - High density (many buildings per cell) AND high occupancy = dense city, use base expansion
|
||||
// - Low density OR low occupancy = spread-out city, need more expansion
|
||||
|
||||
let base_expansion = self.config.cell_expansion;
|
||||
|
||||
// Scale factor: lower density = higher factor
|
||||
// avg_buildings_per_cell < 2 → spread out
|
||||
// occupancy < 0.3 → sparse grid with gaps
|
||||
let density_factor = if avg_buildings_per_cell < 3.0 {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let occupancy_factor = if occupancy < 0.4 {
|
||||
1.5
|
||||
} else if occupancy < 0.6 {
|
||||
1.25
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let adaptive = (base_expansion as f64 * density_factor * occupancy_factor).ceil() as i32;
|
||||
|
||||
// Cap at reasonable maximum (4 cells = 256 blocks with 64-block cells)
|
||||
adaptive.min(4).max(base_expansion)
|
||||
}
|
||||
|
||||
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||
fn expand_cells_adaptive(
|
||||
&self,
|
||||
cells: &HashSet<(i32, i32)>,
|
||||
expansion: i32,
|
||||
) -> HashSet<(i32, i32)> {
|
||||
if expansion <= 0 {
|
||||
return cells.clone();
|
||||
}
|
||||
|
||||
let mut expanded = cells.clone();
|
||||
|
||||
for &(cx, cz) in cells {
|
||||
for dz in -expansion..=expansion {
|
||||
for dx in -expansion..=expansion {
|
||||
expanded.insert((cx + dx, cz + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||
#[allow(dead_code)]
|
||||
fn expand_cells(&self, cells: &HashSet<(i32, i32)>) -> HashSet<(i32, i32)> {
|
||||
self.expand_cells_adaptive(cells, self.config.cell_expansion)
|
||||
}
|
||||
|
||||
/// Computes ground coordinates for a single urban cluster.
|
||||
///
|
||||
/// NOTE: This hull-based method is kept for reference but not used in production.
|
||||
/// The cell-based `fill_cluster_cells` method is much faster.
|
||||
#[allow(dead_code)]
|
||||
fn compute_cluster_ground(
|
||||
&self,
|
||||
cluster: &UrbanCluster,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
// Need at least 3 points for a hull
|
||||
if cluster.building_centroids.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Collect points for hull computation
|
||||
// Include building centroids plus cell corner points for better coverage
|
||||
let mut hull_points: Vec<(f64, f64)> = cluster
|
||||
.building_centroids
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect();
|
||||
|
||||
// Add cell boundary points if expand_hull is enabled
|
||||
// This ensures the hull extends slightly beyond buildings
|
||||
if self.config.expand_hull {
|
||||
for &(cx, cz) in &cluster.cells {
|
||||
// Only add corners for cells that actually have buildings
|
||||
if grid.get(&(cx, cz)).map(|b| !b.is_empty()).unwrap_or(false) {
|
||||
let base_x = (self.xzbbox.min_x() + cx * self.config.cell_size) as f64;
|
||||
let base_z = (self.xzbbox.min_z() + cz * self.config.cell_size) as f64;
|
||||
let size = self.config.cell_size as f64;
|
||||
|
||||
// Add cell corners with small padding
|
||||
let pad = size * 0.1; // 10% padding
|
||||
hull_points.push((base_x - pad, base_z - pad));
|
||||
hull_points.push((base_x + size + pad, base_z - pad));
|
||||
hull_points.push((base_x - pad, base_z + size + pad));
|
||||
hull_points.push((base_x + size + pad, base_z + size + pad));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to geo MultiPoint
|
||||
let multi_point: MultiPoint<f64> =
|
||||
hull_points.iter().map(|&(x, z)| Point::new(x, z)).collect();
|
||||
|
||||
// Compute hull based on point count
|
||||
let hull: Polygon<f64> = if hull_points.len() < 10 {
|
||||
// Too few points for concave hull, use convex
|
||||
multi_point.convex_hull()
|
||||
} else {
|
||||
// Use concave hull for better fit
|
||||
multi_point.concave_hull(self.config.concavity)
|
||||
};
|
||||
|
||||
// Simplify the hull to reduce vertex count (improves flood fill performance)
|
||||
let hull = hull.simplify(2.0);
|
||||
|
||||
// Convert hull to integer coordinates for flood fill
|
||||
self.fill_hull_polygon(&hull, timeout)
|
||||
}
|
||||
|
||||
/// Fills a hull polygon and returns all interior coordinates.
|
||||
///
|
||||
/// NOTE: This method is kept for reference but not used in production.
|
||||
/// The cell-based approach is much faster.
|
||||
#[allow(dead_code)]
|
||||
fn fill_hull_polygon(
|
||||
&self,
|
||||
polygon: &Polygon<f64>,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
// Convert polygon exterior to integer coordinates
|
||||
let exterior: Vec<(i32, i32)> = polygon
|
||||
.exterior()
|
||||
.coords()
|
||||
.map(|c| (c.x.round() as i32, c.y.round() as i32))
|
||||
.collect();
|
||||
|
||||
if exterior.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Remove duplicate consecutive points (can cause flood fill issues)
|
||||
let mut clean_exterior = Vec::with_capacity(exterior.len());
|
||||
for point in exterior {
|
||||
if clean_exterior.last() != Some(&point) {
|
||||
clean_exterior.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the polygon is closed
|
||||
if clean_exterior.first() != clean_exterior.last() && !clean_exterior.is_empty() {
|
||||
clean_exterior.push(clean_exterior[0]);
|
||||
}
|
||||
|
||||
if clean_exterior.len() < 4 {
|
||||
// Need at least 3 unique points + closing point
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Use existing flood fill, clipping to bbox
|
||||
let filled = flood_fill_area(&clean_exterior, timeout);
|
||||
|
||||
// Filter to only include points within world bounds
|
||||
filled
|
||||
.into_iter()
|
||||
.filter(|&(x, z)| {
|
||||
x >= self.xzbbox.min_x()
|
||||
&& x <= self.xzbbox.max_x()
|
||||
&& z >= self.xzbbox.min_z()
|
||||
&& z <= self.xzbbox.max_z()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the centroid of a set of coordinates.
|
||||
///
|
||||
/// Returns None if the slice is empty.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub 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))
|
||||
}
|
||||
|
||||
/// Convenience function to compute urban ground from building centroids.
|
||||
///
|
||||
/// NOTE: This function is kept for backward compatibility and tests.
|
||||
/// For production use, prefer `compute_urban_ground_lookup` which uses
|
||||
/// ~4000x less memory.
|
||||
#[allow(dead_code)]
|
||||
pub fn compute_urban_ground(
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: &XZBBox,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||
computer.add_building_centroids(building_centroids);
|
||||
computer.compute(timeout)
|
||||
}
|
||||
|
||||
/// Computes urban ground and returns a compact lookup structure.
|
||||
///
|
||||
/// This is the preferred entry point for production use. Returns a lookup
|
||||
/// structure that uses ~270 KB instead of ~560 MB for a typical city area.
|
||||
pub fn compute_urban_ground_lookup(
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: &XZBBox,
|
||||
) -> UrbanGroundLookup {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||
computer.add_building_centroids(building_centroids);
|
||||
computer.compute_lookup()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_bbox() -> XZBBox {
|
||||
XZBBox::rect_from_xz_lengths(1000.0, 1000.0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_buildings() {
|
||||
let computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
let result = computer.compute(None);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_few_scattered_buildings() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
// Add a few scattered buildings (not enough for a cluster)
|
||||
computer.add_building_centroid(100, 100);
|
||||
computer.add_building_centroid(500, 500);
|
||||
computer.add_building_centroid(900, 900);
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
result.is_empty(),
|
||||
"Scattered buildings should not form urban area"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dense_cluster() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Add a dense cluster of buildings
|
||||
for i in 0..30 {
|
||||
for j in 0..30 {
|
||||
if (i + j) % 3 == 0 {
|
||||
// Add building every 3rd position
|
||||
computer.add_building_centroid(100 + i * 10, 100 + j * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
!result.is_empty(),
|
||||
"Dense cluster should produce urban area"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_centroid() {
|
||||
let coords = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
|
||||
let centroid = compute_centroid(&coords);
|
||||
assert_eq!(centroid, Some((5, 5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_centroid_empty() {
|
||||
let coords: Vec<(i32, i32)> = vec![];
|
||||
let centroid = compute_centroid(&coords);
|
||||
assert_eq!(centroid, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spread_out_buildings() {
|
||||
// Simulate a spread-out city like Erding where buildings are farther apart
|
||||
// This should still be detected as urban due to adaptive expansion
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Add buildings spread across a larger area with gaps
|
||||
// Buildings are ~100-150 blocks apart (would fail with small expansion)
|
||||
let building_positions = [
|
||||
(100, 100),
|
||||
(250, 100),
|
||||
(400, 100),
|
||||
(100, 250),
|
||||
(250, 250),
|
||||
(400, 250),
|
||||
(100, 400),
|
||||
(250, 400),
|
||||
(400, 400),
|
||||
// Add a few more to ensure cluster threshold is met
|
||||
(175, 175),
|
||||
(325, 175),
|
||||
(175, 325),
|
||||
(325, 325),
|
||||
];
|
||||
|
||||
for (x, z) in building_positions {
|
||||
computer.add_building_centroid(x, z);
|
||||
}
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
!result.is_empty(),
|
||||
"Spread-out buildings should still form urban area with adaptive expansion"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_expansion_calculated() {
|
||||
let bbox = create_test_bbox();
|
||||
let computer = UrbanGroundComputer::with_defaults(bbox);
|
||||
|
||||
// Create a sparse grid with low occupancy
|
||||
let mut dense_cells = HashSet::new();
|
||||
// Only 4 cells in a 10x10 potential grid = 4% occupancy
|
||||
dense_cells.insert((0, 0));
|
||||
dense_cells.insert((5, 0));
|
||||
dense_cells.insert((0, 5));
|
||||
dense_cells.insert((5, 5));
|
||||
|
||||
let mut grid = HashMap::new();
|
||||
// Only 1 building per cell (low density)
|
||||
grid.insert((0, 0), vec![(10, 10)]);
|
||||
grid.insert((5, 0), vec![(330, 10)]);
|
||||
grid.insert((0, 5), vec![(10, 330)]);
|
||||
grid.insert((5, 5), vec![(330, 330)]);
|
||||
|
||||
let expansion = computer.calculate_adaptive_expansion(&dense_cells, &grid);
|
||||
|
||||
// Should be higher than base (2) due to low occupancy and density
|
||||
assert!(
|
||||
expansion > 2,
|
||||
"Sparse grid should trigger higher expansion, got {}",
|
||||
expansion
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_empty() {
|
||||
let lookup = UrbanGroundLookup::empty();
|
||||
assert!(lookup.is_empty());
|
||||
assert!(!lookup.is_urban(100, 100));
|
||||
assert_eq!(lookup.cell_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_membership() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Create a dense cluster of buildings
|
||||
for x in 0..10 {
|
||||
for z in 0..10 {
|
||||
computer.add_building_centroid(100 + x * 10, 100 + z * 10);
|
||||
}
|
||||
}
|
||||
|
||||
let lookup = computer.compute_lookup();
|
||||
assert!(!lookup.is_empty());
|
||||
|
||||
// Points inside the cluster should be urban
|
||||
assert!(
|
||||
lookup.is_urban(150, 150),
|
||||
"Center of cluster should be urban"
|
||||
);
|
||||
|
||||
// Points far outside the cluster should not be urban
|
||||
assert!(
|
||||
!lookup.is_urban(900, 900),
|
||||
"Point far from cluster should not be urban"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_vs_compute_consistency() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Create a medium-sized cluster
|
||||
for x in 0..5 {
|
||||
for z in 0..5 {
|
||||
computer.add_building_centroid(200 + x * 20, 200 + z * 20);
|
||||
}
|
||||
}
|
||||
|
||||
let coords = computer.compute(None);
|
||||
let lookup = computer.compute_lookup();
|
||||
|
||||
// Every coordinate from compute() should be marked urban in lookup
|
||||
for (x, z) in &coords {
|
||||
assert!(
|
||||
lookup.is_urban(*x, *z),
|
||||
"Coordinate ({}, {}) should be urban in lookup",
|
||||
x,
|
||||
z
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user