From 66920efc5d398ed6f5dcdfb1e592e83ea5531e78 Mon Sep 17 00:00:00 2001 From: Bernhard Specht Date: Mon, 22 Jun 2026 13:41:58 +0200 Subject: [PATCH 1/4] Update bevy to 0.19 Bump the bevy dependency 0.18 -> 0.19 and fix all resulting breakage across the library, tests, examples, and benchmarks. Library: - EntityCommand gained an associated `type Out`; add `type Out = ();` to the 10 transform command impls. - Components::{resource_id,get_resource_id} renamed to {component_id,get_id}. - Assets::get_mut now returns AssetMut; use get_mut_untracked for &mut A. Tests: - SystemState::get_mut now returns Result (unwrap). - Assets::get_mut_untracked in test helpers. Examples: - TextFont::{font,font_size} are now FontSource/FontSize (.into()). - TextLayout::new_with_justify removed; use a struct literal. - AmbientLight is now a per-camera component and GlobalAmbientLight is the global resource; the ambient_light example animates GlobalAmbientLight. - Hdr moved to bevy::camera. Also bump the bevy-inspector-egui dev-dependency 0.36 -> 0.37. --- Cargo.toml | 4 ++-- benchmarks/Cargo.toml | 2 +- examples/ambient_light.rs | 14 +++++++------- examples/menu.rs | 9 ++++++--- examples/sequence.rs | 14 ++++++++++---- examples/text_color.rs | 4 ++-- src/lens.rs | 6 +++--- src/lib.rs | 26 ++++++++++++++++++-------- src/tweenable.rs | 2 +- 9 files changed, 50 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3967c08..cb5acf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ bevy_text = ["bevy/bevy_text", "bevy/bevy_render", "bevy/bevy_sprite"] [dependencies] # Note: abuse 'bevy_color' to force 'bevy_math/curve' feature, which defines EaseFunction -bevy = { version = "0.18", default-features = false, features = [ +bevy = { version = "0.19", default-features = false, features = [ "bevy_color", "bevy_asset", "bevy_log", @@ -38,7 +38,7 @@ bevy = { version = "0.18", default-features = false, features = [ thiserror = "2" [dev-dependencies] -bevy-inspector-egui = { version = "0.36", default-features = false, features = [ +bevy-inspector-egui = { version = "0.37", default-features = false, features = [ "bevy_render", "bevy_pbr", ] } diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index 5cedb30..5074586 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -18,7 +18,7 @@ criterion = { version = "0.5", features = ["html_reports"] } bevy_tweening = { path = "../" } [dependencies.bevy] -version = "0.18" +version = "0.19" default-features = false features = ["bevy_render", "bevy_sprite", "bevy_text", "bevy_ui"] diff --git a/examples/ambient_light.rs b/examples/ambient_light.rs index 4d094f6..0ac1231 100644 --- a/examples/ambient_light.rs +++ b/examples/ambient_light.rs @@ -1,6 +1,6 @@ //! Example demonstrating resource animation and various transform shortcuts. //! -//! The example animates the `AmbientLight` resource of Bevy's PBR renderer. +//! The example animates the `GlobalAmbientLight` resource of Bevy's PBR renderer. //! This is mostly for example purpose; you probably want to animate some other //! (custom) resource. It also moves a capsule object back and forth with the //! `move_to()` command extension, and make it "resonate" by quickly scaling it @@ -11,20 +11,20 @@ use std::{ time::Duration, }; -use bevy::{color::palettes::css::*, post_process::bloom::Bloom, prelude::*, render::view::Hdr}; +use bevy::{camera::Hdr, color::palettes::css::*, post_process::bloom::Bloom, prelude::*}; use bevy_tweening::{lens::*, *}; mod utils; -// Define our own `Lens` to animate the `AmbientLight` resource. +// Define our own `Lens` to animate the `GlobalAmbientLight` resource. struct AmbientLightBrightnessLens { pub start: f32, pub end: f32, } // Implement the `Lens` trait. -impl Lens for AmbientLightBrightnessLens { - fn lerp(&mut self, mut target: Mut, ratio: f32) { +impl Lens for AmbientLightBrightnessLens { + fn lerp(&mut self, mut target: Mut, ratio: f32) { target.brightness = self.start.lerp(self.end, ratio); } } @@ -50,7 +50,7 @@ fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, - mut ambient_light: ResMut, + mut ambient_light: ResMut, ) -> Result<(), BevyError> { // Some fancy 3D camera with HDR and bloom, to emphasize the change of ambient // brightness. @@ -97,7 +97,7 @@ fn setup( .with_repeat(RepeatCount::Infinite, RepeatStrategy::MirroredRepeat); commands.spawn(( TweenAnim::new(tween), - AnimTarget::resource::(), + AnimTarget::resource::(), )); // Spawn some animated character-like capsule... diff --git a/examples/menu.rs b/examples/menu.rs index f90a8f5..3df6089 100644 --- a/examples/menu.rs +++ b/examples/menu.rs @@ -117,12 +117,15 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text::new(text.to_string()), TextFont { - font: font.clone(), - font_size: 48.0, + font: font.clone().into(), + font_size: 48.0.into(), ..default() }, TextColor(TEXT_COLOR), - TextLayout::new_with_justify(Justify::Center), + TextLayout { + justify: Justify::Center, + ..default() + }, )], )) .id(); diff --git a/examples/sequence.rs b/examples/sequence.rs index 813ece2..557579c 100644 --- a/examples/sequence.rs +++ b/examples/sequence.rs @@ -43,8 +43,8 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { let font = asset_server.load("fonts/FiraMono-Regular.ttf"); let text_font = TextFont { - font, - font_size: 50.0, + font: font.into(), + font_size: 50.0.into(), ..default() }; @@ -57,7 +57,10 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { commands .spawn(( Text2d::default(), - TextLayout::new_with_justify(justify), + TextLayout { + justify, + ..default() + }, Transform::from_translation(Vec3::new(0., 40., 0.)), RedProgress, )) @@ -79,7 +82,10 @@ fn setup(mut commands: Commands, asset_server: Res) -> Result<()> { commands .spawn(( Text2d::default(), - TextLayout::new_with_justify(justify), + TextLayout { + justify, + ..default() + }, Transform::from_translation(Vec3::new(0., -40., 0.)), BlueProgress, )) diff --git a/examples/text_color.rs b/examples/text_color.rs index bb44899..3796cdc 100644 --- a/examples/text_color.rs +++ b/examples/text_color.rs @@ -84,8 +84,8 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( Text::new(*ease_name), TextFont { - font: font.clone(), - font_size: 24.0, + font: font.clone().into(), + font_size: 24.0.into(), ..default() }, TextColor(Color::WHITE), diff --git a/src/lens.rs b/src/lens.rs index 9974005..3100224 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -1150,7 +1150,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -1167,7 +1167,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -1184,7 +1184,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, diff --git a/src/lib.rs b/src/lib.rs index b7a3ac1..b6ab222 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -915,6 +915,7 @@ pub(crate) struct MoveToCommand { } impl EntityCommand for MoveToCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(start) = entity.get::().map(|tr| tr.translation) { let lens = TransformPositionLens { @@ -950,6 +951,7 @@ pub(crate) struct MoveFromCommand { } impl EntityCommand for MoveFromCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(end) = entity.get::().map(|tr| tr.translation) { let lens = TransformPositionLens { @@ -985,6 +987,7 @@ pub(crate) struct ScaleToCommand { } impl EntityCommand for ScaleToCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(start) = entity.get::().map(|tr| tr.scale) { let lens = TransformScaleLens { @@ -1020,6 +1023,7 @@ pub(crate) struct ScaleFromCommand { } impl EntityCommand for ScaleFromCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(end) = entity.get::().map(|tr| tr.scale) { let lens = TransformScaleLens { @@ -1054,6 +1058,7 @@ pub(crate) struct RotateXCommand { } impl EntityCommand for RotateXCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveXLens { @@ -1090,6 +1095,7 @@ pub(crate) struct RotateYCommand { } impl EntityCommand for RotateYCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveYLens { @@ -1126,6 +1132,7 @@ pub(crate) struct RotateZCommand { } impl EntityCommand for RotateZCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveZLens { @@ -1163,6 +1170,7 @@ pub(crate) struct RotateXByCommand { } impl EntityCommand for RotateXByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveXLens { @@ -1199,6 +1207,7 @@ pub(crate) struct RotateYByCommand { } impl EntityCommand for RotateYByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveYLens { @@ -1235,6 +1244,7 @@ pub(crate) struct RotateZByCommand { } impl EntityCommand for RotateZByCommand { + type Out = (); fn apply(self, mut entity: EntityWorldMut) { if let Some(base_rotation) = entity.get::().map(|tr| tr.rotation) { let lens = TransformRotateAdditiveZLens { @@ -2200,10 +2210,10 @@ impl TweenAnim { .get_id(type_id) .ok_or(TweeningError::ComponentNotRegistered(type_id))?, AnimTargetKind::Resource => components - .get_resource_id(type_id) + .get_id(type_id) .ok_or(TweeningError::ResourceNotRegistered(type_id))?, AnimTargetKind::Asset { assets_type_id, .. } => components - .get_resource_id(*assets_type_id) + .get_id(*assets_type_id) .ok_or(TweeningError::AssetNotRegistered(type_id))?, }; let is_retargetable = false; // explicit target @@ -2622,7 +2632,7 @@ pub struct TweenResolver { impl TweenResolver { /// Register a resolver for the given resource type. pub(crate) fn register_resource_resolver_for(&mut self, components: &Components) { - let resource_id = components.resource_id::().unwrap(); + let resource_id = components.component_id::().unwrap(); let resolver = |world: &mut World, entity: Entity, target_type_id: &TypeId, @@ -2669,7 +2679,7 @@ impl TweenResolver { /// Register a resolver for the given asset type. pub(crate) fn register_asset_resolver_for(&mut self, components: &Components) { - let resource_id = components.resource_id::>().unwrap(); + let resource_id = components.component_id::>().unwrap(); let resolver = |world: &mut World, asset_id: UntypedAssetId, entity: Entity, @@ -2683,7 +2693,7 @@ impl TweenResolver { // parallel of the TweenAnim world.resource_scope(|world, assets: Mut>| { // Next, fetch the asset A itself from its Assets based on its asset ID - let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut(asset_id)) + let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut_untracked(asset_id)) else { return Err(TweeningError::InvalidAssetId(asset_id.into())); }; @@ -2916,7 +2926,7 @@ mod tests { let mut added = Tick::new(0); let mut last_changed = Tick::new(0); let mut caller = MaybeLocation::caller(); - let asset = assets.get_mut(handle.id()).unwrap(); + let asset = assets.get_mut_untracked(handle.id()).unwrap(); let target = Mut::new( asset, &mut added, @@ -3800,7 +3810,7 @@ mod tests { env.world.flush(); let delta_time = Duration::from_millis(200); - let resource_id = env.world.resource_id::().unwrap(); + let resource_id = env.world.component_id::().unwrap(); // Resource resolver not registered; fails env.world @@ -3882,7 +3892,7 @@ mod tests { env.world.flush(); let delta_time = Duration::from_millis(200); - let resource_id = env.world.resource_id::>().unwrap(); + let resource_id = env.world.component_id::>().unwrap(); // Asset resolver not registered; fails env.world diff --git a/src/tweenable.rs b/src/tweenable.rs index d4c5d34..e8ae354 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -1934,7 +1934,7 @@ mod tests { // Messages are only sent when playing forward if playback_direction.is_forward() { //let component_id = world.component_id::().unwrap(); - let mut event_reader = event_reader_system_state.get_mut(&mut world); + let mut event_reader = event_reader_system_state.get_mut(&mut world).unwrap(); let event = event_reader.read().next(); if just_completed { assert!(event.is_some()); From c2fca4aa303bb303d345fba5ca9aeead502a2d34 Mon Sep 17 00:00:00 2001 From: Jerome Humbert Date: Sat, 27 Jun 2026 19:48:24 +0100 Subject: [PATCH 2/4] Fix change detection for assets --- examples/ambient_light.rs | 11 ++++++----- src/lib.rs | 32 ++++++++++++++++++++++++++++---- src/tweenable.rs | 4 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/examples/ambient_light.rs b/examples/ambient_light.rs index 0ac1231..c3db4ec 100644 --- a/examples/ambient_light.rs +++ b/examples/ambient_light.rs @@ -1,10 +1,11 @@ //! Example demonstrating resource animation and various transform shortcuts. //! -//! The example animates the `GlobalAmbientLight` resource of Bevy's PBR renderer. -//! This is mostly for example purpose; you probably want to animate some other -//! (custom) resource. It also moves a capsule object back and forth with the -//! `move_to()` command extension, and make it "resonate" by quickly scaling it -//! between 100% and 110% size with the `scale_to()` command extension. +//! The example animates the `GlobalAmbientLight` resource of Bevy's PBR +//! renderer. This is mostly for example purpose; you probably want to animate +//! some other (custom) resource. It also moves a capsule object back and forth +//! with the `move_to()` command extension, and make it "resonate" by quickly +//! scaling it between 100% and 110% size with the `scale_to()` command +//! extension. use std::{ f32::consts::{FRAC_PI_2, FRAC_PI_4}, diff --git a/src/lib.rs b/src/lib.rs index b6ab222..b73f68f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2691,9 +2691,19 @@ impl TweenResolver { let asset_id = asset_id.typed::(); // First, remove the Assets from the world so we can access it mutably in // parallel of the TweenAnim - world.resource_scope(|world, assets: Mut>| { - // Next, fetch the asset A itself from its Assets based on its asset ID - let Some(asset) = assets.filter_map_unchanged(|assets| assets.get_mut_untracked(asset_id)) + world.resource_scope(|world, mut assets: Mut>| { + // Make a copy of the Mut> so we can use it later to mark the + // asset as modified if needed. This doesn't copy the asset, only the + // Mut<> itself. + let assets_mut = assets.reborrow(); + + // Next, fetch the asset A itself from its Assets based on its asset ID. + // This is a bit convoluted because we want to keep a Mut, and not the + // new AssetMut, which unfortunately has nothing to do with Mut. + // We also don't want to trigger any change detection (ECS) or asset mutation + // (Assets) yet, as we only do that lazily when we detected the animation + // actually modified the asset. + let Some(asset_mut) = assets_mut.filter_map_unchanged(|assets| assets.get_mut_untracked(asset_id)) else { return Err(TweeningError::InvalidAssetId(asset_id.into())); }; @@ -2714,16 +2724,30 @@ impl TweenResolver { }; // Finally, step the TweenAnim and mutate the target + let mut mut_untyped: MutUntyped<'_> = asset_mut.into(); let ret = anim.step_self( commands, entity, delta_time, &target, - asset.into(), + mut_untyped.reborrow(), target_type_id, cycle_events.reborrow(), anim_events.reborrow(), ); + + // If the asset actually changed (as reported by Mut<>), mark it as such + // through Assets (which will send the AssetModified message). This works + // because all Mut<> from reborrow() share the same internal tracking fields, + // so even if we passed a Mut<>::reborrow() copy, the source Mut<> "sees" the + // same change. However that change detection only marks Assets as changed, + // and doesn't trigger the regular flow for a modified asset. + if mut_untyped.is_changed() { + // into_inner() marks the asset as changed, which triggers the regular + // asset modified flow (as if we had used AssetMut). + let _ = assets.get_mut(asset_id).unwrap().into_inner(); + } + ret.map(|result| { assert!(!result.needs_retarget, "Cannot use a multi-target sequence of tweenable animations with an asset target."); result.retain diff --git a/src/tweenable.rs b/src/tweenable.rs index e8ae354..3cb804a 100644 --- a/src/tweenable.rs +++ b/src/tweenable.rs @@ -1933,8 +1933,8 @@ mod tests { // Messages are only sent when playing forward if playback_direction.is_forward() { - //let component_id = world.component_id::().unwrap(); - let mut event_reader = event_reader_system_state.get_mut(&mut world).unwrap(); + let mut event_reader = + event_reader_system_state.get_mut(&mut world).unwrap(); let event = event_reader.read().next(); if just_completed { assert!(event.is_some()); From b09af6cd7176b63babbcdfa6dcca432e4522da64 Mon Sep 17 00:00:00 2001 From: Jerome Humbert Date: Sun, 28 Jun 2026 00:18:33 +0100 Subject: [PATCH 3/4] Fix change detection for assets --- examples/colormaterial_color.rs | 45 ++++++++++++++++++++++++++++----- src/lens.rs | 5 +++- src/lib.rs | 44 +++++++++++++++++--------------- 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/examples/colormaterial_color.rs b/examples/colormaterial_color.rs index 096387b..c3358c9 100644 --- a/examples/colormaterial_color.rs +++ b/examples/colormaterial_color.rs @@ -1,27 +1,48 @@ use std::time::Duration; use bevy::{color::palettes::css::*, prelude::*}; +use bevy_inspector_egui::{bevy_egui::EguiPlugin, prelude::*, quick::ResourceInspectorPlugin}; use bevy_tweening::{lens::*, *}; mod utils; fn main() { App::default() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "ColorMaterialColorLens".to_string(), - resolution: bevy::window::WindowResolution::new(1200, 600), - present_mode: bevy::window::PresentMode::Fifo, // vsync + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "ColorMaterialColorLens".to_string(), + resolution: bevy::window::WindowResolution::new(1200, 600), + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..default() + }), ..default() }), - ..default() - })) + EguiPlugin::default(), + ResourceInspectorPlugin::::new(), + )) + .init_resource::() + .register_type::() .add_systems(Update, utils::close_on_esc) .add_plugins(TweeningPlugin) .add_systems(Startup, setup) + .add_systems(Update, update_animation_speed) .run(); } +#[derive(Resource, Reflect, InspectorOptions)] +#[reflect(InspectorOptions)] +struct Options { + #[inspector(min = 0., max = 100.)] + speed: f64, +} + +impl Default for Options { + fn default() -> Self { + Self { speed: 1. } + } +} + fn setup( mut commands: Commands, mut meshes: ResMut>, @@ -105,3 +126,13 @@ fn setup( Ok(()) } + +fn update_animation_speed(options: Res, mut q_anims: Query<&mut TweenAnim>) { + if !options.is_changed() { + return; + } + + for mut anim in &mut q_anims { + anim.speed = options.speed; + } +} diff --git a/src/lens.rs b/src/lens.rs index 3100224..857859c 100644 --- a/src/lens.rs +++ b/src/lens.rs @@ -507,7 +507,10 @@ pub struct ColorMaterialColorLens { #[cfg(feature = "bevy_sprite")] impl Lens for ColorMaterialColorLens { fn lerp(&mut self, mut target: Mut, ratio: f32) { - target.color = self.start.mix(&self.end, ratio); + let color = self.start.mix(&self.end, ratio); + if target.color != color { + target.color = color; + } } } diff --git a/src/lib.rs b/src/lib.rs index b73f68f..9f83417 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -299,7 +299,7 @@ use std::{ use bevy::{ asset::UntypedAssetId, ecs::{ - change_detection::MutUntyped, + change_detection::{MaybeLocation, MutUntyped, Tick}, component::{ComponentId, Components, Mutable}, }, platform::collections::HashMap, @@ -2692,18 +2692,12 @@ impl TweenResolver { // First, remove the Assets from the world so we can access it mutably in // parallel of the TweenAnim world.resource_scope(|world, mut assets: Mut>| { - // Make a copy of the Mut> so we can use it later to mark the - // asset as modified if needed. This doesn't copy the asset, only the - // Mut<> itself. - let assets_mut = assets.reborrow(); - - // Next, fetch the asset A itself from its Assets based on its asset ID. - // This is a bit convoluted because we want to keep a Mut, and not the - // new AssetMut, which unfortunately has nothing to do with Mut. - // We also don't want to trigger any change detection (ECS) or asset mutation - // (Assets) yet, as we only do that lazily when we detected the animation - // actually modified the asset. - let Some(asset_mut) = assets_mut.filter_map_unchanged(|assets| assets.get_mut_untracked(asset_id)) + // We abuse the fact that Assets is changed every single frame by the asset_events() + // system, and assume that the current tick is therefore equal to the last time + // Assets was changed. + let this_tick = assets.last_changed().get(); + + let Some(mut asset_mut) = assets.get_mut(asset_id) else { return Err(TweeningError::InvalidAssetId(asset_id.into())); }; @@ -2723,8 +2717,20 @@ impl TweenResolver { return Err(TweeningError::MissingTweenAnim(ent.id())); }; + // Create a fake Mut which is always unchanged before the anim steps. + // Its sole purpose is to know if the Lens::lerp() changed the asset. + // Ideally we'd directly use AssetMut<> but the interface doesn't match. + let mut added = Tick::MAX; // hopefully unused... + let last_tick = this_tick.saturating_sub(1); + let mut last_changed = Tick::new(last_tick); + let last_run = last_changed; + let this_run = Tick::new(this_tick); + let mut caller = MaybeLocation::caller(); + let typed_mut = Mut::new(asset_mut.bypass_change_detection(), &mut added, &mut last_changed, last_run, this_run, caller.as_mut()); + assert!(!typed_mut.is_changed()); + let mut mut_untyped: MutUntyped = typed_mut.into(); + // Finally, step the TweenAnim and mutate the target - let mut mut_untyped: MutUntyped<'_> = asset_mut.into(); let ret = anim.step_self( commands, entity, @@ -2737,15 +2743,11 @@ impl TweenResolver { ); // If the asset actually changed (as reported by Mut<>), mark it as such - // through Assets (which will send the AssetModified message). This works - // because all Mut<> from reborrow() share the same internal tracking fields, - // so even if we passed a Mut<>::reborrow() copy, the source Mut<> "sees" the - // same change. However that change detection only marks Assets as changed, - // and doesn't trigger the regular flow for a modified asset. + // through Assets (which will send the asset modified message). if mut_untyped.is_changed() { // into_inner() marks the asset as changed, which triggers the regular - // asset modified flow (as if we had used AssetMut). - let _ = assets.get_mut(asset_id).unwrap().into_inner(); + // asset modified flow (emits AssetEvents::Modified). + let _ = asset_mut.into_inner(); } ret.map(|result| { From 23d141ddf0c46d6d4286d4bfc1ba81e179f324ff Mon Sep 17 00:00:00 2001 From: Jerome Humbert Date: Sun, 28 Jun 2026 00:52:47 +0100 Subject: [PATCH 4/4] Upgrade tarpaulin to 0.32.8 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a04e1f8..e55b4b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -100,7 +100,7 @@ jobs: components: rustfmt, clippy - name: Install cargo-tarpaulin run: | - RUST_BACKTRACE=1 cargo install --version 0.31.2 cargo-tarpaulin + RUST_BACKTRACE=1 cargo install --version 0.32.8 cargo-tarpaulin - name: Generate code coverage run: | RUST_BACKTRACE=1 cargo tarpaulin --engine llvm --verbose --timeout 120 --out Lcov --workspace --all-features