diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 91a98f3ea7..485861ebdf 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -49,7 +49,8 @@ jobs: --exclude ci \ --exclude errors \ --exclude bevy_mobile_example \ - --exclude build-wasm-example + --exclude build-wasm-example \ + --exclude no_std_library - name: Create PR uses: peter-evans/create-pull-request@v7 diff --git a/Cargo.toml b/Cargo.toml index d2e5b92258..e6c75611ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" categories = ["game-engines", "graphics", "gui", "rendering"] description = "A refreshingly simple data-driven game engine and app framework" @@ -72,7 +72,6 @@ allow_attributes_without_reason = "warn" [workspace.lints.rust] missing_docs = "warn" -mismatched_lifetime_syntaxes = "allow" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" unsafe_op_in_unsafe_fn = "warn" @@ -559,12 +558,12 @@ hotpatching = ["bevy_internal/hotpatching"] debug = ["bevy_internal/debug"] [dependencies] -bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "crates/bevy_internal", version = "0.17.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } # Wasm does not support dynamic linking. [target.'cfg(not(target_family = "wasm"))'.dependencies] -bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-features = false, optional = true } +bevy_dylib = { path = "crates/bevy_dylib", version = "0.17.0-dev", default-features = false, optional = true } [dev-dependencies] rand = "0.8.0" @@ -574,14 +573,14 @@ flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" bytemuck = "1.7" -bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. -bevy_ecs = { path = "crates/bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state = { path = "crates/bevy_state", version = "0.16.0-dev", default-features = false } -bevy_asset = { path = "crates/bevy_asset", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "crates/bevy_reflect", version = "0.16.0-dev", default-features = false } -bevy_image = { path = "crates/bevy_image", version = "0.16.0-dev", default-features = false } -bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } +bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.0-dev", default-features = false } +bevy_image = { path = "crates/bevy_image", version = "0.17.0-dev", default-features = false } +bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.17.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" async-std = "1.13" @@ -608,7 +607,7 @@ web-sys = { version = "0.3", features = ["Window"] } [[example]] name = "context_menu" -path = "examples/usages/context_menu.rs" +path = "examples/usage/context_menu.rs" doc-scrape-examples = true [package.metadata.example.context_menu] @@ -1285,9 +1284,9 @@ required-features = ["bevy_solari"] [package.metadata.example.solari] name = "Solari" -description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari." +description = "Demonstrates realtime dynamic raytraced lighting using Bevy Solari." category = "3D Rendering" -wasm = false +wasm = false # Raytracing is not supported on the web [[example]] name = "spherical_area_lights" diff --git a/benches/benches/bevy_ecs/change_detection.rs b/benches/benches/bevy_ecs/change_detection.rs index 92f3251abc..3cfa5bcbed 100644 --- a/benches/benches/bevy_ecs/change_detection.rs +++ b/benches/benches/bevy_ecs/change_detection.rs @@ -49,6 +49,7 @@ impl BenchModify for Table { black_box(self.0) } } + impl BenchModify for Sparse { fn bench_modify(&mut self) -> f32 { self.0 += 1f32; diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 39628ec046..70ee16cf77 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_a11y" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides accessibility support for Bevy Engine" homepage = "https://bevy.org" @@ -40,10 +40,10 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } # other accesskit = { version = "0.19", default-features = false } diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 9db4a97fd0..adf17a8580 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_animation" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides animation functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,22 +10,22 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "petgraph", ] } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_anti_aliasing/Cargo.toml b/crates/bevy_anti_aliasing/Cargo.toml index c54608a883..8c32d70fff 100644 --- a/crates/bevy_anti_aliasing/Cargo.toml +++ b/crates/bevy_anti_aliasing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_anti_aliasing" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides various anti aliasing implementations for Bevy Engine" homepage = "https://bevy.org" @@ -16,17 +16,17 @@ smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } # other tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 6b6120f182..a0c5222b0b 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_app" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides core App functionality for Bevy Engine" homepage = "https://bevy.org" @@ -79,12 +79,12 @@ hotpatching = [ [dependencies] # bevy -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other downcast-rs = { version = "2", default-features = false } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index ed890f981e..05f3de27b1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1582,7 +1582,7 @@ mod tests { app.add_systems(EnterMainMenu, (foo, bar)); app.world_mut().run_schedule(EnterMainMenu); - assert_eq!(app.world().entities().count_constructed(), 2); + assert_eq!(app.world().entity_count(), 2); } #[test] diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs index 754ba3140e..c6ac5139b9 100644 --- a/crates/bevy_app/src/propagate.rs +++ b/crates/bevy_app/src/propagate.rs @@ -88,6 +88,7 @@ impl core::fmt::Debug for PropagateSet { } impl Eq for PropagateSet {} + impl core::hash::Hash for PropagateSet { fn hash(&self, state: &mut H) { self._p.hash(state); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index e91987f40a..0835b3b49a 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides asset functionality for Bevy Engine" homepage = "https://bevy.org" @@ -19,19 +19,19 @@ watch = [] trace = [] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_asset_macros = { path = "macros", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset_macros = { path = "macros", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "uuid", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "async_executor", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -65,7 +65,7 @@ uuid = { version = "1.13.1", default-features = false, features = [ tracing = { version = "0.1", default-features = false } [target.'cfg(target_os = "android")'.dependencies] -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. @@ -78,13 +78,13 @@ web-sys = { version = "0.3", features = [ wasm-bindgen-futures = "0.4" js-sys = "0.3" uuid = { version = "1.13.1", default-features = false, features = ["js"] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index 4d99d228d3..0b525b3c1d 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_asset" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_asset/src/direct_access_ext.rs b/crates/bevy_asset/src/direct_access_ext.rs index 792d523a30..e7e5b993de 100644 --- a/crates/bevy_asset/src/direct_access_ext.rs +++ b/crates/bevy_asset/src/direct_access_ext.rs @@ -20,6 +20,7 @@ pub trait DirectAssetAccessExt { settings: impl Fn(&mut S) + Send + Sync + 'static, ) -> Handle; } + impl DirectAssetAccessExt for World { /// Insert an asset similarly to [`Assets::add`]. /// diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index f7fb56be74..06a0791a50 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -56,6 +56,7 @@ pub(crate) struct EmbeddedEventHandler { dir: Dir, last_event: Option, } + impl FilesystemEventHandler for EmbeddedEventHandler { fn begin(&mut self) { self.last_event = None; diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index f6c44397fc..c49d55ca4a 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -141,16 +141,19 @@ impl EmbeddedAssetRegistry { pub trait GetAssetServer { fn get_asset_server(&self) -> &AssetServer; } + impl GetAssetServer for App { fn get_asset_server(&self) -> &AssetServer { self.world().get_asset_server() } } + impl GetAssetServer for World { fn get_asset_server(&self) -> &AssetServer { self.resource() } } + impl GetAssetServer for AssetServer { fn get_asset_server(&self) -> &AssetServer { self diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 6c470891bd..a3148cecb7 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -223,6 +223,7 @@ pub struct ReflectHandle { downcast_handle_untyped: fn(&dyn Any) -> Option, typed: fn(UntypedHandle) -> Box, } + impl ReflectHandle { /// The [`TypeId`] of the asset pub fn asset_type_id(&self) -> TypeId { diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index ae5385870d..2ffa62db9d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_audio" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides audio functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,13 +10,13 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } # other # TODO: Remove `coreaudio-sys` dep below when updating `cpal`. @@ -36,10 +36,10 @@ coreaudio-sys = { version = "0.2.17", default-features = false } rodio = { version = "0.20", default-features = false, features = [ "wasm-bindgen", ] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 9fc757af44..d02d326501 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -57,6 +57,7 @@ pub struct PlaybackRemoveMarker; pub(crate) struct EarPositions<'w, 's> { pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>, } + impl<'w, 's> EarPositions<'w, 's> { /// Gets a set of transformed ear positions. /// diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index ca7a7a74f5..298f80e54f 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_color" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Types for representing and manipulating color values" homepage = "https://bevy.org" @@ -10,10 +10,10 @@ keywords = ["bevy", "color"] rust-version = "1.85.0" [dependencies] -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "curve", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } bytemuck = { version = "1", features = ["derive"] } serde = { version = "1.0", features = [ "derive", diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 2d903d2cc4..9b3f158af2 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_core_pipeline" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Bevy Contributors ", @@ -20,20 +20,20 @@ tonemapping_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 9bcb2b4f80..f5314c736d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -80,6 +80,7 @@ impl From for Camera3dDepthTextureUsage { Self(value.bits()) } } + impl From for TextureUsages { fn from(value: Camera3dDepthTextureUsage) -> Self { Self::from_bits_truncate(value.0) diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index 83ecd67ede..e93891d8f7 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_core_widgets" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Unstyled common widgets for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,17 +10,17 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ "bevy_ui_picking_backend", ] } diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index ec30b625f9..97b15b878d 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -2,7 +2,6 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; use bevy_ecs::query::Has; -use bevy_ecs::system::ResMut; use bevy_ecs::{ component::Component, entity::Entity, @@ -11,7 +10,8 @@ use bevy_ecs::{ system::{Commands, Query, SystemId}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; -use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; @@ -36,6 +36,7 @@ fn button_on_key_event( if !disabled { let event = &trigger.event().input; if !event.repeat + && event.state == ButtonState::Pressed && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { if let Some(on_click) = bstate.on_click { @@ -65,24 +66,12 @@ fn button_on_pointer_click( fn button_on_pointer_down( mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, - focus: Option>, - focus_visible: Option>, mut commands: Commands, ) { if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); - if !disabled { - if !pressed { - commands.entity(button).insert(Pressed); - } - // Clicking on a button makes it the focused input, - // and hides the focus ring if it was visible. - if let Some(mut focus) = focus { - focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } + if !disabled && !pressed { + commands.entity(button).insert(Pressed); } } } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs new file mode 100644 index 0000000000..fc12811055 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -0,0 +1,179 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::query::{Has, Without}; +use bevy_ecs::system::{In, ResMut}; +use bevy_ecs::{ + component::Component, + observer::On, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current +/// state of the checkbox. The `on_change` field is an optional system id that will be run when the +/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is +/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox +/// will update its own [`Checked`] state directly. +/// +/// # Toggle switches +/// +/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you +/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with +/// the `Switch` role instead of the `Checkbox` role. +#[derive(Component, Debug, Default)] +#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] +pub struct CoreCheckbox { + /// One-shot system that is run when the checkbox state needs to be changed. + pub on_change: Option>>, +} + +fn checkbox_on_key_input( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has), Without>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + ev.propagate(false); + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +fn checkbox_on_pointer_click( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = Some(ev.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + + ev.propagate(false); + if !disabled { + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +/// Event which can be triggered on a checkbox to set the checked state. This can be used to control +/// the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, SetChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(SetChecked(true), checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct SetChecked(pub bool); + +/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to +/// control the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(ToggleChecked, checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct ToggleChecked; + +fn checkbox_on_set_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + let will_be_checked = ev.event().0; + if will_be_checked != is_checked { + set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked); + } + } +} + +fn checkbox_on_toggle_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } +} + +fn set_checkbox_state( + commands: &mut Commands, + entity: impl Into, + checkbox: &CoreCheckbox, + new_state: bool, +) { + if let Some(on_change) = checkbox.on_change { + commands.run_system_with(on_change, new_state); + } else if new_state { + commands.entity(entity.into()).insert(Checked); + } else { + commands.entity(entity.into()).remove::(); + } +} + +/// Plugin that adds the observers for the [`CoreCheckbox`] widget. +pub struct CoreCheckboxPlugin; + +impl Plugin for CoreCheckboxPlugin { + fn build(&self, app: &mut App) { + app.add_observer(checkbox_on_key_input) + .add_observer(checkbox_on_pointer_click) + .add_observer(checkbox_on_set_checked) + .add_observer(checkbox_on_toggle_checked); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index ecd6d52fbe..5a6b90636a 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -3,12 +3,11 @@ use core::ops::RangeInclusive; use accesskit::{Orientation, Role}; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; -use bevy_ecs::entity::Entity; use bevy_ecs::event::{EntityEvent, Event}; -use bevy_ecs::hierarchy::{ChildOf, Children}; +use bevy_ecs::hierarchy::Children; use bevy_ecs::lifecycle::Insert; use bevy_ecs::query::Has; -use bevy_ecs::system::{In, Res, ResMut}; +use bevy_ecs::system::{In, Res}; use bevy_ecs::world::DeferredWorld; use bevy_ecs::{ component::Component, @@ -18,7 +17,7 @@ use bevy_ecs::{ }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; -use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_input_focus::FocusedInput; use bevy_log::warn_once; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; @@ -207,43 +206,18 @@ pub(crate) fn slider_on_pointer_down( )>, q_thumb: Query<&ComputedNode, With>, q_children: Query<&Children>, - q_parents: Query<&ChildOf>, - focus: Option>, - focus_visible: Option>, mut commands: Commands, ui_scale: Res, ) { if q_thumb.contains(trigger.target()) { // Thumb click, stop propagation to prevent track click. trigger.propagate(false); - - // Find the slider entity that's an ancestor of the thumb - if let Some(slider_entity) = q_parents - .iter_ancestors(trigger.target()) - .find(|entity| q_slider.contains(*entity)) - { - // Set focus to slider and hide focus ring - if let Some(mut focus) = focus { - focus.0 = Some(slider_entity); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } - } } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = q_slider.get(trigger.target()) { // Track click trigger.propagate(false); - // Set focus to slider and hide focus ring - if let Some(mut focus) = focus { - focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } - if disabled { return; } diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index 00812bddfc..cdb9142b52 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -15,11 +15,13 @@ // the widget level, like `SliderValue`, should not have the `Core` prefix. mod core_button; +mod core_checkbox; mod core_slider; use bevy_app::{App, Plugin}; pub use core_button::{CoreButton, CoreButtonPlugin}; +pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; pub use core_slider::{ CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, SliderRange, SliderStep, SliderValue, TrackClick, @@ -31,6 +33,6 @@ pub struct CoreWidgetsPlugin; impl Plugin for CoreWidgetsPlugin { fn build(&self, app: &mut App) { - app.add_plugins((CoreButtonPlugin, CoreSliderPlugin)); + app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin)); } } diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index f127dbffb5..f1a1cd44b3 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides derive implementations for Bevy Engine" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index ab5a04f2b2..ef31767c0e 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dev_tools" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Collection of developer tools for the Bevy Engine" homepage = "https://bevy.org" @@ -13,21 +13,21 @@ bevy_ci_testing = ["serde", "ron"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", version = "0.17.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 708f3b9ee1..e930da149a 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_diagnostic" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides diagnostic functionality for Bevy Engine" homepage = "https://bevy.org" @@ -53,12 +53,12 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index a632c1b49a..df195a6122 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -17,11 +17,13 @@ pub struct FrameTimeDiagnosticsPlugin { /// The smoothing factor for the exponential moving average. Usually `2.0 / (history_length + 1.0)`. pub smoothing_factor: f64, } + impl Default for FrameTimeDiagnosticsPlugin { fn default() -> Self { Self::new(DEFAULT_MAX_HISTORY_LENGTH) } } + impl FrameTimeDiagnosticsPlugin { /// Creates a new `FrameTimeDiagnosticsPlugin` with the specified `max_history_length` and a /// reasonable `smoothing_factor`. diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index a980a35b10..334ea10f1f 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dylib" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Force the Bevy Engine to be dynamically linked for faster linking" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] crate-type = ["dylib"] [dependencies] -bevy_internal = { path = "../bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "../bevy_internal", version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index c2c663b7d7..5aa3dfe5ef 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine's entity component system" homepage = "https://bevy.org" @@ -86,14 +86,14 @@ critical-section = [ hotpatching = ["dep:subsecond"] [dependencies] -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "smallvec", ], default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_ecs_macros = { path = "macros", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ecs_macros = { path = "macros", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index 28605a5d67..4f15bd59fd 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Bevy ECS Macros" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0.99", features = ["full", "extra-traits"] } quote = "1.0" diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 53ba284588..ef7fad99f4 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -792,6 +792,11 @@ fn derive_relationship( #relationship_member: entity } } + + #[inline] + fn set_risky(&mut self, entity: Entity) { + self.#relationship_member = entity; + } } })) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 8090cff7de..7750f97259 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -16,7 +16,7 @@ use crate::{ use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, ConstParam, Data, DataStruct, DeriveInput, GenericParam, Index, TypeParam, @@ -79,6 +79,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_kind = Vec::with_capacity(named_fields.len()); for field in named_fields { + let mut kind = BundleFieldKind::Component; + for attr in field .attrs .iter() @@ -86,7 +88,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { { if let Err(error) = attr.parse_nested_meta(|meta| { if meta.path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) { - field_kind.push(BundleFieldKind::Ignore); + kind = BundleFieldKind::Ignore; Ok(()) } else { Err(meta.error(format!( @@ -98,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -111,82 +113,33 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { .map(|field| &field.ty) .collect::>(); - let mut field_component_ids = Vec::new(); - let mut field_get_component_ids = Vec::new(); - let mut field_get_components = Vec::new(); - let mut field_from_components = Vec::new(); - let mut field_required_components = Vec::new(); + let mut active_field_types = Vec::new(); + let mut active_field_tokens = Vec::new(); + let mut inactive_field_tokens = Vec::new(); for (((i, field_type), field_kind), field) in field_type .iter() .enumerate() .zip(field_kind.iter()) .zip(field.iter()) { + let field_tokens = match field { + Some(field) => field.to_token_stream(), + None => Index::from(i).to_token_stream(), + }; match field_kind { BundleFieldKind::Component => { - field_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, &mut *ids); - }); - field_required_components.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, required_components); - }); - field_get_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids); - }); - match field { - Some(field) => { - field_get_components.push(quote! { - self.#field.get_components(&mut *func); - }); - field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - None => { - let index = Index::from(i); - field_get_components.push(quote! { - self.#index.get_components(&mut *func); - }); - field_from_components.push(quote! { - #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - } + active_field_types.push(field_type); + active_field_tokens.push(field_tokens); } - BundleFieldKind::Ignore => { - field_from_components.push(quote! { - #field: ::core::default::Default::default(), - }); - } + BundleFieldKind::Ignore => inactive_field_tokens.push(field_tokens), } } let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let struct_name = &ast.ident; - let from_components = attributes.impl_from_components.then(|| quote! { - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } - } - } - }); - - let attribute_errors = &errors; - - TokenStream::from(quote! { - #(#attribute_errors)* - + let bundle_impl = quote! { // SAFETY: // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass @@ -196,27 +149,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { fn component_ids( components: &mut #ecs_path::component::ComponentsRegistrator, ids: &mut impl FnMut(#ecs_path::component::ComponentId) - ){ - #(#field_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)* } fn get_component_ids( components: &#ecs_path::component::Components, ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>) - ){ - #(#field_get_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)* } fn register_required_components( components: &mut #ecs_path::component::ComponentsRegistrator, required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)* } } + }; - #from_components - + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -226,9 +179,36 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self, func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>) ) { - #(#field_get_components)* + #(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)* } } + }; + + let from_components_impl = attributes.impl_from_components.then(|| quote! { + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + #[allow(deprecated)] + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { + #[allow(unused_variables, non_snake_case)] + unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self + where + __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> + { + Self { + #(#active_field_tokens: <#active_field_types as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),)* + #(#inactive_field_tokens: ::core::default::Default::default(),)* + } + } + } + }); + + let attribute_errors = &errors; + + TokenStream::from(quote! { + #(#attribute_errors)* + #bundle_impl + #from_components_impl + #dynamic_bundle_impl }) } diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 910d9ce3b6..12d9c2bf1c 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -83,7 +83,7 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics_with_world_and_state = { let mut generics = ast.generics; generics.params.insert(0, parse_quote!('__w)); - generics.params.insert(0, parse_quote!('__s)); + generics.params.insert(1, parse_quote!('__s)); generics }; let ( diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 34a2a4c813..1ecbad16a1 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -960,6 +960,7 @@ impl Index> for Archetypes { &self.archetypes[index.start.0.index()..] } } + impl Index for Archetypes { type Output = Archetype; diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index b672a56483..252f7cb0a2 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2410,4 +2410,13 @@ mod tests { assert_eq!(world.resource::().0, 3); } + + #[derive(Bundle)] + #[expect(unused, reason = "tests the output of the derive macro is valid")] + struct Ignore { + #[bundle(ignore)] + foo: i32, + #[bundle(ignore)] + bar: i32, + } } diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index cfcde29ab2..831402240d 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -623,6 +623,7 @@ pub trait ComponentMutability: private::Seal + 'static { pub struct Immutable; impl private::Seal for Immutable {} + impl ComponentMutability for Immutable { const MUTABLE: bool = false; } @@ -633,6 +634,7 @@ impl ComponentMutability for Immutable { pub struct Mutable; impl private::Seal for Mutable {} + impl ComponentMutability for Mutable { const MUTABLE: bool = true; } @@ -2968,6 +2970,7 @@ impl Default for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorBase { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::Default @@ -2979,6 +2982,7 @@ impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorViaClone { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorViaClone for &DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::clone::() diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 4bac468ec0..bae8f9c924 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -866,6 +866,7 @@ impl<'a> Iterator for AllocEntitiesIterator<'a> { } impl<'a> ExactSizeIterator for AllocEntitiesIterator<'a> {} + impl<'a> core::iter::FusedIterator for AllocEntitiesIterator<'a> {} // SAFETY: Newly allocated entity values are unique. diff --git a/crates/bevy_ecs/src/entity/unique_array.rs b/crates/bevy_ecs/src/entity/unique_array.rs index ce31e55448..71df33ec5f 100644 --- a/crates/bevy_ecs/src/entity/unique_array.rs +++ b/crates/bevy_ecs/src/entity/unique_array.rs @@ -154,6 +154,7 @@ impl DerefMut for UniqueEntityEquivalentArr unsafe { UniqueEntityEquivalentSlice::from_slice_unchecked_mut(&mut self.0) } } } + impl Default for UniqueEntityEquivalentArray { fn default() -> Self { Self(Default::default()) @@ -527,6 +528,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq<&UniqueEntityEquivalentArray> for VecDeque { @@ -550,6 +552,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq> for VecDeque { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 17a63593af..bd111b800b 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1592,9 +1592,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread" - )] + #[should_panic] fn non_send_resource_drop_from_different_thread() { let mut world = World::default(); world.insert_non_send_resource(NonSendA::default()); @@ -1659,7 +1657,7 @@ mod tests { assert_eq!(q1.iter(&world).len(), 1); assert_eq!(q2.iter(&world).len(), 1); - assert_eq!(world.entities().count_constructed(), 2); + assert_eq!(world.entity_count(), 2); world.clear_entities(); @@ -1674,7 +1672,7 @@ mod tests { "world should not contain sparse set components" ); assert_eq!( - world.entities().count_constructed(), + world.entity_count(), 0, "world should not have any entities" ); @@ -2609,7 +2607,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."] + #[should_panic] fn required_components_recursion_errors() { #[derive(Component, Default)] #[require(B)] @@ -2627,7 +2625,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → A\nhelp: Remove require(A)."] + #[should_panic] fn required_components_self_errors() { #[derive(Component, Default)] #[require(A)] diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 67719ca18d..317c8f5017 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -159,6 +159,7 @@ impl From<&str> for Name { Name::new(name.to_owned()) } } + impl From for Name { #[inline(always)] fn from(name: String) -> Self { @@ -174,12 +175,14 @@ impl AsRef for Name { &self.name } } + impl From<&Name> for String { #[inline(always)] fn from(val: &Name) -> String { val.as_str().to_owned() } } + impl From for String { #[inline(always)] fn from(val: Name) -> String { diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs new file mode 100644 index 0000000000..e3fa6c530a --- /dev/null +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -0,0 +1,245 @@ +//! Centralized storage for observers, allowing for efficient look-ups. +//! +//! This has multiple levels: +//! - [`World::observers`] provides access to [`Observers`], which is a central storage for all observers. +//! - [`Observers`] contains multiple distinct caches in the form of [`CachedObservers`]. +//! - Most observers are looked up by the [`ComponentId`] of the event they are observing +//! - Lifecycle observers have their own fields to save lookups. +//! - [`CachedObservers`] contains maps of [`ObserverRunner`]s, which are the actual functions that will be run when the observer is triggered. +//! - These are split by target type, in order to allow for different lookup strategies. +//! - [`CachedComponentObservers`] is one of these maps, which contains observers that are specifically targeted at a component. + +use bevy_platform::collections::HashMap; + +use crate::{ + archetype::ArchetypeFlags, + change_detection::MaybeLocation, + component::ComponentId, + entity::EntityHashMap, + observer::{ObserverRunner, ObserverTrigger}, + prelude::*, + world::DeferredWorld, +}; + +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This can be accessed via [`World::observers`]. +#[derive(Default, Debug)] +pub struct Observers { + // Cached ECS observers to save a lookup most common triggers. + add: CachedObservers, + insert: CachedObservers, + replace: CachedObservers, + remove: CachedObservers, + despawn: CachedObservers, + // Map from trigger type to set of observers listening to that trigger + cache: HashMap, +} + +impl Observers { + pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + + match event_type { + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, + _ => self.cache.entry(event_type).or_default(), + } + } + + /// Attempts to get the observers for the given `event_type`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + + match event_type { + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), + _ => self.cache.get(&event_type), + } + } + + /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. + pub(crate) fn invoke( + mut world: DeferredWorld, + event_type: ComponentId, + current_target: Option, + original_target: Option, + components: impl Iterator + Clone, + data: &mut T, + propagate: &mut bool, + caller: MaybeLocation, + ) { + // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` + let (mut world, observers) = unsafe { + let world = world.as_unsafe_world_cell(); + // SAFETY: There are no outstanding world references + world.increment_trigger_id(); + let observers = world.observers(); + let Some(observers) = observers.try_get_observers(event_type) else { + return; + }; + // SAFETY: The only outstanding reference to world is `observers` + (world.into_deferred(), observers) + }; + + let trigger_for_components = components.clone(); + + let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { + (runner)( + world.reborrow(), + ObserverTrigger { + observer, + event_type, + components: components.clone().collect(), + current_target, + original_target, + caller, + }, + data.into(), + propagate, + ); + }; + // Trigger observers listening for any kind of this trigger + observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + // Trigger entity observers listening for this kind of trigger + if let Some(target_entity) = current_target { + if let Some(map) = observers.entity_observers.get(&target_entity) { + map.iter().for_each(&mut trigger_observer); + } + } + + // Trigger observers listening to this trigger targeting a specific component + trigger_for_components.for_each(|id| { + if let Some(component_observers) = observers.component_observers.get(&id) { + component_observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + if let Some(target_entity) = current_target { + if let Some(map) = component_observers + .entity_component_observers + .get(&target_entity) + { + map.iter().for_each(&mut trigger_observer); + } + } + } + }); + } + + pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + + match event_type { + ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), + REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), + _ => None, + } + } + + pub(crate) fn update_archetype_flags( + &self, + component_id: ComponentId, + flags: &mut ArchetypeFlags, + ) { + if self.add.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); + } + + if self.insert.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); + } + + if self.replace.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); + } + + if self.remove.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); + } + + if self.despawn.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); + } + } +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. +#[derive(Default, Debug)] +pub struct CachedObservers { + // Observers listening for any time this event is fired, regardless of target + // This will also respond to events targeting specific components or entities + pub(super) global_observers: ObserverMap, + // Observers listening for this trigger fired at a specific component + pub(super) component_observers: HashMap, + // Observers listening for this trigger fired at a specific entity + pub(super) entity_observers: EntityHashMap, +} + +impl CachedObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific components or entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting components. + pub fn get_component_observers(&self) -> &HashMap { + &self.component_observers + } + + /// Returns the observers listening for this trigger targeting entities. + pub fn entity_observers(&self) -> &HashMap { + &self.component_observers + } +} + +/// Map between an observer entity and its [`ObserverRunner`] +pub type ObserverMap = EntityHashMap; + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. +#[derive(Default, Debug)] +pub struct CachedComponentObservers { + // Observers listening to events targeting this component, but not a specific entity + pub(super) global_observers: ObserverMap, + // Observers listening to events targeting this component on a specific entity + pub(super) entity_component_observers: EntityHashMap, +} + +impl CachedComponentObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting this component on a specific entity. + pub fn entity_component_observers(&self) -> &EntityHashMap { + &self.entity_component_observers + } +} diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs new file mode 100644 index 0000000000..a9a3645121 --- /dev/null +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -0,0 +1,492 @@ +//! Information about observers that is stored on the entities themselves. +//! +//! This allows for easier cleanup, better inspection, and more flexible querying. +//! +//! Each observer is associated with an entity, defined by the [`Observer`] component. +//! The [`Observer`] component contains the system that will be run when the observer is triggered, +//! and the [`ObserverDescriptor`] which contains information about what the observer is observing. +//! +//! When we watch entities, we add the [`ObservedBy`] component to those entities, +//! which links back to the observer entity. + +use core::any::Any; + +use crate::{ + component::{ComponentCloneBehavior, ComponentId, Mutable, StorageType}, + entity::Entity, + error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, + observer::{observer_system_runner, ObserverRunner}, + prelude::*, + system::{IntoObserverSystem, ObserverSystem}, + world::DeferredWorld, +}; +use alloc::boxed::Box; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; + +#[cfg(feature = "bevy_reflect")] +use crate::prelude::ReflectComponent; + +/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". +/// +/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] +/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific +/// entity targets using [`World::trigger_targets`]. +/// +/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. +/// They must be triggered at a specific point in the schedule. +/// +/// # Usage +/// +/// The simplest usage of the observer pattern looks like this: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// #[derive(Event)] +/// struct Speak { +/// message: String, +/// } +/// +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.event().message); +/// }); +/// +/// // Observers currently require a flush() to be registered. In the context of schedules, +/// // this will generally be done for you. +/// world.flush(); +/// +/// world.trigger(Speak { +/// message: "Hello!".into(), +/// }); +/// ``` +/// +/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct Speak; +/// // These are functionally the same: +/// world.add_observer(|trigger: On| {}); +/// world.spawn(Observer::new(|trigger: On| {})); +/// ``` +/// +/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct PrintNames; +/// # #[derive(Component, Debug)] +/// # struct Name; +/// world.add_observer(|trigger: On, names: Query<&Name>| { +/// for name in &names { +/// println!("{name:?}"); +/// } +/// }); +/// ``` +/// +/// Note that [`On`] must always be the first parameter. +/// +/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct SpawnThing; +/// # #[derive(Component, Debug)] +/// # struct Thing; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.spawn(Thing); +/// }); +/// ``` +/// +/// Observers can also trigger new events: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct A; +/// # #[derive(Event)] +/// # struct B; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.trigger(B); +/// }); +/// ``` +/// +/// When the commands are flushed (including these "nested triggers") they will be +/// recursively evaluated until there are no commands left, meaning nested triggers all +/// evaluate at the same time! +/// +/// If the event is an [`EntityEvent`], it can be triggered for specific entities, +/// which will be passed to the [`Observer`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// #[derive(Event, EntityEvent)] +/// struct Explode; +/// +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// println!("Entity {} goes BOOM!", trigger.target()); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.flush(); +/// +/// world.trigger_targets(Explode, entity); +/// ``` +/// +/// You can trigger multiple entities at once: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.trigger_targets(Explode, [e1, e2]); +/// ``` +/// +/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component, Debug)] +/// # struct Name(String); +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { +/// println!("Boom!"); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { +/// println!("The explosion fizzles! This entity is immune!"); +/// }); +/// ``` +/// +/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. +/// This protects against observer "garbage" building up over time. +/// +/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) +/// just shorthand for spawning an [`Observer`] directly: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// let mut observer = Observer::new(|trigger: On| {}); +/// observer.watch_entity(entity); +/// world.spawn(observer); +/// ``` +/// +/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! +/// +/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. +/// serves as the "source of truth" of the observer. +/// +/// [`SystemParam`]: crate::system::SystemParam +pub struct Observer { + hook_on_add: ComponentHook, + pub(crate) error_handler: Option, + pub(crate) system: Box, + pub(crate) descriptor: ObserverDescriptor, + pub(crate) last_trigger_id: u32, + pub(crate) despawned_watched_entities: u32, + pub(crate) runner: ObserverRunner, +} + +impl Observer { + /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered + /// for _any_ entity (or no entity). + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. + pub fn new>(system: I) -> Self { + let system = Box::new(IntoObserverSystem::into_system(system)); + assert!( + !system.is_exclusive(), + concat!( + "Exclusive system `{}` may not be used as observer.\n", + "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." + ), + system.name() + ); + Self { + system, + descriptor: Default::default(), + hook_on_add: hook_on_add::, + error_handler: None, + runner: observer_system_runner::, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer + pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { + Self { + system: Box::new(IntoSystem::into_system(|| {})), + descriptor: Default::default(), + hook_on_add: |mut world, hook_context| { + let default_error_handler = world.default_error_handler(); + world.commands().queue(move |world: &mut World| { + let entity = hook_context.entity; + if let Some(mut observe) = world.get_mut::(entity) { + if observe.descriptor.events.is_empty() { + return; + } + if observe.error_handler.is_none() { + observe.error_handler = Some(default_error_handler); + } + world.register_observer(entity); + } + }); + }, + error_handler: None, + runner, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + pub fn with_entity(mut self, entity: Entity) -> Self { + self.descriptor.entities.push(entity); + self + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. + pub fn watch_entity(&mut self, entity: Entity) { + self.descriptor.entities.push(entity); + } + + /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// with the given component target. + pub fn with_component(mut self, component: ComponentId) -> Self { + self.descriptor.components.push(component); + self + } + + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + /// # Safety + /// The type of the `event` [`ComponentId`] _must_ match the actual value + /// of the event passed into the observer system. + pub unsafe fn with_event(mut self, event: ComponentId) -> Self { + self.descriptor.events.push(event); + self + } + + /// Set the error handler to use for this observer. + /// + /// See the [`error` module-level documentation](crate::error) for more information. + pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { + self.error_handler = Some(error_handler); + self + } + + /// Returns the [`ObserverDescriptor`] for this [`Observer`]. + pub fn descriptor(&self) -> &ObserverDescriptor { + &self.descriptor + } + + /// Returns the name of the [`Observer`]'s system . + pub fn system_name(&self) -> DebugName { + self.system.system_name() + } +} + +impl Component for Observer { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + fn on_add() -> Option { + Some(|world, context| { + let Some(observe) = world.get::(context.entity) else { + return; + }; + let hook = observe.hook_on_add; + hook(world, context); + }) + } + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let descriptor = core::mem::take( + &mut world + .entity_mut(entity) + .get_mut::() + .unwrap() + .as_mut() + .descriptor, + ); + world.commands().queue(move |world: &mut World| { + world.unregister_observer(entity, descriptor); + }); + }) + } +} + +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, +#[derive(Default, Clone)] +pub struct ObserverDescriptor { + /// The events the observer is watching. + pub(super) events: Vec, + + /// The components the observer is watching. + pub(super) components: Vec, + + /// The entities the observer is watching. + pub(super) entities: Vec, +} + +impl ObserverDescriptor { + /// Add the given `events` to the descriptor. + /// # Safety + /// The type of each [`ComponentId`] in `events` _must_ match the actual value + /// of the event passed into the observer. + pub unsafe fn with_events(mut self, events: Vec) -> Self { + self.events = events; + self + } + + /// Add the given `components` to the descriptor. + pub fn with_components(mut self, components: Vec) -> Self { + self.components = components; + self + } + + /// Add the given `entities` to the descriptor. + pub fn with_entities(mut self, entities: Vec) -> Self { + self.entities = entities; + self + } + + /// Returns the `events` that the observer is watching. + pub fn events(&self) -> &[ComponentId] { + &self.events + } + + /// Returns the `components` that the observer is watching. + pub fn components(&self) -> &[ComponentId] { + &self.components + } + + /// Returns the `entities` that the observer is watching. + pub fn entities(&self) -> &[Entity] { + &self.entities + } +} + +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). +/// +/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters +/// erased. +/// +/// The type parameters of this function _must_ match those used to create the [`Observer`]. +/// As such, it is recommended to only use this function within the [`Observer::new`] method to +/// ensure type parameters match. +fn hook_on_add>( + mut world: DeferredWorld<'_>, + HookContext { entity, .. }: HookContext, +) { + world.commands().queue(move |world: &mut World| { + let event_id = E::register_component_id(world); + let mut components = alloc::vec![]; + B::component_ids(&mut world.components_registrator(), &mut |id| { + components.push(id); + }); + if let Some(mut observer) = world.get_mut::(entity) { + observer.descriptor.events.push(event_id); + observer.descriptor.components.extend(components); + + let system: &mut dyn Any = observer.system.as_mut(); + let system: *mut dyn ObserverSystem = system.downcast_mut::().unwrap(); + // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias + unsafe { + (*system).initialize(world); + } + world.register_observer(entity); + } + }); +} + +/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. +#[derive(Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Component, Debug))] +pub struct ObservedBy(pub(crate) Vec); + +impl ObservedBy { + /// Provides a read-only reference to the list of entities observing this entity. + pub fn get(&self) -> &[Entity] { + &self.0 + } +} + +impl Component for ObservedBy { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let observed_by = { + let mut component = world.get_mut::(entity).unwrap(); + core::mem::take(&mut component.0) + }; + for e in observed_by { + let (total_entities, despawned_watched_entities) = { + let Ok(mut entity_mut) = world.get_entity_mut(e) else { + continue; + }; + let Some(mut state) = entity_mut.get_mut::() else { + continue; + }; + state.despawned_watched_entities += 1; + ( + state.descriptor.entities.len(), + state.despawned_watched_entities as usize, + ) + }; + + // Despawn Observer if it has no more active sources. + if total_entities == despawned_watched_entities { + world.commands().entity(e).despawn(); + } + } + }) + } + + fn clone_behavior() -> ComponentCloneBehavior { + ComponentCloneBehavior::Ignore + } +} + +pub(crate) trait AnyNamedSystem: Any + Send + Sync + 'static { + fn system_name(&self) -> DebugName; +} + +impl AnyNamedSystem for T { + fn system_name(&self) -> DebugName { + self.name() + } +} diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs similarity index 61% rename from crates/bevy_ecs/src/observer/entity_observer.rs rename to crates/bevy_ecs/src/observer/entity_cloning.rs index 23be0e9672..ee37300e64 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -1,67 +1,14 @@ +//! Logic to track observers when cloning entities. + use crate::{ - component::{Component, ComponentCloneBehavior, Mutable, StorageType}, - entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, - lifecycle::{ComponentHook, HookContext}, + component::ComponentCloneBehavior, + entity::{ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent}, + observer::ObservedBy, world::World, }; -use alloc::vec::Vec; - -#[cfg(feature = "bevy_reflect")] -use crate::prelude::ReflectComponent; use super::Observer; -/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. -#[derive(Default, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Component, Debug))] -pub struct ObservedBy(pub(crate) Vec); - -impl ObservedBy { - /// Provides a read-only reference to the list of entities observing this entity. - pub fn get(&self) -> &[Entity] { - &self.0 - } -} - -impl Component for ObservedBy { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let observed_by = { - let mut component = world.get_mut::(entity).unwrap(); - core::mem::take(&mut component.0) - }; - for e in observed_by { - let (total_entities, despawned_watched_entities) = { - let Ok(mut entity_mut) = world.get_entity_mut(e) else { - continue; - }; - let Some(mut state) = entity_mut.get_mut::() else { - continue; - }; - state.despawned_watched_entities += 1; - ( - state.descriptor.entities.len(), - state.despawned_watched_entities as usize, - ) - }; - - // Despawn Observer if it has no more active sources. - if total_entities == despawned_watched_entities { - world.commands().entity(e).despawn(); - } - } - }) - } - - fn clone_behavior() -> ComponentCloneBehavior { - ComponentCloneBehavior::Ignore - } -} - impl EntityClonerBuilder<'_> { /// Sets the option to automatically add cloned entities to the observers targeting source entity. pub fn add_observers(&mut self, add_observers: bool) -> &mut Self { diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 06da399de6..e9036eee74 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -129,614 +129,26 @@ //! but allows for a more ad hoc approach with observers, //! and enables indefinite chaining of observers triggering other observers (for both better and worse!). -mod entity_observer; +mod centralized_storage; +mod distributed_storage; +mod entity_cloning; mod runner; +mod system_param; +mod trigger_targets; -pub use entity_observer::ObservedBy; +pub use centralized_storage::*; +pub use distributed_storage::*; pub use runner::*; -use variadics_please::all_tuples; +pub use system_param::*; +pub use trigger_targets::*; use crate::{ - archetype::ArchetypeFlags, change_detection::MaybeLocation, component::ComponentId, - entity::EntityHashMap, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, }; -use alloc::vec::Vec; -use bevy_platform::collections::HashMap; -use bevy_ptr::Ptr; -use core::{ - fmt::Debug, - marker::PhantomData, - ops::{Deref, DerefMut}, -}; -use smallvec::SmallVec; - -/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the -/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also -/// contains event propagation information. See [`On::propagate`] for more information. -/// -/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. -/// The entity involved *does not* have to have these components, but the observer will only be -/// triggered if the event matches the components in `B`. -/// -/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] -/// and the other lifecycle events. -/// -/// Providing multiple components in this bundle will cause this event to be triggered by any -/// matching component in the bundle, -/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -pub struct On<'w, E, B: Bundle = ()> { - event: &'w mut E, - propagate: &'w mut bool, - trigger: ObserverTrigger, - _marker: PhantomData, -} - -/// Deprecated in favor of [`On`]. -#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] -pub type Trigger<'w, E, B = ()> = On<'w, E, B>; - -impl<'w, E, B: Bundle> On<'w, E, B> { - /// Creates a new instance of [`On`] for the given event and observer information. - pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { - Self { - event, - propagate, - trigger, - _marker: PhantomData, - } - } - - /// Returns the event type of this [`On`] instance. - pub fn event_type(&self) -> ComponentId { - self.trigger.event_type - } - - /// Returns a reference to the triggered event. - pub fn event(&self) -> &E { - self.event - } - - /// Returns a mutable reference to the triggered event. - pub fn event_mut(&mut self) -> &mut E { - self.event - } - - /// Returns a pointer to the triggered event. - pub fn event_ptr(&self) -> Ptr { - Ptr::from(&self.event) - } - - /// Returns the components that triggered the observer, out of the - /// components defined in `B`. Does not necessarily include all of them as - /// `B` acts like an `OR` filter rather than an `AND` filter. - pub fn components(&self) -> &[ComponentId] { - &self.trigger.components - } - - /// Returns the [`Entity`] that observed the triggered event. - /// This allows you to despawn the observer, ceasing observation. - /// - /// # Examples - /// - /// ```rust - /// # use bevy_ecs::prelude::{Commands, On}; - /// # - /// # struct MyEvent { - /// # done: bool, - /// # } - /// # - /// /// Handle `MyEvent` and if it is done, stop observation. - /// fn my_observer(trigger: On, mut commands: Commands) { - /// if trigger.event().done { - /// commands.entity(trigger.observer()).despawn(); - /// return; - /// } - /// - /// // ... - /// } - /// ``` - pub fn observer(&self) -> Entity { - self.trigger.observer - } - - /// Returns the source code location that triggered this observer. - pub fn caller(&self) -> MaybeLocation { - self.trigger.caller - } -} - -impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. - /// - /// Note that if event propagation is enabled, this may not be the same as the original target of the event, - /// which can be accessed via [`On::original_target`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. - pub fn target(&self) -> Entity { - self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) - } - - /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. - /// - /// If event propagation is not enabled, this will always return the same value as [`On::target`]. - /// - /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. - pub fn original_target(&self) -> Entity { - self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) - } - - /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. - /// - /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events - /// use `()` which ends the path immediately and prevents propagation. - /// - /// To enable propagation, you must: - /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. - /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. - /// - /// You can prevent an event from propagating further using `propagate(false)`. - /// - /// [`Traversal`]: crate::traversal::Traversal - pub fn propagate(&mut self, should_propagate: bool) { - *self.propagate = should_propagate; - } - - /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. - /// - /// [`propagate`]: On::propagate - pub fn get_propagate(&self) -> bool { - *self.propagate - } -} - -impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("On") - .field("event", &self.event) - .field("propagate", &self.propagate) - .field("trigger", &self.trigger) - .field("_marker", &self._marker) - .finish() - } -} - -impl<'w, E, B: Bundle> Deref for On<'w, E, B> { - type Target = E; - - fn deref(&self) -> &Self::Target { - self.event - } -} - -impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.event - } -} - -/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. -/// -/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific -/// event-target combination will run. -/// -/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. -/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, -/// allowing you to trigger events for multiple targets at once. -pub trait TriggerTargets { - /// The components the trigger should target. - fn components(&self) -> impl Iterator + Clone + '_; - - /// The entities the trigger should target. - fn entities(&self) -> impl Iterator + Clone + '_; -} - -impl TriggerTargets for &T { - fn components(&self) -> impl Iterator + Clone + '_ { - (**self).components() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - (**self).entities() - } -} - -impl TriggerTargets for Entity { - fn components(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } -} - -impl TriggerTargets for ComponentId { - fn components(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } -} - -impl TriggerTargets for Vec { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T; N] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -macro_rules! impl_trigger_targets_tuples { - ($(#[$meta:meta])* $($trigger_targets: ident),*) => { - #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] - #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] - $(#[$meta])* - impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) - { - fn components(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.components()); - )* - iter - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.entities()); - )* - iter - } - } - } -} - -all_tuples!( - #[doc(fake_variadic)] - impl_trigger_targets_tuples, - 0, - 15, - T -); - -/// Store information about what an [`Observer`] observes. -/// -/// This information is stored inside of the [`Observer`] component, -#[derive(Default, Clone)] -pub struct ObserverDescriptor { - /// The events the observer is watching. - events: Vec, - - /// The components the observer is watching. - components: Vec, - - /// The entities the observer is watching. - entities: Vec, -} - -impl ObserverDescriptor { - /// Add the given `events` to the descriptor. - /// # Safety - /// The type of each [`ComponentId`] in `events` _must_ match the actual value - /// of the event passed into the observer. - pub unsafe fn with_events(mut self, events: Vec) -> Self { - self.events = events; - self - } - - /// Add the given `components` to the descriptor. - pub fn with_components(mut self, components: Vec) -> Self { - self.components = components; - self - } - - /// Add the given `entities` to the descriptor. - pub fn with_entities(mut self, entities: Vec) -> Self { - self.entities = entities; - self - } - - /// Returns the `events` that the observer is watching. - pub fn events(&self) -> &[ComponentId] { - &self.events - } - - /// Returns the `components` that the observer is watching. - pub fn components(&self) -> &[ComponentId] { - &self.components - } - - /// Returns the `entities` that the observer is watching. - pub fn entities(&self) -> &[Entity] { - &self.entities - } -} - -/// Metadata about a specific [`Event`] that triggered an observer. -/// -/// This information is exposed via methods on [`On`]. -#[derive(Debug)] -pub struct ObserverTrigger { - /// The [`Entity`] of the observer handling the trigger. - pub observer: Entity, - /// The [`Event`] the trigger targeted. - pub event_type: ComponentId, - /// The [`ComponentId`]s the trigger targeted. - components: SmallVec<[ComponentId; 2]>, - /// The entity that the entity-event targeted, if any. - /// - /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. - pub current_target: Option, - /// The entity that the entity-event was originally targeted at, if any. - /// - /// If event propagation is enabled, this will be the first entity that the event was targeted at, - /// even if the event was propagated to other entities. - pub original_target: Option, - /// The location of the source code that triggered the observer. - pub caller: MaybeLocation, -} - -impl ObserverTrigger { - /// Returns the components that the trigger targeted. - pub fn components(&self) -> &[ComponentId] { - &self.components - } -} - -/// Map between an observer entity and its [`ObserverRunner`] -pub type ObserverMap = EntityHashMap; - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. -/// -/// This is stored inside of [`CachedObservers`]. -#[derive(Default, Debug)] -pub struct CachedComponentObservers { - // Observers listening to events targeting this component, but not a specific entity - global_observers: ObserverMap, - // Observers listening to events targeting this component on a specific entity - entity_component_observers: EntityHashMap, -} - -impl CachedComponentObservers { - /// Returns the observers listening for this trigger, regardless of target. - /// These observers will also respond to events targeting specific entities. - pub fn global_observers(&self) -> &ObserverMap { - &self.global_observers - } - - /// Returns the observers listening for this trigger targeting this component on a specific entity. - pub fn entity_component_observers(&self) -> &EntityHashMap { - &self.entity_component_observers - } -} - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. -/// -/// This is stored inside of [`Observers`], specialized for each kind of observer. -#[derive(Default, Debug)] -pub struct CachedObservers { - // Observers listening for any time this event is fired, regardless of target - // This will also respond to events targeting specific components or entities - global_observers: ObserverMap, - // Observers listening for this trigger fired at a specific component - component_observers: HashMap, - // Observers listening for this trigger fired at a specific entity - entity_observers: EntityHashMap, -} - -impl CachedObservers { - /// Returns the observers listening for this trigger, regardless of target. - /// These observers will also respond to events targeting specific components or entities. - pub fn global_observers(&self) -> &ObserverMap { - &self.global_observers - } - - /// Returns the observers listening for this trigger targeting components. - pub fn get_component_observers(&self) -> &HashMap { - &self.component_observers - } - - /// Returns the observers listening for this trigger targeting entities. - pub fn entity_observers(&self) -> &HashMap { - &self.component_observers - } -} - -/// An internal lookup table tracking all of the observers in the world. -/// -/// Stores a cache mapping trigger ids to the registered observers. -/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, -/// saving lookups for the most common triggers. -/// -/// This can be accessed via [`World::observers`]. -#[derive(Default, Debug)] -pub struct Observers { - // Cached ECS observers to save a lookup most common triggers. - add: CachedObservers, - insert: CachedObservers, - replace: CachedObservers, - remove: CachedObservers, - despawn: CachedObservers, - // Map from trigger type to set of observers listening to that trigger - cache: HashMap, -} - -impl Observers { - pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { - use crate::lifecycle::*; - - match event_type { - ADD => &mut self.add, - INSERT => &mut self.insert, - REPLACE => &mut self.replace, - REMOVE => &mut self.remove, - DESPAWN => &mut self.despawn, - _ => self.cache.entry(event_type).or_default(), - } - } - - /// Attempts to get the observers for the given `event_type`. - /// - /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], - /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. - pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { - use crate::lifecycle::*; - - match event_type { - ADD => Some(&self.add), - INSERT => Some(&self.insert), - REPLACE => Some(&self.replace), - REMOVE => Some(&self.remove), - DESPAWN => Some(&self.despawn), - _ => self.cache.get(&event_type), - } - } - - /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. - pub(crate) fn invoke( - mut world: DeferredWorld, - event_type: ComponentId, - current_target: Option, - original_target: Option, - components: impl Iterator + Clone, - data: &mut T, - propagate: &mut bool, - caller: MaybeLocation, - ) { - // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` - let (mut world, observers) = unsafe { - let world = world.as_unsafe_world_cell(); - // SAFETY: There are no outstanding world references - world.increment_trigger_id(); - let observers = world.observers(); - let Some(observers) = observers.try_get_observers(event_type) else { - return; - }; - // SAFETY: The only outstanding reference to world is `observers` - (world.into_deferred(), observers) - }; - - let trigger_for_components = components.clone(); - - let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { - (runner)( - world.reborrow(), - ObserverTrigger { - observer, - event_type, - components: components.clone().collect(), - current_target, - original_target, - caller, - }, - data.into(), - propagate, - ); - }; - // Trigger observers listening for any kind of this trigger - observers - .global_observers - .iter() - .for_each(&mut trigger_observer); - - // Trigger entity observers listening for this kind of trigger - if let Some(target_entity) = current_target { - if let Some(map) = observers.entity_observers.get(&target_entity) { - map.iter().for_each(&mut trigger_observer); - } - } - - // Trigger observers listening to this trigger targeting a specific component - trigger_for_components.for_each(|id| { - if let Some(component_observers) = observers.component_observers.get(&id) { - component_observers - .global_observers - .iter() - .for_each(&mut trigger_observer); - - if let Some(target_entity) = current_target { - if let Some(map) = component_observers - .entity_component_observers - .get(&target_entity) - { - map.iter().for_each(&mut trigger_observer); - } - } - } - }); - } - - pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { - use crate::lifecycle::*; - - match event_type { - ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), - REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), - _ => None, - } - } - - pub(crate) fn update_archetype_flags( - &self, - component_id: ComponentId, - flags: &mut ArchetypeFlags, - ) { - if self.add.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); - } - - if self.insert.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); - } - - if self.replace.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); - } - - if self.remove.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); - } - - if self.despawn.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); - } - } -} impl World { /// Spawns a "global" [`Observer`] which will watch for the given event. @@ -1315,7 +727,7 @@ mod tests { world.spawn(A).flush(); assert_eq!(vec!["add_2", "add_1"], world.resource::().0); // Our A entity plus our two observers - assert_eq!(world.entities().count_constructed(), 3); + assert_eq!(world.entity_count(), 3); } #[test] diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index d6bffd8f22..acc2830a7d 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,16 +1,10 @@ -use alloc::{boxed::Box, vec}; -use bevy_utils::prelude::DebugName; +//! Logic for evaluating observers, and storing functions inside of observers. + use core::any::Any; use crate::{ - component::{ComponentId, Mutable, StorageType}, - error::{ErrorContext, ErrorHandler}, - lifecycle::{ComponentHook, HookContext}, - observer::{ObserverDescriptor, ObserverTrigger}, - prelude::*, - query::DebugCheckedUnwrap, - system::{IntoObserverSystem, ObserverSystem}, - world::DeferredWorld, + error::ErrorContext, observer::ObserverTrigger, prelude::*, query::DebugCheckedUnwrap, + system::ObserverSystem, world::DeferredWorld, }; use bevy_ptr::PtrMut; @@ -20,323 +14,7 @@ use bevy_ptr::PtrMut; /// but can be overridden for custom behavior. pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool); -/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". -/// -/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] -/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific -/// entity targets using [`World::trigger_targets`]. -/// -/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. -/// They must be triggered at a specific point in the schedule. -/// -/// # Usage -/// -/// The simplest usage of the observer pattern looks like this: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// #[derive(Event)] -/// struct Speak { -/// message: String, -/// } -/// -/// world.add_observer(|trigger: On| { -/// println!("{}", trigger.event().message); -/// }); -/// -/// // Observers currently require a flush() to be registered. In the context of schedules, -/// // this will generally be done for you. -/// world.flush(); -/// -/// world.trigger(Speak { -/// message: "Hello!".into(), -/// }); -/// ``` -/// -/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct Speak; -/// // These are functionally the same: -/// world.add_observer(|trigger: On| {}); -/// world.spawn(Observer::new(|trigger: On| {})); -/// ``` -/// -/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct PrintNames; -/// # #[derive(Component, Debug)] -/// # struct Name; -/// world.add_observer(|trigger: On, names: Query<&Name>| { -/// for name in &names { -/// println!("{name:?}"); -/// } -/// }); -/// ``` -/// -/// Note that [`On`] must always be the first parameter. -/// -/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct SpawnThing; -/// # #[derive(Component, Debug)] -/// # struct Thing; -/// world.add_observer(|trigger: On, mut commands: Commands| { -/// commands.spawn(Thing); -/// }); -/// ``` -/// -/// Observers can also trigger new events: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct A; -/// # #[derive(Event)] -/// # struct B; -/// world.add_observer(|trigger: On, mut commands: Commands| { -/// commands.trigger(B); -/// }); -/// ``` -/// -/// When the commands are flushed (including these "nested triggers") they will be -/// recursively evaluated until there are no commands left, meaning nested triggers all -/// evaluate at the same time! -/// -/// If the event is an [`EntityEvent`], it can be triggered for specific entities, -/// which will be passed to the [`Observer`]: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// #[derive(Event, EntityEvent)] -/// struct Explode; -/// -/// world.add_observer(|trigger: On, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target()); -/// commands.entity(trigger.target()).despawn(); -/// }); -/// -/// world.flush(); -/// -/// world.trigger_targets(Explode, entity); -/// ``` -/// -/// You can trigger multiple entities at once: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event, EntityEvent)] -/// # struct Explode; -/// world.trigger_targets(Explode, [e1, e2]); -/// ``` -/// -/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # #[derive(Component, Debug)] -/// # struct Name(String); -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event, EntityEvent)] -/// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { -/// println!("Boom!"); -/// commands.entity(trigger.target()).despawn(); -/// }); -/// -/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { -/// println!("The explosion fizzles! This entity is immune!"); -/// }); -/// ``` -/// -/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. -/// This protects against observer "garbage" building up over time. -/// -/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) -/// just shorthand for spawning an [`Observer`] directly: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// # #[derive(Event, EntityEvent)] -/// # struct Explode; -/// let mut observer = Observer::new(|trigger: On| {}); -/// observer.watch_entity(entity); -/// world.spawn(observer); -/// ``` -/// -/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! -/// -/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. -/// serves as the "source of truth" of the observer. -/// -/// [`SystemParam`]: crate::system::SystemParam -pub struct Observer { - hook_on_add: ComponentHook, - error_handler: Option, - system: Box, - pub(crate) descriptor: ObserverDescriptor, - pub(crate) last_trigger_id: u32, - pub(crate) despawned_watched_entities: u32, - pub(crate) runner: ObserverRunner, -} - -impl Observer { - /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered - /// for _any_ entity (or no entity). - /// - /// # Panics - /// - /// Panics if the given system is an exclusive system. - pub fn new>(system: I) -> Self { - let system = Box::new(IntoObserverSystem::into_system(system)); - assert!( - !system.is_exclusive(), - concat!( - "Exclusive system `{}` may not be used as observer.\n", - "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - ), - system.name() - ); - Self { - system, - descriptor: Default::default(), - hook_on_add: hook_on_add::, - error_handler: None, - runner: observer_system_runner::, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer - pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { - Self { - system: Box::new(IntoSystem::into_system(|| {})), - descriptor: Default::default(), - hook_on_add: |mut world, hook_context| { - let default_error_handler = world.default_error_handler(); - world.commands().queue(move |world: &mut World| { - let entity = hook_context.entity; - if let Some(mut observe) = world.get_mut::(entity) { - if observe.descriptor.events.is_empty() { - return; - } - if observe.error_handler.is_none() { - observe.error_handler = Some(default_error_handler); - } - world.register_observer(entity); - } - }); - }, - error_handler: None, - runner, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - pub fn with_entity(mut self, entity: Entity) -> Self { - self.descriptor.entities.push(entity); - self - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. - pub fn watch_entity(&mut self, entity: Entity) { - self.descriptor.entities.push(entity); - } - - /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// with the given component target. - pub fn with_component(mut self, component: ComponentId) -> Self { - self.descriptor.components.push(component); - self - } - - /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] - /// is triggered. - /// # Safety - /// The type of the `event` [`ComponentId`] _must_ match the actual value - /// of the event passed into the observer system. - pub unsafe fn with_event(mut self, event: ComponentId) -> Self { - self.descriptor.events.push(event); - self - } - - /// Set the error handler to use for this observer. - /// - /// See the [`error` module-level documentation](crate::error) for more information. - pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { - self.error_handler = Some(error_handler); - self - } - - /// Returns the [`ObserverDescriptor`] for this [`Observer`]. - pub fn descriptor(&self) -> &ObserverDescriptor { - &self.descriptor - } - - /// Returns the name of the [`Observer`]'s system . - pub fn system_name(&self) -> DebugName { - self.system.system_name() - } -} - -impl Component for Observer { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - fn on_add() -> Option { - Some(|world, context| { - let Some(observe) = world.get::(context.entity) else { - return; - }; - let hook = observe.hook_on_add; - hook(world, context); - }) - } - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let descriptor = core::mem::take( - &mut world - .entity_mut(entity) - .get_mut::() - .unwrap() - .as_mut() - .descriptor, - ); - world.commands().queue(move |world: &mut World| { - world.unregister_observer(entity, descriptor); - }); - }) - } -} - -fn observer_system_runner>( +pub(super) fn observer_system_runner>( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, @@ -420,48 +98,6 @@ fn observer_system_runner>( } } -trait AnyNamedSystem: Any + Send + Sync + 'static { - fn system_name(&self) -> DebugName; -} - -impl AnyNamedSystem for T { - fn system_name(&self) -> DebugName { - self.name() - } -} - -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). -/// -/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters -/// erased. -/// -/// The type parameters of this function _must_ match those used to create the [`Observer`]. -/// As such, it is recommended to only use this function within the [`Observer::new`] method to -/// ensure type parameters match. -fn hook_on_add>( - mut world: DeferredWorld<'_>, - HookContext { entity, .. }: HookContext, -) { - world.commands().queue(move |world: &mut World| { - let event_id = E::register_component_id(world); - let mut components = vec![]; - B::component_ids(&mut world.components_registrator(), &mut |id| { - components.push(id); - }); - if let Some(mut observer) = world.get_mut::(entity) { - observer.descriptor.events.push(event_id); - observer.descriptor.components.extend(components); - - let system: &mut dyn Any = observer.system.as_mut(); - let system: *mut dyn ObserverSystem = system.downcast_mut::().unwrap(); - // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias - unsafe { - (*system).initialize(world); - } - world.register_observer(entity); - } - }); -} #[cfg(test)] mod tests { use super::*; @@ -516,9 +152,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - )] + #[should_panic] fn exclusive_system_cannot_be_observer() { fn system(_: On, _world: &mut World) {} let mut world = World::default(); diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs new file mode 100644 index 0000000000..27d6fef5b3 --- /dev/null +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -0,0 +1,206 @@ +//! System parameters for working with observers. + +use core::marker::PhantomData; +use core::ops::DerefMut; +use core::{fmt::Debug, ops::Deref}; + +use bevy_ptr::Ptr; +use smallvec::SmallVec; + +use crate::{ + bundle::Bundle, change_detection::MaybeLocation, component::ComponentId, event::EntityEvent, + prelude::*, +}; + +/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the +/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also +/// contains event propagation information. See [`On::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). +pub struct On<'w, E, B: Bundle = ()> { + event: &'w mut E, + propagate: &'w mut bool, + trigger: ObserverTrigger, + _marker: PhantomData, +} + +/// Deprecated in favor of [`On`]. +#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] +pub type Trigger<'w, E, B = ()> = On<'w, E, B>; + +impl<'w, E, B: Bundle> On<'w, E, B> { + /// Creates a new instance of [`On`] for the given event and observer information. + pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { + Self { + event, + propagate, + trigger, + _marker: PhantomData, + } + } + + /// Returns the event type of this [`On`] instance. + pub fn event_type(&self) -> ComponentId { + self.trigger.event_type + } + + /// Returns a reference to the triggered event. + pub fn event(&self) -> &E { + self.event + } + + /// Returns a mutable reference to the triggered event. + pub fn event_mut(&mut self) -> &mut E { + self.event + } + + /// Returns a pointer to the triggered event. + pub fn event_ptr(&self) -> Ptr { + Ptr::from(&self.event) + } + + /// Returns the components that triggered the observer, out of the + /// components defined in `B`. Does not necessarily include all of them as + /// `B` acts like an `OR` filter rather than an `AND` filter. + pub fn components(&self) -> &[ComponentId] { + &self.trigger.components + } + + /// Returns the [`Entity`] that observed the triggered event. + /// This allows you to despawn the observer, ceasing observation. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(Event, EntityEvent)] + /// struct AssertEvent; + /// + /// fn assert_observer(trigger: On) { + /// assert_eq!(trigger.observer(), trigger.target()); + /// } + /// + /// let mut world = World::new(); + /// let observer = world.spawn(Observer::new(assert_observer)).id(); + /// + /// world.trigger_targets(AssertEvent, observer); + /// ``` + pub fn observer(&self) -> Entity { + self.trigger.observer + } + + /// Returns the source code location that triggered this observer. + pub fn caller(&self) -> MaybeLocation { + self.trigger.caller + } +} + +impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { + /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// + /// Note that if event propagation is enabled, this may not be the same as the original target of the event, + /// which can be accessed via [`On::original_target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn target(&self) -> Entity { + self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. + /// + /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn original_target(&self) -> Entity { + self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. + /// + /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events + /// use `()` which ends the path immediately and prevents propagation. + /// + /// To enable propagation, you must: + /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. + /// + /// You can prevent an event from propagating further using `propagate(false)`. + /// + /// [`Traversal`]: crate::traversal::Traversal + pub fn propagate(&mut self, should_propagate: bool) { + *self.propagate = should_propagate; + } + + /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. + /// + /// [`propagate`]: On::propagate + pub fn get_propagate(&self) -> bool { + *self.propagate + } +} + +impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("On") + .field("event", &self.event) + .field("propagate", &self.propagate) + .field("trigger", &self.trigger) + .field("_marker", &self._marker) + .finish() + } +} + +impl<'w, E, B: Bundle> Deref for On<'w, E, B> { + type Target = E; + + fn deref(&self) -> &Self::Target { + self.event + } +} + +impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.event + } +} + +/// Metadata about a specific [`Event`] that triggered an observer. +/// +/// This information is exposed via methods on [`On`]. +#[derive(Debug)] +pub struct ObserverTrigger { + /// The [`Entity`] of the observer handling the trigger. + pub observer: Entity, + /// The [`Event`] the trigger targeted. + pub event_type: ComponentId, + /// The [`ComponentId`]s the trigger targeted. + pub components: SmallVec<[ComponentId; 2]>, + /// The entity that the entity-event targeted, if any. + /// + /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. + pub current_target: Option, + /// The entity that the entity-event was originally targeted at, if any. + /// + /// If event propagation is enabled, this will be the first entity that the event was targeted at, + /// even if the event was propagated to other entities. + pub original_target: Option, + /// The location of the source code that triggered the observer. + pub caller: MaybeLocation, +} + +impl ObserverTrigger { + /// Returns the components that the trigger targeted. + pub fn components(&self) -> &[ComponentId] { + &self.components + } +} diff --git a/crates/bevy_ecs/src/observer/trigger_targets.rs b/crates/bevy_ecs/src/observer/trigger_targets.rs new file mode 100644 index 0000000000..77728e4acd --- /dev/null +++ b/crates/bevy_ecs/src/observer/trigger_targets.rs @@ -0,0 +1,117 @@ +//! Stores the [`TriggerTargets`] trait. + +use crate::{component::ComponentId, prelude::*}; +use alloc::vec::Vec; +use variadics_please::all_tuples; + +/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. +/// +/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific +/// event-target combination will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. +pub trait TriggerTargets { + /// The components the trigger should target. + fn components(&self) -> impl Iterator + Clone + '_; + + /// The entities the trigger should target. + fn entities(&self) -> impl Iterator + Clone + '_; +} + +impl TriggerTargets for &T { + fn components(&self) -> impl Iterator + Clone + '_ { + (**self).components() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + (**self).entities() + } +} + +impl TriggerTargets for Entity { + fn components(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } +} + +impl TriggerTargets for ComponentId { + fn components(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T; N] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +macro_rules! impl_trigger_targets_tuples { + ($(#[$meta:meta])* $($trigger_targets: ident),*) => { + #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] + #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] + $(#[$meta])* + impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) + { + fn components(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.components()); + )* + iter + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.entities()); + )* + iter + } + } + } +} + +all_tuples!( + #[doc(fake_variadic)] + impl_trigger_targets_tuples, + 0, + 15, + T +); diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index a32f5fed28..66229f446d 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1489,6 +1489,7 @@ impl Clone for ReadFetch<'_, T> { *self } } + impl Copy for ReadFetch<'_, T> {} /// SAFETY: @@ -1665,6 +1666,7 @@ impl Clone for RefFetch<'_, T> { *self } } + impl Copy for RefFetch<'_, T> {} /// SAFETY: @@ -1873,6 +1875,7 @@ impl Clone for WriteFetch<'_, T> { *self } } + impl Copy for WriteFetch<'_, T> {} /// SAFETY: diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 312b330e04..f9f4861b79 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1240,6 +1240,7 @@ unsafe impl QueryFilter for Spawned { pub trait ArchetypeFilter: QueryFilter {} impl ArchetypeFilter for With {} + impl ArchetypeFilter for Without {} macro_rules! impl_archetype_filter_tuple { diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 7c1487fde4..0bd3bbed23 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -507,7 +507,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::query::tests::A conflicts with a previous access in this query."] + #[should_panic] fn self_conflicting_worldquery() { #[derive(QueryData)] #[query_data(mutable)] diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 63df9b4d8d..88db7ef6b4 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1901,9 +1901,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for ((&bevy_ecs::query::state::tests::A, &bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_to_include_data_not_in_original_query() { let mut world = World::new(); world.register_component::(); @@ -1915,9 +1913,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_immut_to_mut() { let mut world = World::new(); world.spawn(A(0)); @@ -1927,9 +1923,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (core::option::Option<&bevy_ecs::query::state::tests::A>, ())." - )] + #[should_panic] fn cannot_transmute_option_to_immut() { let mut world = World::new(); world.spawn(C(0)); @@ -1941,9 +1935,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (bevy_ecs::world::entity_ref::EntityRef, ())." - )] + #[should_panic] fn cannot_transmute_entity_ref() { let mut world = World::new(); world.register_component::(); @@ -2009,9 +2001,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_changed_without_access() { let mut world = World::new(); world.register_component::(); @@ -2021,9 +2011,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. @@ -2130,9 +2118,7 @@ mod tests { } #[test] - #[should_panic(expected = "Joined state for (&bevy_ecs::query::state::tests::C, ()) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, ()) joined with (&bevy_ecs::query::state::tests::B, ()).")] + #[should_panic] fn cannot_join_wrong_fetch() { let mut world = World::new(); world.register_component::(); @@ -2142,12 +2128,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, bevy_ecs::query::filter::Without) \ - joined with (&bevy_ecs::query::state::tests::B, bevy_ecs::query::filter::Without)." - )] + #[should_panic] fn cannot_join_wrong_filter() { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); @@ -2156,9 +2137,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for ((&mut bevy_ecs::query::state::tests::A, &mut bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by state (&bevy_ecs::query::state::tests::A, ()) joined with (&mut bevy_ecs::query::state::tests::B, ())." - )] + #[should_panic] fn cannot_join_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index d570b9fabc..8830998663 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -82,6 +82,20 @@ pub trait Relationship: Component + Sized { /// Creates this [`Relationship`] from the given `entity`. fn from(entity: Entity) -> Self; + /// Changes the current [`Entity`] ID of the entity containing the [`RelationshipTarget`] to another one. + /// + /// This is useful for updating the relationship without overwriting other fields stored in `Self`. + /// + /// # Warning + /// + /// This should generally not be called by user code, as modifying the related entity could invalidate the + /// relationship. If this method is used, then the hooks [`on_replace`](Relationship::on_replace) have to + /// run before and [`on_insert`](Relationship::on_insert) after it. + /// This happens automatically when this method is called with [`EntityWorldMut::modify_component`]. + /// + /// Prefer to use regular means of insertions when possible. + fn set_risky(&mut self, entity: Entity); + /// The `on_insert` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. fn on_insert( mut world: DeferredWorld, diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 1983b6b37c..8bae76a84e 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -6,7 +6,7 @@ use crate::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, system::{Commands, EntityCommands}, - world::{EntityWorldMut, World}, + world::{DeferredWorld, EntityWorldMut, World}, }; use bevy_platform::prelude::{Box, Vec}; use core::{marker::PhantomData, mem}; @@ -42,7 +42,12 @@ impl<'w> EntityWorldMut<'w> { let id = self.id(); self.world_scope(|world| { for related in related { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); } }); self @@ -98,7 +103,12 @@ impl<'w> EntityWorldMut<'w> { .collection_mut_risky() .place(*related, index); } else { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); world .get_mut::(id) .expect("hooks should have added relationship target") @@ -165,10 +175,13 @@ impl<'w> EntityWorldMut<'w> { } for related in potential_relations { - // SAFETY: We'll manually be adjusting the contents of the parent to fit the final state. + // SAFETY: We'll manually be adjusting the contents of the `RelationshipTarget` to fit the final state. world .entity_mut(related) - .insert_with_relationship_hook_mode(R::from(id), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Skip, + ); } }); @@ -266,7 +279,10 @@ impl<'w> EntityWorldMut<'w> { // We changed the target collection manually so don't run the insert hook world .entity_mut(*new_relation) - .insert_with_relationship_hook_mode(R::from(this), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + this, + RelationshipHookMode::Skip, + ); } }); @@ -352,6 +368,40 @@ impl<'w> EntityWorldMut<'w> { self } + + fn modify_or_insert_relation_with_relationship_hook_mode( + &mut self, + entity: Entity, + relationship_hook_mode: RelationshipHookMode, + ) { + // Check if the relation edge holds additional data + if size_of::() > size_of::() { + self.assert_not_despawned(); + + let this = self.id(); + + let modified = self.world_scope(|world| { + let modified = DeferredWorld::from(&mut *world) + .modify_component_with_relationship_hook_mode::( + this, + relationship_hook_mode, + |r| r.set_risky(entity), + ) + .expect("entity access must be valid") + .is_some(); + + world.flush(); + + modified + }); + + if modified { + return; + } + } + + self.insert_with_relationship_hook_mode(R::from(entity), relationship_hook_mode); + } } impl<'a> EntityCommands<'a> { @@ -689,17 +739,132 @@ mod tests { } #[test] - fn replace_related_keeps_data() { - #[derive(Component)] + fn add_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] #[relationship(relationship_target = Parent)] - pub struct Child(Entity); + struct Child { + #[relationship] + parent: Entity, + data: u8, + } #[derive(Component)] #[relationship_target(relationship = Child)] - pub struct Parent { + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world.entity_mut(parent2).add_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn insert_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .insert_related::(0, &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .replace_related_with_difference::(&[], &[child], &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + + world.entity_mut(parent1).replace_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent1, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_target_data() { + #[derive(Component)] + #[relationship(relationship_target = Parent)] + struct Child(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent { #[relationship] children: Vec, - pub data: u8, + data: u8, } let mut world = World::new(); diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 91f1b41312..12f58a7cd3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -26,7 +26,9 @@ pub mod passes { #[cfg(test)] mod tests { use super::*; - use alloc::{string::ToString, vec, vec::Vec}; + #[cfg(feature = "trace")] + use alloc::string::ToString; + use alloc::{vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; use crate::error::BevyError; @@ -770,6 +772,7 @@ mod tests { } mod system_ambiguity { + #[cfg(feature = "trace")] use alloc::collections::BTreeSet; use super::*; @@ -1110,6 +1113,7 @@ mod tests { // Tests that the correct ambiguities were reported in the correct order. #[test] + #[cfg(feature = "trace")] fn correct_ambiguities() { fn system_a(_res: ResMut) {} fn system_b(_res: ResMut) {} @@ -1183,6 +1187,7 @@ mod tests { // Test that anonymous set names work properly // Related issue https://github.com/bevyengine/bevy/issues/9641 #[test] + #[cfg(feature = "trace")] fn anonymous_set_name() { let mut schedule = Schedule::new(TestSchedule); schedule.add_systems((resmut_system, resmut_system).run_if(|| true)); diff --git a/crates/bevy_ecs/src/schedule/pass.rs b/crates/bevy_ecs/src/schedule/pass.rs index 20680e04e0..a602877d65 100644 --- a/crates/bevy_ecs/src/schedule/pass.rs +++ b/crates/bevy_ecs/src/schedule/pass.rs @@ -51,6 +51,7 @@ pub(super) trait ScheduleBuildPassObj: Send + Sync + Debug { ); fn add_dependency(&mut self, from: NodeId, to: NodeId, all_options: &TypeIdMap>); } + impl ScheduleBuildPassObj for T { fn build( &mut self, diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 3c67f1ea07..4455aba2fe 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -235,6 +235,7 @@ pub enum Chain { /// will be added between the successive elements. Chained(TypeIdMap>), } + impl Chain { /// Specify that the systems must be chained. pub fn set_chained(&mut self) { diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index d5014f2240..0c30c14b9c 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -210,6 +210,7 @@ unsafe impl + Send + Sync + 'static> Bundle ); } } + impl> DynamicBundle for SpawnRelatedBundle { type Effect = Self; diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index f3fc677e47..5f1f611856 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -223,6 +223,7 @@ pub fn trigger(event: impl Event) -> impl Command { } /// A [`Command`] that sends an [`EntityEvent`] for the given targets. +#[track_caller] pub fn trigger_targets( event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3e3dcd78da..04b048f909 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -2392,7 +2392,7 @@ mod tests { .spawn((W(1u32), W(2u64))) .id(); command_queue.apply(&mut world); - assert_eq!(world.entities().count_constructed(), 1); + assert_eq!(world.entity_count(), 1); let results = world .query::<(&W, &W)>() .iter(&world) diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 1ba8106cb8..8d53b1d091 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -634,7 +634,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_ref() { fn sys(_: Query>) {} let mut world = World::default(); @@ -642,7 +642,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_ref_and_mut() { fn sys(_: Query>) {} let mut world = World::default(); @@ -650,7 +650,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_option() { fn sys(_: Query)>>) {} let mut world = World::default(); @@ -680,7 +680,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_conflicting() { fn sys(_: Query>) {} let mut world = World::default(); @@ -1629,54 +1629,42 @@ mod tests { } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_first() { fn system(_query: &World, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_second() { fn system(_: Query, _: &World) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_ref_and_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_first() { fn system(_world: DeferredWorld, _query: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "DeferredWorld in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_second::system conflicts with a previous access." - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_second() { fn system(_query: Query, _world: DeferredWorld) {} super::assert_system_does_not_conflict(system); diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 8e927d9529..243c2c3c3f 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -73,6 +73,7 @@ where InfallibleObserverWrapper::new(IntoSystem::into_system(this)) } } + impl IntoObserverSystem for S where S: IntoSystem, Never, M> + Send + 'static, diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 863baf2ea6..0e893d5b80 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -410,7 +410,6 @@ pub enum RunSystemError { mod tests { use super::*; use crate::prelude::*; - use alloc::string::ToString; #[test] fn run_system_once() { @@ -483,7 +482,5 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::InvalidParams { .. }))); - let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."; - assert_eq!(expected, result.unwrap_err().to_string()); } } diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index f38a5eb1aa..e0c3c952cf 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -85,6 +85,7 @@ impl ExclusiveSystemParam for SystemName { } #[cfg(test)] +#[cfg(feature = "trace")] mod tests { use crate::{ system::{IntoSystem, RunSystemOnce, SystemName}, diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index aa44ccbf76..78fe3a2062 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -151,6 +151,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// let mut world = World::new(); /// let err = world.run_system_cached(|param: MyParam| {}).unwrap_err(); /// let expected = "Parameter `MyParam::foo` failed validation: Custom Message"; +/// # #[cfg(feature="Trace")] // Without debug_utils/debug enabled MyParam::foo is stripped and breaks the assert /// assert!(err.to_string().contains(expected)); /// ``` /// @@ -1399,6 +1400,7 @@ impl<'w, T> Deref for NonSend<'w, T> { self.value } } + impl<'a, T> From> for NonSend<'a, T> { fn from(nsm: NonSendMut<'a, T>) -> Self { Self { @@ -3105,7 +3107,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_resource_error::res_system`: Parameter `Res` failed validation: Resource does not exist"] + #[should_panic] fn missing_resource_error() { #[derive(Resource)] pub struct MissingResource; @@ -3119,7 +3121,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: BufferedEvent not initialized"] + #[should_panic] fn missing_event_error() { use crate::prelude::{BufferedEvent, EventReader}; diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index f84f38b4d9..52b4cef7da 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -913,7 +913,6 @@ mod tests { #[test] fn run_system_invalid_params() { use crate::system::RegisteredSystemError; - use alloc::{format, string::ToString}; struct T; impl Resource for T {} @@ -928,8 +927,6 @@ mod tests { result, Err(RegisteredSystemError::InvalidParams { .. }) )); - let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."); - assert_eq!(expected, result.unwrap_err().to_string()); } #[test] diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 18c9eb4d45..0aabe41562 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -420,12 +420,12 @@ mod test { let mut world = World::new(); queue.apply(&mut world); - assert_eq!(world.entities().count_constructed(), 2); + assert_eq!(world.entity_count(), 2); // The previous call to `apply` cleared the queue. // This call should do nothing. queue.apply(&mut world); - assert_eq!(world.entities().count_constructed(), 2); + assert_eq!(world.entity_count(), 2); } #[expect( @@ -459,7 +459,7 @@ mod test { queue.push(SpawnCommand); queue.push(SpawnCommand); queue.apply(&mut world); - assert_eq!(world.entities().count_constructed(), 3); + assert_eq!(world.entity_count(), 3); } #[test] diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 1c56ebb2d4..d1491f341c 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -102,9 +102,11 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut`](DeferredWorld::get_mut). #[inline] - pub(crate) fn modify_component( + #[track_caller] + pub(crate) fn modify_component_with_relationship_hook_mode( &mut self, entity: Entity, + relationship_hook_mode: RelationshipHookMode, f: impl FnOnce(&mut T) -> R, ) -> Result, EntityMutableFetchError> { // If the component is not registered, then it doesn't exist on this entity, so no action required. @@ -112,12 +114,17 @@ impl<'w> DeferredWorld<'w> { return Ok(None); }; - self.modify_component_by_id(entity, component_id, move |component| { - // SAFETY: component matches the component_id collected in the above line - let mut component = unsafe { component.with_type::() }; + self.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + relationship_hook_mode, + move |component| { + // SAFETY: component matches the component_id collected in the above line + let mut component = unsafe { component.with_type::() }; - f(&mut component) - }) + f(&mut component) + }, + ) } /// Temporarily removes a [`Component`] identified by the provided @@ -132,13 +139,15 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut_by_id`](DeferredWorld::get_mut_by_id). /// - /// You should prefer the typed [`modify_component`](DeferredWorld::modify_component) + /// You should prefer the typed [`modify_component_with_relationship_hook_mode`](DeferredWorld::modify_component_with_relationship_hook_mode) /// whenever possible. #[inline] - pub(crate) fn modify_component_by_id( + #[track_caller] + pub(crate) fn modify_component_by_id_with_relationship_hook_mode( &mut self, entity: Entity, component_id: ComponentId, + relationship_hook_mode: RelationshipHookMode, f: impl for<'a> FnOnce(MutUntyped<'a>) -> R, ) -> Result, EntityMutableFetchError> { let entity_cell = self.get_entity_mut(entity)?; @@ -162,7 +171,7 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_replace_observer() { self.trigger_observers( @@ -202,7 +211,7 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_insert_observer() { self.trigger_observers( diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d86c89960c..5d6dfb3d07 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -219,6 +219,14 @@ impl World { &mut self.entities } + /// Retrieves the number of [`Entities`] in the world. + /// + /// This is helpful as a diagnostic, but it can also be used effectively in tests. + #[inline] + pub fn entity_count(&self) -> u32 { + self.entities.len() + } + /// Retrieves this world's [`Archetypes`] collection. #[inline] pub fn archetypes(&self) -> &Archetypes { @@ -1426,6 +1434,7 @@ impl World { /// # assert_eq!(world.get::(entity), Some(&Foo(true))); /// ``` #[inline] + #[track_caller] pub fn modify_component( &mut self, entity: Entity, @@ -1433,7 +1442,11 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component(entity, f)?; + let result = world.modify_component_with_relationship_hook_mode( + entity, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) @@ -1454,6 +1467,7 @@ impl World { /// You should prefer the typed [`modify_component`](World::modify_component) /// whenever possible. #[inline] + #[track_caller] pub fn modify_component_by_id( &mut self, entity: Entity, @@ -1462,7 +1476,12 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component_by_id(entity, component_id, f)?; + let result = world.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index 60a6515fd8..9e05bc7a85 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_encase_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy derive macro for encase" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } encase_derive_impl = "0.10" [lints] diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 0afb6babbf..afb20318bc 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gilrs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Gamepad system made using Gilrs for Bevy Engine" homepage = "https://bevy.org" @@ -10,12 +10,12 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index 9b86edf03a..7ec1c2e93b 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -47,6 +47,7 @@ pub(crate) struct Gilrs { #[cfg(not(target_arch = "wasm32"))] cell: SyncCell, } + impl Gilrs { #[inline] pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) { diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 97a41f15b6..c4833dbe7e 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides gizmos for Bevy Engine" homepage = "https://bevy.org" @@ -15,21 +15,21 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev", optional = true } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_gizmos_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev", optional = true } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_gizmos_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } # other bytemuck = "1.0" diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index e15d0367b2..b7effe24b0 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_gizmos" homepage = "https://bevy.org" @@ -13,7 +13,7 @@ proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index cdcfc41236..2c85a0859d 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -172,6 +172,7 @@ where ); } } + impl GizmoBuffer where Config: GizmoConfigGroup, diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 05b16d1fc9..c46b74b7ca 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gltf" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine GLTF loading" homepage = "https://bevy.org" @@ -18,25 +18,25 @@ pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] [dependencies] # bevy -bevy_animation = { path = "../bevy_animation", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = [ +bevy_animation = { path = "../bevy_animation", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", version = "0.17.0-dev", features = [ "bevy_render", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -66,7 +66,7 @@ smallvec = "1.11" tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 7f8128e365..c26a9da1bb 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_image" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides image types for Bevy Engine" homepage = "https://bevy.org" @@ -40,17 +40,17 @@ zstd = ["ruzstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -77,7 +77,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } [dev-dependencies] -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index 4caeed8c07..67e1b20317 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -34,6 +34,7 @@ pub struct TextureAtlasSources { /// Maps from a specific image handle to the index in `textures` where they can be found. pub texture_ids: HashMap, usize>, } + impl TextureAtlasSources { /// Retrieves the texture *section* index of the given `texture` handle. pub fn texture_index(&self, texture: impl Into>) -> Option { diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index 6b805b83bf..2961c0d115 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_input" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides input functionality for Bevy Engine" homepage = "https://bevy.org" @@ -60,14 +60,14 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1", features = [ diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 49eea8dcea..60b824258d 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_input_focus" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Keyboard focus management" homepage = "https://bevy.org" @@ -60,12 +60,13 @@ libm = ["bevy_math/libm", "bevy_window/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", default-features = false } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index cbf88740fd..df7690ef26 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -147,6 +147,15 @@ pub struct FocusedInput { window: Entity, } +/// An event which is used to set input focus. Trigger this on an entity, and it will bubble +/// until it finds a focusable entity, and then set focus to it. +#[derive(Clone, Event, EntityEvent)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] +pub struct AcquireFocus { + /// The primary window entity. + window: Entity, +} + #[derive(QueryData)] /// These are for accessing components defined on the targeted entity pub struct WindowTraversal { @@ -172,6 +181,24 @@ impl Traversal> for WindowTraversal { } } +impl Traversal for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option { + let WindowTraversalItem { child_of, window } = item; + + // Send event to parent, if it has one. + if let Some(child_of) = child_of { + return Some(child_of.parent()); + }; + + // Otherwise, send it to the window entity (unless this is a window entity). + if window.is_none() { + return Some(event.window); + } + + None + } +} + /// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity. /// /// To add bubbling to your own input events, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 39c6e4ebcf..ef018c56fb 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -38,11 +38,12 @@ use bevy_input::{ keyboard::{KeyCode, KeyboardInput}, ButtonInput, ButtonState, }; -use bevy_window::PrimaryWindow; +use bevy_picking::events::{Pointer, Press}; +use bevy_window::{PrimaryWindow, Window}; use log::warn; use thiserror::Error; -use crate::{FocusedInput, InputFocus, InputFocusVisible}; +use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible}; #[cfg(feature = "bevy_reflect")] use { @@ -312,6 +313,31 @@ impl TabNavigation<'_, '_> { } } +/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling. +pub(crate) fn acquire_focus( + mut ev: On, + focusable: Query<(), With>, + windows: Query<(), With>, + mut focus: ResMut, +) { + // If the entity has a TabIndex + if focusable.contains(ev.target()) { + // Stop and focus it + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0 != Some(ev.target()) { + focus.0 = Some(ev.target()); + } + } else if windows.contains(ev.target()) { + // Stop and clear focus + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0.is_some() { + focus.clear(); + } + } +} + /// Plugin for navigating between focusable entities using keyboard input. pub struct TabNavigationPlugin; @@ -321,6 +347,8 @@ impl Plugin for TabNavigationPlugin { #[cfg(feature = "bevy_reflect")] app.register_type::().register_type::(); + app.add_observer(acquire_focus); + app.add_observer(click_to_focus); } } @@ -330,6 +358,30 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut focus_visible: ResMut, + windows: Query>, + mut commands: Commands, +) { + // Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event + // for every ancestor, but only for the original entity. Also, users may want to stop + // propagation on the pointer event at some point along the bubbling chain, so we need our + // own dedicated event whose propagation we can control. + if ev.target() == ev.original_target() { + // Clicking hides focus + if focus_visible.0 { + focus_visible.0 = false; + } + // Search for a focusable parent entity, defaulting to window if none. + if let Ok(window) = windows.single() { + commands + .entity(ev.target()) + .trigger(AcquireFocus { window }); + } + } +} + /// Observer function which handles tab navigation. /// /// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c9639f1950..4692fe9d15 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_internal" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature" homepage = "https://bevy.org" @@ -350,80 +350,80 @@ debug = ["bevy_utils/debug"] [dependencies] # bevy (no_std) -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev", default-features = false } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false, features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev", default-features = false } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false, features = [ +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", "nostd-libm", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "smallvec", ] } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false, features = [ +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev", default-features = false, features = [ "bevy-support", "bevy_reflect", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } # bevy (std required) -bevy_log = { path = "../bevy_log", version = "0.16.0-dev", optional = true } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev", optional = true } # bevy (optional) -bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.16.0-dev", features = [ +bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.17.0-dev", features = [ "bevy_reflect", ] } -bevy_animation = { path = "../bevy_animation", optional = true, version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", optional = true, version = "0.16.0-dev" } -bevy_audio = { path = "../bevy_audio", optional = true, version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_animation = { path = "../bevy_animation", optional = true, version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", optional = true, version = "0.17.0-dev" } +bevy_audio = { path = "../bevy_audio", optional = true, version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", optional = true, version = "0.17.0-dev", default-features = false, features = [ "alloc", "bevy_reflect", ] } -bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } -bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" } -bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } -bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } -bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } -bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.16.0-dev", default-features = false } -bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", optional = true, version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.17.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.17.0-dev" } +bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.17.0-dev" } +bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.0-dev" } +bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false } +bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-dev" } -bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } -bevy_solari = { path = "../bevy_solari", optional = true, version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.0-dev" } +bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } +bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_app", "bevy_reflect", ] } -bevy_text = { path = "../bevy_text", optional = true, version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", optional = true, version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_text = { path = "../bevy_text", optional = true, version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", optional = true, version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_winit = { path = "../bevy_winit", optional = true, version = "0.16.0-dev", default-features = false } +bevy_winit = { path = "../bevy_winit", optional = true, version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index fce602f7ad..3dcfa27794 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_log" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides logging for Bevy Engine" homepage = "https://bevy.org" @@ -14,10 +14,10 @@ trace_tracy_memory = ["dep:tracy-client"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } # other tracing-subscriber = { version = "0.3.1", features = [ @@ -40,7 +40,7 @@ android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-wasm = "0.2.1" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index bc5989f0f3..b998ae4fd4 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_macro_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" homepage = "https://bevy.org" diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index c420c3fe3c..f28e9466ec 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_math" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides math functionality for Bevy Engine" homepage = "https://bevy.org" @@ -25,7 +25,7 @@ approx = { version = "0.5", default-features = false, optional = true } rand = { version = "0.8", default-features = false, optional = true } rand_distr = { version = "0.4.3", optional = true } smallvec = { version = "1.11" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", ], optional = true } variadics_please = "1.1" diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 1b04603a73..3ea99a60b0 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -1347,6 +1347,7 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } + impl> RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index d666849840..9cb379706c 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -35,6 +35,7 @@ pub struct Circle { /// The radius of the circle pub radius: f32, } + impl Primitive2d for Circle {} impl Default for Circle { @@ -124,6 +125,7 @@ pub struct Arc2d { /// Half the angle defining the arc pub half_angle: f32, } + impl Primitive2d for Arc2d {} impl Default for Arc2d { @@ -290,6 +292,7 @@ pub struct CircularSector { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSector {} impl Default for CircularSector { @@ -433,6 +436,7 @@ pub struct CircularSegment { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSegment {} impl Default for CircularSegment { @@ -453,6 +457,7 @@ impl Measured2d for CircularSegment { self.chord_length() + self.arc_length() } } + impl CircularSegment { /// Create a new [`CircularSegment`] from a `radius`, and an `angle` #[inline(always)] @@ -788,6 +793,7 @@ pub struct Ellipse { /// This corresponds to the two perpendicular radii defining the ellipse. pub half_size: Vec2, } + impl Primitive2d for Ellipse {} impl Default for Ellipse { @@ -939,6 +945,7 @@ pub struct Annulus { /// The outer circle of the annulus pub outer_circle: Circle, } + impl Primitive2d for Annulus {} impl Default for Annulus { @@ -1036,6 +1043,7 @@ pub struct Rhombus { /// Size of the horizontal and vertical diagonals of the rhombus pub half_diagonals: Vec2, } + impl Primitive2d for Rhombus {} impl Default for Rhombus { @@ -1171,6 +1179,7 @@ pub struct Plane2d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir2, } + impl Primitive2d for Plane2d {} impl Default for Plane2d { @@ -1213,6 +1222,7 @@ pub struct Line2d { /// and its opposite direction pub direction: Dir2, } + impl Primitive2d for Line2d {} /// A line segment defined by two endpoints in 2D space. @@ -1232,6 +1242,7 @@ pub struct Segment2d { /// The endpoints of the line segment. pub vertices: [Vec2; 2], } + impl Primitive2d for Segment2d {} impl Segment2d { @@ -1504,6 +1515,7 @@ pub struct Polyline2d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polyline2d {} impl FromIterator for Polyline2d { @@ -1573,6 +1585,7 @@ pub struct Triangle2d { /// The vertices of the triangle pub vertices: [Vec2; 3], } + impl Primitive2d for Triangle2d {} impl Default for Triangle2d { @@ -1745,6 +1758,7 @@ pub struct Rectangle { /// Half of the width and height of the rectangle pub half_size: Vec2, } + impl Primitive2d for Rectangle {} impl Default for Rectangle { @@ -1838,6 +1852,7 @@ pub struct Polygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polygon {} impl FromIterator for Polygon { @@ -1892,6 +1907,7 @@ pub struct ConvexPolygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] vertices: [Vec2; N], } + impl Primitive2d for ConvexPolygon {} /// An error that happens when creating a [`ConvexPolygon`]. @@ -2013,6 +2029,7 @@ pub struct RegularPolygon { /// The number of sides pub sides: u32, } + impl Primitive2d for RegularPolygon {} impl Default for RegularPolygon { @@ -2160,6 +2177,7 @@ pub struct Capsule2d { /// Half the height of the capsule, excluding the semicircles pub half_length: f32, } + impl Primitive2d for Capsule2d {} impl Default for Capsule2d { diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index ea5ccd6e2d..86aa6c5bdf 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -31,6 +31,7 @@ pub struct Sphere { /// The radius of the sphere pub radius: f32, } + impl Primitive3d for Sphere {} impl Default for Sphere { @@ -105,6 +106,7 @@ pub struct Plane3d { /// Half of the width and height of the plane pub half_size: Vec2, } + impl Primitive3d for Plane3d {} impl Default for Plane3d { @@ -175,6 +177,7 @@ pub struct InfinitePlane3d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir3, } + impl Primitive3d for InfinitePlane3d {} impl Default for InfinitePlane3d { @@ -351,6 +354,7 @@ pub struct Line3d { /// The direction of the line pub direction: Dir3, } + impl Primitive3d for Line3d {} /// A line segment defined by two endpoints in 3D space. @@ -370,6 +374,7 @@ pub struct Segment3d { /// The endpoints of the line segment. pub vertices: [Vec3; 2], } + impl Primitive3d for Segment3d {} impl Segment3d { @@ -578,6 +583,7 @@ pub struct Polyline3d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec3; N], } + impl Primitive3d for Polyline3d {} impl FromIterator for Polyline3d { @@ -648,6 +654,7 @@ pub struct Cuboid { /// Half of the width, height and depth of the cuboid pub half_size: Vec3, } + impl Primitive3d for Cuboid {} impl Default for Cuboid { @@ -742,6 +749,7 @@ pub struct Cylinder { /// The half height of the cylinder pub half_height: f32, } + impl Primitive3d for Cylinder {} impl Default for Cylinder { @@ -820,6 +828,7 @@ pub struct Capsule3d { /// Half the height of the capsule, excluding the hemispheres pub half_length: f32, } + impl Primitive3d for Capsule3d {} impl Default for Capsule3d { @@ -890,6 +899,7 @@ pub struct Cone { /// The height of the cone pub height: f32, } + impl Primitive3d for Cone {} impl Default for Cone { @@ -974,6 +984,7 @@ pub struct ConicalFrustum { /// The height of the frustum pub height: f32, } + impl Primitive3d for ConicalFrustum {} impl Default for ConicalFrustum { @@ -1030,6 +1041,7 @@ pub struct Torus { #[doc(alias = "radius_of_revolution")] pub major_radius: f32, } + impl Primitive3d for Torus {} impl Default for Torus { @@ -1326,6 +1338,7 @@ pub struct Tetrahedron { /// The vertices of the tetrahedron. pub vertices: [Vec3; 4], } + impl Primitive3d for Tetrahedron {} impl Default for Tetrahedron { @@ -1433,6 +1446,7 @@ pub struct Extrusion { /// Half of the depth of the extrusion pub half_depth: f32, } + impl Primitive3d for Extrusion {} impl Extrusion { diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_math/src/primitives/polygon.rs index 20d35b552c..9aa261b297 100644 --- a/crates/bevy_math/src/primitives/polygon.rs +++ b/crates/bevy_math/src/primitives/polygon.rs @@ -34,6 +34,7 @@ struct SweepLineEvent { /// Type of the vertex (left or right) endpoint: Endpoint, } + impl SweepLineEvent { #[cfg_attr( not(feature = "alloc"), @@ -46,17 +47,21 @@ impl SweepLineEvent { } } } + impl PartialEq for SweepLineEvent { fn eq(&self, other: &Self) -> bool { self.position() == other.position() } } + impl Eq for SweepLineEvent {} + impl PartialOrd for SweepLineEvent { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } + impl Ord for SweepLineEvent { fn cmp(&self, other: &Self) -> Ordering { xy_order(self.position(), other.position()) @@ -129,11 +134,13 @@ struct Segment { left: Vec2, right: Vec2, } + impl PartialEq for Segment { fn eq(&self, other: &Self) -> bool { self.edge_index == other.edge_index } } + impl Eq for Segment {} impl PartialOrd for Segment { @@ -141,6 +148,7 @@ impl PartialOrd for Segment { Some(self.cmp(other)) } } + impl Ord for Segment { fn cmp(&self, other: &Self) -> Ordering { self.left diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index a235fea5ef..c65c648cfd 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mesh" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides mesh types for Bevy Engine" homepage = "https://bevy.org" @@ -10,16 +10,16 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index d2497e2c50..ca84d63bfb 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -163,6 +163,7 @@ impl Iterator for IndicesIter<'_> { } impl<'a> ExactSizeIterator for IndicesIter<'a> {} + impl<'a> FusedIterator for IndicesIter<'a> {} impl From<&Indices> for IndexFormat { diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index a8ff3be037..fdeeeacc31 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -117,6 +117,7 @@ pub struct MorphWeights { /// The first mesh primitive assigned to these weights first_mesh: Option>, } + impl MorphWeights { pub fn new( weights: Vec, @@ -160,6 +161,7 @@ impl MorphWeights { pub struct MeshMorphWeights { weights: Vec, } + impl MeshMorphWeights { pub fn new(weights: Vec) -> Result { if weights.len() > MAX_MORPH_WEIGHTS { @@ -198,6 +200,7 @@ pub struct MorphAttributes { /// animated, as the `w` component is the sign and cannot be animated. pub tangent: Vec3, } + impl From<[Vec3; 3]> for MorphAttributes { fn from([position, normal, tangent]: [Vec3; 3]) -> Self { MorphAttributes { @@ -207,6 +210,7 @@ impl From<[Vec3; 3]> for MorphAttributes { } } } + impl MorphAttributes { /// How many components `MorphAttributes` has. /// diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 7428504adc..82c2d86553 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mikktspace" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Benjamin Wasty ", diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 41a058b522..4ecf53a773 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_pbr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Adds PBR rendering to Bevy Engine" homepage = "https://bevy.org" @@ -31,22 +31,22 @@ meshlet_processor = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 3113333be3..3559cb52d5 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -535,12 +535,12 @@ pub fn extract_clusters( continue; } - let num_entities: usize = clusters + let entity_count: usize = clusters .clusterable_objects .iter() .map(|l| l.entities.len()) .sum(); - let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities); + let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + entity_count); for cluster_objects in &clusters.clusterable_objects { data.push(ExtractedClusterableObjectElement::ClusterHeader( cluster_objects.counts, diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index db255722b3..cfbe99963b 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -48,6 +48,7 @@ impl Default for AmbientLight { } } } + impl AmbientLight { pub const NONE: AmbientLight = AmbientLight { color: Color::WHITE, diff --git a/crates/bevy_pbr/src/parallax.rs b/crates/bevy_pbr/src/parallax.rs index 0a847b7c25..be588ca87c 100644 --- a/crates/bevy_pbr/src/parallax.rs +++ b/crates/bevy_pbr/src/parallax.rs @@ -33,6 +33,7 @@ pub enum ParallaxMappingMethod { max_steps: u32, }, } + impl ParallaxMappingMethod { /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index b42f0db14c..ac18b1dd89 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_picking" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides screen picking functionality for Bevy Engine" homepage = "https://bevy.org" @@ -13,20 +13,20 @@ bevy_mesh_picking_backend = ["dep:bevy_mesh", "dep:crossbeam-channel"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 7a4313af39..614ff15fce 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_platform" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides common platform agnostic APIs, as well as platform-specific features for Bevy Engine" homepage = "https://bevy.org" diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index b6e72e24f0..07c6eeae68 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ptr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Utilities for working with untyped pointers in a more safe way" homepage = "https://bevy.org" diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 704d60d675..15a193d737 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -27,7 +27,9 @@ pub struct Unaligned; /// Trait that is only implemented for [`Aligned`] and [`Unaligned`] to work around the lack of ability /// to have const generics of an enum. pub trait IsAligned: sealed::Sealed {} + impl IsAligned for Aligned {} + impl IsAligned for Unaligned {} mod sealed { diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 4d72ab1d6a..087cdb44db 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Dynamically interact with rust types" homepage = "https://bevy.org" @@ -74,10 +74,10 @@ web = ["bevy_platform/web", "uuid?/js"] [dependencies] # bevy -bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect_derive = { path = "derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", "serialize", ] } diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index a3685941cc..032046ae2f 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_reflect" homepage = "https://bevy.org" @@ -19,7 +19,7 @@ documentation = [] functions = [] [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_reflect/src/func/dynamic_function.rs b/crates/bevy_reflect/src/func/dynamic_function.rs index 054e8ffaff..ab1d70e4ed 100644 --- a/crates/bevy_reflect/src/func/dynamic_function.rs +++ b/crates/bevy_reflect/src/func/dynamic_function.rs @@ -439,6 +439,7 @@ impl PartialReflect for DynamicFunction<'static> { } impl MaybeTyped for DynamicFunction<'static> {} + impl RegisterForReflection for DynamicFunction<'static> {} impl_type_path!((in bevy_reflect) DynamicFunction<'env>); diff --git a/crates/bevy_reflect/src/func/signature.rs b/crates/bevy_reflect/src/func/signature.rs index 9102049eee..cedeaca952 100644 --- a/crates/bevy_reflect/src/func/signature.rs +++ b/crates/bevy_reflect/src/func/signature.rs @@ -90,6 +90,7 @@ impl<'a, 'b> ArgListSignature<'a, 'b> { } impl Eq for ArgListSignature<'_, '_> {} + impl PartialEq for ArgListSignature<'_, '_> { fn eq(&self, other: &Self) -> bool { self.len() == other.len() && self.iter().eq(other.iter()) diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 942bcbe83f..561111a901 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -77,6 +77,7 @@ where .collect() } } + impl PartialReflect for SmallVec where T::Item: FromReflect + MaybeTyped + TypePath, diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3764e863ba..8988b30aa5 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -220,6 +220,7 @@ pub enum ReflectRef<'a> { /// [opaque]: ReflectKind::Opaque Opaque(&'a dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectRef<'_>); impl<'a> ReflectRef<'a> { @@ -285,6 +286,7 @@ pub enum ReflectMut<'a> { /// [opaque]: ReflectKind::Opaque Opaque(&'a mut dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectMut<'_>); impl<'a> ReflectMut<'a> { @@ -350,6 +352,7 @@ pub enum ReflectOwned { /// [opaque]: ReflectKind::Opaque Opaque(Box), } + impl_reflect_kind_conversions!(ReflectOwned); impl ReflectOwned { diff --git a/crates/bevy_reflect/src/path/error.rs b/crates/bevy_reflect/src/path/error.rs index 0e900c8315..00188a4cc3 100644 --- a/crates/bevy_reflect/src/path/error.rs +++ b/crates/bevy_reflect/src/path/error.rs @@ -74,6 +74,7 @@ impl<'a> AccessError<'a> { self.offset.as_ref() } } + impl fmt::Display for AccessError<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let AccessError { @@ -126,4 +127,5 @@ impl fmt::Display for AccessError<'_> { } } } + impl core::error::Error for AccessError<'_> {} diff --git a/crates/bevy_reflect/src/path/mod.rs b/crates/bevy_reflect/src/path/mod.rs index a52bbb6aaa..f0434686ee 100644 --- a/crates/bevy_reflect/src/path/mod.rs +++ b/crates/bevy_reflect/src/path/mod.rs @@ -82,6 +82,7 @@ pub trait ReflectPath<'a>: Sized { }) } } + impl<'a> ReflectPath<'a> for &'a str { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for (access, offset) in PathParser::new(self) { @@ -437,6 +438,7 @@ impl ParsedPath { Ok(Self(parts)) } } + impl<'a> ReflectPath<'a> for &'a ParsedPath { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for OffsetAccess { access, offset } in &self.0 { @@ -454,11 +456,13 @@ impl<'a> ReflectPath<'a> for &'a ParsedPath { Ok(root) } } + impl From<[OffsetAccess; N]> for ParsedPath { fn from(value: [OffsetAccess; N]) -> Self { ParsedPath(value.to_vec()) } } + impl From>> for ParsedPath { fn from(value: Vec>) -> Self { ParsedPath( @@ -472,6 +476,7 @@ impl From>> for ParsedPath { ) } } + impl From<[Access<'static>; N]> for ParsedPath { fn from(value: [Access<'static>; N]) -> Self { value.to_vec().into() @@ -493,12 +498,14 @@ impl fmt::Display for ParsedPath { Ok(()) } } + impl core::ops::Index for ParsedPath { type Output = OffsetAccess; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } + impl core::ops::IndexMut for ParsedPath { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index 2ab2939a30..be5856834a 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -38,6 +38,7 @@ pub(super) struct PathParser<'a> { path: &'a str, remaining: &'a [u8], } + impl<'a> PathParser<'a> { pub(super) fn new(path: &'a str) -> Self { let remaining = path.as_bytes(); @@ -103,6 +104,7 @@ impl<'a> PathParser<'a> { self.path.len() - self.remaining.len() } } + impl<'a> Iterator for PathParser<'a> { type Item = (Result, ReflectPathError<'a>>, usize); @@ -149,6 +151,7 @@ enum Token<'a> { CloseBracket = b']', Ident(Ident<'a>), } + impl fmt::Display for Token<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -160,6 +163,7 @@ impl fmt::Display for Token<'_> { } } } + impl<'a> Token<'a> { const SYMBOLS: &'static [u8] = b".#[]"; fn symbol_from_byte(byte: u8) -> Option { diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 51f402c698..97da69b5e2 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -649,17 +649,29 @@ macro_rules! impl_reflect_tuple { } impl_reflect_tuple! {} + impl_reflect_tuple! {0: A} + impl_reflect_tuple! {0: A, 1: B} + impl_reflect_tuple! {0: A, 1: B, 2: C} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L} macro_rules! impl_type_path_tuple { diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 1f18396be2..a20074b827 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -689,6 +689,7 @@ pub trait TypeData: Downcast + Send + Sync { /// Creates a type-erased clone of this value. fn clone_type_data(&self) -> Box; } + impl_downcast!(TypeData); impl TypeData for T diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 176e5ed47f..7fc8d2b4de 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_remote" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "The Bevy Remote Protocol" homepage = "https://bevy.org" @@ -9,25 +9,27 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http"] +default = ["http", "bevy_asset"] http = ["dep:async-io", "dep:smol-hyper"] +bevy_asset = ["dep:bevy_asset"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", features = [ "serialize", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ "debug", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } # other anyhow = "1" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index b6747217b1..7680b7f51f 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -24,7 +24,10 @@ use serde_json::{Map, Value}; use crate::{ error_codes, - schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, + schemas::{ + json_schema::{export_type, JsonSchemaBevyType}, + open_rpc::OpenRpcDocument, + }, BrpError, BrpResult, }; @@ -1231,24 +1234,27 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br Some(params) => parse(params)?, }; + let extra_info = world.resource::(); let types = world.resource::(); let types = types.read(); let schemas = types .iter() - .map(crate::schemas::json_schema::export_type) - .filter(|(_, schema)| { - if let Some(crate_name) = &schema.crate_name { + .filter_map(|type_reg| { + let path_table = type_reg.type_info().type_path_table(); + if let Some(crate_name) = &path_table.crate_name() { if !filter.with_crates.is_empty() && !filter.with_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } if !filter.without_crates.is_empty() && filter.without_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } } + let (id, schema) = export_type(type_reg, extra_info); + if !filter.type_limit.with.is_empty() && !filter .type_limit @@ -1256,7 +1262,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } if !filter.type_limit.without.is_empty() && filter @@ -1265,10 +1271,9 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } - - true + Some((id.to_string(), schema)) }) .collect::>(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..348be8089d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -364,6 +364,8 @@ //! [fully-qualified type names]: bevy_reflect::TypePath::type_path //! [fully-qualified type name]: bevy_reflect::TypePath::type_path +extern crate alloc; + use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -539,6 +541,7 @@ impl Plugin for RemotePlugin { .insert_after(Last, RemoteLast); app.insert_resource(remote_methods) + .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 3fcc588f92..4e56625bc8 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -1,47 +1,63 @@ //! Module with JSON Schema type for Bevy Registry Types. //! It tries to follow this standard: -use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use alloc::borrow::Cow; use bevy_platform::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, VariantInfo, + GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -/// Exports schema info for a given type -pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - (reg.type_info().type_path().to_owned(), reg.into()) -} +use crate::schemas::SchemaTypesMetadata; -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } +/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` +pub trait TypeRegistrySchemaReader { + /// Export type JSON Schema. + fn export_type_json_schema( + &self, + extra_info: &SchemaTypesMetadata, + ) -> Option { + self.export_type_json_schema_for_id(extra_info, TypeId::of::()) } - result + /// Export type JSON Schema. + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option; } -impl From<&TypeRegistration> for JsonSchemaBevyType { - fn from(reg: &TypeRegistration) -> Self { +impl TypeRegistrySchemaReader for TypeRegistry { + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option { + let type_reg = self.get(type_id)?; + Some((type_reg, extra_info).into()) + } +} + +/// Exports schema info for a given type +pub fn export_type( + reg: &TypeRegistration, + metadata: &SchemaTypesMetadata, +) -> (Cow<'static, str>, JsonSchemaBevyType) { + (reg.type_info().type_path().into(), (reg, metadata).into()) +} + +impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { + fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { + let (reg, metadata) = value; let t = reg.type_info(); let binding = t.type_path_table(); let short_path = binding.short_path(); let type_path = binding.path(); let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), + reflect_types: metadata.get_registered_reflect_types(reg), short_path: short_path.to_owned(), type_path: type_path.to_owned(), crate_name: binding.crate_name().map(str::to_owned), @@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField { #[cfg(test)] mod tests { use super::*; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::prelude::ReflectResource; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; - use bevy_reflect::Reflect; + use bevy_reflect::prelude::ReflectDefault; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; #[test] fn reflect_export_struct() { @@ -373,7 +393,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -418,7 +438,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -453,7 +473,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), "Should not be a component" @@ -466,6 +486,62 @@ mod tests { assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); } + #[test] + fn reflect_struct_with_custom_type_data() { + #[derive(Reflect, Default, Deserialize, Serialize)] + #[reflect(Default)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + #[derive(Clone)] + pub struct ReflectCustomData; + + impl bevy_reflect::FromType for ReflectCustomData { + fn from_type() -> Self { + ReflectCustomData + } + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register_type_data::(); + } + let mut metadata = SchemaTypesMetadata::default(); + metadata.map_type_data::("CustomData"); + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration, &metadata); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a component" + ); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a resource" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have default" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have CustomData" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + #[test] fn reflect_export_tuple_struct() { #[derive(Reflect, Component, Default, Deserialize, Serialize)] @@ -482,7 +558,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -513,7 +589,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", @@ -538,6 +614,31 @@ mod tests { "a" ] }); - assert_eq!(schema_as_value, value); + assert_normalized_values(schema_as_value, value); + } + + /// This function exist to avoid false failures due to ordering differences between `serde_json` values. + fn assert_normalized_values(mut one: Value, mut two: Value) { + normalize_json(&mut one); + normalize_json(&mut two); + assert_eq!(one, two); + + /// Recursively sorts arrays in a `serde_json::Value` + fn normalize_json(value: &mut Value) { + match value { + Value::Array(arr) => { + for v in arr.iter_mut() { + normalize_json(v); + } + arr.sort_by_key(ToString::to_string); // Sort by stringified version + } + Value::Object(map) => { + for (_k, v) in map.iter_mut() { + normalize_json(v); + } + } + _ => {} + } + } } } diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 7104fd5547..10cb2e9421 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,4 +1,68 @@ //! Module with schemas used for various BRP endpoints +use bevy_ecs::{ + reflect::{ReflectComponent, ReflectResource}, + resource::Resource, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, + TypeRegistration, +}; +use core::any::TypeId; pub mod json_schema; pub mod open_rpc; + +/// Holds mapping of reflect [type data](TypeData) to strings, +/// later on used in Bevy Json Schema. +#[derive(Debug, Resource, Reflect)] +#[reflect(Resource)] +pub struct SchemaTypesMetadata { + /// Type Data id mapping to strings. + pub type_data_map: HashMap, +} + +impl Default for SchemaTypesMetadata { + fn default() -> Self { + let mut data_types = Self { + type_data_map: Default::default(), + }; + data_types.map_type_data::("Component"); + data_types.map_type_data::("Resource"); + data_types.map_type_data::("Default"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("Asset"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("AssetHandle"); + data_types.map_type_data::("Serialize"); + data_types.map_type_data::("Deserialize"); + data_types + } +} + +impl SchemaTypesMetadata { + /// Map `TypeId` of `TypeData` to string + pub fn map_type_data(&mut self, name: impl Into) { + self.type_data_map.insert(TypeId::of::(), name.into()); + } + + /// Build reflect types list for a given type registration + pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec { + self.type_data_map + .iter() + .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) + .collect() + } + + /// Checks if slice contains string value that matches checked `TypeData` + pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { + self.has_type_data_by_id(TypeId::of::(), types_string_slice) + } + + /// Checks if slice contains string value that matches checked `TypeData` by id. + pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { + self.type_data_map + .get(&id) + .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) + } +} diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index d9775e9c8f..143a1f0270 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides rendering functionality for Bevy Engine" homepage = "https://bevy.org" @@ -49,29 +49,29 @@ detailed_trace = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ "wgpu_wrapper", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -140,16 +140,16 @@ web-sys = { version = "0.3.67", features = [ ] } wasm-bindgen = "0.2" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index 74348598fe..016fe88765 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_render" homepage = "https://bevy.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index 7f046036a9..197b9f4e7f 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -148,7 +148,7 @@ pub struct PassSpanGuard<'a, R: ?Sized, P> { } impl PassSpanGuard<'_, R, P> { - /// End the span. You have to provide the same encoder which was used to begin the span. + /// End the span. You have to provide the same pass which was used to begin the span. pub fn end(self, pass: &mut P) { self.recorder.end_pass_span(pass); core::mem::forget(self); diff --git a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs index 195920ee0c..0c5bf36bf6 100644 --- a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs +++ b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs @@ -14,6 +14,7 @@ use wgpu::{BindingResource, BufferUsages}; /// Trait for types able to go in a [`GpuArrayBuffer`]. pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {} + impl GpuArrayBufferable for T {} /// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array. diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 8a6fe517fd..78de17e26f 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_scene" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides scene functionality for Bevy Engine" homepage = "https://bevy.org" @@ -19,15 +19,15 @@ serialize = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml index 85cf85bd49..03976dea3f 100644 --- a/crates/bevy_solari/Cargo.toml +++ b/crates/bevy_solari/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_solari" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides raytraced lighting for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,25 +10,27 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } # other -tracing = { version = "0.1", default-features = false, features = ["std"] } +bytemuck = { version = "1" } derive_more = { version = "1", default-features = false, features = ["from"] } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs index 022f44b5c5..416c850b04 100644 --- a/crates/bevy_solari/src/lib.rs +++ b/crates/bevy_solari/src/lib.rs @@ -6,6 +6,7 @@ //! //! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) pub mod pathtracer; +pub mod realtime; pub mod scene; /// The solari prelude. @@ -13,28 +14,28 @@ pub mod scene; /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { pub use super::SolariPlugin; - pub use crate::pathtracer::Pathtracer; + pub use crate::realtime::SolariLighting; pub use crate::scene::RaytracingMesh3d; } +use crate::realtime::SolariLightingPlugin; +use crate::scene::RaytracingScenePlugin; use bevy_app::{App, Plugin}; use bevy_render::settings::WgpuFeatures; -use pathtracer::PathtracingPlugin; -use scene::RaytracingScenePlugin; /// An experimental plugin for raytraced lighting. /// /// This plugin provides: -/// * (Coming soon) - Raytraced direct and indirect lighting. +/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented). /// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding. -/// * [`PathtracingPlugin`] - A non-realtime pathtracer for validation purposes. +/// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes. /// /// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. pub struct SolariPlugin; impl Plugin for SolariPlugin { fn build(&self, app: &mut App) { - app.add_plugins((RaytracingScenePlugin, PathtracingPlugin)); + app.add_plugins((RaytracingScenePlugin, SolariLightingPlugin)); } } diff --git a/crates/bevy_solari/src/realtime/extract.rs b/crates/bevy_solari/src/realtime/extract.rs new file mode 100644 index 0000000000..8e80f02327 --- /dev/null +++ b/crates/bevy_solari/src/realtime/extract.rs @@ -0,0 +1,27 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use bevy_ecs::system::{Commands, ResMut}; +use bevy_pbr::deferred::SkipDeferredLighting; +use bevy_render::{camera::Camera, sync_world::RenderEntity, MainWorld}; + +pub fn extract_solari_lighting(mut main_world: ResMut, mut commands: Commands) { + let mut cameras_3d = main_world.query::<(RenderEntity, &Camera, Option<&mut SolariLighting>)>(); + + for (entity, camera, mut solari_lighting) in cameras_3d.iter_mut(&mut main_world) { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if solari_lighting.is_some() && camera.is_active { + entity_commands.insert(( + solari_lighting.as_deref().unwrap().clone(), + SkipDeferredLighting, + )); + solari_lighting.as_mut().unwrap().reset = false; + } else { + entity_commands.remove::<( + SolariLighting, + SolariLightingResources, + SkipDeferredLighting, + )>(); + } + } +} diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs new file mode 100644 index 0000000000..9308ab5cf8 --- /dev/null +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -0,0 +1,91 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass}, +}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_pbr::DefaultOpaqueRendererMethod; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + load_shader_library, + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_solari_lighting; +use node::SolariLightingNode; +use prepare::prepare_solari_lighting_resources; +use tracing::warn; + +pub struct SolariLightingPlugin; + +impl Plugin for SolariLightingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "restir_di.wgsl"); + load_shader_library!(app, "reservoir.wgsl"); + + app.register_type::() + .insert_resource(DefaultOpaqueRendererMethod::deferred()); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "SolariLightingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + render_app + .add_systems(ExtractSchedule, extract_solari_lighting) + .add_systems( + Render, + prepare_solari_lighting_resources.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::SolariLightingNode, + ) + .add_render_graph_edges( + Core3d, + (Node3d::EndMainPass, node::graph::SolariLightingNode), + ); + } +} + +/// A component for a 3d camera entity to enable the Solari raytraced lighting system. +/// +/// Must be used with `CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING)`, and +/// `Msaa::Off`. +#[derive(Component, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr, DeferredPrepass, DepthPrepass, MotionVectorPrepass)] +pub struct SolariLighting { + /// Set to true to delete the saved temporal history (past frames). + /// + /// Useful for preventing ghosting when the history is no longer + /// representative of the current frame, such as in sudden camera cuts. + /// + /// After setting this to true, it will automatically be toggled + /// back to false at the end of the frame. + pub reset: bool, +} + +impl Default for SolariLighting { + fn default() -> Self { + Self { + reset: true, // No temporal history on the first frame + } + } +} diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs new file mode 100644 index 0000000000..6060bb3c15 --- /dev/null +++ b/crates/bevy_solari/src/realtime/node.rs @@ -0,0 +1,200 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + storage_buffer_sized, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + }, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, PipelineCache, PushConstantRange, + ShaderStages, StorageTextureAccess, TextureSampleType, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct SolariLightingNode; +} + +pub struct SolariLightingNode { + bind_group_layout: BindGroupLayout, + initial_and_temporal_pipeline: CachedComputePipelineId, + spatial_and_shade_pipeline: CachedComputePipelineId, +} + +impl ViewNode for SolariLightingNode { + type ViewQuery = ( + &'static SolariLighting, + &'static SolariLightingResources, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + solari_lighting, + solari_lighting_resources, + camera, + view_target, + view_prepass_textures, + view_uniform_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let frame_count = world.resource::(); + let ( + Some(initial_and_temporal_pipeline), + Some(spatial_and_shade_pipeline), + Some(scene_bindings), + Some(viewport), + Some(gbuffer), + Some(depth_buffer), + Some(motion_vectors), + Some(view_uniforms), + ) = ( + pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline), + pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_prepass_textures.deferred_view(), + view_prepass_textures.depth_view(), + view_prepass_textures.motion_vectors_view(), + view_uniforms.uniforms.binding(), + ) + else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "solari_lighting_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + view_target.get_unsampled_color_attachment().view, + solari_lighting_resources.reservoirs_a.as_entire_binding(), + solari_lighting_resources.reservoirs_b.as_entire_binding(), + gbuffer, + depth_buffer, + motion_vectors, + view_uniforms, + )), + ); + + // Choice of number here is arbitrary + let frame_index = frame_count.0.wrapping_mul(5782582); + + let diagnostics = render_context.diagnostic_recorder(); + let command_encoder = render_context.command_encoder(); + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("solari_lighting"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut pass, "solari_lighting"); + + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + + pass.set_pipeline(initial_and_temporal_pipeline); + pass.set_push_constants( + 0, + bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]), + ); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass.set_pipeline(spatial_and_shade_pipeline); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass_span.end(&mut pass); + + Ok(()) + } +} + +impl FromWorld for SolariLightingNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "solari_lighting_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + texture_2d(TextureSampleType::Uint), + texture_depth_2d(), + texture_2d(TextureSampleType::Float { filterable: true }), + uniform_buffer::(true), + ), + ), + ); + + let initial_and_temporal_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_initial_and_temporal_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "initial_and_temporal".into(), + zero_initialize_workgroup_memory: false, + }); + + let spatial_and_shade_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_spatial_and_shade_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "spatial_and_shade".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + initial_and_temporal_pipeline, + spatial_and_shade_pipeline, + } + } +} diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs new file mode 100644 index 0000000000..4f153bf0dc --- /dev/null +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -0,0 +1,65 @@ +use super::SolariLighting; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res}, +}; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{Buffer, BufferDescriptor, BufferUsages}, + renderer::RenderDevice, +}; + +/// Size of a Reservoir shader struct in bytes. +const RESERVOIR_STRUCT_SIZE: u64 = 32; + +/// Internal rendering resources used for Solari lighting. +#[derive(Component)] +pub struct SolariLightingResources { + pub reservoirs_a: Buffer, + pub reservoirs_b: Buffer, + pub view_size: UVec2, +} + +pub fn prepare_solari_lighting_resources( + query: Query< + (Entity, &ExtractedCamera, Option<&SolariLightingResources>), + With, + >, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera, solari_lighting_resources) in &query { + let Some(view_size) = camera.physical_viewport_size else { + continue; + }; + + if solari_lighting_resources.map(|r| r.view_size) == Some(view_size) { + continue; + } + + let size = (view_size.x * view_size.y) as u64 * RESERVOIR_STRUCT_SIZE; + + let reservoirs_a = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_a"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + let reservoirs_b = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_b"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + commands.entity(entity).insert(SolariLightingResources { + reservoirs_a, + reservoirs_b, + view_size, + }); + } +} diff --git a/crates/bevy_solari/src/realtime/reservoir.wgsl b/crates/bevy_solari/src/realtime/reservoir.wgsl new file mode 100644 index 0000000000..08a7e26f7c --- /dev/null +++ b/crates/bevy_solari/src/realtime/reservoir.wgsl @@ -0,0 +1,30 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + +#define_import_path bevy_solari::reservoir + +#import bevy_solari::sampling::LightSample + +const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; + +// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. +struct Reservoir { + sample: LightSample, + weight_sum: f32, + confidence_weight: f32, + unbiased_contribution_weight: f32, + _padding: f32, +} + +fn empty_reservoir() -> Reservoir { + return Reservoir( + LightSample(vec2(NULL_RESERVOIR_SAMPLE, 0u), vec2(0.0)), + 0.0, + 0.0, + 0.0, + 0.0 + ); +} + +fn reservoir_valid(reservoir: Reservoir) -> bool { + return reservoir.sample.light_id.x != NULL_RESERVOIR_SAMPLE; +} diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl new file mode 100644 index 0000000000..511fd63d12 --- /dev/null +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -0,0 +1,117 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ +#import bevy_pbr::utils::{rand_f, octahedral_decode} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::reservoir::{Reservoir, empty_reservoir, reservoir_valid} +#import bevy_solari::sampling::{generate_random_light_sample, calculate_light_contribution, trace_light_visibility} + +@group(1) @binding(0) var view_output: texture_storage_2d; +@group(1) @binding(1) var reservoirs_a: array; +@group(1) @binding(2) var reservoirs_b: array; +@group(1) @binding(3) var gbuffer: texture_2d; +@group(1) @binding(4) var depth_buffer: texture_depth_2d; +@group(1) @binding(5) var motion_vectors: texture_2d; +@group(1) @binding(6) var view: View; +struct PushConstants { frame_index: u32, reset: u32 } +var constants: PushConstants; + +const INITIAL_SAMPLES = 32u; +const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; +const CONFIDENCE_WEIGHT_CAP = 20.0 * f32(INITIAL_SAMPLES); + +@compute @workgroup_size(8, 8, 1) +fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_b[pixel_index] = empty_reservoir(); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + + let initial_reservoir = generate_initial_reservoir(world_position, world_normal, diffuse_brdf, &rng); + + reservoirs_b[pixel_index] = initial_reservoir; +} + +@compute @workgroup_size(8, 8, 1) +fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_a[pixel_index] = empty_reservoir(); + textureStore(view_output, global_id.xy, vec4(vec3(0.0), 1.0)); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + let emissive = rgb9e5_to_vec3_(gpixel.g); + + let input_reservoir = reservoirs_b[pixel_index]; + + var radiance = vec3(0.0); + if reservoir_valid(input_reservoir) { + radiance = calculate_light_contribution(input_reservoir.sample, world_position, world_normal).radiance; + } + + reservoirs_a[pixel_index] = input_reservoir; + + var pixel_color = radiance * input_reservoir.unbiased_contribution_weight; + pixel_color *= view.exposure; + pixel_color *= diffuse_brdf; + pixel_color += emissive; + textureStore(view_output, global_id.xy, vec4(pixel_color, 1.0)); +} + +fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, diffuse_brdf: vec3, rng: ptr) -> Reservoir{ + var reservoir = empty_reservoir(); + var reservoir_target_function = 0.0; + for (var i = 0u; i < INITIAL_SAMPLES; i++) { + let light_sample = generate_random_light_sample(rng); + + let mis_weight = 1.0 / f32(INITIAL_SAMPLES); + let light_contribution = calculate_light_contribution(light_sample, world_position, world_normal); + let target_function = luminance(light_contribution.radiance * diffuse_brdf); + let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf); + + reservoir.weight_sum += resampling_weight; + + if rand_f(rng) < resampling_weight / reservoir.weight_sum { + reservoir.sample = light_sample; + reservoir_target_function = target_function; + } + } + + if reservoir_valid(reservoir) { + let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0); + reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function; + reservoir.unbiased_contribution_weight *= trace_light_visibility(reservoir.sample, world_position); + } + + reservoir.confidence_weight = f32(INITIAL_SAMPLES); + return reservoir; +} + +fn reconstruct_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl index 99dbff9d89..aad064590f 100644 --- a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -71,7 +71,7 @@ struct DirectionalLight { @group(0) @binding(9) var light_sources: array; @group(0) @binding(10) var directional_lights: array; -const RAY_T_MIN = 0.0001; +const RAY_T_MIN = 0.01; const RAY_T_MAX = 100000.0; const RAY_NO_CULL = 0xFFu; diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index 4e2c8db33a..06142192b6 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -69,6 +69,7 @@ fn calculate_light_contribution(light_sample: LightSample, ray_origin: vec3 fn calculate_directional_light_contribution(light_sample: LightSample, directional_light_id: u32, origin_world_normal: vec3) -> LightContribution { let directional_light = directional_lights[directional_light_id]; +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS // Sample a random direction within a cone whose base is the sun approximated as a disk // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; @@ -80,6 +81,9 @@ fn calculate_directional_light_contribution(light_sample: LightSample, direction // Rotate the ray so that the cone it was sampled from is aligned with the light direction ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); let radiance = directional_light.luminance * cos_theta_origin; @@ -119,6 +123,7 @@ fn trace_light_visibility(light_sample: LightSample, ray_origin: vec3) -> f fn trace_directional_light_visibility(light_sample: LightSample, directional_light_id: u32, ray_origin: vec3) -> f32 { let directional_light = directional_lights[directional_light_id]; +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS // Sample a random direction within a cone whose base is the sun approximated as a disk // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; @@ -130,6 +135,9 @@ fn trace_directional_light_visibility(light_sample: LightSample, directional_lig // Rotate the ray so that the cone it was sampled from is aligned with the light direction ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT); return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 99d526b1e8..1d356fdc40 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_sprite" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides sprite functionality for Bevy Engine" homepage = "https://bevy.org" @@ -15,21 +15,21 @@ webgpu = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", optional = true } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", optional = true } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index fd780fc486..17f9d6c787 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Finite state machines for Bevy" homepage = "https://bevy.org" @@ -46,12 +46,12 @@ critical-section = [ [dependencies] # bevy -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state_macros = { path = "macros", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state_macros = { path = "macros", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } variadics_please = "1.1" # other diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml index 2f569f395e..2ab531a9b2 100644 --- a/crates/bevy_state/macros/Cargo.toml +++ b/crates/bevy_state/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Macros for bevy_state" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0", features = ["full"] } quote = "1.0" diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index ad162a7ef7..61b07448ed 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_tasks" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A task executor for Bevy Engine" homepage = "https://bevy.org" @@ -47,7 +47,7 @@ web = [ ] [dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_tasks/src/executor.rs b/crates/bevy_tasks/src/executor.rs index 9a9f4f9dfa..01bbe4a669 100644 --- a/crates/bevy_tasks/src/executor.rs +++ b/crates/bevy_tasks/src/executor.rs @@ -65,9 +65,11 @@ impl LocalExecutor<'_> { } impl UnwindSafe for Executor<'_> {} + impl RefUnwindSafe for Executor<'_> {} impl UnwindSafe for LocalExecutor<'_> {} + impl RefUnwindSafe for LocalExecutor<'_> {} impl fmt::Debug for Executor<'_> { diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index ce9aa78883..66899ef36f 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -32,6 +32,7 @@ pub use conditional_send::*; /// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. Wasm), /// futures aren't Send. pub trait ConditionalSendFuture: Future + ConditionalSend {} + impl ConditionalSendFuture for T {} use alloc::boxed::Box; diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs index 48fb3e2861..86d2ab280d 100644 --- a/crates/bevy_tasks/src/thread_executor.rs +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -24,7 +24,7 @@ use futures_lite::Future; /// // we cannot get the ticker from another thread /// let not_thread_ticker = thread_executor.ticker(); /// assert!(not_thread_ticker.is_none()); -/// +/// /// // but we can spawn tasks from another thread /// thread_executor.spawn(async move { /// count_clone.fetch_add(1, Ordering::Relaxed); @@ -98,6 +98,7 @@ pub struct ThreadExecutorTicker<'task, 'ticker> { // make type not send or sync _marker: PhantomData<*const ()>, } + impl<'task, 'ticker> ThreadExecutorTicker<'task, 'ticker> { /// Tick the thread executor. pub async fn tick(&self) { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 9a8c8e8eee..c71fb5ce0c 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_text" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides text functionality for Bevy Engine" homepage = "https://bevy.org" @@ -13,21 +13,21 @@ default_font = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 494725b08c..7c8e840ace 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_time" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides time functionality for Bevy Engine" homepage = "https://bevy.org" @@ -48,10 +48,10 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other crossbeam-channel = { version = "0.5.0", default-features = false, features = [ diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 6801f5734b..6c86be0da6 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_transform" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides transform functionality for Bevy Engine" homepage = "https://bevy.org" @@ -10,13 +10,13 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false, optional = true } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev", default-features = false, optional = true } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, optional = true } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false, optional = true } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev", default-features = false, optional = true } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false, optional = true } serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } @@ -24,8 +24,8 @@ thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } [dev-dependencies] -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "approx", ] } approx = "0.5.1" diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 5224d90296..1f05d7a53b 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ui" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A custom ECS-driven UI framework built specifically for Bevy Engine" homepage = "https://bevy.org" @@ -10,25 +10,25 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 969e062cd7..eb1d255cc7 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -3,6 +3,7 @@ use bevy_color::{Color, Srgba}; use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::prelude::*; +use bevy_utils::default; use core::{f32, f32::consts::TAU}; /// A color stop for a gradient @@ -205,7 +206,7 @@ impl Default for AngularColorStop { /// A linear gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -213,6 +214,8 @@ impl Default for AngularColorStop { reflect(Serialize, Deserialize) )] pub struct LinearGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The direction of the gradient in radians. /// An angle of `0.` points upward, with the value increasing in the clockwise direction. pub angle: f32, @@ -240,7 +243,11 @@ impl LinearGradient { /// Create a new linear gradient pub fn new(angle: f32, stops: Vec) -> Self { - Self { angle, stops } + Self { + angle, + stops, + color_space: InterpolationColorSpace::default(), + } } /// A linear gradient transitioning from bottom to top @@ -248,6 +255,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP, stops, + color_space: InterpolationColorSpace::default(), } } @@ -256,6 +264,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -264,6 +273,7 @@ impl LinearGradient { Self { angle: Self::TO_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -272,6 +282,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_RIGHT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -280,6 +291,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM, stops, + color_space: InterpolationColorSpace::default(), } } @@ -288,6 +300,7 @@ impl LinearGradient { Self { angle: Self::TO_BOTTOM_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -296,6 +309,7 @@ impl LinearGradient { Self { angle: Self::TO_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -304,6 +318,7 @@ impl LinearGradient { Self { angle: Self::TO_TOP_LEFT, stops, + color_space: InterpolationColorSpace::default(), } } @@ -312,8 +327,14 @@ impl LinearGradient { Self { angle: degrees.to_radians(), stops, + color_space: InterpolationColorSpace::default(), } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } /// A radial gradient @@ -327,6 +348,8 @@ impl LinearGradient { reflect(Serialize, Deserialize) )] pub struct RadialGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The center of the radial gradient pub position: UiPosition, /// Defines the end shape of the radial gradient @@ -339,11 +362,17 @@ impl RadialGradient { /// Create a new radial gradient pub fn new(position: UiPosition, shape: RadialGradientShape, stops: Vec) -> Self { Self { + color_space: default(), position, shape, stops, } } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } impl Default for RadialGradient { @@ -352,6 +381,7 @@ impl Default for RadialGradient { position: UiPosition::CENTER, shape: RadialGradientShape::ClosestCorner, stops: Vec::new(), + color_space: default(), } } } @@ -359,7 +389,7 @@ impl Default for RadialGradient { /// A conic gradient /// /// -#[derive(Clone, PartialEq, Debug, Reflect)] +#[derive(Default, Clone, PartialEq, Debug, Reflect)] #[reflect(PartialEq)] #[cfg_attr( feature = "serialize", @@ -367,6 +397,8 @@ impl Default for RadialGradient { reflect(Serialize, Deserialize) )] pub struct ConicGradient { + /// The color space used for interpolation. + pub color_space: InterpolationColorSpace, /// The starting angle of the gradient in radians pub start: f32, /// The center of the conic gradient @@ -379,6 +411,7 @@ impl ConicGradient { /// Create a new conic gradient pub fn new(position: UiPosition, stops: Vec) -> Self { Self { + color_space: default(), start: 0., position, stops, @@ -396,6 +429,11 @@ impl ConicGradient { self.position = position; self } + + pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } } #[derive(Clone, PartialEq, Debug, Reflect)] @@ -573,3 +611,79 @@ impl RadialGradientShape { } } } + +/// The color space used for interpolation. +#[derive(Default, Copy, Clone, Hash, Debug, PartialEq, Eq, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum InterpolationColorSpace { + /// Interpolates in `OKLab` space. + #[default] + OkLab, + /// Interpolates in OKLCH space, taking the shortest hue path. + OkLch, + /// Interpolates in OKLCH space, taking the longest hue path. + OkLchLong, + /// Interpolates in sRGB space. + Srgb, + /// Interpolates in linear sRGB space. + LinearRgb, +} + +/// Set the color space used for interpolation. +pub trait InColorSpace: Sized { + /// Interpolate in the given `color_space`. + fn in_color_space(self, color_space: InterpolationColorSpace) -> Self; + + /// Interpolate in `OKLab` space. + fn in_oklab(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLab) + } + + /// Interpolate in OKLCH space (short hue path). + fn in_oklch(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLch) + } + + /// Interpolate in OKLCH space (long hue path). + fn in_oklch_long(self) -> Self { + self.in_color_space(InterpolationColorSpace::OkLchLong) + } + + /// Interpolate in sRGB space. + fn in_srgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::Srgb) + } + + /// Interpolate in linear sRGB space. + fn in_linear_rgb(self) -> Self { + self.in_color_space(InterpolationColorSpace::LinearRgb) + } +} + +impl InColorSpace for LinearGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for RadialGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} + +impl InColorSpace for ConicGradient { + /// Interpolate in the given `color_space`. + fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + self.color_space = color_space; + self + } +} diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 6659204da4..b50f4cc245 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -2,7 +2,7 @@ use bevy_a11y::AccessibilityNode; use bevy_ecs::{ component::Component, - lifecycle::{Add, Insert, Remove}, + lifecycle::{Add, Remove}, observer::On, world::DeferredWorld, }; @@ -40,21 +40,17 @@ pub(crate) fn on_remove_disabled( #[derive(Component, Default, Debug)] pub struct Pressed; +/// Component that indicates that a widget can be checked. +#[derive(Component, Default, Debug)] +pub struct Checkable; + /// Component that indicates whether a checkbox or radio button is in a checked state. #[derive(Component, Default, Debug)] -#[component(immutable)] -pub struct Checked(pub bool); +pub struct Checked; -impl Checked { - /// Returns whether the checkbox or radio button is currently checked. - pub fn get(&self) -> bool { - self.0 - } -} - -pub(crate) fn on_insert_is_checked(trigger: On, mut world: DeferredWorld) { +pub(crate) fn on_add_checkable(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target()); - let checked = entity.get::().unwrap().get(); + let checked = entity.get::().is_some(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(match checked { true => accesskit::Toggled::True, @@ -63,7 +59,22 @@ pub(crate) fn on_insert_is_checked(trigger: On, mut world: Defe } } -pub(crate) fn on_remove_is_checked(trigger: On, mut world: DeferredWorld) { +pub(crate) fn on_remove_checkable(trigger: On, mut world: DeferredWorld) { + // Remove the 'toggled' attribute entirely. + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_toggled(); + } +} + +pub(crate) fn on_add_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::True); + } +} + +pub(crate) fn on_remove_checked(trigger: On, mut world: DeferredWorld) { let mut entity = world.entity_mut(trigger.target()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_toggled(accesskit::Toggled::False); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ac70897d06..47d396b201 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -39,7 +39,7 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; -pub use interaction_states::{Checked, InteractionDisabled, Pressed}; +pub use interaction_states::{Checkable, Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; @@ -323,8 +323,10 @@ fn build_text_interop(app: &mut App) { app.add_observer(interaction_states::on_add_disabled) .add_observer(interaction_states::on_remove_disabled) - .add_observer(interaction_states::on_insert_is_checked) - .add_observer(interaction_states::on_remove_is_checked); + .add_observer(interaction_states::on_add_checkable) + .add_observer(interaction_states::on_remove_checkable) + .add_observer(interaction_states::on_add_checked) + .add_observer(interaction_states::on_remove_checked); app.configure_sets( PostUpdate, diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index bd818c7d5b..e1c845d481 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -140,6 +140,7 @@ pub fn compute_gradient_line_length(angle: f32, size: Vec2) -> f32 { #[derive(Clone, Copy, Hash, PartialEq, Eq)] pub struct UiGradientPipelineKey { anti_alias: bool, + color_space: InterpolationColorSpace, pub hdr: bool, } @@ -180,10 +181,18 @@ impl SpecializedRenderPipeline for GradientPipeline { VertexFormat::Float32, ], ); + let color_space = match key.color_space { + InterpolationColorSpace::OkLab => "IN_OKLAB", + InterpolationColorSpace::OkLch => "IN_OKLCH", + InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG", + InterpolationColorSpace::Srgb => "IN_SRGB", + InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB", + }; + let shader_defs = if key.anti_alias { - vec!["ANTI_ALIAS".into()] + vec![color_space.into(), "ANTI_ALIAS".into()] } else { - Vec::new() + vec![color_space.into()] }; RenderPipelineDescriptor { @@ -254,6 +263,7 @@ pub struct ExtractedGradient { /// Ordering: left, top, right, bottom. pub border: BorderRect, pub resolved_gradient: ResolvedGradient, + pub color_space: InterpolationColorSpace, } #[derive(Resource, Default)] @@ -422,7 +432,11 @@ pub fn extract_gradients( continue; } match gradient { - Gradient::Linear(LinearGradient { angle, stops }) => { + Gradient::Linear(LinearGradient { + color_space, + angle, + stops, + }) => { let length = compute_gradient_line_length(*angle, uinode.size); let range_start = extracted_color_stops.0.len(); @@ -452,9 +466,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Linear { angle: *angle }, + color_space: *color_space, }); } Gradient::Radial(RadialGradient { + color_space, position: center, shape, stops, @@ -500,9 +516,11 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, resolved_gradient: ResolvedGradient::Radial { center: c, size }, + color_space: *color_space, }); } Gradient::Conic(ConicGradient { + color_space, start, position: center, stops, @@ -557,6 +575,7 @@ pub fn extract_gradients( start: *start, center: g_start, }, + color_space: *color_space, }); } } @@ -601,6 +620,7 @@ pub fn queue_gradient( &gradients_pipeline, UiGradientPipelineKey { anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + color_space: gradient.color_space, hdr: view.hdr, }, ); diff --git a/crates/bevy_ui/src/render/gradient.wgsl b/crates/bevy_ui/src/render/gradient.wgsl index 0223836f2d..074cf35a35 100644 --- a/crates/bevy_ui/src/render/gradient.wgsl +++ b/crates/bevy_ui/src/render/gradient.wgsl @@ -122,6 +122,89 @@ fn mix_linear_rgb_in_srgb_space(a: vec4, b: vec4, t: f32) -> vec4 return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); } +fn linear_rgb_to_oklab(c: vec4) -> vec4 { + let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.); + let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.); + let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.); + return vec4( + 0.21045426 * l + 0.7936178 * m - 0.004072047 * s, + 1.9779985 * l - 2.4285922 * m + 0.4505937 * s, + 0.025904037 * l + 0.78277177 * m - 0.80867577 * s, + c.w + ); +} + +fn oklab_to_linear_rgba(c: vec4) -> vec4 { + let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; + let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; + let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z; + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + return vec4( + 4.0767417 * l - 3.3077116 * m + 0.23096994 * s, + -1.268438 * l + 2.6097574 * m - 0.34131938 * s, + -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s, + c.w + ); +} + +fn mix_linear_rgb_in_oklab_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t)); +} + +/// hue is left in radians and not converted to degrees +fn linear_rgb_to_oklch(c: vec4) -> vec4 { + let o = linear_rgb_to_oklab(c); + let chroma = sqrt(o.y * o.y + o.z * o.z); + let hue = atan2(o.z, o.y); + return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w); +} + +fn oklch_to_linear_rgb(c: vec4) -> vec4 { + let a = c.y * cos(c.z); + let b = c.y * sin(c.z); + return oklab_to_linear_rgba(vec4(c.x, a, b, c.w)); +} + +fn rem_euclid(a: f32, b: f32) -> f32 { + return ((a % b) + b) % b; +} + +fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + diff * t, TAU); +} + +fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU); +} + +fn mix_oklch(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_oklch_long(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue_long(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_linear_rgb_in_oklch_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + +fn mix_linear_rgb_in_oklch_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. // The distance in gradient space is then used to interpolate between the start and end colors. @@ -192,7 +275,16 @@ fn interpolate_gradient( } else { t = 0.5 * (1 + (t - hint) / (1.0 - hint)); } - - // Only color interpolation in SRGB space is supported atm. + +#ifdef IN_SRGB return mix_linear_rgb_in_srgb_space(start_color, end_color, t); +#else ifdef IN_OKLAB + return mix_linear_rgb_in_oklab_space(start_color, end_color, t); +#else ifdef IN_OKLCH + return mix_linear_rgb_in_oklch_space(start_color, end_color, t); +#else ifdef IN_OKLCH_LONG + return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t); +#else + return mix(start_color, end_color, t); +#endif } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index d81f80f8b9..6418f69ff8 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1043,9 +1043,11 @@ pub enum BoxSizing { /// Length styles like width and height refer to the "content box" size (size excluding padding and border) ContentBox, } + impl BoxSizing { pub const DEFAULT: Self = Self::BorderBox; } + impl Default for BoxSizing { fn default() -> Self { Self::DEFAULT diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index aaf6a0834e..4e74e6ea94 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" homepage = "https://bevy.org" @@ -21,7 +21,7 @@ std = ["disqualified/alloc"] debug = [] [dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } disqualified = { version = "1.0", default-features = false } thread_local = { version = "1.0", optional = true } diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 2e3e7edf18..1a5bca6916 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_window" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides windowing functionality for Bevy Engine" homepage = "https://bevy.org" @@ -50,16 +50,16 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", "smol_str", ], optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1.0", features = [ diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 5a320439d7..89f219d269 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -502,38 +502,66 @@ impl AppLifecycle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] pub enum WindowEvent { + /// An application lifecycle event. AppLifecycle(AppLifecycle), + /// The user's cursor has entered a window. CursorEntered(CursorEntered), + ///The user's cursor has left a window. CursorLeft(CursorLeft), + /// The user's cursor has moved inside a window. CursorMoved(CursorMoved), + /// A file drag and drop event. FileDragAndDrop(FileDragAndDrop), + /// An Input Method Editor event. Ime(Ime), + /// A redraw of all of the application's windows has been requested. RequestRedraw(RequestRedraw), + /// The window's OS-reported scale factor has changed. WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + /// The OS has requested that a window be closed. WindowCloseRequested(WindowCloseRequested), + /// A new window has been created. WindowCreated(WindowCreated), + /// A window has been destroyed by the underlying windowing system. WindowDestroyed(WindowDestroyed), + /// A window has received or lost focus. WindowFocused(WindowFocused), + /// A window has been moved. WindowMoved(WindowMoved), + /// A window has started or stopped being occluded. WindowOccluded(WindowOccluded), + /// A window's logical size has changed. WindowResized(WindowResized), + /// A window's scale factor has changed. WindowScaleFactorChanged(WindowScaleFactorChanged), + /// Sent for windows that are using the system theme when the system theme changes. WindowThemeChanged(WindowThemeChanged), + /// The state of a mouse button has changed. MouseButtonInput(MouseButtonInput), + /// The physical position of a pointing device has changed. MouseMotion(MouseMotion), + /// The mouse wheel has moved. MouseWheel(MouseWheel), + /// A two finger pinch gesture. PinchGesture(PinchGesture), + /// A two finger rotation gesture. RotationGesture(RotationGesture), + /// A double tap gesture. DoubleTapGesture(DoubleTapGesture), + /// A pan gesture. PanGesture(PanGesture), + /// A touch input state change. TouchInput(TouchInput), + /// A keyboard input. KeyboardInput(KeyboardInput), + /// Sent when focus has been lost for all Bevy windows. + /// + /// Used to clear pressed key state. KeyboardFocusLost(KeyboardFocusLost), } @@ -542,131 +570,157 @@ impl From for WindowEvent { Self::AppLifecycle(e) } } + impl From for WindowEvent { fn from(e: CursorEntered) -> Self { Self::CursorEntered(e) } } + impl From for WindowEvent { fn from(e: CursorLeft) -> Self { Self::CursorLeft(e) } } + impl From for WindowEvent { fn from(e: CursorMoved) -> Self { Self::CursorMoved(e) } } + impl From for WindowEvent { fn from(e: FileDragAndDrop) -> Self { Self::FileDragAndDrop(e) } } + impl From for WindowEvent { fn from(e: Ime) -> Self { Self::Ime(e) } } + impl From for WindowEvent { fn from(e: RequestRedraw) -> Self { Self::RequestRedraw(e) } } + impl From for WindowEvent { fn from(e: WindowBackendScaleFactorChanged) -> Self { Self::WindowBackendScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowCloseRequested) -> Self { Self::WindowCloseRequested(e) } } + impl From for WindowEvent { fn from(e: WindowCreated) -> Self { Self::WindowCreated(e) } } + impl From for WindowEvent { fn from(e: WindowDestroyed) -> Self { Self::WindowDestroyed(e) } } + impl From for WindowEvent { fn from(e: WindowFocused) -> Self { Self::WindowFocused(e) } } + impl From for WindowEvent { fn from(e: WindowMoved) -> Self { Self::WindowMoved(e) } } + impl From for WindowEvent { fn from(e: WindowOccluded) -> Self { Self::WindowOccluded(e) } } + impl From for WindowEvent { fn from(e: WindowResized) -> Self { Self::WindowResized(e) } } + impl From for WindowEvent { fn from(e: WindowScaleFactorChanged) -> Self { Self::WindowScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowThemeChanged) -> Self { Self::WindowThemeChanged(e) } } + impl From for WindowEvent { fn from(e: MouseButtonInput) -> Self { Self::MouseButtonInput(e) } } + impl From for WindowEvent { fn from(e: MouseMotion) -> Self { Self::MouseMotion(e) } } + impl From for WindowEvent { fn from(e: MouseWheel) -> Self { Self::MouseWheel(e) } } + impl From for WindowEvent { fn from(e: PinchGesture) -> Self { Self::PinchGesture(e) } } + impl From for WindowEvent { fn from(e: RotationGesture) -> Self { Self::RotationGesture(e) } } + impl From for WindowEvent { fn from(e: DoubleTapGesture) -> Self { Self::DoubleTapGesture(e) } } + impl From for WindowEvent { fn from(e: PanGesture) -> Self { Self::PanGesture(e) } } + impl From for WindowEvent { fn from(e: TouchInput) -> Self { Self::TouchInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardInput) -> Self { Self::KeyboardInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardFocusLost) -> Self { Self::KeyboardFocusLost(e) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index e2a9ca3c0f..403801e9d0 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1168,13 +1168,17 @@ pub enum MonitorSelection { /// References an exclusive fullscreen video mode. /// /// Used when setting [`WindowMode::Fullscreen`] on a window. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Clone)] pub enum VideoModeSelection { /// Uses the video mode that the monitor is already in. Current, diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 43db87a1d2..bb28b84fb0 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_winit" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A winit window and input backend for Bevy Engine" homepage = "https://bevy.org" @@ -28,25 +28,25 @@ custom_cursor = ["bevy_image", "bevy_asset", "bytemuck", "wgpu-types"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } # bevy optional -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev", optional = true } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev", optional = true } # other # feature rwh_06 refers to window_raw_handle@v0.6 @@ -68,16 +68,16 @@ wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index 0bab7d7501..88678b90ac 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -128,4 +128,5 @@ fn setup( struct MeshletDebugMaterial { _dummy: (), } + impl Material for MeshletDebugMaterial {} diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs index 47d83a4e76..b0dd376691 100644 --- a/examples/3d/parallax_mapping.rs +++ b/examples/3d/parallax_mapping.rs @@ -53,6 +53,7 @@ impl Default for CurrentMethod { CurrentMethod(ParallaxMappingMethod::Relief { max_steps: 4 }) } } + impl fmt::Display for CurrentMethod { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.0 { @@ -63,6 +64,7 @@ impl fmt::Display for CurrentMethod { } } } + impl CurrentMethod { fn next_method(&mut self) { use ParallaxMappingMethod::*; diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 389272cbb1..895df4d6fd 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -1,28 +1,45 @@ -//! Demonstrates realtime dynamic global illumination rendering using Bevy Solari. +//! Demonstrates realtime dynamic raytraced lighting using Bevy Solari. #[path = "../helpers/camera_controller.rs"] mod camera_controller; +use argh::FromArgs; use bevy::{ prelude::*, render::{camera::CameraMainTextureUsages, mesh::Indices, render_resource::TextureUsages}, scene::SceneInstanceReady, solari::{ - pathtracer::Pathtracer, - prelude::{RaytracingMesh3d, SolariPlugin}, + pathtracer::{Pathtracer, PathtracingPlugin}, + prelude::{RaytracingMesh3d, SolariLighting, SolariPlugin}, }, }; use camera_controller::{CameraController, CameraControllerPlugin}; use std::f32::consts::PI; -fn main() { - App::new() - .add_plugins((DefaultPlugins, SolariPlugin, CameraControllerPlugin)) - .add_systems(Startup, setup) - .run(); +/// `bevy_solari` demo. +#[derive(FromArgs, Resource, Clone, Copy)] +struct Args { + /// use the reference pathtracer instead of the realtime lighting system. + #[argh(switch)] + pathtracer: Option, } -fn setup(mut commands: Commands, asset_server: Res) { +fn main() { + let args: Args = argh::from_env(); + + let mut app = App::new(); + app.add_plugins((DefaultPlugins, SolariPlugin, CameraControllerPlugin)) + .insert_resource(args) + .add_systems(Startup, setup); + + if args.pathtracer == Some(true) { + app.add_plugins(PathtracingPlugin); + } + + app.run(); +} + +fn setup(mut commands: Commands, asset_server: Res, args: Res) { commands .spawn(SceneRoot(asset_server.load( GltfAssetLabel::Scene(0).from_asset("models/CornellBox/CornellBox.glb"), @@ -32,13 +49,13 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( DirectionalLight { illuminance: light_consts::lux::FULL_DAYLIGHT, - shadows_enabled: true, + shadows_enabled: false, // Solari replaces shadow mapping ..default() }, Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, PI * -0.43, PI * -0.08, 0.0)), )); - commands.spawn(( + let mut camera = commands.spawn(( Camera3d::default(), Camera { clear_color: ClearColorConfig::Custom(Color::BLACK), @@ -49,10 +66,16 @@ fn setup(mut commands: Commands, asset_server: Res) { run_speed: 1500.0, ..Default::default() }, - Pathtracer::default(), - CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING), Transform::from_xyz(-278.0, 273.0, 800.0), + // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari + CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING), + Msaa::Off, )); + if args.pathtracer == Some(true) { + camera.insert(Pathtracer::default()); + } else { + camera.insert(SolariLighting::default()); + } } fn add_raytracing_meshes_on_scene_load( @@ -60,11 +83,14 @@ fn add_raytracing_meshes_on_scene_load( children: Query<&Children>, mesh: Query<&Mesh3d>, mut meshes: ResMut>, + mut materials: ResMut>, mut commands: Commands, + args: Res, ) { - // Ensure meshes are bery_solari compatible + // Ensure meshes are bevy_solari compatible for (_, mesh) in meshes.iter_mut() { mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1.id); + mesh.remove_attribute(Mesh::ATTRIBUTE_COLOR.id); mesh.generate_tangents().unwrap(); if let Some(indices) = mesh.indices_mut() { @@ -74,12 +100,21 @@ fn add_raytracing_meshes_on_scene_load( } } + // Add raytracing mesh handles for descendant in children.iter_descendants(trigger.target()) { if let Ok(mesh) = mesh.get(descendant) { commands .entity(descendant) - .insert(RaytracingMesh3d(mesh.0.clone())) - .remove::(); + .insert(RaytracingMesh3d(mesh.0.clone())); + + if args.pathtracer == Some(true) { + commands.entity(descendant).remove::(); + } } } + + // Increase material emissive intensity to make it prettier for the example + for (_, material) in materials.iter_mut() { + material.emissive *= 200.0; + } } diff --git a/examples/README.md b/examples/README.md index bccadd438e..1114802a04 100644 --- a/examples/README.md +++ b/examples/README.md @@ -185,7 +185,7 @@ Example | Description [Shadow Biases](../examples/3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene [Shadow Caster and Receiver](../examples/3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene [Skybox](../examples/3d/skybox.rs) | Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats. -[Solari](../examples/3d/solari.rs) | Demonstrates realtime dynamic global illumination rendering using Bevy Solari. +[Solari](../examples/3d/solari.rs) | Demonstrates realtime dynamic raytraced lighting using Bevy Solari. [Specular Tint](../examples/3d/specular_tint.rs) | Demonstrates specular tints and maps [Spherical Area Lights](../examples/3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior [Split Screen](../examples/3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen" @@ -584,7 +584,7 @@ Example | Description Example | Description --- | --- -[Context Menu](../examples/usages/context_menu.rs) | Example of a context menu +[Context Menu](../examples/usage/context_menu.rs) | Example of a context menu [Cooldown](../examples/usage/cooldown.rs) | Example for cooldown on button clicks ## Window diff --git a/examples/animation/color_animation.rs b/examples/animation/color_animation.rs index df7a764bbf..e2076b96a7 100644 --- a/examples/animation/color_animation.rs +++ b/examples/animation/color_animation.rs @@ -4,10 +4,12 @@ use bevy::{math::VectorSpace, prelude::*}; // We define this trait so we can reuse the same code for multiple color types that may be implemented using curves. trait CurveColor: VectorSpace + Into + Send + Sync + 'static {} + impl + Into + Send + Sync + 'static> CurveColor for T {} // We define this trait so we can reuse the same code for multiple color types that may be implemented using mixing. trait MixedColor: Mix + Into + Send + Sync + 'static {} + impl + Send + Sync + 'static> MixedColor for T {} #[derive(Debug, Component)] diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs index 1ee988b7cf..55e57081ff 100644 --- a/examples/app/log_layers_ecs.rs +++ b/examples/app/log_layers_ecs.rs @@ -63,6 +63,7 @@ fn transfer_log_events( struct CaptureLayer { sender: mpsc::Sender, } + impl Layer for CaptureLayer { fn on_event( &self, diff --git a/examples/camera/2d_screen_shake.rs b/examples/camera/2d_screen_shake.rs index dcdcd68811..d14fdc49ea 100644 --- a/examples/camera/2d_screen_shake.rs +++ b/examples/camera/2d_screen_shake.rs @@ -1,70 +1,216 @@ -//! This example showcases a 2D screen shake using concept in this video: `` +//! This example showcases how to implement 2D screen shake. +//! It follows the GDC talk ["Math for Game Programmers: Juicing Your Cameras With Math"](https://www.youtube.com/watch?v=tu-Qe66AvtY) by Squirrel Eiserloh +//! +//! The key features are: +//! - Camera shake is dependent on a "trauma" value between 0.0 and 1.0. The more trauma, the stronger the shake. +//! - Trauma automatically decays over time. +//! - The camera shake will always only affect the camera `Transform` up to a maximum displacement. +//! - The camera's `Transform` is only affected by the shake for the rendering. The `Transform` stays "normal" for the rest of the game logic. +//! - All displacements are governed by a noise function, guaranteeing that the shake is smooth and continuous. +//! This means that the camera won't jump around wildly. //! //! ## Controls //! -//! | Key Binding | Action | -//! |:-------------|:---------------------| -//! | Space | Trigger screen shake | +//! | Key Binding | Action | +//! |:---------------------------------|:---------------------------| +//! | Space (pressed repeatedly) | Increase camera trauma | -use bevy::{prelude::*, render::camera::SubCameraView, sprite::MeshMaterial2d}; -use rand::{Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; +use bevy::{ + input::common_conditions::input_just_pressed, math::ops::powf, prelude::*, + sprite::MeshMaterial2d, +}; -const CAMERA_DECAY_RATE: f32 = 0.9; // Adjust this for smoother or snappier decay -const TRAUMA_DECAY_SPEED: f32 = 0.5; // How fast trauma decays -const TRAUMA_INCREMENT: f32 = 1.0; // Increment of trauma per frame when holding space +// Before we implement the code, let's quickly introduce the underlying constants. +// They are later encoded in a `CameraShakeConfig` component, but introduced here so we can easily tweak them. +// Try playing around with them and see how the shake behaves! -// screen_shake parameters, maximum addition by frame not actual maximum overall values -const MAX_ANGLE: f32 = 0.5; -const MAX_OFFSET: f32 = 500.0; +/// The trauma decay rate controls how quickly the trauma decays. +/// 0.5 means that a full trauma of 1.0 will decay to 0.0 in 2 seconds. +const TRAUMA_DECAY_PER_SECOND: f32 = 0.5; -#[derive(Component)] -struct Player; +/// The trauma exponent controls how the trauma affects the shake. +/// Camera shakes don't feel punchy when they go up linearly, so we use an exponent of 2.0. +/// The higher the exponent, the more abrupt is the transition between no shake and full shake. +const TRAUMA_EXPONENT: f32 = 2.0; + +/// The maximum angle the camera can rotate on full trauma. +/// 10.0 degrees is a somewhat high but still reasonable shake. Try bigger values for something more silly and wiggly. +const MAX_ANGLE: f32 = 10.0_f32.to_radians(); + +/// The maximum translation the camera will move on full trauma in both the x and y directions. +/// 20.0 px is a low enough displacement to not be distracting. Try higher values for an effect that looks like the camera is wandering around. +const MAX_TRANSLATION: f32 = 20.0; + +/// How much we are traversing the noise function in arbitrary units per second. +/// This dictates how fast the camera shakes. +/// 20.0 is a fairly fast shake. Try lower values for a more dreamy effect. +const NOISE_SPEED: f32 = 20.0; + +/// How much trauma we add per press of the space key. +/// A value of 1.0 would mean that a single press would result in a maximum trauma, i.e. 1.0. +const TRAUMA_PER_PRESS: f32 = 0.4; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, (setup_scene, setup_instructions, setup_camera)) - .add_systems(Update, (screen_shake, trigger_shake_on_space)) + // At the start of the frame, restore the camera's transform to its unshaken state. + .add_systems(PreUpdate, reset_transform) + .add_systems( + Update, + // Increase trauma when the space key is pressed. + increase_trauma.run_if(input_just_pressed(KeyCode::Space)), + ) + // Just before the end of the frame, apply the shake. + // This is ordered so that the transform propagation produces correct values for the global transform, which is used by Bevy's rendering. + .add_systems(PostUpdate, shake_camera.before(TransformSystems::Propagate)) .run(); } +/// Let's start with the core mechanic: how do we shake the camera? +/// This system runs right at the end of the frame, so that we can sneak in the shake effect before rendering kicks in. +fn shake_camera( + camera_shake: Single<(&mut CameraShakeState, &CameraShakeConfig, &mut Transform)>, + time: Res