diff --git a/Cargo.lock b/Cargo.lock index fe4722b..0ebb835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "accesskit" version = "0.18.0" @@ -89,6 +105,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.9.1" @@ -315,6 +337,17 @@ dependencies = [ "bevy_internal", ] +[[package]] +name = "bevy-earcutr" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c170d12da1e8412dc3f5898a6873d262f2deb5f56716e4c121e4ee1ec662a8db" +dependencies = [ + "bevy", + "earcutr", + "num-traits", +] + [[package]] name = "bevy_a11y" version = "0.16.1" @@ -350,13 +383,13 @@ dependencies = [ "bevy_utils", "blake3", "derive_more", - "downcast-rs", + "downcast-rs 2.0.1", "either", "petgraph", "ron", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "thread_local", "tracing", "uuid", @@ -377,9 +410,9 @@ dependencies = [ "cfg-if", "console_error_panic_hook", "ctrlc", - "downcast-rs", + "downcast-rs 2.0.1", "log", - "thiserror 2.0.12", + "thiserror 2.0.18", "variadics_please", "wasm-bindgen", "web-sys", @@ -408,7 +441,7 @@ dependencies = [ "crossbeam-channel", "derive_more", "disqualified", - "downcast-rs", + "downcast-rs 2.0.1", "either", "futures-io", "futures-lite", @@ -417,7 +450,7 @@ dependencies = [ "ron", "serde", "stackfuture", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "uuid", "wasm-bindgen", @@ -467,7 +500,7 @@ dependencies = [ "derive_more", "encase", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "wgpu-types", ] @@ -497,7 +530,7 @@ dependencies = [ "radsort", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -554,7 +587,7 @@ dependencies = [ "nonmax", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "variadics_please", ] @@ -593,7 +626,7 @@ dependencies = [ "bevy_time", "bevy_utils", "gilrs", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -665,7 +698,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -692,7 +725,7 @@ dependencies = [ "rectangle-pack", "ruzstd", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "wgpu-types", ] @@ -712,7 +745,7 @@ dependencies = [ "derive_more", "log", "smol_str", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -728,7 +761,7 @@ dependencies = [ "bevy_reflect", "bevy_window", "log", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -820,7 +853,7 @@ dependencies = [ "rand_distr", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "variadics_please", ] @@ -844,7 +877,7 @@ dependencies = [ "bytemuck", "hexasphere", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "wgpu-types", ] @@ -897,7 +930,7 @@ dependencies = [ "radsort", "smallvec", "static_assertions", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -963,7 +996,7 @@ dependencies = [ "bevy_utils", "derive_more", "disqualified", - "downcast-rs", + "downcast-rs 2.0.1", "erased-serde", "foldhash", "glam 0.29.3", @@ -971,7 +1004,7 @@ dependencies = [ "serde", "smallvec", "smol_str", - "thiserror 2.0.12", + "thiserror 2.0.18", "uuid", "variadics_please", "wgpu-types", @@ -1019,7 +1052,7 @@ dependencies = [ "bytemuck", "codespan-reporting", "derive_more", - "downcast-rs", + "downcast-rs 2.0.1", "encase", "fixedbitset", "futures-lite", @@ -1034,7 +1067,7 @@ dependencies = [ "send_wrapper", "serde", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "variadics_please", "wasm-bindgen", @@ -1071,7 +1104,7 @@ dependencies = [ "bevy_utils", "derive_more", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", "uuid", ] @@ -1180,7 +1213,7 @@ dependencies = [ "serde", "smallvec", "sys-locale", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", "unicode-bidi", ] @@ -1215,7 +1248,7 @@ dependencies = [ "bevy_utils", "derive_more", "serde", - "thiserror 2.0.12", + "thiserror 2.0.18", ] [[package]] @@ -1249,7 +1282,7 @@ dependencies = [ "nonmax", "smallvec", "taffy", - "thiserror 2.0.12", + "thiserror 2.0.18", "tracing", ] @@ -1448,8 +1481,12 @@ version = "0.1.9" dependencies = [ "bevy", "bevy_panorbit_camera", + "geo", + "geo-bevy", "glam 0.30.9", "rayon", + "serde", + "thiserror 2.0.18", "tobj", ] @@ -1511,6 +1548,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.2.26" @@ -1892,6 +1941,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "downcast-rs" version = "2.0.1" @@ -1904,6 +1959,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "earcutr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01" +dependencies = [ + "itertools 0.11.0", + "num-traits", +] + [[package]] name = "either" version = "1.15.0" @@ -2029,6 +2094,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "fnv" version = "1.0.7" @@ -2134,6 +2205,70 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "geo" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3901269ec6d4f6068d3f09e5f02f995bd076398dcd1dfec407cd230b02d11b" +dependencies = [ + "earcutr", + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "rand", + "robust", + "rstar", + "sif-itree", + "spade", +] + +[[package]] +name = "geo-bevy" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e244adbf7ef7abdcd1dfbce94b6c90c748c47344e39ad4627999157db2dc94" +dependencies = [ + "bevy", + "bevy-earcutr", + "geo-traits", + "geo-types", + "num-traits", +] + +[[package]] +name = "geo-traits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc9562dd7e476214a5eea3caf12763dcc2c886a64cc4c3b8ec41554e8c6c5fc" +dependencies = [ + "geo-types", +] + +[[package]] +name = "geo-types" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" +dependencies = [ + "approx", + "num-traits", + "rayon", + "rstar", + "serde", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7" +dependencies = [ + "libm", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -2231,6 +2366,9 @@ name = "glam" version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" +dependencies = [ + "serde_core", +] [[package]] name = "glob" @@ -2387,6 +2525,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ + "allocator-api2", "equivalent", "foldhash", "serde", @@ -2438,6 +2577,49 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "i_float" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b" +dependencies = [ + "libm", +] + +[[package]] +name = "i_key_sort" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27" + +[[package]] +name = "i_overlay" +version = "4.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413183068e6e0289e18d7d0a1f661b81546e6918d5453a44570b9ab30cbed1b3" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082" +dependencies = [ + "i_float", +] + +[[package]] +name = "i_tree" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915" + [[package]] name = "image" version = "0.25.6" @@ -2507,6 +2689,15 @@ dependencies = [ "mach2", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -2785,7 +2976,7 @@ dependencies = [ "spirv", "strum", "termcolor", - "thiserror 2.0.12", + "thiserror 2.0.18", "unicode-xid", ] @@ -3249,6 +3440,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "parking" version = "2.2.1" @@ -3446,6 +3646,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -3614,6 +3823,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + [[package]] name = "rodio" version = "0.20.1" @@ -3642,6 +3857,17 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3727,12 +3953,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "self_cell" version = "1.2.0" @@ -3802,6 +4047,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sif-itree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f45b8998ced5134fb1d75732c77842a3e888f19c1ff98481822e8fbfbf930b" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3842,6 +4093,31 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -3851,6 +4127,18 @@ dependencies = [ "serde", ] +[[package]] +name = "spade" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c" +dependencies = [ + "hashbrown 0.15.3", + "num-traits", + "robust", + "smallvec", +] + [[package]] name = "spin" version = "0.9.8" @@ -3887,6 +4175,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strum" version = "0.26.3" @@ -3991,11 +4285,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.18", ] [[package]] @@ -4011,9 +4305,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -4029,6 +4323,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.9.0" @@ -4169,6 +4488,12 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "twox-hash" version = "2.1.1" @@ -4378,6 +4703,114 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs 1.2.1", + "rustix 1.0.7", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.9.1", + "rustix 1.0.7", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" +dependencies = [ + "rustix 1.0.7", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -4444,7 +4877,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "wgpu-hal", "wgpu-types", ] @@ -4487,7 +4920,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -5018,6 +5451,7 @@ version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ + "ahash", "android-activity", "atomic-waker", "bitflags 2.9.1", @@ -5032,6 +5466,7 @@ dependencies = [ "dpi", "js-sys", "libc", + "memmap2", "ndk 0.9.0", "objc2", "objc2-app-kit", @@ -5043,11 +5478,17 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", "web-sys", "web-time", "windows-sys 0.52.0", @@ -5106,6 +5547,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 53a3c5c..7777569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,19 +16,25 @@ glam = "0.30.9" rayon = { version = "1.11.0", optional = true } tobj = {version = "4.0.3", optional = true} -bevy = { version = "0.16.1", optional = true } +bevy = { version = "0.16.1", optional = true, features = ["wayland"] } bevy_panorbit_camera = { version = "0.26.0", optional = true } +thiserror = "2.0.18" +serde = { version = "1.0.228", features = ["derive"], optional = true } +geo = "0.32" +geo-bevy = { version = "8", optional = true } [features] default = [] verbose = [] f32 = [] rayon = ["dep:rayon"] +serde = ["dep:serde", "glam/serde"] bevy = [ "dep:tobj", "dep:bevy", "dep:bevy_panorbit_camera", + "dep:geo-bevy" ] [[example]] diff --git a/examples/extrusion.rs b/examples/extrusion.rs new file mode 100644 index 0000000..486ec3f --- /dev/null +++ b/examples/extrusion.rs @@ -0,0 +1,223 @@ +//--- Copyright (C) 2025 Saki Komikado , +//--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. + +use std::f64::consts::PI; + +use bevy::asset::RenderAssetUsages; +use bevy::color::palettes::css::*; +use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframePlugin}; +use bevy::prelude::*; +use bevy::render::mesh::{PrimitiveTopology, VertexAttributeValues}; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use boolmesh::{prelude::*, Real, Vec2}; +use geo::{BooleanOps, Coord, MultiPolygon, Rect, Translate}; + +#[derive(Default, Reflect, GizmoConfigGroup)] +struct MyRoundGizmos {} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(WireframePlugin::default()) + .add_plugins(PanOrbitCameraPlugin) + .init_gizmo_group::() + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut cmds: Commands, + mut mats: ResMut>, + mut meshes: ResMut>, +) { + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(-6.0, 0.0, 0.0), + || { + let square = Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }); + square.to_polygon().into() + }, + |polygon| polygon.extrude(1.0, 1, 0.0, Vec2::new(1.0, 1.0)).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(-4.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let inner_square = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + square.boolean_op(&inner_square, geo::OpType::Difference) + }, + |polygon| polygon.extrude(1.0, 1, 0.0, Vec2::new(1.0, 1.0)).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(-2.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let inner_square = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + square.boolean_op(&inner_square, geo::OpType::Difference) + }, + |polygon| { + polygon + .extrude(1.0, 20, PI as Real, Vec2::new(1.0, 1.0)) + .unwrap() + }, + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(0.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let inner_square = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + square.boolean_op(&inner_square, geo::OpType::Difference) + }, + |polygon| polygon.extrude(1.0, 1, 0.0, Vec2::new(0.5, 0.5)).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(2.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let inner_square = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + square.boolean_op(&inner_square, geo::OpType::Difference) + }, + |polygon| polygon.extrude(1.0, 1, 0.0, Vec2::new(0.0, 0.0)).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(4.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let inner_square = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + square.boolean_op(&inner_square, geo::OpType::Difference) + }, + |polygon| { + polygon + .extrude(1.0, 50, PI as Real, Vec2::new(0.0, 0.0)) + .unwrap() + }, + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(6.0, 0.0, 0.0), + || { + let square = + Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let square2 = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + let square2_hole = Rect::new( + Coord { + x: -0.124, + y: -0.125, + }, + Coord { x: 0.125, y: 0.125 }, + ) + .to_polygon(); + let square2 = square2.difference(&square2_hole).translate(0.0, 2.0); + + square.boolean_op(&square2, geo::OpType::Union) + }, + |polygon| polygon.extrude(1.0, 1, 0.0, Vec2::new(1.0, 1.0)).unwrap(), + ); + + cmds.spawn((PointLight::default(), Transform::from_xyz(2., 5., 2.))); + cmds.spawn(( + Transform::from_translation(Vec3::new(0., 2., 3.)), + PanOrbitCamera::default(), + )); +} + +fn spawn_model( + cmds: &mut Commands, + meshes: &mut ResMut>, + mats: &mut ResMut>, + translation: Vec3, + build_polygon: impl FnOnce() -> MultiPolygon, + build_model: impl FnOnce(MultiPolygon) -> Manifold, +) { + let polygon = build_polygon(); + + let polygon_meshes = geo_bevy::multi_polygon_to_mesh(&polygon).unwrap(); + + for mesh in polygon_meshes { + let mut mesh = mesh.mesh; + if let VertexAttributeValues::Float32x3(values) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL).unwrap() + { + values.iter_mut().for_each(|value| *value = [0.0, 0.0, 1.0]); + } + + cmds.spawn(( + Mesh3d(meshes.add(mesh).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(translation + Vec3::new(0.0, 0.0, 2.0)), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + } + + let model = build_model(polygon); + + let mut m = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + let mut pos = vec![]; + let mut vns = vec![]; + for (fid, hs) in model.hs.chunks(3).enumerate() { + let p0 = model.ps[hs[0].tail]; + let p1 = model.ps[hs[1].tail]; + let p2 = model.ps[hs[2].tail]; + let n = model.face_normals[fid]; + pos.push([p0.x as f32, p0.y as f32, p0.z as f32]); + pos.push([p1.x as f32, p1.y as f32, p1.z as f32]); + pos.push([p2.x as f32, p2.y as f32, p2.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + } + m.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos); + m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vns); + + cmds.spawn(( + Mesh3d(meshes.add(m).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(translation), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); +} diff --git a/examples/projection.rs b/examples/projection.rs new file mode 100644 index 0000000..94104ab --- /dev/null +++ b/examples/projection.rs @@ -0,0 +1,113 @@ +//--- Copyright (C) 2025 Saki Komikado , +//--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. + +use std::f64::consts::PI; + +use bevy::asset::RenderAssetUsages; +use bevy::color::palettes::css::*; +use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframePlugin}; +use bevy::prelude::*; +use bevy::render::mesh::{PrimitiveTopology, VertexAttributeValues}; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use boolmesh::{compute_projection, prelude::*}; + +#[derive(Default, Reflect, GizmoConfigGroup)] +struct MyRoundGizmos {} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(WireframePlugin::default()) + .add_plugins(PanOrbitCameraPlugin) + .init_gizmo_group::() + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut cmds: Commands, + mut mats: ResMut>, + mut meshes: ResMut>, +) { + let model = "examples/models/double-torus.obj"; + let (gargoyle, _) = tobj::load_obj( + model, + &tobj::LoadOptions { + ..Default::default() + }, + ) + .expect("Failed to load obj file"); + + let m = &gargoyle[0].mesh; + + let mut model = Manifold::new( + &m.positions.iter().map(|&v| v as f64).collect::>(), + &m.indices.iter().map(|&v| v as usize).collect::>(), + ) + .unwrap(); + + model.translate(-6.0, 0.0, 0.0); + for _ in 0..5 { + model.rotate(PI / 4.0, 0.0, 0.0); + model.translate(2.0, 0.0, 0.0); + + let mut m = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + let mut pos = vec![]; + let mut vns = vec![]; + for (fid, hs) in model.hs.chunks(3).enumerate() { + let p0 = model.ps[hs[0].tail]; + let p1 = model.ps[hs[1].tail]; + let p2 = model.ps[hs[2].tail]; + let n = model.face_normals[fid]; + pos.push([p0.x as f32, p0.y as f32, p0.z as f32]); + pos.push([p1.x as f32, p1.y as f32, p1.z as f32]); + pos.push([p2.x as f32, p2.y as f32, p2.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + } + m.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos); + m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vns); + + cmds.spawn(( + Mesh3d(meshes.add(m).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::default(), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + + let projection = compute_projection(&model).unwrap(); + let projection_meshes = geo_bevy::multi_polygon_to_mesh(&projection).unwrap(); + + for mesh in projection_meshes { + let mut mesh = mesh.mesh; + if let VertexAttributeValues::Float32x3(values) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL).unwrap() + { + values.iter_mut().for_each(|value| *value = [0.0, 0.0, 1.0]); + } + + cmds.spawn(( + Mesh3d(meshes.add(mesh).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(Vec3::new(0.0, 0.0, 2.0)), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + } + } + + cmds.spawn((PointLight::default(), Transform::from_xyz(2., 5., 2.))); + cmds.spawn(( + Transform::from_translation(Vec3::new(0., 2., 3.)), + PanOrbitCamera::default(), + )); +} diff --git a/examples/revolution.rs b/examples/revolution.rs new file mode 100644 index 0000000..2f14e67 --- /dev/null +++ b/examples/revolution.rs @@ -0,0 +1,173 @@ +//--- Copyright (C) 2025 Saki Komikado , +//--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. + +use std::f64::consts::PI; + +use bevy::asset::RenderAssetUsages; +use bevy::color::palettes::css::*; +use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframePlugin}; +use bevy::prelude::*; +use bevy::render::mesh::{PrimitiveTopology, VertexAttributeValues}; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use boolmesh::{prelude::*, Real}; +use geo::{BooleanOps, Coord, MultiPolygon, Rect, Translate}; + +#[derive(Default, Reflect, GizmoConfigGroup)] +struct MyRoundGizmos {} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(WireframePlugin::default()) + .add_plugins(PanOrbitCameraPlugin) + .init_gizmo_group::() + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut cmds: Commands, + mut mats: ResMut>, + mut meshes: ResMut>, +) { + fn square_with_a_bite_out_of_it() -> MultiPolygon { + let square = Rect::new(Coord { x: 0.0, y: -0.5 }, Coord { x: 1.0, y: 0.5 }).to_polygon(); + let bite = Rect::new(Coord { x: 0.25, y: -0.25 }, Coord { x: 0.75, y: 0.5 }).to_polygon(); + square.boolean_op(&bite, geo::OpType::Difference) + } + + fn two_polygons() -> MultiPolygon { + let square = Rect::new(Coord { x: -0.5, y: -0.5 }, Coord { x: 0.5, y: 0.5 }).to_polygon(); + let square2 = + Rect::new(Coord { x: -0.25, y: -0.25 }, Coord { x: 0.25, y: 0.25 }).to_polygon(); + let square2_hole = Rect::new( + Coord { + x: -0.124, + y: -0.125, + }, + Coord { x: 0.125, y: 0.125 }, + ) + .to_polygon(); + let square2 = square2.difference(&square2_hole).translate(0.0, 2.0); + + square + .boolean_op(&square2, geo::OpType::Union) + .translate(0.5, 0.0) + } + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(-6.0, 0.0, 0.0), + || square_with_a_bite_out_of_it().translate(0.5, 0.0), + |polygon| polygon.revolve(5, PI as Real * 0.5).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(-3.0, 0.0, 0.0), + square_with_a_bite_out_of_it, + |polygon| polygon.revolve(15, PI as Real * 2.0).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(0.0, 0.0, 0.0), + || square_with_a_bite_out_of_it().translate(0.5, 0.0), + |polygon| polygon.revolve(15, PI as Real * 2.0).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(3.0, 0.0, 0.0), + two_polygons, + |polygon| polygon.revolve(15, PI as Real * 2.0).unwrap(), + ); + + spawn_model( + &mut cmds, + &mut meshes, + &mut mats, + Vec3::new(6.0, 0.0, 0.0), + two_polygons, + |polygon| polygon.revolve(5, PI as Real * 0.5).unwrap(), + ); + + cmds.spawn((PointLight::default(), Transform::from_xyz(2., 5., 2.))); + cmds.spawn(( + Transform::from_translation(Vec3::new(0., 2., 3.)), + PanOrbitCamera::default(), + )); +} + +fn spawn_model( + cmds: &mut Commands, + meshes: &mut ResMut>, + mats: &mut ResMut>, + translation: Vec3, + build_polygon: impl FnOnce() -> MultiPolygon, + build_model: impl FnOnce(MultiPolygon) -> Manifold, +) { + let polygon = build_polygon(); + + let polygon_meshes = geo_bevy::multi_polygon_to_mesh(&polygon).unwrap(); + + for mesh in polygon_meshes { + let mut mesh = mesh.mesh; + if let VertexAttributeValues::Float32x3(values) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL).unwrap() + { + values.iter_mut().for_each(|value| *value = [0.0, 0.0, 1.0]); + } + + cmds.spawn(( + Mesh3d(meshes.add(mesh).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(translation + Vec3::new(0.0, 0.0, 2.0)), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + } + + let model = build_model(polygon); + + let mut m = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + let mut pos = vec![]; + let mut vns = vec![]; + for (fid, hs) in model.hs.chunks(3).enumerate() { + let p0 = model.ps[hs[0].tail]; + let p1 = model.ps[hs[1].tail]; + let p2 = model.ps[hs[2].tail]; + let n = model.face_normals[fid]; + pos.push([p0.x as f32, p0.y as f32, p0.z as f32]); + pos.push([p1.x as f32, p1.y as f32, p1.z as f32]); + pos.push([p2.x as f32, p2.y as f32, p2.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + } + m.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos); + m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vns); + + cmds.spawn(( + Mesh3d(meshes.add(m).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(translation), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); +} diff --git a/examples/slicing.rs b/examples/slicing.rs new file mode 100644 index 0000000..88ea8fe --- /dev/null +++ b/examples/slicing.rs @@ -0,0 +1,118 @@ +//--- Copyright (C) 2025 Saki Komikado , +//--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. + +use std::f64::consts::PI; + +use bevy::asset::RenderAssetUsages; +use bevy::color::palettes::css::*; +use bevy::pbr::wireframe::{Wireframe, WireframeColor, WireframePlugin}; +use bevy::prelude::*; +use bevy::render::mesh::{PrimitiveTopology, VertexAttributeValues}; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use boolmesh::{compute_slice, prelude::*, Real}; + +#[derive(Default, Reflect, GizmoConfigGroup)] +struct MyRoundGizmos {} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(WireframePlugin::default()) + .add_plugins(PanOrbitCameraPlugin) + .init_gizmo_group::() + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut cmds: Commands, + mut mats: ResMut>, + mut meshes: ResMut>, +) { + let model = "examples/models/double-torus.obj"; + let (gargoyle, _) = tobj::load_obj( + model, + &tobj::LoadOptions { + ..Default::default() + }, + ) + .expect("Failed to load obj file"); + + let m = &gargoyle[0].mesh; + + let mut model = Manifold::new( + &m.positions.iter().map(|&v| v as f64).collect::>(), + &m.indices.iter().map(|&v| v as usize).collect::>(), + ) + .unwrap(); + + model.translate(-6.0, 0.0, 0.0); + for _ in 0..5 { + model.rotate(PI / 4.0, 0.0, 0.0); + model.translate(2.0, 0.0, 0.0); + + let mut m = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + let mut pos = vec![]; + let mut vns = vec![]; + for (fid, hs) in model.hs.chunks(3).enumerate() { + let p0 = model.ps[hs[0].tail]; + let p1 = model.ps[hs[1].tail]; + let p2 = model.ps[hs[2].tail]; + let n = model.face_normals[fid]; + pos.push([p0.x as f32, p0.y as f32, p0.z as f32]); + pos.push([p1.x as f32, p1.y as f32, p1.z as f32]); + pos.push([p2.x as f32, p2.y as f32, p2.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + vns.push([n.x as f32, n.y as f32, n.z as f32]); + } + m.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos); + m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vns); + + cmds.spawn(( + Mesh3d(meshes.add(m).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::default(), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + + for i in -14..14 { + let offset = i as Real / 10.0; + // Some slices will fail to hit the model. + if let Ok(slice) = compute_slice(&model, offset) { + let slice_mesh = geo_bevy::multi_polygon_to_mesh(&slice).unwrap(); + + for mesh in slice_mesh { + let mut mesh = mesh.mesh; + if let VertexAttributeValues::Float32x3(values) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL).unwrap() + { + values.iter_mut().for_each(|value| *value = [0.0, 0.0, 1.0]); + } + + cmds.spawn(( + Mesh3d(meshes.add(mesh).clone()), + MeshMaterial3d(mats.add(StandardMaterial { ..default() })), + Transform::from_translation(Vec3::new(0.0, 0.0, 3.0 + offset as f32)), + Wireframe, + WireframeColor { + color: BLACK.into(), + }, + )); + } + } + } + } + + cmds.spawn((PointLight::default(), Transform::from_xyz(2., 5., 2.))); + cmds.spawn(( + Transform::from_translation(Vec3::new(0., 2., 3.)), + PanOrbitCamera::default(), + )); +} diff --git a/src/common.rs b/src/common.rs index f9ad083..9bf5d10 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,7 +6,9 @@ mod precision { pub type Vec2 = glam::Vec2; pub type Vec3 = glam::Vec3A; pub type Vec4 = glam::Vec4; + pub type Mat2 = glam::Mat2; pub type Mat3 = glam::Mat3A; + pub type Affine3 = glam::Affine3A; pub type Real = f32; pub const K_PRECISION: Real = 1e-4; } @@ -16,20 +18,26 @@ mod precision { pub type Vec2 = glam::DVec2; pub type Vec3 = glam::DVec3; pub type Vec4 = glam::DVec4; + pub type Mat2 = glam::DMat2; pub type Mat3 = glam::DMat3; + pub type Affine3 = glam::DAffine3; pub type Real = f64; pub const K_PRECISION: f64 = 1e-12; } pub type Vec2u = glam::USizeVec2; pub type Vec3u = glam::USizeVec3; -pub use precision::{Real, Vec2, Vec3, Vec4, Mat3, K_PRECISION}; +pub use precision::{Affine3, Mat2, Mat3, Real, Vec2, Vec3, Vec4, K_PRECISION}; pub const K_BEST: Real = Real::MIN; - #[derive(PartialEq)] -pub enum OpType { Add, Subtract, Intersect } +pub enum OpType { + Add, + Subtract, + Intersect, +} +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct Half { pub tail: usize, @@ -38,21 +46,62 @@ pub struct Half { } impl Default for Half { - fn default() -> Self { Self { tail: usize::MAX, head: usize::MAX, pair: usize::MAX } } + fn default() -> Self { + Self { + tail: usize::MAX, + head: usize::MAX, + pair: usize::MAX, + } + } } impl Half { - pub fn new(tail: usize, head: usize, pair: usize) -> Self { Self { tail, head, pair } } - pub fn new_without_pair(tail: usize, head: usize) -> Self { Self { tail, head, pair: usize::MAX } } - pub fn is_forward(&self) -> bool { self.tail < self.head } - pub fn tail(&self) -> Option { if self.tail == usize::MAX {None} else {Some(self.tail)} } - pub fn head(&self) -> Option { if self.head == usize::MAX {None} else {Some(self.head)} } - pub fn pair(&self) -> Option { if self.pair == usize::MAX {None} else {Some(self.pair)} } + pub fn new(tail: usize, head: usize, pair: usize) -> Self { + Self { tail, head, pair } + } + pub fn new_without_pair(tail: usize, head: usize) -> Self { + Self { + tail, + head, + pair: usize::MAX, + } + } + pub fn is_forward(&self) -> bool { + self.tail < self.head + } + pub fn tail(&self) -> Option { + if self.tail == usize::MAX { + None + } else { + Some(self.tail) + } + } + pub fn head(&self) -> Option { + if self.head == usize::MAX { + None + } else { + Some(self.head) + } + } + pub fn pair(&self) -> Option { + if self.pair == usize::MAX { + None + } else { + Some(self.pair) + } + } } -pub fn face_of(hid: usize) -> usize { hid / 3 } -pub fn next_of(hid: usize) -> usize { let mut i = hid + 1; if i.is_multiple_of(3) { i -= 3;} i } - +pub fn face_of(hid: usize) -> usize { + hid / 3 +} +pub fn next_of(hid: usize) -> usize { + let mut i = hid + 1; + if i.is_multiple_of(3) { + i -= 3; + } + i +} #[derive(Clone, Debug, Copy)] pub struct Tref { @@ -66,12 +115,14 @@ impl Default for Tref { Self { mid: usize::MAX, fid: usize::MAX, - pid: -1 + pid: -1, } } } -pub fn det2x2(a: &Vec2, b: &Vec2) -> Real { a.x * b.y - a.y * b.x } +pub fn det2x2(a: &Vec2, b: &Vec2) -> Real { + a.x * b.y - a.y * b.x +} pub fn get_aa_proj_matrix(n: &Vec3) -> (Vec3, Vec3) { let a = n.abs(); @@ -79,11 +130,29 @@ pub fn get_aa_proj_matrix(n: &Vec3) -> (Vec3, Vec3) { let r1: Vec3; let r2: Vec3; - if a.z > a.x && a.z > a.y { r1 = Vec3::new(1., 0., 0.); r2 = Vec3::new(0., 1., 0.); m = n.z; } // preserve x, y - else if a.y > a.x { r1 = Vec3::new(0., 0., 1.); r2 = Vec3::new(1., 0., 0.); m = n.y; } // preserve z, x - else { r1 = Vec3::new(0., 1., 0.); r2 = Vec3::new(0., 0., 1.); m = n.x; } // preserve y, z - - if m < 0. { (-r1, r2) } else { (r1, r2) } + if a.z > a.x && a.z > a.y { + r1 = Vec3::new(1., 0., 0.); + r2 = Vec3::new(0., 1., 0.); + m = n.z; + } + // preserve x, y + else if a.y > a.x { + r1 = Vec3::new(0., 0., 1.); + r2 = Vec3::new(1., 0., 0.); + m = n.y; + } + // preserve z, x + else { + r1 = Vec3::new(0., 1., 0.); + r2 = Vec3::new(0., 0., 1.); + m = n.x; + } // preserve y, z + + if m < 0. { + (-r1, r2) + } else { + (r1, r2) + } } pub fn compute_aa_proj(p: &(Vec3, Vec3), v: &Vec3) -> Vec2 { @@ -95,8 +164,14 @@ pub fn is_ccw_2d(p0: &Vec2, p1: &Vec2, p2: &Vec2, t: Real) -> i32 { let v2 = p2 - p0; let area = v1.x * v2.y - v1.y * v2.x; let base = v1.length_squared().max(v2.length_squared()); - if area.powi(2) * 4. <= base * t.powi(2) { return 0; } - if area > 0. { 1 } else { -1 } + if area.powi(2) * 4. <= base * t.powi(2) { + return 0; + } + if area > 0. { + 1 + } else { + -1 + } } pub fn is_ccw_3d(p0: &Vec3, p1: &Vec3, p2: &Vec3, n: &Vec3, t: Real) -> i32 { @@ -105,21 +180,25 @@ pub fn is_ccw_3d(p0: &Vec3, p1: &Vec3, p2: &Vec3, n: &Vec3, t: Real) -> i32 { &compute_aa_proj(&p, p0), &compute_aa_proj(&p, p1), &compute_aa_proj(&p, p2), - t + t, ) } pub fn safe_normalize(v: Vec2) -> Vec2 { let n = v.normalize(); - if n.x.is_finite() && !n.x.is_nan() && - n.y.is_finite() && !n.y.is_nan() { n } - else { Vec2::new(0., 0.) } + if n.x.is_finite() && !n.x.is_nan() && n.y.is_finite() && !n.y.is_nan() { + n + } else { + Vec2::new(0., 0.) + } } pub fn compute_orthogonal(n: Vec3) -> Vec3 { - let b = if n.x.abs() < 0.9 - { Vec3::new(1., 0., 0.) } - else { Vec3::new(0., 1., 0.) }; + let b = if n.x.abs() < 0.9 { + Vec3::new(1., 0., 0.) + } else { + Vec3::new(0., 1., 0.) + }; n.cross(b).normalize() } diff --git a/src/compose/cone.rs b/src/compose/cone.rs index eedddd9..51a3662 100644 --- a/src/compose/cone.rs +++ b/src/compose/cone.rs @@ -1,16 +1,17 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. -use std::f64::consts::PI; -use crate::{Manifold, Vec3, Real, compute_orthogonal}; use crate::common::Vec3u; +use crate::manifold::ManifoldError; +use crate::{compute_orthogonal, Manifold, Real, Vec3}; +use std::f64::consts::PI; pub fn generate_cone( apex: Vec3, center: Vec3, radius: Real, divide: usize, -) -> Result { +) -> Result { let d = (PI * 2. / divide as f64) as Real; let n = (center - apex).normalize(); let b1 = compute_orthogonal(n); @@ -30,4 +31,4 @@ pub fn generate_cone( ps.push(apex); ps.push(center); Manifold::new_impl(ps, ts, None, None) -} \ No newline at end of file +} diff --git a/src/compose/cube.rs b/src/compose/cube.rs index 88bee39..fff5e8a 100644 --- a/src/compose/cube.rs +++ b/src/compose/cube.rs @@ -1,27 +1,17 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. -use crate::manifold::Manifold; +use crate::manifold::{Manifold, ManifoldError}; -pub fn generate_cube() -> Result { +pub fn generate_cube() -> Result { let ps = [ - -0.5, -0.5, -0.5, - -0.5, -0.5, 0.5, - -0.5, 0.5, -0.5, - -0.5, 0.5, 0.5, - 0.5, -0.5, -0.5, - 0.5, -0.5, 0.5, - 0.5, 0.5, -0.5, - 0.5, 0.5, 0.5 + -0.5, -0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5, -0.5, 0.5, + -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, ]; let ts = [ - 1, 0, 4, 2, 4, 0, - 1, 3, 0, 3, 1, 5, - 3, 2, 0, 3, 7, 2, - 5, 4, 6, 5, 1, 4, - 6, 4, 2, 7, 6, 2, - 7, 3, 5, 7, 5, 6 + 1, 0, 4, 2, 4, 0, 1, 3, 0, 3, 1, 5, 3, 2, 0, 3, 7, 2, 5, 4, 6, 5, 1, 4, 6, 4, 2, 7, 6, 2, + 7, 3, 5, 7, 5, 6, ]; Manifold::new(&ps, &ts) } diff --git a/src/compose/cylinder.rs b/src/compose/cylinder.rs index 2af6f72..88faaa3 100644 --- a/src/compose/cylinder.rs +++ b/src/compose/cylinder.rs @@ -1,20 +1,24 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. +use thiserror::Error; + +use crate::{manifold::ManifoldError, Manifold, Real, Vec3, Vec3u}; use std::f64::consts::PI; -use crate::{Manifold, Vec3, Vec3u, Real}; pub fn generate_cylinder( r: f64, // radius h: f64, // height d0: usize, // sectors d1: usize, // stacks -) -> Result { - if d0 < 3 || d1 < 1 { return Err("sectors must be >= 3 and stacks must be >= 1".into()); } +) -> Result { + if d0 < 3 || d1 < 1 { + return Err(CylinderError::InvalidSectorCount); + } let mut ps = vec![]; let mut ts = vec![]; - ps.push(Vec3::new(0., h as Real * 0.5, 0.)); + ps.push(Vec3::new(0., h as Real * 0.5, 0.)); ps.push(Vec3::new(0., -h as Real * 0.5, 0.)); for i in 0..=d1 { @@ -45,5 +49,16 @@ pub fn generate_cylinder( } } - Manifold::new_impl(ps, ts, None, None) -} \ No newline at end of file + let manifold = Manifold::new_impl(ps, ts, None, None)?; + + Ok(manifold) +} + +#[derive(Debug, Error)] +pub enum CylinderError { + #[error("sectors must be >= 3 and stacks must be >= 1")] + InvalidSectorCount, + + #[error("{0}")] + Manifold(#[from] ManifoldError), +} diff --git a/src/compose/extrusion.rs b/src/compose/extrusion.rs new file mode 100644 index 0000000..ce2a60e --- /dev/null +++ b/src/compose/extrusion.rs @@ -0,0 +1,322 @@ +use std::f64::consts::PI; + +use geo::{BoundingRect, Coord, LineString, MultiPolygon, Polygon}; +use thiserror::Error; + +use crate::{ + common::{Affine3, Vec3u}, + manifold::ManifoldError, + prelude::Manifold, + triangulation::{ear_clip::EarClip, Pt}, + Mat2, Mat3, Real, Vec2, Vec3, K_PRECISION, +}; + +trait IterStrings { + fn strings(&self) -> impl Iterator>; + fn coords(&self) -> impl Iterator>; + fn num_coords(&self) -> usize; +} + +impl IterStrings for Polygon { + fn strings(&self) -> impl Iterator> { + [self.exterior()].into_iter().chain(self.interiors()) + } + + fn coords(&self) -> impl Iterator> { + self.strings().flat_map(|string| string.coords()) + } + + fn num_coords(&self) -> usize { + // TODO instead of counting each coor individually, sum up the length of all the strings. + self.coords().count() + } +} + +/// Controls how faces are created on an extruded/revolved manifold. +enum FaceMode { + /// Close the face + Close, + + /// No face, loop the structure back to its first layer + Loop, +} + +fn raw_extrude_impl<'s, P>( + polygon_iter_builder: impl Fn() -> P, + divisions: usize, + face_mode: FaceMode, + affine: impl Fn(Real) -> Affine3, +) -> Result +where + P: IntoIterator>, +{ + fn points(polygon: &Polygon) -> impl Iterator { + polygon + .coords() + .map(|coord| Vec3::new(coord.x, coord.y, 0.0)) + } + + let face_indicies = if matches!(face_mode, FaceMode::Close) { + let mut i = 0; + + // TODO I dislike collecting the polygons into a throw-away vec like this, but I want to + // avoid changing the core library for now. + let polygons: Vec> = polygon_iter_builder() + .into_iter() + .map(|polygon| { + polygon + .coords() + .map(|c| { + let pt = Pt { + pos: Vec2::new(c.x, c.y), + idx: i, + }; + i += 1; + pt + }) + .collect::>() + }) + .collect(); + + Some(EarClip::new(&polygons, K_PRECISION).triangulate()) + } else { + // If we don't need to close the faces, then we don't need to calculate the face indicies. + None + }; + + let mut oft_ps = vec![]; + let mut oft_ts = vec![]; + let points_per_division: usize = polygon_iter_builder() + .into_iter() + .map(|polygon| polygon.num_coords()) + .sum(); + + // Insert bottom verticies. + for p in polygon_iter_builder().into_iter().flat_map(points) { + oft_ps.push(p); + } + + if let Some(face_indicies) = face_indicies.as_ref() { + // Insert bottom vertex references. + for i in face_indicies.iter() { + oft_ts.push(Vec3u::new(i.z, i.y, i.x)); + } + } + + // Incert divisions. Note that the top of the shape counts as a division. + for layer in 0..divisions { + let alpha = (layer + 1) as Real / divisions as Real; + let affine = affine(alpha); + + // Insert the next division's verticies + let mut polygon_point_offset = 0; + for polygon in polygon_iter_builder() { + let base_offset = layer * points_per_division + polygon_point_offset; + let points_in_polygon = polygon.num_coords(); + for (vertex_index, position) in points(polygon).enumerate() { + // Conversion is necessary for 32bit support. + #[allow(clippy::useless_conversion)] + oft_ps.push(affine.transform_point3(position.into()).into()); + + // Corners of a quardrangle making up a a side of the extruded shape. + // k--l + // | | + // i--j + let i = base_offset + vertex_index; + let j = base_offset + (vertex_index + 1) % points_in_polygon; + let k = i + points_per_division; + let l = j + points_per_division; + + oft_ts.push(Vec3u::new(i, j, k)); + oft_ts.push(Vec3u::new(k, j, l)); + } + + polygon_point_offset += points_in_polygon; + } + } + + if let Some(face_indicies) = face_indicies.as_ref() { + // Insert top vertex references. + // We do not need to insert their verticies because they were provided by the final layer of + // the divisions loop. + for i in face_indicies.iter() { + oft_ts.push(Vec3u::new( + i.x + points_per_division * divisions, + i.y + points_per_division * divisions, + i.z + points_per_division * divisions, + )); + } + } else { + // Loop the final layer back to the first layer. + let mut polygon_point_offset = 0; + for polygon in polygon_iter_builder() { + let ending_offset = points_per_division * divisions + polygon_point_offset; + let starting_offset = polygon_point_offset; + let points_in_polygon = polygon.num_coords(); + for (vertex_index, _position) in points(polygon).enumerate() { + // Corners of a quardrangle making up a a side of the extruded shape. + // k--l + // | | + // i--j + let k = vertex_index + starting_offset; + let l = (vertex_index + 1) % points_in_polygon + starting_offset; + let i = vertex_index + ending_offset; + let j = (vertex_index + 1) % points_in_polygon + ending_offset; + + oft_ts.push(Vec3u::new(i, j, k)); + oft_ts.push(Vec3u::new(k, j, l)); + } + + polygon_point_offset += points_in_polygon; + } + } + + Manifold::new_impl(oft_ps, oft_ts, None, None) +} + +#[derive(Debug, Error)] +pub enum ExtrusionError { + #[error("Extrusion height must be greater than zero")] + InvalidHeight, + + #[error("Height of extrusion top must be greater than or equel to zero")] + InvalidScale, + + #[error("Error buiding manifold: {0}")] + Manifold(#[from] ManifoldError), +} + +fn extrude_impl<'s, P>( + polygon_iter_builder: impl Fn() -> P, + height: Real, + divisions: usize, + twist_radians: Real, + scale_top: Vec2, +) -> Result +where + P: IntoIterator>, +{ + if height <= 0.0 { + return Err(ExtrusionError::InvalidHeight); + } + + if scale_top.x < 0.0 || scale_top.y < 0.0 { + return Err(ExtrusionError::InvalidScale); + } + + let manifold = raw_extrude_impl(polygon_iter_builder, divisions, FaceMode::Close, |alpha| { + let phi = alpha * twist_radians; + let scale = Vec2::splat(1.0).lerp(scale_top, alpha); + let translation = Vec3::new(0.0, 0.0, alpha * height); + let matrix2 = Mat2::from_scale_angle(scale, phi); + let matrix3 = Mat3::from_mat2(matrix2); + Affine3 { + matrix3, + translation, + } + })?; + + Ok(manifold) +} + +#[derive(Debug, Error)] +pub enum RevolveError { + #[error("Revolution angle must be greater than zero")] + InvalidAngle, + + #[error("Geometry must not be present on the left side of the Y axis")] + LeftOfYAxis, + + #[error("Error buiding manifold: {0}")] + Manifold(#[from] ManifoldError), +} + +fn revolve_impl<'s, P>( + polygon_iter_builder: impl Fn() -> P, + divisions: usize, + angle_radians: Real, +) -> Result +where + P: IntoIterator>, +{ + if angle_radians <= 0.0 { + return Err(RevolveError::InvalidAngle); + } + + // Checks if any geometry has a point left of the Y axis + if polygon_iter_builder().into_iter().any(|polygon| { + polygon + .bounding_rect() + .is_some_and(|rect| rect.min().x < 0.0) + }) { + return Err(RevolveError::LeftOfYAxis); + } + + const MAX_ANGLE: Real = PI as Real * 2.0; + + // Cap the angle at 2Pi. + let angle_radians = MAX_ANGLE.min(angle_radians); + + let face_mode = if angle_radians < MAX_ANGLE { + FaceMode::Close + } else { + FaceMode::Loop + }; + + let manifold = raw_extrude_impl(polygon_iter_builder, divisions, face_mode, |alpha| { + let translation = Vec3::new(0.0, 0.0, 0.0); + + #[allow(clippy::useless_conversion)] + let matrix3 = Mat3::from_axis_angle(Vec3::Y.into(), -angle_radians * alpha); + Affine3 { + matrix3, + translation, + } + })?; + + Ok(manifold) +} + +pub trait ExtrudePoly { + fn extrude( + &self, + height: Real, + divisions: usize, + twist_radians: Real, + scale_top: Vec2, + ) -> Result; + + fn revolve(&self, divisions: usize, angle_radians: Real) -> Result; +} + +impl ExtrudePoly for Polygon { + fn extrude( + &self, + height: Real, + divisions: usize, + twist_radians: Real, + scale_top: Vec2, + ) -> Result { + extrude_impl(|| [self], height, divisions, twist_radians, scale_top) + } + + fn revolve(&self, divisions: usize, angle_radians: Real) -> Result { + revolve_impl(|| [self], divisions, angle_radians) + } +} + +impl ExtrudePoly for MultiPolygon { + fn extrude( + &self, + height: Real, + divisions: usize, + twist_radians: Real, + scale_top: Vec2, + ) -> Result { + extrude_impl(|| self.iter(), height, divisions, twist_radians, scale_top) + } + + fn revolve(&self, divisions: usize, angle_radians: Real) -> Result { + revolve_impl(|| self.iter(), divisions, angle_radians) + } +} diff --git a/src/compose/mod.rs b/src/compose/mod.rs index f76049a..9755be0 100644 --- a/src/compose/mod.rs +++ b/src/compose/mod.rs @@ -4,48 +4,29 @@ pub mod cone; pub mod cube; +pub mod cylinder; +pub mod extrusion; pub mod sphere; pub mod torus; -pub mod cylinder; pub use cone::*; pub use cube::*; +pub use cylinder::*; +pub use extrusion::*; pub use sphere::*; pub use torus::*; -pub use cylinder::*; - -use crate::{Manifold, Vec3, K_PRECISION}; -use crate::common::{compute_aa_proj, get_aa_proj_matrix, Vec3u}; -use crate::triangulation::ear_clip::EarClip; -use crate::triangulation::Pt; -pub fn extrude(pts: &[Vec3], offset: Vec3) -> Result { - let n = Vec3::new(0., 0., 1.); - let proj = get_aa_proj_matrix(&n); - let poly = pts.iter().enumerate().map(|(i, p)| Pt {pos: compute_aa_proj(&proj, p), idx: i}).collect::>(); - let idcs = EarClip::new(&[poly], K_PRECISION).triangulate(); +use crate::manifold::ManifoldError; +use crate::Manifold; - let mut oft_ps = vec![]; - let mut oft_ts = vec![]; - let n = pts.len(); - for p in pts.iter() { oft_ps.push(*p); } - for p in pts.iter() { oft_ps.push(p + offset); } - for i in idcs.iter() { oft_ts.push(Vec3u::new(i.z, i.y, i.x)); } - for i in idcs.iter() { oft_ts.push(Vec3u::new(i.x + n, i.y + n, i.z + n)); } - for i in 0..n { - let j = (i + 1) % n; - oft_ts.push(Vec3u::new(i, j, i + n)); - oft_ts.push(Vec3u::new(i + n, j, j + n)); - } - Manifold::new_impl(oft_ps, oft_ts, None, None) -} - -pub fn compose(ms: &Vec) -> Result { +pub fn compose(ms: &Vec) -> Result { let mut ps = vec![]; let mut ts = vec![]; let mut offset = 0; for m in ms { - for h in m.hs.iter() { ts.push(h.tail + offset); } + for h in m.hs.iter() { + ts.push(h.tail + offset); + } for p in m.ps.iter() { ps.push(p.x as f64); ps.push(p.y as f64); @@ -57,7 +38,7 @@ pub fn compose(ms: &Vec) -> Result { } pub fn fractal( - hole : &Manifold, + hole: &Manifold, holes: &mut Vec, x: f64, y: f64, @@ -71,17 +52,19 @@ pub fn fractal( m.translate(x, y, 0.); holes.push(m); - if depth == depth_max { return; } + if depth == depth_max { + return; + } for xy in [ (x - w, y - w), - (x - w, y ), + (x - w, y), (x - w, y + w), - (x , y + w), + (x, y + w), (x + w, y + w), - (x + w, y ), + (x + w, y), (x + w, y - w), - (x , y - w) + (x, y - w), ] { fractal(hole, holes, xy.0, xy.1, w, depth + 1, depth_max); } diff --git a/src/compose/sphere.rs b/src/compose/sphere.rs index e8c1919..cfb7073 100644 --- a/src/compose/sphere.rs +++ b/src/compose/sphere.rs @@ -1,15 +1,19 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. +use thiserror::Error; + +use crate::{manifold::ManifoldError, Manifold, Real, Vec3, Vec3u}; use std::collections::HashMap; use std::f64::consts::PI; -use crate::{Manifold, Vec3, Vec3u, Real}; pub fn generate_uv_sphere( d0: usize, // sectors d1: usize, // stacks -) -> Result { - if d0 < 3 || d1 < 2 { return Err("sectors must be >= 3 and stacks must be >= 2".into()); } +) -> Result { + if d0 < 3 || d1 < 2 { + return Err(UVSphereError::InvalidSectorCount); + } let mut ps = vec![]; let mut ts = vec![]; @@ -42,25 +46,27 @@ pub fn generate_uv_sphere( } } - Manifold::new_impl(ps, ts, None, None) + let manifold = Manifold::new_impl(ps, ts, None, None)?; + + Ok(manifold) } -pub fn generate_icosphere(subdivisions: u32) -> Result { +pub fn generate_icosphere(subdivisions: u32) -> Result { let phi = ((1. + 5.0f32.sqrt()) / 2.) as Real; let mut ps = vec![ - Vec3::new(-1.0, phi, 0.0).normalize(), - Vec3::new( 1.0, phi, 0.0).normalize(), - Vec3::new(-1.0, -phi, 0.0).normalize(), - Vec3::new( 1.0, -phi, 0.0).normalize(), - Vec3::new( 0.0, -1.0, phi).normalize(), - Vec3::new( 0.0, 1.0, phi).normalize(), - Vec3::new( 0.0, -1.0, -phi).normalize(), - Vec3::new( 0.0, 1.0, -phi).normalize(), - Vec3::new( phi, 0.0, -1.0).normalize(), - Vec3::new( phi, 0.0, 1.0).normalize(), - Vec3::new(-phi, 0.0, -1.0).normalize(), - Vec3::new(-phi, 0.0, 1.0).normalize(), + Vec3::new(-1.0, phi, 0.0).normalize(), + Vec3::new(1.0, phi, 0.0).normalize(), + Vec3::new(-1.0, -phi, 0.0).normalize(), + Vec3::new(1.0, -phi, 0.0).normalize(), + Vec3::new(0.0, -1.0, phi).normalize(), + Vec3::new(0.0, 1.0, phi).normalize(), + Vec3::new(0.0, -1.0, -phi).normalize(), + Vec3::new(0.0, 1.0, -phi).normalize(), + Vec3::new(phi, 0.0, -1.0).normalize(), + Vec3::new(phi, 0.0, 1.0).normalize(), + Vec3::new(-phi, 0.0, -1.0).normalize(), + Vec3::new(-phi, 0.0, 1.0).normalize(), ]; let mut ts = vec![ @@ -88,14 +94,18 @@ pub fn generate_icosphere(subdivisions: u32) -> Result { let mut cache = HashMap::new(); - let get_midpoint = | - vid1: usize, - vid2: usize, - verts: &mut Vec, - cache: &mut HashMap<(usize, usize), usize> - | { - let e = if vid1 < vid2 { (vid1, vid2) } else { (vid2, vid1) }; - if let Some(&i) = cache.get(&e) { return i; } + let get_midpoint = |vid1: usize, + vid2: usize, + verts: &mut Vec, + cache: &mut HashMap<(usize, usize), usize>| { + let e = if vid1 < vid2 { + (vid1, vid2) + } else { + (vid2, vid1) + }; + if let Some(&i) = cache.get(&e) { + return i; + } let v1 = verts[vid1]; let v2 = verts[vid2]; @@ -121,4 +131,13 @@ pub fn generate_icosphere(subdivisions: u32) -> Result { } Manifold::new_impl(ps, ts, None, None) -} \ No newline at end of file +} + +#[derive(Debug, Error)] +pub enum UVSphereError { + #[error("sectors must be >= 3 and stacks must be >= 2")] + InvalidSectorCount, + + #[error("{0}")] + Manifold(#[from] ManifoldError), +} diff --git a/src/compose/torus.rs b/src/compose/torus.rs index 3c9df5d..99c0044 100644 --- a/src/compose/torus.rs +++ b/src/compose/torus.rs @@ -1,16 +1,15 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. +use crate::{manifold::ManifoldError, Manifold, Real, Vec3, Vec3u}; use std::f64::consts::PI; -use crate::{Manifold, Vec3, Vec3u, Real}; pub fn generate_torus( r0: f64, // major radius r1: f64, // minor radius d0: usize, // rings d1: usize, // sectors -) -> Result { - +) -> Result { let mut ps = Vec::with_capacity(d0 * d1); let mut ts = Vec::with_capacity(d0 * d1 * 6); @@ -32,9 +31,9 @@ pub fn generate_torus( let ni = (i + 1) % d0; for j in 0..d1 { let nj = (j + 1) % d1; - let v0 = i * d1 + j; - let v1 = i * d1 + nj; - let v2 = ni * d1 + j; + let v0 = i * d1 + j; + let v1 = i * d1 + nj; + let v2 = ni * d1 + j; let v3 = ni * d1 + nj; ts.push(Vec3u::new(v0, v1, v2)); ts.push(Vec3u::new(v1, v3, v2)); @@ -42,4 +41,4 @@ pub fn generate_torus( } Manifold::new_impl(ps, ts, None, None) -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 81af9c8..56167f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,50 +5,49 @@ #![allow(clippy::cast_abs_to_unsigned)] #![allow(unused_braces)] -mod manifold; -mod triangulation; -mod simplification; -mod common; mod boolean03; mod boolean45; +mod common; mod compose; +mod manifold; +mod simplification; mod tests; +mod triangulation; + +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::VecDeque; + +use geo::{LineString, MultiPolygon, Polygon, Coord}; +use thiserror::Error; use crate::boolean03::boolean03; use crate::boolean45::boolean45; -use crate::simplification::simplify_topology; -use crate::triangulation::triangulate; use crate::common::*; +use crate::manifold::bounds::Query; use crate::manifold::*; +use crate::simplification::simplify_topology; +use crate::triangulation::triangulate; +use crate::triangulation::TriangulationError; -pub use crate::common::{Real, Vec2, Vec3, Vec4, Mat3, K_PRECISION}; +pub use crate::common::{Mat3, Real, Vec2, Vec3, Vec4, K_PRECISION}; pub mod prelude { pub use crate::common::OpType; - pub use crate::manifold::Manifold; - pub use crate::compute_boolean; pub use crate::compose::{ - compose, - fractal, - extrude, - generate_cone, - generate_cube, - generate_torus, - generate_cylinder, - generate_uv_sphere, - generate_icosphere, + compose, fractal, generate_cone, generate_cube, generate_cylinder, + generate_icosphere, generate_torus, generate_uv_sphere, ExtrudePoly, ExtrusionError, }; + pub use crate::compute_boolean; + pub use crate::manifold::Manifold; } -pub fn compute_boolean( - mp: &Manifold, - mq: &Manifold, - op: OpType, -) -> Result { +pub fn compute_boolean(mp: &Manifold, mq: &Manifold, op: OpType) -> Result { let eps = mp.eps.max(mq.eps); let tol = mp.tol.max(mq.tol); - let b03 = boolean03(mp, mq, &op); + let b03 = boolean03(mp, mq, &op); let mut b45 = boolean45(mp, mq, &b03, &op); let mut trg = triangulate(mp, mq, &b45, eps)?; @@ -59,20 +58,31 @@ pub fn compute_boolean( &mut trg.rs, b45.nv_from_p, b45.nv_from_q, - eps + eps, ); - cleanup_unused_verts( - &mut b45.ps, - &mut trg.hs - ); + cleanup_unused_verts(&mut b45.ps, &mut trg.hs); - Manifold::new_impl( + let manifold = Manifold::new_impl( b45.ps, - trg.hs.chunks(3).map(|hs| Vec3u::new(hs[0].tail, hs[1].tail, hs[2].tail)).collect(), + trg.hs + .chunks(3) + .map(|hs| Vec3u::new(hs[0].tail, hs[1].tail, hs[2].tail)) + .collect(), Some(eps), - Some(tol) - ) + Some(tol), + )?; + + Ok(manifold) +} + +#[derive(Debug, Error)] +pub enum BooleanError { + #[error("{0}")] + Trangulate(#[from] TriangulationError), + + #[error("{0}")] + Manifold(#[from] ManifoldError), } //pub fn compute_boolean_from_raw_data( @@ -93,7 +103,181 @@ pub fn compute_boolean( // compute_boolean(&mp, &mq, op) //} +#[derive(Debug, Error)] +pub enum ProjectionError { + #[error("No polygons were produced by the operation")] + NoPolygons, +} + +/// Projects the manifold onto the XY plane. Rotate the manifold to project onto custom planes. +/// projection. +/// * manifold - Input manifold to project +pub fn compute_projection(manifold: &Manifold) -> Result, ProjectionError> { + // TODO there should be a way to directly iterate triangles. + let mut edge_ids: BTreeMap> = BTreeMap::new(); + trait EdgeMap { + fn next_starting_edge(&self) -> Option; + fn next_edge(&mut self, manifold: &Manifold, current_edge_id: usize) -> Option; + } + impl EdgeMap for BTreeMap> { + fn next_starting_edge(&self) -> Option { + let (_edge_id, queue) = self.first_key_value()?; + let value = queue.back(); + value.copied() + } + + fn next_edge(&mut self, manifold: &Manifold, current_edge_id: usize) -> Option { + let current_key = manifold.hs[current_edge_id].head; + let queue = self.get_mut(¤t_key)?; + let value = queue.pop_back(); + if queue.is_empty() { + self.remove(¤t_key); + } + value + } + } + for (edge_id, edge) in manifold.hs.iter().enumerate() { + // This filters our faces so that only faces that are connected to another face that is on + // the opposite side of the manifold are included. This instantly gives us the edge + // boundaries. + if manifold.face_normals[manifold.hs[edge.pair].pair / 3].z <= 0.0 + && manifold.face_normals[edge.pair / 3].z > 0.0 { + edge_ids.entry(edge.tail).or_default().push_front(edge_id); + } + } + + let mut polygons = Vec::new(); + while let Some(first_edge_id) = edge_ids.next_starting_edge() { + let mut current_edge_id = first_edge_id; + let mut line_string = Vec::new(); + + loop { + let point = manifold.ps[manifold.hs[current_edge_id].head]; + line_string.push(Coord { x: point.x, y: point.y }); + + let next_edge_id = edge_ids.next_edge(manifold, current_edge_id).expect("Non-manafold edge"); + + if next_edge_id != first_edge_id { + current_edge_id = next_edge_id; + } else { + // We've come back to our initial point. + break; + } + } + + let mut line_string = LineString(line_string); + line_string.close(); + let raw_polygon = Polygon::new(line_string, vec![]); + + polygons.push(raw_polygon); + } + + let polygon = geo::unary_union(&polygons); + + if polygons.is_empty() { + Err(ProjectionError::NoPolygons) + } else { + Ok(polygon) + } +} + +#[derive(Debug, Error)] +pub enum SliceError { + #[error("No polygons were produced by the operation")] + NoPolygons, +} + +/// Slice a manifold into a 2D polygon +/// * manifold - Input manifold to slice +/// * height - z height to slice at +pub fn compute_slice(manifold: &Manifold, height: Real) -> Result, SliceError> { + let mut bounding_box = manifold.bounding_box.clone(); + bounding_box.min.z = height; + bounding_box.max.z = height; + bounding_box.id = Some(0); // Collider will not report collisions without this. + + let mut triangle_ids = BTreeSet::new(); + + manifold + .collider + .collision(&[Query::Bb(bounding_box)], &mut |_query_id, triangle_id| { + let z_points = [0, 1, 2] + .into_iter() + .map(|j| manifold.ps[manifold.hs[3 * triangle_id + j].tail].z); + + // We have to account for NaN with these min/max functions, so we're going to just + // filter out the NaNs. + let min = z_points + .clone() + .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Greater)); + let max = z_points.max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); + + // If the lowest point is below the height threashold, and the highest point is above, + // then this triangle intersects with the height plane. + if let (Some(min), Some(max)) = (min, max) && min <= height && max > height { + triangle_ids.insert(triangle_id); + } + }); + + // At this point, triangle_ids contains a list of triangles that intersect with the height + // plane. + fn next3(j: usize) -> usize { + (j + 1) % 3 + } + + let mut polygons = Vec::new(); + + while !triangle_ids.is_empty() { + let start_triangle_id = *triangle_ids.first().ok_or(SliceError::NoPolygons)?; + + let mut vertex_index = 0; + for j in [0, 1, 2] { + if manifold.ps[manifold.hs[3 * start_triangle_id + j].tail].z > height && + manifold.ps[manifold.hs[3 * start_triangle_id + next3(j)].tail].z <= height { + vertex_index = next3(j); + break; + } + } + + let mut line_string = Vec::new(); + let mut current_triangle_id = start_triangle_id; + loop { + triangle_ids.remove(¤t_triangle_id); + + if manifold.ps[manifold.hs[3 * current_triangle_id + vertex_index].head].z <= height { + vertex_index = next3(vertex_index); + } + + let up = &manifold.hs[3 * current_triangle_id + vertex_index]; + let below = manifold.ps[up.tail]; + let above = manifold.ps[up.head]; + let a = (height - below.z) / (above.z - below.z); + let point = below.lerp(above, a); + line_string.push(geo::Coord { x: point.x, y: point.y }); + + let pair = up.pair; + current_triangle_id = pair / 3; + vertex_index = next3(pair % 3); + + if current_triangle_id == start_triangle_id { + break; + } + } + + let mut line_string = LineString(line_string); + line_string.close(); + polygons.push(Polygon::new(line_string, vec![])); + } + + let polygon = geo::unary_union(&polygons); + + if polygons.is_empty() { + Err(SliceError::NoPolygons) + } else { + Ok(polygon) + } +} diff --git a/src/manifold/bounds.rs b/src/manifold/bounds.rs index 4d77dd7..7dbf64f 100644 --- a/src/manifold/bounds.rs +++ b/src/manifold/bounds.rs @@ -4,8 +4,12 @@ use crate::{Real, Vec2, Vec3}; #[derive(Clone, Debug)] -pub enum Query { Bb(BBox), Pt(BPos) } +pub enum Query { + Bb(BBox), + Pt(BPos), +} +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct BBox { pub id: Option, @@ -21,16 +25,28 @@ pub struct BPos { impl BBox { pub fn default() -> Self { - BBox { id: None, min: Vec3::MAX, max: Vec3::MIN } + BBox { + id: None, + min: Vec3::MAX, + max: Vec3::MIN, + } } - + pub fn new(id: Option, pts: &[Vec3]) -> Self { - let mut b = BBox { id, min: Vec3::MAX, max: Vec3::MIN }; - for pt in pts { b.union(pt); } + let mut b = BBox { + id, + min: Vec3::MAX, + max: Vec3::MIN, + }; + for pt in pts { + b.union(pt); + } b } - pub fn size(&self) -> Vec3 { self.max - self.min } + pub fn size(&self) -> Vec3 { + self.max - self.min + } pub fn scale(&self) -> Real { let s = self.size(); @@ -40,33 +56,38 @@ impl BBox { pub fn overlaps(&self, q: &Query) -> bool { match q { Query::Bb(b) => self.min.cmple(b.max).all() && self.max.cmpge(b.min).all(), - Query::Pt(p) => { // only evaluates xy axis - self.min.x <= p.pos.x && self.min.y <= p.pos.y && - self.max.x >= p.pos.x && self.max.y >= p.pos.y + Query::Pt(p) => { + // only evaluates xy axis + self.min.x <= p.pos.x + && self.min.y <= p.pos.y + && self.max.x >= p.pos.x + && self.max.y >= p.pos.y } } } pub fn union(&mut self, p: &Vec3) { - if p.x.is_nan() { return; } + if p.x.is_nan() { + return; + } self.min = self.min.min(*p); self.max = self.max.max(*p); } pub fn longest_dim(&self) -> usize { let s = self.size(); - if s.x > s.y && s.x > s.z { 0 } - else if s.y > s.z { 1 } - else { 2 } + if s.x > s.y && s.x > s.z { + 0 + } else if s.y > s.z { + 1 + } else { + 2 + } } } - pub fn union_bbs(b0: &BBox, b1: &BBox) -> BBox { let min = b0.min.min(b1.min); let max = b0.max.max(b1.max); BBox { id: None, min, max } } - - - diff --git a/src/manifold/collider.rs b/src/manifold/collider.rs index c5813f6..eb4ffc6 100644 --- a/src/manifold/collider.rs +++ b/src/manifold/collider.rs @@ -125,6 +125,7 @@ fn build_internal_boxes( } } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct MortonCollider { pub node_bb: Vec, diff --git a/src/manifold/hmesh.rs b/src/manifold/hmesh.rs index ce0e6b0..649620a 100644 --- a/src/manifold/hmesh.rs +++ b/src/manifold/hmesh.rs @@ -2,9 +2,11 @@ //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. #![allow(clippy::needless_range_loop)] +use crate::{Real, Vec2u, Vec3, Vec3u}; +#[cfg(feature = "rayon")] +use rayon::prelude::*; use std::f64::consts::PI; -use crate::{Vec3, Vec2u, Vec3u, Real}; -#[cfg(feature = "rayon")] use rayon::prelude::*; +use thiserror::Error; /// Hmesh preserves the order of pos and idx in any cases. /// Edges are ordered so as the edge is forward (tail idx < head idx) @@ -27,24 +29,33 @@ fn edge_topology( e2v: &mut Vec, e2f: &mut Vec, f2e: &mut Vec, -) -> Result<(), String> { - if pos.is_empty() { return Err("empty pos matrix".into()); } - if idx.is_empty() { return Err("empty idx matrix".into()); } +) -> Result<(), HmeshError> { + if pos.is_empty() { + return Err(HmeshError::EmptyPositionMatrix); + } + if idx.is_empty() { + return Err(HmeshError::EmptyIndexMatrix); + } let mut ett: Vec<[usize; 4]> = vec![]; for (i, idx_) in idx.iter().enumerate() { - for j in 0..3 { - let mut v1 = idx_[j]; - let mut v2 = idx_[(j + 1) % 3]; - if v1 > v2 { std::mem::swap(&mut v1, &mut v2); } - ett.push([v1, v2, i, j]); - }} + for j in 0..3 { + let mut v1 = idx_[j]; + let mut v2 = idx_[(j + 1) % 3]; + if v1 > v2 { + std::mem::swap(&mut v1, &mut v2); + } + ett.push([v1, v2, i, j]); + } + } ett.sort(); let mut ne = 1; for i in 0..ett.len() - 1 { - if !(ett[i][0] == ett[i + 1][0] && ett[i][1] == ett[i + 1][1]) { ne += 1; } + if !(ett[i][0] == ett[i + 1][0] && ett[i][1] == ett[i + 1][1]) { + ne += 1; + } } e2v.resize(ne, Vec2u::MAX); @@ -54,13 +65,13 @@ fn edge_topology( let mut i = 0; while i < ett.len() { - if i == ett.len() - 1 || !((ett[i][0] == ett[i+1][0]) && (ett[i][1] == ett[i + 1][1])) { + if i == ett.len() - 1 || !((ett[i][0] == ett[i + 1][0]) && (ett[i][1] == ett[i + 1][1])) { // Border edge let [v1, v2, i, j] = ett[i]; e2v[ne][0] = v1; e2v[ne][1] = v2; e2f[ne][0] = i; - f2e[i][j] = ne; + f2e[i][j] = ne; } else { let r1 = ett[i]; let r2 = ett[i + 1]; @@ -95,10 +106,7 @@ fn edge_topology( } impl Hmesh { - pub fn new( - pos: &[Vec3], - idx: &[Vec3u], - ) -> Result { + pub fn new(pos: &[Vec3], idx: &[Vec3u]) -> Result { let mut e2v = Default::default(); let mut e2f = Default::default(); let mut f2e = Default::default(); @@ -109,9 +117,9 @@ impl Hmesh { let ne = e2v.len(); let nh = e2v.len() * 2; let np = 3; - let mut v2h = vec![usize::MAX; nv]; - let mut e2h = vec![usize::MAX; ne]; - let mut f2h = vec![usize::MAX; nf]; + let mut v2h = vec![usize::MAX; nv]; + let mut e2h = vec![usize::MAX; ne]; + let mut f2h = vec![usize::MAX; nf]; let mut next = vec![usize::MAX; nh]; let mut prev = vec![usize::MAX; nh]; let mut twin = vec![usize::MAX; nh]; @@ -121,45 +129,53 @@ impl Hmesh { let mut face = vec![usize::MAX; nh]; for it in 0..nf { - for ip in 0..np { - let ih_bgn = it * np; - let iv = idx[it][ip]; - let ie = f2e[it][ip]; - let ih = ih_bgn + ip; - next[ih] = ih_bgn + (ip + 1) % np; - prev[ih] = ih_bgn + (ip + np - 1) % np; - head[ih] = idx[it][(ip + 1) % np]; - tail[ih] = iv; - edge[ih] = ie; - face[ih] = it; - if f2h[it] == usize::MAX { f2h[it] = ih; } - if v2h[iv] == usize::MAX { v2h[iv] = ih; } - if e2h[ie] == usize::MAX { e2h[ie] = ih; } - else { - twin[ih] = e2h[ie]; - twin[e2h[ie]] = ih; + for ip in 0..np { + let ih_bgn = it * np; + let iv = idx[it][ip]; + let ie = f2e[it][ip]; + let ih = ih_bgn + ip; + next[ih] = ih_bgn + (ip + 1) % np; + prev[ih] = ih_bgn + (ip + np - 1) % np; + head[ih] = idx[it][(ip + 1) % np]; + tail[ih] = iv; + edge[ih] = ie; + face[ih] = it; + if f2h[it] == usize::MAX { + f2h[it] = ih; + } + if v2h[iv] == usize::MAX { + v2h[iv] = ih; + } + if e2h[ie] == usize::MAX { + e2h[ie] = ih; + } else { + twin[ih] = e2h[ie]; + twin[e2h[ie]] = ih; + } } - }} + } if twin.iter().any(|v| v == &usize::MAX) { - return Err("Input mesh must not contain boundary edges.".into()); + return Err(HmeshError::ContainsBoundaryEdges); } let mut half = vec![]; - for i in 0..nh { half.push(i); } + for i in 0..nh { + half.push(i); + } let mut vns = vec![Vec3::ZERO; nv]; let mut fns = vec![Vec3::ZERO; nf]; #[cfg(feature = "rayon")] fns.par_iter_mut().enumerate().for_each(|(i, n)| { - let ih = f2h[i]; - let p2 = pos[head[ih]]; - let p1 = pos[tail[ih]]; - let p0 = pos[tail[prev[ih]]]; - let x = p2 - p1; - let t = (p1 - p0) * -1.; - *n = x.cross(t).normalize(); - }); + let ih = f2h[i]; + let p2 = pos[head[ih]]; + let p1 = pos[tail[ih]]; + let p0 = pos[tail[prev[ih]]]; + let x = p2 - p1; + let t = (p1 - p0) * -1.; + *n = x.cross(t).normalize(); + }); #[cfg(not(feature = "rayon"))] for i in 0..nf { @@ -173,28 +189,58 @@ impl Hmesh { } for i in 0..nf { - for j in 0..3 { - let i_curr = idx[i][j]; - let v_prev = pos[idx[i][(j + 2) % 3]]; - let v_curr = pos[i_curr]; - let v_next = pos[idx[i][(j + 1) % 3]]; - let e_curr = (v_next - v_curr).normalize(); - let e_prev = (v_curr - v_prev).normalize(); - if e_curr.is_nan() || e_prev.is_nan() { continue; } - let dot = -e_prev.dot(e_curr); - let phi = if dot >= 1. { 0. } - else if dot <= -1. { PI as Real } - else { dot.acos() }; - vns[i_curr] += fns[i] * phi; - }} - + for j in 0..3 { + let i_curr = idx[i][j]; + let v_prev = pos[idx[i][(j + 2) % 3]]; + let v_curr = pos[i_curr]; + let v_next = pos[idx[i][(j + 1) % 3]]; + let e_curr = (v_next - v_curr).normalize(); + let e_prev = (v_curr - v_prev).normalize(); + if e_curr.is_nan() || e_prev.is_nan() { + continue; + } + let dot = -e_prev.dot(e_curr); + let phi = if dot >= 1. { + 0. + } else if dot <= -1. { + PI as Real + } else { + dot.acos() + }; + vns[i_curr] += fns[i] * phi; + } + } #[cfg(feature = "rayon")] vns.par_iter_mut().for_each(|n| *n = n.normalize_or_zero()); #[cfg(not(feature = "rayon"))] - for n in &mut vns { *n = n.normalize_or_zero(); } + for n in &mut vns { + *n = n.normalize_or_zero(); + } - Ok(Hmesh{ nv, nf, nh, twin, head, tail, half, vns, fns }) + Ok(Hmesh { + nv, + nf, + nh, + twin, + head, + tail, + half, + vns, + fns, + }) } } + +#[derive(Debug, Error)] +pub enum HmeshError { + #[error("empty pos matrix")] + EmptyPositionMatrix, + + #[error("empty idx matrix")] + EmptyIndexMatrix, + + #[error("Input mesh must not contain boundary edges.")] + ContainsBoundaryEdges, +} diff --git a/src/manifold/mod.rs b/src/manifold/mod.rs index 9dc5c8e..1b80473 100644 --- a/src/manifold/mod.rs +++ b/src/manifold/mod.rs @@ -1,18 +1,22 @@ //--- Copyright (C) 2025 Saki Komikado , //--- This Source Code Form is subject to the terms of the Mozilla Public License v.2.0. -pub mod hmesh; pub mod bounds; pub mod collider; +pub mod hmesh; +use super::hmesh::Hmesh; +use crate::collider::{morton_code, MortonCollider, K_NO_CODE}; +use crate::manifold::hmesh::HmeshError; +use crate::{next_of, Half, Mat3, Real, Vec3, Vec3u, K_PRECISION}; +use bounds::BBox; +#[cfg(feature = "rayon")] +use rayon::prelude::*; use std::cmp::Ordering; use std::collections::HashMap; -use bounds::BBox; -use crate::collider::{morton_code, MortonCollider, K_NO_CODE}; -use crate::{Real, Half, Vec3, Vec3u, K_PRECISION, next_of, Mat3}; -use super::hmesh::Hmesh; -#[cfg(feature = "rayon")] use rayon::prelude::*; +use thiserror::Error; +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug)] pub struct Manifold { pub ps: Vec, // positions @@ -31,21 +35,25 @@ pub struct Manifold { } impl Manifold { - pub fn new(pos: &[f64], idx: &[usize]) -> Result { - - if pos.len() % 3 != 0 { return Err("pos must be a multiple of 3".into()); } - if idx.len() % 3 != 0 { return Err("idx must be a multiple of 3".into()); } + pub fn new(pos: &[f64], idx: &[usize]) -> Result { + if pos.len() % 3 != 0 { + return Err(ManifoldError::PositionArrayNotMultipleOf3); + } + if idx.len() % 3 != 0 { + return Err(ManifoldError::IndexArrayNotMultipleOf3); + } // dedup vertices - let mut hash = HashMap::with_capacity(pos.len() / 3); - let mut weld = Vec::with_capacity(pos.len() / 3); + let mut hash = HashMap::with_capacity(pos.len() / 3); + let mut weld = Vec::with_capacity(pos.len() / 3); let mut rmap = vec![0; pos.len()]; for (i, p) in pos.chunks(3).enumerate() { let v = Vec3::new(p[0] as Real, p[1] as Real, p[2] as Real); let k = (v.x.to_bits(), v.y.to_bits(), v.z.to_bits()); - if let Some(&w) = hash.get(&k) { rmap[i] = w; } - else { + if let Some(&w) = hash.get(&k) { + rmap[i] = w; + } else { let n = weld.len(); weld.push(v); hash.insert(k, n); @@ -64,15 +72,19 @@ impl Manifold { } pub fn new_impl( - ps : Vec, + ps: Vec, idx: Vec, eps: Option, tol: Option, - ) -> Result { + ) -> Result { let bb = BBox::new(None, &ps); let (mut f_bb, mut f_mt) = compute_face_morton(&ps, &idx, &bb); let hm = sort_faces(&ps, &idx, &mut f_bb, &mut f_mt)?; - let hs = hm.half.iter().map(|&i| Half::new(hm.tail[i], hm.head[i], hm.twin[i])).collect::>(); + let hs = hm + .half + .iter() + .map(|&i| Half::new(hm.tail[i], hm.head[i], hm.twin[i])) + .collect::>(); let mut e = K_PRECISION * bb.scale(); e = if e.is_finite() { e } else { -1. }; @@ -97,28 +109,39 @@ impl Manifold { coplanar, }; - if !mfd.is_manifold() { return Err("The input mesh is not manifold".into()); } + if !mfd.is_manifold() { + return Err(ManifoldError::InputNotManifold); + } Ok(mfd) } pub fn get_indices(&self) -> Vec { - self.hs.chunks(3).map(|cs| Vec3u::new(cs[0].tail, cs[1].tail, cs[2].tail)).collect() + self.hs + .chunks(3) + .map(|cs| Vec3u::new(cs[0].tail, cs[1].tail, cs[2].tail)) + .collect() } pub fn set_epsilon(&mut self, min_epsilon: Real, use_single: bool) { let scl = self.bounding_box.scale(); let mut e = min_epsilon.max(K_PRECISION * scl); e = if e.is_finite() { e } else { -1. }; - let t = if use_single { e.max(Real::EPSILON * scl) } else { e }; + let t = if use_single { + e.max(Real::EPSILON * scl) + } else { + e + }; self.eps = e; self.tol = self.tol.max(t); } pub fn is_manifold(&self) -> bool { self.hs.iter().enumerate().all(|(i, h)| { - if h.tail().is_none() || h.head().is_none() { return true; } + if h.tail().is_none() || h.head().is_none() { + return true; + } match h.pair() { - None => { false }, + None => false, Some(pair) => { let mut good = true; good &= self.hs[pair].pair() == Some(i); @@ -144,21 +167,37 @@ impl Manifold { } pub fn scale(&mut self, x: f64, y: f64, z: f64) { - let p = self.ps.iter().map(|p| Vec3::new(p.x * x as Real, p.y * y as Real, p.z * z as Real)).collect(); + let p = self + .ps + .iter() + .map(|p| Vec3::new(p.x * x as Real, p.y * y as Real, p.z * z as Real)) + .collect(); *self = Manifold::new_impl(p, self.get_indices(), None, None).unwrap(); } } -fn compute_face_morton( - pos: &[Vec3], - idx: &[Vec3u], - bb: &BBox -) -> (Vec, Vec) { +#[derive(Debug, Error)] +pub enum ManifoldError { + #[error("The input mesh is not manifold")] + InputNotManifold, + + #[error("pos must be a multiple of 3")] + PositionArrayNotMultipleOf3, + + #[error("idx must be a multiple of 3")] + IndexArrayNotMultipleOf3, + + #[error("Failed to construct Hmesh: {0:?}")] + Hmesh(#[from] HmeshError), +} + +fn compute_face_morton(pos: &[Vec3], idx: &[Vec3u], bb: &BBox) -> (Vec, Vec) { let n = idx.len(); let mut bbs = vec![BBox::default(); n]; let mut mts = vec![0; n]; - #[cfg(feature = "rayon")] { + #[cfg(feature = "rayon")] + { bbs.par_iter_mut() .zip(mts.par_iter_mut()) .zip(idx.par_iter()) @@ -173,7 +212,8 @@ fn compute_face_morton( }); } - #[cfg(not(feature = "rayon"))] { + #[cfg(not(feature = "rayon"))] + { for (i, f) in idx.iter().enumerate() { let p0 = pos[f.x]; let p1 = pos[f.y]; @@ -185,7 +225,6 @@ fn compute_face_morton( } } - (bbs, mts) } @@ -193,29 +232,30 @@ fn sort_faces( pos: &[Vec3], idx: &[Vec3u], face_bboxes: &mut Vec, - face_morton: &mut Vec -) -> Result { + face_morton: &mut Vec, +) -> Result { let mut map = (0..face_morton.len()).collect::>(); map.sort_by_key(|&i| face_morton[i]); - *face_bboxes = map.iter().map(|&i| face_bboxes[i].clone()).collect::>(); + *face_bboxes = map + .iter() + .map(|&i| face_bboxes[i].clone()) + .collect::>(); *face_morton = map.iter().map(|&i| face_morton[i]).collect::>(); - Hmesh::new(pos, &map.iter().map(|&i| idx[i]).collect::>()) + let hmesh = Hmesh::new(pos, &map.iter().map(|&i| idx[i]).collect::>())?; + Ok(hmesh) } -fn compute_coplanar_idx( - ps: &[Vec3], - ns: &[Vec3], - hs: &[Half], - tol: Real -) -> Vec { +fn compute_coplanar_idx(ps: &[Vec3], ns: &[Vec3], hs: &[Half], tol: Real) -> Vec { let nt = hs.len() / 3; let mut priority = vec![]; let mut res = vec![-1; nt]; for t in 0..nt { let i = t * 3; - let area = if hs[i].tail().is_none() { 0.} else { + let area = if hs[i].tail().is_none() { + 0. + } else { let p0 = ps[hs[i].tail]; let p1 = ps[hs[i].head]; let p2 = ps[hs[i + 1].head]; @@ -228,7 +268,9 @@ fn compute_coplanar_idx( let mut interior = vec![]; for (_, t) in priority.iter() { - if res[*t] != -1 { continue; } + if res[*t] != -1 { + continue; + } res[*t] = *t as i32; let i = t * 3; @@ -242,12 +284,17 @@ fn compute_coplanar_idx( let h1 = next_of(hs[hi].pair); let t1 = h1 / 3; - if res[t1] != -1 { continue; } + if res[t1] != -1 { + continue; + } if (ps[hs[h1].head] - p).dot(n).abs() < tol { res[t1] = *t as i32; - if interior.last().copied() == Some(hs[h1].pair) { interior.pop(); } - else { interior.push(h1); } + if interior.last().copied() == Some(hs[h1].pair) { + interior.pop(); + } else { + interior.push(h1); + } interior.push(next_of(h1)); } } @@ -255,21 +302,22 @@ fn compute_coplanar_idx( res } -pub fn cleanup_unused_verts( - ps: &mut Vec, - hs: &mut Vec -) { +pub fn cleanup_unused_verts(ps: &mut Vec, hs: &mut Vec) { let bb = BBox::new(None, ps); let mt = ps.iter().map(|p| morton_code(p, &bb)).collect::>(); let mut new2old = (0..ps.len()).collect::>(); let mut old2new = vec![0; ps.len()]; new2old.sort_by_key(|&i| mt[i]); - for (new, &old) in new2old.iter().enumerate() { old2new[old] = new; } + for (new, &old) in new2old.iter().enumerate() { + old2new[old] = new; + } // reindex verts for h in hs.iter_mut() { - if h.pair().is_none() { continue; } + if h.pair().is_none() { + continue; + } h.tail = old2new[h.tail]; h.head = old2new[h.head]; } @@ -285,4 +333,3 @@ pub fn cleanup_unused_verts( *ps = new2old.iter().map(|&i| ps[i]).collect(); *hs = hs.iter().filter(|h| h.pair().is_some()).cloned().collect(); } - diff --git a/src/triangulation/mod.rs b/src/triangulation/mod.rs index 2455098..f23fd4d 100644 --- a/src/triangulation/mod.rs +++ b/src/triangulation/mod.rs @@ -5,13 +5,18 @@ pub mod ear_clip; pub mod flat_tree; pub mod tri_halfs; -use std::collections::{BTreeMap, VecDeque}; use crate::boolean45::Boolean45; -use crate::{Manifold, Vec2, Vec3, Vec3u, Half, Tref, get_aa_proj_matrix, compute_aa_proj, is_ccw_3d, Real}; use crate::triangulation::ear_clip::EarClip; +#[cfg(feature = "rayon")] +use crate::triangulation::tri_halfs::tri_halfs_multi; use crate::triangulation::tri_halfs::tri_halfs_single; -#[cfg(feature = "rayon")] use rayon::prelude::*; -#[cfg(feature = "rayon")] use crate::triangulation::tri_halfs::tri_halfs_multi; +use crate::{ + compute_aa_proj, get_aa_proj_matrix, is_ccw_3d, Half, Manifold, Real, Tref, Vec2, Vec3, Vec3u, +}; +#[cfg(feature = "rayon")] +use rayon::prelude::*; +use std::collections::{BTreeMap, VecDeque}; +use thiserror::Error; pub struct Triangulation { pub hs: Vec, @@ -24,9 +29,9 @@ pub fn triangulate( mq: &Manifold, b45: &Boolean45, eps: Real, -) -> Result { - - #[cfg(feature = "rayon")] { +) -> Result { + #[cfg(feature = "rayon")] + { let (mut ts, mut rs, ns) = (0..b45.hid_per_f.len() - 1) .into_par_iter() .map(|fid| { @@ -46,10 +51,15 @@ pub fn triangulate( }, ); update_reference(mp, mq, &mut rs); - Ok(Triangulation { hs: tri_halfs_multi(&mut ts), ns, rs }) + Ok(Triangulation { + hs: tri_halfs_multi(&mut ts), + ns, + rs, + }) } - #[cfg(not(feature = "rayon"))] { + #[cfg(not(feature = "rayon"))] + { let mut ts = vec![]; let mut ns = vec![]; let mut rs = vec![]; @@ -64,21 +74,20 @@ pub fn triangulate( ts.extend(t); } update_reference(mp, mq, &mut rs); - Ok(Triangulation { hs: tri_halfs_single(&ts), ns, rs }) + Ok(Triangulation { + hs: tri_halfs_single(&ts), + ns, + rs, + }) } - } -fn process_face( - b45: &Boolean45, - fid: usize, - eps: Real -) -> Vec { +fn process_face(b45: &Boolean45, fid: usize, eps: Real) -> Vec { let e0 = b45.hid_per_f[fid] as usize; let e1 = b45.hid_per_f[fid + 1] as usize; match e1 - e0 { - 3 => single_triangulate(b45, e0), - 4 => square_triangulate(b45, fid, eps), + 3 => single_triangulate(b45, e0), + 4 => square_triangulate(b45, fid, eps), _ => general_triangulate(b45, fid, eps), } } @@ -99,7 +108,9 @@ fn assemble_halfs(hs: &[Half], hid_f: &[i32], fid: usize) -> Vec> { let mut hid1 = 0; loop { if hid1 == hid0 { - if v2h.is_empty() { break; } + if v2h.is_empty() { + break; + } hid0 = v2h.first_entry().unwrap().get().back().copied().unwrap(); hid1 = hid0; loops.push(Vec::new()); @@ -111,10 +122,7 @@ fn assemble_halfs(hs: &[Half], hid_f: &[i32], fid: usize) -> Vec> { loops } -fn single_triangulate( - b45: &Boolean45, - hid: usize -) -> Vec { +fn single_triangulate(b45: &Boolean45, hid: usize) -> Vec { let mut idcs = [hid, hid + 1, hid + 2]; let mut tails = vec![]; let mut heads = vec![]; @@ -122,7 +130,9 @@ fn single_triangulate( tails.push(b45.hs[*id].tail); heads.push(b45.hs[*id].head); } - if heads[0] == tails[2] { idcs.swap(1, 2); } + if heads[0] == tails[2] { + idcs.swap(1, 2); + } vec![Vec3u::new( b45.hs[idcs[0]].tail, @@ -131,18 +141,14 @@ fn single_triangulate( )] } -fn square_triangulate( - b45: &Boolean45, - fid: usize, - eps: Real -) -> Vec { +fn square_triangulate(b45: &Boolean45, fid: usize, eps: Real) -> Vec { let ccw = |tri: Vec3u| { is_ccw_3d( &b45.ps[b45.hs[tri[0]].tail], &b45.ps[b45.hs[tri[1]].tail], &b45.ps[b45.hs[tri[2]].tail], &b45.ns[fid], - eps + eps, ) >= 0 }; @@ -158,57 +164,57 @@ fn square_triangulate( } else if ccw(tris[1][0]) && ccw(tris[1][1]) { let diag0 = b45.ps[b45.hs[q[0]].tail] - b45.ps[b45.hs[q[2]].tail]; let diag1 = b45.ps[b45.hs[q[1]].tail] - b45.ps[b45.hs[q[3]].tail]; - if diag0.length() > diag1.length() { choice = 1; } + if diag0.length() > diag1.length() { + choice = 1; + } } - tris[choice].iter().map(|t| Vec3u::new( - b45.hs[t.x].tail, - b45.hs[t.y].tail, - b45.hs[t.z].tail - )).collect() + tris[choice] + .iter() + .map(|t| Vec3u::new(b45.hs[t.x].tail, b45.hs[t.y].tail, b45.hs[t.z].tail)) + .collect() } -fn general_triangulate( - b45: &Boolean45, - fid: usize, - eps: Real -) -> Vec { - let proj = get_aa_proj_matrix(&b45.ns[fid]); +fn general_triangulate(b45: &Boolean45, fid: usize, eps: Real) -> Vec { + let proj = get_aa_proj_matrix(&b45.ns[fid]); let loops = assemble_halfs(&b45.hs, &b45.hid_per_f, fid); - let polys = loops.iter().map(|poly| - poly.iter().map(|&e| { - let i = b45.hs[e].tail; - let p = compute_aa_proj(&proj, &b45.ps[i]); - Pt { pos: p, idx: e } - }).collect() - ).collect::>>(); - - EarClip::new(&polys, eps).triangulate().iter().map(|t| Vec3u::new( - b45.hs[t.x].tail, - b45.hs[t.y].tail, - b45.hs[t.z].tail - )).collect() + let polys = loops + .iter() + .map(|poly| { + poly.iter() + .map(|&e| { + let i = b45.hs[e].tail; + let p = compute_aa_proj(&proj, &b45.ps[i]); + Pt { pos: p, idx: e } + }) + .collect() + }) + .collect::>>(); + + EarClip::new(&polys, eps) + .triangulate() + .iter() + .map(|t| Vec3u::new(b45.hs[t.x].tail, b45.hs[t.y].tail, b45.hs[t.z].tail)) + .collect() } - #[derive(Debug, Clone)] pub struct Pt { pub pos: Vec2, - pub idx: usize + pub idx: usize, } -fn update_reference( - mp: &Manifold, - mq: &Manifold, - rs: &mut[Tref], -) { +fn update_reference(mp: &Manifold, mq: &Manifold, rs: &mut [Tref]) { for r in rs.iter_mut() { let fid = r.fid; - let pq = r.mid == 0; - r.pid = if pq { mp.coplanar[fid] } else { mq.coplanar[fid] }; + let pq = r.mid == 0; + r.pid = if pq { + mp.coplanar[fid] + } else { + mq.coplanar[fid] + }; } } - - - +#[derive(Debug, Error)] +pub enum TriangulationError {}