mirror of
https://github.com/bevyengine/bevy
synced 2025-02-17 06:28:34 +00:00
Merge branch 'main' into resize-window-crash
This commit is contained in:
commit
5b426c3080
86 changed files with 983 additions and 886 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -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: |
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>) {}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
] }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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 \
|
||||
/// / \
|
||||
/// |-------------------------------------|
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>>()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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`"
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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 => {}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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,
|
||||
)),
|
||||
))
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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((
|
||||
|
|
|
@ -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)),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Reference in a new issue