@aura3d/ecs 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,182 +0,0 @@
1
- import { ValidationError } from "@aura3d/core";
2
- import { entityKey, sameEntity, type Entity } from "../Entity.js";
3
- import { type System, type SystemContext } from "../System.js";
4
- import { type World } from "../World.js";
5
- import { HierarchyComponent } from "../components/HierarchyComponent.js";
6
-
7
- export class HierarchySystem implements System {
8
- readonly name = "HierarchySystem";
9
- readonly phase = "update";
10
- readonly priority = -100;
11
-
12
- update(world: World, _context: SystemContext): void {
13
- for (const entity of world.query({ include: [HierarchyComponent] }).toArray()) {
14
- const hierarchy = world.get(entity, HierarchyComponent);
15
- if (!hierarchy) continue;
16
- if (hierarchy.parent && !world.entities.isAlive(hierarchy.parent)) {
17
- hierarchy._setParent(null);
18
- hierarchy.depth = 0;
19
- }
20
- }
21
- const cycles = this.detectCycles(world);
22
- if (cycles.length > 0) throw new ValidationError("ECS_HIERARCHY_CYCLE", `Hierarchy cycle detected: ${cycles.map(entityKey).join(", ")}`);
23
- }
24
-
25
- setParent(world: World, child: Entity, parent: Entity | null): void {
26
- world.assertAlive(child);
27
- if (parent) world.assertAlive(parent);
28
- if (parent && sameEntity(child, parent)) throw new ValidationError("ECS_HIERARCHY_SELF_PARENT", "Entity cannot be parented to itself.");
29
- const childHierarchy = ensureHierarchy(world, child);
30
- if (parent) ensureHierarchy(world, parent);
31
- if (parent && this.isAncestorOf(world, child, parent)) {
32
- throw new ValidationError("ECS_HIERARCHY_CYCLE", `Parenting ${entityKey(child)} to ${entityKey(parent)} would create a cycle.`);
33
- }
34
-
35
- const previousParent = childHierarchy.parent;
36
- if (sameOptionalEntity(childHierarchy.parent, parent)) return;
37
- if (previousParent) {
38
- world.get(previousParent, HierarchyComponent)?._removeChild(child);
39
- this.refreshChildSiblings(world, previousParent);
40
- }
41
-
42
- childHierarchy._setParent(parent ? { ...parent } : null);
43
- childHierarchy.previousSibling = null;
44
- childHierarchy.nextSibling = null;
45
- if (parent) {
46
- const parentHierarchy = ensureHierarchy(world, parent);
47
- parentHierarchy._addChild(child);
48
- this.refreshChildSiblings(world, parent);
49
- childHierarchy.depth = parentHierarchy.depth + 1;
50
- } else {
51
- childHierarchy.depth = 0;
52
- }
53
- this.updateDescendantDepths(world, child);
54
- }
55
-
56
- addChild(world: World, parent: Entity, child: Entity): void {
57
- this.setParent(world, child, parent);
58
- }
59
-
60
- removeFromParent(world: World, entity: Entity): void {
61
- this.setParent(world, entity, null);
62
- }
63
-
64
- getParent(world: World, entity: Entity): Entity | null {
65
- return world.get(entity, HierarchyComponent)?.parent ?? null;
66
- }
67
-
68
- getChildren(world: World, entity: Entity): readonly Entity[] {
69
- return world.get(entity, HierarchyComponent)?.children ?? [];
70
- }
71
-
72
- getDepth(world: World, entity: Entity): number {
73
- return world.get(entity, HierarchyComponent)?.depth ?? 0;
74
- }
75
-
76
- forEachDescendant(world: World, entity: Entity, callback: (descendant: Entity, depth: number) => void): void {
77
- const hierarchy = world.get(entity, HierarchyComponent);
78
- if (!hierarchy) return;
79
- for (const child of hierarchy.children) {
80
- const childHierarchy = world.get(child, HierarchyComponent);
81
- callback(child, childHierarchy?.depth ?? hierarchy.depth + 1);
82
- this.forEachDescendant(world, child, callback);
83
- }
84
- }
85
-
86
- isAncestorOf(world: World, ancestor: Entity, descendant: Entity): boolean {
87
- let cursor = world.get(descendant, HierarchyComponent)?.parent ?? null;
88
- const guard = new Set<string>();
89
- while (cursor) {
90
- if (sameEntity(cursor, ancestor)) return true;
91
- const key = entityKey(cursor);
92
- if (guard.has(key)) return false;
93
- guard.add(key);
94
- cursor = world.get(cursor, HierarchyComponent)?.parent ?? null;
95
- }
96
- return false;
97
- }
98
-
99
- isDescendantOf(world: World, descendant: Entity, ancestor: Entity): boolean {
100
- return this.isAncestorOf(world, ancestor, descendant);
101
- }
102
-
103
- setSiblingIndex(world: World, entity: Entity, index: number): void {
104
- const hierarchy = world.get(entity, HierarchyComponent);
105
- if (!hierarchy?.parent) return;
106
- world.get(hierarchy.parent, HierarchyComponent)?._moveChild(entity, index);
107
- this.refreshChildSiblings(world, hierarchy.parent);
108
- }
109
-
110
- detachChildren(world: World, entity: Entity): Entity[] {
111
- const children = [...this.getChildren(world, entity)];
112
- for (const child of children) this.setParent(world, child, null);
113
- return children;
114
- }
115
-
116
- validateHierarchy(world: World): boolean {
117
- return this.detectCycles(world).length === 0 && world.query({ include: [HierarchyComponent] }).toArray().every((entity) => {
118
- const hierarchy = world.get(entity, HierarchyComponent);
119
- if (!hierarchy?.parent) return true;
120
- const parent = world.get(hierarchy.parent, HierarchyComponent);
121
- return Boolean(parent?.includesChild(entity));
122
- });
123
- }
124
-
125
- detectCycles(world: World): Entity[] {
126
- const cycles: Entity[] = [];
127
- const visiting = new Set<string>();
128
- const visited = new Set<string>();
129
- const visit = (entity: Entity): void => {
130
- const key = entityKey(entity);
131
- if (visiting.has(key)) {
132
- cycles.push(entity);
133
- return;
134
- }
135
- if (visited.has(key)) return;
136
- visiting.add(key);
137
- const parent = world.get(entity, HierarchyComponent)?.parent;
138
- if (parent) visit(parent);
139
- visiting.delete(key);
140
- visited.add(key);
141
- };
142
- for (const entity of world.query({ include: [HierarchyComponent] }).toArray()) visit(entity);
143
- return cycles;
144
- }
145
-
146
- private updateDescendantDepths(world: World, entity: Entity): void {
147
- const hierarchy = world.get(entity, HierarchyComponent);
148
- if (!hierarchy) return;
149
- for (const child of hierarchy.children) {
150
- const childHierarchy = world.get(child, HierarchyComponent);
151
- if (!childHierarchy) continue;
152
- childHierarchy.depth = hierarchy.depth + 1;
153
- this.updateDescendantDepths(world, child);
154
- }
155
- }
156
-
157
- private refreshChildSiblings(world: World, parent: Entity): void {
158
- const parentHierarchy = world.get(parent, HierarchyComponent);
159
- if (!parentHierarchy) return;
160
- const children = parentHierarchy.children;
161
- for (let index = 0; index < children.length; index += 1) {
162
- const child = children[index];
163
- const hierarchy = world.get(child, HierarchyComponent);
164
- if (!hierarchy) continue;
165
- hierarchy.previousSibling = children[index - 1] ? { ...children[index - 1] } : null;
166
- hierarchy.nextSibling = children[index + 1] ? { ...children[index + 1] } : null;
167
- }
168
- }
169
- }
170
-
171
- function ensureHierarchy(world: World, entity: Entity): HierarchyComponent {
172
- if (!world.registry.get(HierarchyComponent)) world.registerComponent(HierarchyComponent);
173
- const existing = world.get(entity, HierarchyComponent);
174
- if (existing) return existing;
175
- world.add(entity, HierarchyComponent, new HierarchyComponent());
176
- return world.get(entity, HierarchyComponent) as HierarchyComponent;
177
- }
178
-
179
- function sameOptionalEntity(left: Entity | null, right: Entity | null): boolean {
180
- if (!left || !right) return left === right;
181
- return sameEntity(left, right);
182
- }
@@ -1,108 +0,0 @@
1
- import { Matrix4, Quaternion, Vector3 } from "@aura3d/math";
2
- import { type Entity } from "../Entity.js";
3
- import { type System, type SystemContext } from "../System.js";
4
- import { type World } from "../World.js";
5
- import { HierarchyComponent } from "../components/HierarchyComponent.js";
6
- import { TransformComponent } from "../components/TransformComponent.js";
7
- import { WorldTransformComponent } from "../components/WorldTransformComponent.js";
8
-
9
- /** Safety cap for defensive parent-chain walks (guards against cycles created by direct `parent` mutation). */
10
- const MAX_HIERARCHY_DEPTH = 4096;
11
-
12
- /**
13
- * Computes {@link WorldTransformComponent} for every entity that has both
14
- * {@link TransformComponent} and {@link HierarchyComponent}.
15
- *
16
- * Entities are processed in depth order (parents before children) so the
17
- * world matrix cascade is correct in a single pass.
18
- */
19
- export class TransformSystem implements System {
20
- readonly name = "TransformSystem";
21
- readonly phase = "update";
22
- readonly priority = -80;
23
-
24
- update(world: World, _context: SystemContext): void {
25
- const entities = world
26
- .query({ include: [TransformComponent, HierarchyComponent] })
27
- .toArray()
28
- .sort((a, b) => effectiveDepth(world, a) - effectiveDepth(world, b));
29
-
30
- for (const entity of entities) {
31
- const transform = world.get(entity, TransformComponent)!;
32
- const hierarchy = world.get(entity, HierarchyComponent)!;
33
-
34
- const local = Matrix4.compose(
35
- new Vector3(...transform.position),
36
- new Quaternion(...transform.rotation),
37
- new Vector3(...transform.scale)
38
- );
39
-
40
- let worldMat: Matrix4;
41
- if (hierarchy.parent) {
42
- const parentWtc = world.get(hierarchy.parent, WorldTransformComponent);
43
- worldMat = parentWtc
44
- ? new Matrix4(parentWtc.worldMatrix as unknown as Matrix4["elements"]).multiply(local)
45
- : local;
46
- } else {
47
- worldMat = local;
48
- }
49
-
50
- let wtc = world.get(entity, WorldTransformComponent);
51
- if (!wtc) {
52
- wtc = new WorldTransformComponent();
53
- world.add(entity, WorldTransformComponent, wtc);
54
- }
55
- (wtc.worldMatrix as Float32Array).set(worldMat.elements);
56
- (wtc.normalMatrix as Float32Array).set(computeNormalMatrix(worldMat));
57
- }
58
- }
59
- }
60
-
61
- /**
62
- * Returns the depth used for parents-before-children ordering.
63
- *
64
- * {@link HierarchyComponent.depth} is maintained by `HierarchySystem.setParent`.
65
- * If `parent` was set directly (constructor argument or field mutation) the
66
- * stored depth is stale at 0, so recompute it defensively by walking the
67
- * parent chain.
68
- */
69
- function effectiveDepth(world: World, entity: Entity): number {
70
- const hierarchy = world.get(entity, HierarchyComponent);
71
- if (!hierarchy) return 0;
72
- if (hierarchy.depth > 0 || hierarchy.parent === null) return hierarchy.depth;
73
-
74
- let depth = 0;
75
- let current: Entity | null = hierarchy.parent;
76
- while (current && depth < MAX_HIERARCHY_DEPTH) {
77
- depth += 1;
78
- const parentHierarchy: HierarchyComponent | undefined = world.entities.isAlive(current)
79
- ? world.get(current, HierarchyComponent)
80
- : undefined;
81
- current = parentHierarchy?.parent ?? null;
82
- }
83
- return depth;
84
- }
85
-
86
- function computeNormalMatrix(worldMat: Matrix4): Float32Array {
87
- // Normal matrix = inverse-TRANSPOSE of the world matrix, matching the
88
- // renderer's reference `normalMatrixFromModel` (Renderer.ts):
89
- // transpose(invert(model)), then extract the upper-left 3x3 into a 4x4
90
- // with identity in the 4th row/col (column-major layout).
91
- // Avoid crashing on singular matrices (zero scale) by falling back to identity.
92
- try {
93
- const m = worldMat.inverse().transpose().elements;
94
- return new Float32Array([
95
- m[0], m[1], m[2], 0,
96
- m[4], m[5], m[6], 0,
97
- m[8], m[9], m[10], 0,
98
- 0, 0, 0, 1,
99
- ]);
100
- } catch {
101
- return new Float32Array([
102
- 1, 0, 0, 0,
103
- 0, 1, 0, 0,
104
- 0, 0, 1, 0,
105
- 0, 0, 0, 1,
106
- ]);
107
- }
108
- }
@@ -1,4 +0,0 @@
1
- export * from "./ActiveSystem.js";
2
- export * from "./HierarchySystem.js";
3
- export * from "./TransformSystem.js";
4
- export * from "./CameraSystem.js";
package/tests/ecs.test.ts DELETED
@@ -1,134 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
- import {
4
- CommandBuffer,
5
- NameComponent,
6
- SystemScheduler,
7
- TagComponent,
8
- TransformComponent,
9
- World,
10
- deserializeWorld,
11
- profileWorld,
12
- serializeWorld,
13
- type System
14
- } from "../src/index.js";
15
-
16
- class VelocityComponent {
17
- constructor(public value: [number, number, number]) {}
18
- }
19
-
20
- test("entities reject stale handles after destroy and reuse", () => {
21
- const world = new World();
22
- const first = world.createEntity();
23
- assert.equal(world.destroyEntity(first), true);
24
- const second = world.createEntity();
25
-
26
- assert.equal(first.id, second.id);
27
- assert.notEqual(first.generation, second.generation);
28
- assert.throws(() => world.get(first, NameComponent), /not alive/);
29
- });
30
-
31
- test("component registration is deterministic per world", () => {
32
- const a = new World();
33
- const b = new World();
34
- a.registerComponent(TransformComponent);
35
- a.registerComponent(NameComponent);
36
- b.registerComponent(TransformComponent);
37
- b.registerComponent(NameComponent);
38
-
39
- assert.equal(a.registry.require(TransformComponent).id, 0);
40
- assert.equal(a.registry.require(NameComponent).id, 1);
41
- assert.equal(b.registry.require(TransformComponent).id, 0);
42
- assert.equal(b.registry.require(NameComponent).id, 1);
43
- });
44
-
45
- test("query include and exclude masks update after component transitions", () => {
46
- const world = new World();
47
- const moving = world.createEntity();
48
- const named = world.createEntity();
49
- world.add(moving, TransformComponent, new TransformComponent());
50
- world.add(moving, VelocityComponent, new VelocityComponent([1, 0, 0]));
51
- world.add(named, TransformComponent, new TransformComponent());
52
- world.add(named, NameComponent, new NameComponent("stationary"));
53
-
54
- assert.deepEqual(world.query({ include: [TransformComponent], exclude: [NameComponent] }).toArray(), [moving]);
55
-
56
- world.remove(moving, VelocityComponent);
57
- world.add(moving, NameComponent, new NameComponent("moving"));
58
-
59
- assert.deepEqual(world.query({ include: [TransformComponent], exclude: [NameComponent] }).toArray(), []);
60
- });
61
-
62
- test("command buffer supports create/add/destroy during query iteration", () => {
63
- const world = new World();
64
- const original = world.createEntity();
65
- world.add(original, TransformComponent, new TransformComponent());
66
- const commands = new CommandBuffer();
67
-
68
- world.query({ include: [TransformComponent] }).forEach((entity) => {
69
- const temp = commands.createEntity();
70
- commands.add(temp, TransformComponent, new TransformComponent([1, 2, 3]));
71
- commands.destroyEntity(entity);
72
- });
73
- const created = commands.flush(world);
74
-
75
- assert.equal(created.length, 1);
76
- assert.deepEqual(world.query({ include: [TransformComponent] }).toArray(), created);
77
- assert.deepEqual(world.get(created[0], TransformComponent)?.position, [1, 2, 3]);
78
- });
79
-
80
- test("system scheduler orders dependencies and rejects cycles", () => {
81
- const order: string[] = [];
82
- const make = (name: string, after: string[] = []): System => ({
83
- name,
84
- after,
85
- update: () => order.push(name)
86
- });
87
- const scheduler = new SystemScheduler();
88
- scheduler.add(make("input"));
89
- scheduler.add(make("movement", ["input"]));
90
- scheduler.add(make("commit", ["movement"]));
91
-
92
- assert.deepEqual(scheduler.order().map((system) => system.name), ["input", "movement", "commit"]);
93
-
94
- const cycle = new SystemScheduler();
95
- cycle.add(make("a", ["b"]));
96
- cycle.add(make("b", ["a"]));
97
- assert.throws(() => cycle.order(), /cycle rejected/);
98
- });
99
-
100
- test("world update runs fixed, update, and late phases in explicit order", () => {
101
- const world = new World();
102
- const order: string[] = [];
103
- world.systems.add({ name: "late", phase: "late", update: () => order.push("late") });
104
- world.systems.add({ name: "fixed", phase: "fixed", update: () => order.push("fixed") });
105
- world.systems.add({ name: "update", phase: "update", update: () => order.push("update") });
106
-
107
- world.update({ deltaTime: 1 / 60, elapsedTime: 1, frame: 1 });
108
-
109
- assert.deepEqual(order, ["fixed", "update", "late"]);
110
- });
111
-
112
- test("ECS serialization roundtrips registered component data and remaps entities", () => {
113
- const world = new World();
114
- world.registerComponent(TransformComponent);
115
- world.registerComponent(NameComponent);
116
- const entity = world.createEntity();
117
- world.add(entity, TransformComponent, new TransformComponent([1, 2, 3]));
118
- world.add(entity, NameComponent, new NameComponent("hero"));
119
-
120
- const { world: restored, remap } = deserializeWorld(serializeWorld(world), [TransformComponent, NameComponent]);
121
- const restoredEntity = remap.get(`${entity.id}:${entity.generation}`);
122
-
123
- assert.ok(restoredEntity);
124
- assert.deepEqual(restored.get(restoredEntity, TransformComponent), { position: [1, 2, 3], rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
125
- assert.deepEqual(restored.get(restoredEntity, NameComponent), { name: "hero" });
126
- });
127
-
128
- test("profiler reports entity and component counts", () => {
129
- const world = new World();
130
- const entity = world.createEntity();
131
- world.add(entity, TagComponent, new TagComponent("player"));
132
-
133
- assert.deepEqual(profileWorld(world).components.map((entry) => [entry.name, entry.count]), [["TagComponent", 1]]);
134
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "declaration": true,
5
- "outDir": "dist",
6
- "emitDeclarationOnly": true
7
- },
8
- "include": ["src/**/*.ts"]
9
- }