Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/flame_3d/lib/src/components/component_3d.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,22 +182,28 @@ abstract class Component3D({
}
}

/// Mark this component's world transform as needing recomputation.
/// Mark this component's world transform, and therefore its AABB, as
/// needing recomputation.
///
/// This propagates down to child [Component3D]s so the entire subtree
/// stays consistent.
void _markWorldTransformDirty() {
if (_worldTransformDirty) {
// The world transform and the AABB are recomputed independently, so one
// can be dirty while the other is not. Only stop when both already are.
if (_worldTransformDirty && _aabbDirty) {
return;
}
_worldTransformDirty = true;
_aabbDirty = true;
for (final child in _childComponents) {
child._markWorldTransformDirty();
}
}

void _onTransformChanged() {
_markWorldTransformDirty();
markAabbDirty();
if (parent case final Component3D parent) {
parent.markAabbDirty();
}
}
}
84 changes: 84 additions & 0 deletions packages/flame_3d/test/components/component_3d_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:flame_3d/components.dart';
import 'package:flame_3d/core.dart';
import 'package:flame_3d/graphics.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('Component3D', () {
testWithFlameGame(
'aabb of a nested component follows its parent when it moves',
(game) async {
final child = _Box(position: Vector3(1, 0, 0));
final parent = _Group(children: [child]);
await game.world.ensureAdd(parent);

expect(child.aabb.center, closeToVector3(Vector3(1, 0, 0)));
expect(parent.aabb.center, closeToVector3(Vector3(1, 0, 0)));

parent.position.setValues(10, 0, 0);

expect(child.aabb.center, closeToVector3(Vector3(11, 0, 0)));
expect(parent.aabb.center, closeToVector3(Vector3(11, 0, 0)));
},
);

testWithFlameGame(
'aabb of a deeply nested component follows its ancestors',
(game) async {
final leaf = _Box();
final root = _Group(
children: [
_Group(
position: Vector3(0, 1, 0),
children: [
_Group(position: Vector3(0, 0, 1), children: [leaf]),
],
),
],
);
await game.world.ensureAdd(root);

expect(leaf.aabb.center, closeToVector3(Vector3(0, 1, 1)));

root.position.setValues(1, 0, 0);

expect(leaf.aabb.center, closeToVector3(Vector3(1, 1, 1)));
expect(root.aabb.center, closeToVector3(Vector3(1, 1, 1)));
},
);

testWithFlameGame(
'aabb of a nested component follows its ancestors after repeated moves',
(game) async {
final leaf = _Box();
final middle = _Group(children: [leaf]);
final root = _Group(children: [middle]);
await game.world.ensureAdd(root);

// Only read the leaf in between the moves, so that the ancestors'
// AABBs stay dirty while the leaf's is recomputed.
root.position.setValues(1, 0, 0);
expect(leaf.aabb.center, closeToVector3(Vector3(1, 0, 0)));

root.position.setValues(2, 0, 0);
expect(leaf.aabb.center, closeToVector3(Vector3(2, 0, 0)));

middle.position.setValues(0, 1, 0);
expect(leaf.aabb.center, closeToVector3(Vector3(2, 1, 0)));
expect(root.aabb.center, closeToVector3(Vector3(2, 1, 0)));
},
);
});
}

class _Group({super.position, super.children}) extends Component3D;

class _Box({super.position}) extends Object3D {
@override
Aabb3? computeLocalAabb() =>
Aabb3.minMax(Vector3.all(-0.5), Vector3.all(0.5));

@override
void draw(RenderContext context) {}
}
Loading