Merge branch 'main' into resize-window-crash

This commit is contained in:
Benjamin Brienen 2024-11-06 21:20:08 +01:00 committed by GitHub
commit 5b426c3080
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 983 additions and 886 deletions

View file

@ -243,7 +243,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check for typos
uses: crate-ci/typos@v1.26.8
uses: crate-ci/typos@v1.27.0
- name: Typos info
if: failure()
run: |

View file

@ -1216,7 +1216,7 @@ setup = [
"curl",
"-o",
"assets/models/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/8483db58832542383820c3f44e4730e566910be7/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/defbd9b32072624d40d57de7d345c66a9edf5d0b/bunny.meshlet_mesh",
],
]

View file

@ -15,7 +15,7 @@ bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
accesskit = "0.16"
accesskit = "0.17"
[lints]
workspace = true

View file

@ -13,7 +13,7 @@ use alloc::sync::Arc;
use core::sync::atomic::{AtomicBool, Ordering};
pub use accesskit;
use accesskit::NodeBuilder;
use accesskit::Node;
use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
@ -84,10 +84,10 @@ impl ManageAccessibilityUpdates {
/// If the entity doesn't have a parent, or if the immediate parent doesn't have
/// an `AccessibilityNode`, its node will be an immediate child of the primary window.
#[derive(Component, Clone, Deref, DerefMut)]
pub struct AccessibilityNode(pub NodeBuilder);
pub struct AccessibilityNode(pub Node);
impl From<NodeBuilder> for AccessibilityNode {
fn from(node: NodeBuilder) -> Self {
impl From<Node> for AccessibilityNode {
fn from(node: Node) -> Self {
Self(node)
}
}

View file

@ -164,6 +164,12 @@ impl AssetWriter for FileAssetWriter {
Ok(())
}
async fn create_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
let full_path = self.root_path.join(path);
async_fs::create_dir_all(full_path).await?;
Ok(())
}
async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
let full_path = self.root_path.join(path);
async_fs::remove_dir_all(full_path).await?;

View file

@ -205,6 +205,12 @@ impl AssetWriter for FileAssetWriter {
Ok(())
}
async fn create_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
let full_path = self.root_path.join(path);
std::fs::create_dir_all(full_path)?;
Ok(())
}
async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> {
let full_path = self.root_path.join(path);
std::fs::remove_dir_all(full_path)?;

View file

@ -384,6 +384,12 @@ pub trait AssetWriter: Send + Sync + 'static {
old_path: &'a Path,
new_path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
/// Creates a directory at the given path, including all parent directories if they do not
/// already exist.
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
/// Removes the directory at the given path, including all assets _and_ directories in that directory.
fn remove_directory<'a>(
&'a self,
@ -460,6 +466,12 @@ pub trait ErasedAssetWriter: Send + Sync + 'static {
old_path: &'a Path,
new_path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
/// Creates a directory at the given path, including all parent directories if they do not
/// already exist.
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
/// Removes the directory at the given path, including all assets _and_ directories in that directory.
fn remove_directory<'a>(
&'a self,
@ -523,6 +535,12 @@ impl<T: AssetWriter> ErasedAssetWriter for T {
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::rename_meta(self, old_path, new_path))
}
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::create_directory(self, path))
}
fn remove_directory<'a>(
&'a self,
path: &'a Path,

View file

@ -23,7 +23,7 @@ derive_more = { version = "1", default-features = false, features = [
"from",
"display",
] }
wgpu-types = { version = "22", default-features = false, optional = true }
wgpu-types = { version = "23", default-features = false, optional = true }
encase = { version = "0.10", default-features = false }
[features]

View file

@ -20,7 +20,6 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
# other
serde = { version = "1.0", optional = true }
uuid = "1.0"
[features]
default = ["bevy_reflect"]

View file

@ -39,7 +39,7 @@ use upsampling_pipeline::{
const BLOOM_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(929599476923908);
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Float;
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
pub struct BloomPlugin;

View file

@ -46,28 +46,32 @@ fn prepare_view_upscaling_pipelines(
let mut output_textures = HashSet::new();
for (entity, view_target, camera) in view_targets.iter() {
let out_texture_id = view_target.out_texture().id();
let blend_state = if let Some(ExtractedCamera {
output_mode: CameraOutputMode::Write { blend_state, .. },
..
}) = camera
{
match *blend_state {
None => {
// If we've already seen this output for a camera and it doesn't have a output blend
// mode configured, default to alpha blend so that we don't accidentally overwrite
// the output texture
if output_textures.contains(&out_texture_id) {
Some(BlendState::ALPHA_BLENDING)
} else {
None
let blend_state = if let Some(extracted_camera) = camera {
match extracted_camera.output_mode {
CameraOutputMode::Skip => None,
CameraOutputMode::Write { blend_state, .. } => {
let already_seen = output_textures.contains(&out_texture_id);
output_textures.insert(out_texture_id);
match blend_state {
None => {
// If we've already seen this output for a camera and it doesn't have a output blend
// mode configured, default to alpha blend so that we don't accidentally overwrite
// the output texture
if already_seen {
Some(BlendState::ALPHA_BLENDING)
} else {
None
}
}
_ => blend_state,
}
}
_ => *blend_state,
}
} else {
output_textures.insert(out_texture_id);
None
};
output_textures.insert(out_texture_id);
let key = BlitPipelineKey {
texture_format: view_target.out_texture_format(),

View file

@ -487,7 +487,7 @@ impl ComponentHooks {
/// Will panic if the component already has an `on_add` hook
pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_add(hook)
.expect("Component id: {:?}, already has an on_add hook")
.expect("Component already has an on_add hook")
}
/// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
@ -505,7 +505,7 @@ impl ComponentHooks {
/// Will panic if the component already has an `on_insert` hook
pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_insert(hook)
.expect("Component id: {:?}, already has an on_insert hook")
.expect("Component already has an on_insert hook")
}
/// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
@ -527,7 +527,7 @@ impl ComponentHooks {
/// Will panic if the component already has an `on_replace` hook
pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_replace(hook)
.expect("Component id: {:?}, already has an on_replace hook")
.expect("Component already has an on_replace hook")
}
/// Register a [`ComponentHook`] that will be run when this component is removed from an entity.
@ -538,7 +538,7 @@ impl ComponentHooks {
/// Will panic if the component already has an `on_remove` hook
pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self {
self.try_on_remove(hook)
.expect("Component id: {:?}, already has an on_remove hook")
.expect("Component already has an on_remove hook")
}
/// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity.

View file

@ -650,6 +650,11 @@ impl ScheduleGraph {
.and_then(|system| system.inner.as_deref())
}
/// Returns `true` if the given system set is part of the graph. Otherwise, returns `false`.
pub fn contains_set(&self, set: impl SystemSet) -> bool {
self.system_set_ids.contains_key(&set.intern())
}
/// Returns the system at the given [`NodeId`].
///
/// Panics if it doesn't exist.

View file

@ -1651,6 +1651,8 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>>
/// Use [`Option<Single<D, F>>`] instead if zero or one matching entities can exist.
///
/// See [`Query`] for more details.
///
/// [System parameter]: crate::system::SystemParam
pub struct Single<'w, D: QueryData, F: QueryFilter = ()> {
pub(crate) item: D::Item<'w>,
pub(crate) _filter: PhantomData<F>,
@ -1687,6 +1689,8 @@ impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> {
/// which must individually check each query result for a match.
///
/// See [`Query`] for more details.
///
/// [System parameter]: crate::system::SystemParam
pub struct Populated<'w, 's, D: QueryData, F: QueryFilter = ()>(pub(crate) Query<'w, 's, D, F>);
impl<'w, 's, D: QueryData, F: QueryFilter> Deref for Populated<'w, 's, D, F> {

View file

@ -4036,7 +4036,6 @@ mod tests {
}
#[test]
#[ignore] // This should pass, but it currently fails due to limitations in our access model.
fn ref_compatible_with_resource_mut() {
fn borrow_system(_: Query<EntityRef>, _: ResMut<R>) {}
@ -4067,7 +4066,6 @@ mod tests {
}
#[test]
#[ignore] // This should pass, but it currently fails due to limitations in our access model.
fn mut_compatible_with_resource() {
fn borrow_mut_system(_: Res<R>, _: Query<EntityMut>) {}
@ -4075,7 +4073,6 @@ mod tests {
}
#[test]
#[ignore] // This should pass, but it currently fails due to limitations in our access model.
fn mut_compatible_with_resource_mut() {
fn borrow_mut_system(_: ResMut<R>, _: Query<EntityMut>) {}

View file

@ -8,7 +8,7 @@ use bevy_ecs::prelude::Commands;
use bevy_ecs::system::NonSendMut;
use bevy_ecs::system::ResMut;
use bevy_input::gamepad::{
GamepadConnection, GamepadConnectionEvent, GamepadInfo, RawGamepadAxisChangedEvent,
GamepadConnection, GamepadConnectionEvent, RawGamepadAxisChangedEvent,
RawGamepadButtonChangedEvent, RawGamepadEvent,
};
use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter};
@ -26,15 +26,13 @@ pub fn gilrs_event_startup_system(
gamepads.id_to_entity.insert(id, entity);
gamepads.entity_to_id.insert(entity, id);
let info = GamepadInfo {
name: gamepad.name().into(),
vendor_id: gamepad.vendor_id(),
product_id: gamepad.product_id(),
};
events.send(GamepadConnectionEvent {
gamepad: entity,
connection: GamepadConnection::Connected(info),
connection: GamepadConnection::Connected {
name: gamepad.name().to_string(),
vendor_id: gamepad.vendor_id(),
product_id: gamepad.product_id(),
},
});
}
}
@ -62,20 +60,17 @@ pub fn gilrs_event_system(
entity
});
let info = GamepadInfo {
name: pad.name().into(),
vendor_id: pad.vendor_id(),
product_id: pad.product_id(),
};
events.send(
GamepadConnectionEvent::new(entity, GamepadConnection::Connected(info.clone()))
.into(),
);
connection_events.send(GamepadConnectionEvent::new(
let event = GamepadConnectionEvent::new(
entity,
GamepadConnection::Connected(info),
));
GamepadConnection::Connected {
name: pad.name().to_string(),
vendor_id: pad.vendor_id(),
product_id: pad.product_id(),
},
);
events.send(event.clone().into());
connection_events.send(event);
}
EventType::Disconnected => {
let gamepad = gamepads

View file

@ -49,7 +49,7 @@ image = { version = "0.25.2", default-features = false }
# misc
bitflags = { version = "2.3", features = ["serde"] }
bytemuck = { version = "1.5" }
wgpu = { version = "22", default-features = false }
wgpu = { version = "23", default-features = false }
serde = { version = "1", features = ["derive"] }
derive_more = { version = "1", default-features = false, features = [
"error",

View file

@ -182,7 +182,7 @@ pub fn dds_format_to_texture_format(
DxgiFormat::R10G10B10A2_Typeless | DxgiFormat::R10G10B10A2_UNorm => {
TextureFormat::Rgb10a2Unorm
}
DxgiFormat::R11G11B10_Float => TextureFormat::Rg11b10Float,
DxgiFormat::R11G11B10_Float => TextureFormat::Rg11b10Ufloat,
DxgiFormat::R8G8B8A8_Typeless
| DxgiFormat::R8G8B8A8_UNorm
| DxgiFormat::R8G8B8A8_UNorm_sRGB => {

View file

@ -641,7 +641,7 @@ pub fn ktx2_dfd_to_texture_format(
&& sample_information[2].channel_type == 2
&& sample_information[2].bit_length == 10
{
TextureFormat::Rg11b10Float
TextureFormat::Rg11b10Ufloat
} else if sample_information[0].channel_type == 0
&& sample_information[0].bit_length == 9
&& sample_information[1].channel_type == 1
@ -1276,7 +1276,7 @@ pub fn ktx2_format_to_texture_format(
ktx2::Format::R32G32B32A32_SINT => TextureFormat::Rgba32Sint,
ktx2::Format::R32G32B32A32_SFLOAT => TextureFormat::Rgba32Float,
ktx2::Format::B10G11R11_UFLOAT_PACK32 => TextureFormat::Rg11b10Float,
ktx2::Format::B10G11R11_UFLOAT_PACK32 => TextureFormat::Rg11b10Ufloat,
ktx2::Format::E5B9G9R9_UFLOAT_PACK32 => TextureFormat::Rgb9e5Ufloat,
ktx2::Format::X8_D24_UNORM_PACK32 => TextureFormat::Depth24Plus,

View file

@ -21,6 +21,7 @@ serialize = ["serde", "smol_str/serde"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false }
bevy_core = { path = "../bevy_core", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false, features = [
"serialize",
] }

View file

@ -45,16 +45,16 @@ where
/// If the `input_device`:
/// - was present before, the position data is updated, and the old value is returned.
/// - wasn't present before, `None` is returned.
pub fn set(&mut self, input_device: T, position_data: f32) -> Option<f32> {
self.axis_data.insert(input_device, position_data)
pub fn set(&mut self, input_device: impl Into<T>, position_data: f32) -> Option<f32> {
self.axis_data.insert(input_device.into(), position_data)
}
/// Returns the position data of the provided `input_device`.
///
/// This will be clamped between [`Axis::MIN`] and [`Axis::MAX`] inclusive.
pub fn get(&self, input_device: T) -> Option<f32> {
pub fn get(&self, input_device: impl Into<T>) -> Option<f32> {
self.axis_data
.get(&input_device)
.get(&input_device.into())
.copied()
.map(|value| value.clamp(Self::MIN, Self::MAX))
}
@ -66,13 +66,13 @@ where
/// Use for things like camera zoom, where you want devices like mouse wheels to be able to
/// exceed the normal range. If being able to move faster on one input device
/// than another would give an unfair advantage, you should likely use [`Axis::get`] instead.
pub fn get_unclamped(&self, input_device: T) -> Option<f32> {
self.axis_data.get(&input_device).copied()
pub fn get_unclamped(&self, input_device: impl Into<T>) -> Option<f32> {
self.axis_data.get(&input_device.into()).copied()
}
/// Removes the position data of the `input_device`, returning the position data if the input device was previously set.
pub fn remove(&mut self, input_device: T) -> Option<f32> {
self.axis_data.remove(&input_device)
pub fn remove(&mut self, input_device: impl Into<T>) -> Option<f32> {
self.axis_data.remove(&input_device.into())
}
/// Returns an iterator over all axes.

View file

@ -1,6 +1,7 @@
//! The gamepad input functionality.
use crate::{Axis, ButtonInput, ButtonState};
use bevy_core::Name;
use bevy_ecs::{
change_detection::DetectChangesMut,
component::Component,
@ -148,7 +149,7 @@ impl GamepadConnectionEvent {
/// Is the gamepad connected?
pub fn connected(&self) -> bool {
matches!(self.connection, GamepadConnection::Connected(_))
matches!(self.connection, GamepadConnection::Connected { .. })
}
/// Is the gamepad disconnected?
@ -306,29 +307,29 @@ pub enum ButtonSettingsError {
},
}
/// The [`Gamepad`] [`component`](Component) stores a connected gamepad's metadata such as the `name` and its [`GamepadButton`] and [`GamepadAxis`].
/// Stores a connected gamepad's state and any metadata such as the device name.
///
/// The [`entity`](Entity) representing a gamepad and its [`minimal components`](GamepadSettings) are automatically managed.
/// An entity with this component is spawned automatically after [`GamepadConnectionEvent`]
/// and updated by [`gamepad_event_processing_system`].
///
/// # Usage
///
/// The only way to obtain a [`Gamepad`] is by [`query`](Query).
/// See also [`GamepadSettings`] for configuration.
///
/// # Examples
///
/// ```
/// # use bevy_input::gamepad::{Gamepad, GamepadAxis, GamepadButton};
/// # use bevy_ecs::system::Query;
/// # use bevy_core::Name;
/// #
/// fn gamepad_usage_system(gamepads: Query<&Gamepad>) {
/// for gamepad in gamepads.iter() {
/// println!("{}", gamepad.name());
/// fn gamepad_usage_system(gamepads: Query<(&Name, &Gamepad)>) {
/// for (name, gamepad) in &gamepads {
/// println!("{name}");
///
/// if gamepad.just_pressed(GamepadButton::North) {
/// println!("{} just pressed North", gamepad.name())
/// if gamepad.digital.just_pressed(GamepadButton::North) {
/// println!("{name} just pressed North")
/// }
///
/// if let Some(left_stick_x) = gamepad.get(GamepadAxis::LeftStickX) {
/// if let Some(left_stick_x) = gamepad.analog.get(GamepadAxis::LeftStickX) {
/// println!("left stick X: {}", left_stick_x)
/// }
/// }
@ -338,206 +339,6 @@ pub enum ButtonSettingsError {
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
#[require(GamepadSettings)]
pub struct Gamepad {
info: GamepadInfo,
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state
pub(crate) digital: ButtonInput<GamepadButton>,
/// [`Axis`] of [`GamepadButton`] representing their analog state.
pub(crate) analog: Axis<GamepadInput>,
}
impl Gamepad {
/// Creates a gamepad with the given metadata.
fn new(info: GamepadInfo) -> Self {
let mut analog = Axis::default();
for button in GamepadButton::all().iter().copied() {
analog.set(button.into(), 0.0);
}
for axis_type in GamepadAxis::all().iter().copied() {
analog.set(axis_type.into(), 0.0);
}
Self {
info,
analog,
digital: ButtonInput::default(),
}
}
/// The name of the gamepad.
///
/// This name is generally defined by the OS.
///
/// For example on Windows the name may be "HID-compliant game controller".
pub fn name(&self) -> &str {
self.info.name.as_str()
}
/// Returns the USB vendor ID as assigned by the USB-IF, if available.
pub fn vendor_id(&self) -> Option<u16> {
self.info.vendor_id
}
/// Returns the USB product ID as assigned by the [vendor], if available.
///
/// [vendor]: Self::vendor_id
pub fn product_id(&self) -> Option<u16> {
self.info.product_id
}
/// Returns the analog data of the provided [`GamepadAxis`] or [`GamepadButton`].
///
/// This will be clamped between [[`Axis::MIN`],[`Axis::MAX`]].
pub fn get(&self, input: impl Into<GamepadInput>) -> Option<f32> {
self.analog.get(input.into())
}
/// Returns the unclamped analog data of the provided [`GamepadAxis`] or [`GamepadButton`].
///
/// This value may be outside the [`Axis::MIN`] and [`Axis::MAX`] range.
pub fn get_unclamped(&self, input: impl Into<GamepadInput>) -> Option<f32> {
self.analog.get_unclamped(input.into())
}
/// Returns the left stick as a [`Vec2`]
pub fn left_stick(&self) -> Vec2 {
Vec2 {
x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0),
y: self.get(GamepadAxis::LeftStickY).unwrap_or(0.0),
}
}
/// Returns the right stick as a [`Vec2`]
pub fn right_stick(&self) -> Vec2 {
Vec2 {
x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0),
y: self.get(GamepadAxis::RightStickY).unwrap_or(0.0),
}
}
/// Returns the directional pad as a [`Vec2`]
pub fn dpad(&self) -> Vec2 {
Vec2 {
x: self.get(GamepadButton::DPadRight).unwrap_or(0.0)
- self.get(GamepadButton::DPadLeft).unwrap_or(0.0),
y: self.get(GamepadButton::DPadUp).unwrap_or(0.0)
- self.get(GamepadButton::DPadDown).unwrap_or(0.0),
}
}
/// Returns `true` if the [`GamepadButton`] has been pressed.
pub fn pressed(&self, button_type: GamepadButton) -> bool {
self.digital.pressed(button_type)
}
/// Returns `true` if any item in [`GamepadButton`] has been pressed.
pub fn any_pressed(&self, button_inputs: impl IntoIterator<Item = GamepadButton>) -> bool {
button_inputs
.into_iter()
.any(|button_type| self.pressed(button_type))
}
/// Returns `true` if all items in [`GamepadButton`] have been pressed.
pub fn all_pressed(&self, button_inputs: impl IntoIterator<Item = GamepadButton>) -> bool {
button_inputs
.into_iter()
.all(|button_type| self.pressed(button_type))
}
/// Returns `true` if the [`GamepadButton`] has been pressed during the current frame.
///
/// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_released`].
pub fn just_pressed(&self, button_type: GamepadButton) -> bool {
self.digital.just_pressed(button_type)
}
/// Returns `true` if any item in [`GamepadButton`] has been pressed during the current frame.
pub fn any_just_pressed(&self, button_inputs: impl IntoIterator<Item = GamepadButton>) -> bool {
button_inputs
.into_iter()
.any(|button_type| self.just_pressed(button_type))
}
/// Returns `true` if all items in [`GamepadButton`] have been just pressed.
pub fn all_just_pressed(&self, button_inputs: impl IntoIterator<Item = GamepadButton>) -> bool {
button_inputs
.into_iter()
.all(|button_type| self.just_pressed(button_type))
}
/// Returns `true` if the [`GamepadButton`] has been released during the current frame.
///
/// Note: This function does not imply information regarding the current state of [`ButtonInput::pressed`] or [`ButtonInput::just_pressed`].
pub fn just_released(&self, button_type: GamepadButton) -> bool {
self.digital.just_released(button_type)
}
/// Returns `true` if any item in [`GamepadButton`] has just been released.
pub fn any_just_released(
&self,
button_inputs: impl IntoIterator<Item = GamepadButton>,
) -> bool {
button_inputs
.into_iter()
.any(|button_type| self.just_released(button_type))
}
/// Returns `true` if all items in [`GamepadButton`] have just been released.
pub fn all_just_released(
&self,
button_inputs: impl IntoIterator<Item = GamepadButton>,
) -> bool {
button_inputs
.into_iter()
.all(|button_type| self.just_released(button_type))
}
/// Returns an iterator over all digital [button]s that are pressed.
///
/// [button]: GamepadButton
pub fn get_pressed(&self) -> impl Iterator<Item = &GamepadButton> {
self.digital.get_pressed()
}
/// Returns an iterator over all digital [button]s that were just pressed.
///
/// [button]: GamepadButton
pub fn get_just_pressed(&self) -> impl Iterator<Item = &GamepadButton> {
self.digital.get_just_pressed()
}
/// Returns an iterator over all digital [button]s that were just released.
///
/// [button]: GamepadButton
pub fn get_just_released(&self) -> impl Iterator<Item = &GamepadButton> {
self.digital.get_just_released()
}
/// Returns an iterator over all analog [axes].
///
/// [axes]: GamepadInput
pub fn get_analog_axes(&self) -> impl Iterator<Item = &GamepadInput> {
self.analog.all_axes()
}
}
// Note that we don't expose `gilrs::Gamepad::uuid` due to
// https://gitlab.com/gilrs-project/gilrs/-/issues/153.
//
/// Metadata associated with a [`Gamepad`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize)
)]
pub struct GamepadInfo {
/// The name of the gamepad.
///
/// This name is generally defined by the OS.
///
/// For example on Windows the name may be "HID-compliant game controller".
pub name: String,
/// The USB vendor ID as assigned by the USB-IF, if available.
pub vendor_id: Option<u16>,
@ -545,6 +346,59 @@ pub struct GamepadInfo {
///
/// [vendor]: Self::vendor_id
pub product_id: Option<u16>,
/// [`ButtonInput`] of [`GamepadButton`] representing their digital state
pub digital: ButtonInput<GamepadButton>,
/// [`Axis`] of [`GamepadButton`] representing their analog state.
pub analog: Axis<GamepadInput>,
}
impl Gamepad {
/// Returns the left stick as a [`Vec2`]
pub fn left_stick(&self) -> Vec2 {
Vec2 {
x: self.analog.get(GamepadAxis::LeftStickX).unwrap_or(0.0),
y: self.analog.get(GamepadAxis::LeftStickY).unwrap_or(0.0),
}
}
/// Returns the right stick as a [`Vec2`]
pub fn right_stick(&self) -> Vec2 {
Vec2 {
x: self.analog.get(GamepadAxis::RightStickX).unwrap_or(0.0),
y: self.analog.get(GamepadAxis::RightStickY).unwrap_or(0.0),
}
}
/// Returns the directional pad as a [`Vec2`]
pub fn dpad(&self) -> Vec2 {
Vec2 {
x: self.analog.get(GamepadButton::DPadRight).unwrap_or(0.0)
- self.analog.get(GamepadButton::DPadLeft).unwrap_or(0.0),
y: self.analog.get(GamepadButton::DPadUp).unwrap_or(0.0)
- self.analog.get(GamepadButton::DPadDown).unwrap_or(0.0),
}
}
}
impl Default for Gamepad {
fn default() -> Self {
let mut analog = Axis::default();
for button in GamepadButton::all().iter().copied() {
analog.set(button, 0.0);
}
for axis_type in GamepadAxis::all().iter().copied() {
analog.set(axis_type, 0.0);
}
Self {
vendor_id: None,
product_id: None,
digital: Default::default(),
analog,
}
}
}
/// Represents gamepad input types that are mapped in the range [0.0, 1.0].
@ -1356,12 +1210,23 @@ pub fn gamepad_connection_system(
for connection_event in connection_events.read() {
let id = connection_event.gamepad;
match &connection_event.connection {
GamepadConnection::Connected(info) => {
GamepadConnection::Connected {
name,
vendor_id,
product_id,
} => {
let Some(mut gamepad) = commands.get_entity(id) else {
warn!("Gamepad {:} removed before handling connection event.", id);
continue;
};
gamepad.insert(Gamepad::new(info.clone()));
gamepad.insert((
Name::new(name.clone()),
Gamepad {
vendor_id: *vendor_id,
product_id: *product_id,
..Default::default()
},
));
info!("Gamepad {:?} connected.", id);
}
GamepadConnection::Disconnected => {
@ -1379,6 +1244,9 @@ pub fn gamepad_connection_system(
}
}
// Note that we don't expose `gilrs::Gamepad::uuid` due to
// https://gitlab.com/gilrs-project/gilrs/-/issues/153.
//
/// The connection status of a gamepad.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))]
@ -1389,7 +1257,20 @@ pub fn gamepad_connection_system(
)]
pub enum GamepadConnection {
/// The gamepad is connected.
Connected(GamepadInfo),
Connected {
/// The name of the gamepad.
///
/// This name is generally defined by the OS.
///
/// For example on Windows the name may be "HID-compliant game controller".
name: String,
/// The USB vendor ID as assigned by the USB-IF, if available.
vendor_id: Option<u16>,
/// The USB product ID as assigned by the vendor, if available.
product_id: Option<u16>,
},
/// The gamepad is disconnected.
Disconnected,
}
@ -1426,12 +1307,12 @@ pub fn gamepad_event_processing_system(
};
let Some(filtered_value) = gamepad_settings
.get_axis_settings(axis)
.filter(value, gamepad_axis.get(axis))
.filter(value, gamepad_axis.analog.get(axis))
else {
continue;
};
gamepad_axis.analog.set(axis.into(), filtered_value);
gamepad_axis.analog.set(axis, filtered_value);
let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value);
processed_axis_events.send(send_event);
processed_events.send(GamepadEvent::from(send_event));
@ -1447,16 +1328,16 @@ pub fn gamepad_event_processing_system(
};
let Some(filtered_value) = settings
.get_button_axis_settings(button)
.filter(value, gamepad_buttons.get(button))
.filter(value, gamepad_buttons.analog.get(button))
else {
continue;
};
let button_settings = settings.get_button_settings(button);
gamepad_buttons.analog.set(button.into(), filtered_value);
gamepad_buttons.analog.set(button, filtered_value);
if button_settings.is_released(filtered_value) {
// Check if button was previously pressed
if gamepad_buttons.pressed(button) {
if gamepad_buttons.digital.pressed(button) {
processed_digital_events.send(GamepadButtonStateChangedEvent::new(
gamepad,
button,
@ -1468,7 +1349,7 @@ pub fn gamepad_event_processing_system(
gamepad_buttons.digital.release(button);
} else if button_settings.is_pressed(filtered_value) {
// Check if button was previously not pressed
if !gamepad_buttons.pressed(button) {
if !gamepad_buttons.digital.pressed(button) {
processed_digital_events.send(GamepadButtonStateChangedEvent::new(
gamepad,
button,
@ -1626,8 +1507,8 @@ mod tests {
GamepadAxis, GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent,
GamepadButtonStateChangedEvent,
GamepadConnection::{Connected, Disconnected},
GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadSettings,
RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, RawGamepadEvent,
GamepadConnectionEvent, GamepadEvent, GamepadSettings, RawGamepadAxisChangedEvent,
RawGamepadButtonChangedEvent, RawGamepadEvent,
};
use crate::ButtonState;
use bevy_app::{App, PreUpdate};
@ -2000,11 +1881,11 @@ mod tests {
.resource_mut::<Events<GamepadConnectionEvent>>()
.send(GamepadConnectionEvent::new(
gamepad,
Connected(GamepadInfo {
name: String::from("Gamepad test"),
Connected {
name: "Test gamepad".to_string(),
vendor_id: None,
product_id: None,
}),
},
));
gamepad
}
@ -2522,13 +2403,8 @@ mod tests {
assert_eq!(event.button, GamepadButton::DPadDown);
assert_eq!(event.state, ButtonState::Pressed);
}
assert!(ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.pressed(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(gamepad.digital.pressed(GamepadButton::DPadDown));
ctx.app
.world_mut()
@ -2543,13 +2419,8 @@ mod tests {
.len(),
0
);
assert!(ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.pressed(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(gamepad.digital.pressed(GamepadButton::DPadDown));
}
#[test]
@ -2568,23 +2439,13 @@ mod tests {
ctx.update();
// Check it is flagged for this frame
assert!(ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.just_pressed(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(gamepad.digital.just_pressed(GamepadButton::DPadDown));
ctx.update();
//Check it clears next frame
assert!(!ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.just_pressed(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(!gamepad.digital.just_pressed(GamepadButton::DPadDown));
}
#[test]
fn gamepad_buttons_released() {
@ -2627,13 +2488,8 @@ mod tests {
assert_eq!(event.button, GamepadButton::DPadDown);
assert_eq!(event.state, ButtonState::Released);
}
assert!(!ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.pressed(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(!gamepad.digital.pressed(GamepadButton::DPadDown));
ctx.app
.world_mut()
.resource_mut::<Events<GamepadButtonStateChangedEvent>>()
@ -2672,23 +2528,13 @@ mod tests {
ctx.update();
// Check it is flagged for this frame
assert!(ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.just_released(GamepadButton::DPadDown));
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(gamepad.digital.just_released(GamepadButton::DPadDown));
ctx.update();
//Check it clears next frame
assert!(!ctx
.app
.world_mut()
.query::<&Gamepad>()
.get(ctx.app.world(), entity)
.unwrap()
.just_released(GamepadButton::DPadDown));
// Check it clears next frame
let gamepad = ctx.app.world_mut().get::<Gamepad>(entity).unwrap();
assert!(!gamepad.digital.just_released(GamepadButton::DPadDown));
}
#[test]

View file

@ -57,7 +57,7 @@ use gamepad::{
gamepad_connection_system, gamepad_event_processing_system, GamepadAxis,
GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent,
GamepadButtonStateChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent,
GamepadInfo, GamepadInput, GamepadRumbleRequest, GamepadSettings, RawGamepadAxisChangedEvent,
GamepadInput, GamepadRumbleRequest, GamepadSettings, RawGamepadAxisChangedEvent,
RawGamepadButtonChangedEvent, RawGamepadEvent,
};
@ -142,7 +142,6 @@ impl Plugin for InputPlugin {
.register_type::<GamepadButtonChangedEvent>()
.register_type::<GamepadAxisChangedEvent>()
.register_type::<GamepadButtonStateChangedEvent>()
.register_type::<GamepadInfo>()
.register_type::<GamepadConnection>()
.register_type::<GamepadSettings>()
.register_type::<GamepadAxis>()

View file

@ -180,7 +180,7 @@ impl NormedVectorSpace for f32 {
///
/// 3. Importantly, the interpolation must be *subdivision-stable*: for any interpolation curve
/// between two (unnamed) values and any parameter-value pairs `(t0, p)` and `(t1, q)`, the
/// interpolation curve between `p` and `q` must be the *linear* reparametrization of the original
/// interpolation curve between `p` and `q` must be the *linear* reparameterization of the original
/// interpolation curve restricted to the interval `[t0, t1]`.
///
/// The last of these conditions is very strong and indicates something like constant speed. It
@ -197,7 +197,7 @@ impl NormedVectorSpace for f32 {
/// / \
/// / \
/// / linear \
/// / reparametrization \
/// / reparameterization \
/// / t = t0 * (1 - s) + t1 * s \
/// / \
/// |-------------------------------------|

View file

@ -362,7 +362,7 @@ where
}
}
/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling).
/// A curve that has had its domain changed by a linear reparameterization (stretching and scaling).
/// Curves of this type are produced by [`Curve::reparametrize_linear`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]

View file

@ -903,7 +903,7 @@ where
}
}
/// An error indicating that a linear reparametrization couldn't be performed because of
/// An error indicating that a linear reparameterization couldn't be performed because of
/// malformed inputs.
#[derive(Debug, Error, Display)]
#[display("Could not build a linear function to reparametrize this curve")]
@ -912,8 +912,8 @@ pub enum LinearReparamError {
#[display("This curve has unbounded domain")]
SourceCurveUnbounded,
/// The target interval for reparametrization was unbounded.
#[display("The target interval for reparametrization is unbounded")]
/// The target interval for reparameterization was unbounded.
#[display("The target interval for reparameterization is unbounded")]
TargetIntervalUnbounded,
}
@ -1195,7 +1195,7 @@ mod tests {
}
#[test]
fn reparametrization() {
fn reparameterization() {
let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
let reparametrized_curve = curve
.by_ref()

View file

@ -292,6 +292,22 @@ impl Default for CircularSector {
}
}
impl Measured2d for CircularSector {
#[inline(always)]
fn area(&self) -> f32 {
self.arc.radius.squared() * self.arc.half_angle
}
#[inline(always)]
fn perimeter(&self) -> f32 {
if self.half_angle() >= PI {
self.arc.radius * 2.0 * PI
} else {
2.0 * self.radius() + self.arc_length()
}
}
}
impl CircularSector {
/// Create a new [`CircularSector`] from a `radius` and an `angle`
#[inline(always)]
@ -382,12 +398,6 @@ impl CircularSector {
pub fn sagitta(&self) -> f32 {
self.arc.sagitta()
}
/// Returns the area of this sector
#[inline(always)]
pub fn area(&self) -> f32 {
self.arc.radius.squared() * self.arc.half_angle
}
}
/// A primitive representing a circular segment:
@ -425,6 +435,17 @@ impl Default for CircularSegment {
}
}
impl Measured2d for CircularSegment {
#[inline(always)]
fn area(&self) -> f32 {
0.5 * self.arc.radius.squared() * (self.arc.angle() - ops::sin(self.arc.angle()))
}
#[inline(always)]
fn perimeter(&self) -> f32 {
self.chord_length() + self.arc_length()
}
}
impl CircularSegment {
/// Create a new [`CircularSegment`] from a `radius`, and an `angle`
#[inline(always)]
@ -515,17 +536,12 @@ impl CircularSegment {
pub fn sagitta(&self) -> f32 {
self.arc.sagitta()
}
/// Returns the area of this segment
#[inline(always)]
pub fn area(&self) -> f32 {
0.5 * self.arc.radius.squared() * (self.arc.angle() - ops::sin(self.arc.angle()))
}
}
#[cfg(test)]
mod arc_tests {
use core::f32::consts::FRAC_PI_4;
use core::f32::consts::SQRT_2;
use approx::assert_abs_diff_eq;
@ -548,7 +564,9 @@ mod arc_tests {
is_minor: bool,
is_major: bool,
sector_area: f32,
sector_perimeter: f32,
segment_area: f32,
segment_perimeter: f32,
}
impl ArcTestCase {
@ -581,6 +599,7 @@ mod arc_tests {
assert_abs_diff_eq!(self.apothem, sector.apothem());
assert_abs_diff_eq!(self.sagitta, sector.sagitta());
assert_abs_diff_eq!(self.sector_area, sector.area());
assert_abs_diff_eq!(self.sector_perimeter, sector.perimeter());
}
fn check_segment(&self, segment: CircularSegment) {
@ -593,6 +612,7 @@ mod arc_tests {
assert_abs_diff_eq!(self.apothem, segment.apothem());
assert_abs_diff_eq!(self.sagitta, segment.sagitta());
assert_abs_diff_eq!(self.segment_area, segment.area());
assert_abs_diff_eq!(self.segment_perimeter, segment.perimeter());
}
}
@ -615,7 +635,9 @@ mod arc_tests {
is_minor: true,
is_major: false,
sector_area: 0.0,
sector_perimeter: 2.0,
segment_area: 0.0,
segment_perimeter: 0.0,
};
tests.check_arc(Arc2d::new(1.0, 0.0));
@ -642,7 +664,9 @@ mod arc_tests {
is_minor: true,
is_major: false,
sector_area: 0.0,
sector_perimeter: 0.0,
segment_area: 0.0,
segment_perimeter: 0.0,
};
tests.check_arc(Arc2d::new(0.0, FRAC_PI_4));
@ -670,7 +694,9 @@ mod arc_tests {
is_minor: true,
is_major: false,
sector_area: FRAC_PI_4,
sector_perimeter: FRAC_PI_2 + 2.0,
segment_area: FRAC_PI_4 - 0.5,
segment_perimeter: FRAC_PI_2 + SQRT_2,
};
tests.check_arc(Arc2d::from_turns(1.0, 0.25));
@ -697,7 +723,9 @@ mod arc_tests {
is_minor: true,
is_major: true,
sector_area: FRAC_PI_2,
sector_perimeter: PI + 2.0,
segment_area: FRAC_PI_2,
segment_perimeter: PI + 2.0,
};
tests.check_arc(Arc2d::from_radians(1.0, PI));
@ -724,7 +752,9 @@ mod arc_tests {
is_minor: false,
is_major: true,
sector_area: PI,
sector_perimeter: 2.0 * PI,
segment_area: PI,
segment_perimeter: 2.0 * PI,
};
tests.check_arc(Arc2d::from_degrees(1.0, 360.0));

View file

@ -24,7 +24,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
# misc
bitflags = { version = "2.3", features = ["serde"] }
bytemuck = { version = "1.5" }
wgpu = { version = "22", default-features = false }
wgpu = { version = "23", default-features = false }
serde = { version = "1", features = ["derive"] }
hexasphere = "15.0"
derive_more = { version = "1", default-features = false, features = [

View file

@ -62,7 +62,7 @@ lz4_flex = { version = "0.11", default-features = false, features = [
], optional = true }
range-alloc = { version = "0.1.3", optional = true }
half = { version = "2", features = ["bytemuck"], optional = true }
meshopt = { version = "0.3.0", optional = true }
meshopt = { version = "0.4", optional = true }
metis = { version = "0.2", optional = true }
itertools = { version = "0.13", optional = true }
bitvec = { version = "1", optional = true }

View file

@ -14,9 +14,8 @@ use derive_more::derive::{Display, Error};
use half::f16;
use itertools::Itertools;
use meshopt::{
build_meshlets,
ffi::{meshopt_Meshlet, meshopt_simplifyWithAttributes},
generate_vertex_remap_multi, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream,
build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi,
simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream,
};
use metis::Graph;
use smallvec::SmallVec;
@ -29,7 +28,7 @@ const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.95;
/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`].
///
/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4).
pub const DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4;
pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4;
const CENTIMETERS_PER_METER: f32 = 100.0;
@ -54,7 +53,7 @@ impl MeshletMesh {
/// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`.
/// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter.
///
/// Use [`DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed.
/// Use [`MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed.
///
/// To ensure that two different meshes do not have cracks between them when placed directly next to each other:
/// * Use the same quantization factor when converting each mesh to a meshlet mesh
@ -72,6 +71,7 @@ impl MeshletMesh {
let vertex_buffer = mesh.create_packed_vertex_buffer_data();
let vertex_stride = mesh.get_vertex_size() as usize;
let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap();
let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]);
let mut meshlets = compute_meshlets(&indices, &vertices);
let mut bounding_spheres = meshlets
.iter()
@ -102,7 +102,7 @@ impl MeshletMesh {
Some(&indices),
);
let mut vertex_locks = vec![0; vertices.vertex_count];
let mut vertex_locks = vec![false; vertices.vertex_count];
// Build further LODs
let mut simplification_queue = Vec::from_iter(0..meshlets.len());
@ -138,9 +138,14 @@ impl MeshletMesh {
}
// Simplify the group to ~50% triangle count
let Some((simplified_group_indices, mut group_error)) =
simplify_meshlet_group(&group_meshlets, &meshlets, &vertices, &vertex_locks)
else {
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group(
&group_meshlets,
&meshlets,
&vertices,
vertex_normals,
vertex_stride,
&vertex_locks,
) else {
// Couldn't simplify the group enough, retry its meshlets later
retry_queue.extend_from_slice(&group_meshlets);
continue;
@ -338,7 +343,7 @@ fn group_meshlets(
}
fn lock_group_borders(
vertex_locks: &mut [u8],
vertex_locks: &mut [bool],
groups: &[SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>],
meshlets: &Meshlets,
position_only_vertex_remap: &[u32],
@ -369,17 +374,17 @@ fn lock_group_borders(
// Lock vertices used by more than 1 group
for i in 0..vertex_locks.len() {
let vertex_id = position_only_vertex_remap[i] as usize;
vertex_locks[i] = (position_only_locks[vertex_id] == -2) as u8;
vertex_locks[i] = position_only_locks[vertex_id] == -2;
}
}
#[allow(unsafe_code)]
#[allow(clippy::undocumented_unsafe_blocks)]
fn simplify_meshlet_group(
group_meshlets: &[usize],
meshlets: &Meshlets,
vertices: &VertexDataAdapter<'_>,
vertex_locks: &[u8],
vertex_normals: &[f32],
vertex_stride: usize,
vertex_locks: &[bool],
) -> Option<(Vec<u32>, f16)> {
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
let mut group_indices = Vec::new();
@ -391,33 +396,19 @@ fn simplify_meshlet_group(
}
// Simplify the group to ~50% triangle count
// TODO: Simplify using vertex attributes
let mut error = 0.0;
let simplified_group_indices = unsafe {
let vertex_data = vertices.reader.get_ref();
let vertex_data = vertex_data.as_ptr().cast::<u8>();
let positions = vertex_data.add(vertices.position_offset);
let mut result: Vec<u32> = vec![0; group_indices.len()];
let index_count = meshopt_simplifyWithAttributes(
result.as_mut_ptr().cast(),
group_indices.as_ptr().cast(),
group_indices.len(),
positions.cast::<f32>(),
vertices.vertex_count,
vertices.vertex_stride,
core::ptr::null(),
0,
core::ptr::null(),
0,
vertex_locks.as_ptr().cast(),
group_indices.len() / 2,
f32::MAX,
(SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute).bits(),
core::ptr::from_mut(&mut error),
);
result.resize(index_count, 0u32);
result
};
let simplified_group_indices = simplify_with_attributes_and_locks(
&group_indices,
vertices,
vertex_normals,
&[0.5; 3],
vertex_stride,
vertex_locks,
group_indices.len() / 2,
f32::MAX,
SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
Some(&mut error),
);
// Check if we were able to simplify at least a little
if simplified_group_indices.len() as f32 / group_indices.len() as f32

View file

@ -36,7 +36,7 @@ pub(crate) use self::{
pub use self::asset::{MeshletMesh, MeshletMeshLoader, MeshletMeshSaver};
#[cfg(feature = "meshlet_processor")]
pub use self::from_mesh::{
MeshToMeshletMeshConversionError, DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR,
MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR,
};
use self::{

View file

@ -10,11 +10,10 @@ use core::num::NonZero;
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, Handle};
use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Has, QueryState},
query::{Has, QueryState, Without},
schedule::{common_conditions::resource_exists, IntoSystemConfigs as _},
system::{lifetimeless::Read, Commands, Res, ResMut, Resource},
world::{FromWorld, World},
@ -24,7 +23,8 @@ use bevy_render::{
BatchedInstanceBuffers, GpuPreprocessingSupport, IndirectParameters,
IndirectParametersBuffer, PreprocessWorkItem,
},
render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext},
graph::CameraDriverLabel,
render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext},
render_resource::{
binding_types::{storage_buffer, storage_buffer_read_only, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, BufferBinding,
@ -65,12 +65,15 @@ pub struct GpuMeshPreprocessPlugin {
/// The render node for the mesh uniform building pass.
pub struct GpuPreprocessNode {
view_query: QueryState<(
Entity,
Read<PreprocessBindGroup>,
Read<ViewUniformOffset>,
Has<GpuCulling>,
)>,
view_query: QueryState<
(
Entity,
Read<PreprocessBindGroup>,
Read<ViewUniformOffset>,
Has<GpuCulling>,
),
Without<SkipGpuPreprocess>,
>,
}
/// The compute shader pipelines for the mesh uniform building pass.
@ -108,9 +111,14 @@ bitflags! {
/// The compute shader bind group for the mesh uniform building pass.
///
/// This goes on the view.
#[derive(Component)]
#[derive(Component, Clone)]
pub struct PreprocessBindGroup(BindGroup);
/// Stops the `GpuPreprocessNode` attempting to generate the buffer for this view
/// useful to avoid duplicating effort if the bind group is shared between views
#[derive(Component)]
pub struct SkipGpuPreprocess;
impl Plugin for GpuMeshPreprocessPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
@ -136,10 +144,12 @@ impl Plugin for GpuMeshPreprocessPlugin {
}
// Stitch the node in.
let gpu_preprocess_node = GpuPreprocessNode::from_world(render_app.world_mut());
let mut render_graph = render_app.world_mut().resource_mut::<RenderGraph>();
render_graph.add_node(NodePbr::GpuPreprocess, gpu_preprocess_node);
render_graph.add_node_edge(NodePbr::GpuPreprocess, CameraDriverLabel);
render_app
.add_render_graph_node::<GpuPreprocessNode>(Core3d, NodePbr::GpuPreprocess)
.add_render_graph_edges(Core3d, (NodePbr::GpuPreprocess, Node3d::Prepass))
.add_render_graph_edges(Core3d, (NodePbr::GpuPreprocess, NodePbr::ShadowPass))
.init_resource::<PreprocessPipelines>()
.init_resource::<SpecializedComputePipelines<PreprocessPipeline>>()
.add_systems(
@ -200,7 +210,7 @@ impl Node for GpuPreprocessNode {
// Grab the index buffer for this view.
let Some(index_buffer) = index_buffers.get(&view) else {
warn!("The preprocessing index buffer wasn't present");
return Ok(());
continue;
};
// Select the right pipeline, depending on whether GPU culling is in

View file

@ -110,4 +110,4 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
@group(0) @binding(31) var<storage, read_write> oit_layers: array<vec2<u32>>;
@group(0) @binding(32) var<storage, read_write> oit_layer_ids: array<atomic<i32>>;
@group(0) @binding(33) var<uniform> oit_settings: types::OrderIndependentTransparencySettings;
#endif OIT_ENABLED
#endif // OIT_ENABLED

View file

@ -254,7 +254,7 @@ pub struct DragEntry {
/// 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 {
pub struct PointerButtonState {
/// Stores the press location and start time for each button currently being pressed by the pointer.
pub pressing: HashMap<Entity, (Location, Instant, HitData)>,
/// Stores the starting and current locations for each entity currently being dragged by the pointer.
@ -263,6 +263,42 @@ pub struct PointerState {
pub dragging_over: HashMap<Entity, HitData>,
}
/// State for all pointers.
#[derive(Debug, Clone, Default, Resource)]
pub struct PointerState {
/// Pressing and dragging state, organized by pointer and button.
pub pointer_buttons: HashMap<(PointerId, PointerButton), PointerButtonState>,
}
impl PointerState {
/// Retrieves the current state for a specific pointer and button, if it has been created.
pub fn get(&self, pointer_id: PointerId, button: PointerButton) -> Option<&PointerButtonState> {
self.pointer_buttons.get(&(pointer_id, button))
}
/// Provides write access to the state of a pointer and button, creating it if it does not yet exist.
pub fn get_mut(
&mut self,
pointer_id: PointerId,
button: PointerButton,
) -> &mut PointerButtonState {
self.pointer_buttons
.entry((pointer_id, button))
.or_default()
}
/// Clears all the data assoceated with all of the buttons on a pointer. Does not free the underlying memory.
pub fn clear(&mut self, pointer_id: PointerId) {
for button in PointerButton::iter() {
if let Some(state) = self.pointer_buttons.get_mut(&(pointer_id, button)) {
state.pressing.clear();
state.dragging.clear();
state.dragging_over.clear();
}
}
}
}
/// A helper system param for accessing the picking event writers.
#[derive(SystemParam)]
pub struct PickingEventWriters<'w> {
@ -316,8 +352,7 @@ pub fn pointer_events(
pointer_map: Res<PointerMap>,
hover_map: Res<HoverMap>,
previous_hover_map: Res<PreviousHoverMap>,
// Local state
mut pointer_state: Local<HashMap<(PointerId, PointerButton), PointerState>>,
mut pointer_state: ResMut<PointerState>,
// Output
mut commands: Commands,
mut event_writers: PickingEventWriters,
@ -352,7 +387,7 @@ pub fn pointer_events(
// Possibly send DragEnter events
for button in PointerButton::iter() {
let state = pointer_state.entry((pointer_id, button)).or_default();
let state = pointer_state.get_mut(pointer_id, button);
for drag_target in state
.dragging
@ -397,7 +432,7 @@ pub fn pointer_events(
match action {
// Pressed Button
PointerAction::Pressed { direction, button } => {
let state = pointer_state.entry((pointer_id, button)).or_default();
let state = pointer_state.get_mut(pointer_id, button);
// The sequence of events emitted depends on if this is a press or a release
match direction {
@ -519,7 +554,7 @@ pub fn pointer_events(
PointerAction::Moved { delta } => {
// Triggers during movement even if not over an entity
for button in PointerButton::iter() {
let state = pointer_state.entry((pointer_id, button)).or_default();
let state = pointer_state.get_mut(pointer_id, button);
// Emit DragEntry and DragStart the first time we move while pressing an entity
for (press_target, (location, _, hit)) in state.pressing.iter() {
@ -619,14 +654,8 @@ pub fn pointer_events(
commands.trigger_targets(cancel_event.clone(), hovered_entity);
event_writers.cancel_events.send(cancel_event);
}
// 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();
}
}
// Clear the state for the canceled pointer
pointer_state.clear(pointer_id);
}
}
}
@ -662,7 +691,7 @@ pub fn pointer_events(
// Possibly send DragLeave events
for button in PointerButton::iter() {
let state = pointer_state.entry((pointer_id, button)).or_default();
let state = pointer_state.get_mut(pointer_id, button);
state.dragging_over.remove(&hovered_entity);
for drag_target in state.dragging.keys() {
let drag_leave_event = Pointer::new(

View file

@ -381,6 +381,7 @@ impl Plugin for InteractionPlugin {
app.init_resource::<focus::HoverMap>()
.init_resource::<focus::PreviousHoverMap>()
.init_resource::<PointerState>()
.add_event::<Pointer<Cancel>>()
.add_event::<Pointer<Click>>()
.add_event::<Pointer<Down>>()

View file

@ -51,7 +51,7 @@ glam = { version = "0.29", features = ["serde"], optional = true }
petgraph = { version = "0.6", features = ["serde-1"], optional = true }
smol_str = { version = "0.2.0", features = ["serde"], optional = true }
uuid = { version = "1.0", optional = true, features = ["v4", "serde"] }
wgpu-types = { version = "22", features = ["serde"], optional = true }
wgpu-types = { version = "23", features = ["serde"], optional = true }
[dev-dependencies]
ron = "0.8.0"

View file

@ -301,11 +301,11 @@
//! [fully-qualified type name]: bevy_reflect::TypePath::type_path
use async_channel::{Receiver, Sender};
use bevy_app::prelude::*;
use bevy_app::{prelude::*, MainScheduleOrder};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
entity::Entity,
schedule::IntoSystemConfigs,
schedule::{IntoSystemConfigs, ScheduleLabel},
system::{Commands, In, IntoSystem, ResMut, Resource, System, SystemId},
world::World,
};
@ -434,11 +434,16 @@ impl Plugin for RemotePlugin {
);
}
app.init_schedule(RemoteLast)
.world_mut()
.resource_mut::<MainScheduleOrder>()
.insert_after(Last, RemoteLast);
app.insert_resource(remote_methods)
.init_resource::<RemoteWatchingRequests>()
.add_systems(PreStartup, setup_mailbox_channel)
.add_systems(
Update,
RemoteLast,
(
process_remote_requests,
process_ongoing_watching_requests,
@ -449,6 +454,10 @@ impl Plugin for RemotePlugin {
}
}
/// Schedule that contains all systems to process Bevy Remote Protocol requests
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
struct RemoteLast;
/// A type to hold the allowed types of systems to be used as method handlers.
#[derive(Debug)]
pub enum RemoteMethodHandler {

View file

@ -67,14 +67,14 @@ codespan-reporting = "0.11.0"
# It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm.
# When the 'atomics' feature is enabled `fragile-send-sync-non-atomic` does nothing
# and Bevy instead wraps `wgpu` types to verify they are not used off their origin thread.
wgpu = { version = "22", default-features = false, features = [
wgpu = { version = "23", default-features = false, features = [
"wgsl",
"dx12",
"metal",
"naga-ir",
"fragile-send-sync-non-atomic-wasm",
] }
naga = { version = "22", features = ["wgsl-in"] }
naga = { version = "23", features = ["wgsl-in"] }
serde = { version = "1", features = ["derive"] }
bytemuck = { version = "1.5", features = ["derive", "must_cast"] }
downcast-rs = "1.2.0"
@ -97,12 +97,12 @@ offset-allocator = "0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Omit the `glsl` feature in non-WebAssembly by default.
naga_oil = { version = "0.15", default-features = false, features = [
naga_oil = { version = "0.16", default-features = false, features = [
"test_shader",
] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
naga_oil = "0.15"
naga_oil = "0.16"
js-sys = "0.3"
web-sys = { version = "0.3.67", features = [
'Blob',

View file

@ -45,6 +45,18 @@ impl From<wgpu::BindGroup> for BindGroup {
}
}
impl<'a> From<&'a BindGroup> for Option<&'a wgpu::BindGroup> {
fn from(value: &'a BindGroup) -> Self {
Some(value.deref())
}
}
impl<'a> From<&'a mut BindGroup> for Option<&'a wgpu::BindGroup> {
fn from(value: &'a mut BindGroup) -> Self {
Some(&*value)
}
}
impl Deref for BindGroup {
type Target = wgpu::BindGroup;

View file

@ -735,7 +735,6 @@ impl PipelineCache {
let compilation_options = PipelineCompilationOptions {
constants: &std::collections::HashMap::new(),
zero_initialize_workgroup_memory: false,
vertex_pulling_transform: Default::default(),
};
let descriptor = RawRenderPipelineDescriptor {
@ -747,7 +746,7 @@ impl PipelineCache {
primitive: descriptor.primitive,
vertex: RawVertexState {
buffers: &vertex_buffer_layouts,
entry_point: descriptor.vertex.entry_point.deref(),
entry_point: Some(descriptor.vertex.entry_point.deref()),
module: &vertex_module,
// TODO: Should this be the same as the fragment compilation options?
compilation_options: compilation_options.clone(),
@ -755,7 +754,7 @@ impl PipelineCache {
fragment: fragment_data
.as_ref()
.map(|(module, entry_point, targets)| RawFragmentState {
entry_point,
entry_point: Some(entry_point),
module,
targets,
// TODO: Should this be the same as the vertex compilation options?
@ -812,12 +811,11 @@ impl PipelineCache {
label: descriptor.label.as_deref(),
layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }),
module: &compute_module,
entry_point: &descriptor.entry_point,
entry_point: Some(&descriptor.entry_point),
// TODO: Expose this somehow
compilation_options: PipelineCompilationOptions {
constants: &std::collections::HashMap::new(),
zero_initialize_workgroup_memory: false,
vertex_pulling_transform: Default::default(),
},
cache: None,
};

View file

@ -146,14 +146,14 @@ impl Plugin for ViewPlugin {
}
}
/// Configuration resource for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing).
/// Component for configuring the number of samples for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)
/// for a [`Camera`](crate::camera::Camera).
///
/// The number of samples to run for Multi-Sample Anti-Aliasing for a given camera. Higher numbers
/// result in smoother edges.
/// Defaults to 4 samples. A higher number of samples results in smoother edges.
///
/// Defaults to 4 samples. Some advanced rendering features may require that MSAA be disabled.
/// Some advanced rendering features may require that MSAA is disabled.
///
/// Note that web currently only supports 1 or 4 samples.
/// Note that the web currently only supports 1 or 4 samples.
#[derive(
Component,
Default,

View file

@ -8,12 +8,6 @@ use bevy_render::{
use bevy_transform::components::{GlobalTransform, Transform};
/// A [`Bundle`] of components for drawing a single sprite from an image.
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Clone, Debug, Default)]
#[deprecated(
since = "0.15.0",

View file

@ -30,7 +30,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
bundle::SpriteBundle,
sprite::{ImageScaleMode, Sprite},
sprite::{Sprite, SpriteImageMode},
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
@ -106,7 +106,7 @@ impl Plugin for SpritePlugin {
app.init_asset::<TextureAtlasLayout>()
.register_asset_reflect::<TextureAtlasLayout>()
.register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<SpriteImageMode>()
.register_type::<TextureSlicer>()
.register_type::<Anchor>()
.register_type::<TextureAtlas>()

View file

@ -34,6 +34,8 @@ pub struct Sprite {
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
pub anchor: Anchor,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
}
impl Sprite {
@ -79,9 +81,12 @@ impl From<Handle<Image>> for Sprite {
}
/// Controls how the image is altered when scaled.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Debug)]
pub enum ImageScaleMode {
#[derive(Default, Debug, Clone, Reflect, PartialEq)]
#[reflect(Debug)]
pub enum SpriteImageMode {
/// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set.
#[default]
Auto,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
@ -96,6 +101,17 @@ pub enum ImageScaleMode {
},
}
impl SpriteImageMode {
/// Returns true if this mode uses slices internally ([`SpriteImageMode::Sliced`] or [`SpriteImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. }
)
}
}
/// How a sprite is positioned relative to its [`Transform`].
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]

View file

@ -1,4 +1,4 @@
use crate::{ExtractedSprite, ImageScaleMode, Sprite, TextureAtlasLayout};
use crate::{ExtractedSprite, Sprite, SpriteImageMode, TextureAtlasLayout};
use super::TextureSlice;
use bevy_asset::{AssetEvent, Assets};
@ -8,7 +8,7 @@ use bevy_render::texture::Image;
use bevy_transform::prelude::*;
use bevy_utils::HashSet;
/// Component storing texture slices for sprite entities with a [`ImageScaleMode`]
/// Component storing texture slices for tiled or sliced sprite entities
///
/// This component is automatically inserted and updated
#[derive(Debug, Clone, Component)]
@ -69,24 +69,19 @@ impl ComputedTextureSlices {
}
}
/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices
/// Generates sprite slices for a [`Sprite`] with [`SpriteImageMode::Sliced`] or [`SpriteImageMode::Sliced`]. The slices
/// will be computed according to the `image_handle` dimensions or the sprite rect.
///
/// Returns `None` if the image asset is not loaded
///
/// # Arguments
///
/// * `sprite` - The sprite component, will be used to find the draw area size
/// * `scale_mode` - The image scaling component
/// * `image_handle` - The texture to slice or tile
/// * `sprite` - The sprite component with the image handle and image mode
/// * `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_sprite_slices(
sprite: &Sprite,
scale_mode: &ImageScaleMode,
images: &Assets<Image>,
atlas_layouts: &Assets<TextureAtlasLayout>,
) -> Option<ComputedTextureSlices> {
@ -111,9 +106,9 @@ fn compute_sprite_slices(
(size, rect)
}
};
let slices = match scale_mode {
ImageScaleMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
ImageScaleMode::Tiled {
let slices = match &sprite.image_mode {
SpriteImageMode::Sliced(slicer) => slicer.compute_slices(texture_rect, sprite.custom_size),
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
@ -125,18 +120,21 @@ fn compute_sprite_slices(
};
slice.tiled(*stretch_value, (*tile_x, *tile_y))
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for SpriteImageMode::Stretch")
}
};
Some(ComputedTextureSlices(slices))
}
/// System reacting to added or modified [`Image`] handles, and recompute sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
/// on sprite entities with a matching [`SpriteImageMode`]
pub(crate) fn compute_slices_on_asset_event(
mut commands: Commands,
mut events: EventReader<AssetEvent<Image>>,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
sprites: Query<(Entity, &ImageScaleMode, &Sprite)>,
sprites: Query<(Entity, &Sprite)>,
) {
// We store the asset ids of added/modified image assets
let added_handles: HashSet<_> = events
@ -150,29 +148,31 @@ pub(crate) fn compute_slices_on_asset_event(
return;
}
// We recompute the sprite slices for sprite entities with a matching asset handle id
for (entity, scale_mode, sprite) in &sprites {
for (entity, sprite) in &sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if !added_handles.contains(&sprite.image.id()) {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}
}
/// System reacting to changes on relevant sprite bundle components to compute the sprite slices
/// on matching sprite entities with a [`ImageScaleMode`] component
/// System reacting to changes on the [`Sprite`] component to compute the sprite slices
pub(crate) fn compute_slices_on_sprite_change(
mut commands: Commands,
images: Res<Assets<Image>>,
atlas_layouts: Res<Assets<TextureAtlasLayout>>,
changed_sprites: Query<
(Entity, &ImageScaleMode, &Sprite),
Or<(Changed<ImageScaleMode>, Changed<Sprite>)>,
>,
changed_sprites: Query<(Entity, &Sprite), Changed<Sprite>>,
) {
for (entity, scale_mode, sprite) in &changed_sprites {
if let Some(slices) = compute_sprite_slices(sprite, scale_mode, &images, &atlas_layouts) {
for (entity, sprite) in &changed_sprites {
if !sprite.image_mode.uses_slices() {
continue;
}
if let Some(slices) = compute_sprite_slices(sprite, &images, &atlas_layouts) {
commands.entity(entity).insert(slices);
}
}

View file

@ -10,7 +10,7 @@ use bevy_reflect::Reflect;
/// sections will be scaled or tiled.
///
/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures.
#[derive(Debug, Clone, Reflect)]
#[derive(Debug, Clone, Reflect, PartialEq)]
pub struct TextureSlicer {
/// The sprite borders, defining the 9 sections of the image
pub border: BorderRect,
@ -23,7 +23,7 @@ pub struct TextureSlicer {
}
/// Defines how a texture slice scales when resized
#[derive(Debug, Copy, Clone, Default, Reflect)]
#[derive(Debug, Copy, Clone, Default, Reflect, PartialEq)]
pub enum SliceScaleMode {
/// The slice will be stretched to fit the area
#[default]

View file

@ -211,6 +211,36 @@ impl GlobalTransform {
self.0.translation
}
/// Get the rotation as a [`Quat`].
///
/// The transform is expected to be non-degenerate and without shearing, or the output will be invalid.
///
/// # Warning
///
/// This is calculated using `to_scale_rotation_translation`, meaning that you
/// should probably use it directly if you also need translation or scale.
#[inline]
pub fn rotation(&self) -> Quat {
self.to_scale_rotation_translation().1
}
/// Get the scale as a [`Vec3`].
///
/// The transform is expected to be non-degenerate and without shearing, or the output will be invalid.
///
/// Some of the computations overlap with `to_scale_rotation_translation`, which means you should use
/// it instead if you also need rotation.
#[inline]
pub fn scale(&self) -> Vec3 {
//Formula based on glam's implementation https://github.com/bitshifter/glam-rs/blob/2e4443e70c709710dfb25958d866d29b11ed3e2b/src/f32/affine3a.rs#L290
let det = self.0.matrix3.determinant();
Vec3::new(
self.0.matrix3.x_axis.length() * det.signum(),
self.0.matrix3.y_axis.length(),
self.0.matrix3.z_axis.length(),
)
}
/// Get an upper bound of the radius from the given `extents`.
#[inline]
pub fn radius_vec3a(&self, extents: Vec3A) -> f32 {
@ -363,4 +393,18 @@ mod test {
t1_prime.compute_transform(),
);
}
#[test]
fn scale() {
let test_values = [-42.42, 0., 42.42];
for x in test_values {
for y in test_values {
for z in test_values {
let scale = Vec3::new(x, y, z);
let gt = GlobalTransform::from_scale(scale);
assert_eq!(gt.scale(), gt.to_scale_rotation_translation().0);
}
}
}
}
}

View file

@ -5,7 +5,7 @@ use crate::{
ComputedNode,
};
use bevy_a11y::{
accesskit::{NodeBuilder, Rect, Role},
accesskit::{Node, Rect, Role},
AccessibilityNode,
};
use bevy_app::{App, Plugin, PostUpdate};
@ -19,7 +19,7 @@ use bevy_ecs::{
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
use bevy_transform::prelude::GlobalTransform;
fn calc_name(
fn calc_label(
text_reader: &mut TextUiReader,
children: impl Iterator<Item = Entity>,
) -> Option<Box<str>> {
@ -70,18 +70,18 @@ fn button_changed(
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Button);
if let Some(name) = name {
accessible.set_name(name);
if let Some(name) = label {
accessible.set_label(name);
} else {
accessible.clear_name();
accessible.clear_label();
}
} else {
let mut node = NodeBuilder::new(Role::Button);
if let Some(name) = name {
node.set_name(name);
let mut node = Node::new(Role::Button);
if let Some(label) = label {
node.set_label(label);
}
commands
.entity(entity)
@ -97,18 +97,18 @@ fn image_changed(
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Image);
if let Some(name) = name {
accessible.set_name(name);
if let Some(label) = label {
accessible.set_label(label);
} else {
accessible.clear_name();
accessible.clear_label();
}
} else {
let mut node = NodeBuilder::new(Role::Image);
if let Some(name) = name {
node.set_name(name);
let mut node = Node::new(Role::Image);
if let Some(label) = label {
node.set_label(label);
}
commands
.entity(entity)
@ -127,18 +127,18 @@ fn label_changed(
.iter(entity)
.map(|(_, _, text, _, _)| text.into())
.collect::<Vec<String>>();
let name = Some(values.join(" ").into_boxed_str());
let label = Some(values.join(" ").into_boxed_str());
if let Some(mut accessible) = accessible {
accessible.set_role(Role::Label);
if let Some(name) = name {
accessible.set_name(name);
if let Some(label) = label {
accessible.set_label(label);
} else {
accessible.clear_name();
accessible.clear_label();
}
} else {
let mut node = NodeBuilder::new(Role::Label);
if let Some(name) = name {
node.set_name(name);
let mut node = Node::new(Role::Label);
if let Some(label) = label {
node.set_label(label);
}
commands
.entity(entity)

View file

@ -221,7 +221,7 @@ pub fn ui_layout_system(
|| node.is_changed()
|| content_size
.as_ref()
.map(|c| c.measure.is_some())
.map(|c| c.is_changed() || c.measure.is_some())
.unwrap_or(false)
{
let layout_context = LayoutContext::new(

View file

@ -62,7 +62,7 @@ pub mod prelude {
Interaction, MaterialNode, UiMaterialPlugin, UiScale,
},
// `bevy_sprite` re-exports for texture slicing
bevy_sprite::{BorderRect, ImageScaleMode, SliceScaleMode, TextureSlicer},
bevy_sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer},
};
}

View file

@ -57,12 +57,6 @@ pub struct NodeBundle {
}
/// A UI node that is an image
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Debug, Default)]
#[deprecated(
since = "0.15.0",
@ -110,12 +104,6 @@ pub struct ImageBundle {
}
/// A UI node that is a button
///
/// # Extra behaviors
///
/// You may add one or both of the following components to enable additional behaviors:
/// - [`ImageScaleMode`](bevy_sprite::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](bevy_sprite::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Clone, Debug)]
#[deprecated(
since = "0.15.0",

View file

@ -32,7 +32,7 @@ use bevy_render::{
use bevy_transform::prelude::GlobalTransform;
use bytemuck::{Pod, Zeroable};
use super::{QUAD_INDICES, QUAD_VERTEX_POSITIONS};
use super::{stack_z_offsets, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
pub const BOX_SHADOW_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(17717747047134343426);
@ -365,7 +365,7 @@ pub fn queue_shadows(
pipeline,
entity: (*entity, extracted_shadow.main_entity),
sort_key: (
FloatOrd(extracted_shadow.stack_index as f32 - 0.1),
FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW),
entity.index(),
),
batch_range: 0..0,

View file

@ -41,7 +41,7 @@ use bevy_render::{
ExtractSchedule, Render,
};
use bevy_sprite::TextureAtlasLayout;
use bevy_sprite::{BorderRect, ImageScaleMode, SpriteAssetEvents};
use bevy_sprite::{BorderRect, SpriteAssetEvents};
use crate::{Display, Node};
use bevy_text::{ComputedTextBlock, PositionedGlyph, TextColor, TextLayoutInfo};
@ -75,7 +75,7 @@ pub mod graph {
/// When this is _not_ possible, pick a suitably unique index unlikely to clash with other things (ex: `0.1826823` not `0.1`).
///
/// Offsets should be unique for a given node entity to avoid z fighting.
/// These should pretty much _always_ be larger than -1.0 and smaller than 1.0 to avoid clipping into nodes
/// These should pretty much _always_ be larger than -0.5 and smaller than 0.5 to avoid clipping into nodes
/// above / below the current node in the stack.
///
/// A z-index of 0.0 is the baseline, which is used as the primary "background color" of the node.
@ -83,7 +83,9 @@ pub mod graph {
/// Note that nodes "stack" on each other, so a negative offset on the node above could clip _into_
/// a positive offset on a node below.
pub mod stack_z_offsets {
pub const BACKGROUND_COLOR: f32 = 0.0;
pub const BOX_SHADOW: f32 = -0.1;
pub const TEXTURE_SLICE: f32 = 0.0;
pub const NODE: f32 = 0.0;
pub const MATERIAL: f32 = 0.18267;
}
@ -309,18 +311,15 @@ pub fn extract_uinode_images(
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
default_ui_camera: Extract<DefaultUiCamera>,
uinode_query: Extract<
Query<
(
Entity,
&ComputedNode,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
),
Without<ImageScaleMode>,
>,
Query<(
Entity,
&ComputedNode,
&GlobalTransform,
&ViewVisibility,
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
)>,
>,
mapping: Extract<Query<RenderEntity>>,
) {
@ -338,6 +337,7 @@ pub fn extract_uinode_images(
if !view_visibility.get()
|| image.color.is_fully_transparent()
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
|| image.image_mode.uses_slices()
{
continue;
}
@ -863,7 +863,7 @@ pub fn queue_uinodes(
pipeline,
entity: (*entity, extracted_uinode.main_entity),
sort_key: (
FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::BACKGROUND_COLOR),
FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE),
entity.index(),
),
// batch_range will be calculated in prepare_uinodes

View file

@ -24,7 +24,7 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderSet,
};
use bevy_sprite::{
ImageScaleMode, SliceScaleMode, SpriteAssetEvents, TextureAtlasLayout, TextureSlicer,
SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureAtlasLayout, TextureSlicer,
};
use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashMap;
@ -232,7 +232,7 @@ pub struct ExtractedUiTextureSlice {
pub clip: Option<Rect>,
pub camera_entity: Entity,
pub color: LinearRgba,
pub image_scale_mode: ImageScaleMode,
pub image_scale_mode: SpriteImageMode,
pub flip_x: bool,
pub flip_y: bool,
pub main_entity: MainEntity,
@ -257,14 +257,11 @@ pub fn extract_ui_texture_slices(
Option<&CalculatedClip>,
Option<&TargetCamera>,
&UiImage,
&ImageScaleMode,
)>,
>,
mapping: Extract<Query<RenderEntity>>,
) {
for (entity, uinode, transform, view_visibility, clip, camera, image, image_scale_mode) in
&slicers_query
{
for (entity, uinode, transform, view_visibility, clip, camera, image) in &slicers_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
@ -274,6 +271,22 @@ pub fn extract_ui_texture_slices(
continue;
};
let image_scale_mode = match image.image_mode.clone() {
widget::NodeImageMode::Sliced(texture_slicer) => {
SpriteImageMode::Sliced(texture_slicer)
}
widget::NodeImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
} => SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
},
_ => continue,
};
// Skip invisible images
if !view_visibility.get()
|| image.color.is_fully_transparent()
@ -312,7 +325,7 @@ pub fn extract_ui_texture_slices(
clip: clip.map(|clip| clip.clip),
image: image.image.id(),
camera_entity,
image_scale_mode: image_scale_mode.clone(),
image_scale_mode,
atlas_rect,
flip_x: image.flip_x,
flip_y: image.flip_y,
@ -352,7 +365,7 @@ pub fn queue_ui_slices(
pipeline,
entity: (*entity, extracted_slicer.main_entity),
sort_key: (
FloatOrd(extracted_slicer.stack_index as f32),
FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE),
entity.index(),
),
batch_range: 0..0,
@ -719,10 +732,10 @@ impl<P: PhaseItem> RenderCommand<P> for DrawSlicer {
fn compute_texture_slices(
image_size: Vec2,
target_size: Vec2,
image_scale_mode: &ImageScaleMode,
image_scale_mode: &SpriteImageMode,
) -> [[f32; 4]; 3] {
match image_scale_mode {
ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: border_rect,
center_scale_mode,
sides_scale_mode,
@ -775,7 +788,7 @@ fn compute_texture_slices(
],
]
}
ImageScaleMode::Tiled {
SpriteImageMode::Tiled {
tile_x,
tile_y,
stretch_value,
@ -784,6 +797,9 @@ fn compute_texture_slices(
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]]
}
SpriteImageMode::Auto => {
unreachable!("Slices should not be computed for ImageScaleMode::Stretch")
}
}
}

View file

@ -5,7 +5,7 @@ use bevy_ecs::prelude::*;
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::{Image, TRANSPARENT_IMAGE_HANDLE};
use bevy_sprite::{TextureAtlas, TextureAtlasLayout};
use bevy_sprite::{TextureAtlas, TextureAtlasLayout, TextureSlicer};
use bevy_window::{PrimaryWindow, Window};
use taffy::{MaybeMath, MaybeResolve};
@ -23,11 +23,11 @@ pub struct UiImage {
///
/// This defaults to a [`TRANSPARENT_IMAGE_HANDLE`], which points to a fully transparent 1x1 texture.
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the image
/// The (optional) texture atlas used to render the image.
pub texture_atlas: Option<TextureAtlas>,
/// Whether the image should be flipped along its x-axis
/// Whether the image should be flipped along its x-axis.
pub flip_x: bool,
/// Whether the image should be flipped along its y-axis
/// Whether the image should be flipped along its y-axis.
pub flip_y: bool,
/// An optional rectangle representing the region of the image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
@ -35,6 +35,8 @@ pub struct UiImage {
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space to allocate for the image.
pub image_mode: NodeImageMode,
}
impl Default for UiImage {
@ -56,6 +58,7 @@ impl Default for UiImage {
flip_x: false,
flip_y: false,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
}
@ -81,6 +84,7 @@ impl UiImage {
flip_y: false,
texture_atlas: None,
rect: None,
image_mode: NodeImageMode::Auto,
}
}
@ -119,6 +123,12 @@ impl UiImage {
self.rect = Some(rect);
self
}
#[must_use]
pub const fn with_mode(mut self, mode: NodeImageMode) -> Self {
self.image_mode = mode;
self
}
}
impl From<Handle<Image>> for UiImage {
@ -127,6 +137,39 @@ impl From<Handle<Image>> for UiImage {
}
}
/// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image
#[derive(Default, Debug, Clone, Reflect)]
pub enum NodeImageMode {
/// The image will be sized automatically by taking the size of the source image and applying any layout constraints.
#[default]
Auto,
/// The image will be resized to match the size of the node. The image's original size and aspect ratio will be ignored.
Stretch,
/// The texture will be cut in 9 slices, keeping the texture in proportions on resize
Sliced(TextureSlicer),
/// The texture will be repeated if stretched beyond `stretched_value`
Tiled {
/// Should the image repeat horizontally
tile_x: bool,
/// Should the image repeat vertically
tile_y: bool,
/// The texture will repeat when the ratio between the *drawing dimensions* of texture and the
/// *original texture size* are above this value.
stretch_value: f32,
},
}
impl NodeImageMode {
/// Returns true if this mode uses slices internally ([`NodeImageMode::Sliced`] or [`NodeImageMode::Tiled`])
#[inline]
pub fn uses_slices(&self) -> bool {
matches!(
self,
NodeImageMode::Sliced(..) | NodeImageMode::Tiled { .. }
)
}
}
/// The size of the image's texture
///
/// This component is updated automatically by [`update_image_content_size_system`]
@ -216,7 +259,7 @@ pub fn update_image_content_size_system(
textures: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut ContentSize, &UiImage, &mut UiImageSize), UpdateImageFilter>,
mut query: Query<(&mut ContentSize, Ref<UiImage>, &mut UiImageSize), UpdateImageFilter>,
) {
let combined_scale_factor = windows
.get_single()
@ -225,6 +268,14 @@ pub fn update_image_content_size_system(
* ui_scale.0;
for (mut content_size, image, mut image_size) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto) {
if image.is_changed() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
content_size.measure = None;
}
continue;
}
if let Some(size) = match &image.texture_atlas {
Some(atlas) => atlas.texture_rect(&atlases).map(|t| t.size()),
None => textures.get(&image.image).map(Image::size),

View file

@ -43,7 +43,7 @@ bevy_image = { path = "../bevy_image", version = "0.15.0-dev", optional = true }
# other
# feature rwh_06 refers to window_raw_handle@v0.6
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
accesskit_winit = { version = "0.22", default-features = false, features = [
accesskit_winit = { version = "0.23", default-features = false, features = [
"rwh_06",
] }
approx = { version = "0.5", default-features = false }
@ -51,7 +51,7 @@ cfg-if = "1.0"
raw-window-handle = "0.6"
serde = { version = "1.0", features = ["derive"], optional = true }
bytemuck = { version = "1.5", optional = true }
wgpu-types = { version = "22", optional = true }
wgpu-types = { version = "23", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }

View file

@ -6,8 +6,8 @@ use std::sync::Mutex;
use accesskit_winit::Adapter;
use bevy_a11y::{
accesskit::{
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeBuilder,
NodeId, Role, Tree, TreeUpdate,
ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, Node, NodeId, Role,
Tree, TreeUpdate,
},
AccessibilityNode, AccessibilityRequested, AccessibilitySystem,
ActionRequest as ActionRequestWrapper, Focus, ManageAccessibilityUpdates,
@ -64,9 +64,9 @@ impl AccessKitState {
}
fn build_root(&mut self) -> Node {
let mut builder = NodeBuilder::new(Role::Window);
builder.set_name(self.name.clone());
builder.build()
let mut node = Node::new(Role::Window);
node.set_label(self.name.clone());
node
}
fn build_initial_tree(&mut self) -> TreeUpdate {
@ -227,16 +227,14 @@ fn update_adapter(
queue_node_for_update(entity, parent, &node_entities, &mut window_children);
add_children_nodes(children, &node_entities, &mut node);
let node_id = NodeId(entity.to_bits());
let node = node.build();
to_update.push((node_id, node));
}
let mut window_node = NodeBuilder::new(Role::Window);
let mut window_node = Node::new(Role::Window);
if primary_window.focused {
let title = primary_window.title.clone();
window_node.set_name(title.into_boxed_str());
window_node.set_label(title.into_boxed_str());
}
window_node.set_children(window_children);
let window_node = window_node.build();
let node_id = NodeId(primary_window_id.to_bits());
let window_update = (node_id, window_node);
to_update.insert(0, window_update);
@ -268,7 +266,7 @@ fn queue_node_for_update(
fn add_children_nodes(
children: Option<&Children>,
node_entities: &Query<Entity, With<AccessibilityNode>>,
node: &mut NodeBuilder,
node: &mut Node,
) {
let Some(children) = children else {
return;

View file

@ -6,7 +6,6 @@ use bevy_input::{
ButtonState,
};
use bevy_math::{CompassOctant, Vec2};
#[cfg(feature = "custom_cursor")]
use bevy_window::SystemCursorIcon;
use bevy_window::{EnabledButtons, WindowLevel, WindowTheme};
use winit::keyboard::{Key, NamedKey, NativeKey};
@ -630,7 +629,6 @@ pub fn convert_native_key(native_key: &NativeKey) -> bevy_input::keyboard::Nativ
}
}
#[cfg(feature = "custom_cursor")]
/// Converts a [`SystemCursorIcon`] to a [`winit::window::CursorIcon`].
pub fn convert_system_cursor_icon(cursor_icon: SystemCursorIcon) -> winit::window::CursorIcon {
match cursor_icon {

View file

@ -2,11 +2,18 @@
use crate::{
converters::convert_system_cursor_icon,
state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor},
state::{CursorSource, PendingCursor},
};
#[cfg(feature = "custom_cursor")]
use crate::{
state::{CustomCursorCache, CustomCursorCacheKey},
WinitCustomCursor,
};
use bevy_app::{App, Last, Plugin};
#[cfg(feature = "custom_cursor")]
use bevy_asset::{Assets, Handle};
#[cfg(feature = "custom_cursor")]
use bevy_ecs::system::Res;
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
@ -14,21 +21,27 @@ use bevy_ecs::{
observer::Trigger,
query::With,
reflect::ReflectComponent,
system::{Commands, Local, Query, Res},
system::{Commands, Local, Query},
world::{OnRemove, Ref},
};
#[cfg(feature = "custom_cursor")]
use bevy_image::Image;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_utils::{tracing::warn, HashSet};
#[cfg(feature = "custom_cursor")]
use bevy_utils::tracing::warn;
use bevy_utils::HashSet;
use bevy_window::{SystemCursorIcon, Window};
#[cfg(feature = "custom_cursor")]
use wgpu_types::TextureFormat;
pub(crate) struct CursorPlugin;
impl Plugin for CursorPlugin {
fn build(&self, app: &mut App) {
#[cfg(feature = "custom_cursor")]
app.init_resource::<CustomCursorCache>();
app.register_type::<CursorIcon>()
.init_resource::<CustomCursorCache>()
.add_systems(Last, update_cursors);
app.add_observer(on_remove_cursor_icon);
@ -39,6 +52,7 @@ impl Plugin for CursorPlugin {
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
#[reflect(Component, Debug, Default, PartialEq)]
pub enum CursorIcon {
#[cfg(feature = "custom_cursor")]
/// Custom cursor image.
Custom(CustomCursor),
/// System provided cursor icon.
@ -57,12 +71,14 @@ impl From<SystemCursorIcon> for CursorIcon {
}
}
#[cfg(feature = "custom_cursor")]
impl From<CustomCursor> for CursorIcon {
fn from(cursor: CustomCursor) -> Self {
CursorIcon::Custom(cursor)
}
}
#[cfg(feature = "custom_cursor")]
/// Custom cursor image data.
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)]
pub enum CustomCursor {
@ -90,8 +106,8 @@ pub enum CustomCursor {
fn update_cursors(
mut commands: Commands,
windows: Query<(Entity, Ref<CursorIcon>), With<Window>>,
cursor_cache: Res<CustomCursorCache>,
images: Res<Assets<Image>>,
#[cfg(feature = "custom_cursor")] cursor_cache: Res<CustomCursorCache>,
#[cfg(feature = "custom_cursor")] images: Res<Assets<Image>>,
mut queue: Local<HashSet<Entity>>,
) {
for (entity, cursor) in windows.iter() {
@ -100,6 +116,7 @@ fn update_cursors(
}
let cursor_source = match cursor.as_ref() {
#[cfg(feature = "custom_cursor")]
CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => {
let cache_key = CustomCursorCacheKey::Asset(handle.id());
@ -137,7 +154,11 @@ fn update_cursors(
CursorSource::Custom((cache_key, source))
}
}
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
#[cfg(all(
feature = "custom_cursor",
target_family = "wasm",
target_os = "unknown"
))]
CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => {
let cache_key = CustomCursorCacheKey::Url(url.clone());
@ -170,6 +191,7 @@ fn on_remove_cursor_icon(trigger: Trigger<OnRemove, CursorIcon>, mut commands: C
))));
}
#[cfg(feature = "custom_cursor")]
/// Returns the image data as a `Vec<u8>`.
/// Only supports rgba8 and rgba32float formats.
fn image_to_rgba_pixels(image: &Image) -> Option<Vec<u8>> {

View file

@ -44,7 +44,6 @@ use crate::{
pub mod accessibility;
mod converters;
#[cfg(feature = "custom_cursor")]
pub mod cursor;
mod state;
mod system;
@ -136,7 +135,6 @@ impl<T: Event> Plugin for WinitPlugin<T> {
);
app.add_plugins(AccessKitPlugin);
#[cfg(feature = "custom_cursor")]
app.add_plugins(cursor::CursorPlugin);
let event_loop = event_loop_builder

View file

@ -158,19 +158,19 @@ pub enum CustomCursorCacheKey {
#[derive(Debug, Clone, Default, Resource)]
pub struct CustomCursorCache(pub HashMap<CustomCursorCacheKey, winit::window::CustomCursor>);
#[cfg(feature = "custom_cursor")]
/// A source for a cursor. Consumed by the winit event loop.
#[derive(Debug)]
pub enum CursorSource {
#[cfg(feature = "custom_cursor")]
/// A custom cursor was identified to be cached, no reason to recreate it.
CustomCached(CustomCursorCacheKey),
#[cfg(feature = "custom_cursor")]
/// A custom cursor was not cached, so it needs to be created by the winit event loop.
Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)),
/// A system cursor was requested.
System(winit::window::CursorIcon),
}
#[cfg(feature = "custom_cursor")]
/// Component that indicates what cursor should be used for a window. Inserted
/// automatically after changing `CursorIcon` and consumed by the winit event
/// loop.
@ -560,6 +560,8 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
self.run_app_update();
#[cfg(feature = "custom_cursor")]
self.update_cursors(event_loop);
#[cfg(not(feature = "custom_cursor"))]
self.update_cursors();
self.ran_update_since_last_redraw = true;
} else {
self.redraw_requested = true;
@ -787,15 +789,23 @@ impl<T: Event> WinitAppRunnerState<T> {
.send_batch(buffered_events);
}
#[cfg(feature = "custom_cursor")]
fn update_cursors(&mut self, event_loop: &ActiveEventLoop) {
fn update_cursors(&mut self, #[cfg(feature = "custom_cursor")] event_loop: &ActiveEventLoop) {
#[cfg(feature = "custom_cursor")]
let mut windows_state: SystemState<(
NonSendMut<WinitWindows>,
ResMut<CustomCursorCache>,
Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
)> = SystemState::new(self.world_mut());
#[cfg(feature = "custom_cursor")]
let (winit_windows, mut cursor_cache, mut windows) =
windows_state.get_mut(self.world_mut());
#[cfg(not(feature = "custom_cursor"))]
let mut windows_state: SystemState<(
NonSendMut<WinitWindows>,
Query<(Entity, &mut PendingCursor), Changed<PendingCursor>>,
)> = SystemState::new(self.world_mut());
#[cfg(not(feature = "custom_cursor"))]
let (winit_windows, mut windows) = windows_state.get_mut(self.world_mut());
for (entity, mut pending_cursor) in windows.iter_mut() {
let Some(winit_window) = winit_windows.get_window(entity) else {
@ -806,6 +816,7 @@ impl<T: Event> WinitAppRunnerState<T> {
};
let final_cursor: winit::window::Cursor = match pending_cursor {
#[cfg(feature = "custom_cursor")]
CursorSource::CustomCached(cache_key) => {
let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else {
error!("Cursor should have been cached, but was not found");
@ -813,6 +824,7 @@ impl<T: Event> WinitAppRunnerState<T> {
};
cached_cursor.clone().into()
}
#[cfg(feature = "custom_cursor")]
CursorSource::Custom((cache_key, cursor)) => {
let custom_cursor = event_loop.create_custom_cursor(cursor);
cursor_cache.0.insert(cache_key, custom_cursor.clone());

View file

@ -12,15 +12,11 @@ fn main() {
fn draw_cursor(
camera_query: Single<(&Camera, &GlobalTransform)>,
windows: Query<&Window>,
window: Single<&Window>,
mut gizmos: Gizmos,
) {
let (camera, camera_transform) = *camera_query;
let Ok(window) = windows.get_single() else {
return;
};
let Some(cursor_position) = window.cursor_position() else {
return;
};

View file

@ -19,55 +19,65 @@ fn spawn_sprites(
) {
let cases = [
// Reference sprite
("Original", style.clone(), Vec2::splat(100.0), None),
(
"Original",
style.clone(),
Vec2::splat(100.0),
SpriteImageMode::Auto,
),
// Scaled regular sprite
("Stretched", style.clone(), Vec2::new(100.0, 200.0), None),
(
"Stretched",
style.clone(),
Vec2::new(100.0, 200.0),
SpriteImageMode::Auto,
),
// Stretched Scaled sliced sprite
(
"With Slicing",
style.clone(),
Vec2::new(100.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Stretch,
..default()
})),
}),
),
// Scaled sliced sprite
(
"With Tiling",
style.clone(),
Vec2::new(100.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
..default()
})),
}),
),
// Scaled sliced sprite horizontally
(
"With Tiling",
style.clone(),
Vec2::new(300.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 },
..default()
})),
}),
),
// Scaled sliced sprite horizontally with max scale
(
"With Corners Constrained",
style,
Vec2::new(300.0, 200.0),
Some(ImageScaleMode::Sliced(TextureSlicer {
SpriteImageMode::Sliced(TextureSlicer {
border: BorderRect::square(slice_border),
center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 },
sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 },
max_corner_scale: 0.2,
})),
}),
),
];
@ -77,13 +87,11 @@ fn spawn_sprites(
Sprite {
image: texture_handle.clone(),
custom_size: Some(size),
image_mode: scale_mode,
..default()
},
Transform::from_translation(position),
));
if let Some(scale_mode) = scale_mode {
cmd.insert(scale_mode);
}
cmd.with_children(|builder| {
builder.spawn((
Text2d::new(label),

View file

@ -26,14 +26,15 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
current: 128.0,
speed: 50.0,
});
commands.spawn((
Sprite::from_image(asset_server.load("branding/icon.png")),
ImageScaleMode::Tiled {
commands.spawn(Sprite {
image: asset_server.load("branding/icon.png"),
image_mode: SpriteImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 0.5, // The image will tile every 128px
},
));
..default()
});
}
fn animate(mut sprites: Query<&mut Sprite>, mut state: ResMut<AnimationState>, time: Res<Time>) {

View file

@ -253,13 +253,9 @@ fn move_camera_view(
// To ensure viewports remain the same at any window size
fn resize_viewports(
windows: Query<&Window, With<bevy::window::PrimaryWindow>>,
window: Single<&Window, With<bevy::window::PrimaryWindow>>,
mut viewports: Query<(&mut Camera, &ExampleViewports)>,
) {
let Ok(window) = windows.get_single() else {
return;
};
let window_size = window.physical_size();
let small_height = window_size.y / 5;

View file

@ -17,7 +17,7 @@ use camera_controller::{CameraController, CameraControllerPlugin};
use std::{f32::consts::PI, path::Path, process::ExitCode};
const ASSET_URL: &str =
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/8483db58832542383820c3f44e4730e566910be7/bunny.meshlet_mesh";
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/defbd9b32072624d40d57de7d345c66a9edf5d0b/bunny.meshlet_mesh";
fn main() -> ExitCode {
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {

View file

@ -127,21 +127,17 @@ fn setup(
fn environment_map_load_finish(
mut commands: Commands,
asset_server: Res<AssetServer>,
environment_maps: Query<&EnvironmentMapLight>,
label_query: Query<Entity, With<EnvironmentMapLabel>>,
environment_map: Single<&EnvironmentMapLight>,
label_entity: Single<Entity, With<EnvironmentMapLabel>>,
) {
if let Ok(environment_map) = environment_maps.get_single() {
if asset_server
.load_state(&environment_map.diffuse_map)
if asset_server
.load_state(&environment_map.diffuse_map)
.is_loaded()
&& asset_server
.load_state(&environment_map.specular_map)
.is_loaded()
&& asset_server
.load_state(&environment_map.specular_map)
.is_loaded()
{
if let Ok(label_entity) = label_query.get_single() {
commands.entity(label_entity).despawn();
}
}
{
commands.entity(*label_entity).despawn();
}
}

View file

@ -149,13 +149,11 @@ fn spawn_text(mut commands: Commands) {
fn alter_handle(
asset_server: Res<AssetServer>,
mut right_shape: Query<(&mut Mesh3d, &mut Shape), Without<Left>>,
right_shape: Single<(&mut Mesh3d, &mut Shape), Without<Left>>,
) {
// Mesh handles, like other parts of the ECS, can be queried as mutable and modified at
// runtime. We only spawned one shape without the `Left` marker component.
let Ok((mut mesh, mut shape)) = right_shape.get_single_mut() else {
return;
};
let (mut mesh, mut shape) = right_shape.into_inner();
// Switch to a new Shape variant
shape.set_next_variant();
@ -174,17 +172,11 @@ fn alter_handle(
fn alter_mesh(
mut is_mesh_scaled: Local<bool>,
left_shape: Query<&Mesh3d, With<Left>>,
left_shape: Single<&Mesh3d, With<Left>>,
mut meshes: ResMut<Assets<Mesh>>,
) {
// It's convenient to retrieve the asset handle stored with the shape on the left. However,
// we could just as easily have retained this in a resource or a dedicated component.
let Ok(handle) = left_shape.get_single() else {
return;
};
// Obtain a mutable reference to the Mesh asset.
let Some(mesh) = meshes.get_mut(handle) else {
let Some(mesh) = meshes.get_mut(*left_shape) else {
return;
};

View file

@ -107,13 +107,11 @@ fn spawn_text(mut commands: Commands) {
fn alter_handle(
asset_server: Res<AssetServer>,
mut right_bird: Query<(&mut Bird, &mut Sprite), Without<Left>>,
right_bird: Single<(&mut Bird, &mut Sprite), Without<Left>>,
) {
// Image handles, like other parts of the ECS, can be queried as mutable and modified at
// runtime. We only spawned one bird without the `Left` marker component.
let Ok((mut bird, mut sprite)) = right_bird.get_single_mut() else {
return;
};
let (mut bird, mut sprite) = right_bird.into_inner();
// Switch to a new Bird variant
bird.set_next_variant();
@ -124,15 +122,9 @@ fn alter_handle(
sprite.image = asset_server.load(bird.get_texture_path());
}
fn alter_asset(mut images: ResMut<Assets<Image>>, left_bird: Query<&Sprite, With<Left>>) {
// It's convenient to retrieve the asset handle stored with the bird on the left. However,
// we could just as easily have retained this in a resource or a dedicated component.
let Ok(sprite) = left_bird.get_single() else {
return;
};
fn alter_asset(mut images: ResMut<Assets<Image>>, left_bird: Single<&Sprite, With<Left>>) {
// Obtain a mutable reference to the Image asset.
let Some(image) = images.get_mut(&sprite.image) else {
let Some(image) = images.get_mut(&left_bird.image) else {
return;
};

View file

@ -20,32 +20,20 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
#[derive(Component)]
struct MyMusic;
fn update_speed(music_controller: Query<&AudioSink, With<MyMusic>>, time: Res<Time>) {
if let Ok(sink) = music_controller.get_single() {
sink.set_speed((ops::sin(time.elapsed_secs() / 5.0) + 1.0).max(0.1));
}
fn update_speed(sink: Single<&AudioSink, With<MyMusic>>, time: Res<Time>) {
sink.set_speed((ops::sin(time.elapsed_secs() / 5.0) + 1.0).max(0.1));
}
fn pause(
keyboard_input: Res<ButtonInput<KeyCode>>,
music_controller: Query<&AudioSink, With<MyMusic>>,
) {
fn pause(keyboard_input: Res<ButtonInput<KeyCode>>, sink: Single<&AudioSink, With<MyMusic>>) {
if keyboard_input.just_pressed(KeyCode::Space) {
if let Ok(sink) = music_controller.get_single() {
sink.toggle();
}
sink.toggle();
}
}
fn volume(
keyboard_input: Res<ButtonInput<KeyCode>>,
music_controller: Query<&AudioSink, With<MyMusic>>,
) {
if let Ok(sink) = music_controller.get_single() {
if keyboard_input.just_pressed(KeyCode::Equal) {
sink.set_volume(sink.volume() + 0.1);
} else if keyboard_input.just_pressed(KeyCode::Minus) {
sink.set_volume(sink.volume() - 0.1);
}
fn volume(keyboard_input: Res<ButtonInput<KeyCode>>, sink: Single<&AudioSink, With<MyMusic>>) {
if keyboard_input.just_pressed(KeyCode::Equal) {
sink.set_volume(sink.volume() + 0.1);
} else if keyboard_input.just_pressed(KeyCode::Minus) {
sink.set_volume(sink.volume() - 0.1);
}
}

View file

@ -73,18 +73,10 @@ fn setup_camera(mut commands: Commands) {
/// Update the camera position by tracking the player.
fn update_camera(
mut camera: Query<&mut Transform, (With<Camera2d>, Without<Player>)>,
player: Query<&Transform, (With<Player>, Without<Camera2d>)>,
mut camera: Single<&mut Transform, (With<Camera2d>, Without<Player>)>,
player: Single<&Transform, (With<Player>, Without<Camera2d>)>,
time: Res<Time>,
) {
let Ok(mut camera) = camera.get_single_mut() else {
return;
};
let Ok(player) = player.get_single() else {
return;
};
let Vec3 { x, y, .. } = player.translation;
let direction = Vec3::new(x, y, camera.translation.z);
@ -101,14 +93,10 @@ fn update_camera(
///
/// A more robust solution for player movement can be found in `examples/movement/physics_in_fixed_timestep.rs`.
fn move_player(
mut player: Query<&mut Transform, With<Player>>,
mut player: Single<&mut Transform, With<Player>>,
time: Res<Time>,
kb_input: Res<ButtonInput<KeyCode>>,
) {
let Ok(mut player) = player.get_single_mut() else {
return;
};
let mut direction = Vec2::ZERO;
if kb_input.pressed(KeyCode::KeyW) {

View file

@ -207,11 +207,10 @@ fn spawn_text(mut commands: Commands) {
fn move_player(
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
mut player: Query<(&mut Transform, &CameraSensitivity), With<Player>>,
player: Single<(&mut Transform, &CameraSensitivity), With<Player>>,
) {
let Ok((mut transform, camera_sensitivity)) = player.get_single_mut() else {
return;
};
let (mut transform, camera_sensitivity) = player.into_inner();
let delta = accumulated_mouse_motion.delta;
if delta != Vec2::ZERO {
@ -242,12 +241,9 @@ fn move_player(
fn change_fov(
input: Res<ButtonInput<KeyCode>>,
mut world_model_projection: Query<&mut Projection, With<WorldModelCamera>>,
mut world_model_projection: Single<&mut Projection, With<WorldModelCamera>>,
) {
let Ok(mut projection) = world_model_projection.get_single_mut() else {
return;
};
let Projection::Perspective(ref mut perspective) = projection.as_mut() else {
let Projection::Perspective(ref mut perspective) = world_model_projection.as_mut() else {
unreachable!(
"The `Projection` component was explicitly built with `Projection::Perspective`"
);

View file

@ -109,118 +109,117 @@ fn run_camera_controller(
key_input: Res<ButtonInput<KeyCode>>,
mut toggle_cursor_grab: Local<bool>,
mut mouse_cursor_grab: Local<bool>,
mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>,
query: Single<(&mut Transform, &mut CameraController), With<Camera>>,
) {
let dt = time.delta_secs();
if let Ok((mut transform, mut controller)) = query.get_single_mut() {
if !controller.initialized {
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
controller.yaw = yaw;
controller.pitch = pitch;
controller.initialized = true;
info!("{}", *controller);
}
if !controller.enabled {
return;
}
let (mut transform, mut controller) = query.into_inner();
let mut scroll = 0.0;
if !controller.initialized {
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
controller.yaw = yaw;
controller.pitch = pitch;
controller.initialized = true;
info!("{}", *controller);
}
if !controller.enabled {
return;
}
let amount = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => accumulated_mouse_scroll.delta.y / 16.0,
};
scroll += amount;
controller.walk_speed += scroll * controller.scroll_factor * controller.walk_speed;
controller.run_speed = controller.walk_speed * 3.0;
let mut scroll = 0.0;
// Handle key input
let mut axis_input = Vec3::ZERO;
if key_input.pressed(controller.key_forward) {
axis_input.z += 1.0;
}
if key_input.pressed(controller.key_back) {
axis_input.z -= 1.0;
}
if key_input.pressed(controller.key_right) {
axis_input.x += 1.0;
}
if key_input.pressed(controller.key_left) {
axis_input.x -= 1.0;
}
if key_input.pressed(controller.key_up) {
axis_input.y += 1.0;
}
if key_input.pressed(controller.key_down) {
axis_input.y -= 1.0;
}
let amount = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => accumulated_mouse_scroll.delta.y / 16.0,
};
scroll += amount;
controller.walk_speed += scroll * controller.scroll_factor * controller.walk_speed;
controller.run_speed = controller.walk_speed * 3.0;
let mut cursor_grab_change = false;
if key_input.just_pressed(controller.keyboard_key_toggle_cursor_grab) {
*toggle_cursor_grab = !*toggle_cursor_grab;
cursor_grab_change = true;
}
if mouse_button_input.just_pressed(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = true;
cursor_grab_change = true;
}
if mouse_button_input.just_released(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = false;
cursor_grab_change = true;
}
let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
// Handle key input
let mut axis_input = Vec3::ZERO;
if key_input.pressed(controller.key_forward) {
axis_input.z += 1.0;
}
if key_input.pressed(controller.key_back) {
axis_input.z -= 1.0;
}
if key_input.pressed(controller.key_right) {
axis_input.x += 1.0;
}
if key_input.pressed(controller.key_left) {
axis_input.x -= 1.0;
}
if key_input.pressed(controller.key_up) {
axis_input.y += 1.0;
}
if key_input.pressed(controller.key_down) {
axis_input.y -= 1.0;
}
// Apply movement update
if axis_input != Vec3::ZERO {
let max_speed = if key_input.pressed(controller.key_run) {
controller.run_speed
} else {
controller.walk_speed
};
controller.velocity = axis_input.normalize() * max_speed;
let mut cursor_grab_change = false;
if key_input.just_pressed(controller.keyboard_key_toggle_cursor_grab) {
*toggle_cursor_grab = !*toggle_cursor_grab;
cursor_grab_change = true;
}
if mouse_button_input.just_pressed(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = true;
cursor_grab_change = true;
}
if mouse_button_input.just_released(controller.mouse_key_cursor_grab) {
*mouse_cursor_grab = false;
cursor_grab_change = true;
}
let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
// Apply movement update
if axis_input != Vec3::ZERO {
let max_speed = if key_input.pressed(controller.key_run) {
controller.run_speed
} else {
let friction = controller.friction.clamp(0.0, 1.0);
controller.velocity *= 1.0 - friction;
if controller.velocity.length_squared() < 1e-6 {
controller.velocity = Vec3::ZERO;
}
}
let forward = *transform.forward();
let right = *transform.right();
transform.translation += controller.velocity.x * dt * right
+ controller.velocity.y * dt * Vec3::Y
+ controller.velocity.z * dt * forward;
// Handle cursor grab
if cursor_grab_change {
if cursor_grab {
for mut window in &mut windows {
if !window.focused {
continue;
}
window.cursor_options.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false;
}
} else {
for mut window in &mut windows {
window.cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
}
}
}
// Handle mouse input
if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
// Apply look update
controller.pitch = (controller.pitch
- accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * controller.sensitivity)
.clamp(-PI / 2., PI / 2.);
controller.yaw -=
accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * controller.sensitivity;
transform.rotation =
Quat::from_euler(EulerRot::ZYX, 0.0, controller.yaw, controller.pitch);
controller.walk_speed
};
controller.velocity = axis_input.normalize() * max_speed;
} else {
let friction = controller.friction.clamp(0.0, 1.0);
controller.velocity *= 1.0 - friction;
if controller.velocity.length_squared() < 1e-6 {
controller.velocity = Vec3::ZERO;
}
}
let forward = *transform.forward();
let right = *transform.right();
transform.translation += controller.velocity.x * dt * right
+ controller.velocity.y * dt * Vec3::Y
+ controller.velocity.z * dt * forward;
// Handle cursor grab
if cursor_grab_change {
if cursor_grab {
for mut window in &mut windows {
if !window.focused {
continue;
}
window.cursor_options.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false;
}
} else {
for mut window in &mut windows {
window.cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
}
}
}
// Handle mouse input
if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
// Apply look update
controller.pitch = (controller.pitch
- accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * controller.sensitivity)
.clamp(-PI / 2., PI / 2.);
controller.yaw -=
accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * controller.sensitivity;
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, controller.yaw, controller.pitch);
}
}

View file

@ -11,18 +11,18 @@ fn main() {
fn gamepad_system(gamepads: Query<(Entity, &Gamepad)>) {
for (entity, gamepad) in &gamepads {
if gamepad.just_pressed(GamepadButton::South) {
if gamepad.digital.just_pressed(GamepadButton::South) {
info!("{:?} just pressed South", entity);
} else if gamepad.just_released(GamepadButton::South) {
} else if gamepad.digital.just_released(GamepadButton::South) {
info!("{:?} just released South", entity);
}
let right_trigger = gamepad.get(GamepadButton::RightTrigger2).unwrap();
let right_trigger = gamepad.analog.get(GamepadButton::RightTrigger2).unwrap();
if right_trigger.abs() > 0.01 {
info!("{:?} RightTrigger2 value is {}", entity, right_trigger);
}
let left_stick_x = gamepad.get(GamepadAxis::LeftStickX).unwrap();
let left_stick_x = gamepad.analog.get(GamepadAxis::LeftStickX).unwrap();
if left_stick_x.abs() > 0.01 {
info!("{:?} LeftStickX value is {}", entity, left_stick_x);
}

View file

@ -19,7 +19,7 @@ fn gamepad_system(
mut rumble_requests: EventWriter<GamepadRumbleRequest>,
) {
for (entity, gamepad) in &gamepads {
if gamepad.just_pressed(GamepadButton::North) {
if gamepad.digital.just_pressed(GamepadButton::North) {
info!(
"North face button: strong (low-frequency) with low intensity for rumble for 5 seconds. Press multiple times to increase intensity."
);
@ -30,7 +30,7 @@ fn gamepad_system(
});
}
if gamepad.just_pressed(GamepadButton::East) {
if gamepad.digital.just_pressed(GamepadButton::East) {
info!("East face button: maximum rumble on both motors for 5 seconds");
rumble_requests.send(GamepadRumbleRequest::Add {
gamepad: entity,
@ -39,7 +39,7 @@ fn gamepad_system(
});
}
if gamepad.just_pressed(GamepadButton::South) {
if gamepad.digital.just_pressed(GamepadButton::South) {
info!("South face button: low-intensity rumble on the weak motor for 0.5 seconds");
rumble_requests.send(GamepadRumbleRequest::Add {
gamepad: entity,
@ -48,7 +48,7 @@ fn gamepad_system(
});
}
if gamepad.just_pressed(GamepadButton::West) {
if gamepad.digital.just_pressed(GamepadButton::West) {
info!("West face button: custom rumble intensity for 5 second");
rumble_requests.send(GamepadRumbleRequest::Add {
gamepad: entity,
@ -62,7 +62,7 @@ fn gamepad_system(
});
}
if gamepad.just_pressed(GamepadButton::Start) {
if gamepad.digital.just_pressed(GamepadButton::Start) {
info!("Start button: Interrupt the current rumble");
rumble_requests.send(GamepadRumbleRequest::Stop { gamepad: entity });
}

View file

@ -308,7 +308,7 @@ fn handle_mouse_press(
mouse_position: Res<MousePosition>,
mut edit_move: ResMut<MouseEditMove>,
mut control_points: ResMut<ControlPoints>,
camera: Query<(&Camera, &GlobalTransform)>,
camera: Single<(&Camera, &GlobalTransform)>,
) {
let Some(mouse_pos) = mouse_position.0 else {
return;
@ -336,9 +336,7 @@ fn handle_mouse_press(
continue;
};
let Ok((camera, camera_transform)) = camera.get_single() else {
continue;
};
let (camera, camera_transform) = *camera;
// Convert the starting point and end point (current mouse pos) into world coords:
let Ok(point) = camera.viewport_to_world_2d(camera_transform, start) else {
@ -365,7 +363,7 @@ fn draw_edit_move(
edit_move: Res<MouseEditMove>,
mouse_position: Res<MousePosition>,
mut gizmos: Gizmos,
camera: Query<(&Camera, &GlobalTransform)>,
camera: Single<(&Camera, &GlobalTransform)>,
) {
let Some(start) = edit_move.start else {
return;
@ -373,9 +371,8 @@ fn draw_edit_move(
let Some(mouse_pos) = mouse_position.0 else {
return;
};
let Ok((camera, camera_transform)) = camera.get_single() else {
return;
};
let (camera, camera_transform) = *camera;
// Resources store data in viewport coordinates, so we need to convert to world coordinates
// to display them:

View file

@ -14,6 +14,7 @@
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = A39528EB2CCB182F5328223A /* libc++.tbd */; };
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57CD6305253C7A940098CD4A /* AudioToolbox.framework */; };
57CD630E253C80EC0098CD4A /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 57CD630A253C7F5F0098CD4A /* assets */; };
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -31,6 +32,7 @@
55EAC02897847195D2F44C15 /* bevy_mobile_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bevy_mobile_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
57CD6305253C7A940098CD4A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
57CD630A253C7F5F0098CD4A /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../assets; sourceTree = "<group>"; };
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
8EE7F1E3B0303533925D7E33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
96A1E5B62F48B379829E8A0D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
9F1B41978FA53999AA836D0F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
@ -44,6 +46,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */,
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */,
134866208A035F8615C99114 /* Metal.framework in Frameworks */,
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */,
@ -86,6 +89,7 @@
EB028409C2D0655412DA6E44 /* Frameworks */ = {
isa = PBXGroup;
children = (
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */,
57CD6305253C7A940098CD4A /* AudioToolbox.framework */,
A39528EB2CCB182F5328223A /* libc++.tbd */,
96A1E5B62F48B379829E8A0D /* Metal.framework */,
@ -294,7 +298,7 @@
"-lbevy_mobile_example",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.bevyengine.example";
PRODUCT_BUNDLE_IDENTIFIER = org.bevyengine.example;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -402,7 +406,7 @@
"-lbevy_mobile_example",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.bevyengine.example";
PRODUCT_BUNDLE_IDENTIFIER = org.bevyengine.example;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};

View file

@ -171,12 +171,8 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
// This is handled by the OS on iOS, but not on Android.
fn handle_lifetime(
mut lifecycle_events: EventReader<AppLifecycle>,
music_controller: Query<&AudioSink>,
music_controller: Single<&AudioSink>,
) {
let Ok(music_controller) = music_controller.get_single() else {
return;
};
for event in lifecycle_events.read() {
match event {
AppLifecycle::Idle | AppLifecycle::WillSuspend | AppLifecycle::WillResume => {}

View file

@ -393,12 +393,12 @@ fn update_buttons(
materials: Res<ButtonMaterials>,
mut query: Query<(&mut MeshMaterial2d<ColorMaterial>, &ReactTo)>,
) {
for buttons in &gamepads {
for gamepad in &gamepads {
for (mut handle, react_to) in query.iter_mut() {
if buttons.just_pressed(**react_to) {
if gamepad.digital.just_pressed(**react_to) {
*handle = materials.active.clone();
}
if buttons.just_released(**react_to) {
if gamepad.digital.just_released(**react_to) {
*handle = materials.normal.clone();
}
}
@ -447,7 +447,7 @@ fn update_axes(
fn update_connected(
mut connected: EventReader<GamepadConnectionEvent>,
gamepads: Query<(Entity, &Gamepad)>,
gamepads: Query<(Entity, &Name), With<Gamepad>>,
text: Single<Entity, With<ConnectedGamepadsText>>,
mut writer: TextUiWriter,
) {
@ -458,7 +458,7 @@ fn update_connected(
let formatted = gamepads
.iter()
.map(|(entity, gamepad)| format!("{} - {}", entity, gamepad.name()))
.map(|(entity, name)| format!("{} - {}", entity, name))
.collect::<Vec<_>>()
.join("\n");

View file

@ -2,7 +2,7 @@
use bevy::{
a11y::{
accesskit::{NodeBuilder, Role},
accesskit::{Node as Accessible, Role},
AccessibilityNode,
},
input::mouse::{MouseScrollUnit, MouseWheel},
@ -79,7 +79,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
Label,
AccessibilityNode(NodeBuilder::new(Role::ListItem)),
AccessibilityNode(Accessible::new(Role::ListItem)),
))
.insert(Node {
min_width: Val::Px(200.),
@ -167,7 +167,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
Label,
AccessibilityNode(NodeBuilder::new(
AccessibilityNode(Accessible::new(
Role::ListItem,
)),
))
@ -234,7 +234,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
Label,
AccessibilityNode(NodeBuilder::new(
AccessibilityNode(Accessible::new(
Role::ListItem,
)),
))
@ -310,7 +310,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
Label,
AccessibilityNode(NodeBuilder::new(
AccessibilityNode(Accessible::new(
Role::ListItem,
)),
))

View file

@ -1,14 +1,17 @@
//! This example illustrates the various features of Bevy UI.
use std::f32::consts::PI;
use bevy::{
a11y::{
accesskit::{NodeBuilder, Role},
accesskit::{Node as Accessible, Role},
AccessibilityNode,
},
color::palettes::basic::LIME,
color::palettes::{basic::LIME, css::DARK_GRAY},
input::mouse::{MouseScrollUnit, MouseWheel},
picking::focus::HoverMap,
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -146,7 +149,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
Label,
AccessibilityNode(NodeBuilder::new(Role::ListItem)),
AccessibilityNode(Accessible::new(Role::ListItem)),
))
.insert(PickingBehavior {
should_block_lower: false,
@ -157,28 +160,39 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
});
parent
.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(200.0),
position_type: PositionType::Absolute,
left: Val::Px(210.),
bottom: Val::Px(10.),
border: UiRect::all(Val::Px(20.)),
..default()
},
BorderColor(LIME.into()),
BackgroundColor(Color::srgb(0.4, 0.4, 1.)),
))
.spawn(Node {
left: Val::Px(210.),
bottom: Val::Px(10.),
position_type: PositionType::Absolute,
..default()
})
.with_children(|parent| {
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
));
parent
.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(200.0),
border: UiRect::all(Val::Px(20.)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
..default()
},
BorderColor(LIME.into()),
BackgroundColor(Color::srgb(0.8, 0.8, 1.)),
))
.with_children(|parent| {
parent.spawn((
UiImage::new(asset_server.load("branding/bevy_logo_light.png")),
// Uses the transform to rotate the logo image by 45 degrees
Transform::from_rotation(Quat::from_rotation_z(0.25 * PI)),
BorderRadius::all(Val::Px(10.)),
Outline {
width: Val::Px(2.),
offset: Val::Px(4.),
color: DARK_GRAY.into(),
},
));
});
});
let shadow = BoxShadow {
@ -186,7 +200,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
blur_radius: Val::Px(2.),
x_offset: Val::Px(10.),
y_offset: Val::Px(10.),
..Default::default()
..default()
};
// render order test: reddest in the back, whitest in the front (flex center)
@ -280,7 +294,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// bevy logo (image)
parent
.spawn((
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")),
UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png"))
.with_mode(NodeImageMode::Stretch),
Node {
width: Val::Px(500.0),
height: Val::Px(125.0),
@ -301,6 +316,38 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
));
});
});
// four bevy icons demonstrating image flipping
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::FlexEnd,
column_gap: Val::Px(10.),
padding: UiRect::all(Val::Px(10.)),
..default()
})
.with_children(|parent| {
for (flip_x, flip_y) in
[(false, false), (false, true), (true, true), (true, false)]
{
parent.spawn((
Node {
// The height will be chosen automatically to preserve the image's aspect ratio
width: Val::Px(75.),
..default()
},
UiImage {
image: asset_server.load("branding/icon.png"),
flip_x,
flip_y,
..default()
},
));
}
});
});
}

View file

@ -4,6 +4,7 @@
use bevy::{
color::palettes::css::{GOLD, ORANGE},
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -87,7 +88,8 @@ fn setup(
index: idx,
layout: atlas_layout_handle.clone(),
},
),
)
.with_mode(NodeImageMode::Sliced(slicer.clone())),
Node {
width: Val::Px(w),
height: Val::Px(h),
@ -98,7 +100,6 @@ fn setup(
margin: UiRect::all(Val::Px(20.0)),
..default()
},
ImageScaleMode::Sliced(slicer.clone()),
))
.with_children(|parent| {
parent.spawn((

View file

@ -4,6 +4,7 @@
use bevy::{
color::palettes::css::{GOLD, ORANGE},
prelude::*,
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -77,20 +78,18 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
margin: UiRect::all(Val::Px(20.0)),
..default()
},
UiImage::new(image.clone()),
ImageScaleMode::Sliced(slicer.clone()),
UiImage::new(image.clone())
.with_mode(NodeImageMode::Sliced(slicer.clone())),
))
.with_children(|parent| {
parent.spawn((
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
});
.with_child((
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
}
});
}

View file

@ -3,6 +3,7 @@
use bevy::{
prelude::*,
render::texture::{ImageLoaderSettings, ImageSampler},
ui::widget::NodeImageMode,
winit::WinitSettings,
};
@ -61,6 +62,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
image: image.clone(),
flip_x,
flip_y,
image_mode: NodeImageMode::Sliced(slicer.clone()),
..default()
},
Node {
@ -68,7 +70,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
height: Val::Px(height),
..default()
},
ImageScaleMode::Sliced(slicer.clone()),
));
}
});

View file

@ -32,18 +32,15 @@ fn screenshot_on_spacebar(
fn screenshot_saving(
mut commands: Commands,
screenshot_saving: Query<Entity, With<Capturing>>,
windows: Query<Entity, With<Window>>,
window: Single<Entity, With<Window>>,
) {
let Ok(window) = windows.get_single() else {
return;
};
match screenshot_saving.iter().count() {
0 => {
commands.entity(window).remove::<CursorIcon>();
commands.entity(*window).remove::<CursorIcon>();
}
x if x > 0 => {
commands
.entity(window)
.entity(*window)
.insert(CursorIcon::from(SystemCursorIcon::Progress));
}
_ => {}

View file

@ -1,12 +1,14 @@
//! Illustrates how to change window settings and shows how to affect
//! the mouse pointer in various ways.
#[cfg(feature = "custom_cursor")]
use bevy::winit::cursor::CustomCursor;
use bevy::{
core::FrameCount,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme},
winit::cursor::{CursorIcon, CustomCursor},
winit::cursor::CursorIcon,
};
fn main() {
@ -152,12 +154,16 @@ fn toggle_theme(mut window: Single<&mut Window>, input: Res<ButtonInput<KeyCode>
#[derive(Resource)]
struct CursorIcons(Vec<CursorIcon>);
fn init_cursor_icons(mut commands: Commands, asset_server: Res<AssetServer>) {
fn init_cursor_icons(
mut commands: Commands,
#[cfg(feature = "custom_cursor")] asset_server: Res<AssetServer>,
) {
commands.insert_resource(CursorIcons(vec![
SystemCursorIcon::Default.into(),
SystemCursorIcon::Pointer.into(),
SystemCursorIcon::Wait.into(),
SystemCursorIcon::Text.into(),
#[cfg(feature = "custom_cursor")]
CustomCursor::Image {
handle: asset_server.load("branding/icon.png"),
hotspot: (128, 128),