diff --git a/Cargo.lock b/Cargo.lock index f6b9931..e605c0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "approx" @@ -356,9 +356,9 @@ checksum = "467163b50876d3a4a44da5f4dbd417537e522fc059ede8d518d57941cfb3d745" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" @@ -374,63 +374,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a01cd51a5cd310e4e7aa6e1560b1aabf29efc6a095a01e6daa8bf0a19f1fea" +checksum = "bb2a21c9f3306676077a88700bb8f354be779cf9caba9c21e94da9e696751af4" dependencies = [ "bevy_internal", ] -[[package]] -name = "bevy-inspector-egui" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd64580f4496ed987c6231c6a7d833068914331a9084bf5a3dd9dcbc66fd8a73" -dependencies = [ - "bevy-inspector-egui-derive", - "bevy_app", - "bevy_asset", - "bevy_color", - "bevy_core", - "bevy_core_pipeline", - "bevy_ecs", - "bevy_egui", - "bevy_hierarchy", - "bevy_image", - "bevy_log", - "bevy_math", - "bevy_pbr", - "bevy_reflect", - "bevy_render", - "bevy_state", - "bevy_time", - "bevy_utils", - "bevy_window", - "bytemuck", - "disqualified", - "egui", - "fuzzy-matcher", - "image 0.25.2", - "smallvec", - "winit", -] - -[[package]] -name = "bevy-inspector-egui-derive" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afc67826e0a4347414545e022e748f42550a577a502b26af44e6d03742c9266" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "bevy_a11y" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c66b5bc82a2660a5663d85b3354ddb72c8ab2c443989333cbea146f39a4e9a" +checksum = "f96642402d2cd7c8e58c5994bbd14a2e44ca72dd7e460a2edad82aa3bf0348f9" dependencies = [ "accesskit", "bevy_app", @@ -441,9 +396,9 @@ dependencies = [ [[package]] name = "bevy_animation" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48f3fc65f583e5e320e38874053e20e7a71205a62aaace5d607446781bd742" +checksum = "03064ab96e15b2fda5bd58eac2055692d731c1fba3e211fd1ba48472cced75c3" dependencies = [ "bevy_app", "bevy_asset", @@ -473,9 +428,9 @@ dependencies = [ [[package]] name = "bevy_app" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652574e4c10efcfa70f98036709dd5b67e5cb8d46c58087ef48c2ac6b62df9da" +checksum = "454a8cfd134864dcdcba6ee56fb958531b981021bba6bb2037c9e3df6046603c" dependencies = [ "bevy_derive", "bevy_ecs", @@ -492,9 +447,9 @@ dependencies = [ [[package]] name = "bevy_asset" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d7d501eda01be6d500d843a06d9b9800c3f0fffaae3c29d17d9e4e172c28d37" +checksum = "2d762dd4422fb6219fd904e514a4a5d1d451711a0a8e1d6495dea15a545f04f3" dependencies = [ "async-broadcast", "async-fs", @@ -529,9 +484,9 @@ dependencies = [ [[package]] name = "bevy_asset_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7474b77fc27db11ec03d49ca04f1a7471f369dc373fd5e091a12ad7ab533d8c8" +checksum = "8db6957e3f9649d415ee613901cf487898d0339455aa9c3a2525fc37facee920" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -541,9 +496,9 @@ dependencies = [ [[package]] name = "bevy_audio" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e378c4005d9c47b7ebaf637a6a197e3953463615516ab709ba8b0c3c215c2e" +checksum = "13d13715401172d7616b376362a46bba125ec9ccc73ab262153a43a2402537ca" dependencies = [ "bevy_app", "bevy_asset", @@ -560,9 +515,9 @@ dependencies = [ [[package]] name = "bevy_color" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87bccacba27db37375eb97ffc86e91a7d95db3f5faa6a834fa7306db02cde327" +checksum = "f00aa2966c7ca0c7dd39f5ba8f3b1eaa5c2005a93ffdefb7a4090150d8327678" dependencies = [ "bevy_math", "bevy_reflect", @@ -575,9 +530,9 @@ dependencies = [ [[package]] name = "bevy_core" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecccf7be33330f58d4c7033b212a25c414d388e3a8d55b61331346da5dbabf22" +checksum = "0ff28118f5ae3193f7f6cab30d4fd4246ba1802776910ab256dc7c20e8696381" dependencies = [ "bevy_app", "bevy_ecs", @@ -589,9 +544,9 @@ dependencies = [ [[package]] name = "bevy_core_pipeline" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3fb9f84fa60c2006d4a15e039c3d08d4d10599441b9175907341a77a69d627" +checksum = "c0c0eea548a55fd04acf01d351bd16da4d1198037cb9c7b98dea6519f5d7dade" dependencies = [ "bevy_app", "bevy_asset", @@ -616,9 +571,9 @@ dependencies = [ [[package]] name = "bevy_derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e141b7eda52a23bb88740b37a291e26394524cb9ee3b034c7014669671fc2bb5" +checksum = "b962df2a1bef274ae76ec75279eb6f8ef0ffd85b5e4c43433f5d08ba57b3d071" dependencies = [ "bevy_macro_utils", "quote", @@ -627,9 +582,9 @@ dependencies = [ [[package]] name = "bevy_diagnostic" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa97748337405089edfb2857f7608f21bcc648a7ad272c9209808aad252ed542" +checksum = "21fe41b22fdf47bf11f0a3ca3e61975b003e86fa44d87e070f2dc7e752dd99f5" dependencies = [ "bevy_app", "bevy_core", @@ -643,9 +598,9 @@ dependencies = [ [[package]] name = "bevy_ecs" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4c4b60d2a712c6d5cbe610bac7ecf0838fc56a095fd5b15f30230873e84f15" +checksum = "b747210d7db09dfacc237707d4fd31c8b43d7744cd5e5829e2c4ca86b9e47baf" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -666,9 +621,9 @@ dependencies = [ [[package]] name = "bevy_ecs_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4296b3254b8bd29769f6a4512731b2e6c4b163343ca18b72316927315b6096" +checksum = "0d36ba5874ee278d20f17b8934d2969f8fbab90f3ea3fcf8d3583814b3661ada" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -678,9 +633,9 @@ dependencies = [ [[package]] name = "bevy_egui" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954fbe8551af4b40767ea9390ec7d32fe1070a6ab55d524cf0868c17f8469a55" +checksum = "6a4b8df063d7c4d4171bc853e5ea0d67c7f1b5edd3b014d43acbfe3042dd6cf4" dependencies = [ "arboard", "bevy_app", @@ -691,6 +646,7 @@ dependencies = [ "bevy_input", "bevy_log", "bevy_math", + "bevy_picking", "bevy_reflect", "bevy_render", "bevy_time", @@ -702,7 +658,6 @@ dependencies = [ "egui", "encase", "js-sys", - "log", "thread_local", "wasm-bindgen", "wasm-bindgen-futures", @@ -714,9 +669,9 @@ dependencies = [ [[package]] name = "bevy_encase_derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe562b883fb652acde84cb6bb01cbc9f23c377e411f1484467ecfdd3a3d234e" +checksum = "46db3d4ebc2ab23045a7d32fa1afb4be78894ec3fbe2f52b28f6cd6e4011e400" dependencies = [ "bevy_macro_utils", "encase_derive_impl", @@ -724,9 +679,9 @@ dependencies = [ [[package]] name = "bevy_gilrs" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc3a5f9e872133d7f5c2fab82e17781c19ed0b98f371362a23ed972bb538d20" +checksum = "a20320bd21f379ba4ec885b8217cb7c2c57eb0be014ba29509959e252480c3e9" dependencies = [ "bevy_app", "bevy_ecs", @@ -739,9 +694,9 @@ dependencies = [ [[package]] name = "bevy_gizmos" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c82341f6a3517efeeeef2fe68135ac3a91b11b6e369fc1a07f6e9a4b462b57" +checksum = "ca821905afffe1f3aaf33b496903a24a0c980e4c83fa7523fb41eac16892a57a" dependencies = [ "bevy_app", "bevy_asset", @@ -763,9 +718,9 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9454ac9f0a2141900ef9f3482af9333e490d5546bbea3cab63a777447d35beed" +checksum = "19843a638c93364950ca54a879832f325be7fa9b89f226fced3b4105594afb70" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -775,9 +730,9 @@ dependencies = [ [[package]] name = "bevy_gltf" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21ed694796a001a5cf63de9ddc62fc017302b0e2998a361ef1126880ec93555" +checksum = "c38b79c0e43c6387699d6a332d12f98ed895bcf69dd70c462d5e49ad76d44d1f" dependencies = [ "base64 0.22.1", "bevy_animation", @@ -807,9 +762,9 @@ dependencies = [ [[package]] name = "bevy_hierarchy" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe0b538beea7edbf30a6062242b99e67ff3bfa716566aacf91d5b5e027f02a2" +checksum = "bd9aab2cd1684d30f2eedf953b6377a6416fd6b482f8145b6c05f4684bd60c3e" dependencies = [ "bevy_app", "bevy_core", @@ -822,9 +777,9 @@ dependencies = [ [[package]] name = "bevy_image" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db46fa6a2f9e20435f3231710abbb136d2cc0a376f3f8e6ecfe071e286f5a246" +checksum = "8c5942a7d681b81aa9723bb1d918135c2f88e7871331f5676119c86c01984759" dependencies = [ "bevy_asset", "bevy_color", @@ -854,9 +809,9 @@ dependencies = [ [[package]] name = "bevy_input" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b4ea60095d1a1851e40cb12481ad3d5d234e14376d6b73142a85586c266b74" +checksum = "a9bbf39c1d2d33350e03354a67bebee5c21973c5203b1456a9a4b90a5e6f8e75" dependencies = [ "bevy_app", "bevy_core", @@ -870,9 +825,9 @@ dependencies = [ [[package]] name = "bevy_internal" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4237e6e9b03902321032f00f931f18a4a211093bd9a7cf81276a0228a2a4417" +checksum = "fd7fc4db9a1793ee71f79abb15e7a8fcfe4e2081e5f18ed91b802bf6cf30e088" dependencies = [ "bevy_a11y", "bevy_animation", @@ -913,9 +868,9 @@ dependencies = [ [[package]] name = "bevy_log" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0bdb42b00ac3752f0d6f531fbda8abf313603157a7b3163da8529412119a0a" +checksum = "774238dcf70a0ef4d82aa2860b24b1cffdd4633f3694d3bcbfbb05c4f17ae4fe" dependencies = [ "android_log-sys", "bevy_app", @@ -929,9 +884,9 @@ dependencies = [ [[package]] name = "bevy_macro_utils" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954dbb56a66a6c09c783e767f6ceca0dc0492c22e536e2aeaefb5545eac33c6" +checksum = "9bdb3a681c24abace65bf18ed467ad8befbedb42468b32e459811bfdb01e506c" dependencies = [ "proc-macro2", "quote", @@ -941,9 +896,9 @@ dependencies = [ [[package]] name = "bevy_math" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae26f952598e293acac783d947b21af1809673cbeba25d76b969a56f287160b" +checksum = "edec18d90e6bab27b5c6131ee03172ece75b7edd0abe4e482a26d6db906ec357" dependencies = [ "bevy_reflect", "derive_more", @@ -957,9 +912,9 @@ dependencies = [ [[package]] name = "bevy_mesh" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324d45ca0043a4696d7324b569de65be17066ed3a97dd42205bc28693d20b5" +checksum = "183abae7c6695a80d7408c860bd737410cd66d2a9f910dafc914485da06e43dc" dependencies = [ "bevy_asset", "bevy_derive", @@ -980,9 +935,9 @@ dependencies = [ [[package]] name = "bevy_mikktspace" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5ea3ad25d74ea36ea45418ad799f135d046db35c322b9704c4a8934eb65ce9" +checksum = "b53f0cf879a0682280937f515ecf00ab2140f7224881d6a621f40093a36a2ef6" dependencies = [ "glam", ] @@ -998,18 +953,18 @@ dependencies = [ [[package]] name = "bevy_panorbit_camera" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e025ec14a381fa89ec0ffcb99ae44ebbdd3b2ee4369e13165a70187b96c8c52e" +checksum = "55e54a2fbf69a596957134b387093009c1d4dc046a6c9f3d62f37230525dca19" dependencies = [ "bevy", ] [[package]] name = "bevy_pbr" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b3bd8e646ddd3f27743b712957d2990d7361eb21044accc47c4f66711bf2cb" +checksum = "f7f17067399cf00f4441e93d39fb4c391a16cc223e0d35346ac388e66712c418" dependencies = [ "bevy_app", "bevy_asset", @@ -1036,9 +991,9 @@ dependencies = [ [[package]] name = "bevy_picking" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a137ed706574dc4a01cac527eb2c44a0b0e477d5bce3afc892a9ee95ee0078" +checksum = "125e0c7327ec155c566c044c6eefd1a02e904134fa5dc0ba54665e06a35297b0" dependencies = [ "bevy_app", "bevy_asset", @@ -1069,15 +1024,15 @@ dependencies = [ [[package]] name = "bevy_ptr" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af9e30b40fb3f0a80a658419f670f2de1e743efcaca1952c43cdcc923287944" +checksum = "aa65df6a190b7dfc84d79f09cf02d47ae046fa86a613e202c31559e06d8d3710" [[package]] name = "bevy_reflect" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a37e2ae5ed62df4a0e3f958076effe280b39bc81fe878587350897a89332a2" +checksum = "bab3264acc3b6f48bc23fbd09fdfea6e5d9b7bfec142e4f3333f532acf195bca" dependencies = [ "assert_type_match", "bevy_ptr", @@ -1097,9 +1052,9 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c683fc68c75fc26f90bb1e529590095380d7cec66f6610dbe6b93d9fd26f94" +checksum = "42f83876a322130ab38a47d5dcf75258944bf76b3387d1acdb3750920fda63e2" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1110,9 +1065,9 @@ dependencies = [ [[package]] name = "bevy_render" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d188f392edf4edcae53dfda07f3ec618a7a704183ec3f2e8504657a9fb940c8a" +checksum = "5b14d77d8ff589743237c98502c0e47fd31059cf348ab86a265c4f90bb5e2a22" dependencies = [ "async-channel", "bevy_app", @@ -1157,9 +1112,9 @@ dependencies = [ [[package]] name = "bevy_render_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ab37ee2945f93e9ba8daf91cd968b4cba9c677ac51d349dd8512a107a9a5d92" +checksum = "285769c193b832d67c5742a716c6063db573573d5df5ce0c41aa7584ef0e348e" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1169,9 +1124,9 @@ dependencies = [ [[package]] name = "bevy_scene" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e883fd3c6d6e7761f1fe662e79bc7bdc7e917e73e7bfc434b1d16d2a5852119" +checksum = "cd00a08d01a190a826a5f6ad0fcb3dbf7bd1bd4f64ebe6108c38384691a21111" dependencies = [ "bevy_app", "bevy_asset", @@ -1189,9 +1144,9 @@ dependencies = [ [[package]] name = "bevy_sprite" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e975abc3f3f3432d6ad86ae32de804e96d7faf59d27f32b065b5ddc1e73ed7e1" +checksum = "84c7d22da88e562fb2ae8fe7f8cc749d3024caa4dcb57a777d070ef9141577aa" dependencies = [ "bevy_app", "bevy_asset", @@ -1219,9 +1174,9 @@ dependencies = [ [[package]] name = "bevy_state" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "036ec832197eae51b8a842220d2df03591dff75b4566dcf0f81153bbcb2b593b" +checksum = "fd10c8b01a982642596406fc4486fcd52239aa9c4aa47fed27abab93a69fba59" dependencies = [ "bevy_app", "bevy_ecs", @@ -1233,9 +1188,9 @@ dependencies = [ [[package]] name = "bevy_state_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2828eb6762af9eccfebb5e4a0e56dbc4bd07bf3192083fa3e8525cfdb3e95add" +checksum = "23773797bf8077a6ad9299f10b063b6947f22dad311d855c4b3523102ab4381b" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1245,9 +1200,9 @@ dependencies = [ [[package]] name = "bevy_tasks" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5171c605b462b4e3249e01986505e62e3933aa27642a9f793c841814fcbbfb4f" +checksum = "5c28f2db2619203aa82342dbbe77e49aeea4f933212c0b7a1f285e94c4008e5b" dependencies = [ "async-channel", "async-executor", @@ -1260,9 +1215,9 @@ dependencies = [ [[package]] name = "bevy_text" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb000b2abad9f82f7a137fac7e0e3d2c6488cbf8dd9ddbb68f9a6b7e7af8d84" +checksum = "17ee0b5f52946d222521f93773a6230f42e868548f881c4c5bddb1393a96298b" dependencies = [ "bevy_app", "bevy_asset", @@ -1288,9 +1243,9 @@ dependencies = [ [[package]] name = "bevy_time" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291b6993b899c04554fc034ebb9e0d7fde9cb9b2fb58dcd912bfa6247abdedbb" +checksum = "bb3108ed1ef864bc40bc859ba4c9c3844213c7be3674f982203cf5d87c656848" dependencies = [ "bevy_app", "bevy_ecs", @@ -1301,9 +1256,9 @@ dependencies = [ [[package]] name = "bevy_transform" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35665624d0c728107ab0920d5ad2d352362b906a8c376eaf375ec9c751faf4" +checksum = "056fabcedbf0503417af69447d47a983e18c7cfb5e6b6728636be3ec285cbcfa" dependencies = [ "bevy_app", "bevy_ecs", @@ -1315,9 +1270,9 @@ dependencies = [ [[package]] name = "bevy_ui" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da3326aa592d6f6326e31893901bf17cd6957ded4e0ea02bc54652e5624b7f" +checksum = "4556fc2202c6339f95e0c24ca4c96ee959854b702e23ecf73e05fb20e67d67b0" dependencies = [ "accesskit", "bevy_a11y", @@ -1348,9 +1303,9 @@ dependencies = [ [[package]] name = "bevy_utils" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a48bad33c385a7818b7683a16c8b5c6930eded05cd3f176264fc1f5acea473" +checksum = "4f01088c048960ea50ee847c3f668942ecf49ed26be12a1585a5e59b6a941d9a" dependencies = [ "ahash", "bevy_utils_proc_macros", @@ -1363,9 +1318,9 @@ dependencies = [ [[package]] name = "bevy_utils_proc_macros" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfd8d4a525b8f04f85863e45ccad3e922d4c11ed4a8d54f7f62a40bf83fb90f" +checksum = "4a0c3244d543cc964545b7aa074f6fb18a915a7121cf3de5d7ed37a4aae8662d" dependencies = [ "proc-macro2", "quote", @@ -1374,9 +1329,9 @@ dependencies = [ [[package]] name = "bevy_window" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f3520279aae65935d6a84443202c154ead3abebf8dae906d095665162de358" +checksum = "36139955777cc9e7a40a97833ff3a95b7401ce525a3dbac05fc52557968b31a7" dependencies = [ "android-activity", "bevy_a11y", @@ -1392,9 +1347,9 @@ dependencies = [ [[package]] name = "bevy_winit" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581bb2249a82285707e0977a9a1c79a2248ede587fcb289708faa03a82ebfa7f" +checksum = "36e84e7f94583cac93de4ba641029eb0b6551d35e559c829209f2b1b9fe532d8" dependencies = [ "accesskit", "accesskit_winit", @@ -1554,24 +1509,24 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", @@ -1966,13 +1921,12 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "curvo" -version = "0.1.42" +version = "0.1.43" dependencies = [ "anyhow", "approx", "argmin", "bevy", - "bevy-inspector-egui", "bevy_egui", "bevy_infinite_grid", "bevy_normal_material", @@ -1980,7 +1934,7 @@ dependencies = [ "bevy_points", "easer", "gauss-quad", - "itertools 0.13.0", + "itertools 0.14.0", "log", "nalgebra", "num-traits", @@ -2079,9 +2033,9 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5" dependencies = [ "bytemuck", "emath", @@ -2089,14 +2043,15 @@ dependencies = [ [[package]] name = "egui" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e" dependencies = [ "ahash", "emath", "epaint", "nohash-hasher", + "profiling", ] [[package]] @@ -2107,9 +2062,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d" dependencies = [ "bytemuck", ] @@ -2148,9 +2103,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac" dependencies = [ "ab_glyph", "ahash", @@ -2160,13 +2115,14 @@ dependencies = [ "epaint_default_fonts", "nohash-hasher", "parking_lot", + "profiling", ] [[package]] name = "epaint_default_fonts" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" +checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80" [[package]] name = "equivalent" @@ -2407,20 +2363,11 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - [[package]] name = "gauss-quad" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40416dcbdfb6d981b3e01a9f214fea6d22643fc8d4e84b7e72f8171a990d4c36" +checksum = "2fdf36628fe7cc5b222fbae008160acef0e2d96897897b76843494686eb7c005" dependencies = [ "nalgebra", ] @@ -2437,9 +2384,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -2806,6 +2753,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -2926,9 +2882,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -3008,9 +2964,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" dependencies = [ "autocfg", "rawpointer", @@ -3239,11 +3195,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", "serde", @@ -3271,11 +3237,11 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -3646,9 +3612,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -3750,9 +3716,12 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "presser" @@ -3790,9 +3759,9 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" [[package]] name = "quick-xml" @@ -3805,9 +3774,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -4074,9 +4043,9 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "safe_arch" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" dependencies = [ "bytemuck", ] @@ -4129,18 +4098,18 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -4149,9 +4118,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -4319,9 +4288,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -4625,9 +4594,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-linebreak" @@ -5057,9 +5026,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.15" +version = "0.7.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89beec544f246e679fc25490e3f8e08003bc4bf612068f325120dad4cea02c1c" +checksum = "58e6db2670d2be78525979e9a5f9c69d296fd7d670549fe9ebf70f8708cb5019" dependencies = [ "bytemuck", "safe_arch", @@ -5554,6 +5523,7 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ + "byteorder", "zerocopy-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 500489b..ab26ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "curvo" -version = "0.1.42" +version = "0.1.43" authors = ["Masatatsu Nakamura >, + mut line_materials: ResMut>, + mut points_materials: ResMut>, + mut normal_materials: ResMut<'_, Assets>, +) { + let interpolation_target = vec![ + Point3::new(-1.0, -1.0, 0.), + Point3::new(1.0, -1.0, 0.), + Point3::new(1.0, 1.0, 0.), + Point3::new(-1.0, 1.0, 0.), + Point3::new(-1.0, 2.0, 0.), + Point3::new(1.0, 2.5, 0.), + ]; + let interpolated = NurbsCurve3D::::try_interpolate(&interpolation_target, 3).unwrap(); + + let rotation = Rotation3::from_axis_angle(&Vector3::z_axis(), FRAC_PI_2); + let translation = Translation3::new(0., 0., 1.5); + let m = translation * rotation; + let front = interpolated.transformed(&(translation.inverse()).into()); + let back = interpolated.transformed(&m.into()); + + let (umin, umax) = front.knots_domain(); + + let surface = NurbsSurface::try_loft(&[front, back], Some(3)).unwrap(); + + let u_parameters = vec![ + umin, + (umin + umax) * 0.25, + (umin + umax) * 0.5, + (umin + umax) * 0.75, + umax, + ]; + let vmin = surface.v_knots_domain().0; + + let boundary = BoundaryConstraints::default().with_u_parameters_at_v_min(u_parameters.clone()); + + commands.spawn(( + Mesh3d( + meshes.add(PointsMesh { + vertices: u_parameters + .iter() + .map(|u| surface.point_at(*u, vmin).cast::().into()) + .collect_vec(), + ..Default::default() + }), + ), + MeshMaterial3d(points_materials.add(PointsMaterial { + settings: PointsShaderSettings { + point_size: 0.05, + color: TOMATO.into(), + ..Default::default() + }, + circle: true, + ..Default::default() + })), + )); + + // let tess = surface.tessellate(Some(Default::default())); + let option = AdaptiveTessellationOptions { + ..Default::default() + }; + let tess = surface.constrained_tessellate(boundary, Some(option)); + let tess = tess.cast::(); + + let vertices = tess.points().iter().map(|pt| (*pt).into()).collect_vec(); + commands.spawn(( + Mesh3d(meshes.add(PointsMesh { + vertices: vertices.clone(), + ..Default::default() + })), + MeshMaterial3d(points_materials.add(PointsMaterial { + settings: PointsShaderSettings { + point_size: 0.02, + color: Color::WHITE.into(), + ..Default::default() + }, + circle: true, + ..Default::default() + })), + )); + + let normals = tess.normals().iter().map(|n| (*n).into()).collect(); + let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect(); + let indices = tess + .faces() + .iter() + .flat_map(|f| f.iter().map(|i| *i as u32)) + .collect_vec(); + + let mesh = Mesh::new(PrimitiveTopology::LineList, default()).with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + // triangle edges + VertexAttributeValues::Float32x3( + indices + .chunks(3) + .flat_map(|idx| { + [ + vertices[idx[0] as usize].into(), + vertices[idx[1] as usize].into(), + vertices[idx[1] as usize].into(), + vertices[idx[2] as usize].into(), + vertices[idx[2] as usize].into(), + vertices[idx[0] as usize].into(), + ] + }) + .collect_vec(), + ), + ); + commands.spawn(( + Mesh3d(meshes.add(mesh)), + MeshMaterial3d(line_materials.add(LineMaterial { + color: Color::WHITE.into(), + ..Default::default() + })), + )); + + let mesh = Mesh::new(PrimitiveTopology::TriangleList, default()) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + VertexAttributeValues::Float32x3(vertices.into_iter().map(|v| v.into()).collect()), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_NORMAL, + VertexAttributeValues::Float32x3(normals), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs)) + .with_inserted_indices(Indices::U32(indices)); + + commands.spawn(( + Mesh3d(meshes.add(mesh)), + MeshMaterial3d(normal_materials.add(NormalMaterial { + cull_mode: None, + ..Default::default() + })), + )); + + let (umin, umax) = surface.u_knots_domain(); + let (vmin, vmax) = surface.v_knots_domain(); + let center = surface + .point_at((umax + umin) * 0.5, (vmax + vmin) * 0.5) + .cast::() + .into(); + + let scale = 5.; + commands.spawn(( + Projection::Orthographic(OrthographicProjection { + scale, + near: 1e-1, + far: 1e4, + scaling_mode: ScalingMode::FixedVertical { + viewport_height: 2., + }, + ..OrthographicProjection::default_3d() + }), + Transform::from_translation(center + Vec3::new(0., 0., 3.)).looking_at(center, Vec3::Y), + PanOrbitCamera::default(), + )); +} diff --git a/examples/intersect_curves.rs b/examples/intersect_curves.rs index 5c9a4f9..36714ff 100644 --- a/examples/intersect_curves.rs +++ b/examples/intersect_curves.rs @@ -31,7 +31,6 @@ fn main() { .add_plugins(PanOrbitCameraPlugin) .add_plugins(PointsPlugin) .add_plugins(AppPlugin) - // .add_plugins(WorldInspectorPlugin::new()) .run(); } struct AppPlugin; diff --git a/examples/misc/mod.rs b/examples/misc/mod.rs index 35d5939..71deb2f 100644 --- a/examples/misc/mod.rs +++ b/examples/misc/mod.rs @@ -121,27 +121,6 @@ pub fn add_surface( let tess = surface.tessellate(Some(option)); let tess = tess.cast::(); - let mut line_list = Mesh::new(bevy::render::mesh::PrimitiveTopology::LineList, default()); - let normal_length = 0.15; - let normals = tess.normals(); - - let vertices = tess - .points() - .iter() - .enumerate() - .flat_map(|(i, p)| { - let pt: Vec3 = (*p).into(); - let normal: Vec3 = normals[i].normalize().into(); - [pt, pt + normal * normal_length] - }) - .map(|p| p.to_array()) - .collect(); - - line_list.insert_attribute( - Mesh::ATTRIBUTE_POSITION, - VertexAttributeValues::Float32x3(vertices), - ); - let vertices = tess.points().iter().map(|pt| (*pt).into()).collect(); let normals = tess.normals().iter().map(|n| (*n).into()).collect(); let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect(); diff --git a/examples/trimmed_surface.rs b/examples/trimmed_surface.rs new file mode 100644 index 0000000..6907bde --- /dev/null +++ b/examples/trimmed_surface.rs @@ -0,0 +1,281 @@ +use bevy::{ + color::palettes::css::TOMATO, + prelude::*, + render::mesh::{Indices, PrimitiveTopology, VertexAttributeValues}, +}; +use bevy_infinite_grid::{InfiniteGridBundle, InfiniteGridPlugin}; + +use bevy_normal_material::{material::NormalMaterial, plugin::NormalMaterialPlugin}; +use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; +use bevy_points::{plugin::PointsPlugin, prelude::PointsMaterial}; +use itertools::Itertools; +use materials::*; +use nalgebra::{Point2, Point3, Point4, Vector2, Vector3}; + +use curvo::prelude::*; +use rand::Rng; +mod materials; +mod misc; +mod systems; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + resolution: (640., 480.).into(), + ..Default::default() + }), + ..Default::default() + })) + .add_plugins(LineMaterialPlugin) + .add_plugins(InfiniteGridPlugin) + .add_plugins(PanOrbitCameraPlugin) + .add_plugins(PointsPlugin) + .add_plugins(NormalMaterialPlugin) + .add_plugins(AppPlugin) + .run(); +} +struct AppPlugin; + +impl Plugin for AppPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_systems(Startup, setup); + // .add_systems(Update, screenshot_on_spacebar); + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut line_materials: ResMut>, + _points_materials: ResMut>, + mut normal_materials: ResMut<'_, Assets>, +) { + let add_trimmed_surface = |commands: &mut Commands<'_, '_>, + meshes: &mut ResMut<'_, Assets>, + normal_materials: &mut ResMut<'_, Assets>, + surface: &TrimmedSurface, + transform: Transform| { + let option = AdaptiveTessellationOptions { + norm_tolerance: 1e-2, + ..Default::default() + }; + let tess = surface.tessellate(Some(option)).cast::(); + let vertices = tess.points().iter().map(|pt| (*pt).into()).collect(); + let normals = tess.normals().iter().map(|n| (*n).into()).collect(); + let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect(); + let indices = tess + .faces() + .iter() + .flat_map(|f| f.iter().map(|i| *i as u32)) + .collect(); + let mesh = Mesh::new(PrimitiveTopology::TriangleList, default()) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + VertexAttributeValues::Float32x3(vertices), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_NORMAL, + VertexAttributeValues::Float32x3(normals), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs)) + .with_inserted_indices(Indices::U32(indices)); + commands.spawn(( + Mesh3d(meshes.add(mesh)), + MeshMaterial3d(normal_materials.add(NormalMaterial { + cull_mode: None, + ..Default::default() + })), + transform, + )); + }; + + let add_surface = |commands: &mut Commands<'_, '_>, + meshes: &mut ResMut<'_, Assets>, + normal_materials: &mut ResMut<'_, Assets>, + surface: &NurbsSurface3D, + transform: Transform| { + let option = AdaptiveTessellationOptions { + norm_tolerance: 1e-2, + ..Default::default() + }; + let tess = surface.tessellate(Some(option)).cast::(); + let vertices = tess.points().iter().map(|pt| (*pt).into()).collect(); + let normals = tess.normals().iter().map(|n| (*n).into()).collect(); + let uvs = tess.uvs().iter().map(|uv| (*uv).into()).collect(); + let indices = tess + .faces() + .iter() + .flat_map(|f| f.iter().map(|i| *i as u32)) + .collect(); + let mesh = Mesh::new(PrimitiveTopology::TriangleList, default()) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + VertexAttributeValues::Float32x3(vertices), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_NORMAL, + VertexAttributeValues::Float32x3(normals), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::Float32x2(uvs)) + .with_inserted_indices(Indices::U32(indices)); + commands.spawn(( + Mesh3d(meshes.add(mesh)), + MeshMaterial3d(normal_materials.add(NormalMaterial { + cull_mode: None, + ..Default::default() + })), + transform, + )); + }; + + let add_curve = |commands: &mut Commands<'_, '_>, + meshes: &mut ResMut<'_, Assets>, + line_materials: &mut ResMut<'_, Assets>, + curve: &NurbsCurve3D, + transform: Transform| { + let (min, max) = curve.knots_domain(); + let interval = max - min; + let step = interval / 32.; + let mut t = min; + let mut vertices: Vec = vec![]; + + while t <= max { + let pt = curve.point_at(t).cast::(); + vertices.push(pt.into()); + + t += step; + } + + let samples = curve.tessellate(Some(1e-8)); + let mut line = Mesh::new(bevy::render::mesh::PrimitiveTopology::LineStrip, default()); + let line_vertices = samples + .iter() + .map(|p| p.cast::()) + .map(|p| [p.x, p.y, p.z]) + .collect(); + line.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + VertexAttributeValues::Float32x3(line_vertices), + ); + commands.spawn(( + Mesh3d(meshes.add(line)), + MeshMaterial3d(line_materials.add(LineMaterial { + color: TOMATO.into(), + ..Default::default() + })), + transform, + )); + }; + + let plane_surface = NurbsSurface::plane( + Point3::new(2.5, 0., 5.), + Vector3::x() * 2.5, + Vector3::z() * 5., + ); + let trimmed = TrimmedSurface::new( + plane_surface.clone(), + None, + vec![ + NurbsCurve2D::try_circle(&Point2::new(0.5, 0.5), &Vector2::x(), &Vector2::y(), 0.25) + .unwrap(), + ], + ); + + let offset = 10.0; + + let trimming_curve = NurbsCurve3D::try_circle( + &Point3::new(2.5, 5.0, 2.5), + &Vector3::x(), + &Vector3::z(), + 1., + ) + .unwrap(); + let trimmed2 = TrimmedSurface::try_projection( + plane_surface.clone(), + -Vector3::y(), + Some(trimming_curve.clone()), + vec![], + ) + .unwrap(); + + add_trimmed_surface( + &mut commands, + &mut meshes, + &mut normal_materials, + &trimmed, + Transform::default(), + ); + + let transform = Transform::from_translation(Vec3::new(offset, 0., 0.)); + add_curve( + &mut commands, + &mut meshes, + &mut line_materials, + &trimming_curve, + transform, + ); + add_trimmed_surface( + &mut commands, + &mut meshes, + &mut normal_materials, + &trimmed2, + transform, + ); + + let transform = Transform::from_translation(Vec3::new(offset * 2., 0., 0.)); + let degree = 3; + let n: usize = 6; + let goal = n + degree + 1; + let knots = KnotVector::uniform(goal - degree * 2, degree); + let _hn = (n - 1) as f64 / 2.; + let mut rng: rand::rngs::StdRng = rand::SeedableRng::from_seed([0; 32]); + let pts = (0..n) + .map(|i| { + (0..n) + .map(|j| { + let x = i as f64; + let z = (rng.gen::() - 0.5) * 2.; + let y = j as f64; + Point4::new(x, z, y, 1.) + }) + .collect_vec() + }) + .collect_vec(); + let wavy_surface = NurbsSurface3D::new(degree, degree, knots.to_vec(), knots.to_vec(), pts); + + let trimmed3 = TrimmedSurface::try_projection( + wavy_surface.clone(), + -Vector3::y(), + Some(trimming_curve.clone()), + vec![], + ) + .unwrap(); + add_curve( + &mut commands, + &mut meshes, + &mut line_materials, + &trimming_curve, + transform, + ); + add_surface( + &mut commands, + &mut meshes, + &mut normal_materials, + &wavy_surface, + transform, + ); + add_trimmed_surface( + &mut commands, + &mut meshes, + &mut normal_materials, + &trimmed3, + transform * Transform::from_translation(Vec3::new(0., 1.5, 0.)), + ); + + commands.spawn(( + Transform::from_translation(Vec3::new(18., 18., 18.)), + PanOrbitCamera::default(), + )); + commands.spawn(InfiniteGridBundle::default()); +} diff --git a/src/boolean/vertex.rs b/src/boolean/vertex.rs index b5dd41a..1d042a7 100644 --- a/src/boolean/vertex.rs +++ b/src/boolean/vertex.rs @@ -33,7 +33,7 @@ impl<'a, T: FloatingPoint> Vertex<'a, T> { } } -impl<'a, T: FloatingPoint> HasParameter for Vertex<'a, T> { +impl HasParameter for Vertex<'_, T> { fn parameter(&self) -> T { self.parameter } diff --git a/src/bounding_box/curve_bounding_box_tree.rs b/src/bounding_box/curve_bounding_box_tree.rs index 6523b11..0c1a939 100644 --- a/src/bounding_box/curve_bounding_box_tree.rs +++ b/src/bounding_box/curve_bounding_box_tree.rs @@ -44,7 +44,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> BoundingBoxTree for CurveBoundingBoxTree<'a, T, D> +impl BoundingBoxTree for CurveBoundingBoxTree<'_, T, D> where D: DimNameSub, DefaultAllocator: Allocator, diff --git a/src/bounding_box/surface_bounding_box_tree.rs b/src/bounding_box/surface_bounding_box_tree.rs index 2d27d92..200ace2 100644 --- a/src/bounding_box/surface_bounding_box_tree.rs +++ b/src/bounding_box/surface_bounding_box_tree.rs @@ -54,7 +54,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> BoundingBoxTree for SurfaceBoundingBoxTree<'a, T, D> +impl BoundingBoxTree for SurfaceBoundingBoxTree<'_, T, D> where D: DimNameSub, DefaultAllocator: Allocator, diff --git a/src/closest_parameter/curve_closest_parameter_problem.rs b/src/closest_parameter/curve_closest_parameter_problem.rs index a25c9be..693be46 100644 --- a/src/closest_parameter/curve_closest_parameter_problem.rs +++ b/src/closest_parameter/curve_closest_parameter_problem.rs @@ -29,7 +29,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Gradient for CurveClosestParameterProblem<'a, T, D> +impl Gradient for CurveClosestParameterProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, @@ -47,7 +47,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Hessian for CurveClosestParameterProblem<'a, T, D> +impl Hessian for CurveClosestParameterProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, diff --git a/src/closest_parameter/surface_closest_parameter_problem.rs b/src/closest_parameter/surface_closest_parameter_problem.rs index 351f4db..fc63a90 100644 --- a/src/closest_parameter/surface_closest_parameter_problem.rs +++ b/src/closest_parameter/surface_closest_parameter_problem.rs @@ -30,7 +30,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> CostFunction for SurfaceClosestParameterProblem<'a, T, D> +impl CostFunction for SurfaceClosestParameterProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, @@ -46,7 +46,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Gradient for SurfaceClosestParameterProblem<'a, T, D> +impl Gradient for SurfaceClosestParameterProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, @@ -62,7 +62,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Hessian for SurfaceClosestParameterProblem<'a, T, D> +impl Hessian for SurfaceClosestParameterProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, diff --git a/src/curve/nurbs_curve.rs b/src/curve/nurbs_curve.rs index ebb6644..dde952c 100644 --- a/src/curve/nurbs_curve.rs +++ b/src/curve/nurbs_curve.rs @@ -129,7 +129,6 @@ where where D: DimNameSub, >::Output: DimNameAdd, - DefaultAllocator: Allocator, DefaultAllocator: Allocator>, DefaultAllocator: Allocator<<>::Output as DimNameAdd>::Output>, { @@ -461,11 +460,7 @@ where // the Bezier segments will not be generated correctly, // so reduce the number of segments by the amount that falls below the required duplication degree. let required_multiplicity = self.degree + 1; - let i = if start < required_multiplicity { - required_multiplicity - start - } else { - 0 - }; + let i = required_multiplicity.saturating_sub(start); let j = if end < required_multiplicity { segments.len() - (required_multiplicity - end) } else { @@ -1245,7 +1240,7 @@ where let k = self.degree; let n = self.control_points.len(); let idx = self.knots.add(knot); - let start = if idx > k { idx - k } else { 0 }; + let start = idx.saturating_sub(k); let end = if idx > n { self.control_points .push(self.control_points.last().unwrap().clone()); diff --git a/src/intersects/curve_curve/compound_curve_intersection.rs b/src/intersects/curve_curve/compound_curve_intersection.rs index 5c92cbd..38cbb4f 100644 --- a/src/intersects/curve_curve/compound_curve_intersection.rs +++ b/src/intersects/curve_curve/compound_curve_intersection.rs @@ -57,8 +57,8 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> HasIntersectionParameter - for CompoundCurveIntersection<'a, T, D> +impl HasIntersectionParameter + for CompoundCurveIntersection<'_, T, D> where D: DimNameSub, DefaultAllocator: Allocator, diff --git a/src/intersects/curve_curve/curve_intersection_problem.rs b/src/intersects/curve_curve/curve_intersection_problem.rs index 44a85bc..89a971c 100644 --- a/src/intersects/curve_curve/curve_intersection_problem.rs +++ b/src/intersects/curve_curve/curve_intersection_problem.rs @@ -26,7 +26,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Gradient for CurveIntersectionProblem<'a, T, D> +impl Gradient for CurveIntersectionProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, @@ -43,7 +43,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> CostFunction for CurveIntersectionProblem<'a, T, D> +impl CostFunction for CurveIntersectionProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, diff --git a/src/intersects/surface_curve/surface_curve_intersection_problem.rs b/src/intersects/surface_curve/surface_curve_intersection_problem.rs index 733e6f9..619a439 100644 --- a/src/intersects/surface_curve/surface_curve_intersection_problem.rs +++ b/src/intersects/surface_curve/surface_curve_intersection_problem.rs @@ -28,7 +28,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> Gradient for SurfaceCurveIntersectionProblem<'a, T, D> +impl Gradient for SurfaceCurveIntersectionProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, @@ -48,7 +48,7 @@ where } } -impl<'a, T: FloatingPoint, D: DimName> CostFunction for SurfaceCurveIntersectionProblem<'a, T, D> +impl CostFunction for SurfaceCurveIntersectionProblem<'_, T, D> where DefaultAllocator: Allocator, D: DimNameSub, diff --git a/src/lib.rs b/src/lib.rs index 0efe067..859e263 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,8 +29,9 @@ pub mod prelude { pub use crate::split::*; pub use crate::surface::*; pub use crate::tessellation::{ - adaptive_tessellation_option::AdaptiveTessellationOptions, surface_tessellation::*, - Tessellation, + adaptive_tessellation_option::AdaptiveTessellationOptions, + boundary_constraints::BoundaryConstraints, surface_tessellation::*, + ConstrainedTessellation, Tessellation, }; pub use crate::trim::*; } diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 9c124e7..98906a5 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -1,7 +1,7 @@ pub mod nurbs_surface; -// pub mod trimmed_surface; +pub mod trimmed_surface; pub use nurbs_surface::*; -// pub use trimmed_surface::*; +pub use trimmed_surface::*; /// The direction in the UV space. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/surface/nurbs_surface.rs b/src/surface/nurbs_surface.rs index e3214fb..aa721f8 100644 --- a/src/surface/nurbs_surface.rs +++ b/src/surface/nurbs_surface.rs @@ -530,6 +530,69 @@ where flipped } + /// Create a plane shaped NURBS surface + /// Example + /// ``` + /// use curvo::prelude::*; + /// use nalgebra::{Point3, Vector3}; + /// let plane = NurbsSurface3D::::plane(Point3::origin(), Vector3::x(), Vector3::y()); + /// assert_eq!(plane.u_degree(), 1); + /// assert_eq!(plane.v_degree(), 1); + /// let (ud, vd) = plane.knots_domain(); + /// let p00 = plane.point_at(ud.0, vd.0); + /// let p10 = plane.point_at(ud.1, vd.0); + /// let p11 = plane.point_at(ud.1, vd.1); + /// let p01 = plane.point_at(ud.0, vd.1); + /// assert_eq!(p00, Point3::new(-1.0, -1.0, 0.0)); + /// assert_eq!(p10, Point3::new(1.0, -1.0, 0.0)); + /// assert_eq!(p11, Point3::new(1.0, 1.0, 0.0)); + /// assert_eq!(p01, Point3::new(-1.0, 1.0, 0.0)); + /// ``` + pub fn plane( + center: OPoint>, + x_axis: OVector>, + y_axis: OVector>, + ) -> Self + where + >::Output: DimNameAdd, + DefaultAllocator: Allocator<<>::Output as DimNameAdd>::Output>, + { + let control_points = vec![ + vec![ + OPoint::from_slice( + (center.clone() - x_axis.clone() - y_axis.clone()) + .to_homogeneous() + .as_slice(), + ), + OPoint::from_slice( + (center.clone() - x_axis.clone() + y_axis.clone()) + .to_homogeneous() + .as_slice(), + ), + ], + vec![ + OPoint::from_slice( + (center.clone() + x_axis.clone() - y_axis.clone()) + .to_homogeneous() + .as_slice(), + ), + OPoint::from_slice( + (center.clone() + x_axis + y_axis) + .to_homogeneous() + .as_slice(), + ), + ], + ]; + + Self { + u_degree: 1, + v_degree: 1, + u_knots: KnotVector::new(vec![T::zero(), T::zero(), T::one(), T::one()]), + v_knots: KnotVector::new(vec![T::zero(), T::zero(), T::one(), T::one()]), + control_points, + } + } + /// Extrude a NURBS curve to create a NURBS surface pub fn extrude( profile: &NurbsCurve, diff --git a/src/surface/trimmed_surface.rs b/src/surface/trimmed_surface.rs index 6809a46..755dcde 100644 --- a/src/surface/trimmed_surface.rs +++ b/src/surface/trimmed_surface.rs @@ -1,9 +1,12 @@ +use std::cmp::Ordering; + use argmin::core::ArgminFloat; -use nalgebra::{Point3, Vector3}; +use nalgebra::{ComplexField, Matrix4, Point3, Vector3, U3}; use crate::{ curve::{NurbsCurve2D, NurbsCurve3D}, - misc::FloatingPoint, + misc::{FloatingPoint, Invertible, Transformable}, + prelude::{BoundingBox, HasIntersection, Intersects}, }; use super::NurbsSurface3D; @@ -60,23 +63,103 @@ impl TrimmedSurface { }) } + /// Try to map the trimming curves onto the surface using the closest point + /// This is a more stable method than `try_projection` but may not be as accurate + pub fn try_map_closest_point( + surface: NurbsSurface3D, + exterior: Option>, + interiors: Vec>, + ) -> anyhow::Result + where + T: ArgminFloat, + { + anyhow::ensure!( + exterior.is_some() || !interiors.is_empty(), + "No trimming curves provided" + ); + let exterior = match exterior { + Some(curve) => Some(try_map_curve_closest_point(&surface, &curve)?), + None => None, + }; + let interiors = interiors + .iter() + .map(|curve| try_map_curve_closest_point(&surface, curve)) + .collect::>>()?; + Ok(Self { + surface, + exterior, + interiors, + }) + } + pub fn surface(&self) -> &NurbsSurface3D { &self.surface } + pub fn surface_mut(&mut self) -> &mut NurbsSurface3D { + &mut self.surface + } + pub fn exterior(&self) -> Option<&NurbsCurve2D> { self.exterior.as_ref() } + pub fn exterior_mut(&mut self) -> Option<&mut NurbsCurve2D> { + self.exterior.as_mut() + } + pub fn interiors(&self) -> &[NurbsCurve2D] { &self.interiors } + + pub fn interiors_mut(&mut self) -> &mut [NurbsCurve2D] { + &mut self.interiors + } } +/// Try to project a 3D curve onto a 3D surface to get a 2D curve in parameter space fn try_project_curve( surface: &NurbsSurface3D, curve: &NurbsCurve3D, direction: &Vector3, +) -> anyhow::Result> { + let b0: BoundingBox = surface.into(); + let b1: BoundingBox = curve.into(); + let l0 = ComplexField::abs(b0.size().dot(direction)); + let l1 = ComplexField::abs(b1.size().dot(direction)); + let ray_length = (b0.center() - b1.center()).norm() + l0 + l1; + let offset = direction * T::one(); + let weights = curve.weights(); + let pts = curve + .dehomogenized_control_points() + .iter() + .zip(weights.into_iter()) + .map(|(p, w)| { + let ray = + NurbsCurve3D::polyline(&[p - offset, p + offset + direction * ray_length], true); + let closest = surface + .find_intersections(&ray, None)? + .into_iter() + .map(|it| { + let pt = it.a().0; + let dist = (p - pt).norm_squared(); + (dist, it) + }) + .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + let closest = closest + .ok_or_else(|| anyhow::anyhow!("No intersection found in try project curve"))? + .1; + let uv = closest.a().1; + Ok(Point3::new(uv.0 * w, uv.1 * w, w)) + }) + .collect::>>()?; + NurbsCurve2D::try_new(curve.degree(), pts, curve.knots().to_vec()) +} + +/// Try to map a 3D curve onto a 3D surface to get a 2D curve in parameter space using the closest point +fn try_map_curve_closest_point( + surface: &NurbsSurface3D, + curve: &NurbsCurve3D, ) -> anyhow::Result> { let weights = curve.weights(); let pts = curve @@ -84,10 +167,161 @@ fn try_project_curve( .iter() .zip(weights.into_iter()) .map(|(p, w)| { - surface - .find_closest_parameter(p) - .map(|(x, y)| Point3::new(x * w, y * w, w)) + let uv = surface.find_closest_parameter(p)?; + Ok(Point3::new(uv.0 * w, uv.1 * w, w)) }) .collect::>>()?; NurbsCurve2D::try_new(curve.degree(), pts, curve.knots().to_vec()) } + +impl From> for TrimmedSurface { + fn from(value: NurbsSurface3D) -> Self { + Self::new(value, None, vec![]) + } +} + +/// Enable to transform a Trimmed surface by a given DxD matrix +impl<'a, T: FloatingPoint> Transformable<&'a Matrix4> for TrimmedSurface { + fn transform(&mut self, transform: &'a Matrix4) { + self.surface.transform(transform); + } +} + +impl Invertible for TrimmedSurface { + /// Reverse the direction of the surface + fn invert(&mut self) { + self.surface.invert(); + if let Some(curve) = self.exterior.as_mut() { + curve.invert(); + } + self.interiors.iter_mut().for_each(|curve| curve.invert()); + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for TrimmedSurface +where + T: FloatingPoint + serde::Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut state = serializer.serialize_struct("TrimmedSurface", 3)?; + state.serialize_field("surface", &self.surface)?; + state.serialize_field("exterior", &self.exterior)?; + state.serialize_field("interiors", &self.interiors)?; + state.end() + } +} + +#[cfg(feature = "serde")] +impl<'de, T> serde::Deserialize<'de> for TrimmedSurface +where + T: FloatingPoint + serde::Deserialize<'de>, +{ + fn deserialize(deserializer: S) -> Result + where + S: serde::Deserializer<'de>, + { + use serde::de::{self, MapAccess, Visitor}; + + #[derive(Debug)] + enum Field { + Surface, + Exterior, + Interiors, + } + + impl<'de> serde::Deserialize<'de> for Field { + fn deserialize(deserializer: S) -> Result + where + S: serde::Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("`surface` or `exterior` or `interiors`") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "surface" => Ok(Field::Surface), + "exterior" => Ok(Field::Exterior), + "interiors" => Ok(Field::Interiors), + _ => Err(de::Error::unknown_field(value, FIELDS)), + } + } + } + + deserializer.deserialize_identifier(FieldVisitor) + } + } + + struct TrimmedSurfaceVisitor(std::marker::PhantomData); + + impl TrimmedSurfaceVisitor { + pub fn new() -> Self { + TrimmedSurfaceVisitor(std::marker::PhantomData) + } + } + + impl<'de, T> Visitor<'de> for TrimmedSurfaceVisitor + where + T: FloatingPoint + serde::Deserialize<'de>, + { + type Value = TrimmedSurface; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct TrimmedSurface") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut surface = None; + let mut exterior = None; + let mut interiors = None; + while let Some(key) = map.next_key()? { + match key { + Field::Surface => { + if surface.is_some() { + return Err(de::Error::duplicate_field("surface")); + } + surface = Some(map.next_value()?); + } + Field::Exterior => { + if exterior.is_some() { + return Err(de::Error::duplicate_field("exterior")); + } + exterior = map.next_value().ok(); + } + Field::Interiors => { + if interiors.is_some() { + return Err(de::Error::duplicate_field("interiors")); + } + interiors = Some(map.next_value()?); + } + } + } + + Ok(Self::Value { + surface: surface.ok_or_else(|| de::Error::missing_field("surface"))?, + exterior, + interiors: interiors.ok_or_else(|| de::Error::missing_field("interiors"))?, + }) + } + } + + const FIELDS: &[&str] = &["surface", "exterior", "interiors"]; + deserializer.deserialize_struct("TrimmedSurface", FIELDS, TrimmedSurfaceVisitor::::new()) + } +} diff --git a/src/tessellation/adaptive_tessellation_node.rs b/src/tessellation/adaptive_tessellation_node.rs index 6b038aa..359de12 100644 --- a/src/tessellation/adaptive_tessellation_node.rs +++ b/src/tessellation/adaptive_tessellation_node.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use nalgebra::{ allocator::Allocator, DefaultAllocator, DimName, DimNameDiff, DimNameSub, RealField, Vector2, U1, @@ -6,24 +7,26 @@ use nalgebra::{ use crate::{ misc::FloatingPoint, prelude::{AdaptiveTessellationOptions, NurbsSurface}, + surface::UVDirection, }; use super::SurfacePoint; /// Node for adaptive tessellation of a surface +#[derive(Clone, Debug)] pub struct AdaptiveTessellationNode where D: DimNameSub, DefaultAllocator: Allocator, DefaultAllocator: Allocator>, { - pub(crate) id: usize, - pub(crate) children: Vec, - pub(crate) corners: [SurfacePoint>; 4], + id: usize, + children: Option<[usize; 2]>, + pub(crate) corners: [SurfacePoint>; 4], // [left-bottom, right-bottom, right-top, left-top] order pub(crate) neighbors: [Option; 4], // [south, east, north, west] order (east & west are u direction, north & south are v direction) - pub(crate) mid_points: [Option>>; 4], - pub(crate) horizontal: bool, - pub(crate) center: Vector2, + mid_points: [Option>>; 4], // [south, east, north, west] order + pub(crate) direction: UVDirection, + uv_center: Vector2, } impl AdaptiveTessellationNode @@ -35,16 +38,17 @@ where pub fn new( id: usize, corners: [SurfacePoint>; 4], - neighbors: Option<[Option; 4]>, + neighbors: [Option; 4], ) -> Self { + let uv_center = (corners[0].uv + corners[2].uv) * T::from_f64(0.5).unwrap(); Self { id, corners, - neighbors: neighbors.unwrap_or([None, None, None, None]), - children: vec![], + neighbors, + children: None, mid_points: [None, None, None, None], - horizontal: false, - center: Vector2::zeros(), + direction: UVDirection::V, + uv_center, } } @@ -53,88 +57,59 @@ where } pub fn is_leaf(&self) -> bool { - self.children.is_empty() + self.children.is_none() } - pub fn center(&self, surface: &NurbsSurface) -> SurfacePoint> { - self.evaluate_surface(surface, self.center) + pub fn assign_children(&mut self, children: [usize; 2]) { + self.children = Some(children); } - pub fn evaluate_corners(&mut self, surface: &NurbsSurface) { - //eval the center - let inv = T::from_f64(0.5).unwrap(); - self.center = (self.corners[0].uv + self.corners[2].uv) * inv; - - //eval all of the corners - for i in 0..4 { - let c = &self.corners[i]; - let evaled = self.evaluate_surface(surface, c.uv); - self.corners[i] = evaled; - } - } - - pub fn evaluate_surface( - &self, - surface: &NurbsSurface, - uv: Vector2, - ) -> SurfacePoint> { - let derivs = surface.rational_derivatives(uv.x, uv.y, 1); - let pt = derivs[0][0].clone(); - let mut norm = derivs[1][0].cross(&derivs[0][1]); - let degen = norm.magnitude_squared() < T::default_epsilon(); - if !degen { - norm = norm.normalize(); - } - SurfacePoint { - point: pt.into(), - normal: norm, - uv, - is_normal_degenerated: degen, - } + /// Evaluate the center of the node + pub fn center(&self, surface: &NurbsSurface) -> SurfacePoint> { + evaluate_surface(surface, self.uv_center) } - pub fn get_edge_corners( + fn get_edge_corners( &self, nodes: &Vec, edge_index: usize, ) -> Vec>> { - //if its a leaf, there are no children to obtain uvs from - if self.is_leaf() { - return vec![self.corners[edge_index].clone()]; - } - - if self.horizontal { - match edge_index { - 0 => nodes[self.children[0]].get_edge_corners(nodes, 0), - 1 => [ - nodes[self.children[0]].get_edge_corners(nodes, 1), - nodes[self.children[1]].get_edge_corners(nodes, 1), - ] - .concat(), - 2 => nodes[self.children[1]].get_edge_corners(nodes, 2), - 3 => [ - nodes[self.children[1]].get_edge_corners(nodes, 3), - nodes[self.children[0]].get_edge_corners(nodes, 3), - ] - .concat(), - _ => vec![], - } - } else { - //vertical case - match edge_index { - 0 => [ - nodes[self.children[0]].get_edge_corners(nodes, 0), - nodes[self.children[1]].get_edge_corners(nodes, 0), - ] - .concat(), - 1 => nodes[self.children[1]].get_edge_corners(nodes, 1), - 2 => [ - nodes[self.children[1]].get_edge_corners(nodes, 2), - nodes[self.children[0]].get_edge_corners(nodes, 2), - ] - .concat(), - 3 => nodes[self.children[0]].get_edge_corners(nodes, 3), - _ => vec![], + match &self.children { + Some(children) => match self.direction { + UVDirection::U => match edge_index { + 0 => nodes[children[0]].get_edge_corners(nodes, 0), + 1 => [ + nodes[children[0]].get_edge_corners(nodes, 1), + nodes[children[1]].get_edge_corners(nodes, 1), + ] + .concat(), + 2 => nodes[children[1]].get_edge_corners(nodes, 2), + 3 => [ + nodes[children[1]].get_edge_corners(nodes, 3), + nodes[children[0]].get_edge_corners(nodes, 3), + ] + .concat(), + _ => vec![], + }, + UVDirection::V => match edge_index { + 0 => [ + nodes[children[0]].get_edge_corners(nodes, 0), + nodes[children[1]].get_edge_corners(nodes, 0), + ] + .concat(), + 1 => nodes[children[1]].get_edge_corners(nodes, 1), + 2 => [ + nodes[children[1]].get_edge_corners(nodes, 2), + nodes[children[0]].get_edge_corners(nodes, 2), + ] + .concat(), + 3 => nodes[children[0]].get_edge_corners(nodes, 3), + _ => vec![], + }, + }, + None => { + //if its a leaf, there are no children to obtain uvs from + vec![self.corners[edge_index].clone()] } } } @@ -156,73 +131,58 @@ where //clip the range of uvs to match self one let idx = edge_index % 2; - let mut corner: Vec<_> = corners + let corner = corners .into_iter() .filter(|c| c.uv[idx] > orig[0].uv[idx] + e && c.uv[idx] < orig[2].uv[idx] - e) - .collect(); - corner.reverse(); + .rev() + .collect_vec(); [base_arr, corner].concat() } } } + /// Evaluate the mid point of the node pub fn evaluate_mid_point( &mut self, surface: &NurbsSurface, - index: usize, + direction: NeighborDirection, ) -> SurfacePoint> { + let index = direction as usize; match self.mid_points[index] { Some(ref p) => p.clone(), None => { - match index { - 0 => { - self.mid_points[0] = Some(self.evaluate_surface( - surface, - Vector2::new(self.center.x, self.corners[0].uv.y), - )); - } - 1 => { - self.mid_points[1] = Some(self.evaluate_surface( - surface, - Vector2::new(self.corners[1].uv.x, self.center.y), - )); - } - 2 => { - self.mid_points[2] = Some(self.evaluate_surface( - surface, - Vector2::new(self.center.x, self.corners[2].uv.y), - )); + let uv = match direction { + NeighborDirection::South => { + Vector2::new(self.uv_center.x, self.corners[0].uv.y) } - 3 => { - self.mid_points[3] = Some(self.evaluate_surface( - surface, - Vector2::new(self.corners[0].uv.x, self.center.y), - )); + NeighborDirection::East => Vector2::new(self.corners[1].uv.x, self.uv_center.y), + NeighborDirection::North => { + Vector2::new(self.uv_center.x, self.corners[2].uv.y) } - _ => {} + NeighborDirection::West => Vector2::new(self.corners[0].uv.x, self.uv_center.y), }; - self.mid_points[index].clone().unwrap() + let pt = evaluate_surface(surface, uv); + self.mid_points[index] = Some(pt.clone()); + pt } } } - pub fn has_bad_normals(&self) -> bool { - self.corners[0].is_normal_degenerated - || self.corners[1].is_normal_degenerated - || self.corners[2].is_normal_degenerated - || self.corners[3].is_normal_degenerated + /// Check if the node has bad normals + fn has_bad_normals(&self) -> bool { + self.corners.iter().any(|c| c.is_normal_degenerated()) } - pub fn fix_normals(&mut self) { + fn fix_normals(&mut self) { let l = self.corners.len(); for i in 0..l { - if self.corners[i].is_normal_degenerated { + if self.corners[i].is_normal_degenerated() { //get neighbors let v1 = &self.corners[(i + 1) % l]; let v2 = &self.corners[(i + 3) % l]; //correct the normal - self.corners[i].normal = if v1.is_normal_degenerated { + self.corners[i].normal = if v1.is_normal_degenerated() { v2.normal.clone() } else { v1.normal.clone() @@ -234,41 +194,45 @@ where /// Check if the node should be divided pub fn should_divide( &mut self, - surface: &NurbsSurface, + _surface: &NurbsSurface, options: &AdaptiveTessellationOptions, current_depth: usize, - ) -> DividableDirection { + ) -> Option { if current_depth < options.min_depth { - return DividableDirection::Both; + return Some(DividableDirection::Both); } if current_depth >= options.max_depth { - return DividableDirection::None; + return None; } if self.has_bad_normals() { self.fix_normals(); //don't divide any further when encountering a degenerate normal - return DividableDirection::None; + return None; } // println!("{}, {}", surface.v_degree() >= 2, surface.u_degree() >= 2); - let vertical = (self.corners[0].normal() - self.corners[1].normal()).norm_squared() + let v_direction = (self.corners[0].normal() - self.corners[1].normal()).norm_squared() > options.norm_tolerance || (self.corners[2].normal() - self.corners[3].normal()).norm_squared() > options.norm_tolerance; + let v_direction = v_direction && self.corners.iter().all(|c| !c.is_u_constrained()); - let horizontal = (self.corners[1].normal() - self.corners[2].normal()).norm_squared() + let u_direction = (self.corners[1].normal() - self.corners[2].normal()).norm_squared() > options.norm_tolerance || (self.corners[3].normal() - self.corners[0].normal()).norm_squared() > options.norm_tolerance; + let u_direction = u_direction && self.corners.iter().all(|c| !c.is_v_constrained()); - match (vertical, horizontal) { - (true, true) => DividableDirection::Both, - (true, false) => DividableDirection::Vertical, - (false, true) => DividableDirection::Horizontal, + match (u_direction, v_direction) { + (true, true) => Some(DividableDirection::Both), + (true, false) => Some(DividableDirection::U), + (false, true) => Some(DividableDirection::V), (false, false) => { + None + /* let center = self.center(surface); if (center.normal() - self.corners[0].normal()).norm_squared() > options.norm_tolerance @@ -279,20 +243,55 @@ where || (center.normal() - self.corners[3].normal()).norm_squared() > options.norm_tolerance { - DividableDirection::Both + Some(DividableDirection::Both) } else { - DividableDirection::None + None } + */ } } } } +/// Evaluate the surface at a given uv coordinate +fn evaluate_surface( + surface: &NurbsSurface, + uv: Vector2, +) -> SurfacePoint> +where + D: DimName + DimNameSub, + DefaultAllocator: Allocator, + DefaultAllocator: Allocator>, +{ + let derivs = surface.rational_derivatives(uv.x, uv.y, 1); + let pt = derivs[0][0].clone(); + let norm = derivs[1][0].cross(&derivs[0][1]); + let is_normal_degenerated = norm.magnitude_squared() < T::default_epsilon(); + SurfacePoint::new( + uv, + pt.into(), + if !is_normal_degenerated { + norm.normalize() + } else { + norm + }, + is_normal_degenerated, + ) +} + /// Enum to represent the direction in which a node can be divided #[derive(Debug)] pub enum DividableDirection { Both, - Vertical, - Horizontal, - None, + U, + V, +} + +/// Enum to represent the direction of a neighbor +#[derive(Debug, Clone, Copy)] +pub enum NeighborDirection { + South = 0, + East = 1, + North = 2, + West = 3, } diff --git a/src/tessellation/adaptive_tessellation_processor.rs b/src/tessellation/adaptive_tessellation_processor.rs index 6135ef5..fd79cec 100644 --- a/src/tessellation/adaptive_tessellation_processor.rs +++ b/src/tessellation/adaptive_tessellation_processor.rs @@ -1,12 +1,12 @@ use nalgebra::{allocator::Allocator, DefaultAllocator, DimName, DimNameDiff, DimNameSub, U1}; use crate::{ - misc::FloatingPoint, prelude::NurbsSurface, + misc::FloatingPoint, prelude::NurbsSurface, surface::UVDirection, tessellation::adaptive_tessellation_node::AdaptiveTessellationNode, }; use super::{ - adaptive_tessellation_node::DividableDirection, + adaptive_tessellation_node::{DividableDirection, NeighborDirection}, adaptive_tessellation_option::AdaptiveTessellationOptions, }; @@ -18,9 +18,9 @@ where DefaultAllocator: Allocator>, { /// The surface to tessellate - pub(crate) surface: &'a NurbsSurface, + surface: &'a NurbsSurface, /// The created nodes for the tessellation - pub(crate) nodes: Vec>, + nodes: Vec>, } impl<'a, T: FloatingPoint, D: DimName> AdaptiveTessellationProcessor<'a, T, D> @@ -29,167 +29,123 @@ where DefaultAllocator: Allocator, DefaultAllocator: Allocator>, { - pub fn north( - &self, - index: usize, - i: usize, - _j: usize, - divs_u: usize, - _divs_v: usize, - ) -> Option<&AdaptiveTessellationNode> { - if i == 0 { - None - } else { - Some(&self.nodes[index - divs_u]) - } - } - - pub fn south( - &self, - index: usize, - i: usize, - _j: usize, - divs_u: usize, - divs_v: usize, - ) -> Option<&AdaptiveTessellationNode> { - if i == divs_v - 1 { - None - } else { - Some(&self.nodes[index + divs_u]) - } - } - - pub fn east( - &self, - index: usize, - _i: usize, - j: usize, - divs_u: usize, - _divs_v: usize, - ) -> Option<&AdaptiveTessellationNode> { - if j == divs_u - 1 { - None - } else { - Some(&self.nodes[index + 1]) - } + pub fn new( + surface: &'a NurbsSurface, + nodes: Vec>, + ) -> Self { + Self { surface, nodes } } - pub fn west( - &self, - index: usize, - _i: usize, - j: usize, - _divs_u: usize, - _divs_v: usize, - ) -> Option<&AdaptiveTessellationNode> { - if j == 0 { - None - } else { - Some(&self.nodes[index - 1]) - } + pub fn into_nodes(self) -> Vec> { + self.nodes } pub fn divide(&mut self, id: usize, options: &AdaptiveTessellationOptions) { - let horizontal = self.surface.u_degree() > 1; - self.iterate(id, options, 0, horizontal); + let direction = if self.surface.u_degree() > 1 { + UVDirection::U + } else { + UVDirection::V + }; + self.iterate(id, options, 0, direction); } - /// Iterate over the nodes and divide them if necessary - /// todo: should not divide if a degree on current direction is 1 & duplicate vertices to avoid share a normal + /// iterate over the nodes and divide them if necessary fn iterate( &mut self, id: usize, options: &AdaptiveTessellationOptions, current_depth: usize, - horizontal: bool, + direction: UVDirection, ) { let id0 = self.nodes.len(); let id1 = id0 + 1; let node = self.nodes.get_mut(id).unwrap(); - node.evaluate_corners(self.surface); let dividable = node.should_divide(self.surface, options, current_depth); - node.horizontal = match dividable { - DividableDirection::Both => horizontal, - DividableDirection::Vertical => false, - DividableDirection::Horizontal => true, - DividableDirection::None => { + node.direction = match dividable { + Some(DividableDirection::Both) => direction, + Some(DividableDirection::U) => UVDirection::U, + Some(DividableDirection::V) => UVDirection::V, + None => { return; } }; let (c0, c1) = { - if node.horizontal { - let bottom = [ - node.corners[0].clone(), - node.corners[1].clone(), - node.evaluate_mid_point(self.surface, 1), - node.evaluate_mid_point(self.surface, 3), - ]; - let top = [ - node.evaluate_mid_point(self.surface, 3), - node.evaluate_mid_point(self.surface, 1), - node.corners[2].clone(), - node.corners[3].clone(), - ]; - - node.children = vec![id0, id1]; - - //assign neighbors to bottom node - let bottom_neighbors = [ - node.neighbors[0], - node.neighbors[1], - Some(id1), - node.neighbors[3], - ]; - - //assign neighbors to top node - let top_neighbors = [ - Some(id0), - node.neighbors[1], - node.neighbors[2], - node.neighbors[3], - ]; - - ( - AdaptiveTessellationNode::new(id0, bottom, Some(bottom_neighbors)), - AdaptiveTessellationNode::new(id1, top, Some(top_neighbors)), - ) - } else { - let left = [ - node.corners[0].clone(), - node.evaluate_mid_point(self.surface, 0), - node.evaluate_mid_point(self.surface, 2), - node.corners[3].clone(), - ]; - let right = [ - node.evaluate_mid_point(self.surface, 0), - node.corners[1].clone(), - node.corners[2].clone(), - node.evaluate_mid_point(self.surface, 2), - ]; - - node.children = vec![id0, id1]; - - let left_neighbors = [ - node.neighbors[0], - Some(id1), - node.neighbors[2], - node.neighbors[3], - ]; - let right_neighbors = [ - node.neighbors[0], - node.neighbors[1], - node.neighbors[2], - Some(id0), - ]; - - ( - AdaptiveTessellationNode::new(id0, left, Some(left_neighbors)), - AdaptiveTessellationNode::new(id1, right, Some(right_neighbors)), - ) + match node.direction { + UVDirection::U => { + let east = node.evaluate_mid_point(self.surface, NeighborDirection::East); + let west = node.evaluate_mid_point(self.surface, NeighborDirection::West); + let bottom = [ + node.corners[0].clone(), + node.corners[1].clone(), + east.clone(), + west.clone(), + ]; + let top = [west, east, node.corners[2].clone(), node.corners[3].clone()]; + + node.assign_children([id0, id1]); + + //assign neighbors to bottom node + let bottom_neighbors = [ + node.neighbors[0], + node.neighbors[1], + Some(id1), + node.neighbors[3], + ]; + + //assign neighbors to top node + let top_neighbors = [ + Some(id0), + node.neighbors[1], + node.neighbors[2], + node.neighbors[3], + ]; + + ( + AdaptiveTessellationNode::new(id0, bottom, bottom_neighbors), + AdaptiveTessellationNode::new(id1, top, top_neighbors), + ) + } + UVDirection::V => { + let south = node.evaluate_mid_point(self.surface, NeighborDirection::South); + let north = node.evaluate_mid_point(self.surface, NeighborDirection::North); + + let left = [ + node.corners[0].clone(), + south.clone(), + north.clone(), + node.corners[3].clone(), + ]; + let right = [ + south, + node.corners[1].clone(), + node.corners[2].clone(), + north, + ]; + + node.assign_children([id0, id1]); + + let left_neighbors = [ + node.neighbors[0], + Some(id1), + node.neighbors[2], + node.neighbors[3], + ]; + let right_neighbors = [ + node.neighbors[0], + node.neighbors[1], + node.neighbors[2], + Some(id0), + ]; + + ( + AdaptiveTessellationNode::new(id0, left, left_neighbors), + AdaptiveTessellationNode::new(id1, right, right_neighbors), + ) + } } }; @@ -198,7 +154,7 @@ where //divide all children recursively for child in [id0, id1] { - self.iterate(child, options, current_depth + 1, !horizontal); + self.iterate(child, options, current_depth + 1, direction.opposite()); } } } diff --git a/src/tessellation/boundary_constraints.rs b/src/tessellation/boundary_constraints.rs new file mode 100644 index 0000000..b467088 --- /dev/null +++ b/src/tessellation/boundary_constraints.rs @@ -0,0 +1,223 @@ +use itertools::Itertools; +use nalgebra::{ + allocator::Allocator, DefaultAllocator, DimName, DimNameDiff, DimNameSub, Vector2, U1, +}; + +use crate::{misc::FloatingPoint, surface::NurbsSurface}; + +use super::SurfacePoint; + +/// A struct representing constraints at the boundary of a surface tessellation +#[derive(Clone, Debug)] +pub struct BoundaryConstraints { + v_parameters_at_u_min: Option>, + u_parameters_at_v_min: Option>, + v_parameters_at_u_max: Option>, + u_parameters_at_v_max: Option>, +} + +impl Default for BoundaryConstraints { + fn default() -> Self { + Self { + v_parameters_at_u_min: None, + u_parameters_at_v_min: None, + v_parameters_at_u_max: None, + u_parameters_at_v_max: None, + } + } +} + +impl BoundaryConstraints { + pub fn new() -> Self { + Self::default() + } + + pub fn with_v_parameters_at_u_min(mut self, v_parameters: Vec) -> Self { + self.v_parameters_at_u_min = Some(v_parameters); + self + } + + pub fn with_u_parameters_at_v_min(mut self, u_parameters: Vec) -> Self { + self.u_parameters_at_v_min = Some(u_parameters); + self + } + + pub fn with_v_parameters_at_u_max(mut self, v_parameters: Vec) -> Self { + self.v_parameters_at_u_max = Some(v_parameters); + self + } + + pub fn with_u_parameters_at_v_max(mut self, u_parameters: Vec) -> Self { + self.u_parameters_at_v_max = Some(u_parameters); + self + } + + pub fn u_parameters_at_v_min(&self) -> Option<&Vec> { + self.u_parameters_at_v_min.as_ref() + } + + pub fn u_parameters_at_v_max(&self) -> Option<&Vec> { + self.u_parameters_at_v_max.as_ref() + } + + pub fn v_parameters_at_u_min(&self) -> Option<&Vec> { + self.v_parameters_at_u_min.as_ref() + } + + pub fn v_parameters_at_u_max(&self) -> Option<&Vec> { + self.v_parameters_at_u_max.as_ref() + } + + pub fn u_parameters(&self) -> Option> { + self.sorted_parameters( + self.u_parameters_at_v_min.as_ref(), + self.u_parameters_at_v_max.as_ref(), + ) + } + + pub fn v_parameters(&self) -> Option> { + self.sorted_parameters( + self.v_parameters_at_u_min.as_ref(), + self.v_parameters_at_u_max.as_ref(), + ) + } + + fn sorted_parameters(&self, min: Option<&Vec>, max: Option<&Vec>) -> Option> { + match (min, max) { + (None, None) => None, + (None, Some(ma)) => Some(ma.clone()), + (Some(mi), None) => Some(mi.clone()), + (Some(mi), Some(ma)) => Some( + mi.iter() + .chain(ma.iter()) + .sorted_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .dedup() + .cloned() + .collect(), + ), + } + } +} + +/// A struct representing the boundary evaluation of a surface +#[derive(Clone, Debug)] +pub struct BoundaryEvaluation +where + D: DimNameSub, + DefaultAllocator: Allocator, + DefaultAllocator: Allocator>, +{ + pub(crate) u_points_at_v_min: Option>>>, + pub(crate) u_points_at_v_max: Option>>>, + pub(crate) v_points_at_u_min: Option>>>, + pub(crate) v_points_at_u_max: Option>>>, +} + +impl BoundaryEvaluation +where + D: DimNameSub, + DefaultAllocator: Allocator, + DefaultAllocator: Allocator>, +{ + /// Create a new boundary evaluation + pub fn new(surface: &NurbsSurface, constraints: &BoundaryConstraints) -> Self { + let (u, v) = surface.knots_domain(); + + let evaluate = |uv: Vector2| { + let deriv = surface.rational_derivatives(uv.x, uv.y, 1); + let pt = deriv[0][0].clone(); + let norm = deriv[1][0].cross(&deriv[0][1]); + let is_normal_degenerated = norm.magnitude_squared() < T::default_epsilon(); + SurfacePoint::new(uv, pt.into(), norm, is_normal_degenerated) + }; + + Self { + u_points_at_v_min: constraints.u_parameters_at_v_min().map(|u_parameters| { + u_parameters + .iter() + .map(|u| evaluate(Vector2::new(*u, v.0))) + .collect() + }), + u_points_at_v_max: constraints.u_parameters_at_v_max().map(|u_parameters| { + u_parameters + .iter() + .map(|u| evaluate(Vector2::new(*u, v.1))) + .collect() + }), + v_points_at_u_min: constraints.v_parameters_at_u_min().map(|v_parameters| { + v_parameters + .iter() + .map(|v| evaluate(Vector2::new(u.0, *v))) + .collect() + }), + v_points_at_u_max: constraints.v_parameters_at_u_max().map(|v_parameters| { + v_parameters + .iter() + .map(|v| evaluate(Vector2::new(u.1, *v))) + .collect() + }), + } + } + + pub fn closest_point_at_u_min( + &self, + point: &SurfacePoint>, + ) -> SurfacePoint> { + if let Some(u_points) = self.u_points_at_v_min.as_ref() { + self.closest_point(point, u_points) + } else { + point.clone() + } + } + + pub fn closest_point_at_u_max( + &self, + point: &SurfacePoint>, + ) -> SurfacePoint> { + if let Some(u_points) = self.u_points_at_v_max.as_ref() { + self.closest_point(point, u_points) + } else { + point.clone() + } + } + + pub fn closest_point_at_v_min( + &self, + point: &SurfacePoint>, + ) -> SurfacePoint> { + if let Some(v_points) = self.v_points_at_u_min.as_ref() { + self.closest_point(point, v_points) + } else { + point.clone() + } + } + + pub fn closest_point_at_v_max( + &self, + point: &SurfacePoint>, + ) -> SurfacePoint> { + if let Some(v_points) = self.v_points_at_u_max.as_ref() { + self.closest_point(point, v_points) + } else { + point.clone() + } + } + + /// Find the closest point to the given point from the list of points + fn closest_point( + &self, + point: &SurfacePoint>, + points: &[SurfacePoint>], + ) -> SurfacePoint> { + points + .iter() + .map(|p| { + let dist = (p.point() - point.point()).norm_squared(); + (p, dist) + }) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) + .unwrap() + .0 + .clone() + } +} diff --git a/src/tessellation/mod.rs b/src/tessellation/mod.rs index e7c2380..c3d9d33 100644 --- a/src/tessellation/mod.rs +++ b/src/tessellation/mod.rs @@ -1,18 +1,32 @@ pub mod adaptive_tessellation_node; pub mod adaptive_tessellation_option; pub mod adaptive_tessellation_processor; +pub mod boundary_constraints; pub mod surface; pub mod surface_point; pub mod surface_tessellation; pub mod tessellation_compound_curve; pub mod tessellation_curve; pub mod tessellation_region; -// pub mod trimmed_surface; +pub mod trimmed_surface; pub use surface_point::*; +/// A trait for tessellating a shape pub trait Tessellation { type Option; type Output; fn tessellate(&self, options: Self::Option) -> Self::Output; } + +/// A trait for tessellating a shape with constraints +pub trait ConstrainedTessellation { + type Constraint; + type Option; + type Output; + fn constrained_tessellate( + &self, + constraints: Self::Constraint, + options: Self::Option, + ) -> Self::Output; +} diff --git a/src/tessellation/surface.rs b/src/tessellation/surface.rs index dd682d4..47488b9 100644 --- a/src/tessellation/surface.rs +++ b/src/tessellation/surface.rs @@ -1,8 +1,10 @@ use super::adaptive_tessellation_option::AdaptiveTessellationOptions; use super::adaptive_tessellation_processor::AdaptiveTessellationProcessor; +use super::boundary_constraints::BoundaryConstraints; use super::surface_tessellation::SurfaceTessellation; -use super::SurfacePoint; use super::{adaptive_tessellation_node::AdaptiveTessellationNode, Tessellation}; +use super::{ConstrainedTessellation, SurfacePoint}; +use itertools::Itertools; use nalgebra::{ allocator::Allocator, DefaultAllocator, DimName, DimNameDiff, DimNameSub, Vector2, U1, }; @@ -23,101 +25,230 @@ where /// or else it will be tessellated adaptively based on the options /// this `adaptive` means that the surface will be tessellated based on the curvature of the surface fn tessellate(&self, adaptive_options: Self::Option) -> Self::Output { - let is_adaptive = adaptive_options.is_some(); - let options = adaptive_options.unwrap_or_default(); - - let us: Vec<_> = if self.u_degree() <= 1 { - self.u_knots() - .iter() - .skip(1) - .take(self.u_knots().len() - 2) - .cloned() - .collect() + surface_adaptive_tessellate(self, None, adaptive_options) + } +} + +impl ConstrainedTessellation for NurbsSurface +where + D: DimNameSub, + DefaultAllocator: Allocator, + DefaultAllocator: Allocator>, +{ + type Constraint = BoundaryConstraints; + type Option = Option>; + type Output = SurfaceTessellation; + + /// Tessellate the surface into a meshable form with constraints on the boundary + /// Parameters on the boundary are computed before surface tessellation to ensure that the subdivided vertices are the same at the boundary + fn constrained_tessellate( + &self, + constraints: Self::Constraint, + adaptive_options: Self::Option, + ) -> Self::Output { + surface_adaptive_tessellate(self, Some(constraints), adaptive_options) + } +} + +/// Tessellate the surface adaptively +fn surface_adaptive_tessellate( + s: &NurbsSurface, + constraints: Option>, + adaptive_options: Option>, +) -> SurfaceTessellation +where + D: DimName, + D: DimNameSub, + DefaultAllocator: Allocator, + DefaultAllocator: Allocator>, +{ + let is_adaptive = adaptive_options.is_some(); + let options = adaptive_options.unwrap_or_default(); + + let us: Vec<_> = if s.u_degree() <= 1 { + s.u_knots() + .iter() + .skip(1) + .take(s.u_knots().len() - 2) + .cloned() + .collect() + } else { + let min_u = (s.control_points().len() - 1) * 2; + let divs_u = options.min_divs_u.max(min_u); + let (umin, umax) = s.u_knots_domain(); + let du = (umax - umin) / T::from_usize(divs_u).unwrap(); + (0..=divs_u) + .map(|i| umin + du * T::from_usize(i).unwrap()) + .collect() + }; + + let vs: Vec<_> = if s.v_degree() <= 1 { + s.v_knots() + .iter() + .skip(1) + .take(s.v_knots().len() - 2) + .cloned() + .collect() + } else { + let min_v = (s.control_points()[0].len() - 1) * 2; + let divs_v = options.min_divs_v.max(min_v); + let (vmin, vmax) = s.v_knots_domain(); + let dv = (vmax - vmin) / T::from_usize(divs_v).unwrap(); + (0..=divs_v) + .map(|i| vmin + dv * T::from_usize(i).unwrap()) + .collect() + }; + + // insert boundary parameters to us & vs if constraints are provided + let (us, vs) = if let Some(c) = constraints.as_ref() { + let us = if let Some(iu) = c.u_parameters() { + merge_sorted_parameters(us, iu) } else { - let min_u = (self.control_points().len() - 1) * 2; - let divs_u = options.min_divs_u.max(min_u); - let (umin, umax) = self.u_knots_domain(); - let du = (umax - umin) / T::from_usize(divs_u).unwrap(); - (0..=divs_u) - .map(|i| umin + du * T::from_usize(i).unwrap()) - .collect() + us }; - - let vs: Vec<_> = if self.v_degree() <= 1 { - self.v_knots() - .iter() - .skip(1) - .take(self.v_knots().len() - 2) - .cloned() - .collect() + let vs = if let Some(iv) = c.v_parameters() { + merge_sorted_parameters(vs, iv) } else { - let min_v = (self.control_points()[0].len() - 1) * 2; - let divs_v = options.min_divs_v.max(min_v); - let (vmin, vmax) = self.v_knots_domain(); - let dv = (vmax - vmin) / T::from_usize(divs_v).unwrap(); - (0..=divs_v) - .map(|i| vmin + dv * T::from_usize(i).unwrap()) - .collect() + vs }; + (us, vs) + } else { + (us, vs) + }; + + let (u_min_constraint, u_max_constraint, v_min_constraint, v_max_constraint) = constraints + .as_ref() + .map(|c| { + let u_in_v_min = c.u_parameters_at_v_min().is_some(); + let u_in_v_max = c.u_parameters_at_v_max().is_some(); + let v_in_u_min = c.v_parameters_at_u_min().is_some(); + let v_in_u_max = c.v_parameters_at_u_max().is_some(); + (u_in_v_min, u_in_v_max, v_in_u_min, v_in_u_max) + }) + .unwrap_or((false, false, false, false)); + + let divs_u = us.len() - 1; + let divs_v = vs.len() - 1; - let mut pts = vec![]; + let pts = vs + .iter() + .enumerate() + .map(|(iv, v)| { + let u_min = iv == 0; + let u_max = iv == divs_v - 1; + let u_constraint = (u_min && u_min_constraint) || (u_max && u_max_constraint); - vs.iter().for_each(|v| { - let mut row = vec![]; - us.iter().for_each(|u| { - let ds = self.rational_derivatives(*u, *v, 1); + let row = us.iter().enumerate().map(|(iu, u)| { + let ds = s.rational_derivatives(*u, *v, 1); let norm = ds[1][0].cross(&ds[0][1]).normalize(); - row.push(SurfacePoint { - point: ds[0][0].clone().into(), - normal: norm, - uv: Vector2::new(*u, *v), - is_normal_degenerated: false, - }); + let v_min = iu == 0; + let v_max = iu == divs_u - 1; + let v_constraint = (v_min && v_min_constraint) || (v_max && v_max_constraint); + SurfacePoint::new(Vector2::new(*u, *v), ds[0][0].clone().into(), norm, false) + .with_constraints(u_constraint, v_constraint) + .with_boundary(u_min, u_max, v_min, v_max) }); - pts.push(row); - }); - - let mut divs = vec![]; - let divs_u = us.len() - 1; - let divs_v = vs.len() - 1; - for i in 0..divs_v { - for j in 0..divs_u { - let iv = divs_v - i; + row.collect_vec() + }) + .collect_vec(); + + let pts = &pts; + + let nodes = (0..divs_v) + .flat_map(|iv: usize| { + let iv_r = divs_v - iv; + (0..divs_u).map(move |iu| { let corners = [ - pts[iv - 1][j].clone(), - pts[iv - 1][j + 1].clone(), - pts[iv][j + 1].clone(), - pts[iv][j].clone(), + pts[iv_r - 1][iu].clone(), + pts[iv_r - 1][iu + 1].clone(), + pts[iv_r][iu + 1].clone(), + pts[iv_r][iu].clone(), ]; - let node = AdaptiveTessellationNode::new(divs.len(), corners, None); - divs.push(node); + let index = iv * divs_u + iu; + let s = south(index, iv, divs_u, divs_v); + let e = east(index, iu, divs_u); + let n = north(index, iv, divs_u); + let w = west(index, iv); + AdaptiveTessellationNode::new(index, corners, [s, e, n, w]) + }) + }) + .collect_vec(); + + let nodes = if !is_adaptive { + nodes + } else { + let mut processor = AdaptiveTessellationProcessor::new(s, nodes); + + for iv in 0..divs_v { + for iu in 0..divs_u { + let index = iv * divs_u + iu; + processor.divide(index, &options); } } - let nodes = if !is_adaptive { - divs - } else { - let mut processor = AdaptiveTessellationProcessor { - surface: self, - nodes: divs, - }; - - for i in 0..divs_v { - for j in 0..divs_u { - let ci = i * divs_u + j; - let s = processor.south(ci, i, j, divs_u, divs_v).map(|n| n.id); - let e = processor.east(ci, i, j, divs_u, divs_v).map(|n| n.id); - let n = processor.north(ci, i, j, divs_u, divs_v).map(|n| n.id); - let w = processor.west(ci, i, j, divs_u, divs_v).map(|n| n.id); - let node = processor.nodes.get_mut(ci).unwrap(); - node.neighbors = [s, e, n, w]; - processor.divide(ci, &options); - } - } + processor.into_nodes() + }; - processor.nodes - }; + SurfaceTessellation::new(s, &nodes, constraints) +} + +fn north(index: usize, iv: usize, divs_u: usize) -> Option { + if iv == 0 { + None + } else { + Some(index - divs_u) + } +} - SurfaceTessellation::new(self, &nodes) +fn south(index: usize, iv: usize, divs_u: usize, divs_v: usize) -> Option { + if iv == divs_v - 1 { + None + } else { + Some(index + divs_u) } } + +fn east(index: usize, iu: usize, divs_u: usize) -> Option { + if iu == divs_u - 1 { + None + } else { + Some(index + 1) + } +} + +fn west(index: usize, iv: usize) -> Option { + if iv == 0 { + None + } else { + Some(index - 1) + } +} + +/// Merge two sorted vectors +fn merge_sorted_parameters(p0: Vec, p1: Vec) -> Vec { + let mut c0 = 0; + let mut c1 = 0; + let mut res = vec![]; + while c0 < p0.len() && c1 < p1.len() { + if p0[c0] < p1[c1] { + res.push(p0[c0]); + c0 += 1; + } else if p0[c0] > p1[c1] { + res.push(p1[c1]); + c1 += 1; + } else { + res.push(p0[c0]); + c0 += 1; + c1 += 1; + } + } + + // append the remaining elements + if c0 < p0.len() { + res.extend_from_slice(&p0[c0..]); + } else if c1 < p1.len() { + res.extend_from_slice(&p1[c1..]); + } + + res +} diff --git a/src/tessellation/surface_point.rs b/src/tessellation/surface_point.rs index 3a7d417..69fddfb 100644 --- a/src/tessellation/surface_point.rs +++ b/src/tessellation/surface_point.rs @@ -12,14 +12,96 @@ where pub uv: Vector2, pub point: OPoint, pub normal: OVector, - pub is_normal_degenerated: bool, + is_normal_degenerated: bool, + // constraints to be divided + u_constraint: bool, + v_constraint: bool, + // flags to indicate if this point is on the boundary or not + u_min: bool, + u_max: bool, + v_min: bool, + v_max: bool, } impl SurfacePoint where DefaultAllocator: Allocator, { + pub fn new( + uv: Vector2, + point: OPoint, + normal: OVector, + is_normal_degenerated: bool, + ) -> Self { + Self { + uv, + point, + normal, + is_normal_degenerated, + u_constraint: false, + v_constraint: false, + u_min: false, + u_max: false, + v_min: false, + v_max: false, + } + } + + pub fn with_constraints(mut self, u: bool, v: bool) -> Self { + self.u_constraint = u; + self.v_constraint = v; + self + } + + pub fn is_u_constrained(&self) -> bool { + self.u_constraint + } + + pub fn is_v_constrained(&self) -> bool { + self.v_constraint + } + + pub fn with_boundary(mut self, u_min: bool, u_max: bool, v_min: bool, v_max: bool) -> Self { + self.u_min = u_min; + self.u_max = u_max; + self.v_min = v_min; + self.v_max = v_max; + self + } + + pub fn is_u_min(&self) -> bool { + self.u_min + } + + pub fn is_u_max(&self) -> bool { + self.u_max + } + + pub fn is_v_min(&self) -> bool { + self.v_min + } + + pub fn is_v_max(&self) -> bool { + self.v_max + } + + pub fn into_tuple(self) -> (Vector2, OPoint, OVector) { + (self.uv, self.point, self.normal) + } + pub fn normal(&self) -> &OVector { &self.normal } + + pub fn point(&self) -> &OPoint { + &self.point + } + + pub fn uv(&self) -> &Vector2 { + &self.uv + } + + pub fn is_normal_degenerated(&self) -> bool { + self.is_normal_degenerated + } } diff --git a/src/tessellation/surface_tessellation.rs b/src/tessellation/surface_tessellation.rs index a4664dd..1616789 100644 --- a/src/tessellation/surface_tessellation.rs +++ b/src/tessellation/surface_tessellation.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use nalgebra::{ allocator::Allocator, Const, DefaultAllocator, DimName, DimNameDiff, DimNameSub, OPoint, OVector, Vector2, U1, @@ -9,6 +10,8 @@ use crate::{ tessellation::adaptive_tessellation_node::AdaptiveTessellationNode, }; +use super::boundary_constraints::{BoundaryConstraints, BoundaryEvaluation}; + /// Surface tessellation representation /// This struct is used to create a mesh data from surface #[derive(Clone, Debug)] @@ -37,7 +40,11 @@ where DefaultAllocator: Allocator>, { /// Create a new surface tessellation from surface and adaptive tessellation nodes - pub fn new(surface: &NurbsSurface, nodes: &Vec>) -> Self { + pub fn new( + surface: &NurbsSurface, + nodes: &Vec>, + constraints: Option>, + ) -> Self { let mut tess = Self { points: Default::default(), normals: Default::default(), @@ -45,9 +52,11 @@ where uvs: Default::default(), }; - // Triangulate only leaf nodes - nodes.iter().filter(|n| n.is_leaf()).for_each(|node| { - tess.triangulate(surface, nodes, node); + let boundary_evaluation = constraints.map(|c| BoundaryEvaluation::new(surface, &c)); + + // Triangulate all nodes + nodes.iter().for_each(|node| { + tess.triangulate(surface, nodes, node, boundary_evaluation.as_ref()); }); tess @@ -59,30 +68,48 @@ where surface: &NurbsSurface, nodes: &Vec>, node: &AdaptiveTessellationNode, + boundary_evaluation: Option<&BoundaryEvaluation>, ) { if node.is_leaf() { - let mut base_index = self.points.len(); - let mut uvs = vec![]; - let mut ids = vec![]; - let mut split_id = 0; - for i in 0..4 { - let edge_corners = node.get_all_corners(nodes, i); - if edge_corners.len() == 2 { - split_id = i + 1; - } + let corners = (0..4).map(|i| node.get_all_corners(nodes, i)).collect_vec(); + let split_id = corners + .iter() + .position(|c| c.len() == 2) + .map(|i| i + 1) + .unwrap_or(0); + let uvs = corners.into_iter().flatten().collect_vec(); + + let uvs = if let Some(boundary_evaluation) = boundary_evaluation { + uvs.iter() + .map(|uv| { + if uv.is_u_min() { + boundary_evaluation.closest_point_at_u_min(uv) + } else if uv.is_u_max() { + boundary_evaluation.closest_point_at_u_max(uv) + } else if uv.is_v_min() { + boundary_evaluation.closest_point_at_v_min(uv) + } else if uv.is_v_max() { + boundary_evaluation.closest_point_at_v_max(uv) + } else { + uv.clone() + } + }) + .collect_vec() + } else { + uvs + }; - uvs.extend(edge_corners); + let base_index = self.points.len(); + let n = uvs.len(); + let ids = (0..n).map(|i| base_index + i).collect_vec(); + for corner in uvs.into_iter() { + let (uv, point, normal) = corner.into_tuple(); + self.points.push(point); + self.normals.push(normal); + self.uvs.push(uv); } - uvs.iter().for_each(|corner| { - self.points.push(corner.point.clone()); - self.normals.push(corner.normal.clone()); - self.uvs.push(corner.uv); - ids.push(base_index); - base_index += 1; - }); - - match uvs.len() { + match n { 4 => { self.faces.push([ids[0], ids[1], ids[3]]); self.faces.push([ids[3], ids[1], ids[2]]); @@ -114,11 +141,6 @@ where } } }; - } else { - node.children.iter().for_each(|child| { - let c = &nodes[*child]; - self.triangulate(surface, nodes, c); - }); } } diff --git a/tests/serialize.rs b/tests/serialize.rs index 187cd45..494490d 100644 --- a/tests/serialize.rs +++ b/tests/serialize.rs @@ -1,8 +1,8 @@ #![allow(unused_imports)] use approx::assert_relative_eq; -use curvo::prelude::{NurbsCurve3D, NurbsSurface, NurbsSurface3D}; -use nalgebra::{Point3, Vector3}; +use curvo::prelude::{FloatingPoint, NurbsCurve, NurbsCurve3D, NurbsSurface, NurbsSurface3D}; +use nalgebra::{allocator::Allocator, DefaultAllocator, DimName, Point3, Vector3}; #[test] #[cfg(feature = "serde")] @@ -16,19 +16,7 @@ fn test_curve_serialization() { // println!("{}", json); let der: NurbsCurve3D = serde_json::from_str(&json).unwrap(); - assert_relative_eq!( - curve - .control_points_iter() - .flat_map(|p| p.coords.as_slice()) - .collect::>() - .as_slice(), - der.control_points_iter() - .flat_map(|p| p.coords.as_slice()) - .collect::>() - .as_slice() - ); - assert_eq!(curve.degree(), der.degree()); - assert_relative_eq!(curve.knots().as_slice(), der.knots().as_slice()); + assert_curve_eq(&curve, &der); let compound = CompoundCurve::new(vec![curve.clone()]); let json = serde_json::to_string_pretty(&compound).unwrap(); @@ -49,21 +37,75 @@ fn test_surface_serialization() { let der: NurbsSurface3D = serde_json::from_str(&json).unwrap(); // println!("{}", json); + assert_surface_eq(&surface, &der); +} + +#[test] +#[cfg(feature = "serde")] +fn test_trimmed_surface_serialization() { + use curvo::prelude::{NurbsCurve2D, TrimmedSurface}; + use nalgebra::{Point2, Vector2}; + + let profile = NurbsCurve3D::polyline(&[Point3::origin(), Point3::new(5., 0., 0.)], true); + let plane_surface = NurbsSurface::extrude(&profile, &(Vector3::z() * 10.)); + let trimmed = TrimmedSurface::new( + plane_surface.clone(), + None, + vec![ + NurbsCurve2D::try_circle(&Point2::new(0.5, 0.5), &Vector2::x(), &Vector2::y(), 0.25) + .unwrap(), + ], + ); + let json = serde_json::to_string_pretty(&trimmed).unwrap(); + let der: TrimmedSurface = serde_json::from_str(&json).unwrap(); + + assert_surface_eq(trimmed.surface(), der.surface()); + assert_eq!(trimmed.exterior().is_none(), der.exterior().is_none()); + + trimmed + .interiors() + .iter() + .zip(der.interiors()) + .for_each(|(a, b)| { + assert_curve_eq(a, b); + }); +} + +#[allow(dead_code)] +fn assert_curve_eq(a: &NurbsCurve, b: &NurbsCurve) +where + DefaultAllocator: Allocator, +{ + assert_eq!(a.degree(), b.degree()); + assert_relative_eq!(a.knots().as_slice(), b.knots().as_slice()); + assert_relative_eq!( + a.control_points_iter() + .flat_map(|p| p.coords.as_slice()) + .collect::>() + .as_slice(), + b.control_points_iter() + .flat_map(|p| p.coords.as_slice()) + .collect::>() + .as_slice() + ); +} + +#[allow(dead_code)] +fn assert_surface_eq(a: &NurbsSurface3D, b: &NurbsSurface3D) { + assert_eq!(a.u_degree(), b.u_degree()); + assert_eq!(a.v_degree(), b.v_degree()); + assert_relative_eq!(a.u_knots().as_slice(), b.u_knots().as_slice()); + assert_relative_eq!(a.v_knots().as_slice(), b.v_knots().as_slice()); assert_relative_eq!( - surface - .control_points() + a.control_points() .iter() .flat_map(|column| { column.iter().flat_map(|p| p.coords.as_slice()) }) .collect::>() .as_slice(), - der.control_points() + b.control_points() .iter() .flat_map(|column| { column.iter().flat_map(|p| p.coords.as_slice()) }) .collect::>() .as_slice() ); - assert_eq!(surface.u_degree(), der.u_degree()); - assert_eq!(surface.v_degree(), der.v_degree()); - assert_relative_eq!(surface.u_knots().as_slice(), der.u_knots().as_slice()); - assert_relative_eq!(surface.v_knots().as_slice(), der.v_knots().as_slice()); }