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}