From bdf4b176f6fcb4ad37b2e7f28100791e4e321b7e Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:17:00 +0800 Subject: [PATCH 01/27] Rename `WorldPipeline` -> `BevyWorldPipeline` --- crates/bevy_motiongfx/src/lib.rs | 2 +- crates/bevy_motiongfx/src/pipeline.rs | 10 +++++----- crates/bevy_motiongfx/src/world.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_motiongfx/src/lib.rs b/crates/bevy_motiongfx/src/lib.rs index ae319f8..dac3e6f 100644 --- a/crates/bevy_motiongfx/src/lib.rs +++ b/crates/bevy_motiongfx/src/lib.rs @@ -21,7 +21,7 @@ pub mod prelude { ActionInterpTimelineExt, Interpolation, }; pub use crate::pipeline::{ - PipelineRegistryExt, WorldPipeline, WorldPipelineRegistry, + BevyPipeline, BevyPipelineRegistry, PipelineRegistryExt, }; pub use crate::register_fields; pub use crate::registry::FieldPathRegisterAppExt; diff --git a/crates/bevy_motiongfx/src/pipeline.rs b/crates/bevy_motiongfx/src/pipeline.rs index 2fb1b7f..fc297a0 100644 --- a/crates/bevy_motiongfx/src/pipeline.rs +++ b/crates/bevy_motiongfx/src/pipeline.rs @@ -2,8 +2,8 @@ use bevy_ecs::component::Mutable; use bevy_ecs::prelude::*; use motiongfx::prelude::*; -pub type WorldPipelineRegistry = PipelineRegistry; -pub type WorldPipeline = Pipeline; +pub type BevyPipelineRegistry = PipelineRegistry; +pub type BevyPipeline = Pipeline; pub fn bake_component_actions(world: &World, ctx: BakeCtx) where @@ -83,7 +83,7 @@ pub trait PipelineRegistryExt { T: Clone + ThreadSafe; } -impl PipelineRegistryExt for WorldPipelineRegistry { +impl PipelineRegistryExt for BevyPipelineRegistry { fn register_component(&mut self) -> PipelineKey where S: Component, @@ -93,7 +93,7 @@ impl PipelineRegistryExt for WorldPipelineRegistry { self.register_unchecked( key, - WorldPipeline::new( + BevyPipeline::new( bake_component_actions::, sample_component_actions::, ), @@ -114,7 +114,7 @@ impl PipelineRegistryExt for WorldPipelineRegistry { self.register_unchecked( key, - WorldPipeline::new( + BevyPipeline::new( bake_asset_actions::, sample_asset_actions::, ), diff --git a/crates/bevy_motiongfx/src/world.rs b/crates/bevy_motiongfx/src/world.rs index cbec5a7..7edcb7e 100644 --- a/crates/bevy_motiongfx/src/world.rs +++ b/crates/bevy_motiongfx/src/world.rs @@ -7,7 +7,7 @@ use motiongfx::prelude::{FieldAccessorRegistry, Timeline}; use crate::MotionGfxSet; use crate::controller::FixedRatePlayer; -use crate::pipeline::WorldPipelineRegistry; +use crate::pipeline::BevyPipelineRegistry; use crate::prelude::RealtimePlayer; pub struct MotionGfxWorldPlugin; @@ -79,7 +79,7 @@ pub struct MotionGfxWorld { id: TimelineId, pending_timelines: HashMap>, timelines: HashMap>, - pub pipeline_registry: WorldPipelineRegistry, + pub pipeline_registry: BevyPipelineRegistry, pub accessor_registry: FieldAccessorRegistry, } From c34a42504e6479a7f1be424e3f5bd82a07bcdf85 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:52:20 +0800 Subject: [PATCH 02/27] Add `SubjectSource` trait to unify bake and sample world access --- crates/bevy_motiongfx/src/pipeline.rs | 115 ++++++------ crates/bevy_motiongfx/src/world.rs | 6 +- crates/motiongfx/examples/custom_world.rs | 121 +++++-------- crates/motiongfx/src/lib.rs | 1 + crates/motiongfx/src/pipeline.rs | 208 ++++++++++++---------- crates/motiongfx/src/timeline.rs | 26 ++- 6 files changed, 231 insertions(+), 246 deletions(-) diff --git a/crates/bevy_motiongfx/src/pipeline.rs b/crates/bevy_motiongfx/src/pipeline.rs index fc297a0..31cb85c 100644 --- a/crates/bevy_motiongfx/src/pipeline.rs +++ b/crates/bevy_motiongfx/src/pipeline.rs @@ -1,75 +1,70 @@ use bevy_ecs::component::Mutable; use bevy_ecs::prelude::*; +use motiongfx::pipeline::{bake, sample}; use motiongfx::prelude::*; -pub type BevyPipelineRegistry = PipelineRegistry; -pub type BevyPipeline = Pipeline; +/// Newtype wrapper around [`World`] that is local to this crate, +/// allowing [`SubjectSource`] impls without violating the orphan rule. +#[repr(transparent)] +pub struct BevyWorld(pub World); -pub fn bake_component_actions(world: &World, ctx: BakeCtx) -where - S: Component, - T: Clone + ThreadSafe, -{ - ctx.bake::(|entity| world.get::(entity)); -} +impl BevyWorld { + pub fn from_ref(world: &World) -> &Self { + // SAFETY: `BevyWorld` is repr(transparent) over `World`. + unsafe { &*(world as *const World as *const Self) } + } -pub fn sample_component_actions( - world: &mut World, - ctx: SampleCtx, -) where - S: Component, - T: Clone + ThreadSafe, -{ - ctx.sample::(|entity, target, accessor| { - if let Some(mut source) = world.get_mut::(entity) { - *accessor.get_mut(&mut source) = target; - } - }); + pub fn from_mut(world: &mut World) -> &mut Self { + // SAFETY: `BevyWorld` is repr(transparent) over `World`. + unsafe { &mut *(world as *mut World as *mut Self) } + } } -#[cfg(feature = "asset")] -pub fn bake_asset_actions(world: &World, ctx: BakeCtx) -where - S: bevy_asset::Asset, - T: Clone + ThreadSafe, +impl> SubjectSource + for BevyWorld { - use bevy_asset::Assets; - use bevy_asset::UntypedAssetId; - - let Some(assets) = world.get_resource::>() else { - return; - }; + fn get_source(&self, id: Entity) -> Option<&S> { + self.0.get::(id) + } - ctx.bake::(|asset_id| { - assets.get(asset_id.typed::()) - }); + fn apply_source( + &mut self, + id: Entity, + f: impl FnOnce(&mut S) -> R, + ) -> Option { + self.0.get_mut::(id).map(|mut m| f(m.as_mut())) + } } #[cfg(feature = "asset")] -pub fn sample_asset_actions(world: &mut World, ctx: SampleCtx) -where - S: bevy_asset::Asset, - T: Clone + ThreadSafe, +impl + SubjectSource for BevyWorld { - use bevy_asset::Assets; - use bevy_asset::UntypedAssetId; - - let Some(mut assets) = world.get_resource_mut::>() - else { - return; - }; - - ctx.sample::( - |asset_id, target, accessor| { - if let Some(source) = - assets.get_mut(asset_id.typed::()) - { - *accessor.get_mut(source) = target; - } - }, - ); + fn get_source( + &self, + id: bevy_asset::UntypedAssetId, + ) -> Option<&S> { + self.0 + .get_resource::>()? + .get(id.typed::()) + } + + fn apply_source( + &mut self, + id: bevy_asset::UntypedAssetId, + f: impl FnOnce(&mut S) -> R, + ) -> Option { + self.0 + .get_resource_mut::>()? + .into_inner() + .get_mut(id.typed::()) + .map(f) + } } +pub type BevyPipelineRegistry = PipelineRegistry; +pub type BevyPipeline = Pipeline; + pub trait PipelineRegistryExt { fn register_component(&mut self) -> PipelineKey where @@ -94,8 +89,8 @@ impl PipelineRegistryExt for BevyPipelineRegistry { self.register_unchecked( key, BevyPipeline::new( - bake_component_actions::, - sample_component_actions::, + bake::<_, _, S, T>, + sample::<_, _, S, T>, ), ); @@ -115,8 +110,8 @@ impl PipelineRegistryExt for BevyPipelineRegistry { self.register_unchecked( key, BevyPipeline::new( - bake_asset_actions::, - sample_asset_actions::, + bake::<_, _, S, T>, + sample::<_, _, S, T>, ), ); diff --git a/crates/bevy_motiongfx/src/world.rs b/crates/bevy_motiongfx/src/world.rs index 7edcb7e..f7c86ee 100644 --- a/crates/bevy_motiongfx/src/world.rs +++ b/crates/bevy_motiongfx/src/world.rs @@ -7,7 +7,7 @@ use motiongfx::prelude::{FieldAccessorRegistry, Timeline}; use crate::MotionGfxSet; use crate::controller::FixedRatePlayer; -use crate::pipeline::BevyPipelineRegistry; +use crate::pipeline::{BevyPipelineRegistry, BevyWorld}; use crate::prelude::RealtimePlayer; pub struct MotionGfxWorldPlugin; @@ -135,7 +135,7 @@ impl MotionGfxWorld { timeline.bake_actions( &self.accessor_registry, &self.pipeline_registry, - world, + BevyWorld::from_ref(world), ); self.timelines.insert(id, timeline); } @@ -149,7 +149,7 @@ impl MotionGfxWorld { timeline.sample_queued_actions( &self.accessor_registry, &self.pipeline_registry, - world, + BevyWorld::from_mut(world), ); timeline.reset(); } diff --git a/crates/motiongfx/examples/custom_world.rs b/crates/motiongfx/examples/custom_world.rs index 7ed2c01..8b3dfc8 100644 --- a/crates/motiongfx/examples/custom_world.rs +++ b/crates/motiongfx/examples/custom_world.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use motiongfx::pipeline::{bake, sample}; use motiongfx::prelude::*; struct World { @@ -150,96 +151,62 @@ fn main() { ); } +impl SubjectSource for SubjectWorld { + fn get_source(&self, id: Id) -> Option<&Point> { + match self.world.get(&id)? { + Subject::Point(point) => Some(point), + Subject::Line(_) => None, + } + } + + fn apply_source( + &mut self, + id: Id, + f: impl FnOnce(&mut Point) -> R, + ) -> Option { + match self.world.get_mut(&id)? { + Subject::Point(point) => Some(f(point)), + Subject::Line(_) => None, + } + } +} + +impl SubjectSource for SubjectWorld { + fn get_source(&self, id: Id) -> Option<&Line> { + match self.world.get(&id)? { + Subject::Line(line) => Some(line), + Subject::Point(_) => None, + } + } + + fn apply_source( + &mut self, + id: Id, + f: impl FnOnce(&mut Line) -> R, + ) -> Option { + match self.world.get_mut(&id)? { + Subject::Line(line) => Some(f(line)), + Subject::Point(_) => None, + } + } +} + fn register_pipelines( pipeline_registry: &mut PipelineRegistry, ) { pipeline_registry.register_unchecked( PipelineKey::new::(), - Pipeline::new( - |world, ctx| { - ctx.bake::(|id| { - let subject = world.world.get(&id)?; - match subject { - Subject::Point(point) => Some(point), - Subject::Line(_) => None, - } - }); - }, - |world, ctx| { - ctx.sample::( - |id, target, accessor| { - let Some(subject) = world.world.get_mut(&id) - else { - return; - }; - - if let Subject::Point(point) = subject { - *accessor.get_mut(point) = target; - } - }, - ); - }, - ), + Pipeline::new(bake::<_, _, Point, f32>, sample::<_, _, Point, f32>), ); pipeline_registry.register_unchecked( PipelineKey::new::(), - Pipeline::new( - |world, ctx| { - ctx.bake::(|id| { - let subject = world.world.get(&id)?; - match subject { - Subject::Line(line) => Some(line), - Subject::Point(_) => None, - } - }); - }, - |world, ctx| { - ctx.sample::( - |id, target, accessor| { - let Some(subject) = world.world.get_mut(&id) - else { - return; - }; - - if let Subject::Line(line) = subject { - *accessor.get_mut(line) = target; - } - }, - ); - }, - ), + Pipeline::new(bake::<_, _, Line, Point>, sample::<_, _, Line, Point>), ); - // TODO: This looks almost exactly the same as above, could - // pipeline be simplified enough to ignore the target field? pipeline_registry.register_unchecked( PipelineKey::new::(), - Pipeline::new( - |world, ctx| { - ctx.bake::(|id| { - let subject = world.world.get(&id)?; - match subject { - Subject::Line(line) => Some(line), - Subject::Point(_) => None, - } - }); - }, - |world, ctx| { - ctx.sample::( - |id, target, accessor| { - let Some(subject) = world.world.get_mut(&id) - else { - return; - }; - - if let Subject::Line(line) = subject { - *accessor.get_mut(line) = target; - } - }, - ); - }, - ), + Pipeline::new(bake::<_, _, Line, f32>, sample::<_, _, Line, f32>), ); } diff --git a/crates/motiongfx/src/lib.rs b/crates/motiongfx/src/lib.rs index 7a8c281..0b036e4 100644 --- a/crates/motiongfx/src/lib.rs +++ b/crates/motiongfx/src/lib.rs @@ -29,6 +29,7 @@ pub mod prelude { pub use crate::ease; pub use crate::pipeline::{ BakeCtx, Pipeline, PipelineKey, PipelineRegistry, SampleCtx, + SubjectSource, }; pub use crate::timeline::{Timeline, TimelineBuilder}; pub use crate::track::{Track, TrackFragment, TrackOrdering}; diff --git a/crates/motiongfx/src/pipeline.rs b/crates/motiongfx/src/pipeline.rs index be7a352..576429f 100644 --- a/crates/motiongfx/src/pipeline.rs +++ b/crates/motiongfx/src/pipeline.rs @@ -1,8 +1,8 @@ use core::any::TypeId; +use core::marker::PhantomData; use bevy_ecs::prelude::*; use bevy_platform::collections::HashMap; -use field_path::accessor::Accessor; use field_path::registry::FieldAccessorRegistry; use crate::ThreadSafe; @@ -13,6 +13,27 @@ use crate::action::{ use crate::subject::SubjectId; use crate::track::Track; +pub struct PipelineHandle +where + I: SubjectId, + S: 'static, + T: 'static, +{ + #[expect(clippy::complexity)] + _marker: PhantomData (I, S, T)>, +} + +impl PipelineHandle +where + I: SubjectId, + S: 'static, + T: 'static, +{ + pub fn as_key(&self) -> PipelineKey { + PipelineKey::new::() + } +} + /// Uniquely identifies a [`Pipeline`] to bake and sample a target /// field from a subject's source data structure. #[derive( @@ -50,8 +71,18 @@ impl PipelineKey { } } -pub type BakeFn = fn(&W, BakeCtx); -pub type SampleFn = fn(&mut W, SampleCtx); +/// Provides read and write access to a source type `S` by subject id `I`. +pub trait SubjectSource { + fn get_source(&self, id: I) -> Option<&S>; + fn apply_source( + &mut self, + id: I, + f: impl FnOnce(&mut S) -> R, + ) -> Option; +} + +pub type BakeFn = fn(BakeCtx); +pub type SampleFn = fn(SampleCtx); #[derive(Debug, Clone, Copy)] pub struct Pipeline { @@ -64,12 +95,12 @@ impl Pipeline { Self { bake, sample } } - pub fn bake(&self, world: &W, ctx: BakeCtx) { - (self.bake)(world, ctx) + pub fn bake(&self, ctx: BakeCtx) { + (self.bake)(ctx) } - pub fn sample(&self, world: &mut W, ctx: SampleCtx) { - (self.sample)(world, ctx) + pub fn sample(&self, ctx: SampleCtx) { + (self.sample)(ctx) } } @@ -114,116 +145,111 @@ impl Default for PipelineRegistry { } } -pub struct BakeCtx<'a> { +pub struct BakeCtx<'a, W> { + pub world: &'a W, pub track: &'a Track, pub action_world: &'a mut ActionWorld, pub accessor_registry: &'a FieldAccessorRegistry, } -impl<'a> BakeCtx<'a> { - pub fn bake( - self, - get_source: impl Fn(I) -> Option<&'a S>, - ) where - I: SubjectId, - S: 'static, - T: Clone + ThreadSafe, - { - for (key, span) in self.track.sequences_spans() { - let Ok(accessor) = - self.accessor_registry.get::(key.field()) - else { - continue; - }; +pub fn bake(ctx: BakeCtx) +where + W: SubjectSource, + I: SubjectId, + S: 'static, + T: Clone + ThreadSafe, +{ + for (key, span) in ctx.track.sequences_spans() { + let Ok(accessor) = + ctx.accessor_registry.get::(key.field()) + else { + continue; + }; - let Some(&id) = - self.action_world.get_id(&key.subject_id().uid()) - else { - continue; - }; + let Some(&id) = + ctx.action_world.get_id(&key.subject_id().uid()) + else { + continue; + }; - // Get the source from the target world. - let Some(source) = get_source(id) else { - continue; - }; + let Some(source) = ctx.world.get_source(id) else { + continue; + }; - let mut start = accessor.get_ref(source).clone(); + let mut start = accessor.get_ref(source).clone(); - for ActionClip { id, .. } in self.track.clips(*span) { - let Some(action) = - self.action_world.get_action::(*id) - else { - continue; - }; + for ActionClip { id, .. } in ctx.track.clips(*span) { + let Some(action) = ctx.action_world.get_action::(*id) + else { + continue; + }; - let end = action(&start); - let segment = - Segment::new(start.clone(), end.clone()); + let end = action(&start); + let segment = Segment::new(start.clone(), end.clone()); - self.action_world - .edit_action(*id) - .set_segment(segment); + ctx.action_world.edit_action(*id).set_segment(segment); - start = end; - } + start = end; } } } -pub struct SampleCtx<'a> { +impl BakeCtx<'_, W> {} + +pub struct SampleCtx<'a, W> { + pub world: &'a mut W, pub action_world: &'a ActionWorld, pub accessor_registry: &'a FieldAccessorRegistry, } -impl<'a> SampleCtx<'a> { - pub fn sample( - self, - mut set_target: impl FnMut(I, T, Accessor), - ) where - I: SubjectId, - S: 'static, - T: Clone + ThreadSafe, +pub fn sample(ctx: SampleCtx) +where + W: SubjectSource, + I: SubjectId, + S: 'static, + T: Clone + ThreadSafe, +{ + let Some(mut q) = ctx.action_world.world().try_query::<( + &ActionKey, + &SampleMode, + &Segment, + &InterpStorage, + Option<&EaseStorage>, + )>() else { + return; + }; + + for (key, sample_mode, segment, interp, ease) in + q.iter(ctx.action_world.world()) { - let Some(mut q) = self.action_world.world().try_query::<( - &ActionKey, - &SampleMode, - &Segment, - &InterpStorage, - Option<&EaseStorage>, - )>() else { - return; + let Ok(accessor) = + ctx.accessor_registry.get::(key.field()) + else { + continue; }; - for (key, sample_mode, segment, interp, ease) in - q.iter(self.action_world.world()) - { - let Ok(accessor) = - self.accessor_registry.get::(key.field()) - else { - continue; - }; + let Some(&id) = + ctx.action_world.get_id(&key.subject_id().uid()) + else { + continue; + }; - let Some(&id) = - self.action_world.get_id(&key.subject_id().uid()) - else { - continue; - }; + let target = match sample_mode { + SampleMode::Start => segment.start.clone(), + SampleMode::End => segment.end.clone(), + SampleMode::Interp(t) => { + let t = match ease { + Some(ease) => ease.0(*t), + None => *t, + }; - let target = match sample_mode { - SampleMode::Start => segment.start.clone(), - SampleMode::End => segment.end.clone(), - SampleMode::Interp(t) => { - let t = match ease { - Some(ease) => ease.0(*t), - None => *t, - }; - - interp.0(&segment.start, &segment.end, t) - } - }; + interp.0(&segment.start, &segment.end, t) + } + }; - set_target(id, target, accessor); - } + ctx.world.apply_source(id, |source| { + *accessor.get_mut(source) = target; + }); } } diff --git a/crates/motiongfx/src/timeline.rs b/crates/motiongfx/src/timeline.rs index 35353ee..e5b2de6 100644 --- a/crates/motiongfx/src/timeline.rs +++ b/crates/motiongfx/src/timeline.rs @@ -52,14 +52,12 @@ impl Timeline { }; for track in self.tracks.iter() { - pipeline.bake( - subject_world, - BakeCtx { - track, - action_world: &mut self.action_world, - accessor_registry, - }, - ) + pipeline.bake(BakeCtx { + world: subject_world, + track, + action_world: &mut self.action_world, + accessor_registry, + }) } } } @@ -231,13 +229,11 @@ impl Timeline { continue; }; - pipeline.sample( - subject_world, - SampleCtx { - action_world: &self.action_world, - accessor_registry, - }, - ); + pipeline.sample(SampleCtx { + world: subject_world, + action_world: &self.action_world, + accessor_registry, + }); } } From f928cd5cca091a93a7a6ed74e75d8e4cd6f66d22 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:47:42 +0800 Subject: [PATCH 03/27] Make `Pipeline` `W` agnostic - Move `BakeFnPtr`/`SampleFnPtr` into `pipeline/func_pointers` submodule - Move all registry into a new `registry.rs` file --- Cargo.lock | 7 +- Cargo.toml | 2 +- crates/bevy_motiongfx/src/interpolation.rs | 2 +- crates/bevy_motiongfx/src/lib.rs | 2 +- crates/bevy_motiongfx/src/pipeline.rs | 47 +++-- crates/bevy_motiongfx/src/registry.rs | 10 +- crates/bevy_motiongfx/src/world.rs | 10 +- crates/motiongfx/README.md | 2 +- crates/motiongfx/examples/custom_world.rs | 76 ++++---- crates/motiongfx/src/action.rs | 21 +-- crates/motiongfx/src/lib.rs | 29 ++- crates/motiongfx/src/pipeline.rs | 167 ++++++++++-------- .../motiongfx/src/pipeline/func_pointers.rs | 48 +++++ crates/motiongfx/src/registry.rs | 96 ++++++++++ crates/motiongfx/src/timeline.rs | 28 +-- crates/motiongfx/src/track.rs | 2 +- .../bevy_examples/examples/custom_ease.rs | 2 +- .../bevy_examples/examples/custom_interp.rs | 2 +- examples/bevy_examples/examples/easings.rs | 2 +- .../bevy_examples/examples/hello_world.rs | 2 +- examples/bevy_examples/examples/minimal.rs | 2 +- examples/bevy_examples/examples/recording.rs | 2 +- .../bevy_examples/examples/slide_basic.rs | 2 +- 23 files changed, 358 insertions(+), 205 deletions(-) create mode 100644 crates/motiongfx/src/pipeline/func_pointers.rs create mode 100644 crates/motiongfx/src/registry.rs diff --git a/Cargo.lock b/Cargo.lock index fe0be48..63ff0b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2388,12 +2388,9 @@ dependencies = [ [[package]] name = "field_path" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b15fe157f96af46037f9135145fd540de8a93cf4a32e8d7ba0037796878208a" -dependencies = [ - "hashbrown 0.16.1", -] +checksum = "36ccac07e4c62197b44fd7161abc6bf6b006c8dc725aeca40fd438e62d0f2ada" [[package]] name = "find-msvc-tools" diff --git a/Cargo.toml b/Cargo.toml index 0c0a181..c4843eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ bevy_sprite = { version = "0.18.1", default-features = false } bevy_pbr = { version = "0.18.1", default-features = false } # other -field_path = "0.3" +field_path = "0.4" nonempty = { version = "0.12", default-features = false } [workspace.lints.clippy] diff --git a/crates/bevy_motiongfx/src/interpolation.rs b/crates/bevy_motiongfx/src/interpolation.rs index 33daed3..787a48b 100644 --- a/crates/bevy_motiongfx/src/interpolation.rs +++ b/crates/bevy_motiongfx/src/interpolation.rs @@ -15,7 +15,7 @@ pub trait ActionInterpTimelineExt { T: Interpolation + ThreadSafe; } -impl ActionInterpTimelineExt for TimelineBuilder { +impl ActionInterpTimelineExt for TimelineBuilder { /// Add an [`Action`] with interpolation using /// [`Interpolation::interp`]. fn act_interp( diff --git a/crates/bevy_motiongfx/src/lib.rs b/crates/bevy_motiongfx/src/lib.rs index dac3e6f..6e59f30 100644 --- a/crates/bevy_motiongfx/src/lib.rs +++ b/crates/bevy_motiongfx/src/lib.rs @@ -21,7 +21,7 @@ pub mod prelude { ActionInterpTimelineExt, Interpolation, }; pub use crate::pipeline::{ - BevyPipeline, BevyPipelineRegistry, PipelineRegistryExt, + BevyTimelineBuilder, PipelineRegistryExt, }; pub use crate::register_fields; pub use crate::registry::FieldPathRegisterAppExt; diff --git a/crates/bevy_motiongfx/src/pipeline.rs b/crates/bevy_motiongfx/src/pipeline.rs index 31cb85c..b645f59 100644 --- a/crates/bevy_motiongfx/src/pipeline.rs +++ b/crates/bevy_motiongfx/src/pipeline.rs @@ -1,7 +1,8 @@ use bevy_ecs::component::Mutable; use bevy_ecs::prelude::*; -use motiongfx::pipeline::{bake, sample}; +use motiongfx::pipeline::{Pipeline, PipelineHandle, PipelineKey}; use motiongfx::prelude::*; +use motiongfx::registry::PipelineRegistry; /// Newtype wrapper around [`World`] that is local to this crate, /// allowing [`SubjectSource`] impls without violating the orphan rule. @@ -62,8 +63,7 @@ impl } } -pub type BevyPipelineRegistry = PipelineRegistry; -pub type BevyPipeline = Pipeline; +pub type BevyTimelineBuilder = TimelineBuilder; pub trait PipelineRegistryExt { fn register_component(&mut self) -> PipelineKey @@ -78,23 +78,18 @@ pub trait PipelineRegistryExt { T: Clone + ThreadSafe; } -impl PipelineRegistryExt for BevyPipelineRegistry { +impl PipelineRegistryExt for PipelineRegistry { fn register_component(&mut self) -> PipelineKey where S: Component, T: Clone + ThreadSafe, { - let key = PipelineKey::new::(); - - self.register_unchecked( - key, - BevyPipeline::new( - bake::<_, _, S, T>, - sample::<_, _, S, T>, - ), + let handle = PipelineHandle::::new(); + self.register( + handle, + Pipeline::::new::(), ); - - key + handle.as_key() } #[cfg(feature = "asset")] @@ -103,18 +98,18 @@ impl PipelineRegistryExt for BevyPipelineRegistry { S: bevy_asset::Asset, T: Clone + ThreadSafe, { - use bevy_asset::UntypedAssetId; - - let key = PipelineKey::new::(); - - self.register_unchecked( - key, - BevyPipeline::new( - bake::<_, _, S, T>, - sample::<_, _, S, T>, - ), + let handle = PipelineHandle::< + BevyWorld, + bevy_asset::UntypedAssetId, + S, + T, + >::new(); + self.register( + handle, + Pipeline::::new::< + BevyWorld, + >(), ); - - key + handle.as_key() } } diff --git a/crates/bevy_motiongfx/src/registry.rs b/crates/bevy_motiongfx/src/registry.rs index 156bdbc..1aa8161 100644 --- a/crates/bevy_motiongfx/src/registry.rs +++ b/crates/bevy_motiongfx/src/registry.rs @@ -69,7 +69,7 @@ use crate::world::MotionGfxWorld; /// assert_eq!(accessor.get_ref(&foo), &2.0); /// /// // Get pipeline from the registry. -/// let key = PipelineKey::new::(); +/// let key = PipelineKey::new::(); /// let pipeline = motiongfx.pipeline_registry.get(&key).unwrap(); /// ``` #[macro_export] @@ -178,9 +178,7 @@ impl FieldPathRegisterAppExt for App { let mut motiongfx = self.world_mut().resource_mut::(); - motiongfx - .accessor_registry - .register(field.untyped(), accessor); + motiongfx.accessor_registry.register(field, accessor); motiongfx.pipeline_registry.register_component::(); self @@ -199,9 +197,7 @@ impl FieldPathRegisterAppExt for App { let mut motiongfx = self.world_mut().resource_mut::(); - motiongfx - .accessor_registry - .register(field.untyped(), accessor); + motiongfx.accessor_registry.register(field, accessor); motiongfx.pipeline_registry.register_asset::(); self diff --git a/crates/bevy_motiongfx/src/world.rs b/crates/bevy_motiongfx/src/world.rs index f7c86ee..4f605b7 100644 --- a/crates/bevy_motiongfx/src/world.rs +++ b/crates/bevy_motiongfx/src/world.rs @@ -3,12 +3,12 @@ use core::ops::{Deref, DerefMut}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_platform::collections::HashMap; -use motiongfx::prelude::{FieldAccessorRegistry, Timeline}; +use motiongfx::prelude::*; use crate::MotionGfxSet; use crate::controller::FixedRatePlayer; -use crate::pipeline::{BevyPipelineRegistry, BevyWorld}; -use crate::prelude::RealtimePlayer; +use crate::controller::RealtimePlayer; +use crate::pipeline::BevyWorld; pub struct MotionGfxWorldPlugin; @@ -79,8 +79,8 @@ pub struct MotionGfxWorld { id: TimelineId, pending_timelines: HashMap>, timelines: HashMap>, - pub pipeline_registry: BevyPipelineRegistry, - pub accessor_registry: FieldAccessorRegistry, + pub pipeline_registry: PipelineRegistry, + pub accessor_registry: AccessorRegistry, } impl Default for MotionGfxWorld { diff --git a/crates/motiongfx/README.md b/crates/motiongfx/README.md index 63b88e5..98e1daa 100644 --- a/crates/motiongfx/README.md +++ b/crates/motiongfx/README.md @@ -80,7 +80,7 @@ type SubjectWorld = HashMap<&'static str, f32>; let mut world: SubjectWorld = HashMap::new(); world.insert("x", 0.0); -let accessor_registry = FieldAccessorRegistry::new(); +let accessor_registry = AccessorRegistry::new(); let pipeline_registry = PipelineRegistry::::new(); let mut b = TimelineBuilder::new(); diff --git a/crates/motiongfx/examples/custom_world.rs b/crates/motiongfx/examples/custom_world.rs index 8b3dfc8..53253bc 100644 --- a/crates/motiongfx/examples/custom_world.rs +++ b/crates/motiongfx/examples/custom_world.rs @@ -1,18 +1,18 @@ use std::collections::HashMap; -use motiongfx::pipeline::{bake, sample}; +use motiongfx::pipeline::{Pipeline, PipelineHandle}; use motiongfx::prelude::*; struct World { - accessor_registry: FieldAccessorRegistry, - pipeline_registry: PipelineRegistry, + accessor_registry: AccessorRegistry, + pipeline_registry: PipelineRegistry, subject_world: SubjectWorld, } impl World { pub fn new() -> Self { Self { - accessor_registry: FieldAccessorRegistry::new(), + accessor_registry: AccessorRegistry::new(), pipeline_registry: PipelineRegistry::new(), subject_world: SubjectWorld { world: HashMap::new(), @@ -56,7 +56,7 @@ fn main() { // Register the accessors. register_accessors(&mut world.accessor_registry); - // Regsitre the pipelines. + // Register the pipelines. register_pipelines(&mut world.pipeline_registry); // Spawn in some subjects. @@ -69,7 +69,7 @@ fn main() { .world .insert(Id(1), Subject::Line(Line::default())); - let mut builder = TimelineBuilder::new(); + let mut builder = TimelineBuilder::::new(); // Create the track. let track = [ @@ -191,56 +191,42 @@ impl SubjectSource for SubjectWorld { } } -fn register_pipelines( - pipeline_registry: &mut PipelineRegistry, -) { - pipeline_registry.register_unchecked( - PipelineKey::new::(), - Pipeline::new(bake::<_, _, Point, f32>, sample::<_, _, Point, f32>), - ); +fn register_pipelines(pipeline_registry: &mut PipelineRegistry) { + let handle = + PipelineHandle::::new(); + pipeline_registry + .register(handle, Pipeline::new::()); - pipeline_registry.register_unchecked( - PipelineKey::new::(), - Pipeline::new(bake::<_, _, Line, Point>, sample::<_, _, Line, Point>), - ); + let handle = + PipelineHandle::::new(); + pipeline_registry + .register(handle, Pipeline::new::()); - pipeline_registry.register_unchecked( - PipelineKey::new::(), - Pipeline::new(bake::<_, _, Line, f32>, sample::<_, _, Line, f32>), - ); + let handle = PipelineHandle::::new(); + pipeline_registry + .register(handle, Pipeline::new::()); } -fn register_accessors(accessor_registry: &mut FieldAccessorRegistry) { - // In real use cases, a macro should be used! - // Refer to `bevy_motiongfx` for now... - +fn register_accessors(accessor_registry: &mut AccessorRegistry) { // Point -> f32. accessor_registry - .register_typed(field!(::x), accessor!(::x)); + .register(field!(::x), accessor!(::x)); accessor_registry - .register_typed(field!(::y), accessor!(::y)); + .register(field!(::y), accessor!(::y)); // Line -> Point. accessor_registry - .register_typed(field!(::p0), accessor!(::p0)); + .register(field!(::p0), accessor!(::p0)); accessor_registry - .register_typed(field!(::p1), accessor!(::p1)); + .register(field!(::p1), accessor!(::p1)); // Line -> Point -> f32. - accessor_registry.register_typed( - field!(::p0::x), - accessor!(::p0::x), - ); - accessor_registry.register_typed( - field!(::p0::y), - accessor!(::p0::y), - ); - accessor_registry.register_typed( - field!(::p1::x), - accessor!(::p1::x), - ); - accessor_registry.register_typed( - field!(::p1::y), - accessor!(::p1::y), - ); + accessor_registry + .register(field!(::p0::x), accessor!(::p0::x)); + accessor_registry + .register(field!(::p0::y), accessor!(::p0::y)); + accessor_registry + .register(field!(::p1::x), accessor!(::p1::x)); + accessor_registry + .register(field!(::p1::y), accessor!(::p1::y)); } fn linear_f32(a: &f32, b: &f32, t: f32) -> f32 { diff --git a/crates/motiongfx/src/action.rs b/crates/motiongfx/src/action.rs index 8bb06fc..c596ac4 100644 --- a/crates/motiongfx/src/action.rs +++ b/crates/motiongfx/src/action.rs @@ -118,29 +118,28 @@ pub struct UntypedSubjectId { } impl UntypedSubjectId { - pub fn new(uid: UId) -> Self { + pub const PLACEHOLDER: Self = + Self::placeholder_with_u64(u64::MAX); + + pub const fn new(uid: UId) -> Self { Self { type_id: TypeId::of::(), uid, } } - pub fn placeholder() -> Self { - Self::placeholder_with_u64(0) - } - - pub fn placeholder_with_u64(id: u64) -> Self { + pub const fn placeholder_with_u64(id: u64) -> Self { Self { type_id: TypeId::of::<()>(), uid: UId(id), } } - pub fn type_id(&self) -> TypeId { + pub const fn type_id(&self) -> TypeId { self.type_id } - pub fn uid(&self) -> UId { + pub const fn uid(&self) -> UId { self.uid } } @@ -252,10 +251,8 @@ impl ActionWorld { .get_resource_or_insert_with(|| IdRegistry::new()) .register_instance(target); - let key = ActionKey { - subject_id: UntypedSubjectId::new::(uid), - field, - }; + let key = + ActionKey::new(UntypedSubjectId::new::(uid), field); let world = self.world.spawn(( key, IdType::::new(), diff --git a/crates/motiongfx/src/lib.rs b/crates/motiongfx/src/lib.rs index 0b036e4..9285b2e 100644 --- a/crates/motiongfx/src/lib.rs +++ b/crates/motiongfx/src/lib.rs @@ -6,6 +6,7 @@ extern crate alloc; pub mod action; pub mod ease; pub mod pipeline; +pub mod registry; pub mod sequence; pub mod subject; pub mod timeline; @@ -19,7 +20,6 @@ pub mod prelude { pub use field_path::accessor::{Accessor, UntypedAccessor}; pub use field_path::field; pub use field_path::field::{Field, UntypedField}; - pub use field_path::registry::FieldAccessorRegistry; pub use crate::ThreadSafe; pub use crate::action::{ @@ -27,14 +27,35 @@ pub mod prelude { InterpFn, }; pub use crate::ease; - pub use crate::pipeline::{ - BakeCtx, Pipeline, PipelineKey, PipelineRegistry, SampleCtx, - SubjectSource, + pub use crate::path; + pub use crate::pipeline::{PipelineKey, SubjectSource}; + pub use crate::registry::{ + AccessorRegistry, PipelineRegistry, Registry, }; pub use crate::timeline::{Timeline, TimelineBuilder}; pub use crate::track::{Track, TrackFragment, TrackOrdering}; } +/// See [`field_path::field_accessor!`]. +/// +/// This macro just forwards the tokens to the mentioned macro. +/// +/// ## Example +/// +/// ``` +/// use motiongfx::path; +/// +/// struct Foo(u32); +/// +/// let path = path!(::0); +/// ``` +#[macro_export] +macro_rules! path { + ($($t:tt)*) => { + $crate::field_path::field_accessor!($($t)*) + }; +} + /// Auto trait for types that implements [`Send`] + [`Sync`] + /// `'static`. pub trait ThreadSafe: Send + Sync + 'static {} diff --git a/crates/motiongfx/src/pipeline.rs b/crates/motiongfx/src/pipeline.rs index 576429f..a41c88a 100644 --- a/crates/motiongfx/src/pipeline.rs +++ b/crates/motiongfx/src/pipeline.rs @@ -1,69 +1,99 @@ +pub mod func_pointers; + use core::any::TypeId; use core::marker::PhantomData; -use bevy_ecs::prelude::*; -use bevy_platform::collections::HashMap; -use field_path::registry::FieldAccessorRegistry; +use func_pointers::{BakeFnPtr, SampleFnPtr}; use crate::ThreadSafe; use crate::action::{ ActionClip, ActionKey, ActionWorld, EaseStorage, InterpStorage, SampleMode, Segment, }; +use crate::registry::AccessorRegistry; use crate::subject::SubjectId; use crate::track::Track; -pub struct PipelineHandle -where - I: SubjectId, - S: 'static, - T: 'static, -{ +pub struct PipelineHandle { #[expect(clippy::complexity)] - _marker: PhantomData (I, S, T)>, + _marker: PhantomData (W, I, S, T)>, +} + +impl PipelineHandle { + pub fn new() -> Self + where + W: 'static, + I: SubjectId, + S: 'static, + T: 'static, + { + Self { + _marker: PhantomData, + } + } + + pub fn as_key(&self) -> PipelineKey + where + W: 'static, + I: SubjectId, + S: 'static, + T: 'static, + { + PipelineKey::new::() + } +} + +impl Copy for PipelineHandle {} + +impl Clone for PipelineHandle { + fn clone(&self) -> Self { + *self + } } -impl PipelineHandle +impl Default for PipelineHandle where + W: 'static, I: SubjectId, S: 'static, T: 'static, { - pub fn as_key(&self) -> PipelineKey { - PipelineKey::new::() + fn default() -> Self { + Self::new() } } -/// Uniquely identifies a [`Pipeline`] to bake and sample a target -/// field from a subject's source data structure. +/// Uniquely identifies a [`Pipeline`] by its world, subject, source, +/// and target types. #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, )] pub struct PipelineKey { - /// The [`TypeId`] of the [`SubjectId`]. + world_id: TypeId, subject_id: TypeId, - /// The [`TypeId`] of the source type. source_id: TypeId, - /// The [`TypeId`] of the target type. target_id: TypeId, } impl PipelineKey { - pub fn new() -> Self + pub fn new() -> Self where + W: 'static, I: SubjectId, S: 'static, T: 'static, { Self { + world_id: TypeId::of::(), subject_id: TypeId::of::(), source_id: TypeId::of::(), target_id: TypeId::of::(), } } - pub fn from_action_key(key: ActionKey) -> Self { + pub fn from_action_key(key: ActionKey) -> Self { Self { + world_id: TypeId::of::(), subject_id: key.subject_id().type_id(), source_id: key.field().source_id(), target_id: key.field().target_id(), @@ -81,67 +111,56 @@ pub trait SubjectSource { ) -> Option; } -pub type BakeFn = fn(BakeCtx); -pub type SampleFn = fn(SampleCtx); - +/// A pipeline for baking and sampling actions of type `(I, S, T)`. +/// The world type `W` is erased at storage; it must match at call sites. #[derive(Debug, Clone, Copy)] -pub struct Pipeline { - bake: BakeFn, - sample: SampleFn, +pub struct Pipeline { + bake: BakeFnPtr, + sample: SampleFnPtr, + #[expect(clippy::complexity)] + _marker: PhantomData (I, S, T)>, } -impl Pipeline { - pub fn new(bake: BakeFn, sample: SampleFn) -> Self { - Self { bake, sample } - } - - pub fn bake(&self, ctx: BakeCtx) { - (self.bake)(ctx) +impl Pipeline { + pub fn new() -> Self + where + W: SubjectSource, + I: SubjectId, + S: 'static, + T: Clone + ThreadSafe, + { + Self { + bake: BakeFnPtr::new(bake::), + sample: SampleFnPtr::new(sample::), + _marker: PhantomData, + } } - pub fn sample(&self, ctx: SampleCtx) { - (self.sample)(ctx) + pub fn untyped(&self) -> PipelineUntyped { + PipelineUntyped { + bake: self.bake, + sample: self.sample, + } } } -#[derive(Resource)] -pub struct PipelineRegistry { - pipelines: HashMap>, +#[derive(Debug, Clone, Copy)] +pub struct PipelineUntyped { + bake: BakeFnPtr, + sample: SampleFnPtr, } -impl PipelineRegistry { - pub fn new() -> Self { - Self { - pipelines: HashMap::new(), - } +impl PipelineUntyped { + pub fn bake(&self, ctx: BakeCtx) { + // SAFETY: W matches the W passed to Pipeline::new. + let f = unsafe { self.bake.typed_unchecked::() }; + f(ctx) } - pub fn get(&self, key: &PipelineKey) -> Option<&Pipeline> { - self.pipelines.get(key) - } - - /// Register a pipeline function. - /// - /// Registering the same key twice will result in a replacement. - /// - /// # Note - /// - /// This function assumes that the baker function matches - /// the field that it points towards. Failure to do so will - /// result in a useless baker registry. - pub fn register_unchecked( - &mut self, - key: PipelineKey, - pipeline: Pipeline, - ) -> &mut Self { - self.pipelines.insert(key, pipeline); - self - } -} - -impl Default for PipelineRegistry { - fn default() -> Self { - Self::new() + pub fn sample(&self, ctx: SampleCtx) { + // SAFETY: W matches the W passed to Pipeline::new. + let f = unsafe { self.sample.typed_unchecked::() }; + f(ctx) } } @@ -149,7 +168,7 @@ pub struct BakeCtx<'a, W> { pub world: &'a W, pub track: &'a Track, pub action_world: &'a mut ActionWorld, - pub accessor_registry: &'a FieldAccessorRegistry, + pub accessor_registry: &'a AccessorRegistry, } pub fn bake(ctx: BakeCtx) @@ -160,7 +179,7 @@ where T: Clone + ThreadSafe, { for (key, span) in ctx.track.sequences_spans() { - let Ok(accessor) = + let Some(accessor) = ctx.accessor_registry.get::(key.field()) else { continue; @@ -194,12 +213,10 @@ where } } -impl BakeCtx<'_, W> {} - pub struct SampleCtx<'a, W> { pub world: &'a mut W, pub action_world: &'a ActionWorld, - pub accessor_registry: &'a FieldAccessorRegistry, + pub accessor_registry: &'a AccessorRegistry, } pub fn sample(ctx: SampleCtx) @@ -222,7 +239,7 @@ where for (key, sample_mode, segment, interp, ease) in q.iter(ctx.action_world.world()) { - let Ok(accessor) = + let Some(accessor) = ctx.accessor_registry.get::(key.field()) else { continue; diff --git a/crates/motiongfx/src/pipeline/func_pointers.rs b/crates/motiongfx/src/pipeline/func_pointers.rs new file mode 100644 index 0000000..8ea1853 --- /dev/null +++ b/crates/motiongfx/src/pipeline/func_pointers.rs @@ -0,0 +1,48 @@ +use super::{BakeCtx, SampleCtx}; + +/// A type-erased bake function pointer. +#[derive(Debug, Clone, Copy)] +pub struct BakeFnPtr(*const ()); + +unsafe impl Send for BakeFnPtr {} +unsafe impl Sync for BakeFnPtr {} + +impl BakeFnPtr { + pub const fn new(f: BakeFn) -> Self { + Self(f as *const ()) + } + + /// # Safety + /// + /// `W` must match the type used when constructing this pointer. + pub const unsafe fn typed_unchecked(&self) -> BakeFn { + unsafe { + core::mem::transmute::<*const (), BakeFn>(self.0) + } + } +} + +/// A type-erased sample function pointer. +#[derive(Debug, Clone, Copy)] +pub struct SampleFnPtr(*const ()); + +unsafe impl Send for SampleFnPtr {} +unsafe impl Sync for SampleFnPtr {} + +impl SampleFnPtr { + pub const fn new(f: SampleFn) -> Self { + Self(f as *const ()) + } + + /// # Safety + /// + /// `W` must match the type used when constructing this pointer. + pub const unsafe fn typed_unchecked(&self) -> SampleFn { + unsafe { + core::mem::transmute::<*const (), SampleFn>(self.0) + } + } +} + +pub type BakeFn = fn(BakeCtx<'_, W>); +pub type SampleFn = fn(SampleCtx<'_, W>); diff --git a/crates/motiongfx/src/registry.rs b/crates/motiongfx/src/registry.rs new file mode 100644 index 0000000..2c3f4c0 --- /dev/null +++ b/crates/motiongfx/src/registry.rs @@ -0,0 +1,96 @@ +use bevy_platform::collections::HashMap; +use field_path::accessor::{Accessor, UntypedAccessor}; +use field_path::field::{Field, UntypedField}; + +use crate::pipeline::{ + Pipeline, PipelineHandle, PipelineKey, PipelineUntyped, +}; +use crate::subject::SubjectId; + +pub struct Registry { + pub accessor: AccessorRegistry, + pub pipeline: PipelineRegistry, +} + +pub struct AccessorRegistry { + accessors: HashMap, +} + +impl AccessorRegistry { + pub fn new() -> Self { + Self { + accessors: HashMap::new(), + } + } + + /// Registers a field-accessor pair. Skips fields already registered. + #[inline] + pub fn register( + &mut self, + field: Field, + accessor: Accessor, + ) { + let untyped_field = field.untyped(); + if self.accessors.contains_key(&untyped_field) { + return; + } + + self.accessors.insert(untyped_field, accessor.untyped()); + } + + /// Retrieve a typed [`Accessor`] from the registry. + pub fn get( + &self, + field: &UntypedField, + ) -> Option> { + self.accessors.get(field)?.typed() + } +} + +impl Default for AccessorRegistry { + fn default() -> Self { + Self::new() + } +} + +pub struct PipelineRegistry { + pipelines: HashMap, +} + +impl PipelineRegistry { + pub fn new() -> Self { + Self { + pipelines: HashMap::new(), + } + } + + pub fn get(&self, key: &PipelineKey) -> Option<&PipelineUntyped> { + self.pipelines.get(key) + } + + /// Register a pipeline. Skips pipelines already registered. + pub fn register< + W: 'static, + I: SubjectId, + S: 'static, + T: 'static, + >( + &mut self, + handle: PipelineHandle, + pipeline: Pipeline, + ) -> &mut Self { + let key = handle.as_key(); + if self.pipelines.contains_key(&key) { + return self; + } + + self.pipelines.insert(key, pipeline.untyped()); + self + } +} + +impl Default for PipelineRegistry { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/motiongfx/src/timeline.rs b/crates/motiongfx/src/timeline.rs index e5b2de6..99b621b 100644 --- a/crates/motiongfx/src/timeline.rs +++ b/crates/motiongfx/src/timeline.rs @@ -1,20 +1,18 @@ use core::cmp::Ordering; +use core::marker::PhantomData; use alloc::boxed::Box; use alloc::vec::Vec; use bevy_platform::collections::HashMap; use field_path::field::Field; -use field_path::registry::FieldAccessorRegistry; use crate::ThreadSafe; use crate::action::{ Action, ActionBuilder, ActionId, ActionKey, ActionWorld, InterpActionBuilder, SampleMode, }; -use crate::pipeline::Range; -use crate::pipeline::{ - BakeCtx, PipelineKey, PipelineRegistry, SampleCtx, -}; +use crate::pipeline::{BakeCtx, PipelineKey, Range, SampleCtx}; +use crate::registry::{AccessorRegistry, PipelineRegistry}; use crate::subject::SubjectId; use crate::track::Track; @@ -42,8 +40,8 @@ pub struct Timeline { impl Timeline { pub fn bake_actions( &mut self, - accessor_registry: &FieldAccessorRegistry, - pipeline_registry: &PipelineRegistry, + accessor_registry: &AccessorRegistry, + pipeline_registry: &PipelineRegistry, subject_world: &W, ) { for key in self.pipeline_counts.iter().map(|(key, _)| key) { @@ -220,8 +218,8 @@ impl Timeline { pub fn sample_queued_actions( &self, - accessor_registry: &FieldAccessorRegistry, - pipeline_registry: &PipelineRegistry, + accessor_registry: &AccessorRegistry, + pipeline_registry: &PipelineRegistry, subject_world: &mut W, ) { for key in self.pipeline_counts.iter().map(|(key, _)| key) { @@ -402,19 +400,21 @@ impl Default for QueueCache { } } -pub struct TimelineBuilder { +pub struct TimelineBuilder { action_world: ActionWorld, pipeline_counts: HashMap, tracks: Vec, + _marker: PhantomData W>, } -impl TimelineBuilder { +impl TimelineBuilder { /// Creates an empty timeline builder. pub fn new() -> Self { Self { action_world: ActionWorld::new(), pipeline_counts: HashMap::new(), tracks: Vec::new(), + _marker: PhantomData, } } @@ -430,7 +430,7 @@ impl TimelineBuilder { S: 'static, T: ThreadSafe, { - let key = PipelineKey::new::(); + let key = PipelineKey::new::(); match self.pipeline_counts.get_mut(&key) { Some(count) => *count += 1, @@ -462,7 +462,7 @@ impl TimelineBuilder { /// Remove an [`Action`]. pub fn unact(&mut self, id: ActionId) -> bool { if let Some(key) = self.action_world.remove(id) { - let pipeline_key = PipelineKey::from_action_key(key); + let pipeline_key = PipelineKey::from_action_key::(key); let count = self .pipeline_counts @@ -526,7 +526,7 @@ impl TimelineBuilder { } } -impl Default for TimelineBuilder { +impl Default for TimelineBuilder { fn default() -> Self { Self::new() } diff --git a/crates/motiongfx/src/track.rs b/crates/motiongfx/src/track.rs index aa157ca..1f3c145 100644 --- a/crates/motiongfx/src/track.rs +++ b/crates/motiongfx/src/track.rs @@ -352,7 +352,7 @@ mod tests { fn key(path: &'static str) -> ActionKey { ActionKey::new( - UntypedSubjectId::placeholder(), + UntypedSubjectId::PLACEHOLDER, UntypedField::placeholder_with_path(path), ) } diff --git a/examples/bevy_examples/examples/custom_ease.rs b/examples/bevy_examples/examples/custom_ease.rs index 2b034a8..ceaacdc 100644 --- a/examples/bevy_examples/examples/custom_ease.rs +++ b/examples/bevy_examples/examples/custom_ease.rs @@ -31,7 +31,7 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let track = b .act_interp(cube, field!(::translation::x), |x| { diff --git a/examples/bevy_examples/examples/custom_interp.rs b/examples/bevy_examples/examples/custom_interp.rs index 6130c98..50a7ef3 100644 --- a/examples/bevy_examples/examples/custom_interp.rs +++ b/examples/bevy_examples/examples/custom_interp.rs @@ -33,7 +33,7 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let track = b .act(cube, field!(::translation), |x| { diff --git a/examples/bevy_examples/examples/easings.rs b/examples/bevy_examples/examples/easings.rs index 61a7bc6..10ad786 100644 --- a/examples/bevy_examples/examples/easings.rs +++ b/examples/bevy_examples/examples/easings.rs @@ -71,7 +71,7 @@ fn spawn_timeline( } // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let track = easings .into_iter() diff --git a/examples/bevy_examples/examples/hello_world.rs b/examples/bevy_examples/examples/hello_world.rs index 45807e3..696913e 100644 --- a/examples/bevy_examples/examples/hello_world.rs +++ b/examples/bevy_examples/examples/hello_world.rs @@ -53,7 +53,7 @@ fn spawn_timeline( } // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let mut cube_tracks = Vec::with_capacity(CAPACITY); for w in 0..WIDTH { diff --git a/examples/bevy_examples/examples/minimal.rs b/examples/bevy_examples/examples/minimal.rs index 4dcca8f..c37ef47 100644 --- a/examples/bevy_examples/examples/minimal.rs +++ b/examples/bevy_examples/examples/minimal.rs @@ -48,7 +48,7 @@ fn build_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let track = [ b.act_interp( cube, diff --git a/examples/bevy_examples/examples/recording.rs b/examples/bevy_examples/examples/recording.rs index 10d2eab..d800dab 100644 --- a/examples/bevy_examples/examples/recording.rs +++ b/examples/bevy_examples/examples/recording.rs @@ -107,7 +107,7 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); let track = b .act(cube, field!(::translation), |x| { diff --git a/examples/bevy_examples/examples/slide_basic.rs b/examples/bevy_examples/examples/slide_basic.rs index 9d6e000..22af475 100644 --- a/examples/bevy_examples/examples/slide_basic.rs +++ b/examples/bevy_examples/examples/slide_basic.rs @@ -46,7 +46,7 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = BevyTimelineBuilder::new(); // Generate slide sequences. let slide0 = b From 55ecdfe81ecaaa0010bae04d185a700eb8475cc7 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:59:17 +0800 Subject: [PATCH 04/27] Change `AccessorRegistry::register` to take `FieldAccessor`, use `path!` in examples --- crates/bevy_motiongfx/src/registry.rs | 22 ++++++++------------- crates/motiongfx/examples/custom_world.rs | 24 ++++++++--------------- crates/motiongfx/src/lib.rs | 1 + crates/motiongfx/src/registry.rs | 10 +++++----- 4 files changed, 22 insertions(+), 35 deletions(-) diff --git a/crates/bevy_motiongfx/src/registry.rs b/crates/bevy_motiongfx/src/registry.rs index 1aa8161..1a67807 100644 --- a/crates/bevy_motiongfx/src/registry.rs +++ b/crates/bevy_motiongfx/src/registry.rs @@ -91,8 +91,7 @@ macro_rules! register_fields { $crate::registry::FieldPathRegisterAppExt ::$reg_func::<$source, _>( $app, - ::motiongfx::field_path::field!(<$root>), - ::motiongfx::field_path::accessor!(<$root>), + ::motiongfx::path!(<$root>), ); register_fields!( @@ -115,8 +114,7 @@ macro_rules! register_fields { $crate::registry::FieldPathRegisterAppExt ::$reg_func::<$source, _>( $app, - motiongfx::field_path::field!(<$root>$(::$path)*::$field), - ::motiongfx::field_path::accessor!(<$root>$(::$path)*::$field), + ::motiongfx::path!(<$root>$(::$path)*::$field), ); // Register sub fields. @@ -147,8 +145,7 @@ macro_rules! register_fields { pub trait FieldPathRegisterAppExt { fn register_component_field( &mut self, - field: Field, - accessor: Accessor, + fa: FieldAccessor, ) -> &mut Self where S: Component, @@ -157,8 +154,7 @@ pub trait FieldPathRegisterAppExt { #[cfg(feature = "asset")] fn register_asset_field( &mut self, - field: Field, - accessor: Accessor, + fa: FieldAccessor, ) -> &mut Self where S: bevy_asset::Asset, @@ -168,8 +164,7 @@ pub trait FieldPathRegisterAppExt { impl FieldPathRegisterAppExt for App { fn register_component_field( &mut self, - field: Field, - accessor: Accessor, + fa: FieldAccessor, ) -> &mut Self where S: Component, @@ -178,7 +173,7 @@ impl FieldPathRegisterAppExt for App { let mut motiongfx = self.world_mut().resource_mut::(); - motiongfx.accessor_registry.register(field, accessor); + motiongfx.accessor_registry.register(fa); motiongfx.pipeline_registry.register_component::(); self @@ -187,8 +182,7 @@ impl FieldPathRegisterAppExt for App { #[cfg(feature = "asset")] fn register_asset_field( &mut self, - field: Field, - accessor: Accessor, + fa: FieldAccessor, ) -> &mut Self where S: bevy_asset::Asset, @@ -197,7 +191,7 @@ impl FieldPathRegisterAppExt for App { let mut motiongfx = self.world_mut().resource_mut::(); - motiongfx.accessor_registry.register(field, accessor); + motiongfx.accessor_registry.register(fa); motiongfx.pipeline_registry.register_asset::(); self diff --git a/crates/motiongfx/examples/custom_world.rs b/crates/motiongfx/examples/custom_world.rs index 53253bc..a6dce84 100644 --- a/crates/motiongfx/examples/custom_world.rs +++ b/crates/motiongfx/examples/custom_world.rs @@ -209,24 +209,16 @@ fn register_pipelines(pipeline_registry: &mut PipelineRegistry) { fn register_accessors(accessor_registry: &mut AccessorRegistry) { // Point -> f32. - accessor_registry - .register(field!(::x), accessor!(::x)); - accessor_registry - .register(field!(::y), accessor!(::y)); + accessor_registry.register(path!(::x)); + accessor_registry.register(path!(::y)); // Line -> Point. - accessor_registry - .register(field!(::p0), accessor!(::p0)); - accessor_registry - .register(field!(::p1), accessor!(::p1)); + accessor_registry.register(path!(::p0)); + accessor_registry.register(path!(::p1)); // Line -> Point -> f32. - accessor_registry - .register(field!(::p0::x), accessor!(::p0::x)); - accessor_registry - .register(field!(::p0::y), accessor!(::p0::y)); - accessor_registry - .register(field!(::p1::x), accessor!(::p1::x)); - accessor_registry - .register(field!(::p1::y), accessor!(::p1::y)); + accessor_registry.register(path!(::p0::x)); + accessor_registry.register(path!(::p0::y)); + accessor_registry.register(path!(::p1::x)); + accessor_registry.register(path!(::p1::y)); } fn linear_f32(a: &f32, b: &f32, t: f32) -> f32 { diff --git a/crates/motiongfx/src/lib.rs b/crates/motiongfx/src/lib.rs index 9285b2e..3016c95 100644 --- a/crates/motiongfx/src/lib.rs +++ b/crates/motiongfx/src/lib.rs @@ -20,6 +20,7 @@ pub mod prelude { pub use field_path::accessor::{Accessor, UntypedAccessor}; pub use field_path::field; pub use field_path::field::{Field, UntypedField}; + pub use field_path::field_accessor::FieldAccessor; pub use crate::ThreadSafe; pub use crate::action::{ diff --git a/crates/motiongfx/src/registry.rs b/crates/motiongfx/src/registry.rs index 2c3f4c0..cb4d263 100644 --- a/crates/motiongfx/src/registry.rs +++ b/crates/motiongfx/src/registry.rs @@ -1,6 +1,7 @@ use bevy_platform::collections::HashMap; use field_path::accessor::{Accessor, UntypedAccessor}; -use field_path::field::{Field, UntypedField}; +use field_path::field::UntypedField; +use field_path::field_accessor::FieldAccessor; use crate::pipeline::{ Pipeline, PipelineHandle, PipelineKey, PipelineUntyped, @@ -27,15 +28,14 @@ impl AccessorRegistry { #[inline] pub fn register( &mut self, - field: Field, - accessor: Accessor, + fa: FieldAccessor, ) { - let untyped_field = field.untyped(); + let untyped_field = fa.field.untyped(); if self.accessors.contains_key(&untyped_field) { return; } - self.accessors.insert(untyped_field, accessor.untyped()); + self.accessors.insert(untyped_field, fa.accessor.untyped()); } /// Retrieve a typed [`Accessor`] from the registry. From 968ae82975c1ccf9e2beaa2b9919c5d2ae081a00 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:24:17 +0800 Subject: [PATCH 05/27] Add lazy registration support! --- Cargo.toml | 2 +- crates/bevy_motiongfx/src/interpolation.rs | 18 +- crates/bevy_motiongfx/src/lib.rs | 66 +----- crates/bevy_motiongfx/src/pipeline.rs | 54 +---- crates/bevy_motiongfx/src/registry.rs | 199 ------------------ crates/bevy_motiongfx/src/world.rs | 43 ++-- crates/motiongfx/examples/custom_world.rs | 62 +----- crates/motiongfx/src/registry.rs | 61 ++++-- crates/motiongfx/src/timeline.rs | 66 +++--- .../bevy_examples/examples/custom_ease.rs | 7 +- .../bevy_examples/examples/custom_interp.rs | 7 +- examples/bevy_examples/examples/easings.rs | 10 +- .../bevy_examples/examples/hello_world.rs | 14 +- examples/bevy_examples/examples/minimal.rs | 12 +- examples/bevy_examples/examples/recording.rs | 7 +- .../bevy_examples/examples/slide_basic.rs | 25 ++- 16 files changed, 176 insertions(+), 477 deletions(-) delete mode 100644 crates/bevy_motiongfx/src/registry.rs diff --git a/Cargo.toml b/Cargo.toml index c4843eb..23ab599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ bevy_sprite = { version = "0.18.1", default-features = false } bevy_pbr = { version = "0.18.1", default-features = false } # other -field_path = "0.4" +field_path = "0.4.1" nonempty = { version = "0.12", default-features = false } [workspace.lints.clippy] diff --git a/crates/bevy_motiongfx/src/interpolation.rs b/crates/bevy_motiongfx/src/interpolation.rs index 787a48b..c94cd92 100644 --- a/crates/bevy_motiongfx/src/interpolation.rs +++ b/crates/bevy_motiongfx/src/interpolation.rs @@ -2,34 +2,38 @@ use bevy_math::*; use motiongfx::prelude::*; use motiongfx::subject::SubjectId; -pub trait ActionInterpTimelineExt { +pub trait ActionInterpTimelineExt { fn act_interp( &mut self, target: I, - field: Field, + field_acc: FieldAccessor, action: impl Action, ) -> InterpActionBuilder<'_, T> where + W: SubjectSource, I: SubjectId, S: 'static, - T: Interpolation + ThreadSafe; + T: Interpolation + Clone + ThreadSafe; } -impl ActionInterpTimelineExt for TimelineBuilder { +impl ActionInterpTimelineExt + for TimelineBuilder<'_, W> +{ /// Add an [`Action`] with interpolation using /// [`Interpolation::interp`]. fn act_interp( &mut self, target: I, - field: Field, + field_acc: FieldAccessor, action: impl Action, ) -> InterpActionBuilder<'_, T> where + W: SubjectSource, I: SubjectId, S: 'static, - T: Interpolation + ThreadSafe, + T: Interpolation + Clone + ThreadSafe, { - self.act(target, field, action).with_interp(T::interp) + self.act(target, field_acc, action).with_interp(T::interp) } } diff --git a/crates/bevy_motiongfx/src/lib.rs b/crates/bevy_motiongfx/src/lib.rs index 6e59f30..9a0ddc5 100644 --- a/crates/bevy_motiongfx/src/lib.rs +++ b/crates/bevy_motiongfx/src/lib.rs @@ -10,7 +10,6 @@ use crate::world::MotionGfxWorldPlugin; pub mod controller; pub mod interpolation; pub mod pipeline; -pub mod registry; pub mod world; pub mod prelude { @@ -20,11 +19,7 @@ pub mod prelude { pub use crate::interpolation::{ ActionInterpTimelineExt, Interpolation, }; - pub use crate::pipeline::{ - BevyTimelineBuilder, PipelineRegistryExt, - }; - pub use crate::register_fields; - pub use crate::registry::FieldPathRegisterAppExt; + pub use crate::pipeline::{BevyTimeline, BevyTimelineBuilder}; pub use crate::world::{MotionGfxWorld, TimelineId}; } @@ -48,65 +43,6 @@ impl Plugin for BevyMotionGfxPlugin { .chain(), ); app.add_plugins((MotionGfxWorldPlugin, ControllerPlugin)); - - #[cfg(feature = "transform")] - { - use bevy_transform::components::Transform; - - register_fields!( - app.register_component_field(), - Transform, - ( - translation(x, y, z), - scale(x, y, z), - rotation(x, y, z, w), - ) - ); - } - - #[cfg(feature = "sprite")] - { - use bevy_sprite::prelude::*; - - register_fields!( - app.register_component_field(), - Sprite, - ( - image, - texture_atlas, - color, - flip_x, - flip_y, - custom_size, - rect, - image_mode, - ) - ); - } - - #[cfg(feature = "pbr")] - { - use bevy_pbr::prelude::*; - - register_fields!( - app.register_asset_field(), - StandardMaterial, - ( - base_color, - emissive, - perceptual_roughness, - metallic, - reflectance, - specular_tint, - diffuse_transmission, - specular_transmission, - thickness, - ior, - attenuation_distance, - attenuation_color, - ) - ); - } } } diff --git a/crates/bevy_motiongfx/src/pipeline.rs b/crates/bevy_motiongfx/src/pipeline.rs index b645f59..30888f4 100644 --- a/crates/bevy_motiongfx/src/pipeline.rs +++ b/crates/bevy_motiongfx/src/pipeline.rs @@ -1,8 +1,6 @@ use bevy_ecs::component::Mutable; use bevy_ecs::prelude::*; -use motiongfx::pipeline::{Pipeline, PipelineHandle, PipelineKey}; use motiongfx::prelude::*; -use motiongfx::registry::PipelineRegistry; /// Newtype wrapper around [`World`] that is local to this crate, /// allowing [`SubjectSource`] impls without violating the orphan rule. @@ -63,53 +61,5 @@ impl } } -pub type BevyTimelineBuilder = TimelineBuilder; - -pub trait PipelineRegistryExt { - fn register_component(&mut self) -> PipelineKey - where - S: Component, - T: Clone + ThreadSafe; - - #[cfg(feature = "asset")] - fn register_asset(&mut self) -> PipelineKey - where - S: bevy_asset::Asset, - T: Clone + ThreadSafe; -} - -impl PipelineRegistryExt for PipelineRegistry { - fn register_component(&mut self) -> PipelineKey - where - S: Component, - T: Clone + ThreadSafe, - { - let handle = PipelineHandle::::new(); - self.register( - handle, - Pipeline::::new::(), - ); - handle.as_key() - } - - #[cfg(feature = "asset")] - fn register_asset(&mut self) -> PipelineKey - where - S: bevy_asset::Asset, - T: Clone + ThreadSafe, - { - let handle = PipelineHandle::< - BevyWorld, - bevy_asset::UntypedAssetId, - S, - T, - >::new(); - self.register( - handle, - Pipeline::::new::< - BevyWorld, - >(), - ); - handle.as_key() - } -} +pub type BevyTimeline = Timeline; +pub type BevyTimelineBuilder<'a> = TimelineBuilder<'a, BevyWorld>; diff --git a/crates/bevy_motiongfx/src/registry.rs b/crates/bevy_motiongfx/src/registry.rs deleted file mode 100644 index 1a67807..0000000 --- a/crates/bevy_motiongfx/src/registry.rs +++ /dev/null @@ -1,199 +0,0 @@ -use bevy_app::prelude::*; -use bevy_ecs::component::Mutable; -use bevy_ecs::prelude::*; -use motiongfx::prelude::*; - -use crate::pipeline::PipelineRegistryExt; -use crate::world::MotionGfxWorld; - -// TODO: Move purely the recursive logic back to motiongfx and keep -// the registration logic here. - -/// Recursively register fields. -/// -/// # Example -/// -/// ``` -/// use bevy_ecs::entity::Entity; -/// use bevy_app::App; -/// use bevy_ecs::component::Component; -/// use bevy_motiongfx::BevyMotionGfxPlugin; -/// use bevy_motiongfx::prelude::*; -/// -/// #[derive(Component, Default, Clone)] -/// struct Foo { -/// bar_x: Bar, -/// bar_y: Bar, -/// } -/// -/// #[derive(Clone, Default)] -/// struct Bar { -/// cho_a: Cho, -/// cho_b: Cho, -/// } -/// -/// #[derive(Clone, Default)] -/// struct Cho { -/// bo_c: Bo, -/// bo_d: Bo, -/// } -/// -/// #[derive(Clone, Default)] -/// struct Bo(f32, u32); -/// -/// let mut app = App::new(); -/// app.add_plugins(BevyMotionGfxPlugin); -/// -/// let a = &mut app; -/// register_fields!( -/// a.register_component_field(), -/// Foo, -/// ( -/// bar_x(cho_a(bo_c(0, 1), bo_d(0, 1))), -/// bar_y(cho_b(bo_c(0, 1), bo_d(0, 1))), -/// ) -/// ); -/// -/// let motiongfx = app.world().resource::(); -/// -/// // Get accessor from the registry. -/// let key = field!(::bar_x::cho_a::bo_c::0).untyped(); -/// let accessor = -/// motiongfx.accessor_registry.get::(&key).unwrap(); -/// -/// let mut foo = Foo::default(); -/// -/// assert_eq!(accessor.get_ref(&foo), &foo.bar_x.cho_a.bo_c.0,); -/// -/// *accessor.get_mut(&mut foo) = 2.0; -/// assert_eq!(accessor.get_ref(&foo), &2.0); -/// -/// // Get pipeline from the registry. -/// let key = PipelineKey::new::(); -/// let pipeline = motiongfx.pipeline_registry.get(&key).unwrap(); -/// ``` -#[macro_export] -macro_rules! register_fields { - ( - $app:ident.$reg_func:ident(), - $root:ty $(, $($rest:tt)*)? - ) => { - register_fields!( - $app.$reg_func::<$root>(), - $root $(, $($rest)*)? - ) - }; - - ( - $app:ident.$reg_func:ident::<$source:ty>(), - $root:ty $(, $($rest:tt)*)? - ) => { - $crate::registry::FieldPathRegisterAppExt - ::$reg_func::<$source, _>( - $app, - ::motiongfx::path!(<$root>), - ); - - register_fields!( - @fields $app.$reg_func::<$source>, $root, [] - $(, $($rest)*)? - ); - }; - - // Recursively register all the nested fields! - ( - @fields $app:ident.$reg_func:ident::<$source:ty>, - $root:ty, [$(::$path:tt)*], - ( - $field:tt $(( $($sub_field:tt)+ ))? - $(,$($rest:tt)*)? - ) - ) => { - // Register the current field. - // (translation(x, y, z), rotation, scale) => translation - $crate::registry::FieldPathRegisterAppExt - ::$reg_func::<$source, _>( - $app, - ::motiongfx::path!(<$root>$(::$path)*::$field), - ); - - // Register sub fields. - // (translation(x, y, z), rotation, scale) => (x, y, z) - register_fields!( - @fields $app.$reg_func::<$source>, - $root, [$(::$path)*::$field], - $(( $($sub_field)+ ))? - ); - - // Register the rest of the fields. - // (translation(x, y, z), rotation, scale) => (rotation, scale) - register_fields!( - @fields $app.$reg_func::<$source>, - $root, [$(::$path)*], - $(( $($rest)* ))? - ); - }; - - // There are no fields left! - ( - @fields $app:ident.$reg_func:ident::<$source:ty>, - $root:ty, [$(::$path:tt)*] - $(,)? $(,())? - ) => {}; -} - -pub trait FieldPathRegisterAppExt { - fn register_component_field( - &mut self, - fa: FieldAccessor, - ) -> &mut Self - where - S: Component, - T: Clone + ThreadSafe; - - #[cfg(feature = "asset")] - fn register_asset_field( - &mut self, - fa: FieldAccessor, - ) -> &mut Self - where - S: bevy_asset::Asset, - T: Clone + ThreadSafe; -} - -impl FieldPathRegisterAppExt for App { - fn register_component_field( - &mut self, - fa: FieldAccessor, - ) -> &mut Self - where - S: Component, - T: Clone + ThreadSafe, - { - let mut motiongfx = - self.world_mut().resource_mut::(); - - motiongfx.accessor_registry.register(fa); - motiongfx.pipeline_registry.register_component::(); - - self - } - - #[cfg(feature = "asset")] - fn register_asset_field( - &mut self, - fa: FieldAccessor, - ) -> &mut Self - where - S: bevy_asset::Asset, - T: Clone + ThreadSafe, - { - let mut motiongfx = - self.world_mut().resource_mut::(); - - motiongfx.accessor_registry.register(fa); - motiongfx.pipeline_registry.register_asset::(); - - self - } -} diff --git a/crates/bevy_motiongfx/src/world.rs b/crates/bevy_motiongfx/src/world.rs index 4f605b7..7cc4453 100644 --- a/crates/bevy_motiongfx/src/world.rs +++ b/crates/bevy_motiongfx/src/world.rs @@ -8,7 +8,8 @@ use motiongfx::prelude::*; use crate::MotionGfxSet; use crate::controller::FixedRatePlayer; use crate::controller::RealtimePlayer; -use crate::pipeline::BevyWorld; +use crate::pipeline::{BevyTimeline, BevyWorld}; +use crate::prelude::BevyTimelineBuilder; pub struct MotionGfxWorldPlugin; @@ -77,10 +78,9 @@ fn complete_timelines( #[derive(Resource)] pub struct MotionGfxWorld { id: TimelineId, - pending_timelines: HashMap>, - timelines: HashMap>, - pub pipeline_registry: PipelineRegistry, - pub accessor_registry: AccessorRegistry, + pending_timelines: HashMap>, + timelines: HashMap>, + registry: Registry, } impl Default for MotionGfxWorld { @@ -89,14 +89,26 @@ impl Default for MotionGfxWorld { id: TimelineId(0), pending_timelines: Default::default(), timelines: Default::default(), - pipeline_registry: Default::default(), - accessor_registry: Default::default(), + registry: Default::default(), } } } impl MotionGfxWorld { - pub fn add_timeline(&mut self, timeline: Timeline) -> TimelineId { + pub fn create_builder( + &mut self, + ) -> TimelineBuilder<'_, W> { + TimelineBuilder::new(&mut self.registry) + } + + pub fn create_bevy_builder(&mut self) -> BevyTimelineBuilder<'_> { + TimelineBuilder::new(&mut self.registry) + } + + pub fn add_timeline( + &mut self, + timeline: BevyTimeline, + ) -> TimelineId { let id = self.id; self.pending_timelines.insert(id, MutDetect::new(timeline)); @@ -107,14 +119,17 @@ impl MotionGfxWorld { pub fn remove_timeline( &mut self, id: &TimelineId, - ) -> Option { + ) -> Option { self.timelines .remove(id) .or_else(|| self.pending_timelines.remove(id)) .map(|t| t.take()) } - pub fn get_timeline(&self, id: &TimelineId) -> Option<&Timeline> { + pub fn get_timeline( + &self, + id: &TimelineId, + ) -> Option<&BevyTimeline> { self.timelines .get(id) .or_else(|| self.pending_timelines.get(id)) @@ -124,7 +139,7 @@ impl MotionGfxWorld { pub fn get_timeline_mut( &mut self, id: &TimelineId, - ) -> Option<&mut MutDetect> { + ) -> Option<&mut MutDetect> { self.timelines .get_mut(id) .or_else(|| self.pending_timelines.get_mut(id)) @@ -133,8 +148,7 @@ impl MotionGfxWorld { pub fn load_pending_timelines(&mut self, world: &mut World) { for (id, mut timeline) in self.pending_timelines.drain() { timeline.bake_actions( - &self.accessor_registry, - &self.pipeline_registry, + &self.registry, BevyWorld::from_ref(world), ); self.timelines.insert(id, timeline); @@ -147,8 +161,7 @@ impl MotionGfxWorld { { timeline.queue_actions(); timeline.sample_queued_actions( - &self.accessor_registry, - &self.pipeline_registry, + &self.registry, BevyWorld::from_mut(world), ); timeline.reset(); diff --git a/crates/motiongfx/examples/custom_world.rs b/crates/motiongfx/examples/custom_world.rs index a6dce84..0ccf907 100644 --- a/crates/motiongfx/examples/custom_world.rs +++ b/crates/motiongfx/examples/custom_world.rs @@ -1,19 +1,16 @@ use std::collections::HashMap; -use motiongfx::pipeline::{Pipeline, PipelineHandle}; use motiongfx::prelude::*; struct World { - accessor_registry: AccessorRegistry, - pipeline_registry: PipelineRegistry, + registry: Registry, subject_world: SubjectWorld, } impl World { pub fn new() -> Self { Self { - accessor_registry: AccessorRegistry::new(), - pipeline_registry: PipelineRegistry::new(), + registry: Registry::new(), subject_world: SubjectWorld { world: HashMap::new(), }, @@ -54,11 +51,6 @@ struct Line { fn main() { let mut world = World::new(); - // Register the accessors. - register_accessors(&mut world.accessor_registry); - // Register the pipelines. - register_pipelines(&mut world.pipeline_registry); - // Spawn in some subjects. world .subject_world @@ -69,21 +61,21 @@ fn main() { .world .insert(Id(1), Subject::Line(Line::default())); - let mut builder = TimelineBuilder::::new(); + let mut builder = world.registry.create_builder(); // Create the track. let track = [ builder - .act(Id(0), field!(::x), |x| x + 72.0) + .act(Id(0), path!(::x), |x| x + 72.0) .with_interp(linear_f32) .play(1.0), [ builder - .act(Id(1), field!(::p0::y), |y| y + 42.0) + .act(Id(1), path!(::p0::y), |y| y + 42.0) .with_interp(linear_f32) .play(2.0), builder - .act(Id(1), field!(::p1), |_| Point { + .act(Id(1), path!(::p1), |_| Point { x: 6.0, y: 6.0, }) @@ -99,11 +91,7 @@ fn main() { let mut timeline = builder.compile(); // Bake actions into segments. - timeline.bake_actions( - &world.accessor_registry, - &world.pipeline_registry, - &world.subject_world, - ); + timeline.bake_actions(&world.registry, &world.subject_world); // Change the target time. timeline.set_target_time(1.5); @@ -114,8 +102,7 @@ fn main() { // Queue and sample the actions. timeline.queue_actions(); timeline.sample_queued_actions( - &world.accessor_registry, - &world.pipeline_registry, + &world.registry, &mut world.subject_world, ); @@ -139,8 +126,7 @@ fn main() { // Queue and sample the actions. timeline.queue_actions(); timeline.sample_queued_actions( - &world.accessor_registry, - &world.pipeline_registry, + &world.registry, &mut world.subject_world, ); @@ -191,36 +177,6 @@ impl SubjectSource for SubjectWorld { } } -fn register_pipelines(pipeline_registry: &mut PipelineRegistry) { - let handle = - PipelineHandle::::new(); - pipeline_registry - .register(handle, Pipeline::new::()); - - let handle = - PipelineHandle::::new(); - pipeline_registry - .register(handle, Pipeline::new::()); - - let handle = PipelineHandle::::new(); - pipeline_registry - .register(handle, Pipeline::new::()); -} - -fn register_accessors(accessor_registry: &mut AccessorRegistry) { - // Point -> f32. - accessor_registry.register(path!(::x)); - accessor_registry.register(path!(::y)); - // Line -> Point. - accessor_registry.register(path!(::p0)); - accessor_registry.register(path!(::p1)); - // Line -> Point -> f32. - accessor_registry.register(path!(::p0::x)); - accessor_registry.register(path!(::p0::y)); - accessor_registry.register(path!(::p1::x)); - accessor_registry.register(path!(::p1::y)); -} - fn linear_f32(a: &f32, b: &f32, t: f32) -> f32 { *a + (*b - *a) * t } diff --git a/crates/motiongfx/src/registry.rs b/crates/motiongfx/src/registry.rs index cb4d263..abc9aba 100644 --- a/crates/motiongfx/src/registry.rs +++ b/crates/motiongfx/src/registry.rs @@ -3,9 +3,11 @@ use field_path::accessor::{Accessor, UntypedAccessor}; use field_path::field::UntypedField; use field_path::field_accessor::FieldAccessor; +use crate::ThreadSafe; use crate::pipeline::{ Pipeline, PipelineHandle, PipelineKey, PipelineUntyped, }; +use crate::prelude::{SubjectSource, TimelineBuilder}; use crate::subject::SubjectId; pub struct Registry { @@ -13,6 +15,40 @@ pub struct Registry { pub pipeline: PipelineRegistry, } +impl Registry { + pub fn new() -> Self { + Self { + accessor: AccessorRegistry::new(), + pipeline: PipelineRegistry::new(), + } + } + + pub fn register( + &mut self, + field_acc: FieldAccessor, + ) where + W: SubjectSource + 'static, + I: SubjectId, + S: 'static, + T: Clone + ThreadSafe, + { + self.accessor.register(field_acc); + self.pipeline.register::(); + } + + pub fn create_builder( + &mut self, + ) -> TimelineBuilder<'_, W> { + TimelineBuilder::new(self) + } +} + +impl Default for Registry { + fn default() -> Self { + Self::new() + } +} + pub struct AccessorRegistry { accessors: HashMap, } @@ -28,14 +64,15 @@ impl AccessorRegistry { #[inline] pub fn register( &mut self, - fa: FieldAccessor, + field_acc: FieldAccessor, ) { - let untyped_field = fa.field.untyped(); + let untyped_field = field_acc.field.untyped(); if self.accessors.contains_key(&untyped_field) { return; } - self.accessors.insert(untyped_field, fa.accessor.untyped()); + self.accessors + .insert(untyped_field, field_acc.accessor.untyped()); } /// Retrieve a typed [`Accessor`] from the registry. @@ -69,22 +106,20 @@ impl PipelineRegistry { } /// Register a pipeline. Skips pipelines already registered. - pub fn register< - W: 'static, + pub fn register(&mut self) -> &mut Self + where + W: SubjectSource + 'static, I: SubjectId, S: 'static, - T: 'static, - >( - &mut self, - handle: PipelineHandle, - pipeline: Pipeline, - ) -> &mut Self { - let key = handle.as_key(); + T: Clone + ThreadSafe, + { + let key = PipelineHandle::::new().as_key(); if self.pipelines.contains_key(&key) { return self; } - self.pipelines.insert(key, pipeline.untyped()); + self.pipelines + .insert(key, Pipeline::::new::().untyped()); self } } diff --git a/crates/motiongfx/src/timeline.rs b/crates/motiongfx/src/timeline.rs index 99b621b..736ecaa 100644 --- a/crates/motiongfx/src/timeline.rs +++ b/crates/motiongfx/src/timeline.rs @@ -4,19 +4,21 @@ use core::marker::PhantomData; use alloc::boxed::Box; use alloc::vec::Vec; use bevy_platform::collections::HashMap; -use field_path::field::Field; +use field_path::field_accessor::FieldAccessor; use crate::ThreadSafe; use crate::action::{ Action, ActionBuilder, ActionId, ActionKey, ActionWorld, InterpActionBuilder, SampleMode, }; -use crate::pipeline::{BakeCtx, PipelineKey, Range, SampleCtx}; -use crate::registry::{AccessorRegistry, PipelineRegistry}; +use crate::pipeline::{ + BakeCtx, PipelineKey, Range, SampleCtx, SubjectSource, +}; +use crate::registry::Registry; use crate::subject::SubjectId; use crate::track::Track; -pub struct Timeline { +pub struct Timeline { action_world: ActionWorld, pipeline_counts: Box<[(PipelineKey, u32)]>, /// Track length is guaranteed to be at least 1 by construction. @@ -35,17 +37,17 @@ pub struct Timeline { curr_index: usize, /// The index of the target track. target_index: usize, + _marker: PhantomData W>, } -impl Timeline { - pub fn bake_actions( +impl Timeline { + pub fn bake_actions( &mut self, - accessor_registry: &AccessorRegistry, - pipeline_registry: &PipelineRegistry, + registry: &Registry, subject_world: &W, ) { for key in self.pipeline_counts.iter().map(|(key, _)| key) { - let Some(pipeline) = pipeline_registry.get(key) else { + let Some(pipeline) = registry.pipeline.get(key) else { continue; }; @@ -54,7 +56,7 @@ impl Timeline { world: subject_world, track, action_world: &mut self.action_world, - accessor_registry, + accessor_registry: ®istry.accessor, }) } } @@ -216,21 +218,20 @@ impl Timeline { self.curr_time = self.target_time; } - pub fn sample_queued_actions( + pub fn sample_queued_actions( &self, - accessor_registry: &AccessorRegistry, - pipeline_registry: &PipelineRegistry, + registry: &Registry, subject_world: &mut W, ) { for key in self.pipeline_counts.iter().map(|(key, _)| key) { - let Some(pipeline) = pipeline_registry.get(key) else { + let Some(pipeline) = registry.pipeline.get(key) else { continue; }; pipeline.sample(SampleCtx { world: subject_world, action_world: &self.action_world, - accessor_registry, + accessor_registry: ®istry.accessor, }); } } @@ -242,7 +243,7 @@ impl Timeline { } // Getter methods. -impl Timeline { +impl Timeline { /// Returns the current queue cache. #[inline] pub fn queue_cache(&self) -> &QueueCache { @@ -316,7 +317,7 @@ impl Timeline { } // Setter methods. -impl Timeline { +impl Timeline { /// Set the target time of the current track, clamping the value /// within \[0.0..=track.duration\] pub fn set_target_time(&mut self, target_time: f32) -> &mut Self { @@ -400,17 +401,19 @@ impl Default for QueueCache { } } -pub struct TimelineBuilder { +pub struct TimelineBuilder<'a, W> { + registry: &'a mut Registry, action_world: ActionWorld, pipeline_counts: HashMap, tracks: Vec, _marker: PhantomData W>, } -impl TimelineBuilder { +impl<'a, W: 'static> TimelineBuilder<'a, W> { /// Creates an empty timeline builder. - pub fn new() -> Self { + pub fn new(registry: &'a mut Registry) -> Self { Self { + registry, action_world: ActionWorld::new(), pipeline_counts: HashMap::new(), tracks: Vec::new(), @@ -422,14 +425,17 @@ impl TimelineBuilder { pub fn act( &mut self, target: I, - field: Field, + field_acc: FieldAccessor, action: impl Action, ) -> ActionBuilder<'_, T> where + W: SubjectSource + 'static, I: SubjectId, S: 'static, - T: ThreadSafe, + T: Clone + ThreadSafe, { + let field = field_acc.field; + self.registry.register::(field_acc); let key = PipelineKey::new::(); match self.pipeline_counts.get_mut(&key) { @@ -446,15 +452,16 @@ impl TimelineBuilder { pub fn act_step( &mut self, target: I, - field: Field, + field_acc: FieldAccessor, action: impl Action, ) -> InterpActionBuilder<'_, T> where + W: SubjectSource + 'static, I: SubjectId, S: 'static, T: Clone + ThreadSafe, { - self.act(target, field, action).with_interp(|a, b, t| { + self.act(target, field_acc, action).with_interp(|a, b, t| { if t < 1.0 { a.clone() } else { b.clone() } }) } @@ -498,7 +505,7 @@ impl TimelineBuilder { /// ## Panic /// /// Panics if the track is empty. - pub fn compile(self) -> Timeline { + pub fn compile(self) -> Timeline { debug_assert!( !self.tracks.is_empty(), "Track cannot be empty!" @@ -516,21 +523,16 @@ impl TimelineBuilder { target_time: 0.0, curr_index: 0, target_index: 0, + _marker: PhantomData, } } /// Similar to [`Self::compile()`] but return `None` instead of /// panicking. - pub fn try_compile(self) -> Option { + pub fn try_compile(self) -> Option> { (!self.tracks.is_empty()).then_some(self.compile()) } } -impl Default for TimelineBuilder { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod tests {} diff --git a/examples/bevy_examples/examples/custom_ease.rs b/examples/bevy_examples/examples/custom_ease.rs index ceaacdc..c5bba36 100644 --- a/examples/bevy_examples/examples/custom_ease.rs +++ b/examples/bevy_examples/examples/custom_ease.rs @@ -31,10 +31,10 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = b - .act_interp(cube, field!(::translation::x), |x| { + .act_interp(cube, path!(::translation::x), |x| { x + 10.0 }) // A custom 10 step easing. @@ -44,8 +44,9 @@ fn spawn_timeline( b.add_tracks(track); + let timeline = b.compile(); commands.spawn(( - motiongfx.add_timeline(b.compile()), + motiongfx.add_timeline(timeline), RealtimePlayer::new().with_playing(true), )); } diff --git a/examples/bevy_examples/examples/custom_interp.rs b/examples/bevy_examples/examples/custom_interp.rs index 50a7ef3..51d65a6 100644 --- a/examples/bevy_examples/examples/custom_interp.rs +++ b/examples/bevy_examples/examples/custom_interp.rs @@ -33,10 +33,10 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = b - .act(cube, field!(::translation), |x| { + .act(cube, path!(::translation), |x| { x + Vec3::ZERO.with_x(10.0).with_z(1.0) }) .with_interp(|start, end, t| arc_lerp_3d(*start, *end, t)) @@ -46,8 +46,9 @@ fn spawn_timeline( b.add_tracks(track); + let timeline = b.compile(); commands.spawn(( - motiongfx.add_timeline(b.compile()), + motiongfx.add_timeline(timeline), RealtimePlayer::new(), )); } diff --git a/examples/bevy_examples/examples/easings.rs b/examples/bevy_examples/examples/easings.rs index 10ad786..5bee775 100644 --- a/examples/bevy_examples/examples/easings.rs +++ b/examples/bevy_examples/examples/easings.rs @@ -71,24 +71,23 @@ fn spawn_timeline( } // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = easings .into_iter() .enumerate() - // .zip(easings) .map(|(i, ease_fn)| { [ b.act_interp( spheres[i], - field!(::translation::x), + path!(::translation::x), |x| x + 10.0, ) .with_ease(ease_fn) .play(1.0), b.act_interp( sphere_mats[i], - field!(::emissive), + path!(::emissive), move |_| red, ) .with_ease(ease_fn) @@ -100,8 +99,9 @@ fn spawn_timeline( b.add_tracks(track.compile()); + let timeline = b.compile(); commands.spawn(( - motiongfx.add_timeline(b.compile()), + motiongfx.add_timeline(timeline), RealtimePlayer::new().with_playing(true), )); } diff --git a/examples/bevy_examples/examples/hello_world.rs b/examples/bevy_examples/examples/hello_world.rs index 696913e..8bc3bb8 100644 --- a/examples/bevy_examples/examples/hello_world.rs +++ b/examples/bevy_examples/examples/hello_world.rs @@ -53,7 +53,7 @@ fn spawn_timeline( } // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let mut cube_tracks = Vec::with_capacity(CAPACITY); for w in 0..WIDTH { @@ -64,23 +64,21 @@ fn spawn_timeline( let circ_ease = ease::circ::ease_in_out; let track = [ - b.act_interp( - cube, - field!(::scale), - |_| Vec3::splat(0.9), - ) + b.act_interp(cube, path!(::scale), |_| { + Vec3::splat(0.9) + }) .with_ease(circ_ease) .play(1.0), b.act_interp( cube, - field!(::translation::x), + path!(::translation::x), |x| x + 1.0, ) .with_ease(circ_ease) .play(1.0), b.act_interp( cube, - field!(::rotation), + path!(::rotation), |_| { Quat::from_euler( EulerRot::XYZ, diff --git a/examples/bevy_examples/examples/minimal.rs b/examples/bevy_examples/examples/minimal.rs index c37ef47..00fb52f 100644 --- a/examples/bevy_examples/examples/minimal.rs +++ b/examples/bevy_examples/examples/minimal.rs @@ -48,17 +48,15 @@ fn build_timeline( .id(); // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = [ - b.act_interp( - cube, - field!(::translation::x), - |x| x + 6.0, - ) + b.act_interp(cube, path!(::translation::x), |x| { + x + 6.0 + }) .play(1.0), b.act_interp( material.untyped().id(), - field!(::base_color), + path!(::base_color), |_| Srgba::RED.into(), ) .play(1.0), diff --git a/examples/bevy_examples/examples/recording.rs b/examples/bevy_examples/examples/recording.rs index d800dab..4d77172 100644 --- a/examples/bevy_examples/examples/recording.rs +++ b/examples/bevy_examples/examples/recording.rs @@ -107,10 +107,10 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = b - .act(cube, field!(::translation), |x| { + .act(cube, path!(::translation), |x| { x + Vec3::ZERO.with_x(10.0).with_z(1.0) }) .with_interp(|start, end, t| arc_lerp_3d(*start, *end, t)) @@ -119,8 +119,9 @@ fn spawn_timeline( b.add_tracks(track); + let timeline = b.compile(); commands.spawn(( - motiongfx.add_timeline(b.compile()), + motiongfx.add_timeline(timeline), FixedRatePlayer::new(144), )); } diff --git a/examples/bevy_examples/examples/slide_basic.rs b/examples/bevy_examples/examples/slide_basic.rs index 22af475..da539ed 100644 --- a/examples/bevy_examples/examples/slide_basic.rs +++ b/examples/bevy_examples/examples/slide_basic.rs @@ -21,14 +21,16 @@ fn spawn_timeline( const X_OFFSET: f32 = 2.0; // Spawn 3d models. + let cube_handle = materials.add(StandardMaterial { + base_color: palettes::tailwind::LIME_200.into(), + ..default() + }); + let cube_mat = cube_handle.id().untyped(); let cube = commands .spawn(( Mesh3d(meshes.add(Cuboid::default())), Transform::default().with_scale(Vec3::splat(0.0)), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: palettes::tailwind::LIME_200.into(), - ..default() - })), + MeshMaterial3d(cube_handle), )) .id(); @@ -46,11 +48,11 @@ fn spawn_timeline( .id(); // Build the timeline. - let mut b = BevyTimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); // Generate slide sequences. let slide0 = b - .act_interp(cube, field!(::scale), |_| Vec3::ONE) + .act_interp(cube, path!(::scale), |_| Vec3::ONE) .with_ease(ease::cubic::ease_out) .play(1.0) .compile(); @@ -59,21 +61,21 @@ fn spawn_timeline( [ b.act_interp( cube, - field!(::translation::x), + path!(::translation::x), move |_| -X_OFFSET, ) .with_ease(ease::cubic::ease_out) .play(1.0), b.act_interp( - cube, - field!(::base_color), + cube_mat, + path!(::base_color), move |_| palettes::tailwind::ZINC_700.into(), ) .with_ease(ease::cubic::ease_out) .play(1.0), ] .ord_all(), - b.act_interp(sphere, field!(::scale), |_| { + b.act_interp(sphere, path!(::scale), |_| { Vec3::ONE }) .with_ease(ease::cubic::ease_out) @@ -84,8 +86,9 @@ fn spawn_timeline( b.add_tracks([slide0, slide1]); + let timeline = b.compile(); commands.spawn(( - motiongfx.add_timeline(b.compile()), + motiongfx.add_timeline(timeline), RealtimePlayer::new().with_playing(true), )); } From 5583b2064b6b6a6873d1a5845dc32b1c81666e91 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 1 May 2026 10:24:34 +0800 Subject: [PATCH 06/27] Fix `bevy_motiongfx` README --- crates/bevy_motiongfx/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_motiongfx/README.md b/crates/bevy_motiongfx/README.md index 0fb085d..5c16fc1 100644 --- a/crates/bevy_motiongfx/README.md +++ b/crates/bevy_motiongfx/README.md @@ -48,9 +48,9 @@ fn build_timeline( .id(); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = b - .act_interp(entity, field!(::translation::x), |x| { + .act_interp(entity, path!(::translation::x), |x| { x + 6.0 }) .play(1.0) @@ -82,12 +82,12 @@ fn build_timeline( commands.spawn(MeshMaterial3d(material.clone())); // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); let track = b .act_interp( // AssetIds must be type-erased. material.untyped().id(), - field!(::base_color), + path!(::base_color), |_| Srgba::RED.into(), ) .play(1.0) @@ -115,7 +115,7 @@ fn build_timeline( mut motiongfx: ResMut, ) { // Build the timeline. - let mut b = TimelineBuilder::new(); + let mut b = motiongfx.create_bevy_builder(); // Add tracks here... let timeline = b.compile(); From da5dca5754fe7d76c138e69f1ba751b1176132b8 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 1 May 2026 15:08:11 +0800 Subject: [PATCH 07/27] Remove old field/accessor exports from prelude, add TODO in timeline --- crates/motiongfx/src/lib.rs | 4 ---- crates/motiongfx/src/timeline.rs | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/motiongfx/src/lib.rs b/crates/motiongfx/src/lib.rs index 3016c95..2d4377c 100644 --- a/crates/motiongfx/src/lib.rs +++ b/crates/motiongfx/src/lib.rs @@ -16,10 +16,6 @@ pub mod track; pub use field_path; pub mod prelude { - pub use field_path::accessor; - pub use field_path::accessor::{Accessor, UntypedAccessor}; - pub use field_path::field; - pub use field_path::field::{Field, UntypedField}; pub use field_path::field_accessor::FieldAccessor; pub use crate::ThreadSafe; diff --git a/crates/motiongfx/src/timeline.rs b/crates/motiongfx/src/timeline.rs index 736ecaa..9cb1a71 100644 --- a/crates/motiongfx/src/timeline.rs +++ b/crates/motiongfx/src/timeline.rs @@ -506,6 +506,7 @@ impl<'a, W: 'static> TimelineBuilder<'a, W> { /// /// Panics if the track is empty. pub fn compile(self) -> Timeline { + // TODO(nixon): What happens when track is empty? debug_assert!( !self.tracks.is_empty(), "Track cannot be empty!" From aaad04e3a180c1627d820ed33ca2a879dbd60724 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 1 May 2026 15:08:21 +0800 Subject: [PATCH 08/27] Rewrite motiongfx README for beginner friendliness Closes #98 --- crates/motiongfx/README.md | 405 +++++++++++++++---------------------- 1 file changed, 166 insertions(+), 239 deletions(-) diff --git a/crates/motiongfx/README.md b/crates/motiongfx/README.md index 98e1daa..035728e 100644 --- a/crates/motiongfx/README.md +++ b/crates/motiongfx/README.md @@ -24,95 +24,199 @@ modular foundation for procedural animations. - **Batteries included**: Packed with common easing and interpolation functions. -## Core Concepts +## Quick Start -### Timeline +```rust +use motiongfx::prelude::*; -`Timeline` is a top-level structure that coordinates a sequence of -tracks and their associated actions. Each track acts like a -checkpoint, allowing animations to be grouped into discrete blocks -(especially useful for creating slides). +// The world holds all subject's `f32` values. +struct World(Vec); -A `Track` represents sequences of actions in chronological order, each -with a defined start time and duration. Tracks ensure that actions -within them are played in the correct temporal order. +// Tell MotionGfx how to read and write `f32` values in `World`. +impl SubjectSource for World { + fn get_source(&self, id: usize) -> Option<&f32> { + self.0.get(id) + } -```rust -use motiongfx::prelude::*; + fn apply_source( + &mut self, + id: usize, + f: impl FnOnce(&mut f32) -> R, + ) -> Option { + self.0.get_mut(id).map(f) + } +} + +let mut world = World(vec![0.0]); -// `Timeline` can only be created via a `TimelineBuilder`. -let mut b = TimelineBuilder::new(); -// To create a track, you first have to create the actions. +// The registry tracks which types are animated. +let mut registry = Registry::new(); +let mut b = registry.create_builder::(); + +let id = 0; +// Create an action with: id, field path, action fn. let action = b - // Create an action with: - // id field path action fn - .act("x", field!(), |x| x + 1.0) + // Animate subject 0 from its current value to +10.0 + .act(id, path!(), |x| x + 10.0) // Every action needs an interpolation function. - .with_interp(|&a, &b, t| a + (b - a) * t) - // An optional easing function can be added. - .with_ease(ease::cubic::ease_in_out); + .with_interp(|a, b, t| a + (b - a) * t); -// Once an action is created, it can be "played" into a -// `TrackFragment` with a given duration. +// "Play" the action into a `TrackFragment` with a duration. let frag = action.play(1.0); -// Which can then be compiled into a `Track`. +// Compile into a `Track` (see Track Ordering for composing fragments). let track = frag.compile(); -// 1 or more tracks can be added to the builder to create a timeline. b.add_tracks(track); -let timeline = b.compile(); +let mut timeline = b.compile(); + +// Bake must run once before sampling. +timeline.bake_actions(®istry, &world); + +// Sample at t = 0.5 - world.0[0] should now be 5.0. +timeline.set_target_time(0.5); +timeline.queue_actions(); +timeline.sample_queued_actions(®istry, &mut world); + +assert!((world.0[0] - 5.0).abs() < f32::EPSILON); ``` -#### Bake and Sample Timeline +## Creating your first animation -Once a timeline is created, it is ready for baking and sampling. Bake -must happen before sample. Otherwise, sampling it will be a no-op. +### The World -Registries must be created to perform baking/sampling. For more info -about registries, see below. +Think of a **world** as a container for everything you want to +animate. Each item inside it (called a **subject**) has an ID so +MotionGfx can find it. To connect your world to MotionGfx, implement +`SubjectSource` with two methods: one to read a subject by ID, and +one to write to it. ```rust -use std::collections::HashMap; use motiongfx::prelude::*; -type SubjectWorld = HashMap<&'static str, f32>; +// A Vec is a simple world. Each f32 value is a subject, accessed by its index. +struct World(Vec); + +impl SubjectSource for World { + fn get_source(&self, id: usize) -> Option<&f32> { + self.0.get(id) + } + + fn apply_source( + &mut self, + id: usize, + f: impl FnOnce(&mut f32) -> R, + ) -> Option { + self.0.get_mut(id).map(f) + } +} +``` + +### The Registry + +The `Registry` keeps track of how to animate your types behind the +scenes. MotionGfx is type-erased at runtime, so it needs the registry +to know how to read and write each field. You don't need to set it up +manually, just create one and pass it to the builder. Registration +happens automatically the first time you add an animation. + +```rust +# use motiongfx::prelude::*; +let mut registry = Registry::new(); +``` -let mut world: SubjectWorld = HashMap::new(); -world.insert("x", 0.0); -let accessor_registry = AccessorRegistry::new(); -let pipeline_registry = PipelineRegistry::::new(); +### The Timeline Builder -let mut b = TimelineBuilder::new(); +The `TimelineBuilder` is where you describe your animations. Create +one from the registry, typed to your world: + +```rust +# use motiongfx::prelude::*; +# let mut registry = Registry::new(); +// Using `()` as a placeholder world type for this example. +let mut b = registry.create_builder::<()>(); +``` + +### Building the Timeline + +Animations are built up in layers: + +1. An **action** says what to animate and how to transform it. +2. A **track fragment** gives the action a duration by calling `.play(seconds)`. +3. A **track** is one or more fragments compiled together. You can + order fragments before compiling (see [Track Ordering](#track-ordering)). +4. A **timeline** combines all your tracks into one playable sequence. + +```rust +# use motiongfx::prelude::*; +# struct World(Vec); +# impl SubjectSource for World { +# fn get_source(&self, id: usize) -> Option<&f32> { self.0.get(id) } +# fn apply_source(&mut self, id: usize, f: impl FnOnce(&mut f32) -> R) -> Option { self.0.get_mut(id).map(f) } +# } +# let mut registry = Registry::new(); +# let mut b = registry.create_builder::(); +let id = 0_usize; +// Act: animate subject 0 from its current value to +10.0. let action = b - .act("x", field!(), |x| x + 1.0) - .with_interp(|&a, &b, t| a + (b - a) * t); + .act(id, path!(), |x| x + 10.0) + // Every action needs an interpolation function. + .with_interp(|a: &f32, b: &f32, t| a + (b - a) * t) + // An optional easing function can be added. + .with_ease(ease::cubic::ease_in_out); -let track = action.play(1.0).compile(); -b.add_tracks(track); +// Play: turn the action into a fragment with a 1-second duration. +let frag = action.play(1.0); + +// Compile the fragment into a Track. +let track = frag.compile(); +// Add the track and compile into a Timeline. +b.add_tracks(track); let mut timeline = b.compile(); +``` + +### Bake and Sample + +Before playing an animation, you need to **bake** it. Baking reads +the starting values from your world and prepares the animation data. +This only needs to happen once, right after building the timeline. -// Bake actions into segments. -timeline.bake_actions( - &accessor_registry, - &pipeline_registry, - &world, -); +To advance the animation, set a target time, then **queue** and +**sample**. Queuing figures out which actions are active at that +time, and sampling writes the new values back into your world. -// Actions needs to be queued before it can be sampled. +A timeline can also have multiple tracks, each acting as a chapter. +Use `set_target_track` to jump between them. + +```rust +# use motiongfx::prelude::*; +# struct World(Vec); +# impl SubjectSource for World { +# fn get_source(&self, id: usize) -> Option<&f32> { self.0.get(id) } +# fn apply_source(&mut self, id: usize, f: impl FnOnce(&mut f32) -> R) -> Option { self.0.get_mut(id).map(f) } +# } +# let mut world = World(vec![0.0]); +# let mut registry = Registry::new(); +# let mut b = registry.create_builder::(); +# let track = b.act(0_usize, path!(), |x| x + 10.0).with_interp(|a: &f32, b: &f32, t| a + (b - a) * t).play(1.0).compile(); +# b.add_tracks(track); +# let mut timeline = b.compile(); +// Bake once after building the timeline. +timeline.bake_actions(®istry, &world); + +// Set target time, queue, then sample. +timeline.set_target_time(0.5); timeline.queue_actions(); -timeline.sample_queued_actions( - &accessor_registry, - &pipeline_registry, - &mut world, -); +timeline.sample_queued_actions(®istry, &mut world); + +assert!((world.0[0] - 5.0).abs() < f32::EPSILON); ``` ### Track Ordering -`TrackFragment`s can be ordered using track ordering trait or -functions. There are 4 ways to order track fragments: +You can control how fragments play relative to each other. There are +4 ordering combinators: #### 1. Chain @@ -129,7 +233,7 @@ let f = [f0, f1].ord_chain(); // let f = chain([f0, f1]); ``` -Chaining runs `f1` after `f0` finishes. +`f1` plays after `f0` finishes. #### 2. All @@ -142,8 +246,8 @@ let f1 = TrackFragment::new(); let f = [f0, f1].ord_all(); ``` -All runs `f0` and `f1` concurrently and waits for all of them to -finish. +`f0` and `f1` play at the same time, and the result finishes when +both are done. #### 3. Any @@ -156,7 +260,8 @@ let f1 = TrackFragment::new(); let f = [f0, f1].ord_any(); ``` -Any runs `f0` and `f1` concurrenly and wait for any of them to finish. +`f0` and `f1` play at the same time, and the result finishes when +either one is done. #### 4. Flow @@ -169,186 +274,8 @@ let f1 = TrackFragment::new(); let f = [f0, f1].ord_flow(0.5); ``` -Flow runs `f1` after `f0` with a fixed delay time rather than waiting -for `f0` to finish. - -### Registries - -Registries are used to perform reflection and safely erase types. - -#### Field Accessor Regisry - -The `FieldAccessorRegistry` maintains a mapping between animatable -fields and their corresponding accessors, enabling MotionGfx to read -and write values on arbitrary data structures in a type-safe yet -dynamic way. - -```rust -use motiongfx::prelude::*; - -#[derive(Debug, Clone, Copy)] -struct Subject(f32); - -let mut accessor_registry = FieldAccessorRegistry::new(); -accessor_registry.register_typed( - field!(::0), - accessor!(::0) -); -``` - -#### Pipeline Registry - -Pipelines handle the baking of actions and the sampling of animation -segments for playback or preview. - -```rust -use std::collections::HashMap; - -use motiongfx::prelude::*; - -#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -struct Id(u32); -#[derive(Debug, Clone, Copy)] -struct Subject(f32); -type SubjectWorld = HashMap; - -let mut pipeline_registry = PipelineRegistry::::new(); -pipeline_registry.register_unchecked( - PipelineKey::new::(), - Pipeline::new( - |world, ctx| { - ctx.bake::(|id| world.get(&id)); - }, - |world, ctx| { - ctx.sample::( - |id, target, accessor| { - if let Some(x) = world.get_mut(&id) { - *accessor.get_mut(x) = target; - } - }, - ); - }, - ), -); -``` - -### Subject World - -Because MotionGfx is backend agnostic, it can be used to animate -subjects in any world. A typical subject world would hold unique Ids -that maps subject entities to their associated animatable components. - -A simple example of such would be a `HashMap`. - -```rust -use std::collections::HashMap; - -#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -struct Id(u32); -#[derive(Debug, Clone, Copy)] -struct Subject(f32); -type SubjectWorld = HashMap; -``` - -Below is a comprehensive example on how MotionGfx can be used with a -custom world! - -```rust -use std::collections::HashMap; - -use motiongfx::prelude::*; - -// First, we have to initialize a subject world and the -// registries. -#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -struct Id(u32); -#[derive(Debug, Clone, Copy)] -struct Subject(f32); -type SubjectWorld = HashMap; - -let mut subject_world = SubjectWorld::new(); -let mut accessor_registry = FieldAccessorRegistry::new(); -let mut pipeline_registry = - PipelineRegistry::::new(); - -// The accessor registry should contain accessors to the fields in -// the subjects. In our case, it's just the first field in -// the tuple struct: `Subject::0`. - -accessor_registry.register_typed( - field!(::0), - accessor!(::0), -); - -// Similarly, the pipeline registry shoiud contain pipelines to -// bake and sample the fields in the subjects. - -pipeline_registry.register_unchecked( - PipelineKey::new::(), - Pipeline::new( - |world, ctx| { - ctx.bake::(|id| world.get(&id)); - }, - |world, ctx| { - ctx.sample::( - |id, target, accessor| { - if let Some(x) = world.get_mut(&id) { - *accessor.get_mut(x) = target; - } - }, - ); - }, - ), -); - -// Now that the registries are complete, we can start adding -// subjects into the subject world. - -subject_world.insert(Id(1), Subject(0.0)); - -// A timeline can only be created via the `TimelineBuilder`. - -let mut builder = TimelineBuilder::new(); - -let track = builder - // Creates the action. - .act(Id(1), field!(::0), |x| x + 10.0) - // Adds an interpolation method. - .with_interp(|&a, &b, t| a + (b - a) * t) - // Specifies the duration of the action. - .play(1.0) - // Compiles into a track. - .compile(); - -// Adds the track to the builder. -builder.add_tracks(track); -// And compile it into a timeline. -let mut timeline = builder.compile(); -// The timeline needs to be baked once before sampling can happen. -timeline.bake_actions( - &accessor_registry, - &pipeline_registry, - &subject_world, -); - -// Let's visualize the current state of the subject world before -// the sampling happens. -println!("Before: {:?}", subject_world); - -// We fast forward the timeline. -timeline.set_target_time(0.5); -// Actions need to be queued before it can be sampled. -// The queued actions are stored internally. -timeline.queue_actions(); -timeline.sample_queued_actions( - &accessor_registry, - &pipeline_registry, - &mut subject_world, -); - -// Visualize the state of the subject world after the sampling. -println!("After: {:?}", subject_world); -``` +`f1` starts 0.5 seconds after `f0` begins, regardless of how long +`f0` takes. ## Officially Supported Backends From f5a0e5f339e41c032576959546e33da3daf1a7a2 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 1 May 2026 17:14:50 +0800 Subject: [PATCH 09/27] Do not compile if track is empty in `try_compile` Credit to @CodeRabbit AI --- crates/motiongfx/src/timeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/motiongfx/src/timeline.rs b/crates/motiongfx/src/timeline.rs index 9cb1a71..36f9005 100644 --- a/crates/motiongfx/src/timeline.rs +++ b/crates/motiongfx/src/timeline.rs @@ -531,7 +531,7 @@ impl<'a, W: 'static> TimelineBuilder<'a, W> { /// Similar to [`Self::compile()`] but return `None` instead of /// panicking. pub fn try_compile(self) -> Option> { - (!self.tracks.is_empty()).then_some(self.compile()) + (!self.tracks.is_empty()).then(|| self.compile()) } } From b93f3a3d6587e4eb3e338cb0f1f71088a3e5c125 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Fri, 1 May 2026 17:51:33 +0800 Subject: [PATCH 10/27] Rename pipeline.rs -> world.rs, world.rs -> manager.rs, MotionGfxWorld -> MotionGfxManager --- crates/bevy_motiongfx/README.md | 6 +- crates/bevy_motiongfx/src/controller.rs | 6 +- crates/bevy_motiongfx/src/lib.rs | 10 +- crates/bevy_motiongfx/src/manager.rs | 212 ++++++++++++++++ crates/bevy_motiongfx/src/pipeline.rs | 65 ----- crates/bevy_motiongfx/src/world.rs | 235 ++++-------------- crates/motiongfx/src/pipeline.rs | 1 + .../bevy_examples/examples/custom_ease.rs | 2 +- .../bevy_examples/examples/custom_interp.rs | 2 +- examples/bevy_examples/examples/easings.rs | 2 +- .../bevy_examples/examples/hello_world.rs | 2 +- examples/bevy_examples/examples/minimal.rs | 2 +- examples/bevy_examples/examples/recording.rs | 2 +- .../bevy_examples/examples/slide_basic.rs | 4 +- 14 files changed, 276 insertions(+), 275 deletions(-) create mode 100644 crates/bevy_motiongfx/src/manager.rs delete mode 100644 crates/bevy_motiongfx/src/pipeline.rs diff --git a/crates/bevy_motiongfx/README.md b/crates/bevy_motiongfx/README.md index 5c16fc1..e1587a4 100644 --- a/crates/bevy_motiongfx/README.md +++ b/crates/bevy_motiongfx/README.md @@ -40,7 +40,7 @@ use bevy_motiongfx::prelude::*; fn build_timeline( mut commands: Commands, - mut motiongfx: ResMut, + mut motiongfx: ResMut, ) { // Spawn the Entity. let entity = commands @@ -72,7 +72,7 @@ use bevy_motiongfx::prelude::*; fn build_timeline( mut commands: Commands, - mut motiongfx: ResMut, + mut motiongfx: ResMut, mut materials: ResMut> ) { // Create the asset. @@ -112,7 +112,7 @@ use bevy_motiongfx::prelude::*; fn build_timeline( mut commands: Commands, - mut motiongfx: ResMut, + mut motiongfx: ResMut, ) { // Build the timeline. let mut b = motiongfx.create_bevy_builder(); diff --git a/crates/bevy_motiongfx/src/controller.rs b/crates/bevy_motiongfx/src/controller.rs index 8b09b3e..50fe4a0 100644 --- a/crates/bevy_motiongfx/src/controller.rs +++ b/crates/bevy_motiongfx/src/controller.rs @@ -3,7 +3,7 @@ use bevy_ecs::prelude::*; use bevy_time::prelude::*; use crate::MotionGfxSet; -use crate::world::{MotionGfxWorld, TimelineId}; +use crate::manager::{MotionGfxManager, TimelineId}; pub struct ControllerPlugin; @@ -18,7 +18,7 @@ impl Plugin for ControllerPlugin { } fn realtime_player_timing( - mut motiongfx: ResMut, + mut motiongfx: ResMut, q_timelines: Query<(&TimelineId, &RealtimePlayer)>, time: Res