diff --git a/.gitignore b/.gitignore index 5b2fa51..126e241 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.pyc .venv/ node_modules/ +target/ external/ state/scratch-compaction-*.md experiments/renders/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e6499e4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1081 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bevy_macro_utils" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62d984f9f8bd0f0d9fb020492a955e641e30e7a425f3588bf346cb3e61fec3c3" +dependencies = [ + "parking_lot", + "proc-macro2", + "quote", + "syn", + "toml_edit", +] + +[[package]] +name = "bevy_math" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa74ae5d968749cc073da991757d3c7e3504ac6dbaac5f8c2a54b9d19b0b7ed" +dependencies = [ + "approx", + "bevy_reflect", + "derive_more", + "glam", + "itertools", + "rand", + "rand_distr", + "serde", + "smallvec", + "thiserror", + "variadics_please", +] + +[[package]] +name = "bevy_platform" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4691af6d7cfd1b5deb2fc926a43a180a546cdc3fe1e5a013fcee60db9bb2c81f" +dependencies = [ + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_ptr" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d24d7906c7de556033168b3485de36c59049fbaef0c2c44c715a23e0329b10" + +[[package]] +name = "bevy_reflect" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5472b91928c0f3e4e3988c0d036b00719f19520f53a0c3f8c2af72f00e693c5" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash 0.2.0", + "glam", + "serde", + "smol_str", + "thiserror", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083784255162fa39960aa3cf3c23af0e515db2daa7f2e796ae34df993f4d3f6c" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn", + "uuid", +] + +[[package]] +name = "bevy_utils" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "789d04f88c764877a4552e07745b402dbc45f5d0545e6d102558f2f1752a1d89" +dependencies = [ + "bevy_platform", + "disqualified", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "disqualified" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glam" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" +dependencies = [ + "bytemuck", + "rand", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "equivalent", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "variadics_please" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vg_csg" +version = "0.1.0" +dependencies = [ + "approx", + "bevy_math", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu-types" +version = "26.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "serde", + "thiserror", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bed690a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["crates/vg_csg"] +resolver = "3" diff --git a/crates/vg_csg/Cargo.toml b/crates/vg_csg/Cargo.toml new file mode 100644 index 0000000..3493273 --- /dev/null +++ b/crates/vg_csg/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vg_csg" +version = "0.1.0" +edition = "2024" +description = "Lean Bevy-math constructive geometry assembler for VibeGeometry blockouts" +license = "MIT" + +[dependencies] +bevy_math = "0.17" + +[dev-dependencies] +approx = "0.5" diff --git a/crates/vg_csg/examples/csg_room.rs b/crates/vg_csg/examples/csg_room.rs new file mode 100644 index 0000000..c70252f --- /dev/null +++ b/crates/vg_csg/examples/csg_room.rs @@ -0,0 +1,57 @@ +use bevy_math::Vec3; +use vg_csg::{DomeCapZSpec, FloretArmSpec, LevelDsl, MaterialId}; + +fn main() { + let mut level = LevelDsl::new(); + level.solid_box( + "floor", + Vec3::new(0.0, 0.0, -0.1), + Vec3::new(10.0, 10.0, 0.2), + MaterialId(1), + ); + level.solid_box( + "wall with doorway", + Vec3::new(0.0, 5.0, 1.5), + Vec3::new(10.0, 0.3, 3.0), + MaterialId(2), + ); + level.cut_box( + "doorway", + Vec3::new(0.0, 5.0, 1.0), + Vec3::new(1.4, 0.6, 2.0), + ); + level.dome_cap_z( + "flat-base city dome", + DomeCapZSpec { + center: Vec3::new(0.0, 0.0, 0.0), + radius: 3.0, + height: 1.8, + rings: 8, + segments: 32, + material: MaterialId(3), + }, + ); + level.floret_arm( + "sunflower radiator arm", + FloretArmSpec { + anchor: Vec3::new(3.0, 0.0, 0.1), + direction: Vec3::X, + length: 16.0, + root_width: 1.5, + tip_width: 4.0, + thickness: 0.08, + tip_lift: 0.8, + material: MaterialId(4), + }, + ); + + let output = level.assemble(); + println!( + "brushes={} box_fragments={} vertices={} triangles={} warnings={}", + output.report.input_brushes, + output.report.emitted_box_fragments, + output.mesh.vertex_count(), + output.mesh.triangle_count(), + output.report.warnings.len() + ); +} diff --git a/crates/vg_csg/src/assembler.rs b/crates/vg_csg/src/assembler.rs new file mode 100644 index 0000000..89ad719 --- /dev/null +++ b/crates/vg_csg/src/assembler.rs @@ -0,0 +1,262 @@ +use bevy_math::Vec3; + +use crate::{ + Aabb, Brush, BrushId, BrushOp, MaterialId, Primitive, TriangleMesh, append_cylinder_z, + append_dome_cap_z, append_floret_arm, + primitives::{DomeCapZSpec, FloretArmSpec}, +}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuildWarning { + SubtractIgnoredForNonBox { brush: String }, + IntersectUnsupported { brush: String }, +} + +#[derive(Clone, Debug, Default)] +pub struct BuildReport { + pub input_brushes: usize, + pub emitted_box_fragments: usize, + pub warnings: Vec, +} + +#[derive(Clone, Debug)] +pub struct BuildOutput { + pub mesh: TriangleMesh, + pub report: BuildReport, + pub generation: u64, +} + +#[derive(Clone, Debug)] +struct BoxSolid { + bounds: Aabb, + material: MaterialId, +} + +#[derive(Clone, Debug, Default)] +pub struct Assembler { + brushes: Vec, + next_id: u32, + generation: u64, +} + +impl Assembler { + pub fn new() -> Self { + Self::default() + } + + pub fn brushes(&self) -> &[Brush] { + &self.brushes + } + + pub fn generation(&self) -> u64 { + self.generation + } + + pub fn add_brush( + &mut self, + name: impl Into, + op: BrushOp, + primitive: Primitive, + material: MaterialId, + ) -> BrushId { + let id = BrushId(self.next_id); + self.next_id += 1; + self.brushes + .push(Brush::new(id, name, op, primitive, material)); + self.generation = self.generation.wrapping_add(1); + id + } + + pub fn solid_box( + &mut self, + name: impl Into, + bounds: Aabb, + material: MaterialId, + ) -> BrushId { + self.add_brush(name, BrushOp::Add, Primitive::Box { bounds }, material) + } + + pub fn cut_box(&mut self, name: impl Into, bounds: Aabb) -> BrushId { + self.add_brush( + name, + BrushOp::Subtract, + Primitive::Box { bounds }, + MaterialId::default(), + ) + } + + pub fn build(&self) -> BuildOutput { + let mut report = BuildReport { + input_brushes: self.brushes.len(), + ..BuildReport::default() + }; + let mut mesh = TriangleMesh::new(); + let mut boxes: Vec = Vec::new(); + + for brush in &self.brushes { + match brush.op { + BrushOp::Add => match &brush.primitive { + Primitive::Box { bounds } => boxes.push(BoxSolid { + bounds: *bounds, + material: brush.material, + }), + Primitive::CylinderZ { + center, + radius, + depth, + segments, + } => append_cylinder_z( + &mut mesh, + *center, + *radius, + *depth, + *segments, + brush.material, + ), + Primitive::DomeCapZ { + center, + radius, + height, + rings, + segments, + } => append_dome_cap_z( + &mut mesh, + DomeCapZSpec { + center: *center, + radius: *radius, + height: *height, + rings: *rings, + segments: *segments, + material: brush.material, + }, + ), + Primitive::FloretArm { + anchor, + direction, + length, + root_width, + tip_width, + thickness, + tip_lift, + } => append_floret_arm( + &mut mesh, + FloretArmSpec { + anchor: *anchor, + direction: *direction, + length: *length, + root_width: *root_width, + tip_width: *tip_width, + thickness: *thickness, + tip_lift: *tip_lift, + material: brush.material, + }, + ), + }, + BrushOp::Subtract => { + if let Some(cutter) = brush.as_box() { + boxes = subtract_from_boxes(&boxes, cutter); + } else { + report + .warnings + .push(BuildWarning::SubtractIgnoredForNonBox { + brush: brush.name.clone(), + }); + } + } + BrushOp::Intersect => { + report.warnings.push(BuildWarning::IntersectUnsupported { + brush: brush.name.clone(), + }); + } + } + } + + report.emitted_box_fragments = boxes.len(); + for solid in boxes { + mesh.append_box(solid.bounds, solid.material); + } + + BuildOutput { + mesh, + report, + generation: self.generation, + } + } + + pub fn sample_room_with_door() -> Self { + let wall = MaterialId(1); + let floor = MaterialId(2); + let mut asm = Self::new(); + asm.solid_box( + "floor", + Aabb::from_center_size(Vec3::new(0.0, 0.0, -0.1), Vec3::new(8.0, 8.0, 0.2)), + floor, + ); + asm.solid_box( + "north wall", + Aabb::from_center_size(Vec3::new(0.0, 4.0, 1.5), Vec3::new(8.0, 0.25, 3.0)), + wall, + ); + asm.cut_box( + "door void", + Aabb::from_center_size(Vec3::new(0.0, 4.0, 1.0), Vec3::new(1.2, 0.5, 2.0)), + ); + asm + } +} + +fn subtract_from_boxes(boxes: &[BoxSolid], cutter: Aabb) -> Vec { + let mut out = Vec::with_capacity(boxes.len() + 4); + for solid in boxes { + for bounds in solid.bounds.subtract_box(cutter) { + out.push(BoxSolid { + bounds, + material: solid.material, + }); + } + } + out +} + +#[cfg(test)] +mod tests { + use bevy_math::Vec3; + + use super::*; + + #[test] + fn centered_subtract_splits_box_into_six_fragments() { + let source = Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)); + let cutter = Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)); + let pieces = source.subtract_box(cutter); + + assert_eq!(pieces.len(), 6); + assert!(pieces.iter().all(|piece| piece.is_valid())); + assert!(pieces.iter().all(|piece| !piece.intersects(cutter))); + } + + #[test] + fn door_cut_leaves_no_box_fragment_inside_void() { + let output = Assembler::sample_room_with_door().build(); + let cutter = Aabb::from_center_size(Vec3::new(0.0, 4.0, 1.0), Vec3::new(1.2, 0.5, 2.0)); + + assert!(output.report.emitted_box_fragments > 2); + for tri in output.mesh.indices.chunks_exact(3) { + let center = (output.mesh.positions[tri[0] as usize] + + output.mesh.positions[tri[1] as usize] + + output.mesh.positions[tri[2] as usize]) + / 3.0; + assert!(!cutter.contains_point_strict(center, 1.0e-4)); + } + } + + #[test] + fn mesh_box_emits_twelve_triangles() { + let mut mesh = TriangleMesh::new(); + mesh.append_box(Aabb::from_center_size(Vec3::ZERO, Vec3::ONE), MaterialId(7)); + + assert_eq!(mesh.triangle_count(), 12); + assert_eq!(mesh.triangle_materials.len(), 12); + assert_eq!(mesh.triangle_materials[0], MaterialId(7)); + } +} diff --git a/crates/vg_csg/src/brush.rs b/crates/vg_csg/src/brush.rs new file mode 100644 index 0000000..d17e8a5 --- /dev/null +++ b/crates/vg_csg/src/brush.rs @@ -0,0 +1,216 @@ +use bevy_math::{Quat, Vec3}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct BrushId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] +pub struct MaterialId(pub u32); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BrushOp { + Add, + Subtract, + Intersect, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Aabb { + pub min: Vec3, + pub max: Vec3, +} + +impl Aabb { + pub fn from_center_size(center: Vec3, size: Vec3) -> Self { + let half = size * 0.5; + Self { + min: center - half, + max: center + half, + } + } + + pub fn new(min: Vec3, max: Vec3) -> Self { + Self { min, max } + } + + pub fn size(self) -> Vec3 { + self.max - self.min + } + + pub fn center(self) -> Vec3 { + (self.min + self.max) * 0.5 + } + + pub fn is_valid(self) -> bool { + self.max.x > self.min.x && self.max.y > self.min.y && self.max.z > self.min.z + } + + pub fn intersects(self, other: Self) -> bool { + self.min.x < other.max.x + && self.max.x > other.min.x + && self.min.y < other.max.y + && self.max.y > other.min.y + && self.min.z < other.max.z + && self.max.z > other.min.z + } + + pub fn intersection(self, other: Self) -> Option { + if !self.intersects(other) { + return None; + } + + let min = self.min.max(other.min); + let max = self.max.min(other.max); + let hit = Self { min, max }; + hit.is_valid().then_some(hit) + } + + pub fn contains_point(self, point: Vec3) -> bool { + point.x >= self.min.x + && point.x <= self.max.x + && point.y >= self.min.y + && point.y <= self.max.y + && point.z >= self.min.z + && point.z <= self.max.z + } + + pub fn contains_point_strict(self, point: Vec3, epsilon: f32) -> bool { + point.x > self.min.x + epsilon + && point.x < self.max.x - epsilon + && point.y > self.min.y + epsilon + && point.y < self.max.y - epsilon + && point.z > self.min.z + epsilon + && point.z < self.max.z - epsilon + } + + pub fn subtract_box(self, cutter: Self) -> Vec { + let Some(hit) = self.intersection(cutter) else { + return vec![self]; + }; + + let mut pieces = Vec::with_capacity(6); + + push_valid( + &mut pieces, + Self::new(self.min, Vec3::new(hit.min.x, self.max.y, self.max.z)), + ); + push_valid( + &mut pieces, + Self::new(Vec3::new(hit.max.x, self.min.y, self.min.z), self.max), + ); + + let x_min = self.min.x.max(hit.min.x); + let x_max = self.max.x.min(hit.max.x); + + push_valid( + &mut pieces, + Self::new( + Vec3::new(x_min, self.min.y, self.min.z), + Vec3::new(x_max, hit.min.y, self.max.z), + ), + ); + push_valid( + &mut pieces, + Self::new( + Vec3::new(x_min, hit.max.y, self.min.z), + Vec3::new(x_max, self.max.y, self.max.z), + ), + ); + + let y_min = self.min.y.max(hit.min.y); + let y_max = self.max.y.min(hit.max.y); + + push_valid( + &mut pieces, + Self::new( + Vec3::new(x_min, y_min, self.min.z), + Vec3::new(x_max, y_max, hit.min.z), + ), + ); + push_valid( + &mut pieces, + Self::new( + Vec3::new(x_min, y_min, hit.max.z), + Vec3::new(x_max, y_max, self.max.z), + ), + ); + + pieces + } +} + +fn push_valid(pieces: &mut Vec, bounds: Aabb) { + if bounds.is_valid() { + pieces.push(bounds); + } +} + +#[derive(Clone, Debug)] +pub enum Primitive { + Box { + bounds: Aabb, + }, + CylinderZ { + center: Vec3, + radius: f32, + depth: f32, + segments: usize, + }, + DomeCapZ { + center: Vec3, + radius: f32, + height: f32, + rings: usize, + segments: usize, + }, + FloretArm { + anchor: Vec3, + direction: Vec3, + length: f32, + root_width: f32, + tip_width: f32, + thickness: f32, + tip_lift: f32, + }, +} + +#[derive(Clone, Debug)] +pub struct Brush { + pub id: BrushId, + pub name: String, + pub op: BrushOp, + pub primitive: Primitive, + pub material: MaterialId, + pub rotation: Quat, + pub generation: u64, +} + +impl Brush { + pub fn new( + id: BrushId, + name: impl Into, + op: BrushOp, + primitive: Primitive, + material: MaterialId, + ) -> Self { + Self { + id, + name: name.into(), + op, + primitive, + material, + rotation: Quat::IDENTITY, + generation: 0, + } + } + + pub fn set_dirty(&mut self) { + self.generation = self.generation.wrapping_add(1); + } + + pub fn as_box(&self) -> Option { + match self.primitive { + Primitive::Box { bounds } => Some(bounds), + _ => None, + } + } +} diff --git a/crates/vg_csg/src/dsl.rs b/crates/vg_csg/src/dsl.rs new file mode 100644 index 0000000..dba27ab --- /dev/null +++ b/crates/vg_csg/src/dsl.rs @@ -0,0 +1,140 @@ +use bevy_math::Vec3; + +use crate::{ + Aabb, Assembler, BrushId, BrushOp, BuildOutput, DomeCapZSpec, FloretArmSpec, MaterialId, + Primitive, +}; + +#[derive(Clone, Debug, Default)] +pub struct LevelDsl { + assembler: Assembler, +} + +impl LevelDsl { + pub fn new() -> Self { + Self::default() + } + + pub fn solid_box( + &mut self, + name: impl Into, + center: Vec3, + size: Vec3, + material: MaterialId, + ) -> BrushId { + self.assembler + .solid_box(name, Aabb::from_center_size(center, size), material) + } + + pub fn cut_box(&mut self, name: impl Into, center: Vec3, size: Vec3) -> BrushId { + self.assembler + .cut_box(name, Aabb::from_center_size(center, size)) + } + + pub fn cylinder_z( + &mut self, + name: impl Into, + center: Vec3, + radius: f32, + depth: f32, + segments: usize, + material: MaterialId, + ) -> BrushId { + self.assembler.add_brush( + name, + BrushOp::Add, + Primitive::CylinderZ { + center, + radius, + depth, + segments, + }, + material, + ) + } + + pub fn dome_cap_z(&mut self, name: impl Into, spec: DomeCapZSpec) -> BrushId { + self.assembler.add_brush( + name, + BrushOp::Add, + Primitive::DomeCapZ { + center: spec.center, + radius: spec.radius, + height: spec.height, + rings: spec.rings, + segments: spec.segments, + }, + spec.material, + ) + } + + pub fn floret_arm(&mut self, name: impl Into, spec: FloretArmSpec) -> BrushId { + self.assembler.add_brush( + name, + BrushOp::Add, + Primitive::FloretArm { + anchor: spec.anchor, + direction: spec.direction, + length: spec.length, + root_width: spec.root_width, + tip_width: spec.tip_width, + thickness: spec.thickness, + tip_lift: spec.tip_lift, + }, + spec.material, + ) + } + + pub fn assemble(&self) -> BuildOutput { + self.assembler.build() + } + + pub fn into_assembler(self) -> Assembler { + self.assembler + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dsl_builds_room_and_habitat_primitives() { + let mut dsl = LevelDsl::new(); + dsl.solid_box( + "wall", + Vec3::new(0.0, 0.0, 1.5), + Vec3::new(5.0, 0.25, 3.0), + MaterialId(1), + ); + dsl.cut_box("door", Vec3::new(0.0, 0.0, 1.0), Vec3::new(1.0, 0.5, 2.0)); + dsl.dome_cap_z( + "city dome", + DomeCapZSpec { + center: Vec3::new(0.0, 0.0, 0.0), + radius: 3.0, + height: 1.5, + rings: 4, + segments: 16, + material: MaterialId(2), + }, + ); + dsl.floret_arm( + "solar petal", + FloretArmSpec { + anchor: Vec3::new(3.0, 0.0, 0.0), + direction: Vec3::X, + length: 12.0, + root_width: 1.2, + tip_width: 3.0, + thickness: 0.08, + tip_lift: 0.6, + material: MaterialId(3), + }, + ); + + let output = dsl.assemble(); + assert!(output.mesh.triangle_count() > 12); + assert!(output.report.warnings.is_empty()); + } +} diff --git a/crates/vg_csg/src/lib.rs b/crates/vg_csg/src/lib.rs new file mode 100644 index 0000000..17ba626 --- /dev/null +++ b/crates/vg_csg/src/lib.rs @@ -0,0 +1,21 @@ +//! Lean constructive geometry tools for VibeGeometry. +//! +//! This crate borrows RealtimeCSG's useful public shape: ordered brushes, +//! operation types, dirtied generations, and a rebuildable output mesh. The +//! hidden native RealtimeCSG kernel is not public, so this starts with the +//! smallest honest CSG organ: exact box subtraction against additive boxes, +//! plus additive procedural primitives for the habitat forms we keep needing. + +mod assembler; +mod brush; +mod dsl; +mod mesh; +mod primitives; + +pub use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning}; +pub use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, Primitive}; +pub use dsl::LevelDsl; +pub use mesh::TriangleMesh; +pub use primitives::{ + DomeCapZSpec, FloretArmSpec, append_cylinder_z, append_dome_cap_z, append_floret_arm, +}; diff --git a/crates/vg_csg/src/mesh.rs b/crates/vg_csg/src/mesh.rs new file mode 100644 index 0000000..4bc2d6a --- /dev/null +++ b/crates/vg_csg/src/mesh.rs @@ -0,0 +1,102 @@ +use bevy_math::{Vec2, Vec3}; + +use crate::{Aabb, MaterialId}; + +#[derive(Clone, Debug, Default)] +pub struct TriangleMesh { + pub positions: Vec, + pub normals: Vec, + pub uvs: Vec, + pub indices: Vec, + pub triangle_materials: Vec, +} + +impl TriangleMesh { + pub fn new() -> Self { + Self::default() + } + + pub fn triangle_count(&self) -> usize { + self.indices.len() / 3 + } + + pub fn vertex_count(&self) -> usize { + self.positions.len() + } + + pub fn append_box(&mut self, bounds: Aabb, material: MaterialId) { + let min = bounds.min; + let max = bounds.max; + let corners = [ + Vec3::new(min.x, min.y, min.z), + Vec3::new(max.x, min.y, min.z), + Vec3::new(max.x, max.y, min.z), + Vec3::new(min.x, max.y, min.z), + Vec3::new(min.x, min.y, max.z), + Vec3::new(max.x, min.y, max.z), + Vec3::new(max.x, max.y, max.z), + Vec3::new(min.x, max.y, max.z), + ]; + + self.append_quad( + [corners[0], corners[3], corners[2], corners[1]], + Vec3::NEG_Z, + material, + ); + self.append_quad( + [corners[4], corners[5], corners[6], corners[7]], + Vec3::Z, + material, + ); + self.append_quad( + [corners[0], corners[1], corners[5], corners[4]], + Vec3::NEG_Y, + material, + ); + self.append_quad( + [corners[3], corners[7], corners[6], corners[2]], + Vec3::Y, + material, + ); + self.append_quad( + [corners[0], corners[4], corners[7], corners[3]], + Vec3::NEG_X, + material, + ); + self.append_quad( + [corners[1], corners[2], corners[6], corners[5]], + Vec3::X, + material, + ); + } + + pub fn append_quad(&mut self, points: [Vec3; 4], normal: Vec3, material: MaterialId) { + let base = self.positions.len() as u32; + self.positions.extend(points); + self.normals.extend([normal; 4]); + self.uvs.extend([ + Vec2::new(0.0, 0.0), + Vec2::new(1.0, 0.0), + Vec2::new(1.0, 1.0), + Vec2::new(0.0, 1.0), + ]); + self.indices + .extend([base, base + 1, base + 2, base, base + 2, base + 3]); + self.triangle_materials.extend([material; 2]); + } + + pub fn append_triangle( + &mut self, + points: [Vec3; 3], + normal: Vec3, + uvs: [Vec2; 3], + material: MaterialId, + ) { + let base = self.positions.len() as u32; + self.positions.extend(points); + self.normals.extend([normal; 3]); + self.uvs.extend(uvs); + self.indices.extend([base, base + 1, base + 2]); + self.triangle_materials.push(material); + } +} diff --git a/crates/vg_csg/src/primitives.rs b/crates/vg_csg/src/primitives.rs new file mode 100644 index 0000000..576955b --- /dev/null +++ b/crates/vg_csg/src/primitives.rs @@ -0,0 +1,172 @@ +use bevy_math::{Vec2, Vec3}; + +use crate::{MaterialId, TriangleMesh}; + +#[derive(Clone, Copy, Debug)] +pub struct DomeCapZSpec { + pub center: Vec3, + pub radius: f32, + pub height: f32, + pub rings: usize, + pub segments: usize, + pub material: MaterialId, +} + +#[derive(Clone, Copy, Debug)] +pub struct FloretArmSpec { + pub anchor: Vec3, + pub direction: Vec3, + pub length: f32, + pub root_width: f32, + pub tip_width: f32, + pub thickness: f32, + pub tip_lift: f32, + pub material: MaterialId, +} + +pub fn append_cylinder_z( + mesh: &mut TriangleMesh, + center: Vec3, + radius: f32, + depth: f32, + segments: usize, + material: MaterialId, +) { + let segments = segments.max(3); + let half = depth * 0.5; + let bottom_center = center - Vec3::Z * half; + let top_center = center + Vec3::Z * half; + + for i in 0..segments { + let a0 = ring_angle(i, segments); + let a1 = ring_angle(i + 1, segments); + let p0 = center + Vec3::new(a0.cos() * radius, a0.sin() * radius, -half); + let p1 = center + Vec3::new(a1.cos() * radius, a1.sin() * radius, -half); + let p2 = center + Vec3::new(a1.cos() * radius, a1.sin() * radius, half); + let p3 = center + Vec3::new(a0.cos() * radius, a0.sin() * radius, half); + let normal = Vec3::new(((a0 + a1) * 0.5).cos(), ((a0 + a1) * 0.5).sin(), 0.0); + mesh.append_quad([p0, p1, p2, p3], normal, material); + mesh.append_triangle( + [bottom_center, p1, p0], + Vec3::NEG_Z, + [Vec2::ZERO, Vec2::X, Vec2::Y], + material, + ); + mesh.append_triangle( + [top_center, p3, p2], + Vec3::Z, + [Vec2::ZERO, Vec2::X, Vec2::Y], + material, + ); + } +} + +pub fn append_dome_cap_z(mesh: &mut TriangleMesh, spec: DomeCapZSpec) { + let rings = spec.rings.max(2); + let segments = spec.segments.max(6); + let top = spec.center + Vec3::Z * spec.height; + + for ring in 0..rings { + let t0 = ring as f32 / rings as f32; + let t1 = (ring + 1) as f32 / rings as f32; + let r0 = spec.radius * (1.0 - t0 * t0).sqrt(); + let r1 = spec.radius * (1.0 - t1 * t1).sqrt(); + let z0 = spec.center.z + spec.height * t0; + let z1 = spec.center.z + spec.height * t1; + + for seg in 0..segments { + let a0 = ring_angle(seg, segments); + let a1 = ring_angle(seg + 1, segments); + let p0 = Vec3::new( + spec.center.x + a0.cos() * r0, + spec.center.y + a0.sin() * r0, + z0, + ); + let p1 = Vec3::new( + spec.center.x + a1.cos() * r0, + spec.center.y + a1.sin() * r0, + z0, + ); + + if ring + 1 == rings { + let normal = (p0 - spec.center) + .cross(top - spec.center) + .normalize_or_zero(); + mesh.append_triangle( + [p0, p1, top], + normal, + [Vec2::ZERO, Vec2::X, Vec2::Y], + spec.material, + ); + } else { + let p2 = Vec3::new( + spec.center.x + a1.cos() * r1, + spec.center.y + a1.sin() * r1, + z1, + ); + let p3 = Vec3::new( + spec.center.x + a0.cos() * r1, + spec.center.y + a0.sin() * r1, + z1, + ); + let normal = ((p0 + p1 + p2 + p3) * 0.25 - spec.center).normalize_or_zero(); + mesh.append_quad([p0, p1, p2, p3], normal, spec.material); + } + } + } +} + +pub fn append_floret_arm(mesh: &mut TriangleMesh, spec: FloretArmSpec) { + let forward = spec.direction.normalize_or_zero(); + let forward = if forward.length_squared() == 0.0 { + Vec3::X + } else { + forward + }; + let mut side = forward.cross(Vec3::Z).normalize_or_zero(); + if side.length_squared() == 0.0 { + side = Vec3::Y; + } + + let root_center = spec.anchor; + let tip_center = spec.anchor + forward * spec.length + Vec3::Z * spec.tip_lift; + let z = Vec3::Z * (spec.thickness * 0.5); + let r = side * (spec.root_width * 0.5); + let t = side * (spec.tip_width * 0.5); + + let bottom = [ + root_center - r - z, + root_center + r - z, + tip_center + t - z, + tip_center - t - z, + ]; + let top = [ + root_center - r + z, + root_center + r + z, + tip_center + t + z, + tip_center - t + z, + ]; + + mesh.append_quad( + [bottom[0], bottom[3], bottom[2], bottom[1]], + Vec3::NEG_Z, + spec.material, + ); + mesh.append_quad([top[0], top[1], top[2], top[3]], Vec3::Z, spec.material); + mesh.append_quad( + [bottom[0], bottom[1], top[1], top[0]], + -forward, + spec.material, + ); + mesh.append_quad( + [bottom[3], top[3], top[2], bottom[2]], + forward, + spec.material, + ); + mesh.append_quad([bottom[0], top[0], top[3], bottom[3]], -side, spec.material); + mesh.append_quad([bottom[1], bottom[2], top[2], top[1]], side, spec.material); +} + +fn ring_angle(index: usize, segments: usize) -> f32 { + index as f32 * std::f32::consts::TAU / segments as f32 +} diff --git a/docs/research/realtime-csg-bevy-assembler.md b/docs/research/realtime-csg-bevy-assembler.md new file mode 100644 index 0000000..0058472 --- /dev/null +++ b/docs/research/realtime-csg-bevy-assembler.md @@ -0,0 +1,136 @@ +# Realtime CSG Bevy Assembler + +## Source Read + +LogicalError's RealtimeCSG is useful as an architectural reference, not as a +kernel we can directly port. The public repository describes a Unity editor +plugin for CSG level editing and states that runtime editing is not supported +through a maintained API. It also states that the native C++ source is not +public. + +The public C# layer still exposes the parts worth stealing cleanly: + +- control meshes made from vertices, half edges, polygons, and texgen/surface + metadata +- tree, branch, and brush handles +- explicit brush operation types +- dirty generation counters +- a rebuild step that turns authored brushes into renderable meshes + +That shape maps well to Bevy: keep authoring data as lightweight brush records, +rebuild only dirty worlds, and emit plain mesh buffers that can later be handed +to Bevy render assets. + +## Current Port Shape + +`crates/vg_csg` is the first lean Rust organ. + +It uses `bevy_math` for `Vec3`, `Vec2`, and Bevy-compatible spatial types while +keeping output as simple arrays: + +```rust +use bevy_math::Vec3; +use vg_csg::{LevelDsl, MaterialId}; + +let mut level = LevelDsl::new(); +level.solid_box( + "wall", + Vec3::new(0.0, 0.0, 1.5), + Vec3::new(6.0, 0.3, 3.0), + MaterialId(1), +); +level.cut_box( + "door", + Vec3::new(0.0, 0.0, 1.0), + Vec3::new(1.2, 0.6, 2.0), +); + +let output = level.assemble(); +assert!(output.mesh.triangle_count() > 12); +``` + +The first actual CSG operator is exact AABB subtraction against additive AABB +solids. A cutter splits a source box into up to six surviving slabs. This is +enough for doors, windows, vents, corridor punches, terraces, anchor trenches, +and level blockout loops. + +Procedural non-CSG primitives are additive: + +- `CylinderZ` for tethers, shafts, columns, conduits, and ring anchors +- `DomeCapZ` for flat-base pressure bubbles and caps +- `FloretArm` for sunflower/Citadel-scale solar and radiator petals + +Those primitives are intentionally in the same brush stream as CSG boxes, so a +level script can mix hard architectural cuts with habitat-specific forms: + +```rust +level.dome_cap_z( + "city dome", + vg_csg::DomeCapZSpec { + center: Vec3::ZERO, + radius: 20.0, + height: 8.0, + rings: 12, + segments: 64, + material: MaterialId(20), + }, +); +level.floret_arm( + "radiator petal", + vg_csg::FloretArmSpec { + anchor: Vec3::new(20.0, 0.0, 0.2), + direction: Vec3::X, + length: 120.0, + root_width: 6.0, + tip_width: 18.0, + thickness: 0.12, + tip_lift: 3.0, + material: MaterialId(21), + }, +); +``` + +## Doctrine + +Think of brushes as legal claims on volume. + +An additive brush says: "this matter exists." A subtractive brush says: "this +space must remain empty." The assembler is the clerk that resolves claims in +order and emits triangles only after the claims stop arguing. + +For interactive work, the brush list is the durable design surface. The mesh is +a cache. This matters because agents should revise intent by moving, adding, or +renaming brushes, not by poking triangles after the fact. + +Use CSG where negative space carries meaning: + +- doorways, windows, hatches, service trenches +- docking sockets and tether anchor gaps +- ring terraces and mechanically repeated cutouts +- fast blockouts where the same cutter can punch many solids + +Use additive procedural primitives where a form is better described by a field +or sweep than by carving: + +- domes, rings, tethers, panels, radiators +- city-field parcels after street generation +- flower-like utility crests and other habitat-specific silhouettes + +The immediate next upgrade is not "more primitives." It is a stronger convex +brush kernel: plane classification, polygon clipping, fragment merge policy, +and a BVH or spatial hash so cutter cost does not scale like a punishment. + +## Limits + +`vg_csg` is not yet a full RealtimeCSG replacement. + +- only box subtractors affect box solids +- non-box subtractors are reported and ignored +- intersect brushes are reported and ignored +- output is Bevy-compatible mesh data, not yet a Bevy `Mesh` asset constructor +- there is no editor gizmo, ECS plugin, or incremental spatial index yet + +Those limits are explicit because invisible ambition is how you get haunted by +your own abstractions. The useful loop exists now: script brushes, assemble, +inspect mesh stats, and replace the kernel under the same DSL when the target +demands it. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 05f1071..90322ee 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -22,37 +22,33 @@ git log --oneline -5 ## Current State -VibeGeometry is still an incubation scaffold: persistence, graph-translation -corpus, repo-local Geometry Script tooling, verifiers, and doctrine exist; the -builder library and artifact schemas do not. +VibeGeometry is an incubation scaffold with persistence, graph-translation +corpus notes, repo-local Geometry Script tooling, verifiers, doctrine, a Python +helper library, and now a first Rust CSG assembler. Artifact schemas are still +pending. Corpus inventory, graph maps, verification notes, and doctrine live in -`docs/research/`. This handoff is not the ledger. +`docs/research/`; this handoff is not the ledger. Accepted corpus families +include local Shriinivas samples, Quellenform `CurveToMeshUV`, official Blender +demos, and IRCSS French Houses footholds. IRCSS blockers remain repeat-zone +feedback, repeat `Top` outputs, foreach zones, gizmo nodes, and the rejected +empty-output `WindowBeams` harness. -Accepted corpus families include local Shriinivas samples, Quellenform -`CurveToMeshUV`, official Blender demos, and IRCSS French Houses footholds. -IRCSS blockers remain repeat-zone feedback, repeat `Top` outputs, foreach -zones, gizmo nodes, and the rejected empty-output `WindowBeams` harness. Maps -live in `docs/research/translations/ircss-french-houses-*.md`. +From-scratch story-bound builds live in `examples/geometry_script/`: -From-scratch story-bound builds: +- `aetheria_bloom_habitat.py`: whole Bloom study with explicit lore coordinate + frames, cylindrical/endcap attachment, golden-angle transfer curves, + deterministic settlement noise, batched dense detail, and inspection cameras. +- `lucent_tether_habitat.py`: Ghostlight/Lucent tether habitat with orthogonal + spin-gravity city plane, flat-cut dome base, street-field city, shader work, + tether attachment, and exterior Citadel-scale solar/radiator florets. -- `examples/geometry_script/aetheria_bloom_habitat.py` generates the Aetheria - Bloom whole-habitat study. It carries the key spatial lessons: explicit lore - coordinate frames, cylindrical/endcap attachment, golden-angle transfer - curves, deterministic settlement noise, batched dense detail, and inspection - cameras. Build notes: `docs/research/builds/aetheria-bloom-habitat.md`; - verifier: `tools/verify_aetheria_bloom_habitat.py`. -- `examples/geometry_script/lucent_tether_habitat.py` generates the Lucent - tether habitat from Ghostlight `Lucent Hostage Feed`: tether axis, city bubble - below, media-eye lounge, crisis threshold, overlays, control/side panels, and - security doors. Accepted revision uses Cycles, shader nodes, explicit bubble - attachment, an orthogonal spin-gravity city plane, a flat-cut dome base, - radial/tangential streets, irregular parcels, prestige anchor towers, parks, - faux-rural edge falloff, and an exterior sunflower-like crest of alternating - Citadel-scale solar panels/radiators. - Build notes: `docs/research/builds/lucent-tether-habitat.md`; verifier: - `tools/verify_lucent_tether_habitat.py`. +Build notes and verifiers: + +- `docs/research/builds/aetheria-bloom-habitat.md` +- `docs/research/builds/lucent-tether-habitat.md` +- `tools/verify_aetheria_bloom_habitat.py` +- `tools/verify_lucent_tether_habitat.py` Current authoring prior: use the full Python stack. Python structures intent, tables, loops, variants, cleanup, validation, docs, and scene/render @@ -64,6 +60,15 @@ coordinate-frame helpers, deterministic field/noise helpers, mesh batching, thin Blender object/material/camera helpers, and verifier helpers. The Bloom script imports these primitives directly, so the library is not just a shrine. +`crates/vg_csg` is the first Rust/Bevy-side interactive geometry organ. +RealtimeCSG's public Unity repo exposes a useful brush/tree/operation/generation +model, but not the optimized native C++ kernel or a supported runtime editing +API. `vg_csg` therefore ports the architecture shape: ordered brushes, exact +AABB subtraction for box solids, additive `CylinderZ`, `DomeCapZ`, and +`FloretArm` primitives, Bevy-math mesh buffers, `LevelDsl`, tests, clippy, and +an example room. Research note: +`docs/research/realtime-csg-bevy-assembler.md`. + The repo-local Geometry Script clone has Blender 5.1 work for nested `@tree` group reuse, a ported `nodes_to_script` converter, and menu-socket default tolerance. See `docs/research/geometry-script-fork-notes.md`. Run @@ -78,11 +83,11 @@ The GameCult fork is intentionally pruned to `main` plus VibeGeometry-owned tooling branches. Do not copy upstream branch soup forward unless a future patch actually needs it. -The current next action is to extract the first higher-level reusable builder -seam from the completed story-bound builds: either threshold/control-rail -interface kits from the Lucent habitat or endcap terrace/spiral artery kits -from the Bloom, then bind that seam to scene-brief, manifest, -inspection-render, and verifier contract drafts. +The current next action is to extend `vg_csg` from a tested brush assembler into +an interactive Bevy loop: add Bevy `Mesh` conversion, an ECS plugin/resource +boundary, visual debug rendering for brush claims versus emitted mesh, and then +upgrade the CSG kernel from AABB slabs toward convex half-space clipping and +spatial indexing. ## Important Invariants diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 692fbe8..1879d63 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -48,3 +48,4 @@ {"ts": "2026-05-05T03:01:59+00:00", "type": "implementation", "status": "accepted", "note": "Corrected Lucent dome city coordinate frame: ground is now a YZ disk orthogonal to the tether/spin axis, buildings extrude along +X, dome is boolean-cut flat at the base, city fills to the rim, and a Perlin-edged sunflower ray-floret crest rises from the base partway up the dome."} {"ts": "2026-05-05T03:16:04+00:00", "type": "implementation", "status": "accepted", "note": "Moved Lucent sunflower ray florets outside the dome: alternating solar panel and radiator meshes now radiate sunward from the flat base rim, with verifier checks that their geometry remains outside the dome cut."} {"ts": "2026-05-05T03:31:22+00:00", "type": "implementation", "status": "accepted", "note": "Scaled Lucent external ray florets to Citadel-like utility arms: alternating solar/radiator panels now dwarf the dome and span far along the tether, so a folded defensive posture can plausibly reach toward the media-eye end."} +{"ts": "2026-05-06T23:06:51+00:00", "type": "implementation", "status": "accepted", "note": "Added vg_csg, a Rust workspace crate for the new interactive Bevy-compatible CSG path: RealtimeCSG-inspired brush ordering and generations, exact AABB box subtraction, additive cylinder/dome/floret primitives, mesh-array output, DSL, tests, clippy, example, and research doctrine. Public RealtimeCSG source informed the shape but not the hidden native kernel."} diff --git a/state/map.yaml b/state/map.yaml index c0e940c..67e6fb7 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -13,8 +13,9 @@ mission: geometry-node graphs that produce specific visual results from reference images, text instructions, and iterative render review. near_term: > - Define the scene brief, node graph manifest, Python builder, render runner, - and review loop before building a large library of graph idioms. + Define the scene brief, node graph manifest, Python/Geometry Script builder, + Rust/Bevy CSG assembler, render runner, and review loop before building a + large library of graph idioms. hard_boundaries: - Blender 5.0+ is the target runtime unless a task explicitly chooses another version. @@ -44,6 +45,7 @@ live_architecture: - `vibegeometry/` provides reusable Python helpers for coordinate contracts, deterministic fields, batched geometry, Blender object/material/camera setup, and verifier checks. - Geometry Script emits inspectable reusable Geometry Nodes groups. - bpy handles Blender objects, modifiers, materials, cameras, lights, files, renders, and evaluated-geometry checks. + - Rust/Bevy-side CSG tooling provides an interactive brush-and-boolean assembler path for fast level geometry, starting with box CSG and habitat-specific additive primitives. - External Python tools inspect source blends, generate converter drafts, compare outputs, and maintain corpus evidence. persistence: - `state/map.yaml` stores canonical mission, boundaries, architecture, artifacts, and next action. @@ -75,6 +77,8 @@ current_artifacts: - docs/research/translations/ircss-french-houses-node-mechanisms.md - docs/research/builds/aetheria-bloom-habitat.md - docs/research/builds/lucent-tether-habitat.md + - docs/research/realtime-csg-bevy-assembler.md + - crates/vg_csg/ - vibegeometry/ tools: - tools/vibegeometry_state.py @@ -101,8 +105,8 @@ data_flow: - If state grows faster than understanding, cut it back. current_status: - summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. - next_action: Extract the first higher-level reusable builder seam from the completed story-bound builds: either threshold/control-rail interface kits from the Lucent habitat or endcap terrace/spiral artery kits from the Bloom, then bind that seam to scene-brief, manifest, inspection-render, and verifier contract drafts. + summary: VibeGeometry has verified Geometry Script translations for local samples, official Blender demos, and IRCSS French Houses footholds, plus from-scratch Aetheria Bloom and Lucent tether habitat builds. Durable lessons now driving the pipeline: Python orchestrates intent and batching; Geometry Script emits inspectable node groups; source-bound scenes need explicit coordinate contracts before ornament; spin-gravity habitats need ground planes orthogonal to their tether/rotation axis; utility petals, radiators, and solar arrays belong outside pressure glass and may need Citadel-scale presence; large habitat parts need visible load paths across coordinate frames; organic city maps need street fields, irregular parcels, parks, and land-use gradients rather than grids; dense detail must be batched; institutional spaces should model power as surfaces, rails, overlays, thresholds, shader behavior, and camera evidence; and visual acceptance needs domain-facing inspection renders plus verifier checks, not only overview renders. The first reusable `vibegeometry/` helper library now carries coordinate frames, fields, batching, Blender scene primitives, and verifier helpers used by the Bloom and Lucent builds. A new Rust workspace crate, `vg_csg`, starts the interactive Bevy-compatible geometry path: RealtimeCSG-inspired ordered brushes, operation types, generation tracking, exact AABB subtraction for box solids, additive cylinder/dome/floret primitives, mesh-array output, tests, clippy coverage, and a sample room. Known graph-tooling blockers remain repeat-zone feedback, repeat Top outputs, foreach zones, and gizmo nodes. + next_action: Extend `vg_csg` from a tested brush assembler into an interactive Bevy loop: add Bevy `Mesh` conversion, an ECS plugin/resource boundary, visual debug rendering for brush claims versus emitted mesh, and then upgrade the CSG kernel from AABB slabs toward convex half-space clipping and spatial indexing. remotes: vibegeometry: https://github.com/GameCult/VibeGeometry geometry_script_fork: https://github.com/GameCult/geometry-script