Skip to main content

mittens_engine/engine/ecs/system/
transition_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::{
3    TransitionComponent, TransitionEasing, TransitionReplacePolicy,
4};
5use crate::engine::graphics::VisualWorld;
6use crate::engine::graphics::primitives::Transform;
7use crate::engine::user_input::InputState;
8
9use super::System;
10use crate::engine::ecs::World;
11
12#[derive(Debug, Clone, Copy)]
13struct ActiveTransformTransition {
14    component: ComponentId,
15    from: Transform,
16    to: Transform,
17    start_beat: f64,
18    duration_beats: f64,
19    easing: TransitionEasing,
20    last_sampled_beat: Option<f64>,
21}
22
23#[derive(Debug, Clone, Copy)]
24struct ActiveEmissiveTransition {
25    component: ComponentId,
26    from: f32,
27    to: f32,
28    start_beat: f64,
29    duration_beats: f64,
30    easing: TransitionEasing,
31    last_sampled_beat: Option<f64>,
32}
33
34#[derive(Debug, Clone, Copy)]
35pub struct TransformTransitionUpdate {
36    pub component: ComponentId,
37    pub transform: Transform,
38}
39
40#[derive(Debug, Clone, Copy)]
41pub struct EmissiveTransitionUpdate {
42    pub component: ComponentId,
43    pub intensity: f32,
44}
45
46#[derive(Debug, Default)]
47pub struct TransitionSystem {
48    active_transforms: Vec<ActiveTransformTransition>,
49    active_emissives: Vec<ActiveEmissiveTransition>,
50}
51
52impl TransitionSystem {
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    pub fn start_transform_transition(
58        &mut self,
59        component: ComponentId,
60        current: Transform,
61        target: Transform,
62        transition: TransitionComponent,
63        start_beat: f64,
64    ) -> bool {
65        if !transition.enabled || transition.duration_beats <= 0.0 {
66            return false;
67        }
68
69        match transition.replace {
70            TransitionReplacePolicy::ReplaceSameTarget | TransitionReplacePolicy::AllowParallel => {
71                self.active_transforms
72                    .retain(|active| active.component != component);
73            }
74        }
75
76        self.active_transforms.push(ActiveTransformTransition {
77            component,
78            from: current,
79            to: target,
80            start_beat,
81            duration_beats: transition.duration_beats,
82            easing: transition.easing,
83            last_sampled_beat: None,
84        });
85
86        true
87    }
88
89    pub fn start_emissive_transition(
90        &mut self,
91        component: ComponentId,
92        current: f32,
93        target: f32,
94        transition: TransitionComponent,
95        start_beat: f64,
96    ) -> bool {
97        if !transition.enabled || transition.duration_beats <= 0.0 {
98            return false;
99        }
100
101        match transition.replace {
102            TransitionReplacePolicy::ReplaceSameTarget | TransitionReplacePolicy::AllowParallel => {
103                self.active_emissives
104                    .retain(|active| active.component != component);
105            }
106        }
107
108        self.active_emissives.push(ActiveEmissiveTransition {
109            component,
110            from: current,
111            to: target,
112            start_beat,
113            duration_beats: transition.duration_beats,
114            easing: transition.easing,
115            last_sampled_beat: None,
116        });
117
118        true
119    }
120
121    pub fn cancel_transform_transitions(&mut self, component: ComponentId) {
122        self.active_transforms
123            .retain(|active| active.component != component);
124    }
125
126    pub fn cancel_emissive_transitions(&mut self, component: ComponentId) {
127        self.active_emissives
128            .retain(|active| active.component != component);
129    }
130
131    pub fn sample_transform_updates(&mut self, beat_now: f64) -> Vec<TransformTransitionUpdate> {
132        let mut updates = Vec::new();
133        let mut completed = Vec::new();
134
135        for (index, active) in self.active_transforms.iter_mut().enumerate() {
136            if active.last_sampled_beat == Some(beat_now) {
137                continue;
138            }
139
140            let raw_progress = if active.duration_beats <= f64::EPSILON {
141                1.0
142            } else {
143                ((beat_now - active.start_beat) / active.duration_beats).clamp(0.0, 1.0)
144            };
145            let eased_progress = apply_easing(active.easing, raw_progress as f32);
146            let transform = interpolate_transform(active.from, active.to, eased_progress);
147
148            active.last_sampled_beat = Some(beat_now);
149            updates.push(TransformTransitionUpdate {
150                component: active.component,
151                transform: if raw_progress >= 1.0 {
152                    active.to
153                } else {
154                    transform
155                },
156            });
157
158            if raw_progress >= 1.0 {
159                completed.push(index);
160            }
161        }
162
163        for index in completed.into_iter().rev() {
164            self.active_transforms.remove(index);
165        }
166
167        updates
168    }
169
170    pub fn sample_emissive_updates(&mut self, beat_now: f64) -> Vec<EmissiveTransitionUpdate> {
171        let mut updates = Vec::new();
172        let mut completed = Vec::new();
173
174        for (index, active) in self.active_emissives.iter_mut().enumerate() {
175            if active.last_sampled_beat == Some(beat_now) {
176                continue;
177            }
178
179            let raw_progress = if active.duration_beats <= f64::EPSILON {
180                1.0
181            } else {
182                ((beat_now - active.start_beat) / active.duration_beats).clamp(0.0, 1.0)
183            };
184            let eased_progress = apply_easing(active.easing, raw_progress as f32);
185            let intensity = active.from + (active.to - active.from) * eased_progress;
186
187            active.last_sampled_beat = Some(beat_now);
188            updates.push(EmissiveTransitionUpdate {
189                component: active.component,
190                intensity: if raw_progress >= 1.0 {
191                    active.to
192                } else {
193                    intensity
194                },
195            });
196
197            if raw_progress >= 1.0 {
198                completed.push(index);
199            }
200        }
201
202        for index in completed.into_iter().rev() {
203            self.active_emissives.remove(index);
204        }
205
206        updates
207    }
208
209    #[cfg(test)]
210    pub(crate) fn active_transform_count(&self) -> usize {
211        self.active_transforms.len()
212    }
213
214    #[cfg(test)]
215    pub(crate) fn active_emissive_count(&self) -> usize {
216        self.active_emissives.len()
217    }
218}
219
220impl System for TransitionSystem {
221    fn tick(
222        &mut self,
223        _world: &mut World,
224        _visuals: &mut VisualWorld,
225        _input: &InputState,
226        _dt_sec: f32,
227    ) {
228    }
229}
230
231fn apply_easing(easing: TransitionEasing, t: f32) -> f32 {
232    let t = t.clamp(0.0, 1.0);
233    match easing {
234        TransitionEasing::Step => {
235            if t >= 1.0 {
236                1.0
237            } else {
238                0.0
239            }
240        }
241        TransitionEasing::Linear => t,
242        TransitionEasing::EaseInQuad => t * t,
243        TransitionEasing::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
244        TransitionEasing::EaseInOutQuad => {
245            if t < 0.5 {
246                2.0 * t * t
247            } else {
248                1.0 - ((-2.0 * t + 2.0).powi(2) / 2.0)
249            }
250        }
251        TransitionEasing::EaseInCubic => t * t * t,
252        TransitionEasing::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
253        TransitionEasing::EaseInOutCubic => {
254            if t < 0.5 {
255                4.0 * t * t * t
256            } else {
257                1.0 - ((-2.0 * t + 2.0).powi(3) / 2.0)
258            }
259        }
260        TransitionEasing::EaseInOutSine => -(std::f32::consts::PI * t).cos() * 0.5 + 0.5,
261    }
262}
263
264fn interpolate_transform(from: Transform, to: Transform, t: f32) -> Transform {
265    let mut out = Transform::default();
266    out.translation = lerp3(from.translation, to.translation, t);
267    out.scale = lerp3(from.scale, to.scale, t);
268    out.rotation = nlerp_quat(from.rotation, to.rotation, t);
269    out.recompute_model();
270    out
271}
272
273fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
274    [
275        a[0] + (b[0] - a[0]) * t,
276        a[1] + (b[1] - a[1]) * t,
277        a[2] + (b[2] - a[2]) * t,
278    ]
279}
280
281fn nlerp_quat(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
282    let mut target = b;
283    let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
284    if dot < 0.0 {
285        target = [-b[0], -b[1], -b[2], -b[3]];
286    }
287
288    let blended = [
289        a[0] + (target[0] - a[0]) * t,
290        a[1] + (target[1] - a[1]) * t,
291        a[2] + (target[2] - a[2]) * t,
292        a[3] + (target[3] - a[3]) * t,
293    ];
294
295    normalize_quat(blended)
296}
297
298fn normalize_quat(q: [f32; 4]) -> [f32; 4] {
299    let len2 = q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3];
300    if len2 <= f32::EPSILON {
301        [0.0, 0.0, 0.0, 1.0]
302    } else {
303        let inv_len = len2.sqrt().recip();
304        [
305            q[0] * inv_len,
306            q[1] * inv_len,
307            q[2] * inv_len,
308            q[3] * inv_len,
309        ]
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use std::sync::{Arc, Mutex};
316
317    use super::TransitionSystem;
318    use crate::engine::ecs::CommandQueue;
319    use crate::engine::ecs::World;
320    use crate::engine::ecs::component::{
321        EmissiveComponent, RenderableComponent, TransformComponent, TransitionComponent,
322        TransitionEasing,
323    };
324    use crate::engine::ecs::system::{ClockDriver, SystemWorld};
325    use crate::engine::graphics::primitives::Transform;
326    use crate::engine::graphics::primitives::{MaterialHandle, Renderable};
327    use crate::engine::graphics::{CpuMesh, MeshUploader, RenderAssets, VisualWorld};
328
329    #[derive(Clone, Default)]
330    struct TestClockDriver {
331        now_sec: Arc<Mutex<f64>>,
332    }
333
334    impl TestClockDriver {
335        fn set_time_sec(&self, time_sec: f64) {
336            *self.now_sec.lock().expect("clock mutex poisoned") = time_sec;
337        }
338    }
339
340    impl ClockDriver for TestClockDriver {
341        fn name(&self) -> &'static str {
342            "test"
343        }
344
345        fn time_now_sec(&self) -> f64 {
346            *self.now_sec.lock().expect("clock mutex poisoned")
347        }
348    }
349
350    #[derive(Default)]
351    struct TestUploader {
352        next_mesh: u32,
353    }
354
355    impl MeshUploader for TestUploader {
356        fn upload_mesh(
357            &mut self,
358            _mesh: &CpuMesh,
359        ) -> Result<crate::engine::graphics::primitives::MeshHandle, Box<dyn std::error::Error>>
360        {
361            let handle = crate::engine::graphics::primitives::MeshHandle(self.next_mesh);
362            self.next_mesh += 1;
363            Ok(handle)
364        }
365    }
366
367    #[test]
368    fn transform_transition_reaches_exact_final_value() {
369        let mut world = World::default();
370        let component =
371            world.add_component(crate::engine::ecs::component::TransformComponent::new());
372
373        let mut system = TransitionSystem::new();
374        let from = Transform::default();
375        let mut to = Transform::default();
376        to.translation = [10.0, -2.0, 3.0];
377        to.scale = [2.0, 3.0, 4.0];
378        to.recompute_model();
379
380        let started = system.start_transform_transition(
381            component,
382            from,
383            to,
384            TransitionComponent::new()
385                .with_duration_beats(2.0)
386                .with_easing(TransitionEasing::Linear),
387            0.0,
388        );
389
390        assert!(started);
391        let halfway = system.sample_transform_updates(1.0);
392        assert_eq!(halfway.len(), 1);
393        assert!((halfway[0].transform.translation[0] - 5.0).abs() < 1e-5);
394        assert_eq!(system.active_transform_count(), 1);
395
396        let finished = system.sample_transform_updates(2.0);
397        assert_eq!(finished.len(), 1);
398        assert_eq!(finished[0].transform.translation, to.translation);
399        assert_eq!(finished[0].transform.scale, to.scale);
400        assert_eq!(system.active_transform_count(), 0);
401    }
402
403    #[test]
404    fn transform_transition_dedupes_same_beat_sampling() {
405        let mut world = World::default();
406        let component =
407            world.add_component(crate::engine::ecs::component::TransformComponent::new());
408
409        let mut system = TransitionSystem::new();
410        let from = Transform::default();
411        let mut to = Transform::default();
412        to.translation = [4.0, 0.0, 0.0];
413        to.recompute_model();
414
415        assert!(system.start_transform_transition(
416            component,
417            from,
418            to,
419            TransitionComponent::new().with_duration_beats(1.0),
420            0.0,
421        ));
422
423        assert_eq!(system.sample_transform_updates(0.0).len(), 1);
424        assert!(system.sample_transform_updates(0.0).is_empty());
425    }
426
427    #[test]
428    fn replace_same_target_keeps_only_latest_transition() {
429        let mut world = World::default();
430        let component =
431            world.add_component(crate::engine::ecs::component::TransformComponent::new());
432
433        let mut system = TransitionSystem::new();
434        let from = Transform::default();
435
436        let mut first_target = Transform::default();
437        first_target.translation = [10.0, 0.0, 0.0];
438        first_target.recompute_model();
439        assert!(system.start_transform_transition(
440            component,
441            from,
442            first_target,
443            TransitionComponent::new().with_duration_beats(2.0),
444            0.0,
445        ));
446
447        let mut current = Transform::default();
448        current.translation = [2.5, 0.0, 0.0];
449        current.recompute_model();
450        let mut second_target = Transform::default();
451        second_target.translation = [20.0, 0.0, 0.0];
452        second_target.recompute_model();
453        assert!(system.start_transform_transition(
454            component,
455            current,
456            second_target,
457            TransitionComponent::new().with_duration_beats(1.0),
458            0.25,
459        ));
460
461        assert_eq!(system.active_transform_count(), 1);
462        let finished = system.sample_transform_updates(1.25);
463        assert_eq!(finished.len(), 1);
464        assert_eq!(finished[0].transform.translation, second_target.translation);
465        assert_eq!(system.active_transform_count(), 0);
466    }
467
468    #[test]
469    fn emissive_transition_reaches_exact_final_value() {
470        let mut world = World::default();
471        let component = world.add_component(EmissiveComponent::off());
472
473        let mut system = TransitionSystem::new();
474        let started = system.start_emissive_transition(
475            component,
476            0.0,
477            2.5,
478            TransitionComponent::new()
479                .with_duration_beats(2.0)
480                .with_easing(TransitionEasing::Linear),
481            0.0,
482        );
483
484        assert!(started);
485        let halfway = system.sample_emissive_updates(1.0);
486        assert_eq!(halfway.len(), 1);
487        assert!((halfway[0].intensity - 1.25).abs() < 1e-5);
488        assert_eq!(system.active_emissive_count(), 1);
489
490        let finished = system.sample_emissive_updates(2.0);
491        assert_eq!(finished.len(), 1);
492        assert!((finished[0].intensity - 2.5).abs() < 1e-6);
493        assert_eq!(system.active_emissive_count(), 0);
494    }
495
496    #[test]
497    fn replace_same_target_keeps_only_latest_emissive_transition() {
498        let mut world = World::default();
499        let component = world.add_component(EmissiveComponent::off());
500
501        let mut system = TransitionSystem::new();
502        assert!(system.start_emissive_transition(
503            component,
504            0.0,
505            1.0,
506            TransitionComponent::new().with_duration_beats(2.0),
507            0.0,
508        ));
509
510        assert!(system.start_emissive_transition(
511            component,
512            0.25,
513            3.0,
514            TransitionComponent::new().with_duration_beats(1.0),
515            0.25,
516        ));
517
518        assert_eq!(system.active_emissive_count(), 1);
519        let finished = system.sample_emissive_updates(1.25);
520        assert_eq!(finished.len(), 1);
521        assert!((finished[0].intensity - 3.0).abs() < 1e-6);
522        assert_eq!(system.active_emissive_count(), 0);
523    }
524
525    #[test]
526    fn system_world_update_transform_is_immediate_without_transition_child() {
527        let mut world = World::default();
528        let mut systems = SystemWorld::default();
529        let mut visuals = VisualWorld::default();
530        let component =
531            world.add_component(crate::engine::ecs::component::TransformComponent::new());
532
533        let mut target = Transform::default();
534        target.translation = [3.0, 4.0, 5.0];
535        target.recompute_model();
536
537        systems.update_transform(&mut world, &mut visuals, component, target);
538
539        let current = world
540            .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(component)
541            .unwrap();
542        assert_eq!(current.transform.translation, target.translation);
543        assert_eq!(systems.transition.active_transform_count(), 0);
544    }
545
546    #[test]
547    fn system_world_update_transform_starts_runtime_when_transition_child_exists() {
548        let mut world = World::default();
549        let mut systems = SystemWorld::default();
550        let mut visuals = VisualWorld::default();
551        let component =
552            world.add_component(crate::engine::ecs::component::TransformComponent::new());
553        let transition = world.add_component(
554            TransitionComponent::new()
555                .with_duration_beats(1.0)
556                .with_easing(TransitionEasing::Linear),
557        );
558        world.add_child(component, transition).unwrap();
559
560        let initial = world
561            .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(component)
562            .unwrap()
563            .transform
564            .translation;
565
566        let mut target = Transform::default();
567        target.translation = [9.0, 0.0, 0.0];
568        target.recompute_model();
569
570        systems.update_transform(&mut world, &mut visuals, component, target);
571
572        let current = world
573            .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(component)
574            .unwrap();
575        assert_eq!(current.transform.translation, initial);
576        assert_eq!(systems.transition.active_transform_count(), 1);
577    }
578
579    #[test]
580    fn system_world_update_emissive_is_immediate_without_transition_child() {
581        let mut world = World::default();
582        let mut systems = SystemWorld::default();
583        let mut visuals = VisualWorld::default();
584        let component = world.add_component(EmissiveComponent::off());
585
586        systems.update_emissive_intensity(&mut world, &mut visuals, component, 2.5);
587
588        let current = world
589            .get_component_by_id_as::<EmissiveComponent>(component)
590            .unwrap();
591        assert!((current.intensity - 2.5).abs() < 1.0e-6);
592        assert_eq!(systems.transition.active_emissive_count(), 0);
593    }
594
595    #[test]
596    fn system_world_update_emissive_starts_runtime_when_transition_child_exists() {
597        let mut world = World::default();
598        let mut systems = SystemWorld::default();
599        let mut visuals = VisualWorld::default();
600        let component = world.add_component(EmissiveComponent::off());
601        let transition = world.add_component(
602            TransitionComponent::new()
603                .with_duration_beats(1.0)
604                .with_easing(TransitionEasing::Linear),
605        );
606        world.add_child(component, transition).unwrap();
607
608        systems.update_emissive_intensity(&mut world, &mut visuals, component, 2.5);
609
610        let current = world
611            .get_component_by_id_as::<EmissiveComponent>(component)
612            .unwrap();
613        assert_eq!(current.intensity, 0.0);
614        assert_eq!(systems.transition.active_emissive_count(), 1);
615    }
616
617    #[test]
618    fn system_world_tick_advances_emissive_transition_and_reaches_final_value() {
619        let mut world = World::default();
620        let mut systems = SystemWorld::default();
621        let mut visuals = VisualWorld::default();
622        let component = world.add_component(EmissiveComponent::off());
623        let transition = world.add_component(
624            TransitionComponent::new()
625                .with_duration_beats(1.0)
626                .with_easing(TransitionEasing::Linear),
627        );
628        world.add_child(component, transition).unwrap();
629
630        let driver = TestClockDriver::default();
631        systems.clock.set_driver(Arc::new(driver.clone()));
632        systems.clock.set_bpm(60.0);
633        driver.set_time_sec(0.0);
634        systems.clock.sample();
635
636        systems.update_emissive_intensity(&mut world, &mut visuals, component, 2.0);
637
638        driver.set_time_sec(0.5);
639        systems.clock.sample();
640        systems.tick_transition_runtime(&mut world, &mut visuals);
641        let halfway = world
642            .get_component_by_id_as::<EmissiveComponent>(component)
643            .unwrap();
644        assert!((halfway.intensity - 1.0).abs() < 1.0e-6);
645        assert_eq!(systems.transition.active_emissive_count(), 1);
646
647        driver.set_time_sec(1.0);
648        systems.clock.sample();
649        systems.tick_transition_runtime(&mut world, &mut visuals);
650        let finished = world
651            .get_component_by_id_as::<EmissiveComponent>(component)
652            .unwrap();
653        assert!((finished.intensity - 2.0).abs() < 1.0e-6);
654        assert_eq!(systems.transition.active_emissive_count(), 0);
655    }
656
657    #[test]
658    fn direct_parent_emissive_transition_updates_renderable_visual_over_time() {
659        let mut world = World::default();
660        let mut systems = SystemWorld::default();
661        let mut visuals = VisualWorld::default();
662        let mut render_assets = RenderAssets::new();
663        let mut uploader = TestUploader::default();
664        let mut queue = CommandQueue::new();
665
666        let renderable = world.add_component(RenderableComponent::new(Renderable::new(
667            crate::engine::graphics::primitives::CpuMeshHandle::CUBE,
668            MaterialHandle::TOON_MESH,
669        )));
670        let emissive = world.add_component(EmissiveComponent::off());
671        let transition = world.add_component(
672            TransitionComponent::new()
673                .with_duration_beats(1.0)
674                .with_easing(TransitionEasing::Linear),
675        );
676        world.add_child(renderable, emissive).unwrap();
677        world.add_child(emissive, transition).unwrap();
678
679        systems
680            .renderable
681            .register_renderable_from_world(&mut world, &mut visuals, renderable);
682        systems.renderable.flush_pending(
683            &mut world,
684            &mut visuals,
685            &mut render_assets,
686            &mut uploader,
687            &mut queue,
688        );
689        let handle = world
690            .get_component_by_id_as::<RenderableComponent>(renderable)
691            .and_then(|r| r.get_handle())
692            .expect("renderable handle");
693
694        let driver = TestClockDriver::default();
695        systems.clock.set_driver(Arc::new(driver.clone()));
696        systems.clock.set_bpm(60.0);
697        driver.set_time_sec(0.0);
698        systems.clock.sample();
699
700        systems.update_emissive_intensity(&mut world, &mut visuals, emissive, 2.0);
701        driver.set_time_sec(0.5);
702        systems.clock.sample();
703        systems.tick_transition_runtime(&mut world, &mut visuals);
704        systems.renderable.flush_pending(
705            &mut world,
706            &mut visuals,
707            &mut render_assets,
708            &mut uploader,
709            &mut queue,
710        );
711        let halfway = visuals.instance(handle).expect("visual instance");
712        assert!((halfway.emissive - 1.0).abs() < 1.0e-6);
713
714        driver.set_time_sec(1.0);
715        systems.clock.sample();
716        systems.tick_transition_runtime(&mut world, &mut visuals);
717        systems.renderable.flush_pending(
718            &mut world,
719            &mut visuals,
720            &mut render_assets,
721            &mut uploader,
722            &mut queue,
723        );
724        let finished = visuals.instance(handle).expect("visual instance");
725        assert!((finished.emissive - 2.0).abs() < 1.0e-6);
726    }
727
728    #[test]
729    fn inherited_emissive_transition_updates_descendant_renderables_over_time() {
730        let mut world = World::default();
731        let mut systems = SystemWorld::default();
732        let mut visuals = VisualWorld::default();
733        let mut render_assets = RenderAssets::new();
734        let mut uploader = TestUploader::default();
735        let mut queue = CommandQueue::new();
736
737        let container = world.add_component(TransformComponent::new());
738        let emissive = world.add_component(EmissiveComponent::off());
739        let transition = world.add_component(
740            TransitionComponent::new()
741                .with_duration_beats(1.0)
742                .with_easing(TransitionEasing::Linear),
743        );
744        let child_t = world.add_component(TransformComponent::new());
745        let child_r = world.add_component(RenderableComponent::new(Renderable::new(
746            crate::engine::graphics::primitives::CpuMeshHandle::CUBE,
747            MaterialHandle::TOON_MESH,
748        )));
749        world.add_child(container, emissive).unwrap();
750        world.add_child(emissive, transition).unwrap();
751        world.add_child(container, child_t).unwrap();
752        world.add_child(child_t, child_r).unwrap();
753
754        systems
755            .renderable
756            .register_renderable_from_world(&mut world, &mut visuals, child_r);
757        systems.renderable.flush_pending(
758            &mut world,
759            &mut visuals,
760            &mut render_assets,
761            &mut uploader,
762            &mut queue,
763        );
764        let handle = world
765            .get_component_by_id_as::<RenderableComponent>(child_r)
766            .and_then(|r| r.get_handle())
767            .expect("renderable handle");
768
769        let driver = TestClockDriver::default();
770        systems.clock.set_driver(Arc::new(driver.clone()));
771        systems.clock.set_bpm(60.0);
772        driver.set_time_sec(0.0);
773        systems.clock.sample();
774
775        systems.update_emissive_intensity(&mut world, &mut visuals, emissive, 3.0);
776        driver.set_time_sec(0.5);
777        systems.clock.sample();
778        systems.tick_transition_runtime(&mut world, &mut visuals);
779        systems.renderable.flush_pending(
780            &mut world,
781            &mut visuals,
782            &mut render_assets,
783            &mut uploader,
784            &mut queue,
785        );
786        let halfway = visuals.instance(handle).expect("visual instance");
787        assert!((halfway.emissive - 1.5).abs() < 1.0e-6);
788
789        driver.set_time_sec(1.0);
790        systems.clock.sample();
791        systems.tick_transition_runtime(&mut world, &mut visuals);
792        systems.renderable.flush_pending(
793            &mut world,
794            &mut visuals,
795            &mut render_assets,
796            &mut uploader,
797            &mut queue,
798        );
799        let finished = visuals.instance(handle).expect("visual instance");
800        assert!((finished.emissive - 3.0).abs() < 1.0e-6);
801    }
802}