@aura3d/animation 1.0.6
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/README.md +19 -0
- package/dist/AnimationAction.d.ts +40 -0
- package/dist/AnimationAction.d.ts.map +1 -0
- package/dist/AnimationAction.js +143 -0
- package/dist/AnimationAction.js.map +1 -0
- package/dist/AnimationClip.d.ts +24 -0
- package/dist/AnimationClip.d.ts.map +1 -0
- package/dist/AnimationClip.js +45 -0
- package/dist/AnimationClip.js.map +1 -0
- package/dist/AnimationClipEvents.d.ts +57 -0
- package/dist/AnimationClipEvents.d.ts.map +1 -0
- package/dist/AnimationClipEvents.js +171 -0
- package/dist/AnimationClipEvents.js.map +1 -0
- package/dist/AnimationClipRegistry.d.ts +76 -0
- package/dist/AnimationClipRegistry.d.ts.map +1 -0
- package/dist/AnimationClipRegistry.js +130 -0
- package/dist/AnimationClipRegistry.js.map +1 -0
- package/dist/AnimationController.d.ts +168 -0
- package/dist/AnimationController.d.ts.map +1 -0
- package/dist/AnimationController.js +619 -0
- package/dist/AnimationController.js.map +1 -0
- package/dist/AnimationEvents.d.ts +10 -0
- package/dist/AnimationEvents.d.ts.map +1 -0
- package/dist/AnimationEvents.js +26 -0
- package/dist/AnimationEvents.js.map +1 -0
- package/dist/AnimationLayer.d.ts +27 -0
- package/dist/AnimationLayer.d.ts.map +1 -0
- package/dist/AnimationLayer.js +53 -0
- package/dist/AnimationLayer.js.map +1 -0
- package/dist/AnimationMixer.d.ts +54 -0
- package/dist/AnimationMixer.d.ts.map +1 -0
- package/dist/AnimationMixer.js +319 -0
- package/dist/AnimationMixer.js.map +1 -0
- package/dist/AnimationStateGraph.d.ts +2 -0
- package/dist/AnimationStateGraph.d.ts.map +1 -0
- package/dist/AnimationStateGraph.js +2 -0
- package/dist/AnimationStateGraph.js.map +1 -0
- package/dist/AnimationStateMachine.d.ts +59 -0
- package/dist/AnimationStateMachine.d.ts.map +1 -0
- package/dist/AnimationStateMachine.js +154 -0
- package/dist/AnimationStateMachine.js.map +1 -0
- package/dist/AnimationTrack.d.ts +23 -0
- package/dist/AnimationTrack.d.ts.map +1 -0
- package/dist/AnimationTrack.js +124 -0
- package/dist/AnimationTrack.js.map +1 -0
- package/dist/BlendTree.d.ts +23 -0
- package/dist/BlendTree.d.ts.map +1 -0
- package/dist/BlendTree.js +68 -0
- package/dist/BlendTree.js.map +1 -0
- package/dist/Bone.d.ts +20 -0
- package/dist/Bone.d.ts.map +1 -0
- package/dist/Bone.js +24 -0
- package/dist/Bone.js.map +1 -0
- package/dist/CrowdAnimation.d.ts +13 -0
- package/dist/CrowdAnimation.d.ts.map +1 -0
- package/dist/CrowdAnimation.js +14 -0
- package/dist/CrowdAnimation.js.map +1 -0
- package/dist/ECSAnimationBridge.d.ts +9 -0
- package/dist/ECSAnimationBridge.d.ts.map +1 -0
- package/dist/ECSAnimationBridge.js +24 -0
- package/dist/ECSAnimationBridge.js.map +1 -0
- package/dist/HumanoidRetargeting.d.ts +76 -0
- package/dist/HumanoidRetargeting.d.ts.map +1 -0
- package/dist/HumanoidRetargeting.js +331 -0
- package/dist/HumanoidRetargeting.js.map +1 -0
- package/dist/IK.d.ts +24 -0
- package/dist/IK.d.ts.map +1 -0
- package/dist/IK.js +76 -0
- package/dist/IK.js.map +1 -0
- package/dist/Keyframe.d.ts +50 -0
- package/dist/Keyframe.d.ts.map +1 -0
- package/dist/Keyframe.js +118 -0
- package/dist/Keyframe.js.map +1 -0
- package/dist/LocomotionController.d.ts +47 -0
- package/dist/LocomotionController.d.ts.map +1 -0
- package/dist/LocomotionController.js +106 -0
- package/dist/LocomotionController.js.map +1 -0
- package/dist/MotionMatchingFixtures.d.ts +58 -0
- package/dist/MotionMatchingFixtures.d.ts.map +1 -0
- package/dist/MotionMatchingFixtures.js +175 -0
- package/dist/MotionMatchingFixtures.js.map +1 -0
- package/dist/MotionQuality.d.ts +36 -0
- package/dist/MotionQuality.d.ts.map +1 -0
- package/dist/MotionQuality.js +117 -0
- package/dist/MotionQuality.js.map +1 -0
- package/dist/Retargeting.d.ts +12 -0
- package/dist/Retargeting.d.ts.map +1 -0
- package/dist/Retargeting.js +19 -0
- package/dist/Retargeting.js.map +1 -0
- package/dist/RootMotion.d.ts +21 -0
- package/dist/RootMotion.d.ts.map +1 -0
- package/dist/RootMotion.js +84 -0
- package/dist/RootMotion.js.map +1 -0
- package/dist/SceneAnimationBridge.d.ts +18 -0
- package/dist/SceneAnimationBridge.d.ts.map +1 -0
- package/dist/SceneAnimationBridge.js +69 -0
- package/dist/SceneAnimationBridge.js.map +1 -0
- package/dist/SecondaryAnimationFixtures.d.ts +55 -0
- package/dist/SecondaryAnimationFixtures.d.ts.map +1 -0
- package/dist/SecondaryAnimationFixtures.js +147 -0
- package/dist/SecondaryAnimationFixtures.js.map +1 -0
- package/dist/Skeleton.d.ts +9 -0
- package/dist/Skeleton.d.ts.map +1 -0
- package/dist/Skeleton.js +31 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Skinning.d.ts +7 -0
- package/dist/Skinning.d.ts.map +1 -0
- package/dist/Skinning.js +12 -0
- package/dist/Skinning.js.map +1 -0
- package/dist/browser-index.d.ts +39 -0
- package/dist/browser-index.d.ts.map +1 -0
- package/dist/browser-index.js +34 -0
- package/dist/browser-index.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/threejs-compatibility/AnimationAction.d.ts +16 -0
- package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationAction.js +25 -0
- package/dist/threejs-compatibility/AnimationAction.js.map +1 -0
- package/dist/threejs-compatibility/AnimationClip.d.ts +14 -0
- package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationClip.js +11 -0
- package/dist/threejs-compatibility/AnimationClip.js.map +1 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.d.ts +44 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.js +29 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -0
- package/dist/threejs-compatibility/AnimationMixer.d.ts +9 -0
- package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationMixer.js +24 -0
- package/dist/threejs-compatibility/AnimationMixer.js.map +1 -0
- package/dist/threejs-compatibility/MorphTargetMixer.d.ts +10 -0
- package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -0
- package/dist/threejs-compatibility/MorphTargetMixer.js +10 -0
- package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -0
- package/dist/threejs-compatibility/Skeleton.d.ts +10 -0
- package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -0
- package/dist/threejs-compatibility/Skeleton.js +8 -0
- package/dist/threejs-compatibility/Skeleton.js.map +1 -0
- package/dist/threejs-compatibility/SkinnedMesh.d.ts +11 -0
- package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -0
- package/dist/threejs-compatibility/SkinnedMesh.js +16 -0
- package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -0
- package/dist/threejs-compatibility/index.d.ts +12 -0
- package/dist/threejs-compatibility/index.d.ts.map +1 -0
- package/dist/threejs-compatibility/index.js +8 -0
- package/dist/threejs-compatibility/index.js.map +1 -0
- package/package.json +17 -0
- package/src/AnimationAction.ts +166 -0
- package/src/AnimationClip.ts +63 -0
- package/src/AnimationClipEvents.ts +320 -0
- package/src/AnimationClipRegistry.ts +280 -0
- package/src/AnimationController.ts +967 -0
- package/src/AnimationEvents.ts +34 -0
- package/src/AnimationLayer.ts +73 -0
- package/src/AnimationMixer.ts +366 -0
- package/src/AnimationStateGraph.ts +8 -0
- package/src/AnimationStateMachine.ts +217 -0
- package/src/AnimationTrack.ts +176 -0
- package/src/BlendTree.ts +89 -0
- package/src/Bone.ts +35 -0
- package/src/CrowdAnimation.ts +25 -0
- package/src/ECSAnimationBridge.ts +30 -0
- package/src/HumanoidRetargeting.ts +476 -0
- package/src/IK.ts +107 -0
- package/src/Keyframe.ts +159 -0
- package/src/LocomotionController.ts +149 -0
- package/src/MotionMatchingFixtures.ts +254 -0
- package/src/MotionQuality.ts +156 -0
- package/src/Retargeting.ts +29 -0
- package/src/RootMotion.ts +124 -0
- package/src/SceneAnimationBridge.ts +80 -0
- package/src/SecondaryAnimationFixtures.ts +212 -0
- package/src/Skeleton.ts +34 -0
- package/src/Skinning.ts +18 -0
- package/src/browser-index.ts +55 -0
- package/src/index.ts +56 -0
- package/src/threejs-compatibility/AnimationAction.ts +27 -0
- package/src/threejs-compatibility/AnimationClip.ts +16 -0
- package/src/threejs-compatibility/AnimationDiagnostics.ts +42 -0
- package/src/threejs-compatibility/AnimationMixer.ts +24 -0
- package/src/threejs-compatibility/MorphTargetMixer.ts +16 -0
- package/src/threejs-compatibility/Skeleton.ts +9 -0
- package/src/threejs-compatibility/SkinnedMesh.ts +12 -0
- package/src/threejs-compatibility/index.ts +11 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type AnimationEventMarker = {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly time: number;
|
|
4
|
+
readonly payload?: unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type AnimationEvent = AnimationEventMarker & {
|
|
8
|
+
readonly clipName: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function eventsBetween(markers: readonly AnimationEventMarker[], clipName: string, fromTime: number, toTime: number, duration: number, looped: boolean): readonly AnimationEvent[] {
|
|
12
|
+
const events: AnimationEvent[] = [];
|
|
13
|
+
if (duration <= 0) {
|
|
14
|
+
return events;
|
|
15
|
+
}
|
|
16
|
+
if (!looped || toTime >= fromTime) {
|
|
17
|
+
for (const marker of markers) {
|
|
18
|
+
if (marker.time > fromTime && marker.time <= toTime) {
|
|
19
|
+
events.push({ ...marker, clipName });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return events;
|
|
23
|
+
}
|
|
24
|
+
const lateEvents: AnimationEvent[] = [];
|
|
25
|
+
const earlyEvents: AnimationEvent[] = [];
|
|
26
|
+
for (const marker of markers) {
|
|
27
|
+
if (marker.time > fromTime && marker.time <= duration) {
|
|
28
|
+
lateEvents.push({ ...marker, clipName });
|
|
29
|
+
} else if (marker.time >= 0 && marker.time <= toTime) {
|
|
30
|
+
earlyEvents.push({ ...marker, clipName });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...lateEvents, ...earlyEvents];
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AnimationAction } from "./AnimationAction.js";
|
|
2
|
+
|
|
3
|
+
export type AnimationLayerSnapshot = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly weight: number;
|
|
6
|
+
readonly additive: boolean;
|
|
7
|
+
readonly mask: readonly string[];
|
|
8
|
+
readonly actions: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AnimationLayerOptions = {
|
|
12
|
+
readonly weight?: number;
|
|
13
|
+
readonly additive?: boolean;
|
|
14
|
+
readonly mask?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class AnimationLayer {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
weight: number;
|
|
20
|
+
additive: boolean;
|
|
21
|
+
readonly mask: readonly string[];
|
|
22
|
+
readonly actions: AnimationAction[] = [];
|
|
23
|
+
private readonly baseWeights = new WeakMap<AnimationAction, number>();
|
|
24
|
+
|
|
25
|
+
constructor(name: string, weightOrOptions: number | AnimationLayerOptions = 1) {
|
|
26
|
+
if (name.trim().length === 0) {
|
|
27
|
+
throw new Error("AnimationLayer name cannot be empty.");
|
|
28
|
+
}
|
|
29
|
+
this.name = name;
|
|
30
|
+
const options = typeof weightOrOptions === "number" ? { weight: weightOrOptions } : weightOrOptions;
|
|
31
|
+
this.weight = options.weight ?? 1;
|
|
32
|
+
this.additive = options.additive ?? false;
|
|
33
|
+
this.mask = [...(options.mask ?? [])];
|
|
34
|
+
if (!Number.isFinite(this.weight) || this.weight < 0) {
|
|
35
|
+
throw new Error("AnimationLayer weight must be finite and non-negative.");
|
|
36
|
+
}
|
|
37
|
+
for (const entry of this.mask) {
|
|
38
|
+
if (entry.trim().length === 0) {
|
|
39
|
+
throw new Error("AnimationLayer mask entries cannot be empty.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
add(action: AnimationAction): void {
|
|
45
|
+
if (!this.actions.includes(action)) {
|
|
46
|
+
this.actions.push(action);
|
|
47
|
+
this.baseWeights.set(action, action.weight);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
applyWeight(): void {
|
|
52
|
+
for (const action of this.actions) {
|
|
53
|
+
action.setWeight((this.baseWeights.get(action) ?? action.weight) * this.weight);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
capturesTarget(target: string): boolean {
|
|
58
|
+
if (this.mask.length === 0) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return this.mask.some((entry) => target === entry || target.startsWith(`${entry}.`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
snapshot(): AnimationLayerSnapshot {
|
|
65
|
+
return {
|
|
66
|
+
name: this.name,
|
|
67
|
+
weight: this.weight,
|
|
68
|
+
additive: this.additive,
|
|
69
|
+
mask: [...this.mask],
|
|
70
|
+
actions: this.actions.map((action) => action.clip.name)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { AnimationAction, type AnimationActionSnapshot } from "./AnimationAction.js";
|
|
2
|
+
import type { AnimationClip } from "./AnimationClip.js";
|
|
3
|
+
import type { AnimationEvent } from "./AnimationEvents.js";
|
|
4
|
+
import type { AnimationLayer, AnimationLayerSnapshot } from "./AnimationLayer.js";
|
|
5
|
+
import { cloneAnimationValue, normalizeQuat, slerpQuat, type AnimationValue } from "./Keyframe.js";
|
|
6
|
+
import { applyRootMotion, extractRootMotion, type RootMotionSample, type RootMotionTarget } from "./RootMotion.js";
|
|
7
|
+
|
|
8
|
+
export type AnimationTarget = {
|
|
9
|
+
setAnimationValue?: (target: string, value: AnimationValue) => void;
|
|
10
|
+
applyRootMotion?: (sample: RootMotionSample) => void;
|
|
11
|
+
position?: [number, number, number];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AnimationMixerOptions = {
|
|
15
|
+
readonly applyRootMotion?: boolean;
|
|
16
|
+
readonly rootMotionTrack?: string;
|
|
17
|
+
readonly rootMotionScale?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AnimationMixerSnapshot = {
|
|
21
|
+
readonly timeScale: number;
|
|
22
|
+
readonly actionCount: number;
|
|
23
|
+
readonly actions: readonly AnimationActionSnapshot[];
|
|
24
|
+
readonly layers: readonly AnimationLayerSnapshot[];
|
|
25
|
+
readonly values: Readonly<Record<string, AnimationValue>>;
|
|
26
|
+
readonly applyErrors: readonly AnimationApplyError[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AnimationApplyError = {
|
|
30
|
+
readonly target: string;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type MixerLayer = Pick<AnimationLayer, "name" | "weight" | "additive" | "mask" | "actions" | "capturesTarget" | "snapshot">;
|
|
35
|
+
type WeightedAccumulator = { value: AnimationValue; weight: number; type: string };
|
|
36
|
+
type TargetAccumulator = { type: string; base?: WeightedAccumulator; additive?: AnimationValue };
|
|
37
|
+
|
|
38
|
+
export class AnimationMixer {
|
|
39
|
+
readonly target: AnimationTarget | undefined;
|
|
40
|
+
timeScale = 1;
|
|
41
|
+
private readonly actions: AnimationAction[] = [];
|
|
42
|
+
private readonly layers: AnimationLayer[] = [];
|
|
43
|
+
private readonly values = new Map<string, AnimationValue>();
|
|
44
|
+
private applyErrors: AnimationApplyError[] = [];
|
|
45
|
+
private readonly eventListeners = new Set<(event: AnimationEvent) => void>();
|
|
46
|
+
private disposed = false;
|
|
47
|
+
|
|
48
|
+
constructor(target?: AnimationTarget, private readonly options: AnimationMixerOptions = {}) {
|
|
49
|
+
this.target = target;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
play(clip: AnimationClip): AnimationAction {
|
|
53
|
+
this.assertAlive();
|
|
54
|
+
const action = new AnimationAction(clip).play();
|
|
55
|
+
this.actions.push(action);
|
|
56
|
+
return action;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addAction(action: AnimationAction): void {
|
|
60
|
+
this.assertAlive();
|
|
61
|
+
if (!this.actions.includes(action)) {
|
|
62
|
+
this.actions.push(action);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addLayer(layer: AnimationLayer): void {
|
|
67
|
+
this.assertAlive();
|
|
68
|
+
if (!this.layers.includes(layer)) {
|
|
69
|
+
this.layers.push(layer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
stopAll(): void {
|
|
74
|
+
this.assertAlive();
|
|
75
|
+
for (const action of this.actions) {
|
|
76
|
+
action.stop();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
crossFade(from: AnimationAction, to: AnimationAction, duration: number): void {
|
|
81
|
+
this.assertAlive();
|
|
82
|
+
from.fadeTo(0, duration);
|
|
83
|
+
to.play().fadeTo(1, duration);
|
|
84
|
+
this.addAction(to);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update(delta: number): readonly AnimationEvent[] {
|
|
88
|
+
this.assertAlive();
|
|
89
|
+
if (!Number.isFinite(delta) || delta < 0) {
|
|
90
|
+
throw new Error("AnimationMixer delta must be finite and non-negative.");
|
|
91
|
+
}
|
|
92
|
+
const events: AnimationEvent[] = [];
|
|
93
|
+
const accumulators = new Map<string, TargetAccumulator>();
|
|
94
|
+
for (const action of this.actions) {
|
|
95
|
+
const previousTime = action.time;
|
|
96
|
+
const actionEvents = action.update(delta * this.timeScale);
|
|
97
|
+
if (actionEvents.length > 0) {
|
|
98
|
+
events.push(...actionEvents);
|
|
99
|
+
}
|
|
100
|
+
if (!action.playing && action.weight <= 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
this.applyActionRootMotion(action, previousTime);
|
|
104
|
+
for (const track of action.clip.tracks) {
|
|
105
|
+
const sample = track.sample(action.time);
|
|
106
|
+
for (const layer of this.layersForAction(action)) {
|
|
107
|
+
if (layer.capturesTarget(track.target)) {
|
|
108
|
+
blendInto(accumulators, track.target, track.valueType, sample, action.weight * layer.weight, layer.additive);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.values.clear();
|
|
114
|
+
this.applyErrors = [];
|
|
115
|
+
for (const [target, accumulator] of accumulators) {
|
|
116
|
+
const value = finalizeTargetBlend(accumulator);
|
|
117
|
+
this.values.set(target, value);
|
|
118
|
+
try {
|
|
119
|
+
this.target?.setAnimationValue?.(target, value);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this.applyErrors.push({
|
|
122
|
+
target,
|
|
123
|
+
message: error instanceof Error ? error.message : String(error)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const event of events) {
|
|
128
|
+
for (const listener of Array.from(this.eventListeners)) {
|
|
129
|
+
listener(event);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return events;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getValue(target: string): AnimationValue | undefined {
|
|
136
|
+
const value = this.values.get(target);
|
|
137
|
+
return value === undefined ? undefined : cloneAnimationValue(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onEvent(listener: (event: AnimationEvent) => void): () => void {
|
|
141
|
+
this.assertAlive();
|
|
142
|
+
this.eventListeners.add(listener);
|
|
143
|
+
return () => this.eventListeners.delete(listener);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
snapshot(): AnimationMixerSnapshot {
|
|
147
|
+
const values: Record<string, AnimationValue> = {};
|
|
148
|
+
for (const [target, value] of this.values) {
|
|
149
|
+
values[target] = cloneAnimationValue(value);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
timeScale: this.timeScale,
|
|
153
|
+
actionCount: this.actions.length,
|
|
154
|
+
actions: this.actions.map((action) => action.snapshot()),
|
|
155
|
+
layers: this.layers.map((layer) => layer.snapshot()),
|
|
156
|
+
values,
|
|
157
|
+
applyErrors: this.applyErrors.map((error) => ({ ...error }))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dispose(): void {
|
|
162
|
+
this.actions.length = 0;
|
|
163
|
+
this.layers.length = 0;
|
|
164
|
+
this.values.clear();
|
|
165
|
+
this.applyErrors = [];
|
|
166
|
+
this.eventListeners.clear();
|
|
167
|
+
this.disposed = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private assertAlive(): void {
|
|
171
|
+
if (this.disposed) {
|
|
172
|
+
throw new Error("AnimationMixer has been disposed.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private applyActionRootMotion(action: AnimationAction, previousTime: number): void {
|
|
177
|
+
if (!this.options.applyRootMotion || action.weight <= 0) return;
|
|
178
|
+
const rootMotionTarget = this.options.rootMotionTrack ?? "root.position";
|
|
179
|
+
if (!action.clip.tracks.some((track) => track.target === rootMotionTarget)) return;
|
|
180
|
+
const sample = extractRootMotion(action.clip, {
|
|
181
|
+
fromTime: previousTime,
|
|
182
|
+
toTime: action.time,
|
|
183
|
+
loop: action.loopMode === "repeat",
|
|
184
|
+
target: rootMotionTarget
|
|
185
|
+
});
|
|
186
|
+
const scaledSample = action.weight === 1
|
|
187
|
+
? sample
|
|
188
|
+
: {
|
|
189
|
+
...sample,
|
|
190
|
+
delta: [
|
|
191
|
+
sample.delta[0] * action.weight,
|
|
192
|
+
sample.delta[1] * action.weight,
|
|
193
|
+
sample.delta[2] * action.weight
|
|
194
|
+
] as [number, number, number]
|
|
195
|
+
};
|
|
196
|
+
this.target?.applyRootMotion?.(scaledSample);
|
|
197
|
+
if (this.target?.position) {
|
|
198
|
+
applyRootMotion(this.target as RootMotionTarget, scaledSample, this.options.rootMotionScale ?? 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private layersForAction(action: AnimationAction): readonly MixerLayer[] {
|
|
203
|
+
if (this.layers.length === 0) {
|
|
204
|
+
return UNMASKED_LAYERS;
|
|
205
|
+
}
|
|
206
|
+
const owningLayers = this.layers.filter((layer) => layer.actions.includes(action));
|
|
207
|
+
return owningLayers.length > 0 ? owningLayers : UNMASKED_LAYERS;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const UNMASKED_LAYER: MixerLayer = {
|
|
212
|
+
name: "default",
|
|
213
|
+
weight: 1,
|
|
214
|
+
additive: false,
|
|
215
|
+
mask: [],
|
|
216
|
+
actions: [],
|
|
217
|
+
capturesTarget: () => true,
|
|
218
|
+
snapshot: () => ({ name: "default", weight: 1, additive: false, mask: [], actions: [] })
|
|
219
|
+
};
|
|
220
|
+
const UNMASKED_LAYERS: readonly MixerLayer[] = [UNMASKED_LAYER];
|
|
221
|
+
|
|
222
|
+
function blendInto(accumulators: Map<string, TargetAccumulator>, target: string, type: string, value: AnimationValue, weight: number, additive: boolean): void {
|
|
223
|
+
if (weight <= 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const current = accumulators.get(target);
|
|
227
|
+
if (!current) {
|
|
228
|
+
const accumulator: TargetAccumulator = { type };
|
|
229
|
+
if (additive) {
|
|
230
|
+
accumulator.additive = additiveContribution(type, value, weight);
|
|
231
|
+
} else {
|
|
232
|
+
accumulator.base = { value: cloneAnimationValue(value), weight, type };
|
|
233
|
+
}
|
|
234
|
+
accumulators.set(target, accumulator);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (current.type !== type) {
|
|
238
|
+
throw new Error(`Cannot blend ${type} animation track into existing ${current.type} target ${target}.`);
|
|
239
|
+
}
|
|
240
|
+
if (additive) {
|
|
241
|
+
current.additive = current.additive === undefined
|
|
242
|
+
? additiveContribution(type, value, weight)
|
|
243
|
+
: combineAdditive(type, current.additive, value, weight);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!current.base) {
|
|
247
|
+
current.base = { value: cloneAnimationValue(value), weight, type };
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
blendBase(current.base, type, value, weight);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function blendBase(current: WeightedAccumulator, type: string, value: AnimationValue, weight: number): void {
|
|
254
|
+
const total = current.weight + weight;
|
|
255
|
+
const t = weight / total;
|
|
256
|
+
if (type === "scalar") {
|
|
257
|
+
current.value = (current.value as number) + ((value as number) - (current.value as number)) * t;
|
|
258
|
+
} else if (type === "vector3") {
|
|
259
|
+
const a = current.value as [number, number, number];
|
|
260
|
+
const b = value as [number, number, number];
|
|
261
|
+
current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
262
|
+
} else if (type === "quaternion") {
|
|
263
|
+
current.value = slerpQuat(current.value as [number, number, number, number], value as [number, number, number, number], t);
|
|
264
|
+
} else if (type === "number-array") {
|
|
265
|
+
const a = current.value as readonly number[];
|
|
266
|
+
const b = value as readonly number[];
|
|
267
|
+
if (a.length !== b.length) {
|
|
268
|
+
throw new Error("Cannot blend number-array animation values with different lengths.");
|
|
269
|
+
}
|
|
270
|
+
current.value = a.map((component, index) => component + (b[index]! - component) * t);
|
|
271
|
+
} else {
|
|
272
|
+
current.value = cloneAnimationValue(value);
|
|
273
|
+
}
|
|
274
|
+
current.weight = total;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function finalizeTargetBlend(accumulator: TargetAccumulator): AnimationValue {
|
|
278
|
+
const base = accumulator.base
|
|
279
|
+
? finalizeBaseBlend(accumulator.base)
|
|
280
|
+
: accumulator.additive !== undefined
|
|
281
|
+
? additiveNeutral(accumulator.type, accumulator.additive)
|
|
282
|
+
: undefined;
|
|
283
|
+
if (base === undefined) {
|
|
284
|
+
throw new Error("Animation target accumulator has no sampled value.");
|
|
285
|
+
}
|
|
286
|
+
if (accumulator.additive === undefined) {
|
|
287
|
+
return base;
|
|
288
|
+
}
|
|
289
|
+
return applyAdditive(accumulator.type, base, accumulator.additive);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function finalizeBaseBlend(accumulator: WeightedAccumulator): AnimationValue {
|
|
293
|
+
if (accumulator.type === "quaternion") {
|
|
294
|
+
return normalizeQuat(accumulator.value as [number, number, number, number]);
|
|
295
|
+
}
|
|
296
|
+
return cloneAnimationValue(accumulator.value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function additiveContribution(type: string, value: AnimationValue, weight: number): AnimationValue {
|
|
300
|
+
if (type === "scalar") {
|
|
301
|
+
return (value as number) * weight;
|
|
302
|
+
}
|
|
303
|
+
if (type === "vector3") {
|
|
304
|
+
const vector = value as [number, number, number];
|
|
305
|
+
return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
|
|
306
|
+
}
|
|
307
|
+
if (type === "number-array") {
|
|
308
|
+
return (value as readonly number[]).map((component) => component * weight);
|
|
309
|
+
}
|
|
310
|
+
if (type === "quaternion") {
|
|
311
|
+
return slerpQuat([0, 0, 0, 1], normalizeQuat(value as [number, number, number, number]), weight);
|
|
312
|
+
}
|
|
313
|
+
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function combineAdditive(type: string, current: AnimationValue, value: AnimationValue, weight: number): AnimationValue {
|
|
317
|
+
return applyAdditive(type, current, additiveContribution(type, value, weight));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function applyAdditive(type: string, base: AnimationValue, delta: AnimationValue): AnimationValue {
|
|
321
|
+
if (type === "scalar") {
|
|
322
|
+
return (base as number) + (delta as number);
|
|
323
|
+
}
|
|
324
|
+
if (type === "vector3") {
|
|
325
|
+
const a = base as [number, number, number];
|
|
326
|
+
const b = delta as [number, number, number];
|
|
327
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
328
|
+
}
|
|
329
|
+
if (type === "number-array") {
|
|
330
|
+
const a = base as readonly number[];
|
|
331
|
+
const b = delta as readonly number[];
|
|
332
|
+
if (a.length !== b.length) {
|
|
333
|
+
throw new Error("Cannot add number-array animation values with different lengths.");
|
|
334
|
+
}
|
|
335
|
+
return a.map((component, index) => component + b[index]!);
|
|
336
|
+
}
|
|
337
|
+
if (type === "quaternion") {
|
|
338
|
+
return normalizeQuat(multiplyQuat(base as [number, number, number, number], delta as [number, number, number, number]));
|
|
339
|
+
}
|
|
340
|
+
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function additiveNeutral(type: string, value: AnimationValue): AnimationValue {
|
|
344
|
+
if (type === "scalar") {
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
if (type === "vector3") {
|
|
348
|
+
return [0, 0, 0];
|
|
349
|
+
}
|
|
350
|
+
if (type === "number-array") {
|
|
351
|
+
return (value as readonly number[]).map(() => 0);
|
|
352
|
+
}
|
|
353
|
+
if (type === "quaternion") {
|
|
354
|
+
return [0, 0, 0, 1];
|
|
355
|
+
}
|
|
356
|
+
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function multiplyQuat(a: [number, number, number, number], b: [number, number, number, number]): [number, number, number, number] {
|
|
360
|
+
return [
|
|
361
|
+
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
362
|
+
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
363
|
+
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
364
|
+
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
365
|
+
];
|
|
366
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AnimationStateMachine as AnimationStateGraph,
|
|
3
|
+
type AnimationState,
|
|
4
|
+
type AnimationStateMachineGraphSnapshot,
|
|
5
|
+
type AnimationStateMachineGraphState,
|
|
6
|
+
type AnimationStateMachineGraphTransition,
|
|
7
|
+
type StateTransition
|
|
8
|
+
} from "./AnimationStateMachine.js";
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
export type StateTransition = {
|
|
2
|
+
readonly to: string;
|
|
3
|
+
readonly label?: string;
|
|
4
|
+
readonly priority?: number;
|
|
5
|
+
readonly exitTime?: number;
|
|
6
|
+
readonly consumeParameters?: readonly string[];
|
|
7
|
+
readonly condition: (parameters: Readonly<Record<string, number | boolean | string>>) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type AnimationState = {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly duration?: number;
|
|
13
|
+
readonly oneShot?: boolean;
|
|
14
|
+
readonly terminal?: boolean;
|
|
15
|
+
readonly onComplete?: string;
|
|
16
|
+
readonly completedParameter?: string;
|
|
17
|
+
readonly resetParametersOnEnter?: readonly string[];
|
|
18
|
+
readonly transitions?: readonly StateTransition[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type AnimationStateMachineGraphState = {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly current: boolean;
|
|
24
|
+
readonly transitionCount: number;
|
|
25
|
+
readonly duration?: number;
|
|
26
|
+
readonly oneShot?: boolean;
|
|
27
|
+
readonly terminal?: boolean;
|
|
28
|
+
readonly completed?: boolean;
|
|
29
|
+
readonly onComplete?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AnimationStateMachineGraphTransition = {
|
|
33
|
+
readonly from: string;
|
|
34
|
+
readonly to: string;
|
|
35
|
+
readonly index: number;
|
|
36
|
+
readonly priority: number;
|
|
37
|
+
readonly label: string;
|
|
38
|
+
readonly exitTime?: number;
|
|
39
|
+
readonly consumeParameters?: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AnimationStateMachineGraphSnapshot = {
|
|
43
|
+
readonly currentState: string;
|
|
44
|
+
readonly stateTime: number;
|
|
45
|
+
readonly parameters: Readonly<Record<string, number | boolean | string>>;
|
|
46
|
+
readonly states: readonly AnimationStateMachineGraphState[];
|
|
47
|
+
readonly transitions: readonly AnimationStateMachineGraphTransition[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class AnimationStateMachine {
|
|
51
|
+
readonly states = new Map<string, AnimationState>();
|
|
52
|
+
readonly parameters: Record<string, number | boolean | string> = {};
|
|
53
|
+
currentState: string;
|
|
54
|
+
stateTime = 0;
|
|
55
|
+
|
|
56
|
+
constructor(states: readonly AnimationState[], initialState: string) {
|
|
57
|
+
for (const state of states) {
|
|
58
|
+
if (state.duration !== undefined && (!Number.isFinite(state.duration) || state.duration < 0)) {
|
|
59
|
+
throw new Error(`State ${state.name} has invalid duration.`);
|
|
60
|
+
}
|
|
61
|
+
for (const transition of state.transitions ?? []) {
|
|
62
|
+
if (transition.exitTime !== undefined && (!Number.isFinite(transition.exitTime) || transition.exitTime < 0)) {
|
|
63
|
+
throw new Error(`Transition from ${state.name} to ${transition.to} has invalid exit time.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.states.set(state.name, state);
|
|
67
|
+
}
|
|
68
|
+
if (!this.states.has(initialState)) {
|
|
69
|
+
throw new Error(`Initial state ${initialState} does not exist.`);
|
|
70
|
+
}
|
|
71
|
+
for (const state of this.states.values()) {
|
|
72
|
+
if (state.onComplete !== undefined && !this.states.has(state.onComplete)) {
|
|
73
|
+
throw new Error(`State ${state.name} completion target ${state.onComplete} does not exist.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
this.currentState = initialState;
|
|
77
|
+
this.resetParametersForState(this.requireState(initialState));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setParameter(name: string, value: number | boolean | string): void {
|
|
81
|
+
this.parameters[name] = value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
update(delta = 0): string {
|
|
85
|
+
if (!Number.isFinite(delta) || delta < 0) {
|
|
86
|
+
throw new Error("AnimationStateMachine delta must be finite and non-negative.");
|
|
87
|
+
}
|
|
88
|
+
this.stateTime += delta;
|
|
89
|
+
const state = this.requireState(this.currentState);
|
|
90
|
+
|
|
91
|
+
if (isStateComplete(state, this.stateTime)) {
|
|
92
|
+
if (state.completedParameter) {
|
|
93
|
+
this.parameters[state.completedParameter] = true;
|
|
94
|
+
}
|
|
95
|
+
if (state.onComplete) {
|
|
96
|
+
this.transitionTo(state.onComplete);
|
|
97
|
+
return this.currentState;
|
|
98
|
+
}
|
|
99
|
+
if (state.terminal || state.oneShot) {
|
|
100
|
+
return this.currentState;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (state.terminal || state.oneShot) {
|
|
105
|
+
return this.currentState;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const transition = [...(state.transitions ?? [])]
|
|
109
|
+
.filter((candidate) => candidate.condition(this.parameters))
|
|
110
|
+
.filter((candidate) => candidate.exitTime === undefined || this.stateTime >= candidate.exitTime)
|
|
111
|
+
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.to.localeCompare(b.to))[0];
|
|
112
|
+
if (transition) {
|
|
113
|
+
if (!this.states.has(transition.to)) {
|
|
114
|
+
throw new Error(`Transition target ${transition.to} does not exist.`);
|
|
115
|
+
}
|
|
116
|
+
consumeParameters(this.parameters, transition.consumeParameters);
|
|
117
|
+
this.transitionTo(transition.to);
|
|
118
|
+
}
|
|
119
|
+
return this.currentState;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
graphSnapshot(): AnimationStateMachineGraphSnapshot {
|
|
123
|
+
const states: AnimationStateMachineGraphState[] = [];
|
|
124
|
+
const transitions: AnimationStateMachineGraphTransition[] = [];
|
|
125
|
+
for (const state of this.states.values()) {
|
|
126
|
+
const stateTransitions = state.transitions ?? [];
|
|
127
|
+
states.push({
|
|
128
|
+
name: state.name,
|
|
129
|
+
current: state.name === this.currentState,
|
|
130
|
+
transitionCount: stateTransitions.length,
|
|
131
|
+
...(state.duration === undefined ? {} : { duration: state.duration }),
|
|
132
|
+
...(state.oneShot ? { oneShot: true } : {}),
|
|
133
|
+
...(state.terminal ? { terminal: true } : {}),
|
|
134
|
+
...(state.name === this.currentState && isStateComplete(state, this.stateTime) ? { completed: true } : {}),
|
|
135
|
+
...(state.onComplete === undefined ? {} : { onComplete: state.onComplete })
|
|
136
|
+
});
|
|
137
|
+
stateTransitions.forEach((transition, index) => {
|
|
138
|
+
transitions.push({
|
|
139
|
+
from: state.name,
|
|
140
|
+
to: transition.to,
|
|
141
|
+
index,
|
|
142
|
+
priority: transition.priority ?? 0,
|
|
143
|
+
label: transition.label ?? `${state.name}->${transition.to}`,
|
|
144
|
+
...(transition.exitTime === undefined ? {} : { exitTime: transition.exitTime }),
|
|
145
|
+
...(transition.consumeParameters === undefined || transition.consumeParameters.length === 0
|
|
146
|
+
? {}
|
|
147
|
+
: { consumeParameters: [...transition.consumeParameters] })
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
currentState: this.currentState,
|
|
153
|
+
stateTime: this.stateTime,
|
|
154
|
+
parameters: { ...this.parameters },
|
|
155
|
+
states,
|
|
156
|
+
transitions
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
debugGraph(): string {
|
|
161
|
+
const snapshot = this.graphSnapshot();
|
|
162
|
+
const lines = [
|
|
163
|
+
`AnimationStateMachine current=${snapshot.currentState} time=${formatNumber(snapshot.stateTime)}`
|
|
164
|
+
];
|
|
165
|
+
for (const state of snapshot.states) {
|
|
166
|
+
lines.push(`state ${state.name}${state.current ? " *" : ""} transitions=${state.transitionCount}`);
|
|
167
|
+
for (const transition of snapshot.transitions.filter((candidate) => candidate.from === state.name)) {
|
|
168
|
+
const exitTime = transition.exitTime === undefined ? "" : ` exit=${formatNumber(transition.exitTime)}`;
|
|
169
|
+
lines.push(` -> ${transition.to} priority=${transition.priority}${exitTime} label=${transition.label}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private requireState(name: string): AnimationState {
|
|
176
|
+
const state = this.states.get(name);
|
|
177
|
+
if (!state) {
|
|
178
|
+
throw new Error(`Current state ${name} does not exist.`);
|
|
179
|
+
}
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private transitionTo(name: string): void {
|
|
184
|
+
const state = this.requireState(name);
|
|
185
|
+
this.currentState = name;
|
|
186
|
+
this.stateTime = 0;
|
|
187
|
+
this.resetParametersForState(state);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private resetParametersForState(state: AnimationState): void {
|
|
191
|
+
consumeParameters(this.parameters, state.resetParametersOnEnter);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatNumber(value: number): string {
|
|
196
|
+
return Number(value.toFixed(6)).toString();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isStateComplete(state: AnimationState, stateTime: number): boolean {
|
|
200
|
+
return state.duration !== undefined && stateTime >= state.duration;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function consumeParameters(
|
|
204
|
+
parameters: Record<string, number | boolean | string>,
|
|
205
|
+
names: readonly string[] | undefined
|
|
206
|
+
): void {
|
|
207
|
+
for (const name of names ?? []) {
|
|
208
|
+
const value = parameters[name];
|
|
209
|
+
if (typeof value === "boolean") {
|
|
210
|
+
parameters[name] = false;
|
|
211
|
+
} else if (typeof value === "number") {
|
|
212
|
+
parameters[name] = 0;
|
|
213
|
+
} else if (typeof value === "string") {
|
|
214
|
+
parameters[name] = "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|