@lovelace_lol/loom3 1.0.36 → 1.0.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var THREE = require('three');
3
+ var THREE2 = require('three');
4
4
 
5
5
  function _interopNamespace(e) {
6
6
  if (e && e.__esModule) return e;
@@ -20,7 +20,7 @@ function _interopNamespace(e) {
20
20
  return Object.freeze(n);
21
21
  }
22
22
 
23
- var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
23
+ var THREE2__namespace = /*#__PURE__*/_interopNamespace(THREE2);
24
24
 
25
25
  var __defProp = Object.defineProperty;
26
26
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -90,12 +90,12 @@ function getRuntimeClipName(sourceClipName, channel) {
90
90
  function parseTrackTarget(trackName, model) {
91
91
  let parsed;
92
92
  try {
93
- parsed = THREE.PropertyBinding.parseTrackName(trackName);
93
+ parsed = THREE2.PropertyBinding.parseTrackName(trackName);
94
94
  } catch {
95
95
  return null;
96
96
  }
97
97
  const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? String(parsed.objectIndex) : parsed.nodeName;
98
- const target = targetKey ? model.getObjectByProperty("uuid", targetKey) ?? THREE.PropertyBinding.findNode(model, targetKey) : null;
98
+ const target = targetKey ? model.getObjectByProperty("uuid", targetKey) ?? THREE2.PropertyBinding.findNode(model, targetKey) : null;
99
99
  return {
100
100
  propertyName: parsed.propertyName,
101
101
  target,
@@ -183,7 +183,7 @@ function partitionBakedClip(clip, model, bones) {
183
183
  }
184
184
  runtimeClips.push({
185
185
  channel,
186
- clip: new THREE.AnimationClip(getRuntimeClipName(clip.name, channel), clip.duration, tracks)
186
+ clip: new THREE2.AnimationClip(getRuntimeClipName(clip.name, channel), clip.duration, tracks)
187
187
  });
188
188
  }
189
189
  return {
@@ -288,9 +288,11 @@ var AnimationThree = class {
288
288
  }
289
289
  };
290
290
  var makeActionId = () => `act_${Math.random().toString(36).slice(2, 8)}_${Date.now().toString(36)}`;
291
- var X_AXIS = new THREE.Vector3(1, 0, 0);
292
- var Y_AXIS = new THREE.Vector3(0, 1, 0);
293
- var Z_AXIS = new THREE.Vector3(0, 0, 1);
291
+ var X_AXIS = new THREE2.Vector3(1, 0, 0);
292
+ var Y_AXIS = new THREE2.Vector3(0, 1, 0);
293
+ var Z_AXIS = new THREE2.Vector3(0, 0, 1);
294
+ var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
295
+ var CLIP_EVENT_EPSILON = 1e-4;
294
296
  var BakedAnimationController = class {
295
297
  constructor(host) {
296
298
  __publicField(this, "host");
@@ -309,6 +311,7 @@ var BakedAnimationController = class {
309
311
  __publicField(this, "playbackState", /* @__PURE__ */ new Map());
310
312
  __publicField(this, "actionIds", /* @__PURE__ */ new WeakMap());
311
313
  __publicField(this, "actionIdToClip", /* @__PURE__ */ new Map());
314
+ __publicField(this, "clipMonitors", /* @__PURE__ */ new Map());
312
315
  this.host = host;
313
316
  }
314
317
  getActionId(action) {
@@ -322,6 +325,161 @@ var BakedAnimationController = class {
322
325
  action.__actionId = actionId;
323
326
  return actionId;
324
327
  }
328
+ setClipEventMetadata(clip, metadata) {
329
+ const userData = clip.userData ?? (clip.userData = {});
330
+ userData[CLIP_EVENT_METADATA_KEY] = metadata;
331
+ }
332
+ getClipEventMetadata(clip) {
333
+ const userData = clip.userData;
334
+ const keyframeTimes = Array.isArray(userData?.[CLIP_EVENT_METADATA_KEY]?.keyframeTimes) ? userData[CLIP_EVENT_METADATA_KEY].keyframeTimes.filter((time) => Number.isFinite(time)) : [];
335
+ return { keyframeTimes };
336
+ }
337
+ getKeyframeIndex(times, currentTime) {
338
+ if (!times.length) return -1;
339
+ const target = Math.max(0, currentTime) + 1e-3;
340
+ let lo = 0;
341
+ let hi = times.length - 1;
342
+ let idx = 0;
343
+ while (lo <= hi) {
344
+ const mid = lo + hi >>> 1;
345
+ if (times[mid] <= target) {
346
+ idx = mid;
347
+ lo = mid + 1;
348
+ } else {
349
+ hi = mid - 1;
350
+ }
351
+ }
352
+ return idx;
353
+ }
354
+ emitClipEvent(monitor, event) {
355
+ for (const listener of Array.from(monitor.listeners)) {
356
+ try {
357
+ listener(event);
358
+ } catch (error) {
359
+ console.error("[Loom3] clip event listener failed", error);
360
+ }
361
+ }
362
+ }
363
+ emitKeyframesForRange(monitor, startTime, endTime, direction, includeStart) {
364
+ if (!monitor.keyframeTimes.length) return;
365
+ const times = direction === 1 ? monitor.keyframeTimes : [...monitor.keyframeTimes].reverse();
366
+ for (const time of times) {
367
+ const matchesForward = direction === 1 && (includeStart ? time >= startTime - CLIP_EVENT_EPSILON : time > startTime + CLIP_EVENT_EPSILON) && time <= endTime + CLIP_EVENT_EPSILON;
368
+ const matchesReverse = direction === -1 && (includeStart ? time <= startTime + CLIP_EVENT_EPSILON : time < startTime - CLIP_EVENT_EPSILON) && time >= endTime - CLIP_EVENT_EPSILON;
369
+ if (!matchesForward && !matchesReverse) continue;
370
+ const keyframeIndex = monitor.keyframeTimes.indexOf(time);
371
+ monitor.lastKeyframeIndex = keyframeIndex;
372
+ this.emitClipEvent(monitor, {
373
+ type: "keyframe",
374
+ clipName: monitor.clipName,
375
+ keyframeIndex,
376
+ totalKeyframes: monitor.keyframeTimes.length,
377
+ currentTime: time,
378
+ duration: monitor.duration,
379
+ iteration: monitor.iteration
380
+ });
381
+ }
382
+ }
383
+ resetClipMonitor(monitor, currentTime) {
384
+ monitor.iteration = 0;
385
+ monitor.direction = monitor.initialDirection;
386
+ monitor.lastTime = currentTime;
387
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, currentTime);
388
+ monitor.finishedPending = false;
389
+ }
390
+ syncClipMonitorTime(monitor, currentTime, emitSeek = false) {
391
+ const clamped = Math.max(0, Math.min(monitor.duration, currentTime));
392
+ monitor.lastTime = clamped;
393
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, clamped);
394
+ if (emitSeek) {
395
+ this.emitClipEvent(monitor, {
396
+ type: "seek",
397
+ clipName: monitor.clipName,
398
+ currentTime: clamped,
399
+ duration: monitor.duration,
400
+ iteration: monitor.iteration
401
+ });
402
+ }
403
+ }
404
+ cleanupClipMonitor(actionId) {
405
+ const monitor = this.clipMonitors.get(actionId);
406
+ if (!monitor || monitor.cleanedUp) return;
407
+ monitor.cleanedUp = true;
408
+ try {
409
+ monitor.action.paused = true;
410
+ } catch {
411
+ }
412
+ monitor.resolveFinished();
413
+ monitor.listeners.clear();
414
+ this.clipMonitors.delete(actionId);
415
+ this.actionIdToClip.delete(actionId);
416
+ }
417
+ advanceClipMonitor(monitor, previousTime) {
418
+ if (monitor.cleanedUp || monitor.action.paused && !monitor.finishedPending) return;
419
+ const currentTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
420
+ const delta = currentTime - previousTime;
421
+ if (monitor.loopMode === "pingpong") {
422
+ const movingForward = monitor.direction === 1;
423
+ const bouncedAtEnd = movingForward && delta < -CLIP_EVENT_EPSILON;
424
+ const bouncedAtStart = !movingForward && delta > CLIP_EVENT_EPSILON;
425
+ if (bouncedAtEnd) {
426
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
427
+ monitor.direction = -1;
428
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, false);
429
+ } else if (bouncedAtStart) {
430
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
431
+ monitor.direction = 1;
432
+ monitor.iteration += 1;
433
+ this.emitClipEvent(monitor, {
434
+ type: "loop",
435
+ clipName: monitor.clipName,
436
+ iteration: monitor.iteration,
437
+ currentTime: 0,
438
+ duration: monitor.duration
439
+ });
440
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, false);
441
+ } else if (delta > CLIP_EVENT_EPSILON) {
442
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
443
+ monitor.direction = 1;
444
+ } else if (delta < -CLIP_EVENT_EPSILON) {
445
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
446
+ monitor.direction = -1;
447
+ }
448
+ } else if (monitor.direction === 1) {
449
+ const wrapped = currentTime + CLIP_EVENT_EPSILON < previousTime;
450
+ if (wrapped) {
451
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
452
+ monitor.iteration += 1;
453
+ this.emitClipEvent(monitor, {
454
+ type: "loop",
455
+ clipName: monitor.clipName,
456
+ iteration: monitor.iteration,
457
+ currentTime: 0,
458
+ duration: monitor.duration
459
+ });
460
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, true);
461
+ } else if (delta > CLIP_EVENT_EPSILON) {
462
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
463
+ }
464
+ } else {
465
+ const wrapped = currentTime > previousTime + CLIP_EVENT_EPSILON;
466
+ if (wrapped) {
467
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
468
+ monitor.iteration += 1;
469
+ this.emitClipEvent(monitor, {
470
+ type: "loop",
471
+ clipName: monitor.clipName,
472
+ iteration: monitor.iteration,
473
+ currentTime: monitor.duration,
474
+ duration: monitor.duration
475
+ });
476
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, true);
477
+ } else if (delta < -CLIP_EVENT_EPSILON) {
478
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
479
+ }
480
+ }
481
+ this.syncClipMonitorTime(monitor, currentTime);
482
+ }
325
483
  normalizePlaybackOptions(options, defaults) {
326
484
  const clipOptions = options;
327
485
  const rawRate = options?.playbackRate ?? options?.speed ?? 1;
@@ -348,14 +506,14 @@ var BakedAnimationController = class {
348
506
  const signedRate = state.reverse ? -state.playbackRate : state.playbackRate;
349
507
  action.setEffectiveTimeScale(signedRate);
350
508
  action.setEffectiveWeight(state.weight);
351
- action.blendMode = state.blendMode === "additive" ? THREE.AdditiveAnimationBlendMode : THREE.NormalAnimationBlendMode;
509
+ action.blendMode = state.blendMode === "additive" ? THREE2.AdditiveAnimationBlendMode : THREE2.NormalAnimationBlendMode;
352
510
  const reps = state.repeatCount ?? Infinity;
353
511
  if (state.loopMode === "pingpong") {
354
- action.setLoop(THREE.LoopPingPong, reps);
512
+ action.setLoop(THREE2.LoopPingPong, reps);
355
513
  } else if (state.loopMode === "once") {
356
- action.setLoop(THREE.LoopOnce, 1);
514
+ action.setLoop(THREE2.LoopOnce, 1);
357
515
  } else {
358
- action.setLoop(THREE.LoopRepeat, reps);
516
+ action.setLoop(THREE2.LoopRepeat, reps);
359
517
  }
360
518
  action.clampWhenFinished = state.loopMode === "once";
361
519
  }
@@ -533,7 +691,28 @@ var BakedAnimationController = class {
533
691
  }
534
692
  update(dtSeconds) {
535
693
  if (this.animationMixer) {
694
+ const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
695
+ actionId: monitor.actionId,
696
+ previousTime: monitor.action.time
697
+ }));
536
698
  this.animationMixer.update(dtSeconds);
699
+ for (const { actionId, previousTime } of snapshots) {
700
+ const monitor = this.clipMonitors.get(actionId);
701
+ if (!monitor) continue;
702
+ this.advanceClipMonitor(monitor, previousTime);
703
+ if (monitor.finishedPending) {
704
+ const finalTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
705
+ this.syncClipMonitorTime(monitor, finalTime);
706
+ this.emitClipEvent(monitor, {
707
+ type: "completed",
708
+ clipName: monitor.clipName,
709
+ currentTime: finalTime,
710
+ duration: monitor.duration,
711
+ iteration: monitor.iteration
712
+ });
713
+ this.cleanupClipMonitor(actionId);
714
+ }
715
+ }
537
716
  }
538
717
  }
539
718
  dispose() {
@@ -553,6 +732,7 @@ var BakedAnimationController = class {
553
732
  this.clipHandles.clear();
554
733
  this.clipSources.clear();
555
734
  this.playbackState.clear();
735
+ this.clipMonitors.clear();
556
736
  }
557
737
  loadAnimationClips(clips) {
558
738
  const model = this.host.getModel();
@@ -691,6 +871,7 @@ var BakedAnimationController = class {
691
871
  }
692
872
  const action = this.animationActions.get(clipName);
693
873
  if (action) {
874
+ const actionId = this.getActionId(action);
694
875
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
695
876
  action.stop();
696
877
  if (!isBaked && this.animationMixer) {
@@ -713,9 +894,11 @@ var BakedAnimationController = class {
713
894
  }
714
895
  }
715
896
  this.animationFinishedCallbacks.delete(clipName);
897
+ if (actionId) this.cleanupClipMonitor(actionId);
716
898
  }
717
899
  const clipAction = this.clipActions.get(clipName);
718
900
  if (clipAction && clipAction !== action) {
901
+ const actionId = this.getActionId(clipAction);
719
902
  try {
720
903
  clipAction.stop();
721
904
  if (this.animationMixer) {
@@ -728,6 +911,7 @@ var BakedAnimationController = class {
728
911
  } catch {
729
912
  }
730
913
  this.clipActions.delete(clipName);
914
+ if (actionId) this.cleanupClipMonitor(actionId);
731
915
  }
732
916
  if (this.clipActions.get(clipName) === action) {
733
917
  this.clipActions.delete(clipName);
@@ -974,7 +1158,7 @@ var BakedAnimationController = class {
974
1158
  if (!state2 && !action2) {
975
1159
  return null;
976
1160
  }
977
- const loopMode2 = state2?.loopMode ?? (action2?.loop === THREE.LoopPingPong ? "pingpong" : action2?.loop === THREE.LoopOnce ? "once" : "repeat");
1161
+ const loopMode2 = state2?.loopMode ?? (action2?.loop === THREE2.LoopPingPong ? "pingpong" : action2?.loop === THREE2.LoopOnce ? "once" : "repeat");
978
1162
  const playbackRate2 = state2?.playbackRate ?? Math.abs(action2?.getEffectiveTimeScale?.() ?? 1);
979
1163
  const reverse2 = state2?.reverse ?? (action2?.getEffectiveTimeScale?.() ?? 1) < 0;
980
1164
  const pausedValues = this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).map((entry) => entry.paused) : [];
@@ -1005,7 +1189,7 @@ var BakedAnimationController = class {
1005
1189
  if (!action) return null;
1006
1190
  const clip = action.getClip();
1007
1191
  const state = this.playbackState.get(clipName);
1008
- const loopMode = state?.loopMode ?? (action.loop === THREE.LoopPingPong ? "pingpong" : action.loop === THREE.LoopOnce ? "once" : "repeat");
1192
+ const loopMode = state?.loopMode ?? (action.loop === THREE2.LoopPingPong ? "pingpong" : action.loop === THREE2.LoopOnce ? "once" : "repeat");
1009
1193
  const playbackRate = state?.playbackRate ?? Math.abs(action.getEffectiveTimeScale());
1010
1194
  const reverse = state?.reverse ?? action.getEffectiveTimeScale() < 0;
1011
1195
  return {
@@ -1192,12 +1376,12 @@ var BakedAnimationController = class {
1192
1376
  const jawBinding = config.auToBones[26]?.[0];
1193
1377
  const maxDegrees = jawBinding?.maxDegrees ?? 30;
1194
1378
  const radians = maxDegrees * Math.PI / 180 * jawAmount;
1195
- const jawQ = new THREE.Quaternion().copy(jawEntry.baseQuat);
1196
- jawQ.multiply(new THREE.Quaternion().setFromAxisAngle(Z_AXIS, radians));
1379
+ const jawQ = new THREE2.Quaternion().copy(jawEntry.baseQuat);
1380
+ jawQ.multiply(new THREE2.Quaternion().setFromAxisAngle(Z_AXIS, radians));
1197
1381
  jawValues.push(jawQ.x, jawQ.y, jawQ.z, jawQ.w);
1198
1382
  }
1199
1383
  const trackName = `${jawEntry.obj.uuid}.quaternion`;
1200
- tracks.push(new THREE.QuaternionKeyframeTrack(trackName, keyframeTimes, jawValues));
1384
+ tracks.push(new THREE2.QuaternionKeyframeTrack(trackName, keyframeTimes, jawValues));
1201
1385
  }
1202
1386
  }
1203
1387
  if (keyframeTimes.length > 0) {
@@ -1241,7 +1425,7 @@ var BakedAnimationController = class {
1241
1425
  }
1242
1426
  const values = [];
1243
1427
  for (const t of keyframeTimes) {
1244
- const compositeQ = new THREE.Quaternion().copy(entry.baseQuat);
1428
+ const compositeQ = new THREE2.Quaternion().copy(entry.baseQuat);
1245
1429
  const applyAxis = (axisConfig) => {
1246
1430
  if (!axisConfig) return;
1247
1431
  let axisValue = getAxisValue(nodeKey, axisConfig, t);
@@ -1250,7 +1434,7 @@ var BakedAnimationController = class {
1250
1434
  if (!binding?.maxDegrees || !binding.channel) return;
1251
1435
  const radians = binding.maxDegrees * Math.PI / 180 * Math.abs(axisValue) * binding.scale;
1252
1436
  const axis = binding.channel === "rx" ? X_AXIS : binding.channel === "ry" ? Y_AXIS : Z_AXIS;
1253
- const deltaQ = new THREE.Quaternion().setFromAxisAngle(axis, radians);
1437
+ const deltaQ = new THREE2.Quaternion().setFromAxisAngle(axis, radians);
1254
1438
  compositeQ.multiply(deltaQ);
1255
1439
  };
1256
1440
  applyAxis(composite.yaw);
@@ -1259,7 +1443,7 @@ var BakedAnimationController = class {
1259
1443
  values.push(compositeQ.x, compositeQ.y, compositeQ.z, compositeQ.w);
1260
1444
  }
1261
1445
  const trackName = `${entry.obj.uuid}.quaternion`;
1262
- tracks.push(new THREE.QuaternionKeyframeTrack(trackName, keyframeTimes, values));
1446
+ tracks.push(new THREE2.QuaternionKeyframeTrack(trackName, keyframeTimes, values));
1263
1447
  }
1264
1448
  for (const curveId of Object.keys(curves)) {
1265
1449
  if (!isNumericAU(curveId)) continue;
@@ -1280,7 +1464,7 @@ var BakedAnimationController = class {
1280
1464
  values.push(basePos + delta);
1281
1465
  }
1282
1466
  const trackName = `${entry.obj.uuid}.position[${axisIndex}]`;
1283
- tracks.push(new THREE.NumberKeyframeTrack(trackName, keyframeTimes, values));
1467
+ tracks.push(new THREE2.NumberKeyframeTrack(trackName, keyframeTimes, values));
1284
1468
  }
1285
1469
  }
1286
1470
  }
@@ -1288,7 +1472,8 @@ var BakedAnimationController = class {
1288
1472
  console.warn(`[Loom3] snippetToClip: No tracks created for "${clipName}"`);
1289
1473
  return null;
1290
1474
  }
1291
- const clip = new THREE.AnimationClip(clipName, maxTime, tracks);
1475
+ const clip = new THREE2.AnimationClip(clipName, maxTime, tracks);
1476
+ this.setClipEventMetadata(clip, { keyframeTimes });
1292
1477
  console.log(`[Loom3] snippetToClip: Created clip "${clipName}" with ${tracks.length} tracks, duration ${maxTime.toFixed(2)}s`);
1293
1478
  return clip;
1294
1479
  }
@@ -1320,28 +1505,38 @@ var BakedAnimationController = class {
1320
1505
  this.animationClips.push(clip);
1321
1506
  }
1322
1507
  this.applyPlaybackState(action, playbackState);
1508
+ if (actionId) {
1509
+ this.cleanupClipMonitor(actionId);
1510
+ }
1323
1511
  let resolveFinished;
1324
1512
  const finishedPromise = new Promise((resolve) => {
1325
1513
  resolveFinished = resolve;
1326
1514
  });
1327
- const cleanup = () => {
1328
- try {
1329
- this.animationFinishedCallbacks.delete(clip.name);
1330
- } catch {
1331
- }
1332
- try {
1333
- action.paused = true;
1334
- } catch {
1335
- }
1515
+ const keyframeTimes = this.getClipEventMetadata(clip).keyframeTimes;
1516
+ const initialDirection = playbackState.reverse ? -1 : 1;
1517
+ const monitor = {
1518
+ action,
1519
+ actionId,
1520
+ clip,
1521
+ clipName: clip.name,
1522
+ duration: clip.duration,
1523
+ keyframeTimes,
1524
+ listeners: /* @__PURE__ */ new Set(),
1525
+ initialDirection,
1526
+ direction: initialDirection,
1527
+ iteration: 0,
1528
+ lastTime: Math.max(0, Math.min(clip.duration, action.time)),
1529
+ lastKeyframeIndex: this.getKeyframeIndex(keyframeTimes, action.time),
1530
+ loopMode: playbackState.loopMode,
1531
+ finishedPending: false,
1532
+ cleanedUp: false,
1533
+ resolveFinished
1336
1534
  };
1337
- this.animationFinishedCallbacks.set(clip.name, () => {
1338
- resolveFinished();
1339
- cleanup();
1340
- });
1341
- finishedPromise.catch(() => cleanup());
1535
+ this.clipMonitors.set(actionId, monitor);
1342
1536
  action.reset();
1343
1537
  action.time = startTime;
1344
1538
  action.play();
1539
+ this.resetClipMonitor(monitor, action.time);
1345
1540
  this.clipActions.set(clip.name, action);
1346
1541
  this.animationActions.set(clip.name, action);
1347
1542
  this.setPlaybackState(clip.name, playbackState);
@@ -1359,6 +1554,7 @@ var BakedAnimationController = class {
1359
1554
  })
1360
1555
  );
1361
1556
  action.play();
1557
+ this.resetClipMonitor(monitor, action.time);
1362
1558
  },
1363
1559
  stop: () => {
1364
1560
  action.stop();
@@ -1376,8 +1572,7 @@ var BakedAnimationController = class {
1376
1572
  this.animationActions.delete(clip.name);
1377
1573
  this.animationFinishedCallbacks.delete(clip.name);
1378
1574
  this.playbackState.delete(clip.name);
1379
- resolveFinished();
1380
- cleanup();
1575
+ this.cleanupClipMonitor(actionId);
1381
1576
  },
1382
1577
  pause: () => {
1383
1578
  action.paused = true;
@@ -1395,6 +1590,8 @@ var BakedAnimationController = class {
1395
1590
  const next = this.playbackState.get(clip.name) ?? playbackState;
1396
1591
  next.playbackRate = Number.isFinite(r) ? Math.max(0, Math.abs(r)) : 1;
1397
1592
  this.applyPlaybackState(action, next);
1593
+ monitor.direction = next.reverse ? -1 : 1;
1594
+ monitor.initialDirection = monitor.direction;
1398
1595
  this.setPlaybackState(clip.name, next);
1399
1596
  },
1400
1597
  setLoop: (mode, repeatCount) => {
@@ -1403,6 +1600,7 @@ var BakedAnimationController = class {
1403
1600
  next.loop = mode !== "once";
1404
1601
  next.repeatCount = repeatCount;
1405
1602
  this.applyPlaybackState(action, next);
1603
+ monitor.loopMode = mode;
1406
1604
  this.setPlaybackState(clip.name, next);
1407
1605
  },
1408
1606
  setTime: (t) => {
@@ -1412,9 +1610,16 @@ var BakedAnimationController = class {
1412
1610
  this.animationMixer?.update(0);
1413
1611
  } catch {
1414
1612
  }
1613
+ this.syncClipMonitorTime(monitor, clamped, true);
1415
1614
  },
1416
1615
  getTime: () => action.time,
1417
1616
  getDuration: () => clip.duration,
1617
+ subscribe: (listener) => {
1618
+ monitor.listeners.add(listener);
1619
+ return () => {
1620
+ monitor.listeners.delete(listener);
1621
+ };
1622
+ },
1418
1623
  finished: finishedPromise
1419
1624
  };
1420
1625
  this.clipHandles.set(clip.name, handle);
@@ -1438,6 +1643,7 @@ var BakedAnimationController = class {
1438
1643
  if (!this.animationMixer || !this.host.getModel()) return;
1439
1644
  for (const [clipName, action] of Array.from(this.clipActions.entries())) {
1440
1645
  if (clipName === name || clipName.startsWith(`${name}_`)) {
1646
+ const actionId = this.getActionId(action);
1441
1647
  try {
1442
1648
  action.stop();
1443
1649
  const clip = action.getClip();
@@ -1452,6 +1658,7 @@ var BakedAnimationController = class {
1452
1658
  this.clipHandles.delete(clipName);
1453
1659
  this.animationFinishedCallbacks.delete(clipName);
1454
1660
  this.playbackState.delete(clipName);
1661
+ if (actionId) this.cleanupClipMonitor(actionId);
1455
1662
  }
1456
1663
  }
1457
1664
  }
@@ -1475,6 +1682,8 @@ var BakedAnimationController = class {
1475
1682
  console.log("[Loom3] updateClipParams start", debugSnapshot());
1476
1683
  const apply = (action) => {
1477
1684
  if (!action) return;
1685
+ const actionId = this.getActionId(action);
1686
+ const monitor = actionId ? this.clipMonitors.get(actionId) : void 0;
1478
1687
  const clipName = action.getClip().name;
1479
1688
  const next = this.playbackState.get(clipName) ?? this.normalizePlaybackOptions(void 0, { loop: false, source: this.clipSources.get(clipName) ?? "clip" });
1480
1689
  try {
@@ -1493,6 +1702,10 @@ var BakedAnimationController = class {
1493
1702
  }
1494
1703
  const signedRate = next.reverse ? -next.playbackRate : next.playbackRate;
1495
1704
  action.setEffectiveTimeScale(signedRate);
1705
+ if (monitor) {
1706
+ monitor.direction = next.reverse ? -1 : 1;
1707
+ monitor.initialDirection = monitor.direction;
1708
+ }
1496
1709
  updated = true;
1497
1710
  }
1498
1711
  if (typeof params.loop === "boolean" || params.loopMode || params.repeatCount !== void 0) {
@@ -1500,6 +1713,7 @@ var BakedAnimationController = class {
1500
1713
  next.loop = next.loopMode !== "once";
1501
1714
  next.repeatCount = params.repeatCount;
1502
1715
  this.applyPlaybackState(action, next);
1716
+ if (monitor) monitor.loopMode = next.loopMode;
1503
1717
  updated = true;
1504
1718
  }
1505
1719
  this.setPlaybackState(clipName, next);
@@ -1540,7 +1754,7 @@ var BakedAnimationController = class {
1540
1754
  values.push(Math.max(0, Math.min(2, kf.intensity * intensityScale)));
1541
1755
  }
1542
1756
  const trackName = `${mesh.uuid}.morphTargetInfluences[${morphIndex}]`;
1543
- const track = new THREE.NumberKeyframeTrack(trackName, times, values);
1757
+ const track = new THREE2.NumberKeyframeTrack(trackName, times, values);
1544
1758
  tracks.push(track);
1545
1759
  };
1546
1760
  for (const mesh of targetMeshes) {
@@ -1563,7 +1777,7 @@ var BakedAnimationController = class {
1563
1777
  values.push(Math.max(0, Math.min(2, kf.intensity * intensityScale)));
1564
1778
  }
1565
1779
  const trackName = `${mesh.uuid}.morphTargetInfluences[${morphIndex}]`;
1566
- const track = new THREE.NumberKeyframeTrack(trackName, times, values);
1780
+ const track = new THREE2.NumberKeyframeTrack(trackName, times, values);
1567
1781
  tracks.push(track);
1568
1782
  };
1569
1783
  for (const mesh of targetMeshes) {
@@ -1574,11 +1788,19 @@ var BakedAnimationController = class {
1574
1788
  const model = this.host.getModel();
1575
1789
  if (!model) return null;
1576
1790
  if (!this.animationMixer) {
1577
- this.animationMixer = new THREE.AnimationMixer(model);
1791
+ this.animationMixer = new THREE2.AnimationMixer(model);
1578
1792
  }
1579
1793
  if (this.animationMixer && !this.mixerFinishedListenerAttached) {
1580
1794
  this.animationMixer.addEventListener("finished", (event) => {
1581
1795
  const action = event.action;
1796
+ const actionId = this.getActionId(action);
1797
+ if (actionId) {
1798
+ const monitor = this.clipMonitors.get(actionId);
1799
+ if (monitor) {
1800
+ monitor.finishedPending = true;
1801
+ return;
1802
+ }
1803
+ }
1582
1804
  const clip = action.getClip();
1583
1805
  const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1584
1806
  if (bakedRuntime) {
@@ -2703,7 +2925,7 @@ var HairPhysicsController = class {
2703
2925
  this.clearRegisteredHairObjects();
2704
2926
  const result = [];
2705
2927
  for (const obj of objects) {
2706
- if (obj instanceof THREE.Mesh) {
2928
+ if (obj instanceof THREE2.Mesh) {
2707
2929
  const mesh = obj;
2708
2930
  this.registeredHairObjects.set(mesh.name, mesh);
2709
2931
  const meshInfo = CC4_MESHES[mesh.name];
@@ -4285,15 +4507,17 @@ function getPreset(presetType) {
4285
4507
  return CC4_PRESET;
4286
4508
  }
4287
4509
  }
4510
+ var resolvePreset = getPreset;
4288
4511
  function getPresetWithProfile(presetType, profile) {
4289
4512
  return extendPresetWithProfile(getPreset(presetType), profile);
4290
4513
  }
4514
+ var resolvePresetWithOverrides = getPresetWithProfile;
4291
4515
 
4292
4516
  // src/engines/three/Loom3.ts
4293
4517
  var deg2rad = (d) => d * Math.PI / 180;
4294
- var X_AXIS2 = new THREE.Vector3(1, 0, 0);
4295
- var Y_AXIS2 = new THREE.Vector3(0, 1, 0);
4296
- var Z_AXIS2 = new THREE.Vector3(0, 0, 1);
4518
+ var X_AXIS2 = new THREE2.Vector3(1, 0, 0);
4519
+ var Y_AXIS2 = new THREE2.Vector3(0, 1, 0);
4520
+ var Z_AXIS2 = new THREE2.Vector3(0, 0, 1);
4297
4521
  function buildAUToCompositeMap(composites) {
4298
4522
  const map = /* @__PURE__ */ new Map();
4299
4523
  composites.forEach((comp) => {
@@ -4357,7 +4581,7 @@ var _Loom3 = class _Loom3 {
4357
4581
  __publicField(this, "bakedAnimations");
4358
4582
  __publicField(this, "hairPhysics");
4359
4583
  // Internal animation loop
4360
- __publicField(this, "clock", new THREE.Clock(false));
4584
+ __publicField(this, "clock", new THREE2.Clock(false));
4361
4585
  // Don't auto-start
4362
4586
  __publicField(this, "animationFrameId", null);
4363
4587
  __publicField(this, "isRunning", false);
@@ -4504,11 +4728,11 @@ var _Loom3 = class _Loom3 {
4504
4728
  }
4505
4729
  const head = this.bones["HEAD"]?.obj;
4506
4730
  if (head && availableMorphMeshes.length > 0) {
4507
- const headPos = new THREE.Vector3();
4731
+ const headPos = new THREE2.Vector3();
4508
4732
  head.getWorldPosition?.(headPos);
4509
4733
  const headCandidates = availableMorphMeshes.map((mesh) => {
4510
- const box = new THREE.Box3().setFromObject(mesh);
4511
- const center = new THREE.Vector3();
4734
+ const box = new THREE2.Box3().setFromObject(mesh);
4735
+ const center = new THREE2.Vector3();
4512
4736
  box.getCenter(center);
4513
4737
  const distance = box.containsPoint(headPos) ? 0 : center.distanceTo(headPos);
4514
4738
  const morphCount = mesh.morphTargetDictionary ? Object.keys(mesh.morphTargetDictionary).length : 0;
@@ -5543,13 +5767,13 @@ var _Loom3 = class _Loom3 {
5543
5767
  return;
5544
5768
  }
5545
5769
  const getAxis = (channel) => channel === "rx" ? X_AXIS2 : channel === "ry" ? Y_AXIS2 : Z_AXIS2;
5546
- const compositeQ = new THREE.Quaternion().copy(baseQuat);
5770
+ const compositeQ = new THREE2.Quaternion().copy(baseQuat);
5547
5771
  if (config.yaw && rotState.yaw !== 0) {
5548
5772
  const binding = this.getCompositeAxisBindingForNode(nodeKey, config.yaw, rotState.yaw);
5549
5773
  if (binding?.maxDegrees && binding.channel) {
5550
5774
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.yaw) * binding.scale;
5551
5775
  const axis = getAxis(binding.channel);
5552
- const deltaQ = new THREE.Quaternion().setFromAxisAngle(axis, radians);
5776
+ const deltaQ = new THREE2.Quaternion().setFromAxisAngle(axis, radians);
5553
5777
  compositeQ.multiply(deltaQ);
5554
5778
  }
5555
5779
  }
@@ -5558,7 +5782,7 @@ var _Loom3 = class _Loom3 {
5558
5782
  if (binding?.maxDegrees && binding.channel) {
5559
5783
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.pitch) * binding.scale;
5560
5784
  const axis = getAxis(binding.channel);
5561
- const deltaQ = new THREE.Quaternion().setFromAxisAngle(axis, radians);
5785
+ const deltaQ = new THREE2.Quaternion().setFromAxisAngle(axis, radians);
5562
5786
  compositeQ.multiply(deltaQ);
5563
5787
  }
5564
5788
  }
@@ -5567,7 +5791,7 @@ var _Loom3 = class _Loom3 {
5567
5791
  if (binding?.maxDegrees && binding.channel) {
5568
5792
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.roll) * binding.scale;
5569
5793
  const axis = getAxis(binding.channel);
5570
- const deltaQ = new THREE.Quaternion().setFromAxisAngle(axis, radians);
5794
+ const deltaQ = new THREE2.Quaternion().setFromAxisAngle(axis, radians);
5571
5795
  compositeQ.multiply(deltaQ);
5572
5796
  }
5573
5797
  }
@@ -6006,10 +6230,11 @@ function extractProfileOverrides(config) {
6006
6230
  if (key === "annotationRegions") {
6007
6231
  const topLevelAnnotationRegions = Array.isArray(topLevelConfig.annotationRegions) ? topLevelConfig.annotationRegions : void 0;
6008
6232
  const legacyAnnotationRegions = Array.isArray(legacyNestedOverrides.annotationRegions) ? legacyNestedOverrides.annotationRegions : void 0;
6009
- const presetOverrideRegions = mergeRegionsByName(legacyAnnotationRegions, topLevelAnnotationRegions);
6233
+ const legacyRegionFallback = Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6234
+ const legacyProfileRegions = mergeRegionsByName(legacyRegionFallback, legacyAnnotationRegions);
6010
6235
  const regions = mergeRegionsByName(
6011
- presetOverrideRegions,
6012
- Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0
6236
+ legacyProfileRegions,
6237
+ topLevelAnnotationRegions
6013
6238
  );
6014
6239
  if (regions) {
6015
6240
  overrides.annotationRegions = regions.map((region) => cloneRegion(region));
@@ -6025,6 +6250,13 @@ function extractProfileOverrides(config) {
6025
6250
  }
6026
6251
  return overrides;
6027
6252
  }
6253
+ function hasCanonicalAnnotationRegionOverrides(config) {
6254
+ const topLevelConfig = config;
6255
+ if (Array.isArray(topLevelConfig.annotationRegions)) {
6256
+ return true;
6257
+ }
6258
+ return isPlainObject2(config.profile) && Array.isArray(config.profile.annotationRegions);
6259
+ }
6028
6260
  function applyCharacterProfileToPreset(config) {
6029
6261
  const presetType = config.auPresetType;
6030
6262
  if (!presetType) {
@@ -6062,7 +6294,7 @@ function extendCharacterConfigWithPreset(config) {
6062
6294
  return config;
6063
6295
  }
6064
6296
  const presetRegions = extendedPresetProfile.annotationRegions;
6065
- const mergedRegions = mergeRegionsByName(presetRegions, config.regions);
6297
+ const mergedRegions = hasCanonicalAnnotationRegionOverrides(config) ? mergeRegionsByName(config.regions, presetRegions) : mergeRegionsByName(presetRegions, config.regions);
6066
6298
  const normalizedRegions = normalizeRegionTree(
6067
6299
  mergedRegions,
6068
6300
  profileOverrides.disabledRegions
@@ -6077,6 +6309,43 @@ function extendCharacterConfigWithPreset(config) {
6077
6309
  regions: extendedRegions ?? config.regions
6078
6310
  };
6079
6311
  }
6312
+ var DEFAULT_EPSILON = 1e-4;
6313
+ var DEFAULT_YAW_WEIGHT = 0.35;
6314
+ var DEFAULT_PITCH_WEIGHT = 0.2;
6315
+ var ZERO_OFFSET = { x: 0, y: 0 };
6316
+ function clampOffset(value) {
6317
+ return THREE2__namespace.MathUtils.clamp(value, -1, 1);
6318
+ }
6319
+ function toModelLocalDirection(model, worldDirection) {
6320
+ const localDirection = worldDirection.clone();
6321
+ model.updateWorldMatrix(true, false);
6322
+ const worldQuaternion = new THREE2__namespace.Quaternion();
6323
+ model.getWorldQuaternion(worldQuaternion);
6324
+ localDirection.applyQuaternion(worldQuaternion.invert());
6325
+ return localDirection.normalize();
6326
+ }
6327
+ function computeCameraRelativeGazeOffset(model, cameraPosition, targetPosition, options = {}) {
6328
+ if (!model) {
6329
+ return ZERO_OFFSET;
6330
+ }
6331
+ const epsilon = options.epsilon ?? DEFAULT_EPSILON;
6332
+ const yawWeight = options.yawWeight ?? DEFAULT_YAW_WEIGHT;
6333
+ const pitchWeight = options.pitchWeight ?? DEFAULT_PITCH_WEIGHT;
6334
+ const worldOffset = new THREE2__namespace.Vector3().subVectors(cameraPosition, targetPosition);
6335
+ if (worldOffset.lengthSq() < epsilon) {
6336
+ return ZERO_OFFSET;
6337
+ }
6338
+ const localDirection = toModelLocalDirection(model, worldOffset.normalize());
6339
+ const yawAngle = Math.atan2(localDirection.x, localDirection.z);
6340
+ const pitchAngle = Math.atan2(
6341
+ localDirection.y,
6342
+ Math.max(Math.hypot(localDirection.x, localDirection.z), epsilon)
6343
+ );
6344
+ return {
6345
+ x: clampOffset(yawAngle / (Math.PI / 2)) * yawWeight,
6346
+ y: clampOffset(pitchAngle / (Math.PI / 3)) * pitchWeight
6347
+ };
6348
+ }
6080
6349
  var DEFAULT_HEAD_BONE_NAMES = ["CC_Base_Head", "Head", "head", "Bip01_Head"];
6081
6350
  var DEFAULT_REFERENCE_HEIGHT = 1.8;
6082
6351
  var DEFAULT_FORWARD_OFFSET = 0.08;
@@ -6092,8 +6361,8 @@ function findFaceCenter(model, options = {}) {
6092
6361
  referenceHeight = DEFAULT_REFERENCE_HEIGHT
6093
6362
  } = options;
6094
6363
  const debugInfo = [];
6095
- const boundingBox = new THREE__namespace.Box3().setFromObject(model);
6096
- const modelSize = new THREE__namespace.Vector3();
6364
+ const boundingBox = new THREE2__namespace.Box3().setFromObject(model);
6365
+ const modelSize = new THREE2__namespace.Vector3();
6097
6366
  boundingBox.getSize(modelSize);
6098
6367
  const scale = modelSize.y / referenceHeight;
6099
6368
  debugInfo.push(`Model height: ${modelSize.y.toFixed(2)}m, scale factor: ${scale.toFixed(2)}`);
@@ -6104,7 +6373,7 @@ function findFaceCenter(model, options = {}) {
6104
6373
  const objName = obj.name.toLowerCase();
6105
6374
  for (const boneName of headBoneNames) {
6106
6375
  if (objName === boneName.toLowerCase() || objName.includes(boneName.toLowerCase())) {
6107
- headPos2 = new THREE__namespace.Vector3();
6376
+ headPos2 = new THREE2__namespace.Vector3();
6108
6377
  obj.getWorldPosition(headPos2);
6109
6378
  debugInfo.push(`Found head bone "${obj.name}" at (${headPos2.x.toFixed(3)}, ${headPos2.y.toFixed(3)}, ${headPos2.z.toFixed(3)})`);
6110
6379
  return;
@@ -6123,7 +6392,7 @@ function findFaceCenter(model, options = {}) {
6123
6392
  if (!eyes2.left) {
6124
6393
  for (const name of EYE_BONE_NAMES.left) {
6125
6394
  if (objName.includes(name)) {
6126
- eyes2.left = new THREE__namespace.Vector3();
6395
+ eyes2.left = new THREE2__namespace.Vector3();
6127
6396
  obj.getWorldPosition(eyes2.left);
6128
6397
  debugInfo.push(`Found left eye "${obj.name}" at (${eyes2.left.x.toFixed(3)}, ${eyes2.left.y.toFixed(3)}, ${eyes2.left.z.toFixed(3)})`);
6129
6398
  break;
@@ -6133,7 +6402,7 @@ function findFaceCenter(model, options = {}) {
6133
6402
  if (!eyes2.right) {
6134
6403
  for (const name of EYE_BONE_NAMES.right) {
6135
6404
  if (objName.includes(name)) {
6136
- eyes2.right = new THREE__namespace.Vector3();
6405
+ eyes2.right = new THREE2__namespace.Vector3();
6137
6406
  obj.getWorldPosition(eyes2.right);
6138
6407
  debugInfo.push(`Found right eye "${obj.name}" at (${eyes2.right.x.toFixed(3)}, ${eyes2.right.y.toFixed(3)}, ${eyes2.right.z.toFixed(3)})`);
6139
6408
  break;
@@ -6145,7 +6414,7 @@ function findFaceCenter(model, options = {}) {
6145
6414
  };
6146
6415
  if (faceMeshNames && faceMeshNames.length > 0) {
6147
6416
  debugInfo.push(`Looking for face meshes: ${faceMeshNames.join(", ")}`);
6148
- const meshBox = new THREE__namespace.Box3();
6417
+ const meshBox = new THREE2__namespace.Box3();
6149
6418
  let foundMesh = false;
6150
6419
  model.traverse((obj) => {
6151
6420
  const isMesh = obj.isMesh;
@@ -6153,7 +6422,7 @@ function findFaceCenter(model, options = {}) {
6153
6422
  for (const meshName of faceMeshNames) {
6154
6423
  if (obj.name === meshName || obj.name.includes(meshName)) {
6155
6424
  const mesh = obj;
6156
- const tempBox = new THREE__namespace.Box3().setFromObject(mesh);
6425
+ const tempBox = new THREE2__namespace.Box3().setFromObject(mesh);
6157
6426
  meshBox.union(tempBox);
6158
6427
  foundMesh = true;
6159
6428
  debugInfo.push(`Found mesh: "${obj.name}"`);
@@ -6162,11 +6431,11 @@ function findFaceCenter(model, options = {}) {
6162
6431
  }
6163
6432
  });
6164
6433
  if (foundMesh && !meshBox.isEmpty()) {
6165
- const meshBoundsSize = new THREE__namespace.Vector3();
6434
+ const meshBoundsSize = new THREE2__namespace.Vector3();
6166
6435
  meshBox.getSize(meshBoundsSize);
6167
6436
  const isFullBodyMesh = meshBoundsSize.y > modelSize.y * 0.7;
6168
6437
  if (!isFullBodyMesh) {
6169
- const meshCenter = new THREE__namespace.Vector3();
6438
+ const meshCenter = new THREE2__namespace.Vector3();
6170
6439
  meshBox.getCenter(meshCenter);
6171
6440
  debugInfo.push(`Using face mesh center: (${meshCenter.x.toFixed(3)}, ${meshCenter.y.toFixed(3)}, ${meshCenter.z.toFixed(3)})`);
6172
6441
  return {
@@ -6180,7 +6449,7 @@ function findFaceCenter(model, options = {}) {
6180
6449
  }
6181
6450
  const eyes = findEyes();
6182
6451
  if (eyes.left && eyes.right) {
6183
- const eyeCenter = new THREE__namespace.Vector3().addVectors(eyes.left, eyes.right).multiplyScalar(0.5);
6452
+ const eyeCenter = new THREE2__namespace.Vector3().addVectors(eyes.left, eyes.right).multiplyScalar(0.5);
6184
6453
  debugInfo.push(`Eye center (face position): (${eyeCenter.x.toFixed(3)}, ${eyeCenter.y.toFixed(3)}, ${eyeCenter.z.toFixed(3)})`);
6185
6454
  const headPos2 = findHeadBone();
6186
6455
  return {
@@ -6205,10 +6474,10 @@ function findFaceCenter(model, options = {}) {
6205
6474
  };
6206
6475
  }
6207
6476
  debugInfo.push("No head bone found, using model center fallback");
6208
- const modelCenter = new THREE__namespace.Vector3();
6477
+ const modelCenter = new THREE2__namespace.Vector3();
6209
6478
  boundingBox.getCenter(modelCenter);
6210
6479
  const headHeight = boundingBox.min.y + modelSize.y * 0.9;
6211
- const fallbackCenter = new THREE__namespace.Vector3(modelCenter.x, headHeight, modelCenter.z);
6480
+ const fallbackCenter = new THREE2__namespace.Vector3(modelCenter.x, headHeight, modelCenter.z);
6212
6481
  debugInfo.push(`Fallback center: (${fallbackCenter.x.toFixed(3)}, ${fallbackCenter.y.toFixed(3)}, ${fallbackCenter.z.toFixed(3)})`);
6213
6482
  return {
6214
6483
  center: fallbackCenter,
@@ -6217,7 +6486,7 @@ function findFaceCenter(model, options = {}) {
6217
6486
  };
6218
6487
  }
6219
6488
  function getModelForwardDirection(model) {
6220
- const forward = new THREE__namespace.Vector3(0, 0, 1);
6489
+ const forward = new THREE2__namespace.Vector3(0, 0, 1);
6221
6490
  forward.applyQuaternion(model.quaternion);
6222
6491
  return forward.normalize();
6223
6492
  }
@@ -6234,7 +6503,7 @@ function detectFacingDirection(model, eyeBoneNames = {
6234
6503
  if (!eyes.left) {
6235
6504
  for (const name of eyeBoneNames.left) {
6236
6505
  if (objName.includes(name)) {
6237
- eyes.left = new THREE__namespace.Vector3();
6506
+ eyes.left = new THREE2__namespace.Vector3();
6238
6507
  obj.getWorldPosition(eyes.left);
6239
6508
  break;
6240
6509
  }
@@ -6243,7 +6512,7 @@ function detectFacingDirection(model, eyeBoneNames = {
6243
6512
  if (!eyes.right) {
6244
6513
  for (const name of eyeBoneNames.right) {
6245
6514
  if (objName.includes(name)) {
6246
- eyes.right = new THREE__namespace.Vector3();
6515
+ eyes.right = new THREE2__namespace.Vector3();
6247
6516
  obj.getWorldPosition(eyes.right);
6248
6517
  break;
6249
6518
  }
@@ -7159,19 +7428,19 @@ function extractBones(root) {
7159
7428
  const bones = [];
7160
7429
  const boneDepths = /* @__PURE__ */ new Map();
7161
7430
  root.traverse((obj) => {
7162
- if (obj instanceof THREE__namespace.Bone || obj.type === "Bone") {
7163
- const worldPos = new THREE__namespace.Vector3();
7431
+ if (obj instanceof THREE2__namespace.Bone || obj.type === "Bone") {
7432
+ const worldPos = new THREE2__namespace.Vector3();
7164
7433
  obj.getWorldPosition(worldPos);
7165
7434
  let depth = 0;
7166
7435
  let parent = obj.parent;
7167
7436
  while (parent) {
7168
- if (parent instanceof THREE__namespace.Bone || parent.type === "Bone") {
7437
+ if (parent instanceof THREE2__namespace.Bone || parent.type === "Bone") {
7169
7438
  depth++;
7170
7439
  }
7171
7440
  parent = parent.parent;
7172
7441
  }
7173
7442
  boneDepths.set(obj.name, depth);
7174
- const parentBone = obj.parent instanceof THREE__namespace.Bone || obj.parent?.type === "Bone" ? obj.parent.name : null;
7443
+ const parentBone = obj.parent instanceof THREE2__namespace.Bone || obj.parent?.type === "Bone" ? obj.parent.name : null;
7175
7444
  bones.push({
7176
7445
  name: obj.name,
7177
7446
  parent: parentBone,
@@ -7424,6 +7693,7 @@ exports.VISEME_KEYS = VISEME_KEYS;
7424
7693
  exports.analyzeModel = analyzeModel;
7425
7694
  exports.applyCharacterProfileToPreset = applyCharacterProfileToPreset;
7426
7695
  exports.collectMorphMeshes = collectMorphMeshes;
7696
+ exports.computeCameraRelativeGazeOffset = computeCameraRelativeGazeOffset;
7427
7697
  exports.detectFacingDirection = detectFacingDirection;
7428
7698
  exports.extendCharacterConfigWithPreset = extendCharacterConfigWithPreset;
7429
7699
  exports.extendPresetWithProfile = extendPresetWithProfile;
@@ -7443,6 +7713,8 @@ exports.mergeCharacterRegionsByName = mergeRegionsByName;
7443
7713
  exports.resolveBoneName = resolveBoneName;
7444
7714
  exports.resolveBoneNames = resolveBoneNames;
7445
7715
  exports.resolveFaceCenter = resolveFaceCenter;
7716
+ exports.resolvePreset = resolvePreset;
7717
+ exports.resolvePresetWithOverrides = resolvePresetWithOverrides;
7446
7718
  exports.suggestBestPreset = suggestBestPreset;
7447
7719
  exports.validateMappingConfig = validateMappingConfig;
7448
7720
  exports.validateMappings = validateMappings;