diff --git a/Cargo.lock b/Cargo.lock index a6e5fd76..8dde6c84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80179d7dd5d7e8c285d67c4a1e652972a92de7475beddfb92028c76463b13225" +checksum = "2e53b0a3d5760cd2ba9b787ae0c6440ad18ee294ff71b05e3381c900a7d16cfd" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -41,7 +41,7 @@ checksum = "cd3b6ae1eabbfbced10e840fd3fce8a93ae84f174b3e4ba892ab7bcb42e477a7" dependencies = [ "accesskit", "accesskit_consumer", - "objc2", + "objc2 0.3.0-beta.3.patch-leaks.3", "once_cell", ] @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -101,18 +101,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alsa" @@ -182,47 +182,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -230,9 +231,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "approx" @@ -245,19 +246,18 @@ dependencies = [ [[package]] name = "arboard" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", - "core-graphics 0.23.1", - "image", + "core-graphics 0.23.2", + "image 0.25.1", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", - "thiserror", "windows-sys 0.48.0", "x11rb", ] @@ -306,27 +306,25 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ - "async-lock 3.3.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-lite 2.3.0", "slab", ] @@ -337,7 +335,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" dependencies = [ - "async-lock 2.8.0", + "async-lock", "autocfg", "blocking", "futures-lite 1.13.0", @@ -352,22 +350,11 @@ dependencies = [ "event-listener 2.5.3", ] -[[package]] -name = "async-lock" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" -dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", - "pin-project-lite", -] - [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "atk-sys" @@ -398,15 +385,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -429,6 +416,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bevy" version = "0.12.1" @@ -493,7 +486,7 @@ checksum = "935984568f75867dd7357133b06f4b1502cd2be55e4642d483ce597e46e63bff" dependencies = [ "async-broadcast", "async-fs", - "async-lock 2.8.0", + "async-lock", "bevy_app", "bevy_asset_macros", "bevy_ecs", @@ -526,7 +519,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -592,7 +585,7 @@ checksum = "f484318350462c58ba3942a45a656c1fd6b6e484a6b6b7abc3a787ad1a51e500" dependencies = [ "bevy_macro_utils", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -640,7 +633,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -743,7 +736,7 @@ dependencies = [ "bevy_transform", "gltf", "gltf-json", - "image", + "image 0.24.9", "serde", "serde_json", "thiserror", @@ -850,7 +843,7 @@ dependencies = [ "proc-macro2", "quote", "rustc-hash", - "syn 2.0.53", + "syn 2.0.67", "toml_edit 0.20.7", ] @@ -880,7 +873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35d72134cd809630e78bc953318156f37ff2839a50bebb148bc5c3a669e133e0" dependencies = [ "bevy", - "bitfield", + "bitfield 0.14.0", "interpolation", "thiserror", "wgpu-types", @@ -996,7 +989,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", "uuid", ] @@ -1031,7 +1024,7 @@ dependencies = [ "encase", "futures-lite 1.13.0", "hexasphere", - "image", + "image 0.24.9", "js-sys", "ktx2", "naga", @@ -1055,7 +1048,7 @@ dependencies = [ "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -1221,7 +1214,7 @@ dependencies = [ "ahash", "bevy_utils_proc_macros", "getrandom", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "instant", "nonmax", "petgraph", @@ -1238,7 +1231,7 @@ checksum = "7aafecc952b6b8eb1a93c12590bd867d25df2f4ae1033a01dfdfc3c35ebccfff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -1299,7 +1292,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -1329,6 +1322,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +[[package]] +name = "bitfield" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c821a6e124197eb56d907ccc2188eab1038fb919c914f47976e64dd8dbc855d1" + [[package]] name = "bitflags" version = "1.3.2" @@ -1378,7 +1377,7 @@ version = "0.1.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" dependencies = [ - "objc-sys", + "objc-sys 0.2.0-beta.2", ] [[package]] @@ -1388,23 +1387,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" dependencies = [ "block-sys", - "objc2-encode", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", ] [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.2.0", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", - "fastrand 2.0.1", "futures-io", "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] @@ -1419,28 +1424,28 @@ 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.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -1451,9 +1456,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cairo-sys-rs" @@ -1467,12 +1472,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -1492,9 +1498,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon", @@ -1512,23 +1518,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] name = "chrono-tz" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" dependencies = [ "chrono", "chrono-tz-build", @@ -1537,9 +1549,9 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" dependencies = [ "parse-zoneinfo", "phf", @@ -1548,9 +1560,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -1559,9 +1571,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -1569,9 +1581,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -1581,27 +1593,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] @@ -1624,9 +1636,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "com-rs" @@ -1636,9 +1648,9 @@ checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -1646,9 +1658,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -1730,9 +1742,9 @@ dependencies = [ [[package]] name = "core-graphics" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1806,9 +1818,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1821,9 +1833,9 @@ checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1849,9 +1861,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -1888,15 +1900,15 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deunicode" -version = "1.4.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "digest" @@ -1946,9 +1958,9 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "earcutr" @@ -1986,7 +1998,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e598cc2bfc28612f26426259ed99a978270e9433d63ae6d2843e30fb0974cd02" dependencies = [ - "async-channel 2.2.0", + "async-channel 2.3.1", "document-features", "js-sys", "ureq", @@ -1997,9 +2009,9 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "emath" @@ -2039,7 +2051,7 @@ checksum = "3fe2568f851fd6144a45fa91cfed8fe5ca8fc0b56ba6797bfc1ed2771b90e37c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -2074,9 +2086,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2090,9 +2102,9 @@ checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" [[package]] name = "euclid" -version = "0.22.9" +version = "0.22.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20" dependencies = [ "num-traits", ] @@ -2105,20 +2117,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -2127,21 +2128,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -2172,9 +2163,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -2193,9 +2184,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -2264,7 +2255,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -2321,7 +2312,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -2418,9 +2409,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", @@ -2441,9 +2432,9 @@ dependencies = [ [[package]] name = "gilrs" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "499067aa54af19f88732dc418f61f23d5912de1518665bb0eca034ca0d07574c" +checksum = "b54e5e39844ab5cddaf3bbbdfdc2923a6cb34e36818b95618da4e3f26302c24c" dependencies = [ "fnv", "gilrs-core", @@ -2454,9 +2445,9 @@ dependencies = [ [[package]] name = "gilrs-core" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c132270a155f2548e67d66e731075c336c39098afc555752f3df8f882c720e" +checksum = "b922f294d9f062af517ea0bd0a036ddcf11c2842211c2f9c71a3ceee859e10b6" dependencies = [ "core-foundation", "inotify", @@ -2470,14 +2461,14 @@ dependencies = [ "vec_map", "wasm-bindgen", "web-sys", - "windows 0.54.0", + "windows 0.57.0", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gio-sys" @@ -2527,17 +2518,17 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] name = "globwalk" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "ignore", "walkdir", ] @@ -2556,9 +2547,9 @@ dependencies = [ [[package]] name = "gltf" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b78f069cf941075835822953c345b9e1edd67ae347b81ace3aea9de38c2ef33" +checksum = "e3ce1918195723ce6ac74e80542c5a96a40c2b26162c1957a5cd70799b8cacf7" dependencies = [ "byteorder", "gltf-json", @@ -2568,21 +2559,21 @@ dependencies = [ [[package]] name = "gltf-derive" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438ffe1a5540d75403feaf23636b164e816e93f6f03131674722b3886ce32a57" +checksum = "14070e711538afba5d6c807edb74bcb84e5dbb9211a3bf5dea0dfab5b24f4c51" dependencies = [ "inflections", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "gltf-json" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655951ba557f2bc69ea4b0799446bae281fa78efae6319968bdd2c3e9a06d8e1" +checksum = "e6176f9d60a7eab0a877e8e96548605dedbde9190a7ae1e80bbcc1c9af03ab14" dependencies = [ "gltf-derive", "serde", @@ -2652,7 +2643,7 @@ checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ "bitflags 2.5.0", "gpu-descriptor-types", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -2701,21 +2692,22 @@ dependencies = [ [[package]] name = "gz-fuel" version = "0.1.0" -source = "git+https://github.com/open-rmf/gz-fuel-rs?branch=luca/ehttp#44b3fc616146a9f0b32652ba9a9fe34eaf40a629" +source = "git+https://github.com/open-rmf/gz-fuel-rs?branch=main#cbdb4b587834ac9af78d0018c6dcd9ab3efd3cb2" dependencies = [ + "crossbeam-channel", "dirs", "ehttp", "futures-lite 2.3.0", - "itertools 0.12.1", + "itertools 0.13.0", "serde", "serde_json", ] [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -2738,9 +2730,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -2867,7 +2859,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.6", + "regex-automata 0.4.7", "same-file", "walkdir", "winapi-util", @@ -2891,6 +2883,19 @@ dependencies = [ "tiff", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits", + "png", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2903,12 +2908,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -2939,9 +2944,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", @@ -2965,6 +2970,12 @@ dependencies = [ "mach2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.11.0" @@ -2983,11 +2994,20 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -3013,9 +3033,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -3060,9 +3080,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -3089,9 +3109,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -3110,7 +3130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3121,9 +3141,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ "bitflags 2.5.0", "libc", @@ -3132,13 +3152,12 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -3159,9 +3178,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litrs" @@ -3171,9 +3190,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3224,9 +3243,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "metal" @@ -3251,9 +3270,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -3314,9 +3333,9 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.32.4" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4541eb06dce09c0241ebbaab7102f0a01a0c8994afed2e5d0d66775016e25ac2" +checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" dependencies = [ "approx", "matrixmultiply", @@ -3393,13 +3412,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.5.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", ] @@ -3446,9 +3465,9 @@ dependencies = [ [[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", ] @@ -3472,7 +3491,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -3486,20 +3505,19 @@ 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-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -3553,7 +3571,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -3565,7 +3583,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -3595,15 +3613,71 @@ version = "0.2.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + [[package]] name = "objc2" version = "0.3.0-beta.3.patch-leaks.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" dependencies = [ - "block2", - "objc-sys", - "objc2-encode", + "block2 0.2.0-alpha.6", + "objc-sys 0.2.0-beta.2", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys 0.3.5", + "objc2-encode 4.0.3", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.5.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.5.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", ] [[package]] @@ -3612,7 +3686,50 @@ version = "2.0.0-pre.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" dependencies = [ - "objc-sys", + "objc-sys 0.2.0-beta.2", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.5.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.5.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.5.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", ] [[package]] @@ -3635,9 +3752,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -3723,9 +3840,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owned_ttf_parser" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" +checksum = "6b41438d2fc63c46c74a2203bf5ccd82c41ba04347b2fcf5754f230b167067d5" dependencies = [ "ttf-parser", ] @@ -3750,9 +3867,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -3760,31 +3877,31 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "parse-zoneinfo" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" dependencies = [ "regex", ] [[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 = "pathdiff" @@ -3800,9 +3917,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -3811,9 +3928,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -3821,22 +3938,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -3845,12 +3962,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.5", + "indexmap 2.2.6", ] [[package]] @@ -3893,18 +4010,18 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.1.0", "futures-io", ] @@ -3963,9 +4080,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -3987,9 +4104,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -4050,9 +4167,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -4092,27 +4209,36 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", - "libredox 0.0.1", + "libredox 0.1.3", "thiserror", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -4126,13 +4252,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -4149,9 +4275,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "renderdoc-sys" @@ -4210,7 +4336,7 @@ dependencies = [ "bevy_obj", "bevy_polyline", "bevy_stl", - "bitfield", + "bitfield 0.15.0", "clap", "console_error_panic_hook", "crossbeam-channel", @@ -4219,7 +4345,7 @@ dependencies = [ "futures-lite 1.13.0", "geo", "gz-fuel", - "itertools 0.12.1", + "itertools 0.13.0", "pathdiff", "rfd", "rmf_site_format", @@ -4300,9 +4426,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4321,9 +4447,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -4334,9 +4460,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -4348,15 +4474,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -4376,15 +4502,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[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", ] @@ -4418,35 +4544,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -4455,9 +4581,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -4555,29 +4681,29 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] [[package]] name = "smol_str" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ "serde", ] [[package]] name = "spade" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61addf9117b11d1f5b4bf6fe94242ba25f59d2d4b2080544b771bd647024fd00" +checksum = "9f4ec45f91925e2c9ab3b6a857ee9ed36916990df76a1c475d783a328e247cc8" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "num-traits", "robust", "smallvec", @@ -4626,21 +4752,21 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "svg_fmt" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83ba502a3265efb76efb89b0a2f7782ad6f2675015d4ce37e4b547dda42b499" +checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" [[package]] name = "syn" @@ -4655,9 +4781,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" dependencies = [ "proc-macro2", "quote", @@ -4693,9 +4819,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2287b6d7f721ada4cddf61ade5e760b2c6207df041cac9bfaa192897362fd3" +checksum = "b1315457ccd9c3def787a18fae91914e623e4dcff019b64ce39f5268ded53d3d" dependencies = [ "arrayvec", "grid", @@ -4711,9 +4837,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tera" -version = "1.19.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" dependencies = [ "chrono", "chrono-tz", @@ -4742,9 +4868,9 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] @@ -4766,18 +4892,18 @@ checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -4818,9 +4944,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tobj" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7ca3ec0405b0f2f95e0dbcced28882190dc79b0dac63ff82533d256d770223" +checksum = "c3bd4ba05f29e4c65b6c0c11a58b6465ffa820bac890d76ad407b4e81d8372e8" dependencies = [ "ahash", "log", @@ -4828,21 +4954,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.8", + "toml_edit 0.22.14", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -4853,7 +4979,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -4864,7 +4990,7 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -4875,22 +5001,22 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -4912,7 +5038,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] @@ -4978,9 +5104,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "twox-hash" @@ -5083,9 +5209,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -5114,11 +5240,11 @@ dependencies = [ [[package]] name = "ureq" -version = "2.9.6" +version = "2.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "flate2", "log", "once_cell", @@ -5131,9 +5257,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -5142,9 +5268,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utm" @@ -5188,9 +5314,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -5229,7 +5355,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", "wasm-bindgen-shared", ] @@ -5263,7 +5389,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5297,9 +5423,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.13" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1b04c569c83a9bb971dd47ec6fd48753315f4bf989b9b04a2e7ca4d7f0dc950" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" dependencies = [ "core-foundation", "home", @@ -5314,9 +5440,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -5428,9 +5554,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.15" +version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89beec544f246e679fc25490e3f8e08003bc4bf612068f325120dad4cea02c1c" +checksum = "8a040b111774ab63a19ef46bbc149398ab372b4ccdcfd719e9814dbd7dfd76c8" dependencies = [ "bytemuck", "safe_arch", @@ -5438,9 +5564,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -5460,11 +5586,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5488,8 +5614,8 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.48.0", + "windows-interface 0.48.0", "windows-targets 0.48.5", ] @@ -5500,7 +5626,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.5", ] [[package]] @@ -5509,7 +5645,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5519,7 +5655,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result", + "windows-targets 0.52.5", ] [[package]] @@ -5533,6 +5681,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.67", +] + [[package]] name = "windows-interface" version = "0.48.0" @@ -5544,13 +5703,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.67", +] + [[package]] name = "windows-result" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5577,7 +5747,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -5612,17 +5782,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -5639,9 +5810,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -5657,9 +5828,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -5675,9 +5846,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -5693,9 +5870,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -5711,9 +5888,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -5729,9 +5906,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -5747,9 +5924,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winit" @@ -5759,7 +5936,7 @@ checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" dependencies = [ "android-activity", "bitflags 1.3.2", - "cfg_aliases", + "cfg_aliases 0.1.1", "core-foundation", "core-graphics 0.22.3", "dispatch", @@ -5768,7 +5945,7 @@ dependencies = [ "log", "mio", "ndk 0.7.0", - "objc2", + "objc2 0.3.0-beta.3.patch-leaks.3", "once_cell", "orbclient", "percent-encoding", @@ -5792,9 +5969,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -5812,9 +5989,9 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix", @@ -5823,9 +6000,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xi-unicode" @@ -5835,9 +6012,9 @@ checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" [[package]] name = "xmltree" @@ -5883,29 +6060,29 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.67", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zune-inflate" diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index 447851bd..8230df9d 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -47,7 +47,7 @@ urdf-rs = "0.7" yaserde = "0.7" utm = "0.1.6" sdformat_rs = { git = "https://github.com/open-rmf/sdf_rust_experimental", rev = "9fc35f2"} -gz-fuel = { git = "https://github.com/open-rmf/gz-fuel-rs", branch = "luca/ehttp" } +gz-fuel = { git = "https://github.com/open-rmf/gz-fuel-rs", branch = "main" } pathdiff = "*" tera = "1.19.1" ehttp = { version = "0.4", features = ["native-async"] } diff --git a/rmf_site_editor/src/aabb.rs b/rmf_site_editor/src/aabb.rs index 1a42323d..6bf91f1a 100644 --- a/rmf_site_editor/src/aabb.rs +++ b/rmf_site_editor/src/aabb.rs @@ -17,10 +17,12 @@ use bevy::{ use smallvec::SmallVec; /// Tracks which [`Entities`](Entity) have which meshes for entities whose [`Aabb`]s are managed by -/// the [`calculate_bounds`] and [`update_bounds`] systems. This is needed because `update_bounds` +/// the [`calculate_bounds`][1] and [`update_bounds`] systems. This is needed because `update_bounds` /// recomputes `Aabb`s for entities whose mesh has been mutated. These mutations are visible via /// [`AssetEvent`](AssetEvent) which tells us which mesh was changed but not which entities /// have that mesh. +/// +/// [1]: bevy::render::view::calculate_bounds #[derive(Debug, Default, Clone, Resource)] pub struct EntityMeshMap { entities_with_mesh: HashMap, SmallVec<[Entity; 1]>>, @@ -76,8 +78,6 @@ pub fn register_bounds( /// Updates [`Aabb`]s for [`Entities`](Entity) with [`Mesh`]es. This includes `Entities` that have /// been assigned new `Mesh`es as well as `Entities` whose `Mesh` has been directly mutated. /// -/// To opt out of bound calculation for an `Entity`, give it the [`NoAabbUpdate`] component. -/// /// NOTE: This system needs to remove entities from their collection in /// [`EntityMeshMap`] whenever a mesh handle is reassigned or an entity's mesh handle is /// removed. This may impact performance if meshes with many entities are frequently diff --git a/rmf_site_editor/src/autoload.rs b/rmf_site_editor/src/autoload.rs new file mode 100644 index 00000000..5608e5f0 --- /dev/null +++ b/rmf_site_editor/src/autoload.rs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::prelude::*; +use std::path::PathBuf; + +#[derive(Resource)] +pub struct Autoload { + pub filename: Option, + pub import: Option, +} + +impl Autoload { + pub fn file(filename: PathBuf, import: Option) -> Self { + Autoload { + filename: Some(filename), + import, + } + } +} diff --git a/rmf_site_editor/src/interaction/gizmo.rs b/rmf_site_editor/src/interaction/gizmo.rs index b3b60d55..196dbaa3 100644 --- a/rmf_site_editor/src/interaction/gizmo.rs +++ b/rmf_site_editor/src/interaction/gizmo.rs @@ -218,7 +218,7 @@ impl Default for GizmoState { } /// Instruction to move an entity to a new transform. This should be caught with -/// an EventReader. +/// an `EventReader`. #[derive(Debug, Clone, Copy, Event)] pub struct MoveTo { pub entity: Entity, diff --git a/rmf_site_editor/src/interaction/mod.rs b/rmf_site_editor/src/interaction/mod.rs index d583b15a..16290689 100644 --- a/rmf_site_editor/src/interaction/mod.rs +++ b/rmf_site_editor/src/interaction/mod.rs @@ -98,7 +98,18 @@ pub struct SiteRaycastSet; #[derive(Default)] pub struct InteractionPlugin { - pub headless: bool, + headless: bool, +} + +impl InteractionPlugin { + pub fn new() -> Self { + Self::default() + } + + pub fn headless(mut self, is_headless: bool) -> Self { + self.headless = is_headless; + self + } } #[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, States)] diff --git a/rmf_site_editor/src/interaction/mode.rs b/rmf_site_editor/src/interaction/mode.rs index 6b25618d..42162586 100644 --- a/rmf_site_editor/src/interaction/mode.rs +++ b/rmf_site_editor/src/interaction/mode.rs @@ -101,8 +101,8 @@ pub struct BackoutParams<'w, 's> { /// cannot expect users to know this or handle the cleanup correctly. /// /// User-defined systems should never -/// use ResMut. Instead they should always use -/// EventWriter. +/// use `ResMut`. Instead they should always use +/// `EventWriter`. // // TODO(MXG): We could enforce this by letting InteractionMode be public but // wrapping it in a newtype to store it in the resource. The inner type would diff --git a/rmf_site_editor/src/issue.rs b/rmf_site_editor/src/issue.rs index eeab0ef9..84dbab60 100644 --- a/rmf_site_editor/src/issue.rs +++ b/rmf_site_editor/src/issue.rs @@ -16,15 +16,11 @@ */ use crate::site::ChangePlugin; -use crate::widgets::{ - diagnostic_window::DiagnosticWindowState, - menu_bar::{MenuEvent, MenuItem, MenuVisualizationStates, ToolMenu}, +use bevy::{ + prelude::*, + utils::{HashMap, Uuid}, }; -use crate::AppState; -use bevy::prelude::*; -use bevy::utils::{HashMap, Uuid}; use rmf_site_format::{FilteredIssueKinds, FilteredIssues, IssueKey}; -use std::collections::HashSet; #[derive(Component, Debug, Clone)] pub struct Issue { @@ -60,45 +56,6 @@ pub struct IssueDictionary(HashMap); #[derive(Default)] pub struct IssuePlugin; -#[derive(Resource)] -pub struct IssueMenu { - diagnostic_tool: Entity, -} - -impl FromWorld for IssueMenu { - fn from_world(world: &mut World) -> Self { - let target_states = HashSet::from([ - AppState::SiteEditor, - AppState::SiteDrawingEditor, - AppState::SiteVisualizer, - ]); - // Tools menu - let diagnostic_tool = world - .spawn(MenuItem::Text("Diagnostic Tool".to_string())) - .insert(MenuVisualizationStates(target_states)) - .id(); - - let tool_header = world.resource::().get(); - world - .entity_mut(tool_header) - .push_children(&[diagnostic_tool]); - - IssueMenu { diagnostic_tool } - } -} - -fn handle_diagnostic_window_visibility( - mut menu_events: EventReader, - issue_menu: Res, - mut diagnostic_window: ResMut, -) { - for event in menu_events.read() { - if event.clicked() && event.source() == issue_menu.diagnostic_tool { - diagnostic_window.show = true; - } - } -} - impl Plugin for IssuePlugin { fn build(&self, app: &mut App) { app.add_event::() @@ -107,8 +64,7 @@ impl Plugin for IssuePlugin { ChangePlugin::::default(), )) .init_resource::() - .init_resource::() - .add_systems(Update, handle_diagnostic_window_visibility); + .add_systems(PostUpdate, clear_old_issues_on_new_validate_event); } } diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index 5e97cd86..5537b60a 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -9,6 +9,8 @@ use wasm_bindgen::prelude::*; pub mod aabb; pub mod animate; +pub mod autoload; +pub use autoload::*; pub mod asset_loaders; use asset_loaders::*; @@ -31,7 +33,6 @@ mod shapes; use log::LogHistoryPlugin; pub mod main_menu; -use main_menu::Autoload; pub mod site; // mod warehouse_generator; pub mod workcell; @@ -56,7 +57,7 @@ use wireframe::*; use aabb::AabbUpdatePlugin; use animate::AnimationPlugin; use interaction::InteractionPlugin; -use site::{OSMViewPlugin, SiteFileMenuPlugin, SitePlugin}; +use site::{OSMViewPlugin, SitePlugin}; use site_asset_io::SiteAssetIoPlugin; pub mod osm_slippy_map; @@ -134,14 +135,25 @@ pub fn run(command_line_args: Vec) { headless_export = command_line_args.headless_export; } - app.add_plugins(SiteEditor { headless_export }); + app.add_plugins(SiteEditor::default().headless_export(headless_export)); app.run(); } #[derive(Default)] pub struct SiteEditor { /// Contains Some(path) if the site editor is running in headless mode exporting its site. - pub headless_export: Option, + headless_export: Option, +} + +impl SiteEditor { + pub fn new() -> Self { + Self::default() + } + + pub fn headless_export(mut self, export_to_file: Option) -> Self { + self.headless_export = export_to_file; + self + } } impl Plugin for SiteEditor { @@ -212,28 +224,24 @@ impl Plugin for SiteEditor { AabbUpdatePlugin, EguiPlugin, KeyboardInputPlugin, - MainMenuPlugin, - WorkcellEditorPlugin, SitePlugin, - InteractionPlugin { - headless: self.headless_export.is_some(), - }, - StandardUiLayout { - headless: self.headless_export.is_some(), - }, + InteractionPlugin::new().headless(self.headless_export.is_some()), AnimationPlugin, OccupancyPlugin, WorkspacePlugin, - )) - // Note order matters, plugins that edit the menus must be initialized after the UI - .add_plugins(( - ViewMenuPlugin, IssuePlugin, - OSMViewPlugin, - SiteWireframePlugin, - SiteFileMenuPlugin, )); + if self.headless_export.is_none() { + app.add_plugins(( + StandardUiPlugin::default(), + MainMenuPlugin, + WorkcellEditorPlugin, + )) + // Note order matters, plugins that edit the menus must be initialized after the UI + .add_plugins((ViewMenuPlugin, OSMViewPlugin, SiteWireframePlugin)); + } + // Ref https://github.com/bevyengine/bevy/issues/10877. The default behavior causes issues // with events being accumulated when not read (i.e. scrolling mouse wheel on a UI widget). app.world diff --git a/rmf_site_editor/src/main_menu.rs b/rmf_site_editor/src/main_menu.rs index bf2417fc..d6822509 100644 --- a/rmf_site_editor/src/main_menu.rs +++ b/rmf_site_editor/src/main_menu.rs @@ -16,27 +16,9 @@ */ use super::demo_world::*; -use crate::{AppState, LoadWorkspace, WorkspaceData}; -use bevy::{app::AppExit, prelude::*, tasks::Task, window::PrimaryWindow}; +use crate::{AppState, Autoload, LoadWorkspace, WorkspaceData}; +use bevy::{app::AppExit, prelude::*, window::PrimaryWindow}; use bevy_egui::{egui, EguiContexts}; -use std::path::PathBuf; - -#[derive(Resource)] -pub struct Autoload { - pub filename: Option, - pub import: Option, - pub importing: Option>>, -} - -impl Autoload { - pub fn file(filename: PathBuf, import: Option) -> Self { - Autoload { - filename: Some(filename), - import, - importing: None, - } - } -} fn egui_ui( mut egui_context: EguiContexts, @@ -49,10 +31,9 @@ fn egui_ui( if let Some(mut autoload) = autoload { #[cfg(not(target_arch = "wasm32"))] { - if let Some(filename) = autoload.filename.clone() { + if let Some(filename) = autoload.filename.take() { _load_workspace.send(LoadWorkspace::Path(filename)); } - autoload.filename = None; } return; } diff --git a/rmf_site_editor/src/site/fuel_cache.rs b/rmf_site_editor/src/site/fuel_cache.rs index 51aeea9d..a4185b00 100644 --- a/rmf_site_editor/src/site/fuel_cache.rs +++ b/rmf_site_editor/src/site/fuel_cache.rs @@ -40,12 +40,26 @@ pub struct SetFuelApiKey(pub String); /// Using channels instead of events to allow usage in wasm since, unlike event writers, they can /// be cloned and moved into async functions therefore don't have lifetime issues #[derive(Debug, Resource)] -pub struct UpdateFuelCacheChannels { +pub struct FuelCacheUpdateChannel { pub sender: Sender, pub receiver: Receiver, } -impl Default for UpdateFuelCacheChannels { +impl Default for FuelCacheUpdateChannel { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded(); + Self { sender, receiver } + } +} + +/// Channels that give incremental updates about what models have been fetched. +#[derive(Debug, Resource)] +pub struct FuelCacheProgressChannel { + pub sender: Sender, + pub receiver: Receiver, +} + +impl Default for FuelCacheProgressChannel { fn default() -> Self { let (sender, receiver) = crossbeam_channel::unbounded(); Self { sender, receiver } @@ -54,15 +68,19 @@ impl Default for UpdateFuelCacheChannels { pub fn handle_update_fuel_cache_requests( mut events: EventReader, - mut gallery_status: ResMut, + gallery_status: Option>, fuel_client: Res, - channels: Res, + update_channel: Res, + progress_channel: Res, ) { if events.read().last().is_some() { info!("Updating fuel cache, this might take a few minutes"); - gallery_status.fetching_cache = true; + if let Some(mut gallery_status) = gallery_status { + gallery_status.fetching_cache = true; + } let mut fuel_client = fuel_client.clone(); - let sender = channels.sender.clone(); + let sender = update_channel.sender.clone(); + let progress = progress_channel.sender.clone(); IoTaskPool::get() .spawn(async move { // Only write to cache in non wasm, no file system in web @@ -71,26 +89,38 @@ pub fn handle_update_fuel_cache_requests( #[cfg(not(target_arch = "wasm32"))] let write_to_disk = true; // Send client if update was successful - let res = fuel_client.update_cache(write_to_disk).await; + + let res = fuel_client + .update_cache_with_progress(write_to_disk, Some(progress)) + .await; sender .send(FuelCacheUpdated(res)) .expect("Failed sending fuel cache update event"); }) .detach(); } + + while let Ok(next_model) = progress_channel.receiver.try_recv() { + info!( + "Detected model {} owned by {}", + next_model.name, next_model.owner, + ); + } } pub fn read_update_fuel_cache_results( - channels: Res, + channels: Res, mut fuel_client: ResMut, - mut gallery_status: ResMut, + gallery_status: Option>, ) { if let Ok(result) = channels.receiver.try_recv() { match result.0 { Some(models) => fuel_client.models = Some(models), None => error!("Failed updating fuel cache"), } - gallery_status.fetching_cache = false; + if let Some(mut gallery_status) = gallery_status { + gallery_status.fetching_cache = false; + } } } diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index 904efcb9..3dd7de9c 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -42,9 +42,6 @@ pub use drawing::*; pub mod fiducial; pub use fiducial::*; -pub mod file_menu; -pub use file_menu::*; - pub mod floor; pub use floor::*; @@ -192,7 +189,8 @@ impl Plugin for SitePlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() + .init_resource::() .init_resource::() .register_type::() .register_type::() @@ -339,7 +337,6 @@ impl Plugin for SitePlugin { update_walls, update_transforms_for_changed_poses, align_site_drawings, - clear_old_issues_on_new_validate_event, export_lights, set_camera_transform_for_changed_site, ) diff --git a/rmf_site_editor/src/site/sdf_exporter.rs b/rmf_site_editor/src/site/sdf_exporter.rs index d3c14e74..acf43b31 100644 --- a/rmf_site_editor/src/site/sdf_exporter.rs +++ b/rmf_site_editor/src/site/sdf_exporter.rs @@ -6,9 +6,12 @@ use std::path::Path; use crate::SaveWorkspace; -use crate::site::{ - ChildLiftCabinGroup, CollisionMeshMarker, DoorSegments, DrawingMarker, FloorSegments, - LiftDoormat, ModelSceneRoot, TentativeModelFormat, VisualMeshMarker, +use crate::{ + site::{ + ChildLiftCabinGroup, CollisionMeshMarker, DoorSegments, DrawingMarker, FloorSegments, + LiftDoormat, ModelSceneRoot, TentativeModelFormat, VisualMeshMarker, + }, + Autoload, LoadWorkspace, }; use rmf_site_format::{ IsStatic, LevelElevation, LiftCabin, ModelMarker, NameInSite, NameOfSite, SiteID, WallMarker, @@ -47,14 +50,24 @@ pub fn headless_sdf_export( mut export_state: ResMut, sites: Query<(Entity, &NameOfSite)>, drawings: Query>, + autoload: Option>, + mut load_workspace: EventWriter, ) { + if let Some(mut autoload) = autoload { + if let Some(filename) = autoload.filename.take() { + load_workspace.send(LoadWorkspace::Path(filename)); + } + } else { + error!("Cannot perform a headless export since no site file was specified for loading"); + } + export_state.iterations += 1; if export_state.iterations < 5 { return; } if sites.is_empty() { warn!( - "Unable to load site from file [{}] so we cannot export an SDF from it", + "No site is loaded so we cannot export an SDF file into [{}]", export_state.target_path, ); exit.send(bevy::app::AppExit); diff --git a/rmf_site_editor/src/site/util.rs b/rmf_site_editor/src/site/util.rs index 84d499c5..2c1a445e 100644 --- a/rmf_site_editor/src/site/util.rs +++ b/rmf_site_editor/src/site/util.rs @@ -72,6 +72,13 @@ impl EdgeLabels { Self::LeftRight => "right", } } + + pub fn side(&self, side: Side) -> &'static str { + match side { + Side::Left => self.start(), + Side::Right => self.end(), + } + } } #[derive(Component, Debug, Default, Clone, Deref, DerefMut)] diff --git a/rmf_site_editor/src/widgets/building_preview.rs b/rmf_site_editor/src/widgets/building_preview.rs new file mode 100644 index 00000000..9aad1b02 --- /dev/null +++ b/rmf_site_editor/src/widgets/building_preview.rs @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + site::{AlignSiteDrawings, FinishEditDrawing}, + widgets::prelude::*, + AppState, CurrentWorkspace, Icons, +}; +use bevy::prelude::*; +use bevy_egui::egui::{Button, Ui}; + +#[derive(Default)] +pub struct BuildingPreviewPlugin {} + +impl Plugin for BuildingPreviewPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(PropertiesTilePlugin::::new()); + } +} + +#[derive(SystemParam)] +pub struct BuildingPreview<'w> { + app_state: Res<'w, State>, + next_app_state: ResMut<'w, NextState>, + icons: Res<'w, Icons>, + current_workspace: Res<'w, CurrentWorkspace>, + align_site: EventWriter<'w, AlignSiteDrawings>, + finish_edit_drawing: EventWriter<'w, FinishEditDrawing>, +} + +impl<'w> WidgetSystem for BuildingPreview<'w> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state == AppState::SiteEditor { + if ui.add(Button::new("Building preview")).clicked() { + params.next_app_state.set(AppState::SiteVisualizer); + } + } + + if *params.app_state == AppState::SiteVisualizer { + if ui + .add(Button::image_and_text( + params.icons.alignment.egui(), + "Align Drawings", + )) + .on_hover_text( + "Align all drawings in the site based on their fiducials and measurements", + ) + .clicked() + { + if let Some(site) = params.current_workspace.root { + params.align_site.send(AlignSiteDrawings(site)); + } + } + + if ui + .add(Button::image_and_text( + params.icons.exit.egui(), + "Return to site editor", + )) + .clicked() + { + params.next_app_state.set(AppState::SiteEditor); + } + } + + if *params.app_state == AppState::SiteDrawingEditor { + if ui + .add(Button::image_and_text( + params.icons.exit.egui(), + "Return to site editor", + )) + .clicked() + { + params.finish_edit_drawing.send(FinishEditDrawing(None)); + } + } + } +} diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index 52a17b1e..70baf797 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -15,110 +15,99 @@ * */ -use crate::{log::*, widgets::AppEvents}; -use bevy_egui::egui::{self, CollapsingHeader, Color32, RichText, Ui}; +use crate::{log::*, widgets::prelude::*}; +use bevy::prelude::*; +use bevy_egui::egui::{self, CollapsingHeader, Color32, RichText}; -pub struct ConsoleWidget<'a, 'w2, 's2> { - events: &'a mut AppEvents<'w2, 's2>, -} +/// This widget provides a console that displays information, warning, and error +/// messages. +#[derive(Default)] +pub struct ConsoleWidgetPlugin {} -impl<'a, 'w2, 's2> ConsoleWidget<'a, 'w2, 's2> { - pub fn new(events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { events } +impl Plugin for ConsoleWidgetPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + let widget = PanelWidget::new(console_widget, &mut app.world); + app.world.spawn(widget); } +} - pub fn show(self, ui: &mut Ui) { - ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 0.5; - let status = self.events.display.log_history.top(); - match status { - Some(log) => print_log(ui, log), - None => (), - } - }); - ui.add_space(5.0); - CollapsingHeader::new("RMF Site Editor Console") - .default_open(false) - .show(ui, |ui| { - ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 10.0; - // Filter logs by category - let mut all_are_checked = self - .events - .display - .log_history - .all_categories_are_selected(); - let all_were_checked = all_are_checked; - ui.checkbox(&mut all_are_checked, "All"); - ui.checkbox( - self.events - .display - .log_history - .category_present_mut(LogCategory::Status), - "Status", - ); - ui.checkbox( - self.events - .display - .log_history - .category_present_mut(LogCategory::Warning), - "Warning", - ); - ui.checkbox( - self.events - .display - .log_history - .category_present_mut(LogCategory::Error), - "Error", - ); - ui.checkbox( - self.events - .display - .log_history - .category_present_mut(LogCategory::Bevy), - "Bevy", - ); - // Copy full log history to clipboard - if ui.button("Copy Log History").clicked() { - ui.output_mut(|o| { - o.copied_text = self.events.display.log_history.copy_log_history() - }); - } - // Slider to adjust display limit - // TODO(@mxgrey): Consider allowing this range to - // automatically grow/shrink when the selected value - // approaches or leaves the upper limit. - ui.add(egui::Slider::new( - self.events.display.log_history.display_limit_mut(), - 10..=1000, - )); - - if !all_were_checked && all_are_checked { - // The user has asked to select all categories - self.events.display.log_history.select_all_categories(); - } - }); - ui.add_space(10.); - - egui::ScrollArea::both() - .auto_shrink([false, false]) - .stick_to_bottom(true) - .show(ui, |ui| { - let mut count = 0; - for element in self.events.display.log_history.iter() { - print_log(ui, element); - count += 1; +fn console_widget(In(input): In, mut log_history: ResMut) { + egui::TopBottomPanel::bottom("log_consolse") + .resizable(true) + .min_height(30.0) + .max_height(300.0) + .show(&input.context, |ui| { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.5; + let status = log_history.top(); + if let Some(log) = status { + print_log(ui, log); + } + }); + ui.add_space(5.0); + CollapsingHeader::new("Log Console") + .default_open(false) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 10.0; + // Filter logs by category + let mut all_are_checked = log_history.all_categories_are_selected(); + let all_were_checked = all_are_checked; + ui.checkbox(&mut all_are_checked, "All"); + ui.checkbox( + log_history.category_present_mut(LogCategory::Status), + "Status", + ); + ui.checkbox( + log_history.category_present_mut(LogCategory::Warning), + "Warning", + ); + ui.checkbox( + log_history.category_present_mut(LogCategory::Error), + "Error", + ); + ui.checkbox(log_history.category_present_mut(LogCategory::Bevy), "Bevy"); + // Copy full log history to clipboard + if ui.button("Copy Log History").clicked() { + ui.output_mut(|o| { + o.copied_text = log_history.copy_log_history(); + }); } - if count >= self.events.display.log_history.display_limit() { - ui.add_space(5.0); - if ui.button("See more").clicked() { - *self.events.display.log_history.display_limit_mut() += 100; - } + // Slider to adjust display limit + // TODO(@mxgrey): Consider allowing this range to + // automatically grow/shrink when the selected value + // approaches or leaves the upper limit. + ui.add(egui::Slider::new( + log_history.display_limit_mut(), + 10..=1000, + )); + + if !all_were_checked && all_are_checked { + log_history.select_all_categories(); } }); - ui.add_space(10.); - }); - } + ui.add_space(10.0); + + egui::ScrollArea::both() + .auto_shrink([false, false]) + .stick_to_bottom(true) + .show(ui, |ui| { + let mut count = 0; + for element in log_history.iter() { + print_log(ui, element); + count += 1; + } + if count >= log_history.display_limit() { + ui.add_space(5.0); + if ui.button("See more").clicked() { + *log_history.display_limit_mut() += 100; + } + } + }); + ui.add_space(10.0); + }); + }); } fn print_log(ui: &mut egui::Ui, element: &LogHistoryElement) { diff --git a/rmf_site_editor/src/widgets/create.rs b/rmf_site_editor/src/widgets/create.rs deleted file mode 100644 index 4ab0d59e..00000000 --- a/rmf_site_editor/src/widgets/create.rs +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (C) 2022 Open Source Robotics Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -*/ - -use crate::{ - inspector::{InspectAssetSource, InspectScale}, - interaction::{ChangeMode, SelectAnchor, SelectAnchor3D}, - site::{DefaultFile, DrawingBundle, Recall}, - AppEvents, AppState, -}; -use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{CollapsingHeader, Ui}; - -use rmf_site_format::{DrawingProperties, Geometry, Model, WorkcellModel}; - -#[derive(SystemParam)] -pub struct CreateParams<'w, 's> { - pub default_file: Query<'w, 's, &'static DefaultFile>, -} - -pub struct CreateWidget<'a, 'w1, 'w2, 's1, 's2> { - pub params: &'a CreateParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 'w2, 's1, 's2> CreateWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new(params: &'a CreateParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } - } - - pub fn show(self, ui: &mut Ui) { - ui.vertical(|ui| { - match self.events.app_state.get() { - AppState::MainMenu | AppState::SiteVisualizer => { - return; - } - AppState::SiteEditor => { - if ui.button("Lane").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_edge_sequence().for_lane().into(), - )); - } - - if ui.button("Location").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_point().for_location().into(), - )); - } - - if ui.button("Wall").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_edge_sequence().for_wall().into(), - )); - } - - if ui.button("Door").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_one_new_edge().for_door().into(), - )); - } - - if ui.button("Lift").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_one_new_edge().for_lift().into(), - )); - } - - if ui.button("Floor").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_path().for_floor().into(), - )); - } - if ui.button("Fiducial").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_point().for_site_fiducial().into(), - )); - } - - ui.add_space(10.0); - CollapsingHeader::new("New drawing") - .default_open(false) - .show(ui, |ui| { - let default_file = self - .events - .request - .current_workspace - .root - .map(|e| self.params.default_file.get(e).ok()) - .flatten(); - if let Some(new_asset_source) = InspectAssetSource::new( - &self.events.display.pending_drawings.source, - &self.events.display.pending_drawings.recall_source, - default_file, - ) - .show(ui) - { - self.events - .display - .pending_drawings - .recall_source - .remember(&new_asset_source); - self.events.display.pending_drawings.source = new_asset_source; - } - ui.add_space(5.0); - if ui.button("Add Drawing").clicked() { - self.events - .commands - .spawn(DrawingBundle::new(DrawingProperties { - source: self.events.display.pending_drawings.source.clone(), - ..default() - })); - } - }); - } - AppState::SiteDrawingEditor => { - if ui.button("Fiducial").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_new_point() - .for_drawing_fiducial() - .into(), - )); - } - if ui.button("Measurement").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor::create_one_new_edge().for_measurement().into(), - )); - } - } - AppState::WorkcellEditor => { - if ui.button("Frame").clicked() { - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor3D::create_new_point().for_anchor(None).into(), - )); - } - } - } - match self.events.app_state.get() { - AppState::MainMenu | AppState::SiteDrawingEditor | AppState::SiteVisualizer => {} - AppState::SiteEditor | AppState::WorkcellEditor => { - ui.add_space(10.0); - CollapsingHeader::new("New model") - .default_open(false) - .show(ui, |ui| { - let default_file = self - .events - .request - .current_workspace - .root - .map(|e| self.params.default_file.get(e).ok()) - .flatten(); - if let Some(new_asset_source) = InspectAssetSource::new( - &self.events.display.pending_model.source, - &self.events.display.pending_model.recall_source, - default_file, - ) - .show(ui) - { - self.events - .display - .pending_model - .recall_source - .remember(&new_asset_source); - self.events.display.pending_model.source = new_asset_source; - } - ui.add_space(5.0); - if let Some(new_scale) = - InspectScale::new(&self.events.display.pending_model.scale).show(ui) - { - self.events.display.pending_model.scale = new_scale; - } - ui.add_space(5.0); - match self.events.app_state.get() { - AppState::MainMenu - | AppState::SiteDrawingEditor - | AppState::SiteVisualizer => {} - AppState::SiteEditor => { - if ui.button("Browse fuel").clicked() { - self.events.new_model.asset_gallery_status.show = true; - } - if ui.button("Spawn model").clicked() { - let model = Model { - source: self - .events - .display - .pending_model - .source - .clone(), - scale: self.events.display.pending_model.scale, - ..default() - }; - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor3D::create_new_point() - .for_model(model) - .into(), - )); - } - } - AppState::WorkcellEditor => { - if ui.button("Browse fuel").clicked() { - self.events.new_model.asset_gallery_status.show = true; - } - if ui.button("Spawn visual").clicked() { - let workcell_model = WorkcellModel { - geometry: Geometry::Mesh { - source: self - .events - .display - .pending_model - .source - .clone(), - scale: Some( - *self.events.display.pending_model.scale, - ), - }, - ..default() - }; - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor3D::create_new_point() - .for_visual(workcell_model) - .into(), - )); - } - if ui.button("Spawn collision").clicked() { - let workcell_model = WorkcellModel { - geometry: Geometry::Mesh { - source: self - .events - .display - .pending_model - .source - .clone(), - scale: Some( - *self.events.display.pending_model.scale, - ), - }, - ..default() - }; - self.events.request.change_mode.send(ChangeMode::To( - SelectAnchor3D::create_new_point() - .for_collision(workcell_model) - .into(), - )); - } - ui.add_space(10.0); - } - } - }); - } - } - }); - } -} diff --git a/rmf_site_editor/src/widgets/creation.rs b/rmf_site_editor/src/widgets/creation.rs new file mode 100644 index 00000000..9166a137 --- /dev/null +++ b/rmf_site_editor/src/widgets/creation.rs @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + inspector::{InspectAssetSourceComponent, InspectScaleComponent}, + interaction::{ChangeMode, SelectAnchor, SelectAnchor3D}, + site::{AssetSource, DefaultFile, DrawingBundle, Recall, RecallAssetSource, Scale}, + widgets::{prelude::*, AssetGalleryStatus}, + AppState, CurrentWorkspace, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{CollapsingHeader, Ui}; + +use rmf_site_format::{DrawingProperties, Geometry, Model, WorkcellModel}; + +/// This widget provides a widget with buttons for creating new site elements. +#[derive(Default)] +pub struct CreationPlugin {} + +impl Plugin for CreationPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); + } +} + +#[derive(SystemParam)] +struct Creation<'w, 's> { + default_file: Query<'w, 's, &'static DefaultFile>, + app_state: Res<'w, State>, + change_mode: EventWriter<'w, ChangeMode>, + current_workspace: Res<'w, CurrentWorkspace>, + pending_drawings: ResMut<'w, PendingDrawing>, + pending_model: ResMut<'w, PendingModel>, + asset_gallery: Option>, + commands: Commands<'w, 's>, +} + +impl<'w, 's> WidgetSystem for Creation<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { + let mut params = state.get_mut(world); + match params.app_state.get() { + AppState::SiteEditor | AppState::SiteDrawingEditor | AppState::WorkcellEditor => {} + AppState::MainMenu | AppState::SiteVisualizer => return, + } + CollapsingHeader::new("Create") + .default_open(true) + .show(ui, |ui| { + params.show_widget(ui); + }); + } +} + +impl<'w, 's> Creation<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + ui.vertical(|ui| { + match self.app_state.get() { + AppState::MainMenu | AppState::SiteVisualizer => { + return; + } + AppState::SiteEditor => { + if ui.button("Lane").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_edge_sequence().for_lane().into(), + )); + } + + if ui.button("Location").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_point().for_location().into(), + )); + } + + if ui.button("Wall").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_edge_sequence().for_wall().into(), + )); + } + + if ui.button("Door").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_one_new_edge().for_door().into(), + )); + } + + if ui.button("Lift").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_one_new_edge().for_lift().into(), + )); + } + + if ui.button("Floor").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_path().for_floor().into(), + )); + } + if ui.button("Fiducial").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_point().for_site_fiducial().into(), + )); + } + + ui.add_space(10.0); + CollapsingHeader::new("New drawing") + .default_open(false) + .show(ui, |ui| { + let default_file = self + .current_workspace + .root + .map(|e| self.default_file.get(e).ok()) + .flatten(); + if let Some(new_asset_source) = InspectAssetSourceComponent::new( + &self.pending_drawings.source, + &self.pending_drawings.recall_source, + default_file, + ) + .show(ui) + { + self.pending_drawings + .recall_source + .remember(&new_asset_source); + self.pending_drawings.source = new_asset_source; + } + ui.add_space(5.0); + if ui.button("Add Drawing").clicked() { + self.commands.spawn(DrawingBundle::new(DrawingProperties { + source: self.pending_drawings.source.clone(), + ..default() + })); + } + }); + } + AppState::SiteDrawingEditor => { + if ui.button("Fiducial").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_new_point() + .for_drawing_fiducial() + .into(), + )); + } + if ui.button("Measurement").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor::create_one_new_edge().for_measurement().into(), + )); + } + } + AppState::WorkcellEditor => { + if ui.button("Frame").clicked() { + self.change_mode.send(ChangeMode::To( + SelectAnchor3D::create_new_point().for_anchor(None).into(), + )); + } + } + } + match self.app_state.get() { + AppState::MainMenu | AppState::SiteDrawingEditor | AppState::SiteVisualizer => {} + AppState::SiteEditor | AppState::WorkcellEditor => { + ui.add_space(10.0); + CollapsingHeader::new("New model") + .default_open(false) + .show(ui, |ui| { + let default_file = self + .current_workspace + .root + .map(|e| self.default_file.get(e).ok()) + .flatten(); + if let Some(new_asset_source) = InspectAssetSourceComponent::new( + &self.pending_model.source, + &self.pending_model.recall_source, + default_file, + ) + .show(ui) + { + self.pending_model.recall_source.remember(&new_asset_source); + self.pending_model.source = new_asset_source; + } + ui.add_space(5.0); + if let Some(new_scale) = + InspectScaleComponent::new(&self.pending_model.scale).show(ui) + { + self.pending_model.scale = new_scale; + } + ui.add_space(5.0); + if let Some(asset_gallery) = &mut self.asset_gallery { + match self.app_state.get() { + AppState::MainMenu + | AppState::SiteDrawingEditor + | AppState::SiteVisualizer => {} + AppState::SiteEditor => { + if ui.button("Browse fuel").clicked() { + asset_gallery.show = true; + } + if ui.button("Spawn model").clicked() { + let model = Model { + source: self.pending_model.source.clone(), + scale: self.pending_model.scale, + ..default() + }; + self.change_mode.send(ChangeMode::To( + SelectAnchor3D::create_new_point() + .for_model(model) + .into(), + )); + } + } + AppState::WorkcellEditor => { + if ui.button("Browse fuel").clicked() { + asset_gallery.show = true; + } + if ui.button("Spawn visual").clicked() { + let workcell_model = WorkcellModel { + geometry: Geometry::Mesh { + source: self.pending_model.source.clone(), + scale: Some(*self.pending_model.scale), + }, + ..default() + }; + self.change_mode.send(ChangeMode::To( + SelectAnchor3D::create_new_point() + .for_visual(workcell_model) + .into(), + )); + } + if ui.button("Spawn collision").clicked() { + let workcell_model = WorkcellModel { + geometry: Geometry::Mesh { + source: self.pending_model.source.clone(), + scale: Some(*self.pending_model.scale), + }, + ..default() + }; + self.change_mode.send(ChangeMode::To( + SelectAnchor3D::create_new_point() + .for_collision(workcell_model) + .into(), + )); + } + ui.add_space(10.0); + } + } + } + }); + } + } + }); + } +} + +#[derive(Resource, Clone, Default)] +struct PendingDrawing { + pub source: AssetSource, + pub recall_source: RecallAssetSource, +} + +#[derive(Resource, Clone, Default)] +struct PendingModel { + pub source: AssetSource, + pub recall_source: RecallAssetSource, + pub scale: Scale, +} diff --git a/rmf_site_editor/src/widgets/diagnostic_window.rs b/rmf_site_editor/src/widgets/diagnostics.rs similarity index 53% rename from rmf_site_editor/src/widgets/diagnostic_window.rs rename to rmf_site_editor/src/widgets/diagnostics.rs index 3a21c21b..41a25028 100644 --- a/rmf_site_editor/src/widgets/diagnostic_window.rs +++ b/rmf_site_editor/src/widgets/diagnostics.rs @@ -15,56 +15,89 @@ * */ -use crate::inspector::SelectionWidget; -use crate::site::{Change, FilteredIssueKinds, FilteredIssues, IssueKey, SiteID}; -use crate::{AppEvents, Icons, Issue}; -use crate::{IssueDictionary, ValidateWorkspace}; -use bevy::ecs::system::SystemParam; -use bevy::prelude::*; -use bevy_egui::egui::{Button, Checkbox, Grid, ImageButton, ScrollArea, Ui}; +use crate::{ + site::{Change, FilteredIssueKinds, FilteredIssues, IssueKey}, + widgets::{ + menu_bar::{MenuEvent, MenuItem, MenuVisualizationStates, ToolMenu}, + prelude::*, + SelectorWidget, + }, + AppState, CurrentWorkspace, Icons, Issue, IssueDictionary, ValidateWorkspace, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{self, Button, Checkbox, Grid, ImageButton, ScrollArea, Ui}; +use std::collections::HashSet; -#[derive(Resource, Debug, Clone, Default)] -pub struct DiagnosticWindowState { - pub show: bool, - pub selected: Option>, +/// Add a [`Diagnostics`] widget to your application. +#[derive(Default)] +pub struct DiagnosticsPlugin {} + +impl Plugin for DiagnosticsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .init_resource::() + .add_systems(Update, handle_diagnostic_panel_visibility); + + let panel = PanelWidget::new(diagnostics_panel, &mut app.world); + let widget = Widget::new::(&mut app.world); + app.world.spawn((panel, widget)); + } } -#[derive(SystemParam)] -pub struct DiagnosticParams<'w, 's> { - pub icons: Res<'w, Icons>, - pub site_id: Query<'w, 's, &'static SiteID>, - pub filters: Query<'w, 's, (&'static FilteredIssues, &'static FilteredIssueKinds)>, - pub issue_dictionary: Res<'w, IssueDictionary>, - pub issues: Query<'w, 's, (&'static Issue, &'static Parent)>, +fn diagnostics_panel(In(input): In, world: &mut World) { + if world.resource::().show { + egui::SidePanel::left("diagnostics") + .resizable(true) + .min_width(320.0) + .show(&input.context, |ui| { + if let Err(err) = world.try_show(input.id, ui) { + error!("Unable to display diagnostics panel: {err:?}"); + } + }); + } } -pub struct DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { - events: &'a mut AppEvents<'w1, 's1>, - params: &'a DiagnosticParams<'w2, 's2>, +/// A widget that displays diagnostic information about the current site. This +/// helps identify potential problems that you should consider addressing. +/// +/// Use [`DiagnosticsPlugin`] to add this to your application. +#[derive(SystemParam)] +pub struct Diagnostics<'w, 's> { + icons: Res<'w, Icons>, + filters: Query<'w, 's, (&'static FilteredIssues, &'static FilteredIssueKinds)>, + issue_dictionary: Res<'w, IssueDictionary>, + issues: Query<'w, 's, (&'static Issue, &'static Parent)>, + display_diagnostics: ResMut<'w, DiagnosticsDisplay>, + current_workspace: ResMut<'w, CurrentWorkspace>, + validate_workspace: EventWriter<'w, ValidateWorkspace>, + change_filtered_issues: EventWriter<'w, Change>>, + change_filtered_issue_kinds: EventWriter<'w, Change>, + selector: SelectorWidget<'w, 's>, } -impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { - pub fn new( - events: &'a mut AppEvents<'w1, 's1>, - params: &'a DiagnosticParams<'w2, 's2>, - ) -> Self { - Self { events, params } +impl<'w, 's> WidgetSystem for Diagnostics<'w, 's> { + fn show(_: (), ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { + let mut params = state.get_mut(world); + params.show_widget(ui); } +} - pub fn show(self, ui: &mut Ui) { +impl<'w, 's> Diagnostics<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { //let state = &mut self.events.file_events.diagnostic_window; - let mut state = (*self.events.file_events.diagnostic_window).clone(); - let Some(root) = self.events.request.current_workspace.root else { + let mut state = (*self.display_diagnostics).clone(); + let Some(root) = self.current_workspace.root else { return; }; - let Ok((filtered_issues, filtered_issue_kinds)) = self.params.filters.get(root) else { + let Ok((filtered_issues, filtered_issue_kinds)) = self.filters.get(root) else { return; }; let mut new_filtered_issues = filtered_issues.clone(); let mut new_filtered_issue_kinds = filtered_issue_kinds.clone(); ui.vertical(|ui| { ui.collapsing("Filters", |ui| { - for (uuid, name) in self.params.issue_dictionary.iter() { + for (uuid, name) in self.issue_dictionary.iter() { let mut show_category = !new_filtered_issue_kinds.contains(uuid); if ui.add(Checkbox::new(&mut show_category, name)).clicked() { match show_category { @@ -80,14 +113,13 @@ impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { for (idx, issue) in new_filtered_issues.iter().enumerate() { ui.horizontal(|ui| { let issue_type = self - .params .issue_dictionary .get(&issue.kind) .cloned() .unwrap_or("Unknown Type".to_owned()); ui.label(issue_type); if ui - .add(ImageButton::new(self.params.icons.trash.egui())) + .add(ImageButton::new(self.icons.trash.egui())) .on_hover_text("Remove this suppression") .clicked() { @@ -98,13 +130,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { ui, |ui| { for e in &issue.entities { - SelectionWidget::new( - *e, - self.params.site_id.get(*e).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(*e, ui); } }, ); @@ -122,10 +148,10 @@ impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { .auto_shrink([false, false]) .show(ui, |ui| { let mut issue_still_exists = false; - if self.params.issues.is_empty() { + if self.issues.is_empty() { ui.label("No issues found"); } - for (issue, parent) in &self.params.issues { + for (issue, parent) in &self.issues { if new_filtered_issue_kinds.contains(&issue.key.kind) || new_filtered_issues.contains(&issue.key) || **parent != root @@ -136,7 +162,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { issue_still_exists |= sel; ui.horizontal(|ui| { if ui - .add(ImageButton::new(self.params.icons.hide.egui())) + .add(ImageButton::new(self.icons.hide.egui())) .on_hover_text("Suppress this issue") .clicked() { @@ -164,39 +190,71 @@ impl<'a, 'w1, 's1, 'w2, 's2> DiagnosticWindow<'a, 'w1, 's1, 'w2, 's2> { ui.label("Affected entities"); Grid::new("diagnostic_affected_entities").show(ui, |ui| { for e in &sel.entities { - SelectionWidget::new( - *e, - self.params.site_id.get(*e).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(*e, ui); } }); } if ui.add(Button::new("Validate")).clicked() { - self.events - .request - .validate_workspace - .send(ValidateWorkspace(root)); + self.validate_workspace.send(ValidateWorkspace(root)); } if ui.add(Button::new("Close")).clicked() { state.show = false; } }); if new_filtered_issues != *filtered_issues { - self.events - .change - .filtered_issues + self.change_filtered_issues .send(Change::new(new_filtered_issues, root)); } if new_filtered_issue_kinds != *filtered_issue_kinds { - self.events - .change - .filtered_issue_kinds + self.change_filtered_issue_kinds .send(Change::new(new_filtered_issue_kinds, root)); } - *self.events.file_events.diagnostic_window = state; + *self.display_diagnostics = state; + } +} + +#[derive(Resource, Debug, Clone, Default)] +pub struct DiagnosticsDisplay { + pub show: bool, + pub selected: Option>, +} + +fn handle_diagnostic_panel_visibility( + mut menu_events: EventReader, + issue_menu: Res, + mut diagnostic_window: ResMut, +) { + for event in menu_events.read() { + if event.clicked() && event.source() == issue_menu.diagnostic_tool { + diagnostic_window.show = true; + } + } +} + +#[derive(Resource)] +pub struct IssueMenu { + diagnostic_tool: Entity, +} + +impl FromWorld for IssueMenu { + fn from_world(world: &mut World) -> Self { + let target_states = HashSet::from([ + AppState::SiteEditor, + AppState::SiteDrawingEditor, + AppState::SiteVisualizer, + ]); + // Tools menu + let diagnostic_tool = world + .spawn(MenuItem::Text("Diagnostic Tool".to_string())) + .insert(MenuVisualizationStates(target_states)) + .id(); + + let tool_header = world.resource::().get(); + world + .entity_mut(tool_header) + .push_children(&[diagnostic_tool]); + + IssueMenu { diagnostic_tool } } } diff --git a/rmf_site_editor/src/widgets/new_model.rs b/rmf_site_editor/src/widgets/fuel_asset_browser.rs similarity index 79% rename from rmf_site_editor/src/widgets/new_model.rs rename to rmf_site_editor/src/widgets/fuel_asset_browser.rs index 5550542d..0c7a2885 100644 --- a/rmf_site_editor/src/widgets/new_model.rs +++ b/rmf_site_editor/src/widgets/fuel_asset_browser.rs @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Open Source Robotics Foundation + * Copyright (C) 2024 Open Source Robotics Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,28 @@ * */ -use crate::interaction::{ChangeMode, ModelPreviewCamera, SelectAnchor3D}; -use crate::site::{AssetSource, FuelClient, Model, SetFuelApiKey, UpdateFuelCache}; -use crate::AppEvents; +use crate::{ + interaction::{ChangeMode, ModelPreviewCamera, SelectAnchor3D}, + site::{AssetSource, FuelClient, Model, SetFuelApiKey, UpdateFuelCache}, + widgets::prelude::*, +}; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{Button, ComboBox, ImageSource, RichText, ScrollArea, Ui, Window}; +use bevy_egui::egui::{self, Button, ComboBox, ImageSource, RichText, ScrollArea, Ui, Window}; use gz_fuel::FuelModel; +/// Add a [`FuelAssetBrowser`] widget to your application. +#[derive(Default)] +pub struct FuelAssetBrowserPlugin {} + +impl Plugin for FuelAssetBrowserPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + let panel = PanelWidget::new(fuel_asset_browser_panel, &mut app.world); + let widget = Widget::new::(&mut app.world); + app.world.spawn((panel, widget)); + } +} + /// Filters applied to models in the fuel list #[derive(Default)] pub struct ShowAssetFilters { @@ -33,7 +48,7 @@ pub struct ShowAssetFilters { pub recall_private: Option, } -/// Used to signals whether to show or hide the left side panel with the asset gallery +/// Used to indicate whether to show or hide the [`FuelAssetBrowser`]. #[derive(Resource, Default)] pub struct AssetGalleryStatus { pub show: bool, @@ -46,28 +61,49 @@ pub struct AssetGalleryStatus { pub show_api_window: bool, } +/// A widget for browsing models that can be downloaded from fuel. +/// +/// This is part of the [`StandardUiPlugin`][1]. If you are not using the +/// `StandardUiPlugin` then it is recommended that you use the +/// [`FuelAssetBrowserPlugin`] to add this to the editor. +/// +/// [1]: crate::widgets::StandardUiPlugin #[derive(SystemParam)] -pub struct NewModelParams<'w> { - pub fuel_client: ResMut<'w, FuelClient>, +pub struct FuelAssetBrowser<'w, 's> { + fuel_client: ResMut<'w, FuelClient>, // TODO(luca) refactor to see whether we need - pub asset_gallery_status: ResMut<'w, AssetGalleryStatus>, - pub model_preview_camera: Res<'w, ModelPreviewCamera>, - pub update_cache: EventWriter<'w, UpdateFuelCache>, - pub set_api_key: EventWriter<'w, SetFuelApiKey>, + asset_gallery_status: ResMut<'w, AssetGalleryStatus>, + model_preview_camera: Res<'w, ModelPreviewCamera>, + update_cache: EventWriter<'w, UpdateFuelCache>, + set_api_key: EventWriter<'w, SetFuelApiKey>, + commands: Commands<'w, 's>, + change_mode: EventWriter<'w, ChangeMode>, } -pub struct NewModel<'a, 'w, 's> { - events: &'a mut AppEvents<'w, 's>, +fn fuel_asset_browser_panel(In(input): In, world: &mut World) { + if world.resource::().show { + egui::SidePanel::left("asset_gallery") + .resizable(true) + .min_width(320.0) + .show(&input.context, |ui| { + if let Err(err) = world.try_show(input.id, ui) { + error!("Unable to display asset gallery: {err:?}"); + } + }); + } } -impl<'a, 'w, 's> NewModel<'a, 'w, 's> { - pub fn new(events: &'a mut AppEvents<'w, 's>) -> Self { - Self { events } +impl<'w, 's> WidgetSystem for FuelAssetBrowser<'w, 's> { + fn show(_: (), ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + params.show_widget(ui); } +} - pub fn show(self, ui: &mut Ui) { - let fuel_client = &mut self.events.new_model.fuel_client; - let gallery_status = &mut self.events.new_model.asset_gallery_status; +impl<'w, 's> FuelAssetBrowser<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let fuel_client = &mut self.fuel_client; + let gallery_status = &mut self.asset_gallery_status; ui.label(RichText::new("Asset Gallery").size(18.0)); ui.add_space(10.0); match &fuel_client.models { @@ -212,25 +248,21 @@ impl<'a, 'w, 's> NewModel<'a, 'w, 's> { ui.add_space(10.0); ui.image(ImageSource::Texture( - ( - self.events.new_model.model_preview_camera.egui_handle, - [320.0, 240.0].into(), - ) - .into(), + (self.model_preview_camera.egui_handle, [320.0, 240.0].into()).into(), )); ui.add_space(10.0); if gallery_status.selected.as_ref() != new_selected { if let Some(selected) = new_selected { // Set the model preview source to what is selected - let model_entity = self.events.new_model.model_preview_camera.model_entity; + let model_entity = self.model_preview_camera.model_entity; let model = Model { source: AssetSource::Remote( selected.owner.clone() + "/" + &selected.name + "/model.sdf", ), ..default() }; - self.events.commands.entity(model_entity).insert(model); + self.commands.entity(model_entity).insert(model); gallery_status.selected = Some(selected.clone()); } } @@ -243,7 +275,7 @@ impl<'a, 'w, 's> NewModel<'a, 'w, 's> { ), ..default() }; - self.events.request.change_mode.send(ChangeMode::To( + self.change_mode.send(ChangeMode::To( SelectAnchor3D::create_new_point().for_model(model).into(), )); } @@ -260,9 +292,7 @@ impl<'a, 'w, 's> NewModel<'a, 'w, 's> { ui.text_edit_singleline(&mut gallery_status.proposed_api_key); if ui.add(Button::new("Save")).clicked() { // Take it to avoid leaking the information in the dialog - self.events - .new_model - .set_api_key + self.set_api_key .send(SetFuelApiKey(gallery_status.proposed_api_key.clone())); fuel_client.token = Some(std::mem::take(&mut gallery_status.proposed_api_key)); gallery_status.show_api_window = false; @@ -279,7 +309,7 @@ impl<'a, 'w, 's> NewModel<'a, 'w, 's> { ui.label("Updating model cache..."); } else { if ui.add(Button::new("Update model cache")).clicked() { - self.events.new_model.update_cache.send(UpdateFuelCache); + self.update_cache.send(UpdateFuelCache); } } if ui.add(Button::new("Close")).clicked() { diff --git a/rmf_site_editor/src/widgets/icons.rs b/rmf_site_editor/src/widgets/icons.rs index f55ededb..3c488dae 100644 --- a/rmf_site_editor/src/widgets/icons.rs +++ b/rmf_site_editor/src/widgets/icons.rs @@ -16,9 +16,20 @@ */ use crate::{recency::RankAdjustment, site::LayerVisibility}; -use bevy::{ecs::system::SystemState, prelude::*}; +use bevy::{asset::embedded_asset, ecs::system::SystemState, prelude::*}; use bevy_egui::{egui::ImageSource, egui::TextureId, EguiContexts}; +/// Add a resource for the common icons of the application. +#[derive(Default)] +pub struct IconsPlugin {} + +impl Plugin for IconsPlugin { + fn build(&self, app: &mut App) { + add_widgets_icons(app); + app.init_resource::(); + } +} + struct IconBuilder(Handle); impl IconBuilder { pub fn new(name: &str, asset_server: &AssetServer) -> Self { @@ -48,7 +59,7 @@ impl Icon { } } -// TODO(MXG): Create a struct to manage bevy-egui image pairs +/// A collection of icons used by the standard widgets. #[derive(Clone, Debug, Resource)] pub struct Icons { pub select: Icon, @@ -156,3 +167,56 @@ impl Icons { } } } + +fn add_widgets_icons(app: &mut App) { + // Taken from https://github.com/bevyengine/bevy/issues/10377#issuecomment-1858797002 + // TODO(luca) remove once we migrate to Bevy 0.13 that includes the fix + #[cfg(any(not(target_family = "windows"), target_env = "gnu"))] + { + embedded_asset!(app, "src/", "icons/add.png"); + embedded_asset!(app, "src/", "icons/alignment.png"); + embedded_asset!(app, "src/", "icons/alpha.png"); + embedded_asset!(app, "src/", "icons/confirm.png"); + embedded_asset!(app, "src/", "icons/down.png"); + embedded_asset!(app, "src/", "icons/edit.png"); + embedded_asset!(app, "src/", "icons/empty.png"); + embedded_asset!(app, "src/", "icons/exit.png"); + embedded_asset!(app, "src/", "icons/global.png"); + embedded_asset!(app, "src/", "icons/hidden.png"); + embedded_asset!(app, "src/", "icons/hide.png"); + embedded_asset!(app, "src/", "icons/merge.png"); + embedded_asset!(app, "src/", "icons/opaque.png"); + embedded_asset!(app, "src/", "icons/reject.png"); + embedded_asset!(app, "src/", "icons/search.png"); + embedded_asset!(app, "src/", "icons/select.png"); + embedded_asset!(app, "src/", "icons/selected.png"); + embedded_asset!(app, "src/", "icons/to_bottom.png"); + embedded_asset!(app, "src/", "icons/to_top.png"); + embedded_asset!(app, "src/", "icons/trash.png"); + embedded_asset!(app, "src/", "icons/up.png"); + } + #[cfg(all(target_family = "windows", not(target_env = "gnu")))] + { + embedded_asset!(app, "src\\", "icons\\add.png"); + embedded_asset!(app, "src\\", "icons\\alignment.png"); + embedded_asset!(app, "src\\", "icons\\alpha.png"); + embedded_asset!(app, "src\\", "icons\\confirm.png"); + embedded_asset!(app, "src\\", "icons\\down.png"); + embedded_asset!(app, "src\\", "icons\\edit.png"); + embedded_asset!(app, "src\\", "icons\\empty.png"); + embedded_asset!(app, "src\\", "icons\\exit.png"); + embedded_asset!(app, "src\\", "icons\\global.png"); + embedded_asset!(app, "src\\", "icons\\hidden.png"); + embedded_asset!(app, "src\\", "icons\\hide.png"); + embedded_asset!(app, "src\\", "icons\\merge.png"); + embedded_asset!(app, "src\\", "icons\\opaque.png"); + embedded_asset!(app, "src\\", "icons\\reject.png"); + embedded_asset!(app, "src\\", "icons\\search.png"); + embedded_asset!(app, "src\\", "icons\\select.png"); + embedded_asset!(app, "src\\", "icons\\selected.png"); + embedded_asset!(app, "src\\", "icons\\to_bottom.png"); + embedded_asset!(app, "src\\", "icons\\to_top.png"); + embedded_asset!(app, "src\\", "icons\\trash.png"); + embedded_asset!(app, "src\\", "icons\\up.png"); + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_anchor.rs b/rmf_site_editor/src/widgets/inspector/inspect_anchor.rs index 27f5168e..1b472973 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_anchor.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_anchor.rs @@ -17,20 +17,21 @@ use crate::{ interaction::{Hover, MoveTo}, - site::{ - latlon_to_world, world_to_latlon, Anchor, AssociatedGraphs, Category, Change, Dependents, - GeographicComponent, JointProperties, LocationTags, MeshConstraint, SiteID, Subordinate, + site::{Anchor, Category, Change, Dependents, JointProperties, MeshConstraint, Subordinate}, + widgets::{ + inspector::{Inspect, InspectPoseComponent}, + prelude::*, + Icons, SelectorWidget, }, - widgets::{inspector::InspectPose, inspector::SelectionWidget, AppEvents, Icons}, workcell::CreateJoint, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{DragValue, ImageButton, Ui}; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet}; #[derive(SystemParam)] -pub struct InspectAnchorParams<'w, 's> { - pub anchors: Query< +pub struct InspectAnchor<'w, 's> { + anchors: Query< 'w, 's, ( @@ -41,269 +42,222 @@ pub struct InspectAnchorParams<'w, 's> { Option<&'static MeshConstraint>, ), >, - pub icons: Res<'w, Icons>, - pub site_id: Query<'w, 's, &'static SiteID>, - pub joints: Query<'w, 's, Entity, With>, - pub geographic_offset: Query<'w, 's, &'static GeographicComponent>, + icons: Res<'w, Icons>, + joints: Query<'w, 's, Entity, With>, + hover: EventWriter<'w, Hover>, + move_to: EventWriter<'w, MoveTo>, + mesh_constraints: EventWriter<'w, Change>>, + create_joint: EventWriter<'w, CreateJoint>, } -pub struct InspectAnchorWidget<'a, 'w1, 'w2, 's1, 's2> { - pub anchor: Entity, - pub params: &'a InspectAnchorParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, - pub is_dependency: bool, -} +impl<'w, 's> ShareableWidget for InspectAnchor<'w, 's> {} -impl<'a, 'w1, 'w2, 's1, 's2> InspectAnchorWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - anchor: Entity, - params: &'a InspectAnchorParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - anchor, - params, - events, - is_dependency: false, - } +impl<'w, 's> WidgetSystem for InspectAnchor<'w, 's> { + fn show( + Inspect { + selection: anchor, + panel, + .. + }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + impl_inspect_anchor( + InspectAnchorInput { + anchor, + is_dependency: false, + panel, + }, + ui, + state, + world, + ); } +} - pub fn as_dependency(self) -> Self { - Self { - is_dependency: true, - ..self - } +impl<'w, 's> WidgetSystem> + for InspectAnchor<'w, 's> +{ + fn show( + input: InspectAnchorInput, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) -> Option { + impl_inspect_anchor(input, ui, state, world) } +} - pub fn show(self, ui: &mut Ui) -> InspectAnchorResponse { - let mut replace = false; - if self.is_dependency { - SelectionWidget::new( - self.anchor, - self.params.site_id.get(self.anchor).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); - - let assign_response = ui.add(ImageButton::new(self.params.icons.edit.egui())); +pub struct InspectAnchorInput { + pub anchor: Entity, + pub is_dependency: bool, + pub panel: PanelSide, +} - if assign_response.hovered() { - self.events.request.hover.send(Hover(Some(self.anchor))); - } +fn impl_inspect_anchor( + InspectAnchorInput { + anchor: id, + is_dependency, + panel, + }: InspectAnchorInput, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, +) -> Option { + if world.get::(id).is_none() { + return None; + } - replace = assign_response.clicked(); - assign_response.on_hover_text("Reassign"); - } + let mut replace = false; + if is_dependency { + world.show::(id, ui); - if let Ok((anchor, tf, subordinate, parent, mesh_constraint)) = - self.params.anchors.get(self.anchor) - { - if let Some(subordinate) = subordinate { - ui.horizontal(|ui| { - if let Some(boss) = subordinate.0 { - ui.label("Subordinate to ").on_hover_text( - "The position of a subordinate anchor is \ - managed by the properties of another entity.", - ); - SelectionWidget::new( - boss, - self.params.site_id.get(boss).ok().copied(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); - } else { - ui.label("Anonymous subordinate"); - } - }); - } else { - match anchor { - Anchor::Translate2D(_anchor) => { - ui.vertical(|ui| { - ui.horizontal(|ui| { - if !self.is_dependency { - ui.label("x"); - } - let mut x = tf.translation.x; - ui.add(DragValue::new(&mut x).speed(0.01)); - // TODO(MXG): Make the drag speed a user-defined setting + let mut params = state.get_mut(world); + let edit_icon = params.icons.edit.egui(); + let assign_response = ui.add(ImageButton::new(edit_icon)); - if !self.is_dependency { - ui.label("y"); - } - let mut y = tf.translation.y; - ui.add(DragValue::new(&mut y).speed(0.01)); + if assign_response.hovered() { + params.hover.send(Hover(Some(id))); + } - if x != tf.translation.x || y != tf.translation.y { - self.events.request.move_to.send(MoveTo { - entity: self.anchor, - transform: Transform::from_translation([x, y, 0.0].into()), - }); - } - }); + replace = assign_response.clicked(); + assign_response.on_hover_text("Reassign"); + } - for comp in &self.params.geographic_offset { - let Some(offset) = comp.0 else { - continue; - }; - let Ok((mut lat, mut lon)) = - world_to_latlon(tf.translation, offset.anchor) - else { - continue; - }; + let mut params = state.get_mut(world); - let old_lat = lat.clone(); - let old_lon = lon.clone(); + if let Ok((anchor, tf, subordinate, parent, mesh_constraint)) = params.anchors.get(id) { + if let Some(subordinate) = subordinate.map(|s| s.0) { + panel.orthogonal(ui, |ui| { + if let Some(boss) = subordinate { + ui.label("Subordinate to ").on_hover_text( + "The position of a subordinate anchor is \ + managed by the properties of another entity.", + ); + world.show::(boss, ui); + } else { + ui.label("Anonymous subordinate"); + } + }); + } else { + match anchor { + Anchor::Translate2D(_) => { + if !is_dependency { + ui.label("x"); + } + let mut x = tf.translation.x; + ui.add(DragValue::new(&mut x).speed(0.01)); - if !self.is_dependency { - ui.label("Latitude"); - ui.add(DragValue::new(&mut lat).speed(1e-16)); - ui.label("Longitude"); - ui.add(DragValue::new(&mut lon).speed(1e-16)); + if !is_dependency { + ui.label("y"); + } + let mut y = tf.translation.y; + ui.add(DragValue::new(&mut y).speed(0.01)); - if old_lat != lat || old_lon != lon { - self.events.request.move_to.send(MoveTo { - entity: self.anchor, - transform: Transform::from_translation( - latlon_to_world( - lat as f32, - lon as f32, - offset.anchor, - ), - ), - }); - } - } - } + if x != tf.translation.x || y != tf.translation.y { + {} + params.move_to.send(MoveTo { + entity: id, + transform: Transform::from_translation([x, y, 0.0].into()), }); } - Anchor::CategorizedTranslate2D(_) => { - warn!("Categorized translate inspector not implemented yet"); - } - Anchor::Pose3D(pose) => { - ui.vertical(|ui| { - if let Some(c) = mesh_constraint { - // For mesh constraints we only allow rotation editing - if let Some(new_pose) = - InspectPose::new(&c.relative_pose).for_rotation().show(ui) - { - // TODO(luca) Using moveto doesn't allow switching between variants of - // Pose3D - self.events - .workcell_change - .mesh_constraints - .send(Change::new( - MeshConstraint { - entity: c.entity, - element: c.element.clone(), - relative_pose: new_pose, - }, - self.anchor, - )); - } - } else { - if let Some(new_pose) = InspectPose::new(pose).show(ui) { - // TODO(luca) Using moveto doesn't allow switching between variants of - // Pose3D - self.events.request.move_to.send(MoveTo { - entity: self.anchor, - transform: new_pose.transform(), - }); - } + } + Anchor::CategorizedTranslate2D(_) => { + warn!("Categorized translate inspector not implemented yet"); + } + Anchor::Pose3D(pose) => { + panel.align(ui, |ui| { + if let Some(c) = mesh_constraint { + if let Some(new_pose) = + InspectPoseComponent::new(&c.relative_pose).for_rotation().show(ui) + { + // TODO(luca) Using moveto doesn't allow switching between + // variants of Pose3D + params.mesh_constraints.send(Change::new( + MeshConstraint { + entity: c.entity, + element: c.element.clone(), + relative_pose: new_pose, + }, + id, + )); } - // If the parent is not a joint, add a joint creation widget - if self.params.joints.get(parent.get()).is_err() { - if ui.button("Create joint").on_hover_text("Create a fixed joint and place it between the parent frame and this frame").clicked() { - self.events.request.create_joint.send(CreateJoint { - parent: parent.get(), - child: self.anchor, - }); - } + } else { + if let Some(new_pose) = InspectPoseComponent::new(pose).show(ui) { + // TODO(luca) Using moveto doesn't allow switching between variants of + // Pose3D + params.move_to.send(MoveTo { + entity: id, + transform: new_pose.transform(), + }); } - }); - } + } + // If the parent is not a joint, add a joint creation widget + if params.joints.get(parent.get()).is_err() { + if ui.button("Create joint").on_hover_text("Create a fixed joint and place it between the parent frame and this frame").clicked() { + params.create_joint.send(CreateJoint { + parent: parent.get(), + child: id, + }); + } + } + }); } } } - - InspectAnchorResponse { replace } } -} -pub struct InspectAnchorResponse { - pub replace: bool, + Some(InspectAnchorResponse { replace }) } #[derive(SystemParam)] -pub struct InspectAnchorDependentsParams<'w, 's> { - pub dependents: Query<'w, 's, &'static Dependents, With>, - pub locations: Query<'w, 's, &'static LocationTags, &'static AssociatedGraphs>, - pub info: Query<'w, 's, (&'static Category, Option<&'static SiteID>)>, - pub icons: Res<'w, Icons>, -} - -pub struct InspectAnchorDependentsWidget<'a, 'w1, 'w2, 's1, 's2> { - pub anchor: Entity, - pub params: &'a InspectAnchorDependentsParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, +pub struct InspectAnchorDependents<'w, 's> { + dependents: Query<'w, 's, &'static Dependents, With>, + category: Query<'w, 's, &'static Category>, } -impl<'a, 'w1, 'w2, 's1, 's2> InspectAnchorDependentsWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - anchor: Entity, - params: &'a InspectAnchorDependentsParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - anchor, - params, - events, - } - } - - fn show_dependents( - dependents: &HashSet, - params: &InspectAnchorDependentsParams<'w1, 's1>, - events: &mut AppEvents<'w2, 's2>, +impl<'w, 's> WidgetSystem for InspectAnchorDependents<'w, 's> { + fn show( + Inspect { + selection, panel, .. + }: Inspect, ui: &mut Ui, + state: &mut SystemState, + world: &mut World, ) { - ui.heading("Dependents"); - let mut category_map: BTreeMap>> = BTreeMap::new(); - for e in dependents { - if let Ok((category, site_id)) = params.info.get(*e) { - category_map - .entry(*category) - .or_default() - .insert(*e, site_id.map(|s| s.0)); - } else { - ui.label(format!("ERROR: Broken reference to entity {e:?}")); - } + let params = state.get(world); + let Ok(dependents) = params.dependents.get(selection) else { + return; + }; + if dependents.is_empty() { + ui.label("No dependents"); + return; } - for (category, entities) in &category_map { - ui.label(category.label()); - - for (e, site_id) in entities { - ui.horizontal(|ui| { - SelectionWidget::new(*e, site_id.map(SiteID), params.icons.as_ref(), events) - .show(ui); - }); + let mut category_map: BTreeMap> = BTreeMap::new(); + for e in &dependents.0 { + if let Ok(category) = params.category.get(*e) { + category_map.entry(*category).or_default().insert(*e); + } else { + error!("Broken reference to entity {e:?}"); } } - } - pub fn show(mut self, ui: &mut Ui) { - ui.vertical(|ui| { - if let Ok(dependents) = self.params.dependents.get(self.anchor) { - if dependents.is_empty() { - ui.label("No dependents"); - } else { - Self::show_dependents(&dependents.0, &self.params, &mut self.events, ui); + panel.align(ui, |ui| { + ui.heading("Dependencies"); + for (category, entities) in &category_map { + ui.label(category.label()); + for e in entities { + panel.orthogonal(ui, |ui| { + world.show::(*e, ui); + }); } - } else { - ui.label("ERROR: Unable to find dependents info for this anchor"); } }); } } + +pub struct InspectAnchorResponse { + pub replace: bool, +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_asset_source.rs b/rmf_site_editor/src/widgets/inspector/inspect_asset_source.rs index 246777a0..f85298b2 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_asset_source.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_asset_source.rs @@ -15,7 +15,12 @@ * */ -use crate::site::DefaultFile; +use crate::{ + site::{Change, DefaultFile, Pending}, + widgets::{prelude::*, Inspect}, + CurrentWorkspace, +}; +use bevy::prelude::*; use bevy_egui::egui::{ComboBox, Ui}; use pathdiff::diff_paths; use rmf_site_format::{AssetSource, RecallAssetSource}; @@ -23,13 +28,51 @@ use rmf_site_format::{AssetSource, RecallAssetSource}; #[cfg(not(target_arch = "wasm32"))] use rfd::FileDialog; -pub struct InspectAssetSource<'a> { +#[derive(SystemParam)] +pub struct InspectAssetSource<'w, 's> { + asset_sources: + Query<'w, 's, (&'static AssetSource, &'static RecallAssetSource), Without>, + change_asset_source: EventWriter<'w, Change>, + current_workspace: Res<'w, CurrentWorkspace>, + default_file: Query<'w, 's, &'static DefaultFile>, +} + +impl<'w, 's> WidgetSystem for InspectAssetSource<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok((source, recall)) = params.asset_sources.get(selection) else { + return; + }; + + let default_file = params + .current_workspace + .root + .map(|e| params.default_file.get(e).ok()) + .flatten(); + + if let Some(new_source) = + InspectAssetSourceComponent::new(source, recall, default_file).show(ui) + { + params + .change_asset_source + .send(Change::new(new_source, selection)); + } + ui.add_space(10.0); + } +} + +pub struct InspectAssetSourceComponent<'a> { pub source: &'a AssetSource, pub recall: &'a RecallAssetSource, pub default_file: Option<&'a DefaultFile>, } -impl<'a> InspectAssetSource<'a> { +impl<'a> InspectAssetSourceComponent<'a> { pub fn new( source: &'a AssetSource, recall: &'a RecallAssetSource, diff --git a/rmf_site_editor/src/widgets/inspector/inspect_associated_graphs.rs b/rmf_site_editor/src/widgets/inspector/inspect_associated_graphs.rs index 48450ece..e28d2371 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_associated_graphs.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_associated_graphs.rs @@ -20,7 +20,7 @@ use crate::{ AssociatedGraphs, Change, ConsiderAssociatedGraph, NameInSite, NavGraphMarker, RecallAssociatedGraphs, }, - widgets::{AppEvents, Icons}, + widgets::{prelude::*, Icons, Inspect}, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{ComboBox, ImageButton, Ui}; @@ -28,7 +28,7 @@ use smallvec::SmallVec; use std::collections::BTreeMap; #[derive(SystemParam)] -pub struct InspectAssociatedGraphsParams<'w, 's> { +pub struct InspectAssociatedGraphs<'w, 's> { associated: Query< 'w, 's, @@ -39,29 +39,25 @@ pub struct InspectAssociatedGraphsParams<'w, 's> { >, graphs: Query<'w, 's, (Entity, &'static NameInSite), With>, icons: Res<'w, Icons>, + consider_graph: EventWriter<'w, ConsiderAssociatedGraph>, + change_associated_graphs: EventWriter<'w, Change>>, } -pub struct InspectAssociatedGraphsWidget<'a, 'w1, 's1, 'w2, 's2> { - pub entity: Entity, - pub params: &'a InspectAssociatedGraphsParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 's1, 'w2, 's2> InspectAssociatedGraphsWidget<'a, 'w1, 's1, 'w2, 's2> { - pub fn new( - entity: Entity, - params: &'a InspectAssociatedGraphsParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - params, - events, - } +impl<'w, 's> WidgetSystem for InspectAssociatedGraphs<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} - pub fn show(self, ui: &mut Ui) { - let (associated, recall) = match self.params.associated.get(self.entity) { +impl<'w, 's> InspectAssociatedGraphs<'w, 's> { + fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let (associated, recall) = match self.associated.get(id) { Ok(q) => q, _ => return, }; @@ -87,24 +83,20 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectAssociatedGraphsWidget<'a, 'w1, 's1, 'w2, 's AssociatedGraphs::Only(set) | AssociatedGraphs::AllExcept(set) => { let mut removed_graphs: SmallVec<[Entity; 2]> = SmallVec::new(); for g in set.iter() { - let (_, name) = match self.params.graphs.get(*g) { + let (_, name) = match self.graphs.get(*g) { Ok(q) => q, _ => continue, }; ui.horizontal(|ui| { - if ui - .add(ImageButton::new(self.params.icons.trash.egui())) - .clicked() - { + if ui.add(ImageButton::new(self.icons.trash.egui())).clicked() { removed_graphs.push(*g); } ui.label(&name.0); }); } - let unused_graphs: BTreeMap = BTreeMap::from_iter( - self.params.graphs.iter().filter(|(e, _)| !set.contains(e)), - ); + let unused_graphs: BTreeMap = + BTreeMap::from_iter(self.graphs.iter().filter(|(e, _)| !set.contains(e))); if let Some((first, _)) = unused_graphs.iter().next() { ui.horizontal(|ui| { @@ -124,16 +116,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectAssociatedGraphsWidget<'a, 'w1, 's1, 'w2, 's if add_graph { set.insert(choice); - self.events - .request - .consider_graph - .send(ConsiderAssociatedGraph::new(None, self.entity)); + self.consider_graph + .send(ConsiderAssociatedGraph::new(None, id)); } else { if Some(choice) != recall.consider { - self.events - .request - .consider_graph - .send(ConsiderAssociatedGraph::new(Some(choice), self.entity)); + self.consider_graph + .send(ConsiderAssociatedGraph::new(Some(choice), id)); } } }); @@ -146,10 +134,8 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectAssociatedGraphsWidget<'a, 'w1, 's1, 'w2, 's } if new_associated != *associated { - self.events - .change - .associated_graphs - .send(Change::new(new_associated, self.entity)); + self.change_associated_graphs + .send(Change::new(new_associated, id)); } ui.add_space(10.0); diff --git a/rmf_site_editor/src/widgets/inspector/inspect_door.rs b/rmf_site_editor/src/widgets/inspector/inspect_door.rs index 2710ff67..86e1df17 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_door.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_door.rs @@ -15,10 +15,43 @@ * */ -use crate::widgets::inspector::{InspectAngle, InspectSide}; +use crate::{ + site::Change, + widgets::{ + inspector::{InspectAngle, InspectSide}, + prelude::*, + Inspect, + }, +}; +use bevy::prelude::*; use bevy_egui::egui::{ComboBox, DragValue, Ui}; use rmf_site_format::{DoorType, RecallDoorType, Swing}; +#[derive(SystemParam)] +pub struct InspectDoor<'w, 's> { + doors: Query<'w, 's, (&'static DoorType, &'static RecallDoorType)>, + change_door: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectDoor<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok((door, recall)) = params.doors.get(selection) else { + return; + }; + + if let Some(new_door) = InspectDoorType::new(door, recall).show(ui) { + params.change_door.send(Change::new(new_door, selection)); + } + ui.add_space(10.0); + } +} + pub struct InspectDoorType<'a> { pub kind: &'a DoorType, pub recall: &'a RecallDoorType, diff --git a/rmf_site_editor/src/widgets/inspector/inspect_drawing.rs b/rmf_site_editor/src/widgets/inspector/inspect_drawing.rs new file mode 100644 index 00000000..dee238f4 --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_drawing.rs @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + site::{AlignSiteDrawings, BeginEditDrawing, Change, PixelsPerMeter}, + widgets::{prelude::*, Inspect, InspectValue}, + AppState, CurrentWorkspace, Icons, +}; +use bevy::prelude::*; +use bevy_egui::egui::Button; + +#[derive(SystemParam)] +pub struct InspectDrawing<'w, 's> { + pixels_per_meter: Query<'w, 's, &'static PixelsPerMeter>, + change_pixels_per_meter: EventWriter<'w, Change>, + current_workspace: Res<'w, CurrentWorkspace>, + align_site: EventWriter<'w, AlignSiteDrawings>, + app_state: Res<'w, State>, + icons: Res<'w, Icons>, + begin_edit_drawing: EventWriter<'w, BeginEditDrawing>, +} + +impl<'w, 's> WidgetSystem for InspectDrawing<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok(ppm) = params.pixels_per_meter.get(selection) else { + return; + }; + + if *params.app_state.get() == AppState::SiteEditor { + ui.add_space(10.0); + if ui + .add(Button::image_and_text( + params.icons.edit.egui(), + "Edit Drawing", + )) + .clicked() + { + params.begin_edit_drawing.send(BeginEditDrawing(selection)); + } + ui.add_space(10.0); + } + + if *params.app_state.get() != AppState::SiteDrawingEditor { + if ui + .add(Button::image_and_text( + params.icons.alignment.egui(), + "Align Drawings", + )) + .on_hover_text( + "Align all drawings in the site based on their fiducials and measurements", + ) + .clicked() + { + if let Some(site) = params.current_workspace.root { + params.align_site.send(AlignSiteDrawings(site)); + } + } + ui.add_space(10.0); + } + + if let Some(new_ppm) = InspectValue::::new("Pixels per meter", ppm.0) + .clamp_range(0.0001..=std::f32::INFINITY) + .tooltip("How many image pixels per meter") + .show(ui) + { + params + .change_pixels_per_meter + .send(Change::new(PixelsPerMeter(new_ppm), selection)); + } + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_edge.rs b/rmf_site_editor/src/widgets/inspector/inspect_edge.rs index 280ca60e..b71ea1a1 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_edge.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_edge.rs @@ -17,70 +17,65 @@ use crate::{ interaction::{ChangeMode, SelectAnchor}, - site::{Category, EdgeLabels, Original}, + site::{Category, EdgeLabels, Original, SiteID}, widgets::{ - inspector::{InspectAnchorParams, InspectAnchorWidget}, - AppEvents, + inspector::{Inspect, InspectAnchor, InspectAnchorInput}, + prelude::*, }, }; use bevy::prelude::*; use bevy_egui::egui::{Grid, Ui}; use rmf_site_format::{Edge, Side}; -pub struct InspectEdgeWidget<'a, 'w1, 'w2, 's1, 's2> { - pub entity: Entity, - pub category: &'a Category, - pub edge: &'a Edge, - pub original: Option<&'a Original>>, - pub labels: Option<&'a EdgeLabels>, - pub anchor_params: &'a InspectAnchorParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, +#[derive(SystemParam)] +pub struct InspectEdge<'w, 's> { + edges: Query< + 'w, + 's, + ( + &'static Category, + &'static Edge, + Option<&'static Original>>, + Option<&'static EdgeLabels>, + ), + >, + change_mode: EventWriter<'w, ChangeMode>, } -impl<'a, 'w1, 'w2, 's1, 's2> InspectEdgeWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - entity: Entity, - category: &'a Category, - edge: &'a Edge, - original: Option<&'a Original>>, - labels: Option<&'a EdgeLabels>, - anchor_params: &'a InspectAnchorParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - category, - edge, - original, - labels, - anchor_params, - events, - } - } +impl<'w, 's> ShareableWidget for InspectEdge<'w, 's> {} - pub fn start_text(&self) -> &'static str { - self.labels.unwrap_or(&EdgeLabels::default()).start() - } - - pub fn end_text(&self) -> &'static str { - self.labels.unwrap_or(&EdgeLabels::default()).end() - } +impl<'w, 's> WidgetSystem for InspectEdge<'w, 's> { + fn show( + Inspect { + selection: id, + panel, + .. + }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let params = state.get_mut(world); + let Ok((category, current_edge, original, labels)) = params.edges.get(id) else { + return; + }; - pub fn show(self, ui: &mut Ui) { - let edge = if let Some(original) = self.original { - if original.is_reverse_of(self.edge) { - // The user is previewing a flipped edge. To avoid ugly - // high frequency UI flipping, we will display the edge - // in its original form until the user has committed to - // the flip. - &original.0 + let edge = if let Some(original) = original { + if original.is_reverse_of(current_edge) { + // The user is previewing a flipped edge. To avoid ugly high + // frequency UI flipping, we will display the edge in its + // original form until the user has committed to the flip. + original.0 } else { - self.edge + *current_edge } } else { - self.edge + *current_edge }; + let labels = labels.copied().unwrap_or_default(); + let category = *category; + Grid::new("inspect_edge").show(ui, |ui| { ui.label(""); ui.label("ID"); @@ -89,39 +84,85 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectEdgeWidget<'a, 'w1, 'w2, 's1, 's2> { ui.label("y"); ui.end_row(); - ui.label(self.start_text()); - let start_response = - InspectAnchorWidget::new(edge.start(), self.anchor_params, self.events) - .as_dependency() - .show(ui); - ui.end_row(); - if start_response.replace { - if let Some(request) = - SelectAnchor::replace_side(self.entity, Side::Left).for_category(*self.category) - { - self.events - .request - .change_mode - .send(ChangeMode::To(request.into())); - } - } + Self::show_anchor( + Side::Left, + id, + edge, + labels, + category, + panel, + ui, + state, + world, + ); + Self::show_anchor( + Side::Right, + id, + edge, + labels, + category, + panel, + ui, + state, + world, + ); + }); + ui.add_space(10.0); + } +} - ui.label(self.end_text()); - let end_response = - InspectAnchorWidget::new(edge.end(), self.anchor_params, self.events) - .as_dependency() - .show(ui); - ui.end_row(); - if end_response.replace { - if let Some(request) = SelectAnchor::replace_side(self.entity, Side::Right) - .for_category(*self.category) - { - self.events - .request - .change_mode - .send(ChangeMode::To(request.into())); +impl<'w, 's> InspectEdge<'w, 's> { + fn show_anchor( + side: Side, + id: Entity, + edge: Edge, + labels: EdgeLabels, + category: Category, + panel: PanelSide, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + ui.label(labels.side(side)); + let anchor = edge.side(side); + let response = world.show::( + InspectAnchorInput { + anchor, + is_dependency: true, + panel, + }, + ui, + ); + ui.end_row(); + + match response { + Some(response) => { + if response.replace { + if let Some(request) = + SelectAnchor::replace_side(id, side).for_category(category) + { + info!( + "Triggered anchor replacement for side \ + {side:?} of edge {edge:?} with category {category:?}" + ); + let mut params = state.get_mut(world); + params.change_mode.send(ChangeMode::To(request.into())); + } else { + error!( + "Failed to trigger an anchor replacement for side \ + {side:?} of edge {edge:?} with category {category:?}" + ); + } } } - }); + None => { + error!( + "An endpoint in the edge {id:?} (Site ID {:?}) is not an \ + anchor: {anchor:?}! This should never happen! Please report \ + this to the site editor developers.", + world.get::(anchor), + ); + } + } } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs index 0a2af2d4..4e78394b 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs @@ -17,7 +17,7 @@ use crate::{ site::{Affiliation, Change, FiducialGroup, FiducialMarker, FiducialUsage, Group, NameInSite}, - widgets::{AppEvents, Icons}, + widgets::{prelude::*, Icons, Inspect, InspectionPlugin}, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{ComboBox, ImageButton, Ui}; @@ -57,207 +57,208 @@ impl SearchResult { } } +#[derive(Default)] +pub struct InspectFiducialPlugin {} + +impl Plugin for InspectFiducialPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(InspectionPlugin::::new()); + } +} + #[derive(SystemParam)] -pub struct InspectFiducialParams<'w, 's> { +pub struct InspectFiducial<'w, 's> { fiducials: Query<'w, 's, (&'static Affiliation, &'static Parent), With>, group_names: Query<'w, 's, &'static NameInSite, (With, With)>, usage: Query<'w, 's, &'static FiducialUsage>, icons: Res<'w, Icons>, + search_for_fiducial: ResMut<'w, SearchForFiducial>, + commands: Commands<'w, 's>, + change_affiliation: EventWriter<'w, Change>>, } -pub struct InspectFiducialWidget<'a, 'w1, 'w2, 's1, 's2> { - entity: Entity, - params: &'a InspectFiducialParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 'w2, 's1, 's2> InspectFiducialWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - entity: Entity, - params: &'a InspectFiducialParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - params, - events, - } - } - - pub fn show(self, ui: &mut Ui) { - let Ok((affiliation, parent)) = self.params.fiducials.get(self.entity) else { +impl<'w, 's> WidgetSystem for InspectFiducial<'w, 's> { + fn show( + Inspect { + selection, panel, .. + }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok((affiliation, parent)) = params.fiducials.get(selection) else { return; }; - let Ok(tracker) = self.params.usage.get(parent.get()) else { + let Ok(tracker) = params.usage.get(parent.get()) else { return; }; - ui.separator(); - ui.label("Affiliation"); - let get_group_name = |affiliation: Affiliation| { - if let Some(group) = affiliation.0 { - if let Ok(name) = self.params.group_names.get(group) { - Some(name.0.clone()) + panel.align(ui, |ui| { + ui.separator(); + ui.label("Affiliation"); + let get_group_name = |affiliation: Affiliation| { + if let Some(group) = affiliation.0 { + if let Ok(name) = params.group_names.get(group) { + Some(name.0.clone()) + } else { + None + } } else { - None + Some("".to_owned()) } - } else { - Some("".to_owned()) - } - }; - let selected_text = - get_group_name(*affiliation).unwrap_or_else(|| "".to_owned()); - - ui.horizontal(|ui| { - let search = &mut self.events.change.search_for_fiducial.0; - let mut result = SearchResult::NoMatch; - let mut any_partial_matches = false; + }; + let selected_text = + get_group_name(*affiliation).unwrap_or_else(|| "".to_owned()); - if search.is_empty() { - // An empty string should not be used - result = SearchResult::Empty; - } + panel.orthogonal(ui, |ui| { + let search = &mut params.search_for_fiducial.0; + let mut result = SearchResult::NoMatch; + let mut any_partial_matches = false; - if *search == selected_text { - result = SearchResult::Current; - } + if search.is_empty() { + // An empty string should not be used + result = SearchResult::Empty; + } - for (e, name) in tracker.unused().iter() { - if *search == *name { - result.consider(*e); + if *search == selected_text { + result = SearchResult::Current; } - if !any_partial_matches { - if name.contains(&*search) { - any_partial_matches = true; + for (e, name) in tracker.unused().iter() { + if *search == *name { + result.consider(*e); } - } - } - for (e, name) in tracker.used().iter() { - if *search == *name && !affiliation.0.is_some_and(|a| a == *e) { - result.conflict("Group name is already taken"); + if !any_partial_matches { + if name.contains(&*search) { + any_partial_matches = true; + } + } } - } - if any_partial_matches { - if ui - .add(ImageButton::new(self.params.icons.search.egui())) - .on_hover_text("Search results for this text can be found below") - .clicked() - { - info!("Use the drop-down box to choose a group for this fiducial"); + for (e, name) in tracker.used().iter() { + if *search == *name && !affiliation.0.is_some_and(|a| a == *e) { + result.conflict("Group name is already taken"); + } } - } else { - ui.add(ImageButton::new(self.params.icons.empty.egui())) - .on_hover_text("No search results can be found for this text"); - } - match result { - SearchResult::Empty => { + if any_partial_matches { if ui - .add(ImageButton::new(self.params.icons.hidden.egui())) - .on_hover_text("An empty string is not a good fiducial group name") + .add(ImageButton::new(params.icons.search.egui())) + .on_hover_text("Search results for this text can be found below") .clicked() { - warn!("You should not use an empty string as a fiducial group name"); + info!("Use the drop-down box to choose a group for this fiducial"); } + } else { + ui.add(ImageButton::new(params.icons.empty.egui())) + .on_hover_text("No search results can be found for this text"); } - SearchResult::Current => { - if ui - .add(ImageButton::new(self.params.icons.selected.egui())) - .on_hover_text("This is the name of the fiducial's current group") - .clicked() - { - info!("This fiducial group is already selected"); + + match result { + SearchResult::Empty => { + if ui + .add(ImageButton::new(params.icons.hidden.egui())) + .on_hover_text("An empty string is not a good fiducial group name") + .clicked() + { + warn!("You should not use an empty string as a fiducial group name"); + } } - } - SearchResult::NoMatch => { - if ui - .add(ImageButton::new(self.params.icons.add.egui())) - .on_hover_text("Create a new group for this fiducial") - .clicked() - { - let new_group = self - .events - .commands - .spawn(FiducialGroup::new(NameInSite(search.clone()))) - .set_parent(tracker.site()) - .id(); - self.events - .change - .affiliation - .send(Change::new(Affiliation(Some(new_group)), self.entity)); + SearchResult::Current => { + if ui + .add(ImageButton::new(params.icons.selected.egui())) + .on_hover_text("This is the name of the fiducial's current group") + .clicked() + { + info!("This fiducial group is already selected"); + } } - } - SearchResult::Match(group) => { - if ui - .add(ImageButton::new(self.params.icons.confirm.egui())) - .on_hover_text("Select this group") - .clicked() - { - self.events - .change - .affiliation - .send(Change::new(Affiliation(Some(group)), self.entity)); + SearchResult::NoMatch => { + if ui + .add(ImageButton::new(params.icons.add.egui())) + .on_hover_text("Create a new group for this fiducial") + .clicked() + { + let new_group = params + .commands + .spawn(FiducialGroup::new(NameInSite(search.clone()))) + .set_parent(tracker.site()) + .id(); + params + .change_affiliation + .send(Change::new(Affiliation(Some(new_group)), selection)); + } } - } - SearchResult::Conflict(text) => { - if ui - .add(ImageButton::new(self.params.icons.reject.egui())) - .on_hover_text(text) - .clicked() - { - warn!("Cannot set {search} as the fiducial group name: {text}"); + SearchResult::Match(group) => { + if ui + .add(ImageButton::new(params.icons.confirm.egui())) + .on_hover_text("Select this group") + .clicked() + { + params + .change_affiliation + .send(Change::new(Affiliation(Some(group)), selection)); + } + } + SearchResult::Conflict(text) => { + if ui + .add(ImageButton::new(params.icons.reject.egui())) + .on_hover_text(text) + .clicked() + { + warn!("Cannot set {search} as the fiducial group name: {text}"); + } } } - } - ui.text_edit_singleline(search) - .on_hover_text("Search or add a group name for this fiducial"); - }); + ui.text_edit_singleline(search) + .on_hover_text("Search or add a group name for this fiducial"); + }); - let mut new_affiliation = affiliation.clone(); - ui.horizontal(|ui| { - if ui - .add(ImageButton::new(self.params.icons.exit.egui())) - .on_hover_text("Remove this fiducial from its current group") - .clicked() - { - new_affiliation = Affiliation(None); - } + let mut new_affiliation = affiliation.clone(); + panel.orthogonal(ui, |ui| { + if ui + .add(ImageButton::new(params.icons.exit.egui())) + .on_hover_text("Remove this fiducial from its current group") + .clicked() + { + new_affiliation = Affiliation(None); + } - let mut clear_filter = false; - ComboBox::from_id_source("fiducial_affiliation") - .selected_text(selected_text) - .show_ui(ui, |ui| { - if let Some(group_name) = get_group_name(new_affiliation) { - ui.selectable_value(&mut new_affiliation, *affiliation, group_name); - } + let mut clear_filter = false; + ComboBox::from_id_source("fiducial_affiliation") + .selected_text(selected_text) + .show_ui(ui, |ui| { + if let Some(group_name) = get_group_name(new_affiliation) { + ui.selectable_value(&mut new_affiliation, *affiliation, group_name); + } - for (group, name) in tracker.unused() { - if name.contains(&self.events.change.search_for_fiducial.0) { - let select_affiliation = Affiliation(Some(*group)); - ui.selectable_value(&mut new_affiliation, select_affiliation, name); + for (group, name) in tracker.unused() { + if name.contains(¶ms.search_for_fiducial.0) { + let select_affiliation = Affiliation(Some(*group)); + ui.selectable_value(&mut new_affiliation, select_affiliation, name); + } } - } - if !self.events.change.search_for_fiducial.0.is_empty() { - ui.selectable_value(&mut clear_filter, true, "more..."); - } - }); + if !params.search_for_fiducial.0.is_empty() { + ui.selectable_value(&mut clear_filter, true, "more..."); + } + }); - if clear_filter { - self.events.change.search_for_fiducial.0.clear(); + if clear_filter { + params.search_for_fiducial.0.clear(); + } + }); + + if new_affiliation != *affiliation { + params + .change_affiliation + .send(Change::new(new_affiliation, selection)); } + ui.separator(); }); - - if new_affiliation != *affiliation { - self.events - .change - .affiliation - .send(Change::new(new_affiliation, self.entity)); - } - ui.separator(); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_geography.rs b/rmf_site_editor/src/widgets/inspector/inspect_geography.rs new file mode 100644 index 00000000..9daf5f50 --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_geography.rs @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + interaction::MoveTo, + site::*, + widgets::{inspector::*, prelude::*}, + CurrentWorkspace, +}; +use bevy::prelude::*; +use bevy_egui::egui::DragValue; + +#[derive(SystemParam)] +pub struct InspectGeography<'w, 's> { + geographical: Query<'w, 's, &'static GeographicComponent>, + tfs: Query<'w, 's, &'static Transform, Or<(With, With)>>, + move_to: EventWriter<'w, MoveTo>, + current_workspace: Res<'w, CurrentWorkspace>, +} + +impl<'w, 's> WidgetSystem for InspectGeography<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut param = state.get_mut(world); + let Some(workspace) = param.current_workspace.root else { + return; + }; + let Ok(geo) = param.geographical.get(workspace) else { + return; + }; + let Ok(tf) = param.tfs.get(selection) else { + return; + }; + + if let Some(offset) = geo.0 { + let (mut lat, mut lon) = match world_to_latlon(tf.translation, offset.anchor) { + Ok(values) => values, + Err(err) => { + warn!("Unable to obtain latitude and longitude: {err:?}"); + return; + } + }; + + let old_lat = lat; + let old_lon = lon; + + ui.label("Latitude"); + ui.add(DragValue::new(&mut lat).speed(0.0)); + ui.label("Longitude"); + ui.add(DragValue::new(&mut lon).speed(0.0)); + + if old_lat != lat || old_lon != lon { + param.move_to.send(MoveTo { + entity: selection, + transform: Transform::from_translation(latlon_to_world( + lat as f32, + lon as f32, + offset.anchor, + )), + }); + } + } + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_group.rs b/rmf_site_editor/src/widgets/inspector/inspect_group.rs index 23e25e81..f7b0df47 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_group.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_group.rs @@ -16,69 +16,71 @@ */ use crate::{ - site::{Affiliation, Change, DefaultFile, Group, Members, SiteID, Texture}, - widgets::{ - inspector::{InspectTexture, SelectionWidget}, - AppEvents, - }, - Icons, + site::{Affiliation, Change, DefaultFile, Group, Members, NameInSite, Texture}, + widgets::{inspector::InspectTexture, prelude::*, Inspect, SelectorWidget}, + CurrentWorkspace, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{CollapsingHeader, RichText, Ui}; #[derive(SystemParam)] -pub struct InspectGroupParams<'w, 's> { - pub is_group: Query<'w, 's, (), With>, - pub affiliation: Query<'w, 's, &'static Affiliation>, - pub textures: Query<'w, 's, &'static Texture>, - pub members: Query<'w, 's, &'static Members>, - pub site_id: Query<'w, 's, &'static SiteID>, - pub icons: Res<'w, Icons>, +pub struct InspectGroup<'w, 's> { + is_group: Query<'w, 's, (), With>, + affiliation: Query<'w, 's, &'static Affiliation>, + names: Query<'w, 's, &'static NameInSite>, + textures: Query<'w, 's, &'static Texture>, + members: Query<'w, 's, &'static Members>, + default_file: Query<'w, 's, &'static DefaultFile>, + current_workspace: Res<'w, CurrentWorkspace>, + change_texture: EventWriter<'w, Change>, + selector: SelectorWidget<'w, 's>, } -pub struct InspectGroup<'a, 'w1, 'w2, 's1, 's2> { - group: Entity, - selection: Entity, - default_file: Option<&'a DefaultFile>, - params: &'a InspectGroupParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, +impl<'w, 's> WidgetSystem for InspectGroup<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); + } } -impl<'a, 'w1, 'w2, 's1, 's2> InspectGroup<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - group: Entity, - selection: Entity, - default_file: Option<&'a DefaultFile>, - params: &'a InspectGroupParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - group, - selection, - default_file, - params, - events, +impl<'w, 's> InspectGroup<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + if self.is_group.contains(id) { + self.show_group_properties(id, ui); + } + + if let Ok(Affiliation(Some(group))) = self.affiliation.get(id) { + ui.separator(); + let name = self.names.get(*group).map(|n| n.0.as_str()).unwrap_or(""); + ui.label(RichText::new(format!("Group Properties of [{}]", name)).size(18.0)); + ui.add_space(5.0); + self.show_group_properties(*group, ui); } } - pub fn show(self, ui: &mut Ui) { - if let Ok(texture) = self.params.textures.get(self.group) { + pub fn show_group_properties(&mut self, id: Entity, ui: &mut Ui) { + let default_file = self + .current_workspace + .root + .map(|e| self.default_file.get(e).ok()) + .flatten(); + + if let Ok(texture) = self.textures.get(id) { ui.label(RichText::new("Texture Properties").size(18.0)); - if let Some(new_texture) = InspectTexture::new(texture, self.default_file).show(ui) { - self.events - .change - .texture - .send(Change::new(new_texture, self.group)); + if let Some(new_texture) = InspectTexture::new(texture, default_file).show(ui) { + self.change_texture.send(Change::new(new_texture, id)); } ui.add_space(10.0); } - if let Ok(members) = self.params.members.get(self.group) { + if let Ok(members) = self.members.get(id) { CollapsingHeader::new("Members").show(ui, |ui| { for member in members.iter() { - let site_id = self.params.site_id.get(self.group).ok().cloned(); - SelectionWidget::new(*member, site_id, &self.params.icons, self.events) - .as_selected(self.selection == *member) - .show(ui); + self.selector.show_widget(*member, ui); } }); } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_joint.rs b/rmf_site_editor/src/widgets/inspector/inspect_joint.rs index 089b0dd8..8f3686eb 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_joint.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_joint.rs @@ -16,16 +16,15 @@ */ use crate::{ - site::{Dependents, FrameMarker, JointProperties, SiteID}, - widgets::{inspector::SelectionWidget, AppEvents}, - Icons, + site::{Dependents, FrameMarker, JointProperties}, + widgets::{prelude::*, Inspect, SelectorWidget}, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::Ui; #[derive(SystemParam)] -pub struct InspectJointParams<'w, 's> { - pub joints: Query< +pub struct InspectJoint<'w, 's> { + joints: Query< 'w, 's, ( @@ -34,53 +33,34 @@ pub struct InspectJointParams<'w, 's> { &'static JointProperties, ), >, - pub icons: Res<'w, Icons>, - pub site_id: Query<'w, 's, &'static SiteID>, - pub frames: Query<'w, 's, (), With>, + frames: Query<'w, 's, (), With>, + selector: SelectorWidget<'w, 's>, } -pub struct InspectJointWidget<'a, 'w1, 'w2, 's1, 's2> { - pub joint: Entity, - pub params: &'a InspectJointParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 'w2, 's1, 's2> InspectJointWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - joint: Entity, - params: &'a InspectJointParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - joint, - params, - events, - } +impl<'w, 's> WidgetSystem for InspectJoint<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} - pub fn show(self, ui: &mut Ui) { - let Ok((parent, deps, joint_properties)) = self.params.joints.get(self.joint) else { +impl<'w, 's> InspectJoint<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((parent, deps, joint_properties)) = self.joints.get(id) else { return; }; ui.label("Parent frame"); - SelectionWidget::new( - **parent, - self.params.site_id.get(**parent).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(**parent, ui); - if let Some(frame_dep) = deps.iter().find(|d| self.params.frames.get(**d).is_ok()) { + if let Some(frame_dep) = deps.iter().find(|d| self.frames.get(**d).is_ok()) { ui.label("Child frame"); - SelectionWidget::new( - *frame_dep, - self.params.site_id.get(*frame_dep).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(*frame_dep, ui); } ui.horizontal(|ui| { diff --git a/rmf_site_editor/src/widgets/inspector/inspect_layer.rs b/rmf_site_editor/src/widgets/inspector/inspect_layer.rs index 5acc28c6..dd3c7790 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_layer.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_layer.rs @@ -16,128 +16,182 @@ */ use crate::{ - interaction::Hover, + interaction::{Hover, Selection}, site::{ - BeginEditDrawing, Change, LayerVisibility, PreferredSemiTransparency, SiteID, - VisibilityCycle, + BeginEditDrawing, Change, DrawingMarker, FloorMarker, LayerVisibility, + PreferredSemiTransparency, VisibilityCycle, }, - widgets::{inspector::SelectionWidget, AppEvents, Icons}, + widgets::{inspector::Inspect, prelude::*, Icons, MoveLayer, SelectorWidget}, + ChangeRank, }; use bevy::prelude::*; use bevy_egui::egui::{DragValue, ImageButton, Ui}; -pub struct InspectLayer<'a, 'w, 's> { - pub entity: Entity, - pub icons: &'a Icons, - /// Does the layer have a custom visibility setting? - pub layer_vis: Option, - pub default_alpha: f32, - // TODO(luca) make this an enum - pub is_floor: bool, - pub as_selected: bool, - /// Outer Option: Can this be selected? - /// Inner Option: Does this have a SiteID? - pub site_id: Option>, - pub events: &'a mut AppEvents<'w, 's>, +#[derive(SystemParam)] +pub struct InspectLayer<'w, 's> { + pub drawings: Query<'w, 's, (), With>, + pub floors: Query<'w, 's, (), With>, + pub layer: Query< + 'w, + 's, + ( + Option<&'static LayerVisibility>, + &'static PreferredSemiTransparency, + ), + Or<(With, With)>, + >, + pub icons: Res<'w, Icons>, + pub selection: Res<'w, Selection>, + pub begin_edit_drawing: EventWriter<'w, BeginEditDrawing>, + pub change_layer_visibility: EventWriter<'w, Change>, + pub change_preferred_alpha: EventWriter<'w, Change>, + pub floor_change_rank: EventWriter<'w, ChangeRank>, + pub drawing_change_rank: EventWriter<'w, ChangeRank>, + pub commands: Commands<'w, 's>, + pub selector: SelectorWidget<'w, 's>, } -impl<'a, 'w, 's> InspectLayer<'a, 'w, 's> { - pub fn new( - entity: Entity, - icons: &'a Icons, - events: &'a mut AppEvents<'w, 's>, - layer_vis: Option, - default_alpha: f32, - is_floor: bool, - ) -> Self { +impl<'w, 's> WidgetSystem for InspectLayer<'w, 's> { + fn show( + Inspect { selection: id, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(InspectLayerInput::new(id).with_moving(), ui); + } +} + +impl<'w, 's> WidgetSystem for InspectLayer<'w, 's> { + fn show( + input: InspectLayerInput, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) -> () { + let mut params = state.get_mut(world); + params.show_widget(input, ui); + } +} + +pub struct InspectLayerInput { + pub id: Entity, + pub with_selecting: bool, + pub with_moving: bool, +} + +impl InspectLayerInput { + pub fn new(id: Entity) -> Self { Self { - entity, - icons, - events, - layer_vis, - default_alpha, - is_floor, - as_selected: false, - site_id: None, + id, + with_selecting: false, + with_moving: false, } } - pub fn with_selecting(mut self, site_id: Option) -> Self { - self.site_id = Some(site_id); + + pub fn with_selecting(mut self) -> Self { + self.with_selecting = true; self } - pub fn as_selected(mut self, as_selected: bool) -> Self { - self.as_selected = as_selected; + pub fn with_moving(mut self) -> Self { + self.with_moving = true; self } +} - pub fn show(self, ui: &mut Ui) { - if let Some(site_id) = self.site_id { - SelectionWidget::new(self.entity, site_id, self.icons, self.events) - .as_selected(self.as_selected) - .show(ui); - - if !self.is_floor { - let response = ui - .add(ImageButton::new(self.events.layers.icons.edit.egui())) - .on_hover_text("Edit Drawing"); +impl<'w, 's> InspectLayer<'w, 's> { + pub fn show_widget( + &mut self, + InspectLayerInput { + id, + with_selecting, + with_moving, + }: InspectLayerInput, + ui: &mut Ui, + ) { + if !self.layer.contains(id) { + return; + } - if response.hovered() { - self.events.request.hover.send(Hover(Some(self.entity))); - } + if with_moving { + if self.drawings.contains(id) { + ui.horizontal(|ui| { + MoveLayer::::new(id, &mut self.drawing_change_rank, &self.icons) + .show(ui); + }); + } - if response.clicked() { - self.events - .layers - .begin_edit_drawing - .send(BeginEditDrawing(self.entity)); - } + if self.floors.contains(id) { + ui.horizontal(|ui| { + MoveLayer::::new(id, &mut self.floor_change_rank, &self.icons) + .show(ui); + }); } } - let icon = self.icons.layer_visibility_of(self.layer_vis); - let resp = ui.add(ImageButton::new(icon)).on_hover_text(format!( - "Change to {}", - self.layer_vis.next(self.default_alpha).label() - )); - if resp.hovered() { - self.events.request.hover.send(Hover(Some(self.entity))); - } - if resp.clicked() { - match self.layer_vis.next(self.default_alpha) { - Some(v) => { - self.events - .layers - .layer_vis - .send(Change::new(v, self.entity).or_insert()); + ui.horizontal(|ui| { + if with_selecting { + self.selector.show_widget(id, ui); + } + + if with_selecting { + if self.drawings.contains(id) { + let response = ui + .add(ImageButton::new(self.icons.edit.egui())) + .on_hover_text("Edit Drawing"); + + if response.hovered() { + self.selector.hover.send(Hover(Some(id))); + } + + if response.clicked() { + self.begin_edit_drawing.send(BeginEditDrawing(id)); + } } - None => { - self.events - .commands - .entity(self.entity) - .remove::(); + } + + let Ok((vis, default_alpha)) = self.layer.get(id) else { + return; + }; + let vis = vis.copied(); + let default_alpha = default_alpha.0; + + let icon = self.icons.layer_visibility_of(vis); + let resp = ui + .add(ImageButton::new(icon)) + .on_hover_text(format!("Change to {}", vis.next(default_alpha).label())); + if resp.hovered() { + self.selector.hover.send(Hover(Some(id))); + } + if resp.clicked() { + match vis.next(default_alpha) { + Some(v) => { + self.change_layer_visibility + .send(Change::new(v, id).or_insert()); + } + None => { + self.commands.entity(id).remove::(); + } } } - } - if let Some(LayerVisibility::Alpha(mut alpha)) = self.layer_vis { - if ui - .add( - DragValue::new(&mut alpha) - .clamp_range(0_f32..=1_f32) - .speed(0.01), - ) - .changed() - { - self.events - .layers - .layer_vis - .send(Change::new(LayerVisibility::Alpha(alpha), self.entity)); - self.events - .layers - .preferred_alpha - .send(Change::new(PreferredSemiTransparency(alpha), self.entity)); + if let Some(LayerVisibility::Alpha(mut alpha)) = vis { + if ui + .add( + DragValue::new(&mut alpha) + .clamp_range(0_f32..=1_f32) + .speed(0.01), + ) + .changed() + { + self.change_layer_visibility + .send(Change::new(LayerVisibility::Alpha(alpha), id)); + self.change_preferred_alpha + .send(Change::new(PreferredSemiTransparency(alpha), id)); + } } - } + }); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_lift.rs b/rmf_site_editor/src/widgets/inspector/inspect_lift.rs index 04a4c7ee..eaeae148 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_lift.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_lift.rs @@ -16,48 +16,56 @@ */ use crate::{ - site::{CabinDoorId, LevelElevation, NameInSite, SiteID, ToggleLiftDoorAvailability}, + site::{ + CabinDoorId, Change, CurrentLevel, LevelElevation, NameInSite, ToggleLiftDoorAvailability, + }, widgets::{ - inspector::{InspectOptionF32, SelectionWidget}, - AppEvents, Icons, + inspector::InspectOptionF32, prelude::*, Inspect, InspectionPlugin, LevelDisplay, + SelectorWidget, }, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{CollapsingHeader, DragValue, Ui}; use rmf_site_format::lift::*; -#[derive(SystemParam)] -pub struct InspectLiftParams<'w, 's> { - pub cabins: Query<'w, 's, (&'static LiftCabin, &'static RecallLiftCabin)>, - pub doors: Query<'w, 's, &'static LevelVisits>, - pub levels: Query<'w, 's, (&'static NameInSite, &'static LevelElevation)>, - pub icons: Res<'w, Icons>, - pub site_id: Query<'w, 's, &'static SiteID>, +#[derive(Default)] +pub struct InspectLiftPlugin {} + +impl Plugin for InspectLiftPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(InspectionPlugin::::new()); + } } -pub struct InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { - pub lift: Entity, - pub params: &'a InspectLiftParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, +#[derive(SystemParam)] +pub struct InspectLiftCabin<'w, 's> { + cabins: Query<'w, 's, (&'static LiftCabin, &'static RecallLiftCabin)>, + doors: Query<'w, 's, &'static LevelVisits>, + levels: Query<'w, 's, (&'static NameInSite, &'static LevelElevation)>, + display_level: Res<'w, LevelDisplay>, + change_lift_cabin: EventWriter<'w, Change>>, + selector: SelectorWidget<'w, 's>, + toggle_door_levels: EventWriter<'w, ToggleLiftDoorAvailability>, + current_level: Res<'w, CurrentLevel>, } -impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { - pub fn new( - lift: Entity, - params: &'a InspectLiftParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - lift, - params, - events, - } +impl<'w, 's> WidgetSystem for InspectLiftCabin<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} - pub fn show(mut self, ui: &mut Ui) -> Option> { - let (cabin, recall) = match self.params.cabins.get(self.lift) { - Ok(r) => r, - Err(_) => return None, +impl<'w, 's> InspectLiftCabin<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((cabin, recall)) = self.cabins.get(id) else { + return; }; let mut new_cabin = cabin.clone(); match &mut new_cabin { @@ -85,14 +93,14 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { }); if let Some(new_t) = InspectOptionF32::new( - "Wall Thickness".to_string(), + "Wall Thickness", params.wall_thickness, recall .wall_thickness .unwrap_or(DEFAULT_CABIN_WALL_THICKNESS), ) .clamp_range(0.001..=std::f32::INFINITY) - .suffix("m".to_string()) + .suffix("m") .min_decimals(2) .max_decimals(4) .speed(0.001) @@ -102,12 +110,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { } if let Some(new_gap) = InspectOptionF32::new( - "Gap".to_string(), + "Gap", params.gap, recall.gap.unwrap_or(DEFAULT_CABIN_GAP), ) .clamp_range(0.001..=std::f32::INFINITY) - .suffix("m".to_string()) + .suffix("m") .min_decimals(2) .max_decimals(4) .speed(0.001) @@ -116,16 +124,13 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { params.gap = new_gap; } - if let Some(new_shift) = InspectOptionF32::new( - "Shift".to_string(), - params.shift, - recall.shift.unwrap_or(0.0), - ) - .suffix("m".to_string()) - .min_decimals(2) - .max_decimals(4) - .speed(0.001) - .show(ui) + if let Some(new_shift) = + InspectOptionF32::new("Shift", params.shift, recall.shift.unwrap_or(0.0)) + .suffix("m") + .min_decimals(2) + .max_decimals(4) + .speed(0.001) + .show(ui) { params.shift = new_shift; } @@ -137,14 +142,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { CollapsingHeader::new(format!("{} Door", face.label())) .default_open(false) .show(ui, |ui| { - SelectionWidget::new( - placement.door, - self.params.site_id.get(placement.door).copied().ok(), - &self.params.icons, - &mut self.events, - ) - .show(ui); - + self.selector.show_widget(placement.door, ui); ui.horizontal(|ui| { ui.label("width"); ui.add( @@ -157,26 +155,23 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { ); }); - if let Some(new_shift) = InspectOptionF32::new( - "Shifted".to_string(), - placement.shifted, - 0.0, - ) - .suffix("m".to_string()) - .min_decimals(2) - .max_decimals(4) - .speed(0.005) - .show(ui) + if let Some(new_shift) = + InspectOptionF32::new("Shifted", placement.shifted, 0.0) + .suffix("m") + .min_decimals(2) + .max_decimals(4) + .speed(0.005) + .show(ui) { placement.shifted = new_shift; } if let Some(new_gap) = InspectOptionF32::new( - "Custom Gap".to_string(), + "Custom Gap", placement.custom_gap, cabin_gap, ) - .suffix("m".to_string()) + .suffix("m") .clamp_range(0.0..=std::f32::INFINITY) .min_decimals(2) .max_decimals(4) @@ -187,11 +182,11 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { } if let Some(new_t) = InspectOptionF32::new( - "Thickness".to_string(), + "Thickness", placement.thickness, DEFAULT_CABIN_DOOR_THICKNESS, ) - .suffix("m".to_string()) + .suffix("m") .clamp_range(0.001..=std::f32::INFINITY) .min_decimals(2) .max_decimals(4) @@ -201,26 +196,25 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { placement.thickness = new_t; } - if let Ok(visits) = self.params.doors.get(placement.door) { + if let Ok(visits) = self.doors.get(placement.door) { CollapsingHeader::new(format!("Level Access")) .default_open(true) .show(ui, |ui| { - for level in &self.events.display.level.order { + for level in &self.display_level.order { let mut visits_level = visits.contains(level); if ui .checkbox( &mut visits_level, - self.params - .levels + self.levels .get(*level) .map(|(n, _)| &n.0) .unwrap_or(&"".to_owned()), ) .changed() { - self.events.request.toggle_door_levels.send( + self.toggle_door_levels.send( ToggleLiftDoorAvailability { - for_lift: self.lift, + for_lift: id, on_level: *level, cabin_door: CabinDoorId::RectFace(face), door_available: visits_level, @@ -231,16 +225,14 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { }); } }); - } else if let Some(current_level) = **self.events.request.current_level { + } else if let Some(current_level) = **self.current_level { if ui.button(format!("Add {} Door", face.label())).clicked() { - self.events.request.toggle_door_levels.send( - ToggleLiftDoorAvailability { - for_lift: self.lift, - on_level: current_level, - cabin_door: CabinDoorId::RectFace(face), - door_available: true, - }, - ); + self.toggle_door_levels.send(ToggleLiftDoorAvailability { + for_lift: id, + on_level: current_level, + cabin_door: CabinDoorId::RectFace(face), + door_available: true, + }); } } } @@ -261,9 +253,8 @@ impl<'a, 'w1, 's1, 'w2, 's2> InspectLiftCabin<'a, 'w1, 's1, 'w2, 's2> { } } - return Some(new_cabin); + self.change_lift_cabin.send(Change::new(new_cabin, id)); } - - return None; + ui.add_space(10.0); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_light.rs b/rmf_site_editor/src/widgets/inspector/inspect_light.rs index a3348bad..58769d12 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_light.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_light.rs @@ -15,12 +15,41 @@ * */ -use crate::site::{LightKind, RecallLightKind}; +use crate::{ + site::{Change, LightKind, RecallLightKind}, + widgets::{prelude::*, Inspect}, +}; +use bevy::prelude::*; use bevy_egui::egui::{ color_picker::{color_edit_button_rgba, Alpha}, ComboBox, DragValue, Rgba, Ui, }; +#[derive(SystemParam)] +pub struct InspectLight<'w, 's> { + lights: Query<'w, 's, (&'static LightKind, &'static RecallLightKind)>, + change_light: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectLight<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok((light, recall)) = params.lights.get(selection) else { + return; + }; + + if let Some(new_light) = InspectLightKind::new(light, recall).show(ui) { + params.change_light.send(Change::new(new_light, selection)); + } + ui.add_space(10.0); + } +} + pub struct InspectLightKind<'a> { pub kind: &'a LightKind, pub recall: &'a RecallLightKind, diff --git a/rmf_site_editor/src/widgets/inspector/inspect_location.rs b/rmf_site_editor/src/widgets/inspector/inspect_location.rs index aa21a3eb..177260c9 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_location.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_location.rs @@ -16,48 +16,42 @@ */ use crate::{ - site::{ - ConsiderLocationTag, DefaultFile, LocationTag, LocationTags, Model, RecallAssetSource, - RecallLocationTags, - }, - widgets::{ - inspector::{InspectAssetSource, InspectName}, - AppEvents, Icons, - }, + site::{Change, ConsiderLocationTag, LocationTag, LocationTags, RecallLocationTags}, + widgets::{prelude::*, Icons, Inspect}, }; use bevy::prelude::*; use bevy_egui::egui::{ComboBox, ImageButton, RichText, Ui}; use smallvec::SmallVec; -pub struct InspectLocationWidget<'a, 'w1, 'w2, 's2> { - pub entity: Entity, - pub tags: &'a LocationTags, - pub recall: &'a RecallLocationTags, - pub icons: &'a Res<'w1, Icons>, - pub events: &'a mut AppEvents<'w2, 's2>, +#[derive(SystemParam)] +pub struct InspectLocation<'w, 's> { + location_tags: Query<'w, 's, (&'static LocationTags, &'static RecallLocationTags)>, + icons: Res<'w, Icons>, + consider_tag: EventWriter<'w, ConsiderLocationTag>, + change_tags: EventWriter<'w, Change>, } -impl<'a, 'w1, 'w2, 's2> InspectLocationWidget<'a, 'w1, 'w2, 's2> { - pub fn new( - entity: Entity, - tags: &'a LocationTags, - recall: &'a RecallLocationTags, - icons: &'a Res<'w1, Icons>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - tags, - recall, - icons, - events, - } +impl<'w, 's> WidgetSystem for InspectLocation<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} + +impl<'w, 's> InspectLocation<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((tags, recall)) = self.location_tags.get(id) else { + return; + }; - pub fn show(self, ui: &mut Ui) -> Option { ui.label(RichText::new("Location Tags").size(18.0)); let mut deleted_tag = None; - for (i, tag) in self.tags.0.iter().enumerate() { + for (i, tag) in tags.0.iter().enumerate() { ui.horizontal(|ui| { if ui.add(ImageButton::new(self.icons.trash.egui())).clicked() { deleted_tag = Some(i); @@ -74,19 +68,19 @@ impl<'a, 'w1, 'w2, 's2> InspectLocationWidget<'a, 'w1, 'w2, 's2> { let (add, consider) = ui .horizontal(|ui| { let add = ui.button("Confirm").clicked(); - let mut consider = self.recall.assume_tag(self.tags); + let mut consider = recall.assume_tag(tags); let mut variants: SmallVec<[LocationTag; 5]> = SmallVec::new(); - if self.tags.iter().find(|t| t.is_charger()).is_none() { + if tags.iter().find(|t| t.is_charger()).is_none() { variants.push(LocationTag::Charger); } - if self.tags.iter().find(|t| t.is_parking_spot()).is_none() { + if tags.iter().find(|t| t.is_parking_spot()).is_none() { variants.push(LocationTag::ParkingSpot); } - if self.tags.iter().find(|t| t.is_holding_point()).is_none() { + if tags.iter().find(|t| t.is_holding_point()).is_none() { variants.push(LocationTag::HoldingPoint); } - variants.push(self.recall.assume_spawn_robot()); - variants.push(self.recall.assume_workcell()); + variants.push(recall.assume_spawn_robot()); + variants.push(recall.assume_workcell()); ComboBox::from_id_source("Add Location Tag") .selected_text(consider.label()) @@ -101,19 +95,14 @@ impl<'a, 'w1, 'w2, 's2> InspectLocationWidget<'a, 'w1, 'w2, 's2> { }) .inner; - let consider_changed = if let Some(original) = &self.recall.consider_tag { + let consider_changed = if let Some(original) = &recall.consider_tag { consider != *original } else { true }; if consider_changed { - self.events - .request - .consider_tag - .send(ConsiderLocationTag::new( - Some(consider.clone()), - self.entity, - )); + self.consider_tag + .send(ConsiderLocationTag::new(Some(consider.clone()), id)); } if add { @@ -126,7 +115,7 @@ impl<'a, 'w1, 'w2, 's2> InspectLocationWidget<'a, 'w1, 'w2, 's2> { .flatten(); if deleted_tag.is_some() || added_tag.is_some() { - let mut new_tags = self.tags.clone(); + let mut new_tags = tags.clone(); if let Some(i) = deleted_tag { new_tags.remove(i); } @@ -135,36 +124,7 @@ impl<'a, 'w1, 'w2, 's2> InspectLocationWidget<'a, 'w1, 'w2, 's2> { new_tags.push(new_tag); } - Some(new_tags) - } else { - None - } - } - - #[allow(dead_code)] - fn inspect_model( - &self, - ui: &mut Ui, - model: &Model, - recall_asset: &RecallAssetSource, - default_file: Option<&'a DefaultFile>, - ) -> Option { - let new_name = InspectName::new(&model.name).show(ui); - let new_source = - InspectAssetSource::new(&model.source, &recall_asset, default_file).show(ui); - - if new_name.is_some() || new_source.is_some() { - let mut new_model = model.clone(); - if let Some(new_name) = new_name { - new_model.name = new_name; - } - if let Some(new_source) = new_source { - new_model.source = new_source; - } - - Some(new_model) - } else { - None + self.change_tags.send(Change::new(new_tags, id)); } } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_measurement.rs b/rmf_site_editor/src/widgets/inspector/inspect_measurement.rs new file mode 100644 index 00000000..be1b7aea --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_measurement.rs @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + site::{Change, Distance}, + widgets::{prelude::*, Inspect, InspectOptionF32}, +}; +use bevy::prelude::*; + +#[derive(SystemParam)] +pub struct InspectMeasurement<'w, 's> { + distances: Query<'w, 's, &'static Distance>, + change_distance: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectMeasurement<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok(distance) = params.distances.get(selection) else { + return; + }; + + if let Some(new_distance) = InspectOptionF32::new("Distance", distance.0, 10.0) + .clamp_range(0.0..=10000.0) + .min_decimals(2) + .max_decimals(2) + .speed(0.01) + .suffix(" m") + .show(ui) + { + params + .change_distance + .send(Change::new(Distance(new_distance), selection)); + } + ui.add_space(10.0); + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_mesh_constraint.rs b/rmf_site_editor/src/widgets/inspector/inspect_mesh_constraint.rs index a590310a..9197373f 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_mesh_constraint.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_mesh_constraint.rs @@ -15,84 +15,64 @@ * */ -use bevy_egui::egui::Ui; +use crate::widgets::{prelude::*, Inspect, SelectorWidget}; +use bevy::prelude::*; use rmf_site_format::*; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet}; -use crate::{ - site::{Category, SiteID}, - widgets::{inspector::SelectionWidget, AppEvents, Icons}, -}; -use bevy::{ecs::system::SystemParam, prelude::*}; +use crate::site::Category; #[derive(SystemParam)] -pub struct InspectModelDependentsParams<'w, 's> { - pub dependents: Query<'w, 's, &'static ConstraintDependents, With>, - pub info: Query<'w, 's, (&'static Category, Option<&'static SiteID>)>, - pub icons: Res<'w, Icons>, +pub struct InspectModelDependents<'w, 's> { + dependents: Query<'w, 's, &'static ConstraintDependents, With>, + categories: Query<'w, 's, &'static Category>, + selector: SelectorWidget<'w, 's>, } -pub struct InspectModelDependentsWidget<'a, 'w1, 'w2, 's1, 's2> { - pub model: Entity, - pub params: &'a InspectModelDependentsParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, +impl<'w, 's> WidgetSystem for InspectModelDependents<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); + } } -impl<'a, 'w1, 'w2, 's1, 's2> InspectModelDependentsWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - model: Entity, - params: &'a InspectModelDependentsParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - model, - params, - events, - } - } +impl<'w, 's> InspectModelDependents<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok(dependents) = self.dependents.get(id) else { + return; + }; - fn show_dependents( - dependents: &HashSet, - params: &InspectModelDependentsParams<'w1, 's1>, - events: &mut AppEvents<'w2, 's2>, - ui: &mut Ui, - ) { - ui.heading("Constraint Dependents"); - let mut category_map: BTreeMap>> = BTreeMap::new(); - for e in dependents { - if let Ok((category, site_id)) = params.info.get(*e) { - category_map - .entry(*category) - .or_default() - .insert(*e, site_id.map(|s| s.0)); + ui.vertical(|ui| { + if dependents.0.is_empty() { + ui.label("No dependents"); } else { - ui.label(format!("ERROR: Broken reference to entity {e:?}")); - } - } - - for (category, entities) in &category_map { - ui.label(category.label()); + ui.heading("Constraint Dependents"); + let mut category_map: BTreeMap> = BTreeMap::new(); + for e in &dependents.0 { + if let Ok(category) = self.categories.get(*e) { + category_map.entry(*category).or_default().insert(*e); + } else { + ui.label(format!("ERROR: Broken reference to entity {e:?}")); + } + } - for (e, site_id) in entities { - ui.horizontal(|ui| { - SelectionWidget::new(*e, site_id.map(SiteID), params.icons.as_ref(), events) - .show(ui); - }); - } - } - } + for (category, entities) in &category_map { + ui.label(category.label()); - pub fn show(mut self, ui: &mut Ui) { - ui.vertical(|ui| { - if let Ok(dependents) = self.params.dependents.get(self.model) { - if dependents.0.is_empty() { - ui.label("No dependents"); - } else { - Self::show_dependents(&dependents.0, &self.params, &mut self.events, ui); + for e in entities { + ui.horizontal(|ui| { + self.selector.show_widget(*e, ui); + }); + } } - } else { - ui.label("ERROR: Unable to find dependents info for this model"); } }); + + ui.add_space(10.0); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_motion.rs b/rmf_site_editor/src/widgets/inspector/inspect_motion.rs index b5810410..73dc1453 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_motion.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_motion.rs @@ -15,18 +15,138 @@ * */ -use crate::widgets::inspector::{InspectAngle, InspectOptionF32}; +use crate::{ + site::Change, + widgets::{ + inspector::{InspectAngle, InspectOptionF32}, + prelude::*, + Inspect, + }, +}; +use bevy::prelude::*; use bevy_egui::egui::{ComboBox, RichText, Ui}; use rmf_site_format::{ Angle, Dock, Motion, OrientationConstraint, RecallMotion, RecallReverseLane, ReverseLane, }; -pub struct InspectMotionWidget<'a> { +#[derive(SystemParam)] +pub struct InspectMotion<'w, 's> { + forward: InspectForwardMotion<'w, 's>, +} + +impl<'w, 's> WidgetSystem for InspectMotion<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.forward.show_widget(selection, ui); + } +} + +#[derive(SystemParam)] +pub struct InspectForwardMotion<'w, 's> { + motions: Query<'w, 's, (&'static Motion, &'static RecallMotion)>, + change_lane_motion: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectForwardMotion<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); + } +} + +impl<'w, 's> InspectForwardMotion<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((motion, recall)) = self.motions.get(id) else { + return; + }; + + if let Some(new_motion) = InspectMotionComponent::new(motion, recall).show(ui) { + self.change_lane_motion.send(Change::new(new_motion, id)); + } + ui.add_space(10.0); + } +} + +#[derive(SystemParam)] +pub struct InspectReverseMotion<'w, 's> { + reverse_motions: Query<'w, 's, (&'static ReverseLane, &'static RecallReverseLane)>, + change_lane_reverse: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectReverseMotion<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); + } +} + +impl<'w, 's> InspectReverseMotion<'w, 's> { + fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((reverse, recall)) = self.reverse_motions.get(id) else { + return; + }; + + let assumed_motion = reverse + .different_motion() + .cloned() + .unwrap_or(recall.motion.clone().unwrap_or(Motion::default())); + + let mut new_reverse = reverse.clone(); + ui.label(RichText::new("Reverse Motion").size(18.0)); + ComboBox::from_id_source("Reverse Lane") + .selected_text(new_reverse.label()) + .show_ui(ui, |ui| { + for variant in &[ + ReverseLane::Same, + ReverseLane::Disable, + ReverseLane::Different(assumed_motion), + ] { + ui.selectable_value(&mut new_reverse, variant.clone(), variant.label()); + } + }); + + match &mut new_reverse { + ReverseLane::Different(motion) => { + ui.add_space(10.0); + if let Some(new_motion) = + InspectMotionComponent::new(motion, &recall.previous).show(ui) + { + new_reverse = ReverseLane::Different(new_motion); + } + } + _ => { + // Do nothing + } + } + + if new_reverse != *reverse { + self.change_lane_reverse.send(Change::new(new_reverse, id)); + } + ui.add_space(10.0); + } +} + +pub struct InspectMotionComponent<'a> { pub motion: &'a Motion, pub recall: &'a RecallMotion, } -impl<'a> InspectMotionWidget<'a> { +impl<'a> InspectMotionComponent<'a> { pub fn new(motion: &'a Motion, recall: &'a RecallMotion) -> Self { Self { motion, recall } } @@ -84,7 +204,7 @@ impl<'a> InspectMotionWidget<'a> { ui.add_space(10.0); let new_speed = InspectOptionF32::new( - "Speed Limit".to_string(), + "Speed Limit", self.motion.speed_limit, self.recall.speed_limit.unwrap_or(1.0), ) @@ -92,7 +212,7 @@ impl<'a> InspectMotionWidget<'a> { .min_decimals(2) .max_decimals(2) .speed(0.01) - .suffix(" m/s".to_string()) + .suffix(" m/s") .show(ui); ui.add_space(10.0); @@ -120,7 +240,7 @@ impl<'a> InspectMotionWidget<'a> { }); let new_duration = InspectOptionF32::new( - "Duration".to_string(), + "Duration", dock.duration, self.recall.dock_duration.unwrap_or(30.0), ) @@ -128,8 +248,8 @@ impl<'a> InspectMotionWidget<'a> { .min_decimals(0) .max_decimals(1) .speed(1.0) - .suffix(" s".to_string()) - .tooltip("How long does the docking take?".to_string()) + .suffix(" s") + .tooltip("How long does the docking take?") .show(ui); if let Some(new_duration) = new_duration { @@ -169,59 +289,3 @@ impl<'a> InspectMotionWidget<'a> { return None; } } - -pub struct InspectReverseWidget<'a> { - pub reverse: &'a ReverseLane, - pub recall: &'a RecallReverseLane, -} - -impl<'a> InspectReverseWidget<'a> { - pub fn new(reverse: &'a ReverseLane, previous: &'a RecallReverseLane) -> Self { - Self { - reverse, - recall: previous, - } - } - - pub fn show(self, ui: &mut Ui) -> Option { - let assumed_motion = self - .reverse - .different_motion() - .cloned() - .unwrap_or(self.recall.motion.clone().unwrap_or(Motion::default())); - - let mut new_reverse = self.reverse.clone(); - ui.label(RichText::new("Reverse Motion").size(18.0)); - ComboBox::from_id_source("Reverse Lane") - .selected_text(new_reverse.label()) - .show_ui(ui, |ui| { - for variant in &[ - ReverseLane::Same, - ReverseLane::Disable, - ReverseLane::Different(assumed_motion), - ] { - ui.selectable_value(&mut new_reverse, variant.clone(), variant.label()); - } - }); - - match &mut new_reverse { - ReverseLane::Different(motion) => { - ui.add_space(10.0); - if let Some(new_motion) = - InspectMotionWidget::new(motion, &self.recall.previous).show(ui) - { - new_reverse = ReverseLane::Different(new_motion); - } - } - _ => { - // Do nothing - } - } - - if new_reverse != *self.reverse { - Some(new_reverse) - } else { - None - } - } -} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_name.rs b/rmf_site_editor/src/widgets/inspector/inspect_name.rs index 9ce0457a..3b5af089 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_name.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_name.rs @@ -15,78 +15,71 @@ * */ +use crate::{ + site::Change, + widgets::{prelude::*, Inspect}, +}; +use bevy::prelude::*; use bevy_egui::egui::Ui; use rmf_site_format::{NameInSite, NameInWorkcell, NameOfWorkcell}; -// TODO(luca) refactor all these into a generic name inspection widget -pub struct InspectName<'a> { - pub name: &'a NameInSite, +#[derive(SystemParam)] +pub struct InspectName<'w, 's> { + names_in_site: Query<'w, 's, &'static NameInSite>, + change_name_in_site: EventWriter<'w, Change>, + names_in_workcell: Query<'w, 's, &'static NameInWorkcell>, + change_name_in_workcell: EventWriter<'w, Change>, + names_of_workcells: Query<'w, 's, &'static NameOfWorkcell>, + change_name_of_workcell: EventWriter<'w, Change>, } -impl<'a> InspectName<'a> { - pub fn new(name: &'a NameInSite) -> Self { - Self { name } - } +impl<'w, 's> ShareableWidget for InspectName<'w, 's> {} - pub fn show(self, ui: &mut Ui) -> Option { - ui.horizontal(|ui| { - ui.label("Name"); - let mut new_name = self.name.clone(); - ui.text_edit_singleline(&mut new_name.0); - if new_name != *self.name { - Some(new_name) - } else { - None +impl<'w, 's> WidgetSystem for InspectName<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + if let Ok(name) = params.names_in_site.get(selection) { + let mut new_name = name.clone(); + ui.horizontal(|ui| { + ui.label("Name"); + ui.text_edit_singleline(&mut new_name.0); + }); + if new_name != *name { + params + .change_name_in_site + .send(Change::new(new_name, selection)); } - }) - .inner - } -} - -pub struct InspectNameInWorkcell<'a> { - pub name: &'a NameInWorkcell, -} - -impl<'a> InspectNameInWorkcell<'a> { - pub fn new(name: &'a NameInWorkcell) -> Self { - Self { name } - } + } - pub fn show(self, ui: &mut Ui) -> Option { - ui.horizontal(|ui| { - ui.label("Name"); - let mut new_name = self.name.clone(); - ui.text_edit_singleline(&mut new_name.0); - if new_name != *self.name { - Some(new_name) - } else { - None + if let Ok(name) = params.names_in_workcell.get(selection) { + let mut new_name = name.clone(); + ui.horizontal(|ui| { + ui.label("Name"); + ui.text_edit_singleline(&mut new_name.0); + }); + if new_name != *name { + params + .change_name_in_workcell + .send(Change::new(new_name, selection)); } - }) - .inner - } -} - -pub struct InspectNameOfWorkcell<'a> { - pub name: &'a NameOfWorkcell, -} - -impl<'a> InspectNameOfWorkcell<'a> { - pub fn new(name: &'a NameOfWorkcell) -> Self { - Self { name } - } + } - pub fn show(self, ui: &mut Ui) -> Option { - ui.horizontal(|ui| { - ui.label("Name"); - let mut new_name = self.name.clone(); - ui.text_edit_singleline(&mut new_name.0); - if new_name != *self.name { - Some(new_name) - } else { - None + if let Ok(name) = params.names_of_workcells.get(selection) { + let mut new_name = name.clone(); + ui.horizontal(|ui| { + ui.label("Name of workcell"); + ui.text_edit_singleline(&mut new_name.0); + }); + if new_name != *name { + params + .change_name_of_workcell + .send(Change::new(new_name, selection)); } - }) - .inner + } } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_option_f32.rs b/rmf_site_editor/src/widgets/inspector/inspect_option_f32.rs index 9582cca3..d67c0264 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_option_f32.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_option_f32.rs @@ -18,20 +18,20 @@ use bevy_egui::egui::{DragValue, Ui}; use std::ops::RangeInclusive; -pub struct InspectOptionF32 { - title: String, +pub struct InspectOptionF32<'a> { + title: &'a str, current_value: Option, assumed_value: f32, range: RangeInclusive, min_decimals: usize, max_decimals: Option, speed: f64, - suffix: String, - tooltip: Option, + suffix: &'a str, + tooltip: Option<&'a str>, } -impl InspectOptionF32 { - pub fn new(title: String, current_value: Option, assumed_value: f32) -> Self { +impl<'a> InspectOptionF32<'a> { + pub fn new(title: &'a str, current_value: Option, assumed_value: f32) -> Self { Self { title, current_value, @@ -65,12 +65,12 @@ impl InspectOptionF32 { self } - pub fn suffix(mut self, suffix: String) -> Self { + pub fn suffix(mut self, suffix: &'a str) -> Self { self.suffix = suffix; self } - pub fn tooltip(mut self, tooltip: String) -> Self { + pub fn tooltip(mut self, tooltip: &'a str) -> Self { self.tooltip = Some(tooltip); self } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_physical_camera_properties.rs b/rmf_site_editor/src/widgets/inspector/inspect_physical_camera_properties.rs index e6992e40..3100e1ad 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_physical_camera_properties.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_physical_camera_properties.rs @@ -15,46 +15,56 @@ * */ -use crate::inspector::{InspectAngle, InspectValue}; -use crate::widgets::egui::RichText; +use crate::{ + inspector::{Inspect, InspectAngle, InspectValue}, + site::Change, + widgets::{egui::RichText, prelude::*}, +}; +use bevy::prelude::*; use bevy_egui::egui::{Grid, Ui}; use rmf_site_format::PhysicalCameraProperties; -pub struct InspectPhysicalCameraProperties<'a> { - pub properties: &'a PhysicalCameraProperties, +#[derive(SystemParam)] +pub struct InspectPhysicalCameraProperties<'w, 's> { + physical_camera_properties: Query<'w, 's, &'static PhysicalCameraProperties>, + change_physical_camera_properties: EventWriter<'w, Change>, } -impl<'a> InspectPhysicalCameraProperties<'a> { - pub fn new(properties: &'a PhysicalCameraProperties) -> Self { - Self { properties } - } +impl<'w, 's> WidgetSystem for InspectPhysicalCameraProperties<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok(properties) = params.physical_camera_properties.get(selection) else { + return; + }; - pub fn show(self, ui: &mut Ui) -> Option { - let mut new_properties = self.properties.clone(); + let mut new_properties = properties.clone(); ui.label(RichText::new("Camera Properties").size(18.0)); Grid::new("physical_camera_properties").show(ui, |ui| { - if let Some(new_width) = - InspectValue::::new(String::from("Width"), new_properties.width) - .clamp_range(1..=std::u32::MAX) - .tooltip("Image width in pixels".to_string()) - .show(ui) + if let Some(new_width) = InspectValue::::new("Width", new_properties.width) + .clamp_range(1..=std::u32::MAX) + .tooltip("Image width in pixels") + .show(ui) { new_properties.width = new_width; } ui.end_row(); - if let Some(new_height) = - InspectValue::::new(String::from("Height"), new_properties.height) - .clamp_range(1..=std::u32::MAX) - .tooltip("Image height in pixels".to_string()) - .show(ui) + if let Some(new_height) = InspectValue::::new("Height", new_properties.height) + .clamp_range(1..=std::u32::MAX) + .tooltip("Image height in pixels") + .show(ui) { new_properties.height = new_height; } ui.end_row(); if let Some(new_frame_rate) = - InspectValue::::new(String::from("Frame rate"), new_properties.frame_rate) + InspectValue::::new("Frame rate", new_properties.frame_rate) .clamp_range(0.0..=std::f32::MAX) - .tooltip("Frame rate in images per second".to_string()) + .tooltip("Frame rate in images per second") .show(ui) { new_properties.frame_rate = new_frame_rate; @@ -68,14 +78,15 @@ impl<'a> InspectPhysicalCameraProperties<'a> { .range_degrees(0.0..=180.0) .show(ui); }); - if new_properties.width != self.properties.width - || new_properties.height != self.properties.height - || new_properties.horizontal_fov != self.properties.horizontal_fov - || new_properties.frame_rate != self.properties.frame_rate + if new_properties.width != properties.width + || new_properties.height != properties.height + || new_properties.horizontal_fov != properties.horizontal_fov + || new_properties.frame_rate != properties.frame_rate { - Some(new_properties) - } else { - None + params + .change_physical_camera_properties + .send(Change::new(new_properties, selection)); } + ui.add_space(10.0); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_pose.rs b/rmf_site_editor/src/widgets/inspector/inspect_pose.rs index 36f5b77e..86491c7b 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_pose.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_pose.rs @@ -15,17 +15,44 @@ * */ -use crate::widgets::inspector::InspectAngle; -use bevy::math::Quat; +use crate::{ + site::Change, + widgets::{inspector::InspectAngle, prelude::*, Inspect}, +}; +use bevy::{math::Quat, prelude::*}; use bevy_egui::egui::{ComboBox, DragValue, Grid, Ui}; use rmf_site_format::{Pose, Rotation}; -pub struct InspectPose<'a> { +#[derive(SystemParam)] +pub struct InspectPose<'w, 's> { + poses: Query<'w, 's, &'static Pose>, + change_pose: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectPose<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok(pose) = params.poses.get(selection) else { + return; + }; + if let Some(new_pose) = InspectPoseComponent::new(pose).show(ui) { + params.change_pose.send(Change::new(new_pose, selection)); + } + ui.add_space(10.0); + } +} + +pub struct InspectPoseComponent<'a> { pub pose: &'a Pose, pub for_rotation: &'a bool, } -impl<'a> InspectPose<'a> { +impl<'a> InspectPoseComponent<'a> { pub fn new(pose: &'a Pose) -> Self { Self { pose, diff --git a/rmf_site_editor/src/widgets/inspector/inspect_preview.rs b/rmf_site_editor/src/widgets/inspector/inspect_preview.rs new file mode 100644 index 00000000..dddee4cf --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_preview.rs @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + interaction::SpawnPreview, + site::PreviewableMarker, + widgets::{prelude::*, Inspect}, +}; +use bevy::prelude::*; + +#[derive(SystemParam)] +pub struct InspectPreview<'w, 's> { + previewable: Query<'w, 's, &'static PreviewableMarker>, + spawn_preview: EventWriter<'w, SpawnPreview>, +} + +impl<'w, 's> WidgetSystem for InspectPreview<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + if params.previewable.contains(selection) { + if ui.button("Preview").clicked() { + params + .spawn_preview + .send(SpawnPreview::new(Some(selection))); + } + ui.add_space(10.0); + } + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_primitive_shape.rs b/rmf_site_editor/src/widgets/inspector/inspect_primitive_shape.rs index 919578ff..a5500d8e 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_primitive_shape.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_primitive_shape.rs @@ -15,15 +15,46 @@ * */ -use bevy_egui::egui::Ui; +use crate::{ + site::Change, + widgets::{prelude::*, Inspect}, +}; +use bevy::prelude::*; use rmf_site_format::{PrimitiveShape, RecallPrimitiveShape}; -pub struct InspectPrimitiveShape<'a> { +#[derive(SystemParam)] +pub struct InspectPrimitiveShape<'w, 's> { + primitive_shapes: Query<'w, 's, (&'static PrimitiveShape, &'static RecallPrimitiveShape)>, + change_primitive_shape: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectPrimitiveShape<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok((shape, recall)) = params.primitive_shapes.get(selection) else { + return; + }; + + if let Some(new_shape) = InspectPrimitiveShapeComponent::new(shape, recall).show(ui) { + params + .change_primitive_shape + .send(Change::new(new_shape, selection)); + } + ui.add_space(10.0); + } +} + +pub struct InspectPrimitiveShapeComponent<'a> { pub primitive: &'a PrimitiveShape, pub recall: &'a RecallPrimitiveShape, } -impl<'a> InspectPrimitiveShape<'a> { +impl<'a> InspectPrimitiveShapeComponent<'a> { pub fn new(primitive: &'a PrimitiveShape, recall: &'a RecallPrimitiveShape) -> Self { Self { primitive, recall } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_scale.rs b/rmf_site_editor/src/widgets/inspector/inspect_scale.rs index 46d11c79..173dd319 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_scale.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_scale.rs @@ -15,14 +15,44 @@ * */ +use crate::{ + site::Change, + widgets::{prelude::*, Inspect}, +}; +use bevy::prelude::*; use bevy_egui::egui::{DragValue, Grid, Ui}; use rmf_site_format::Scale; -pub struct InspectScale<'a> { +#[derive(SystemParam)] +pub struct InspectScale<'w, 's> { + scales: Query<'w, 's, &'static Scale>, + change_scale: EventWriter<'w, Change>, +} + +impl<'w, 's> WidgetSystem for InspectScale<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Ok(scale) = params.scales.get(selection) else { + return; + }; + + if let Some(new_scale) = InspectScaleComponent::new(scale).show(ui) { + params.change_scale.send(Change::new(new_scale, selection)); + } + ui.add_space(10.0); + } +} + +pub struct InspectScaleComponent<'a> { pub scale: &'a Scale, } -impl<'a> InspectScale<'a> { +impl<'a> InspectScaleComponent<'a> { pub fn new(scale: &'a Scale) -> Self { Self { scale } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index ab87e3d5..9bf6dbce 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -16,9 +16,10 @@ */ use crate::{ - inspector::{InspectAssetSource, InspectValue, SearchResult}, + inspector::{InspectAssetSourceComponent, InspectValue, SearchResult}, site::{Category, Change, DefaultFile}, - AppEvents, Icons, WorkspaceMarker, + widgets::{prelude::*, Inspect, InspectionPlugin}, + Icons, WorkspaceMarker, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{ComboBox, Grid, ImageButton, Ui}; @@ -27,11 +28,18 @@ use rmf_site_format::{ WallMarker, }; -#[derive(Resource, Default)] -pub struct SearchForTexture(pub String); +#[derive(Default)] +pub struct InspectTexturePlugin {} + +impl Plugin for InspectTexturePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(InspectionPlugin::::new()); + } +} #[derive(SystemParam)] -pub struct InspectTextureAffiliationParams<'w, 's> { +pub struct InspectTextureAffiliation<'w, 's> { with_texture: Query< 'w, 's, @@ -42,38 +50,35 @@ pub struct InspectTextureAffiliationParams<'w, 's> { parents: Query<'w, 's, &'static Parent>, sites: Query<'w, 's, &'static Children, With>, icons: Res<'w, Icons>, + search_for_texture: ResMut<'w, SearchForTexture>, + commands: Commands<'w, 's>, + change_affiliation: EventWriter<'w, Change>>, } -pub struct InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { - entity: Entity, - params: &'a InspectTextureAffiliationParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - entity: Entity, - params: &'a InspectTextureAffiliationParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - params, - events, - } +impl<'w, 's> WidgetSystem for InspectTextureAffiliation<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} - pub fn show(self, ui: &mut Ui) { - let Ok((category, affiliation)) = self.params.with_texture.get(self.entity) else { +impl<'w, 's> InspectTextureAffiliation<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { + let Ok((category, affiliation)) = self.with_texture.get(id) else { return; }; - let mut site = self.entity; + let mut site = id; let children = loop { - if let Ok(children) = self.params.sites.get(site) { + if let Ok(children) = self.sites.get(site) { break children; } - if let Ok(parent) = self.params.parents.get(site) { + if let Ok(parent) = self.parents.get(site) { site = parent.get(); } else { return; @@ -81,12 +86,12 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }; let site = site; - let search = &mut self.events.change.search_for_texture.0; + let search = &mut self.search_for_texture.0; let mut any_partial_matches = false; let mut result = SearchResult::NoMatch; for child in children { - let Ok((name, _)) = self.params.texture_groups.get(*child) else { + let Ok((name, _)) = self.texture_groups.get(*child) else { continue; }; if name.0.contains(&*search) { @@ -114,21 +119,21 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { ui.horizontal(|ui| { if any_partial_matches { if ui - .add(ImageButton::new(self.params.icons.search.egui())) + .add(ImageButton::new(self.icons.search.egui())) .on_hover_text("Search results for this text can be found below") .clicked() { info!("Use the drop-down box to choose a texture"); } } else { - ui.add(ImageButton::new(self.params.icons.empty.egui())) + ui.add(ImageButton::new(self.icons.empty.egui())) .on_hover_text("No search results can be found for this text"); } match result { SearchResult::Empty => { if ui - .add(ImageButton::new(self.params.icons.hidden.egui())) + .add(ImageButton::new(self.icons.hidden.egui())) .on_hover_text("An empty string is not a good texture name") .clicked() { @@ -137,7 +142,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } SearchResult::Current => { if ui - .add(ImageButton::new(self.params.icons.selected.egui())) + .add(ImageButton::new(self.icons.selected.egui())) .on_hover_text("This is the name of the currently selected texture") .clicked() { @@ -146,7 +151,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } SearchResult::NoMatch => { if ui - .add(ImageButton::new(self.params.icons.add.egui())) + .add(ImageButton::new(self.icons.add.egui())) .on_hover_text(if affiliation.0.is_some() { "Create a new copy of the current texture" } else { @@ -156,7 +161,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { { let new_texture = if let Some((_, t)) = affiliation .0 - .map(|a| self.params.texture_groups.get(a).ok()) + .map(|a| self.texture_groups.get(a).ok()) .flatten() { t.clone() @@ -165,7 +170,6 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }; let new_texture_group = self - .events .commands .spawn(TextureGroup { name: NameInSite(search.clone()), @@ -174,27 +178,23 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }) .set_parent(site) .id(); - self.events.change.affiliation.send(Change::new( - Affiliation(Some(new_texture_group)), - self.entity, - )); + self.change_affiliation + .send(Change::new(Affiliation(Some(new_texture_group)), id)); } } SearchResult::Match(group) => { if ui - .add(ImageButton::new(self.params.icons.confirm.egui())) + .add(ImageButton::new(self.icons.confirm.egui())) .on_hover_text("Select this texture") .clicked() { - self.events - .change - .affiliation - .send(Change::new(Affiliation(Some(group)), self.entity)); + self.change_affiliation + .send(Change::new(Affiliation(Some(group)), id)); } } SearchResult::Conflict(text) => { if ui - .add(ImageButton::new(self.params.icons.reject.egui())) + .add(ImageButton::new(self.icons.reject.egui())) .on_hover_text(text) .clicked() { @@ -208,8 +208,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }); let (current_texture_name, _current_texture) = if let Some(a) = affiliation.0 { - self.params - .texture_groups + self.texture_groups .get(a) .ok() .map(|(n, t)| (n.0.as_str(), Some((a, t)))) @@ -221,7 +220,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { let mut new_affiliation = affiliation.clone(); ui.horizontal(|ui| { if ui - .add(ImageButton::new(self.params.icons.exit.egui())) + .add(ImageButton::new(self.icons.exit.egui())) .on_hover_text(format!("Remove this texture from the {}", category.label())) .clicked() { @@ -237,34 +236,35 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { continue; } - if let Ok((n, _)) = self.params.texture_groups.get(*child) { - if n.0.contains(&self.events.change.search_for_texture.0) { + if let Ok((n, _)) = self.texture_groups.get(*child) { + if n.0.contains(&self.search_for_texture.0) { let select_affiliation = Affiliation(Some(*child)); ui.selectable_value(&mut new_affiliation, select_affiliation, &n.0); } } } - if !self.events.change.search_for_texture.0.is_empty() { + if !self.search_for_texture.0.is_empty() { ui.selectable_value(&mut clear_filter, true, "more..."); } }); if clear_filter { - self.events.change.search_for_texture.0.clear(); + self.search_for_texture.0.clear(); } }); if new_affiliation != *affiliation { - self.events - .change - .affiliation - .send(Change::new(new_affiliation, self.entity)); + self.change_affiliation + .send(Change::new(new_affiliation, id)); } ui.add_space(10.0); } } +#[derive(Resource, Default)] +pub struct SearchForTexture(pub String); + pub struct InspectTexture<'a> { texture: &'a Texture, default_file: Option<&'a DefaultFile>, @@ -282,7 +282,7 @@ impl<'a> InspectTexture<'a> { let mut new_texture = self.texture.clone(); // TODO(luca) recall - if let Some(new_source) = InspectAssetSource::new( + if let Some(new_source) = InspectAssetSourceComponent::new( &new_texture.source, &RecallAssetSource::default(), self.default_file, @@ -294,10 +294,10 @@ impl<'a> InspectTexture<'a> { ui.add_space(10.0); Grid::new("texture_properties").show(ui, |ui| { if let Some(width) = new_texture.width { - if let Some(new_width) = InspectValue::::new(String::from("Width"), width) + if let Some(new_width) = InspectValue::::new("Width", width) .clamp_range(0.001..=std::f32::MAX) .speed(0.01) - .tooltip("Texture width in meters".to_string()) + .tooltip("Texture width in meters") .show(ui) { new_texture.width = Some(new_width); @@ -305,10 +305,10 @@ impl<'a> InspectTexture<'a> { ui.end_row(); } if let Some(height) = new_texture.height { - if let Some(new_height) = InspectValue::::new(String::from("Height"), height) + if let Some(new_height) = InspectValue::::new("Height", height) .clamp_range(0.001..=std::f32::MAX) .speed(0.01) - .tooltip("Texture height in meters".to_string()) + .tooltip("Texture height in meters") .show(ui) { new_texture.height = Some(new_height); @@ -316,10 +316,10 @@ impl<'a> InspectTexture<'a> { ui.end_row(); } if let Some(alpha) = new_texture.alpha { - if let Some(new_alpha) = InspectValue::::new(String::from("Alpha"), alpha) + if let Some(new_alpha) = InspectValue::::new("Alpha", alpha) .clamp_range(0.0..=1.0) .speed(0.1) - .tooltip("Transparency (0 = transparent, 1 = opaque)".to_string()) + .tooltip("Transparency (0 = transparent, 1 = opaque)") .show(ui) { new_texture.alpha = Some(new_alpha); diff --git a/rmf_site_editor/src/widgets/inspector/inspect_value.rs b/rmf_site_editor/src/widgets/inspector/inspect_value.rs index e0a7b975..0b1ff2a1 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_value.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_value.rs @@ -19,19 +19,19 @@ use bevy_egui::egui::emath::Numeric; use bevy_egui::egui::{DragValue, Ui}; use std::ops::RangeInclusive; -pub struct InspectValue { - title: String, +pub struct InspectValue<'a, T> { + title: &'a str, current_value: T, range: RangeInclusive, min_decimals: usize, max_decimals: Option, speed: f64, - suffix: String, - tooltip: Option, + suffix: &'a str, + tooltip: Option<&'a str>, } -impl InspectValue { - pub fn new(title: String, current_value: T) -> Self { +impl<'a, T: Numeric> InspectValue<'a, T> { + pub fn new(title: &'a str, current_value: T) -> Self { Self { title, current_value, @@ -68,12 +68,12 @@ impl InspectValue { } #[allow(dead_code)] - pub fn suffix(mut self, suffix: String) -> Self { + pub fn suffix(mut self, suffix: &'a str) -> Self { self.suffix = suffix; self } - pub fn tooltip(mut self, tooltip: String) -> Self { + pub fn tooltip(mut self, tooltip: &'a str) -> Self { self.tooltip = Some(tooltip); self } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_workcell_parent.rs b/rmf_site_editor/src/widgets/inspector/inspect_workcell_parent.rs index 74693344..4f8fd877 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_workcell_parent.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_workcell_parent.rs @@ -17,16 +17,16 @@ use crate::{ interaction::{ChangeMode, Hover, SelectAnchor3D}, - site::{FrameMarker, MeshConstraint, NameInWorkcell, NameOfWorkcell, SiteID}, - widgets::{inspector::SelectionWidget, AppEvents, Icons}, + site::{FrameMarker, MeshConstraint, NameInWorkcell, NameOfWorkcell}, + widgets::{prelude::*, Icons, Inspect, SelectorWidget}, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{ImageButton, Ui}; #[derive(SystemParam)] -pub struct InspectWorkcellParentParams<'w, 's> { - pub parents: Query<'w, 's, &'static Parent>, - pub workcell_elements: Query< +pub struct InspectWorkcellParent<'w, 's> { + parents: Query<'w, 's, &'static Parent>, + workcell_elements: Query< 'w, 's, Entity, @@ -36,62 +36,42 @@ pub struct InspectWorkcellParentParams<'w, 's> { With, )>, >, - pub mesh_constraints: Query<'w, 's, &'static MeshConstraint>, - pub site_id: Query<'w, 's, &'static SiteID>, - pub icons: Res<'w, Icons>, + mesh_constraints: Query<'w, 's, &'static MeshConstraint>, + icons: Res<'w, Icons>, + selector: SelectorWidget<'w, 's>, + change_mode: ResMut<'w, Events>, } -pub struct InspectWorkcellParentWidget<'a, 'w1, 'w2, 's1, 's2> { - pub entity: Entity, - pub params: &'a InspectWorkcellParentParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 'w2, 's1, 's2> InspectWorkcellParentWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new( - entity: Entity, - params: &'a InspectWorkcellParentParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - ) -> Self { - Self { - entity, - params, - events, - } +impl<'w, 's> WidgetSystem for InspectWorkcellParent<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + params.show_widget(selection, ui); } +} - pub fn show(self, ui: &mut Ui) { +impl<'w, 's> InspectWorkcellParent<'w, 's> { + pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { // If the parent is a frame it should be reassignable if let Ok(parent) = self - .params .parents - .get(self.entity) - .and_then(|p| self.params.workcell_elements.get(**p)) + .get(id) + .and_then(|p| self.workcell_elements.get(**p)) { ui.vertical(|ui| { - if let Ok(c) = self.params.mesh_constraints.get(self.entity) { + if let Ok(c) = self.mesh_constraints.get(id) { ui.label("Mesh Parent"); - SelectionWidget::new( - c.entity, - self.params.site_id.get(c.entity).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(c.entity, ui); } ui.label("Parent Frame"); - SelectionWidget::new( - parent, - self.params.site_id.get(parent).ok().cloned(), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); - - let assign_response = ui.add(ImageButton::new(self.params.icons.edit.egui())); - + self.selector.show_widget(parent, ui); + let assign_response = ui.add(ImageButton::new(self.icons.edit.egui())); if assign_response.hovered() { - self.events.request.hover.send(Hover(Some(self.entity))); + self.selector.hover.send(Hover(Some(id))); } let parent_replace = assign_response.clicked(); @@ -99,11 +79,8 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectWorkcellParentWidget<'a, 'w1, 'w2, 's1, 's2> if parent_replace { let request = - SelectAnchor3D::replace_point(self.entity, parent).for_anchor(Some(parent)); - self.events - .request - .change_mode - .send(ChangeMode::To(request.into())); + SelectAnchor3D::replace_point(id, parent).for_anchor(Some(parent)); + self.change_mode.send(ChangeMode::To(request.into())); } }); } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index 74b0447e..544549de 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -30,12 +30,18 @@ pub use inspect_asset_source::*; pub mod inspect_door; pub use inspect_door::*; +pub mod inspect_drawing; +pub use inspect_drawing::*; + pub mod inspect_edge; pub use inspect_edge::*; pub mod inspect_fiducial; pub use inspect_fiducial::*; +pub mod inspect_geography; +pub use inspect_geography::*; + pub mod inspect_group; pub use inspect_group::*; @@ -66,6 +72,9 @@ pub use inspect_mesh_constraint::*; pub mod inspect_primitive_shape; pub use inspect_primitive_shape::*; +pub mod inspect_measurement; +pub use inspect_measurement::*; + pub mod inspect_motion; pub use inspect_motion::*; @@ -81,6 +90,9 @@ pub use inspect_physical_camera_properties::*; pub mod inspect_pose; pub use inspect_pose::*; +pub mod inspect_preview; +pub use inspect_preview::*; + pub mod inspect_scale; pub use inspect_scale::*; @@ -96,545 +108,242 @@ pub use inspect_value::*; pub mod inspect_workcell_parent; pub use inspect_workcell_parent::*; -pub mod selection_widget; -pub use selection_widget::*; - -use super::move_layer::MoveLayer; - use crate::{ - interaction::{Selection, SpawnPreview}, - site::{ - AlignSiteDrawings, BeginEditDrawing, Category, Change, DefaultFile, DrawingMarker, - EdgeLabels, LayerVisibility, Original, SiteID, - }, - widgets::AppEvents, + interaction::Selection, + site::{Category, SiteID}, + widgets::prelude::*, AppState, }; -use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{Button, RichText, Ui}; +use bevy::{ + ecs::system::{SystemParam, SystemState}, + prelude::*, +}; +use bevy_egui::egui::{CollapsingHeader, Ui}; use rmf_site_format::*; +use smallvec::SmallVec; + +/// Use this plugin to add a single inspection tile into the [`MainInspector`] +/// widget. +/// +/// ```no_run +/// use bevy::prelude::{App, Query, Entity, Res}; +/// use librmf_site_editor::{SiteEditor, site::NameInSite, widgets::prelude::*}; +/// +/// #[derive(SystemParam)] +/// pub struct HelloSelection<'w, 's> { +/// names: Query<'w, 's, &'static NameInSite>, +/// } +/// +/// impl<'w, 's> WidgetSystem for HelloSelection<'w, 's> { +/// fn show( +/// Inspect { selection, .. }: Inspect, +/// ui: &mut Ui, +/// state: &mut SystemState, +/// world: &mut World, +/// ) { +/// let mut params = state.get_mut(world); +/// let name = params.names.get(selection) +/// .map(|name| name.as_str()) +/// .unwrap_or(""); +/// ui.add_space(20.0); +/// ui.heading(format!("Hello, {name}!")); +/// ui.add_space(20.0); +/// } +/// } +/// +/// fn main() { +/// let mut app = App::new(); +/// app.add_plugins(( +/// SiteEditor::default(), +/// InspectionPlugin::::new(), +/// )); +/// +/// app.run(); +/// } +/// ``` +pub struct InspectionPlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + _ignore: std::marker::PhantomData, +} -// Bevy seems to have a limit of 16 fields in a SystemParam struct, so we split -// some of the InspectorParams fields into the InspectorComponentParams struct. -#[derive(SystemParam)] -pub struct InspectorParams<'w, 's> { - pub selection: Res<'w, Selection>, - pub heading: Query<'w, 's, (Option<&'static Category>, Option<&'static SiteID>)>, - pub anchor_params: InspectAnchorParams<'w, 's>, - pub anchor_dependents_params: InspectAnchorDependentsParams<'w, 's>, - pub workcell_params: InspectorWorkcellParams<'w, 's>, - pub component: InspectorComponentParams<'w, 's>, - pub drawing: InspectDrawingParams<'w, 's>, - // TODO(luca) move to new systemparam, reached 16 limit on main one - pub primitive_shapes: Query<'w, 's, (&'static PrimitiveShape, &'static RecallPrimitiveShape)>, - pub scales: Query<'w, 's, &'static Scale>, - pub layer: InspectorLayerParams<'w, 's>, - pub texture: InspectTextureAffiliationParams<'w, 's>, - pub groups: InspectGroupParams<'w, 's>, - pub default_file: Query<'w, 's, &'static DefaultFile>, +/// Use this to create a standard inspector plugin that covers the common use +/// cases of the site editor. +#[derive(Default)] +pub struct StandardInspectorPlugin {} + +impl Plugin for StandardInspectorPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(MinimalInspectorPlugin::default()) + .add_plugins(( + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectFiducialPlugin::default(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectTexturePlugin::default(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + // Reached the tuple limit + )) + .add_plugins(( + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + InspectLiftPlugin::default(), + InspectionPlugin::::new(), + InspectionPlugin::::new(), + )); + } } -#[derive(SystemParam)] -pub struct InspectorWorkcellParams<'w, 's> { - pub joints: InspectJointParams<'w, 's>, - pub constraint_dependents_params: InspectModelDependentsParams<'w, 's>, - pub names_in_workcell: Query<'w, 's, &'static NameInWorkcell>, - pub workcell_names: Query<'w, 's, &'static NameOfWorkcell>, - pub parent_params: InspectWorkcellParentParams<'w, 's>, +/// Use this to create a minimal inspector plugin. You will be able to add your +/// own [`InspectionPlugin`]s to the application, but none of the standard +/// inspection plugins will be included. +#[derive(Default)] +pub struct MinimalInspectorPlugin {} + +impl Plugin for MinimalInspectorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + } } -// NOTE: We may need to split this struct into multiple structs if we ever need -// it to have more than 16 fields. -#[derive(SystemParam)] -pub struct InspectorComponentParams<'w, 's> { - pub edges: Query< - 'w, - 's, - ( - &'static Edge, - Option<&'static Original>>, - Option<&'static EdgeLabels>, - &'static Category, - ), - >, - pub associated_graphs: InspectAssociatedGraphsParams<'w, 's>, - pub location_tags: Query<'w, 's, (&'static LocationTags, &'static RecallLocationTags)>, - pub motions: Query<'w, 's, (&'static Motion, &'static RecallMotion)>, - pub reverse_motions: Query<'w, 's, (&'static ReverseLane, &'static RecallReverseLane)>, - pub names: Query<'w, 's, &'static NameInSite>, - pub doors: Query<'w, 's, (&'static DoorType, &'static RecallDoorType)>, - pub lifts: InspectLiftParams<'w, 's>, - pub poses: Query<'w, 's, &'static Pose>, - pub asset_sources: - Query<'w, 's, (&'static AssetSource, &'static RecallAssetSource), Without>, - pub constraint_dependents: Query<'w, 's, With>, - pub pixels_per_meter: Query<'w, 's, &'static PixelsPerMeter>, - pub physical_camera_properties: Query<'w, 's, &'static PhysicalCameraProperties>, - pub lights: Query<'w, 's, (&'static LightKind, &'static RecallLightKind)>, - pub previewable: Query<'w, 's, &'static PreviewableMarker>, +impl InspectionPlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + pub fn new() -> Self { + Self { + _ignore: Default::default(), + } + } } -#[derive(SystemParam)] -pub struct InspectDrawingParams<'w, 's> { - pub distance: Query<'w, 's, &'static Distance>, - pub fiducial: InspectFiducialParams<'w, 's>, +impl Plugin for InspectionPlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + fn build(&self, app: &mut App) { + let inspector = app.world.resource::().id; + let widget = Widget::::new::(&mut app.world); + app.world.spawn(widget).set_parent(inspector); + } } -#[derive(SystemParam)] -pub struct InspectorLayerParams<'w, 's> { - pub floors: Query< - 'w, - 's, - ( - Option<&'static LayerVisibility>, - &'static PreferredSemiTransparency, - ), - With, - >, - pub drawings: Query< - 'w, - 's, - ( - Option<&'static LayerVisibility>, - &'static PreferredSemiTransparency, - ), - With, - >, - pub levels: Query< - 'w, - 's, - ( - &'static GlobalFloorVisibility, - &'static GlobalDrawingVisibility, - ), - >, +/// This is the input type for inspection widgets. Use [`InspectionPlugin`] to +/// add the widget to the application. +#[derive(Clone, Copy)] +pub struct Inspect { + /// What entity should be treated as selected. + pub selection: Entity, + /// What entity is the current inspection widget attached to. + pub inspection: Entity, + /// What kind of panel is the inspector rendered on. + pub panel: PanelSide, } -pub struct InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { - pub params: &'a InspectorParams<'w1, 's1>, - pub events: &'a mut AppEvents<'w2, 's2>, +/// This contains a reference to the main inspector widget of the application. +#[derive(Resource)] +pub struct MainInspector { + id: Entity, } -impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { - pub fn new(params: &'a InspectorParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } +impl MainInspector { + pub fn get(&self) -> Entity { + self.id } +} - fn heading(&self, selection: Entity, ui: &mut Ui) { - let (label, site_id) = if let Ok((category, site_id)) = self.params.heading.get(selection) { - ( - category.map(|x| x.label()).unwrap_or(""), - site_id, - ) - } else { - ("", None) - }; - - if let Some(site_id) = site_id { - ui.heading(format!("{} #{}", label, site_id.0)); - } else { - ui.heading(format!("{} (unsaved)", label)); - } +impl FromWorld for MainInspector { + fn from_world(world: &mut World) -> Self { + let widget = Widget::new::(world); + let properties_panel = world.resource::().id(); + let id = world.spawn(widget).set_parent(properties_panel).id(); + Self { id } } +} - pub fn show(mut self, ui: &mut Ui) { - let default_file = self - .events - .request - .current_workspace - .root - .map(|e| self.params.default_file.get(e).ok()) - .flatten(); - - if let Some(selection) = self.params.selection.0 { - self.heading(selection, ui); - if self.params.anchor_params.anchors.contains(selection) { - ui.horizontal(|ui| { - InspectAnchorWidget::new(selection, &self.params.anchor_params, self.events) - .show(ui); - }); - ui.separator(); - InspectAnchorDependentsWidget::new( - selection, - &self.params.anchor_dependents_params, - self.events, - ) - .show(ui); - ui.add_space(10.0); - } - - InspectFiducialWidget::new(selection, &self.params.drawing.fiducial, &mut self.events) - .show(ui); - - if let Ok(name) = self.params.component.names.get(selection) { - if let Some(new_name) = InspectName::new(name).show(ui) { - self.events - .change - .name - .send(Change::new(new_name, selection)); - } - ui.add_space(10.0); - } - - if let Ok(name) = self.params.workcell_params.names_in_workcell.get(selection) { - if let Some(new_name) = InspectNameInWorkcell::new(name).show(ui) { - self.events - .workcell_change - .name_in_workcell - .send(Change::new(new_name, selection)); - } - ui.add_space(10.0); - } - - if let Ok(name) = self.params.workcell_params.workcell_names.get(selection) { - if let Some(new_name) = InspectNameOfWorkcell::new(name).show(ui) { - self.events - .workcell_change - .workcell_name - .send(Change::new(new_name, selection)); - } - ui.add_space(10.0); - } - - if let Ok((floor_vis, alpha)) = self.params.layer.floors.get(selection) { - ui.horizontal(|ui| { - MoveLayer::new( - selection, - &mut self.events.layers.floors, - &self.events.layers.icons, - ) - .show(ui); - }); - ui.horizontal(|ui| { - InspectLayer::new( - selection, - &self.params.anchor_params.icons, - self.events, - floor_vis.copied(), - alpha.0, - true, - ) - .show(ui); - }); - } - - if let Ok((drawing_vis, alpha)) = self.params.layer.drawings.get(selection) { - ui.horizontal(|ui| { - MoveLayer::new( - selection, - &mut self.events.layers.drawings, - &self.events.layers.icons, - ) - .show(ui); - }); - ui.horizontal(|ui| { - InspectLayer::new( - selection, - &self.params.anchor_params.icons, - self.events, - drawing_vis.copied(), - alpha.0, - false, - ) - .show(ui); - }); - } - - if let Ok(ppm) = self.params.component.pixels_per_meter.get(selection) { - if *self.events.app_state.get() == AppState::SiteEditor { - ui.add_space(10.0); - if ui - .add(Button::image_and_text( - self.events.layers.icons.edit.egui(), - "Edit Drawing", - )) - .clicked() - { - self.events - .layers - .begin_edit_drawing - .send(BeginEditDrawing(selection)); - } - } - ui.add_space(10.0); - if ui - .add(Button::image_and_text( - self.events.layers.icons.alignment.egui(), - "Align Drawings", - )) - .on_hover_text( - "Align all drawings in the site based on their fiducials and measurements", - ) - .clicked() - { - if let Some(site) = self.events.request.current_workspace.root { - self.events.request.align_site.send(AlignSiteDrawings(site)); - } - } - ui.add_space(10.0); - if let Some(new_ppm) = - InspectValue::::new(String::from("Pixels per meter"), ppm.0) - .clamp_range(0.0001..=std::f32::INFINITY) - .tooltip("How many image pixels per meter".to_string()) - .show(ui) - { - self.events - .change - .pixels_per_meter - .send(Change::new(PixelsPerMeter(new_ppm), selection)); - } - } - - if let Ok((edge, original, labels, category)) = - self.params.component.edges.get(selection) - { - InspectEdgeWidget::new( - selection, - category, - edge, - original, - labels, - &self.params.anchor_params, - self.events, - ) - .show(ui); - ui.add_space(10.0); - } - - InspectAssociatedGraphsWidget::new( - selection, - &self.params.component.associated_graphs, - self.events, - ) - .show(ui); - - if let Ok((tags, recall)) = self.params.component.location_tags.get(selection) { - if let Some(new_tags) = InspectLocationWidget::new( - selection, - tags, - recall, - &self.params.anchor_params.icons, - self.events, - ) - .show(ui) - { - self.events - .change - .location_tags - .send(Change::new(new_tags, selection)); - } - } +#[derive(SystemParam)] +pub struct Inspector<'w, 's> { + children: Query<'w, 's, &'static Children>, + heading: Query<'w, 's, (Option<&'static Category>, Option<&'static SiteID>)>, +} - InspectTextureAffiliation::new(selection, &self.params.texture, self.events).show(ui); +impl<'w, 's> WidgetSystem for Inspector<'w, 's> { + fn show( + Tile { id, panel }: Tile, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + match world.resource::>().get() { + AppState::SiteEditor | AppState::SiteDrawingEditor | AppState::WorkcellEditor => {} + _ => return, + } - if let Ok((motion, recall)) = self.params.component.motions.get(selection) { - ui.label(RichText::new("Forward Motion").size(18.0)); - if let Some(new_motion) = InspectMotionWidget::new(motion, recall).show(ui) { - self.events - .change - .lane_motion - .send(Change::new(new_motion, selection)); + CollapsingHeader::new("Inspect") + .default_open(true) + .show(ui, |ui| { + let Some(selection) = world.get_resource::() else { + ui.label("ERROR: Selection resource is not available"); + return; + }; + + let Some(selection) = selection.0 else { + ui.label("Nothing selected"); + return; + }; + + let params = state.get(world); + + let (label, site_id) = + if let Ok((category, site_id)) = params.heading.get(selection) { + ( + category.map(|x| x.label()).unwrap_or(""), + site_id, + ) + } else { + ("", None) + }; + + if let Some(site_id) = site_id { + ui.heading(format!("{} #{}", label, site_id.0)); + } else { + ui.heading(format!("{} (unsaved)", label)); } - ui.add_space(10.0); - } - - if let Ok((reverse, recall)) = self.params.component.reverse_motions.get(selection) { - ui.separator(); - ui.push_id("Reverse Motion", |ui| { - if let Some(new_reverse) = InspectReverseWidget::new(reverse, recall).show(ui) { - self.events - .change - .lane_reverse - .send(Change::new(new_reverse, selection)); + + let children: Result, _> = params + .children + .get(id) + .map(|children| children.iter().copied().collect()); + let Ok(children) = children else { + return; + }; + + panel.align(ui, |ui| { + for child in children { + let inspect = Inspect { + selection, + inspection: child, + panel, + }; + let _ = world.try_show_in(child, inspect, ui); } }); - ui.add_space(10.0); - } - - if let Ok(pose) = self.params.component.poses.get(selection) { - if let Some(new_pose) = InspectPose::new(pose).show(ui) { - self.events - .change - .pose - .send(Change::new(new_pose, selection)); - } - ui.add_space(10.0); - } - - if let Ok(scale) = self.params.scales.get(selection) { - if let Some(new_scale) = InspectScale::new(scale).show(ui) { - self.events - .workcell_change - .scale - .send(Change::new(new_scale, selection)); - } - ui.add_space(10.0); - } - - if let Ok((light, recall)) = self.params.component.lights.get(selection) { - if let Some(new_light) = InspectLightKind::new(light, recall).show(ui) { - self.events - .change - .light - .send(Change::new(new_light, selection)); - } - ui.add_space(10.0); - } - - if let Ok((door, recall)) = self.params.component.doors.get(selection) { - if let Some(new_door) = InspectDoorType::new(door, recall).show(ui) { - self.events - .change - .door - .send(Change::new(new_door, selection)); - } - ui.add_space(10.0); - } - - if let Ok((source, recall)) = self.params.component.asset_sources.get(selection) { - if let Some(new_asset_source) = - InspectAssetSource::new(source, recall, default_file).show(ui) - { - self.events - .change - .asset_source - .send(Change::new(new_asset_source, selection)); - } - ui.add_space(10.0); - } - - if let Ok((source, recall)) = self.params.primitive_shapes.get(selection) { - if let Some(new_primitive_shape) = - InspectPrimitiveShape::new(source, recall).show(ui) - { - self.events - .workcell_change - .primitive_shapes - .send(Change::new(new_primitive_shape, selection)); - } - ui.add_space(10.0); - } - - if self - .params - .component - .constraint_dependents - .get(selection) - .is_ok() - { - InspectModelDependentsWidget::new( - selection, - &self.params.workcell_params.constraint_dependents_params, - self.events, - ) - .show(ui); - ui.add_space(10.0); - } - - InspectWorkcellParentWidget::new( - selection, - &self.params.workcell_params.parent_params, - &mut self.events, - ) - .show(ui); - InspectJointWidget::new( - selection, - &self.params.workcell_params.joints, - &mut self.events, - ) - .show(ui); - - if let Ok(distance) = self.params.drawing.distance.get(selection) { - if let Some(new_distance) = - InspectOptionF32::new("Distance".to_string(), distance.0, 10.0) - .clamp_range(0.0..=10000.0) - .min_decimals(2) - .max_decimals(2) - .speed(0.01) - .suffix(" m".to_string()) - .show(ui) - { - self.events - .change - .distance - .send(Change::new(Distance(new_distance), selection)); - } - ui.add_space(10.0); - } - - if let Ok(camera_properties) = self - .params - .component - .physical_camera_properties - .get(selection) - { - if let Some(new_camera_properties) = - InspectPhysicalCameraProperties::new(camera_properties).show(ui) - { - self.events - .change - .physical_camera_properties - .send(Change::new(new_camera_properties, selection)); - } - ui.add_space(10.0); - } - - if let Some(new_cabin) = - InspectLiftCabin::new(selection, &self.params.component.lifts, &mut self.events) - .show(ui) - { - self.events - .change - .lift_cabin - .send(Change::new(new_cabin, selection)); - ui.add_space(10.0); - } - - if let Ok(_previewable) = self.params.component.previewable.get(selection) { - if ui.button("Preview").clicked() { - self.events - .request - .spawn_preview - .send(SpawnPreview::new(Some(selection))); - } - ui.add_space(10.0); - } - - if let Ok(Affiliation(Some(group))) = self.params.groups.affiliation.get(selection) { - ui.separator(); - let empty = String::new(); - let name = self - .params - .component - .names - .get(*group) - .map(|n| &n.0) - .unwrap_or(&empty); - - ui.label(RichText::new(format!("Group Properties of [{}]", name)).size(18.0)); - ui.add_space(5.0); - InspectGroup::new( - *group, - selection, - default_file, - &self.params.groups, - self.events, - ) - .show(ui); - } - - if self.params.groups.is_group.contains(selection) { - InspectGroup::new( - selection, - selection, - default_file, - &self.params.groups, - self.events, - ) - .show(ui); - } - } else { - ui.label("Nothing selected"); - } + }); } } diff --git a/rmf_site_editor/src/widgets/inspector/selection_widget.rs b/rmf_site_editor/src/widgets/inspector/selection_widget.rs deleted file mode 100644 index 7d05c4aa..00000000 --- a/rmf_site_editor/src/widgets/inspector/selection_widget.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2022 Open Source Robotics Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * -*/ - -use crate::{ - interaction::{Hover, Select}, - site::SiteID, - widgets::{AppEvents, Icons}, -}; -use bevy::prelude::*; -use bevy_egui::egui::{Button, Ui}; - -pub struct SelectionWidget<'a, 'w, 's> { - entity: Entity, - site_id: Option, - icons: &'a Icons, - events: &'a mut AppEvents<'w, 's>, - as_selected: bool, -} - -impl<'a, 'w, 's> SelectionWidget<'a, 'w, 's> { - pub fn new( - entity: Entity, - site_id: Option, - icons: &'a Icons, - events: &'a mut AppEvents<'w, 's>, - ) -> Self { - Self { - entity, - site_id, - icons, - events, - as_selected: false, - } - } - - pub fn as_selected(mut self, as_selected: bool) -> Self { - self.as_selected = as_selected; - self - } - - pub fn show(self, ui: &mut Ui) { - let text = match self.site_id { - Some(id) => format!("#{}", id.0), - None => "*".to_string(), - }; - - let icon = if self.as_selected { - self.icons.selected.egui() - } else { - self.icons.select.egui() - }; - - let response = ui.add(Button::image_and_text(icon, text)); - - if response.clicked() { - self.events.request.select.send(Select(Some(self.entity))); - } else if response.hovered() { - self.events.request.hover.send(Hover(Some(self.entity))); - } - - response.on_hover_text("Select"); - } -} diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index dfcb9a40..98d2bc4f 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -15,16 +15,32 @@ * */ -use crate::{AppState, CreateNewWorkspace, FileEvents, LoadWorkspace, MenuParams, SaveWorkspace}; +use crate::{widgets::prelude::*, AppState, CreateNewWorkspace, LoadWorkspace, SaveWorkspace}; use bevy::ecs::query::Has; use bevy::prelude::*; -use bevy_egui::egui::{self, Button, Context, Ui}; +use bevy_egui::egui::{self, Button, Ui}; use std::collections::HashSet; -/// Adding this to an entity to an entity with the MenuItem component -/// will grey out and disable a MenuItem. +/// Add the standard menu bar to the application. +#[derive(Default)] +pub struct MenuBarPlugin {} + +impl Plugin for MenuBarPlugin { + fn build(&self, app: &mut App) { + let widget = PanelWidget::new(top_menu_bar, &mut app.world); + app.world.spawn(widget); + + app.add_event::() + .init_resource::() + .init_resource::() + .init_resource::(); + } +} + +/// Adding this to an entity to an entity with the [`MenuItem`] component +/// will grey out and disable a [`MenuItem`]. #[derive(Component)] pub struct MenuDisabled; @@ -165,17 +181,6 @@ impl MenuEvent { } } -pub struct MenuPluginManager; - -impl Plugin for MenuPluginManager { - fn build(&self, app: &mut App) { - app.add_event::() - .init_resource::() - .init_resource::() - .init_resource::(); - } -} - /// Helper function to render a submenu starting at the entity. fn render_sub_menu( state: &State, @@ -258,19 +263,31 @@ fn render_sub_menu( } } -pub fn top_menu_bar( - egui_context: &mut Context, - file_events: &mut FileEvents, - file_menu: &Res, - top_level_components: &Query<(), Without>, - children: &Query<&Children>, - menu_params: &mut MenuParams, +#[derive(SystemParam)] +struct MenuParams<'w, 's> { + state: Res<'w, State>, + menus: Query<'w, 's, (&'static Menu, Entity)>, + menu_items: Query<'w, 's, (&'static mut MenuItem, Has)>, + menu_states: Query<'w, 's, Option<&'static MenuVisualizationStates>>, + extension_events: EventWriter<'w, MenuEvent>, + view_menu: Res<'w, ViewMenu>, +} + +fn top_menu_bar( + In(input): In, + mut new_workspace: EventWriter, + mut save: EventWriter, + mut load_workspace: EventWriter, + file_menu: Res, + top_level_components: Query<(), Without>, + children: Query<&Children>, + mut menu_params: MenuParams, ) { - egui::TopBottomPanel::top("top_panel").show(egui_context, |ui| { + egui::TopBottomPanel::top("top_panel").show(&input.context, |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("File", |ui| { if ui.add(Button::new("New").shortcut_text("Ctrl+N")).clicked() { - file_events.new_workspace.send(CreateNewWorkspace); + new_workspace.send(CreateNewWorkspace); } #[cfg(not(target_arch = "wasm32"))] { @@ -278,29 +295,27 @@ pub fn top_menu_bar( .add(Button::new("Save").shortcut_text("Ctrl+S")) .clicked() { - file_events - .save - .send(SaveWorkspace::new().to_default_file()); + save.send(SaveWorkspace::new().to_default_file()); } if ui .add(Button::new("Save As").shortcut_text("Ctrl+Shift+S")) .clicked() { - file_events.save.send(SaveWorkspace::new().to_dialog()); + save.send(SaveWorkspace::new().to_dialog()); } } if ui .add(Button::new("Open").shortcut_text("Ctrl+O")) .clicked() { - file_events.load_workspace.send(LoadWorkspace::Dialog); + load_workspace.send(LoadWorkspace::Dialog); } render_sub_menu( &menu_params.state, ui, &file_menu.get(), - children, + &children, &menu_params.menus, &menu_params.menu_items, &menu_params.menu_states, @@ -313,7 +328,7 @@ pub fn top_menu_bar( &menu_params.state, ui, &menu_params.view_menu.get(), - children, + &children, &menu_params.menus, &menu_params.menu_items, &menu_params.menu_states, @@ -330,7 +345,7 @@ pub fn top_menu_bar( &menu_params.state, ui, &entity, - children, + &children, &menu_params.menus, &menu_params.menu_items, &menu_params.menu_states, diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index df662e6f..f331e210 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -15,47 +15,100 @@ * */ +//! The site editor allows you to insert your own egui widgets into the UI. +//! Simple examples of custom widgets can be found in the docs for +//! [`PropertiesTilePlugin`] and [`InspectionPlugin`]. +//! +//! There are three categories of widgets that the site editor provides +//! out-of-the-box support for inserting, but the widget system itself is +//! highly extensible, allowing you to define your own categories of widgets. +//! +//! The three categories provided out of the box include: +//! - [Panel widget][1]: Add a new panel to the UI. +//! - Tile widget: Add a tile into a [panel of tiles][2] such as the [`PropertiesPanel`]. Use [`PropertiesTilePlugin`] to make a new tile widget that goes inside of the standard `PropertiesPanel`. +//! - [`InspectionPlugin`]: Add a widget to the [`MainInspector`] to display more information about the currently selected entity. +//! +//! In our terminology, there are two kinds of panels: +//! - Side panels: A vertical column widget on the left or right side of the screen. +//! - [`PropertiesPanel`] is usually a side panel placed on the right side of the screen. +//! - [`FuelAssetBrowser`] is a side panel typically placed on the left side of the screen. +//! - [`Diagnostics`] is a side panel that interactively flags issues that have been found in the site. +//! - Top / Bottom Panels: +//! - The [`MenuBarPlugin`] provides a menu bar at the top of the screen. +//! - Create an entity with a [`Menu`] component to create a new menu inside the menu bar. +//! - Add an entity with a [`MenuItem`] component as a child to a menu entity to add a new item into a menu. +//! - The [`FileMenu`], [`ToolMenu`], and [`ViewMenu`] are resources that provide access to various standard menus. +//! - The [`ConsoleWidgetPlugin`] provides a console at the bottom of the screen to display information, warning, and error messages. +//! +//! [1]: crate::widgets::PanelWidget +//! [2]: crate::widgets::show_panel_of_tiles + use crate::{ - interaction::{ - ChangeMode, HeadlightToggle, Hover, MoveTo, PickingBlockers, Select, SpawnPreview, - }, - log::LogHistory, - occupancy::CalculateGrid, - recency::ChangeRank, - site::{ - AlignSiteDrawings, AssociatedGraphs, BeginEditDrawing, Change, ConsiderAssociatedGraph, - ConsiderLocationTag, CurrentLevel, Delete, DrawingMarker, ExportLights, FinishEditDrawing, - GlobalDrawingVisibility, GlobalFloorVisibility, JointProperties, LayerVisibility, - MergeGroups, PhysicalLightToggle, SaveNavGraphs, Texture, ToggleLiftDoorAvailability, + interaction::{Hover, PickingBlockers}, + AppState, +}; +use bevy::{ + ecs::{ + system::{SystemParam, SystemState}, + world::EntityWorldMut, }, - workcell::CreateJoint, - AppState, CreateNewWorkspace, CurrentWorkspace, LoadWorkspace, SaveWorkspace, - ValidateWorkspace, + prelude::*, }; -use bevy::{asset::embedded_asset, ecs::query::Has, ecs::system::SystemParam, prelude::*}; use bevy_egui::{ - egui::{self, Button, CollapsingHeader}, + egui::{self, Ui}, EguiContexts, }; -use rmf_site_format::*; -pub mod create; -use create::*; +pub mod building_preview; +use building_preview::*; + +pub mod console; +use console::*; + +pub mod creation; +use creation::*; + +pub mod diagnostics; +use diagnostics::*; + +pub mod fuel_asset_browser; +pub use fuel_asset_browser::*; + +pub mod icons; +pub use icons::*; + +pub mod inspector; +pub use inspector::*; pub mod menu_bar; -use menu_bar::*; +pub use menu_bar::*; + +pub mod move_layer; +pub use move_layer::*; + +pub mod panel_of_tiles; +pub use panel_of_tiles::*; + +pub mod panel; +pub use panel::*; + +pub mod properties_panel; +pub use properties_panel::*; + +pub mod sdf_export_menu; +pub use sdf_export_menu::*; + +pub mod selector_widget; +pub use selector_widget::*; pub mod view_groups; use view_groups::*; -pub mod diagnostic_window; -use diagnostic_window::*; - pub mod view_layers; use view_layers::*; pub mod view_levels; -use view_levels::{LevelDisplay, LevelParams, ViewLevels}; +use view_levels::*; pub mod view_lights; use view_lights::*; @@ -66,633 +119,298 @@ use view_nav_graphs::*; pub mod view_occupancy; use view_occupancy::*; -pub mod console; -pub use console::*; - -pub mod icons; -pub use icons::*; - -pub mod inspector; -use inspector::{InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture}; - -pub mod move_layer; -pub use move_layer::*; - -pub mod new_model; -pub use new_model::*; - -#[derive(Resource, Clone, Default)] -pub struct PendingDrawing { - pub source: AssetSource, - pub recall_source: RecallAssetSource, -} - -#[derive(Resource, Clone, Default)] -pub struct PendingModel { - pub source: AssetSource, - pub recall_source: RecallAssetSource, - pub scale: Scale, +pub mod prelude { + //! This module gives easy access to the traits, structs, and plugins that + //! we expect downstream users are likely to want easy access to if they are + //! implementing and inserting their own widgets. + + pub use super::{ + properties_panel::*, Inspect, InspectionPlugin, PanelSide, PanelWidget, PanelWidgetInput, + PropertiesPanel, PropertiesTilePlugin, ShareableWidget, ShowError, ShowResult, + ShowSharedWidget, Tile, TryShowWidgetEntity, TryShowWidgetWorld, Widget, WidgetSystem, + }; + pub use bevy::ecs::{ + system::{SystemParam, SystemState}, + world::World, + }; + pub use bevy_egui::egui::Ui; } +/// This plugin provides the standard UI layout that was designed for the common +/// use cases of the site editor. #[derive(Default)] -pub struct StandardUiLayout { - pub headless: bool, -} +pub struct StandardUiPlugin {} -fn add_widgets_icons(app: &mut App) { - // Taken from https://github.com/bevyengine/bevy/issues/10377#issuecomment-1858797002 - // TODO(luca) remove once we migrate to Bevy 0.13 that includes the fix - #[cfg(any(not(target_family = "windows"), target_env = "gnu"))] - { - embedded_asset!(app, "src/", "icons/add.png"); - embedded_asset!(app, "src/", "icons/alignment.png"); - embedded_asset!(app, "src/", "icons/alpha.png"); - embedded_asset!(app, "src/", "icons/confirm.png"); - embedded_asset!(app, "src/", "icons/down.png"); - embedded_asset!(app, "src/", "icons/edit.png"); - embedded_asset!(app, "src/", "icons/empty.png"); - embedded_asset!(app, "src/", "icons/exit.png"); - embedded_asset!(app, "src/", "icons/global.png"); - embedded_asset!(app, "src/", "icons/hidden.png"); - embedded_asset!(app, "src/", "icons/hide.png"); - embedded_asset!(app, "src/", "icons/merge.png"); - embedded_asset!(app, "src/", "icons/opaque.png"); - embedded_asset!(app, "src/", "icons/reject.png"); - embedded_asset!(app, "src/", "icons/search.png"); - embedded_asset!(app, "src/", "icons/select.png"); - embedded_asset!(app, "src/", "icons/selected.png"); - embedded_asset!(app, "src/", "icons/to_bottom.png"); - embedded_asset!(app, "src/", "icons/to_top.png"); - embedded_asset!(app, "src/", "icons/trash.png"); - embedded_asset!(app, "src/", "icons/up.png"); - } - #[cfg(all(target_family = "windows", not(target_env = "gnu")))] - { - embedded_asset!(app, "src\\", "icons\\add.png"); - embedded_asset!(app, "src\\", "icons\\alignment.png"); - embedded_asset!(app, "src\\", "icons\\alpha.png"); - embedded_asset!(app, "src\\", "icons\\confirm.png"); - embedded_asset!(app, "src\\", "icons\\down.png"); - embedded_asset!(app, "src\\", "icons\\edit.png"); - embedded_asset!(app, "src\\", "icons\\empty.png"); - embedded_asset!(app, "src\\", "icons\\exit.png"); - embedded_asset!(app, "src\\", "icons\\global.png"); - embedded_asset!(app, "src\\", "icons\\hidden.png"); - embedded_asset!(app, "src\\", "icons\\hide.png"); - embedded_asset!(app, "src\\", "icons\\merge.png"); - embedded_asset!(app, "src\\", "icons\\opaque.png"); - embedded_asset!(app, "src\\", "icons\\reject.png"); - embedded_asset!(app, "src\\", "icons\\search.png"); - embedded_asset!(app, "src\\", "icons\\select.png"); - embedded_asset!(app, "src\\", "icons\\selected.png"); - embedded_asset!(app, "src\\", "icons\\to_bottom.png"); - embedded_asset!(app, "src\\", "icons\\to_top.png"); - embedded_asset!(app, "src\\", "icons\\trash.png"); - embedded_asset!(app, "src\\", "icons\\up.png"); +impl Plugin for StandardUiPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + IconsPlugin::default(), + MenuBarPlugin::default(), + SdfExportMenuPlugin::default(), + StandardPropertiesPanelPlugin::default(), + FuelAssetBrowserPlugin::default(), + DiagnosticsPlugin::default(), + ConsoleWidgetPlugin::default(), + )) + .add_systems(Startup, init_ui_style) + .add_systems( + Update, + site_ui_layout.run_if(AppState::in_displaying_mode()), + ) + .add_systems( + PostUpdate, + ( + resolve_light_export_file, + resolve_nav_graph_import_export_files, + ) + .run_if(AppState::in_site_mode()), + ); } } -impl Plugin for StandardUiLayout { - fn build(&self, app: &mut App) { - add_widgets_icons(app); - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_plugins(MenuPluginManager) - .init_resource::() - .init_resource::(); - if !self.headless { - app.add_systems(Startup, init_ui_style) - .add_systems( - Update, - site_ui_layout.run_if(in_state(AppState::SiteEditor)), - ) - .add_systems( - Update, - workcell_ui_layout.run_if(in_state(AppState::WorkcellEditor)), - ) - .add_systems( - Update, - site_drawing_ui_layout.run_if(in_state(AppState::SiteDrawingEditor)), - ) - .add_systems( - Update, - site_visualizer_ui_layout.run_if(in_state(AppState::SiteVisualizer)), - ) - .add_systems( - PostUpdate, - ( - resolve_light_export_file, - resolve_nav_graph_import_export_files, - ) - .run_if(AppState::in_site_mode()), - ); +/// This component should be given to an entity that needs to be rendered as a +/// nested widget in the UI. +/// +/// For standard types of widgets you don't need to create this component yourself, +/// instead use one of the generic convenience plugins: +/// - [`InspectionPlugin`] +/// - [`PropertiesTilePlugin`] +#[derive(Component)] +pub struct Widget { + inner: Option + 'static + Send + Sync>>, + _ignore: std::marker::PhantomData<(Input, Output)>, +} + +impl Widget +where + Input: 'static + Send + Sync, + Output: 'static + Send + Sync, +{ + pub fn new(world: &mut World) -> Self + where + W: WidgetSystem + 'static + Send + Sync, + { + let inner = InnerWidget:: { + state: SystemState::new(world), + _ignore: Default::default(), + }; + + Self { + inner: Some(Box::new(inner)), + _ignore: Default::default(), } } } -#[derive(SystemParam)] -pub struct ChangeEvents<'w> { - pub lane_motion: EventWriter<'w, Change>, - pub lane_reverse: EventWriter<'w, Change>, - pub name: EventWriter<'w, Change>, - pub pose: EventWriter<'w, Change>, - pub door: EventWriter<'w, Change>, - pub lift_cabin: EventWriter<'w, Change>>, - pub asset_source: EventWriter<'w, Change>, - pub pixels_per_meter: EventWriter<'w, Change>, - pub physical_camera_properties: EventWriter<'w, Change>, - pub light: EventWriter<'w, Change>, - pub level_elevation: EventWriter<'w, Change>, - pub color: EventWriter<'w, Change>, - pub visibility: EventWriter<'w, Change>, - pub associated_graphs: EventWriter<'w, Change>>, - pub location_tags: EventWriter<'w, Change>, - pub affiliation: EventWriter<'w, Change>>, - pub search_for_fiducial: ResMut<'w, SearchForFiducial>, - pub search_for_texture: ResMut<'w, SearchForTexture>, - pub distance: EventWriter<'w, Change>, - pub texture: EventWriter<'w, Change>, - pub joint_properties: EventWriter<'w, Change>, - pub merge_groups: EventWriter<'w, MergeGroups>, - pub filtered_issues: EventWriter<'w, Change>>, - pub filtered_issue_kinds: EventWriter<'w, Change>, +/// Do not implement this widget directly. Instead create a struct that derives +/// [`SystemParam`] and then implement [`WidgetSystem`] for that struct. +pub trait ExecuteWidget { + fn show(&mut self, input: Input, ui: &mut Ui, world: &mut World) -> Output; } -#[derive(SystemParam)] -pub struct WorkcellChangeEvents<'w> { - pub mesh_constraints: EventWriter<'w, Change>>, - pub name_in_workcell: EventWriter<'w, Change>, - pub workcell_name: EventWriter<'w, Change>, - pub scale: EventWriter<'w, Change>, - pub primitive_shapes: EventWriter<'w, Change>, +/// Implement this on a [`SystemParam`] struct to make it a widget that can be +/// plugged into the site editor UI. +/// +/// See documentation of [`PropertiesTilePlugin`] or [`InspectionPlugin`] to see +/// examples of using this. +pub trait WidgetSystem: SystemParam { + fn show(input: Input, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> Output; } -#[derive(SystemParam)] -pub struct FileEvents<'w> { - pub save: EventWriter<'w, SaveWorkspace>, - pub load_workspace: EventWriter<'w, LoadWorkspace>, - pub new_workspace: EventWriter<'w, CreateNewWorkspace>, - pub diagnostic_window: ResMut<'w, DiagnosticWindowState>, +struct InnerWidget + 'static> { + state: SystemState, + _ignore: std::marker::PhantomData<(Input, Output)>, } -#[derive(SystemParam)] -pub struct PanelResources<'w> { - pub level: ResMut<'w, LevelDisplay>, - pub nav_graph: ResMut<'w, NavGraphDisplay>, - pub light: ResMut<'w, LightDisplay>, - pub occupancy: ResMut<'w, OccupancyDisplay>, - pub log_history: ResMut<'w, LogHistory>, - pub pending_model: ResMut<'w, PendingModel>, - pub pending_drawings: ResMut<'w, PendingDrawing>, +impl ExecuteWidget for InnerWidget +where + W: WidgetSystem, +{ + fn show(&mut self, input: Input, ui: &mut Ui, world: &mut World) -> Output { + let u = W::show(input, ui, &mut self.state, world); + self.state.apply(world); + u + } } -#[derive(SystemParam)] -pub struct Requests<'w> { - pub hover: ResMut<'w, Events>, - pub select: ResMut<'w, Events(&mut self, entity: Entity, input: Input, ui: &mut Ui) -> ShowResult<()> + where + Input: 'static + Send + Sync, + { + self.try_show_out(entity, input, ui) + } -/// We collect all the events into its own SystemParam because we are not -/// allowed to receive more than one EventWriter of a given type per system call -/// (for borrow-checker reasons). Bundling them all up into an AppEvents -/// parameter at least makes the EventWriters easy to pass around. -#[derive(SystemParam)] -pub struct AppEvents<'w, 's> { - pub commands: Commands<'w, 's>, - pub change: ChangeEvents<'w>, - pub workcell_change: WorkcellChangeEvents<'w>, - pub display: PanelResources<'w>, - pub request: Requests<'w>, - pub file_events: FileEvents<'w>, - pub layers: LayerEvents<'w>, - pub new_model: NewModelParams<'w>, - pub app_state: Res<'w, State>, - pub next_app_state: ResMut<'w, NextState>, + /// Same as [`Self::try_show`] but takes an input for the widget and provides + /// an output from the widget. + fn try_show_out( + &mut self, + entity: Entity, + input: Input, + ui: &mut Ui, + ) -> ShowResult + where + Input: 'static + Send + Sync, + Output: 'static + Send + Sync; } -fn site_ui_layout( - mut egui_context: EguiContexts, - mut picking_blocker: Option>, - open_sites: Query>, - inspector_params: InspectorParams, - create_params: CreateParams, - levels: LevelParams, - lights: LightParams, - nav_graphs: NavGraphParams, - diagnostic_params: DiagnosticParams, - layers: LayersParams, - mut groups: GroupParams, - mut events: AppEvents, - file_menu: Res, - children: Query<&Children>, - top_level_components: Query<(), Without>, - mut menu_params: MenuParams, -) { - egui::SidePanel::right("right_panel") - .resizable(true) - .default_width(300.0) - .show(egui_context.ctx_mut(), |ui| { - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - ui.vertical(|ui| { - CollapsingHeader::new("Levels") - .default_open(true) - .show(ui, |ui| { - ViewLevels::new(&levels, &mut events) - .for_editing_visibility() - .show(ui); - }); - ui.separator(); - CollapsingHeader::new("Navigation Graphs") - .default_open(true) - .show(ui, |ui| { - ViewNavGraphs::new(&nav_graphs, &mut events).show(ui, &open_sites); - }); - ui.separator(); - // TODO(MXG): Consider combining Nav Graphs and Layers - CollapsingHeader::new("Layers") - .default_open(false) - .show(ui, |ui| { - ViewLayers::new(&layers, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Inspect") - .default_open(true) - .show(ui, |ui| { - InspectorWidget::new(&inspector_params, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Create") - .default_open(false) - .show(ui, |ui| { - CreateWidget::new(&create_params, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Groups") - .default_open(false) - .show(ui, |ui| { - ViewGroups::new(&mut groups, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Lights") - .default_open(false) - .show(ui, |ui| { - ViewLights::new(&lights, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Occupancy") - .default_open(false) - .show(ui, |ui| { - ViewOccupancy::new(&mut events).show(ui); - }); - if ui.add(Button::new("Building preview")).clicked() { - events.next_app_state.set(AppState::SiteVisualizer); - } - }); - }); - }); - - top_menu_bar( - egui_context.ctx_mut(), - &mut events.file_events, - &file_menu, - &top_level_components, - &children, - &mut menu_params, - ); - - egui::TopBottomPanel::bottom("log_console") - .resizable(true) - .min_height(30.) - .max_height(300.) - .show(egui_context.ctx_mut(), |ui| { - ui.add_space(10.0); - ConsoleWidget::new(&mut events).show(ui); - }); - - if events.file_events.diagnostic_window.show { - egui::SidePanel::left("diagnostic_window") - .resizable(true) - .exact_width(320.0) - .show(egui_context.ctx_mut(), |ui| { - DiagnosticWindow::new(&mut events, &diagnostic_params).show(ui); - }); - } - if events.new_model.asset_gallery_status.show { - egui::SidePanel::left("asset_gallery") - .resizable(true) - .exact_width(320.0) - .show(egui_context.ctx_mut(), |ui| { - NewModel::new(&mut events).show(ui); - }); +impl TryShowWidgetWorld for World { + fn try_show_out( + &mut self, + entity: Entity, + input: Input, + ui: &mut Ui, + ) -> ShowResult + where + Input: 'static + Send + Sync, + Output: 'static + Send + Sync, + { + let Some(mut entity_mut) = self.get_entity_mut(entity) else { + return Err(ShowError::EntityMissing); + }; + entity_mut.try_show_out(input, ui) } +} - let egui_context = egui_context.ctx_mut(); - let ui_has_focus = egui_context.wants_pointer_input() - || egui_context.wants_keyboard_input() - || egui_context.is_pointer_over_area(); - - if let Some(picking_blocker) = &mut picking_blocker { - picking_blocker.ui = ui_has_focus; +/// Same as [`TryShowWidgetWorld`] but is implemented for [`EntityWorldMut`] so +/// you do not need to specify the target entity. +pub trait TryShowWidgetEntity { + /// Try to show a widget that has `()` for input and output + fn try_show(&mut self, ui: &mut Ui) -> ShowResult<()> { + self.try_show_out((), ui) } - if ui_has_focus { - // If the UI has focus and there were no hover events emitted by the UI, - // then we should emit a None hover event - if events.request.hover.is_empty() { - events.request.hover.send(Hover(None)); - } + fn try_show_in(&mut self, input: Input, ui: &mut Ui) -> ShowResult<()> + where + Input: 'static + Send + Sync, + { + self.try_show_out(input, ui) } + + fn try_show_out(&mut self, input: Input, ui: &mut Ui) -> ShowResult + where + Input: 'static + Send + Sync, + Output: 'static + Send + Sync; } -fn site_drawing_ui_layout( - mut egui_context: EguiContexts, - mut picking_blocker: Option>, - inspector_params: InspectorParams, - create_params: CreateParams, - mut events: AppEvents, - file_menu: Res, - children: Query<&Children>, - top_level_components: Query<(), Without>, - mut menu_params: MenuParams, -) { - egui::SidePanel::right("right_panel") - .resizable(true) - .show(egui_context.ctx_mut(), |ui| { - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - ui.vertical(|ui| { - CollapsingHeader::new("Inspect") - .default_open(true) - .show(ui, |ui| { - InspectorWidget::new(&inspector_params, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Create") - .default_open(true) - .show(ui, |ui| { - CreateWidget::new(&create_params, &mut events).show(ui); - }); - ui.separator(); - if ui - .add(Button::image_and_text( - events.layers.icons.exit.egui(), - "Return to site editor", - )) - .clicked() - { - events - .layers - .finish_edit_drawing - .send(FinishEditDrawing(None)); - } - }); - }); - }); - - egui::TopBottomPanel::bottom("log_console") - .resizable(true) - .min_height(30.) - .max_height(300.) - .show(egui_context.ctx_mut(), |ui| { - ui.add_space(10.0); - ConsoleWidget::new(&mut events).show(ui); - }); - - top_menu_bar( - egui_context.ctx_mut(), - &mut events.file_events, - &file_menu, - &top_level_components, - &children, - &mut menu_params, - ); - - let egui_context = egui_context.ctx_mut(); - let ui_has_focus = egui_context.wants_pointer_input() - || egui_context.wants_keyboard_input() - || egui_context.is_pointer_over_area(); - - if let Some(picking_blocker) = &mut picking_blocker { - picking_blocker.ui = ui_has_focus; - } +impl<'w> TryShowWidgetEntity for EntityWorldMut<'w> { + fn try_show_out(&mut self, input: Input, ui: &mut Ui) -> ShowResult + where + Input: 'static + Send + Sync, + Output: 'static + Send + Sync, + { + let Some(mut widget) = self.get_mut::>() else { + return Err(ShowError::WidgetMissing); + }; - if ui_has_focus { - // If the UI has focus and there were no hover events emitted by the UI, - // then we should emit a None hover event - if events.request.hover.is_empty() { - events.request.hover.send(Hover(None)); + let Some(mut inner) = widget.inner.take() else { + return Err(ShowError::Recursion); + }; + + let output = self.world_scope(|world| inner.show(input, ui, world)); + + if let Some(mut widget) = self.get_mut::>() { + widget.inner = Some(inner); } + + Ok(output) } } -fn site_visualizer_ui_layout( - mut egui_context: EguiContexts, - mut picking_blocker: Option>, - mut events: AppEvents, - levels: LevelParams, - file_menu: Res, - top_level_components: Query<(), Without>, - children: Query<&Children>, - mut menu_params: MenuParams, -) { - egui::SidePanel::right("right_panel") - .resizable(true) - .default_width(300.0) - .show(egui_context.ctx_mut(), |ui| { - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - ui.vertical(|ui| { - CollapsingHeader::new("Levels") - .default_open(true) - .show(ui, |ui| { - ViewLevels::new(&levels, &mut events).show(ui); - }); - ui.separator(); - if ui.add(Button::image_and_text( - events.layers.icons.alignment.egui(), - "Align Drawings", - )) - .on_hover_text("Align all drawings in the site based on their fiducials and measurements") - .clicked() - { - if let Some(site) = events.request.current_workspace.root { - events.request.align_site.send(AlignSiteDrawings(site)); - } - } - if ui.add(Button::image_and_text( - events.layers.icons.exit.egui(), - "Return to site editor" - )).clicked() { - events.next_app_state.set(AppState::SiteEditor); - } - }); - }); - }); - - egui::TopBottomPanel::bottom("log_console") - .resizable(true) - .min_height(30.) - .max_height(300.) - .show(egui_context.ctx_mut(), |ui| { - ui.add_space(10.0); - ConsoleWidget::new(&mut events).show(ui); - }); - - top_menu_bar( - egui_context.ctx_mut(), - &mut events.file_events, - &file_menu, - &top_level_components, - &children, - &mut menu_params, - ); - - let egui_context = egui_context.ctx_mut(); - let ui_has_focus = egui_context.wants_pointer_input() - || egui_context.wants_keyboard_input() - || egui_context.is_pointer_over_area(); - - if let Some(picking_blocker) = &mut picking_blocker { - picking_blocker.ui = ui_has_focus; - } +/// This is a marker trait to indicate that the system state of a widget can be +/// safely shared across multiple renders of the widget. For example, the system +/// parameters do not use the [`Changed`] filter. It is the responsibility of +/// the user to ensure that sharing this widget will not have any bad side +/// effects. +/// +/// [`ShareableWidget`]s can be used by the [`ShowSharedWidget`] trait which is +/// implemented for the [`World`] struct. +pub trait ShareableWidget {} + +/// A resource to store a widget so that it can be reused multiple times in one +/// render pass. +#[derive(Resource)] +pub struct SharedWidget { + state: SystemState, +} - if ui_has_focus { - // If the UI has focus and there were no hover events emitted by the UI, - // then we should emit a None hover event - if events.request.hover.is_empty() { - events.request.hover.send(Hover(None)); +/// This gives a convenient function for rendering a widget using a world. +pub trait ShowSharedWidget { + fn show(&mut self, input: Input, ui: &mut Ui) -> Output + where + W: ShareableWidget + WidgetSystem + 'static; +} + +impl ShowSharedWidget for World { + fn show(&mut self, input: Input, ui: &mut Ui) -> Output + where + W: ShareableWidget + WidgetSystem + 'static, + { + if !self.contains_resource::>() { + let widget = SharedWidget:: { + state: SystemState::new(self), + }; + self.insert_resource(widget); } + + self.resource_scope::, Output>(|world, mut widget| { + let u = W::show(input, ui, &mut widget.state, world); + widget.state.apply(world); + u + }) } } -fn workcell_ui_layout( - mut egui_context: EguiContexts, - mut picking_blocker: Option>, - inspector_params: InspectorParams, - create_params: CreateParams, - mut events: AppEvents, - file_menu: Res, - top_level_components: Query<(), Without>, - children: Query<&Children>, - mut menu_params: MenuParams, +/// This system renders all UI panels in the application and makes sure that the +/// UI rendering works correctly with the picking system, and any other systems +/// as needed. +pub fn site_ui_layout( + world: &mut World, + panel_widgets: &mut QueryState<(Entity, &mut PanelWidget)>, + egui_context_state: &mut SystemState, ) { - egui::SidePanel::right("right_panel") - .resizable(true) - .show(egui_context.ctx_mut(), |ui| { - egui::ScrollArea::both() - .auto_shrink([false, false]) - .show(ui, |ui| { - ui.vertical(|ui| { - CollapsingHeader::new("Inspect") - .default_open(true) - .show(ui, |ui| { - InspectorWidget::new(&inspector_params, &mut events).show(ui); - }); - ui.separator(); - CollapsingHeader::new("Create") - .default_open(true) - .show(ui, |ui| { - CreateWidget::new(&create_params, &mut events).show(ui); - }); - ui.separator(); - }); - }); - }); - - egui::TopBottomPanel::bottom("log_console") - .resizable(true) - .min_height(30.) - .max_height(300.) - .show(egui_context.ctx_mut(), |ui| { - ui.add_space(10.0); - ConsoleWidget::new(&mut events).show(ui); - }); - - top_menu_bar( - egui_context.ctx_mut(), - &mut events.file_events, - &file_menu, - &top_level_components, - &children, - &mut menu_params, - ); - - if events.new_model.asset_gallery_status.show { - egui::SidePanel::left("asset_gallery") - .resizable(true) - .exact_width(320.0) - .show(egui_context.ctx_mut(), |ui| { - NewModel::new(&mut events).show(ui); - }); - } + render_panels(world, panel_widgets, egui_context_state); - let egui_context = egui_context.ctx_mut(); - let ui_has_focus = egui_context.wants_pointer_input() - || egui_context.wants_keyboard_input() - || egui_context.is_pointer_over_area(); + let mut egui_context = egui_context_state.get_mut(world); + let ctx = egui_context.ctx_mut(); + let ui_has_focus = + ctx.wants_pointer_input() || ctx.wants_keyboard_input() || ctx.is_pointer_over_area(); - if let Some(picking_blocker) = &mut picking_blocker { + if let Some(mut picking_blocker) = world.get_resource_mut::() { picking_blocker.ui = ui_has_focus; } if ui_has_focus { // If the UI has focus and there were no hover events emitted by the UI, // then we should emit a None hover event - if events.request.hover.is_empty() { - events.request.hover.send(Hover(None)); + let mut hover = world.resource_mut::>(); + if hover.is_empty() { + hover.send(Hover(None)); } } } diff --git a/rmf_site_editor/src/widgets/move_layer.rs b/rmf_site_editor/src/widgets/move_layer.rs index b8f018b2..c85940dd 100644 --- a/rmf_site_editor/src/widgets/move_layer.rs +++ b/rmf_site_editor/src/widgets/move_layer.rs @@ -22,6 +22,7 @@ use crate::{ use bevy::prelude::*; use bevy_egui::egui::{ImageButton, Ui}; +/// A widget that helps move layers up or down in their ranking. pub struct MoveLayer<'a, 'w, T: Component> { entity: Entity, rank_events: &'a mut EventWriter<'w, ChangeRank>, @@ -43,11 +44,8 @@ impl<'a, 'w, 's, T: Component> MoveLayer<'a, 'w, T> { pub fn show(self, ui: &mut Ui) { MoveLayerButton::to_top(self.entity, self.rank_events, self.icons).show(ui); - MoveLayerButton::up(self.entity, self.rank_events, self.icons).show(ui); - MoveLayerButton::down(self.entity, self.rank_events, self.icons).show(ui); - MoveLayerButton::to_bottom(self.entity, self.rank_events, self.icons).show(ui); } } diff --git a/rmf_site_editor/src/widgets/panel.rs b/rmf_site_editor/src/widgets/panel.rs new file mode 100644 index 00000000..f60d6636 --- /dev/null +++ b/rmf_site_editor/src/widgets/panel.rs @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::{ + ecs::system::{BoxedSystem, SystemState}, + prelude::*, +}; +use bevy_egui::{ + egui::{self, Ui}, + EguiContexts, +}; +use smallvec::SmallVec; + +/// To create a panel widget (a widget that renders itself directly to one of +/// the egui side or top/bottom panels), add this component to an entity. +/// +/// Use the context field of the input to create a panel with one of the following: +/// - [`EguiPanel::show`] +/// - [`egui::SidePanel::left`] +/// - [`egui::SidePanel::right`] +/// - [`egui::TopBottomPanel::top`] +/// - [`egui::TopBottomPanel::bottom`] +#[derive(Component)] +pub struct PanelWidget { + inner: Option>, +} + +/// Input provided to panel widgets. +pub struct PanelWidgetInput { + /// The entity of the panel widget. + pub id: Entity, + /// The context that the panel should use for rendering. + pub context: egui::Context, +} + +impl PanelWidget { + /// Pass in a system that takes takes [`PanelWidgetInput`] as its input parameter. + pub fn new>(system: S, world: &mut World) -> Self { + let mut system = Box::new(IntoSystem::into_system(system)); + system.initialize(world); + Self { + inner: Some(system), + } + } +} + +/// This function can be used to render all panels in an application, either by +/// adding this function to a schedule as a system or by calling it from inside +/// of an exclusive system. Note that this is automatically run by +/// [`site_ui_layout`][1] so there is no need to use this function yourself +/// unless you are not using the [`StandardUiPlugin`][2]. +/// +/// [1]: crate::widgets::site_ui_layout +/// [2]: crate::widgets::StandardUiPlugin +pub fn render_panels( + world: &mut World, + panel_widgets: &mut QueryState<(Entity, &mut PanelWidget)>, + egui_contexts: &mut SystemState, +) { + let context = egui_contexts.get_mut(world).ctx_mut().clone(); + let mut panels: SmallVec<[_; 16]> = panel_widgets + .iter_mut(world) + .map(|(entity, mut widget)| { + ( + entity, + widget + .inner + .take() + .expect("Inner system of PanelWidget is missing"), + ) + }) + .collect(); + + for (e, inner) in &mut panels { + inner.run( + PanelWidgetInput { + id: *e, + context: context.clone(), + }, + world, + ); + inner.apply_deferred(world); + } + + for (e, inner) in panels { + if let Some(mut widget) = world.get_mut::(e) { + let _ = widget.inner.insert(inner); + } + } +} + +/// Indicate which side a panel is on +#[derive(Clone, Copy, Debug, Component)] +pub enum PanelSide { + Top, + Bottom, + Left, + Right, +} + +/// Wrapper to hold either a vertical or horizontal egui panel +pub enum EguiPanel { + Vertical(egui::SidePanel), + Horizontal(egui::TopBottomPanel), +} + +impl EguiPanel { + /// Modify this panel if it's a vertical panel + pub fn map_vertical(self, f: impl FnOnce(egui::SidePanel) -> egui::SidePanel) -> Self { + match self { + Self::Vertical(panel) => Self::Vertical(f(panel)), + other => other, + } + } + + /// Modify this panel if it's a horizontal panel + pub fn map_horizontal( + self, + f: impl FnOnce(egui::TopBottomPanel) -> egui::TopBottomPanel, + ) -> Self { + match self { + Self::Horizontal(panel) => Self::Horizontal(f(panel)), + other => other, + } + } + + /// Display something in this panel. + pub fn show( + self, + ctx: &egui::Context, + add_content: impl FnOnce(&mut Ui) -> R, + ) -> egui::InnerResponse { + match self { + Self::Vertical(panel) => panel.show(ctx, add_content), + Self::Horizontal(panel) => panel.show(ctx, add_content), + } + } +} + +impl PanelSide { + /// Is the long direction of the panel horizontal + pub fn is_horizontal(&self) -> bool { + matches!(self, Self::Top | Self::Bottom) + } + + /// Is the long direction of the panel vertical + pub fn is_vertical(&self) -> bool { + matches!(self, Self::Left | Self::Right) + } + + /// Align the Ui to line up with the long direction of the panel + pub fn align(self, ui: &mut Ui, f: impl FnOnce(&mut Ui) -> R) -> egui::InnerResponse { + if self.is_horizontal() { + ui.horizontal(f) + } else { + ui.vertical(f) + } + } + + /// Align the Ui to run orthogonal to long direction of the panel, + /// i.e. the Ui will run along the short direction of the panel. + pub fn orthogonal( + self, + ui: &mut Ui, + f: impl FnOnce(&mut Ui) -> R, + ) -> egui::InnerResponse { + if self.is_horizontal() { + ui.vertical(f) + } else { + ui.horizontal(f) + } + } + + /// Get the egui panel that is associated with this panel type. + pub fn get_panel(self) -> EguiPanel { + match self { + Self::Left => EguiPanel::Vertical(egui::SidePanel::left("left_panel")), + Self::Right => EguiPanel::Vertical(egui::SidePanel::right("right_panel")), + Self::Top => EguiPanel::Horizontal(egui::TopBottomPanel::top("top_panel")), + Self::Bottom => EguiPanel::Horizontal(egui::TopBottomPanel::bottom("bottom_panel")), + } + } +} diff --git a/rmf_site_editor/src/widgets/panel_of_tiles.rs b/rmf_site_editor/src/widgets/panel_of_tiles.rs new file mode 100644 index 00000000..57f5d40b --- /dev/null +++ b/rmf_site_editor/src/widgets/panel_of_tiles.rs @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::widgets::prelude::*; +use bevy::prelude::*; +use bevy_egui::egui; +use smallvec::SmallVec; + +/// Input type for [`WidgetSystem`]s that can be put into a "Panel of Tiles" +/// widget, such as the [`PropertiesPanel`]. See [`PropertiesTilePlugin`] for a +/// usage example. +pub struct Tile { + /// The entity of the tile widget which is being rendered. This lets you + /// store additional component data inside the entity which may be relevant + /// to your widget. + pub id: Entity, + /// What kind of panel is this tile inside of. Use this if you want your + /// widget layout to be different based on what kind of panel it was placed + /// in. + pub panel: PanelSide, +} + +/// Reusable widget that defines a panel with "tiles" where each tile is a child widget. +pub fn show_panel_of_tiles( + In(PanelWidgetInput { id, context }): In, + world: &mut World, +) { + let children: Option> = world + .get::(id) + .map(|children| children.iter().copied().collect()); + + let Some(children) = children else { + return; + }; + if children.is_empty() { + // Do not even begin to create a panel if there are no children to render + return; + } + + let Some(side) = world.get::(id) else { + error!("Side component missing for panel_of_tiles_widget {id:?}"); + return; + }; + + let side = *side; + side.get_panel() + .map_vertical(|panel| { + // TODO(@mxgrey): Make this configurable via a component + panel.resizable(true).default_width(300.0) + }) + .show(&context, |ui| { + egui::ScrollArea::both() + .auto_shrink([false, false]) + .show(ui, |ui| { + for child in children { + let tile = Tile { + id: child, + panel: side, + }; + if let Err(err) = world.try_show_in(child, tile, ui) { + error!( + "Could not render child widget {child:?} in \ + tile panel {id:?} on side {side:?}: {err:?}" + ); + } + } + }); + }); +} diff --git a/rmf_site_editor/src/widgets/properties_panel.rs b/rmf_site_editor/src/widgets/properties_panel.rs new file mode 100644 index 00000000..f99e218b --- /dev/null +++ b/rmf_site_editor/src/widgets/properties_panel.rs @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::widgets::{ + show_panel_of_tiles, BuildingPreviewPlugin, CreationPlugin, PanelSide, PanelWidget, + StandardInspectorPlugin, Tile, ViewGroupsPlugin, ViewLayersPlugin, ViewLevelsPlugin, + ViewLightsPlugin, ViewNavGraphsPlugin, ViewOccupancyPlugin, Widget, WidgetSystem, +}; +use bevy::prelude::*; + +/// This plugins produces the standard properties panel. This is the panel which +/// includes widgets to display and edit all the properties in a site that we +/// expect are needed by common use cases of the editor. +#[derive(Default)] +pub struct StandardPropertiesPanelPlugin {} + +impl Plugin for StandardPropertiesPanelPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + PropertiesPanelPlugin::new(PanelSide::Right), + ViewLevelsPlugin::default(), + ViewNavGraphsPlugin::default(), + ViewLayersPlugin::default(), + StandardInspectorPlugin::default(), + CreationPlugin::default(), + ViewGroupsPlugin::default(), + ViewLightsPlugin::default(), + ViewOccupancyPlugin::default(), + BuildingPreviewPlugin::default(), + )); + } +} + +/// Use this plugin to add a single tile into the properties panel. +/// +/// ```no_run +/// use bevy::prelude::{App, Query, Entity, Res}; +/// use librmf_site_editor::{ +/// SiteEditor, workspace::CurrentWorkspace, +/// site::NameOfSite, +/// widgets::prelude::*, +/// }; +/// +/// #[derive(SystemParam)] +/// pub struct HelloSiteWidget<'w, 's> { +/// sites: Query<'w, 's, &'static NameOfSite>, +/// current: Res<'w, CurrentWorkspace>, +/// } +/// +/// impl<'w, 's> WidgetSystem for HelloSiteWidget<'w, 's> { +/// fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { +/// let mut params = state.get_mut(world); +/// if let Some(name) = params.current.root.map(|e| params.sites.get(e).ok()).flatten() { +/// ui.add_space(20.0); +/// ui.heading(format!("Hello, {}!", name.0)); +/// } +/// } +/// } +/// +/// fn main() { +/// let mut app = App::new(); +/// app.add_plugins(( +/// SiteEditor::default(), +/// PropertiesTilePlugin::::new(), +/// )); +/// +/// app.run(); +/// } +/// ``` +pub struct PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + _ignore: std::marker::PhantomData, +} + +impl PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + pub fn new() -> Self { + Self { + _ignore: Default::default(), + } + } +} + +impl Plugin for PropertiesTilePlugin +where + W: WidgetSystem + 'static + Send + Sync, +{ + fn build(&self, app: &mut App) { + let widget = Widget::::new::(&mut app.world); + let properties_panel = app.world.resource::().id; + app.world.spawn(widget).set_parent(properties_panel); + } +} + +/// Get the ID of the properties panel. +#[derive(Resource)] +pub struct PropertiesPanel { + side: PanelSide, + id: Entity, +} + +impl PropertiesPanel { + pub fn side(&self) -> PanelSide { + self.side + } + + pub fn id(&self) -> Entity { + self.id + } +} + +/// This plugin builds a properties panel for the editor. It is usually recommended +/// to use [`StandardPropertiesPanelPlugin`] unless you need very specific +/// customization of the properties panel. +pub struct PropertiesPanelPlugin { + side: PanelSide, +} + +impl PropertiesPanelPlugin { + pub fn new(side: PanelSide) -> Self { + Self { side } + } +} + +impl Default for PropertiesPanelPlugin { + fn default() -> Self { + Self::new(PanelSide::Right) + } +} + +impl Plugin for PropertiesPanelPlugin { + fn build(&self, app: &mut App) { + let widget = PanelWidget::new(show_panel_of_tiles, &mut app.world); + let id = app.world.spawn((widget, self.side)).id(); + app.world.insert_resource(PropertiesPanel { + side: self.side, + id, + }); + } +} diff --git a/rmf_site_editor/src/site/file_menu.rs b/rmf_site_editor/src/widgets/sdf_export_menu.rs similarity index 86% rename from rmf_site_editor/src/site/file_menu.rs rename to rmf_site_editor/src/widgets/sdf_export_menu.rs index c73516f8..4b8ff591 100644 --- a/rmf_site_editor/src/site/file_menu.rs +++ b/rmf_site_editor/src/widgets/sdf_export_menu.rs @@ -22,17 +22,17 @@ use std::collections::HashSet; /// Keeps track of which entity is associated to the export sdf button. #[derive(Resource)] -pub struct ExportSdfMenu { +pub struct SdfExportMenu { export_sdf: Entity, } -impl ExportSdfMenu { +impl SdfExportMenu { pub fn get(&self) -> Entity { self.export_sdf } } -impl FromWorld for ExportSdfMenu { +impl FromWorld for SdfExportMenu { fn from_world(world: &mut World) -> Self { let site_states = HashSet::from([ AppState::SiteEditor, @@ -48,13 +48,13 @@ impl FromWorld for ExportSdfMenu { .set_parent(file_header) .id(); - ExportSdfMenu { export_sdf } + SdfExportMenu { export_sdf } } } -pub fn handle_export_sdf_menu_events( +fn handle_export_sdf_menu_events( mut menu_events: EventReader, - sdf_menu: Res, + sdf_menu: Res, mut save_events: EventWriter, ) { for event in menu_events.read() { @@ -68,11 +68,11 @@ pub fn handle_export_sdf_menu_events( } #[derive(Default)] -pub struct SiteFileMenuPlugin; +pub struct SdfExportMenuPlugin {} -impl Plugin for SiteFileMenuPlugin { +impl Plugin for SdfExportMenuPlugin { fn build(&self, app: &mut App) { - app.init_resource::().add_systems( + app.init_resource::().add_systems( Update, handle_export_sdf_menu_events.run_if(AppState::in_site_mode()), ); diff --git a/rmf_site_editor/src/widgets/selector_widget.rs b/rmf_site_editor/src/widgets/selector_widget.rs new file mode 100644 index 00000000..5db89737 --- /dev/null +++ b/rmf_site_editor/src/widgets/selector_widget.rs @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + interaction::{Hover, Select, Selection}, + site::SiteID, + widgets::{prelude::*, Icons}, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{Button, Ui}; + +/// A widget that can be used to select entities. +#[derive(SystemParam)] +pub struct SelectorWidget<'w, 's> { + pub site_id: Query<'w, 's, &'static SiteID>, + pub icons: Res<'w, Icons>, + pub selection: Res<'w, Selection>, + pub select: EventWriter<'w, Select>, + pub hover: EventWriter<'w, Hover>, +} + +impl<'w, 's> WidgetSystem for SelectorWidget<'w, 's> { + fn show(entity: Entity, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + params.show_widget(entity, ui); + } +} + +impl<'w, 's> SelectorWidget<'w, 's> { + pub fn show_widget(&mut self, entity: Entity, ui: &mut Ui) { + let site_id = self.site_id.get(entity).ok().cloned(); + let is_selected = self.selection.0.is_some_and(|s| s == entity); + + let text = match site_id { + Some(id) => format!("#{}", id.0), + None => "*".to_owned(), + }; + + let icon = if is_selected { + self.icons.selected.egui() + } else { + self.icons.select.egui() + }; + + let response = ui.add(Button::image_and_text(icon, text)); + + if response.clicked() { + self.select.send(Select(Some(entity))); + } else if response.hovered() { + self.hover.send(Hover(Some(entity))); + } + + response.on_hover_text("Select"); + } +} + +impl<'w, 's> ShareableWidget for SelectorWidget<'w, 's> {} diff --git a/rmf_site_editor/src/widgets/view_groups.rs b/rmf_site_editor/src/widgets/view_groups.rs index f6f0ceaf..b495462a 100644 --- a/rmf_site_editor/src/widgets/view_groups.rs +++ b/rmf_site_editor/src/widgets/view_groups.rs @@ -17,82 +17,86 @@ use crate::{ site::{Change, FiducialMarker, MergeGroups, NameInSite, SiteID, Texture}, - widgets::{inspector::SelectionWidget, AppEvents}, - Icons, + widgets::{prelude::*, SelectorWidget}, + AppState, CurrentWorkspace, Icons, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{Button, CollapsingHeader, Ui}; -#[derive(Default, Clone, Copy)] -pub enum GroupViewMode { - #[default] - View, - SelectMergeFrom, - MergeFrom(Entity), - Delete, -} - -#[derive(Default, Resource)] -pub struct GroupViewModes { - site: Option, - textures: GroupViewMode, - fiducials: GroupViewMode, -} +/// Add a widget for viewing different kinds of groups. +#[derive(Default)] +pub struct ViewGroupsPlugin {} -impl GroupViewModes { - pub fn reset(&mut self, site: Entity) { - *self = GroupViewModes::default(); - self.site = Some(site); +impl Plugin for ViewGroupsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct GroupParams<'w, 's> { +pub struct ViewGroups<'w, 's> { children: Query<'w, 's, &'static Children>, textures: Query<'w, 's, (&'static NameInSite, Option<&'static SiteID>), With>, fiducials: Query<'w, 's, (&'static NameInSite, Option<&'static SiteID>), With>, icons: Res<'w, Icons>, group_view_modes: ResMut<'w, GroupViewModes>, + app_state: Res<'w, State>, + events: ViewGroupsEvents<'w, 's>, } -pub struct ViewGroups<'a, 'w1, 's1, 'w2, 's2> { - params: &'a mut GroupParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, +#[derive(SystemParam)] +pub struct ViewGroupsEvents<'w, 's> { + current_workspace: ResMut<'w, CurrentWorkspace>, + selector: SelectorWidget<'w, 's>, + merge_groups: EventWriter<'w, MergeGroups>, + name: EventWriter<'w, Change>, + commands: Commands<'w, 's>, } -impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { - pub fn new(params: &'a mut GroupParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } +impl<'w, 's> WidgetSystem for ViewGroups<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state.get() != AppState::SiteEditor { + return; + } + CollapsingHeader::new("Groups") + .default_open(false) + .show(ui, |ui| { + params.show_widget(ui); + }); } +} - pub fn show(self, ui: &mut Ui) { - let Some(site) = self.events.request.current_workspace.root else { +impl<'w, 's> ViewGroups<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let Some(site) = self.events.current_workspace.root else { return; }; - let modes = &mut *self.params.group_view_modes; + let modes = &mut *self.group_view_modes; if !modes.site.is_some_and(|s| s == site) { modes.reset(site); } - let Ok(children) = self.params.children.get(site) else { + let Ok(children) = self.children.get(site) else { return; }; CollapsingHeader::new("Textures").show(ui, |ui| { Self::show_groups( children, - &self.params.textures, + &self.textures, &mut modes.textures, - &self.params.icons, - self.events, + &self.icons, + &mut self.events, ui, ); }); CollapsingHeader::new("Fiducials").show(ui, |ui| { Self::show_groups( children, - &self.params.fiducials, + &self.fiducials, &mut modes.fiducials, - &self.params.icons, - self.events, + &self.icons, + &mut self.events, ui, ); }); @@ -103,7 +107,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { q_groups: &Query<(&NameInSite, Option<&SiteID>), With>, mode: &mut GroupViewMode, icons: &Res, - events: &mut AppEvents, + events: &mut ViewGroupsEvents, ui: &mut Ui, ) { ui.horizontal(|ui| match mode { @@ -155,7 +159,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { ui.horizontal(|ui| { match mode.clone() { GroupViewMode::View => { - SelectionWidget::new(*child, site_id.cloned(), &icons, events).show(ui); + events.selector.show_widget(*child, ui); } GroupViewMode::SelectMergeFrom => { if ui @@ -181,7 +185,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { .on_hover_text("Merge into this group") .clicked() { - events.change.merge_groups.send(MergeGroups { + events.merge_groups.send(MergeGroups { from_group: merge_from, into_group: *child, }); @@ -203,12 +207,32 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { let mut new_name = name.0.clone(); if ui.text_edit_singleline(&mut new_name).changed() { - events - .change - .name - .send(Change::new(NameInSite(new_name), *child)); + events.name.send(Change::new(NameInSite(new_name), *child)); } }); } } } + +#[derive(Default, Clone, Copy)] +pub enum GroupViewMode { + #[default] + View, + SelectMergeFrom, + MergeFrom(Entity), + Delete, +} + +#[derive(Default, Resource)] +pub struct GroupViewModes { + site: Option, + textures: GroupViewMode, + fiducials: GroupViewMode, +} + +impl GroupViewModes { + pub fn reset(&mut self, site: Entity) { + *self = GroupViewModes::default(); + self.site = Some(site); + } +} diff --git a/rmf_site_editor/src/widgets/view_layers.rs b/rmf_site_editor/src/widgets/view_layers.rs index 5ca14202..3b159f7d 100644 --- a/rmf_site_editor/src/widgets/view_layers.rs +++ b/rmf_site_editor/src/widgets/view_layers.rs @@ -19,14 +19,29 @@ use crate::{ interaction::Selection, recency::RecencyRanking, site::*, - widgets::{inspector::InspectLayer, AppEvents, Icons, MoveLayer}, + widgets::{ + inspector::{InspectLayer, InspectLayerInput}, + prelude::*, + Icons, MoveLayer, + }, + AppState, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{Button, CollapsingHeader, DragValue, ScrollArea, Ui}; +/// Add a widget for viewing a list of layers +#[derive(Default)] +pub struct ViewLayersPlugin {} + +impl Plugin for ViewLayersPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(PropertiesTilePlugin::::new()); + } +} + #[derive(SystemParam)] -pub struct LayersParams<'w, 's> { - pub floors: Query< +pub struct ViewLayers<'w, 's> { + floors: Query< 'w, 's, ( @@ -34,7 +49,7 @@ pub struct LayersParams<'w, 's> { &'static GlobalFloorVisibility, ), >, - pub drawings: Query< + drawings: Query< 'w, 's, ( @@ -42,51 +57,43 @@ pub struct LayersParams<'w, 's> { &'static GlobalDrawingVisibility, ), >, - pub layer_visibility: Query< - 'w, - 's, - ( - Option<&'static LayerVisibility>, - &'static PreferredSemiTransparency, - ), - >, - pub levels: Query< - 'w, - 's, - ( - &'static GlobalFloorVisibility, - &'static GlobalDrawingVisibility, - ), - >, - pub site_id: Query<'w, 's, Option<&'static SiteID>>, - pub icons: Res<'w, Icons>, - pub selection: Res<'w, Selection>, + icons: Res<'w, Icons>, + selection: Res<'w, Selection>, + current_level: Res<'w, CurrentLevel>, + global_floor_vis: EventWriter<'w, Change>, + global_drawing_vis: EventWriter<'w, Change>, + view_layer: InspectLayer<'w, 's>, + app_state: Res<'w, State>, } -pub struct ViewLayers<'a, 'w1, 's1, 'w2, 's2> { - params: &'a LayersParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, -} - -impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { - pub fn new(params: &'a LayersParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } +impl<'w, 's> WidgetSystem for ViewLayers<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state.get() != AppState::SiteEditor { + return; + } + ui.separator(); + CollapsingHeader::new("Layers") + .default_open(false) + .show(ui, |ui| { + params.show_widget(ui); + }); } +} - pub fn show(mut self, ui: &mut Ui) { - let current_level = match &self.events.request.current_level.0 { - Some(s) => *s, - None => return, +impl<'w, 's> ViewLayers<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let Some(current_level) = self.current_level.0 else { + return; }; let has_drawings = self - .params .drawings .get(current_level) .ok() .is_some_and(|(ranking, _)| !ranking.is_empty()); - if let Ok((ranking, global)) = self.params.floors.get(current_level) { + if let Ok((ranking, global)) = self.floors.get(current_level) { CollapsingHeader::new("Floors") .default_open(true) .show(ui, |ui| { @@ -101,22 +108,25 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { ) }; let default_alpha = &mut shown_global.preferred_semi_transparency; - self.show_global(text, vis, default_alpha, ui); + Self::show_global(text, &self.icons, vis, default_alpha, ui); if shown_global != *global { - self.events - .layers - .global_floor_vis + self.global_floor_vis .send(Change::new(shown_global, current_level)); } }); ui.separator(); - if let Some(selected) = self.show_rankings(ranking.entities(), true, ui) { + if let Some(selected) = Self::show_rankings( + ranking.entities(), + &self.selection, + &mut self.view_layer, + ui, + ) { ui.horizontal(|ui| { MoveLayer::new( selected, - &mut self.events.layers.floors, - &self.events.layers.icons, + &mut self.view_layer.floor_change_rank, + &self.icons, ) .show(ui); }); @@ -125,7 +135,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { }); } - if let Ok((ranking, global)) = self.params.drawings.get(current_level) { + if let Ok((ranking, global)) = self.drawings.get(current_level) { CollapsingHeader::new("Drawings") .default_open(true) .show(ui, |ui| { @@ -133,12 +143,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { ui.horizontal(|ui| { let vis = &mut shown_global.general; let default_alpha = &mut shown_global.preferred_general_semi_transparency; - self.show_global("Global (general)", vis, default_alpha, ui); + Self::show_global("Global (general)", &self.icons, vis, default_alpha, ui); }); ui.horizontal(|ui| { let vis = &mut shown_global.bottom; let default_alpha = &mut shown_global.preferred_bottom_semi_transparency; - self.show_global("Global (bottom)", vis, default_alpha, ui); + Self::show_global("Global (bottom)", &self.icons, vis, default_alpha, ui); }); ui.horizontal(|ui| { ui.label("Bottom Count").on_hover_text( @@ -155,18 +165,21 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { }); if shown_global != *global { - self.events - .layers - .global_drawing_vis + self.global_drawing_vis .send(Change::new(shown_global, current_level)); } - if let Some(selected) = self.show_rankings(ranking.entities(), false, ui) { + if let Some(selected) = Self::show_rankings( + ranking.entities(), + &self.selection, + &mut self.view_layer, + ui, + ) { ui.horizontal(|ui| { MoveLayer::new( selected, - &mut self.events.layers.drawings, - &self.events.layers.icons, + &mut self.view_layer.drawing_change_rank, + &self.icons, ) .show(ui); }); @@ -176,13 +189,13 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { } fn show_global( - &self, text: &str, + icons: &Icons, vis: &mut LayerVisibility, default_alpha: &mut f32, ui: &mut Ui, ) { - let icon = self.params.icons.layer_visibility_of(Some(*vis)); + let icon = icons.layer_visibility_of(Some(*vis)); if ui .add(Button::image_and_text(icon, text)) .on_hover_text(format!("Change to {}", vis.next(*default_alpha).label())) @@ -202,35 +215,20 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { } fn show_rankings( - &mut self, ranking: &Vec, - is_floor: bool, + selection: &Selection, + view_layer: &mut InspectLayer, ui: &mut Ui, ) -> Option { let mut layer_selected = None; ui.vertical(|ui| { ScrollArea::vertical().show(ui, |ui| { - for e in ranking.iter().rev() { - let mut as_selected = false; - if self.params.selection.0.is_some_and(|sel| sel == *e) { - as_selected = true; - layer_selected = Some(*e); + for id in ranking.iter().rev().copied() { + if selection.0.is_some_and(|s| s == id) { + layer_selected = Some(id); } - let Ok((vis, alpha)) = self.params.layer_visibility.get(*e) else { - continue; - }; ui.horizontal(|ui| { - InspectLayer::new( - *e, - &self.params.icons, - &mut self.events, - vis.copied(), - alpha.0, - is_floor, - ) - .with_selecting(self.params.site_id.get(*e).ok().flatten().copied()) - .as_selected(as_selected) - .show(ui); + view_layer.show_widget(InspectLayerInput::new(id).with_selecting(), ui); }); } }); diff --git a/rmf_site_editor/src/widgets/view_levels.rs b/rmf_site_editor/src/widgets/view_levels.rs index b65c5770..ef7ebb84 100644 --- a/rmf_site_editor/src/widgets/view_levels.rs +++ b/rmf_site_editor/src/widgets/view_levels.rs @@ -17,106 +17,106 @@ use crate::{ site::{ - Category, Change, Delete, DrawingMarker, FloorMarker, LevelElevation, LevelProperties, - NameInSite, + Category, Change, CurrentLevel, Delete, DrawingMarker, FloorMarker, LevelElevation, + LevelProperties, NameInSite, }, - widgets::{AppEvents, Icons}, - RecencyRanking, + widgets::{prelude::*, Icons}, + AppState, CurrentWorkspace, RecencyRanking, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{DragValue, ImageButton, Ui}; +use bevy_egui::egui::{CollapsingHeader, DragValue, ImageButton, Ui}; use std::cmp::{Ordering, Reverse}; -#[derive(Resource)] -pub struct LevelDisplay { - pub new_elevation: f32, - pub new_name: String, - pub order: Vec, - pub freeze: bool, - pub removing: bool, -} +/// Add a plugin for viewing and editing a list of all levels +#[derive(Default)] +pub struct ViewLevelsPlugin {} -impl Default for LevelDisplay { - fn default() -> Self { - Self { - new_elevation: 0.0, - new_name: "".to_string(), - order: Vec::new(), - freeze: false, - removing: false, - } +impl Plugin for ViewLevelsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct LevelParams<'w, 's> { - pub levels: Query<'w, 's, (Entity, &'static NameInSite, &'static LevelElevation)>, - pub parents: Query<'w, 's, &'static Parent>, - pub icons: Res<'w, Icons>, +pub struct ViewLevels<'w, 's> { + levels: Query<'w, 's, (Entity, &'static NameInSite, &'static LevelElevation)>, + parents: Query<'w, 's, &'static Parent>, + icons: Res<'w, Icons>, + display_levels: ResMut<'w, LevelDisplay>, + current_level: ResMut<'w, CurrentLevel>, + current_workspace: ResMut<'w, CurrentWorkspace>, + change_name: EventWriter<'w, Change>, + change_level_elevation: EventWriter<'w, Change>, + delete: EventWriter<'w, Delete>, + commands: Commands<'w, 's>, + app_state: Res<'w, State>, } -pub struct ViewLevels<'a, 'w1, 's1, 'w2, 's2> { - params: &'a LevelParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, - edit_visibility: bool, +impl<'w, 's> WidgetSystem for ViewLevels<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { + let mut params = state.get_mut(world); + CollapsingHeader::new("Levels") + .default_open(true) + .show(ui, |ui| { + params.show_widget(ui); + }); + } } -impl<'a, 'w1, 's1, 'w2, 's2> ViewLevels<'a, 'w1, 's1, 'w2, 's2> { - pub fn new(params: &'a LevelParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { - params, - events, - edit_visibility: false, - } - } +impl<'w, 's> ViewLevels<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let editing = match self.app_state.get() { + AppState::SiteEditor => true, + AppState::SiteVisualizer => false, + _ => return, + }; - pub fn for_editing_visibility(mut self) -> Self { - self.edit_visibility = true; - self - } + if !editing { + self.display_levels.removing = false; + } - pub fn show(self, ui: &mut Ui) { - ui.horizontal(|ui| { - let make_new_level = ui.button("Add").clicked(); - let mut show_elevation = self.events.display.level.new_elevation; - ui.add(DragValue::new(&mut show_elevation).suffix("m")) - .on_hover_text("Elevation for the new level"); - - let mut show_name = self.events.display.level.new_name.clone(); - ui.text_edit_singleline(&mut show_name) - .on_hover_text("Name for the new level"); - - if make_new_level { - let new_level = self - .events - .commands - .spawn(( - SpatialBundle::default(), - LevelProperties { - elevation: LevelElevation(show_elevation), - name: NameInSite(show_name.clone()), - ..Default::default() - }, - Category::Level, - RecencyRanking::::default(), - RecencyRanking::::default(), - )) - .id(); - self.events.request.current_level.0 = Some(new_level); - } + if editing { + ui.horizontal(|ui| { + let make_new_level = ui.button("Add").clicked(); + let mut show_elevation = self.display_levels.new_elevation; + ui.add(DragValue::new(&mut show_elevation).suffix("m")) + .on_hover_text("Elevation for the new level"); + + let mut show_name = self.display_levels.new_name.clone(); + ui.text_edit_singleline(&mut show_name) + .on_hover_text("Name for the new level"); + + if make_new_level { + let new_level = self + .commands + .spawn(( + SpatialBundle::default(), + LevelProperties { + elevation: LevelElevation(show_elevation), + name: NameInSite(show_name.clone()), + ..Default::default() + }, + Category::Level, + RecencyRanking::::default(), + RecencyRanking::::default(), + )) + .id(); + self.current_level.0 = Some(new_level); + } - self.events.display.level.new_elevation = show_elevation; - self.events.display.level.new_name = show_name; - }); + self.display_levels.new_elevation = show_elevation; + self.display_levels.new_name = show_name; + }); + } - if !self.events.display.level.freeze { + if !self.display_levels.freeze { let mut ordered_level_list: Vec<_> = self - .params .levels .iter() .filter(|(e, _, _)| { - AncestorIter::new(&self.params.parents, *e) - .any(|e| Some(e) == self.events.request.current_workspace.root) + AncestorIter::new(&self.parents, *e) + .any(|e| Some(e) == self.current_workspace.root) }) .map(|(e, _, elevation)| (Reverse(elevation.0), e)) .collect(); @@ -132,51 +132,44 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLevels<'a, 'w1, 's1, 'w2, 's2> { } }); - self.events.display.level.order = - ordered_level_list.into_iter().map(|(_, e)| e).collect(); + self.display_levels.order = ordered_level_list.into_iter().map(|(_, e)| e).collect(); } - if self.events.display.level.removing { + if self.display_levels.removing { ui.horizontal(|ui| { if ui.button("Select").clicked() { - self.events.display.level.removing = false; + self.display_levels.removing = false; } ui.label("Remove"); }); - } else { + } else if editing { ui.horizontal(|ui| { ui.label("Select"); if ui.button("Remove").clicked() { - self.events.display.level.removing = true; + self.display_levels.removing = true; } }); } let mut any_dragging = false; let mut any_deleted = false; - for e in self.events.display.level.order.iter().copied() { - if let Ok((_, name, elevation)) = self.params.levels.get(e) { + for e in self.display_levels.order.iter().copied() { + if let Ok((_, name, elevation)) = self.levels.get(e) { let mut shown_elevation = elevation.clone().0; let mut shown_name = name.clone().0; ui.horizontal(|ui| { - if self.events.display.level.removing { + if self.display_levels.removing { if ui - .add(ImageButton::new(self.params.icons.trash.egui())) + .add(ImageButton::new(self.icons.trash.egui())) .on_hover_text("Remove this level") .clicked() { - self.events - .request - .delete - .send(Delete::new(e).and_dependents()); + self.delete.send(Delete::new(e).and_dependents()); any_deleted = true; } - } else if self.edit_visibility == true { - if ui - .radio(Some(e) == **self.events.request.current_level, "") - .clicked() - { - self.events.request.current_level.0 = Some(e); + } else if editing { + if ui.radio(Some(e) == **self.current_level, "").clicked() { + self.current_level.0 = Some(e); } } @@ -192,24 +185,41 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLevels<'a, 'w1, 's1, 'w2, 's2> { }); if shown_name != name.0 { - self.events - .change - .name + self.change_name .send(Change::new(NameInSite(shown_name), e)); } if shown_elevation != elevation.0 { - self.events - .change - .level_elevation + self.change_level_elevation .send(Change::new(LevelElevation(shown_elevation), e)); } } } - self.events.display.level.freeze = any_dragging; + self.display_levels.freeze = any_dragging; if any_deleted { - self.events.display.level.removing = false; + self.display_levels.removing = false; + } + } +} + +#[derive(Resource)] +pub struct LevelDisplay { + pub new_elevation: f32, + pub new_name: String, + pub order: Vec, + pub freeze: bool, + pub removing: bool, +} + +impl Default for LevelDisplay { + fn default() -> Self { + Self { + new_elevation: 0.0, + new_name: "".to_string(), + order: Vec::new(), + freeze: false, + removing: false, } } } diff --git a/rmf_site_editor/src/widgets/view_lights.rs b/rmf_site_editor/src/widgets/view_lights.rs index 84a87ab5..aa2df1c4 100644 --- a/rmf_site_editor/src/widgets/view_lights.rs +++ b/rmf_site_editor/src/widgets/view_lights.rs @@ -16,84 +16,79 @@ */ use crate::{ - icons::Icons, - interaction::Select, + interaction::{HeadlightToggle, Select}, site::{ - Angle, Category, ExportLights, Light, LightKind, Pose, Recall, RecallLightKind, Rotation, - SiteID, + Angle, Category, ExportLights, Light, LightKind, PhysicalLightToggle, Pose, Recall, + RecallLightKind, Rotation, SiteID, }, widgets::{ - inspector::{InspectLightKind, InspectPose, SelectionWidget}, - AppEvents, + inspector::{InspectLightKind, InspectPoseComponent}, + prelude::*, + SelectorWidget, }, + AppState, }; use bevy::{ ecs::system::SystemParam, prelude::*, tasks::{AsyncComputeTaskPool, Task}, }; -use bevy_egui::egui::Ui; +use bevy_egui::egui::{CollapsingHeader, Ui}; use futures_lite::future; #[cfg(not(target_arch = "wasm32"))] use rfd::AsyncFileDialog; use std::cmp::Reverse; use std::collections::BTreeMap; -#[derive(Resource)] -pub struct LightDisplay { - pub pose: Pose, - pub kind: LightKind, - pub recall: RecallLightKind, - pub choosing_file_for_export: Option>>, - pub export_file: Option, -} +/// Add a plugin for viewing and editing a list of all lights +#[derive(Default)] +pub struct ViewLightsPlugin {} -impl Default for LightDisplay { - fn default() -> Self { - Self { - pose: Pose { - trans: [0.0, 0.0, 2.6], - rot: Rotation::EulerExtrinsicXYZ([ - Angle::Deg(0.0), - Angle::Deg(0.0), - Angle::Deg(0.0), - ]), - }, - kind: Default::default(), - recall: Default::default(), - choosing_file_for_export: None, - export_file: None, - } +impl Plugin for ViewLightsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct LightParams<'w, 's> { - pub lights: Query<'w, 's, (Entity, &'static LightKind, Option<&'static SiteID>)>, - pub icons: Res<'w, Icons>, -} - -pub struct ViewLights<'a, 'w1, 's1, 'w2, 's2> { - params: &'a LightParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, +pub struct ViewLights<'w, 's> { + lights: Query<'w, 's, (Entity, &'static LightKind, Option<&'static SiteID>)>, + toggle_headlights: ResMut<'w, HeadlightToggle>, + toggle_physical_lights: ResMut<'w, PhysicalLightToggle>, + export_lights: EventWriter<'w, ExportLights>, + display_light: ResMut<'w, LightDisplay>, + selector: SelectorWidget<'w, 's>, + commands: Commands<'w, 's>, + app_state: Res<'w, State>, } -impl<'a, 'w1, 's1, 'w2, 's2> ViewLights<'a, 'w1, 's1, 'w2, 's2> { - pub fn new(params: &'a LightParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } +impl<'w, 's> WidgetSystem for ViewLights<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state.get() != AppState::SiteEditor { + return; + } + CollapsingHeader::new("Lights") + .default_open(false) + .show(ui, |ui| { + params.show_widget(ui); + }); } +} - pub fn show(self, ui: &mut Ui) { - let mut use_headlight = self.events.request.toggle_headlights.0; +impl<'w, 's> ViewLights<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let mut use_headlight = self.toggle_headlights.0; ui.checkbox(&mut use_headlight, "Use Headlight"); - if use_headlight != self.events.request.toggle_headlights.0 { - self.events.request.toggle_headlights.0 = use_headlight; + if use_headlight != self.toggle_headlights.0 { + self.toggle_headlights.0 = use_headlight; } - let mut use_physical_lights = self.events.request.toggle_physical_lights.0; + let mut use_physical_lights = self.toggle_physical_lights.0; ui.checkbox(&mut use_physical_lights, "Use Physical Lights"); - if use_physical_lights != self.events.request.toggle_physical_lights.0 { - self.events.request.toggle_physical_lights.0 = use_physical_lights; + if use_physical_lights != self.toggle_physical_lights.0 { + self.toggle_physical_lights.0 = use_physical_lights; } ui.separator(); @@ -101,16 +96,13 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLights<'a, 'w1, 's1, 'w2, 's2> { #[cfg(not(target_arch = "wasm32"))] { ui.horizontal(|ui| { - if let Some(export_file) = &self.events.display.light.export_file { + if let Some(export_file) = &self.display_light.export_file { if ui.button("Export").clicked() { - self.events - .request - .export_lights - .send(ExportLights(export_file.clone())); + self.export_lights.send(ExportLights(export_file.clone())); } } if ui.button("Export Lights As...").clicked() { - match &self.events.display.light.choosing_file_for_export { + match &self.display_light.choosing_file_for_export { Some(_) => { warn!("A file is already being chosen!"); } @@ -123,12 +115,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLights<'a, 'w1, 's1, 'w2, 's2> { Some(file.path().to_path_buf()) }); - self.events.display.light.choosing_file_for_export = Some(future); + self.display_light.choosing_file_for_export = Some(future); } } } }); - match &self.events.display.light.export_file { + match &self.display_light.export_file { Some(path) => match path.to_str() { Some(s) => { ui.label(s); @@ -145,41 +137,37 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLights<'a, 'w1, 's1, 'w2, 's2> { } ui.heading("Create new light"); - if let Some(new_pose) = InspectPose::new(&self.events.display.light.pose).show(ui) { - self.events.display.light.pose = new_pose; + if let Some(new_pose) = InspectPoseComponent::new(&self.display_light.pose).show(ui) { + self.display_light.pose = new_pose; } ui.push_id("Add Light", |ui| { - if let Some(new_kind) = InspectLightKind::new( - &self.events.display.light.kind, - &self.events.display.light.recall, - ) - .show(ui) + if let Some(new_kind) = + InspectLightKind::new(&self.display_light.kind, &self.display_light.recall).show(ui) { - self.events.display.light.recall.remember(&new_kind); - self.events.display.light.kind = new_kind; + self.display_light.recall.remember(&new_kind); + self.display_light.kind = new_kind; } }); // TODO(MXG): Add a + icon to this button to make it more visible if ui.button("Add").clicked() { let new_light = self - .events .commands .spawn(Light { - pose: self.events.display.light.pose, - kind: self.events.display.light.kind, + pose: self.display_light.pose, + kind: self.display_light.kind, }) .insert(Category::Light) .id(); - self.events.request.select.send(Select(Some(new_light))); + self.selector.select.send(Select(Some(new_light))); } ui.separator(); let mut unsaved_lights = BTreeMap::new(); let mut saved_lights = BTreeMap::new(); - for (e, kind, site_id) in &self.params.lights { + for (e, kind, site_id) in &self.lights { if let Some(site_id) = site_id { saved_lights.insert(Reverse(site_id.0), (e, kind.label())); } else { @@ -189,26 +177,48 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLights<'a, 'w1, 's1, 'w2, 's2> { for (e, label) in unsaved_lights { ui.horizontal(|ui| { - SelectionWidget::new(e.0, None, self.params.icons.as_ref(), self.events).show(ui); + self.selector.show_widget(e.0, ui); ui.label(label); }); } - for (site_id, (e, label)) in saved_lights { + for (_, (e, label)) in saved_lights { ui.horizontal(|ui| { - SelectionWidget::new( - e, - Some(SiteID(site_id.0)), - self.params.icons.as_ref(), - self.events, - ) - .show(ui); + self.selector.show_widget(e, ui); ui.label(label); }); } } } +#[derive(Resource)] +pub struct LightDisplay { + pub pose: Pose, + pub kind: LightKind, + pub recall: RecallLightKind, + pub choosing_file_for_export: Option>>, + pub export_file: Option, +} + +impl Default for LightDisplay { + fn default() -> Self { + Self { + pose: Pose { + trans: [0.0, 0.0, 2.6], + rot: Rotation::EulerExtrinsicXYZ([ + Angle::Deg(0.0), + Angle::Deg(0.0), + Angle::Deg(0.0), + ]), + }, + kind: Default::default(), + recall: Default::default(), + choosing_file_for_export: None, + export_file: None, + } + } +} + pub fn resolve_light_export_file( mut light_display: ResMut, mut export_lights: EventWriter, diff --git a/rmf_site_editor/src/widgets/view_nav_graphs.rs b/rmf_site_editor/src/widgets/view_nav_graphs.rs index 6752717d..80f51bcf 100644 --- a/rmf_site_editor/src/widgets/view_nav_graphs.rs +++ b/rmf_site_editor/src/widgets/view_nav_graphs.rs @@ -16,87 +16,82 @@ */ use crate::{ - interaction::Selection, recency::RecencyRanking, site::{ Change, Delete, DisplayColor, ImportNavGraphs, NameInSite, NameOfSite, NavGraph, - NavGraphMarker, SaveNavGraphs, SiteID, DEFAULT_NAV_GRAPH_COLORS, + NavGraphMarker, SaveNavGraphs, DEFAULT_NAV_GRAPH_COLORS, }, - widgets::{ - inspector::{color_edit, selection_widget::SelectionWidget}, - AppEvents, Icons, MoveLayerButton, - }, - Autoload, CurrentWorkspace, + widgets::{inspector::color_edit, prelude::*, Icons, MoveLayerButton, SelectorWidget}, + AppState, Autoload, ChangeRank, CurrentWorkspace, }; use bevy::{ ecs::system::SystemParam, prelude::*, tasks::{AsyncComputeTaskPool, Task}, }; -use bevy_egui::egui::{ImageButton, Ui}; +use bevy_egui::egui::{CollapsingHeader, ImageButton, Ui}; use futures_lite::future; #[cfg(not(target_arch = "wasm32"))] use rfd::AsyncFileDialog; -#[derive(Resource)] -pub struct NavGraphDisplay { - pub color: Option<[f32; 4]>, - pub name: String, - pub removing: bool, - pub choosing_file_for_export: Option>>, - pub export_file: Option, - pub choosing_file_to_import: Option>>, -} +/// Add a widget for viewing and editing navigation graphs. +#[derive(Default)] +pub struct ViewNavGraphsPlugin {} -impl FromWorld for NavGraphDisplay { - fn from_world(world: &mut World) -> Self { - let export_file = world - .get_resource::() - .map(|a| a.import.clone()) - .flatten(); - Self { - color: None, - name: "".to_string(), - removing: false, - choosing_file_for_export: None, - export_file, - choosing_file_to_import: None, - } +impl Plugin for ViewNavGraphsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct NavGraphParams<'w, 's> { - pub ranking: Query<'w, 's, &'static RecencyRanking>, - pub graphs: Query< +pub struct ViewNavGraphs<'w, 's> { + ranking: Query<'w, 's, &'static RecencyRanking>, + graphs: Query< 'w, 's, ( &'static NameInSite, &'static DisplayColor, &'static Visibility, - Option<&'static SiteID>, ), With, >, - pub icons: Res<'w, Icons>, - pub selection: Res<'w, Selection>, -} - -pub struct ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { - params: &'a NavGraphParams<'w1, 's1>, - events: &'a mut AppEvents<'w2, 's2>, + icons: Res<'w, Icons>, + open_sites: Query<'w, 's, Entity, With>, + current_workspace: ResMut<'w, CurrentWorkspace>, + display_nav_graph: ResMut<'w, NavGraphDisplay>, + delete: EventWriter<'w, Delete>, + change_visibility: EventWriter<'w, Change>, + change_name: EventWriter<'w, Change>, + change_color: EventWriter<'w, Change>, + change_rank: EventWriter<'w, ChangeRank>, + save_nav_graphs: EventWriter<'w, SaveNavGraphs>, + selector: SelectorWidget<'w, 's>, + commands: Commands<'w, 's>, + app_state: Res<'w, State>, } -impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { - pub fn new(params: &'a NavGraphParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { params, events } +impl<'w, 's> WidgetSystem for ViewNavGraphs<'w, 's> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state.get() != AppState::SiteEditor { + return; + } + CollapsingHeader::new("Navigation") + .default_open(true) + .show(ui, |ui| { + params.show_widget(ui); + }); } +} - pub fn show(self, ui: &mut Ui, open_sites: &Query>) { - let ranking = match self.events.request.current_workspace.root { - Some(c) => match self.params.ranking.get(c) { +impl<'w, 's> ViewNavGraphs<'w, 's> { + pub fn show_widget(&mut self, ui: &mut Ui) { + let ranking = match self.current_workspace.root { + Some(c) => match self.ranking.get(c) { Ok(r) => r, Err(_) => return, }, @@ -105,13 +100,13 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { let graph_count = ranking.len(); ui.horizontal(|ui| { - if self.events.display.nav_graph.removing { + if self.display_nav_graph.removing { if ui .button("View") .on_hover_text("Toggle visibility of graphs") .clicked() { - self.events.display.nav_graph.removing = false; + self.display_nav_graph.removing = false; } ui.label("Remove"); } else { @@ -121,53 +116,48 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { .on_hover_text("Choose a graph to remove") .clicked() { - self.events.display.nav_graph.removing = true; + self.display_nav_graph.removing = true; } } }); ui.horizontal(|ui| { let add = ui.button("Add").clicked(); - if self.events.display.nav_graph.color.is_none() { + if self.display_nav_graph.color.is_none() { let next_color_index = graph_count % DEFAULT_NAV_GRAPH_COLORS.len(); - self.events.display.nav_graph.color = - Some(DEFAULT_NAV_GRAPH_COLORS[next_color_index]); + self.display_nav_graph.color = Some(DEFAULT_NAV_GRAPH_COLORS[next_color_index]); } - if let Some(color) = &mut self.events.display.nav_graph.color { + if let Some(color) = &mut self.display_nav_graph.color { color_edit(ui, color); } - ui.text_edit_singleline(&mut self.events.display.nav_graph.name); + ui.text_edit_singleline(&mut self.display_nav_graph.name); if add { - self.events - .commands + self.commands .spawn(SpatialBundle::default()) .insert(NavGraph { - name: NameInSite(self.events.display.nav_graph.name.clone()), - color: DisplayColor(self.events.display.nav_graph.color.unwrap().clone()), + name: NameInSite(self.display_nav_graph.name.clone()), + color: DisplayColor(self.display_nav_graph.color.unwrap().clone()), marker: Default::default(), }); - self.events.display.nav_graph.color = None; + self.display_nav_graph.color = None; } }); let mut selected_graph = None; for e in ranking.iter().rev() { let e = *e; - if self.params.selection.0.is_some_and(|sel| sel == e) { + if self.selector.selection.0.is_some_and(|sel| sel == e) { selected_graph = Some(e); } - let (name, color, vis, site_id) = match self.params.graphs.get(e) { + let (name, color, vis) = match self.graphs.get(e) { Ok(g) => g, Err(_) => continue, }; ui.horizontal(|ui| { - if self.events.display.nav_graph.removing { - if ui - .add(ImageButton::new(self.params.icons.trash.egui())) - .clicked() - { - self.events.request.delete.send(Delete::new(e)); - self.events.display.nav_graph.removing = false; + if self.display_nav_graph.removing { + if ui.add(ImageButton::new(self.icons.trash.egui())).clicked() { + self.delete.send(Delete::new(e)); + self.display_nav_graph.removing = false; } } else { let mut is_visible = !matches!(vis, Visibility::Hidden); @@ -185,51 +175,32 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { } else { Visibility::Hidden }; - self.events - .change - .visibility - .send(Change::new(visibility, e)); + self.change_visibility.send(Change::new(visibility, e)); } } - SelectionWidget::new(e, site_id.copied(), &self.params.icons, self.events).show(ui); + self.selector.show_widget(e, ui); let mut new_color = color.0; color_edit(ui, &mut new_color); if new_color != color.0 { - self.events - .change - .color + self.change_color .send(Change::new(DisplayColor(new_color), e)); } let mut new_name = name.0.clone(); if ui.text_edit_singleline(&mut new_name).changed() { - self.events - .change - .name - .send(Change::new(NameInSite(new_name), e)); + self.change_name.send(Change::new(NameInSite(new_name), e)); } }); } if let Some(e) = selected_graph { ui.horizontal(|ui| { - MoveLayerButton::to_top(e, &mut self.events.layers.nav_graphs, &self.params.icons) - .show(ui); - - MoveLayerButton::up(e, &mut self.events.layers.nav_graphs, &self.params.icons) - .show(ui); - - MoveLayerButton::down(e, &mut self.events.layers.nav_graphs, &self.params.icons) - .show(ui); - - MoveLayerButton::to_bottom( - e, - &mut self.events.layers.nav_graphs, - &self.params.icons, - ) - .show(ui); + MoveLayerButton::to_top(e, &mut self.change_rank, &self.icons).show(ui); + MoveLayerButton::up(e, &mut self.change_rank, &self.icons).show(ui); + MoveLayerButton::down(e, &mut self.change_rank, &self.icons).show(ui); + MoveLayerButton::to_bottom(e, &mut self.change_rank, &self.icons).show(ui); }); } @@ -237,40 +208,35 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { { ui.separator(); if ui.button("Import Graphs...").clicked() { - match self.events.request.current_workspace.to_site(open_sites) { - Some(into_site) => { - match &self.events.display.nav_graph.choosing_file_to_import { - Some(_) => { - warn!("A file is already being chosen!"); - } - None => { - let future = AsyncComputeTaskPool::get().spawn(async move { - let file = match AsyncFileDialog::new().pick_file().await { - Some(file) => file, - None => return None, - }; + match self.current_workspace.to_site(&self.open_sites) { + Some(into_site) => match &self.display_nav_graph.choosing_file_to_import { + Some(_) => { + warn!("A file is already being chosen!"); + } + None => { + let future = AsyncComputeTaskPool::get().spawn(async move { + let file = match AsyncFileDialog::new().pick_file().await { + Some(file) => file, + None => return None, + }; - // TODO(luca) make this accept different file formats - match rmf_site_format::Site::from_bytes_ron(&file.read().await) - { - Ok(from_site) => Some(( - file.path().to_owned(), - ImportNavGraphs { - into_site, - from_site, - }, - )), - Err(err) => { - error!("Unable to parse file:\n{err}"); - None - } + match rmf_site_format::Site::from_bytes_ron(&file.read().await) { + Ok(from_site) => Some(( + file.path().to_owned(), + ImportNavGraphs { + into_site, + from_site, + }, + )), + Err(err) => { + error!("Unable to parse file:\n{err}"); + None } - }); - self.events.display.nav_graph.choosing_file_to_import = - Some(future); - } + } + }); + self.display_nav_graph.choosing_file_to_import = Some(future); } - } + }, None => { error!("No current site??"); } @@ -278,12 +244,11 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { } ui.separator(); ui.horizontal(|ui| { - if let Some(export_file) = &self.events.display.nav_graph.export_file { + if let Some(export_file) = &self.display_nav_graph.export_file { if ui.button("Export").clicked() { - if let Some(current_site) = - self.events.request.current_workspace.to_site(open_sites) + if let Some(current_site) = self.current_workspace.to_site(&self.open_sites) { - self.events.request.save_nav_graphs.send(SaveNavGraphs { + self.save_nav_graphs.send(SaveNavGraphs { site: current_site, to_file: export_file.clone(), }) @@ -293,7 +258,7 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { } } if ui.button("Export Graphs As...").clicked() { - match &self.events.display.nav_graph.choosing_file_for_export { + match &self.display_nav_graph.choosing_file_for_export { Some(_) => { warn!("A file is already being chosen!"); } @@ -305,12 +270,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { }; Some(file.path().to_path_buf()) }); - self.events.display.nav_graph.choosing_file_for_export = Some(future); + self.display_nav_graph.choosing_file_for_export = Some(future); } } } }); - if let Some(export_file) = &self.events.display.nav_graph.export_file { + if let Some(export_file) = &self.display_nav_graph.export_file { if let Some(export_file) = export_file.as_os_str().to_str() { ui.horizontal(|ui| { ui.label("Chosen file:"); @@ -322,6 +287,33 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewNavGraphs<'a, 'w1, 's1, 'w2, 's2> { } } +#[derive(Resource)] +pub struct NavGraphDisplay { + pub color: Option<[f32; 4]>, + pub name: String, + pub removing: bool, + pub choosing_file_for_export: Option>>, + pub export_file: Option, + pub choosing_file_to_import: Option>>, +} + +impl FromWorld for NavGraphDisplay { + fn from_world(world: &mut World) -> Self { + let export_file = world + .get_resource::() + .map(|a| a.import.clone()) + .flatten(); + Self { + color: None, + name: "".to_string(), + removing: false, + choosing_file_for_export: None, + export_file, + choosing_file_to_import: None, + } + } +} + pub fn resolve_nav_graph_import_export_files( mut nav_graph_display: ResMut, mut save_nav_graphs: EventWriter, diff --git a/rmf_site_editor/src/widgets/view_occupancy.rs b/rmf_site_editor/src/widgets/view_occupancy.rs index 4cd4ca30..2580c3a8 100644 --- a/rmf_site_editor/src/widgets/view_occupancy.rs +++ b/rmf_site_editor/src/widgets/view_occupancy.rs @@ -15,50 +15,64 @@ * */ -use crate::{occupancy::CalculateGrid, widgets::AppEvents}; -use bevy::prelude::Resource; -use bevy_egui::egui::{DragValue, Ui}; +use crate::{occupancy::CalculateGrid, widgets::prelude::*, AppState}; +use bevy::prelude::*; +use bevy_egui::egui::{CollapsingHeader, DragValue, Ui}; -#[derive(Resource)] -pub struct OccupancyDisplay { - pub cell_size: f32, -} +/// Add a widget that provides a button for producing an occupancy grid +/// visualization. +#[derive(Default)] +pub struct ViewOccupancyPlugin {} -impl Default for OccupancyDisplay { - fn default() -> Self { - Self { cell_size: 0.5 } +impl Plugin for ViewOccupancyPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.init_resource::() + .add_plugins(PropertiesTilePlugin::::new()); } } -pub struct ViewOccupancy<'a, 'w2, 's2> { - events: &'a mut AppEvents<'w2, 's2>, +#[derive(SystemParam)] +pub struct ViewOccupancy<'w> { + calculate_grid: EventWriter<'w, CalculateGrid>, + display_occupancy: ResMut<'w, OccupancyDisplay>, + app_state: Res<'w, State>, } -impl<'a, 'w2, 's2> ViewOccupancy<'a, 'w2, 's2> { - pub fn new(events: &'a mut AppEvents<'w2, 's2>) -> Self { - Self { events } +impl<'w> WidgetSystem for ViewOccupancy<'w> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) { + let mut params = state.get_mut(world); + if *params.app_state.get() != AppState::SiteEditor { + return; + } + CollapsingHeader::new("Occupancy") + .default_open(false) + .show(ui, |ui| { + params.show_widget(ui); + }); } +} - pub fn show(self, ui: &mut Ui) { +impl<'w> ViewOccupancy<'w> { + pub fn show_widget(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { if ui.button("Calculate Occupancy").clicked() { - self.events.request.calculate_grid.send(CalculateGrid { - cell_size: self.events.display.occupancy.cell_size, + self.calculate_grid.send(CalculateGrid { + cell_size: self.display_occupancy.cell_size, floor: 0.01, ceiling: 1.5, }); } if ui .add( - DragValue::new(&mut self.events.display.occupancy.cell_size) + DragValue::new(&mut self.display_occupancy.cell_size) .clamp_range(0.01..=f32::INFINITY) .speed(0.01), ) .changed() { - if self.events.display.occupancy.cell_size > 0.1 { - self.events.request.calculate_grid.send(CalculateGrid { - cell_size: self.events.display.occupancy.cell_size, + if self.display_occupancy.cell_size > 0.1 { + self.calculate_grid.send(CalculateGrid { + cell_size: self.display_occupancy.cell_size, floor: 0.01, ceiling: 1.5, }); @@ -67,3 +81,14 @@ impl<'a, 'w2, 's2> ViewOccupancy<'a, 'w2, 's2> { }); } } + +#[derive(Resource)] +pub struct OccupancyDisplay { + pub cell_size: f32, +} + +impl Default for OccupancyDisplay { + fn default() -> Self { + Self { cell_size: 0.5 } + } +} diff --git a/rmf_site_editor/src/wireframe.rs b/rmf_site_editor/src/wireframe.rs index 0af9defc..cde4af12 100644 --- a/rmf_site_editor/src/wireframe.rs +++ b/rmf_site_editor/src/wireframe.rs @@ -15,7 +15,7 @@ * */ -use crate::menu_bar::{MenuEvent, MenuItem, ViewMenu}; +use crate::widgets::menu_bar::{MenuEvent, MenuItem, ViewMenu}; use bevy::pbr::wireframe::{Wireframe, WireframePlugin}; use bevy::prelude::*; diff --git a/rmf_site_editor/src/workcell/mod.rs b/rmf_site_editor/src/workcell/mod.rs index 4992fbef..9a26296b 100644 --- a/rmf_site_editor/src/workcell/mod.rs +++ b/rmf_site_editor/src/workcell/mod.rs @@ -73,8 +73,6 @@ impl Plugin for WorkcellEditorPlugin { fn build(&self, app: &mut App) { app.add_plugins(InfiniteGridPlugin) .add_event::() - .add_event::() - .add_event::() .add_event::() .add_systems(OnEnter(AppState::WorkcellEditor), spawn_grid) .add_systems(OnExit(AppState::WorkcellEditor), delete_grid) diff --git a/rmf_site_editor/src/workspace.rs b/rmf_site_editor/src/workspace.rs index d03b5e6f..57cae9cb 100644 --- a/rmf_site_editor/src/workspace.rs +++ b/rmf_site_editor/src/workspace.rs @@ -214,6 +214,8 @@ impl Plugin for WorkspacePlugin { .add_event::() .add_event::() .add_event::() + .add_event::() + .add_event::() .init_resource::() .init_resource::() .init_resource::() diff --git a/rmf_site_format/src/workcell.rs b/rmf_site_format/src/workcell.rs index 842dc5e1..af4d2e4b 100644 --- a/rmf_site_format/src/workcell.rs +++ b/rmf_site_format/src/workcell.rs @@ -15,7 +15,11 @@ * */ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap}; + +#[cfg(feature = "bevy")] +use std::collections::HashSet; + use std::io; use crate::misc::Rotation;