1use alloc::collections::BTreeMap;
19use alloc::vec::Vec;
20
21use crate::components::{GlobalTransform, Parent, Transform};
22use crate::ecs::{ColumnTicks, Entity, MAX_CHANGE_AGE, PipelineContext, Tick};
23use crate::gfx::transform::{IDENTITY, mat4_mul};
24
25pub type WorldMatrix = [[f32; 4]; 4];
27
28const NO_SLOT: u32 = u32::MAX;
30
31const CYCLIC: u32 = u32::MAX;
36const VISITING: u32 = u32::MAX - 1;
37const UNVISITED: u32 = u32::MAX - 2;
38
39const DIRTY_BUDGET_DIVISOR: usize = 8;
43
44#[derive(Clone, Copy)]
49struct SourceTicks {
50 transform: ColumnTicks,
51 parent: Tick,
52}
53
54#[derive(Default)]
63pub struct TransformCache {
64 entity: Vec<Entity>,
67 local: Vec<WorldMatrix>,
68 world: Vec<WorldMatrix>,
69 parent: Vec<u32>,
70 depth: Vec<u32>,
71 order: Vec<u32>,
73 child_start: Vec<u32>,
76 child_list: Vec<u32>,
77 slot_of: Vec<u32>,
79 visited: Vec<u32>,
82 pass: u32,
83 path: Vec<u32>,
87 dirty: Vec<u32>,
88 stack: Vec<u32>,
89 offsets: Vec<u32>,
90 last: Option<SourceTicks>,
91}
92
93impl TransformCache {
94 fn slot_of(&self, entity: Entity) -> Option<u32> {
97 let slot = *self.slot_of.get(entity.index() as usize)?;
98 if slot == NO_SLOT {
99 return None;
100 }
101 (self.entity[slot as usize] == entity).then_some(slot)
102 }
103
104 fn resolve(&mut self, ctx: &PipelineContext) {
108 self.entity.clear();
109 self.local.clear();
110 for (entity, transform) in ctx.query_with_entity::<Transform>() {
111 self.entity.push(entity);
112 self.local.push(transform.model_matrix());
113 }
114 let slots = self.entity.len();
115
116 let widest = self
117 .entity
118 .iter()
119 .map(|e| e.index() as usize)
120 .max()
121 .map_or(0, |i| i + 1);
122 self.slot_of.clear();
123 self.slot_of.resize(widest, NO_SLOT);
124 for (slot, entity) in self.entity.iter().enumerate() {
125 self.slot_of[entity.index() as usize] = slot as u32;
126 }
127
128 self.parent.clear();
131 self.parent.resize(slots, NO_SLOT);
132 for (entity, parent) in ctx.query_with_entity::<Parent>() {
133 if let Some(slot) = self.slot_of(entity) {
134 self.parent[slot as usize] = self.slot_of(parent.0).unwrap_or(NO_SLOT);
135 }
136 }
137
138 self.compute_depths();
139 self.order_by_depth();
140 self.build_children();
141
142 self.world.clear();
143 self.world.resize(slots, IDENTITY);
144 for i in 0..self.order.len() {
145 let slot = self.order[i] as usize;
146 self.world[slot] = self.compose(slot);
147 }
148
149 self.visited.clear();
150 self.visited.resize(slots, 0);
151 self.pass = 0;
152 }
153
154 fn compose(&self, slot: usize) -> WorldMatrix {
158 match (self.depth[slot], self.parent[slot]) {
159 (CYCLIC, _) | (_, NO_SLOT) => self.local[slot],
160 (_, parent) => mat4_mul(self.world[parent as usize], self.local[slot]),
161 }
162 }
163
164 fn compute_depths(&mut self) {
169 self.depth.clear();
170 self.depth.resize(self.local.len(), UNVISITED);
171 for start in 0..self.depth.len() {
172 if self.depth[start] != UNVISITED {
173 continue;
174 }
175 self.path.clear();
176 let mut current = start;
177 let base = loop {
179 match self.depth[current] {
180 UNVISITED => {}
181 VISITING | CYCLIC => break CYCLIC,
183 known => break known + 1,
184 }
185 self.depth[current] = VISITING;
186 self.path.push(current as u32);
187 match self.parent[current] {
188 NO_SLOT => break 0,
189 parent => current = parent as usize,
190 }
191 };
192 let mut depth = base;
193 for &slot in self.path.iter().rev() {
194 self.depth[slot as usize] = depth;
195 if depth != CYCLIC {
196 depth += 1;
197 }
198 }
199 }
200 }
201
202 fn order_by_depth(&mut self) {
205 let deepest = self
206 .depth
207 .iter()
208 .copied()
209 .filter(|&d| d != CYCLIC)
210 .max()
211 .unwrap_or(0) as usize;
212 let cyclic_bucket = deepest + 1;
213 self.offsets.clear();
214 self.offsets.resize(cyclic_bucket + 1, 0);
215 for &depth in &self.depth {
216 let bucket = if depth == CYCLIC {
217 cyclic_bucket
218 } else {
219 depth as usize
220 };
221 self.offsets[bucket] += 1;
222 }
223 let mut offset = 0;
224 for count in &mut self.offsets {
225 let bucket = *count;
226 *count = offset;
227 offset += bucket;
228 }
229 self.order.clear();
230 self.order.resize(self.depth.len(), 0);
231 for slot in 0..self.depth.len() {
232 let bucket = if self.depth[slot] == CYCLIC {
233 cyclic_bucket
234 } else {
235 self.depth[slot] as usize
236 };
237 self.order[self.offsets[bucket] as usize] = slot as u32;
238 self.offsets[bucket] += 1;
239 }
240 }
241
242 fn build_children(&mut self) {
246 let slots = self.parent.len();
247 self.child_start.clear();
248 self.child_start.resize(slots + 1, 0);
249 for &parent in &self.parent {
250 if parent != NO_SLOT {
251 self.child_start[parent as usize + 1] += 1;
252 }
253 }
254 for i in 0..slots {
255 self.child_start[i + 1] += self.child_start[i];
256 }
257 self.child_list.clear();
258 self.child_list.resize(self.child_start[slots] as usize, 0);
259 self.offsets.clear();
261 self.offsets.extend_from_slice(&self.child_start[..slots]);
262 for slot in 0..slots {
263 let parent = self.parent[slot];
264 if parent == NO_SLOT {
265 continue;
266 }
267 let at = self.offsets[parent as usize] as usize;
268 self.offsets[parent as usize] += 1;
269 self.child_list[at] = slot as u32;
270 }
271 }
272
273 fn write_all(&self, ctx: &mut PipelineContext) {
276 for (slot, &entity) in self.entity.iter().enumerate() {
277 if let Some(global) = ctx.get_mut::<GlobalTransform>(entity) {
278 global.0 = self.world[slot];
279 }
280 }
281 }
282
283 fn resolve_incremental(&mut self, ctx: &mut PipelineContext, since: Tick) -> bool {
288 let budget = (self.entity.len() / DIRTY_BUDGET_DIVISOR).max(1);
289 self.dirty.clear();
290 for (entity, transform) in ctx.changed_rows::<Transform>(since) {
291 if self.dirty.len() >= budget {
292 return false;
293 }
294 let Some(slot) = self.slot_of(entity) else {
295 return false;
296 };
297 self.local[slot as usize] = transform.model_matrix();
298 self.dirty.push(slot);
299 }
300
301 self.dirty
304 .sort_unstable_by_key(|&slot| self.depth[slot as usize]);
305 self.pass = self.pass.wrapping_add(1);
306 if self.pass == 0 {
307 self.visited.fill(0);
308 self.pass = 1;
309 }
310 for i in 0..self.dirty.len() {
311 self.walk_subtree(ctx, self.dirty[i]);
312 }
313 true
314 }
315
316 fn walk_subtree(&mut self, ctx: &mut PipelineContext, seed: u32) {
320 self.stack.clear();
321 self.stack.push(seed);
322 while let Some(slot) = self.stack.pop() {
323 let slot = slot as usize;
324 if self.visited[slot] == self.pass {
325 continue;
326 }
327 self.visited[slot] = self.pass;
328 self.world[slot] = self.compose(slot);
329 if let Some(global) = ctx.get_mut::<GlobalTransform>(self.entity[slot]) {
330 global.0 = self.world[slot];
331 }
332 let (start, end) = (
333 self.child_start[slot] as usize,
334 self.child_start[slot + 1] as usize,
335 );
336 self.stack.extend_from_slice(&self.child_list[start..end]);
337 }
338 }
339
340 fn world_map(&self) -> BTreeMap<Entity, WorldMatrix> {
343 self.entity
344 .iter()
345 .copied()
346 .zip(self.world.iter().copied())
347 .collect()
348 }
349}
350
351pub fn resolve_world_matrices(ctx: &PipelineContext) -> BTreeMap<Entity, WorldMatrix> {
357 let mut cache = TransformCache::default();
358 cache.resolve(ctx);
359 cache.world_map()
360}
361
362pub fn propagate_transforms(ctx: &mut PipelineContext) {
366 let mut cache = TransformCache::default();
367 cache.resolve(ctx);
368 cache.write_all(ctx);
369}
370
371pub fn propagate_transforms_cached(ctx: &mut PipelineContext, cache: &mut TransformCache) {
379 let transform = ctx.column_ticks::<Transform>();
380 let parent = ctx.changed_tick::<Parent>();
381
382 if let Some(last) = cache.last {
383 if last.transform.changed == transform.changed && last.parent == parent {
384 return;
385 }
386 let targeted_only = last.parent == parent
391 && last.transform.bulk == transform.bulk
392 && last.transform.structural == transform.structural
393 && transform
394 .changed
395 .get()
396 .wrapping_sub(last.transform.changed.get())
397 <= MAX_CHANGE_AGE;
398 if targeted_only && cache.resolve_incremental(ctx, last.transform.changed) {
399 cache.last = Some(SourceTicks { transform, parent });
400 return;
401 }
402 }
403
404 cache.resolve(ctx);
405 cache.write_all(ctx);
406 cache.last = Some(SourceTicks { transform, parent });
407}
408
409pub fn reparent(ctx: &mut PipelineContext, child: Entity, new_parent: Option<Entity>) {
415 use crate::components::Children;
416
417 if let Some(old) = ctx.remove::<Parent>(child)
419 && let Some(siblings) = ctx.get_mut::<Children>(old.0)
420 {
421 siblings.0.retain(|&e| e != child);
422 }
423
424 if let Some(parent) = new_parent {
427 ctx.insert(child, Parent(parent));
428 match ctx.get_mut::<Children>(parent) {
429 Some(kids) => {
430 if !kids.0.contains(&child) {
431 kids.0.push(child);
432 }
433 }
434 None => ctx.insert(parent, Children(concinnity_memory::InlineVec::one(child))),
435 }
436 }
437
438 propagate_transforms(ctx);
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use alloc::vec;
445
446 use crate::components::Children;
447 use crate::ecs::{Arena, ComponentStorage, FrameContext, NoPayloads, Resources};
448 use crate::gfx::profile::FrameProfile;
449
450 const IDENTITY4: WorldMatrix = IDENTITY;
451
452 struct TestWorld {
455 components: ComponentStorage,
456 blob: NoPayloads,
457 profile: FrameProfile,
458 resources: Resources,
459 scratch: Arena,
460 }
461
462 impl TestWorld {
463 fn new() -> TestWorld {
464 TestWorld {
465 components: ComponentStorage::default(),
466 blob: NoPayloads,
467 profile: FrameProfile::default(),
468 resources: Resources::new(),
469 scratch: Arena::with_capacity(64 * 1024),
470 }
471 }
472
473 fn ctx(&mut self) -> PipelineContext<'_> {
474 PipelineContext {
475 components: &mut self.components,
476 blob: &mut self.blob,
477 profile: &mut self.profile,
478 resources: &mut self.resources,
479 frame: FrameContext::new(&self.scratch),
480 }
481 }
482 }
483
484 fn translate(x: f32) -> Transform {
485 Transform {
486 position: [x, 0.0, 0.0],
487 rotation_deg: [0.0; 3],
488 scale: [1.0; 3],
489 }
490 }
491
492 fn spawn(ctx: &mut PipelineContext, t: Transform, parent: Option<Entity>) -> Entity {
495 let entity = ctx.components.spawn();
496 ctx.insert(entity, t);
497 ctx.insert(entity, GlobalTransform::default());
498 if let Some(p) = parent {
499 ctx.insert(entity, Parent(p));
500 }
501 entity
502 }
503
504 fn global(ctx: &PipelineContext, entity: Entity) -> WorldMatrix {
505 ctx.get::<GlobalTransform>(entity).unwrap().0
506 }
507
508 fn chain(ctx: &mut PipelineContext, depth: usize) -> Vec<Entity> {
511 let mut chain = Vec::with_capacity(depth);
512 let mut parent = None;
513 for i in 0..depth {
514 let entity = spawn(ctx, translate(i as f32 + 1.0), parent);
515 chain.push(entity);
516 parent = Some(entity);
517 }
518 chain
519 }
520
521 #[test]
524 fn propagate_transforms_composes_parent_then_child() {
525 let parent_t = Transform {
526 position: [1.0, 2.0, 3.0],
527 rotation_deg: [0.0, 30.0, 0.0],
528 scale: [1.0, 1.0, 1.0],
529 };
530 let child_t = Transform {
531 position: [0.0, 0.0, 1.0],
532 rotation_deg: [10.0, 0.0, 5.0],
533 scale: [2.0, 2.0, 2.0],
534 };
535
536 let mut world = TestWorld::new();
537 let mut ctx = world.ctx();
538 let parent_e = spawn(&mut ctx, parent_t, None);
539 let child_e = spawn(&mut ctx, child_t, Some(parent_e));
540
541 propagate_transforms(&mut ctx);
542
543 assert_eq!(
544 global(&ctx, parent_e),
545 parent_t.model_matrix(),
546 "root world = local"
547 );
548 assert_eq!(
549 global(&ctx, child_e),
550 mat4_mul(parent_t.model_matrix(), child_t.model_matrix()),
551 "child world = parent_world * local"
552 );
553 }
554
555 #[test]
558 fn cached_propagation_matches_the_uncached_path() {
559 let parent_t = Transform {
560 position: [1.0, 2.0, 3.0],
561 rotation_deg: [0.0, 30.0, 0.0],
562 scale: [1.0, 1.0, 1.0],
563 };
564 let child_t = Transform {
565 position: [0.0, 0.0, 1.0],
566 rotation_deg: [10.0, 0.0, 5.0],
567 scale: [2.0, 2.0, 2.0],
568 };
569
570 let mut world = TestWorld::new();
571 let mut ctx = world.ctx();
572 let parent_e = spawn(&mut ctx, parent_t, None);
573 let child_e = spawn(&mut ctx, child_t, Some(parent_e));
574
575 let mut cache = TransformCache::default();
576 propagate_transforms_cached(&mut ctx, &mut cache);
577
578 assert_eq!(global(&ctx, parent_e), parent_t.model_matrix());
579 assert_eq!(
580 global(&ctx, child_e),
581 mat4_mul(parent_t.model_matrix(), child_t.model_matrix())
582 );
583 }
584
585 #[test]
588 fn cached_propagation_skips_until_a_transform_changes() {
589 let mut world = TestWorld::new();
590 let mut ctx = world.ctx();
591 let t0 = translate(1.0);
592 let e = spawn(&mut ctx, t0, None);
593
594 let mut cache = TransformCache::default();
595 propagate_transforms_cached(&mut ctx, &mut cache);
596 assert_eq!(global(&ctx, e), t0.model_matrix());
597
598 ctx.get_mut::<GlobalTransform>(e).unwrap().0 = IDENTITY4;
601 propagate_transforms_cached(&mut ctx, &mut cache);
602 assert_eq!(
603 global(&ctx, e),
604 IDENTITY4,
605 "unchanged Transform => propagation skipped"
606 );
607
608 let t1 = Transform {
610 position: [0.0, 5.0, 0.0],
611 rotation_deg: [0.0; 3],
612 scale: [1.0; 3],
613 };
614 *ctx.get_mut::<Transform>(e).unwrap() = t1;
615 propagate_transforms_cached(&mut ctx, &mut cache);
616 assert_eq!(
617 global(&ctx, e),
618 t1.model_matrix(),
619 "changed Transform => propagation recomputed"
620 );
621 }
622
623 #[test]
626 fn moving_a_root_recomposes_its_whole_subtree() {
627 let mut world = TestWorld::new();
628 let mut ctx = world.ctx();
629 let links = chain(&mut ctx, 4);
630
631 let mut cache = TransformCache::default();
632 propagate_transforms_cached(&mut ctx, &mut cache);
633
634 let moved = translate(100.0);
635 *ctx.get_mut::<Transform>(links[0]).unwrap() = moved;
636 propagate_transforms_cached(&mut ctx, &mut cache);
637
638 let mut expected = moved.model_matrix();
640 assert_eq!(global(&ctx, links[0]), expected);
641 for (depth, &link) in links.iter().enumerate().skip(1) {
642 let local = translate(depth as f32 + 1.0).model_matrix();
643 expected = mat4_mul(expected, local);
644 assert_eq!(global(&ctx, link), expected, "link at depth {depth}");
645 }
646 }
647
648 #[test]
651 fn an_untouched_subtree_is_not_rewritten() {
652 let mut world = TestWorld::new();
653 let mut ctx = world.ctx();
654 let root_a = spawn(&mut ctx, translate(1.0), None);
655 let child_a = spawn(&mut ctx, translate(2.0), Some(root_a));
656 let root_b = spawn(&mut ctx, translate(3.0), None);
657 let child_b = spawn(&mut ctx, translate(4.0), Some(root_b));
658
659 let mut cache = TransformCache::default();
660 propagate_transforms_cached(&mut ctx, &mut cache);
661
662 ctx.get_mut::<GlobalTransform>(child_b).unwrap().0 = IDENTITY4;
663 *ctx.get_mut::<Transform>(root_a).unwrap() = translate(50.0);
664 propagate_transforms_cached(&mut ctx, &mut cache);
665
666 assert_eq!(
667 global(&ctx, child_a),
668 mat4_mul(
669 translate(50.0).model_matrix(),
670 translate(2.0).model_matrix()
671 ),
672 "the moved root's subtree recomposed"
673 );
674 assert_eq!(
675 global(&ctx, child_b),
676 IDENTITY4,
677 "the other tree was never walked"
678 );
679 assert_eq!(global(&ctx, root_b), translate(3.0).model_matrix());
680 }
681
682 #[test]
686 fn a_dirty_ancestor_and_descendant_resolve_against_the_new_parent() {
687 let mut world = TestWorld::new();
688 let mut ctx = world.ctx();
689 let links = chain(&mut ctx, 3);
690
691 let mut cache = TransformCache::default();
692 propagate_transforms_cached(&mut ctx, &mut cache);
693
694 *ctx.get_mut::<Transform>(links[2]).unwrap() = translate(7.0);
697 *ctx.get_mut::<Transform>(links[0]).unwrap() = translate(9.0);
698 propagate_transforms_cached(&mut ctx, &mut cache);
699
700 let root = translate(9.0).model_matrix();
701 let mid = mat4_mul(root, translate(2.0).model_matrix());
702 assert_eq!(global(&ctx, links[0]), root);
703 assert_eq!(global(&ctx, links[1]), mid);
704 assert_eq!(
705 global(&ctx, links[2]),
706 mat4_mul(mid, translate(7.0).model_matrix())
707 );
708 }
709
710 #[test]
714 fn a_spawned_entity_forces_a_full_resolve() {
715 let mut world = TestWorld::new();
716 let mut ctx = world.ctx();
717 let root = spawn(&mut ctx, translate(1.0), None);
718
719 let mut cache = TransformCache::default();
720 propagate_transforms_cached(&mut ctx, &mut cache);
721
722 let late = spawn(&mut ctx, translate(5.0), Some(root));
723 propagate_transforms_cached(&mut ctx, &mut cache);
724
725 assert_eq!(
726 global(&ctx, late),
727 mat4_mul(translate(1.0).model_matrix(), translate(5.0).model_matrix()),
728 "the entity added after the last resolve composed correctly"
729 );
730 }
731
732 #[test]
735 fn a_despawned_entity_leaves_the_survivors_correct() {
736 let mut world = TestWorld::new();
737 let mut ctx = world.ctx();
738 let root = spawn(&mut ctx, translate(1.0), None);
739 let doomed = spawn(&mut ctx, translate(2.0), Some(root));
740 let kept = spawn(&mut ctx, translate(3.0), Some(root));
741
742 let mut cache = TransformCache::default();
743 propagate_transforms_cached(&mut ctx, &mut cache);
744
745 ctx.despawn(doomed);
746 *ctx.get_mut::<Transform>(root).unwrap() = translate(20.0);
747 propagate_transforms_cached(&mut ctx, &mut cache);
748
749 assert_eq!(
750 global(&ctx, kept),
751 mat4_mul(
752 translate(20.0).model_matrix(),
753 translate(3.0).model_matrix()
754 )
755 );
756 }
757
758 #[test]
761 fn a_whole_column_write_falls_back_to_a_full_resolve() {
762 let mut world = TestWorld::new();
763 let mut ctx = world.ctx();
764 let root = spawn(&mut ctx, translate(1.0), None);
765 let child = spawn(&mut ctx, translate(2.0), Some(root));
766
767 let mut cache = TransformCache::default();
768 propagate_transforms_cached(&mut ctx, &mut cache);
769
770 for t in ctx.query_mut::<Transform>() {
771 t.position[1] += 3.0;
772 }
773 propagate_transforms_cached(&mut ctx, &mut cache);
774
775 let shifted = |x: f32| Transform {
776 position: [x, 3.0, 0.0],
777 rotation_deg: [0.0; 3],
778 scale: [1.0; 3],
779 };
780 assert_eq!(global(&ctx, root), shifted(1.0).model_matrix());
781 assert_eq!(
782 global(&ctx, child),
783 mat4_mul(shifted(1.0).model_matrix(), shifted(2.0).model_matrix())
784 );
785 }
786
787 #[test]
790 fn a_dirty_set_past_the_budget_falls_back_and_still_resolves() {
791 let mut world = TestWorld::new();
792 let mut ctx = world.ctx();
793 let entities: Vec<Entity> = (0..16)
794 .map(|i| spawn(&mut ctx, translate(i as f32), None))
795 .collect();
796
797 let mut cache = TransformCache::default();
798 propagate_transforms_cached(&mut ctx, &mut cache);
799
800 for (i, &e) in entities.iter().enumerate().take(8) {
802 *ctx.get_mut::<Transform>(e).unwrap() = translate(100.0 + i as f32);
803 }
804 propagate_transforms_cached(&mut ctx, &mut cache);
805
806 for (i, &e) in entities.iter().enumerate() {
807 let expected = if i < 8 {
808 translate(100.0 + i as f32)
809 } else {
810 translate(i as f32)
811 };
812 assert_eq!(global(&ctx, e), expected.model_matrix(), "entity {i}");
813 }
814 }
815
816 #[test]
819 fn a_parent_without_a_transform_leaves_the_child_a_root() {
820 let mut world = TestWorld::new();
821 let mut ctx = world.ctx();
822 let bare = ctx.components.spawn();
823 ctx.insert(bare, Children(concinnity_memory::InlineVec::new()));
824 let child = spawn(&mut ctx, translate(4.0), Some(bare));
825
826 propagate_transforms(&mut ctx);
827 assert_eq!(global(&ctx, child), translate(4.0).model_matrix());
828 }
829
830 #[test]
833 fn resolve_world_matrices_breaks_parent_cycle() {
834 let mut world = TestWorld::new();
835 let mut ctx = world.ctx();
836 let a_t = translate(1.0);
837 let b_t = Transform {
838 position: [0.0, 2.0, 0.0],
839 rotation_deg: [0.0; 3],
840 scale: [1.0; 3],
841 };
842
843 let a = ctx.components.spawn();
845 ctx.insert(a, a_t);
846 let b = ctx.components.spawn();
847 ctx.insert(b, b_t);
848 ctx.insert(a, Parent(b));
849 ctx.insert(b, Parent(a));
850
851 let resolved = resolve_world_matrices(&ctx);
852 assert_eq!(resolved.len(), 2);
853 assert_eq!(resolved.get(&a).copied(), Some(a_t.model_matrix()));
855 assert_eq!(resolved.get(&b).copied(), Some(b_t.model_matrix()));
856 }
857
858 #[test]
862 fn an_entity_below_a_cycle_falls_back_to_its_local() {
863 let mut world = TestWorld::new();
864 let mut ctx = world.ctx();
865 let a = spawn(&mut ctx, translate(1.0), None);
866 let b = spawn(&mut ctx, translate(2.0), Some(a));
867 ctx.insert(a, Parent(b));
868 let below = spawn(&mut ctx, translate(3.0), Some(b));
869
870 propagate_transforms(&mut ctx);
871
872 assert_eq!(global(&ctx, a), translate(1.0).model_matrix());
873 assert_eq!(global(&ctx, b), translate(2.0).model_matrix());
874 assert_eq!(global(&ctx, below), translate(3.0).model_matrix());
875 }
876
877 #[test]
882 fn a_very_deep_chain_resolves_iteratively() {
883 const DEPTH: usize = 20_000;
884
885 let mut world = TestWorld::new();
886 let mut ctx = world.ctx();
887 let mut links = Vec::with_capacity(DEPTH);
888 let mut parent = None;
889 for _ in 0..DEPTH {
890 let entity = spawn(&mut ctx, translate(1.0), parent);
891 links.push(entity);
892 parent = Some(entity);
893 }
894
895 propagate_transforms(&mut ctx);
896
897 assert_eq!(global(&ctx, links[DEPTH - 1])[3][0], DEPTH as f32);
898 }
899
900 #[test]
901 fn reparent_recomposes_child_world_matrix_and_relists() {
902 let (a_t, b_t, child_t) = (translate(10.0), translate(-5.0), translate(1.0));
903
904 let mut world = TestWorld::new();
905 let mut ctx = world.ctx();
906 let a = spawn(&mut ctx, a_t, None);
907 let b = spawn(&mut ctx, b_t, None);
908 let child = spawn(&mut ctx, child_t, None);
909
910 reparent(&mut ctx, child, Some(a));
913 let under_a = global(&ctx, child);
914 assert_eq!(
915 under_a,
916 mat4_mul(a_t.model_matrix(), child_t.model_matrix())
917 );
918 assert_eq!(ctx.get::<Children>(a).unwrap().0, vec![child]);
919
920 reparent(&mut ctx, child, Some(b));
922 let under_b = global(&ctx, child);
923 assert_eq!(
924 under_b,
925 mat4_mul(b_t.model_matrix(), child_t.model_matrix())
926 );
927 assert_ne!(under_a, under_b, "the child actually moved");
928 assert!(
929 ctx.get::<Children>(a).unwrap().0.is_empty(),
930 "A unlisted the child"
931 );
932 assert_eq!(ctx.get::<Children>(b).unwrap().0, vec![child]);
933 assert_eq!(ctx.get::<Parent>(child).unwrap().0, b);
934
935 reparent(&mut ctx, child, None);
937 assert_eq!(global(&ctx, child), child_t.model_matrix());
938 assert!(ctx.get::<Parent>(child).is_none(), "child is now a root");
939 assert!(
940 ctx.get::<Children>(b).unwrap().0.is_empty(),
941 "B unlisted the child"
942 );
943 }
944}