@lovelace_lol/loom3 1.0.34 → 1.0.36
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/dist/index.cjs +584 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +585 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import { Vector3, Clock, Box3, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh } from 'three';
|
|
2
|
+
import { Vector3, Clock, Box3, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh, PropertyBinding } from 'three';
|
|
3
3
|
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
@@ -58,6 +58,119 @@ function resolveCurveBalance(curveId, globalBalance, balanceMap) {
|
|
|
58
58
|
}
|
|
59
59
|
return clampBalance(globalBalance);
|
|
60
60
|
}
|
|
61
|
+
var RUNTIME_CLIP_PREFIX = "__loom3_baked_partition__/";
|
|
62
|
+
var FACE_SAFE_TARGET_RE = /(head|neck|jaw|eye|brow|lid|mouth|lip|face|cheek|nose|tongue|teeth)/i;
|
|
63
|
+
var BODY_LIKE_TARGET_RE = /(root|armature|hips?|pelvis|spine|waist|chest|torso|shoulder|arm|forearm|hand|finger|leg|thigh|calf|knee|foot|toe|tail|wing|fin|body|abdomen|clavicle)/i;
|
|
64
|
+
var SCENE_LIKE_TARGET_RE = /(camera|cam|scene|world|global|origin|pivot|cube)/i;
|
|
65
|
+
var CHANNEL_ORDER = ["face", "body", "scene"];
|
|
66
|
+
function getRuntimeClipName(sourceClipName, channel) {
|
|
67
|
+
return `${RUNTIME_CLIP_PREFIX}${sourceClipName}/${channel}`;
|
|
68
|
+
}
|
|
69
|
+
function parseTrackTarget(trackName, model) {
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = PropertyBinding.parseTrackName(trackName);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? String(parsed.objectIndex) : parsed.nodeName;
|
|
77
|
+
const target = targetKey ? model.getObjectByProperty("uuid", targetKey) ?? PropertyBinding.findNode(model, targetKey) : null;
|
|
78
|
+
return {
|
|
79
|
+
propertyName: parsed.propertyName,
|
|
80
|
+
target,
|
|
81
|
+
targetName: target?.name ?? parsed.nodeName ?? ""
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isSceneTrackTarget(target, targetName) {
|
|
85
|
+
if (!target) return true;
|
|
86
|
+
if (target.isCamera) return true;
|
|
87
|
+
return SCENE_LIKE_TARGET_RE.test(targetName);
|
|
88
|
+
}
|
|
89
|
+
function isFaceSafeTransformTarget(target, targetName, safeTransformTargets) {
|
|
90
|
+
if (target && safeTransformTargets.has(target)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (!targetName) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (BODY_LIKE_TARGET_RE.test(targetName) || SCENE_LIKE_TARGET_RE.test(targetName)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return FACE_SAFE_TARGET_RE.test(targetName);
|
|
100
|
+
}
|
|
101
|
+
function classifyBakedTrack(track, model, bones) {
|
|
102
|
+
const parsed = parseTrackTarget(track.name, model);
|
|
103
|
+
if (!parsed) {
|
|
104
|
+
return "scene";
|
|
105
|
+
}
|
|
106
|
+
if (parsed.propertyName === "morphTargetInfluences" || parsed.propertyName === "weights") {
|
|
107
|
+
return "face";
|
|
108
|
+
}
|
|
109
|
+
if (isSceneTrackTarget(parsed.target, parsed.targetName)) {
|
|
110
|
+
return "scene";
|
|
111
|
+
}
|
|
112
|
+
if (parsed.propertyName === "quaternion") {
|
|
113
|
+
const safeTransformTargets = new Set(
|
|
114
|
+
Object.values(bones).map((entry) => entry?.obj).filter((entry) => !!entry)
|
|
115
|
+
);
|
|
116
|
+
if (isFaceSafeTransformTarget(parsed.target, parsed.targetName, safeTransformTargets)) {
|
|
117
|
+
return "face";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return "body";
|
|
121
|
+
}
|
|
122
|
+
function resolveBakedChannelBlendMode(channel, requestedBlendMode) {
|
|
123
|
+
if (channel === "face") {
|
|
124
|
+
return requestedBlendMode === "additive" ? "additive" : "replace";
|
|
125
|
+
}
|
|
126
|
+
if (channel === "body") {
|
|
127
|
+
return "replace";
|
|
128
|
+
}
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
function resolveBakedAggregateBlendMode(channels, requestedBlendMode) {
|
|
132
|
+
if (requestedBlendMode !== "additive") {
|
|
133
|
+
return "replace";
|
|
134
|
+
}
|
|
135
|
+
return channels.some((channel) => channel.channel === "face" && channel.playable && channel.trackCount > 0) ? "additive" : "replace";
|
|
136
|
+
}
|
|
137
|
+
function partitionBakedClip(clip, model, bones) {
|
|
138
|
+
const tracksByChannel = new Map(
|
|
139
|
+
CHANNEL_ORDER.map((channel) => [channel, []])
|
|
140
|
+
);
|
|
141
|
+
for (const track of clip.tracks) {
|
|
142
|
+
const channel = classifyBakedTrack(track, model, bones);
|
|
143
|
+
tracksByChannel.get(channel)?.push(track.clone());
|
|
144
|
+
}
|
|
145
|
+
const runtimeClips = [];
|
|
146
|
+
const channels = [];
|
|
147
|
+
for (const channel of CHANNEL_ORDER) {
|
|
148
|
+
const tracks = tracksByChannel.get(channel) ?? [];
|
|
149
|
+
if (tracks.length === 0) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const playable = channel !== "scene";
|
|
153
|
+
const blendMode = resolveBakedChannelBlendMode(channel, "additive");
|
|
154
|
+
channels.push({
|
|
155
|
+
channel,
|
|
156
|
+
trackCount: tracks.length,
|
|
157
|
+
playable,
|
|
158
|
+
blendMode
|
|
159
|
+
});
|
|
160
|
+
if (!playable) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
runtimeClips.push({
|
|
164
|
+
channel,
|
|
165
|
+
clip: new AnimationClip(getRuntimeClipName(clip.name, channel), clip.duration, tracks)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
sourceClip: clip,
|
|
170
|
+
channels,
|
|
171
|
+
runtimeClips
|
|
172
|
+
};
|
|
173
|
+
}
|
|
61
174
|
|
|
62
175
|
// src/engines/three/AnimationThree.ts
|
|
63
176
|
var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
@@ -163,6 +276,10 @@ var BakedAnimationController = class {
|
|
|
163
276
|
__publicField(this, "animationMixer", null);
|
|
164
277
|
__publicField(this, "mixerFinishedListenerAttached", false);
|
|
165
278
|
__publicField(this, "animationClips", []);
|
|
279
|
+
__publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
|
|
280
|
+
__publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
|
|
281
|
+
__publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
|
|
282
|
+
__publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
|
|
166
283
|
__publicField(this, "animationActions", /* @__PURE__ */ new Map());
|
|
167
284
|
__publicField(this, "animationFinishedCallbacks", /* @__PURE__ */ new Map());
|
|
168
285
|
__publicField(this, "clipActions", /* @__PURE__ */ new Map());
|
|
@@ -191,6 +308,7 @@ var BakedAnimationController = class {
|
|
|
191
308
|
const rawWeight = options?.weight ?? options?.intensity ?? clipOptions?.mixerWeight ?? 1;
|
|
192
309
|
const weight = Number.isFinite(rawWeight) ? Math.max(0, rawWeight) : 1;
|
|
193
310
|
const loopMode = options?.loopMode ?? (typeof options?.loop === "boolean" ? options.loop ? "repeat" : "once" : defaults.loop ? "repeat" : "once");
|
|
311
|
+
const requestedBlendMode = options?.blendMode ?? (clipOptions?.mixerAdditive ? "additive" : "replace");
|
|
194
312
|
return {
|
|
195
313
|
source: options?.source ?? defaults.source,
|
|
196
314
|
loop: loopMode !== "once",
|
|
@@ -200,7 +318,8 @@ var BakedAnimationController = class {
|
|
|
200
318
|
playbackRate,
|
|
201
319
|
weight,
|
|
202
320
|
balance: Number.isFinite(options?.balance) ? options?.balance ?? 0 : 0,
|
|
203
|
-
|
|
321
|
+
requestedBlendMode,
|
|
322
|
+
blendMode: requestedBlendMode,
|
|
204
323
|
easing: options?.easing ?? "linear"
|
|
205
324
|
};
|
|
206
325
|
}
|
|
@@ -260,15 +379,49 @@ var BakedAnimationController = class {
|
|
|
260
379
|
next.balance = Math.max(-1, Math.min(1, options.balance));
|
|
261
380
|
}
|
|
262
381
|
if (options.blendMode) {
|
|
263
|
-
next.
|
|
382
|
+
next.requestedBlendMode = options.blendMode;
|
|
264
383
|
} else if (typeof clipOptions?.mixerAdditive === "boolean") {
|
|
265
|
-
next.
|
|
384
|
+
next.requestedBlendMode = clipOptions.mixerAdditive ? "additive" : "replace";
|
|
266
385
|
}
|
|
386
|
+
next.blendMode = next.requestedBlendMode;
|
|
267
387
|
if (options.easing) {
|
|
268
388
|
next.easing = options.easing;
|
|
269
389
|
}
|
|
270
390
|
return next;
|
|
271
391
|
}
|
|
392
|
+
isBakedSourceClip(clipName) {
|
|
393
|
+
return this.bakedSourceClips.has(clipName);
|
|
394
|
+
}
|
|
395
|
+
getBakedSourceClip(clipName) {
|
|
396
|
+
return this.bakedSourceClips.get(clipName);
|
|
397
|
+
}
|
|
398
|
+
getBakedChannelInfo(clipName, playbackState) {
|
|
399
|
+
const bakedClip = this.getBakedSourceClip(clipName);
|
|
400
|
+
if (!bakedClip) {
|
|
401
|
+
return void 0;
|
|
402
|
+
}
|
|
403
|
+
const requestedBlendMode = playbackState?.requestedBlendMode ?? "replace";
|
|
404
|
+
return bakedClip.channels.map((channel) => ({
|
|
405
|
+
...channel,
|
|
406
|
+
blendMode: resolveBakedChannelBlendMode(channel.channel, requestedBlendMode)
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
getBakedAggregateBlendMode(clipName, playbackState) {
|
|
410
|
+
const channels = this.getBakedChannelInfo(clipName, playbackState);
|
|
411
|
+
if (!channels) {
|
|
412
|
+
return playbackState?.requestedBlendMode ?? playbackState?.blendMode ?? "replace";
|
|
413
|
+
}
|
|
414
|
+
return resolveBakedAggregateBlendMode(
|
|
415
|
+
channels,
|
|
416
|
+
playbackState?.requestedBlendMode ?? "replace"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
applyPlaybackStateToBakedAction(action, state, channel) {
|
|
420
|
+
this.applyPlaybackState(action, {
|
|
421
|
+
...state,
|
|
422
|
+
blendMode: resolveBakedChannelBlendMode(channel, state.requestedBlendMode) ?? "replace"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
272
425
|
resolveStartTime(duration, state, explicitStartTime) {
|
|
273
426
|
if (typeof explicitStartTime === "number" && Number.isFinite(explicitStartTime)) {
|
|
274
427
|
return Math.max(0, Math.min(duration, explicitStartTime));
|
|
@@ -278,8 +431,13 @@ var BakedAnimationController = class {
|
|
|
278
431
|
}
|
|
279
432
|
return 0;
|
|
280
433
|
}
|
|
281
|
-
|
|
282
|
-
const
|
|
434
|
+
getOrCreateBakedRuntimeAction(sourceClipName, channel) {
|
|
435
|
+
const bakedClip = this.getBakedSourceClip(sourceClipName);
|
|
436
|
+
const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
|
|
437
|
+
if (!runtimeClip) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const existing = this.bakedRuntimeActions.get(runtimeClip.name);
|
|
283
441
|
if (existing) {
|
|
284
442
|
return existing;
|
|
285
443
|
}
|
|
@@ -287,13 +445,44 @@ var BakedAnimationController = class {
|
|
|
287
445
|
if (!this.animationMixer) {
|
|
288
446
|
return null;
|
|
289
447
|
}
|
|
290
|
-
const
|
|
291
|
-
|
|
448
|
+
const action = this.animationMixer.clipAction(runtimeClip);
|
|
449
|
+
this.bakedRuntimeActions.set(runtimeClip.name, action);
|
|
450
|
+
return action;
|
|
451
|
+
}
|
|
452
|
+
getRepresentativeBakedAction(clipName) {
|
|
453
|
+
const group = this.bakedActionGroups.get(clipName);
|
|
454
|
+
if (!group) {
|
|
292
455
|
return null;
|
|
293
456
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
457
|
+
return group.channelActions.values().next().value ?? null;
|
|
458
|
+
}
|
|
459
|
+
createBakedActionGroup(clipName, playbackState) {
|
|
460
|
+
const bakedClip = this.getBakedSourceClip(clipName);
|
|
461
|
+
if (!bakedClip) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const channelActions = /* @__PURE__ */ new Map();
|
|
465
|
+
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
466
|
+
const action = this.getOrCreateBakedRuntimeAction(clipName, runtimeClip.channel);
|
|
467
|
+
if (action) {
|
|
468
|
+
channelActions.set(runtimeClip.channel, action);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (channelActions.size === 0) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
let resolveFinished = () => {
|
|
475
|
+
};
|
|
476
|
+
const finishedPromise = new Promise((resolve) => {
|
|
477
|
+
resolveFinished = resolve;
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
actionId: makeActionId(),
|
|
481
|
+
channelActions,
|
|
482
|
+
pendingFinishedChannels: playbackState.loopMode === "once" ? new Set(channelActions.keys()) : /* @__PURE__ */ new Set(),
|
|
483
|
+
finishedPromise,
|
|
484
|
+
resolveFinished
|
|
485
|
+
};
|
|
297
486
|
}
|
|
298
487
|
getMeshNamesForAU(auId, config, explicitMeshNames) {
|
|
299
488
|
if (explicitMeshNames && explicitMeshNames.length > 0) {
|
|
@@ -333,6 +522,10 @@ var BakedAnimationController = class {
|
|
|
333
522
|
this.animationMixer = null;
|
|
334
523
|
}
|
|
335
524
|
this.animationClips = [];
|
|
525
|
+
this.bakedSourceClips.clear();
|
|
526
|
+
this.bakedRuntimeActions.clear();
|
|
527
|
+
this.bakedActionGroups.clear();
|
|
528
|
+
this.bakedRuntimeClipToSource.clear();
|
|
336
529
|
this.animationActions.clear();
|
|
337
530
|
this.animationFinishedCallbacks.clear();
|
|
338
531
|
this.clipActions.clear();
|
|
@@ -341,17 +534,47 @@ var BakedAnimationController = class {
|
|
|
341
534
|
this.playbackState.clear();
|
|
342
535
|
}
|
|
343
536
|
loadAnimationClips(clips) {
|
|
344
|
-
|
|
537
|
+
const model = this.host.getModel();
|
|
538
|
+
if (!model) {
|
|
345
539
|
console.warn("Loom3: Cannot load animation clips before calling onReady()");
|
|
346
540
|
return;
|
|
347
541
|
}
|
|
542
|
+
for (const clipName of this.bakedSourceClips.keys()) {
|
|
543
|
+
this.stopAnimation(clipName);
|
|
544
|
+
}
|
|
545
|
+
if (this.animationMixer) {
|
|
546
|
+
for (const bakedClip of this.bakedSourceClips.values()) {
|
|
547
|
+
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
548
|
+
try {
|
|
549
|
+
this.animationMixer.uncacheAction(runtimeClip.clip);
|
|
550
|
+
} catch {
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
this.animationMixer.uncacheClip(runtimeClip.clip);
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const clipName of this.bakedSourceClips.keys()) {
|
|
560
|
+
this.playbackState.delete(clipName);
|
|
561
|
+
this.clipSources.delete(clipName);
|
|
562
|
+
}
|
|
563
|
+
this.bakedSourceClips.clear();
|
|
564
|
+
this.bakedRuntimeActions.clear();
|
|
565
|
+
this.bakedActionGroups.clear();
|
|
566
|
+
this.bakedRuntimeClipToSource.clear();
|
|
348
567
|
this.ensureMixer();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
568
|
+
const partitionedClips = clips.map((clip) => partitionBakedClip(clip, model, this.host.getBones()));
|
|
569
|
+
this.animationClips = partitionedClips.map((clip) => clip.sourceClip);
|
|
570
|
+
for (const bakedClip of partitionedClips) {
|
|
571
|
+
this.bakedSourceClips.set(bakedClip.sourceClip.name, bakedClip);
|
|
572
|
+
this.clipSources.set(bakedClip.sourceClip.name, "baked");
|
|
573
|
+
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
574
|
+
this.bakedRuntimeClipToSource.set(runtimeClip.clip.name, {
|
|
575
|
+
sourceClipName: bakedClip.sourceClip.name,
|
|
576
|
+
channel: runtimeClip.channel
|
|
577
|
+
});
|
|
355
578
|
}
|
|
356
579
|
}
|
|
357
580
|
}
|
|
@@ -360,84 +583,91 @@ var BakedAnimationController = class {
|
|
|
360
583
|
name: clip.name,
|
|
361
584
|
duration: clip.duration,
|
|
362
585
|
trackCount: clip.tracks.length,
|
|
363
|
-
source: this.clipSources.get(clip.name) ?? "baked"
|
|
586
|
+
source: this.clipSources.get(clip.name) ?? "baked",
|
|
587
|
+
channels: this.getBakedSourceClip(clip.name)?.channels
|
|
364
588
|
}));
|
|
365
589
|
}
|
|
366
590
|
removeAnimationClip(clipName) {
|
|
367
|
-
const
|
|
368
|
-
if (!
|
|
591
|
+
const bakedClip = this.getBakedSourceClip(clipName);
|
|
592
|
+
if (!bakedClip) {
|
|
369
593
|
return false;
|
|
370
594
|
}
|
|
371
|
-
const relatedActions = /* @__PURE__ */ new Set();
|
|
372
|
-
const bakedAction = this.animationActions.get(clipName);
|
|
373
|
-
const clipAction = this.clipActions.get(clipName);
|
|
374
|
-
if (bakedAction) relatedActions.add(bakedAction);
|
|
375
|
-
if (clipAction) relatedActions.add(clipAction);
|
|
376
595
|
this.stopAnimation(clipName);
|
|
377
596
|
if (this.animationMixer) {
|
|
378
|
-
for (const
|
|
597
|
+
for (const runtimeClip of bakedClip.runtimeClips) {
|
|
598
|
+
const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
|
|
379
599
|
try {
|
|
380
|
-
this.animationMixer.uncacheAction(clip);
|
|
600
|
+
this.animationMixer.uncacheAction(runtimeClip.clip);
|
|
381
601
|
} catch {
|
|
382
602
|
}
|
|
383
603
|
try {
|
|
384
|
-
this.animationMixer.uncacheClip(clip);
|
|
604
|
+
this.animationMixer.uncacheClip(runtimeClip.clip);
|
|
385
605
|
} catch {
|
|
386
606
|
}
|
|
607
|
+
this.bakedRuntimeActions.delete(runtimeClip.clip.name);
|
|
608
|
+
this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
|
|
387
609
|
const actionId = this.getActionId(action);
|
|
388
|
-
if (actionId) {
|
|
610
|
+
if (actionId && action) {
|
|
389
611
|
this.actionIdToClip.delete(actionId);
|
|
612
|
+
this.actionIds.delete(action);
|
|
390
613
|
}
|
|
391
|
-
this.actionIds.delete(action);
|
|
392
614
|
}
|
|
393
615
|
}
|
|
394
616
|
this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
|
|
395
|
-
this.
|
|
396
|
-
this.
|
|
397
|
-
this.clipHandles.delete(clipName);
|
|
398
|
-
this.animationFinishedCallbacks.delete(clipName);
|
|
617
|
+
this.bakedSourceClips.delete(clipName);
|
|
618
|
+
this.bakedActionGroups.delete(clipName);
|
|
399
619
|
this.playbackState.delete(clipName);
|
|
400
620
|
this.clipSources.delete(clipName);
|
|
401
621
|
return true;
|
|
402
622
|
}
|
|
403
623
|
playAnimation(clipName, options = {}) {
|
|
404
|
-
const
|
|
405
|
-
if (!
|
|
624
|
+
const bakedClip = this.getBakedSourceClip(clipName);
|
|
625
|
+
if (!bakedClip) {
|
|
406
626
|
console.warn(`Loom3: Animation clip "${clipName}" not found`);
|
|
407
627
|
return null;
|
|
408
628
|
}
|
|
409
|
-
if (!this.getActionId(action)) {
|
|
410
|
-
this.setActionId(action, clipName);
|
|
411
|
-
}
|
|
412
629
|
const playbackState = this.mergePlaybackOptions(
|
|
413
630
|
this.getPlaybackStateSnapshot(clipName, { loop: true, source: "baked" }),
|
|
414
631
|
options
|
|
415
632
|
);
|
|
633
|
+
playbackState.blendMode = this.getBakedAggregateBlendMode(clipName, playbackState);
|
|
634
|
+
const actionGroup = this.createBakedActionGroup(clipName, playbackState);
|
|
635
|
+
if (!actionGroup) {
|
|
636
|
+
console.warn(`Loom3: Animation clip "${clipName}" has no character-runtime channels to play`);
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
416
639
|
const crossfadeDuration = options.crossfadeDuration ?? 0;
|
|
417
640
|
const clampWhenFinished = options.clampWhenFinished ?? playbackState.loopMode === "once";
|
|
418
|
-
const startTime = this.resolveStartTime(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
641
|
+
const startTime = this.resolveStartTime(bakedClip.sourceClip.duration, playbackState, options.startTime);
|
|
642
|
+
for (const [channel, action] of actionGroup.channelActions) {
|
|
643
|
+
this.applyPlaybackStateToBakedAction(action, playbackState, channel);
|
|
644
|
+
action.clampWhenFinished = clampWhenFinished;
|
|
645
|
+
if (crossfadeDuration > 0) {
|
|
646
|
+
action.reset();
|
|
647
|
+
action.fadeIn(crossfadeDuration);
|
|
648
|
+
} else {
|
|
649
|
+
action.reset();
|
|
650
|
+
}
|
|
651
|
+
action.time = startTime;
|
|
652
|
+
action.play();
|
|
426
653
|
}
|
|
427
|
-
|
|
428
|
-
action.play();
|
|
429
|
-
this.animationActions.set(clipName, action);
|
|
654
|
+
this.bakedActionGroups.set(clipName, actionGroup);
|
|
430
655
|
this.setPlaybackState(clipName, playbackState);
|
|
431
|
-
|
|
432
|
-
const finishedPromise = new Promise((resolve) => {
|
|
433
|
-
resolveFinished = resolve;
|
|
434
|
-
});
|
|
435
|
-
if (playbackState.loopMode === "once") {
|
|
436
|
-
this.animationFinishedCallbacks.set(clipName, () => resolveFinished());
|
|
437
|
-
}
|
|
438
|
-
return this.createAnimationHandle(clipName, action, finishedPromise);
|
|
656
|
+
return this.createBakedAnimationHandle(clipName, actionGroup);
|
|
439
657
|
}
|
|
440
658
|
stopAnimation(clipName) {
|
|
659
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
660
|
+
if (bakedGroup) {
|
|
661
|
+
for (const action2 of bakedGroup.channelActions.values()) {
|
|
662
|
+
action2.stop();
|
|
663
|
+
try {
|
|
664
|
+
action2.paused = false;
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.bakedActionGroups.delete(clipName);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
441
671
|
const action = this.animationActions.get(clipName);
|
|
442
672
|
if (action) {
|
|
443
673
|
const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
|
|
@@ -485,6 +715,7 @@ var BakedAnimationController = class {
|
|
|
485
715
|
}
|
|
486
716
|
stopAllAnimations() {
|
|
487
717
|
for (const clipName of /* @__PURE__ */ new Set([
|
|
718
|
+
...this.bakedActionGroups.keys(),
|
|
488
719
|
...this.animationActions.keys(),
|
|
489
720
|
...this.clipActions.keys()
|
|
490
721
|
])) {
|
|
@@ -492,18 +723,39 @@ var BakedAnimationController = class {
|
|
|
492
723
|
}
|
|
493
724
|
}
|
|
494
725
|
pauseAnimation(clipName) {
|
|
726
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
727
|
+
if (bakedGroup) {
|
|
728
|
+
for (const action2 of bakedGroup.channelActions.values()) {
|
|
729
|
+
action2.paused = true;
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
495
733
|
const action = this.animationActions.get(clipName);
|
|
496
734
|
if (action) {
|
|
497
735
|
action.paused = true;
|
|
498
736
|
}
|
|
499
737
|
}
|
|
500
738
|
resumeAnimation(clipName) {
|
|
739
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
740
|
+
if (bakedGroup) {
|
|
741
|
+
for (const action2 of bakedGroup.channelActions.values()) {
|
|
742
|
+
action2.paused = false;
|
|
743
|
+
}
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
501
746
|
const action = this.animationActions.get(clipName);
|
|
502
747
|
if (action) {
|
|
503
748
|
action.paused = false;
|
|
504
749
|
}
|
|
505
750
|
}
|
|
506
751
|
pauseAllAnimations() {
|
|
752
|
+
for (const group of this.bakedActionGroups.values()) {
|
|
753
|
+
for (const action of group.channelActions.values()) {
|
|
754
|
+
if (action.isRunning()) {
|
|
755
|
+
action.paused = true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
507
759
|
for (const action of this.animationActions.values()) {
|
|
508
760
|
if (action.isRunning()) {
|
|
509
761
|
action.paused = true;
|
|
@@ -511,6 +763,13 @@ var BakedAnimationController = class {
|
|
|
511
763
|
}
|
|
512
764
|
}
|
|
513
765
|
resumeAllAnimations() {
|
|
766
|
+
for (const group of this.bakedActionGroups.values()) {
|
|
767
|
+
for (const action of group.channelActions.values()) {
|
|
768
|
+
if (action.paused) {
|
|
769
|
+
action.paused = false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
514
773
|
for (const action of this.animationActions.values()) {
|
|
515
774
|
if (action.paused) {
|
|
516
775
|
action.paused = false;
|
|
@@ -518,76 +777,161 @@ var BakedAnimationController = class {
|
|
|
518
777
|
}
|
|
519
778
|
}
|
|
520
779
|
setAnimationSpeed(clipName, speed) {
|
|
521
|
-
|
|
522
|
-
if (action) {
|
|
780
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
523
781
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
524
782
|
loop: true,
|
|
525
783
|
source: this.clipSources.get(clipName) ?? "baked"
|
|
526
784
|
});
|
|
527
785
|
next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
|
|
786
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
787
|
+
if (bakedGroup) {
|
|
788
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
789
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
this.setPlaybackState(clipName, next);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const action = this.animationActions.get(clipName);
|
|
796
|
+
if (action) {
|
|
797
|
+
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
798
|
+
loop: true,
|
|
799
|
+
source: this.clipSources.get(clipName) ?? "clip"
|
|
800
|
+
});
|
|
801
|
+
next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
|
|
528
802
|
this.applyPlaybackState(action, next);
|
|
529
803
|
this.setPlaybackState(clipName, next);
|
|
530
804
|
}
|
|
531
805
|
}
|
|
532
806
|
setAnimationIntensity(clipName, intensity) {
|
|
533
|
-
|
|
534
|
-
if (action) {
|
|
807
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
535
808
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
536
809
|
loop: true,
|
|
537
810
|
source: this.clipSources.get(clipName) ?? "baked"
|
|
538
811
|
});
|
|
539
812
|
next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
|
|
813
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
814
|
+
if (bakedGroup) {
|
|
815
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
816
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
this.setPlaybackState(clipName, next);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const action = this.animationActions.get(clipName);
|
|
823
|
+
if (action) {
|
|
824
|
+
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
825
|
+
loop: true,
|
|
826
|
+
source: this.clipSources.get(clipName) ?? "clip"
|
|
827
|
+
});
|
|
828
|
+
next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
|
|
540
829
|
action.setEffectiveWeight(next.weight);
|
|
541
830
|
this.setPlaybackState(clipName, next);
|
|
542
831
|
}
|
|
543
832
|
}
|
|
544
833
|
setAnimationLoopMode(clipName, loopMode) {
|
|
545
|
-
const action = this.getOrCreateBakedAction(clipName);
|
|
546
|
-
if (!action) return;
|
|
547
834
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
548
835
|
loop: true,
|
|
549
|
-
source: this.clipSources.get(clipName) ?? "baked"
|
|
836
|
+
source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
|
|
550
837
|
});
|
|
551
838
|
next.loopMode = loopMode;
|
|
552
839
|
next.loop = loopMode !== "once";
|
|
840
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
841
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
842
|
+
if (bakedGroup) {
|
|
843
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
844
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
this.setPlaybackState(clipName, next);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const action = this.animationActions.get(clipName);
|
|
851
|
+
if (!action) return;
|
|
553
852
|
this.applyPlaybackState(action, next);
|
|
554
853
|
this.setPlaybackState(clipName, next);
|
|
555
854
|
}
|
|
556
855
|
setAnimationRepeatCount(clipName, repeatCount) {
|
|
557
|
-
const action = this.getOrCreateBakedAction(clipName);
|
|
558
|
-
if (!action) return;
|
|
559
856
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
560
857
|
loop: true,
|
|
561
|
-
source: this.clipSources.get(clipName) ?? "baked"
|
|
858
|
+
source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
|
|
562
859
|
});
|
|
563
860
|
next.repeatCount = typeof repeatCount === "number" && Number.isFinite(repeatCount) ? Math.max(0, repeatCount) : void 0;
|
|
861
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
862
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
863
|
+
if (bakedGroup) {
|
|
864
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
865
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
this.setPlaybackState(clipName, next);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const action = this.animationActions.get(clipName);
|
|
872
|
+
if (!action) return;
|
|
564
873
|
this.applyPlaybackState(action, next);
|
|
565
874
|
this.setPlaybackState(clipName, next);
|
|
566
875
|
}
|
|
567
876
|
setAnimationReverse(clipName, reverse) {
|
|
568
|
-
const action = this.getOrCreateBakedAction(clipName);
|
|
569
|
-
if (!action) return;
|
|
570
877
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
571
878
|
loop: true,
|
|
572
|
-
source: this.clipSources.get(clipName) ?? "baked"
|
|
879
|
+
source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
|
|
573
880
|
});
|
|
574
881
|
next.reverse = !!reverse;
|
|
882
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
883
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
884
|
+
if (bakedGroup) {
|
|
885
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
886
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.setPlaybackState(clipName, next);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const action = this.animationActions.get(clipName);
|
|
893
|
+
if (!action) return;
|
|
575
894
|
this.applyPlaybackState(action, next);
|
|
576
895
|
this.setPlaybackState(clipName, next);
|
|
577
896
|
}
|
|
578
897
|
setAnimationBlendMode(clipName, blendMode) {
|
|
579
|
-
const action = this.getOrCreateBakedAction(clipName);
|
|
580
|
-
if (!action) return;
|
|
581
898
|
const next = this.getPlaybackStateSnapshot(clipName, {
|
|
582
899
|
loop: true,
|
|
583
|
-
source: this.clipSources.get(clipName) ?? "baked"
|
|
900
|
+
source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
|
|
584
901
|
});
|
|
902
|
+
next.requestedBlendMode = blendMode;
|
|
903
|
+
if (this.isBakedSourceClip(clipName)) {
|
|
904
|
+
next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
|
|
905
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
906
|
+
if (bakedGroup) {
|
|
907
|
+
for (const [channel, action2] of bakedGroup.channelActions) {
|
|
908
|
+
this.applyPlaybackStateToBakedAction(action2, next, channel);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
this.setPlaybackState(clipName, next);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
585
914
|
next.blendMode = blendMode;
|
|
915
|
+
const action = this.animationActions.get(clipName);
|
|
916
|
+
if (!action) return;
|
|
586
917
|
this.applyPlaybackState(action, next);
|
|
587
918
|
this.setPlaybackState(clipName, next);
|
|
588
919
|
}
|
|
589
920
|
seekAnimation(clipName, time) {
|
|
590
|
-
const
|
|
921
|
+
const bakedGroup = this.bakedActionGroups.get(clipName);
|
|
922
|
+
if (bakedGroup) {
|
|
923
|
+
const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
|
|
924
|
+
const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
|
|
925
|
+
for (const action2 of bakedGroup.channelActions.values()) {
|
|
926
|
+
action2.time = clamped;
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
this.animationMixer?.update(0);
|
|
930
|
+
} catch {
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const action = this.animationActions.get(clipName);
|
|
591
935
|
if (!action) return;
|
|
592
936
|
const duration = action.getClip().duration;
|
|
593
937
|
action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
|
|
@@ -602,6 +946,40 @@ var BakedAnimationController = class {
|
|
|
602
946
|
}
|
|
603
947
|
}
|
|
604
948
|
getAnimationState(clipName) {
|
|
949
|
+
const bakedClip = this.getBakedSourceClip(clipName);
|
|
950
|
+
if (bakedClip) {
|
|
951
|
+
const state2 = this.playbackState.get(clipName);
|
|
952
|
+
const action2 = this.getRepresentativeBakedAction(clipName);
|
|
953
|
+
if (!state2 && !action2) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
const loopMode2 = state2?.loopMode ?? (action2?.loop === LoopPingPong ? "pingpong" : action2?.loop === LoopOnce ? "once" : "repeat");
|
|
957
|
+
const playbackRate2 = state2?.playbackRate ?? Math.abs(action2?.getEffectiveTimeScale?.() ?? 1);
|
|
958
|
+
const reverse2 = state2?.reverse ?? (action2?.getEffectiveTimeScale?.() ?? 1) < 0;
|
|
959
|
+
const pausedValues = this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).map((entry) => entry.paused) : [];
|
|
960
|
+
return {
|
|
961
|
+
name: bakedClip.sourceClip.name,
|
|
962
|
+
actionId: this.bakedActionGroups.get(clipName)?.actionId,
|
|
963
|
+
source: state2?.source ?? this.clipSources.get(clipName) ?? "baked",
|
|
964
|
+
isPlaying: this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).some((entry) => entry.isRunning() && !entry.paused) : false,
|
|
965
|
+
isPaused: pausedValues.length > 0 ? pausedValues.every(Boolean) : false,
|
|
966
|
+
time: action2?.time ?? 0,
|
|
967
|
+
duration: bakedClip.sourceClip.duration,
|
|
968
|
+
speed: playbackRate2,
|
|
969
|
+
playbackRate: playbackRate2,
|
|
970
|
+
reverse: reverse2,
|
|
971
|
+
weight: state2?.weight ?? action2?.getEffectiveWeight?.() ?? 1,
|
|
972
|
+
balance: state2?.balance ?? 0,
|
|
973
|
+
requestedBlendMode: state2?.requestedBlendMode ?? "replace",
|
|
974
|
+
blendMode: this.getBakedAggregateBlendMode(clipName, state2),
|
|
975
|
+
channels: this.getBakedChannelInfo(clipName, state2),
|
|
976
|
+
easing: state2?.easing ?? "linear",
|
|
977
|
+
loop: loopMode2 !== "once",
|
|
978
|
+
loopMode: loopMode2,
|
|
979
|
+
repeatCount: state2?.repeatCount,
|
|
980
|
+
isLooping: loopMode2 !== "once"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
605
983
|
const action = this.animationActions.get(clipName);
|
|
606
984
|
if (!action) return null;
|
|
607
985
|
const clip = action.getClip();
|
|
@@ -622,7 +1000,9 @@ var BakedAnimationController = class {
|
|
|
622
1000
|
reverse,
|
|
623
1001
|
weight: state?.weight ?? action.getEffectiveWeight(),
|
|
624
1002
|
balance: state?.balance ?? 0,
|
|
1003
|
+
requestedBlendMode: state?.requestedBlendMode ?? state?.blendMode ?? "replace",
|
|
625
1004
|
blendMode: state?.blendMode ?? "replace",
|
|
1005
|
+
channels: state?.source === "baked" ? this.getBakedChannelInfo(clipName, state) : void 0,
|
|
626
1006
|
easing: state?.easing ?? "linear",
|
|
627
1007
|
loop: loopMode !== "once",
|
|
628
1008
|
loopMode,
|
|
@@ -632,6 +1012,12 @@ var BakedAnimationController = class {
|
|
|
632
1012
|
}
|
|
633
1013
|
getPlayingAnimations() {
|
|
634
1014
|
const playing = [];
|
|
1015
|
+
for (const name of this.bakedActionGroups.keys()) {
|
|
1016
|
+
const state = this.getAnimationState(name);
|
|
1017
|
+
if (state?.isPlaying) {
|
|
1018
|
+
playing.push(state);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
635
1021
|
for (const [name, action] of this.animationActions) {
|
|
636
1022
|
if (action.isRunning()) {
|
|
637
1023
|
const state = this.getAnimationState(name);
|
|
@@ -641,6 +1027,13 @@ var BakedAnimationController = class {
|
|
|
641
1027
|
return playing;
|
|
642
1028
|
}
|
|
643
1029
|
crossfadeTo(clipName, duration = 0.3, options = {}) {
|
|
1030
|
+
for (const group of this.bakedActionGroups.values()) {
|
|
1031
|
+
for (const action of group.channelActions.values()) {
|
|
1032
|
+
if (action.isRunning()) {
|
|
1033
|
+
action.fadeOut(duration);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
644
1037
|
for (const action of this.animationActions.values()) {
|
|
645
1038
|
if (action.isRunning()) {
|
|
646
1039
|
action.fadeOut(duration);
|
|
@@ -1166,6 +1559,14 @@ var BakedAnimationController = class {
|
|
|
1166
1559
|
this.animationMixer.addEventListener("finished", (event) => {
|
|
1167
1560
|
const action = event.action;
|
|
1168
1561
|
const clip = action.getClip();
|
|
1562
|
+
const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
|
|
1563
|
+
if (bakedRuntime) {
|
|
1564
|
+
const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
|
|
1565
|
+
if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
|
|
1566
|
+
group.resolveFinished();
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1169
1570
|
const callback = this.animationFinishedCallbacks.get(clip.name);
|
|
1170
1571
|
if (callback) {
|
|
1171
1572
|
callback();
|
|
@@ -1190,6 +1591,20 @@ var BakedAnimationController = class {
|
|
|
1190
1591
|
finished: finishedPromise
|
|
1191
1592
|
};
|
|
1192
1593
|
}
|
|
1594
|
+
createBakedAnimationHandle(clipName, group) {
|
|
1595
|
+
return {
|
|
1596
|
+
actionId: group.actionId,
|
|
1597
|
+
stop: () => this.stopAnimation(clipName),
|
|
1598
|
+
pause: () => this.pauseAnimation(clipName),
|
|
1599
|
+
resume: () => this.resumeAnimation(clipName),
|
|
1600
|
+
setSpeed: (speed) => this.setAnimationSpeed(clipName, speed),
|
|
1601
|
+
setWeight: (weight) => this.setAnimationIntensity(clipName, weight),
|
|
1602
|
+
seekTo: (time) => this.seekAnimation(clipName, time),
|
|
1603
|
+
getState: () => this.getAnimationState(clipName),
|
|
1604
|
+
crossfadeTo: (targetClip, dur) => this.crossfadeTo(targetClip, dur),
|
|
1605
|
+
finished: group.finishedPromise
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1193
1608
|
};
|
|
1194
1609
|
|
|
1195
1610
|
// src/presets/cc4.ts
|
|
@@ -3916,7 +4331,8 @@ var _Loom3 = class _Loom3 {
|
|
|
3916
4331
|
__publicField(this, "bones", {});
|
|
3917
4332
|
__publicField(this, "mixWeights", {});
|
|
3918
4333
|
// Viseme state
|
|
3919
|
-
__publicField(this, "visemeValues",
|
|
4334
|
+
__publicField(this, "visemeValues", []);
|
|
4335
|
+
__publicField(this, "visemeJawScales", []);
|
|
3920
4336
|
__publicField(this, "bakedAnimations");
|
|
3921
4337
|
__publicField(this, "hairPhysics");
|
|
3922
4338
|
// Internal animation loop
|
|
@@ -3929,6 +4345,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3929
4345
|
const basePreset = config.presetType ? getPreset(config.presetType) : CC4_PRESET;
|
|
3930
4346
|
this.config = extendPresetWithProfile(basePreset, config.profile);
|
|
3931
4347
|
this.mixWeights = { ...this.config.auMixDefaults };
|
|
4348
|
+
this.syncVisemeRuntimeState();
|
|
3932
4349
|
this.animation = animation || new AnimationThree();
|
|
3933
4350
|
this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
|
|
3934
4351
|
this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
|
|
@@ -4461,6 +4878,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4461
4878
|
if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) return;
|
|
4462
4879
|
const val = clamp012(value);
|
|
4463
4880
|
this.visemeValues[visemeIndex] = val;
|
|
4881
|
+
this.visemeJawScales[visemeIndex] = jawScale;
|
|
4464
4882
|
const targets = this.resolvedVisemeTargets[visemeIndex];
|
|
4465
4883
|
if (targets && targets.length > 0) {
|
|
4466
4884
|
this.applyMorphTargets(targets, val);
|
|
@@ -4473,7 +4891,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4473
4891
|
this.setMorph(morphKey, val, visemeMeshNames);
|
|
4474
4892
|
}
|
|
4475
4893
|
}
|
|
4476
|
-
const jawAmount =
|
|
4894
|
+
const jawAmount = this.getVisemeJawAmount(visemeIndex) * val * jawScale;
|
|
4477
4895
|
if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
|
|
4478
4896
|
this.updateBoneRotation("JAW", "pitch", jawAmount);
|
|
4479
4897
|
}
|
|
@@ -4488,9 +4906,10 @@ var _Loom3 = class _Loom3 {
|
|
|
4488
4906
|
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
4489
4907
|
const target = clamp012(to);
|
|
4490
4908
|
this.visemeValues[visemeIndex] = target;
|
|
4909
|
+
this.visemeJawScales[visemeIndex] = jawScale;
|
|
4491
4910
|
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
4492
4911
|
const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
|
|
4493
|
-
const jawAmount =
|
|
4912
|
+
const jawAmount = this.getVisemeJawAmount(visemeIndex) * target * jawScale;
|
|
4494
4913
|
if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
|
|
4495
4914
|
return morphHandle;
|
|
4496
4915
|
}
|
|
@@ -4544,6 +4963,9 @@ var _Loom3 = class _Loom3 {
|
|
|
4544
4963
|
}
|
|
4545
4964
|
resetToNeutral() {
|
|
4546
4965
|
this.auValues = {};
|
|
4966
|
+
this.visemeValues = new Array(this.config.visemeKeys.length).fill(0);
|
|
4967
|
+
this.visemeJawScales = new Array(this.config.visemeKeys.length).fill(1);
|
|
4968
|
+
this.translations = {};
|
|
4547
4969
|
this.initBoneRotations();
|
|
4548
4970
|
this.clearTransitions();
|
|
4549
4971
|
for (const m of this.meshes) {
|
|
@@ -4559,6 +4981,33 @@ var _Loom3 = class _Loom3 {
|
|
|
4559
4981
|
entry.obj.quaternion.copy(entry.baseQuat);
|
|
4560
4982
|
});
|
|
4561
4983
|
}
|
|
4984
|
+
reinitializeRuntimeStateFromCurrentControls(staleMorphTargets = []) {
|
|
4985
|
+
this.clearTransitions();
|
|
4986
|
+
this.resetMorphTargetHandles(staleMorphTargets);
|
|
4987
|
+
this.translations = {};
|
|
4988
|
+
this.initBoneRotations();
|
|
4989
|
+
Object.values(this.bones).forEach((entry) => {
|
|
4990
|
+
if (!entry) return;
|
|
4991
|
+
entry.obj.position.copy(entry.basePos);
|
|
4992
|
+
entry.obj.quaternion.copy(entry.baseQuat);
|
|
4993
|
+
entry.obj.updateMatrixWorld(false);
|
|
4994
|
+
});
|
|
4995
|
+
for (const [auIdStr, value] of Object.entries(this.auValues)) {
|
|
4996
|
+
if (value <= 0) continue;
|
|
4997
|
+
const auId = Number(auIdStr);
|
|
4998
|
+
if (Number.isNaN(auId)) continue;
|
|
4999
|
+
this.setAU(auId, value, this.auBalances[auId]);
|
|
5000
|
+
}
|
|
5001
|
+
for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
|
|
5002
|
+
const value = this.visemeValues[visemeIndex] ?? 0;
|
|
5003
|
+
if (value <= 0) continue;
|
|
5004
|
+
this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
|
|
5005
|
+
}
|
|
5006
|
+
if (this.model) {
|
|
5007
|
+
this.flushPendingComposites();
|
|
5008
|
+
this.model.updateMatrixWorld(true);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
4562
5011
|
// ============================================================================
|
|
4563
5012
|
// MESH CONTROL
|
|
4564
5013
|
// ============================================================================
|
|
@@ -4789,12 +5238,20 @@ var _Loom3 = class _Loom3 {
|
|
|
4789
5238
|
}
|
|
4790
5239
|
setProfile(profile) {
|
|
4791
5240
|
this.config = profile;
|
|
5241
|
+
this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
|
|
5242
|
+
this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
|
|
4792
5243
|
this.mixWeights = { ...profile.auMixDefaults };
|
|
5244
|
+
this.syncVisemeRuntimeState();
|
|
5245
|
+
let staleMorphTargets = [];
|
|
4793
5246
|
if (this.model) {
|
|
5247
|
+
staleMorphTargets = this.collectResolvedExpressionMorphTargets();
|
|
5248
|
+
this.bones = this.resolveBones(this.model);
|
|
5249
|
+
this.missingBoneWarnings.clear();
|
|
4794
5250
|
this.rebuildMorphTargetsCache();
|
|
4795
5251
|
}
|
|
4796
5252
|
this.hairPhysics.refreshMeshSelection();
|
|
4797
5253
|
this.applyHairPhysicsProfileConfig();
|
|
5254
|
+
this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
|
|
4798
5255
|
}
|
|
4799
5256
|
getProfile() {
|
|
4800
5257
|
return this.config;
|
|
@@ -4932,6 +5389,39 @@ var _Loom3 = class _Loom3 {
|
|
|
4932
5389
|
getMorphIndexCacheKey(index, meshNames) {
|
|
4933
5390
|
return meshNames?.length ? `idx:${index}@${meshNames.join(",")}` : `idx:${index}`;
|
|
4934
5391
|
}
|
|
5392
|
+
syncVisemeRuntimeState() {
|
|
5393
|
+
const visemeCount = this.config.visemeKeys.length;
|
|
5394
|
+
this.visemeValues = Array.from(
|
|
5395
|
+
{ length: visemeCount },
|
|
5396
|
+
(_, index) => this.visemeValues[index] ?? 0
|
|
5397
|
+
);
|
|
5398
|
+
this.visemeJawScales = Array.from(
|
|
5399
|
+
{ length: visemeCount },
|
|
5400
|
+
(_, index) => this.visemeJawScales[index] ?? 1
|
|
5401
|
+
);
|
|
5402
|
+
}
|
|
5403
|
+
getVisemeJawAmount(visemeIndex) {
|
|
5404
|
+
return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
|
|
5405
|
+
}
|
|
5406
|
+
collectResolvedExpressionMorphTargets() {
|
|
5407
|
+
const targets = [];
|
|
5408
|
+
for (const resolved of this.resolvedAUMorphTargets.values()) {
|
|
5409
|
+
targets.push(...resolved.left, ...resolved.right, ...resolved.center);
|
|
5410
|
+
}
|
|
5411
|
+
for (const resolved of this.resolvedVisemeTargets) {
|
|
5412
|
+
if (resolved?.length) {
|
|
5413
|
+
targets.push(...resolved);
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
return targets;
|
|
5417
|
+
}
|
|
5418
|
+
resetMorphTargetHandles(targets) {
|
|
5419
|
+
for (const { infl, idx } of targets) {
|
|
5420
|
+
if (idx < infl.length) {
|
|
5421
|
+
infl[idx] = 0;
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
4935
5425
|
isMixedAU(id) {
|
|
4936
5426
|
const morphs = this.config.auToMorphs[id];
|
|
4937
5427
|
const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
|
|
@@ -5073,12 +5563,25 @@ var _Loom3 = class _Loom3 {
|
|
|
5073
5563
|
}
|
|
5074
5564
|
resolveBones(root) {
|
|
5075
5565
|
const resolved = {};
|
|
5566
|
+
const previousBones = this.bones;
|
|
5076
5567
|
const snapshot = (obj) => ({
|
|
5077
5568
|
obj,
|
|
5078
5569
|
basePos: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
|
|
5079
5570
|
baseQuat: obj.quaternion.clone(),
|
|
5080
5571
|
baseEuler: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, order: obj.rotation.order }
|
|
5081
5572
|
});
|
|
5573
|
+
const snapshotPreservingBasePose = (obj) => {
|
|
5574
|
+
const existing = Object.values(previousBones).find((entry) => entry?.obj === obj);
|
|
5575
|
+
if (!existing) {
|
|
5576
|
+
return snapshot(obj);
|
|
5577
|
+
}
|
|
5578
|
+
return {
|
|
5579
|
+
obj,
|
|
5580
|
+
basePos: { ...existing.basePos },
|
|
5581
|
+
baseQuat: existing.baseQuat.clone(),
|
|
5582
|
+
baseEuler: { ...existing.baseEuler }
|
|
5583
|
+
};
|
|
5584
|
+
};
|
|
5082
5585
|
const prefix = this.config.bonePrefix || "";
|
|
5083
5586
|
const suffix = this.config.boneSuffix || "";
|
|
5084
5587
|
const suffixRegex = this.config.suffixPattern ? new RegExp(this.config.suffixPattern) : null;
|
|
@@ -5109,19 +5612,19 @@ var _Loom3 = class _Loom3 {
|
|
|
5109
5612
|
for (const [key, nodeName] of Object.entries(this.config.boneNodes)) {
|
|
5110
5613
|
const node = findNode(nodeName);
|
|
5111
5614
|
if (node) {
|
|
5112
|
-
resolved[key] =
|
|
5615
|
+
resolved[key] = snapshotPreservingBasePose(node);
|
|
5113
5616
|
}
|
|
5114
5617
|
}
|
|
5115
5618
|
if (!resolved.EYE_L && this.config.eyeMeshNodes) {
|
|
5116
5619
|
const node = findNode(this.config.eyeMeshNodes.LEFT);
|
|
5117
5620
|
if (node) {
|
|
5118
|
-
resolved.EYE_L =
|
|
5621
|
+
resolved.EYE_L = snapshotPreservingBasePose(node);
|
|
5119
5622
|
}
|
|
5120
5623
|
}
|
|
5121
5624
|
if (!resolved.EYE_R && this.config.eyeMeshNodes) {
|
|
5122
5625
|
const node = findNode(this.config.eyeMeshNodes.RIGHT);
|
|
5123
5626
|
if (node) {
|
|
5124
|
-
resolved.EYE_R =
|
|
5627
|
+
resolved.EYE_R = snapshotPreservingBasePose(node);
|
|
5125
5628
|
}
|
|
5126
5629
|
}
|
|
5127
5630
|
return resolved;
|