@forgeax/engine-animation 0.0.0-dev.8d955ade1c79
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.
- package/LICENSE +202 -0
- package/README.md +141 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-diagnostic.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-diagnostic.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/animation-recovery.integration.test.d.ts.map +1 -0
- package/dist/__tests__/animation-target-id.browser.test.d.ts +2 -0
- package/dist/__tests__/animation-target-id.browser.test.d.ts.map +1 -0
- package/dist/__tests__/animation-target-id.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-id.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-target-isolation.integration.test.d.ts +2 -0
- package/dist/__tests__/animation-target-isolation.integration.test.d.ts.map +1 -0
- package/dist/__tests__/animation-target-relationship.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-relationship.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-target-source-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-source-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bind-animation-targets.unit.test.d.ts +2 -0
- package/dist/__tests__/bind-animation-targets.unit.test.d.ts.map +1 -0
- package/dist/__tests__/documentation-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/documentation-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/ecs-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/graph-player.integration.test.d.ts +2 -0
- package/dist/__tests__/graph-player.integration.test.d.ts.map +1 -0
- package/dist/__tests__/graph-player.unit.test.d.ts +2 -0
- package/dist/__tests__/graph-player.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/handle-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/morph-weights-playback.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-weights-playback.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-weights.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-weights.unit.test.d.ts.map +1 -0
- package/dist/__tests__/public-animation-target-api.test-d.d.ts +2 -0
- package/dist/__tests__/public-animation-target-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/resolver-ownership.integration.test.d.ts +2 -0
- package/dist/__tests__/resolver-ownership.integration.test.d.ts.map +1 -0
- package/dist/__tests__/root-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/root-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-target-collection.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-target-collection.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/time-delta-regression.integration.test.d.ts +2 -0
- package/dist/__tests__/time-delta-regression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/transform-target-playback.integration.test.d.ts +2 -0
- package/dist/__tests__/transform-target-playback.integration.test.d.ts.map +1 -0
- package/dist/animation-diagnostic.d.ts +27 -0
- package/dist/animation-diagnostic.d.ts.map +1 -0
- package/dist/animation-player.d.ts +13 -0
- package/dist/animation-player.d.ts.map +1 -0
- package/dist/animation-target.d.ts +21 -0
- package/dist/animation-target.d.ts.map +1 -0
- package/dist/assets/animation-decoder.d.ts +4 -0
- package/dist/assets/animation-decoder.d.ts.map +1 -0
- package/dist/chunk-DMXAQUPI.mjs +52 -0
- package/dist/chunk-DMXAQUPI.mjs.map +1 -0
- package/dist/errors.d.ts +120 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/graph/define-animation-graph.d.ts +42 -0
- package/dist/graph/define-animation-graph.d.ts.map +1 -0
- package/dist/graph/describe-animation-graph.d.ts +37 -0
- package/dist/graph/describe-animation-graph.d.ts.map +1 -0
- package/dist/graph/serialize-animation-graph.d.ts +27 -0
- package/dist/graph/serialize-animation-graph.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1459 -0
- package/dist/index.mjs.map +1 -0
- package/dist/player-errors.d.ts +43 -0
- package/dist/player-errors.d.ts.map +1 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/resolve-animation-asset.d.ts +42 -0
- package/dist/resolve-animation-asset.d.ts.map +1 -0
- package/dist/systems/advance-animation-player.d.ts +40 -0
- package/dist/systems/advance-animation-player.d.ts.map +1 -0
- package/dist/systems/evaluate-animation-graph.d.ts +32 -0
- package/dist/systems/evaluate-animation-graph.d.ts.map +1 -0
- package/dist/target-id.d.ts +4 -0
- package/dist/target-id.d.ts.map +1 -0
- package/dist/target-id.mjs +9 -0
- package/dist/target-id.mjs.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/animation-diagnostic.unit.test.ts +237 -0
- package/src/__tests__/animation-recovery.integration.test.ts +125 -0
- package/src/__tests__/animation-target-id.browser.test.ts +22 -0
- package/src/__tests__/animation-target-id.unit.test.ts +41 -0
- package/src/__tests__/animation-target-isolation.integration.test.ts +179 -0
- package/src/__tests__/animation-target-relationship.unit.test.ts +65 -0
- package/src/__tests__/animation-target-source-boundary.unit.test.ts +49 -0
- package/src/__tests__/bind-animation-targets.unit.test.ts +167 -0
- package/src/__tests__/documentation-contract.unit.test.ts +94 -0
- package/src/__tests__/ecs-consumer.integration.test.ts +10 -0
- package/src/__tests__/errors.unit.test.ts +19 -0
- package/src/__tests__/graph-player.integration.test.ts +22 -0
- package/src/__tests__/graph-player.unit.test.ts +12 -0
- package/src/__tests__/handle-errors.unit.test.ts +17 -0
- package/src/__tests__/loader-boundary.unit.test.ts +49 -0
- package/src/__tests__/morph-weights-playback.integration.test.ts +84 -0
- package/src/__tests__/morph-weights.unit.test.ts +26 -0
- package/src/__tests__/public-animation-target-api.test-d.ts +47 -0
- package/src/__tests__/resolver-ownership.integration.test.ts +99 -0
- package/src/__tests__/root-surface.unit.test.ts +10 -0
- package/src/__tests__/scene-instance-target-collection.integration.test.ts +26 -0
- package/src/__tests__/scriptable-pack-consumer.test-d.ts +8 -0
- package/src/__tests__/time-delta-regression.integration.test.ts +79 -0
- package/src/__tests__/transform-target-playback.integration.test.ts +198 -0
- package/src/animation-diagnostic.ts +88 -0
- package/src/animation-player.ts +84 -0
- package/src/animation-target.ts +257 -0
- package/src/assets/animation-decoder.ts +107 -0
- package/src/errors.ts +201 -0
- package/src/graph/define-animation-graph.ts +193 -0
- package/src/graph/describe-animation-graph.ts +74 -0
- package/src/graph/serialize-animation-graph.ts +90 -0
- package/src/index.ts +57 -0
- package/src/player-errors.ts +72 -0
- package/src/plugin.ts +73 -0
- package/src/resolve-animation-asset.ts +128 -0
- package/src/systems/advance-animation-player.ts +971 -0
- package/src/systems/evaluate-animation-graph.ts +263 -0
- package/src/target-id.ts +51 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// @forgeax/engine-animation -- evaluateAnimationGraph system (post-order DAG eval).
|
|
2
|
+
//
|
|
3
|
+
// feat-20260713-animation-state-machine-plugin M3 / w25 (plan D-2 independent
|
|
4
|
+
// before-advance system, D-7 eval owns time):
|
|
5
|
+
//
|
|
6
|
+
// This is the "graph -> N-slot" seam. It runs BEFORE advanceAnimationPlayer (the
|
|
7
|
+
// "N-slot -> pose" seam) and is the SOLE writer of the derived slot columns for
|
|
8
|
+
// entities carrying an AnimationGraph handle (AnimationPlayer.graph != 0):
|
|
9
|
+
//
|
|
10
|
+
// 1. Resolve the shared<AnimationGraph> handle to its POD.
|
|
11
|
+
// 2. Resolve every clip leaf's shared<AnimationClip> BEFORE writing anything --
|
|
12
|
+
// a dangling handle throws the structured animation asset error so no
|
|
13
|
+
// dirty pose is left (AC-04 / AC-11).
|
|
14
|
+
// 3. Post-order evaluate effective weights: effective = runtime weight
|
|
15
|
+
// (nodeWeights, default 1) x graph static weight (AC-07 orthogonal product);
|
|
16
|
+
// Blend normalizes its children (AC-04), Add stacks additive layers without
|
|
17
|
+
// normalization (AC-05), nesting propagates a subtree's normalized weight
|
|
18
|
+
// down before its parent combines it (AC-06).
|
|
19
|
+
// 4. Advance each clip node's seek-time by nodeSpeeds*dt (wrap/clamp reusing
|
|
20
|
+
// advance's looping logic) and persist it back to nodeTimes (D-7: eval owns
|
|
21
|
+
// the time SSOT).
|
|
22
|
+
// 5. Spread one derived slot per clip leaf into the variable clips[] / times[]
|
|
23
|
+
// / weights[] columns and park speeds[]=0 so advance does not re-advance the
|
|
24
|
+
// time (D-7). advance's blend math is left completely unchanged (D-2).
|
|
25
|
+
//
|
|
26
|
+
// Entities without a graph (graph == 0) are left untouched -- their slots stay
|
|
27
|
+
// the direct-write SSOT (single evaluation path, plan D-3). advance's mixing
|
|
28
|
+
// math never branches on graph mode; it just consumes whatever fills the slots.
|
|
29
|
+
|
|
30
|
+
import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
|
|
31
|
+
import type { EntityHandle, SystemHandle, World } from '@forgeax/engine-ecs';
|
|
32
|
+
import { defineSystem, Time, Update } from '@forgeax/engine-ecs';
|
|
33
|
+
import type { AnimationClip, AnimationGraph, Handle } from '@forgeax/engine-types';
|
|
34
|
+
import { AnimationPlayer } from '../animation-player';
|
|
35
|
+
import { resolveAnimationAsset } from '../resolve-animation-asset';
|
|
36
|
+
import { ADVANCE_ANIMATION_PLAYER_SYSTEM } from './advance-animation-player';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* System name used when `registerEvaluateAnimationGraph` installs the system.
|
|
40
|
+
* External consumers can reference this constant to declare ordering against the
|
|
41
|
+
* graph-evaluation seam.
|
|
42
|
+
*/
|
|
43
|
+
export const EVALUATE_ANIMATION_GRAPH_SYSTEM = 'evaluateAnimationGraph' as const;
|
|
44
|
+
|
|
45
|
+
export type AnimationPayloadLookup = (
|
|
46
|
+
guid: string,
|
|
47
|
+
) => AnimationClip | { readonly code: 'stale' } | undefined;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolved per-entity graph-mode columns (a `world.get` snapshot). `graph` is the
|
|
51
|
+
* raw shared handle (0 = no graph); the three node-* columns are per-node runtime
|
|
52
|
+
* knobs indexed by graph node index.
|
|
53
|
+
*/
|
|
54
|
+
interface PlayerGraphColumns {
|
|
55
|
+
readonly graph: number;
|
|
56
|
+
readonly nodeWeights: Float32Array;
|
|
57
|
+
readonly nodeTimes: Float32Array;
|
|
58
|
+
readonly nodeSpeeds: Float32Array;
|
|
59
|
+
readonly paused: boolean;
|
|
60
|
+
readonly looping: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Read `arr[i]` when in range, else the default (missing per-node knobs default). */
|
|
64
|
+
function readAt(arr: Float32Array, i: number, dflt: number): number {
|
|
65
|
+
return i >= 0 && i < arr.length ? (arr[i] ?? dflt) : dflt;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wrap / clamp an advanced time against a clip duration -- byte-for-byte the same
|
|
70
|
+
* rule advanceAnimationPlayer applies (looping = modulo into [0, duration);
|
|
71
|
+
* non-looping = clamp to [0, duration]). Shared logic so eval and advance agree
|
|
72
|
+
* on the seek-time domain (D-7).
|
|
73
|
+
*/
|
|
74
|
+
function wrapTime(time: number, duration: number, looping: boolean): number {
|
|
75
|
+
if (duration <= 0) return time;
|
|
76
|
+
if (looping) {
|
|
77
|
+
let wrapped = time % duration;
|
|
78
|
+
if (wrapped < 0) wrapped += duration;
|
|
79
|
+
return wrapped;
|
|
80
|
+
}
|
|
81
|
+
if (time > duration) return duration;
|
|
82
|
+
if (time < 0) return 0;
|
|
83
|
+
return time;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Evaluate every graph-carrying AnimationPlayer for one frame, filling the
|
|
88
|
+
* derived N-slot columns. Iterates rows through the system Query (like advance),
|
|
89
|
+
* collecting entity handles first, then resolving + writing each player outside
|
|
90
|
+
* the transient row facade.
|
|
91
|
+
*/
|
|
92
|
+
export function evaluateAnimationGraph(
|
|
93
|
+
world: World,
|
|
94
|
+
dt: number,
|
|
95
|
+
lookup: AnimationPayloadLookup = () => undefined,
|
|
96
|
+
): void {
|
|
97
|
+
const query = world.query({ with: [AnimationPlayer] }).unwrap();
|
|
98
|
+
|
|
99
|
+
const entities: EntityHandle[] = [];
|
|
100
|
+
for (const row of query) entities.push(row.entity);
|
|
101
|
+
|
|
102
|
+
for (const entityRaw of entities) {
|
|
103
|
+
evaluateOneEntity(world, entityRaw, dt, lookup);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluate one entity's AnimationGraph into its derived slots. No-op when the
|
|
109
|
+
* entity has no graph (graph == 0). Every non-zero graph or clip handle is
|
|
110
|
+
* resolved through the World-local animation lookup and invalid handles throw
|
|
111
|
+
* a structured animation error before any derived write (AC-04 / AC-11).
|
|
112
|
+
*/
|
|
113
|
+
function evaluateOneEntity(
|
|
114
|
+
world: World,
|
|
115
|
+
entityRaw: number,
|
|
116
|
+
dt: number,
|
|
117
|
+
lookup: AnimationPayloadLookup,
|
|
118
|
+
): void {
|
|
119
|
+
const entity = entityRaw as EntityHandle;
|
|
120
|
+
const apRes = world.get(entity, AnimationPlayer);
|
|
121
|
+
if (!apRes.ok) return;
|
|
122
|
+
const ap = apRes.value as unknown as PlayerGraphColumns;
|
|
123
|
+
|
|
124
|
+
const graphRaw = ap.graph;
|
|
125
|
+
if (graphRaw === 0) return; // no graph -> direct-write path, untouched.
|
|
126
|
+
|
|
127
|
+
const graphLookup = resolveAssetHandle<AnimationGraph>(
|
|
128
|
+
world,
|
|
129
|
+
graphRaw as Handle<string, 'shared'>,
|
|
130
|
+
);
|
|
131
|
+
if (!graphLookup.ok) throw graphLookup.error;
|
|
132
|
+
const graph = graphLookup.value;
|
|
133
|
+
|
|
134
|
+
const nodes = graph.nodes;
|
|
135
|
+
if (nodes.length === 0) return; // construction rejects empty graphs; guard anyway.
|
|
136
|
+
|
|
137
|
+
// Collect clip leaves (ascending node index) and resolve every clip BEFORE any
|
|
138
|
+
// write (AC-11: a dangling handle must not leave a dirty pose).
|
|
139
|
+
const clipNodeIndices: number[] = [];
|
|
140
|
+
const clipHandles: Handle<'AnimationClip', 'shared'>[] = [];
|
|
141
|
+
const clipDurations: number[] = [];
|
|
142
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
143
|
+
const node = nodes[i];
|
|
144
|
+
if (node === undefined || node.type !== 'clip') continue;
|
|
145
|
+
const clipLookup = resolveAnimationAsset<AnimationClip>(
|
|
146
|
+
world,
|
|
147
|
+
node.clip,
|
|
148
|
+
'animation-clip',
|
|
149
|
+
(guid) => {
|
|
150
|
+
return lookup(guid);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
if (!clipLookup.ok) throw clipLookup.error;
|
|
154
|
+
const handle = clipLookup.value.handle;
|
|
155
|
+
clipNodeIndices.push(i);
|
|
156
|
+
clipHandles.push(handle);
|
|
157
|
+
clipDurations.push(clipLookup.value.asset.duration);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Post-order effective-weight evaluation from the root (incoming influence 1).
|
|
161
|
+
const effByNode = new Map<number, number>();
|
|
162
|
+
const runtimeWeight = (n: number): number => readAt(ap.nodeWeights, n, 1);
|
|
163
|
+
const evalNode = (nodeIndex: number, incoming: number): void => {
|
|
164
|
+
const node = nodes[nodeIndex];
|
|
165
|
+
if (node === undefined) return;
|
|
166
|
+
const eff = incoming * runtimeWeight(nodeIndex) * node.weight;
|
|
167
|
+
switch (node.type) {
|
|
168
|
+
case 'clip':
|
|
169
|
+
effByNode.set(nodeIndex, (effByNode.get(nodeIndex) ?? 0) + eff);
|
|
170
|
+
return;
|
|
171
|
+
case 'blend': {
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const child of node.children)
|
|
174
|
+
total += runtimeWeight(child) * (nodes[child]?.weight ?? 0);
|
|
175
|
+
if (total > 0) {
|
|
176
|
+
for (const child of node.children) evalNode(child, eff / total);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
case 'add':
|
|
181
|
+
evalNode(node.base, eff);
|
|
182
|
+
for (const layer of node.additive) evalNode(layer, eff);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
evalNode(graph.root, 1);
|
|
187
|
+
|
|
188
|
+
// Advance each clip node's seek-time (D-7: eval owns the time). Persist the full
|
|
189
|
+
// per-node time column so it stays bounded frame-to-frame.
|
|
190
|
+
const newNodeTimes = new Float32Array(nodes.length);
|
|
191
|
+
for (let i = 0; i < nodes.length; i++) newNodeTimes[i] = readAt(ap.nodeTimes, i, 0);
|
|
192
|
+
for (let k = 0; k < clipNodeIndices.length; k++) {
|
|
193
|
+
// biome-ignore lint/style/noNonNullAssertion: parallel to clipNodeIndices
|
|
194
|
+
const nodeIndex = clipNodeIndices[k]!;
|
|
195
|
+
// biome-ignore lint/style/noNonNullAssertion: parallel to clipNodeIndices
|
|
196
|
+
const duration = clipDurations[k]!;
|
|
197
|
+
const current = readAt(ap.nodeTimes, nodeIndex, 0);
|
|
198
|
+
const speed = readAt(ap.nodeSpeeds, nodeIndex, 0);
|
|
199
|
+
const advanced = ap.paused ? current : current + speed * dt;
|
|
200
|
+
newNodeTimes[nodeIndex] = wrapTime(advanced, duration, ap.looping);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Spread one derived slot per clip leaf into the variable columns; speeds[]=0
|
|
204
|
+
// parks the time so advance does not re-advance it (D-7).
|
|
205
|
+
const slotCount = clipNodeIndices.length;
|
|
206
|
+
const times = new Float32Array(slotCount);
|
|
207
|
+
const weights = new Float32Array(slotCount);
|
|
208
|
+
const speeds = new Float32Array(slotCount);
|
|
209
|
+
for (let k = 0; k < slotCount; k++) {
|
|
210
|
+
// biome-ignore lint/style/noNonNullAssertion: parallel to clipNodeIndices
|
|
211
|
+
const nodeIndex = clipNodeIndices[k]!;
|
|
212
|
+
weights[k] = effByNode.get(nodeIndex) ?? 0;
|
|
213
|
+
times[k] = newNodeTimes[nodeIndex] ?? 0;
|
|
214
|
+
speeds[k] = 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
world.set(entity, AnimationPlayer, {
|
|
218
|
+
clips: clipHandles,
|
|
219
|
+
times,
|
|
220
|
+
weights,
|
|
221
|
+
speeds,
|
|
222
|
+
nodeTimes: newNodeTimes,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The `evaluateAnimationGraph` system token. Runs `before` advanceAnimationPlayer
|
|
228
|
+
* (plan D-2 / R-5) so the derived slots are ready when advance blends them, and
|
|
229
|
+
* is labelled `'animation'`. Registered by the default `animationPlugin` (w26),
|
|
230
|
+
* so it is on the default path (AC-09), not opt-in.
|
|
231
|
+
*/
|
|
232
|
+
export const EvaluateAnimationGraph: SystemHandle<readonly []> = defineSystem({
|
|
233
|
+
name: EVALUATE_ANIMATION_GRAPH_SYSTEM,
|
|
234
|
+
queries: [],
|
|
235
|
+
before: [ADVANCE_ANIMATION_PLAYER_SYSTEM],
|
|
236
|
+
fn: (world) => {
|
|
237
|
+
evaluateAnimationGraph(world, world.getResource(Time).delta);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Register `evaluateAnimationGraph` into the ECS schedule (before
|
|
243
|
+
* advanceAnimationPlayer). Called by `animationPlugin` so both createApp forms
|
|
244
|
+
* get graph evaluation on the default path for free.
|
|
245
|
+
*/
|
|
246
|
+
export function registerEvaluateAnimationGraph(
|
|
247
|
+
world: World,
|
|
248
|
+
lookup: AnimationPayloadLookup = () => undefined,
|
|
249
|
+
): () => void {
|
|
250
|
+
world
|
|
251
|
+
.addSystem(Update, {
|
|
252
|
+
name: EVALUATE_ANIMATION_GRAPH_SYSTEM,
|
|
253
|
+
queries: [],
|
|
254
|
+
before: [ADVANCE_ANIMATION_PLAYER_SYSTEM],
|
|
255
|
+
fn: (world) => {
|
|
256
|
+
evaluateAnimationGraph(world, world.getResource(Time).delta, lookup);
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
.unwrap();
|
|
260
|
+
return () => {
|
|
261
|
+
world.removeSystem(Update, EVALUATE_ANIMATION_GRAPH_SYSTEM);
|
|
262
|
+
};
|
|
263
|
+
}
|
package/src/target-id.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AnimationTargetIdValue } from '@forgeax/engine-types';
|
|
2
|
+
import { blake3 } from '@noble/hashes/blake3.js';
|
|
3
|
+
|
|
4
|
+
const NAMESPACE = Uint8Array.of(
|
|
5
|
+
0x31,
|
|
6
|
+
0x79,
|
|
7
|
+
0xf5,
|
|
8
|
+
0x19,
|
|
9
|
+
0xd9,
|
|
10
|
+
0x27,
|
|
11
|
+
0x4f,
|
|
12
|
+
0xf2,
|
|
13
|
+
0xb5,
|
|
14
|
+
0x96,
|
|
15
|
+
0x6f,
|
|
16
|
+
0xd0,
|
|
17
|
+
0x77,
|
|
18
|
+
0x02,
|
|
19
|
+
0x39,
|
|
20
|
+
0x11,
|
|
21
|
+
);
|
|
22
|
+
const TARGET_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
23
|
+
const textEncoder = new TextEncoder();
|
|
24
|
+
|
|
25
|
+
export function deriveAnimationTargetId(path: readonly string[]): AnimationTargetIdValue {
|
|
26
|
+
const segments = path.map((segment) => textEncoder.encode(segment));
|
|
27
|
+
const input = new Uint8Array(
|
|
28
|
+
NAMESPACE.length + segments.reduce((length, segment) => length + 4 + segment.length, 0),
|
|
29
|
+
);
|
|
30
|
+
input.set(NAMESPACE);
|
|
31
|
+
|
|
32
|
+
const view = new DataView(input.buffer);
|
|
33
|
+
let offset = NAMESPACE.length;
|
|
34
|
+
for (const segment of segments) {
|
|
35
|
+
view.setUint32(offset, segment.length, true);
|
|
36
|
+
offset += 4;
|
|
37
|
+
input.set(segment, offset);
|
|
38
|
+
offset += segment.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bytes = blake3(input).slice(0, 16);
|
|
42
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x80;
|
|
43
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
44
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(
|
|
45
|
+
'',
|
|
46
|
+
) as AnimationTargetIdValue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isAnimationTargetId(value: unknown): value is AnimationTargetIdValue {
|
|
50
|
+
return typeof value === 'string' && TARGET_ID_PATTERN.test(value);
|
|
51
|
+
}
|