Absorb CSG kernel into GameCult.Geometry
This commit is contained in:
parent
99c893bd64
commit
6b44f969a9
14 changed files with 6598 additions and 10 deletions
|
|
@ -4,7 +4,7 @@ Plan: `docs/cultmath-gamecult-geometry-migration-plan.md` (locked)
|
|||
|
||||
Started: 2026-07-22
|
||||
|
||||
Status: Stages 0-1 complete; Stage 2 in progress
|
||||
Status: Stages 0-4 complete; Stage 5 in progress
|
||||
|
||||
## Objective
|
||||
|
||||
|
|
@ -58,8 +58,8 @@ Demotions:
|
|||
| 1. Destination/dependency/formatters/SoA | Complete | NuGet and dedicated UPM artifacts prove dependency direction and owned vector integration |
|
||||
| 2. Primitive deletion and v2 schemas | Complete | C# and Geometry-owned Rust emit identical v2 bytes and record keys for all four roots |
|
||||
| 3. Planetary transfer | Complete | Managed, HLSL, CMPT, Unity, web, tool, docs, tests, and package authority moved; CultMath owners deleted |
|
||||
| 4. Mine `vg-csg` | Pending | Requires v1 fixture capture and target Rust package seam |
|
||||
| 5. CultCache/CultNet/CultMesh integration | Pending | Requires current schemas and runtime packages |
|
||||
| 4. Mine `vg-csg` | Complete | Geometry-owned Rust kernel passes correctness, parity, performance, formatting, and warning-denied lint gates |
|
||||
| 5. CultCache/CultNet/CultMesh integration | In progress | Provider/dataflow mapped; typed commit-path smoke remains |
|
||||
| 6. Consumer cutover/release | Pending | Requires local candidate feed and downstream passes |
|
||||
| 7. Delete obsolete authorities/audit | Pending | Requires every prior stage gate |
|
||||
|
||||
|
|
@ -226,11 +226,31 @@ a broader stage gate.
|
|||
planetary tests pass 41/41. Negative searches find obsolete CultMath geometry
|
||||
names only inside tests that assert their absence or ignored generated output.
|
||||
|
||||
### Stage 4 receipts
|
||||
|
||||
- `packages/gamecult-geometry-rs` now owns the mined convex splitting, ordered
|
||||
brush CSG, tree lowering, dirty frontiers, prefix checkpoints, domain/LOD
|
||||
selection, feature lowering, and triangle/collider assembly kernels. No build
|
||||
or runtime dependency points at VibeGeometry or `vg-csg`.
|
||||
- The public `GeometryBrushAssembler` facade accepts Geometry-owned
|
||||
`Float3` values and returns `GeometryTriangleMesh`; `bevy_math` remains a
|
||||
private implementation dependency and is not re-exported.
|
||||
- The duplicate quarry persistence authority (`DomainSpecDocument`,
|
||||
`DomainNodeDocument`, `FeatureClaimDocument`, and its local schema version)
|
||||
was deleted. Private specs are in-memory builders; the v2 `Geometry*`
|
||||
documents remain the sole wire authority.
|
||||
- Nine RealtimeCSG behavioral fixtures and a bounded 512-distant-cutter
|
||||
performance fixture moved under Geometry ownership. `cargo test --locked`
|
||||
passes 67/67 plus doc tests; rustfmt and warning-denied Clippy pass.
|
||||
- The authority transfer commits are CultLib `756f5a33`, CultMath `6a16260`,
|
||||
and the post-transfer label cleanup `99c893bd`. The Rust absorption commit is
|
||||
recorded after this progress update is committed.
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. Mine convex splitting, brush/CSG tree, domain lowering, dirty-frontier,
|
||||
selected-cut, and mesh assembly into `gamecult-geometry-rs` in dependency
|
||||
order, preserving the pinned v1 evidence while emitting v2.
|
||||
2. Remove `bevy_math` from the public Rust boundary and port earned correctness
|
||||
and performance fixtures before deleting VibeGeometry build authority.
|
||||
3. Connect the completed Rust runtime to the v2 CultCache/CultNet/CultMesh path.
|
||||
1. Add the Geometry-owned worker provider with `CultNetDatabase.PutAsync` as
|
||||
the single commit/publication primitive for direct and operation-handle use.
|
||||
2. Prove domain/request to selected-cut/artifact persistence, raw envelope
|
||||
replication, worker-state observation, and typed dev-probe agreement.
|
||||
3. Add exact C#/Rust raw-envelope parity without growing a second networking
|
||||
stack or a load-bearing JSON projection.
|
||||
|
|
|
|||
777
packages/gamecult-geometry-rs/Cargo.lock
generated
777
packages/gamecult-geometry-rs/Cargo.lock
generated
|
|
@ -2,12 +2,147 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[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 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[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 2.0.119",
|
||||
"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",
|
||||
"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",
|
||||
"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 2.0.119",
|
||||
"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.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -17,6 +152,32 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck_derive"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
|
|
@ -32,6 +193,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -42,6 +209,27 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[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 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -52,10 +240,86 @@ dependencies = [
|
|||
"crypto-common",
|
||||
]
|
||||
|
||||
[[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.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[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.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gamecult-geometry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"bevy_math",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"sha2",
|
||||
|
|
@ -71,12 +335,120 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[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.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
]
|
||||
|
||||
[[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.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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[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.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -84,6 +456,66 @@ 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.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||
|
||||
[[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]]
|
||||
|
|
@ -104,6 +536,66 @@ 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.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
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 = "rmp"
|
||||
version = "0.8.15"
|
||||
|
|
@ -123,6 +615,33 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[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.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[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.229"
|
||||
|
|
@ -150,7 +669,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -164,6 +683,47 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[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.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
|
|
@ -175,6 +735,62 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[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.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
|
|
@ -187,8 +803,167 @@ version = "1.0.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"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 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||
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.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ repository = "https://github.com/GameCult/CultLib"
|
|||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bevy_math = "0.17"
|
||||
rmp-serde = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sha2 = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
|
|
|
|||
1509
packages/gamecult-geometry-rs/src/assembler.rs
Normal file
1509
packages/gamecult-geometry-rs/src/assembler.rs
Normal file
File diff suppressed because it is too large
Load diff
370
packages/gamecult-geometry-rs/src/brush.rs
Normal file
370
packages/gamecult-geometry-rs/src/brush.rs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
#![allow(dead_code)]
|
||||
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, Eq, Hash)]
|
||||
pub enum PolygonCategory {
|
||||
Inside,
|
||||
Outside,
|
||||
Aligned,
|
||||
ReverseAligned,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Aabb {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
|
||||
impl Aabb {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
min: Vec3::splat(f32::INFINITY),
|
||||
max: Vec3::splat(f32::NEG_INFINITY),
|
||||
}
|
||||
}
|
||||
|
||||
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 from_points(points: &[Vec3]) -> Self {
|
||||
let mut bounds = Self::empty();
|
||||
for point in points {
|
||||
bounds.include_point(*point);
|
||||
}
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn include_point(&mut self, point: Vec3) {
|
||||
self.min = self.min.min(point);
|
||||
self.max = self.max.max(point);
|
||||
}
|
||||
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
if !self.has_finite_extents() {
|
||||
return other;
|
||||
}
|
||||
if !other.has_finite_extents() {
|
||||
return self;
|
||||
}
|
||||
Self {
|
||||
min: self.min.min(other.min),
|
||||
max: self.max.max(other.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 has_finite_extents(self) -> bool {
|
||||
self.min.is_finite() && self.max.is_finite()
|
||||
}
|
||||
|
||||
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 intersects_any(self, others: impl IntoIterator<Item = Self>) -> bool {
|
||||
others.into_iter().any(|other| self.intersects(other))
|
||||
}
|
||||
|
||||
pub fn intersection(self, other: Self) -> Option<Self> {
|
||||
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<Self> {
|
||||
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<Aabb>, bounds: Aabb) {
|
||||
if bounds.is_valid() {
|
||||
pieces.push(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Primitive {
|
||||
Box {
|
||||
bounds: Aabb,
|
||||
},
|
||||
OrientedBox {
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
},
|
||||
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<String>,
|
||||
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<Aabb> {
|
||||
match self.primitive {
|
||||
Primitive::Box { bounds } => Some(bounds),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> Aabb {
|
||||
self.primitive.bounds()
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive {
|
||||
pub fn bounds(&self) -> Aabb {
|
||||
match *self {
|
||||
Self::Box { bounds } => bounds,
|
||||
Self::OrientedBox {
|
||||
center,
|
||||
size,
|
||||
rotation,
|
||||
} => oriented_box_bounds(center, size, rotation),
|
||||
Self::CylinderZ {
|
||||
center,
|
||||
radius,
|
||||
depth,
|
||||
..
|
||||
} => Aabb::new(
|
||||
center - Vec3::new(radius, radius, depth * 0.5),
|
||||
center + Vec3::new(radius, radius, depth * 0.5),
|
||||
),
|
||||
Self::DomeCapZ {
|
||||
center,
|
||||
radius,
|
||||
height,
|
||||
..
|
||||
} => Aabb::new(
|
||||
center - Vec3::new(radius, radius, 0.0),
|
||||
center + Vec3::new(radius, radius, height),
|
||||
),
|
||||
Self::FloretArm {
|
||||
anchor,
|
||||
direction,
|
||||
length,
|
||||
root_width,
|
||||
tip_width,
|
||||
thickness,
|
||||
tip_lift,
|
||||
} => floret_bounds(
|
||||
anchor, direction, length, root_width, tip_width, thickness, tip_lift,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn oriented_box_bounds(center: Vec3, size: Vec3, rotation: Quat) -> Aabb {
|
||||
let half = size * 0.5;
|
||||
let local_corners = [
|
||||
Vec3::new(-half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, half.y, half.z),
|
||||
Vec3::new(-half.x, half.y, half.z),
|
||||
];
|
||||
let corners = local_corners.map(|point| center + rotation * point);
|
||||
Aabb::from_points(&corners)
|
||||
}
|
||||
|
||||
fn floret_bounds(
|
||||
anchor: Vec3,
|
||||
direction: Vec3,
|
||||
length: f32,
|
||||
root_width: f32,
|
||||
tip_width: f32,
|
||||
thickness: f32,
|
||||
tip_lift: f32,
|
||||
) -> Aabb {
|
||||
let forward = 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 = anchor;
|
||||
let tip_center = anchor + forward * length + Vec3::Z * tip_lift;
|
||||
let z = Vec3::Z * (thickness * 0.5);
|
||||
let r = side * (root_width * 0.5);
|
||||
let t = side * (tip_width * 0.5);
|
||||
Aabb::from_points(&[
|
||||
root_center - r - z,
|
||||
root_center + r - z,
|
||||
root_center - r + z,
|
||||
root_center + r + z,
|
||||
tip_center - t - z,
|
||||
tip_center + t - z,
|
||||
tip_center - t + z,
|
||||
tip_center + t + z,
|
||||
])
|
||||
}
|
||||
905
packages/gamecult-geometry-rs/src/convex.rs
Normal file
905
packages/gamecult-geometry-rs/src/convex.rs
Normal file
|
|
@ -0,0 +1,905 @@
|
|||
#![allow(dead_code)]
|
||||
use bevy_math::{Quat, Vec2, Vec3};
|
||||
|
||||
use crate::{Aabb, MaterialId, PolygonCategory, TriangleMesh};
|
||||
|
||||
const EPSILON: f32 = 1.0e-5;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Plane {
|
||||
pub normal: Vec3,
|
||||
pub distance: f32,
|
||||
}
|
||||
|
||||
impl Plane {
|
||||
pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
|
||||
let normal = normal.normalize_or_zero();
|
||||
Self {
|
||||
normal,
|
||||
distance: -normal.dot(point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signed_distance(self, point: Vec3) -> f32 {
|
||||
self.normal.dot(point) + self.distance
|
||||
}
|
||||
|
||||
pub fn is_coplanar_with(self, other: Self) -> bool {
|
||||
self.normal.dot(other.normal).abs() > 1.0 - EPSILON
|
||||
&& (self.distance - other.distance).abs() <= EPSILON
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ConvexPolygon {
|
||||
pub vertices: Vec<Vec3>,
|
||||
pub normal: Vec3,
|
||||
pub material: MaterialId,
|
||||
pub category: PolygonCategory,
|
||||
pub visible: bool,
|
||||
pub reversed: bool,
|
||||
pub bounds: Aabb,
|
||||
}
|
||||
|
||||
impl ConvexPolygon {
|
||||
pub fn new(vertices: Vec<Vec3>, normal: Vec3, material: MaterialId) -> Self {
|
||||
Self {
|
||||
bounds: Aabb::from_points(&vertices),
|
||||
vertices,
|
||||
normal: normal.normalize_or_zero(),
|
||||
material,
|
||||
category: PolygonCategory::Aligned,
|
||||
visible: true,
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn centroid(&self) -> Vec3 {
|
||||
if self.vertices.is_empty() {
|
||||
return Vec3::ZERO;
|
||||
}
|
||||
self.vertices.iter().copied().sum::<Vec3>() / self.vertices.len() as f32
|
||||
}
|
||||
|
||||
fn with_vertices(&self, vertices: Vec<Vec3>) -> Self {
|
||||
Self {
|
||||
bounds: Aabb::from_points(&vertices),
|
||||
vertices,
|
||||
normal: self.normal,
|
||||
material: self.material,
|
||||
category: self.category,
|
||||
visible: self.visible,
|
||||
reversed: self.reversed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ConvexSolid {
|
||||
pub polygons: Vec<ConvexPolygon>,
|
||||
pub clip_planes: Vec<Plane>,
|
||||
pub material: MaterialId,
|
||||
pub bounds: Aabb,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CategorizedPolygons {
|
||||
pub inside: Vec<ConvexPolygon>,
|
||||
pub outside: Vec<ConvexPolygon>,
|
||||
pub aligned: Vec<ConvexPolygon>,
|
||||
pub reverse_aligned: Vec<ConvexPolygon>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PolygonRouteScratch {
|
||||
pieces: Vec<CategorizationPiece>,
|
||||
inside_pieces: Vec<CategorizationPiece>,
|
||||
cap_points: Vec<Vec3>,
|
||||
}
|
||||
|
||||
impl ConvexSolid {
|
||||
pub fn from_aabb(bounds: Aabb, material: MaterialId) -> Self {
|
||||
Self::box_from_center_size(bounds.center(), bounds.size(), Quat::IDENTITY, material)
|
||||
}
|
||||
|
||||
pub fn box_from_center_size(
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
material: MaterialId,
|
||||
) -> Self {
|
||||
let half = size * 0.5;
|
||||
let local_corners = [
|
||||
Vec3::new(-half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, -half.y, -half.z),
|
||||
Vec3::new(half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, half.y, -half.z),
|
||||
Vec3::new(-half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, -half.y, half.z),
|
||||
Vec3::new(half.x, half.y, half.z),
|
||||
Vec3::new(-half.x, half.y, half.z),
|
||||
];
|
||||
let corners = local_corners.map(|point| center + rotation * point);
|
||||
let faces = [
|
||||
([0, 3, 2, 1], Vec3::NEG_Z),
|
||||
([4, 5, 6, 7], Vec3::Z),
|
||||
([0, 1, 5, 4], Vec3::NEG_Y),
|
||||
([3, 7, 6, 2], Vec3::Y),
|
||||
([0, 4, 7, 3], Vec3::NEG_X),
|
||||
([1, 2, 6, 5], Vec3::X),
|
||||
];
|
||||
|
||||
let mut polygons = Vec::with_capacity(6);
|
||||
let mut clip_planes = Vec::with_capacity(6);
|
||||
for (indices, local_normal) in faces {
|
||||
let normal = rotation * local_normal;
|
||||
let vertices = indices.map(|index| corners[index]).to_vec();
|
||||
clip_planes.push(Plane::from_point_normal(vertices[0], normal));
|
||||
polygons.push(ConvexPolygon::new(vertices, normal, material));
|
||||
}
|
||||
|
||||
Self {
|
||||
bounds: Aabb::from_points(&corners),
|
||||
polygons,
|
||||
clip_planes,
|
||||
material,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subtract_convex(&self, cutter: &Self) -> Vec<Self> {
|
||||
self.clone().subtract_convex_owned(cutter)
|
||||
}
|
||||
|
||||
pub fn subtract_convex_owned(self, cutter: &Self) -> Vec<Self> {
|
||||
let mut fragments = Vec::new();
|
||||
let mut remainder = Some(self);
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
let Some(current) = remainder.take() else {
|
||||
break;
|
||||
};
|
||||
|
||||
match current.split_owned(*plane) {
|
||||
SplitResult::Front(front) => {
|
||||
fragments.push(front);
|
||||
break;
|
||||
}
|
||||
SplitResult::Back(back) => {
|
||||
remainder = Some(back);
|
||||
}
|
||||
SplitResult::Both { front, back } => {
|
||||
fragments.push(front);
|
||||
remainder = Some(back);
|
||||
}
|
||||
SplitResult::Coplanar(solid) => {
|
||||
remainder = Some(solid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragments
|
||||
}
|
||||
|
||||
pub fn intersect_convex(&self, cutter: &Self) -> Option<Self> {
|
||||
self.clone().intersect_convex_owned(cutter)
|
||||
}
|
||||
|
||||
pub fn intersect_convex_owned(self, cutter: &Self) -> Option<Self> {
|
||||
if cutter.contains_solid(&self) {
|
||||
return Some(self);
|
||||
}
|
||||
if self.contains_solid(cutter) {
|
||||
return Some(cutter.clone_with_material(self.material));
|
||||
}
|
||||
|
||||
let mut remainder = self;
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
remainder = match remainder.split_owned(*plane) {
|
||||
SplitResult::Front(_) => return None,
|
||||
SplitResult::Back(back) | SplitResult::Coplanar(back) => back,
|
||||
SplitResult::Both { back, .. } => back,
|
||||
};
|
||||
}
|
||||
|
||||
Some(remainder)
|
||||
}
|
||||
|
||||
pub fn contains_point(&self, point: Vec3) -> bool {
|
||||
self.clip_planes
|
||||
.iter()
|
||||
.all(|plane| plane.signed_distance(point) <= EPSILON)
|
||||
}
|
||||
|
||||
pub fn contains_solid(&self, other: &Self) -> bool {
|
||||
other
|
||||
.polygons
|
||||
.iter()
|
||||
.flat_map(|polygon| polygon.vertices.iter())
|
||||
.all(|point| self.contains_point(*point))
|
||||
}
|
||||
|
||||
pub fn clone_with_material(&self, material: MaterialId) -> Self {
|
||||
let mut solid = self.clone();
|
||||
solid.material = material;
|
||||
for polygon in &mut solid.polygons {
|
||||
polygon.material = material;
|
||||
}
|
||||
solid
|
||||
}
|
||||
|
||||
pub fn append_to_mesh(&self, mesh: &mut TriangleMesh) {
|
||||
for polygon in &self.polygons {
|
||||
if !polygon.visible || polygon.vertices.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let base = polygon.vertices[0];
|
||||
for i in 1..polygon.vertices.len() - 1 {
|
||||
let triangle = if polygon.reversed {
|
||||
[base, polygon.vertices[i + 1], polygon.vertices[i]]
|
||||
} else {
|
||||
[base, polygon.vertices[i], polygon.vertices[i + 1]]
|
||||
};
|
||||
let normal = if polygon.reversed {
|
||||
-polygon.normal
|
||||
} else {
|
||||
polygon.normal
|
||||
};
|
||||
mesh.append_triangle(
|
||||
triangle,
|
||||
normal,
|
||||
[Vec2::ZERO, Vec2::X, Vec2::Y],
|
||||
polygon.material,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn categorize_whole_polygons_against(&mut self, cutter: &Self) {
|
||||
for polygon in &mut self.polygons {
|
||||
if let Some(category) = whole_polygon_category_against(polygon, cutter) {
|
||||
polygon.category = category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn categorize_polygons_against(&self, cutter: &Self) -> CategorizedPolygons {
|
||||
PolygonRouteScratch::default().categorize_against(&self.polygons, cutter)
|
||||
}
|
||||
|
||||
pub fn route_polygons_outside_of(
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &Self,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
let categorized = PolygonRouteScratch::default().categorize_against(&polygons, cutter);
|
||||
categorized.outside
|
||||
}
|
||||
|
||||
pub fn route_polygons_inside_of(
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &Self,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
let categorized = PolygonRouteScratch::default().categorize_against(&polygons, cutter);
|
||||
let mut inside = categorized.inside;
|
||||
inside.extend(categorized.aligned);
|
||||
inside.extend(categorized.reverse_aligned);
|
||||
inside
|
||||
}
|
||||
|
||||
pub fn subtract_convex_routed_polygons(&self, cutter: &Self) -> Vec<ConvexPolygon> {
|
||||
let mut polygons = Vec::new();
|
||||
let source = self.categorize_polygons_against(cutter);
|
||||
polygons.extend(source.outside);
|
||||
|
||||
let cutter_inside_source = cutter.categorize_polygons_against(self);
|
||||
for mut polygon in cutter_inside_source.inside {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
for mut polygon in cutter_inside_source.aligned {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
for mut polygon in cutter_inside_source.reverse_aligned {
|
||||
polygon.reversed = !polygon.reversed;
|
||||
polygons.push(polygon);
|
||||
}
|
||||
|
||||
polygons
|
||||
}
|
||||
|
||||
pub fn append_polygons_to_mesh(polygons: &[ConvexPolygon], mesh: &mut TriangleMesh) {
|
||||
for polygon in polygons {
|
||||
if !polygon.visible || polygon.vertices.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let base = polygon.vertices[0];
|
||||
for i in 1..polygon.vertices.len() - 1 {
|
||||
let triangle = if polygon.reversed {
|
||||
[base, polygon.vertices[i + 1], polygon.vertices[i]]
|
||||
} else {
|
||||
[base, polygon.vertices[i], polygon.vertices[i + 1]]
|
||||
};
|
||||
let normal = if polygon.reversed {
|
||||
-polygon.normal
|
||||
} else {
|
||||
polygon.normal
|
||||
};
|
||||
mesh.append_triangle(
|
||||
triangle,
|
||||
normal,
|
||||
[Vec2::ZERO, Vec2::X, Vec2::Y],
|
||||
polygon.material,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_owned(self, plane: Plane) -> SplitResult {
|
||||
let mut saw_front = false;
|
||||
let mut saw_back = false;
|
||||
for polygon in &self.polygons {
|
||||
for vertex in &polygon.vertices {
|
||||
let distance = plane.signed_distance(*vertex);
|
||||
saw_front |= distance > EPSILON;
|
||||
saw_back |= distance < -EPSILON;
|
||||
}
|
||||
}
|
||||
|
||||
match (saw_front, saw_back) {
|
||||
(true, false) => return SplitResult::Front(self),
|
||||
(false, true) => return SplitResult::Back(self),
|
||||
(false, false) => return SplitResult::Coplanar(self),
|
||||
(true, true) => {}
|
||||
}
|
||||
|
||||
let mut front_polygons = Vec::with_capacity(self.polygons.len() + 1);
|
||||
let mut back_polygons = Vec::with_capacity(self.polygons.len() + 1);
|
||||
let mut cap_points = Vec::with_capacity(self.polygons.len() * 2);
|
||||
|
||||
for polygon in &self.polygons {
|
||||
let front = clip_polygon(&polygon.vertices, plane, KeepSide::Front, &mut cap_points);
|
||||
if front.len() >= 3 {
|
||||
front_polygons.push(polygon.with_vertices(front));
|
||||
}
|
||||
|
||||
let back = clip_polygon(&polygon.vertices, plane, KeepSide::Back, &mut cap_points);
|
||||
if back.len() >= 3 {
|
||||
back_polygons.push(polygon.with_vertices(back));
|
||||
}
|
||||
}
|
||||
|
||||
let cap_points = unique_points_on_plane(cap_points);
|
||||
if cap_points.len() >= 3 {
|
||||
front_polygons.push(make_cap_polygon(&cap_points, -plane.normal, self.material));
|
||||
back_polygons.push(make_cap_polygon(&cap_points, plane.normal, self.material));
|
||||
}
|
||||
|
||||
SplitResult::Both {
|
||||
front: Self {
|
||||
bounds: bounds_from_polygons(&front_polygons),
|
||||
clip_planes: planes_from_polygons(&front_polygons),
|
||||
polygons: front_polygons,
|
||||
material: self.material,
|
||||
},
|
||||
back: Self {
|
||||
bounds: bounds_from_polygons(&back_polygons),
|
||||
clip_planes: planes_from_polygons(&back_polygons),
|
||||
polygons: back_polygons,
|
||||
material: self.material,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PolygonRouteScratch {
|
||||
pub fn categorize_against(
|
||||
&mut self,
|
||||
polygons: &[ConvexPolygon],
|
||||
cutter: &ConvexSolid,
|
||||
) -> CategorizedPolygons {
|
||||
let mut categorized = CategorizedPolygons::default();
|
||||
|
||||
for polygon in polygons {
|
||||
self.pieces.clear();
|
||||
self.pieces.push(CategorizationPiece {
|
||||
polygon: polygon.clone(),
|
||||
aligned_category: None,
|
||||
});
|
||||
|
||||
for plane in &cutter.clip_planes {
|
||||
self.inside_pieces.clear();
|
||||
for piece in self.pieces.drain(..) {
|
||||
classify_piece_against_plane(
|
||||
piece,
|
||||
*plane,
|
||||
&mut self.inside_pieces,
|
||||
&mut categorized,
|
||||
&mut self.cap_points,
|
||||
);
|
||||
}
|
||||
std::mem::swap(&mut self.pieces, &mut self.inside_pieces);
|
||||
}
|
||||
|
||||
for piece in self.pieces.drain(..) {
|
||||
match piece.aligned_category {
|
||||
Some(PolygonCategory::Aligned) => categorized.aligned.push(piece.polygon),
|
||||
Some(PolygonCategory::ReverseAligned) => {
|
||||
categorized.reverse_aligned.push(piece.polygon);
|
||||
}
|
||||
_ => categorized.inside.push(piece.polygon),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
categorized
|
||||
}
|
||||
|
||||
pub fn route_outside_of(
|
||||
&mut self,
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &ConvexSolid,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
self.categorize_against(&polygons, cutter).outside
|
||||
}
|
||||
|
||||
pub fn route_inside_of(
|
||||
&mut self,
|
||||
polygons: Vec<ConvexPolygon>,
|
||||
cutter: &ConvexSolid,
|
||||
) -> Vec<ConvexPolygon> {
|
||||
let categorized = self.categorize_against(&polygons, cutter);
|
||||
let mut inside = categorized.inside;
|
||||
inside.extend(categorized.aligned);
|
||||
inside.extend(categorized.reverse_aligned);
|
||||
inside
|
||||
}
|
||||
}
|
||||
|
||||
enum SplitResult {
|
||||
Front(ConvexSolid),
|
||||
Back(ConvexSolid),
|
||||
Both {
|
||||
front: ConvexSolid,
|
||||
back: ConvexSolid,
|
||||
},
|
||||
Coplanar(ConvexSolid),
|
||||
}
|
||||
|
||||
struct CategorizationPiece {
|
||||
polygon: ConvexPolygon,
|
||||
aligned_category: Option<PolygonCategory>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum KeepSide {
|
||||
Front,
|
||||
Back,
|
||||
}
|
||||
|
||||
fn classify_piece_against_plane(
|
||||
mut piece: CategorizationPiece,
|
||||
plane: Plane,
|
||||
inside_pieces: &mut Vec<CategorizationPiece>,
|
||||
categorized: &mut CategorizedPolygons,
|
||||
cap_points: &mut Vec<Vec3>,
|
||||
) {
|
||||
let piece_plane = piece
|
||||
.polygon
|
||||
.vertices
|
||||
.first()
|
||||
.map(|point| Plane::from_point_normal(*point, piece.polygon.normal));
|
||||
|
||||
if piece_plane.is_some_and(|piece_plane| piece_plane.is_coplanar_with(plane)) {
|
||||
piece.aligned_category = Some(if piece.polygon.normal.dot(plane.normal) >= 0.0 {
|
||||
PolygonCategory::Aligned
|
||||
} else {
|
||||
PolygonCategory::ReverseAligned
|
||||
});
|
||||
inside_pieces.push(piece);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut saw_front = false;
|
||||
let mut saw_back = false;
|
||||
for vertex in &piece.polygon.vertices {
|
||||
let distance = plane.signed_distance(*vertex);
|
||||
saw_front |= distance > EPSILON;
|
||||
saw_back |= distance < -EPSILON;
|
||||
}
|
||||
|
||||
match (saw_front, saw_back) {
|
||||
(true, false) => {
|
||||
let mut polygon = piece.polygon;
|
||||
polygon.category = PolygonCategory::Outside;
|
||||
categorized.outside.push(polygon);
|
||||
}
|
||||
(false, true) | (false, false) => inside_pieces.push(piece),
|
||||
(true, true) => {
|
||||
cap_points.clear();
|
||||
let outside = clip_polygon(&piece.polygon.vertices, plane, KeepSide::Front, cap_points);
|
||||
if outside.len() >= 3 {
|
||||
let mut outside_polygon = piece.polygon.with_vertices(outside);
|
||||
outside_polygon.category = PolygonCategory::Outside;
|
||||
categorized.outside.push(outside_polygon);
|
||||
}
|
||||
|
||||
let inside = clip_polygon(&piece.polygon.vertices, plane, KeepSide::Back, cap_points);
|
||||
if inside.len() >= 3 {
|
||||
inside_pieces.push(CategorizationPiece {
|
||||
polygon: piece.polygon.with_vertices(inside),
|
||||
aligned_category: piece.aligned_category,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_polygon(
|
||||
vertices: &[Vec3],
|
||||
plane: Plane,
|
||||
keep: KeepSide,
|
||||
cap_points: &mut Vec<Vec3>,
|
||||
) -> Vec<Vec3> {
|
||||
let mut output = Vec::with_capacity(vertices.len() + 1);
|
||||
if vertices.is_empty() {
|
||||
return output;
|
||||
}
|
||||
|
||||
let mut previous = *vertices.last().expect("checked non-empty");
|
||||
let mut previous_distance = plane.signed_distance(previous);
|
||||
let mut previous_inside = inside(previous_distance, keep);
|
||||
|
||||
for ¤t in vertices {
|
||||
let current_distance = plane.signed_distance(current);
|
||||
let current_inside = inside(current_distance, keep);
|
||||
|
||||
if current_inside != previous_inside {
|
||||
let t = previous_distance / (previous_distance - current_distance);
|
||||
let intersection = previous + (current - previous) * t;
|
||||
output.push(intersection);
|
||||
cap_points.push(intersection);
|
||||
}
|
||||
|
||||
if current_inside {
|
||||
output.push(current);
|
||||
}
|
||||
|
||||
previous = current;
|
||||
previous_distance = current_distance;
|
||||
previous_inside = current_inside;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn inside(distance: f32, keep: KeepSide) -> bool {
|
||||
match keep {
|
||||
KeepSide::Front => distance >= -EPSILON,
|
||||
KeepSide::Back => distance <= EPSILON,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_cap_polygon(points: &[Vec3], normal: Vec3, material: MaterialId) -> ConvexPolygon {
|
||||
let center = points.iter().copied().sum::<Vec3>() / points.len() as f32;
|
||||
let axis = if normal.z.abs() < 0.9 {
|
||||
Vec3::Z
|
||||
} else {
|
||||
Vec3::Y
|
||||
};
|
||||
let tangent = normal.cross(axis).normalize_or_zero();
|
||||
let bitangent = normal.cross(tangent).normalize_or_zero();
|
||||
let mut vertices = points.to_vec();
|
||||
|
||||
vertices.sort_by(|a, b| {
|
||||
let da = *a - center;
|
||||
let db = *b - center;
|
||||
let aa = da.dot(bitangent).atan2(da.dot(tangent));
|
||||
let ab = db.dot(bitangent).atan2(db.dot(tangent));
|
||||
aa.total_cmp(&ab)
|
||||
});
|
||||
|
||||
if polygon_normal(&vertices).dot(normal) < 0.0 {
|
||||
vertices.reverse();
|
||||
}
|
||||
|
||||
ConvexPolygon::new(vertices, normal, material)
|
||||
}
|
||||
|
||||
fn unique_points_on_plane(points: Vec<Vec3>) -> Vec<Vec3> {
|
||||
let mut unique = Vec::new();
|
||||
'outer: for point in points {
|
||||
for seen in &unique {
|
||||
let delta: Vec3 = point - *seen;
|
||||
if delta.length_squared() < EPSILON * EPSILON {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
unique.push(point);
|
||||
}
|
||||
unique
|
||||
}
|
||||
|
||||
fn planes_from_polygons(polygons: &[ConvexPolygon]) -> Vec<Plane> {
|
||||
polygons
|
||||
.iter()
|
||||
.filter_map(|polygon| {
|
||||
polygon
|
||||
.vertices
|
||||
.first()
|
||||
.map(|point| Plane::from_point_normal(*point, polygon.normal))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bounds_from_polygons(polygons: &[ConvexPolygon]) -> Aabb {
|
||||
polygons.iter().fold(Aabb::empty(), |bounds, polygon| {
|
||||
bounds.union(polygon.bounds)
|
||||
})
|
||||
}
|
||||
|
||||
fn whole_polygon_category_against(
|
||||
polygon: &ConvexPolygon,
|
||||
cutter: &ConvexSolid,
|
||||
) -> Option<PolygonCategory> {
|
||||
let polygon_plane = polygon
|
||||
.vertices
|
||||
.first()
|
||||
.map(|point| Plane::from_point_normal(*point, polygon.normal))?;
|
||||
|
||||
for cutter_plane in &cutter.clip_planes {
|
||||
if polygon_plane.is_coplanar_with(*cutter_plane)
|
||||
&& polygon
|
||||
.vertices
|
||||
.iter()
|
||||
.all(|vertex| cutter_plane.signed_distance(*vertex).abs() <= EPSILON)
|
||||
{
|
||||
return Some(if polygon.normal.dot(cutter_plane.normal) >= 0.0 {
|
||||
PolygonCategory::Aligned
|
||||
} else {
|
||||
PolygonCategory::ReverseAligned
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut fully_inside = true;
|
||||
for cutter_plane in &cutter.clip_planes {
|
||||
let mut all_outside_this_plane = true;
|
||||
for vertex in &polygon.vertices {
|
||||
let distance = cutter_plane.signed_distance(*vertex);
|
||||
if distance > EPSILON {
|
||||
fully_inside = false;
|
||||
} else {
|
||||
all_outside_this_plane = false;
|
||||
}
|
||||
}
|
||||
if all_outside_this_plane {
|
||||
return Some(PolygonCategory::Outside);
|
||||
}
|
||||
}
|
||||
|
||||
fully_inside.then_some(PolygonCategory::Inside)
|
||||
}
|
||||
|
||||
fn polygon_normal(vertices: &[Vec3]) -> Vec3 {
|
||||
if vertices.len() < 3 {
|
||||
return Vec3::ZERO;
|
||||
}
|
||||
(vertices[1] - vertices[0])
|
||||
.cross(vertices[2] - vertices[0])
|
||||
.normalize_or_zero()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_box_parity(solid: &ConvexSolid, material: MaterialId) {
|
||||
assert_eq!(solid.polygons.len(), 6);
|
||||
assert_eq!(solid.clip_planes.len(), 6);
|
||||
assert!(
|
||||
solid
|
||||
.polygons
|
||||
.iter()
|
||||
.all(|polygon| polygon.vertices.len() == 4)
|
||||
);
|
||||
assert!(
|
||||
solid
|
||||
.polygons
|
||||
.iter()
|
||||
.all(|polygon| polygon.material == material)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_box_brush_from_planes_has_six_quad_polygons() {
|
||||
let solid = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(9),
|
||||
);
|
||||
|
||||
assert_box_parity(&solid, MaterialId(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtract_center_box_emits_six_convex_fragments() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let fragments = source.subtract_convex(&cutter);
|
||||
assert_eq!(fragments.len(), 6);
|
||||
assert!(
|
||||
fragments
|
||||
.iter()
|
||||
.all(|fragment| !fragment.polygons.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_overlapping_boxes_emits_single_common_solid() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::X,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let common = source.intersect_convex(&cutter).expect("intersection");
|
||||
|
||||
assert!(common.bounds.is_valid());
|
||||
assert_eq!(common.polygons.len(), 6);
|
||||
assert!(common.bounds.contains_point(Vec3::ZERO));
|
||||
assert!(common.bounds.contains_point(Vec3::new(2.0, 0.0, 0.0)));
|
||||
assert!(!common.bounds.contains_point(Vec3::new(-2.0, 0.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn categorize_crossing_coplanar_face_splits_visible_categories() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(2.0, 2.0, 2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let categorized = source.categorize_polygons_against(&cutter);
|
||||
|
||||
assert_eq!(categorized.aligned.len(), 1);
|
||||
assert!(categorized.outside.len() > 6);
|
||||
assert!(categorized.inside.is_empty());
|
||||
assert!(categorized.reverse_aligned.is_empty());
|
||||
assert!(
|
||||
categorized
|
||||
.aligned
|
||||
.iter()
|
||||
.all(|polygon| polygon.category == PolygonCategory::Aligned)
|
||||
);
|
||||
assert!(
|
||||
categorized
|
||||
.outside
|
||||
.iter()
|
||||
.all(|polygon| polygon.category == PolygonCategory::Outside)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtract_rotated_box_creates_non_axis_aligned_faces() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::new(2.0, 5.0, 2.0),
|
||||
Quat::from_rotation_z(std::f32::consts::FRAC_PI_4),
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let fragments = source.subtract_convex(&cutter);
|
||||
assert!(fragments.len() > 2);
|
||||
assert!(fragments.iter().any(|fragment| {
|
||||
fragment
|
||||
.polygons
|
||||
.iter()
|
||||
.any(|polygon| polygon.normal.x.abs() > 0.01 && polygon.normal.y.abs() > 0.01)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_subtraction_emits_source_shell_and_reversed_cutter_shell() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let routed = source.subtract_convex_routed_polygons(&cutter);
|
||||
|
||||
assert_eq!(routed.len(), 24);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(1))
|
||||
.count(),
|
||||
18
|
||||
);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(0))
|
||||
.count(),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
routed
|
||||
.iter()
|
||||
.filter(|polygon| polygon.material == MaterialId(0) && polygon.reversed)
|
||||
.count(),
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routed_subtraction_clips_crossing_cutter_surfaces_to_source() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::new(1.5, 0.0, 0.0),
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let routed = source.subtract_convex_routed_polygons(&cutter);
|
||||
|
||||
assert!(!routed.is_empty());
|
||||
assert!(
|
||||
routed
|
||||
.iter()
|
||||
.flat_map(|polygon| polygon.vertices.iter())
|
||||
.all(|vertex| source.bounds.contains_point(*vertex))
|
||||
);
|
||||
assert!(
|
||||
routed
|
||||
.iter()
|
||||
.any(|polygon| polygon.material == MaterialId(0) && polygon.reversed)
|
||||
);
|
||||
}
|
||||
}
|
||||
1560
packages/gamecult-geometry-rs/src/domain.rs
Normal file
1560
packages/gamecult-geometry-rs/src/domain.rs
Normal file
File diff suppressed because it is too large
Load diff
164
packages/gamecult-geometry-rs/src/dsl.rs
Normal file
164
packages/gamecult-geometry-rs/src/dsl.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
#![allow(dead_code)]
|
||||
use bevy_math::{Quat, 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<String>,
|
||||
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<String>, center: Vec3, size: Vec3) -> BrushId {
|
||||
self.assembler
|
||||
.cut_box(name, Aabb::from_center_size(center, size))
|
||||
}
|
||||
|
||||
pub fn solid_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
material: MaterialId,
|
||||
) -> BrushId {
|
||||
self.assembler
|
||||
.solid_oriented_box(name, center, size, rotation, material)
|
||||
}
|
||||
|
||||
pub fn cut_oriented_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Vec3,
|
||||
size: Vec3,
|
||||
rotation: Quat,
|
||||
) -> BrushId {
|
||||
self.assembler
|
||||
.cut_oriented_box(name, center, size, rotation)
|
||||
}
|
||||
|
||||
pub fn cylinder_z(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
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<String>, 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<String>, 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());
|
||||
}
|
||||
}
|
||||
321
packages/gamecult-geometry-rs/src/frontier.rs
Normal file
321
packages/gamecult-geometry-rs/src/frontier.rs
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
#![allow(dead_code)]
|
||||
use crate::{Brush, BrushOp};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DemandPair {
|
||||
pub source_index: usize,
|
||||
pub operator_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct DemandFrontier {
|
||||
pub pairs: Vec<DemandPair>,
|
||||
pub rejected_pairs: usize,
|
||||
pub operator_brushes: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct DirtyDemandFrontier {
|
||||
pub first_dirty_index: Option<usize>,
|
||||
pub pairs: Vec<DemandPair>,
|
||||
pub rejected_pairs: usize,
|
||||
pub operator_brushes: usize,
|
||||
pub skipped_prefix_pairs: usize,
|
||||
}
|
||||
|
||||
impl DemandFrontier {
|
||||
pub fn from_ordered_brushes(brushes: &[Brush]) -> Self {
|
||||
let mut frontier = Self::default();
|
||||
let mut active_sources = Vec::<usize>::new();
|
||||
|
||||
for (operator_index, brush) in brushes.iter().enumerate() {
|
||||
match brush.op {
|
||||
BrushOp::Add => active_sources.push(operator_index),
|
||||
BrushOp::Subtract => {
|
||||
frontier.operator_brushes += 1;
|
||||
frontier.collect_pairs(brushes, &active_sources, operator_index);
|
||||
}
|
||||
BrushOp::Intersect => {
|
||||
frontier.operator_brushes += 1;
|
||||
let retained = frontier.collect_pairs(brushes, &active_sources, operator_index);
|
||||
active_sources = retained;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frontier
|
||||
}
|
||||
|
||||
pub fn candidate_pairs(&self) -> usize {
|
||||
self.pairs.len()
|
||||
}
|
||||
|
||||
fn collect_pairs(
|
||||
&mut self,
|
||||
brushes: &[Brush],
|
||||
active_sources: &[usize],
|
||||
operator_index: usize,
|
||||
) -> Vec<usize> {
|
||||
let operator_bounds = brushes[operator_index].bounds();
|
||||
let mut retained = Vec::new();
|
||||
|
||||
for &source_index in active_sources {
|
||||
if brushes[source_index].bounds().intersects(operator_bounds) {
|
||||
self.pairs.push(DemandPair {
|
||||
source_index,
|
||||
operator_index,
|
||||
});
|
||||
retained.push(source_index);
|
||||
} else {
|
||||
self.rejected_pairs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
retained
|
||||
}
|
||||
}
|
||||
|
||||
impl DirtyDemandFrontier {
|
||||
pub fn from_dirty_indices(
|
||||
brushes: &[Brush],
|
||||
dirty_indices: impl IntoIterator<Item = usize>,
|
||||
) -> Self {
|
||||
let first_dirty_index = dirty_indices
|
||||
.into_iter()
|
||||
.filter(|index| *index < brushes.len())
|
||||
.min();
|
||||
let Some(first_dirty_index) = first_dirty_index else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let mut frontier = Self {
|
||||
first_dirty_index: Some(first_dirty_index),
|
||||
..Self::default()
|
||||
};
|
||||
let mut active_sources = Vec::<usize>::new();
|
||||
|
||||
for (operator_index, brush) in brushes.iter().enumerate() {
|
||||
match brush.op {
|
||||
BrushOp::Add => active_sources.push(operator_index),
|
||||
BrushOp::Subtract => {
|
||||
frontier.operator_brushes += usize::from(operator_index >= first_dirty_index);
|
||||
frontier.collect_pairs(
|
||||
brushes,
|
||||
&active_sources,
|
||||
operator_index,
|
||||
first_dirty_index,
|
||||
);
|
||||
}
|
||||
BrushOp::Intersect => {
|
||||
frontier.operator_brushes += usize::from(operator_index >= first_dirty_index);
|
||||
let retained = frontier.collect_pairs(
|
||||
brushes,
|
||||
&active_sources,
|
||||
operator_index,
|
||||
first_dirty_index,
|
||||
);
|
||||
active_sources = retained;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frontier
|
||||
}
|
||||
|
||||
pub fn candidate_pairs(&self) -> usize {
|
||||
self.pairs.len()
|
||||
}
|
||||
|
||||
fn collect_pairs(
|
||||
&mut self,
|
||||
brushes: &[Brush],
|
||||
active_sources: &[usize],
|
||||
operator_index: usize,
|
||||
first_dirty_index: usize,
|
||||
) -> Vec<usize> {
|
||||
let operator_bounds = brushes[operator_index].bounds();
|
||||
let mut retained = Vec::new();
|
||||
|
||||
for &source_index in active_sources {
|
||||
if brushes[source_index].bounds().intersects(operator_bounds) {
|
||||
if operator_index >= first_dirty_index {
|
||||
self.pairs.push(DemandPair {
|
||||
source_index,
|
||||
operator_index,
|
||||
});
|
||||
} else {
|
||||
self.skipped_prefix_pairs += 1;
|
||||
}
|
||||
retained.push(source_index);
|
||||
} else if operator_index >= first_dirty_index {
|
||||
self.rejected_pairs += 1;
|
||||
} else {
|
||||
self.skipped_prefix_pairs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
retained
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_math::{Quat, Vec3};
|
||||
|
||||
use super::*;
|
||||
use crate::{Aabb, Brush, MaterialId, Primitive};
|
||||
|
||||
fn brush(name: &str, op: BrushOp, bounds: Aabb) -> Brush {
|
||||
Brush::new(
|
||||
crate::BrushId(0),
|
||||
name,
|
||||
op,
|
||||
Primitive::Box { bounds },
|
||||
MaterialId(1),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontier_keeps_only_touching_ordered_pairs() {
|
||||
let brushes = vec![
|
||||
brush(
|
||||
"source",
|
||||
BrushOp::Add,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
),
|
||||
brush(
|
||||
"far cutter",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::splat(20.0), Vec3::ONE),
|
||||
),
|
||||
brush(
|
||||
"near cutter",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::ONE),
|
||||
),
|
||||
];
|
||||
|
||||
let frontier = DemandFrontier::from_ordered_brushes(&brushes);
|
||||
|
||||
assert_eq!(frontier.operator_brushes, 2);
|
||||
assert_eq!(frontier.rejected_pairs, 1);
|
||||
assert_eq!(
|
||||
frontier.pairs,
|
||||
vec![DemandPair {
|
||||
source_index: 0,
|
||||
operator_index: 2,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_frontier_reduces_active_sources() {
|
||||
let brushes = vec![
|
||||
brush(
|
||||
"a",
|
||||
BrushOp::Add,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
),
|
||||
brush(
|
||||
"b",
|
||||
BrushOp::Add,
|
||||
Aabb::from_center_size(Vec3::new(20.0, 0.0, 0.0), Vec3::splat(2.0)),
|
||||
),
|
||||
Brush::new(
|
||||
crate::BrushId(2),
|
||||
"common",
|
||||
BrushOp::Intersect,
|
||||
Primitive::OrientedBox {
|
||||
center: Vec3::ZERO,
|
||||
size: Vec3::splat(3.0),
|
||||
rotation: Quat::IDENTITY,
|
||||
},
|
||||
MaterialId(1),
|
||||
),
|
||||
brush(
|
||||
"cut",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::ONE),
|
||||
),
|
||||
];
|
||||
|
||||
let frontier = DemandFrontier::from_ordered_brushes(&brushes);
|
||||
|
||||
assert_eq!(frontier.operator_brushes, 2);
|
||||
assert_eq!(frontier.rejected_pairs, 1);
|
||||
assert_eq!(
|
||||
frontier.pairs,
|
||||
vec![
|
||||
DemandPair {
|
||||
source_index: 0,
|
||||
operator_index: 2,
|
||||
},
|
||||
DemandPair {
|
||||
source_index: 0,
|
||||
operator_index: 3,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_frontier_preserves_prefix_before_first_dirty_brush() {
|
||||
let brushes = vec![
|
||||
brush(
|
||||
"source",
|
||||
BrushOp::Add,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(8.0)),
|
||||
),
|
||||
brush(
|
||||
"early",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::new(-2.0, 0.0, 0.0), Vec3::splat(2.0)),
|
||||
),
|
||||
brush(
|
||||
"dirty",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
),
|
||||
brush(
|
||||
"later",
|
||||
BrushOp::Subtract,
|
||||
Aabb::from_center_size(Vec3::new(2.0, 0.0, 0.0), Vec3::splat(2.0)),
|
||||
),
|
||||
];
|
||||
|
||||
let frontier = DirtyDemandFrontier::from_dirty_indices(&brushes, [2]);
|
||||
|
||||
assert_eq!(frontier.first_dirty_index, Some(2));
|
||||
assert_eq!(frontier.operator_brushes, 2);
|
||||
assert_eq!(frontier.skipped_prefix_pairs, 1);
|
||||
assert_eq!(frontier.candidate_pairs(), 2);
|
||||
assert_eq!(
|
||||
frontier.pairs,
|
||||
vec![
|
||||
DemandPair {
|
||||
source_index: 0,
|
||||
operator_index: 2,
|
||||
},
|
||||
DemandPair {
|
||||
source_index: 0,
|
||||
operator_index: 3,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_frontier_ignores_out_of_range_dirty_indices() {
|
||||
let brushes = vec![brush(
|
||||
"source",
|
||||
BrushOp::Add,
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
)];
|
||||
|
||||
let frontier = DirtyDemandFrontier::from_dirty_indices(&brushes, [99]);
|
||||
|
||||
assert_eq!(frontier.first_dirty_index, None);
|
||||
assert_eq!(frontier.candidate_pairs(), 0);
|
||||
assert_eq!(frontier.operator_brushes, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,50 @@
|
|||
//! commit `8f070f4`, then rewritten around typed vector values. This crate has
|
||||
//! no runtime or build dependency on VibeGeometry or `vg-csg`.
|
||||
|
||||
mod assembler;
|
||||
mod brush;
|
||||
mod convex;
|
||||
mod domain;
|
||||
mod dsl;
|
||||
mod frontier;
|
||||
mod mesh;
|
||||
mod primitives;
|
||||
#[cfg(test)]
|
||||
mod realtime_csg_parity;
|
||||
mod tree;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use assembler::{Assembler, BuildOutput, BuildReport, BuildWarning};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use brush::{Aabb, Brush, BrushId, BrushOp, MaterialId, PolygonCategory, Primitive};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use convex::{
|
||||
CategorizedPolygons, ConvexPolygon, ConvexSolid, Plane, PolygonRouteScratch,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use domain::{
|
||||
ClaimLoweringTarget, ContributionManifest, ContributionRow, CsgClaimLowering, DomainChunkBuild,
|
||||
DomainFrame, DomainKey, DomainKind, DomainNode, DomainNodeSpec, DomainQuery, DomainSummary,
|
||||
FeatureClaim, FeatureClaimKind, FeatureClaimSpec, FeatureLoweringPolicy, FieldEncoding,
|
||||
FieldLayer, SelectedCut, SelectedCutManifest, TriangleChunk, TriangleChunkManifest,
|
||||
build_domain_chunks, lower_feature_claims_to_csg_tree, lower_selected_cut,
|
||||
lower_selected_cut_chunks, ragnarok_column_fixture, ragnarok_column_spec, select_domain_cut,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use dsl::LevelDsl;
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use frontier::{DemandFrontier, DemandPair, DirtyDemandFrontier};
|
||||
pub(crate) use mesh::TriangleMesh;
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use primitives::{
|
||||
DomeCapZSpec, FloretArmSpec, append_cylinder_z, append_dome_cap_z, append_floret_arm,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use tree::{
|
||||
CsgBranchOp, CsgNode, CsgNodeId, CsgOperationType, CsgTree, CsgTreeArena, CsgTreeBranch,
|
||||
CsgTreeBrush,
|
||||
};
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
|
@ -40,6 +84,54 @@ pub trait GeometryDocument: Serialize + DeserializeOwned + Sized {
|
|||
}
|
||||
}
|
||||
|
||||
/// Geometry-owned CSG facade. The implementation uses an internal math kernel,
|
||||
/// but no `bevy_math` type crosses this public boundary.
|
||||
#[derive(Default)]
|
||||
pub struct GeometryBrushAssembler(Assembler);
|
||||
|
||||
impl GeometryBrushAssembler {
|
||||
pub fn new() -> Self {
|
||||
Self(Assembler::new())
|
||||
}
|
||||
pub fn add_box(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
center: Float3,
|
||||
size: Float3,
|
||||
material: u32,
|
||||
) {
|
||||
self.0
|
||||
.solid_box(name, internal_aabb(center, size), MaterialId(material));
|
||||
}
|
||||
pub fn subtract_box(&mut self, name: impl Into<String>, center: Float3, size: Float3) {
|
||||
self.0.cut_box(name, internal_aabb(center, size));
|
||||
}
|
||||
pub fn build(&self) -> GeometryTriangleMesh {
|
||||
internal_mesh(&self.0.build().mesh)
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_aabb(center: Float3, size: Float3) -> Aabb {
|
||||
use bevy_math::Vec3;
|
||||
Aabb::from_center_size(
|
||||
Vec3::new(center.0, center.1, center.2),
|
||||
Vec3::new(size.0, size.1, size.2),
|
||||
)
|
||||
}
|
||||
|
||||
fn internal_mesh(mesh: &TriangleMesh) -> GeometryTriangleMesh {
|
||||
GeometryTriangleMesh(
|
||||
mesh.positions
|
||||
.iter()
|
||||
.map(|v| Float3(v.x, v.y, v.z))
|
||||
.collect(),
|
||||
mesh.normals.iter().map(|v| Float3(v.x, v.y, v.z)).collect(),
|
||||
mesh.uvs.iter().map(|v| Float2(v.x, v.y)).collect(),
|
||||
mesh.indices.clone(),
|
||||
mesh.triangle_materials.iter().map(|v| v.0).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GeometryDomainDocument(
|
||||
pub String,
|
||||
|
|
@ -463,6 +555,31 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mined_csg_performance_fixture_keeps_large_distant_cutters_bounded() {
|
||||
use bevy_math::Vec3;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let mut assembler = Assembler::new();
|
||||
assembler.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
for index in 0..512 {
|
||||
assembler.cut_box(
|
||||
format!("distant-{index}"),
|
||||
Aabb::from_center_size(Vec3::new(100.0 + index as f32 * 2.0, 0.0, 0.0), Vec3::ONE),
|
||||
);
|
||||
}
|
||||
let started = Instant::now();
|
||||
let output = assembler.build();
|
||||
assert_eq!(output.mesh.triangle_count(), 12);
|
||||
assert_eq!(output.report.candidate_pairs, 0);
|
||||
assert_eq!(output.report.rejected_pairs, 512);
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_v2_messagepack_and_record_key_witnesses() {
|
||||
let domain = domain();
|
||||
|
|
|
|||
103
packages/gamecult-geometry-rs/src/mesh.rs
Normal file
103
packages/gamecult-geometry-rs/src/mesh.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#![allow(dead_code)]
|
||||
use bevy_math::{Vec2, Vec3};
|
||||
|
||||
use crate::{Aabb, MaterialId};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TriangleMesh {
|
||||
pub positions: Vec<Vec3>,
|
||||
pub normals: Vec<Vec3>,
|
||||
pub uvs: Vec<Vec2>,
|
||||
pub indices: Vec<u32>,
|
||||
pub triangle_materials: Vec<MaterialId>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
173
packages/gamecult-geometry-rs/src/primitives.rs
Normal file
173
packages/gamecult-geometry-rs/src/primitives.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#![allow(dead_code)]
|
||||
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
|
||||
}
|
||||
166
packages/gamecult-geometry-rs/src/realtime_csg_parity.rs
Normal file
166
packages/gamecult-geometry-rs/src/realtime_csg_parity.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
use crate::{
|
||||
Aabb, Assembler, ConvexSolid, CsgBranchOp, CsgOperationType, CsgTreeArena, MaterialId,
|
||||
PolygonCategory, Primitive,
|
||||
};
|
||||
use bevy_math::{Quat, Vec3};
|
||||
|
||||
#[test]
|
||||
fn parity_polygon_category_vocabulary_matches_public_demo() {
|
||||
let categories = [
|
||||
PolygonCategory::Inside,
|
||||
PolygonCategory::Outside,
|
||||
PolygonCategory::Aligned,
|
||||
PolygonCategory::ReverseAligned,
|
||||
];
|
||||
|
||||
assert_eq!(categories.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_operation_type_ordinals_match_public_api() {
|
||||
assert_eq!(CsgOperationType::Additive as u8, 0);
|
||||
assert_eq!(CsgOperationType::Subtractive as u8, 1);
|
||||
assert_eq!(CsgOperationType::Intersecting as u8, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_demo_branch_vocabulary_matches_public_demo() {
|
||||
let branches = [
|
||||
CsgBranchOp::Addition,
|
||||
CsgBranchOp::Subtraction,
|
||||
CsgBranchOp::Common,
|
||||
];
|
||||
|
||||
assert_eq!(branches.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_tree_api_exposes_brush_branch_tree_handles() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let brush = arena.generate_brush(
|
||||
"brush",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::ONE),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let branch = arena.generate_branch("branch", CsgBranchOp::Addition, [brush.node]);
|
||||
let tree = arena.generate_tree(branch.node);
|
||||
|
||||
assert_eq!(arena.brush_count(), 1);
|
||||
assert_eq!(arena.branch_count(), 1);
|
||||
assert_eq!(arena.child_nodes(branch).expect("branch"), &[brush.node]);
|
||||
assert_eq!(tree.root, branch.node);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_box_brush_observable_counts_match_realtime_csg_demo() {
|
||||
let solid = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(3),
|
||||
);
|
||||
|
||||
assert_eq!(solid.clip_planes.len(), 6);
|
||||
assert_eq!(solid.polygons.len(), 6);
|
||||
assert_eq!(
|
||||
solid
|
||||
.polygons
|
||||
.iter()
|
||||
.map(|polygon| polygon.vertices.len())
|
||||
.sum::<usize>(),
|
||||
24
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_center_box_subtraction_has_six_surviving_regions() {
|
||||
let mut asm = Assembler::new();
|
||||
asm.solid_box(
|
||||
"source",
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
MaterialId(1),
|
||||
);
|
||||
asm.cut_box("void", Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)));
|
||||
|
||||
let output = asm.build();
|
||||
assert_eq!(output.report.emitted_convex_fragments, 6);
|
||||
assert_eq!(output.report.warnings.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_identical_box_polygons_classify_as_aligned() {
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
let mut source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
|
||||
source.categorize_whole_polygons_against(&cutter);
|
||||
|
||||
assert_eq!(source.polygons.len(), 6);
|
||||
assert!(
|
||||
source
|
||||
.polygons
|
||||
.iter()
|
||||
.all(|polygon| polygon.category == PolygonCategory::Aligned)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_outer_box_polygons_classify_as_outside_inner_box() {
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
let mut source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
|
||||
source.categorize_whole_polygons_against(&cutter);
|
||||
|
||||
assert_eq!(source.polygons.len(), 6);
|
||||
assert!(
|
||||
source
|
||||
.polygons
|
||||
.iter()
|
||||
.all(|polygon| polygon.category == PolygonCategory::Outside)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_crossing_polygons_split_before_final_category() {
|
||||
let source = ConvexSolid::box_from_center_size(
|
||||
Vec3::ZERO,
|
||||
Vec3::splat(4.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(1),
|
||||
);
|
||||
let cutter = ConvexSolid::box_from_center_size(
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
Vec3::new(2.0, 2.0, 2.0),
|
||||
Quat::IDENTITY,
|
||||
MaterialId(0),
|
||||
);
|
||||
|
||||
let categorized = source.categorize_polygons_against(&cutter);
|
||||
|
||||
assert_eq!(categorized.aligned.len(), 1);
|
||||
assert!(categorized.outside.len() > source.polygons.len());
|
||||
assert_eq!(categorized.inside.len(), 0);
|
||||
assert_eq!(categorized.reverse_aligned.len(), 0);
|
||||
}
|
||||
402
packages/gamecult-geometry-rs/src/tree.rs
Normal file
402
packages/gamecult-geometry-rs/src/tree.rs
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
#![allow(dead_code)]
|
||||
use crate::{Aabb, Assembler, Brush, BrushId, BrushOp, MaterialId, Primitive};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgNodeId(pub u32);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTreeBrush {
|
||||
pub node: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTreeBranch {
|
||||
pub node: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CsgTree {
|
||||
pub root: CsgNodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CsgOperationType {
|
||||
Additive = 0,
|
||||
Subtractive = 1,
|
||||
Intersecting = 2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CsgBranchOp {
|
||||
Addition,
|
||||
Subtraction,
|
||||
Common,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CsgNode {
|
||||
Brush(Brush),
|
||||
Branch {
|
||||
name: String,
|
||||
op: CsgBranchOp,
|
||||
children: Vec<CsgNodeId>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CsgTreeArena {
|
||||
nodes: Vec<CsgNode>,
|
||||
}
|
||||
|
||||
impl CsgTreeArena {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn nodes(&self) -> &[CsgNode] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
pub fn node(&self, id: CsgNodeId) -> Option<&CsgNode> {
|
||||
self.nodes.get(id.0 as usize)
|
||||
}
|
||||
|
||||
pub fn node_mut(&mut self, id: CsgNodeId) -> Option<&mut CsgNode> {
|
||||
self.nodes.get_mut(id.0 as usize)
|
||||
}
|
||||
|
||||
pub fn brush_count(&self) -> usize {
|
||||
self.nodes
|
||||
.iter()
|
||||
.filter(|node| matches!(node, CsgNode::Brush(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn branch_count(&self) -> usize {
|
||||
self.nodes
|
||||
.iter()
|
||||
.filter(|node| matches!(node, CsgNode::Branch { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn generate_brush(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
operation: CsgOperationType,
|
||||
primitive: Primitive,
|
||||
material: MaterialId,
|
||||
) -> CsgTreeBrush {
|
||||
let id = CsgNodeId(self.nodes.len() as u32);
|
||||
let brush = Brush::new(
|
||||
BrushId(id.0),
|
||||
name,
|
||||
operation.into_brush_op(),
|
||||
primitive,
|
||||
material,
|
||||
);
|
||||
self.nodes.push(CsgNode::Brush(brush));
|
||||
CsgTreeBrush { node: id }
|
||||
}
|
||||
|
||||
pub fn generate_branch(
|
||||
&mut self,
|
||||
name: impl Into<String>,
|
||||
op: CsgBranchOp,
|
||||
children: impl IntoIterator<Item = CsgNodeId>,
|
||||
) -> CsgTreeBranch {
|
||||
let id = CsgNodeId(self.nodes.len() as u32);
|
||||
self.nodes.push(CsgNode::Branch {
|
||||
name: name.into(),
|
||||
op,
|
||||
children: children.into_iter().collect(),
|
||||
});
|
||||
CsgTreeBranch { node: id }
|
||||
}
|
||||
|
||||
pub fn generate_tree(&self, root: CsgNodeId) -> CsgTree {
|
||||
CsgTree { root }
|
||||
}
|
||||
|
||||
pub fn set_brush_operation_type(
|
||||
&mut self,
|
||||
brush: CsgTreeBrush,
|
||||
operation: CsgOperationType,
|
||||
) -> bool {
|
||||
let Some(CsgNode::Brush(node)) = self.node_mut(brush.node) else {
|
||||
return false;
|
||||
};
|
||||
node.op = operation.into_brush_op();
|
||||
node.set_dirty();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_branch_operation_type(&mut self, branch: CsgTreeBranch, op: CsgBranchOp) -> bool {
|
||||
let Some(CsgNode::Branch { op: current_op, .. }) = self.node_mut(branch.node) else {
|
||||
return false;
|
||||
};
|
||||
*current_op = op;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_child_nodes(
|
||||
&mut self,
|
||||
branch: CsgTreeBranch,
|
||||
children: impl IntoIterator<Item = CsgNodeId>,
|
||||
) -> bool {
|
||||
let Some(CsgNode::Branch {
|
||||
children: current_children,
|
||||
..
|
||||
}) = self.node_mut(branch.node)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
*current_children = children.into_iter().collect();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn child_nodes(&self, branch: CsgTreeBranch) -> Option<&[CsgNodeId]> {
|
||||
let Some(CsgNode::Branch { children, .. }) = self.node(branch.node) else {
|
||||
return None;
|
||||
};
|
||||
Some(children)
|
||||
}
|
||||
|
||||
pub fn node_bounds(&self, id: CsgNodeId) -> Option<Aabb> {
|
||||
match self.node(id)? {
|
||||
CsgNode::Brush(brush) => Some(brush.bounds()),
|
||||
CsgNode::Branch { op, children, .. } => self.branch_bounds(*op, children),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_tree_to_assembler(&self, tree: CsgTree) -> Assembler {
|
||||
let mut assembler = Assembler::new();
|
||||
self.compile_node(tree.root, &mut assembler, None);
|
||||
assembler
|
||||
}
|
||||
|
||||
fn compile_node(
|
||||
&self,
|
||||
id: CsgNodeId,
|
||||
assembler: &mut Assembler,
|
||||
operation_override: Option<CsgOperationType>,
|
||||
) {
|
||||
let Some(node) = self.node(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match node {
|
||||
CsgNode::Brush(brush) => {
|
||||
let op = operation_override
|
||||
.map(CsgOperationType::into_brush_op)
|
||||
.unwrap_or(brush.op);
|
||||
assembler.add_brush(
|
||||
brush.name.clone(),
|
||||
op,
|
||||
brush.primitive.clone(),
|
||||
brush.material,
|
||||
);
|
||||
}
|
||||
CsgNode::Branch { op, children, .. } => match op {
|
||||
CsgBranchOp::Addition => {
|
||||
for child in children {
|
||||
self.compile_node(*child, assembler, operation_override);
|
||||
}
|
||||
}
|
||||
CsgBranchOp::Subtraction => {
|
||||
if let Some((first, rest)) = children.split_first() {
|
||||
self.compile_node(*first, assembler, operation_override);
|
||||
for child in rest {
|
||||
self.compile_node(
|
||||
*child,
|
||||
assembler,
|
||||
Some(CsgOperationType::Subtractive),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
CsgBranchOp::Common => {
|
||||
if let Some((first, rest)) = children.split_first() {
|
||||
self.compile_node(*first, assembler, operation_override);
|
||||
for child in rest {
|
||||
self.compile_node(
|
||||
*child,
|
||||
assembler,
|
||||
Some(CsgOperationType::Intersecting),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_bounds(&self, op: CsgBranchOp, children: &[CsgNodeId]) -> Option<Aabb> {
|
||||
let mut child_bounds = children.iter().filter_map(|child| self.node_bounds(*child));
|
||||
match op {
|
||||
CsgBranchOp::Addition => child_bounds.reduce(Aabb::union),
|
||||
CsgBranchOp::Subtraction => child_bounds.next(),
|
||||
CsgBranchOp::Common => {
|
||||
let first = child_bounds.next()?;
|
||||
child_bounds.try_fold(first, |bounds, child| bounds.intersection(child))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CsgOperationType {
|
||||
pub fn into_brush_op(self) -> BrushOp {
|
||||
match self {
|
||||
Self::Additive => BrushOp::Add,
|
||||
Self::Subtractive => BrushOp::Subtract,
|
||||
Self::Intersecting => BrushOp::Intersect,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrushOp> for CsgOperationType {
|
||||
fn from(value: BrushOp) -> Self {
|
||||
match value {
|
||||
BrushOp::Add => Self::Additive,
|
||||
BrushOp::Subtract => Self::Subtractive,
|
||||
BrushOp::Intersect => Self::Intersecting,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_math::Vec3;
|
||||
|
||||
use super::*;
|
||||
use crate::{Aabb, Primitive};
|
||||
|
||||
#[test]
|
||||
fn tree_subtraction_compiles_to_ordered_brush_stream() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let solid = arena.generate_brush(
|
||||
"solid",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let void = arena.generate_brush(
|
||||
"void",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
},
|
||||
MaterialId(0),
|
||||
);
|
||||
let branch = arena.generate_branch(
|
||||
"solid minus void",
|
||||
CsgBranchOp::Subtraction,
|
||||
[solid.node, void.node],
|
||||
);
|
||||
let assembler = arena.compile_tree_to_assembler(arena.generate_tree(branch.node));
|
||||
|
||||
assert_eq!(assembler.brushes().len(), 2);
|
||||
assert_eq!(assembler.brushes()[0].op, BrushOp::Add);
|
||||
assert_eq!(assembler.brushes()[1].op, BrushOp::Subtract);
|
||||
assert_eq!(assembler.build().report.emitted_convex_fragments, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_edit_api_replaces_children_and_operations() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let a = arena.generate_brush(
|
||||
"a",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(2.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let b = arena.generate_brush(
|
||||
"b",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::X, Vec3::splat(1.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let branch = arena.generate_branch("branch", CsgBranchOp::Addition, [a.node]);
|
||||
|
||||
assert!(arena.set_child_nodes(branch, [a.node, b.node]));
|
||||
assert_eq!(arena.child_nodes(branch).expect("branch").len(), 2);
|
||||
assert!(arena.set_branch_operation_type(branch, CsgBranchOp::Subtraction));
|
||||
assert!(arena.set_brush_operation_type(b, CsgOperationType::Subtractive));
|
||||
assert_eq!(arena.brush_count(), 2);
|
||||
assert_eq!(arena.branch_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_bounds_follow_boolean_branch_shape() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let a = arena.generate_brush(
|
||||
"a",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let b = arena.generate_brush(
|
||||
"b",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::X * 2.5, Vec3::splat(2.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let union = arena.generate_branch("union", CsgBranchOp::Addition, [a.node, b.node]);
|
||||
let common = arena.generate_branch("common", CsgBranchOp::Common, [a.node, b.node]);
|
||||
let subtract =
|
||||
arena.generate_branch("subtract", CsgBranchOp::Subtraction, [a.node, b.node]);
|
||||
|
||||
assert_eq!(
|
||||
arena.node_bounds(union.node).expect("union"),
|
||||
Aabb::new(Vec3::splat(-2.0), Vec3::new(3.5, 2.0, 2.0))
|
||||
);
|
||||
assert_eq!(
|
||||
arena.node_bounds(common.node).expect("common"),
|
||||
Aabb::new(Vec3::new(1.5, -1.0, -1.0), Vec3::new(2.0, 1.0, 1.0))
|
||||
);
|
||||
assert_eq!(
|
||||
arena.node_bounds(subtract.node).expect("subtract"),
|
||||
Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_common_compiles_to_additive_source_then_intersections() {
|
||||
let mut arena = CsgTreeArena::new();
|
||||
let a = arena.generate_brush(
|
||||
"a",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::ZERO, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let b = arena.generate_brush(
|
||||
"b",
|
||||
CsgOperationType::Additive,
|
||||
Primitive::Box {
|
||||
bounds: Aabb::from_center_size(Vec3::X, Vec3::splat(4.0)),
|
||||
},
|
||||
MaterialId(1),
|
||||
);
|
||||
let branch = arena.generate_branch("common", CsgBranchOp::Common, [a.node, b.node]);
|
||||
|
||||
let assembler = arena.compile_tree_to_assembler(arena.generate_tree(branch.node));
|
||||
let output = assembler.build();
|
||||
|
||||
assert_eq!(assembler.brushes()[0].op, BrushOp::Add);
|
||||
assert_eq!(assembler.brushes()[1].op, BrushOp::Intersect);
|
||||
assert_eq!(output.report.emitted_convex_fragments, 1);
|
||||
assert_eq!(output.report.warnings.len(), 0);
|
||||
assert_eq!(output.mesh.triangle_count(), 12);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue