diff --git a/Cargo.lock b/Cargo.lock index e4a5010..75a57d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,14 +24,23 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25ae84c0260bdf5df07796d7cc4882460de26a2b406ec0e6c42461a723b271b" +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + [[package]] name = "accesskit_atspi_common" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29bd41de2e54451a8ca0dd95ebf45b54d349d29ebceb7f20be264eee14e3d477" dependencies = [ - "accesskit", - "accesskit_consumer", + "accesskit 0.19.0", + "accesskit_consumer 0.28.0", "atspi-common", "serde", "thiserror 1.0.69", @@ -44,31 +53,69 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bfae7c152994a31dc7d99b8eeac7784a919f71d1b306f4b83217e110fd3824c" dependencies = [ - "accesskit", + "accesskit 0.19.0", "hashbrown 0.15.5", ] +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit 0.24.1", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_ios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750c4e9f6ce888dfe8a10c0f1b5ceb646a7854fd46579d74919219d1bb314083" +dependencies = [ + "accesskit 0.24.1", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.3", + "objc2-foundation 0.2.2", + "objc2-ui-kit", +] + [[package]] name = "accesskit_macos" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692dd318ff8a7a0ffda67271c4bd10cf32249656f4e49390db0b26ca92b095f2" dependencies = [ - "accesskit", - "accesskit_consumer", + "accesskit 0.19.0", + "accesskit_consumer 0.28.0", "hashbrown 0.15.5", "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit 0.24.1", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.3", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "accesskit_unix" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f7474c36606d0fe4f438291d667bae7042ea2760f506650ad2366926358fc8" dependencies = [ - "accesskit", + "accesskit 0.19.0", "accesskit_atspi_common", "async-channel", "async-executor", @@ -86,24 +133,52 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70a042b62c9c05bf7b616f015515c17d2813f3ba89978d6f4fc369735d60700a" dependencies = [ - "accesskit", - "accesskit_consumer", + "accesskit 0.19.0", + "accesskit_consumer 0.28.0", "hashbrown 0.15.5", "static_assertions", "windows 0.61.3", "windows-core 0.61.2", ] +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit 0.24.1", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "accesskit_winit" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1f0d3d13113d8857542a4f8d1a1c24d1dc1527b77aee8426127f4901588708" dependencies = [ - "accesskit", - "accesskit_macos", + "accesskit 0.19.0", + "accesskit_macos 0.20.0", "accesskit_unix", - "accesskit_windows", + "accesskit_windows 0.27.0", + "raw-window-handle", + "winit", +] + +[[package]] +name = "accesskit_winit" +version = "0.33.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b41e63a69f36d9f1f41e70464c7e5f72eee485ef26aab19f0b4f86e6c0a84c" +dependencies = [ + "accesskit 0.24.1", + "accesskit_ios", + "accesskit_macos 0.26.3", + "accesskit_windows 0.34.0", "raw-window-handle", "winit", ] @@ -127,6 +202,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.1" @@ -152,6 +242,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + [[package]] name = "android_system_properties" version = "0.1.6" @@ -161,6 +257,17 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.12.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a5b0a5ab8561c603f16c3b78c24a386b1f70e7fc1b1b1e8c15738828e4a176" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + [[package]] name = "anstream" version = "1.0.0" @@ -217,6 +324,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arboard" version = "3.7.0" @@ -234,6 +350,7 @@ dependencies = [ "parking_lot", "percent-encoding", "windows-sys 0.61.2", + "wl-clipboard-rs", "x11rb", ] @@ -264,6 +381,17 @@ dependencies = [ "libloading", ] +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -302,6 +430,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.6.0" @@ -383,6 +522,9 @@ name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] [[package]] name = "async-trait" @@ -400,6 +542,19 @@ name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomicow" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301801c08259e328a1c7da556608c0c22687708831b22024dbd3a57ea741e6de" +dependencies = [ + "portable-atomic", + "portable-atomic-util", +] [[package]] name = "atspi" @@ -463,6 +618,340 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bevy" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd0e4ea5ec677698955f9f285221eab78606d8c27b57b889def199c16ce36e6" +dependencies = [ + "bevy_internal", +] + +[[package]] +name = "bevy_a11y" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1070b9b7f087a5073be2da713367f29a831c85cd3391afcfc95bc0498125704" +dependencies = [ + "accesskit 0.24.1", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_reflect", +] + +[[package]] +name = "bevy_android" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fd56a4708085b36f8aa1a011e347f8bd9334f0343bb3d5d9c3742897968a55" +dependencies = [ + "android-activity", +] + +[[package]] +name = "bevy_app" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2489da2972d57cccd19060d7faa44c4c4cfb296a657c84ee6ab8bde2b1c0115" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "console_error_panic_hook", + "ctrlc", + "downcast-rs 2.0.2", + "log", + "thiserror 2.0.21", + "variadics_please", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "bevy_asset" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88b3767a2c3f5a017b2febca08a670819ea2acb46846e4d8cd02980aeb0ed3e" +dependencies = [ + "async-broadcast", + "async-channel", + "async-fs", + "async-io", + "async-lock", + "atomicow", + "bevy_android", + "bevy_app", + "bevy_asset_macros", + "bevy_diagnostic", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.13.2", + "blake3", + "crossbeam-channel", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "either", + "futures-io", + "futures-lite", + "futures-util", + "js-sys", + "ron 0.12.2", + "serde", + "stackfuture", + "thiserror 2.0.21", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_asset_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b4fd236612744dbd61fb0a77ffde0aee37b8b780cf8a89caa308511c70646" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_camera" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32c0d8436fb9373dc3b64fa30c3dec51d6a7cfda61de3c70fd1f62f8a903887" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_shape", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "downcast-rs 2.0.2", + "serde", + "smallvec", + "thiserror 2.0.21", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_clipboard" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0267b0beafd7e587a1b25b5ce14b271577f5fc1d8fd7828b2b66f0cf5dd649b" +dependencies = [ + "arboard", + "bevy_app", + "bevy_ecs", + "bevy_log", + "bevy_platform", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "bevy_color" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0e57bfc1a0da1c018235fb98bb0e15ab61f1d0253122020ab6e1450e6cfbcf9" +dependencies = [ + "bevy_curve", + "bevy_math", + "bevy_reflect", + "bytemuck", + "derive_more", + "encase", + "serde", + "thiserror 2.0.21", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_core_pipeline" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a077b301be4b373a4cb224c54988e1f8385ecf3ab9e60bd3727a65b3d6901a" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_extract", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.13.2", + "indexmap", + "nonmax", +] + +[[package]] +name = "bevy_curve" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ae28ed95c1c8a1bfc8930471840ee5a4b99b6b31138b17598e7ee6f4291b64" +dependencies = [ + "approx", + "bevy_math", + "bevy_reflect", + "itertools 0.14.0", + "serde", + "thiserror 2.0.21", + "variadics_please", +] + +[[package]] +name = "bevy_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd59e9e600d792e60df17b4d52719b28c288bf047884ac89ad29d9c7a6592d2c" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_diagnostic" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8851dc1b253b350bf941695457007e6e44ad4b8f075bf52a1ef202e6866b6a" +dependencies = [ + "atomic-waker", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_tasks", + "bevy_time", + "const-fnv1a-hash", + "log", + "serde", +] + +[[package]] +name = "bevy_ecs" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5541a1658c5029b39e3ec59c6ecb47ad0a6d85fa083312a5ee2a95d6982a926" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags 2.13.2", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.21", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macro_logic" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5197fea7643d2733e8b6d81c3dfa99adad3a754cdfd159c00255cea157b795d7" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dde5305169503c9101404bf8cf17e8351f56fccf5152b8e5d7d7c32dcf1c486" +dependencies = [ + "bevy_ecs_macro_logic", + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_encase_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e4c4183c39ae8d00e53afeeab66aa71c41b08d82638149f9d485a2a62df569" +dependencies = [ + "bevy_macro_utils", + "encase_derive_impl", +] + +[[package]] +name = "bevy_extract" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e27fc07ef8e3ab7c217fed1b171d42c90f3b6e4f7f526ffa28f9006ac49aa4" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_extract_macros", + "bevy_log", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_utils", +] + +[[package]] +name = "bevy_extract_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5c67e8c55ef35c65295e8cb614eda169780e21e2748f6df470d3bff21849a3" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bevy_forge_cli" version = "0.1.0" @@ -479,11 +968,11 @@ name = "bevy_forge_core" version = "0.1.0" dependencies = [ "indexmap", - "ron", + "ron 0.8.1", "serde", "tempfile", "thiserror 2.0.21", - "toml", + "toml 0.8.23", ] [[package]] @@ -497,13 +986,925 @@ dependencies = [ "tempfile", ] +[[package]] +name = "bevy_forge_runtime" +version = "0.1.0" +dependencies = [ + "bevy", +] + +[[package]] +name = "bevy_gizmos" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc92f635ec5eaef5733afee45e6a0452e8c65de42d818dc7269dc5265087689" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_curve", + "bevy_ecs", + "bevy_gizmos_macros", + "bevy_input", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_shape", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_gizmos_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501e72c4ac31caef17ef9e627dee212d2107b95402a891ec6f0fc4107dd0cdf4" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_gizmos_render" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5be0c9f114dc9b2c929076517d088e059b971edec5dfff4d5a225df6796df" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_extract", + "bevy_gizmos", + "bevy_image", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_shape", + "bevy_sprite_render", + "bevy_transform", + "bevy_utils", + "bytemuck", + "tracing", +] + +[[package]] +name = "bevy_image" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb818dd8d705809d54adea08e346edce81a51ddbf06406ddf12a72ee467a3855" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "bitflags 2.13.2", + "bytemuck", + "futures-lite", + "guillotiere", + "half", + "image", + "rectangle-pack", + "serde", + "thiserror 2.0.21", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_input" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261ca5cc8a0bf228bfe4e841570db18a3ab1d034ce089772b8ec49c163ae74ea" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "derive_more", + "log", + "smol_str", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_input_focus" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c81d369d0ab3ec17ba14bab1e07da7cacf3751a3104a93b1a3cd99d887887b" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_window", + "log", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_internal" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ed1cb58d1761426a077a8aff55e6a2e0cdfd19e411211d4b0217409330bb84" +dependencies = [ + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_clipboard", + "bevy_color", + "bevy_core_pipeline", + "bevy_curve", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_extract", + "bevy_gizmos", + "bevy_gizmos_render", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_light", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_pbr", + "bevy_picking", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_shape", + "bevy_sprite", + "bevy_sprite_render", + "bevy_state", + "bevy_tasks", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_ui", + "bevy_ui_render", + "bevy_ui_widgets", + "bevy_utils", + "bevy_window", + "bevy_winit", +] + +[[package]] +name = "bevy_light" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529d5ec7db637bbce7d016b2bfd0798362ad67118de8e9d37aaac24ecb3337cd" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_curve", + "bevy_ecs", + "bevy_gizmos", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_shape", + "bevy_transform", + "bevy_utils", + "half", + "smallvec", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_log" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87d5152292b0e581a589cffb6b7e83e0afe345279d4126545ce7b786bd8a47a" +dependencies = [ + "android_log-sys", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_utils", + "tracing", + "tracing-log", + "tracing-oslog", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d03a948df65138e30917d91237797d7e38763135ebacd0f28c891e1a3b2b52" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "bevy_material" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be7ee4b2f4764e8d58a59afb47a3ba8bae36f955f409f08c658cace2808ef40" +dependencies = [ + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_material_macros", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_shader", + "bevy_utils", + "encase", + "smallvec", + "thiserror 2.0.21", + "tracing", + "variadics_please", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_material_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70551fa201e1416ab44968631b7ce6320c194acfdb652a1971ec6882af122333" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_math" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598cbdcec4d0f0d966905996240ac76f77a644c583f95fb612d4f59f5f649038" +dependencies = [ + "approx", + "bevy_reflect", + "derive_more", + "glam", + "libm", + "rand", + "serde", + "thiserror 2.0.21", + "variadics_please", +] + +[[package]] +name = "bevy_mesh" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09358ac9a3ede5855568c10eaba6bcf126bf86639e786fb21b413457ebec5f26" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_encase_derive", + "bevy_math", + "bevy_mikktspace", + "bevy_platform", + "bevy_reflect", + "bevy_shape", + "bevy_transform", + "bitflags 2.13.2", + "bytemuck", + "derive_more", + "encase", + "glam", + "half", + "hexasphere", + "thiserror 2.0.21", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_mikktspace" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff34eb29ff4b8a8688bc7299f14fb6b597461ca80fec03ed7d22939ab33e48f" + +[[package]] +name = "bevy_pbr" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb760932d1e683e7c00011f5c99e4e1cc893646a3a7b7224da6daf9bfa94853d" +dependencies = [ + "arrayvec", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_extract", + "bevy_image", + "bevy_light", + "bevy_log", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_shape", + "bevy_tasks", + "bevy_transform", + "bevy_utils", + "bitflags 2.13.2", + "bytemuck", + "derive_more", + "fixedbitset", + "indexmap", + "nonmax", + "offset-allocator", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_picking" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a04fb644c62a57a4f338f5cd8b61519c569139742eafe87dccb523e4b7cf2e" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_shape", + "bevy_time", + "bevy_transform", + "bevy_window", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_platform" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000bc467d1b344b6609fcf217a9b08d349d49c8e85dc4179fb7656f2b6e3db47" +dependencies = [ + "bytemuck", + "critical-section", + "foldhash 0.2.0", + "futures-channel", + "futures-lite", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", + "windows-sys 0.61.2", +] + +[[package]] +name = "bevy_ptr" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41935a00c02004753bd7ed5094754397800b3f673bf15553bb007b06216872d5" + +[[package]] +name = "bevy_reflect" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eecc733575b873deedcc37639e36290092388657091b3321fa6e073e7818e6" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs 2.0.2", + "erased-serde", + "foldhash 0.2.0", + "glam", + "indexmap", + "inventory", + "serde", + "smallvec", + "smol_str", + "thiserror 2.0.21", + "uuid", + "variadics_please", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfaa1e315676c625ed8d30ba8a671ebf917cad6355320295b9802a8cbc06853b" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.119", + "uuid", +] + +[[package]] +name = "bevy_render" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f438a921fd3ef82cc09ccf5e0cd8f1214dcae6286890f9d4acba0409d9a61a8" +dependencies = [ + "async-channel", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_encase_derive", + "bevy_extract", + "bevy_extract_macros", + "bevy_image", + "bevy_log", + "bevy_material", + "bevy_material_macros", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render_macros", + "bevy_shader", + "bevy_shape", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.13.2", + "bytemuck", + "derive_more", + "downcast-rs 2.0.2", + "encase", + "glam", + "image", + "indexmap", + "itertools 0.14.0", + "js-sys", + "nonmax", + "offset-allocator", + "send_wrapper", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "tracing", + "variadics_please", + "wasm-bindgen", + "weak-table", + "web-sys", + "wgpu 30.0.1", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_render_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119ca6d445eba5c9976c658f29bac854094b27c27624aa92c4d9668c319618d0" +dependencies = [ + "bevy_extract_macros", + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_shader" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2becafd54079da13835d6a42fc743e305d6270c27a91de613afe6e3f4b5de19" +dependencies = [ + "bevy_asset", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "serde", + "thiserror 2.0.21", + "tracing", + "wesl", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_shape" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8b6096ad1ec7dfe1af8e51d7cfa7de1a9e0a3bd02be58a061092c5e1ff1eb1" +dependencies = [ + "arrayvec", + "bevy_math", + "bevy_reflect", + "derive_more", + "rand", + "rand_distr", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_sprite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24dc4f69160513952a1969a3df5b4a2c729acfe0caf773854acea4a8c71f0918" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_text", + "bevy_transform", + "bevy_window", + "radsort", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_sprite_render" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8918649806697740b61d3fd0559cb8c7de2ade5046aacf8abbef20e49ea1de0c" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_extract", + "bevy_image", + "bevy_material", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_shape", + "bevy_sprite", + "bevy_text", + "bevy_transform", + "bevy_utils", + "bitflags 2.13.2", + "bytemuck", + "derive_more", + "fixedbitset", + "nonmax", + "smallvec", + "static_assertions", + "tracing", +] + +[[package]] +name = "bevy_state" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09292e08e1eb34d2dd915630fb3c04ba75220c7819943c9f213d3a5f9a8be1e8" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_state_macros", + "bevy_utils", + "log", + "variadics_please", +] + +[[package]] +name = "bevy_state_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec1a8f6a253e39ddf7e447b1b9286caacb205ed8e619e6fb9a2e010d58dd04b" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_tasks" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd8116f12dc50bde3758a72fba85e1f4c00e6c66d0ad58bdc46c0c0690829a8" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "concurrent-queue", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless", + "web-task", +] + +[[package]] +name = "bevy_text" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119d2b37ce3f5ea5a5b9092a7c9eec8d3734de62592d2869bd1d925976f34d05" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_clipboard", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "parley", + "serde", + "smallvec", + "smol_str", + "swash", + "sys-locale", + "thiserror 2.0.21", + "tracing", + "wgpu-types 30.0.1", +] + +[[package]] +name = "bevy_time" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13901174b792bb7848d8e088075f0009ea409b99179934c0d5bdd2b95622694e" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "crossbeam-channel", + "log", + "serde", +] + +[[package]] +name = "bevy_transform" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83379023d77a0739569a5bcb859eed652781a1b3271e94a86dfbb56fc5d067e0" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "derive_more", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_ui" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7926fba78f133376db0cba73a054db6931952379b5bfdc63f2fe9ba0c3b96c39" +dependencies = [ + "accesskit 0.24.1", + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_sprite", + "bevy_text", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "parley", + "smallvec", + "swash", + "taffy", + "thiserror 2.0.21", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_ui_render" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620b51b51eb8e7d8b2362ba96263142829560d9aa254e2f28032570667c6b0fb" +dependencies = [ + "bevy_a11y", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_core_pipeline", + "bevy_derive", + "bevy_ecs", + "bevy_extract", + "bevy_image", + "bevy_input_focus", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_sprite", + "bevy_sprite_render", + "bevy_text", + "bevy_transform", + "bevy_ui", + "bevy_utils", + "bytemuck", + "derive_more", + "indexmap", + "smallvec", + "tracing", +] + +[[package]] +name = "bevy_ui_widgets" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442cd402df6a54f0e51ed8c1bd004c8f9c6715d0f8045e33c68ea9aebb1ce16" +dependencies = [ + "accesskit 0.24.1", + "bevy_a11y", + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_platform", + "bevy_reflect", + "bevy_text", + "bevy_time", + "bevy_ui", + "bevy_window", + "parley", + "smol_str", + "web-sys", +] + +[[package]] +name = "bevy_utils" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbd58495039d4e63e6fcfef1242fecab33ffe62b4a5e010467e69e790879812" +dependencies = [ + "async-channel", + "bevy_platform", + "disqualified", + "indexmap", + "thread_local", +] + +[[package]] +name = "bevy_window" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9a83f3cbaa04155b5dc9b247819c0e215aee754451a4324b82ddeb80dc53f1" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "log", + "raw-window-handle", + "serde", +] + +[[package]] +name = "bevy_winit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef0a5a55524e793ab13536ef244229fa66a4839fae6564df2aa4b3c13956a82" +dependencies = [ + "accesskit 0.24.1", + "accesskit_winit 0.33.2", + "approx", + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_window", + "js-sys", + "tracing", + "wasm-bindgen", + "web-sys", + "winit", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -512,6 +1913,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "1.3.2" @@ -524,9 +1931,23 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ + "bytemuck", "serde_core", ] +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "block" version = "0.1.6" @@ -542,6 +1963,15 @@ dependencies = [ "objc2 0.5.3", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.5", +] + [[package]] name = "blocking" version = "1.7.0" @@ -581,6 +2011,12 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -736,6 +2172,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -759,6 +2204,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + +[[package]] +name = "const_panic" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "constgebra" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aaf9b65849a68662ac6c0810c8893a765c960b907dd7cfab9c4a50bf764fbc" +dependencies = [ + "const_soft_float", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", ] [[package]] @@ -801,6 +2302,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.3" @@ -810,6 +2320,30 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -822,12 +2356,46 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -841,6 +2409,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.13.2", + "block2 0.6.2", + "libc", "objc2 0.6.5", ] @@ -855,6 +2425,12 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "disqualified" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90a5dd66beeca277ae954f8055b02a712792b6b6b43f2bc2f92b0b7d8291614" + [[package]] name = "dlib" version = "0.5.3" @@ -879,6 +2455,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dpi" version = "0.1.2" @@ -937,7 +2519,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d5d0306cd61ca75e29682926d71f2390160247f135965242e904a636f51c0dc" dependencies = [ - "accesskit", + "accesskit 0.19.0", "ahash", "bitflags 2.13.2", "emath", @@ -965,7 +2547,7 @@ dependencies = [ "thiserror 1.0.69", "type-map", "web-time", - "wgpu", + "wgpu 25.0.2", "winit", ] @@ -975,7 +2557,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f95d0a91f9cb0dc2e732d49c2d521ac8948e1f0b758f306fb7b14d6f5db3927f" dependencies = [ - "accesskit_winit", + "accesskit_winit 0.27.0", "ahash", "arboard", "bytemuck", @@ -1007,6 +2589,12 @@ dependencies = [ "winit", ] +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + [[package]] name = "emath" version = "0.32.3" @@ -1016,6 +2604,37 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "encase" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1acbc46b960480c4959213714511e22f9a2e0d361520693bcf5057b5924f53e" +dependencies = [ + "const_panic", + "encase_derive", + "thiserror 2.0.21", +] + +[[package]] +name = "encase_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e30d38c47336a0afbdfde297b442944041cf6dccbb1a79eceea16abfb27a92a" +dependencies = [ + "encase_derive_impl", +] + +[[package]] +name = "encase_derive_impl" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495d24abe406f090f2ba8fc362ecaa8a001e192522aa75eae7b144163c3235ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "endi" version = "1.1.1" @@ -1073,6 +2692,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" +[[package]] +name = "erased-serde" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1089,6 +2719,15 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.4.2" @@ -1136,6 +2775,12 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.10" @@ -1147,12 +2792,47 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontique" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "parlance", + "read-fonts", + "smallvec", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1189,6 +2869,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.34" @@ -1288,6 +2977,26 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glam" +version = "0.33.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" +dependencies = [ + "approx", + "bytemuck", + "encase", + "libm", + "rand", + "serde_core", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "glow" version = "0.16.0" @@ -1397,6 +3106,20 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.61.3", +] + [[package]] name = "gpu-descriptor" version = "0.3.2" @@ -1417,25 +3140,69 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ + "bytemuck", "cfg-if", "crunchy", "num-traits", "zerocopy", ] +[[package]] +name = "harfrust" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "read-fonts", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -1443,6 +3210,20 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] [[package]] name = "heck" @@ -1462,6 +3243,17 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hexasphere" +version = "19.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163385eff5a0cf606ebe2c10b3a44ab02f4306e83fbd068a64f5e1d72a3049ac" +dependencies = [ + "constgebra", + "glam", + "tinyvec", +] + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1490,11 +3282,32 @@ checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + [[package]] name = "icu_normalizer" version = "2.4.0" @@ -1544,6 +3357,8 @@ checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1551,6 +3366,29 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + [[package]] name = "idna" version = "1.1.0" @@ -1600,12 +3438,39 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1708,6 +3573,87 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lalrpop-util" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364b49d1c98c07972e16fe1346463becc08bcd663da04e6e37ab1bea068dce6a" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "lexical" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc8a009b2ff1f419ccc62706f04fe0ca6e67b37460513964a3dfdb919bb37d6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libc" version = "0.2.191" @@ -1742,6 +3688,12 @@ dependencies = [ "redox_syscall 0.9.5", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1781,6 +3733,38 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.119", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1790,6 +3774,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1859,6 +3852,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + [[package]] name = "naga" version = "25.0.1" @@ -1866,10 +3865,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.8.0", "bitflags 2.13.2", "cfg_aliases", - "codespan-reporting", + "codespan-reporting 0.12.0", "half", "hashbrown 0.15.5", "hexf-parse", @@ -1878,12 +3877,51 @@ dependencies = [ "num-traits", "once_cell", "rustc-hash 1.1.0", - "spirv", + "spirv 0.3.0+sdk-1.3.268.0", "strum", "thiserror 2.0.21", "unicode-ident", ] +[[package]] +name = "naga" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting 0.13.1", + "half", + "hashbrown 0.17.1", + "indexmap", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv 0.4.0+sdk-1.4.341.0", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "serde", + "thiserror 2.0.21", +] + [[package]] name = "ndk" version = "0.9.0" @@ -1923,12 +3961,48 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nohash-hasher" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2002,13 +4076,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "libc", "objc2 0.5.3", "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -2031,7 +4105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-core-location", "objc2-foundation 0.2.2", @@ -2043,7 +4117,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -2055,7 +4129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -2090,10 +4164,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -2102,7 +4176,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-contacts", "objc2-foundation 0.2.2", @@ -2121,7 +4195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "dispatch", "libc", "objc2 0.5.3", @@ -2155,7 +4229,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -2168,11 +4242,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -2180,10 +4266,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", ] [[package]] @@ -2203,7 +4303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-cloud-kit", "objc2-core-data", @@ -2211,7 +4311,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -2223,7 +4323,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-foundation 0.2.2", ] @@ -2235,12 +4335,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.13.2", - "block2", + "block2 0.5.1", "objc2 0.5.3", "objc2-core-location", "objc2-foundation 0.2.2", ] +[[package]] +name = "offset-allocator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234d535da3521eb95106f40f0b73483d80bfb3aacf27c40d7e2b72f1a3e00a2" +dependencies = [ + "log", + "nonmax", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2282,6 +4392,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -2320,6 +4440,39 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "parlance" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73072a9470554c8e794778acece2312661218aaabb635600957d952cf01c716e" + +[[package]] +name = "parley" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa", +] + +[[package]] +name = "parley_data" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889" +dependencies = [ + "icu_properties", +] + [[package]] name = "paste" version = "1.0.15" @@ -2332,6 +4485,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2414,12 +4578,23 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -2495,6 +4670,38 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radsort" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019b4b213425016d7d84a153c4c73afb0946fbb4840e4eece7ba8848b9d6da22" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "range-alloc" version = "0.1.5" @@ -2507,6 +4714,35 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types", + "once_cell", +] + +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2534,6 +4770,23 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -2552,6 +4805,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.2", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2645,6 +4912,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.229" @@ -2708,6 +4981,24 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -2746,6 +5037,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" @@ -2839,6 +5140,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "portable-atomic", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -2848,12 +5158,30 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stackfuture" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115beb9c69db2393ff10b75a1b8587a51716e5551d015001e55320ed279d32f9" +dependencies = [ + "const_panic", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2894,6 +5222,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "2.0.119" @@ -2927,6 +5266,27 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "taffy" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639627c87f43b9181c811f40a6296409e093a17bc761214cba3c15df74f86b99" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -2989,6 +5349,15 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.11.3" @@ -3035,9 +5404,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + [[package]] name = "tokio" version = "1.54.1" @@ -3054,11 +5430,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3085,7 +5476,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow 0.7.15", @@ -3118,6 +5509,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + [[package]] name = "tracing" version = "0.1.44" @@ -3148,6 +5545,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", ] [[package]] @@ -3165,6 +5626,18 @@ dependencies = [ "rustc-hash 2.1.3", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + [[package]] name = "uds_windows" version = "1.2.1" @@ -3194,6 +5667,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsynn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a" +dependencies = [ + "mutants", + "proc-macro2", + "rustc-hash 2.1.3", +] + [[package]] name = "url" version = "2.5.8" @@ -3224,11 +5714,28 @@ version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "variadics_please" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53d0f7f2d0182759a549f1f313ce16b07d8f9ba93098157b5fa10ee3e61117f" +dependencies = [ + "quote", + "unsynn", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3317,7 +5824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" dependencies = [ "cc", - "downcast-rs", + "downcast-rs 1.2.1", "rustix 1.1.6", "scoped-tls", "smallvec", @@ -3445,6 +5952,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "weak-table" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eba2ec791141b0bf4db542f6b038a8f9310cee33e076cb181245780b09a24ff" +dependencies = [ + "cfg-if", + "hashbrown 0.16.1", +] + [[package]] name = "web-sys" version = "0.3.106" @@ -3455,6 +5972,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-task" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" +dependencies = [ + "async-task", + "cfg-if", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3487,6 +6016,46 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wesl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd8f9619baa4c7bde4df0899d6f626b70ee92d6e9c18c5f74473419b5bb6fa6" +dependencies = [ + "annotate-snippets", + "anstream", + "derive_more", + "glob", + "half", + "itertools 0.15.0", + "num-traits", + "serde", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "wesl-core", + "wesl-macros", + "wgsl-parse", + "wgsl-types", +] + +[[package]] +name = "wesl-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84dd3e600497eda0b47afcd21bd84c8953a9dfce7e93ed27a38ba97ac0747e03" + +[[package]] +name = "wesl-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8767ec26e75262a8e581d5f188b1f63a288baaf885bb33c9f4f73a693e06fc" +dependencies = [ + "itertools 0.15.0", + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "wgpu" version = "25.0.2" @@ -3500,7 +6069,7 @@ dependencies = [ "hashbrown 0.15.5", "js-sys", "log", - "naga", + "naga 25.0.1", "parking_lot", "portable-atomic", "profiling", @@ -3510,9 +6079,33 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 25.0.2", + "wgpu-hal 25.0.2", + "wgpu-types 25.0.0", +] + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wgpu-core 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-types 30.0.1", ] [[package]] @@ -3522,15 +6115,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.13.2", "cfg_aliases", "document-features", "hashbrown 0.15.5", "indexmap", "log", - "naga", + "naga 25.0.1", "once_cell", "parking_lot", "portable-atomic", @@ -3539,11 +6132,44 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.21", - "wgpu-core-deps-apple", + "wgpu-core-deps-apple 25.0.0", "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", + "wgpu-core-deps-windows-linux-android 25.0.0", + "wgpu-hal 25.0.2", + "wgpu-types 25.0.0", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga 30.0.1", + "naga-types", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple 30.0.1", + "wgpu-core-deps-windows-linux-android 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-naga-bridge", + "wgpu-types 30.0.1", ] [[package]] @@ -3552,7 +6178,16 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" dependencies = [ - "wgpu-hal", + "wgpu-hal 25.0.2", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal 30.0.1", ] [[package]] @@ -3561,7 +6196,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" dependencies = [ - "wgpu-hal", + "wgpu-hal 25.0.2", ] [[package]] @@ -3570,7 +6205,16 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" dependencies = [ - "wgpu-hal", + "wgpu-hal 25.0.2", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal 30.0.1", ] [[package]] @@ -3582,7 +6226,7 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.8.0", "bitflags 2.13.2", "block", "bytemuck", @@ -3592,7 +6236,7 @@ dependencies = [ "glow", "glutin_wgl_sys", "gpu-alloc", - "gpu-allocator", + "gpu-allocator 0.27.0", "gpu-descriptor", "hashbrown 0.15.5", "js-sys", @@ -3601,7 +6245,7 @@ dependencies = [ "libloading", "log", "metal", - "naga", + "naga 25.0.1", "ndk-sys 0.5.0+25.2.9519653", "objc", "ordered-float", @@ -3615,11 +6259,68 @@ dependencies = [ "thiserror 2.0.21", "wasm-bindgen", "web-sys", - "wgpu-types", + "wgpu-types 25.0.0", "windows 0.58.0", "windows-core 0.58.0", ] +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.10.0", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "gpu-allocator 0.28.0", + "hashbrown 0.17.1", + "libc", + "libloading", + "log", + "naga 30.0.1", + "naga-types", + "objc2 0.6.5", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "static_assertions", + "thiserror 2.0.21", + "wgpu-naga-bridge", + "wgpu-types 30.0.1", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga 30.0.1", + "wgpu-types 30.0.1", +] + [[package]] name = "wgpu-types" version = "25.0.0" @@ -3634,6 +6335,50 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "serde", + "static_assertions", + "web-sys", +] + +[[package]] +name = "wgsl-parse" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cee524ba5cfabfd65d10a316b646f8a4c50f3663e7e787471f21b783261f47" +dependencies = [ + "annotate-snippets", + "derive_more", + "itertools 0.15.0", + "lalrpop-util", + "lexical", + "logos", + "thiserror 2.0.21", + "wgsl-types", +] + +[[package]] +name = "wgsl-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e119dd12e57d937397535201959ddddb5dbf26505402db5ef305ebb1ca9a7e5f" +dependencies = [ + "half", + "itertools 0.15.0", + "num-traits", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3681,11 +6426,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -3697,6 +6454,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -3723,6 +6489,19 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -3731,7 +6510,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -3800,6 +6590,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -3818,6 +6618,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -3837,6 +6646,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3889,6 +6707,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3947,7 +6774,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.13.2", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases", @@ -4013,6 +6840,24 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.6", + "thiserror 2.0.21", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.4" @@ -4082,6 +6927,12 @@ version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + [[package]] name = "yoke" version = "0.8.4" @@ -4211,6 +7062,12 @@ dependencies = [ "serde", ] +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + [[package]] name = "zerocopy" version = "0.8.63" @@ -4261,6 +7118,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -4269,6 +7127,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index e15fc94..b0e443f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/bevy_forge_core", "crates/bevy_forge_editor", "crates/bevy_forge_cli", + "crates/bevy_forge_runtime", ] exclude = ["compat/bsn-smoke"] resolver = "2" diff --git a/README.md b/README.md index 2040633..b0bcf5f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,20 @@ the output easy to review, diff and check in. Live in-game editing (a Bevy plugin with a remote protocol) is a roadmap item, not part of this editor. +## v2 live editor (M0 spike) + +The `v2-live-editor` branch moves editing into the game as a **Bevy 0.20 plugin** with native Bevy +UI and official `bevy_ui_widgets` — no egui in the runtime crate. M0 ships the `EditorPlugin`, a +fixed editor shell, live hierarchy, reflection inspector, translate gizmo, editor camera and +deterministic screenshots at 1440×900 / 1000×640. + +```bash +cargo run -p bevy_forge_runtime --example editor_spike +``` + +See [docs/m0-spike.md](docs/m0-spike.md) for controls, architecture and M0 limits. The v1 standalone +editor below remains the current released workflow. + ## Two viewports | 2D scene editing | 3D wireframe preview | diff --git a/crates/bevy_forge_runtime/Cargo.toml b/crates/bevy_forge_runtime/Cargo.toml new file mode 100644 index 0000000..54366f1 --- /dev/null +++ b/crates/bevy_forge_runtime/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "bevy_forge_runtime" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false +description = "Live in-game scene editor plugin for Bevy 0.20 (M0 spike)" + +[dependencies] +# Frozen M0 feature set. Do not add `tonemapping_luts` (ktx2/zstd) — cameras use +# `Tonemapping::AcesFitted` instead of the LUT-based default. +bevy = { version = "0.20", default-features = false, features = [ + "std", + "async_executor", + "multi_threaded", + "bevy_asset", + "bevy_log", + "bevy_color", + "bevy_image", + "bevy_camera", + "bevy_light", + "bevy_mesh", + "bevy_pbr", + "bevy_render", + "bevy_core_pipeline", + "bevy_ui", + "bevy_ui_render", + "bevy_ui_widgets", + "bevy_text", + "bevy_input_focus", + "bevy_window", + "bevy_winit", + "bevy_picking", + "ui_picking", + "bevy_gizmos", + "bevy_gizmos_render", + "default_font", + "png", + "x11", + "wayland", + "reflect_auto_register", +] } diff --git a/crates/bevy_forge_runtime/examples/editor_spike.rs b/crates/bevy_forge_runtime/examples/editor_spike.rs new file mode 100644 index 0000000..1626a2d --- /dev/null +++ b/crates/bevy_forge_runtime/examples/editor_spike.rs @@ -0,0 +1,155 @@ +//! M0 spike demo: a small hardcoded 3D scene plus the Bevy Forge editor plugin. +//! +//! Run: +//! cargo run -p bevy_forge_runtime --example editor_spike +//! +//! Deterministic screenshot: +//! FORGE_SCREENSHOT=$PWD/.swarm/m0/evidence/m0-1440x900.png \ +//! FORGE_SCREENSHOT_SIZE=1440x900 \ +//! cargo run -p bevy_forge_runtime --example editor_spike + +use bevy::{prelude::*, render::view::Tonemapping, window::WindowResolution}; +use bevy_forge_runtime::{EditorPlugin, Selection}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Forge — M0 Spike".into(), + resolution: WindowResolution::new(1440, 900).with_scale_factor_override(1.0), + ..default() + }), + ..default() + })) + .add_plugins(EditorPlugin) + .add_systems(Startup, demo_scene) + .run(); +} + +fn demo_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut selection: ResMut, +) { + commands.insert_resource(GlobalAmbientLight { + color: Color::WHITE, + brightness: 600.0, + ..default() + }); + + let ground = materials.add(StandardMaterial { + base_color: Color::srgb(0.16, 0.20, 0.17), + perceptual_roughness: 0.95, + metallic: 0.0, + ..default() + }); + commands.spawn(( + Name::new("Ground"), + Mesh3d(meshes.add(Plane3d::default().mesh().size(14.0, 14.0))), + MeshMaterial3d(ground), + Transform::from_xyz(0.0, -0.02, 0.0), + )); + + let hero_mat = materials.add(StandardMaterial { + base_color: Color::srgb(0.85, 0.55, 0.25), + perceptual_roughness: 0.65, + metallic: 0.0, + ..default() + }); + let hat_mat = materials.add(StandardMaterial { + base_color: Color::srgb(0.25, 0.45, 0.75), + perceptual_roughness: 0.5, + metallic: 0.0, + ..default() + }); + let hero = commands + .spawn(( + Name::new("Hero Cube"), + Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + MeshMaterial3d(hero_mat), + Transform::from_xyz(0.0, 0.5, 0.0), + )) + .with_children(|parent| { + parent.spawn(( + Name::new("Hat"), + Mesh3d(meshes.add(Cuboid::new(0.55, 0.18, 0.55))), + MeshMaterial3d(hat_mat), + Transform::from_xyz(0.0, 0.62, 0.0), + )); + }) + .id(); + + let red_mat = materials.add(StandardMaterial { + base_color: Color::srgb(0.75, 0.18, 0.16), + perceptual_roughness: 0.7, + ..default() + }); + let blue_mat = materials.add(StandardMaterial { + base_color: Color::srgb(0.18, 0.35, 0.78), + perceptual_roughness: 0.7, + ..default() + }); + commands.spawn(( + Name::new("Red Cube"), + Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))), + MeshMaterial3d(red_mat), + Transform::from_xyz(-2.6, 0.4, 0.6), + )); + commands.spawn(( + Name::new("Blue Cube"), + Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))), + MeshMaterial3d(blue_mat), + Transform::from_xyz(2.6, 0.4, -0.6), + )); + + let crate_mat = materials.add(StandardMaterial { + base_color: Color::srgb(0.55, 0.38, 0.20), + perceptual_roughness: 0.9, + ..default() + }); + let stack = commands + .spawn(( + Name::new("Crate Stack"), + Transform::from_xyz(0.0, 0.0, -2.8), + Visibility::default(), + )) + .with_children(|parent| { + parent.spawn(( + Name::new("Crate A"), + Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))), + MeshMaterial3d(crate_mat.clone()), + Transform::from_xyz(0.0, 0.4, 0.0), + )); + parent.spawn(( + Name::new("Crate B"), + Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))), + MeshMaterial3d(crate_mat), + Transform::from_xyz(0.0, 1.2, 0.0), + )); + }) + .id(); + + commands.spawn(( + Name::new("Sun"), + DirectionalLight { + illuminance: 12_000.0, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Placeholder camera. T04 adopts it for the editor orbit/pan/zoom controls. + commands.spawn(( + Name::new("Editor Camera"), + Camera3d::default(), + Tonemapping::AcesFitted, + Transform::from_xyz(6.0, 5.0, 10.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y), + )); + + selection.0 = Some(hero); + info!( + "demo scene ready: hero={hero:?}, stack={stack:?}, selected={:?}", + selection.0 + ); +} diff --git a/crates/bevy_forge_runtime/src/gizmo.rs b/crates/bevy_forge_runtime/src/gizmo.rs new file mode 100644 index 0000000..96c25aa --- /dev/null +++ b/crates/bevy_forge_runtime/src/gizmo.rs @@ -0,0 +1,727 @@ +//! Translate gizmo and editor camera. +//! +//! M0 scope: +//! - adopt the host app's placeholder `Camera3d` (or spawn one) and drive it with +//! right-drag orbit, middle-drag pan and wheel zoom; +//! - draw world-aligned RGB axes on the selected entity's `GlobalTransform`; +//! - pick an axis in screen space, then drag it with a ray/plane calculation and +//! write the result back to the selected entity's `Transform.translation`. +//! +//! M0 limit: the world-space drag delta is converted into parent-local space +//! with the parent's *inverse rotation* only. Parent scale is ignored, so a +//! scaled parent makes the gizmo travel at the wrong world-space rate. Generic +//! component editing, local-space gizmos and snapping are M1+. + +use bevy::{ + input::mouse::{MouseMotion, MouseScrollUnit, MouseWheel}, + prelude::*, + render::view::Tonemapping, + window::PrimaryWindow, +}; + +use crate::{ + plugin::{editor_active, Selection}, + ui::EditorLayout, +}; + +/// Screen-space distance (logical pixels) within which a click hits a gizmo axis. +pub const AXIS_PICK_RADIUS_PX: f32 = 12.0; + +const GIZMO_LENGTH_PER_DISTANCE: f32 = 0.18; +const GIZMO_MIN_LENGTH: f32 = 0.8; +const GIZMO_MAX_LENGTH: f32 = 3.0; + +const MIN_CAMERA_DISTANCE: f32 = 2.0; +const MAX_CAMERA_DISTANCE: f32 = 40.0; +const DEFAULT_CAMERA_DISTANCE: f32 = 12.5; +const MAX_CAMERA_PITCH: f32 = 85.0 * std::f32::consts::PI / 180.0; + +const ORBIT_SENSITIVITY: f32 = 0.005; +const PAN_SENSITIVITY: f32 = 0.0015; +const ZOOM_STEP: f32 = 0.9; + +const RAY_EPSILON: f32 = 1e-6; + +/// Orbit/pan/zoom state of the one editor camera. +/// +/// The camera position is derived from `focus`, `yaw`, `pitch` and `distance` +/// instead of being stored directly so the controls stay a simple orbit model. +#[derive(Component, Debug, Clone, Copy, PartialEq)] +pub struct EditorCamera { + /// World-space point the camera orbits around and looks at. + pub focus: Vec3, + /// Rotation around `Vec3::Y`, radians. + pub yaw: f32, + /// Elevation above the focus plane, radians, clamped to ±85°. + pub pitch: f32, + /// Distance from `focus` to the camera, clamped to `2.0..=40.0`. + pub distance: f32, +} + +impl Default for EditorCamera { + /// The M0 placeholder pose from the demo scene: eye `(6, 5, 10)`, looking + /// at `(0, 0.5, 0)`. + fn default() -> Self { + let placeholder = + Transform::from_xyz(6.0, 5.0, 10.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y); + editor_camera_from_transform(&placeholder) + } +} + +/// One of the three translate axes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GizmoAxis { + /// World +X. + X, + /// World +Y. + Y, + /// World +Z. + Z, +} + +impl GizmoAxis { + /// All axes in RGB order. + pub const ALL: [Self; 3] = [Self::X, Self::Y, Self::Z]; + + /// Unit world-space direction of this axis. + pub fn direction(self) -> Vec3 { + match self { + Self::X => Vec3::X, + Self::Y => Vec3::Y, + Self::Z => Vec3::Z, + } + } +} + +/// Active translate-gizmo drag. Reset to `Default` when no drag is running. +#[derive(Resource, Default, Debug, Clone)] +pub struct GizmoDrag { + /// Whether a drag is currently in progress. + pub active: bool, + /// The entity being moved. + pub entity: Option, + /// The axis being dragged. + pub axis: Option, + /// `Transform.translation` of the entity when the drag began (parent-local). + pub start_translation: Vec3, + /// First ray/plane hit in world space. + pub start_world_hit: Vec3, + /// World-space gizmo origin when the drag began. + pub origin: Vec3, + /// Unit world-space direction of the dragged axis. + pub axis_world: Vec3, + /// Normal of the drag plane (perpendicular to the axis). + pub plane_normal: Vec3, +} + +impl GizmoDrag { + /// Ends the current drag and clears all stored state. + pub fn clear(&mut self) { + *self = Self::default(); + } +} + +/// Adds the editor camera controls and the translate gizmo. +pub struct GizmoPlugin; + +impl Plugin for GizmoPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + // `PostStartup` deliberately runs after the host app's `Startup` scene + // so the placeholder camera exists and can be adopted. + .add_systems(PostStartup, ensure_editor_camera) + .add_systems( + Update, + ( + editor_camera_control, + log_selection, + begin_gizmo_drag, + update_gizmo_drag, + draw_gizmo, + ) + .chain() + .run_if(editor_active), + ) + // Always clean up, even if the editor was disabled mid-drag. + .add_systems(Update, end_gizmo_drag); + } +} + +/// Adopts the host app's placeholder `Camera3d`, or spawns one if there is none. +/// +/// Adoption never touches the existing `Transform`, so the initial framing is +/// preserved; only the orbit state is derived from it. A spawned camera uses the +/// M0 placeholder pose and `Tonemapping::AcesFitted` (no LUT assets). +fn ensure_editor_camera( + mut commands: Commands, + existing: Query<(), With>, + cameras: Query<(Entity, &Transform), With>, +) { + if !existing.is_empty() { + info!("gizmo: camera ready"); + return; + } + + if let Some((entity, transform)) = cameras.iter().next() { + commands + .entity(entity) + .insert(editor_camera_from_transform(transform)); + debug!("gizmo: adopted camera {entity}"); + info!("gizmo: camera ready"); + return; + } + + let editor = EditorCamera::default(); + let transform = camera_transform(&editor); + commands.spawn(( + Name::new("Editor Camera"), + Camera3d::default(), + Tonemapping::AcesFitted, + transform, + GlobalTransform::from(transform), + editor, + )); + debug!("gizmo: spawned camera"); + info!("gizmo: camera ready"); +} + +/// Derives orbit state from a camera transform by looking along its forward ray. +/// +/// The distance is estimated as the camera's distance from the world origin, +/// which keeps the placeholder framing stable while giving the orbit controls a +/// sensible pivot. Returns the default distance for a camera at the origin. +pub fn editor_camera_from_transform(transform: &Transform) -> EditorCamera { + let position = transform.translation; + let mut distance = position.length(); + if !distance.is_finite() || distance < 0.01 { + distance = DEFAULT_CAMERA_DISTANCE; + } + + let focus = position + *transform.forward() * distance; + let offset = position - focus; + let horizontal = Vec2::new(offset.x, offset.z).length(); + let yaw = offset.x.atan2(offset.z); + let pitch = offset + .y + .atan2(horizontal) + .clamp(-MAX_CAMERA_PITCH, MAX_CAMERA_PITCH); + + EditorCamera { + focus, + yaw, + pitch, + distance, + } +} + +/// Builds a camera transform from orbit state. `looking_at` keeps roll at zero. +pub fn camera_transform(camera: &EditorCamera) -> Transform { + let (sin_yaw, cos_yaw) = camera.yaw.sin_cos(); + let (sin_pitch, cos_pitch) = camera.pitch.sin_cos(); + let offset = Vec3::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw) * camera.distance; + + Transform::from_translation(camera.focus + offset).looking_at(camera.focus, Vec3::Y) +} + +/// Gizmo axis length for a camera distance: roughly screen-constant, clamped. +pub fn gizmo_length(camera_distance: f32) -> f32 { + (camera_distance * GIZMO_LENGTH_PER_DISTANCE).clamp(GIZMO_MIN_LENGTH, GIZMO_MAX_LENGTH) +} + +/// Right-drag orbit, middle-drag pan, wheel zoom while the cursor is in the viewport. +fn editor_camera_control( + windows: Query<&Window, With>, + layout: Res, + mouse: Res>, + mut motion: MessageReader, + mut wheel: MessageReader, + mut cameras: Query<(&mut EditorCamera, &mut Transform)>, +) { + // Consume input every frame so a dragged-then-released button does not leave + // stale deltas queued for the next drag. + let motion_delta = motion + .read() + .fold(Vec2::ZERO, |total, event| total + event.delta); + let wheel_delta: f32 = wheel + .read() + .map(|event| match event.unit { + MouseScrollUnit::Line => event.y, + MouseScrollUnit::Pixel => event.y / 100.0, + }) + .sum(); + + let Ok(window) = windows.single() else { + return; + }; + let in_viewport = window + .cursor_position() + .is_some_and(|cursor| layout.viewport_rect(window).contains(cursor)); + if !in_viewport { + return; + } + + let Ok((mut camera, mut transform)) = cameras.single_mut() else { + return; + }; + let mut changed = false; + + if mouse.pressed(MouseButton::Right) && motion_delta != Vec2::ZERO { + camera.yaw -= motion_delta.x * ORBIT_SENSITIVITY; + camera.pitch = (camera.pitch + motion_delta.y * ORBIT_SENSITIVITY) + .clamp(-MAX_CAMERA_PITCH, MAX_CAMERA_PITCH); + changed = true; + } else if mouse.pressed(MouseButton::Middle) && motion_delta != Vec2::ZERO { + let right = *transform.right(); + let up = *transform.up(); + let scale = camera.distance * PAN_SENSITIVITY; + camera.focus += (-right * motion_delta.x + up * motion_delta.y) * scale; + changed = true; + } + + if wheel_delta != 0.0 { + camera.distance = (camera.distance * ZOOM_STEP.powf(wheel_delta)) + .clamp(MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); + changed = true; + } + + if changed { + *transform = camera_transform(&camera); + } +} + +/// Logs `gizmo: selected {entity}` once per selection, not once per frame. +fn log_selection(selection: Res, mut last_logged: Local>) { + if *last_logged == selection.0 { + return; + } + *last_logged = selection.0; + if let Some(entity) = selection.0 { + info!("gizmo: selected {entity}"); + } +} + +/// Draws the world-aligned axes at the selected entity's global position. +fn draw_gizmo( + mut gizmos: Gizmos, + selection: Res, + entities: Query<&GlobalTransform>, + cameras: Query<&EditorCamera>, +) { + let Some(entity) = selection.0 else { + return; + }; + let Ok(global) = entities.get(entity) else { + return; + }; + + let distance = cameras + .iter() + .next() + .map_or(DEFAULT_CAMERA_DISTANCE, |camera| camera.distance); + let length = gizmo_length(distance); + gizmos.axes(Transform::from_translation(global.translation()), length); +} + +/// Picks the nearest axis on left press inside the viewport and starts a drag. +fn begin_gizmo_drag( + mouse: Res>, + windows: Query<&Window, With>, + layout: Res, + selection: Res, + targets: Query<(&Transform, &GlobalTransform)>, + cameras: Query<(&Camera, &GlobalTransform, &EditorCamera)>, + mut drag: ResMut, +) { + if !mouse.just_pressed(MouseButton::Left) { + return; + } + + let Ok(window) = windows.single() else { + return; + }; + let Some(cursor) = window.cursor_position() else { + return; + }; + if !layout.viewport_rect(window).contains(cursor) { + return; + } + + let Some(entity) = selection.0 else { + return; + }; + let Ok((target_transform, target_global)) = targets.get(entity) else { + return; + }; + let Ok((camera, camera_global, editor_camera)) = cameras.single() else { + return; + }; + + let origin = target_global.translation(); + let length = gizmo_length(editor_camera.distance); + let Ok(origin_screen) = camera.world_to_viewport(camera_global, origin) else { + return; + }; + + let mut endpoints = [(GizmoAxis::X, Vec2::ZERO); 3]; + let mut endpoint_count = 0; + for axis in GizmoAxis::ALL { + let world_end = origin + axis.direction() * length; + if let Ok(screen_end) = camera.world_to_viewport(camera_global, world_end) { + endpoints[endpoint_count] = (axis, screen_end); + endpoint_count += 1; + } + } + + let Some(axis) = pick_axis( + cursor, + origin_screen, + &endpoints[..endpoint_count], + AXIS_PICK_RADIUS_PX, + ) else { + return; + }; + + let axis_world = axis.direction(); + let normal = axis_plane_normal(axis_world, *camera_global.forward()); + let Ok(ray) = camera.viewport_to_world(camera_global, cursor) else { + return; + }; + let Some(start_world_hit) = ray_plane_hit(ray.origin, *ray.direction, origin, normal) else { + return; + }; + + *drag = GizmoDrag { + active: true, + entity: Some(entity), + axis: Some(axis), + start_translation: target_transform.translation, + start_world_hit, + origin, + axis_world, + plane_normal: normal, + }; +} + +/// Updates the dragged translation while the left button stays held. +fn update_gizmo_drag( + mouse: Res>, + windows: Query<&Window, With>, + layout: Res, + cameras: Query<(&Camera, &GlobalTransform)>, + parents: Query<&ChildOf>, + globals: Query<&GlobalTransform>, + mut transforms: Query<&mut Transform>, + mut drag: ResMut, +) { + if !drag.active { + return; + } + if !mouse.pressed(MouseButton::Left) { + drag.clear(); + return; + } + let (Some(entity), Some(_axis)) = (drag.entity, drag.axis) else { + drag.clear(); + return; + }; + + let Ok(window) = windows.single() else { + return; + }; + let Some(cursor) = window.cursor_position() else { + return; + }; + if !layout.viewport_rect(window).contains(cursor) { + return; + } + let Ok((camera, camera_global)) = cameras.single() else { + return; + }; + let Ok(ray) = camera.viewport_to_world(camera_global, cursor) else { + return; + }; + let Some(hit) = ray_plane_hit(ray.origin, *ray.direction, drag.origin, drag.plane_normal) + else { + return; + }; + + let parent_rotation = parent_rotation_of(entity, &parents, &globals); + let translation = dragged_translation( + drag.start_translation, + drag.start_world_hit, + hit, + drag.axis_world, + parent_rotation, + ); + + let Ok(mut transform) = transforms.get_mut(entity) else { + drag.clear(); + return; + }; + transform.translation = translation; +} + +/// Ends the drag on left release. Runs even when the editor is disabled. +fn end_gizmo_drag(mouse: Res>, mut drag: ResMut) { + if drag.active && mouse.just_released(MouseButton::Left) { + drag.clear(); + } +} + +/// Reads the parent's global rotation for a selected entity, or identity. +fn parent_rotation_of( + entity: Entity, + parents: &Query<&ChildOf>, + globals: &Query<&GlobalTransform>, +) -> Quat { + parents + .get(entity) + .ok() + .and_then(|child_of| globals.get(child_of.parent()).ok()) + .map(GlobalTransform::rotation) + .unwrap_or(Quat::IDENTITY) +} + +/// Screen-space distance from `point` to the segment `start..end`. +pub fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f32 { + let segment = end - start; + let length_squared = segment.length_squared(); + if length_squared <= f32::EPSILON { + return point.distance(start); + } + let t = ((point - start).dot(segment) / length_squared).clamp(0.0, 1.0); + point.distance(start + segment * t) +} + +/// Picks the nearest axis segment within `max_distance_px`, independent of order. +pub fn pick_axis( + cursor: Vec2, + origin: Vec2, + endpoints: &[(GizmoAxis, Vec2)], + max_distance_px: f32, +) -> Option { + let mut best: Option<(GizmoAxis, f32)> = None; + for &(axis, endpoint) in endpoints { + let distance = point_segment_distance(cursor, origin, endpoint); + if distance > max_distance_px { + continue; + } + if best.is_none_or(|(_, best_distance)| distance < best_distance) { + best = Some((axis, distance)); + } + } + best.map(|(axis, _)| axis) +} + +/// Returns the drag-plane normal for an axis: camera forward projected onto the +/// plane perpendicular to the axis. Falls back to any perpendicular vector when +/// the view direction and the axis are (nearly) parallel. +pub fn axis_plane_normal(axis: Vec3, camera_forward: Vec3) -> Vec3 { + let axis = axis.normalize_or_zero(); + let projected = camera_forward - axis * camera_forward.dot(axis); + if projected.length_squared() > RAY_EPSILON * RAY_EPSILON { + projected.normalize() + } else { + axis.any_orthonormal_vector() + } +} + +/// Intersects a ray with a plane. Returns `None` for parallel rays and hits +/// behind the ray origin. +pub fn ray_plane_hit( + ray_origin: Vec3, + ray_direction: Vec3, + plane_origin: Vec3, + plane_normal: Vec3, +) -> Option { + let denominator = ray_direction.dot(plane_normal); + if denominator.abs() <= RAY_EPSILON { + return None; + } + let t = (plane_origin - ray_origin).dot(plane_normal) / denominator; + if t < 0.0 { + return None; + } + Some(ray_origin + ray_direction * t) +} + +/// Scalar distance a plane hit moved along `axis`. +pub fn axis_delta(start_hit: Vec3, current_hit: Vec3, axis: Vec3) -> f32 { + (current_hit - start_hit).dot(axis.normalize_or_zero()) +} + +/// Converts a world-space delta into the parent's local space. +/// +/// M0 limit: only the parent's inverse rotation is applied; parent scale is +/// intentionally ignored. A scaled parent therefore changes the effective +/// world-space speed of the drag. +pub fn world_delta_to_local(world_delta: Vec3, parent_rotation: Quat) -> Vec3 { + parent_rotation.inverse() * world_delta +} + +/// Computes the new parent-local translation for a drag from two plane hits. +pub fn dragged_translation( + start_translation: Vec3, + start_hit: Vec3, + current_hit: Vec3, + axis_world: Vec3, + parent_rotation: Quat, +) -> Vec3 { + let scalar = axis_delta(start_hit, current_hit, axis_world); + let world_delta = axis_world * scalar; + start_translation + world_delta_to_local(world_delta, parent_rotation) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_approx(actual: f32, expected: f32) { + assert!( + (actual - expected).abs() < 1e-4, + "expected {expected}, got {actual}" + ); + } + + fn assert_vec3_approx(actual: Vec3, expected: Vec3) { + assert!( + actual.abs_diff_eq(expected, 1e-4), + "expected {expected:?}, got {actual:?}" + ); + } + + #[test] + fn point_segment_distance_inside_and_outside() { + let start = Vec2::new(0.0, 0.0); + let end = Vec2::new(10.0, 0.0); + + // Perpendicular projection lands inside the segment. + assert_approx(point_segment_distance(Vec2::new(5.0, 3.0), start, end), 3.0); + // Outside the segment, nearest point is the start cap. + assert_approx( + point_segment_distance(Vec2::new(-4.0, 3.0), start, end), + 5.0, + ); + // Outside the segment, nearest point is the end cap. + assert_approx( + point_segment_distance(Vec2::new(14.0, -3.0), start, end), + 5.0, + ); + // Degenerate segment. + assert_approx( + point_segment_distance(Vec2::new(3.0, 4.0), start, start), + 5.0, + ); + } + + #[test] + fn nearest_axis_is_picked_within_the_screen_threshold() { + let origin = Vec2::new(100.0, 100.0); + let endpoints = [ + (GizmoAxis::X, Vec2::new(160.0, 100.0)), + (GizmoAxis::Y, Vec2::new(100.0, 40.0)), + (GizmoAxis::Z, Vec2::new(40.0, 100.0)), + ]; + + assert_eq!( + pick_axis(Vec2::new(140.0, 104.0), origin, &endpoints, 12.0), + Some(GizmoAxis::X) + ); + assert_eq!( + pick_axis(Vec2::new(100.0, 44.0), origin, &endpoints, 12.0), + Some(GizmoAxis::Y) + ); + // Far away from every axis segment. + assert_eq!( + pick_axis(Vec2::new(100.0, 130.0), origin, &endpoints, 12.0), + None + ); + } + + #[test] + fn ray_plane_hit_returns_expected_point_and_rejects_parallel() { + let hit = ray_plane_hit(Vec3::new(0.0, 5.0, 0.0), Vec3::NEG_Y, Vec3::ZERO, Vec3::Y) + .expect("downward ray should hit the XZ plane"); + assert_vec3_approx(hit, Vec3::ZERO); + + // Parallel ray never intersects the plane. + assert_eq!( + ray_plane_hit(Vec3::ZERO, Vec3::X, Vec3::new(0.0, 1.0, 0.0), Vec3::Y), + None + ); + + // A hit behind the ray origin is not a valid hit. + assert_eq!( + ray_plane_hit(Vec3::new(0.0, 1.0, 0.0), Vec3::Y, Vec3::ZERO, Vec3::Y), + None + ); + } + + #[test] + fn axis_delta_projects_a_plane_hit_onto_the_axis() { + let start = Vec3::new(0.0, 0.0, 0.0); + let current = Vec3::new(2.5, 9.0, -4.0); + + assert_approx(axis_delta(start, current, Vec3::X), 2.5); + assert_approx(axis_delta(start, current, Vec3::Y), 9.0); + // Non-normalized axis directions are normalized before projecting. + assert_approx(axis_delta(start, current, Vec3::Z * 2.0), -4.0); + } + + #[test] + fn world_delta_to_local_uses_the_inverse_parent_rotation() { + // Parent rotated +90° around Y: a world +X delta is local +Z. + let parent_rotation = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2); + let local = world_delta_to_local(Vec3::new(2.0, 0.0, 0.0), parent_rotation); + assert_vec3_approx(local, Vec3::new(0.0, 0.0, 2.0)); + } + + #[test] + fn drag_helper_changes_transform_by_expected_axis_delta() { + let start_translation = Vec3::new(1.0, 2.0, 3.0); + let start_hit = Vec3::ZERO; + let current_hit = Vec3::new(2.0, 7.0, -3.0); + + let translation = dragged_translation( + start_translation, + start_hit, + current_hit, + Vec3::X, + Quat::IDENTITY, + ); + let mut transform = Transform::from_translation(start_translation); + transform.translation = translation; + assert_vec3_approx(transform.translation, Vec3::new(3.0, 2.0, 3.0)); + + // Under a parent rotated +90° around Y, the same world delta becomes +2 on local Z. + let parent_rotation = Quat::from_rotation_y(std::f32::consts::FRAC_PI_2); + let translation = dragged_translation( + start_translation, + start_hit, + current_hit, + Vec3::X, + parent_rotation, + ); + let mut transform = Transform::from_translation(start_translation); + transform.translation = translation; + assert_vec3_approx(transform.translation, Vec3::new(1.0, 2.0, 5.0)); + } + + #[test] + fn gizmo_length_scales_with_distance_and_clamps() { + assert_approx(gizmo_length(2.0), GIZMO_MIN_LENGTH); + assert_approx(gizmo_length(12.5), 2.25); + assert_approx(gizmo_length(40.0), GIZMO_MAX_LENGTH); + } + + #[test] + fn adopted_camera_keeps_its_position() { + let transform = + Transform::from_xyz(6.0, 5.0, 10.0).looking_at(Vec3::new(0.0, 0.5, 0.0), Vec3::Y); + let camera = editor_camera_from_transform(&transform); + let rebuilt = camera_transform(&camera); + + assert_vec3_approx(rebuilt.translation, transform.translation); + assert_vec3_approx(*rebuilt.forward(), *transform.forward()); + } +} diff --git a/crates/bevy_forge_runtime/src/hierarchy.rs b/crates/bevy_forge_runtime/src/hierarchy.rs new file mode 100644 index 0000000..db1144a --- /dev/null +++ b/crates/bevy_forge_runtime/src/hierarchy.rs @@ -0,0 +1,510 @@ +//! Live hierarchy panel. +//! +//! Collects the scene tree from `ChildOf` / `Children`, renders it as an +//! indented, scrollable [`ListBox`] inside the left [`HierarchyPanel`] and binds +//! row activation to [`Selection`]. +//! +//! The panel is rebuilt only when the scene snapshot changes. Editor-owned UI +//! entities, observers and window entities are not part of the scene snapshot, +//! so rebuilding the panel can never feed back into itself. + +use std::collections::HashSet; + +use bevy::{ + picking::hover::Hovered, + prelude::*, + text::FontSize, + ui::{ScrollPosition, Selected}, + ui_widgets::{ListBox, ListItem, ScrollArea, ValueChange}, +}; + +use crate::{ + plugin::{editor_active, Selection}, + ui::{HierarchyPanel, UiTheme}, +}; + +/// Largest depth a hierarchy row may have. Deeper descendants are not listed. +const MAX_DEPTH: usize = 64; + +/// One row of the hierarchy tree, in render order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HierarchyRowData { + /// Entity this row represents. + pub entity: Entity, + /// Display name: the entity's `Name`, or `Entity#{index}` when missing. + pub name: String, + /// Zero-based depth. Roots have depth `0`. + pub depth: usize, +} + +/// Marker component on a list row, mapping it to the scene entity it selects. +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +pub struct HierarchyRow { + /// Scene entity selected when this row is activated. + pub target: Entity, +} + +/// Marker for the single list entity inside the hierarchy panel. +#[derive(Component)] +struct HierarchyList; + +/// One collected tree row including its parent, used for snapshot comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +struct TreeRow { + entity: Entity, + name: String, + parent: Option, + depth: usize, +} + +/// Snapshot of the last rendered tree: `(entity, name, parent, depth)` in render order. +#[derive(Resource, Default)] +struct HierarchyTreeState { + signature: Vec<(Entity, String, Option, usize)>, + rows: usize, + roots: usize, +} + +/// Adds the live hierarchy panel. +pub struct HierarchyPlugin; + +impl Plugin for HierarchyPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_systems( + Update, + (rebuild_hierarchy, sync_selected_rows, style_hierarchy_rows) + .chain() + .run_if(editor_active), + ); + } +} + +/// Collects the full world tree: roots first, children in `Children` order. +/// +/// Roots are entities without `ChildOf`, sorted by [`Entity`]. Unnamed entities +/// fall back to `Entity#{index}`. A visited set prevents duplicate rows and a +/// depth cap of [`MAX_DEPTH`] prevents pathological trees from stalling the UI. +pub fn collect_rows(world: &mut World) -> Vec { + collect_tree(world, false) + .into_iter() + .map(|row| HierarchyRowData { + entity: row.entity, + name: row.name, + depth: row.depth, + }) + .collect() +} + +/// Collects the scene tree: only named, non-UI spatial entities may be roots. +/// +/// Children of a scene root are listed regardless of whether they have a +/// `Name`, so unnamed scene children still show up with the index fallback. +fn collect_scene_rows(world: &World) -> Vec { + collect_tree(world, true) +} + +/// Shared tree walk. +/// +/// When `scene_only` is set, roots additionally require `Name` and reject +/// `Node`, which keeps the editor UI, global observers and window entities out +/// of the hierarchy (and out of the rebuild snapshot). +fn collect_tree(world: &World, scene_only: bool) -> Vec { + let mut roots: Vec = world + .iter_entities() + .filter_map(|entity| { + let id = entity.id(); + if entity.get::().is_some() { + return None; + } + if scene_only && !is_scene_root(world, id) { + return None; + } + Some(id) + }) + .collect(); + roots.sort(); + + let mut rows = Vec::new(); + let mut visited = HashSet::new(); + for root in roots { + push_subtree(world, root, None, 0, &mut visited, &mut rows); + } + rows +} + +/// Whether an entity is a root of the editable scene rather than of the editor +/// or of an engine-internal helper (UI nodes, observers, window and gizmo +/// renderer entities, ...). +fn is_scene_root(world: &World, entity: Entity) -> bool { + world.get::(entity).is_some() + && world.get::(entity).is_some() + && world.get::(entity).is_none() +} + +/// Depth-first, in-order push of one entity and its descendants. +fn push_subtree( + world: &World, + entity: Entity, + parent: Option, + depth: usize, + visited: &mut HashSet, + rows: &mut Vec, +) { + if depth > MAX_DEPTH || !visited.insert(entity) { + return; + } + + let name = world + .get::(entity) + .map(|name| name.as_str().to_owned()) + .unwrap_or_else(|| format!("Entity#{}", entity.index())); + let children: Vec = world + .get::(entity) + .map(|children| children.to_vec()) + .unwrap_or_default(); + + rows.push(TreeRow { + entity, + name, + parent, + depth, + }); + + if depth >= MAX_DEPTH { + return; + } + for child in children { + push_subtree(world, child, Some(entity), depth + 1, visited, rows); + } +} + +/// Rebuilds the panel contents when the scene tree snapshot changes. +/// +/// This is an exclusive system so the old list is despawned and the new one is +/// spawned in the same tick: `sync_selected_rows` never observes stale rows and +/// a rebuild can never run twice for one snapshot. +fn rebuild_hierarchy(world: &mut World) { + let rows = collect_scene_rows(world); + let root_count = rows.iter().filter(|row| row.depth == 0).count(); + let signature: Vec<(Entity, String, Option, usize)> = rows + .iter() + .map(|row| (row.entity, row.name.clone(), row.parent, row.depth)) + .collect(); + + let previous = { + let state = world.resource::(); + (state.signature.clone(), state.rows, state.roots) + }; + if previous.0 == signature { + return; + } + + let panel = world + .query_filtered::>() + .iter(world) + .next(); + let Some(panel) = panel else { + return; + }; + + let theme = world.resource::().clone(); + let selection = *world.resource::(); + + // Drop the previous list (and its rows) before re-populating the panel. + let mut list_query = world.query_filtered::>(); + let old_lists: Vec = list_query.iter(world).collect(); + drop(list_query); + for list in old_lists { + world.despawn(list); + } + // The shell's placeholder ("Waiting for scene...") is replaced by the list. + world.entity_mut(panel).despawn_children(); + + let header = world + .spawn(hierarchy_label("Hierarchy", 14.0, theme.text)) + .id(); + world.entity_mut(panel).add_child(header); + + let list = world + .spawn(( + HierarchyList, + ListBox, + ScrollArea, + ScrollPosition::default(), + Node { + width: percent(100), + flex_grow: 1.0, + flex_direction: FlexDirection::Column, + overflow: Overflow::scroll_y(), + ..default() + }, + BackgroundColor(Color::NONE), + )) + .id(); + world.entity_mut(list).observe(hierarchy_row_activated); + world.entity_mut(panel).add_child(list); + + for row in &rows { + let is_selected = selection.0 == Some(row.entity); + let row_entity = world + .spawn(( + ListItem, + HierarchyRow { target: row.entity }, + Node { + width: percent(100), + height: px(22.0), + flex_shrink: 0.0, + padding: UiRect::left(px(6.0 + row.depth as f32 * 14.0)), + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(if is_selected { + theme.row_selected + } else { + Color::NONE + }), + Hovered::default(), + )) + .id(); + if is_selected { + world.entity_mut(row_entity).insert(Selected); + } + + let text = world + .spawn(hierarchy_label( + row.name.clone(), + 13.0, + if is_selected { + theme.text + } else { + theme.text_muted + }, + )) + .id(); + world.entity_mut(row_entity).add_child(text); + world.entity_mut(list).add_child(row_entity); + } + + let count_changed = previous.1 != rows.len() || previous.2 != root_count; + *world.resource_mut::() = HierarchyTreeState { + signature, + rows: rows.len(), + roots: root_count, + }; + if count_changed || previous.1 == 0 { + info!("hierarchy: {} rows, {} roots", rows.len(), root_count); + debug!( + "hierarchy rows: {:?}", + rows.iter().map(|row| row.name.as_str()).collect::>() + ); + } +} + +/// Creates a plain editor label bundle. +fn hierarchy_label(value: impl Into, size: f32, color: Color) -> impl Bundle { + ( + Text::new(value.into()), + TextFont { + font: FontSource::default(), + font_size: FontSize::Px(size), + ..default() + }, + TextColor(color), + ) +} + +/// Observer on the list entity: a committed row activation selects its target. +fn hierarchy_row_activated( + change: On>, + rows: Query<&HierarchyRow>, + mut selection: ResMut, +) { + let value = change.event().value; + let Ok(row) = rows.get(value) else { + return; + }; + if selection.0 != Some(row.target) { + selection.0 = Some(row.target); + } +} + +/// Keeps the widget's `Selected` marker in sync with [`Selection`]. +fn sync_selected_rows( + mut commands: Commands, + selection: Res, + rows: Query<(Entity, &HierarchyRow, Has)>, +) { + for (entity, row, selected) in &rows { + let should_select = selection.0 == Some(row.target); + if should_select && !selected { + commands.entity(entity).insert(Selected); + } else if !should_select && selected { + commands.entity(entity).remove::(); + } + } +} + +/// Styles rows from [`UiTheme`] using the `Selected` marker and pointer hover. +fn style_hierarchy_rows( + theme: Res, + selection: Res, + mut rows: Query<( + &HierarchyRow, + &mut BackgroundColor, + Has, + &Hovered, + &Children, + )>, + mut texts: Query<&mut TextColor>, +) { + for (row, mut background, selected, hovered, children) in &mut rows { + let is_selected = selected || selection.0 == Some(row.target); + let is_hovered = hovered.get() && !is_selected; + let background_color = if is_selected { + theme.row_selected + } else if is_hovered { + theme.row_hover + } else { + Color::NONE + }; + if background.0 != background_color { + background.0 = background_color; + } + + let text_color = if is_selected || is_hovered { + theme.text + } else { + theme.text_muted + }; + for child in children.iter() { + if let Ok(mut color) = texts.get_mut(child) { + if color.0 != text_color { + color.0 = text_color; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Keeps only the rows for entities spawned by a test. + /// + /// A bare [`World`] can contain internal entities (for example the reserved + /// index-zero entity); they are valid hierarchy roots but not part of these + /// focused ordering tests. + fn rows_for(world: &mut World, entities: &[Entity]) -> Vec { + collect_rows(world) + .into_iter() + .filter(|row| entities.contains(&row.entity)) + .collect() + } + + #[test] + fn collect_rows_walks_three_levels_in_children_order() { + let mut world = World::new(); + let root = world.spawn(Name::new("Root")).id(); + let child = world.spawn((Name::new("Child"), ChildOf(root))).id(); + let grandchild = world.spawn((Name::new("Grandchild"), ChildOf(child))).id(); + let sibling = world.spawn((Name::new("Sibling"), ChildOf(root))).id(); + + let rows = rows_for(&mut world, &[root, child, grandchild, sibling]); + let order: Vec<(&str, usize)> = rows + .iter() + .map(|row| (row.name.as_str(), row.depth)) + .collect(); + assert_eq!( + order, + vec![("Root", 0), ("Child", 1), ("Grandchild", 2), ("Sibling", 1),] + ); + assert_eq!(rows[0].entity, root); + assert_eq!(rows[1].entity, child); + assert_eq!(rows[2].entity, grandchild); + assert_eq!(rows[3].entity, sibling); + } + + #[test] + fn collect_rows_falls_back_to_entity_index() { + let mut world = World::new(); + let root = world.spawn(Name::new("Root")).id(); + let unnamed_child = world.spawn(ChildOf(root)).id(); + let unnamed_root = world.spawn_empty().id(); + + let rows = rows_for(&mut world, &[root, unnamed_child, unnamed_root]); + assert_eq!(rows.len(), 3); + let row = |entity: Entity| { + rows.iter() + .find(|row| row.entity == entity) + .expect("row for entity") + }; + assert_eq!(row(root).name, "Root"); + assert_eq!(row(root).depth, 0); + assert_eq!( + row(unnamed_child).name, + format!("Entity#{}", unnamed_child.index()) + ); + assert_eq!(row(unnamed_child).depth, 1); + assert_eq!( + row(unnamed_root).name, + format!("Entity#{}", unnamed_root.index()) + ); + assert_eq!(row(unnamed_root).depth, 0); + } + + #[test] + fn collect_rows_caps_depth_at_64() { + let mut world = World::new(); + let mut parent = world.spawn(Name::new("0")).id(); + let mut chain = vec![parent]; + for depth in 1..70 { + parent = world + .spawn((Name::new(format!("{depth}")), ChildOf(parent))) + .id(); + chain.push(parent); + } + + let rows = rows_for(&mut world, &chain); + assert_eq!(rows.len(), MAX_DEPTH + 1); + assert_eq!(rows.last().expect("rows").depth, MAX_DEPTH); + assert!(rows.iter().all(|row| row.depth <= MAX_DEPTH)); + } + + #[test] + fn value_change_selects_the_row_target() { + let mut world = World::new(); + world.init_resource::(); + + let list = world.spawn(HierarchyList).id(); + let target = world.spawn(Name::new("Target")).id(); + let row = world.spawn((ListItem, HierarchyRow { target })).id(); + world.entity_mut(list).observe(hierarchy_row_activated); + + world.trigger(ValueChange:: { + source: list, + value: row, + is_final: true, + }); + + assert_eq!(world.resource::().0, Some(target)); + } + + #[test] + fn value_change_ignores_entities_without_a_row() { + let mut world = World::new(); + world.init_resource::(); + + let list = world.spawn(HierarchyList).id(); + let stranger = world.spawn(Name::new("Stranger")).id(); + world.entity_mut(list).observe(hierarchy_row_activated); + + world.trigger(ValueChange:: { + source: list, + value: stranger, + is_final: true, + }); + + assert_eq!(world.resource::().0, None); + } +} diff --git a/crates/bevy_forge_runtime/src/inspector.rs b/crates/bevy_forge_runtime/src/inspector.rs new file mode 100644 index 0000000..28461a2 --- /dev/null +++ b/crates/bevy_forge_runtime/src/inspector.rs @@ -0,0 +1,784 @@ +//! Reflection-driven inspector for the selected entity. +//! +//! M0 inspects three known components (`Name`, `Transform`, `Visibility`): they are +//! fetched with typed queries and then walked with `bevy_reflect`. Generic discovery +//! of arbitrary component types through `TypeRegistry` is explicitly **M1** work and +//! is not implemented here; this panel only shows the three M0 components the brief +//! promises. + +use bevy::{ + prelude::*, + reflect::{enums::VariantType, PartialReflect, ReflectMut, ReflectRef}, + text::FontSize, + ui_widgets::{ + Slider, SliderRange, SliderStep, SliderThumb, SliderValue, TrackClick, ValueChange, + }, +}; + +use crate::{ + plugin::{editor_active, Selection}, + ui::{InspectorPanel, UiTheme}, +}; + +/// Minimum / maximum value of the translation sliders. +const SLIDER_MIN: f32 = -20.0; +const SLIDER_MAX: f32 = 20.0; +/// Step used by keyboard interaction and by the widget's step notifications. +const SLIDER_STEP: f32 = 0.1; +/// Above this delta the slider value is considered stale and is re-synced. +const SYNC_TOLERANCE: f32 = 0.001; +/// Maximum reflection nesting depth for the field summary. +const MAX_SUMMARY_DEPTH: usize = 4; + +/// One flattened, displayable reflection field. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReflectedField { + /// Dotted display label, e.g. `translation.x`. + pub label: String, + /// Formatted value, e.g. `1.5`. + pub value: String, + /// Path of field names (or tuple indices) below the summarized root. + pub path: Vec, + /// Whether this leaf is a scalar numeric value a generic editor could edit. + pub editable: bool, +} + +/// Flattens the reflected field tree below `value` into display rows. +/// +/// Named structs, tuple structs, enums and scalar leaves are handled. The walk is +/// depth-limited so opaque wrappers (for example the hash wrapper inside [`Name`]) +/// cannot recurse forever. +pub fn summarize_reflect(value: &dyn Reflect) -> Vec { + // `Name` wraps a pre-hashed `Hashed>`, whose reflection is opaque and + // whose Display output is a debug dump. Present the readable name instead; the + // value still comes from a reflected downcast, not from a hard-coded string. + if let Some(name) = value.downcast_ref::() { + return vec![ReflectedField { + label: "Name".to_string(), + value: name.as_str().to_string(), + path: vec!["name".to_string()], + editable: false, + }]; + } + + let mut fields = Vec::new(); + collect_fields(value.as_partial_reflect(), &mut Vec::new(), 0, &mut fields); + fields +} + +/// Writes `value` into an `f32` field at `path`, e.g. `["translation", "x"]`. +/// +/// Returns `false` (without panicking) when a path segment does not exist or when +/// the addressed field is not an `f32`. +pub fn apply_f32_path(root: &mut dyn Reflect, path: &[&str], value: f32) -> bool { + apply_f32_in(root.as_partial_reflect_mut(), path, value) +} + +/// Slider attached to one translation axis of the selected entity. +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +pub struct InspectorSlider { + /// Entity whose `Transform` is edited by this slider. + pub entity: Entity, + /// Translation axis edited by this slider. + pub axis: TransformAxis, +} + +/// Translation axis edited by an [`InspectorSlider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransformAxis { + /// X axis. + X, + /// Y axis. + Y, + /// Z axis. + Z, +} + +impl TransformAxis { + const ALL: [Self; 3] = [Self::X, Self::Y, Self::Z]; + + fn index(self) -> usize { + match self { + Self::X => 0, + Self::Y => 1, + Self::Z => 2, + } + } + + fn name(self) -> &'static str { + match self { + Self::X => "x", + Self::Y => "y", + Self::Z => "z", + } + } +} + +/// Adds the editor inspector panel behavior. +pub struct InspectorPlugin; + +impl Plugin for InspectorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::().add_systems( + Update, + // Sync the current sliders before a selection change rebuilds/despawns + // them. The reverse order queues a SliderValue insert for an entity that + // the rebuild has already despawned, which panics in Bevy's command queue. + (sync_inspector_from_transform, rebuild_inspector) + .chain() + .run_if(editor_active), + ); + } +} + +/// Value label next to one translation slider. +#[derive(Component)] +struct InspectorValueLabel { + axis: TransformAxis, +} + +/// Tracks what the panel currently displays so it only rebuilds on selection change. +#[derive(Resource, Default)] +struct InspectorBuildState { + initialized: bool, + selection: Option, +} + +/// Owned description of the selected entity, built once per selection change. +struct InspectorView { + name: String, + translation: [f32; 3], + rotation: String, + scale: String, + visibility: String, + has_transform: bool, + has_visibility: bool, +} + +/// Rebuilds the inspector body when the selection changes. +fn rebuild_inspector( + mut commands: Commands, + panels: Query<(Entity, Option<&Children>), With>, + selection: Res, + names: Query<&Name>, + transforms: Query<&Transform>, + visibilities: Query<&Visibility>, + theme: Res, + mut built: ResMut, +) { + if built.initialized && built.selection == selection.0 { + return; + } + let Some((panel_entity, children)) = panels.iter().next() else { + return; + }; + + built.initialized = true; + built.selection = selection.0; + + if let Some(children) = children { + for child in children.iter() { + commands.entity(child).despawn(); + } + } + + let selected = selection.0; + let view = selected.map(|entity| build_view(entity, &names, &transforms, &visibilities)); + + if let Some(entity) = selected { + let name = view + .as_ref() + .map(|view| view.name.as_str()) + .unwrap_or(""); + info!("inspector: selected {entity} ({name})"); + } + + commands.entity(panel_entity).with_children(|panel| { + panel.spawn(label("Inspector", 14.0, theme.text)); + + let Some(view) = &view else { + panel.spawn(label( + "Select an entity in the hierarchy", + 11.0, + theme.text_muted, + )); + return; + }; + + spawn_read_only_row(panel, "Name", &view.name, &theme); + panel.spawn(label("Transform", 11.0, theme.accent)); + + if view.has_transform { + if let Some(entity) = selected { + for axis in TransformAxis::ALL { + spawn_slider_row(panel, entity, axis, view.translation[axis.index()], &theme); + } + } + spawn_read_only_row(panel, "Rotation", &view.rotation, &theme); + spawn_read_only_row(panel, "Scale", &view.scale, &theme); + } else { + panel.spawn(label("No Transform component", 11.0, theme.text_muted)); + } + + if view.has_visibility { + spawn_read_only_row(panel, "Visibility", &view.visibility, &theme); + } else { + panel.spawn(label("No Visibility component", 11.0, theme.text_muted)); + } + }); +} + +/// Applies a slider edit to the selected entity's `Transform` through reflection. +fn on_slider_value_change( + change: On>, + sliders: Query<&InspectorSlider>, + mut transforms: Query<&mut Transform>, + mut commands: Commands, +) { + let Ok(slider) = sliders.get(change.source) else { + return; + }; + let Ok(mut transform) = transforms.get_mut(slider.entity) else { + return; + }; + + let path = ["translation", slider.axis.name()]; + if apply_f32_path(transform.as_reflect_mut(), &path, change.value) { + // Keep the widget's own state in step with the edit; the gizmo-facing + // sync system below handles external Transform changes. + commands + .entity(change.source) + .insert(SliderValue(change.value)); + } +} + +/// Copies the selected entity's `Transform.translation` into slider state and labels. +/// +/// This runs every frame while the editor is active. The small tolerance prevents a +/// slider drag (which already applied the value to the `Transform`) from being +/// overwritten by a slightly stale widget value. +fn sync_inspector_from_transform( + mut commands: Commands, + selection: Res, + transforms: Query<&Transform>, + sliders: Query<(Entity, &InspectorSlider, &SliderValue, &SliderRange)>, + children: Query<&Children>, + mut thumbs: Query<&mut Node, With>, + mut labels: Query<(&InspectorValueLabel, &mut Text)>, +) { + let Some(entity) = selection.0 else { + return; + }; + let Ok(transform) = transforms.get(entity) else { + return; + }; + let translation = transform.translation; + let values = [translation.x, translation.y, translation.z]; + + for (slider_entity, slider, current, range) in &sliders { + let target = range.clamp(values[slider.axis.index()]); + if (current.0 - target).abs() > SYNC_TOLERANCE { + commands.entity(slider_entity).insert(SliderValue(target)); + } + + let thumb_percent = percent(range.thumb_position(target) * 100.0); + for descendant in children.iter_descendants(slider_entity) { + if let Ok(mut thumb) = thumbs.get_mut(descendant) { + if thumb.left != thumb_percent { + thumb.left = thumb_percent; + } + break; + } + } + } + + for (label, mut text) in &mut labels { + let formatted = format!("{:.1}", values[label.axis.index()]); + if text.0 != formatted { + text.0 = formatted; + } + } +} + +/// Builds the owned display model for one entity with reflection traversal. +fn build_view( + entity: Entity, + names: &Query<&Name>, + transforms: &Query<&Transform>, + visibilities: &Query<&Visibility>, +) -> InspectorView { + let mut view = InspectorView { + name: format!("Entity#{}", entity.index()), + translation: [0.0; 3], + rotation: "-".to_string(), + scale: "-".to_string(), + visibility: "-".to_string(), + has_transform: false, + has_visibility: false, + }; + + if let Ok(name) = names.get(entity) { + let fields = summarize_reflect(name); + view.name = fields + .first() + .map(|field| field.value.clone()) + .unwrap_or_else(|| name.as_str().to_string()); + } + + if let Ok(transform) = transforms.get(entity) { + let fields = summarize_reflect(transform); + for axis in TransformAxis::ALL { + view.translation[axis.index()] = field_f32(&fields, &["translation", axis.name()]) + .unwrap_or_else(|| translation_component(transform.translation, axis)); + } + view.rotation = joined_component_values(&fields, "rotation"); + view.scale = joined_component_values(&fields, "scale"); + view.has_transform = true; + } + + if let Ok(visibility) = visibilities.get(entity) { + let fields = summarize_reflect(visibility); + view.visibility = fields + .first() + .map(|field| field.value.clone()) + .unwrap_or_else(|| format!("{visibility:?}")); + view.has_visibility = true; + } + + view +} + +fn translation_component(translation: Vec3, axis: TransformAxis) -> f32 { + match axis { + TransformAxis::X => translation.x, + TransformAxis::Y => translation.y, + TransformAxis::Z => translation.z, + } +} + +/// Spawns one `X`/`Y`/`Z` slider row with its value label. +fn spawn_slider_row( + parent: &mut ChildSpawnerCommands, + entity: Entity, + axis: TransformAxis, + value: f32, + theme: &UiTheme, +) { + parent + .spawn(Node { + width: percent(100), + height: px(24), + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + column_gap: px(6), + ..default() + }) + .with_children(|row| { + row.spawn(label(axis.name(), 12.0, theme.text_muted)); + + row.spawn(( + Node { + flex_grow: 1.0, + height: px(12), + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + ..default() + }, + Slider { + track_click: TrackClick::Snap, + ..default() + }, + SliderValue(value), + SliderRange::new(SLIDER_MIN, SLIDER_MAX), + SliderStep(SLIDER_STEP), + InspectorSlider { entity, axis }, + )) + .observe(on_slider_value_change) + .with_children(|slider| { + // Background rail. + slider.spawn(( + Node { + height: px(6), + border_radius: BorderRadius::all(px(3)), + ..default() + }, + BackgroundColor(theme.panel_bg_alt), + )); + + // Invisible track, inset by the thumb width so percent placement works. + slider + .spawn(Node { + display: Display::Flex, + position_type: PositionType::Absolute, + left: px(0), + right: px(12), + top: px(0), + bottom: px(0), + ..default() + }) + .with_children(|track| { + track.spawn(( + SliderThumb, + Node { + display: Display::Flex, + width: px(12), + height: px(12), + position_type: PositionType::Absolute, + left: percent(slider_thumb_percent(value, SLIDER_MIN, SLIDER_MAX)), + border_radius: BorderRadius::MAX, + ..default() + }, + BackgroundColor(theme.accent), + )); + }); + }); + + row.spawn(( + InspectorValueLabel { axis }, + Text::new(format!("{value:.1}")), + TextFont { + font: FontSource::default(), + font_size: FontSize::Px(11.0), + ..default() + }, + TextColor(theme.text_muted), + Node { + width: px(44), + ..default() + }, + )); + }); +} + +/// Spawns a label/value row for read-only data. +fn spawn_read_only_row( + parent: &mut ChildSpawnerCommands, + name: &str, + value: &str, + theme: &UiTheme, +) { + parent + .spawn(Node { + width: percent(100), + min_height: px(18), + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::SpaceBetween, + align_items: AlignItems::Center, + column_gap: px(8), + ..default() + }) + .with_children(|row| { + row.spawn(label(name, 12.0, theme.text_muted)); + row.spawn(label(value, 12.0, theme.text)); + }); +} + +/// Builds a UI label bundle with the split `Text` / `TextFont` / `TextColor` API. +fn label(value: impl Into, size: f32, color: Color) -> impl Bundle { + ( + Text::new(value.into()), + TextFont { + font: FontSource::default(), + font_size: FontSize::Px(size), + ..default() + }, + TextColor(color), + ) +} + +fn slider_thumb_percent(value: f32, min: f32, max: f32) -> f32 { + if max <= min { + return 0.0; + } + ((value - min) / (max - min) * 100.0).clamp(0.0, 100.0) +} + +// --------------------------------------------------------------------------- +// Reflection helpers +// --------------------------------------------------------------------------- + +fn collect_fields( + value: &dyn PartialReflect, + path: &mut Vec, + depth: usize, + out: &mut Vec, +) { + if depth > MAX_SUMMARY_DEPTH { + push_leaf(value, path, out); + return; + } + + match value.reflect_ref() { + ReflectRef::Struct(struct_value) => { + let field_count = struct_value.field_len(); + if field_count == 0 { + push_leaf(value, path, out); + return; + } + for index in 0..field_count { + let Some(field) = struct_value.field_at(index) else { + continue; + }; + let field_name = struct_value.name_at(index).unwrap_or("?"); + path.push(field_name.to_string()); + collect_fields(field, path, depth + 1, out); + path.pop(); + } + } + ReflectRef::TupleStruct(tuple_struct) => { + let field_count = tuple_struct.field_len(); + if field_count == 0 { + push_leaf(value, path, out); + return; + } + for index in 0..field_count { + let Some(field) = tuple_struct.field(index) else { + continue; + }; + path.push(index.to_string()); + collect_fields(field, path, depth + 1, out); + path.pop(); + } + } + ReflectRef::Enum(enum_value) => match enum_value.variant_type() { + VariantType::Unit => { + let label = if path.is_empty() { + short_type_label(value) + } else { + path.join(".") + }; + out.push(ReflectedField { + label, + value: enum_value.variant_name().to_string(), + path: path.clone(), + editable: false, + }); + } + VariantType::Tuple | VariantType::Struct => { + for index in 0..enum_value.field_len() { + let Some(field) = enum_value.field_at(index) else { + continue; + }; + let field_name = enum_value + .name_at(index) + .map(str::to_string) + .unwrap_or_else(|| index.to_string()); + path.push(field_name); + collect_fields(field, path, depth + 1, out); + path.pop(); + } + } + }, + _ => push_leaf(value, path, out), + } +} + +fn push_leaf(value: &dyn PartialReflect, path: &[String], out: &mut Vec) { + let label = if path.is_empty() { + short_type_label(value) + } else { + path.join(".") + }; + out.push(ReflectedField { + label, + value: display_value(value), + path: path.to_vec(), + editable: is_numeric(value), + }); +} + +fn apply_f32_in(value: &mut dyn PartialReflect, path: &[&str], new_value: f32) -> bool { + let Some((head, tail)) = path.split_first() else { + return false; + }; + let Some(field) = reflected_field_mut(value, head) else { + return false; + }; + if tail.is_empty() { + match field.try_downcast_mut::() { + Some(slot) => { + *slot = new_value; + true + } + None => false, + } + } else { + apply_f32_in(field, tail, new_value) + } +} + +fn reflected_field_mut<'a>( + value: &'a mut dyn PartialReflect, + name: &str, +) -> Option<&'a mut dyn PartialReflect> { + match value.reflect_mut() { + ReflectMut::Struct(struct_value) => struct_value.field_mut(name), + ReflectMut::TupleStruct(tuple_struct) => name + .parse::() + .ok() + .and_then(|index| tuple_struct.field_mut(index)), + ReflectMut::Enum(enum_value) => enum_value.field_mut(name), + _ => None, + } +} + +fn field_f32(fields: &[ReflectedField], path: &[&str]) -> Option { + field_by_path(fields, path).and_then(|field| field.value.parse::().ok()) +} + +fn field_by_path<'a>(fields: &'a [ReflectedField], path: &[&str]) -> Option<&'a ReflectedField> { + fields.iter().find(|field| { + field + .path + .iter() + .map(String::as_str) + .eq(path.iter().copied()) + }) +} + +fn joined_component_values(fields: &[ReflectedField], group: &str) -> String { + let values: Vec<&str> = fields + .iter() + .filter(|field| field.path.first().map(String::as_str) == Some(group)) + .map(|field| field.value.as_str()) + .collect(); + if values.is_empty() { + "-".to_string() + } else { + values.join(", ") + } +} + +fn short_type_label(value: &dyn PartialReflect) -> String { + value + .reflect_type_path() + .rsplit("::") + .next() + .unwrap_or("value") + .to_string() +} + +fn is_numeric(value: &dyn PartialReflect) -> bool { + value.try_downcast_ref::().is_some() || value.try_downcast_ref::().is_some() +} + +fn display_value(value: &dyn PartialReflect) -> String { + if let Some(value) = value.try_downcast_ref::() { + return format_f32(*value); + } + if let Some(value) = value.try_downcast_ref::() { + return format_f64(*value); + } + if let Some(value) = value.try_downcast_ref::() { + return value.to_string(); + } + if let Some(value) = value.try_downcast_ref::() { + return value.clone(); + } + value.to_string() +} + +fn format_f32(value: f32) -> String { + format_number(value as f64) +} + +fn format_f64(value: f64) -> String { + format_number(value) +} + +fn format_number(value: f64) -> String { + let rounded = (value * 1000.0).round() / 1000.0; + if rounded == 0.0 { + "0.0".to_string() + } else if rounded.fract() == 0.0 { + format!("{rounded:.1}") + } else { + format!("{rounded}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Reflect)] + struct Pair(f32, bool); + + #[derive(Reflect)] + struct Point { + x: f32, + y: f32, + } + + fn field<'a>(fields: &'a [ReflectedField], path: &[&str]) -> &'a ReflectedField { + field_by_path(fields, path) + .unwrap_or_else(|| panic!("missing field {path:?} in {fields:?}")) + } + + #[test] + fn transform_summary_contains_translation_axes() { + let transform = Transform::from_xyz(1.5, -2.0, 3.25); + let fields = summarize_reflect(&transform); + + assert_eq!(field(&fields, &["translation", "x"]).label, "translation.x"); + assert_eq!(field(&fields, &["translation", "x"]).value, "1.5"); + assert_eq!(field(&fields, &["translation", "y"]).value, "-2.0"); + assert_eq!(field(&fields, &["translation", "z"]).value, "3.25"); + assert!(field(&fields, &["translation", "x"]).editable); + assert!(field_by_path(&fields, &["rotation", "w"]).is_some()); + assert!(field_by_path(&fields, &["scale", "y"]).is_some()); + } + + #[test] + fn name_summary_contains_the_name() { + let fields = summarize_reflect(&Name::new("Cube")); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].value, "Cube"); + assert!(!fields[0].editable); + } + + #[test] + fn visibility_summary_is_non_empty_and_contains_variant() { + let fields = summarize_reflect(&Visibility::Visible); + assert!(!fields.is_empty()); + assert!(fields.iter().any(|field| field.value.contains("Visible"))); + } + + #[test] + fn tuple_struct_summary_uses_index_paths() { + let fields = summarize_reflect(&Pair(1.25, true)); + assert_eq!(field(&fields, &["0"]).value, "1.25"); + assert_eq!(field(&fields, &["1"]).value, "true"); + } + + #[test] + fn named_struct_summary_flattens_fields() { + let fields = summarize_reflect(&Point { x: 2.0, y: -0.5 }); + assert_eq!(field(&fields, &["x"]).value, "2.0"); + assert_eq!(field(&fields, &["y"]).value, "-0.5"); + } + + #[test] + fn apply_f32_path_changes_translation_x() { + let mut transform = Transform::from_xyz(0.0, 0.0, 0.0); + assert!(apply_f32_path(&mut transform, &["translation", "x"], 2.5)); + assert_eq!(transform.translation, Vec3::new(2.5, 0.0, 0.0)); + } + + #[test] + fn apply_f32_path_rejects_wrong_path_without_panicking() { + let mut transform = Transform::default(); + assert!(!apply_f32_path(&mut transform, &[], 1.0)); + assert!(!apply_f32_path(&mut transform, &["translation"], 1.0)); + assert!(!apply_f32_path(&mut transform, &["translation", "w"], 1.0)); + assert!(!apply_f32_path( + &mut transform, + &["translation", "x", "extra"], + 1.0 + )); + assert!(!apply_f32_path(&mut transform, &["nonexistent"], 1.0)); + assert_eq!(transform, Transform::default()); + } +} diff --git a/crates/bevy_forge_runtime/src/lib.rs b/crates/bevy_forge_runtime/src/lib.rs new file mode 100644 index 0000000..8ccc0bd --- /dev/null +++ b/crates/bevy_forge_runtime/src/lib.rs @@ -0,0 +1,30 @@ +//! Bevy Forge live editor runtime (v2 M0 spike). +//! +//! [`EditorPlugin`] installs the editor resources, the fixed Bevy UI shell +//! (toolbar, hierarchy panel, inspector panel, status bar) and the module plugins +//! that provide hierarchy, inspector, gizmo and screenshot behavior. +//! +//! The plugin is designed to be added after `DefaultPlugins`: +//! +//! ```ignore +//! App::new() +//! .add_plugins(DefaultPlugins) +//! .add_plugins(EditorPlugin) +//! .run(); +//! ``` + +pub mod gizmo; +pub mod hierarchy; +pub mod inspector; +pub mod plugin; +pub mod screenshot; +pub mod ui; + +pub use gizmo::GizmoPlugin; +pub use hierarchy::HierarchyPlugin; +pub use inspector::InspectorPlugin; +pub use plugin::{editor_active, EditorPlugin, EditorState, InspectorSource, Selection}; +pub use screenshot::ScreenshotPlugin; +pub use ui::{ + EditorLayout, EditorRoot, HierarchyPanel, InspectorPanel, StatusBar, Toolbar, UiTheme, +}; diff --git a/crates/bevy_forge_runtime/src/plugin.rs b/crates/bevy_forge_runtime/src/plugin.rs new file mode 100644 index 0000000..5af857b --- /dev/null +++ b/crates/bevy_forge_runtime/src/plugin.rs @@ -0,0 +1,151 @@ +//! The root editor plugin and the shared editor resources. + +use bevy::prelude::*; + +use crate::{ + gizmo::GizmoPlugin, hierarchy::HierarchyPlugin, inspector::InspectorPlugin, + screenshot::ScreenshotPlugin, ui::shell::EditorUiPlugin, +}; + +/// Installs the Bevy Forge live editor on top of `DefaultPlugins`. +/// +/// The plugin is usable exactly once per app. It does not overwrite the +/// [`EditorState`] or [`Selection`] resources when the host app has already +/// inserted them. +pub struct EditorPlugin; + +impl Plugin for EditorPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_plugins(( + EditorUiPlugin, + HierarchyPlugin, + InspectorPlugin, + GizmoPlugin, + ScreenshotPlugin, + )) + .add_systems(Update, clear_dead_selection); + } +} + +/// Whether the editor UI and behavior are currently enabled. +#[derive(Resource, Debug, Clone)] +pub struct EditorState { + /// `true` while the editor is shown. Defaults to `true`. + pub enabled: bool, +} + +impl Default for EditorState { + fn default() -> Self { + Self { enabled: true } + } +} + +impl EditorState { + /// Flips the editor on or off. + pub fn toggle(&mut self) { + self.enabled = !self.enabled; + } +} + +/// Current single selection. `Selection(None)` means nothing is selected. +#[derive(Resource, Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Selection(pub Option); + +/// Optional bridge between the inspector and host-app-specific reflection data. +/// +/// M0 does not need a concrete implementation; T03 uses typed `Transform` / +/// `Name` / `Visibility` queries. The trait is part of the frozen public API so +/// M1 can plug in a component registry without changing the plugin surface. +pub trait InspectorSource { + /// Returns the reflected component data for `entity`, if any. + fn reflect_component(&self, entity: Entity) -> Option<&dyn Reflect>; +} + +/// Run condition: systems gated on this only run while the editor is enabled. +pub fn editor_active(state: Res) -> bool { + state.enabled +} + +/// Clears [`Selection`] when the selected entity was despawned. +fn clear_dead_selection(mut selection: ResMut, entities: Query) { + let cleared = clear_selection_if_dead(&mut selection, |entity| entities.get(entity).is_ok()); + if cleared { + debug!("selection: cleared (entity no longer exists)"); + } +} + +/// Clears the selection if `entity_alive` reports that it is gone. +/// +/// Returns `true` when a selection was cleared. Extracted so the cleanup rule +/// can be unit tested without configuring a full app. +pub(crate) fn clear_selection_if_dead( + selection: &mut Selection, + entity_alive: impl Fn(Entity) -> bool, +) -> bool { + let Some(entity) = selection.0 else { + return false; + }; + if entity_alive(entity) { + return false; + } + selection.0 = None; + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn editor_state_defaults_to_enabled() { + assert!(EditorState::default().enabled); + } + + #[test] + fn editor_state_toggles() { + let mut state = EditorState::default(); + state.toggle(); + assert!(!state.enabled); + state.toggle(); + assert!(state.enabled); + } + + #[test] + fn selection_defaults_to_none() { + assert_eq!(Selection::default().0, None); + } + + #[test] + fn dead_selection_is_cleared() { + let mut selection = Selection(Some(Entity::PLACEHOLDER)); + assert!(clear_selection_if_dead(&mut selection, |_| false)); + assert_eq!(selection.0, None); + } + + #[test] + fn live_selection_is_kept() { + let mut selection = Selection(Some(Entity::PLACEHOLDER)); + assert!(!clear_selection_if_dead(&mut selection, |_| true)); + assert_eq!(selection.0, Some(Entity::PLACEHOLDER)); + } + + #[test] + fn no_selection_is_a_no_op() { + let mut selection = Selection(None); + assert!(!clear_selection_if_dead(&mut selection, |_| false)); + assert_eq!(selection.0, None); + } + + #[test] + fn plugin_registers_resources_without_overwriting_existing_ones() { + let mut app = App::new(); + app.insert_resource(EditorState { enabled: false }); + app.add_plugins(EditorPlugin); + + assert!(app.world().contains_resource::()); + assert_eq!(app.world().resource::().0, None); + assert!(!app.world().resource::().enabled); + } +} diff --git a/crates/bevy_forge_runtime/src/screenshot.rs b/crates/bevy_forge_runtime/src/screenshot.rs new file mode 100644 index 0000000..064cbb5 --- /dev/null +++ b/crates/bevy_forge_runtime/src/screenshot.rs @@ -0,0 +1,374 @@ +//! Deterministic screenshot plugin. +//! +//! Opt-in environment contract: +//! +//! | Variable | Default | Meaning | +//! |---|---|---| +//! | `FORGE_SCREENSHOT` | unset | output PNG path; unset disables the mode | +//! | `FORGE_SCREENSHOT_SIZE` | `1440x900` | `WIDTHxHEIGHT`, each `320..=7680` | +//! | `FORGE_SCREENSHOT_FRAMES` | `300` | frames before capture, `1..=1200` | +//! | `FORGE_SCREENSHOT_WARMUP_SECS` | `10` | wall-clock seconds, `0..=120` | +//! +//! Unset means a complete no-op: no systems, no window resize, no exit. Invalid +//! values log an error and disable the mode. Capture waits for both thresholds +//! because the probe showed mesh/PBR pipelines need about ten seconds to warm up +//! (a 30-frame shot is blank). Success saves the PNG and writes +//! [`AppExit::Success`]; if no capture is confirmed within 300 frames, a fallback +//! exits with an error. + +use std::path::{Path, PathBuf}; + +use bevy::{ + prelude::*, + render::view::screenshot::{save_to_disk, Screenshot, ScreenshotCaptured}, + window::{PrimaryWindow, WindowResolution}, +}; + +const PATH_ENV: &str = "FORGE_SCREENSHOT"; +const SIZE_ENV: &str = "FORGE_SCREENSHOT_SIZE"; +const FRAMES_ENV: &str = "FORGE_SCREENSHOT_FRAMES"; +const WARMUP_ENV: &str = "FORGE_SCREENSHOT_WARMUP_SECS"; + +const DEFAULT_WIDTH: u32 = 1440; +const DEFAULT_HEIGHT: u32 = 900; +const DEFAULT_FRAMES: u32 = 300; +const DEFAULT_WARMUP_SECS: u32 = 10; + +const MIN_DIMENSION: u32 = 320; +const MAX_DIMENSION: u32 = 7680; +const MIN_FRAMES: u32 = 1; +const MAX_FRAMES: u32 = 1200; +const MAX_WARMUP_SECS: u32 = 120; +const FALLBACK_FRAMES: u32 = 300; + +/// Adds opt-in deterministic screenshot capture (`FORGE_SCREENSHOT`). +pub struct ScreenshotPlugin; + +impl Plugin for ScreenshotPlugin { + fn build(&self, app: &mut App) { + match config_from_env() { + Ok(config) => configure(app, config), + Err(error) => error!("screenshot: {error}; screenshot mode disabled"), + } + } +} + +#[derive(Resource, Debug, Clone, PartialEq, Eq)] +struct ScreenshotConfig { + path: PathBuf, + width: u32, + height: u32, + frames: u32, + warmup_secs: u32, +} + +#[derive(Resource, Debug, Default)] +struct ScreenshotState { + frames_elapsed: u32, + capture_requested: bool, + frames_since_capture: u32, + exit_requested: bool, +} + +/// Installs the mode only when `config` is `Some`; `None` is a complete no-op. +fn configure(app: &mut App, config: Option) { + let Some(config) = config else { + return; + }; + app.insert_resource(config) + .init_resource::() + .add_systems(Startup, apply_screenshot_window) + .add_systems(Update, request_screenshot); +} + +/// Forces the primary window to the requested physical size at scale factor 1.0. +fn apply_screenshot_window( + config: Res, + mut windows: Query<&mut Window, With>, +) { + let Ok(mut window) = windows.single_mut() else { + error!( + "screenshot: no primary window; cannot set {}x{}", + config.width, config.height + ); + return; + }; + window.resolution = + WindowResolution::new(config.width, config.height).with_scale_factor_override(1.0); + info!( + "screenshot: window set to {}x{} (scale factor 1.0)", + config.width, config.height + ); +} + +/// Requests one capture after both thresholds, then exits on capture or fallback. +fn request_screenshot( + mut commands: Commands, + config: Res, + mut state: ResMut, + real_time: Res>, + mut exit: MessageWriter, +) { + if state.exit_requested { + return; + } + if state.capture_requested { + state.frames_since_capture = state.frames_since_capture.saturating_add(1); + if state.frames_since_capture >= FALLBACK_FRAMES { + warn!("screenshot: capture was not confirmed within {FALLBACK_FRAMES} frames; exiting"); + state.exit_requested = true; + exit.write(AppExit::error()); + } + return; + } + + state.frames_elapsed = state.frames_elapsed.saturating_add(1); + let warmup_elapsed = real_time.elapsed_secs_f64() >= f64::from(config.warmup_secs); + if state.frames_elapsed < config.frames || !warmup_elapsed { + return; + } + if let Err(error) = create_parent_dirs(&config.path) { + error!("screenshot: {error}; exiting"); + state.exit_requested = true; + exit.write(AppExit::error()); + return; + } + info!( + "screenshot: saving {} ({}x{})", + config.path.display(), + config.width, + config.height + ); + commands + .spawn(Screenshot::primary_window()) + .observe(save_to_disk(config.path.clone())) + .observe(exit_after_screenshot); + state.capture_requested = true; +} + +fn exit_after_screenshot(_captured: On, mut exit: MessageWriter) { + exit.write(AppExit::Success); +} + +fn create_parent_dirs(path: &Path) -> Result<(), String> { + match path.parent() { + Some(parent) if !parent.as_os_str().is_empty() => { + std::fs::create_dir_all(parent).map_err(|error| { + format!( + "cannot create output directory {}: {error}", + parent.display() + ) + }) + } + _ => Ok(()), + } +} + +/// Reads the environment once; the path is checked first so unset stays a no-op +/// even when one of the other variables is malformed. +fn config_from_env() -> Result, String> { + let Some(path) = std::env::var_os(PATH_ENV) else { + return Ok(None); + }; + let path = PathBuf::from(path); + let size = env_string(SIZE_ENV)?; + let frames = env_string(FRAMES_ENV)?; + let warmup_secs = env_string(WARMUP_ENV)?; + parse_config( + Some(&path), + size.as_deref(), + frames.as_deref(), + warmup_secs.as_deref(), + ) +} + +fn env_string(name: &str) -> Result, String> { + match std::env::var(name) { + Ok(value) => Ok(Some(value)), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(std::env::VarError::NotUnicode(_)) => Err(format!("{name} must be valid UTF-8")), + } +} + +/// Returns `Ok(None)` when the path is absent; `Err` when it is malformed. +fn parse_config( + path: Option<&Path>, + size: Option<&str>, + frames: Option<&str>, + warmup_secs: Option<&str>, +) -> Result, String> { + let Some(path) = path else { + return Ok(None); + }; + if path.as_os_str().is_empty() { + return Err(format!("{PATH_ENV} must not be empty")); + } + let (width, height) = match size { + Some(raw) => parse_size(raw)?, + None => (DEFAULT_WIDTH, DEFAULT_HEIGHT), + }; + let frames = match frames { + Some(raw) => parse_decimal(FRAMES_ENV, raw, MIN_FRAMES, MAX_FRAMES)?, + None => DEFAULT_FRAMES, + }; + let warmup_secs = match warmup_secs { + Some(raw) => parse_decimal(WARMUP_ENV, raw, 0, MAX_WARMUP_SECS)?, + None => DEFAULT_WARMUP_SECS, + }; + Ok(Some(ScreenshotConfig { + path: path.to_path_buf(), + width, + height, + frames, + warmup_secs, + })) +} + +fn parse_size(raw: &str) -> Result<(u32, u32), String> { + let Some((width, height)) = raw.split_once('x') else { + return Err(format!("{SIZE_ENV} must be WIDTHxHEIGHT, got {raw:?}")); + }; + Ok(( + parse_decimal("width", width, MIN_DIMENSION, MAX_DIMENSION) + .map_err(|error| format!("{SIZE_ENV}: {error}"))?, + parse_decimal("height", height, MIN_DIMENSION, MAX_DIMENSION) + .map_err(|error| format!("{SIZE_ENV}: {error}"))?, + )) +} + +fn parse_decimal(label: &str, raw: &str, min: u32, max: u32) -> Result { + if raw.is_empty() || !raw.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(format!("{label} must be a decimal integer, got {raw:?}")); + } + let value: u32 = raw + .parse() + .map_err(|_| format!("{label} is out of range, got {raw:?}"))?; + if !(min..=max).contains(&value) { + return Err(format!("{label} must be in {min}..={max}, got {value}")); + } + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn path() -> &'static Path { + Path::new("/tmp/forge-screenshot.png") + } + + #[test] + fn screenshot_defaults_are_1440x900_300_frames_10_seconds() { + let config = parse_config(Some(path()), None, None, None) + .expect("defaults must parse") + .expect("a path enables the mode"); + assert_eq!( + (config.width, config.height), + (DEFAULT_WIDTH, DEFAULT_HEIGHT) + ); + assert_eq!((config.width, config.height), (1440, 900)); + assert_eq!(config.frames, 300); + assert_eq!(config.warmup_secs, 10); + assert_eq!(config.path, PathBuf::from(path())); + } + + #[test] + fn screenshot_parses_valid_size_and_limits() { + let config = parse_config(Some(path()), Some("1000x640"), Some("12"), Some("0")) + .expect("valid values must parse") + .expect("a path enables the mode"); + assert_eq!((config.width, config.height), (1000, 640)); + assert_eq!(config.frames, 12); + assert_eq!(config.warmup_secs, 0); + + let min = parse_config(Some(path()), Some("320x320"), Some("1"), Some("120")) + .unwrap() + .unwrap(); + assert_eq!((min.width, min.height), (320, 320)); + assert_eq!((min.frames, min.warmup_secs), (1, 120)); + + let max = parse_config(Some(path()), Some("7680x7680"), Some("1200"), None) + .unwrap() + .unwrap(); + assert_eq!((max.width, max.height), (7680, 7680)); + assert_eq!(max.frames, 1200); + } + + #[test] + fn screenshot_rejects_invalid_sizes() { + for raw in [ + "", + "abc", + "1440", + "1440x", + "x900", + "1440X900", + "1440 x 900", + "0x900", + "1440x0", + "319x900", + "1440x319", + "7681x900", + "1440x7681", + "1440x900x2", + "-1440x900", + ] { + assert!( + parse_config(Some(path()), Some(raw), None, None).is_err(), + "{raw:?} must be rejected" + ); + } + } + + #[test] + fn screenshot_rejects_invalid_frames_and_warmup() { + for raw in ["", "abc", "0", "1201", "-1", "300.0"] { + assert!( + parse_config(Some(path()), None, Some(raw), None).is_err(), + "frames {raw:?} must be rejected" + ); + } + for raw in ["", "abc", "121", "-1", "10.0"] { + assert!( + parse_config(Some(path()), None, None, Some(raw)).is_err(), + "warmup {raw:?} must be rejected" + ); + } + } + + #[test] + fn screenshot_unset_path_is_disabled_even_with_invalid_values() { + let config = parse_config(None, Some("not-a-size"), Some("0"), Some("999")) + .expect("an absent path must short-circuit validation"); + assert!(config.is_none()); + } + + #[test] + fn screenshot_unset_mode_touches_no_window_and_installs_no_systems() { + let config = parse_config(None, None, None, None).expect("unset is not an error"); + assert!(config.is_none()); + + let mut app = App::new(); + configure(&mut app, config); + app.update(); + + assert!(!app.world().contains_resource::()); + assert!(!app.world().contains_resource::()); + let mut windows = app.world_mut().query::<&Window>(); + assert_eq!(windows.iter(app.world()).count(), 0); + let mut screenshots = app.world_mut().query::<&Screenshot>(); + assert_eq!(screenshots.iter(app.world()).count(), 0); + } + + #[test] + fn screenshot_enabled_config_installs_mode_state() { + let config = parse_config(Some(path()), Some("1000x640"), None, None) + .unwrap() + .unwrap(); + let mut app = App::new(); + configure(&mut app, Some(config)); + assert!(app.world().contains_resource::()); + assert!(app.world().contains_resource::()); + assert_eq!(app.world().resource::().width, 1000); + } +} diff --git a/crates/bevy_forge_runtime/src/ui/mod.rs b/crates/bevy_forge_runtime/src/ui/mod.rs new file mode 100644 index 0000000..8c6abd3 --- /dev/null +++ b/crates/bevy_forge_runtime/src/ui/mod.rs @@ -0,0 +1,10 @@ +//! Editor UI: the fixed shell, its layout metrics and the shared theme. + +pub(crate) mod shell; +pub mod theme; + +pub use shell::{ + EditorLayout, EditorRoot, EntityCountText, HierarchyPanel, InspectorPanel, StatusBar, + StatusText, Toolbar, +}; +pub use theme::UiTheme; diff --git a/crates/bevy_forge_runtime/src/ui/shell.rs b/crates/bevy_forge_runtime/src/ui/shell.rs new file mode 100644 index 0000000..f267db1 --- /dev/null +++ b/crates/bevy_forge_runtime/src/ui/shell.rs @@ -0,0 +1,377 @@ +//! Fixed editor UI shell: toolbar, hierarchy panel, inspector panel and status bar. +//! +//! The center of the window is deliberately not a UI node: the 3D camera renders +//! full-window behind the panels and [`EditorLayout::viewport_rect`] is the +//! authoritative rectangle for viewport-only input (used by the gizmo). + +use bevy::{ + prelude::*, + text::FontSize, + ui_widgets::{Activate, Button as UiButton}, +}; + +use crate::{ + plugin::{EditorState, Selection}, + ui::theme::UiTheme, +}; + +/// Full-window root of the editor overlay. Hidden as a whole when the editor is disabled. +#[derive(Component)] +pub struct EditorRoot; + +/// Left-hand scene hierarchy panel. +#[derive(Component)] +pub struct HierarchyPanel; + +/// Right-hand inspector panel. +#[derive(Component)] +pub struct InspectorPanel; + +/// Top toolbar. +#[derive(Component)] +pub struct Toolbar; + +/// Bottom status bar. +#[derive(Component)] +pub struct StatusBar; + +/// The status bar's primary text node. +#[derive(Component)] +pub struct StatusText; + +/// Toolbar text that reports how many scene entities exist. +#[derive(Component)] +pub struct EntityCountText; + +/// Screen-space metrics of the overlay panels. +/// +/// The viewport is the area left uncovered by the panels: +/// `x = left`, `y = top`, `width = window - left - right`, +/// `height = window - top - bottom`. +#[derive(Resource, Debug, Clone, Copy)] +pub struct EditorLayout { + /// Width of the left hierarchy panel in logical pixels. + pub left: f32, + /// Width of the right inspector panel in logical pixels. + pub right: f32, + /// Height of the top toolbar in logical pixels. + pub top: f32, + /// Height of the bottom status bar in logical pixels. + pub bottom: f32, +} + +impl Default for EditorLayout { + fn default() -> Self { + Self { + left: 260.0, + right: 300.0, + top: 38.0, + bottom: 26.0, + } + } +} + +impl EditorLayout { + /// Rectangle of the 3D viewport in logical window coordinates. + /// + /// Width and height are clamped to be non-negative for tiny windows. + pub fn viewport_rect(&self, window: &Window) -> Rect { + let width = (window.width() - self.left - self.right).max(0.0); + let height = (window.height() - self.top - self.bottom).max(0.0); + Rect::new(self.left, self.top, self.left + width, self.top + height) + } +} + +/// Installs the fixed editor shell and its state-sync systems. +pub(crate) struct EditorUiPlugin; + +impl Plugin for EditorUiPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::() + .add_systems(Startup, build_shell) + .add_systems( + Update, + ( + toggle_editor_f1, + sync_editor_visibility, + update_status_text, + update_entity_count, + ) + .chain(), + ); + } +} + +/// Builds the editor overlay. Runs at `Startup`, before host-app scene spawning. +fn build_shell(mut commands: Commands, theme: Res, layout: Res) { + commands + .spawn(( + EditorRoot, + Node { + position_type: PositionType::Absolute, + width: percent(100), + height: percent(100), + ..default() + }, + BackgroundColor(Color::NONE), + )) + .with_children(|root| { + // --- Toolbar --------------------------------------------------- + root.spawn(( + Toolbar, + Node { + position_type: PositionType::Absolute, + top: px(0), + left: px(0), + right: px(0), + height: px(layout.top), + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + column_gap: px(10), + padding: UiRect::horizontal(px(10)), + border: UiRect::bottom(px(1)), + ..default() + }, + BackgroundColor(theme.toolbar_bg), + BorderColor::all(theme.border), + )) + .with_children(|bar| { + bar.spawn(label("Bevy Forge", 15.0, theme.accent)); + + bar.spawn(( + UiButton, + Node { + height: px(24), + padding: UiRect::horizontal(px(10)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + border: UiRect::all(px(1)), + ..default() + }, + BackgroundColor(theme.panel_bg_alt), + BorderColor::all(theme.border), + )) + .observe(toggle_editor_on_activate) + .with_children(|button| { + button.spawn(label("Editor (F1)", 13.0, theme.text)); + }); + + bar.spawn(Node { + flex_grow: 1.0, + ..default() + }); + bar.spawn(( + EntityCountText, + label("Entities: 0", 12.0, theme.text_muted), + )); + }); + + // --- Left hierarchy panel -------------------------------------- + root.spawn(( + HierarchyPanel, + Node { + position_type: PositionType::Absolute, + left: px(0), + top: px(layout.top), + bottom: px(layout.bottom), + width: px(layout.left), + flex_direction: FlexDirection::Column, + padding: UiRect::all(px(8)), + row_gap: px(4), + border: UiRect::right(px(1)), + ..default() + }, + BackgroundColor(theme.panel_bg), + BorderColor::all(theme.border), + )) + .with_children(|panel| { + panel.spawn(label("Hierarchy", 14.0, theme.text)); + panel.spawn(label("Waiting for scene...", 11.0, theme.text_muted)); + }); + + // --- Right inspector panel ------------------------------------- + root.spawn(( + InspectorPanel, + Node { + position_type: PositionType::Absolute, + right: px(0), + top: px(layout.top), + bottom: px(layout.bottom), + width: px(layout.right), + flex_direction: FlexDirection::Column, + padding: UiRect::all(px(8)), + row_gap: px(4), + border: UiRect::left(px(1)), + ..default() + }, + BackgroundColor(theme.panel_bg), + BorderColor::all(theme.border), + )) + .with_children(|panel| { + panel.spawn(label("Inspector", 14.0, theme.text)); + panel.spawn(label("Waiting for selection...", 11.0, theme.text_muted)); + }); + + // --- Bottom status bar ----------------------------------------- + root.spawn(( + StatusBar, + Node { + position_type: PositionType::Absolute, + left: px(0), + right: px(0), + bottom: px(0), + height: px(layout.bottom), + flex_direction: FlexDirection::Row, + align_items: AlignItems::Center, + padding: UiRect::horizontal(px(10)), + border: UiRect::top(px(1)), + ..default() + }, + BackgroundColor(theme.status_bg), + BorderColor::all(theme.border), + )) + .with_children(|bar| { + bar.spawn(( + StatusText, + label( + "No selection | Editor: on | F1 editor", + 12.0, + theme.text_muted, + ), + )); + }); + }); + + info!("editor shell ready (toolbar, hierarchy, inspector, status)"); +} + +/// Builds a UI label bundle with the split `Text` / `TextFont` / `TextColor` API. +fn label(value: impl Into, size: f32, color: Color) -> impl Bundle { + ( + Text::new(value.into()), + TextFont { + font: FontSource::default(), + font_size: FontSize::Px(size), + ..default() + }, + TextColor(color), + ) +} + +/// Flips [`EditorState`] when the toolbar button is activated. +fn toggle_editor_on_activate(_activate: On, mut state: ResMut) { + state.toggle(); +} + +/// F1 toggles the editor; the toolbar button uses the same state. +fn toggle_editor_f1(keys: Res>, mut state: ResMut) { + if keys.just_pressed(KeyCode::F1) { + state.toggle(); + } +} + +/// Matches the [`EditorRoot`] display mode to [`EditorState`]. +/// +/// The root is hidden as a whole; child panels keep their own display mode. +fn sync_editor_visibility(state: Res, mut roots: Query<&mut Node, With>) { + if !state.is_changed() { + return; + } + let display = if state.enabled { + Display::Flex + } else { + Display::None + }; + for mut node in &mut roots { + if node.display != display { + node.display = display; + } + } +} + +/// Updates the status line from the current selection, the selected entity's +/// `Name` and the editor state. +fn update_status_text( + mut status: Query<&mut Text, With>, + selection: Res, + names: Query<&Name>, + state: Res, +) { + let mut value = match selection.0 { + Some(entity) => match names.get(entity) { + Ok(name) => format!("Selected: {}", name.as_str()), + Err(_) => format!("Selected: {entity}"), + }, + None => "No selection".to_string(), + }; + value.push_str(if state.enabled { + " | Editor: on | F1 editor" + } else { + " | Editor: off | F1 editor" + }); + + for mut text in &mut status { + if text.0 != value { + text.0 = value.clone(); + } + } +} + +/// Updates the toolbar entity count for named, non-UI scene entities. +fn update_entity_count( + mut counters: Query<&mut Text, With>, + scene_entities: Query<(), (With, With, Without)>, +) { + let value = format!("Entities: {}", scene_entities.iter().count()); + for mut text in &mut counters { + if text.0 != value { + text.0 = value.clone(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy::window::WindowResolution; + + fn window(width: u32, height: u32) -> Window { + Window { + resolution: WindowResolution::new(width, height).with_scale_factor_override(1.0), + ..default() + } + } + + #[test] + fn default_layout_matches_contract() { + let layout = EditorLayout::default(); + assert_eq!(layout.left, 260.0); + assert_eq!(layout.right, 300.0); + assert_eq!(layout.top, 38.0); + assert_eq!(layout.bottom, 26.0); + } + + #[test] + fn viewport_rect_1440x900() { + let rect = EditorLayout::default().viewport_rect(&window(1440, 900)); + assert_eq!(rect.min, Vec2::new(260.0, 38.0)); + assert_eq!(rect.max, Vec2::new(1140.0, 874.0)); + assert_eq!(rect.size(), Vec2::new(880.0, 836.0)); + } + + #[test] + fn viewport_rect_1000x640() { + let rect = EditorLayout::default().viewport_rect(&window(1000, 640)); + assert_eq!(rect.min, Vec2::new(260.0, 38.0)); + assert_eq!(rect.max, Vec2::new(700.0, 614.0)); + assert_eq!(rect.size(), Vec2::new(440.0, 576.0)); + } + + #[test] + fn viewport_rect_clamps_tiny_windows() { + let rect = EditorLayout::default().viewport_rect(&window(400, 100)); + assert_eq!(rect.size(), Vec2::new(0.0, 36.0)); + assert!(rect.size().cmple(Vec2::new(400.0, 100.0)).all()); + } +} diff --git a/crates/bevy_forge_runtime/src/ui/theme.rs b/crates/bevy_forge_runtime/src/ui/theme.rs new file mode 100644 index 0000000..2ddec09 --- /dev/null +++ b/crates/bevy_forge_runtime/src/ui/theme.rs @@ -0,0 +1,51 @@ +//! Dark slate color theme shared by the editor UI. + +use bevy::prelude::*; + +/// Colors used by the editor shell and by later hierarchy/inspector/gizmo work. +/// +/// All colors are stored as [`Color`] so widgets can use them directly in +/// `TextColor`, `BackgroundColor` and `BorderColor` components. +#[derive(Resource, Debug, Clone)] +pub struct UiTheme { + /// Background of the left and right panels. + pub panel_bg: Color, + /// Alternate background used for buttons and rows. + pub panel_bg_alt: Color, + /// Border and divider color. + pub border: Color, + /// Primary text color. + pub text: Color, + /// Secondary text color for hints and read-only values. + pub text_muted: Color, + /// Accent color for the title, selection and gizmo hints. + pub accent: Color, + /// Row background on hover. + pub row_hover: Color, + /// Row background while selected. + pub row_selected: Color, + /// Toolbar background. + pub toolbar_bg: Color, + /// Status bar background. + pub status_bg: Color, + /// Warning / destructive color. + pub danger: Color, +} + +impl Default for UiTheme { + fn default() -> Self { + Self { + panel_bg: Color::srgba(0.074, 0.086, 0.102, 0.94), + panel_bg_alt: Color::srgb(0.121, 0.141, 0.172), + border: Color::srgba(0.258, 0.290, 0.337, 0.85), + text: Color::srgb(0.878, 0.898, 0.925), + text_muted: Color::srgb(0.576, 0.615, 0.674), + accent: Color::srgb(0.309, 0.576, 0.949), + row_hover: Color::srgb(0.168, 0.200, 0.247), + row_selected: Color::srgb(0.196, 0.333, 0.510), + toolbar_bg: Color::srgba(0.098, 0.113, 0.137, 0.96), + status_bg: Color::srgba(0.086, 0.098, 0.118, 0.96), + danger: Color::srgb(0.851, 0.298, 0.278), + } + } +} diff --git a/docs/m0-spike-1000x640.png b/docs/m0-spike-1000x640.png new file mode 100644 index 0000000..67037ff Binary files /dev/null and b/docs/m0-spike-1000x640.png differ diff --git a/docs/m0-spike-1440x900.png b/docs/m0-spike-1440x900.png new file mode 100644 index 0000000..a0690de Binary files /dev/null and b/docs/m0-spike-1440x900.png differ diff --git a/docs/m0-spike.md b/docs/m0-spike.md new file mode 100644 index 0000000..866a3c4 --- /dev/null +++ b/docs/m0-spike.md @@ -0,0 +1,163 @@ +# Bevy Forge v2 — M0 Spike + +Status: **M0 spike**, not a release. It proves the v2 architecture: a real in-game Bevy 0.20 editor +plugin with native Bevy UI, a live hierarchy, a reflection inspector, one translate gizmo and +deterministic screenshots. No egui is involved in the runtime crate. + +![Bevy Forge M0 spike](m0-spike-1440x900.png) + +_1000×640 variant: [m0-spike-1000x640.png](m0-spike-1000x640.png)._ + +## What M0 delivers + +- `bevy_forge_runtime` — a new crate in the workspace, added to a game with + `.add_plugins(EditorPlugin)` after `DefaultPlugins`. +- Fixed editor shell: toolbar, hierarchy (left), inspector (right), status bar; F1 or the toolbar + button toggles the whole editor. +- Live hierarchy: `Children`/`ChildOf` tree, indentation, scrolling, click selection, selected-row + highlight. +- Reflection inspector for `Name`, `Transform` and `Visibility`; translation X/Y/Z are editable via + `bevy_ui_widgets` sliders and update the ECS entity. Rotation/scale/visibility are read-only in + M0. +- Editor camera (right-drag orbit, middle-drag pan, wheel zoom) and a translate gizmo with + screen-space axis picking and ray/plane dragging. +- Deterministic screenshot mode at fixed sizes with a scale factor override. +- A small hardcoded 3D demo scene (nine named scene entities plus the editor camera) in + `crates/bevy_forge_runtime/examples/editor_spike.rs`. + +## Requirements + +- Rust 1.85+ (the workspace pins `rust-version = "1.85"`; this environment used 1.99). +- Linux with Vulkan/GL and a windowing session (Wayland or X11). The reference run is Wayland with + an NVIDIA RTX 3050; the UI and screenshots also work through the probe on this machine. +- No Bevy installation is required; Bevy 0.20.0 is a normal Cargo dependency. + +## Run + +```bash +cd bevy-forge +cargo run -p bevy_forge_runtime --example editor_spike +``` + +The first run compiles the Bevy UI/PBR pipeline and is slow. The window opens at 1440×900. + +## Controls + +| Input | Action | +|---|---| +| F1 / toolbar **Editor (F1)** | Show or hide the editor UI | +| Left click a hierarchy row | Select that entity | +| Left drag a gizmo axis | Translate the selected entity along X/Y/Z | +| Right drag in the viewport | Orbit the editor camera | +| Middle drag in the viewport | Pan the editor camera | +| Mouse wheel | Zoom the editor camera | + +The inspector sliders edit `Transform.translation.x/y/z` in the range -20..=20 with 0.1 steps. +A gizmo drag updates the ECS `Transform`, and the inspector sliders follow. + +## Deterministic screenshots + +The screenshot mode is opt-in and exits after saving: + +```bash +FORGE_SCREENSHOT=$PWD/.swarm/m0/evidence/m0-1440x900.png \ +FORGE_SCREENSHOT_SIZE=1440x900 \ +cargo run -p bevy_forge_runtime --example editor_spike + +FORGE_SCREENSHOT=$PWD/.swarm/m0/evidence/m0-1000x640.png \ +FORGE_SCREENSHOT_SIZE=1000x640 \ +cargo run -p bevy_forge_runtime --example editor_spike +``` + +| Variable | Default | Meaning | +|---|---|---| +| `FORGE_SCREENSHOT` | unset | output PNG; unset disables the mode | +| `FORGE_SCREENSHOT_SIZE` | `1440x900` | `WIDTHxHEIGHT`, 320..=7680 | +| `FORGE_SCREENSHOT_FRAMES` | `300` | minimum frames before capture | +| `FORGE_SCREENSHOT_WARMUP_SECS` | `10` | minimum wall-clock seconds before capture | + +The warmup is deliberate: UI/gizmo shaders are ready quickly, but the 3D mesh/PBR pipeline can take +~10 s to compile on a cold run. A 30-frame screenshot is blank; the mode waits for both thresholds. +The window is forced to `WindowResolution::new(w, h).with_scale_factor_override(1.0)`, so the PNG is +exactly the requested size on HiDPI displays. + +**Determinism caveat:** the hierarchy hover highlight follows the pointer. Park the cursor outside +the editor window before capturing if you need byte-identical pixels; the committed evidence was +captured that way. The screenshot mode fixes size, scene and frame timing, but does not currently +neutralise pointer hover. + +## Architecture + +``` +crates/bevy_forge_runtime/ +├── src/lib.rs public prelude: EditorPlugin, EditorState, Selection, InspectorSource +├── src/plugin.rs resources, plugin registration, selection cleanup +├── src/ui/ fixed shell, theme, markers, EditorLayout +├── src/hierarchy.rs live ListBox/ScrollArea tree + selection observer +├── src/inspector.rs reflection summary/apply, transform sliders, live sync +├── src/gizmo.rs editor camera, axes drawing, picking/drag +├── src/screenshot.rs deterministic PNG mode +└── examples/editor_spike.rs +``` + +The editor UI is an overlay: the 3D camera renders full-window and the panels cover the left/right +edges. `EditorLayout::viewport_rect` defines the center rectangle used by gizmo input. + +## M0 acceptance + +The full evidence is in `.swarm/m0/reviews/V01.md` and `.swarm/m0/final-report.md`; the commands +are listed in `.swarm/m0/plan.md`. In short: + +- `cargo check -p bevy_forge_runtime` passes; `cargo test -p bevy_forge_runtime` is **38 passed, + 0 failed, 0 ignored** plus one ignored doctest; the full workspace is **384 passed, 0 failed, + 2 ignored** (v1 + M0). +- `cargo tree -p bevy_forge_runtime -i egui` finds no egui; a plain tree grep for `egui` is empty. + The v1 editor still uses egui; the runtime crate does not. `bevy_ui_widgets` resolves to 0.20.0. +- The example launches, logs `editor shell ready`, `hierarchy: 10 rows, 7 roots`, + `inspector: selected ... (Hero Cube)` and `gizmo: camera ready`, with no warnings/panics. +- The gizmo is visible on the selected entity; its drag math is unit-tested (including a rotated + parent). Computer-use on 2026-10-27 clicked hierarchy rows, dragged the inspector X slider and + the X gizmo axis, toggled the UI and zoomed the camera; selection, inspector, ECS and gizmo + followed, with no panic. +- Screenshots are exactly 1440×900 and 1000×640; both show the shell, hierarchy, inspector and gizmo. +- No v1/core/CLI source changes; only the workspace member/lock, the new crate, docs and README. +- Independent review: **pass_with_risks** (`.swarm/m0/reviews/V01.md`, local coordination artifact). + All acceptance gates pass; the residual risks are right/middle orbit-pan (not scripted) and the + disclosed hierarchy root filter. + +## Interactive verification (computer-use, 2026-10-27) + +The editor was driven through the KDE Wayland remote-desktop portal with the computer-use tool: + +- clicked `Red Cube`, `Blue Cube`, `Ground` and `Hero Cube` in the hierarchy — selection, inspector + fields and gizmo moved to the clicked entity every time; +- dragged the Red Cube Translation X slider from −2.6 to 3.2 — the cube and gizmo moved and the + inspector value updated; +- dragged the Hero Cube X gizmo axis from 0.0 to 1.0 — the cube moved and the inspector slider + followed; +- the toolbar button hid the UI; F1 restored it; wheel zoom changed the camera distance. + +This found a real bug that unit tests missed: on a selection change, the inspector's transform-sync +system ran after the panel rebuild and queued a `SliderValue` insert for a slider that had just been +despawned, panicking in Bevy's command queue. The systems are now chained **sync-first, then +rebuild**; repeated selection/slider/gizmo runs are clean. Orbit and pan (right/middle drag) are not +scripted by the portal drag helper and remain a manual check; wheel zoom is covered. + +## Limits (M0, by design) + +- No save/load of `.forge.ron`, no BSN export from the UI, no undo/redo, no play/pause, no asset + browser, no docking, no multi-select, no rotate/scale gizmo. +- The demo scene is hardcoded; `bevy_forge_core` is not a runtime dependency yet. +- The live hierarchy panel lists named, transformed scene roots and their descendants; unnamed root + entities are omitted in M0 (a full-world mode is M1 work). `collect_rows` remains the literal tree + model for tests. +- The inspector covers `Name`/`Transform`/`Visibility`; generic `TypeRegistry` component discovery + is M1 work. +- Parent scale is ignored during gizmo drags (M0); rotations are honored. +- 3D only in the demo; the v2 plan adds 2D and scene loading in M1/M2. + +## Next + +M1 Core: `.forge.ron` save/load via `bevy_forge_core`, selection API/multi-select, undo/redo command +stack, editor camera polish, and the first integration tests against the real scene format. Do not +start M1 until M0 is accepted by the verifier and the actual request/token spend is recorded.