diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4988610d5b71..500eb34bb98b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.24.1 + uses: crate-ci/typos@v1.24.3 - name: Typos info if: failure() run: | diff --git a/Cargo.toml b/Cargo.toml index 8556fc2d63795..dfd74de1e2d6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1407,7 +1407,8 @@ doc-scrape-examples = true name = "Custom Asset IO" description = "Implements a custom AssetReader" category = "Assets" -wasm = true +# Incompatible with the asset path patching of the example-showcase tool +wasm = false [[example]] name = "embedded_asset" @@ -1429,7 +1430,8 @@ doc-scrape-examples = true name = "Extra asset source" description = "Load an asset from a non-standard asset source" category = "Assets" -wasm = true +# Uses non-standard asset path +wasm = false [[example]] name = "hot_asset_reloading" @@ -2415,6 +2417,17 @@ description = "A shader that shows how to bind and sample multiple textures as a category = "Shaders" wasm = false +[[example]] +name = "storage_buffer" +path = "examples/shader/storage_buffer.rs" +doc-scrape-examples = true + +[package.metadata.example.storage_buffer] +name = "Storage Buffer" +description = "A shader that shows how to bind a storage buffer using a custom material." +category = "Shaders" +wasm = true + [[example]] name = "specialized_mesh_pipeline" path = "examples/shader/specialized_mesh_pipeline.rs" @@ -2904,6 +2917,17 @@ description = "Illustrates how to use 9 Slicing in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_texture_slice_flip_and_tile" +path = "examples/ui/ui_texture_slice_flip_and_tile.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_texture_slice_flip_and_tile] +name = "UI Texture Slice Flipping and Tiling" +description = "Illustrates how to flip and tile images with 9 Slicing in UI" +category = "UI (User Interface)" +wasm = true + [[example]] name = "ui_texture_atlas_slice" path = "examples/ui/ui_texture_atlas_slice.rs" @@ -3395,6 +3419,17 @@ description = "Demonstrates picking sprites and sprite atlases" category = "Picking" wasm = true +[[example]] +name = "animation_masks" +path = "examples/animation/animation_masks.rs" +doc-scrape-examples = true + +[package.metadata.example.animation_masks] +name = "Animation Masks" +description = "Demonstrates animation masks" +category = "Animation" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index a1b21f1254172..cf87b1400e3b2 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -3,22 +3,27 @@ nodes: [ ( clip: None, + mask: 0, weight: 1.0, ), ( clip: None, + mask: 0, weight: 0.5, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation0")), + mask: 0, weight: 1.0, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation1")), + mask: 0, weight: 1.0, ), ( clip: Some(AssetPath("models/animated/Fox.glb#Animation2")), + mask: 0, weight: 1.0, ), ], @@ -32,4 +37,5 @@ ], ), root: 0, + mask_groups: {}, ) \ No newline at end of file diff --git a/assets/shaders/storage_buffer.wgsl b/assets/shaders/storage_buffer.wgsl new file mode 100644 index 0000000000000..c052411e3f198 --- /dev/null +++ b/assets/shaders/storage_buffer.wgsl @@ -0,0 +1,38 @@ +#import bevy_pbr::{ + mesh_functions, + view_transformations::position_world_to_clip +} + +@group(2) @binding(0) var colors: array, 5>; + +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec4, + @location(1) color: vec4, +}; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.clip_position = position_world_to_clip(out.world_position.xyz); + + // We have 5 colors in the storage buffer, but potentially many instances of the mesh, so + // we use the instance index to select a color from the storage buffer. + out.color = colors[vertex.instance_index % 5]; + + return out; +} + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { + return mesh.color; +} \ No newline at end of file diff --git a/assets/textures/fantasy_ui_borders/numbered_slices.png b/assets/textures/fantasy_ui_borders/numbered_slices.png new file mode 100644 index 0000000000000..612c3120ac647 Binary files /dev/null and b/assets/textures/fantasy_ui_borders/numbered_slices.png differ diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs b/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs index 73eb55cfdbf8e..0db614c9aca44 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs @@ -20,7 +20,7 @@ impl<'w> Benchmark<'w> { let mut v = vec![]; for _ in 0..10000 { - world.spawn((TableData(0.0), SparseData(0.0))).id(); + world.spawn((TableData(0.0), SparseData(0.0))); v.push(world.spawn(TableData(0.)).id()); } diff --git a/benches/benches/bevy_ecs/iteration/mod.rs b/benches/benches/bevy_ecs/iteration/mod.rs index baa1bb385bb87..a5dcca441c2fe 100644 --- a/benches/benches/bevy_ecs/iteration/mod.rs +++ b/benches/benches/bevy_ecs/iteration/mod.rs @@ -20,6 +20,7 @@ mod iter_simple_system; mod iter_simple_wide; mod iter_simple_wide_sparse_set; mod par_iter_simple; +mod par_iter_simple_foreach_hybrid; use heavy_compute::*; @@ -135,4 +136,8 @@ fn par_iter_simple(c: &mut Criterion) { b.iter(move || bench.run()); }); } + group.bench_function(format!("hybrid"), |b| { + let mut bench = par_iter_simple_foreach_hybrid::Benchmark::new(); + b.iter(move || bench.run()); + }); } diff --git a/benches/benches/bevy_ecs/iteration/par_iter_simple_foreach_hybrid.rs b/benches/benches/bevy_ecs/iteration/par_iter_simple_foreach_hybrid.rs new file mode 100644 index 0000000000000..e2044c0956287 --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/par_iter_simple_foreach_hybrid.rs @@ -0,0 +1,45 @@ +use bevy_ecs::prelude::*; +use bevy_tasks::{ComputeTaskPool, TaskPool}; +use rand::{prelude::SliceRandom, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +#[derive(Component, Copy, Clone)] +struct TableData(f32); + +#[derive(Component, Copy, Clone)] +#[component(storage = "SparseSet")] +struct SparseData(f32); + +fn deterministic_rand() -> ChaCha8Rng { + ChaCha8Rng::seed_from_u64(42) +} +pub struct Benchmark<'w>(World, QueryState<(&'w mut TableData, &'w SparseData)>); + +impl<'w> Benchmark<'w> { + pub fn new() -> Self { + let mut world = World::new(); + ComputeTaskPool::get_or_init(TaskPool::default); + + let mut v = vec![]; + for _ in 0..100000 { + world.spawn((TableData(0.0), SparseData(0.0))); + v.push(world.spawn(TableData(0.)).id()); + } + + // by shuffling ,randomize the archetype iteration order to significantly deviate from the table order. This maximizes the loss of cache locality during archetype-based iteration. + v.shuffle(&mut deterministic_rand()); + for e in v.into_iter() { + world.entity_mut(e).despawn(); + } + + let query = world.query::<(&mut TableData, &SparseData)>(); + Self(world, query) + } + + #[inline(never)] + pub fn run(&mut self) { + self.1 + .par_iter_mut(&mut self.0) + .for_each(|(mut v1, v2)| v1.0 += v2.0) + } +} diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 4e59ccc8b2875..c51b995b3c20a 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,3 +1,5 @@ +//! Traits and type for interpolating between values. + use crate::util; use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_ecs::world::World; diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aeeed9fcdf631..6f04ca794f958 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -3,15 +3,15 @@ use std::io::{self, Write}; use std::ops::{Index, IndexMut}; -use bevy_asset::io::Reader; -use bevy_asset::{Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; +use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; use bevy_reflect::{Reflect, ReflectSerialize}; +use bevy_utils::HashMap; use petgraph::graph::{DiGraph, NodeIndex}; use ron::de::SpannedError; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::AnimationClip; +use crate::{AnimationClip, AnimationTargetId}; /// A graph structure that describes how animation clips are to be blended /// together. @@ -57,6 +57,28 @@ use crate::AnimationClip; /// their weights will be halved and finally blended with the Idle animation. /// Thus the weight of Run and Walk are effectively half of the weight of Idle. /// +/// Nodes can optionally have a *mask*, a bitfield that restricts the set of +/// animation targets that the node and its descendants affect. Each bit in the +/// mask corresponds to a *mask group*, which is a set of animation targets +/// (bones). An animation target can belong to any number of mask groups within +/// the context of an animation graph. +/// +/// When the appropriate bit is set in a node's mask, neither the node nor its +/// descendants will animate any animation targets belonging to that mask group. +/// That is, setting a mask bit to 1 *disables* the animation targets in that +/// group. If an animation target belongs to multiple mask groups, masking any +/// one of the mask groups that it belongs to will mask that animation target. +/// (Thus an animation target will only be animated if *all* of its mask groups +/// are unmasked.) +/// +/// A common use of masks is to allow characters to hold objects. For this, the +/// typical workflow is to assign each character's hand to a mask group. Then, +/// when the character picks up an object, the application masks out the hand +/// that the object is held in for the character's animation set, then positions +/// the hand's digits as necessary to grasp the object. The character's +/// animations will continue to play but will not affect the hand, which will +/// continue to be depicted as holding the object. +/// /// Animation graphs are assets and can be serialized to and loaded from [RON] /// files. Canonically, such files have an `.animgraph.ron` extension. /// @@ -72,8 +94,20 @@ use crate::AnimationClip; pub struct AnimationGraph { /// The `petgraph` data structure that defines the animation graph. pub graph: AnimationDiGraph, + /// The index of the root node in the animation graph. pub root: NodeIndex, + + /// The mask groups that each animation target (bone) belongs to. + /// + /// Each value in this map is a bitfield, in which 0 in bit position N + /// indicates that the animation target doesn't belong to mask group N, and + /// a 1 in position N indicates that the animation target does belong to + /// mask group N. + /// + /// Animation targets not in this collection are treated as though they + /// don't belong to any mask groups. + pub mask_groups: HashMap, } /// A type alias for the `petgraph` data structure that defines the animation @@ -99,6 +133,14 @@ pub struct AnimationGraphNode { /// Otherwise, this node is a *blend node*. pub clip: Option>, + /// A bitfield specifying the mask groups that this node and its descendants + /// will not affect. + /// + /// A 0 in bit N indicates that this node and its descendants *can* animate + /// animation targets in mask group N, while a 1 in bit N indicates that + /// this node and its descendants *cannot* animate mask group N. + pub mask: AnimationMask, + /// The weight of this node. /// /// Weights are propagated down to descendants. Thus if an animation clip @@ -145,6 +187,8 @@ pub struct SerializedAnimationGraph { pub graph: DiGraph, /// Corresponds to the `root` field on [`AnimationGraph`]. pub root: NodeIndex, + /// Corresponds to the `mask_groups` field on [`AnimationGraph`]. + pub mask_groups: HashMap, } /// A version of [`AnimationGraphNode`] suitable for serializing as an asset. @@ -154,6 +198,8 @@ pub struct SerializedAnimationGraph { pub struct SerializedAnimationGraphNode { /// Corresponds to the `clip` field on [`AnimationGraphNode`]. pub clip: Option, + /// Corresponds to the `mask` field on [`AnimationGraphNode`]. + pub mask: AnimationMask, /// Corresponds to the `weight` field on [`AnimationGraphNode`]. pub weight: f32, } @@ -173,12 +219,24 @@ pub enum SerializedAnimationClip { AssetId(AssetId), } +/// The type of an animation mask bitfield. +/// +/// Bit N corresponds to mask group N. +/// +/// Because this is a 64-bit value, there is currently a limitation of 64 mask +/// groups per animation graph. +pub type AnimationMask = u64; + impl AnimationGraph { /// Creates a new animation graph with a root node and no other nodes. pub fn new() -> Self { let mut graph = DiGraph::default(); let root = graph.add_node(AnimationGraphNode::default()); - Self { graph, root } + Self { + graph, + root, + mask_groups: HashMap::new(), + } } /// A convenience function for creating an [`AnimationGraph`] from a single @@ -212,7 +270,8 @@ impl AnimationGraph { /// Adds an [`AnimationClip`] to the animation graph with the given weight /// and returns its index. /// - /// The animation clip will be the child of the given parent. + /// The animation clip will be the child of the given parent. The resulting + /// node will have no mask. pub fn add_clip( &mut self, clip: Handle, @@ -221,6 +280,27 @@ impl AnimationGraph { ) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { clip: Some(clip), + mask: 0, + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds an [`AnimationClip`] to the animation graph with the given weight + /// and mask, and returns its index. + /// + /// The animation clip will be the child of the given parent. + pub fn add_clip_with_mask( + &mut self, + clip: Handle, + mask: AnimationMask, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: Some(clip), + mask, weight, }); self.graph.add_edge(parent, node_index, ()); @@ -254,11 +334,37 @@ impl AnimationGraph { /// /// The blend node will be placed under the supplied `parent` node. During /// animation evaluation, the descendants of this blend node will have their - /// weights multiplied by the weight of the blend. + /// weights multiplied by the weight of the blend. The blend node will have + /// no mask. pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex { - let node_index = self - .graph - .add_node(AnimationGraphNode { clip: None, weight }); + let node_index = self.graph.add_node(AnimationGraphNode { + clip: None, + mask: 0, + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds a blend node to the animation graph with the given weight and + /// returns its index. + /// + /// The blend node will be placed under the supplied `parent` node. During + /// animation evaluation, the descendants of this blend node will have their + /// weights multiplied by the weight of the blend. Neither this node nor its + /// descendants will affect animation targets that belong to mask groups not + /// in the given `mask`. + pub fn add_blend_with_mask( + &mut self, + mask: AnimationMask, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + clip: None, + mask, + weight, + }); self.graph.add_edge(parent, node_index, ()); node_index } @@ -314,6 +420,55 @@ impl AnimationGraph { let mut ron_serializer = ron::ser::Serializer::new(writer, None)?; Ok(self.serialize(&mut ron_serializer)?) } + + /// Adds an animation target (bone) to the mask group with the given ID. + /// + /// Calling this method multiple times with the same animation target but + /// different mask groups will result in that target being added to all of + /// the specified groups. + pub fn add_target_to_mask_group(&mut self, target: AnimationTargetId, mask_group: u32) { + *self.mask_groups.entry(target).or_default() |= 1 << mask_group; + } +} + +impl AnimationGraphNode { + /// Masks out the mask groups specified by the given `mask` bitfield. + /// + /// A 1 in bit position N causes this function to mask out mask group N, and + /// thus neither this node nor its descendants will animate any animation + /// targets that belong to group N. + pub fn add_mask(&mut self, mask: AnimationMask) -> &mut Self { + self.mask |= mask; + self + } + + /// Unmasks the mask groups specified by the given `mask` bitfield. + /// + /// A 1 in bit position N causes this function to unmask mask group N, and + /// thus this node and its descendants will be allowed to animate animation + /// targets that belong to group N, unless another mask masks those targets + /// out. + pub fn remove_mask(&mut self, mask: AnimationMask) -> &mut Self { + self.mask &= !mask; + self + } + + /// Masks out the single mask group specified by `group`. + /// + /// After calling this function, neither this node nor its descendants will + /// animate any animation targets that belong to the given `group`. + pub fn add_mask_group(&mut self, group: u32) -> &mut Self { + self.add_mask(1 << group) + } + + /// Unmasks the single mask group specified by `group`. + /// + /// After calling this function, this node and its descendants will be + /// allowed to animate animation targets that belong to the given `group`, + /// unless another mask masks those targets out. + pub fn remove_mask_group(&mut self, group: u32) -> &mut Self { + self.remove_mask(1 << group) + } } impl Index for AnimationGraph { @@ -334,6 +489,7 @@ impl Default for AnimationGraphNode { fn default() -> Self { Self { clip: None, + mask: 0, weight: 1.0, } } @@ -378,11 +534,13 @@ impl AssetLoader for AnimationGraphAssetLoader { load_context.load(asset_path) } }), + mask: serialized_node.mask, weight: serialized_node.weight, }, |_, _| (), ), root: serialized_animation_graph.root, + mask_groups: serialized_animation_graph.mask_groups, }) } @@ -400,6 +558,7 @@ impl From for SerializedAnimationGraph { graph: animation_graph.graph.map( |_, node| SerializedAnimationGraphNode { weight: node.weight, + mask: node.mask, clip: node.clip.as_ref().map(|clip| match clip.path() { Some(path) => SerializedAnimationClip::AssetPath(path.clone()), None => SerializedAnimationClip::AssetId(clip.id()), @@ -408,6 +567,7 @@ impl From for SerializedAnimationGraph { |_, _| (), ), root: animation_graph.root, + mask_groups: animation_graph.mask_groups, } } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index bf2904440ce5b..7be1b6d88ed00 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -7,9 +7,9 @@ //! Animation for the game engine Bevy -mod animatable; -mod graph; -mod transition; +pub mod animatable; +pub mod graph; +pub mod transition; mod util; use std::cell::RefCell; @@ -21,28 +21,27 @@ use std::ops::{Add, Mul}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; use bevy_core::Name; -use bevy_ecs::entity::MapEntities; -use bevy_ecs::prelude::*; -use bevy_ecs::reflect::ReflectMapEntities; +use bevy_ecs::{entity::MapEntities, prelude::*, reflect::ReflectMapEntities}; use bevy_math::{FloatExt, Quat, Vec3}; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; -use bevy_utils::hashbrown::HashMap; use bevy_utils::{ + hashbrown::HashMap, tracing::{error, trace}, NoOpHash, }; use fixedbitset::FixedBitSet; -use graph::{AnimationGraph, AnimationNodeIndex}; -use petgraph::graph::NodeIndex; -use petgraph::Direction; -use prelude::{AnimationGraphAssetLoader, AnimationTransitions}; +use graph::AnimationMask; +use petgraph::{graph::NodeIndex, Direction}; +use serde::{Deserialize, Serialize}; use thread_local::ThreadLocal; use uuid::Uuid; -#[allow(missing_docs)] +/// The animation prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -51,7 +50,10 @@ pub mod prelude { }; } -use crate::transition::{advance_transitions, expire_completed_transitions}; +use crate::{ + graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, + transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, +}; /// The [UUID namespace] of animation targets (e.g. bones). /// @@ -228,7 +230,7 @@ pub type AnimationCurves = HashMap, NoOpHa /// connected to a bone named `Stomach`. /// /// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug, Serialize, Deserialize)] pub struct AnimationTargetId(pub Uuid); impl Hash for AnimationTargetId { @@ -356,6 +358,9 @@ pub struct ActiveAnimation { /// The actual weight of this animation this frame, taking the /// [`AnimationGraph`] into account. computed_weight: f32, + /// The mask groups that are masked out (i.e. won't be animated) this frame, + /// taking the `AnimationGraph` into account. + computed_mask: AnimationMask, repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -377,6 +382,7 @@ impl Default for ActiveAnimation { Self { weight: 1.0, computed_weight: 1.0, + computed_mask: 0, repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, @@ -575,8 +581,19 @@ pub struct AnimationGraphEvaluator { dfs_stack: Vec, /// The list of visited nodes during the depth-first traversal. dfs_visited: FixedBitSet, - /// Accumulated weights for each node. - weights: Vec, + /// Accumulated weights and masks for each node. + nodes: Vec, +} + +/// The accumulated weight and computed mask for a single node. +#[derive(Clone, Copy, Default, Debug)] +struct EvaluatedAnimationGraphNode { + /// The weight that has been accumulated for this node, taking its + /// ancestors' weights into account. + weight: f32, + /// The mask that has been computed for this node, taking its ancestors' + /// masks into account. + mask: AnimationMask, } thread_local! { @@ -764,15 +781,17 @@ pub fn advance_animations( let node = &animation_graph[node_index]; - // Calculate weight from the graph. - let mut weight = node.weight; + // Calculate weight and mask from the graph. + let (mut weight, mut mask) = (node.weight, node.mask); for parent_index in animation_graph .graph .neighbors_directed(node_index, Direction::Incoming) { - weight *= animation_graph[parent_index].weight; + let evaluated_parent = &evaluator.nodes[parent_index.index()]; + weight *= evaluated_parent.weight; + mask |= evaluated_parent.mask; } - evaluator.weights[node_index.index()] = weight; + evaluator.nodes[node_index.index()] = EvaluatedAnimationGraphNode { weight, mask }; if let Some(active_animation) = active_animations.get_mut(&node_index) { // Tick the animation if necessary. @@ -789,9 +808,10 @@ pub fn advance_animations( weight *= blend_weight; } - // Write in the computed weight. + // Write in the computed weight and mask for this node. if let Some(active_animation) = active_animations.get_mut(&node_index) { active_animation.computed_weight = weight; + active_animation.computed_mask = mask; } // Push children. @@ -850,6 +870,13 @@ pub fn animate_targets( morph_weights, }; + // Determine which mask groups this animation target belongs to. + let target_mask = animation_graph + .mask_groups + .get(&target.id) + .cloned() + .unwrap_or_default(); + // Apply the animations one after another. The way we accumulate // weights ensures that the order we apply them in doesn't matter. // @@ -870,7 +897,11 @@ pub fn animate_targets( for (&animation_graph_node_index, active_animation) in animation_player.active_animations.iter() { - if active_animation.weight == 0.0 { + // If the weight is zero or the current animation target is + // masked out, stop here. + if active_animation.weight == 0.0 + || (target_mask & active_animation.computed_mask) != 0 + { continue; } @@ -1205,7 +1236,7 @@ impl Plugin for AnimationPlugin { ( advance_transitions, advance_animations, - animate_targets, + animate_targets.after(bevy_render::mesh::morph::inherit_weights), expire_completed_transitions, ) .chain() @@ -1256,8 +1287,9 @@ impl AnimationGraphEvaluator { self.dfs_visited.grow(node_count); self.dfs_visited.clear(); - self.weights.clear(); - self.weights.extend(iter::repeat(0.0).take(node_count)); + self.nodes.clear(); + self.nodes + .extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count)); } } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 07a29800f93df..da41efbc152c4 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -18,7 +18,7 @@ use std::{ process::{ExitCode, Termination}, }; use std::{ - num::NonZeroU8, + num::NonZero, panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, }; use thiserror::Error; @@ -1061,14 +1061,14 @@ pub enum AppExit { Success, /// The [`App`] experienced an unhandleable error. /// Holds the exit code we expect our app to return. - Error(NonZeroU8), + Error(NonZero), } impl AppExit { /// Creates a [`AppExit::Error`] with a error code of 1. #[must_use] pub const fn error() -> Self { - Self::Error(NonZeroU8::MIN) + Self::Error(NonZero::::MIN) } /// Returns `true` if `self` is a [`AppExit::Success`]. @@ -1089,7 +1089,7 @@ impl AppExit { /// [`AppExit::Error`] is constructed. #[must_use] pub const fn from_code(code: u8) -> Self { - match NonZeroU8::new(code) { + match NonZero::::new(code) { Some(code) => Self::Error(code), None => Self::Success, } diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index b389395d3dee7..8ea7c0e767566 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -27,7 +27,9 @@ pub use sub_app::*; #[cfg(not(target_arch = "wasm32"))] pub use terminal_ctrl_c_handler::*; -#[allow(missing_docs)] +/// The app prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_app/src/terminal_ctrl_c_handler.rs b/crates/bevy_app/src/terminal_ctrl_c_handler.rs index 54e0bc5338ee2..8bd90ffaa62b3 100644 --- a/crates/bevy_app/src/terminal_ctrl_c_handler.rs +++ b/crates/bevy_app/src/terminal_ctrl_c_handler.rs @@ -48,7 +48,7 @@ impl TerminalCtrlCHandlerPlugin { } /// Sends a [`AppExit`] event when the user presses `Ctrl+C` on the terminal. - fn exit_on_flag(mut events: EventWriter) { + pub fn exit_on_flag(mut events: EventWriter) { if SHOULD_EXIT.load(Ordering::Relaxed) { events.send(AppExit::from_code(130)); } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 0b95ab505bae7..b2348d65094d3 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -29,6 +29,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } stackfuture = "0.3" +atomicow = "1.0" async-broadcast = "0.5" async-fs = "2.0" async-lock = "3.0" diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index a979a3327791e..ec1947a3fee1e 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -2,9 +2,10 @@ use crate::{ io::{processor_gated::ProcessorGatedReader, AssetSourceEvent, AssetWatcher}, processor::AssetProcessorData, }; +use atomicow::CowArc; use bevy_ecs::system::Resource; use bevy_utils::tracing::{error, warn}; -use bevy_utils::{CowArc, Duration, HashMap}; +use bevy_utils::{Duration, HashMap}; use std::{fmt::Display, hash::Hash, sync::Arc}; use thiserror::Error; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 6c790b2b47d97..f7787fb809719 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -12,6 +12,9 @@ pub mod processor; pub mod saver; pub mod transformer; +/// The asset prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -222,7 +225,11 @@ impl Plugin for AssetPlugin { .init_asset::<()>() .add_event::() .configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events)) - .add_systems(PreUpdate, handle_internal_asset_events) + // `handle_internal_asset_events` requires the use of `&mut World`, + // and as a result has ambiguous system ordering with all other systems in `PreUpdate`. + // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it + // needs to be robust to stochastic delays anyways. + .add_systems(PreUpdate, handle_internal_asset_events.ambiguous_with_all()) .register_type::(); } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 1b444bba8c19b..f0dce3593da40 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -6,8 +6,9 @@ use crate::{ Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, UntypedHandle, }; +use atomicow::CowArc; use bevy_ecs::world::World; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap, HashSet}; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap, HashSet}; use downcast_rs::{impl_downcast, Downcast}; use ron::error::SpannedError; use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index dc7719a25f9ad..67c7c65286fac 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -1,6 +1,6 @@ use crate::io::AssetSourceId; +use atomicow::CowArc; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_utils::CowArc; use serde::{de::Visitor, Deserialize, Serialize}; use std::{ fmt::{Debug, Display}, diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index 36408dd125f29..4d5925dc5492c 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -1,7 +1,8 @@ use crate::transformer::TransformedAsset; use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset}; use crate::{AssetLoader, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, CowArc, HashMap}; +use atomicow::CowArc; +use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap}; use serde::{Deserialize, Serialize}; use std::{borrow::Borrow, hash::Hash, ops::Deref}; diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index ef72d2b404295..a9e5fcddbf31c 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -17,10 +17,11 @@ use crate::{ DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle, }; +use atomicow::CowArc; use bevy_ecs::prelude::*; use bevy_tasks::IoTaskPool; use bevy_utils::tracing::{error, info}; -use bevy_utils::{CowArc, HashSet}; +use bevy_utils::HashSet; use crossbeam_channel::{Receiver, Sender}; use futures_lite::{FutureExt, StreamExt}; use info::*; @@ -414,7 +415,7 @@ impl AssetServer { ) -> Handle { let path = path.into().into_owned(); let untyped_source = AssetSourceId::Name(match path.source() { - AssetSourceId::Default => CowArc::Borrowed(UNTYPED_SOURCE_SUFFIX), + AssetSourceId::Default => CowArc::Static(UNTYPED_SOURCE_SUFFIX), AssetSourceId::Name(source) => { CowArc::Owned(format!("{source}--{UNTYPED_SOURCE_SUFFIX}").into()) } diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index 0ffddc4658a43..1b2cd92991c15 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -1,5 +1,6 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle}; -use bevy_utils::{ConditionalSendFuture, CowArc, HashMap}; +use atomicow::CowArc; +use bevy_utils::{ConditionalSendFuture, HashMap}; use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 1d5df733a00e5..65d7b4e6bce5b 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -33,7 +33,9 @@ mod audio_source; mod pitch; mod sinks; -#[allow(missing_docs)] +/// The audio prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs new file mode 100644 index 0000000000000..60920d6ca3576 --- /dev/null +++ b/crates/bevy_color/src/color_gradient.rs @@ -0,0 +1,102 @@ +use crate::Mix; +use bevy_math::curve::{ + cores::{EvenCore, EvenCoreError}, + Curve, Interval, +}; + +/// A curve whose samples are defined by a collection of colors. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct ColorCurve { + core: EvenCore, +} + +impl ColorCurve +where + T: Mix + Clone, +{ + /// Create a new [`ColorCurve`] from a collection of [mixable] types. The domain of this curve + /// will always be `[0.0, len - 1]` where `len` is the amount of mixable objects in the + /// collection. + /// + /// This fails if there's not at least two mixable things in the collection. + /// + /// [mixable]: `Mix` + /// + /// # Example + /// + /// ``` + /// # use bevy_color::palettes::basic::*; + /// # use bevy_color::Mix; + /// # use bevy_color::Srgba; + /// # use bevy_color::ColorCurve; + /// # use bevy_math::curve::Interval; + /// # use bevy_math::curve::Curve; + /// let broken = ColorCurve::new([RED]); + /// assert!(broken.is_err()); + /// let gradient = ColorCurve::new([RED, GREEN, BLUE]); + /// assert!(gradient.is_ok()); + /// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap()); + /// ``` + pub fn new(colors: impl IntoIterator) -> Result { + let colors = colors.into_iter().collect::>(); + Interval::new(0.0, colors.len().saturating_sub(1) as f32) + .map_err(|_| EvenCoreError::NotEnoughSamples { + samples: colors.len(), + }) + .and_then(|domain| EvenCore::new(domain, colors)) + .map(|core| Self { core }) + } +} + +impl Curve for ColorCurve +where + T: Mix + Clone, +{ + fn domain(&self) -> Interval { + self.core.domain() + } + + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, T::mix) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::palettes::basic; + use crate::Srgba; + + #[test] + fn test_color_curve() { + let broken = ColorCurve::new([basic::RED]); + assert!(broken.is_err()); + + let gradient = [basic::RED, basic::LIME, basic::BLUE]; + let curve = ColorCurve::new(gradient).unwrap(); + + assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap()); + + let brighter_curve = curve.map(|c: Srgba| c.mix(&basic::WHITE, 0.5)); + + [ + (-0.1, None), + (0.0, Some([1.0, 0.5, 0.5, 1.0])), + (0.5, Some([0.75, 0.75, 0.5, 1.0])), + (1.0, Some([0.5, 1.0, 0.5, 1.0])), + (1.5, Some([0.5, 0.75, 0.75, 1.0])), + (2.0, Some([0.5, 0.5, 1.0, 1.0])), + (2.1, None), + ] + .map(|(t, maybe_rgba)| { + let maybe_srgba = maybe_rgba.map(|[r, g, b, a]| Srgba::new(r, g, b, a)); + (t, maybe_srgba) + }) + .into_iter() + .for_each(|(t, maybe_color)| { + assert_eq!(brighter_curve.sample(t), maybe_color); + }); + } +} diff --git a/crates/bevy_color/src/color_range.rs b/crates/bevy_color/src/color_range.rs index 16d6f04866670..72260b27f4a7e 100644 --- a/crates/bevy_color/src/color_range.rs +++ b/crates/bevy_color/src/color_range.rs @@ -15,7 +15,7 @@ pub trait ColorRange { impl ColorRange for Range { fn at(&self, factor: f32) -> T { - self.start.mix(&self.end, factor) + self.start.mix(&self.end, factor.clamp(0.0, 1.0)) } } @@ -28,16 +28,20 @@ mod tests { #[test] fn test_color_range() { let range = basic::RED..basic::BLUE; + assert_eq!(range.at(-0.5), basic::RED); assert_eq!(range.at(0.0), basic::RED); assert_eq!(range.at(0.5), Srgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), basic::BLUE); + assert_eq!(range.at(1.5), basic::BLUE); let lred: LinearRgba = basic::RED.into(); let lblue: LinearRgba = basic::BLUE.into(); let range = lred..lblue; + assert_eq!(range.at(-0.5), lred); assert_eq!(range.at(0.0), lred); assert_eq!(range.at(0.5), LinearRgba::new(0.5, 0.0, 0.5, 1.0)); assert_eq!(range.at(1.0), lblue); + assert_eq!(range.at(1.5), lblue); } } diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 95c51a494db14..344a159695935 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -92,6 +92,7 @@ mod color; pub mod color_difference; +mod color_gradient; mod color_ops; mod color_range; mod hsla; @@ -110,7 +111,9 @@ mod test_colors; mod testing; mod xyza; -/// Commonly used color types and traits. +/// The color prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { pub use crate::color::*; pub use crate::color_ops::*; @@ -127,6 +130,7 @@ pub mod prelude { } pub use color::*; +pub use color_gradient::*; pub use color_ops::*; pub use color_range::*; pub use hsla::*; diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index df41f46c46df6..94a8b3a541f88 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -16,8 +16,10 @@ use bevy_ecs::system::Resource; pub use name::*; pub use task_pool_options::*; +/// The core prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - //! The Bevy Core Prelude. #[doc(hidden)] pub use crate::{ FrameCountPlugin, Name, NameOrEntity, TaskPoolOptions, TaskPoolPlugin, diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index eacff931c7211..937e18f410485 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -10,7 +10,7 @@ use bevy_render::{ texture::Image, view::ViewUniform, }; -use std::num::NonZeroU64; +use std::num::NonZero; #[derive(Resource)] pub struct AutoExposurePipeline { @@ -64,8 +64,8 @@ impl FromWorld for AutoExposurePipeline { texture_2d(TextureSampleType::Float { filterable: false }), texture_1d(TextureSampleType::Float { filterable: false }), uniform_buffer::(false), - storage_buffer_sized(false, NonZeroU64::new(HISTOGRAM_BIN_COUNT * 4)), - storage_buffer_sized(false, NonZeroU64::new(4)), + storage_buffer_sized(false, NonZero::::new(HISTOGRAM_BIN_COUNT * 4)), + storage_buffer_sized(false, NonZero::::new(4)), storage_buffer::(true), ), ), diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 568f2cf3f1824..ec77241466b74 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -41,6 +41,9 @@ pub mod experimental { } } +/// The core pipeline prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 59949fa0bf94f..d6d5c6d4bed63 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -6,7 +6,7 @@ mod systems; pub use self::config::*; use bevy_app::prelude::*; -use bevy_ecs::schedule::IntoSystemConfigs; +use bevy_ecs::prelude::*; use bevy_render::view::screenshot::trigger_screenshots; use bevy_time::TimeUpdateStrategy; use std::time::Duration; @@ -54,7 +54,19 @@ impl Plugin for CiTestingPlugin { Update, systems::send_events .before(trigger_screenshots) - .before(bevy_window::close_when_requested), + .before(bevy_window::close_when_requested) + .in_set(SendEvents), ); + + // The offending system does not exist in the wasm32 target. + // As a result, we must conditionally order the two systems using a system set. + #[cfg(not(target_arch = "wasm32"))] + app.configure_sets( + Update, + SendEvents.before(bevy_app::TerminalCtrlCHandlerPlugin::exit_on_flag), + ); } } + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +struct SendEvents; diff --git a/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs b/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs index 86be2146c73d7..92522f6fbe68f 100644 --- a/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs +++ b/crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs @@ -137,7 +137,7 @@ impl<'w, 's> InsetGizmo<'w, 's> { let Ok(cam) = self.cam.get_single() else { return Vec2::ZERO; }; - if let Some(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) { + if let Ok(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) { position = new_position; }; position.xy() diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index ed5cc22c247a7..f33659ca148b1 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -423,6 +423,56 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { let state_struct_visibility = &ast.vis; let state_struct_name = ensure_no_collision(format_ident!("FetchState"), token_stream); + let mut builder_name = None; + for meta in ast + .attrs + .iter() + .filter(|a| a.path().is_ident("system_param")) + { + if let Err(e) = meta.parse_nested_meta(|nested| { + if nested.path.is_ident("builder") { + builder_name = Some(format_ident!("{struct_name}Builder")); + Ok(()) + } else { + Err(nested.error("Unsupported attribute")) + } + }) { + return e.into_compile_error().into(); + } + } + + let builder = builder_name.map(|builder_name| { + let builder_type_parameters: Vec<_> = (0..fields.len()).map(|i| format_ident!("B{i}")).collect(); + let builder_doc_comment = format!("A [`SystemParamBuilder`] for a [`{struct_name}`]."); + let builder_struct = quote! { + #[doc = #builder_doc_comment] + struct #builder_name<#(#builder_type_parameters,)*> { + #(#fields: #builder_type_parameters,)* + } + }; + let lifetimes: Vec<_> = generics.lifetimes().collect(); + let generic_struct = quote!{ #struct_name <#(#lifetimes,)* #punctuated_generic_idents> }; + let builder_impl = quote!{ + // SAFETY: This delegates to the `SystemParamBuilder` for tuples. + unsafe impl< + #(#lifetimes,)* + #(#builder_type_parameters: #path::system::SystemParamBuilder<#field_types>,)* + #punctuated_generics + > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> + #where_clause + { + fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + let #builder_name { #(#fields: #field_locals,)* } = self; + #state_struct_name { + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + } + } + } + }; + (builder_struct, builder_impl) + }); + let (builder_struct, builder_impl) = builder.unzip(); + TokenStream::from(quote! { // We define the FetchState struct in an anonymous scope to avoid polluting the user namespace. // The struct can still be accessed via SystemParam::State, e.g. EventReaderState can be accessed via @@ -479,7 +529,11 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { // Safety: Each field is `ReadOnlySystemParam`, so this can only read from the `World` unsafe impl<'w, 's, #punctuated_generics> #path::system::ReadOnlySystemParam for #struct_name #ty_generics #read_only_where_clause {} + + #builder_impl }; + + #builder_struct }) } diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index acec7548e9845..9480bfbadf42a 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -250,9 +250,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { user_where_clauses_with_world, ); let read_only_structs = quote! { - #[doc = "Automatically generated [`WorldQuery`] type for a read-only variant of [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`]."] + #[doc = concat!( + "Automatically generated [`WorldQuery`](", + stringify!(#path), + "::query::WorldQuery) type for a read-only variant of [`", + stringify!(#struct_name), + "`]." + )] #[automatically_derived] #visibility struct #read_only_struct_name #user_impl_generics #user_where_clauses { #( @@ -331,9 +335,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { const _: () = { #[doc(hidden)] - #[doc = "Automatically generated internal [`WorldQuery`] state type for [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`], used for caching."] + #[doc = concat!( + "Automatically generated internal [`WorldQuery`](", + stringify!(#path), + "::query::WorldQuery) state type for [`", + stringify!(#struct_name), + "`], used for caching." + )] #[automatically_derived] #visibility struct #state_struct_name #user_impl_generics #user_where_clauses { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::State,)* diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index ff056857df41f..2a2916905ae20 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -145,9 +145,13 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { const _: () = { #[doc(hidden)] - #[doc = "Automatically generated internal [`WorldQuery`] state type for [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`], used for caching."] + #[doc = concat!( + "Automatically generated internal [`WorldQuery`](", + stringify!(#path), + "::query::WorldQuery) state type for [`", + stringify!(#struct_name), + "`], used for caching." + )] #[automatically_derived] #visibility struct #state_struct_name #user_impl_generics #user_where_clauses { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::State,)* diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 26cb8edadd8f1..9af4e2b3a148a 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -19,12 +19,16 @@ pub(crate) fn item_struct( user_ty_generics_with_world: &TypeGenerics, user_where_clauses_with_world: Option<&WhereClause>, ) -> proc_macro2::TokenStream { - let item_attrs = quote!( - #[doc = "Automatically generated [`WorldQuery`](#path::query::WorldQuery) item type for [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`], returned when iterating over query results."] - #[automatically_derived] - ); + let item_attrs = quote! { + #[doc = concat!( + "Automatically generated [`WorldQuery`](", + stringify!(#path), + "::query::WorldQuery) item type for [`", + stringify!(#struct_name), + "`], returned when iterating over query results." + )] + #[automatically_derived] + }; match fields { Fields::Named(_) => quote! { @@ -69,9 +73,13 @@ pub(crate) fn world_query_impl( ) -> proc_macro2::TokenStream { quote! { #[doc(hidden)] - #[doc = "Automatically generated internal [`WorldQuery`] fetch type for [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`], used to define the world data accessed by this query."] + #[doc = concat!( + "Automatically generated internal [`WorldQuery`](", + stringify!(#path), + "::query::WorldQuery) fetch type for [`", + stringify!(#struct_name), + "`], used to define the world data accessed by this query." + )] #[automatically_derived] #visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)* diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index b7cad4e389512..15e962291cbd3 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -367,7 +367,7 @@ pub struct Archetype { edges: Edges, entities: Vec, components: ImmutableSparseSet, - flags: ArchetypeFlags, + pub(crate) flags: ArchetypeFlags, } impl Archetype { diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index deee320fcd0b2..93b1e78ffabe3 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -59,7 +59,7 @@ use crate::{ }; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -use std::{fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering}; +use std::{fmt, hash::Hash, mem, num::NonZero, sync::atomic::Ordering}; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicI64 as AtomicIdCursor; @@ -157,7 +157,7 @@ pub struct Entity { // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] index: u32, - generation: NonZeroU32, + generation: NonZero, #[cfg(target_endian = "big")] index: u32, } @@ -223,7 +223,7 @@ impl Entity { /// Construct an [`Entity`] from a raw `index` value and a non-zero `generation` value. /// Ensure that the generation value is never greater than `0x7FFF_FFFF`. #[inline(always)] - pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZeroU32) -> Entity { + pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZero) -> Entity { debug_assert!(generation.get() <= HIGH_MASK); Self { index, generation } @@ -279,7 +279,7 @@ impl Entity { /// a component. #[inline(always)] pub const fn from_raw(index: u32) -> Entity { - Self::from_raw_and_generation(index, NonZeroU32::MIN) + Self::from_raw_and_generation(index, NonZero::::MIN) } /// Convert to a form convenient for passing outside of rust. @@ -722,7 +722,7 @@ impl Entities { meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, 1); - if meta.generation == NonZeroU32::MIN { + if meta.generation == NonZero::::MIN { warn!( "Entity({}) generation wrapped on Entities::free, aliasing may occur", entity.index @@ -949,7 +949,7 @@ impl Entities { #[repr(C)] struct EntityMeta { /// The current generation of the [`Entity`]. - pub generation: NonZeroU32, + pub generation: NonZero, /// The current location of the [`Entity`] pub location: EntityLocation, } @@ -957,7 +957,7 @@ struct EntityMeta { impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { - generation: NonZeroU32::MIN, + generation: NonZero::::MIN, location: EntityLocation::INVALID, }; } @@ -1014,7 +1014,8 @@ mod tests { #[test] fn entity_bits_roundtrip() { // Generation cannot be greater than 0x7FFF_FFFF else it will be an invalid Entity id - let e = Entity::from_raw_and_generation(0xDEADBEEF, NonZeroU32::new(0x5AADF00D).unwrap()); + let e = + Entity::from_raw_and_generation(0xDEADBEEF, NonZero::::new(0x5AADF00D).unwrap()); assert_eq!(Entity::from_bits(e.to_bits()), e); } @@ -1091,65 +1092,65 @@ mod tests { #[allow(clippy::nonminimal_bool)] // This is intentionally testing `lt` and `ge` as separate functions. fn entity_comparison() { assert_eq!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZero::::new(789).unwrap()) ); assert_ne!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), - Entity::from_raw_and_generation(456, NonZeroU32::new(123).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), + Entity::from_raw_and_generation(456, NonZero::::new(123).unwrap()) ); // ordering is by generation then by index assert!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - >= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + >= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert!( - Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - <= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + <= Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) ); assert!( - !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - < Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + < Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) ); assert!( - !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) - > Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + !(Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()) + > Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap())) ); assert!( - Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) - < Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) + Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) + < Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) - > Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(9).unwrap()) + > Entity::from_raw_and_generation(9, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) - < Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) + < Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) - <= Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + Entity::from_raw_and_generation(1, NonZero::::new(1).unwrap()) + <= Entity::from_raw_and_generation(2, NonZero::::new(1).unwrap()) ); assert!( - Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) - > Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) + > Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) ); assert!( - Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) - >= Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + Entity::from_raw_and_generation(2, NonZero::::new(2).unwrap()) + >= Entity::from_raw_and_generation(1, NonZero::::new(2).unwrap()) ); } diff --git a/crates/bevy_ecs/src/identifier/masks.rs b/crates/bevy_ecs/src/identifier/masks.rs index d5adc856ddb88..85fb393cb6f13 100644 --- a/crates/bevy_ecs/src/identifier/masks.rs +++ b/crates/bevy_ecs/src/identifier/masks.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::NonZero; use super::kinds::IdKind; @@ -61,7 +61,7 @@ impl IdentifierMask { /// Will never be greater than [`HIGH_MASK`] or less than `1`, and increments are masked to /// never be greater than [`HIGH_MASK`]. #[inline(always)] - pub(crate) const fn inc_masked_high_by(lhs: NonZeroU32, rhs: u32) -> NonZeroU32 { + pub(crate) const fn inc_masked_high_by(lhs: NonZero, rhs: u32) -> NonZero { let lo = (lhs.get() & HIGH_MASK).wrapping_add(rhs & HIGH_MASK); // Checks high 32 bit for whether we have overflowed 31 bits. let overflowed = lo >> 31; @@ -70,7 +70,7 @@ impl IdentifierMask { // - Adding the overflow flag will offset overflows to start at 1 instead of 0 // - The sum of `0x7FFF_FFFF` + `u32::MAX` + 1 (overflow) == `0x7FFF_FFFF` // - If the operation doesn't overflow at 31 bits, no offsetting takes place - unsafe { NonZeroU32::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } + unsafe { NonZero::::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } } } @@ -166,68 +166,68 @@ mod tests { // Adding from lowest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 0) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 0) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 1) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 1) ); assert_eq!( - NonZeroU32::new(3).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 2) + NonZero::::new(3).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MIN, 2) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, HIGH_MASK) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, HIGH_MASK) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, u32::MAX) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MIN, u32::MAX) ); // Adding from absolute highest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 0) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 0) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 1) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 1) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 2) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, 2) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, HIGH_MASK) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, HIGH_MASK) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, u32::MAX) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::MAX, u32::MAX) ); // Adding from actual highest value with lowest to highest increment // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 0) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 0) ); assert_eq!( - NonZeroU32::MIN, - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 1) + NonZero::::MIN, + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 1) ); assert_eq!( - NonZeroU32::new(2).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 2) + NonZero::::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), 2) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), HIGH_MASK) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), HIGH_MASK) ); assert_eq!( - NonZeroU32::new(HIGH_MASK).unwrap(), - IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), u32::MAX) + NonZero::::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZero::::new(HIGH_MASK).unwrap(), u32::MAX) ); } } diff --git a/crates/bevy_ecs/src/identifier/mod.rs b/crates/bevy_ecs/src/identifier/mod.rs index 04a93cde45c0c..e9c7df8006e38 100644 --- a/crates/bevy_ecs/src/identifier/mod.rs +++ b/crates/bevy_ecs/src/identifier/mod.rs @@ -7,7 +7,7 @@ use bevy_reflect::Reflect; use self::{error::IdentifierError, kinds::IdKind, masks::IdentifierMask}; -use std::{hash::Hash, num::NonZeroU32}; +use std::{hash::Hash, num::NonZero}; pub mod error; pub(crate) mod kinds; @@ -28,7 +28,7 @@ pub struct Identifier { // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] low: u32, - high: NonZeroU32, + high: NonZero, #[cfg(target_endian = "big")] low: u32, } @@ -56,7 +56,7 @@ impl Identifier { unsafe { Ok(Self { low, - high: NonZeroU32::new_unchecked(packed_high), + high: NonZero::::new_unchecked(packed_high), }) } } @@ -71,7 +71,7 @@ impl Identifier { /// Returns the value of the high segment of the [`Identifier`]. This /// does not apply any masking. #[inline(always)] - pub const fn high(self) -> NonZeroU32 { + pub const fn high(self) -> NonZero { self.high } @@ -114,7 +114,7 @@ impl Identifier { /// This method is the fallible counterpart to [`Identifier::from_bits`]. #[inline(always)] pub const fn try_from_bits(value: u64) -> Result { - let high = NonZeroU32::new(IdentifierMask::get_high(value)); + let high = NonZero::::new(IdentifierMask::get_high(value)); match high { Some(high) => Ok(Self { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 09e86f7711cca..cbaf638e29ad1 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -36,7 +36,9 @@ pub mod world; pub use bevy_ptr as ptr; -/// Most commonly used re-exported types. +/// The ECS prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] #[cfg(feature = "reflect_functions")] @@ -88,7 +90,7 @@ mod tests { }; use bevy_tasks::{ComputeTaskPool, TaskPool}; use bevy_utils::HashSet; - use std::num::NonZeroU32; + use std::num::NonZero; use std::{ any::TypeId, marker::PhantomData, @@ -1659,7 +1661,7 @@ mod tests { ); let e4_mismatched_generation = - Entity::from_raw_and_generation(3, NonZeroU32::new(2).unwrap()); + Entity::from_raw_and_generation(3, NonZero::::new(2).unwrap()); assert!( world_b.get_or_spawn(e4_mismatched_generation).is_none(), "attempting to spawn on top of an entity with a mismatched entity generation fails" @@ -1754,7 +1756,8 @@ mod tests { let e0 = world.spawn(A(0)).id(); let e1 = Entity::from_raw(1); let e2 = world.spawn_empty().id(); - let invalid_e2 = Entity::from_raw_and_generation(e2.index(), NonZeroU32::new(2).unwrap()); + let invalid_e2 = + Entity::from_raw_and_generation(e2.index(), NonZero::::new(2).unwrap()); let values = vec![(e0, (B(0), C)), (e1, (B(1), C)), (invalid_e2, (B(2), C))]; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 3ef2d6b28d63d..c6ace80ca5202 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -429,7 +429,17 @@ impl World { if observers.map.is_empty() && observers.entity_map.is_empty() { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { - archetypes.update_flags(*component, flag, false); + for archetype in &mut archetypes.archetypes { + if archetype.contains(*component) { + let no_longer_observed = archetype + .components() + .all(|id| !cache.component_observers.contains_key(&id)); + + if no_longer_observed { + archetype.flags.set(flag, false); + } + } + } } } } @@ -656,6 +666,26 @@ mod tests { world.spawn(A).flush(); } + // Regression test for https://github.com/bevyengine/bevy/issues/14961 + #[test] + fn observer_despawn_archetype_flags() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world.spawn((A, B)).flush(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + let observer = world + .observe(|_: Trigger| panic!("Observer triggered after being despawned.")) + .flush(); + world.despawn(observer); + + world.despawn(entity); + + assert_eq!(1, world.resource::().0); + } + #[test] fn observer_multiple_matches() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index ed4e2cd4ba71d..94a8af27c598a 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -261,7 +261,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { /// Create a [`QueryState`] with the accesses of the builder. /// - /// Takes `&mut self` to access the innner world reference while initializing + /// Takes `&mut self` to access the inner world reference while initializing /// state for the new [`QueryState`] pub fn build(&mut self) -> QueryState { QueryState::::from_builder(self) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 5c1912b7bcc18..90749bcee963b 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -122,6 +122,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } } + /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment + /// from an storage. + /// + /// # Safety + /// - `range` must be in `[0, storage::entity_count)` or None. + #[inline] + pub(super) unsafe fn fold_over_storage_range( + &mut self, + mut accum: B, + func: &mut Func, + storage: StorageId, + range: Option>, + ) -> B + where + Func: FnMut(B, D::Item<'w>) -> B, + { + if self.cursor.is_dense { + // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. + let table_id = unsafe { storage.table_id }; + // SAFETY: Matched table IDs are guaranteed to still exist. + let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() }; + + let range = range.unwrap_or(0..table.entity_count()); + accum = + // SAFETY: + // - The fetched table matches both D and F + // - caller ensures `range` is within `[0, table.entity_count)` + // - The if block ensures that the query iteration is dense + unsafe { self.fold_over_table_range(accum, func, table, range) }; + } else { + // SAFETY: `self.cursor.is_dense` is false, so storage ids are guaranteed to be archetype ids. + let archetype_id = unsafe { storage.archetype_id }; + // SAFETY: Matched archetype IDs are guaranteed to still exist. + let archetype = unsafe { self.archetypes.get(archetype_id).debug_checked_unwrap() }; + // SAFETY: Matched table IDs are guaranteed to still exist. + let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() }; + + let range = range.unwrap_or(0..archetype.len()); + + // When an archetype and its table have equal entity counts, dense iteration can be safely used. + // this leverages cache locality to optimize performance. + if table.entity_count() == archetype.len() { + accum = + // SAFETY: + // - The fetched archetype matches both D and F + // - The provided archetype and its' table have the same length. + // - caller ensures `range` is within `[0, archetype.len)` + // - The if block ensures that the query iteration is not dense. + unsafe { self.fold_over_dense_archetype_range(accum, func, archetype,range) }; + } else { + accum = + // SAFETY: + // - The fetched archetype matches both D and F + // - caller ensures `range` is within `[0, archetype.len)` + // - The if block ensures that the query iteration is not dense. + unsafe { self.fold_over_archetype_range(accum, func, archetype,range) }; + } + } + accum + } + /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment /// from an table. /// @@ -143,7 +204,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { if table.is_empty() { return accum; } - assert!( + debug_assert!( rows.end <= u32::MAX as usize, "TableRow is only valid up to u32::MAX" ); @@ -267,12 +328,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { if archetype.is_empty() { return accum; } - assert!( + debug_assert!( rows.end <= u32::MAX as usize, "TableRow is only valid up to u32::MAX" ); let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); - debug_assert!( archetype.len() == table.entity_count(), "archetype and it's table must have the same length. " @@ -1032,48 +1092,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> accum = func(accum, item); } - if self.cursor.is_dense { - for id in self.cursor.storage_id_iter.clone() { - // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. - let table_id = unsafe { id.table_id }; - // SAFETY: Matched table IDs are guaranteed to still exist. - let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() }; - - accum = - // SAFETY: - // - The fetched table matches both D and F - // - The provided range is equivalent to [0, table.entity_count) - // - The if block ensures that the query iteration is dense - unsafe { self.fold_over_table_range(accum, &mut func, table, 0..table.entity_count()) }; - } - } else { - for id in self.cursor.storage_id_iter.clone() { - // SAFETY: `self.cursor.is_dense` is false, so storage ids are guaranteed to be archetype ids. - let archetype_id = unsafe { id.archetype_id }; - // SAFETY: Matched archetype IDs are guaranteed to still exist. - let archetype = unsafe { self.archetypes.get(archetype_id).debug_checked_unwrap() }; - // SAFETY: Matched table IDs are guaranteed to still exist. - let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() }; - - // When an archetype and its table have equal entity counts, dense iteration can be safely used. - // this leverages cache locality to optimize performance. - if table.entity_count() == archetype.len() { - accum = - // SAFETY: - // - The fetched archetype matches both D and F - // - The provided archetype and its' table have the same length. - // - The provided range is equivalent to [0, archetype.len) - // - The if block ensures that the query iteration is not dense. - unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype, 0..archetype.len()) }; - } else { - accum = - // SAFETY: - // - The fetched archetype matches both D and F - // - The provided range is equivalent to [0, archetype.len) - // - The if block ensures that the query iteration is not dense. - unsafe { self.fold_over_archetype_range(accum, &mut func, archetype, 0..archetype.len()) }; - } - } + for id in self.cursor.storage_id_iter.clone().copied() { + // SAFETY: + // - The range(None) is equivalent to [0, storage.entity_count) + accum = unsafe { self.fold_over_storage_range(accum, &mut func, id, None) }; } accum } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index b94cb4aa90454..93d71551492d6 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1505,25 +1505,7 @@ impl QueryState { let mut iter = self.iter_unchecked_manual(world, last_run, this_run); let mut accum = init_accum(); for storage_id in queue { - if self.is_dense { - let id = storage_id.table_id; - let table = &world.storages().tables.get(id).debug_checked_unwrap(); - accum = iter.fold_over_table_range( - accum, - &mut func, - table, - 0..table.entity_count(), - ); - } else { - let id = storage_id.archetype_id; - let archetype = world.archetypes().get(id).debug_checked_unwrap(); - accum = iter.fold_over_archetype_range( - accum, - &mut func, - archetype, - 0..archetype.len(), - ); - } + accum = iter.fold_over_storage_range(accum, &mut func, storage_id, None); } }); }; @@ -1539,17 +1521,8 @@ impl QueryState { #[cfg(feature = "trace")] let _span = self.par_iter_span.enter(); let accum = init_accum(); - if self.is_dense { - let id = storage_id.table_id; - let table = world.storages().tables.get(id).debug_checked_unwrap(); - self.iter_unchecked_manual(world, last_run, this_run) - .fold_over_table_range(accum, &mut func, table, batch); - } else { - let id = storage_id.archetype_id; - let archetype = world.archetypes().get(id).debug_checked_unwrap(); - self.iter_unchecked_manual(world, last_run, this_run) - .fold_over_archetype_range(accum, &mut func, archetype, batch); - } + self.iter_unchecked_manual(world, last_run, this_run) + .fold_over_storage_range(accum, &mut func, storage_id, Some(batch)); }); } }; diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index dca9c1542a551..d5699f37164cb 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -1,7 +1,7 @@ use std::{ alloc::{handle_alloc_error, Layout}, cell::UnsafeCell, - num::NonZeroUsize, + num::NonZero, ptr::NonNull, }; @@ -56,7 +56,7 @@ impl BlobVec { drop: Option)>, capacity: usize, ) -> BlobVec { - let align = NonZeroUsize::new(item_layout.align()).expect("alignment must be > 0"); + let align = NonZero::::new(item_layout.align()).expect("alignment must be > 0"); let data = bevy_ptr::dangling_with_align(align); if item_layout.size() == 0 { BlobVec { @@ -119,7 +119,8 @@ impl BlobVec { let available_space = self.capacity - self.len; if available_space < additional { // SAFETY: `available_space < additional`, so `additional - available_space > 0` - let increment = unsafe { NonZeroUsize::new_unchecked(additional - available_space) }; + let increment = + unsafe { NonZero::::new_unchecked(additional - available_space) }; self.grow_exact(increment); } } @@ -132,7 +133,7 @@ impl BlobVec { #[cold] fn do_reserve(slf: &mut BlobVec, additional: usize) { let increment = slf.capacity.max(additional - (slf.capacity - slf.len)); - let increment = NonZeroUsize::new(increment).unwrap(); + let increment = NonZero::::new(increment).unwrap(); slf.grow_exact(increment); } @@ -148,7 +149,7 @@ impl BlobVec { /// Panics if the new capacity overflows `usize`. /// For ZST it panics unconditionally because ZST `BlobVec` capacity /// is initialized to `usize::MAX` and always stays that way. - fn grow_exact(&mut self, increment: NonZeroUsize) { + fn grow_exact(&mut self, increment: NonZero) { let new_capacity = self .capacity .checked_add(increment.get()) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index cd532776e5b88..b9734e41774e8 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -194,7 +194,8 @@ unsafe impl< } macro_rules! impl_system_param_builder_tuple { - ($(($param: ident, $builder: ident)),*) => { + ($(#[$meta:meta])* $(($param: ident, $builder: ident)),*) => { + $(#[$meta])* // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<$($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder<($($param,)*)> for ($($builder,)*) { fn build(self, _world: &mut World, _meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { @@ -207,7 +208,14 @@ macro_rules! impl_system_param_builder_tuple { }; } -all_tuples!(impl_system_param_builder_tuple, 0, 16, P, B); +all_tuples!( + #[doc(fake_variadic)] + impl_system_param_builder_tuple, + 0, + 16, + P, + B +); // SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl> SystemParamBuilder> for Vec { @@ -556,4 +564,31 @@ mod tests { let result = world.run_system_once(system); assert_eq!(result, 4); } + + #[derive(SystemParam)] + #[system_param(builder)] + struct CustomParam<'w, 's> { + query: Query<'w, 's, ()>, + local: Local<'s, usize>, + } + + #[test] + fn custom_param_builder() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let system = (CustomParamBuilder { + local: LocalBuilder(100), + query: QueryParamBuilder::new(|builder| { + builder.with::(); + }), + },) + .build_state(&mut world) + .build_system(|param: CustomParam| *param.local + param.query.iter().count()); + + let result = world.run_system_once(system); + assert_eq!(result, 101); + } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 55468cbcad094..c6676fa26a113 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1049,7 +1049,7 @@ impl EntityCommands<'_> { let caller = Location::caller(); // SAFETY: same invariants as parent call self.add(unsafe {insert_by_id(component_id, value, move |entity| { - panic!("error[B0003]: {caller}: Could not insert a component {component_id:?} (with type {}) for entity {entity:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003", std::any::type_name::()); + panic!("error[B0003]: {caller}: Could not insert a component {component_id:?} (with type {}) for entity {entity:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", std::any::type_name::()); })}) } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 10e7fd4ec3583..6b604c54c2a9a 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -121,6 +121,55 @@ use std::{ /// This will most commonly occur when working with `SystemParam`s generically, as the requirement /// has not been proven to the compiler. /// +/// ## Builders +/// +/// If you want to use a [`SystemParamBuilder`](crate::system::SystemParamBuilder) with a derived [`SystemParam`] implementation, +/// add a `#[system_param(builder)]` attribute to the struct. +/// This will generate a builder struct whose name is the param struct suffixed with `Builder`. +/// The builder will not be `pub`, so you may want to expose a method that returns an `impl SystemParamBuilder`. +/// +/// ``` +/// mod custom_param { +/// # use bevy_ecs::{ +/// # prelude::*, +/// # system::{LocalBuilder, QueryParamBuilder, SystemParam}, +/// # }; +/// # +/// #[derive(SystemParam)] +/// #[system_param(builder)] +/// pub struct CustomParam<'w, 's> { +/// query: Query<'w, 's, ()>, +/// local: Local<'s, usize>, +/// } +/// +/// impl<'w, 's> CustomParam<'w, 's> { +/// pub fn builder( +/// local: usize, +/// query: impl FnOnce(&mut QueryBuilder<()>), +/// ) -> impl SystemParamBuilder { +/// CustomParamBuilder { +/// local: LocalBuilder(local), +/// query: QueryParamBuilder::new(query), +/// } +/// } +/// } +/// } +/// +/// use custom_param::CustomParam; +/// +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] +/// # struct A; +/// # +/// # let mut world = World::new(); +/// # +/// let system = (CustomParam::builder(100, |builder| { +/// builder.with::(); +/// }),) +/// .build_state(&mut world) +/// .build_system(|param: CustomParam| {}); +/// ``` +/// /// # Safety /// /// The implementor must ensure the following is true. diff --git a/crates/bevy_gizmos/src/aabb.rs b/crates/bevy_gizmos/src/aabb.rs index 1b62775245418..6496dc91250c3 100644 --- a/crates/bevy_gizmos/src/aabb.rs +++ b/crates/bevy_gizmos/src/aabb.rs @@ -40,6 +40,7 @@ impl Plugin for AabbGizmoPlugin { config.config::().1.draw_all }), ) + .after(bevy_render::view::VisibilitySystems::CalculateBounds) .after(TransformSystem::TransformPropagate), ); } diff --git a/crates/bevy_gizmos/src/curves.rs b/crates/bevy_gizmos/src/curves.rs new file mode 100644 index 0000000000000..4a7b1aec1e045 --- /dev/null +++ b/crates/bevy_gizmos/src/curves.rs @@ -0,0 +1,175 @@ +//! Additional [`Gizmos`] Functions -- Curves +//! +//! Includes the implementation of [`Gizmos::curve_2d`], +//! [`Gizmos::curve_3d`] and assorted support items. + +use bevy_color::Color; +use bevy_math::{curve::Curve, Vec2, Vec3}; + +use crate::prelude::{GizmoConfigGroup, Gizmos}; + +impl<'w, 's, Config, Clear> Gizmos<'w, 's, Config, Clear> +where + Config: GizmoConfigGroup, + Clear: 'static + Send + Sync, +{ + /// Draw a curve, at the given time points, sampling in 2D. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_2d` some type that implements the [`Curve`] trait and samples `Vec2`s + /// - `times` some iterable type yielding `f32` which will be used for sampling the curve + /// - `color` the color of the curve + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::{RED}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// gizmos.curve_2d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_2d( + &mut self, + curve_2d: impl Curve, + times: impl IntoIterator, + color: impl Into, + ) { + self.linestrip_2d(curve_2d.sample_iter(times).flatten(), color); + } + + /// Draw a curve, at the given time points, sampling in 3D. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_3d` some type that implements the [`Curve`] trait and samples `Vec3`s + /// - `times` some iterable type yielding `f32` which will be used for sampling the curve + /// - `color` the color of the curve + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::{RED}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| { + /// let (x,y) = t.sin_cos(); + /// Vec3::new(x, y, t) + /// }); + /// gizmos.curve_3d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_3d( + &mut self, + curve_3d: impl Curve, + times: impl IntoIterator, + color: impl Into, + ) { + self.linestrip(curve_3d.sample_iter(times).flatten(), color); + } + + /// Draw a curve, at the given time points, sampling in 2D, with a color gradient. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_2d` some type that implements the [`Curve`] trait and samples `Vec2`s + /// - `times_with_colors` some iterable type yielding `f32` which will be used for sampling + /// the curve together with the color at this position + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); + /// gizmos.curve_gradient_2d( + /// curve, + /// (0..=100).map(|n| n as f32 / 100.0) + /// .map(|t| (t, GREEN.mix(&RED, t))) + /// ); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_gradient_2d( + &mut self, + curve_2d: impl Curve, + times_with_colors: impl IntoIterator, + ) where + C: Into, + { + self.linestrip_gradient_2d( + times_with_colors + .into_iter() + .filter_map(|(time, color)| curve_2d.sample(time).map(|sample| (sample, color))), + ); + } + + /// Draw a curve, at the given time points, sampling in 3D, with a color gradient. + /// + /// This should be called for each frame the curve needs to be rendered. + /// + /// Samples of time points outside of the curve's domain will be filtered out and won't + /// contribute to the rendering. If you wish to render the curve outside of its domain you need + /// to create a new curve with an extended domain. + /// + /// # Arguments + /// - `curve_3d` some type that implements the [`Curve`] trait and samples `Vec3`s + /// - `times_with_colors` some iterable type yielding `f32` which will be used for sampling + /// the curve together with the color at this position + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; + /// fn system(mut gizmos: Gizmos) { + /// let domain = Interval::UNIT; + /// let curve = function_curve(domain, |t| { + /// let (x,y) = t.sin_cos(); + /// Vec3::new(x, y, t) + /// }); + /// gizmos.curve_gradient_3d( + /// curve, + /// (0..=100).map(|n| n as f32 / 100.0) + /// .map(|t| (t, GREEN.mix(&RED, t))) + /// ); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn curve_gradient_3d( + &mut self, + curve_3d: impl Curve, + times_with_colors: impl IntoIterator, + ) where + C: Into, + { + self.linestrip_gradient( + times_with_colors + .into_iter() + .filter_map(|(time, color)| curve_3d.sample(time).map(|sample| (sample, color))), + ); + } +} diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index 05b04c0376735..8c387fa349e41 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -182,6 +182,8 @@ where /// /// This should be called for each frame the grid needs to be rendered. /// + /// The grid's default orientation aligns with the XY-plane. + /// /// # Arguments /// /// - `isometry` defines the translation and rotation of the grid. diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3e28516b608b7..bbef44df46677 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -37,6 +37,7 @@ pub mod arrows; pub mod circles; pub mod config; pub mod cross; +pub mod curves; pub mod gizmos; pub mod grid; pub mod primitives; @@ -50,7 +51,9 @@ mod pipeline_2d; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] mod pipeline_3d; -/// The `bevy_gizmos` prelude. +/// The gizmos prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[cfg(feature = "bevy_render")] pub use crate::aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}; @@ -241,6 +244,9 @@ impl AppGizmoBuilder for App { handles.list.insert(TypeId::of::(), None); handles.strip.insert(TypeId::of::(), None); + // These handles are safe to mutate in any order + self.allow_ambiguous_resource::(); + self.init_resource::>() .init_resource::>() .init_resource::>>() diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index e2da1a115894e..f2ee075c16c75 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -1,14 +1,13 @@ //! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`]. use super::helpers::*; -use std::f32::consts::TAU; use bevy_color::Color; use bevy_math::primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, }; -use bevy_math::{Dir3, Isometry3d, Quat, Vec3}; +use bevy_math::{Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3}; use crate::circles::SphereBuilder; use crate::prelude::{GizmoConfigGroup, Gizmos}; @@ -83,19 +82,17 @@ where { gizmos: &'a mut Gizmos<'w, 's, Config, Clear>, - // direction of the normal orthogonal to the plane + // Direction of the normal orthogonal to the plane normal: Dir3, isometry: Isometry3d, // Color of the plane color: Color, - // Number of axis to hint the plane - axis_count: u32, - // Number of segments used to hint the plane - segment_count: u32, - // Length of segments used to hint the plane - segment_length: f32, + // Defines the amount of cells in the x and y axes + cell_count: UVec2, + // Defines the distance between cells along the x and y axes + spacing: Vec2, } impl Plane3dBuilder<'_, '_, '_, Config, Clear> @@ -103,21 +100,15 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { - /// Set the number of segments used to hint the plane. - pub fn segment_count(mut self, count: u32) -> Self { - self.segment_count = count; + /// Set the number of cells in the x and y axes direction. + pub fn cell_count(mut self, cell_count: UVec2) -> Self { + self.cell_count = cell_count; self } - /// Set the length of segments used to hint the plane. - pub fn segment_length(mut self, length: f32) -> Self { - self.segment_length = length; - self - } - - /// Set the number of axis used to hint the plane. - pub fn axis_count(mut self, count: u32) -> Self { - self.axis_count = count; + /// Set the distance between cells along the x and y axes. + pub fn spacing(mut self, spacing: Vec2) -> Self { + self.spacing = spacing; self } } @@ -140,9 +131,8 @@ where normal: primitive.normal, isometry, color: color.into(), - axis_count: 4, - segment_count: 3, - segment_length: 0.25, + cell_count: UVec2::splat(3), + spacing: Vec2::splat(1.0), } } } @@ -157,35 +147,16 @@ where return; } - // draws the normal self.gizmos .primitive_3d(&self.normal, self.isometry, self.color); - - // draws the axes - // get rotation for each direction - let normals_normal = self.normal.any_orthonormal_vector(); - (0..self.axis_count) - .map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU) - .map(|angle| Quat::from_axis_angle(self.normal.as_vec3(), angle)) - .flat_map(|quat| { - let segment_length = self.segment_length; - let isometry = self.isometry; - // for each axis draw dotted line - (0..) - .filter(|i| i % 2 != 0) - .take(self.segment_count as usize) - .map(|i| [i, i + 1]) - .map(move |percents| { - percents - .map(|percent| percent as f32 + 0.5) - .map(|percent| percent * segment_length * normals_normal) - .map(|vec3| quat * vec3) - .map(|vec3| isometry * vec3) - }) - }) - .for_each(|[start, end]| { - self.gizmos.line(start, end, self.color); - }); + // the default orientation of the grid is Z-up + let rot = Quat::from_rotation_arc(Vec3::Z, self.normal.as_vec3()); + self.gizmos.grid( + Isometry3d::new(self.isometry.translation, self.isometry.rotation * rot), + self.cell_count, + self.spacing, + self.color, + ); } } diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 722dde2150d59..bca24f0af72b1 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -115,7 +115,9 @@ use bevy_render::{ }; use bevy_scene::Scene; -/// The `bevy_gltf` prelude. +/// The glTF prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{Gltf, GltfAssetLabel, GltfExtras}; diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 6b949119cd111..90ea42af39e4a 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -585,7 +585,8 @@ impl BuildChildren for EntityWorldMut<'_> { } fn with_child(&mut self, bundle: B) -> &mut Self { - let child = self.world_scope(|world| world.spawn(bundle).id()); + let parent = self.id(); + let child = self.world_scope(|world| world.spawn((bundle, Parent(parent))).id()); if let Some(mut children_component) = self.get_mut::() { children_component.0.retain(|value| child != *value); children_component.0.push(child); diff --git a/crates/bevy_hierarchy/src/lib.rs b/crates/bevy_hierarchy/src/lib.rs index 75fc8492024a6..98011ced58591 100755 --- a/crates/bevy_hierarchy/src/lib.rs +++ b/crates/bevy_hierarchy/src/lib.rs @@ -69,7 +69,9 @@ pub use valid_parent_check_plugin::*; mod query_extension; pub use query_extension::*; -#[doc(hidden)] +/// The hierarchy prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{child_builder::*, components::*, hierarchy::*, query_extension::*}; diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 83984af1e69fd..b4dac1ee7f934 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -24,7 +24,9 @@ pub mod touch; pub use axis::*; pub use button_input::*; -/// Most commonly used re-exported types. +/// The input prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index a84bbdb573590..7aadcb50fbf6a 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -90,6 +90,11 @@ impl Plugin for IgnoreAmbiguitiesPlugin { bevy_animation::advance_animations, bevy_ui::ui_layout_system, ); + app.ignore_ambiguity( + bevy_app::PostUpdate, + bevy_animation::animate_targets, + bevy_ui::ui_layout_system, + ); } } } diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index c2fdd1639b83d..0cbb6b207d4bf 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -28,8 +28,10 @@ mod android_tracing; static GLOBAL: tracy_client::ProfiledAllocator = tracy_client::ProfiledAllocator::new(std::alloc::System, 100); +/// The log prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - //! The Bevy Log Prelude. #[doc(hidden)] pub use bevy_utils::tracing::{ debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, @@ -133,6 +135,20 @@ pub(crate) struct FlushGuard(SyncCell); /// This plugin should not be added multiple times in the same process. This plugin /// sets up global logging configuration for **all** Apps in a given process, and /// rerunning the same initialization multiple times will lead to a panic. +/// +/// # Performance +/// +/// Filters applied through this plugin are computed at _runtime_, which will +/// have a non-zero impact on performance. +/// To achieve maximum performance, consider using +/// [_compile time_ filters](https://docs.rs/log/#compile-time-filters) +/// provided by the [`log`](https://crates.io/crates/log) crate. +/// +/// ```toml +/// # cargo.toml +/// [dependencies] +/// log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] } +/// ``` pub struct LogPlugin { /// Filters logs using the [`EnvFilter`] format pub filter: String, @@ -157,10 +173,13 @@ pub struct LogPlugin { /// A boxed [`Layer`] that can be used with [`LogPlugin`]. pub type BoxedLayer = Box + Send + Sync + 'static>; +/// The default [`LogPlugin`] [`EnvFilter`]. +pub const DEFAULT_FILTER: &str = "wgpu=error,naga=warn"; + impl Default for LogPlugin { fn default() -> Self { Self { - filter: "wgpu=error,naga=warn".to_string(), + filter: DEFAULT_FILTER.to_string(), level: Level::INFO, custom_layer: |_| None, } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 76ad5b06a7b5a..70df0086940f0 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -43,7 +43,9 @@ pub use rotation2d::Rot2; #[cfg(feature = "rand")] pub use sampling::{FromRng, ShapeSample}; -/// The `bevy_math` prelude. +/// The math prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] #[cfg(feature = "rand")] @@ -55,6 +57,7 @@ pub mod prelude { CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator, RationalCurve, RationalGenerator, RationalSegment, }, + curve::*, direction::{Dir2, Dir3, Dir3A}, primitives::*, BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index fe913a1d196bb..7da6c5da026cf 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -1,6 +1,6 @@ //! Spatial clustering of objects, currently just point and spot lights. -use std::num::NonZeroU64; +use std::num::NonZero; use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ @@ -468,7 +468,7 @@ impl GpuClusterableObjects { } } - pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZeroU64 { + pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterableObjectsStorage::min_size(), BufferBindingType::Uniform => GpuClusterableObjectsUniform::min_size(), @@ -749,7 +749,7 @@ impl ViewClusterBindings { pub fn min_size_clusterable_object_index_lists( buffer_binding_type: BufferBindingType, - ) -> NonZeroU64 { + ) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterableObjectIndexListsStorage::min_size(), BufferBindingType::Uniform => GpuClusterableObjectIndexListsUniform::min_size(), @@ -758,7 +758,7 @@ impl ViewClusterBindings { pub fn min_size_cluster_offsets_and_counts( buffer_binding_type: BufferBindingType, - ) -> NonZeroU64 { + ) -> NonZero { match buffer_binding_type { BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(), BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(), diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index ea84c49f18709..ba2848b8a8034 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -61,6 +61,9 @@ pub use volumetric_fog::{ FogVolume, FogVolumeBundle, VolumetricFogPlugin, VolumetricFogSettings, VolumetricLight, }; +/// The PBR prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -340,10 +343,22 @@ impl Plugin for PbrPlugin { ) .chain(), ) + .configure_sets( + PostUpdate, + SimulationLightSystems::UpdateDirectionalLightCascades + .ambiguous_with(SimulationLightSystems::UpdateDirectionalLightCascades), + ) + .configure_sets( + PostUpdate, + SimulationLightSystems::CheckLightVisibility + .ambiguous_with(SimulationLightSystems::CheckLightVisibility), + ) .add_systems( PostUpdate, ( - add_clusters.in_set(SimulationLightSystems::AddClusters), + add_clusters + .in_set(SimulationLightSystems::AddClusters) + .after(CameraUpdateSystem), assign_objects_to_clusters .in_set(SimulationLightSystems::AssignLightsToClusters) .after(TransformSystem::TransformPropagate) diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index d895ac767289c..d0db103c93884 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -496,12 +496,19 @@ pub enum ShadowFilteringMethod { Temporal, } +/// System sets used to run light-related systems. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SimulationLightSystems { AddClusters, AssignLightsToClusters, + /// System order ambiguities between systems in this set are ignored: + /// each [`build_directional_light_cascades`] system is independent of the others, + /// and should operate on distinct sets of entities. UpdateDirectionalLightCascades, UpdateLightFrusta, + /// System order ambiguities between systems in this set are ignored: + /// the order of systems within this set is irrelevant, as the various visibility-checking systesms + /// assumes that their operations are irreversible during the frame. CheckLightVisibility, } diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 8a78e93083024..1b1604df4d48d 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -65,7 +65,7 @@ use bevy_render::{ texture::{FallbackImage, GpuImage, Image}, }; -use std::num::NonZeroU32; +use std::num::NonZero; use std::ops::Deref; use crate::{ @@ -217,7 +217,7 @@ pub(crate) fn get_bind_group_layout_entries( binding_types::texture_cube(TextureSampleType::Float { filterable: true }); if binding_arrays_are_usable(render_device) { texture_cube_binding = - texture_cube_binding.count(NonZeroU32::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + texture_cube_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } [ diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 618da04fa4490..58110e98afb29 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -142,7 +142,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{FallbackImage, GpuImage, Image}, }; -use std::{num::NonZeroU32, ops::Deref}; +use std::{num::NonZero, ops::Deref}; use bevy_asset::{AssetId, Handle}; use bevy_reflect::Reflect; @@ -306,7 +306,7 @@ pub(crate) fn get_bind_group_layout_entries( binding_types::texture_3d(TextureSampleType::Float { filterable: true }); if binding_arrays_are_usable(render_device) { texture_3d_binding = - texture_3d_binding.count(NonZeroU32::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + texture_3d_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } [ diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a02f57b2f3441..e11087d88daf8 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -35,7 +35,7 @@ use bevy_render::{ use bevy_utils::tracing::error; use std::marker::PhantomData; use std::sync::atomic::{AtomicU32, Ordering}; -use std::{hash::Hash, num::NonZeroU32}; +use std::{hash::Hash, num::NonZero}; use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; @@ -984,7 +984,7 @@ impl AtomicMaterialBindGroupId { /// See also: [`AtomicU32::store`]. pub fn set(&self, id: MaterialBindGroupId) { let id = if let Some(id) = id.0 { - NonZeroU32::from(id).get() + NonZero::::from(id).get() } else { 0 }; @@ -996,7 +996,9 @@ impl AtomicMaterialBindGroupId { /// /// See also: [`AtomicU32::load`]. pub fn get(&self) -> MaterialBindGroupId { - MaterialBindGroupId(NonZeroU32::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from)) + MaterialBindGroupId( + NonZero::::new(self.0.load(Ordering::Relaxed)).map(BindGroupId::from), + ) } } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs index 60e163a87446a..e10dad6ef0aed 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -6,7 +6,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, }; use range_alloc::RangeAllocator; -use std::{num::NonZeroU64, ops::Range}; +use std::{num::NonZero, ops::Range}; /// Wrapper for a GPU buffer holding a large amount of data that persists across frames. pub struct PersistentGpuBuffer { @@ -66,7 +66,8 @@ impl PersistentGpuBuffer { let queue_count = self.write_queue.len(); for (data, metadata, buffer_slice) in self.write_queue.drain(..) { - let buffer_slice_size = NonZeroU64::new(buffer_slice.end - buffer_slice.start).unwrap(); + let buffer_slice_size = + NonZero::::new(buffer_slice.end - buffer_slice.start).unwrap(); let mut buffer_view = render_queue .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) .unwrap(); diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 67eca5df5e13d..38650d280891b 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -6,7 +6,7 @@ //! [`MeshInputUniform`]s instead and use the GPU to calculate the remaining //! derived fields in [`MeshUniform`]. -use std::num::NonZeroU64; +use std::num::NonZero; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; @@ -408,7 +408,7 @@ pub fn prepare_preprocess_bind_groups( // Don't use `as_entire_binding()` here; the shader reads the array // length and the underlying buffer may be longer than the actual size // of the vector. - let index_buffer_size = NonZeroU64::try_from( + let index_buffer_size = NonZero::::try_from( index_buffer_vec.buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), ) .ok(); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 36c5d7bfe044f..d0a506e9c3e42 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -1,4 +1,4 @@ -use std::{array, num::NonZeroU64, sync::Arc}; +use std::{array, num::NonZero, sync::Arc}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, @@ -164,7 +164,7 @@ impl From> for MeshPipelineViewLayoutKey { fn buffer_layout( buffer_binding_type: BufferBindingType, has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { match buffer_binding_type { BufferBindingType::Uniform => uniform_buffer_sized(has_dynamic_offset, min_binding_size), diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index eadd869e2d313..64eada2dbb04e 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } @@ -16,13 +17,11 @@ bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } - uuid = { version = "1.1", features = ["v4"] } [lints] diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index fb46ac7864172..03bb26ee2e489 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -35,7 +35,9 @@ use bevy_ecs::prelude::*; use bevy_math::Vec3; use bevy_reflect::Reflect; -/// Common imports for implementing a picking backend. +/// The picking backend prelude. +/// +/// This includes the most common types in this module, re-exported for your convenience. pub mod prelude { pub use super::{ray::RayMap, HitData, PointerHits}; pub use crate::{ @@ -231,6 +233,6 @@ pub mod ray { let viewport_logical = camera.to_logical(viewport.physical_position)?; viewport_pos -= viewport_logical; } - camera.viewport_to_world(camera_tfm, viewport_pos) + camera.viewport_to_world(camera_tfm, viewport_pos).ok() } } diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index a1d5aee04bace..00293c1d5e297 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -1,8 +1,44 @@ -//! Processes data from input and backends, producing interaction events. +//! This module defines a stateful set of interaction events driven by the `PointerInput` stream +//! and the hover state of each Pointer. +//! +//! # Usage +//! +//! To receive events from this module, you must use an [`Observer`] +//! The simplest example, registering a callback when an entity is hovered over by a pointer, looks like this: +//! +//! ```rust +//! # use bevy_ecs::prelude::*; +//! # use bevy_picking::prelude::*; +//! # let mut world = World::default(); +//! world.spawn_empty() +//! .observe(|trigger: Trigger>| { +//! println!("I am being hovered over"); +//! }); +//! ``` +//! +//! Observers give us three important properties: +//! 1. They allow for attaching event handlers to specific entities, +//! 2. they allow events to bubble up the entity hierarchy, +//! 3. and they allow events of different types to be called in a specific order. +//! +//! The order in which interaction events are received is extremely important, and you can read more +//! about it on the docs for the dispatcher system: [`pointer_events`]. This system runs in +//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickSet::Focus`](crate::PickSet::Focus). All pointer-event +//! observers resolve during the sync point between [`pointer_events`] and +//! [`update_interactions`](crate::focus::update_interactions). +//! +//! # Events Types +//! +//! The events this module defines fall into a few broad categories: +//! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. +//! + Clicking and pressing: [`Down`], [`Up`], and [`Click`]. +//! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. +//! +//! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains +//! general metadata about the pointer and it's location. use std::fmt::Debug; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_hierarchy::Parent; use bevy_math::Vec2; @@ -13,15 +49,16 @@ use crate::{ backend::{prelude::PointerLocation, HitData}, focus::{HoverMap, PreviousHoverMap}, pointer::{ - InputMove, InputPress, Location, PointerButton, PointerId, PointerMap, PressDirection, + Location, PointerAction, PointerButton, PointerId, PointerInput, PointerMap, PressDirection, }, }; -/// Stores the common data needed for all `PointerEvent`s. +/// Stores the common data needed for all pointer events. +/// +/// The documentation for the [`pointer_events`] explains the events this module exposes and +/// the order in which they fire. #[derive(Clone, PartialEq, Debug, Reflect, Component)] pub struct Pointer { - /// The target of this event - pub target: Entity, /// The pointer that triggered this event pub pointer_id: PointerId, /// The location of the pointer during this event @@ -36,14 +73,15 @@ where E: Debug + Clone + Reflect, { type Traversal = Parent; + const AUTO_PROPAGATE: bool = true; } impl std::fmt::Display for Pointer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "{:?}, {:.1?}, {:?}, {:.1?}", - self.pointer_id, self.pointer_location.position, self.target, self.event + "{:?}, {:.1?}, {:.1?}", + self.pointer_id, self.pointer_location.position, self.event )) } } @@ -57,23 +95,21 @@ impl std::ops::Deref for Pointer { } impl Pointer { - /// Construct a new `PointerEvent`. - pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + /// Construct a new `Pointer` event. + pub fn new(id: PointerId, location: Location, event: E) -> Self { Self { pointer_id: id, pointer_location: location, - target, event, } } } -/// Fires when a pointer is no longer available. -#[derive(Event, Clone, PartialEq, Debug, Reflect)] -pub struct PointerCancel { - /// ID of the pointer that was cancelled. - #[reflect(ignore)] - pub pointer_id: PointerId, +/// Fires when a pointer is canceled, and it's current interaction state is dropped. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Cancel { + /// Information about the picking intersection. + pub hit: HitData, } /// Fires when a the pointer crosses into the bounds of the `target` entity. @@ -202,24 +238,68 @@ pub struct DragDrop { pub hit: HitData, } -/// Generates pointer events from input and focus data +/// Dragging state. +#[derive(Debug, Clone)] +pub struct DragEntry { + /// The position of the pointer at drag start. + pub start_pos: Vec2, + /// The latest position of the pointer during this drag, used to compute deltas. + pub latest_pos: Vec2, +} + +/// An entry in the cache that drives the `pointer_events` system, storing additional data +/// about pointer button presses. +#[derive(Debug, Clone, Default)] +pub struct PointerState { + /// Stores the press location and start time for each button currently being pressed by the pointer. + pub pressing: HashMap, + /// Stores the the starting and current locations for each entity currently being dragged by the pointer. + pub dragging: HashMap, + /// Stores the hit data for each entity currently being dragged over by the pointer. + pub dragging_over: HashMap, +} + +/// Dispatches interaction events to the target entities. +/// +/// Within a single frame, events are dispatched in the following order: +/// + The sequence [`DragEnter`], [`Over`]. +/// + Any number of any of the following: +/// + For each movement: The sequence [`DragStart`], [`Drag`], [`DragOver`], [`Move`]. +/// + For each button press: Either [`Down`], or the sequence [`DragDrop`], [`DragEnd`], [`DragLeave`], [`Click`], [`Up`]. +/// + For each pointer cancellation: Simply [`Cancel`]. +/// + Finally the sequence [`DragLeave`], [`Out`]. +/// +/// Only the last event in a given sequence is garenteed to be present. +/// +/// Additionally, across multiple frames, the following are also strictly ordered by the interaction state machine: +/// + When a pointer moves over the target: [`Over`], [`Move`], [`Out`]. +/// + When a pointer presses buttons on the target: [`Down`], [`Up`], [`Click`]. +/// + When a pointer drags the target: [`DragStart`], [`Drag`], [`DragEnd`]. +/// + When a pointer drags something over the target: [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. +/// + When a pointer is canceled: No other events will follow the [`Cancel`] event for that pointer. +/// +/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. The rest rely on additional data from the +/// [`PointerInput`] event stream. To receive these events for a custom pointer, you must add [`PointerInput`] events. +/// +/// Note: Though it is common for the [`PointerInput`] stream may contain multiple pointer movements and presses each frame, +/// the hover state is determined only by the pointer's *final position*. Since the hover state ultimately determines which +/// entities receive events, this may mean that an entity can receive events which occurred before it was actually hovered. #[allow(clippy::too_many_arguments)] pub fn pointer_events( - mut commands: Commands, // Input - mut input_presses: EventReader, - mut input_moves: EventReader, - pointer_map: Res, + mut input_events: EventReader, + // ECS State pointers: Query<&PointerLocation>, + pointer_map: Res, hover_map: Res, previous_hover_map: Res, + // Local state + mut pointer_state: Local>, // Output - mut pointer_move: EventWriter>, - mut pointer_over: EventWriter>, - mut pointer_out: EventWriter>, - mut pointer_up: EventWriter>, - mut pointer_down: EventWriter>, + mut commands: Commands, ) { + // Setup utilities + let now = Instant::now(); let pointer_location = |pointer_id: PointerId| { pointer_map .get_entity(pointer_id) @@ -227,81 +307,6 @@ pub fn pointer_events( .and_then(|pointer| pointer.location.clone()) }; - for InputMove { - pointer_id, - location, - delta, - } in input_moves.read().cloned() - { - for (hovered_entity, hit) in hover_map - .get(&pointer_id) - .iter() - .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) - { - let event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Move { hit, delta }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_move.send(event); - } - } - - for press_event in input_presses.read() { - let button = press_event.button; - // We use the previous hover map because we want to consider pointers that just left the - // entity. Without this, touch inputs would never send up events because they are lifted up - // and leave the bounds of the entity at the same time. - for (hovered_entity, hit) in previous_hover_map - .get(&press_event.pointer_id) - .iter() - .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) - { - if let PressDirection::Up = press_event.direction { - let Some(location) = pointer_location(press_event.pointer_id) else { - debug!( - "Unable to get location for pointer {:?} during event {:?}", - press_event.pointer_id, press_event - ); - continue; - }; - let event = Pointer::new( - press_event.pointer_id, - location, - hovered_entity, - Up { button, hit }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_up.send(event); - } - } - for (hovered_entity, hit) in hover_map - .get(&press_event.pointer_id) - .iter() - .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) - { - if let PressDirection::Down = press_event.direction { - let Some(location) = pointer_location(press_event.pointer_id) else { - debug!( - "Unable to get location for pointer {:?} during event {:?}", - press_event.pointer_id, press_event - ); - continue; - }; - let event = Pointer::new( - press_event.pointer_id, - location, - hovered_entity, - Down { button, hit }, - ); - commands.trigger_targets(event.clone(), event.target); - pointer_down.send(event); - } - } - } - // If the entity is hovered... for (pointer_id, hovered_entity, hit) in hover_map .iter() @@ -320,9 +325,252 @@ pub fn pointer_events( ); continue; }; - let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit }); - commands.trigger_targets(event.clone(), event.target); - pointer_over.send(event); + // Possibly send DragEnter events + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + for drag_target in state + .dragging + .keys() + .filter(|&&drag_target| hovered_entity != drag_target) + { + state.dragging_over.insert(hovered_entity, hit.clone()); + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragEnter { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + } + } + // Always send Over events + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }), + hovered_entity, + ); + } + } + + // Dispatch input events... + for PointerInput { + pointer_id, + location, + action, + } in input_events.read().cloned() + { + match action { + // Pressed Button + PointerAction::Pressed { direction, button } => { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + // Possibly emit DragEnd, DragDrop, DragLeave on button releases + if direction == PressDirection::Up { + // For each currently dragged entity + for (drag_target, drag) in state.dragging.drain() { + // Emit DragDrop + for (dragged_over, hit) in state.dragging_over.iter() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragDrop { + button, + dropped: drag_target, + hit: hit.clone(), + }, + ), + *dragged_over, + ); + } + // Emit DragEnd + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragEnd { + button, + distance: drag.latest_pos - drag.start_pos, + }, + ), + drag_target, + ); + // Emit DragLeave + for (dragged_over, hit) in state.dragging_over.iter() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragLeave { + button, + dragged: drag_target, + hit: hit.clone(), + }, + ), + *dragged_over, + ); + } + } + } + + // Send a Down or possibly a Click and an Up button events + for (hovered_entity, hit) in previous_hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) + { + match direction { + PressDirection::Down => { + // Send the Down event first + let event = + Pointer::new(pointer_id, location.clone(), Down { button, hit }); + commands.trigger_targets(event.clone(), hovered_entity); + // Also insert the press into the state + state + .pressing + .insert(hovered_entity, (event.pointer_location, now)); + } + PressDirection::Up => { + // If this pointer previously pressed the hovered entity, first send a Click event + if let Some((_location, press_instant)) = + state.pressing.get(&hovered_entity) + { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Click { + button, + hit: hit.clone(), + duration: now - *press_instant, + }, + ), + hovered_entity, + ); + } + // Always send the Up event + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Up { + button, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + // Also clear the state + state.pressing.clear(); + state.dragging.clear(); + state.dragging_over.clear(); + } + }; + } + } + // Moved + PointerAction::Moved { delta } => { + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + { + // Send drag events to entities being dragged or dragged over + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + + // Emit a DragStart the first time the pointer moves while pressing an entity + for (location, _instant) in state.pressing.values() { + if state.dragging.contains_key(&hovered_entity) { + continue; // this entity is already logged as being dragged + } + state.dragging.insert( + hovered_entity, + DragEntry { + start_pos: location.position, + latest_pos: location.position, + }, + ); + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragStart { + button, + hit: hit.clone(), + }, + ), + hovered_entity, + ); + } + + // Emit a Drag event to the dragged entity when it is dragged over another entity. + for (dragged_entity, drag) in state.dragging.iter_mut() { + let drag_event = Drag { + button, + distance: location.position - drag.start_pos, + delta: location.position - drag.latest_pos, + }; + drag.latest_pos = location.position; + let target = *dragged_entity; + let event = Pointer::new(pointer_id, location.clone(), drag_event); + commands.trigger_targets(event, target); + } + + // Emit a DragOver to the hovered entity when dragging a different entity over it. + for drag_target in state.dragging.keys() + .filter( + |&&drag_target| hovered_entity != drag_target, /* can't drag over itself */ + ) + { + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), DragOver { button, dragged: *drag_target, hit: hit.clone() }), + hovered_entity, + ); + } + } + + // Always send Move event + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + Move { + hit: hit.clone(), + delta, + }, + ), + hovered_entity, + ); + } + } + // Canceled + PointerAction::Canceled => { + // Emit a Cancel to the hovered entity. + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + { + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Cancel { hit }), + hovered_entity, + ); + } + // Clear the local state for the canceled pointer + for button in PointerButton::iter() { + if let Some(state) = pointer_state.get_mut(&(pointer_id, button)) { + state.pressing.clear(); + state.dragging.clear(); + state.dragging_over.clear(); + } + } + } } } @@ -344,329 +592,31 @@ pub fn pointer_events( ); continue; }; - let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit }); - commands.trigger_targets(event.clone(), event.target); - pointer_out.send(event); - } - } -} - -/// Maps pointers to the entities they are dragging. -#[derive(Debug, Deref, DerefMut, Default, Resource)] -pub struct DragMap(pub HashMap<(PointerId, PointerButton), HashMap>); - -/// An entry in the [`DragMap`]. -#[derive(Debug, Clone)] -pub struct DragEntry { - /// The position of the pointer at drag start. - pub start_pos: Vec2, - /// The latest position of the pointer during this drag, used to compute deltas. - pub latest_pos: Vec2, -} - -/// Uses pointer events to determine when click and drag events occur. -#[allow(clippy::too_many_arguments)] -pub fn send_click_and_drag_events( - // for triggering observers - // - Pointer - // - Pointer - // - Pointer - mut commands: Commands, - // Input - mut pointer_down: EventReader>, - mut pointer_up: EventReader>, - mut input_move: EventReader, - mut input_presses: EventReader, - pointer_map: Res, - pointers: Query<&PointerLocation>, - // Locals - mut down_map: Local< - HashMap<(PointerId, PointerButton), HashMap, Instant)>>, - >, - // Outputs used for further processing - mut drag_map: ResMut, - mut pointer_drag_end: EventWriter>, -) { - let pointer_location = |pointer_id: PointerId| { - pointer_map - .get_entity(pointer_id) - .and_then(|entity| pointers.get(entity).ok()) - .and_then(|pointer| pointer.location.clone()) - }; - - // Triggers during movement even if not over an entity - for InputMove { - pointer_id, - location, - delta: _, - } in input_move.read().cloned() - { - for button in PointerButton::iter() { - let Some(down_list) = down_map.get(&(pointer_id, button)) else { - continue; - }; - let drag_list = drag_map.entry((pointer_id, button)).or_default(); - - for (down, _instant) in down_list.values() { - if drag_list.contains_key(&down.target) { - continue; // this entity is already logged as being dragged + // Possibly send DragLeave events + for button in PointerButton::iter() { + let state = pointer_state.entry((pointer_id, button)).or_default(); + state.dragging_over.remove(&hovered_entity); + for drag_target in state.dragging.keys() { + commands.trigger_targets( + Pointer::new( + pointer_id, + location.clone(), + DragLeave { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ), + hovered_entity, + ); } - drag_list.insert( - down.target, - DragEntry { - start_pos: down.pointer_location.position, - latest_pos: down.pointer_location.position, - }, - ); - let event = Pointer::new( - pointer_id, - down.pointer_location.clone(), - down.target, - DragStart { - button, - hit: down.hit.clone(), - }, - ); - commands.trigger_targets(event, down.target); } - for (dragged_entity, drag) in drag_list.iter_mut() { - let drag_event = Drag { - button, - distance: location.position - drag.start_pos, - delta: location.position - drag.latest_pos, - }; - drag.latest_pos = location.position; - let target = *dragged_entity; - let event = Pointer::new(pointer_id, location.clone(), target, drag_event); - commands.trigger_targets(event, target); - } - } - } - - // Triggers when button is released over an entity - let now = Instant::now(); - for Pointer { - pointer_id, - pointer_location, - target, - event: Up { button, hit }, - } in pointer_up.read().cloned() - { - // Can't have a click without the button being pressed down first - if let Some((_down, down_instant)) = down_map - .get(&(pointer_id, button)) - .and_then(|down| down.get(&target)) - { - let duration = now - *down_instant; - let event = Pointer::new( - pointer_id, - pointer_location, - target, - Click { - button, - hit, - duration, - }, - ); - commands.trigger_targets(event, target); - } - } - - // Triggers when button is pressed over an entity - for event in pointer_down.read() { - let button = event.button; - let down_button_entity_map = down_map.entry((event.pointer_id, button)).or_default(); - down_button_entity_map.insert(event.target, (event.clone(), now)); - } - - // Triggered for all button presses - for press in input_presses.read() { - if press.direction != PressDirection::Up { - continue; // We are only interested in button releases - } - down_map.insert((press.pointer_id, press.button), HashMap::new()); - let Some(drag_list) = drag_map.insert((press.pointer_id, press.button), HashMap::new()) - else { - continue; - }; - let Some(location) = pointer_location(press.pointer_id) else { - debug!( - "Unable to get location for pointer {:?} during event {:?}", - press.pointer_id, press - ); - continue; - }; - - for (drag_target, drag) in drag_list { - let drag_end = DragEnd { - button: press.button, - distance: drag.latest_pos - drag.start_pos, - }; - let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end); - commands.trigger_targets(event.clone(), event.target); - pointer_drag_end.send(event); - } - } -} - -/// Uses pointer events to determine when drag-over events occur -#[allow(clippy::too_many_arguments)] -pub fn send_drag_over_events( - // uses this to trigger the following - // - Pointer, - // - Pointer, - // - Pointer, - // - Pointer, - mut commands: Commands, - // Input - drag_map: Res, - mut pointer_over: EventReader>, - mut pointer_move: EventReader>, - mut pointer_out: EventReader>, - mut pointer_drag_end: EventReader>, - // Local - mut drag_over_map: Local>>, -) { - // Fire PointerDragEnter events. - for Pointer { - pointer_id, - pointer_location, - target, - event: Over { hit }, - } in pointer_over.read().cloned() - { - for button in PointerButton::iter() { - for drag_target in drag_map - .get(&(pointer_id, button)) - .iter() - .flat_map(|drag_list| drag_list.keys()) - .filter( - |&&drag_target| target != drag_target, /* can't drag over itself */ - ) - { - let drag_entry = drag_over_map.entry((pointer_id, button)).or_default(); - drag_entry.insert(target, hit.clone()); - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragEnter { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } - } - } - - // Fire PointerDragOver events. - for Pointer { - pointer_id, - pointer_location, - target, - event: Move { hit, delta: _ }, - } in pointer_move.read().cloned() - { - for button in PointerButton::iter() { - for drag_target in drag_map - .get(&(pointer_id, button)) - .iter() - .flat_map(|drag_list| drag_list.keys()) - .filter( - |&&drag_target| target != drag_target, /* can't drag over itself */ - ) - { - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragOver { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } - } - } - - // Fire PointerDragLeave and PointerDrop events when the pointer stops dragging. - for Pointer { - pointer_id, - pointer_location, - target: drag_end_target, - event: DragEnd { - button, - distance: _, - }, - } in pointer_drag_end.read().cloned() - { - let Some(drag_over_set) = drag_over_map.get_mut(&(pointer_id, button)) else { - continue; - }; - for (dragged_over, hit) in drag_over_set.drain() { - let target = dragged_over; - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - dragged_over, - DragLeave { - button, - dragged: drag_end_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragDrop { - button, - dropped: target, - hit: hit.clone(), - }, + // Always send Out events + commands.trigger_targets( + Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }), + hovered_entity, ); - commands.trigger_targets(event, target); - } - } - - // Fire PointerDragLeave events when the pointer goes out of the target. - for Pointer { - pointer_id, - pointer_location, - target, - event: Out { hit }, - } in pointer_out.read().cloned() - { - for button in PointerButton::iter() { - let Some(dragged_over) = drag_over_map.get_mut(&(pointer_id, button)) else { - continue; - }; - if dragged_over.remove(&target).is_none() { - continue; - } - let Some(drag_list) = drag_map.get(&(pointer_id, button)) else { - continue; - }; - for drag_target in drag_list.keys() { - let event = Pointer::new( - pointer_id, - pointer_location.clone(), - target, - DragLeave { - button, - dragged: *drag_target, - hit: hit.clone(), - }, - ); - commands.trigger_targets(event, target); - } } } } diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs index 8ae93ce2befad..c5fd0b989c96d 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/focus.rs @@ -1,11 +1,16 @@ //! Determines which entities are being hovered by which pointers. +//! +//! The most important type in this module is the [`HoverMap`], which maps pointers to the entities +//! they are hovering over. -use std::{collections::BTreeMap, fmt::Debug}; +use std::{ + collections::{BTreeMap, HashSet}, + fmt::Debug, +}; use crate::{ backend::{self, HitData}, - events::PointerCancel, - pointer::{PointerId, PointerInteraction, PointerPress}, + pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPress}, Pickable, }; @@ -63,7 +68,7 @@ pub fn update_focus( pickable: Query<&Pickable>, pointers: Query<&PointerId>, mut under_pointer: EventReader, - mut cancellations: EventReader, + mut pointer_input: EventReader, // Local mut over_map: Local, // Output @@ -76,7 +81,7 @@ pub fn update_focus( &mut over_map, &pointers, ); - build_over_map(&mut under_pointer, &mut over_map, &mut cancellations); + build_over_map(&mut under_pointer, &mut over_map, &mut pointer_input); build_hover_map(&pointers, pickable, &over_map, &mut hover_map); } @@ -109,9 +114,18 @@ fn reset_maps( fn build_over_map( backend_events: &mut EventReader, pointer_over_map: &mut Local, - pointer_cancel: &mut EventReader, + pointer_input: &mut EventReader, ) { - let cancelled_pointers: Vec = pointer_cancel.read().map(|p| p.pointer_id).collect(); + let cancelled_pointers: HashSet = pointer_input + .read() + .filter_map(|p| { + if let PointerAction::Canceled = p.action { + Some(p.pointer_id) + } else { + None + } + }) + .collect(); for entities_under_pointer in backend_events .read() diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs new file mode 100644 index 0000000000000..bef8b57c47aad --- /dev/null +++ b/crates/bevy_picking/src/input.rs @@ -0,0 +1,269 @@ +//! This module provides unsurprising default inputs to `bevy_picking` through [`PointerInput`]. +//! The included systems are responsible for sending mouse and touch inputs to their +//! respective `Pointer`s. +//! +//! Because this has it's own plugin, it's easy to omit it, and provide your own inputs as +//! needed. Because `Pointer`s aren't coupled to the underlying input hardware, you can easily mock +//! inputs, and allow users full accessibility to map whatever inputs they need to pointer input. +//! +//! If, for example, you wanted to add support for VR input, all you need to do is spawn a pointer +//! entity with a custom [`PointerId`], and write a system +//! that updates its position. If you want this to work properly with the existing interaction events, +//! you need to be sure that you also write a [`PointerInput`] event stream. + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_hierarchy::DespawnRecursiveExt; +use bevy_input::touch::{TouchInput, TouchPhase}; +use bevy_input::{prelude::*, ButtonState}; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; +use bevy_render::camera::RenderTarget; +use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_window::{PrimaryWindow, WindowEvent, WindowRef}; + +use crate::{ + pointer::{Location, PointerAction, PointerButton, PointerId, PointerInput, PressDirection}, + PointerBundle, +}; + +use crate::PickSet; + +/// The picking input prelude. +/// +/// This includes the most common types in this module, re-exported for your convenience. +pub mod prelude { + pub use crate::input::PointerInputPlugin; +} + +/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin, +/// that you can replace with your own plugin as needed. +/// +/// [`crate::PickingPlugin::is_input_enabled`] can be used to toggle whether +/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place. +/// +/// This plugin contains several settings, and is added to the world as a resource after initialization. +/// You can configure pointer input settings at runtime by accessing the resource. +#[derive(Copy, Clone, Resource, Debug, Reflect)] +#[reflect(Resource, Default)] +pub struct PointerInputPlugin { + /// Should touch inputs be updated? + pub is_touch_enabled: bool, + /// Should mouse inputs be updated? + pub is_mouse_enabled: bool, +} + +impl PointerInputPlugin { + fn is_mouse_enabled(state: Res) -> bool { + state.is_mouse_enabled + } + + fn is_touch_enabled(state: Res) -> bool { + state.is_touch_enabled + } +} + +impl Default for PointerInputPlugin { + fn default() -> Self { + Self { + is_touch_enabled: true, + is_mouse_enabled: true, + } + } +} + +impl Plugin for PointerInputPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(*self) + .add_systems(Startup, spawn_mouse_pointer) + .add_systems( + First, + ( + mouse_pick_events.run_if(PointerInputPlugin::is_mouse_enabled), + touch_pick_events.run_if(PointerInputPlugin::is_touch_enabled), + ) + .chain() + .in_set(PickSet::Input), + ) + .add_systems( + Last, + deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled), + ) + .register_type::() + .register_type::(); + } +} + +/// Spawns the default mouse pointer. +pub fn spawn_mouse_pointer(mut commands: Commands) { + commands.spawn((PointerBundle::new(PointerId::Mouse),)); +} + +/// Sends mouse pointer events to be processed by the core plugin +pub fn mouse_pick_events( + // Input + mut window_events: EventReader, + primary_window: Query>, + // Locals + mut cursor_last: Local, + // Output + mut pointer_events: EventWriter, +) { + for window_event in window_events.read() { + match window_event { + // Handle cursor movement events + WindowEvent::CursorMoved(event) => { + let location = Location { + target: match RenderTarget::Window(WindowRef::Entity(event.window)) + .normalize(primary_window.get_single().ok()) + { + Some(target) => target, + None => continue, + }, + position: event.position, + }; + pointer_events.send(PointerInput::new( + PointerId::Mouse, + location, + PointerAction::Moved { + delta: event.position - *cursor_last, + }, + )); + *cursor_last = event.position; + } + // Handle mouse button press events + WindowEvent::MouseButtonInput(input) => { + let location = Location { + target: match RenderTarget::Window(WindowRef::Entity(input.window)) + .normalize(primary_window.get_single().ok()) + { + Some(target) => target, + None => continue, + }, + position: *cursor_last, + }; + let button = match input.button { + MouseButton::Left => PointerButton::Primary, + MouseButton::Right => PointerButton::Secondary, + MouseButton::Middle => PointerButton::Middle, + MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue, + }; + let direction = match input.state { + ButtonState::Pressed => PressDirection::Down, + ButtonState::Released => PressDirection::Up, + }; + pointer_events.send(PointerInput::new( + PointerId::Mouse, + location, + PointerAction::Pressed { direction, button }, + )); + } + _ => {} + } + } +} + +/// Sends touch pointer events to be consumed by the core plugin +pub fn touch_pick_events( + // Input + mut window_events: EventReader, + primary_window: Query>, + // Locals + mut touch_cache: Local>, + // Output + mut commands: Commands, + mut pointer_events: EventWriter, +) { + for window_event in window_events.read() { + if let WindowEvent::TouchInput(touch) = window_event { + let pointer = PointerId::Touch(touch.id); + let location = Location { + target: match RenderTarget::Window(WindowRef::Entity(touch.window)) + .normalize(primary_window.get_single().ok()) + { + Some(target) => target, + None => continue, + }, + position: touch.position, + }; + match touch.phase { + TouchPhase::Started => { + debug!("Spawning pointer {:?}", pointer); + commands.spawn(PointerBundle::new(pointer).with_location(location.clone())); + + pointer_events.send(PointerInput::new( + pointer, + location, + PointerAction::Pressed { + direction: PressDirection::Down, + button: PointerButton::Primary, + }, + )); + + touch_cache.insert(touch.id, *touch); + } + TouchPhase::Moved => { + // Send a move event only if it isn't the same as the last one + if let Some(last_touch) = touch_cache.get(&touch.id) { + if last_touch == touch { + continue; + } + pointer_events.send(PointerInput::new( + pointer, + location, + PointerAction::Moved { + delta: touch.position - last_touch.position, + }, + )); + } + touch_cache.insert(touch.id, *touch); + } + TouchPhase::Ended => { + pointer_events.send(PointerInput::new( + pointer, + location, + PointerAction::Pressed { + direction: PressDirection::Up, + button: PointerButton::Primary, + }, + )); + touch_cache.remove(&touch.id); + } + TouchPhase::Canceled => { + pointer_events.send(PointerInput::new( + pointer, + location, + PointerAction::Canceled, + )); + touch_cache.remove(&touch.id); + } + } + } + } +} + +/// Deactivates unused touch pointers. +/// +/// Because each new touch gets assigned a new ID, we need to remove the pointers associated with +/// touches that are no longer active. +pub fn deactivate_touch_pointers( + mut commands: Commands, + mut despawn_list: Local>, + pointers: Query<(Entity, &PointerId)>, + mut touches: EventReader, +) { + for touch in touches.read() { + if let TouchPhase::Ended | TouchPhase::Canceled = touch.phase { + for (entity, pointer) in &pointers { + if pointer.get_touch_id() == Some(touch.id) { + despawn_list.insert((entity, *pointer)); + } + } + } + } + // A hash set is used to prevent despawning the same entity twice. + for (entity, pointer) in despawn_list.drain() { + debug!("Despawning pointer {:?}", pointer); + commands.entity(entity).despawn_recursive(); + } +} diff --git a/crates/bevy_picking/src/input/mod.rs b/crates/bevy_picking/src/input/mod.rs deleted file mode 100644 index e3dd9e3695e6d..0000000000000 --- a/crates/bevy_picking/src/input/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! `bevy_picking::input` is a thin layer that provides unsurprising default inputs to `bevy_picking`. -//! The included systems are responsible for sending mouse and touch inputs to their -//! respective `Pointer`s. -//! -//! Because this resides in its own crate, it's easy to omit it, and provide your own inputs as -//! needed. Because `Pointer`s aren't coupled to the underlying input hardware, you can easily mock -//! inputs, and allow users full accessibility to map whatever inputs they need to pointer input. -//! -//! If, for example, you wanted to add support for VR input, all you need to do is spawn a pointer -//! entity with a custom [`PointerId`](crate::pointer::PointerId), and write a system -//! that updates its position. -//! -//! TODO: Update docs - -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use bevy_reflect::prelude::*; - -use crate::PickSet; - -pub mod mouse; -pub mod touch; - -/// Common imports for `bevy_picking_input`. -pub mod prelude { - pub use crate::input::InputPlugin; -} - -/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin, -/// that you can replace with your own plugin as needed. -/// -/// [`crate::PickingPluginsSettings::is_input_enabled`] can be used to toggle whether -/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place. -#[derive(Copy, Clone, Resource, Debug, Reflect)] -#[reflect(Resource, Default)] -pub struct InputPlugin { - /// Should touch inputs be updated? - pub is_touch_enabled: bool, - /// Should mouse inputs be updated? - pub is_mouse_enabled: bool, -} - -impl InputPlugin { - fn is_mouse_enabled(state: Res) -> bool { - state.is_mouse_enabled - } - - fn is_touch_enabled(state: Res) -> bool { - state.is_touch_enabled - } -} - -impl Default for InputPlugin { - fn default() -> Self { - Self { - is_touch_enabled: true, - is_mouse_enabled: true, - } - } -} - -impl Plugin for InputPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(*self) - .add_systems(Startup, mouse::spawn_mouse_pointer) - .add_systems( - First, - ( - mouse::mouse_pick_events.run_if(InputPlugin::is_mouse_enabled), - touch::touch_pick_events.run_if(InputPlugin::is_touch_enabled), - // IMPORTANT: the commands must be flushed after `touch_pick_events` is run - // because we need pointer spawning to happen immediately to prevent issues with - // missed events during drag and drop. - apply_deferred, - ) - .chain() - .in_set(PickSet::Input), - ) - .add_systems( - Last, - touch::deactivate_touch_pointers.run_if(InputPlugin::is_touch_enabled), - ) - .register_type::() - .register_type::(); - } -} diff --git a/crates/bevy_picking/src/input/mouse.rs b/crates/bevy_picking/src/input/mouse.rs deleted file mode 100644 index 73cf321f61165..0000000000000 --- a/crates/bevy_picking/src/input/mouse.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Provides sensible defaults for mouse picking inputs. - -use bevy_ecs::prelude::*; -use bevy_input::{mouse::MouseButtonInput, prelude::*, ButtonState}; -use bevy_math::Vec2; -use bevy_render::camera::RenderTarget; -use bevy_window::{CursorMoved, PrimaryWindow, Window, WindowRef}; - -use crate::{ - pointer::{InputMove, InputPress, Location, PointerButton, PointerId}, - PointerBundle, -}; - -/// Spawns the default mouse pointer. -pub fn spawn_mouse_pointer(mut commands: Commands) { - commands.spawn((PointerBundle::new(PointerId::Mouse),)); -} - -/// Sends mouse pointer events to be processed by the core plugin -pub fn mouse_pick_events( - // Input - windows: Query<(Entity, &Window), With>, - mut cursor_moves: EventReader, - mut cursor_last: Local, - mut mouse_inputs: EventReader, - // Output - mut pointer_move: EventWriter, - mut pointer_presses: EventWriter, -) { - for event in cursor_moves.read() { - pointer_move.send(InputMove::new( - PointerId::Mouse, - Location { - target: RenderTarget::Window(WindowRef::Entity(event.window)) - .normalize(Some( - match windows.get_single() { - Ok(w) => w, - Err(_) => continue, - } - .0, - )) - .unwrap(), - position: event.position, - }, - event.position - *cursor_last, - )); - *cursor_last = event.position; - } - - for input in mouse_inputs.read() { - let button = match input.button { - MouseButton::Left => PointerButton::Primary, - MouseButton::Right => PointerButton::Secondary, - MouseButton::Middle => PointerButton::Middle, - MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue, - }; - - match input.state { - ButtonState::Pressed => { - pointer_presses.send(InputPress::new_down(PointerId::Mouse, button)); - } - ButtonState::Released => { - pointer_presses.send(InputPress::new_up(PointerId::Mouse, button)); - } - } - } -} diff --git a/crates/bevy_picking/src/input/touch.rs b/crates/bevy_picking/src/input/touch.rs deleted file mode 100644 index b6b7e6a33c85c..0000000000000 --- a/crates/bevy_picking/src/input/touch.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Provides sensible defaults for touch picking inputs. - -use bevy_ecs::prelude::*; -use bevy_hierarchy::DespawnRecursiveExt; -use bevy_input::touch::{TouchInput, TouchPhase}; -use bevy_math::Vec2; -use bevy_render::camera::RenderTarget; -use bevy_utils::{tracing::debug, HashMap, HashSet}; -use bevy_window::{PrimaryWindow, WindowRef}; - -use crate::{ - events::PointerCancel, - pointer::{InputMove, InputPress, Location, PointerButton, PointerId}, - PointerBundle, -}; - -/// Sends touch pointer events to be consumed by the core plugin -/// -/// IMPORTANT: the commands must be flushed after this system is run because we need spawning to -/// happen immediately to prevent issues with missed events needed for drag and drop. -pub fn touch_pick_events( - // Input - mut touches: EventReader, - primary_window: Query>, - // Local - mut location_cache: Local>, - // Output - mut commands: Commands, - mut input_moves: EventWriter, - mut input_presses: EventWriter, - mut cancel_events: EventWriter, -) { - for touch in touches.read() { - let pointer = PointerId::Touch(touch.id); - let location = Location { - target: match RenderTarget::Window(WindowRef::Entity(touch.window)) - .normalize(primary_window.get_single().ok()) - { - Some(target) => target, - None => continue, - }, - position: touch.position, - }; - match touch.phase { - TouchPhase::Started => { - debug!("Spawning pointer {:?}", pointer); - commands.spawn((PointerBundle::new(pointer).with_location(location.clone()),)); - - input_moves.send(InputMove::new(pointer, location, Vec2::ZERO)); - input_presses.send(InputPress::new_down(pointer, PointerButton::Primary)); - location_cache.insert(touch.id, *touch); - } - TouchPhase::Moved => { - // Send a move event only if it isn't the same as the last one - if let Some(last_touch) = location_cache.get(&touch.id) { - if last_touch == touch { - continue; - } - input_moves.send(InputMove::new( - pointer, - location, - touch.position - last_touch.position, - )); - } - location_cache.insert(touch.id, *touch); - } - TouchPhase::Ended | TouchPhase::Canceled => { - input_presses.send(InputPress::new_up(pointer, PointerButton::Primary)); - location_cache.remove(&touch.id); - cancel_events.send(PointerCancel { - pointer_id: pointer, - }); - } - } - } -} - -/// Deactivates unused touch pointers. -/// -/// Because each new touch gets assigned a new ID, we need to remove the pointers associated with -/// touches that are no longer active. -pub fn deactivate_touch_pointers( - mut commands: Commands, - mut despawn_list: Local>, - pointers: Query<(Entity, &PointerId)>, - mut touches: EventReader, -) { - for touch in touches.read() { - match touch.phase { - TouchPhase::Ended | TouchPhase::Canceled => { - for (entity, pointer) in &pointers { - if pointer.get_touch_id() == Some(touch.id) { - despawn_list.insert((entity, *pointer)); - } - } - } - _ => {} - } - } - // A hash set is used to prevent despawning the same entity twice. - for (entity, pointer) in despawn_list.drain() { - debug!("Despawning pointer {:?}", pointer); - commands.entity(entity).despawn_recursive(); - } -} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index cd75515c97422..29d294913b025 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -1,4 +1,152 @@ -//! TODO, write module doc +//! This crate provides 'picking' capabilities for the Bevy game engine. That means, in simple terms, figuring out +//! how to connect up a user's clicks or taps to the entities they are trying to interact with. +//! +//! ## Overview +//! +//! In the simplest case, this plugin allows you to click on things in the scene. However, it also +//! allows you to express more complex interactions, like detecting when a touch input drags a UI +//! element and drops it on a 3d mesh rendered to a different camera. The crate also provides a set of +//! interaction callbacks, allowing you to receive input directly on entities like here: +//! +//! ```rust +//! # use bevy_ecs::prelude::*; +//! # use bevy_picking::prelude::*; +//! # #[derive(Component)] +//! # struct MyComponent; +//! # let mut world = World::new(); +//! world.spawn(MyComponent) +//! .observe(|mut trigger: Trigger>| { +//! // Get the underlying event type +//! let click_event: &Pointer = trigger.event(); +//! // Stop the event from bubbling up the entity hierarchjy +//! trigger.propagate(false); +//! }); +//! ``` +//! +//! At its core, this crate provides a robust abstraction for computing picking state regardless of +//! pointing devices, or what you are hit testing against. It is designed to work with any input, including +//! mouse, touch, pens, or virtual pointers controlled by gamepads. +//! +//! ## Expressive Events +//! +//! The events in this module (see [`events`]) cannot be listened to with normal `EventReader`s. +//! Instead, they are dispatched to *ovservers* attached to specific entities. When events are generated, they +//! bubble up the entity hierarchy starting from their target, until they reach the root or bubbling is haulted +//! with a call to [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). +//! See [`Observer`] for details. +//! +//! This allows you to run callbacks when any children of an entity are interacted with, and leads +//! to succinct, expressive code: +//! +//! ``` +//! # use bevy_ecs::prelude::*; +//! # use bevy_transform::prelude::*; +//! # use bevy_picking::prelude::*; +//! # #[derive(Event)] +//! # struct Greeting; +//! fn setup(mut commands: Commands) { +//! commands.spawn(Transform::default()) +//! // Spawn your entity here, e.g. a Mesh. +//! // When dragged, mutate the `Transform` component on the dragged target entity: +//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { +//! let mut transform = transforms.get_mut(trigger.entity()).unwrap(); +//! let drag = trigger.event(); +//! transform.rotate_local_y(drag.delta.x / 50.0); +//! }) +//! .observe(|trigger: Trigger>, mut commands: Commands| { +//! println!("Entity {:?} goes BOOM!", trigger.entity()); +//! commands.entity(trigger.entity()).despawn(); +//! }) +//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! events.send(Greeting); +//! }); +//! } +//! ``` +//! +//! ## Modularity +//! +//! #### Mix and Match Hit Testing Backends +//! +//! The plugin attempts to handle all the hard parts for you, all you need to do is tell it when a +//! pointer is hitting any entities. Multiple backends can be used at the same time! [Use this +//! simple API to write your own backend](crate::backend) in about 100 lines of code. +//! +//! #### Input Agnostic +//! +//! Picking provides a generic Pointer abstracton, which is useful for reacting to many different +//! types of input devices. Pointers can be controlled with anything, whether its the included mouse +//! or touch inputs, or a custom gamepad input system you write yourself to control a virtual pointer. +//! +//! ## Robustness +//! +//! In addition to these features, this plugin also correctly handles multitouch, multiple windows, +//! multiple cameras, viewports, and render layers. Using this as a library allows you to write a +//! picking backend that can interoperate with any other picking backend. +//! +//! # Getting Started +//! +//! TODO: This section will need to be re-written once more backends are introduced. +//! +//! #### Next Steps +//! +//! To learn more, take a look at the examples in the +//! [examples](https://github.com/bevyengine/bevy/tree/main/examples/picking). You +//! can read the next section to understand how the plugin works. +//! +//! # The Picking Pipeline +//! +//! This plugin is designed to be extremely modular. To do so, it works in well-defined stages that +//! form a pipeline, where events are used to pass data between each stage. +//! +//! #### Pointers ([`pointer`](mod@pointer)) +//! +//! The first stage of the pipeline is to gather inputs and update pointers. This stage is +//! ultimately responsible for generating [`PointerInput`](pointer::PointerInput) events. The provided +//! crate does this automatically for mouse, touch, and pen inputs. If you wanted to implement your own +//! pointer, controlled by some other input, you can do that here. The ordering of events within the +//! [`PointerInput`](pointer::PointerInput) stream is meaningful for events with the same +//! [`PointerId`](pointer::PointerId), but not between different pointers. +//! +//! Because pointer positions and presses are driven by these events, you can use them to mock +//! inputs for testing. +//! +//! After inputs are generated, they are then collected to update the current +//! [`PointerLocation`](pointer::PointerLocation) for each pointer. +//! +//! #### Backend ([`backend`]) +//! +//! A picking backend only has one job: reading [`PointerLocation`](pointer::PointerLocation) components, +//! and producing [`PointerHits`](backend::PointerHits). You can find all documentation and types needed to +//! implement a backend at [`backend`]. +//! +//! You will eventually need to choose which picking backend(s) you want to use. This crate does not +//! supply any backends, and expects you to select some from the other bevy crates or the third-party +//! ecosystem. You can find all the provided backends in the [`backend`] module. +//! +//! It's important to understand that you can mix and match backends! For example, you might have a +//! backend for your UI, and one for the 3d scene, with each being specialized for their purpose. +//! This crate provides some backends out of the box, but you can even write your own. It's been +//! made as easy as possible intentionally; the `bevy_mod_raycast` backend is 50 lines of code. +//! +//! #### Focus ([`focus`]) +//! +//! The next step is to use the data from the backends, combine and sort the results, and determine +//! what each cursor is hovering over, producing a [`HoverMap`](`crate::focus::HoverMap`). Note that +//! just because a pointer is over an entity, it is not necessarily *hovering* that entity. Although +//! multiple backends may be reporting that a pointer is hitting an entity, the focus system needs +//! to determine which entities are actually being hovered by this pointer based on the pick depth, +//! order of the backend, and the [`Pickable`] state of the entity. In other words, if one entity is +//! in front of another, usually only the topmost one will be hovered. +//! +//! #### Events ([`events`]) +//! +//! In the final step, the high-level pointer events are generated, such as events that trigger when +//! a pointer hovers or clicks an entity. These simple events are then used to generate more complex +//! events for dragging and dropping. +//! +//! Because it is completely agnostic to the the earlier stages of the pipeline, you can easily +//! extend the plugin with arbitrary backends and input methods, yet still use all the high level +//! features. #![deny(missing_docs)] @@ -12,49 +160,17 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; -/// common exports for picking interaction +/// The picking prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ - events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins, - InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings, + events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, + InteractionPlugin, Pickable, PickingPlugin, }; } -/// Used to globally toggle picking features at runtime. -#[derive(Clone, Debug, Resource, Reflect)] -#[reflect(Resource, Default)] -pub struct PickingPluginsSettings { - /// Enables and disables all picking features. - pub is_enabled: bool, - /// Enables and disables input collection. - pub is_input_enabled: bool, - /// Enables and disables updating interaction states of entities. - pub is_focus_enabled: bool, -} - -impl PickingPluginsSettings { - /// Whether or not input collection systems should be running. - pub fn input_should_run(state: Res) -> bool { - state.is_input_enabled && state.is_enabled - } - /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) - /// component should be running. - pub fn focus_should_run(state: Res) -> bool { - state.is_focus_enabled && state.is_enabled - } -} - -impl Default for PickingPluginsSettings { - fn default() -> Self { - Self { - is_enabled: true, - is_input_enabled: true, - is_focus_enabled: true, - } - } -} - /// An optional component that overrides default picking behavior for an entity, allowing you to /// make an entity non-hoverable, or allow items below it to be hovered. See the documentation on /// the fields for more details. @@ -176,8 +292,9 @@ pub enum PickSet { Last, } -/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`], -/// this is probably the plugin that will be most used. +/// One plugin that contains the [`PointerInputPlugin`](input::PointerInputPlugin), [`PickingPlugin`] +/// and the [`InteractionPlugin`], this is probably the plugin that will be most used. +/// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] /// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. @@ -187,8 +304,8 @@ pub struct DefaultPickingPlugins; impl Plugin for DefaultPickingPlugins { fn build(&self, app: &mut App) { app.add_plugins(( - input::InputPlugin::default(), - PickingPlugin, + input::PointerInputPlugin::default(), + PickingPlugin::default(), InteractionPlugin, )); } @@ -196,16 +313,48 @@ impl Plugin for DefaultPickingPlugins { /// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared /// types used by other picking plugins. -#[derive(Default)] -pub struct PickingPlugin; +/// +/// This plugin contains several settings, and is added to the wrold as a resource after initialization. You +/// can configure picking settings at runtime through the resource. +#[derive(Copy, Clone, Debug, Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct PickingPlugin { + /// Enables and disables all picking features. + pub is_enabled: bool, + /// Enables and disables input collection. + pub is_input_enabled: bool, + /// Enables and disables updating interaction states of entities. + pub is_focus_enabled: bool, +} + +impl PickingPlugin { + /// Whether or not input collection systems should be running. + pub fn input_should_run(state: Res) -> bool { + state.is_input_enabled && state.is_enabled + } + /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) + /// component should be running. + pub fn focus_should_run(state: Res) -> bool { + state.is_focus_enabled && state.is_enabled + } +} + +impl Default for PickingPlugin { + fn default() -> Self { + Self { + is_enabled: true, + is_input_enabled: true, + is_focus_enabled: true, + } + } +} impl Plugin for PickingPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.insert_resource(*self) .init_resource::() .init_resource::() - .add_event::() - .add_event::() + .add_event::() .add_event::() // Rather than try to mark all current and future backends as ambiguous with each other, // we allow them to send their hits in any order. These are later sorted, so submission @@ -215,9 +364,8 @@ impl Plugin for PickingPlugin { PreUpdate, ( pointer::update_pointer_map, - pointer::InputMove::receive, - pointer::InputPress::receive, - backend::ray::RayMap::repopulate.after(pointer::InputMove::receive), + pointer::PointerInput::receive, + backend::ray::RayMap::repopulate.after(pointer::PointerInput::receive), ) .in_set(PickSet::ProcessInput), ) @@ -225,29 +373,26 @@ impl Plugin for PickingPlugin { First, (PickSet::Input, PickSet::PostInput) .after(bevy_time::TimeSystem) - .ambiguous_with(bevy_asset::handle_internal_asset_events) .after(bevy_ecs::event::EventUpdates) .chain(), ) .configure_sets( PreUpdate, ( - PickSet::ProcessInput.run_if(PickingPluginsSettings::input_should_run), + PickSet::ProcessInput.run_if(Self::input_should_run), PickSet::Backend, - PickSet::Focus.run_if(PickingPluginsSettings::focus_should_run), + PickSet::Focus.run_if(Self::focus_should_run), PickSet::PostFocus, - // Eventually events will need to be dispatched here PickSet::Last, ) - .ambiguous_with(bevy_asset::handle_internal_asset_events) .chain(), ) + .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() .register_type::() - .register_type::() - .register_type::() .register_type::(); } } @@ -263,23 +408,9 @@ impl Plugin for InteractionPlugin { app.init_resource::() .init_resource::() - .init_resource::() - .add_event::() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() - .add_event::>() .add_systems( PreUpdate, - ( - update_focus, - pointer_events, - update_interactions, - send_click_and_drag_events, - send_drag_over_events, - ) + (update_focus, pointer_events, update_interactions) .chain() .in_set(PickSet::Focus), ); diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 3d1991c1ec246..01c292bc1dec7 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -1,4 +1,12 @@ //! Types and systems for pointer inputs, such as position and buttons. +//! +//! The picking system is built around the concept of a 'Pointer', which is an +//! abstract representation of a user input with a specific screen location. The cursor +//! and touch input is provided under [`crate::input`], but you can also implement +//! your own custom pointers by supplying a unique ID. +//! +//! The purpose of this module is primarily to provide a common interface that can be +//! driven by lower-level input devices and consumed by higher-level interaction systems. use bevy_ecs::prelude::*; use bevy_math::{Rect, Vec2}; @@ -83,7 +91,7 @@ pub fn update_pointer_map(pointers: Query<(Entity, &PointerId)>, mut map: ResMut } } -/// Tracks the state of the pointer's buttons in response to [`InputPress`]s. +/// Tracks the state of the pointer's buttons in response to [`PointerInput`] events. #[derive(Debug, Default, Clone, Component, Reflect, PartialEq, Eq)] #[reflect(Component, Default)] pub struct PointerPress { @@ -118,68 +126,6 @@ impl PointerPress { } } -/// Pointer input event for button presses. Fires when a pointer button changes state. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)] -pub struct InputPress { - /// The [`PointerId`] of the pointer that pressed a button. - pub pointer_id: PointerId, - /// Direction of the button press. - pub direction: PressDirection, - /// Identifies the pointer button changing in this event. - pub button: PointerButton, -} - -impl InputPress { - /// Create a new pointer button down event. - pub fn new_down(id: PointerId, button: PointerButton) -> InputPress { - Self { - pointer_id: id, - direction: PressDirection::Down, - button, - } - } - - /// Create a new pointer button up event. - pub fn new_up(id: PointerId, button: PointerButton) -> InputPress { - Self { - pointer_id: id, - direction: PressDirection::Up, - button, - } - } - - /// Returns true if the `button` of this pointer was just pressed. - #[inline] - pub fn is_just_down(&self, button: PointerButton) -> bool { - self.button == button && self.direction == PressDirection::Down - } - - /// Returns true if the `button` of this pointer was just released. - #[inline] - pub fn is_just_up(&self, button: PointerButton) -> bool { - self.button == button && self.direction == PressDirection::Up - } - - /// Receives [`InputPress`] events and updates corresponding [`PointerPress`] components. - pub fn receive( - mut events: EventReader, - mut pointers: Query<(&PointerId, &mut PointerPress)>, - ) { - for input_press_event in events.read() { - pointers.iter_mut().for_each(|(pointer_id, mut pointer)| { - if *pointer_id == input_press_event.pointer_id { - let is_down = input_press_event.direction == PressDirection::Down; - match input_press_event.button { - PointerButton::Primary => pointer.primary = is_down, - PointerButton::Secondary => pointer.secondary = is_down, - PointerButton::Middle => pointer.middle = is_down, - } - } - }); - } - } -} - /// The stage of the pointer button press event #[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] pub enum PressDirection { @@ -225,42 +171,6 @@ impl PointerLocation { } } -/// Pointer input event for pointer moves. Fires when a pointer changes location. -#[derive(Event, Debug, Clone, Reflect)] -pub struct InputMove { - /// The [`PointerId`] of the pointer that is moving. - pub pointer_id: PointerId, - /// The [`Location`] of the pointer. - pub location: Location, - /// The distance moved (change in `position`) since the last event. - pub delta: Vec2, -} - -impl InputMove { - /// Create a new [`InputMove`] event. - pub fn new(id: PointerId, location: Location, delta: Vec2) -> InputMove { - Self { - pointer_id: id, - location, - delta, - } - } - - /// Receives [`InputMove`] events and updates corresponding [`PointerLocation`] components. - pub fn receive( - mut events: EventReader, - mut pointers: Query<(&PointerId, &mut PointerLocation)>, - ) { - for event_pointer in events.read() { - pointers.iter_mut().for_each(|(id, mut pointer)| { - if *id == event_pointer.pointer_id { - pointer.location = Some(event_pointer.location.to_owned()); - } - }); - } - } -} - /// The location of a pointer, including the current [`NormalizedRenderTarget`], and the x/y /// position of the pointer on this render target. /// @@ -309,3 +219,79 @@ impl Location { .unwrap_or(false) } } + +/// Types of actions that can be taken by pointers. +#[derive(Debug, Clone, Copy, Reflect)] +pub enum PointerAction { + /// A button has been pressed on the pointer. + Pressed { + /// The press direction, either down or up. + direction: PressDirection, + /// The button that was pressed. + button: PointerButton, + }, + /// The pointer has moved. + Moved { + /// How much the pointer moved from the previous position. + delta: Vec2, + }, + /// The pointer has been canceled. The OS can cause this to happen to touch events. + Canceled, +} + +/// An input event effecting a pointer. +#[derive(Event, Debug, Clone, Reflect)] +pub struct PointerInput { + /// The id of the pointer. + pub pointer_id: PointerId, + /// The location of the pointer. For [[`PointerAction::Moved`]], this is the location after the movement. + pub location: Location, + /// The action that the event describes. + pub action: PointerAction, +} + +impl PointerInput { + /// Creates a new pointer input event. + /// + /// Note that `location` refers to the position of the pointer *after* the event occurred. + pub fn new(pointer_id: PointerId, location: Location, action: PointerAction) -> PointerInput { + PointerInput { + pointer_id, + location, + action, + } + } + + /// Updates pointer entities according to the input events. + pub fn receive( + mut events: EventReader, + mut pointers: Query<(&PointerId, &mut PointerLocation, &mut PointerPress)>, + ) { + for event in events.read() { + match event.action { + PointerAction::Pressed { direction, button } => { + pointers + .iter_mut() + .for_each(|(pointer_id, _, mut pointer)| { + if *pointer_id == event.pointer_id { + let is_down = direction == PressDirection::Down; + match button { + PointerButton::Primary => pointer.primary = is_down, + PointerButton::Secondary => pointer.secondary = is_down, + PointerButton::Middle => pointer.middle = is_down, + } + } + }); + } + PointerAction::Moved { .. } => { + pointers.iter_mut().for_each(|(id, mut pointer, _)| { + if *id == event.pointer_id { + pointer.location = Some(event.location.to_owned()); + } + }); + } + _ => {} + } + } + } +} diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 24ea602296256..5b2186c44fbc5 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -12,7 +12,7 @@ use core::{ fmt::{self, Formatter, Pointer}, marker::PhantomData, mem::{align_of, ManuallyDrop}, - num::NonZeroUsize, + num::NonZero, ptr::NonNull, }; @@ -535,10 +535,10 @@ impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> { /// Creates a dangling pointer with specified alignment. /// See [`NonNull::dangling`]. -pub fn dangling_with_align(align: NonZeroUsize) -> NonNull { +pub fn dangling_with_align(align: NonZero) -> NonNull { debug_assert!(align.is_power_of_two(), "Alignment must be power of two."); // SAFETY: The pointer will not be null, since it was created - // from the address of a `NonZeroUsize`. + // from the address of a `NonZero`. unsafe { NonNull::new_unchecked(align.get() as *mut u8) } } diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 5cfbf833009fe..65ded3f8deb6b 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -37,7 +37,7 @@ smallvec = { version = "1.11", optional = true } glam = { version = "0.28", features = ["serde"], optional = true } petgraph = { version = "0.6", features = ["serde-1"], optional = true } -smol_str = { version = "0.2.0", optional = true } +smol_str = { version = "0.2.0", features = ["serde"], optional = true } uuid = { version = "1.0", optional = true, features = ["v4", "serde"] } [dev-dependencies] diff --git a/crates/bevy_reflect/src/impls/smol_str.rs b/crates/bevy_reflect/src/impls/smol_str.rs index d8e3251c91d35..d8bd625c1f28f 100644 --- a/crates/bevy_reflect/src/impls/smol_str.rs +++ b/crates/bevy_reflect/src/impls/smol_str.rs @@ -1,8 +1,15 @@ -use crate::std_traits::ReflectDefault; use crate::{self as bevy_reflect}; +use crate::{std_traits::ReflectDefault, ReflectDeserialize, ReflectSerialize}; use bevy_reflect_derive::impl_reflect_value; -impl_reflect_value!(::smol_str::SmolStr(Debug, Hash, PartialEq, Default)); +impl_reflect_value!(::smol_str::SmolStr( + Debug, + Hash, + PartialEq, + Default, + Serialize, + Deserialize, +)); #[cfg(test)] mod tests { diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 6320090e6d4f6..c9f27a4cb34a4 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -2444,11 +2444,11 @@ mod tests { #[test] fn nonzero_usize_impl_reflect_from_reflect() { - let a: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); - let b: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); + let a: &dyn PartialReflect = &std::num::NonZero::::new(42).unwrap(); + let b: &dyn PartialReflect = &std::num::NonZero::::new(42).unwrap(); assert!(a.reflect_partial_eq(b).unwrap_or_default()); - let forty_two: std::num::NonZeroUsize = FromReflect::from_reflect(a).unwrap(); - assert_eq!(forty_two, std::num::NonZeroUsize::new(42).unwrap()); + let forty_two: std::num::NonZero = FromReflect::from_reflect(a).unwrap(); + assert_eq!(forty_two, std::num::NonZero::::new(42).unwrap()); } #[test] diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 29829130feeac..64216bfc09467 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -564,6 +564,9 @@ pub mod serde; pub mod std_traits; pub mod utility; +/// The reflect prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { pub use crate::std_traits::*; #[doc(hidden)] diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 81fb920f3c444..9e4a4ab9fdc30 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -212,13 +212,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { visibility.hygienic_quote("e! { #render_path::render_resource }); let field_name = field.ident.as_ref().unwrap(); - let field_ty = &field.ty; - - let min_binding_size = if buffer { - quote! {None} - } else { - quote! {Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size())} - }; if buffer { binding_impls.push(quote! { @@ -230,21 +223,15 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }); } else { - binding_impls.push(quote! {{ - use #render_path::render_resource::AsBindGroupShaderType; - let mut buffer = #render_path::render_resource::encase::StorageBuffer::new(Vec::new()); - buffer.write(&self.#field_name).unwrap(); - ( - #binding_index, - #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( - &#render_path::render_resource::BufferInitDescriptor { - label: None, - usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE, - contents: buffer.as_ref(), - }, - )) - ) - }}); + binding_impls.push(quote! { + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer({ + let handle: &#asset_path::Handle<#render_path::storage::ShaderStorageBuffer> = (&self.#field_name); + storage_buffers.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.buffer.clone() + }) + ) + }); } binding_layouts.push(quote! { @@ -254,7 +241,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ty: #render_path::render_resource::BindingType::Buffer { ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only }, has_dynamic_offset: false, - min_binding_size: #min_binding_size, + min_binding_size: None, }, count: None, } @@ -527,6 +514,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { type Param = ( #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::texture::GpuImage>>, #ecs_path::system::lifetimeless::SRes<#render_path::texture::FallbackImage>, + #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::storage::GpuShaderStorageBuffer>>, ); fn label() -> Option<&'static str> { @@ -537,7 +525,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { &self, layout: &#render_path::render_resource::BindGroupLayout, render_device: &#render_path::renderer::RenderDevice, - (images, fallback_image): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, + (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { let bindings = vec![#(#binding_impls,)*]; diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 78dbec4a370cc..426450be8a3c8 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -187,6 +187,34 @@ impl Default for PhysicalCameraParameters { } } +/// Error returned when a conversion between world-space and viewport-space coordinates fails. +/// +/// See [`world_to_viewport`][Camera::world_to_viewport] and [`viewport_to_world`][Camera::viewport_to_world]. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum ViewportConversionError { + /// The pre-computed size of the viewport was not available. + /// + /// This may be because the `Camera` was just created and [`camera_system`] has not been executed + /// yet, or because the [`RenderTarget`] is misconfigured in one of the following ways: + /// - it references the [`PrimaryWindow`](RenderTarget::Window) when there is none, + /// - it references a [`Window`](RenderTarget::Window) entity that doesn't exist or doesn't actually have a `Window` component, + /// - it references an [`Image`](RenderTarget::Image) that doesn't exist (invalid handle), + /// - it references a [`TextureView`](RenderTarget::TextureView) that doesn't exist (invalid handle). + NoViewportSize, + /// The computed coordinate was beyond the `Camera`'s near plane. + /// + /// Only applicable when converting from world-space to viewport-space. + PastNearPlane, + /// The computed coordinate was beyond the `Camera`'s far plane. + /// + /// Only applicable when converting from world-space to viewport-space. + PastFarPlane, + /// The Normalized Device Coordinates could not be computed because the `camera_transform`, the + /// `world_position`, or the projection matrix defined by [`CameraProjection`] contained `NAN` + /// (see [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]). + InvalidData, +} + /// The defining [`Component`] for camera entities, /// storing information about how and what to render through this camera. /// @@ -348,29 +376,35 @@ impl Camera { /// To get the coordinates in Normalized Device Coordinates, you should use /// [`world_to_ndc`](Self::world_to_ndc). /// - /// Returns `None` if any of these conditions occur: - /// - The computed coordinates are beyond the near or far plane - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc) - /// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc). + /// # Panics + /// + /// Will panic if `glam_assert` is enabled and the `camera_transform` contains `NAN` + /// (see [`world_to_ndc`][Self::world_to_ndc]). #[doc(alias = "world_to_screen")] pub fn world_to_viewport( &self, camera_transform: &GlobalTransform, world_position: Vec3, - ) -> Option { - let target_size = self.logical_viewport_size()?; - let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .ok_or(ViewportConversionError::NoViewportSize)?; + let ndc_space_coords = self + .world_to_ndc(camera_transform, world_position) + .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space - if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 { - return None; + if ndc_space_coords.z < 0.0 { + return Err(ViewportConversionError::PastNearPlane); + } + if ndc_space_coords.z > 1.0 { + return Err(ViewportConversionError::PastFarPlane); } // Once in NDC space, we can discard the z element and rescale x/y to fit the screen let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size; // Flip the Y co-ordinate origin from the bottom to the top. viewport_position.y = target_size.y - viewport_position.y; - Some(viewport_position) + Ok(viewport_position) } /// Given a position in world space, use the camera to compute the viewport-space coordinates and depth. @@ -378,22 +412,28 @@ impl Camera { /// To get the coordinates in Normalized Device Coordinates, you should use /// [`world_to_ndc`](Self::world_to_ndc). /// - /// Returns `None` if any of these conditions occur: - /// - The computed coordinates are beyond the near or far plane - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc) - /// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc). + /// # Panics + /// + /// Will panic if `glam_assert` is enabled and the `camera_transform` contains `NAN` + /// (see [`world_to_ndc`][Self::world_to_ndc]). #[doc(alias = "world_to_screen_with_depth")] pub fn world_to_viewport_with_depth( &self, camera_transform: &GlobalTransform, world_position: Vec3, - ) -> Option { - let target_size = self.logical_viewport_size()?; - let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .ok_or(ViewportConversionError::NoViewportSize)?; + let ndc_space_coords = self + .world_to_ndc(camera_transform, world_position) + .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space - if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 { - return None; + if ndc_space_coords.z < 0.0 { + return Err(ViewportConversionError::PastNearPlane); + } + if ndc_space_coords.z > 1.0 { + return Err(ViewportConversionError::PastFarPlane); } // Stretching ndc depth to value via near plane and negating result to be in positive room again. @@ -403,7 +443,7 @@ impl Camera { let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size; // Flip the Y co-ordinate origin from the bottom to the top. viewport_position.y = target_size.y - viewport_position.y; - Some(viewport_position.extend(depth)) + Ok(viewport_position.extend(depth)) } /// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`. @@ -415,16 +455,18 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// - /// Returns `None` if any of these conditions occur: - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The near or far plane cannot be computed. This can happen if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. - /// Panics if the projection matrix is null and `glam_assert` is enabled. + /// # Panics + /// + /// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and + /// `glam_assert` is enabled (see [`ndc_to_world`](Self::ndc_to_world). pub fn viewport_to_world( &self, camera_transform: &GlobalTransform, mut viewport_position: Vec2, - ) -> Option { - let target_size = self.logical_viewport_size()?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .ok_or(ViewportConversionError::NoViewportSize)?; // Flip the Y co-ordinate origin from the top to the bottom. viewport_position.y = target_size.y - viewport_position.y; let ndc = viewport_position * 2. / target_size - Vec2::ONE; @@ -436,12 +478,12 @@ impl Camera { let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); // The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN. - Dir3::new(world_far_plane - world_near_plane).map_or(None, |direction| { - Some(Ray3d { + Dir3::new(world_far_plane - world_near_plane) + .map_err(|_| ViewportConversionError::InvalidData) + .map(|direction| Ray3d { origin: world_near_plane, direction, }) - }) } /// Returns a 2D world position computed from a position on this [`Camera`]'s viewport. @@ -451,23 +493,27 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// - /// Returns `None` if any of these conditions occur: - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The viewport position cannot be mapped to the world. See [`ndc_to_world`](Camera::ndc_to_world) - /// May panic. See [`ndc_to_world`](Camera::ndc_to_world). + /// # Panics + /// + /// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and + /// `glam_assert` is enabled (see [`ndc_to_world`](Self::ndc_to_world). pub fn viewport_to_world_2d( &self, camera_transform: &GlobalTransform, mut viewport_position: Vec2, - ) -> Option { - let target_size = self.logical_viewport_size()?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .ok_or(ViewportConversionError::NoViewportSize)?; // Flip the Y co-ordinate origin from the top to the bottom. viewport_position.y = target_size.y - viewport_position.y; let ndc = viewport_position * 2. / target_size - Vec2::ONE; - let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?; + let world_near_plane = self + .ndc_to_world(camera_transform, ndc.extend(1.)) + .ok_or(ViewportConversionError::InvalidData)?; - Some(world_near_plane.truncate()) + Ok(world_near_plane.truncate()) } /// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates. @@ -478,7 +524,10 @@ impl Camera { /// [`world_to_viewport`](Self::world_to_viewport). /// /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. - /// Panics if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled. + /// + /// # Panics + /// + /// Will panic if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled. pub fn world_to_ndc( &self, camera_transform: &GlobalTransform, @@ -501,7 +550,10 @@ impl Camera { /// [`world_to_viewport`](Self::world_to_viewport). /// /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. - /// Panics if the projection matrix is null and `glam_assert` is enabled. + /// + /// # Panics + /// + /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { // Build a transformation matrix to convert from NDC to world space using camera data let ndc_to_world = diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 32dff45d1df7c..3393d0ef44b78 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -35,8 +35,13 @@ pub mod render_resource; pub mod renderer; pub mod settings; mod spatial_bundle; +pub mod storage; pub mod texture; pub mod view; + +/// The render prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -75,6 +80,7 @@ use crate::{ render_resource::{PipelineCache, Shader, ShaderLoader}, renderer::{render_system, RenderInstance}, settings::RenderCreation, + storage::StoragePlugin, view::{ViewPlugin, WindowRenderPlugin}, }; use bevy_app::{App, AppLabel, Plugin, SubApp}; @@ -356,6 +362,7 @@ impl Plugin for RenderPlugin { GlobalsPlugin, MorphPlugin, BatchingPlugin, + StoragePlugin, )); app.init_resource::() diff --git a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs index d92c7a3897e06..75a747cf46fe3 100644 --- a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs @@ -8,7 +8,7 @@ use encase::{ ShaderType, }; use nonmax::NonMaxU32; -use std::{marker::PhantomData, num::NonZeroU64}; +use std::{marker::PhantomData, num::NonZero}; use wgpu::{BindingResource, Limits}; // 1MB else we will make really large arrays on macOS which reports very large @@ -69,7 +69,7 @@ impl BatchedUniformBuffer { } #[inline] - pub fn size(&self) -> NonZeroU64 { + pub fn size(&self) -> NonZero { self.temp.size() } @@ -141,7 +141,7 @@ where const METADATA: Metadata = T::METADATA; - fn size(&self) -> NonZeroU64 { + fn size(&self) -> NonZero { Self::METADATA.stride().mul(self.1.max(1) as u64).0 } } diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 4380890e17c27..cb3a6fb347631 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -77,6 +77,8 @@ impl Deref for BindGroup { /// # use bevy_render::{render_resource::*, texture::Image}; /// # use bevy_color::LinearRgba; /// # use bevy_asset::Handle; +/// # use bevy_render::storage::ShaderStorageBuffer; +/// /// #[derive(AsBindGroup)] /// struct CoolMaterial { /// #[uniform(0)] @@ -85,9 +87,9 @@ impl Deref for BindGroup { /// #[sampler(2)] /// color_texture: Handle, /// #[storage(3, read_only)] -/// values: Vec, +/// storage_buffer: Handle, /// #[storage(4, read_only, buffer)] -/// buffer: Buffer, +/// raw_buffer: Buffer, /// #[storage_texture(5)] /// storage_texture: Handle, /// } @@ -99,7 +101,8 @@ impl Deref for BindGroup { /// @group(2) @binding(0) var color: vec4; /// @group(2) @binding(1) var color_texture: texture_2d; /// @group(2) @binding(2) var color_sampler: sampler; -/// @group(2) @binding(3) var values: array; +/// @group(2) @binding(3) var storage_buffer: array; +/// @group(2) @binding(4) var raw_buffer: array; /// @group(2) @binding(5) var storage_texture: texture_storage_2d; /// ``` /// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups @@ -151,15 +154,17 @@ impl Deref for BindGroup { /// |------------------------|-------------------------------------------------------------------------|------------------------| /// | `sampler_type` = "..." | `"filtering"`, `"non_filtering"`, `"comparison"`. | `"filtering"` | /// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | -/// /// * `storage(BINDING_INDEX, arguments)` -/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a storage buffer. +/// * The field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Buffer`] GPU resource, which +/// will be bound as a storage buffer in shaders. If the `storage` attribute is used, the field is expected a raw +/// buffer, and the buffer will be bound as a storage buffer in shaders. /// * It supports and optional `read_only` parameter. Defaults to false if not present. /// /// | Arguments | Values | Default | /// |------------------------|-------------------------------------------------------------------------|----------------------| /// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | /// | `read_only` | if present then value is true, otherwise false | `false` | +/// | `buffer` | if present then the field will be assumed to be a raw wgpu buffer | | /// /// Note that fields without field-level binding attributes will be ignored. /// ``` diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index 05c5ee9c3e818..58d1d62a19889 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -1,5 +1,5 @@ use bevy_utils::all_tuples_with_size; -use std::num::NonZeroU32; +use std::num::NonZero; use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// Helper for constructing bind group layouts. @@ -130,7 +130,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; pub struct BindGroupLayoutEntryBuilder { ty: BindingType, visibility: Option, - count: Option, + count: Option>, } impl BindGroupLayoutEntryBuilder { @@ -139,7 +139,7 @@ impl BindGroupLayoutEntryBuilder { self } - pub fn count(mut self, count: NonZeroU32) -> Self { + pub fn count(mut self, count: NonZero) -> Self { self.count = Some(count); self } @@ -353,7 +353,7 @@ pub mod binding_types { BufferBindingType, SamplerBindingType, TextureSampleType, TextureViewDimension, }; use encase::ShaderType; - use std::num::NonZeroU64; + use std::num::NonZero; use wgpu::{StorageTextureAccess, TextureFormat}; use super::*; @@ -364,7 +364,7 @@ pub mod binding_types { pub fn storage_buffer_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Storage { read_only: false }, @@ -382,7 +382,7 @@ pub mod binding_types { pub fn storage_buffer_read_only_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Storage { read_only: true }, @@ -398,7 +398,7 @@ pub mod binding_types { pub fn uniform_buffer_sized( has_dynamic_offset: bool, - min_binding_size: Option, + min_binding_size: Option>, ) -> BindGroupLayoutEntryBuilder { BindingType::Buffer { ty: BufferBindingType::Uniform, diff --git a/crates/bevy_render/src/render_resource/resource_macros.rs b/crates/bevy_render/src/render_resource/resource_macros.rs index 68896092ce016..e22c59b1c031f 100644 --- a/crates/bevy_render/src/render_resource/resource_macros.rs +++ b/crates/bevy_render/src/render_resource/resource_macros.rs @@ -149,7 +149,7 @@ macro_rules! render_resource_wrapper { macro_rules! define_atomic_id { ($atomic_id_type:ident) => { #[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] - pub struct $atomic_id_type(core::num::NonZeroU32); + pub struct $atomic_id_type(core::num::NonZero); // We use new instead of default to indicate that each ID created will be unique. #[allow(clippy::new_without_default)] @@ -160,7 +160,7 @@ macro_rules! define_atomic_id { static COUNTER: AtomicU32 = AtomicU32::new(1); let counter = COUNTER.fetch_add(1, Ordering::Relaxed); - Self(core::num::NonZeroU32::new(counter).unwrap_or_else(|| { + Self(core::num::NonZero::::new(counter).unwrap_or_else(|| { panic!( "The system ran out of unique `{}`s.", stringify!($atomic_id_type) @@ -169,14 +169,14 @@ macro_rules! define_atomic_id { } } - impl From<$atomic_id_type> for core::num::NonZeroU32 { + impl From<$atomic_id_type> for core::num::NonZero { fn from(value: $atomic_id_type) -> Self { value.0 } } - impl From for $atomic_id_type { - fn from(value: core::num::NonZeroU32) -> Self { + impl From> for $atomic_id_type { + fn from(value: core::num::NonZero) -> Self { Self(value) } } diff --git a/crates/bevy_render/src/render_resource/uniform_buffer.rs b/crates/bevy_render/src/render_resource/uniform_buffer.rs index de4d84c94a06e..db51653f146ea 100644 --- a/crates/bevy_render/src/render_resource/uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/uniform_buffer.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, num::NonZeroU64}; +use std::{marker::PhantomData, num::NonZero}; use crate::{ render_resource::Buffer, @@ -309,7 +309,7 @@ impl DynamicUniformBuffer { if let Some(buffer) = self.buffer.as_deref() { let buffer_view = queue - .write_buffer_with(buffer, 0, NonZeroU64::new(buffer.size())?) + .write_buffer_with(buffer, 0, NonZero::::new(buffer.size())?) .unwrap(); Some(DynamicUniformBufferWriter { buffer: encase::DynamicUniformBuffer::new_with_alignment( diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs new file mode 100644 index 0000000000000..4225ee7e28834 --- /dev/null +++ b/crates/bevy_render/src/storage.rs @@ -0,0 +1,109 @@ +use crate::render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssetUsages}; +use crate::render_resource::{Buffer, BufferUsages}; +use crate::renderer::RenderDevice; +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, AssetApp}; +use bevy_ecs::system::lifetimeless::SRes; +use bevy_ecs::system::SystemParamItem; +use bevy_reflect::prelude::ReflectDefault; +use bevy_reflect::Reflect; +use bevy_utils::default; +use wgpu::util::BufferInitDescriptor; + +/// Adds [`ShaderStorageBuffer`] as an asset that is extracted and uploaded to the GPU. +#[derive(Default)] +pub struct StoragePlugin; + +impl Plugin for StoragePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(RenderAssetPlugin::::default()) + .register_type::() + .init_asset::() + .register_asset_reflect::(); + } +} + +/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU. +#[derive(Asset, Reflect, Debug, Clone)] +#[reflect_value(Default)] +pub struct ShaderStorageBuffer { + /// Optional data used to initialize the buffer. + pub data: Option>, + /// The buffer description used to create the buffer. + pub buffer_description: wgpu::BufferDescriptor<'static>, + /// The asset usage of the storage buffer. + pub asset_usage: RenderAssetUsages, +} + +impl Default for ShaderStorageBuffer { + fn default() -> Self { + Self { + data: None, + buffer_description: wgpu::BufferDescriptor { + label: None, + size: 0, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }, + asset_usage: RenderAssetUsages::default(), + } + } +} + +impl ShaderStorageBuffer { + /// Creates a new storage buffer with the given data and asset usage. + pub fn new(data: &[u8], asset_usage: RenderAssetUsages) -> Self { + let mut storage = ShaderStorageBuffer { + data: Some(data.to_vec()), + ..default() + }; + storage.asset_usage = asset_usage; + storage + } + + /// Creates a new storage buffer with the given size and asset usage. + pub fn with_size(size: usize, asset_usage: RenderAssetUsages) -> Self { + let mut storage = ShaderStorageBuffer { + data: None, + ..default() + }; + storage.buffer_description.size = size as u64; + storage.buffer_description.mapped_at_creation = false; + storage.asset_usage = asset_usage; + storage + } +} + +/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU. +pub struct GpuShaderStorageBuffer { + pub buffer: Buffer, +} + +impl RenderAsset for GpuShaderStorageBuffer { + type SourceAsset = ShaderStorageBuffer; + type Param = SRes; + + fn asset_usage(source_asset: &Self::SourceAsset) -> RenderAssetUsages { + source_asset.asset_usage + } + + fn prepare_asset( + source_asset: Self::SourceAsset, + render_device: &mut SystemParamItem, + ) -> Result> { + match source_asset.data { + Some(data) => { + let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: source_asset.buffer_description.label, + contents: &data, + usage: source_asset.buffer_description.usage, + }); + Ok(GpuShaderStorageBuffer { buffer }) + } + None => { + let buffer = render_device.create_buffer(&source_asset.buffer_description); + Ok(GpuShaderStorageBuffer { buffer }) + } + } + } +} diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 3729f4048ff21..d4392c8e7c6ff 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -123,7 +123,8 @@ impl Plugin for ViewPlugin { ( prepare_view_attachments .in_set(RenderSet::ManageViews) - .before(prepare_view_targets), + .before(prepare_view_targets) + .after(prepare_windows), prepare_view_targets .in_set(RenderSet::ManageViews) .after(prepare_windows) diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 345055ca3d3f2..8181d3c91118f 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -278,7 +278,11 @@ pub enum VisibilitySystems { /// [`hierarchy`](bevy_hierarchy). VisibilityPropagate, /// Label for the [`check_visibility`] system updating [`ViewVisibility`] - /// of each entity and the [`VisibleEntities`] of each view. + /// of each entity and the [`VisibleEntities`] of each view.\ + /// + /// System order ambiguities between systems in this set are ignored: + /// the order of systems within this set is irrelevant, as [`check_visibility`] + /// assumes that its operations are irreversible during the frame. CheckVisibility, } @@ -294,6 +298,7 @@ impl Plugin for VisibilityPlugin { .before(CheckVisibility) .after(TransformSystem::TransformPropagate), ) + .configure_sets(PostUpdate, CheckVisibility.ambiguous_with(CheckVisibility)) .add_systems( PostUpdate, ( diff --git a/crates/bevy_render/src/view/window/cursor.rs b/crates/bevy_render/src/view/window/cursor.rs index eed41cec114be..3bed2236316fd 100644 --- a/crates/bevy_render/src/view/window/cursor.rs +++ b/crates/bevy_render/src/view/window/cursor.rs @@ -1,3 +1,4 @@ +use bevy_app::{App, Last, Plugin}; use bevy_asset::{AssetId, Assets, Handle}; use bevy_ecs::{ change_detection::DetectChanges, @@ -19,6 +20,16 @@ use wgpu::TextureFormat; use crate::prelude::Image; +pub struct CursorPlugin; + +impl Plugin for CursorPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .init_resource::() + .add_systems(Last, update_cursors); + } +} + /// Insert into a window entity to set the cursor for that window. #[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] #[reflect(Component, Debug, Default)] diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 816d8c4e8dfd7..0eb9e95d8718e 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -3,7 +3,7 @@ use crate::{ renderer::{RenderAdapter, RenderDevice, RenderInstance}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper, }; -use bevy_app::{App, Last, Plugin}; +use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; #[cfg(target_os = "linux")] use bevy_utils::warn_once; @@ -11,9 +11,9 @@ use bevy_utils::{default, tracing::debug, HashSet}; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; -use bevy_winit::CustomCursorCache; +use cursor::CursorPlugin; use std::{ - num::NonZeroU32, + num::NonZero, ops::{Deref, DerefMut}, }; use wgpu::{ @@ -23,16 +23,13 @@ use wgpu::{ pub mod cursor; pub mod screenshot; -use self::cursor::update_cursors; use screenshot::{ScreenshotPlugin, ScreenshotToScreenPipeline}; pub struct WindowRenderPlugin; impl Plugin for WindowRenderPlugin { fn build(&self, app: &mut App) { - app.add_plugins(ScreenshotPlugin) - .init_resource::() - .add_systems(Last, update_cursors); + app.add_plugins((ScreenshotPlugin, CursorPlugin)); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -63,7 +60,7 @@ pub struct ExtractedWindow { pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, - pub desired_maximum_frame_latency: Option, + pub desired_maximum_frame_latency: Option>, /// Note: this will not always be the swap chain texture view. When taking a screenshot, /// this will point to an alternative texture instead to allow for copying the render result /// to CPU memory. @@ -395,7 +392,7 @@ pub fn create_surfaces( }, desired_maximum_frame_latency: window .desired_maximum_frame_latency - .map(NonZeroU32::get) + .map(NonZero::::get) .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY), alpha_mode: match window.alpha_mode { CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto, diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index b7bc473ca06d4..1fbf3b11b0c97 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -34,7 +34,9 @@ pub use scene_filter::*; pub use scene_loader::*; pub use scene_spawner::*; -#[allow(missing_docs)] +/// The scene prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 1278b73fdbde7..152b612bd16ae 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -19,6 +19,9 @@ mod texture_atlas; mod texture_atlas_builder; mod texture_slice; +/// The sprite prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[allow(deprecated)] #[doc(hidden)] diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index cc1728b151ac2..f47ca22a5a6d6 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -70,7 +70,7 @@ pub fn sprite_picking( continue; }; - let Some(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position) + let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position) else { continue; }; diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 5506221ce35cf..e8dc8264ef522 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -47,14 +47,16 @@ pub mod state_scoped; /// Provides definitions for the basic traits required by the state system pub mod reflect; -/// Most commonly used re-exported types. +/// The state prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[cfg(feature = "bevy_app")] #[doc(hidden)] pub use crate::app::AppExtStates; #[doc(hidden)] pub use crate::condition::*; - #[cfg(feature = "bevy_app")] + #[cfg(feature = "bevy_reflect")] #[doc(hidden)] pub use crate::reflect::{ReflectFreelyMutableState, ReflectState}; #[doc(hidden)] diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 3eb33c6603e70..d8736d55cab07 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -44,7 +44,9 @@ pub use iter::ParallelIterator; pub use futures_lite; -#[allow(missing_docs)] +/// The tasks prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -55,7 +57,7 @@ pub mod prelude { }; } -use std::num::NonZeroUsize; +use std::num::NonZero; /// Gets the logical CPU core count available to the current process. /// @@ -65,6 +67,6 @@ use std::num::NonZeroUsize; /// This will always return at least 1. pub fn available_parallelism() -> usize { std::thread::available_parallelism() - .map(NonZeroUsize::get) + .map(NonZero::::get) .unwrap_or(1) } diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index a5a337c676d56..5be451b84df12 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -29,7 +29,7 @@ bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } # other -cosmic-text = "0.12" +cosmic-text = { version = "0.12", features = ["shape-run-cache"] } thiserror = "1.0" serde = { version = "1", features = ["derive"] } unicode-bidi = "0.3.13" diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 8922f0f1032ef..4c379e61e5ae1 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -55,7 +55,9 @@ pub use pipeline::*; pub use text::*; pub use text2d::*; -/// Most commonly used re-exported types. +/// The text prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{Font, JustifyText, Text, Text2dBundle, TextError, TextSection, TextStyle}; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 6568a9204462e..7ac46096270e8 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use bevy_asset::{AssetId, Assets}; -use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; +use bevy_ecs::{component::Component, entity::Entity, reflect::ReflectComponent, system::Resource}; use bevy_math::{UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::texture::Image; @@ -51,6 +51,10 @@ pub struct TextPipeline { /// /// See [`cosmic_text::SwashCache`] for more information. swash_cache: SwashCache, + /// Buffered vec for collecting spans. + /// + /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). + spans_buffer: Vec<(&'static str, Attrs<'static>)>, } impl TextPipeline { @@ -95,23 +99,29 @@ impl TextPipeline { // The section index is stored in the metadata of the spans, and could be used // to look up the section the span came from and is not used internally // in cosmic-text. - let spans: Vec<(&str, Attrs)> = sections - .iter() - .enumerate() - .filter(|(_section_index, section)| section.style.font_size > 0.0) - .map(|(section_index, section)| { - ( - §ion.value[..], - get_attrs( - section, - section_index, - font_system, - &self.map_handle_to_font_id, - scale_factor, - ), - ) - }) + let mut spans: Vec<(&str, Attrs)> = std::mem::take(&mut self.spans_buffer) + .into_iter() + .map(|_| -> (&str, Attrs) { unreachable!() }) .collect(); + spans.extend( + sections + .iter() + .enumerate() + .filter(|(_section_index, section)| section.style.font_size > 0.0) + .map(|(section_index, section)| { + ( + §ion.value[..], + get_attrs( + section, + section_index, + font_system, + &self.map_handle_to_font_id, + scale_factor, + ), + ) + }), + ); + let spans_iter = spans.iter().copied(); buffer.set_metrics(font_system, metrics); buffer.set_size(font_system, bounds.width, bounds.height); @@ -126,7 +136,7 @@ impl TextPipeline { }, ); - buffer.set_rich_text(font_system, spans, Attrs::new(), Shaping::Advanced); + buffer.set_rich_text(font_system, spans_iter, Attrs::new(), Shaping::Advanced); // PERF: https://github.com/pop-os/cosmic-text/issues/166: // Setting alignment afterwards appears to invalidate some layouting performed by `set_text` which is presumably not free? @@ -135,6 +145,13 @@ impl TextPipeline { } buffer.shape_until_scroll(font_system, false); + // Recover the spans buffer. + spans.clear(); + self.spans_buffer = spans + .into_iter() + .map(|_| -> (&'static str, Attrs<'static>) { unreachable!() }) + .collect(); + Ok(()) } @@ -145,6 +162,7 @@ impl TextPipeline { #[allow(clippy::too_many_arguments)] pub fn queue_text( &mut self, + layout_info: &mut TextLayoutInfo, fonts: &Assets, sections: &[TextSection], scale_factor: f64, @@ -156,9 +174,12 @@ impl TextPipeline { textures: &mut Assets, y_axis_orientation: YAxisOrientation, buffer: &mut CosmicBuffer, - ) -> Result { + ) -> Result<(), TextError> { + layout_info.glyphs.clear(); + layout_info.size = Default::default(); + if sections.is_empty() { - return Ok(TextLayoutInfo::default()); + return Ok(()); } self.update_buffer( @@ -175,14 +196,14 @@ impl TextPipeline { let font_system = &mut self.font_system.0; let swash_cache = &mut self.swash_cache.0; - let glyphs = buffer + buffer .layout_runs() .flat_map(|run| { run.glyphs .iter() .map(move |layout_glyph| (layout_glyph, run.line_y)) }) - .map(|(layout_glyph, line_y)| { + .try_for_each(|(layout_glyph, line_y)| { let section_index = layout_glyph.metadata; let font_handle = sections[section_index].style.font.clone_weak(); @@ -224,22 +245,22 @@ impl TextPipeline { // when glyphs are not limited to single byte representation, relevant for #1319 let pos_glyph = PositionedGlyph::new(position, glyph_size.as_vec2(), atlas_info, section_index); - Ok(pos_glyph) - }) - .collect::, _>>()?; + layout_info.glyphs.push(pos_glyph); + Ok(()) + })?; - Ok(TextLayoutInfo { - glyphs, - size: box_size, - }) + layout_info.size = box_size; + Ok(()) } /// Queues text for measurement /// /// Produces a [`TextMeasureInfo`] which can be used by a layout system /// to measure the text area on demand. + #[allow(clippy::too_many_arguments)] pub fn create_text_measure( &mut self, + entity: Entity, fonts: &Assets, sections: &[TextSection], scale_factor: f64, @@ -270,9 +291,7 @@ impl TextPipeline { Ok(TextMeasureInfo { min: min_width_content_size, max: max_width_content_size, - // TODO: This clone feels wasteful, is there another way to structure TextMeasureInfo - // that it doesn't need to own a buffer? - bytemunch - buffer: buffer.0.clone(), + entity, }) } @@ -299,23 +318,14 @@ pub struct TextLayoutInfo { /// Size information for a corresponding [`Text`](crate::Text) component. /// /// Generated via [`TextPipeline::create_text_measure`]. +#[derive(Debug)] pub struct TextMeasureInfo { /// Minimum size for a text area in pixels, to be used when laying out widgets with taffy pub min: Vec2, /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy pub max: Vec2, - buffer: Buffer, -} - -impl std::fmt::Debug for TextMeasureInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TextMeasureInfo") - .field("min", &self.min) - .field("max", &self.max) - .field("buffer", &"_") - .field("font_system", &"_") - .finish() - } + /// The entity that is measured. + pub entity: Entity, } impl TextMeasureInfo { @@ -323,11 +333,13 @@ impl TextMeasureInfo { pub fn compute_size( &mut self, bounds: TextBounds, + buffer: &mut Buffer, font_system: &mut cosmic_text::FontSystem, ) -> Vec2 { - self.buffer - .set_size(font_system, bounds.width, bounds.height); - buffer_dimensions(&self.buffer) + // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' + // whenever a canonical state is required. + buffer.set_size(font_system, bounds.width, bounds.height); + buffer_dimensions(buffer) } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 8c28fe3079aca..2c0570836b312 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -170,7 +170,7 @@ pub fn update_text2d_layout( let inverse_scale_factor = scale_factor.recip(); - for (entity, text, bounds, mut text_layout_info, mut buffer) in &mut text_query { + for (entity, text, bounds, text_layout_info, mut buffer) in &mut text_query { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { let text_bounds = TextBounds { width: if text.linebreak_behavior == BreakLineOn::NoWrap { @@ -183,7 +183,9 @@ pub fn update_text2d_layout( .map(|height| scale_value(height, scale_factor)), }; + let text_layout_info = text_layout_info.into_inner(); match text_pipeline.queue_text( + text_layout_info, &fonts, &text.sections, scale_factor.into(), @@ -204,10 +206,11 @@ pub fn update_text2d_layout( Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } - Ok(mut info) => { - info.size.x = scale_value(info.size.x, inverse_scale_factor); - info.size.y = scale_value(info.size.y, inverse_scale_factor); - *text_layout_info = info; + Ok(()) => { + text_layout_info.size.x = + scale_value(text_layout_info.size.x, inverse_scale_factor); + text_layout_info.size.y = + scale_value(text_layout_info.size.y, inverse_scale_factor); } } } diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 5181bc4335c4c..8cb1c77ab8a8f 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -23,8 +23,10 @@ pub use time::*; pub use timer::*; pub use virt::*; +/// The time prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - //! The Bevy Time Prelude. #[doc(hidden)] pub use crate::{Fixed, Real, Time, Timer, TimerMode, Virtual}; } diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index 0e170f111a29a..c6f55362924bd 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -29,7 +29,9 @@ pub mod helper; #[cfg(feature = "bevy-support")] pub mod systems; -#[doc(hidden)] +/// The transform prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::components::*; diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index cbcf8e82d36ca..53cfb272b2219 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -41,7 +41,7 @@ fn calc_bounds( if let Ok((camera, camera_transform)) = camera.get_single() { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - if let Some(translation) = + if let Ok(translation) = camera.world_to_viewport(camera_transform, transform.translation()) { let bounds = Rect::new( @@ -154,7 +154,6 @@ impl Plugin for AccessibilityPlugin { .after(bevy_transform::TransformSystem::TransformPropagate) .after(CameraUpdateSystem) // the listed systems do not affect calculated size - .ambiguous_with(crate::resolve_outlines_system) .ambiguous_with(crate::ui_stack_system), button_changed, image_changed, diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 7bd0dfc95ebf3..387d47c06cec4 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,6 @@ -use crate::{CalculatedClip, DefaultUiCamera, Node, TargetCamera, UiScale, UiStack}; +use crate::{ + CalculatedClip, DefaultUiCamera, Node, ResolvedBorderRadius, TargetCamera, UiScale, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, @@ -249,19 +251,18 @@ pub fn ui_focus_system( .map(|clip| node_rect.intersect(clip.clip)) .unwrap_or(node_rect); + let cursor_position = camera_cursor_positions.get(&camera_entity); + // The mouse position relative to the node // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = - camera_cursor_positions - .get(&camera_entity) - .and_then(|cursor_position| { - // ensure node size is non-zero in all dimensions, otherwise relative position will be - // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to - // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) - }); + let relative_cursor_position = cursor_position.and_then(|cursor_position| { + // ensure node size is non-zero in all dimensions, otherwise relative position will be + // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to + // false positives for mouse_over (#12395) + (node_rect.size().cmpgt(Vec2::ZERO).all()) + .then_some((*cursor_position - node_rect.min) / node_rect.size()) + }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking @@ -270,7 +271,16 @@ pub fn ui_focus_system( normalized: relative_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over(); + let contains_cursor = relative_cursor_position_component.mouse_over() + && cursor_position + .map(|point| { + pick_rounded_rect( + *point - node_rect.center(), + node_rect.size(), + node.node.border_radius, + ) + }) + .unwrap_or(false); // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position @@ -332,3 +342,26 @@ pub fn ui_focus_system( } } } + +// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with +// the given size and border radius. +// +// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. +pub(crate) fn pick_rounded_rect( + point: Vec2, + size: Vec2, + border_radius: ResolvedBorderRadius, +) -> bool { + let s = point.signum(); + let r = (border_radius.top_left * (1. - s.x) * (1. - s.y) + + border_radius.top_right * (1. + s.x) * (1. - s.y) + + border_radius.bottom_right * (1. + s.x) * (1. + s.y) + + border_radius.bottom_left * (1. - s.x) * (1. + s.y)) + / 4.; + + let corner_to_point = point.abs() - 0.5 * size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. +} diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index f74ef601319cb..b529df6f30f40 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,21 +1,22 @@ -use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale}; +use crate::{ + BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale, +}; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, - entity::Entity, + entity::{Entity, EntityHashMap, EntityHashSet}, event::EventReader, query::{With, Without}, removal_detection::RemovedComponents, - system::{Query, Res, ResMut, SystemParam}, + system::{Local, Query, Res, ResMut, SystemParam}, world::Ref, }; use bevy_hierarchy::{Children, Parent}; use bevy_math::{UVec2, Vec2}; use bevy_render::camera::{Camera, NormalizedRenderTarget}; #[cfg(feature = "bevy_text")] -use bevy_text::TextPipeline; +use bevy_text::{CosmicBuffer, TextPipeline}; use bevy_transform::components::Transform; use bevy_utils::tracing::warn; -use bevy_utils::{HashMap, HashSet}; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use thiserror::Error; use ui_surface::UiSurface; @@ -63,6 +64,7 @@ pub enum LayoutError { TaffyError(#[from] taffy::TaffyError), } +#[doc(hidden)] #[derive(SystemParam)] pub struct UiLayoutSystemRemovedComponentParam<'w, 's> { removed_cameras: RemovedComponents<'w, 's, Camera>, @@ -71,9 +73,25 @@ pub struct UiLayoutSystemRemovedComponentParam<'w, 's> { removed_nodes: RemovedComponents<'w, 's, Node>, } +#[doc(hidden)] +#[derive(Default)] +pub struct UiLayoutSystemBuffers { + interned_root_notes: Vec>, + resized_windows: EntityHashSet, + camera_layout_info: EntityHashMap, +} + +struct CameraLayoutInfo { + size: UVec2, + resized: bool, + scale_factor: f32, + root_nodes: Vec, +} + /// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes. #[allow(clippy::too_many_arguments)] pub fn ui_layout_system( + mut buffers: Local, primary_window: Query<(Entity, &Window), With>, cameras: Query<(Entity, &Camera)>, default_ui_camera: DefaultUiCamera, @@ -94,23 +112,29 @@ pub fn ui_layout_system( children_query: Query<(Entity, Ref), With>, just_children_query: Query<&Children>, mut removed_components: UiLayoutSystemRemovedComponentParam, - mut node_transform_query: Query<(&mut Node, &mut Transform)>, + mut node_transform_query: Query<( + &mut Node, + &mut Transform, + Option<&BorderRadius>, + Option<&Outline>, + )>, + #[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut CosmicBuffer>, #[cfg(feature = "bevy_text")] mut text_pipeline: ResMut, ) { - struct CameraLayoutInfo { - size: UVec2, - resized: bool, - scale_factor: f32, - root_nodes: Vec, - } + let UiLayoutSystemBuffers { + interned_root_notes, + resized_windows, + camera_layout_info, + } = &mut *buffers; let default_camera = default_ui_camera.get(); let camera_with_default = |target_camera: Option<&TargetCamera>| { target_camera.map(TargetCamera::entity).or(default_camera) }; - let resized_windows: HashSet = resize_events.read().map(|event| event.window).collect(); - let calculate_camera_layout_info = |camera: &Camera| { + resized_windows.clear(); + resized_windows.extend(resize_events.read().map(|event| event.window)); + let mut calculate_camera_layout_info = |camera: &Camera| { let size = camera.physical_viewport_size().unwrap_or(UVec2::ZERO); let scale_factor = camera.target_scaling_factor().unwrap_or(1.0); let camera_target = camera @@ -123,12 +147,12 @@ pub fn ui_layout_system( size, resized, scale_factor: scale_factor * ui_scale.0, - root_nodes: Vec::new(), + root_nodes: interned_root_notes.pop().unwrap_or_default(), } }; // Precalculate the layout info for each camera, so we have fast access to it for each node - let mut camera_layout_info: HashMap = HashMap::new(); + camera_layout_info.clear(); root_node_query.iter().for_each(|(entity,target_camera)|{ match camera_with_default(target_camera) { Some(camera_entity) => { @@ -217,6 +241,8 @@ pub fn ui_layout_system( } }); + #[cfg(feature = "bevy_text")] + let text_buffers = &mut buffer_query; #[cfg(feature = "bevy_text")] let font_system = text_pipeline.font_system_mut(); // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used) @@ -229,19 +255,22 @@ pub fn ui_layout_system( } }); - for (camera_id, camera) in &camera_layout_info { + for (camera_id, mut camera) in camera_layout_info.drain() { let inverse_target_scale_factor = camera.scale_factor.recip(); ui_surface.compute_camera_layout( - *camera_id, + camera_id, camera.size, #[cfg(feature = "bevy_text")] + text_buffers, + #[cfg(feature = "bevy_text")] font_system, ); for root in &camera.root_nodes { update_uinode_geometry_recursive( *root, &ui_surface, + None, &mut node_transform_query, &just_children_query, inverse_target_scale_factor, @@ -249,18 +278,29 @@ pub fn ui_layout_system( Vec2::ZERO, ); } + + camera.root_nodes.clear(); + interned_root_notes.push(camera.root_nodes); } fn update_uinode_geometry_recursive( entity: Entity, ui_surface: &UiSurface, - node_transform_query: &mut Query<(&mut Node, &mut Transform)>, + root_size: Option, + node_transform_query: &mut Query<( + &mut Node, + &mut Transform, + Option<&BorderRadius>, + Option<&Outline>, + )>, children_query: &Query<&Children>, inverse_target_scale_factor: f32, parent_size: Vec2, mut absolute_location: Vec2, ) { - if let Ok((mut node, mut transform)) = node_transform_query.get_mut(entity) { + if let Ok((mut node, mut transform, maybe_border_radius, maybe_outline)) = + node_transform_query.get_mut(entity) + { let Ok(layout) = ui_surface.get_layout(entity) else { return; }; @@ -282,14 +322,41 @@ pub fn ui_layout_system( node.calculated_size = rounded_size; node.unrounded_size = layout_size; } + + let viewport_size = root_size.unwrap_or(node.calculated_size); + + if let Some(border_radius) = maybe_border_radius { + // We don't trigger change detection for changes to border radius + node.bypass_change_detection().border_radius = + border_radius.resolve(node.calculated_size, viewport_size); + } + + if let Some(outline) = maybe_outline { + // don't trigger change detection when only outlines are changed + let node = node.bypass_change_detection(); + node.outline_width = outline + .width + .resolve(node.size().x, viewport_size) + .unwrap_or(0.) + .max(0.); + + node.outline_offset = outline + .offset + .resolve(node.size().x, viewport_size) + .unwrap_or(0.) + .max(0.); + } + if transform.translation.truncate() != rounded_location { transform.translation = rounded_location.extend(0.); } + if let Ok(children) = children_query.get(entity) { for &child_uinode in children { update_uinode_geometry_recursive( child_uinode, ui_surface, + Some(viewport_size), node_transform_query, children_query, inverse_target_scale_factor, @@ -302,34 +369,6 @@ pub fn ui_layout_system( } } -/// Resolve and update the widths of Node outlines -pub fn resolve_outlines_system( - primary_window: Query<&Window, With>, - ui_scale: Res, - mut outlines_query: Query<(&Outline, &mut Node)>, -) { - let viewport_size = primary_window - .get_single() - .map(Window::size) - .unwrap_or(Vec2::ZERO) - / ui_scale.0; - - for (outline, mut node) in outlines_query.iter_mut() { - let node = node.bypass_change_detection(); - node.outline_width = outline - .width - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.); - - node.outline_offset = outline - .offset - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.); - } -} - #[inline] /// Round `value` to the nearest whole integer, with ties (values with a fractional part equal to 0.5) rounded towards positive infinity. fn approx_round_ties_up(value: f32) -> f32 { diff --git a/crates/bevy_ui/src/layout/ui_surface.rs b/crates/bevy_ui/src/layout/ui_surface.rs index f191042aa3c13..0cca1d6a0b695 100644 --- a/crates/bevy_ui/src/layout/ui_surface.rs +++ b/crates/bevy_ui/src/layout/ui_surface.rs @@ -196,11 +196,14 @@ without UI components as a child of an entity with UI components, results may be } /// Compute the layout for each window entity's corresponding root node in the layout. - pub fn compute_camera_layout( + pub fn compute_camera_layout<'a>( &mut self, camera: Entity, render_target_resolution: UVec2, - #[cfg(feature = "bevy_text")] font_system: &mut bevy_text::cosmic_text::FontSystem, + #[cfg(feature = "bevy_text")] buffer_query: &'a mut bevy_ecs::prelude::Query< + &mut bevy_text::CosmicBuffer, + >, + #[cfg(feature = "bevy_text")] font_system: &'a mut bevy_text::cosmic_text::FontSystem, ) { let Some(camera_root_nodes) = self.camera_roots.get(&camera) else { return; @@ -223,6 +226,15 @@ without UI components as a child of an entity with UI components, results may be -> taffy::Size { context .map(|ctx| { + #[cfg(feature = "bevy_text")] + let buffer = get_text_buffer( + crate::widget::TextMeasure::needs_buffer( + known_dimensions.height, + available_space.width, + ), + ctx, + buffer_query, + ); let size = ctx.measure( MeasureArgs { width: known_dimensions.width, @@ -231,6 +243,8 @@ without UI components as a child of an entity with UI components, results may be available_height: available_space.height, #[cfg(feature = "bevy_text")] font_system, + #[cfg(feature = "bevy_text")] + buffer, #[cfg(not(feature = "bevy_text"))] font_system: std::marker::PhantomData, }, @@ -284,3 +298,22 @@ with UI components as a child of an entity without UI components, results may be } } } + +#[cfg(feature = "bevy_text")] +fn get_text_buffer<'a>( + needs_buffer: bool, + ctx: &mut NodeMeasure, + query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::CosmicBuffer>, +) -> Option<&'a mut bevy_text::cosmic_text::Buffer> { + // We avoid a query lookup whenever the buffer is not required. + if !needs_buffer { + return None; + } + let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else { + return None; + }; + let Ok(buffer) = query.get_mut(info.entity) else { + return None; + }; + Some(buffer.into_inner()) +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 44eacb51af3fc..948e52ebfa50a 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -29,7 +29,6 @@ mod geometry; mod layout; mod render; mod stack; -mod texture_slice; mod ui_node; pub use focus::*; @@ -41,7 +40,9 @@ pub use ui_material::*; pub use ui_node::*; use widget::UiImageSize; -#[doc(hidden)] +/// The UI prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -92,10 +93,6 @@ pub enum UiSystem { /// /// Runs in [`PostUpdate`]. Stack, - /// After this label, node outline widths have been updated. - /// - /// Runs in [`PostUpdate`]. - Outlines, } /// The current scale of the UI. @@ -153,7 +150,7 @@ impl Plugin for UiPlugin { CameraUpdateSystem, UiSystem::Prepare.before(UiSystem::Stack), UiSystem::Layout, - (UiSystem::PostLayout, UiSystem::Outlines), + UiSystem::PostLayout, ) .chain(), ) @@ -169,17 +166,13 @@ impl Plugin for UiPlugin { update_target_camera_system.in_set(UiSystem::Prepare), ui_layout_system .in_set(UiSystem::Layout) - .before(TransformSystem::TransformPropagate), - resolve_outlines_system - .in_set(UiSystem::Outlines) - // clipping doesn't care about outlines - .ambiguous_with(update_clipping_system) - .in_set(AmbiguousWithTextSystem), + .before(TransformSystem::TransformPropagate) + // Text and Text2D operate on disjoint sets of entities + .ambiguous_with(bevy_text::update_text2d_layout), ui_stack_system .in_set(UiSystem::Stack) // the systems don't care about stack index .ambiguous_with(update_clipping_system) - .ambiguous_with(resolve_outlines_system) .ambiguous_with(ui_layout_system) .in_set(AmbiguousWithTextSystem), update_clipping_system.after(TransformSystem::TransformPropagate), @@ -191,11 +184,6 @@ impl Plugin for UiPlugin { .in_set(UiSystem::Prepare) .in_set(AmbiguousWithTextSystem) .in_set(AmbiguousWithUpdateText2DLayout), - ( - texture_slice::compute_slices_on_asset_event, - texture_slice::compute_slices_on_image_change, - ) - .in_set(UiSystem::PostLayout), ), ); @@ -242,7 +230,8 @@ fn build_text_interop(app: &mut App) { .in_set(UiSystem::PostLayout) .after(bevy_text::remove_dropped_font_atlas_sets) // Text2d and bevy_ui text are entirely on separate entities - .ambiguous_with(bevy_text::update_text2d_layout), + .ambiguous_with(bevy_text::update_text2d_layout) + .ambiguous_with(bevy_text::calculate_bounds_text2d), ), ); diff --git a/crates/bevy_ui/src/measurement.rs b/crates/bevy_ui/src/measurement.rs index 5c565930f53d0..647bc27a4a92a 100644 --- a/crates/bevy_ui/src/measurement.rs +++ b/crates/bevy_ui/src/measurement.rs @@ -23,6 +23,8 @@ pub struct MeasureArgs<'a> { pub available_height: AvailableSpace, #[cfg(feature = "bevy_text")] pub font_system: &'a mut bevy_text::cosmic_text::FontSystem, + #[cfg(feature = "bevy_text")] + pub buffer: Option<&'a mut bevy_text::cosmic_text::Buffer>, // When `bevy_text` is disabled, use `PhantomData` in order to keep lifetime in type signature. #[cfg(not(feature = "bevy_text"))] pub font_system: std::marker::PhantomData<&'a mut ()>, diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 5cb31e209838b..e88409f27c8bf 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -23,7 +23,7 @@ #![allow(clippy::too_many_arguments)] #![deny(missing_docs)] -use crate::{prelude::*, UiStack}; +use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; use bevy_math::Vec2; @@ -163,6 +163,11 @@ pub fn ui_picking( if visible_rect .normalize(node_rect) .contains(relative_cursor_position) + && pick_rounded_rect( + *cursor_position - node_rect.center(), + node_rect.size(), + node.node.border_radius, + ) { hit_nodes .entry((camera_entity, *pointer_id)) diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index e1f25482cfaa6..cd9c66727ad43 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -1,6 +1,7 @@ mod pipeline; mod render_pass; mod ui_material_pipeline; +pub mod ui_texture_slice_pipeline; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d}; @@ -15,16 +16,16 @@ use bevy_render::{ view::ViewVisibility, ExtractSchedule, Render, }; -use bevy_sprite::{SpriteAssetEvents, TextureAtlas}; +use bevy_sprite::{ImageScaleMode, SpriteAssetEvents, TextureAtlas}; pub use pipeline::*; pub use render_pass::*; pub use ui_material_pipeline::*; +use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::graph::{NodeUi, SubGraphUi}; use crate::{ - texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius, - CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, - UiScale, Val, + BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Display, Node, Outline, Style, + TargetCamera, UiImage, UiScale, Val, }; use bevy_app::prelude::*; @@ -106,7 +107,6 @@ pub fn build_ui_render(app: &mut App) { extract_uinode_background_colors.in_set(RenderUiSystem::ExtractBackgrounds), extract_uinode_images.in_set(RenderUiSystem::ExtractImages), extract_uinode_borders.in_set(RenderUiSystem::ExtractBorders), - extract_uinode_outlines.in_set(RenderUiSystem::ExtractBorders), #[cfg(feature = "bevy_text")] extract_uinode_text.in_set(RenderUiSystem::ExtractText), ), @@ -140,6 +140,8 @@ pub fn build_ui_render(app: &mut App) { graph_3d.add_node_edge(Node3d::EndMainPassPostProcessing, NodeUi::UiPass); graph_3d.add_node_edge(NodeUi::UiPass, Node3d::Upscaling); } + + app.add_plugins(UiTextureSlicerPlugin); } fn get_ui_graph(render_app: &mut SubApp) -> RenderGraph { @@ -199,7 +201,6 @@ pub fn extract_uinode_background_colors( Option<&CalculatedClip>, Option<&TargetCamera>, &BackgroundColor, - Option<&BorderRadius>, &Style, Option<&Parent>, )>, @@ -214,7 +215,6 @@ pub fn extract_uinode_background_colors( clip, camera, background_color, - border_radius, style, parent, ) in &uinode_query @@ -255,16 +255,13 @@ pub fn extract_uinode_background_colors( let border = [left, top, right, bottom]; - let border_radius = if let Some(border_radius) = border_radius { - resolve_border_radius( - border_radius, - uinode.size(), - ui_logical_viewport_size, - ui_scale.0, - ) - } else { - [0.; 4] - }; + let border_radius = [ + uinode.border_radius.top_left, + uinode.border_radius.top_right, + uinode.border_radius.bottom_right, + uinode.border_radius.bottom_left, + ] + .map(|r| r * ui_scale.0); extracted_uinodes.uinodes.insert( entity, @@ -299,35 +296,25 @@ pub fn extract_uinode_images( ui_scale: Extract>, default_ui_camera: Extract, uinode_query: Extract< - Query<( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - &UiImage, - Option<&TextureAtlas>, - Option<&ComputedTextureSlices>, - Option<&BorderRadius>, - Option<&Parent>, - &Style, - )>, + Query< + ( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + Option<&TextureAtlas>, + Option<&Parent>, + &Style, + ), + Without, + >, >, node_query: Extract>, ) { - for ( - uinode, - transform, - view_visibility, - clip, - camera, - image, - atlas, - slices, - border_radius, - parent, - style, - ) in &uinode_query + for (uinode, transform, view_visibility, clip, camera, image, atlas, parent, style) in + &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -342,15 +329,6 @@ pub fn extract_uinode_images( continue; } - if let Some(slices) = slices { - extracted_uinodes.uinodes.extend( - slices - .extract_ui_nodes(transform, uinode, image, clip, camera_entity) - .map(|e| (commands.spawn_empty().id(), e)), - ); - continue; - } - let (rect, atlas_scaling) = match atlas { Some(atlas) => { let Some(layout) = texture_atlases.get(&atlas.layout) else { @@ -398,16 +376,13 @@ pub fn extract_uinode_images( let border = [left, top, right, bottom]; - let border_radius = if let Some(border_radius) = border_radius { - resolve_border_radius( - border_radius, - uinode.size(), - ui_logical_viewport_size, - ui_scale.0, - ) - } else { - [0.; 4] - }; + let border_radius = [ + uinode.border_radius.top_left, + uinode.border_radius.top_right, + uinode.border_radius.bottom_right, + uinode.border_radius.bottom_left, + ] + .map(|r| r * ui_scale.0); extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), @@ -442,33 +417,6 @@ pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_s } } -pub(crate) fn resolve_border_radius( - &values: &BorderRadius, - node_size: Vec2, - viewport_size: Vec2, - ui_scale: f32, -) -> [f32; 4] { - let max_radius = 0.5 * node_size.min_element() * ui_scale; - [ - values.top_left, - values.top_right, - values.bottom_right, - values.bottom_left, - ] - .map(|value| { - match value { - Val::Auto => 0., - Val::Px(px) => ui_scale * px, - Val::Percent(percent) => node_size.min_element() * percent / 100., - Val::Vw(percent) => viewport_size.x * percent / 100., - Val::Vh(percent) => viewport_size.y * percent / 100., - Val::VMin(percent) => viewport_size.min_element() * percent / 100., - Val::VMax(percent) => viewport_size.max_element() * percent / 100., - } - .clamp(0., max_radius) - }) -} - #[inline] fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { let s = 0.5 * size + offset; @@ -498,47 +446,44 @@ pub fn extract_uinode_borders( default_ui_camera: Extract, ui_scale: Extract>, uinode_query: Extract< - Query< - ( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - Option<&Parent>, - &Style, - &BorderColor, - &BorderRadius, - ), - Without, - >, + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + Option<&Parent>, + &Style, + AnyOf<(&BorderColor, &Outline)>, + )>, >, node_query: Extract>, ) { let image = AssetId::::default(); for ( - node, + uinode, global_transform, view_visibility, - clip, - camera, - parent, + maybe_clip, + maybe_camera, + maybe_parent, style, - border_color, - border_radius, + (maybe_border_color, maybe_outline), ) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + let Some(camera_entity) = maybe_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) else { continue; }; // Skip invisible borders if !view_visibility.get() - || border_color.0.is_fully_transparent() - || node.size().x <= 0. - || node.size().y <= 0. + || style.display == Display::None + || maybe_border_color.is_some_and(|border_color| border_color.0.is_fully_transparent()) + && maybe_outline.is_some_and(|outline| outline.color.is_fully_transparent()) { continue; } @@ -554,7 +499,7 @@ pub fn extract_uinode_borders( // Both vertical and horizontal percentage border values are calculated based on the width of the parent node // - let parent_width = parent + let parent_width = maybe_parent .and_then(|parent| node_query.get(parent.get()).ok()) .map(|parent_node| parent_node.size().x) .unwrap_or(ui_logical_viewport_size.x); @@ -569,123 +514,27 @@ pub fn extract_uinode_borders( let border = [left, top, right, bottom]; - // don't extract border if no border - if left == 0.0 && top == 0.0 && right == 0.0 && bottom == 0.0 { - continue; - } - - let border_radius = resolve_border_radius( - border_radius, - node.size(), - ui_logical_viewport_size, - ui_scale.0, - ); - - let border_radius = clamp_radius(border_radius, node.size(), border.into()); - let transform = global_transform.compute_matrix(); - - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index: node.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform, - color: border_color.0.into(), - rect: Rect { - max: node.size(), - ..Default::default() - }, - image, - atlas_scaling: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - camera_entity, - border_radius, - border, - node_type: NodeType::Border, - }, - ); - } -} - -pub fn extract_uinode_outlines( - mut commands: Commands, - mut extracted_uinodes: ResMut, - default_ui_camera: Extract, - uinode_query: Extract< - Query<( - &Node, - &GlobalTransform, - &ViewVisibility, - Option<&CalculatedClip>, - Option<&TargetCamera>, - &Outline, - )>, - >, -) { - let image = AssetId::::default(); - for (node, global_transform, view_visibility, maybe_clip, camera, outline) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { - continue; - }; - - // Skip invisible outlines - if !view_visibility.get() - || outline.color.is_fully_transparent() - || node.outline_width == 0. - { - continue; - } + let border_radius = [ + uinode.border_radius.top_left, + uinode.border_radius.top_right, + uinode.border_radius.bottom_right, + uinode.border_radius.bottom_left, + ] + .map(|r| r * ui_scale.0); - // Calculate the outline rects. - let inner_rect = Rect::from_center_size(Vec2::ZERO, node.size() + 2. * node.outline_offset); - let outer_rect = inner_rect.inflate(node.outline_width()); - let outline_edges = [ - // Left edge - Rect::new( - outer_rect.min.x, - outer_rect.min.y, - inner_rect.min.x, - outer_rect.max.y, - ), - // Right edge - Rect::new( - inner_rect.max.x, - outer_rect.min.y, - outer_rect.max.x, - outer_rect.max.y, - ), - // Top edge - Rect::new( - inner_rect.min.x, - outer_rect.min.y, - inner_rect.max.x, - inner_rect.min.y, - ), - // Bottom edge - Rect::new( - inner_rect.min.x, - inner_rect.max.y, - inner_rect.max.x, - outer_rect.max.y, - ), - ]; + let border_radius = clamp_radius(border_radius, uinode.size(), border.into()); - let world_from_local = global_transform.compute_matrix(); - for edge in outline_edges { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { + // don't extract border if no border or the node is zero-sized (a zero sized node can still have an outline). + if !uinode.is_empty() && border != [0.; 4] { + if let Some(border_color) = maybe_border_color { extracted_uinodes.uinodes.insert( commands.spawn_empty().id(), ExtractedUiNode { - stack_index: node.stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: world_from_local - * Mat4::from_translation(edge.center().extend(0.)), - color: outline.color.into(), + stack_index: uinode.stack_index, + transform: global_transform.compute_matrix(), + color: border_color.0.into(), rect: Rect { - max: edge.size(), + max: uinode.size(), ..Default::default() }, image, @@ -694,13 +543,46 @@ pub fn extract_uinode_outlines( flip_x: false, flip_y: false, camera_entity, - border: [0.; 4], - border_radius: [0.; 4], - node_type: NodeType::Rect, + border_radius, + border, + node_type: NodeType::Border, }, ); } } + + if let Some(outline) = maybe_outline { + let outer_distance = uinode.outline_offset() + uinode.outline_width(); + let outline_radius = border_radius.map(|radius| { + if radius > 0. { + radius + outer_distance + } else { + 0. + } + }); + let outline_size = uinode.size() + 2. * outer_distance; + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index: uinode.stack_index, + transform: global_transform.compute_matrix(), + color: outline.color.into(), + rect: Rect { + max: outline_size, + ..Default::default() + }, + image, + atlas_scaling: None, + clip: maybe_clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + camera_entity, + border: [uinode.outline_width(); 4], + border_radius: outline_radius, + node_type: NodeType::Border, + }, + ); + } } } @@ -816,7 +698,7 @@ pub fn extract_uinode_text( }; // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { + if !view_visibility.get() || uinode.is_empty() { continue; } diff --git a/crates/bevy_ui/src/render/ui_texture_slice.wgsl b/crates/bevy_ui/src/render/ui_texture_slice.wgsl new file mode 100644 index 0000000000000..edcc992154ce2 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_texture_slice.wgsl @@ -0,0 +1,127 @@ +#import bevy_render::view::View; +#import bevy_render::globals::Globals; + +@group(0) @binding(0) +var view: View; +@group(0) @binding(1) +var globals: Globals; + +@group(1) @binding(0) var sprite_texture: texture_2d; +@group(1) @binding(1) var sprite_sampler: sampler; + +struct UiVertexOutput { + @location(0) uv: vec2, + @location(1) color: vec4, + + // Defines the dividing line that are used to split the texture atlas rect into corner, side and center slices + // The distances are normalized and from the top left corner of the texture atlas rect + // x = distance of the left vertical dividing line + // y = distance of the top horizontal dividing line + // z = distance of the right vertical dividing line + // w = distance of the bottom horizontal dividing line + @location(2) @interpolate(flat) texture_slices: vec4, + + // Defines the dividing line that are used to split the render target into into corner, side and center slices + // The distances are normalized and from the top left corner of the render target + // x = distance of left vertical dividing line + // y = distance of top horizontal dividing line + // z = distance of right vertical dividing line + // w = distance of bottom horizontal dividing line + @location(3) @interpolate(flat) target_slices: vec4, + + // The number of times the side or center texture slices should be repeated when mapping them to the border slices + // x = number of times to repeat along the horizontal axis for the side textures + // y = number of times to repeat along the vertical axis for the side textures + // z = number of times to repeat along the horizontal axis for the center texture + // w = number of times to repeat along the vertical axis for the center texture + @location(4) @interpolate(flat) repeat: vec4, + + // normalized texture atlas rect coordinates + // x, y = top, left corner of the atlas rect + // z, w = bottom, right corner of the atlas rect + @location(5) @interpolate(flat) atlas_rect: vec4, + @builtin(position) position: vec4, +} + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) vertex_color: vec4, + @location(3) texture_slices: vec4, + @location(4) target_slices: vec4, + @location(5) repeat: vec4, + @location(6) atlas_rect: vec4, +) -> UiVertexOutput { + var out: UiVertexOutput; + out.uv = vertex_uv; + out.color = vertex_color; + out.position = view.clip_from_world * vec4(vertex_position, 1.0); + out.texture_slices = texture_slices; + out.target_slices = target_slices; + out.repeat = repeat; + out.atlas_rect = atlas_rect; + return out; +} + +/// maps a point along the axis of the render target to slice coordinates +fn map_axis_with_repeat( + // normalized distance along the axis + p: f32, + // target min dividing point + il: f32, + // target max dividing point + ih: f32, + // slice min dividing point + tl: f32, + // slice max dividing point + th: f32, + // number of times to repeat the slice for sides and the center + r: f32, +) -> f32 { + if p < il { + // inside one of the two left (horizontal axis) or top (vertical axis) corners + return (p / il) * tl; + } else if ih < p { + // inside one of the two (horizontal axis) or top (vertical axis) corners + return th + ((p - ih) / (1 - ih)) * (1 - th); + } else { + // not inside a corner, repeat the texture + return tl + fract((r * (p - il)) / (ih - il)) * (th - tl); + } +} + +fn map_uvs_to_slice( + uv: vec2, + target_slices: vec4, + texture_slices: vec4, + repeat: vec4, +) -> vec2 { + var r: vec2; + if target_slices.x <= uv.x && uv.x <= target_slices.z && target_slices.y <= uv.y && uv.y <= target_slices.w { + // use the center repeat values if the uv coords are inside the center slice of the target + r = repeat.zw; + } else { + // use the side repeat values if the uv coords are outside the center slice + r = repeat.xy; + } + + // map horizontal axis + let x = map_axis_with_repeat(uv.x, target_slices.x, target_slices.z, texture_slices.x, texture_slices.z, r.x); + + // map vertical axis + let y = map_axis_with_repeat(uv.y, target_slices.y, target_slices.w, texture_slices.y, texture_slices.w, r.y); + + return vec2(x, y); +} + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + // map the target uvs to slice coords + let uv = map_uvs_to_slice(in.uv, in.target_slices, in.texture_slices, in.repeat); + + // map the slice coords to texture coords + let atlas_uv = in.atlas_rect.xy + uv * (in.atlas_rect.zw - in.atlas_rect.xy); + + return in.color * textureSample(sprite_texture, sprite_sampler, atlas_uv); +} diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs new file mode 100644 index 0000000000000..ce60087cf24af --- /dev/null +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -0,0 +1,788 @@ +use std::{hash::Hash, ops::Range}; + +use bevy_asset::*; +use bevy_color::{Alpha, ColorToComponents, LinearRgba}; +use bevy_ecs::{ + prelude::Component, + storage::SparseSet, + system::{ + lifetimeless::{Read, SRes}, + *, + }, +}; +use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_render::{ + render_asset::RenderAssets, + render_phase::*, + render_resource::{binding_types::uniform_buffer, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, Image, TRANSPARENT_IMAGE_HANDLE}, + view::*, + Extract, ExtractSchedule, Render, RenderSet, +}; +use bevy_sprite::{ + ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlas, TextureAtlasLayout, + TextureSlicer, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::HashMap; +use binding_types::{sampler, texture_2d}; +use bytemuck::{Pod, Zeroable}; + +use crate::*; + +pub const UI_SLICER_SHADER_HANDLE: Handle = Handle::weak_from_u128(11156288772117983964); + +pub struct UiTextureSlicerPlugin; + +impl Plugin for UiTextureSlicerPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + UI_SLICER_SHADER_HANDLE, + "ui_texture_slice.wgsl", + Shader::from_wgsl + ); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_render_command::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_ui_texture_slices.after(extract_uinode_images), + ) + .add_systems( + Render, + ( + queue_ui_slices.in_set(RenderSet::Queue), + prepare_ui_slices.in_set(RenderSet::PrepareBindGroups), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::(); + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct UiTextureSliceVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub color: [f32; 4], + pub slices: [f32; 4], + pub border: [f32; 4], + pub repeat: [f32; 4], + pub atlas: [f32; 4], +} + +#[derive(Component)] +pub struct UiTextureSlicerBatch { + pub range: Range, + pub image: AssetId, + pub camera: Entity, +} + +#[derive(Resource)] +pub struct UiTextureSliceMeta { + vertices: RawBufferVec, + indices: RawBufferVec, + view_bind_group: Option, +} + +impl Default for UiTextureSliceMeta { + fn default() -> Self { + Self { + vertices: RawBufferVec::new(BufferUsages::VERTEX), + indices: RawBufferVec::new(BufferUsages::INDEX), + view_bind_group: None, + } + } +} + +#[derive(Resource, Default)] +pub struct UiTextureSliceImageBindGroups { + pub values: HashMap, BindGroup>, +} + +#[derive(Resource)] +pub struct UiTextureSlicePipeline { + pub view_layout: BindGroupLayout, + pub image_layout: BindGroupLayout, +} + +impl FromWorld for UiTextureSlicePipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + let view_layout = render_device.create_bind_group_layout( + "ui_texture_slice_view_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(true), + ), + ); + + let image_layout = render_device.create_bind_group_layout( + "ui_texture_slice_image_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + ), + ), + ); + + UiTextureSlicePipeline { + view_layout, + image_layout, + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct UiTextureSlicePipelineKey { + pub hdr: bool, +} + +impl SpecializedRenderPipeline for UiTextureSlicePipeline { + type Key = UiTextureSlicePipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let vertex_layout = VertexBufferLayout::from_vertex_formats( + VertexStepMode::Vertex, + vec![ + // position + VertexFormat::Float32x3, + // uv + VertexFormat::Float32x2, + // color + VertexFormat::Float32x4, + // normalized texture slicing lines (left, top, right, bottom) + VertexFormat::Float32x4, + // normalized target slicing lines (left, top, right, bottom) + VertexFormat::Float32x4, + // repeat values (horizontal side, vertical side, horizontal center, vertical center) + VertexFormat::Float32x4, + // normalized texture atlas rect (left, top, right, bottom) + VertexFormat::Float32x4, + ], + ); + let shader_defs = Vec::new(); + + RenderPipelineDescriptor { + vertex: VertexState { + shader: UI_SLICER_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_layout], + }, + fragment: Some(FragmentState { + shader: UI_SLICER_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![self.view_layout.clone(), self.image_layout.clone()], + push_constant_ranges: Vec::new(), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("ui_texture_slice_pipeline".into()), + } + } +} + +pub struct ExtractedUiTextureSlice { + pub stack_index: u32, + pub transform: Mat4, + pub rect: Rect, + pub atlas_rect: Option, + pub image: AssetId, + pub clip: Option, + pub camera_entity: Entity, + pub color: LinearRgba, + pub image_scale_mode: ImageScaleMode, + pub flip_x: bool, + pub flip_y: bool, +} + +#[derive(Resource, Default)] +pub struct ExtractedUiTextureSlices { + pub slices: SparseSet, +} + +pub fn extract_ui_texture_slices( + mut commands: Commands, + mut extracted_ui_slicers: ResMut, + default_ui_camera: Extract, + texture_atlases: Extract>>, + slicers_query: Extract< + Query<( + &Node, + &GlobalTransform, + &ViewVisibility, + Option<&CalculatedClip>, + Option<&TargetCamera>, + &UiImage, + &ImageScaleMode, + Option<&TextureAtlas>, + )>, + >, +) { + for (uinode, transform, view_visibility, clip, camera, image, image_scale_mode, atlas) in + &slicers_query + { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) + else { + continue; + }; + + // Skip invisible images + if !view_visibility.get() + || image.color.is_fully_transparent() + || image.texture.id() == TRANSPARENT_IMAGE_HANDLE.id() + { + continue; + } + + let atlas_rect = atlas.and_then(|atlas| { + texture_atlases + .get(&atlas.layout) + .map(|layout| layout.textures[atlas.index].as_rect()) + }); + + extracted_ui_slicers.slices.insert( + commands.spawn_empty().id(), + ExtractedUiTextureSlice { + stack_index: uinode.stack_index, + transform: transform.compute_matrix(), + color: image.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + clip: clip.map(|clip| clip.clip), + image: image.texture.id(), + camera_entity, + image_scale_mode: image_scale_mode.clone(), + atlas_rect, + flip_x: image.flip_x, + flip_y: image.flip_y, + }, + ); + } +} + +pub fn queue_ui_slices( + extracted_ui_slicers: ResMut, + ui_slicer_pipeline: Res, + mut pipelines: ResMut>, + mut transparent_render_phases: ResMut>, + mut views: Query<(Entity, &ExtractedView)>, + pipeline_cache: Res, + draw_functions: Res>, +) { + let draw_function = draw_functions.read().id::(); + for (entity, extracted_slicer) in extracted_ui_slicers.slices.iter() { + let Ok((view_entity, view)) = views.get_mut(extracted_slicer.camera_entity) else { + continue; + }; + + let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + continue; + }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_slicer_pipeline, + UiTextureSlicePipelineKey { hdr: view.hdr }, + ); + + transparent_phase.add(TransparentUi { + draw_function, + pipeline, + entity: *entity, + sort_key: ( + FloatOrd(extracted_slicer.stack_index as f32), + entity.index(), + ), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::NONE, + }); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_ui_slices( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut ui_meta: ResMut, + mut extracted_slices: ResMut, + view_uniforms: Res, + texture_slicer_pipeline: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut phases: ResMut>, + events: Res, + mut previous_len: Local, +) { + // If an image has changed, the GpuImage has (probably) changed + for event in &events.images { + match event { + AssetEvent::Added { .. } | + AssetEvent::Unused { .. } | + // Images don't have dependencies + AssetEvent::LoadedWithDependencies { .. } => {} + AssetEvent::Modified { id } | AssetEvent::Removed { id } => { + image_bind_groups.values.remove(id); + } + }; + } + + if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); + ui_meta.indices.clear(); + ui_meta.view_bind_group = Some(render_device.create_bind_group( + "ui_texture_slice_view_bind_group", + &texture_slicer_pipeline.view_layout, + &BindGroupEntries::single(view_binding), + )); + + // Buffer indexes + let mut vertices_index = 0; + let mut indices_index = 0; + + for ui_phase in phases.values_mut() { + let mut batch_item_index = 0; + let mut batch_image_handle = AssetId::invalid(); + let mut batch_image_size = Vec2::ZERO; + + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + if let Some(texture_slices) = extracted_slices.slices.get(item.entity) { + let mut existing_batch = batches.last_mut(); + + if batch_image_handle == AssetId::invalid() + || existing_batch.is_none() + || (batch_image_handle != AssetId::default() + && texture_slices.image != AssetId::default() + && batch_image_handle != texture_slices.image) + || existing_batch.as_ref().map(|(_, b)| b.camera) + != Some(texture_slices.camera_entity) + { + if let Some(gpu_image) = gpu_images.get(texture_slices.image) { + batch_item_index = item_index; + batch_image_handle = texture_slices.image; + batch_image_size = gpu_image.size.as_vec2(); + + let new_batch = UiTextureSlicerBatch { + range: vertices_index..vertices_index, + image: texture_slices.image, + camera: texture_slices.camera_entity, + }; + + batches.push((item.entity, new_batch)); + + image_bind_groups + .values + .entry(batch_image_handle) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_texture_slice_image_layout", + &texture_slicer_pipeline.image_layout, + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + + existing_batch = batches.last_mut(); + } else { + continue; + } + } else if batch_image_handle == AssetId::default() + && texture_slices.image != AssetId::default() + { + if let Some(gpu_image) = gpu_images.get(texture_slices.image) { + batch_image_handle = texture_slices.image; + batch_image_size = gpu_image.size.as_vec2(); + existing_batch.as_mut().unwrap().1.image = texture_slices.image; + + image_bind_groups + .values + .entry(batch_image_handle) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_texture_slice_image_layout", + &texture_slicer_pipeline.image_layout, + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + } else { + continue; + } + } + + let uinode_rect = texture_slices.rect; + + let rect_size = uinode_rect.size().extend(1.0); + + // Specify the corners of the node + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + + // Calculate the effect of clipping + // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) + let positions_diff = if let Some(clip) = texture_slices.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + + let transformed_rect_size = + texture_slices.transform.transform_vector3(rect_size); + + // Don't try to cull nodes that have a rotation + // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or Ï€ + // In those two cases, the culling check can proceed normally as corners will be on + // horizontal / vertical lines + // For all other angles, bypass the culling check + // This does not properly handles all rotations on all axis + if texture_slices.transform.x_axis[1] == 0.0 { + // Cull nodes that are completely clipped + if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y + { + continue; + } + } + let flags = if texture_slices.image != AssetId::default() { + shader_flags::TEXTURED + } else { + shader_flags::UNTEXTURED + }; + + let uvs = if flags == shader_flags::UNTEXTURED { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + } else { + let atlas_extent = uinode_rect.max; + [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) + }; + + let color = texture_slices.color.to_f32_array(); + + let (image_size, mut atlas) = if let Some(atlas) = texture_slices.atlas_rect { + ( + atlas.size(), + [ + atlas.min.x / batch_image_size.x, + atlas.min.y / batch_image_size.y, + atlas.max.x / batch_image_size.x, + atlas.max.y / batch_image_size.y, + ], + ) + } else { + (batch_image_size, [0., 0., 1., 1.]) + }; + + if texture_slices.flip_x { + atlas.swap(0, 2); + } + + if texture_slices.flip_y { + atlas.swap(1, 3); + } + + let [slices, border, repeat] = compute_texture_slices( + image_size, + uinode_rect.size(), + &texture_slices.image_scale_mode, + ); + + for i in 0..4 { + ui_meta.vertices.push(UiTextureSliceVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + slices, + border, + repeat, + atlas, + }); + } + + for &i in &QUAD_INDICES { + ui_meta.indices.push(indices_index + i as u32); + } + + vertices_index += 6; + indices_index += 4; + + existing_batch.unwrap().1.range.end = vertices_index; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; + } else { + batch_image_handle = AssetId::invalid(); + } + } + } + ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); + } + extracted_slices.slices.clear(); +} + +pub type DrawUiTextureSlices = ( + SetItemPipeline, + SetSlicerViewBindGroup<0>, + SetSlicerTextureBindGroup<1>, + DrawSlicer, +); + +pub struct SetSlicerViewBindGroup; +impl RenderCommand

for SetSlicerViewBindGroup { + type Param = SRes; + type ViewQuery = Read; + type ItemQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: Option<()>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else { + return RenderCommandResult::Failure("view_bind_group not available"); + }; + pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]); + RenderCommandResult::Success + } +} +pub struct SetSlicerTextureBindGroup; +impl RenderCommand

for SetSlicerTextureBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w UiTextureSlicerBatch>, + image_bind_groups: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let image_bind_groups = image_bind_groups.into_inner(); + let Some(batch) = batch else { + return RenderCommandResult::Skip; + }; + + pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]); + RenderCommandResult::Success + } +} +pub struct DrawSlicer; +impl RenderCommand

for DrawSlicer { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w UiTextureSlicerBatch>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(batch) = batch else { + return RenderCommandResult::Skip; + }; + let ui_meta = ui_meta.into_inner(); + let Some(vertices) = ui_meta.vertices.buffer() else { + return RenderCommandResult::Failure("missing vertices to draw ui"); + }; + let Some(indices) = ui_meta.indices.buffer() else { + return RenderCommandResult::Failure("missing indices to draw ui"); + }; + + // Store the vertices + pass.set_vertex_buffer(0, vertices.slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); + RenderCommandResult::Success + } +} + +fn compute_texture_slices( + image_size: Vec2, + target_size: Vec2, + image_scale_mode: &ImageScaleMode, +) -> [[f32; 4]; 3] { + match image_scale_mode { + ImageScaleMode::Sliced(TextureSlicer { + border: border_rect, + center_scale_mode, + sides_scale_mode, + max_corner_scale, + }) => { + let min_coeff = (target_size / image_size) + .min_element() + .min(*max_corner_scale); + + // calculate the normalized extents of the nine-patched image slices + let slices = [ + border_rect.left / image_size.x, + border_rect.top / image_size.y, + 1. - border_rect.right / image_size.x, + 1. - border_rect.bottom / image_size.y, + ]; + + // calculate the normalized extents of the target slices + let border = [ + (border_rect.left / target_size.x) * min_coeff, + (border_rect.top / target_size.y) * min_coeff, + 1. - (border_rect.right / target_size.x) * min_coeff, + 1. - (border_rect.bottom / target_size.y) * min_coeff, + ]; + + let image_side_width = image_size.x * (slices[2] - slices[0]); + let image_side_height = image_size.y * (slices[2] - slices[1]); + let target_side_height = target_size.x * (border[2] - border[0]); + let target_side_width = target_size.y * (border[3] - border[1]); + + // compute the number of times to repeat the side and center slices when tiling along each axis + // if the returned value is `1.` the slice will be stretched to fill the axis. + let repeat_side_x = + compute_tiled_subaxis(image_side_width, target_side_height, sides_scale_mode); + let repeat_side_y = + compute_tiled_subaxis(image_side_height, target_side_width, sides_scale_mode); + let repeat_center_x = + compute_tiled_subaxis(image_side_width, target_side_height, center_scale_mode); + let repeat_center_y = + compute_tiled_subaxis(image_side_height, target_side_width, center_scale_mode); + + [ + slices, + border, + [ + repeat_side_x, + repeat_side_y, + repeat_center_x, + repeat_center_y, + ], + ] + } + ImageScaleMode::Tiled { + tile_x, + tile_y, + stretch_value, + } => { + let rx = compute_tiled_axis(*tile_x, image_size.x, target_size.x, *stretch_value); + let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value); + [[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]] + } + } +} + +fn compute_tiled_axis(tile: bool, image_extent: f32, target_extent: f32, stretch: f32) -> f32 { + if tile { + let s = image_extent * stretch; + target_extent / s + } else { + 1. + } +} + +fn compute_tiled_subaxis(image_extent: f32, target_extent: f32, mode: &SliceScaleMode) -> f32 { + match mode { + SliceScaleMode::Stretch => 1., + SliceScaleMode::Tile { stretch_value } => { + let s = image_extent * *stretch_value; + target_extent / s + } + } +} diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs deleted file mode 100644 index 6b0ed38f1c186..0000000000000 --- a/crates/bevy_ui/src/texture_slice.rs +++ /dev/null @@ -1,213 +0,0 @@ -// This module is mostly copied and pasted from `bevy_sprite::texture_slice` -// -// A more centralized solution should be investigated in the future - -use bevy_asset::{AssetEvent, Assets}; -use bevy_ecs::prelude::*; -use bevy_math::{Rect, Vec2}; -use bevy_render::texture::Image; -use bevy_sprite::{ImageScaleMode, TextureAtlas, TextureAtlasLayout, TextureSlice}; -use bevy_transform::prelude::*; -use bevy_utils::HashSet; - -use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage}; - -/// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] -/// -/// This component is automatically inserted and updated -#[derive(Debug, Clone, Component)] -pub struct ComputedTextureSlices { - slices: Vec, -} - -impl ComputedTextureSlices { - /// Computes [`ExtractedUiNode`] iterator from the sprite slices - /// - /// # Arguments - /// - /// * `transform` - the sprite entity global transform - /// * `original_entity` - the sprite entity - /// * `sprite` - The sprite component - /// * `handle` - The sprite texture handle - #[must_use] - pub(crate) fn extract_ui_nodes<'a>( - &'a self, - transform: &'a GlobalTransform, - node: &'a Node, - image: &'a UiImage, - clip: Option<&'a CalculatedClip>, - camera_entity: Entity, - ) -> impl ExactSizeIterator + 'a { - let mut flip = Vec2::new(1.0, -1.0); - let [mut flip_x, mut flip_y] = [false; 2]; - if image.flip_x { - flip.x *= -1.0; - flip_x = true; - } - if image.flip_y { - flip.y *= -1.0; - flip_y = true; - } - self.slices.iter().map(move |slice| { - let offset = (slice.offset * flip).extend(0.0); - let transform = transform.mul_transform(Transform::from_translation(offset)); - let scale = slice.draw_size / slice.texture_rect.size(); - let mut rect = slice.texture_rect; - rect.min *= scale; - rect.max *= scale; - ExtractedUiNode { - stack_index: node.stack_index, - color: image.color.into(), - transform: transform.compute_matrix(), - rect, - flip_x, - flip_y, - image: image.texture.id(), - atlas_scaling: Some(scale), - clip: clip.map(|clip| clip.clip), - camera_entity, - border: [0.; 4], - border_radius: [0.; 4], - node_type: NodeType::Rect, - } - }) - } -} - -/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices -/// will be computed according to the `image_handle` dimensions. -/// -/// Returns `None` if the image asset is not loaded -/// -/// # Arguments -/// -/// * `draw_area` - The size of the drawing area the slices will have to fit into -/// * `scale_mode` - The image scaling component -/// * `image_handle` - The texture to slice or tile -/// * `images` - The image assets, use to retrieve the image dimensions -/// * `atlas` - Optional texture atlas, if set the slicing will happen on the matching sub section -/// of the texture -/// * `atlas_layouts` - The atlas layout assets, used to retrieve the texture atlas section rect -#[must_use] -fn compute_texture_slices( - draw_area: Vec2, - scale_mode: &ImageScaleMode, - image_handle: &UiImage, - images: &Assets, - atlas: Option<&TextureAtlas>, - atlas_layouts: &Assets, -) -> Option { - let texture_rect = match atlas { - Some(a) => { - let layout = atlas_layouts.get(&a.layout)?; - layout.textures.get(a.index)?.as_rect() - } - None => { - let image = images.get(&image_handle.texture)?; - let size = Vec2::new( - image.texture_descriptor.size.width as f32, - image.texture_descriptor.size.height as f32, - ); - Rect { - min: Vec2::ZERO, - max: size, - } - } - }; - let slices = match scale_mode { - ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, Some(draw_area)), - ImageScaleMode::Tiled { - tile_x, - tile_y, - stretch_value, - } => { - let slice = TextureSlice { - texture_rect, - draw_size: draw_area, - offset: Vec2::ZERO, - }; - slice.tiled(*stretch_value, (*tile_x, *tile_y)) - } - }; - Some(ComputedTextureSlices { slices }) -} - -/// System reacting to added or modified [`Image`] handles, and recompute sprite slices -/// on matching sprite entities with a [`ImageScaleMode`] component -pub(crate) fn compute_slices_on_asset_event( - mut commands: Commands, - mut events: EventReader>, - images: Res>, - atlas_layouts: Res>, - ui_nodes: Query<( - Entity, - &ImageScaleMode, - &Node, - &UiImage, - Option<&TextureAtlas>, - )>, -) { - // We store the asset ids of added/modified image assets - let added_handles: HashSet<_> = events - .read() - .filter_map(|e| match e { - AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id), - _ => None, - }) - .collect(); - if added_handles.is_empty() { - return; - } - // We recompute the sprite slices for sprite entities with a matching asset handle id - for (entity, scale_mode, ui_node, image, atlas) in &ui_nodes { - if !added_handles.contains(&image.texture.id()) { - continue; - } - if let Some(slices) = compute_texture_slices( - ui_node.size(), - scale_mode, - image, - &images, - atlas, - &atlas_layouts, - ) { - commands.entity(entity).try_insert(slices); - } - } -} - -/// System reacting to changes on relevant sprite bundle components to compute the sprite slices -/// on matching sprite entities with a [`ImageScaleMode`] component -pub(crate) fn compute_slices_on_image_change( - mut commands: Commands, - images: Res>, - atlas_layouts: Res>, - changed_nodes: Query< - ( - Entity, - &ImageScaleMode, - &Node, - &UiImage, - Option<&TextureAtlas>, - ), - Or<( - Changed, - Changed, - Changed, - Changed, - )>, - >, -) { - for (entity, scale_mode, ui_node, image, atlas) in &changed_nodes { - if let Some(slices) = compute_texture_slices( - ui_node.size(), - scale_mode, - image, - &images, - atlas, - &atlas_layouts, - ) { - commands.entity(entity).try_insert(slices); - } - } -} diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index fb285823b3798..eb6e6fed9967a 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -12,7 +12,7 @@ use bevy_transform::prelude::GlobalTransform; use bevy_utils::warn_once; use bevy_window::{PrimaryWindow, WindowRef}; use smallvec::SmallVec; -use std::num::{NonZeroI16, NonZeroU16}; +use std::num::NonZero; use thiserror::Error; /// Base component for a UI node, which also provides the computed size of the node. @@ -35,15 +35,24 @@ pub struct Node { pub(crate) calculated_size: Vec2, /// The width of this node's outline. /// If this value is `Auto`, negative or `0.` then no outline will be rendered. + /// Outline updates bypass change detection. /// - /// Automatically calculated by [`super::layout::resolve_outlines_system`]. + /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) outline_width: f32, /// The amount of space between the outline and the edge of the node. + /// Outline updates bypass change detection. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) outline_offset: f32, /// The unrounded size of the node as width and height in logical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) unrounded_size: Vec2, + /// Resolved border radius values in logical pixels. + /// Border radius updates bypass change detection. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + pub(crate) border_radius: ResolvedBorderRadius, } impl Node { @@ -54,6 +63,13 @@ impl Node { self.calculated_size } + /// Check if the node is empty. + /// A node is considered empty if it has a zero or negative extent along either of its axes. + #[inline] + pub fn is_empty(&self) -> bool { + self.size().cmple(Vec2::ZERO).any() + } + /// The order of the node in the UI layout. /// Nodes with a higher stack index are drawn on top of and receive interactions before nodes with lower stack indices. pub const fn stack_index(&self) -> u32 { @@ -104,11 +120,17 @@ impl Node { } #[inline] - /// Returns the thickness of the UI node's outline. + /// Returns the thickness of the UI node's outline in logical pixels. /// If this value is negative or `0.` then no outline will be rendered. pub fn outline_width(&self) -> f32 { self.outline_width } + + #[inline] + /// Returns the amount of space between the outline and the edge of the node in logical pixels. + pub fn outline_offset(&self) -> f32 { + self.outline_offset + } } impl Node { @@ -118,6 +140,7 @@ impl Node { outline_width: 0., outline_offset: 0., unrounded_size: Vec2::ZERO, + border_radius: ResolvedBorderRadius::ZERO, }; } @@ -1481,15 +1504,15 @@ pub struct GridPlacement { /// Lines are 1-indexed. /// Negative indexes count backwards from the end of the grid. /// Zero is not a valid index. - pub(crate) start: Option, + pub(crate) start: Option>, /// How many grid tracks the item should span. /// Defaults to 1. - pub(crate) span: Option, + pub(crate) span: Option>, /// The grid line at which the item should end. /// Lines are 1-indexed. /// Negative indexes count backwards from the end of the grid. /// Zero is not a valid index. - pub(crate) end: Option, + pub(crate) end: Option>, } impl GridPlacement { @@ -1497,7 +1520,7 @@ impl GridPlacement { pub const DEFAULT: Self = Self { start: None, // SAFETY: This is trivially safe as 1 is non-zero. - span: Some(unsafe { NonZeroU16::new_unchecked(1) }), + span: Some(unsafe { NonZero::::new_unchecked(1) }), end: None, }; @@ -1614,17 +1637,17 @@ impl GridPlacement { /// Returns the grid line at which the item should start, or `None` if not set. pub fn get_start(self) -> Option { - self.start.map(NonZeroI16::get) + self.start.map(NonZero::::get) } /// Returns the grid line at which the item should end, or `None` if not set. pub fn get_end(self) -> Option { - self.end.map(NonZeroI16::get) + self.end.map(NonZero::::get) } /// Returns span for this grid item, or `None` if not set. pub fn get_span(self) -> Option { - self.span.map(NonZeroU16::get) + self.span.map(NonZero::::get) } } @@ -1634,17 +1657,17 @@ impl Default for GridPlacement { } } -/// Convert an `i16` to `NonZeroI16`, fails on `0` and returns the `InvalidZeroIndex` error. -fn try_into_grid_index(index: i16) -> Result, GridPlacementError> { +/// Convert an `i16` to `NonZero`, fails on `0` and returns the `InvalidZeroIndex` error. +fn try_into_grid_index(index: i16) -> Result>, GridPlacementError> { Ok(Some( - NonZeroI16::new(index).ok_or(GridPlacementError::InvalidZeroIndex)?, + NonZero::::new(index).ok_or(GridPlacementError::InvalidZeroIndex)?, )) } -/// Convert a `u16` to `NonZeroU16`, fails on `0` and returns the `InvalidZeroSpan` error. -fn try_into_grid_span(span: u16) -> Result, GridPlacementError> { +/// Convert a `u16` to `NonZero`, fails on `0` and returns the `InvalidZeroSpan` error. +fn try_into_grid_span(span: u16) -> Result>, GridPlacementError> { Ok(Some( - NonZeroU16::new(span).ok_or(GridPlacementError::InvalidZeroSpan)?, + NonZero::::new(span).ok_or(GridPlacementError::InvalidZeroSpan)?, )) } @@ -2184,6 +2207,49 @@ impl BorderRadius { self.bottom_right = radius; self } + + /// Compute the logical border radius for a single corner from the given values + pub fn resolve_single_corner(radius: Val, node_size: Vec2, viewport_size: Vec2) -> f32 { + match radius { + Val::Auto => 0., + Val::Px(px) => px, + Val::Percent(percent) => node_size.min_element() * percent / 100., + Val::Vw(percent) => viewport_size.x * percent / 100., + Val::Vh(percent) => viewport_size.y * percent / 100., + Val::VMin(percent) => viewport_size.min_element() * percent / 100., + Val::VMax(percent) => viewport_size.max_element() * percent / 100., + } + .clamp(0., 0.5 * node_size.min_element()) + } + + pub fn resolve(&self, node_size: Vec2, viewport_size: Vec2) -> ResolvedBorderRadius { + ResolvedBorderRadius { + top_left: Self::resolve_single_corner(self.top_left, node_size, viewport_size), + top_right: Self::resolve_single_corner(self.top_right, node_size, viewport_size), + bottom_left: Self::resolve_single_corner(self.bottom_left, node_size, viewport_size), + bottom_right: Self::resolve_single_corner(self.bottom_right, node_size, viewport_size), + } + } +} + +/// Represents the resolved border radius values for a UI node. +/// +/// The values are in logical pixels. +#[derive(Copy, Clone, Debug, PartialEq, Reflect)] +pub struct ResolvedBorderRadius { + pub top_left: f32, + pub top_right: f32, + pub bottom_left: f32, + pub bottom_right: f32, +} + +impl ResolvedBorderRadius { + pub const ZERO: Self = Self { + top_left: 0., + top_right: 0., + bottom_left: 0., + bottom_right: 0., + }; } #[cfg(test)] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 5750cc6e77e4d..515dae7f05bf6 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -19,7 +19,7 @@ use bevy_text::{ scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextBounds, TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, }; -use bevy_utils::Entry; +use bevy_utils::{tracing::error, Entry}; use taffy::style::AvailableSpace; /// Text system flags @@ -47,12 +47,20 @@ pub struct TextMeasure { pub info: TextMeasureInfo, } +impl TextMeasure { + /// Checks if the cosmic text buffer is needed for measuring the text. + pub fn needs_buffer(height: Option, available_width: AvailableSpace) -> bool { + height.is_none() && matches!(available_width, AvailableSpace::Definite(_)) + } +} + impl Measure for TextMeasure { fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { let MeasureArgs { width, height, available_width, + buffer, font_system, .. } = measure_args; @@ -71,9 +79,18 @@ impl Measure for TextMeasure { height .map_or_else( || match available_width { - AvailableSpace::Definite(_) => self - .info - .compute_size(TextBounds::new_horizontal(x), font_system), + AvailableSpace::Definite(_) => { + if let Some(buffer) = buffer { + self.info.compute_size( + TextBounds::new_horizontal(x), + buffer, + font_system, + ) + } else { + error!("text measure failed, buffer is missing"); + Vec2::default() + } + } AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, @@ -86,6 +103,7 @@ impl Measure for TextMeasure { #[allow(clippy::too_many_arguments)] #[inline] fn create_text_measure( + entity: Entity, fonts: &Assets, scale_factor: f64, text: Ref, @@ -96,6 +114,7 @@ fn create_text_measure( text_alignment: JustifyText, ) { match text_pipeline.create_text_measure( + entity, fonts, &text.sections, scale_factor, @@ -133,7 +152,9 @@ fn create_text_measure( /// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on /// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection) /// method should be called when only changing the `Text`'s colors. +#[allow(clippy::too_many_arguments)] pub fn measure_text_system( + mut scale_factors_buffer: Local>, mut last_scale_factors: Local>, fonts: Res>, camera_query: Query<(Entity, &Camera)>, @@ -141,6 +162,7 @@ pub fn measure_text_system( ui_scale: Res, mut text_query: Query< ( + Entity, Ref, &mut ContentSize, &mut TextFlags, @@ -151,14 +173,14 @@ pub fn measure_text_system( >, mut text_pipeline: ResMut, ) { - let mut scale_factors: EntityHashMap = EntityHashMap::default(); + scale_factors_buffer.clear(); - for (text, content_size, text_flags, camera, mut buffer) in &mut text_query { + for (entity, text, content_size, text_flags, camera, mut buffer) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; - let scale_factor = match scale_factors.entry(camera_entity) { + let scale_factor = match scale_factors_buffer.entry(camera_entity) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => *entry.insert( camera_query @@ -176,6 +198,7 @@ pub fn measure_text_system( { let text_alignment = text.justify; create_text_measure( + entity, &fonts, scale_factor.into(), text, @@ -187,7 +210,7 @@ pub fn measure_text_system( ); } } - *last_scale_factors = scale_factors; + std::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer); } #[allow(clippy::too_many_arguments)] @@ -203,7 +226,7 @@ fn queue_text( text: &Text, node: Ref, mut text_flags: Mut, - mut text_layout_info: Mut, + text_layout_info: Mut, buffer: &mut CosmicBuffer, ) { // Skip the text node if it is waiting for a new measure func @@ -219,7 +242,9 @@ fn queue_text( ) }; + let text_layout_info = text_layout_info.into_inner(); match text_pipeline.queue_text( + text_layout_info, fonts, &text.sections, scale_factor.into(), @@ -239,10 +264,11 @@ fn queue_text( Err(e @ (TextError::FailedToAddGlyph(_) | TextError::FailedToGetGlyphImage(_))) => { panic!("Fatal error when processing text: {e}."); } - Ok(mut info) => { - info.size.x = scale_value(info.size.x, inverse_scale_factor); - info.size.y = scale_value(info.size.y, inverse_scale_factor); - *text_layout_info = info; + Ok(()) => { + text_layout_info.size.x = + scale_value(text_layout_info.size.x, inverse_scale_factor); + text_layout_info.size.y = + scale_value(text_layout_info.size.y, inverse_scale_factor); text_flags.needs_recompute = false; } } @@ -260,6 +286,7 @@ fn queue_text( #[allow(clippy::too_many_arguments)] pub fn text_system( mut textures: ResMut>, + mut scale_factors_buffer: Local>, mut last_scale_factors: Local>, fonts: Res>, camera_query: Query<(Entity, &Camera)>, @@ -277,14 +304,14 @@ pub fn text_system( &mut CosmicBuffer, )>, ) { - let mut scale_factors: EntityHashMap = EntityHashMap::default(); + scale_factors_buffer.clear(); for (node, text, text_layout_info, text_flags, camera, mut buffer) in &mut text_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; - let scale_factor = match scale_factors.entry(camera_entity) { + let scale_factor = match scale_factors_buffer.entry(camera_entity) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => *entry.insert( camera_query @@ -317,5 +344,5 @@ pub fn text_system( ); } } - *last_scale_factors = scale_factors; + std::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer); } diff --git a/crates/bevy_utils/src/cow_arc.rs b/crates/bevy_utils/src/cow_arc.rs deleted file mode 100644 index 635d31a583ef6..0000000000000 --- a/crates/bevy_utils/src/cow_arc.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::{ - borrow::Borrow, - fmt::{Debug, Display}, - hash::Hash, - ops::Deref, - path::{Path, PathBuf}, - sync::Arc, -}; - -/// Much like a [`Cow`](std::borrow::Cow), but owned values are Arc-ed to make clones cheap. This should be used for values that -/// are cloned for use across threads and change rarely (if ever). -/// -/// This also makes an opinionated tradeoff by adding a [`CowArc::Static`] and implementing [`From<&'static T>`] instead of -/// [`From<'a T>`]. This preserves the static context and prevents conversion to [`CowArc::Owned`] in cases where a reference -/// is known to be static. This is an optimization that prevents allocations and atomic ref-counting. -/// -/// This means that static references should prefer [`From::from`] or [`CowArc::Static`] and non-static references must -/// use [`CowArc::Borrowed`]. -pub enum CowArc<'a, T: ?Sized + 'static> { - /// A borrowed value - Borrowed(&'a T), - /// A static value reference. This exists to avoid conversion to [`CowArc::Owned`] in cases where a reference is - /// known to be static. This is an optimization that prevents allocations and atomic ref-counting. - Static(&'static T), - /// An owned [`Arc`]-ed value - Owned(Arc), -} - -impl CowArc<'static, T> { - /// Indicates this [`CowArc`] should have a static lifetime. - /// This ensures if this was created with a value `Borrowed(&'static T)`, it is replaced with `Static(&'static T)`. - #[inline] - pub fn as_static(self) -> Self { - match self { - Self::Borrowed(value) | Self::Static(value) => Self::Static(value), - Self::Owned(value) => Self::Owned(value), - } - } -} - -impl<'a, T: ?Sized> Deref for CowArc<'a, T> { - type Target = T; - - #[inline] - fn deref(&self) -> &Self::Target { - match self { - CowArc::Borrowed(v) | CowArc::Static(v) => v, - CowArc::Owned(v) => v, - } - } -} - -impl<'a, T: ?Sized> Borrow for CowArc<'a, T> { - #[inline] - fn borrow(&self) -> &T { - self - } -} - -impl<'a, T: ?Sized> AsRef for CowArc<'a, T> { - #[inline] - fn as_ref(&self) -> &T { - self - } -} - -impl<'a, T: ?Sized> CowArc<'a, T> -where - &'a T: Into>, -{ - /// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already a [`CowArc::Owned`] or a [`CowArc::Static`], it will remain unchanged. - #[inline] - pub fn into_owned(self) -> CowArc<'static, T> { - match self { - CowArc::Borrowed(value) => CowArc::Owned(value.into()), - CowArc::Static(value) => CowArc::Static(value), - CowArc::Owned(value) => CowArc::Owned(value), - } - } - - /// Clones into an owned [`CowArc<'static>`]. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]". - /// If it is already a [`CowArc::Owned`] or [`CowArc::Static`], the value will be cloned. - /// This is equivalent to `.clone().into_owned()`. - #[inline] - pub fn clone_owned(&self) -> CowArc<'static, T> { - self.clone().into_owned() - } -} - -impl<'a, T: ?Sized> Clone for CowArc<'a, T> { - #[inline] - fn clone(&self) -> Self { - match self { - Self::Borrowed(value) => Self::Borrowed(value), - Self::Static(value) => Self::Static(value), - Self::Owned(value) => Self::Owned(value.clone()), - } - } -} - -impl<'a, T: PartialEq + ?Sized> PartialEq for CowArc<'a, T> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.deref().eq(other.deref()) - } -} - -impl<'a, T: PartialEq + ?Sized> Eq for CowArc<'a, T> {} - -impl<'a, T: Hash + ?Sized> Hash for CowArc<'a, T> { - #[inline] - fn hash(&self, state: &mut H) { - self.deref().hash(state); - } -} - -impl<'a, T: Debug + ?Sized> Debug for CowArc<'a, T> { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(self.deref(), f) - } -} - -impl<'a, T: Display + ?Sized> Display for CowArc<'a, T> { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Display::fmt(self.deref(), f) - } -} - -impl<'a, T: PartialOrd + ?Sized> PartialOrd for CowArc<'a, T> { - #[inline] - fn partial_cmp(&self, other: &Self) -> Option { - self.deref().partial_cmp(other.deref()) - } -} - -impl Default for CowArc<'static, str> { - fn default() -> Self { - CowArc::Static(Default::default()) - } -} - -impl Default for CowArc<'static, Path> { - fn default() -> Self { - CowArc::Static(Path::new("")) - } -} - -impl<'a, T: Ord + ?Sized> Ord for CowArc<'a, T> { - #[inline] - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.deref().cmp(other.deref()) - } -} - -impl From for CowArc<'static, Path> { - #[inline] - fn from(value: PathBuf) -> Self { - CowArc::Owned(value.into()) - } -} - -impl From<&'static str> for CowArc<'static, Path> { - #[inline] - fn from(value: &'static str) -> Self { - CowArc::Static(Path::new(value)) - } -} - -impl From for CowArc<'static, str> { - #[inline] - fn from(value: String) -> Self { - CowArc::Owned(value.into()) - } -} - -impl<'a> From<&'a String> for CowArc<'a, str> { - #[inline] - fn from(value: &'a String) -> Self { - CowArc::Borrowed(value) - } -} - -impl From<&'static T> for CowArc<'static, T> { - #[inline] - fn from(value: &'static T) -> Self { - CowArc::Static(value) - } -} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index a37a6e18ba3fc..b53bcd3e2ea89 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -10,7 +10,9 @@ //! [Bevy]: https://bevyengine.org/ //! -#[allow(missing_docs)] +/// The utilities prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { pub use crate::default; } @@ -21,7 +23,6 @@ pub use short_names::get_short_name; pub mod synccell; pub mod syncunsafecell; -mod cow_arc; mod default; mod object_safe; pub use object_safe::assert_object_safe; @@ -30,7 +31,6 @@ mod parallel_queue; pub use ahash::{AHasher, RandomState}; pub use bevy_utils_proc_macros::*; -pub use cow_arc::*; pub use default::default; pub use hashbrown; pub use parallel_queue::*; diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 6159c6ec94698..8c922a9c3d10c 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -16,6 +16,7 @@ serialize = ["serde", "smol_str/serde", "bevy_ecs/serialize"] bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "glam", diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 682ccde3bd80f..caf97be6f41e6 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,8 +1,13 @@ #![allow(deprecated)] use std::path::PathBuf; -use bevy_ecs::entity::Entity; -use bevy_ecs::event::Event; +use bevy_ecs::{entity::Entity, event::Event}; +use bevy_input::{ + gestures::*, + keyboard::{KeyboardFocusLost, KeyboardInput}, + mouse::{MouseButtonInput, MouseMotion, MouseWheel}, + touch::TouchInput, +}; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; use smol_str::SmolStr; @@ -413,3 +418,193 @@ impl AppLifecycle { } } } + +/// Wraps all `bevy_window` and `bevy_input` events in a common enum. +/// +/// Read these events with `EventReader` if you need to +/// access window events in the order they were received from the +/// operating system. Otherwise, the event types are individually +/// readable with `EventReader` (e.g. `EventReader`). +#[derive(Event, Debug, Clone, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[allow(missing_docs)] +pub enum WindowEvent { + AppLifecycle(AppLifecycle), + CursorEntered(CursorEntered), + CursorLeft(CursorLeft), + CursorMoved(CursorMoved), + FileDragAndDrop(FileDragAndDrop), + Ime(Ime), + ReceivedCharacter(ReceivedCharacter), + RequestRedraw(RequestRedraw), + WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + WindowCloseRequested(WindowCloseRequested), + WindowCreated(WindowCreated), + WindowDestroyed(WindowDestroyed), + WindowFocused(WindowFocused), + WindowMoved(WindowMoved), + WindowOccluded(WindowOccluded), + WindowResized(WindowResized), + WindowScaleFactorChanged(WindowScaleFactorChanged), + WindowThemeChanged(WindowThemeChanged), + + MouseButtonInput(MouseButtonInput), + MouseMotion(MouseMotion), + MouseWheel(MouseWheel), + + PinchGesture(PinchGesture), + RotationGesture(RotationGesture), + DoubleTapGesture(DoubleTapGesture), + PanGesture(PanGesture), + + TouchInput(TouchInput), + + KeyboardInput(KeyboardInput), + KeyboardFocusLost(KeyboardFocusLost), +} + +impl From for WindowEvent { + fn from(e: AppLifecycle) -> Self { + 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: ReceivedCharacter) -> Self { + Self::ReceivedCharacter(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/lib.rs b/crates/bevy_window/src/lib.rs index f27af26699424..49d6e96f40812 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -30,7 +30,9 @@ pub use system::*; pub use system_cursor::*; pub use window::*; -#[allow(missing_docs)] +/// The windowing prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[allow(deprecated)] #[doc(hidden)] @@ -91,7 +93,8 @@ impl Plugin for WindowPlugin { fn build(&self, app: &mut App) { // User convenience events #[allow(deprecated)] - app.add_event::() + app.add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -143,7 +146,8 @@ impl Plugin for WindowPlugin { // Register event types #[allow(deprecated)] - app.register_type::() + app.register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index dfaa0ecdae36b..eb8ca610fc9af 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroU32; +use std::num::NonZero; use bevy_ecs::{ entity::{Entity, EntityMapper, MapEntities}, @@ -279,7 +279,7 @@ pub struct Window { /// /// [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`]: /// https://docs.rs/wgpu/latest/wgpu/type.SurfaceConfiguration.html#structfield.desired_maximum_frame_latency - pub desired_maximum_frame_latency: Option, + pub desired_maximum_frame_latency: Option>, /// Sets whether this window recognizes [`PinchGesture`](https://docs.rs/bevy/latest/bevy/input/gestures/struct.PinchGesture.html) /// /// ## Platform-specific diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 4901790ac7915..b70720deceb0c 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -13,7 +13,7 @@ //! See `winit_runner` for details. use bevy_derive::Deref; -use bevy_window::RawHandleWrapperHolder; +use bevy_window::{RawHandleWrapperHolder, WindowEvent}; use std::marker::PhantomData; use winit::event_loop::EventLoop; #[cfg(target_os = "android")] @@ -33,7 +33,6 @@ pub use winit::event_loop::EventLoopProxy; pub use winit::platform::web::CustomCursorExtWebSys; pub use winit::window::{CustomCursor as WinitCustomCursor, CustomCursorSource}; pub use winit_config::*; -pub use winit_event::*; pub use winit_windows::*; use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}; @@ -45,7 +44,6 @@ mod converters; mod state; mod system; mod winit_config; -pub mod winit_event; mod winit_monitors; mod winit_windows; @@ -122,7 +120,6 @@ impl Plugin for WinitPlugin { app.init_non_send_resource::() .init_resource::() .init_resource::() - .add_event::() .set_runner(winit_runner::) .add_systems( Last, @@ -162,12 +159,12 @@ pub struct WakeUp; pub struct EventLoopProxyWrapper(EventLoopProxy); trait AppSendEvent { - fn send(&mut self, event: impl Into); + fn send(&mut self, event: impl Into); } -impl AppSendEvent for Vec { - fn send(&mut self, event: impl Into) { - self.push(Into::::into(event)); +impl AppSendEvent for Vec { + fn send(&mut self, event: impl Into) { + self.push(Into::::into(event)); } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 871a559b060a8..f515513a325b6 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -28,8 +28,8 @@ use winit::window::WindowId; use bevy_window::{ AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed, - WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, - WindowThemeChanged, + WindowEvent as BevyWindowEvent, WindowFocused, WindowMoved, WindowOccluded, WindowResized, + WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] use bevy_window::{PrimaryWindow, RawHandleWrapper}; @@ -38,7 +38,7 @@ use crate::accessibility::AccessKitAdapters; use crate::system::{create_monitors, CachedWindow}; use crate::{ converters, create_windows, AppSendEvent, CreateMonitorParams, CreateWindowParams, - EventLoopProxyWrapper, UpdateMode, WinitEvent, WinitSettings, WinitWindows, + EventLoopProxyWrapper, UpdateMode, WinitSettings, WinitWindows, }; /// Persistent state that is used to run the [`App`] according to the current @@ -69,8 +69,8 @@ struct WinitAppRunnerState { lifecycle: AppLifecycle, /// The previous app lifecycle state. previous_lifecycle: AppLifecycle, - /// Winit events to send - winit_events: Vec, + /// Bevy window events to send + bevy_window_events: Vec, _marker: PhantomData, event_writer_system_state: SystemState<( @@ -110,7 +110,7 @@ impl WinitAppRunnerState { wait_elapsed: false, // 3 seems to be enough, 5 is a safe margin startup_forced_updates: 5, - winit_events: Vec::new(), + bevy_window_events: Vec::new(), _marker: PhantomData, event_writer_system_state, } @@ -258,7 +258,9 @@ impl ApplicationHandler for WinitAppRunnerState { &mut window_scale_factor_changed, ); } - WindowEvent::CloseRequested => self.winit_events.send(WindowCloseRequested { window }), + WindowEvent::CloseRequested => self + .bevy_window_events + .send(WindowCloseRequested { window }), WindowEvent::KeyboardInput { ref event, is_synthetic, @@ -274,10 +276,11 @@ impl ApplicationHandler for WinitAppRunnerState { if let Some(char) = &event.text { let char = char.clone(); #[allow(deprecated)] - self.winit_events.send(ReceivedCharacter { window, char }); + self.bevy_window_events + .send(ReceivedCharacter { window, char }); } } - self.winit_events + self.bevy_window_events .send(converters::convert_keyboard_input(event, window)); } } @@ -291,44 +294,44 @@ impl ApplicationHandler for WinitAppRunnerState { win.set_physical_cursor_position(Some(physical_position)); let position = (physical_position / win.resolution.scale_factor() as f64).as_vec2(); - self.winit_events.send(CursorMoved { + self.bevy_window_events.send(CursorMoved { window, position, delta, }); } WindowEvent::CursorEntered { .. } => { - self.winit_events.send(CursorEntered { window }); + self.bevy_window_events.send(CursorEntered { window }); } WindowEvent::CursorLeft { .. } => { win.set_physical_cursor_position(None); - self.winit_events.send(CursorLeft { window }); + self.bevy_window_events.send(CursorLeft { window }); } WindowEvent::MouseInput { state, button, .. } => { - self.winit_events.send(MouseButtonInput { + self.bevy_window_events.send(MouseButtonInput { button: converters::convert_mouse_button(button), state: converters::convert_element_state(state), window, }); } WindowEvent::PinchGesture { delta, .. } => { - self.winit_events.send(PinchGesture(delta as f32)); + self.bevy_window_events.send(PinchGesture(delta as f32)); } WindowEvent::RotationGesture { delta, .. } => { - self.winit_events.send(RotationGesture(delta)); + self.bevy_window_events.send(RotationGesture(delta)); } WindowEvent::DoubleTapGesture { .. } => { - self.winit_events.send(DoubleTapGesture); + self.bevy_window_events.send(DoubleTapGesture); } WindowEvent::PanGesture { delta, .. } => { - self.winit_events.send(PanGesture(Vec2 { + self.bevy_window_events.send(PanGesture(Vec2 { x: delta.x, y: delta.y, })); } WindowEvent::MouseWheel { delta, .. } => match delta { event::MouseScrollDelta::LineDelta(x, y) => { - self.winit_events.send(MouseWheel { + self.bevy_window_events.send(MouseWheel { unit: MouseScrollUnit::Line, x, y, @@ -336,7 +339,7 @@ impl ApplicationHandler for WinitAppRunnerState { }); } event::MouseScrollDelta::PixelDelta(p) => { - self.winit_events.send(MouseWheel { + self.bevy_window_events.send(MouseWheel { unit: MouseScrollUnit::Pixel, x: p.x as f32, y: p.y as f32, @@ -348,62 +351,65 @@ impl ApplicationHandler for WinitAppRunnerState { let location = touch .location .to_logical(win.resolution.scale_factor() as f64); - self.winit_events + self.bevy_window_events .send(converters::convert_touch_input(touch, location, window)); } WindowEvent::Focused(focused) => { win.focused = focused; - self.winit_events.send(WindowFocused { window, focused }); + self.bevy_window_events + .send(WindowFocused { window, focused }); if !focused { - self.winit_events.send(KeyboardFocusLost); + self.bevy_window_events.send(KeyboardFocusLost); } } WindowEvent::Occluded(occluded) => { - self.winit_events.send(WindowOccluded { window, occluded }); + self.bevy_window_events + .send(WindowOccluded { window, occluded }); } WindowEvent::DroppedFile(path_buf) => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::DroppedFile { window, path_buf }); } WindowEvent::HoveredFile(path_buf) => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::HoveredFile { window, path_buf }); } WindowEvent::HoveredFileCancelled => { - self.winit_events + self.bevy_window_events .send(FileDragAndDrop::HoveredFileCanceled { window }); } WindowEvent::Moved(position) => { let position = ivec2(position.x, position.y); win.position.set(position); - self.winit_events.send(WindowMoved { window, position }); + self.bevy_window_events + .send(WindowMoved { window, position }); } WindowEvent::Ime(event) => match event { event::Ime::Preedit(value, cursor) => { - self.winit_events.send(Ime::Preedit { + self.bevy_window_events.send(Ime::Preedit { window, value, cursor, }); } event::Ime::Commit(value) => { - self.winit_events.send(Ime::Commit { window, value }); + self.bevy_window_events.send(Ime::Commit { window, value }); } event::Ime::Enabled => { - self.winit_events.send(Ime::Enabled { window }); + self.bevy_window_events.send(Ime::Enabled { window }); } event::Ime::Disabled => { - self.winit_events.send(Ime::Disabled { window }); + self.bevy_window_events.send(Ime::Disabled { window }); } }, WindowEvent::ThemeChanged(theme) => { - self.winit_events.send(WindowThemeChanged { + self.bevy_window_events.send(WindowThemeChanged { window, theme: converters::convert_winit_theme(theme), }); } WindowEvent::Destroyed => { - self.winit_events.send(WindowDestroyed { window }); + self.bevy_window_events.send(WindowDestroyed { window }); } WindowEvent::RedrawRequested => { self.ran_update_since_last_redraw = false; @@ -429,7 +435,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let DeviceEvent::MouseMotion { delta: (x, y) } = event { let delta = Vec2::new(x as f32, y as f32); - self.winit_events.send(MouseMotion { delta }); + self.bevy_window_events.send(MouseMotion { delta }); } } @@ -534,7 +540,7 @@ impl ApplicationHandler for WinitAppRunnerState { // Notifies a lifecycle change if self.lifecycle != self.previous_lifecycle { self.previous_lifecycle = self.lifecycle; - self.winit_events.send(self.lifecycle); + self.bevy_window_events.send(self.lifecycle); } // This is recorded before running app.update(), to run the next cycle after a correct timeout. @@ -674,15 +680,15 @@ impl WinitAppRunnerState { fn run_app_update(&mut self) { self.reset_on_update(); - self.forward_winit_events(); + self.forward_bevy_events(); if self.app.plugins_state() == PluginsState::Cleaned { self.app.update(); } } - fn forward_winit_events(&mut self) { - let buffered_events = self.winit_events.drain(..).collect::>(); + fn forward_bevy_events(&mut self) { + let buffered_events = self.bevy_window_events.drain(..).collect::>(); if buffered_events.is_empty() { return; @@ -692,95 +698,95 @@ impl WinitAppRunnerState { for winit_event in buffered_events.iter() { match winit_event.clone() { - WinitEvent::AppLifecycle(e) => { + BevyWindowEvent::AppLifecycle(e) => { world.send_event(e); } - WinitEvent::CursorEntered(e) => { + BevyWindowEvent::CursorEntered(e) => { world.send_event(e); } - WinitEvent::CursorLeft(e) => { + BevyWindowEvent::CursorLeft(e) => { world.send_event(e); } - WinitEvent::CursorMoved(e) => { + BevyWindowEvent::CursorMoved(e) => { world.send_event(e); } - WinitEvent::FileDragAndDrop(e) => { + BevyWindowEvent::FileDragAndDrop(e) => { world.send_event(e); } - WinitEvent::Ime(e) => { + BevyWindowEvent::Ime(e) => { world.send_event(e); } - WinitEvent::ReceivedCharacter(e) => { + BevyWindowEvent::ReceivedCharacter(e) => { world.send_event(e); } - WinitEvent::RequestRedraw(e) => { + BevyWindowEvent::RequestRedraw(e) => { world.send_event(e); } - WinitEvent::WindowBackendScaleFactorChanged(e) => { + BevyWindowEvent::WindowBackendScaleFactorChanged(e) => { world.send_event(e); } - WinitEvent::WindowCloseRequested(e) => { + BevyWindowEvent::WindowCloseRequested(e) => { world.send_event(e); } - WinitEvent::WindowCreated(e) => { + BevyWindowEvent::WindowCreated(e) => { world.send_event(e); } - WinitEvent::WindowDestroyed(e) => { + BevyWindowEvent::WindowDestroyed(e) => { world.send_event(e); } - WinitEvent::WindowFocused(e) => { + BevyWindowEvent::WindowFocused(e) => { world.send_event(e); } - WinitEvent::WindowMoved(e) => { + BevyWindowEvent::WindowMoved(e) => { world.send_event(e); } - WinitEvent::WindowOccluded(e) => { + BevyWindowEvent::WindowOccluded(e) => { world.send_event(e); } - WinitEvent::WindowResized(e) => { + BevyWindowEvent::WindowResized(e) => { world.send_event(e); } - WinitEvent::WindowScaleFactorChanged(e) => { + BevyWindowEvent::WindowScaleFactorChanged(e) => { world.send_event(e); } - WinitEvent::WindowThemeChanged(e) => { + BevyWindowEvent::WindowThemeChanged(e) => { world.send_event(e); } - WinitEvent::MouseButtonInput(e) => { + BevyWindowEvent::MouseButtonInput(e) => { world.send_event(e); } - WinitEvent::MouseMotion(e) => { + BevyWindowEvent::MouseMotion(e) => { world.send_event(e); } - WinitEvent::MouseWheel(e) => { + BevyWindowEvent::MouseWheel(e) => { world.send_event(e); } - WinitEvent::PinchGesture(e) => { + BevyWindowEvent::PinchGesture(e) => { world.send_event(e); } - WinitEvent::RotationGesture(e) => { + BevyWindowEvent::RotationGesture(e) => { world.send_event(e); } - WinitEvent::DoubleTapGesture(e) => { + BevyWindowEvent::DoubleTapGesture(e) => { world.send_event(e); } - WinitEvent::PanGesture(e) => { + BevyWindowEvent::PanGesture(e) => { world.send_event(e); } - WinitEvent::TouchInput(e) => { + BevyWindowEvent::TouchInput(e) => { world.send_event(e); } - WinitEvent::KeyboardInput(e) => { + BevyWindowEvent::KeyboardInput(e) => { world.send_event(e); } - WinitEvent::KeyboardFocusLost(e) => { + BevyWindowEvent::KeyboardFocusLost(e) => { world.send_event(e); } } } world - .resource_mut::>() + .resource_mut::>() .send_batch(buffered_events); } diff --git a/crates/bevy_winit/src/winit_event.rs b/crates/bevy_winit/src/winit_event.rs deleted file mode 100644 index 9244b1265db73..0000000000000 --- a/crates/bevy_winit/src/winit_event.rs +++ /dev/null @@ -1,209 +0,0 @@ -#![allow(deprecated)] -#![allow(missing_docs)] - -use bevy_ecs::prelude::*; -use bevy_input::keyboard::KeyboardInput; -use bevy_input::touch::TouchInput; -use bevy_input::{ - gestures::*, - keyboard::KeyboardFocusLost, - mouse::{MouseButtonInput, MouseMotion, MouseWheel}, -}; -use bevy_reflect::Reflect; -#[cfg(feature = "serialize")] -use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_window::{ - AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, - RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, - WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized, - WindowScaleFactorChanged, WindowThemeChanged, -}; - -/// Wraps all `bevy_window` events in a common enum. -/// -/// Read these events with `EventReader` if you need to -/// access window events in the order they were received from `winit`. -/// Otherwise, the event types are individually readable with -/// `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum WinitEvent { - AppLifecycle(AppLifecycle), - CursorEntered(CursorEntered), - CursorLeft(CursorLeft), - CursorMoved(CursorMoved), - FileDragAndDrop(FileDragAndDrop), - Ime(Ime), - ReceivedCharacter(ReceivedCharacter), - RequestRedraw(RequestRedraw), - WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), - WindowCloseRequested(WindowCloseRequested), - WindowCreated(WindowCreated), - WindowDestroyed(WindowDestroyed), - WindowFocused(WindowFocused), - WindowMoved(WindowMoved), - WindowOccluded(WindowOccluded), - WindowResized(WindowResized), - WindowScaleFactorChanged(WindowScaleFactorChanged), - WindowThemeChanged(WindowThemeChanged), - - MouseButtonInput(MouseButtonInput), - MouseMotion(MouseMotion), - MouseWheel(MouseWheel), - - PinchGesture(PinchGesture), - RotationGesture(RotationGesture), - DoubleTapGesture(DoubleTapGesture), - PanGesture(PanGesture), - - TouchInput(TouchInput), - - KeyboardInput(KeyboardInput), - KeyboardFocusLost(KeyboardFocusLost), -} - -impl From for WinitEvent { - fn from(e: AppLifecycle) -> Self { - Self::AppLifecycle(e) - } -} -impl From for WinitEvent { - fn from(e: CursorEntered) -> Self { - Self::CursorEntered(e) - } -} -impl From for WinitEvent { - fn from(e: CursorLeft) -> Self { - Self::CursorLeft(e) - } -} -impl From for WinitEvent { - fn from(e: CursorMoved) -> Self { - Self::CursorMoved(e) - } -} -impl From for WinitEvent { - fn from(e: FileDragAndDrop) -> Self { - Self::FileDragAndDrop(e) - } -} -impl From for WinitEvent { - fn from(e: Ime) -> Self { - Self::Ime(e) - } -} -impl From for WinitEvent { - fn from(e: ReceivedCharacter) -> Self { - Self::ReceivedCharacter(e) - } -} -impl From for WinitEvent { - fn from(e: RequestRedraw) -> Self { - Self::RequestRedraw(e) - } -} -impl From for WinitEvent { - fn from(e: WindowBackendScaleFactorChanged) -> Self { - Self::WindowBackendScaleFactorChanged(e) - } -} -impl From for WinitEvent { - fn from(e: WindowCloseRequested) -> Self { - Self::WindowCloseRequested(e) - } -} -impl From for WinitEvent { - fn from(e: WindowCreated) -> Self { - Self::WindowCreated(e) - } -} -impl From for WinitEvent { - fn from(e: WindowDestroyed) -> Self { - Self::WindowDestroyed(e) - } -} -impl From for WinitEvent { - fn from(e: WindowFocused) -> Self { - Self::WindowFocused(e) - } -} -impl From for WinitEvent { - fn from(e: WindowMoved) -> Self { - Self::WindowMoved(e) - } -} -impl From for WinitEvent { - fn from(e: WindowOccluded) -> Self { - Self::WindowOccluded(e) - } -} -impl From for WinitEvent { - fn from(e: WindowResized) -> Self { - Self::WindowResized(e) - } -} -impl From for WinitEvent { - fn from(e: WindowScaleFactorChanged) -> Self { - Self::WindowScaleFactorChanged(e) - } -} -impl From for WinitEvent { - fn from(e: WindowThemeChanged) -> Self { - Self::WindowThemeChanged(e) - } -} -impl From for WinitEvent { - fn from(e: MouseButtonInput) -> Self { - Self::MouseButtonInput(e) - } -} -impl From for WinitEvent { - fn from(e: MouseMotion) -> Self { - Self::MouseMotion(e) - } -} -impl From for WinitEvent { - fn from(e: MouseWheel) -> Self { - Self::MouseWheel(e) - } -} -impl From for WinitEvent { - fn from(e: PinchGesture) -> Self { - Self::PinchGesture(e) - } -} -impl From for WinitEvent { - fn from(e: RotationGesture) -> Self { - Self::RotationGesture(e) - } -} -impl From for WinitEvent { - fn from(e: DoubleTapGesture) -> Self { - Self::DoubleTapGesture(e) - } -} -impl From for WinitEvent { - fn from(e: PanGesture) -> Self { - Self::PanGesture(e) - } -} -impl From for WinitEvent { - fn from(e: TouchInput) -> Self { - Self::TouchInput(e) - } -} -impl From for WinitEvent { - fn from(e: KeyboardInput) -> Self { - Self::KeyboardInput(e) - } -} -impl From for WinitEvent { - fn from(e: KeyboardFocusLost) -> Self { - Self::KeyboardFocusLost(e) - } -} diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 37e81e40c2de2..bd007d77df5db 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -33,7 +33,7 @@ fn setup( commands.spawn(Camera2dBundle::default()); let shapes = [ - Mesh2dHandle(meshes.add(Circle { radius: 50.0 })), + Mesh2dHandle(meshes.add(Circle::new(50.0))), Mesh2dHandle(meshes.add(CircularSector::new(50.0, 1.0))), Mesh2dHandle(meshes.add(CircularSegment::new(50.0, 1.25))), Mesh2dHandle(meshes.add(Ellipse::new(25.0, 50.0))), diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index c065d1c119b5f..3913a5c5e9367 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -26,7 +26,7 @@ fn draw_cursor( }; // Calculate a world position based on the cursor's position. - let Some(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { + let Ok(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { return; }; diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 98c143c5537aa..569a10a003e05 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -24,7 +24,7 @@ fn draw_cursor( }; // Calculate a ray pointing from the camera into the world based on the cursor's position. - let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { + let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { return; }; diff --git a/examples/3d/anisotropy.rs b/examples/3d/anisotropy.rs index 84583c19b1c37..728e792a04dab 100644 --- a/examples/3d/anisotropy.rs +++ b/examples/3d/anisotropy.rs @@ -90,8 +90,8 @@ fn spawn_text(commands: &mut Commands, app_status: &AppStatus) { } .with_style(Style { position_type: PositionType::Absolute, - bottom: Val::Px(10.0), - left: Val::Px(10.0), + bottom: Val::Px(12.0), + left: Val::Px(12.0), ..default() }), ); diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 81822c32c65fa..f2a58e32fa70f 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -351,7 +351,7 @@ fn setup( /// Writes a simple menu item that can be on or off. fn draw_selectable_menu_item(ui: &mut String, label: &str, shortcut: char, enabled: bool) { let star = if enabled { "*" } else { "" }; - let _ = writeln!(*ui, "({}) {}{}{}", shortcut, star, label, star); + let _ = writeln!(*ui, "({shortcut}) {star}{label}{star}"); } /// Creates a colorful test pattern diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 6984ea27ea63c..e1d04016c687d 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -150,8 +150,8 @@ fn setup( commands.spawn(( TextBundle::from_section("", text_style).with_style(Style { position_type: PositionType::Absolute, - top: Val::Px(10.0), - right: Val::Px(10.0), + top: Val::Px(12.0), + right: Val::Px(12.0), ..default() }), ExampleDisplay, diff --git a/examples/3d/color_grading.rs b/examples/3d/color_grading.rs index 6ed6d12d8a48e..f7548cb36553d 100644 --- a/examples/3d/color_grading.rs +++ b/examples/3d/color_grading.rs @@ -143,8 +143,8 @@ fn add_buttons(commands: &mut Commands, font: &Handle, color_grading: &Col flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, row_gap: Val::Px(6.0), - left: Val::Px(10.0), - bottom: Val::Px(10.0), + left: Val::Px(12.0), + bottom: Val::Px(12.0), ..default() }, ..default() @@ -318,8 +318,8 @@ fn add_help_text( .spawn(TextBundle { style: Style { position_type: PositionType::Absolute, - left: Val::Px(10.0), - top: Val::Px(10.0), + left: Val::Px(12.0), + top: Val::Px(12.0), ..default() }, ..TextBundle::from_section( @@ -459,9 +459,9 @@ impl Display for SelectedSectionColorGradingOption { impl Display for SelectedColorGradingOption { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - SelectedColorGradingOption::Global(option) => write!(f, "\"{}\"", option), + SelectedColorGradingOption::Global(option) => write!(f, "\"{option}\""), SelectedColorGradingOption::Section(section, option) => { - write!(f, "\"{}\" for \"{}\"", option, section) + write!(f, "\"{option}\" for \"{section}\"") } } } @@ -633,7 +633,7 @@ fn update_ui_state( /// Creates the help text at the top left of the window. fn create_help_text(currently_selected_option: &SelectedColorGradingOption) -> String { - format!("Press Left/Right to adjust {}", currently_selected_option) + format!("Press Left/Right to adjust {currently_selected_option}") } /// Processes keyboard input to change the value of the currently-selected color diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 0c4c44f00bf60..e213c1f3a28e6 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -351,12 +351,11 @@ impl AppStatus { Text::from_section( format!( - "{}\n{}\n{}\n{}\n{}", - CLICK_TO_MOVE_HELP_TEXT, - voxels_help_text, - irradiance_volume_help_text, - rotation_help_text, - switch_mesh_help_text + "{CLICK_TO_MOVE_HELP_TEXT} + {voxels_help_text} + {irradiance_volume_help_text} + {rotation_help_text} + {switch_mesh_help_text}" ), TextStyle::default(), ) @@ -483,7 +482,7 @@ fn handle_mouse_clicks( }; // Figure out where the user clicked on the plane. - let Some(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { + let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else { diff --git a/examples/README.md b/examples/README.md index e592f427dcdc5..e2e3bbf426d40 100644 --- a/examples/README.md +++ b/examples/README.md @@ -187,6 +187,7 @@ Example | Description [Animated Fox](../examples/animation/animated_fox.rs) | Plays an animation from a skinned glTF [Animated Transform](../examples/animation/animated_transform.rs) | Create and play an animation defined by code that operates on the `Transform` component [Animation Graph](../examples/animation/animation_graph.rs) | Blends multiple animations together with a graph +[Animation Masks](../examples/animation/animation_masks.rs) | Demonstrates animation masks [Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces [Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve [Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code @@ -404,6 +405,7 @@ Example | Description [Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Specialized Mesh Pipeline](../examples/shader/specialized_mesh_pipeline.rs) | Demonstrates how to write a specialized mesh pipeline +[Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material. [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). ## State @@ -489,6 +491,7 @@ Example | Description [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI +[UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality. diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs new file mode 100644 index 0000000000000..b5ca888edd30c --- /dev/null +++ b/examples/animation/animation_masks.rs @@ -0,0 +1,365 @@ +//! Demonstrates how to use masks to limit the scope of animations. + +use bevy::{animation::AnimationTargetId, color::palettes::css::WHITE, prelude::*}; + +// IDs of the mask groups we define for the running fox model. +// +// Each mask group defines a set of bones for which animations can be toggled on +// and off. +const MASK_GROUP_LEFT_FRONT_LEG: u32 = 0; +const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 1; +const MASK_GROUP_LEFT_HIND_LEG: u32 = 2; +const MASK_GROUP_RIGHT_HIND_LEG: u32 = 3; +const MASK_GROUP_TAIL: u32 = 4; + +// The width in pixels of the small buttons that allow the user to toggle a mask +// group on or off. +const MASK_GROUP_SMALL_BUTTON_WIDTH: f32 = 150.0; + +// The ID of the animation in the glTF file that we're going to play. +const FOX_RUN_ANIMATION: usize = 2; + +// The names of the bones that each mask group consists of. Each mask group is +// defined as a (prefix, suffix) tuple. The mask group consists of a single +// bone chain rooted at the prefix. For example, if the chain's prefix is +// "A/B/C" and the suffix is "D/E", then the bones that will be included in the +// mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E". +// +// The fact that our mask groups are single chains of bones isn't anything +// specific to Bevy; it just so happens to be the case for the model we're +// using. A mask group can consist of any set of animation targets, regardless +// of whether they form a single chain. +const MASK_GROUP_PATHS: [(&str, &str); 5] = [ + // Left front leg + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09", + "b_LeftForeArm_010/b_LeftHand_011", + ), + // Right front leg + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_RightUpperArm_06", + "b_RightForeArm_07/b_RightHand_08", + ), + // Left hind leg + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_LeftLeg01_015", + "b_LeftLeg02_016/b_LeftFoot01_017/b_LeftFoot02_018", + ), + // Right hind leg + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_RightLeg01_019", + "b_RightLeg02_020/b_RightFoot01_021/b_RightFoot02_022", + ), + // Tail + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012", + "b_Tail02_013/b_Tail03_014", + ), +]; + +// A component that identifies a clickable button that allows the user to toggle +// a mask group on or off. +#[derive(Component)] +struct MaskGroupControl { + // The ID of the mask group that this button controls. + group_id: u32, + + // Whether animations are playing for this mask group. + // + // Note that this is the opposite of the `mask` field in `AnimationGraph`: + // i.e. it's true if the group is *not* presently masked, and false if the + // group *is* masked. + enabled: bool, +} + +// The application entry point. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Animation Masks Example".into(), + ..default() + }), + ..default() + })) + .add_systems(Startup, (setup_scene, setup_ui)) + .add_systems(Update, setup_animation_graph_once_loaded) + .add_systems(Update, handle_button_toggles) + .insert_resource(AmbientLight { + color: WHITE.into(), + brightness: 100.0, + }) + .run(); +} + +// Spawns the 3D objects in the scene, and loads the fox animation from the glTF +// file. +fn setup_scene( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Spawn the camera. + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-15.0, 10.0, 20.0) + .looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + ..default() + }); + + // Spawn the light. + commands.spawn(PointLightBundle { + point_light: PointLight { + intensity: 10_000_000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(-4.0, 8.0, 13.0), + ..default() + }); + + // Spawn the fox. + commands.spawn(SceneBundle { + scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), + transform: Transform::from_scale(Vec3::splat(0.07)), + ..default() + }); + + // Spawn the ground. + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(7.0)), + material: materials.add(Color::srgb(0.3, 0.5, 0.3)), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); +} + +// Creates the UI. +fn setup_ui(mut commands: Commands) { + // Add help text. + commands.spawn( + TextBundle::from_section( + "Click on a button to toggle animations for its associated bones", + TextStyle::default(), + ) + .with_style(Style { + position_type: PositionType::Absolute, + left: Val::Px(12.0), + top: Val::Px(12.0), + ..default() + }), + ); + + // Add the buttons that allow the user to toggle mask groups on and off. + commands + .spawn(NodeBundle { + style: Style { + flex_direction: FlexDirection::Column, + position_type: PositionType::Absolute, + row_gap: Val::Px(6.0), + left: Val::Px(12.0), + bottom: Val::Px(12.0), + ..default() + }, + ..default() + }) + .with_children(|parent| { + let row_style = Style { + flex_direction: FlexDirection::Row, + column_gap: Val::Px(6.0), + ..default() + }; + + parent + .spawn(NodeBundle { + style: row_style.clone(), + ..default() + }) + .with_children(|parent| { + add_mask_group_control( + parent, + "Left Front Leg", + Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + MASK_GROUP_LEFT_FRONT_LEG, + ); + add_mask_group_control( + parent, + "Right Front Leg", + Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + MASK_GROUP_RIGHT_FRONT_LEG, + ); + }); + + parent + .spawn(NodeBundle { + style: row_style, + ..default() + }) + .with_children(|parent| { + add_mask_group_control( + parent, + "Left Hind Leg", + Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + MASK_GROUP_LEFT_HIND_LEG, + ); + add_mask_group_control( + parent, + "Right Hind Leg", + Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + MASK_GROUP_RIGHT_HIND_LEG, + ); + }); + + add_mask_group_control(parent, "Tail", Val::Auto, MASK_GROUP_TAIL); + }); +} + +// Adds a button that allows the user to toggle a mask group on and off. +// +// The button will automatically become a child of the parent that owns the +// given `ChildBuilder`. +fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, mask_group_id: u32) { + parent + .spawn(ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + width, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::all(Val::Px(6.0)), + margin: UiRect::ZERO, + ..default() + }, + border_color: BorderColor(Color::WHITE), + border_radius: BorderRadius::all(Val::Px(3.0)), + background_color: Color::WHITE.into(), + ..default() + }) + .insert(MaskGroupControl { + group_id: mask_group_id, + enabled: true, + }) + .with_child(TextBundle::from_section( + label, + TextStyle { + font_size: 14.0, + color: Color::BLACK, + ..default() + }, + )); +} + +// Builds up the animation graph, including the mask groups, and adds it to the +// entity with the `AnimationPlayer` that the glTF loader created. +fn setup_animation_graph_once_loaded( + mut commands: Commands, + asset_server: Res, + mut animation_graphs: ResMut>, + mut players: Query<(Entity, &mut AnimationPlayer), Added>, +) { + for (entity, mut player) in &mut players { + // Load the animation clip from the glTF file. + let (mut animation_graph, node_index) = AnimationGraph::from_clip(asset_server.load( + GltfAssetLabel::Animation(FOX_RUN_ANIMATION).from_asset("models/animated/Fox.glb"), + )); + + // Create each mask group. + for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in + MASK_GROUP_PATHS.iter().enumerate() + { + // Split up the prefix and suffix, and convert them into `Name`s. + let prefix: Vec<_> = mask_group_prefix.split('/').map(Name::new).collect(); + let suffix: Vec<_> = mask_group_suffix.split('/').map(Name::new).collect(); + + // Add each bone in the chain to the appropriate mask group. + for chain_length in 0..=suffix.len() { + let animation_target_id = AnimationTargetId::from_names( + prefix.iter().chain(suffix[0..chain_length].iter()), + ); + animation_graph + .add_target_to_mask_group(animation_target_id, mask_group_index as u32); + } + } + + // We're doing constructing the animation graph. Add it as an asset. + let animation_graph = animation_graphs.add(animation_graph); + commands.entity(entity).insert(animation_graph); + + // Finally, play the animation. + player.play(node_index).repeat(); + } +} + +// A system that handles requests from the user to toggle mask groups on and +// off. +fn handle_button_toggles( + mut interactions: Query< + ( + &Interaction, + &mut MaskGroupControl, + &mut BackgroundColor, + &Children, + ), + Changed, + >, + mut texts: Query<&mut Text>, + mut animation_players: Query<(&Handle, &AnimationPlayer)>, + mut animation_graphs: ResMut>, +) { + for (interaction, mut mask_group_control, mut button_background_color, children) in + interactions.iter_mut() + { + // We only care about press events. + if *interaction != Interaction::Pressed { + continue; + } + + // Toggle the state of the mask. + mask_group_control.enabled = !mask_group_control.enabled; + + // Update the background color of the button. + button_background_color.0 = if mask_group_control.enabled { + Color::WHITE + } else { + Color::BLACK + }; + + // Update the text color of the button. + for &kid in children.iter() { + if let Ok(mut text) = texts.get_mut(kid) { + for section in &mut text.sections { + section.style.color = if mask_group_control.enabled { + Color::BLACK + } else { + Color::WHITE + }; + } + } + } + + // Now grab the animation player. (There's only one in our case, but we + // iterate just for clarity's sake.) + for (animation_graph_handle, animation_player) in animation_players.iter_mut() { + // The animation graph needs to have loaded. + let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else { + continue; + }; + + // Grab the animation graph node that's currently playing. + let Some((&animation_node_index, _)) = animation_player.playing_animations().next() + else { + continue; + }; + let Some(animation_node) = animation_graph.get_mut(animation_node_index) else { + continue; + }; + + // Enable or disable the mask group as appropriate. + if mask_group_control.enabled { + animation_node.mask &= !(1 << mask_group_control.group_id); + } else { + animation_node.mask |= 1 << mask_group_control.group_id; + } + } + } +} diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 2a882063d9abf..4e7c7a7f3f1dc 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -381,7 +381,7 @@ impl render_graph::Node for ImageCopyDriver { layout: ImageDataLayout { offset: 0, bytes_per_row: Some( - std::num::NonZeroU32::new(padded_bytes_per_row as u32) + std::num::NonZero::::new(padded_bytes_per_row as u32) .unwrap() .into(), ), @@ -533,7 +533,7 @@ fn update( // Finally saving image to file, this heavy blocking operation is kept here // for example simplicity, but in real app you should move it to a separate task if let Err(e) = img.save(image_path) { - panic!("Failed to save image: {}", e); + panic!("Failed to save image: {e}"); }; } if scene_controller.single_image { diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index 34244e7669228..1828807da1d86 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -69,10 +69,7 @@ fn setup(world: &mut World) { .on_add(|mut world, entity, component_id| { // You can access component data from within the hook let value = world.get::(entity).unwrap().0; - println!( - "Component: {:?} added to: {:?} with value {:?}", - component_id, entity, value - ); + println!("Component: {component_id:?} added to: {entity:?} with value {value:?}"); // Or access resources world .resource_mut::() @@ -96,10 +93,7 @@ fn setup(world: &mut World) { // since it runs before the component is removed you can still access the component data .on_remove(|mut world, entity, component_id| { let value = world.get::(entity).unwrap().0; - println!( - "Component: {:?} removed from: {:?} with value {:?}", - component_id, entity, value - ); + println!("Component: {component_id:?} removed from: {entity:?} with value {value:?}"); // You can also issue commands through `.commands()` world.commands().entity(entity).despawn(); }); diff --git a/examples/ecs/dynamic.rs b/examples/ecs/dynamic.rs index 9aac6fffb22c6..79ec202ae4452 100644 --- a/examples/ecs/dynamic.rs +++ b/examples/ecs/dynamic.rs @@ -50,7 +50,7 @@ fn main() { let mut component_names = HashMap::::new(); let mut component_info = HashMap::::new(); - println!("{}", PROMPT); + println!("{PROMPT}"); loop { print!("\n> "); let _ = std::io::stdout().flush(); @@ -64,10 +64,10 @@ fn main() { let Some((first, rest)) = line.trim().split_once(|c: char| c.is_whitespace()) else { match &line.chars().next() { - Some('c') => println!("{}", COMPONENT_PROMPT), - Some('s') => println!("{}", ENTITY_PROMPT), - Some('q') => println!("{}", QUERY_PROMPT), - _ => println!("{}", PROMPT), + Some('c') => println!("{COMPONENT_PROMPT}"), + Some('s') => println!("{ENTITY_PROMPT}"), + Some('q') => println!("{QUERY_PROMPT}"), + _ => println!("{PROMPT}"), } continue; }; @@ -112,7 +112,7 @@ fn main() { // Get the id for the component with the given name let Some(&id) = component_names.get(name) else { - println!("Component {} does not exist", name); + println!("Component {name} does not exist"); return; }; @@ -245,7 +245,7 @@ fn parse_term( }; if !matched { - println!("Unable to find component: {}", str); + println!("Unable to find component: {str}"); } } diff --git a/examples/ecs/hierarchy.rs b/examples/ecs/hierarchy.rs index b61f21f24ae94..2729b63b3eb88 100644 --- a/examples/ecs/hierarchy.rs +++ b/examples/ecs/hierarchy.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // Store parent entity for next sections .id(); - // Another way is to use the push_children function to add children after the parent + // Another way is to use the add_child function to add children after the parent // entity has already been spawned. let child = commands .spawn(SpriteBundle { diff --git a/examples/ecs/observers.rs b/examples/ecs/observers.rs index 8820ce4919a9a..82100f6b7ee1b 100644 --- a/examples/ecs/observers.rs +++ b/examples/ecs/observers.rs @@ -184,7 +184,7 @@ fn handle_click( if let Some(pos) = windows .single() .cursor_position() - .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor)) + .and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok()) .map(|ray| ray.origin.truncate()) { if mouse_button_input.just_pressed(MouseButton::Left) { diff --git a/examples/ecs/send_and_receive_events.rs b/examples/ecs/send_and_receive_events.rs index ce65c9c1e6eea..cabcc26d36886 100644 --- a/examples/ecs/send_and_receive_events.rs +++ b/examples/ecs/send_and_receive_events.rs @@ -102,7 +102,7 @@ fn send_events(mut events: EventWriter, frame_count: Res /// Note that some events will be printed twice, because they were sent twice. fn debug_events(mut events: EventReader) { for event in events.read() { - println!("{:?}", event); + println!("{event:?}"); } } diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index 7da985991959c..3147694176d38 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -222,9 +222,11 @@ fn get_cursor_world_pos( let primary_window = q_primary_window.single(); let (main_camera, main_camera_transform) = q_camera.single(); // Get the cursor position in the world - cursor_world_pos.0 = primary_window - .cursor_position() - .and_then(|cursor_pos| main_camera.viewport_to_world_2d(main_camera_transform, cursor_pos)); + cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| { + main_camera + .viewport_to_world_2d(main_camera_transform, cursor_pos) + .ok() + }); } /// Update whether the window is clickable or not diff --git a/examples/games/stepping.rs b/examples/games/stepping.rs index f0d5593c03998..be36551d02e92 100644 --- a/examples/games/stepping.rs +++ b/examples/games/stepping.rs @@ -115,7 +115,7 @@ fn build_ui( for label in schedule_order { let schedule = schedules.get(*label).unwrap(); text_sections.push(TextSection::new( - format!("{:?}\n", label), + format!("{label:?}\n"), TextStyle { font: asset_server.load(FONT_BOLD), color: FONT_COLOR, diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index 23d07a8b1a06f..0fa75863d50f8 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -73,6 +73,15 @@ fn draw_example_collection( FUCHSIA, ); + let domain = Interval::EVERYWHERE; + let curve = function_curve(domain, |t| Vec2::new(t, (t / 25.0).sin() * 100.0)); + let resolution = ((time.elapsed_seconds().sin() + 1.0) * 50.0) as usize; + let times_and_colors = (0..=resolution) + .map(|n| n as f32 / resolution as f32) + .map(|t| (t - 0.5) * 600.0) + .map(|t| (t, TEAL.mix(&HOT_PINK, (t + 300.0) / 600.0))); + gizmos.curve_gradient_2d(curve, times_and_colors); + my_gizmos .rounded_rect_2d(Isometry2d::IDENTITY, Vec2::splat(630.), BLACK) .corner_radius((time.elapsed_seconds() / 3.).cos() * 100.); diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index e66682180c3fd..1e4238a58a93c 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -97,6 +97,21 @@ fn draw_example_collection( ); gizmos.sphere(Isometry3d::from_translation(Vec3::ONE * 10.0), 1.0, PURPLE); + gizmos + .primitive_3d( + &Plane3d { + normal: Dir3::Y, + half_size: Vec2::splat(1.0), + }, + Isometry3d::new( + Vec3::ONE * 4.0 + Vec2::from(time.elapsed_seconds().sin_cos()).extend(0.0), + Quat::from_rotation_x(PI / 2. + time.elapsed_seconds()), + ), + GREEN, + ) + .cell_count(UVec2::new(5, 10)) + .spacing(Vec2::new(0.2, 0.1)); + gizmos.cuboid( Transform::from_translation(Vec3::Y * 0.5).with_scale(Vec3::splat(1.25)), BLACK, @@ -116,6 +131,17 @@ fn draw_example_collection( FUCHSIA, ); + let domain = Interval::EVERYWHERE; + let curve = function_curve(domain, |t| { + (Vec2::from((t * 10.0).sin_cos())).extend(t - 6.0) + }); + let resolution = ((time.elapsed_seconds().sin() + 1.0) * 100.0) as usize; + let times_and_colors = (0..=resolution) + .map(|n| n as f32 / resolution as f32) + .map(|t| t * 5.0) + .map(|t| (t, TEAL.mix(&HOT_PINK, t / 5.0))); + gizmos.curve_gradient_3d(curve, times_and_colors); + my_gizmos.sphere( Isometry3d::from_translation(Vec3::new(1., 0.5, 0.)), 0.5, diff --git a/examples/input/text_input.rs b/examples/input/text_input.rs index 2da56568b79f8..7efe43a2eae1f 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -30,61 +30,30 @@ fn main() { fn setup_scene(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); + // The default font has a limited number of glyphs, so use the full version for + // sections that will hold text input. let font = asset_server.load("fonts/FiraMono-Medium.ttf"); commands.spawn( TextBundle::from_sections([ + TextSection::from("Click to toggle IME. Press return to start a new line.\n\n"), + TextSection::from("IME Enabled: "), + TextSection::from("false\n"), + TextSection::from("IME Active: "), + TextSection::from("false\n"), + TextSection::from("IME Buffer: "), TextSection { - value: "IME Enabled: ".to_string(), + value: "\n".to_string(), style: TextStyle { - font: font.clone_weak(), - ..default() - }, - }, - TextSection { - value: "false\n".to_string(), - style: TextStyle { - font: font.clone_weak(), - font_size: 30.0, - ..default() - }, - }, - TextSection { - value: "IME Active: ".to_string(), - style: TextStyle { - font: font.clone_weak(), - ..default() - }, - }, - TextSection { - value: "false\n".to_string(), - style: TextStyle { - font: font.clone_weak(), - font_size: 30.0, - ..default() - }, - }, - TextSection { - value: "click to toggle IME, press return to start a new line\n\n".to_string(), - style: TextStyle { - font: font.clone_weak(), - font_size: 18.0, - ..default() - }, - }, - TextSection { - value: "".to_string(), - style: TextStyle { - font, - font_size: 25.0, + font: font.clone(), ..default() }, }, ]) .with_style(Style { position_type: PositionType::Absolute, - top: Val::Px(10.0), - left: Val::Px(10.0), + top: Val::Px(12.0), + left: Val::Px(12.0), ..default() }), ); @@ -93,7 +62,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { text: Text::from_section( "".to_string(), TextStyle { - font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font, font_size: 100.0, ..default() }, @@ -114,7 +83,7 @@ fn toggle_ime( window.ime_enabled = !window.ime_enabled; let mut text = text.single_mut(); - text.sections[1].value = format!("{}\n", window.ime_enabled); + text.sections[2].value = format!("{}\n", window.ime_enabled); } } @@ -144,19 +113,19 @@ fn listen_ime_events( for event in events.read() { match event { Ime::Preedit { value, cursor, .. } if !cursor.is_none() => { - status_text.single_mut().sections[5].value = format!("IME buffer: {value}"); + status_text.single_mut().sections[6].value = format!("{value}\n"); } Ime::Preedit { cursor, .. } if cursor.is_none() => { - status_text.single_mut().sections[5].value = "".to_string(); + status_text.single_mut().sections[6].value = "\n".to_string(); } Ime::Commit { value, .. } => { edit_text.single_mut().sections[0].value.push_str(value); } Ime::Enabled { .. } => { - status_text.single_mut().sections[3].value = "true\n".to_string(); + status_text.single_mut().sections[4].value = "true\n".to_string(); } Ime::Disabled { .. } => { - status_text.single_mut().sections[3].value = "false\n".to_string(); + status_text.single_mut().sections[4].value = "false\n".to_string(); } _ => (), } diff --git a/examples/math/cubic_splines.rs b/examples/math/cubic_splines.rs index 37fab11ef687e..dbf43cef65808 100644 --- a/examples/math/cubic_splines.rs +++ b/examples/math/cubic_splines.rs @@ -73,8 +73,8 @@ fn setup(mut commands: Commands) { R: Remove the last control point\n\ S: Cycle the spline construction being used\n\ C: Toggle cyclic curve construction"; - let spline_mode_text = format!("Spline: {}", spline_mode); - let cycling_mode_text = format!("{}", cycling_mode); + let spline_mode_text = format!("Spline: {spline_mode}"); + let cycling_mode_text = format!("{cycling_mode}"); let style = TextStyle::default(); commands @@ -357,11 +357,10 @@ fn handle_mouse_press( }; // Convert the starting point and end point (current mouse pos) into world coords: - let Some(point) = camera.viewport_to_world_2d(camera_transform, start) else { + let Ok(point) = camera.viewport_to_world_2d(camera_transform, start) else { continue; }; - let Some(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos) - else { + let Ok(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else { continue; }; let tangent = end_point - point; @@ -396,10 +395,10 @@ fn draw_edit_move( // Resources store data in viewport coordinates, so we need to convert to world coordinates // to display them: - let Some(start) = camera.viewport_to_world_2d(camera_transform, start) else { + let Ok(start) = camera.viewport_to_world_2d(camera_transform, start) else { return; }; - let Some(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else { + let Ok(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else { return; }; diff --git a/examples/picking/simple_picking.rs b/examples/picking/simple_picking.rs index d417e42558ccc..50276f0902e13 100644 --- a/examples/picking/simple_picking.rs +++ b/examples/picking/simple_picking.rs @@ -23,8 +23,8 @@ fn setup( text: Text::from_section("Click Me to get a box", TextStyle::default()), style: Style { position_type: PositionType::Absolute, - top: Val::Percent(10.0), - left: Val::Percent(10.0), + top: Val::Percent(12.0), + left: Val::Percent(12.0), ..default() }, ..Default::default() diff --git a/examples/shader/storage_buffer.rs b/examples/shader/storage_buffer.rs new file mode 100644 index 0000000000000..7661a103a7533 --- /dev/null +++ b/examples/shader/storage_buffer.rs @@ -0,0 +1,113 @@ +//! This example demonstrates how to use a storage buffer with `AsBindGroup` in a custom material. +use bevy::{ + prelude::*, + reflect::TypePath, + render::render_resource::{AsBindGroup, ShaderRef}, +}; +use bevy_render::render_asset::RenderAssetUsages; +use bevy_render::storage::ShaderStorageBuffer; + +const SHADER_ASSET_PATH: &str = "shaders/storage_buffer.wgsl"; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, MaterialPlugin::::default())) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut buffers: ResMut>, + mut materials: ResMut>, +) { + // Example data for the storage buffer + let color_data: Vec<[f32; 4]> = vec![ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + ]; + + let colors = buffers.add(ShaderStorageBuffer::new( + bytemuck::cast_slice(color_data.as_slice()), + RenderAssetUsages::default(), + )); + + // Create the custom material with the storage buffer + let custom_material = CustomMaterial { colors }; + + let material_handle = materials.add(custom_material); + commands.insert_resource(CustomMaterialHandle(material_handle.clone())); + + // Spawn cubes with the custom material + for i in -6..=6 { + for j in -3..=3 { + commands.spawn(MaterialMeshBundle { + mesh: meshes.add(Cuboid::from_size(Vec3::splat(0.3))), + material: material_handle.clone(), + transform: Transform::from_xyz(i as f32, j as f32, 0.0), + ..default() + }); + } + } + + // Camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} + +// Update the material color by time +fn update( + time: Res