@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE2 from 'three';
2
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;
@@ -270,6 +270,8 @@ var makeActionId = () => `act_${Math.random().toString(36).slice(2, 8)}_${Date.n
270
270
  var X_AXIS = new Vector3(1, 0, 0);
271
271
  var Y_AXIS = new Vector3(0, 1, 0);
272
272
  var Z_AXIS = new Vector3(0, 0, 1);
273
+ var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
274
+ var CLIP_EVENT_EPSILON = 1e-4;
273
275
  var BakedAnimationController = class {
274
276
  constructor(host) {
275
277
  __publicField(this, "host");
@@ -288,6 +290,7 @@ var BakedAnimationController = class {
288
290
  __publicField(this, "playbackState", /* @__PURE__ */ new Map());
289
291
  __publicField(this, "actionIds", /* @__PURE__ */ new WeakMap());
290
292
  __publicField(this, "actionIdToClip", /* @__PURE__ */ new Map());
293
+ __publicField(this, "clipMonitors", /* @__PURE__ */ new Map());
291
294
  this.host = host;
292
295
  }
293
296
  getActionId(action) {
@@ -301,6 +304,161 @@ var BakedAnimationController = class {
301
304
  action.__actionId = actionId;
302
305
  return actionId;
303
306
  }
307
+ setClipEventMetadata(clip, metadata) {
308
+ const userData = clip.userData ?? (clip.userData = {});
309
+ userData[CLIP_EVENT_METADATA_KEY] = metadata;
310
+ }
311
+ getClipEventMetadata(clip) {
312
+ const userData = clip.userData;
313
+ const keyframeTimes = Array.isArray(userData?.[CLIP_EVENT_METADATA_KEY]?.keyframeTimes) ? userData[CLIP_EVENT_METADATA_KEY].keyframeTimes.filter((time) => Number.isFinite(time)) : [];
314
+ return { keyframeTimes };
315
+ }
316
+ getKeyframeIndex(times, currentTime) {
317
+ if (!times.length) return -1;
318
+ const target = Math.max(0, currentTime) + 1e-3;
319
+ let lo = 0;
320
+ let hi = times.length - 1;
321
+ let idx = 0;
322
+ while (lo <= hi) {
323
+ const mid = lo + hi >>> 1;
324
+ if (times[mid] <= target) {
325
+ idx = mid;
326
+ lo = mid + 1;
327
+ } else {
328
+ hi = mid - 1;
329
+ }
330
+ }
331
+ return idx;
332
+ }
333
+ emitClipEvent(monitor, event) {
334
+ for (const listener of Array.from(monitor.listeners)) {
335
+ try {
336
+ listener(event);
337
+ } catch (error) {
338
+ console.error("[Loom3] clip event listener failed", error);
339
+ }
340
+ }
341
+ }
342
+ emitKeyframesForRange(monitor, startTime, endTime, direction, includeStart) {
343
+ if (!monitor.keyframeTimes.length) return;
344
+ const times = direction === 1 ? monitor.keyframeTimes : [...monitor.keyframeTimes].reverse();
345
+ for (const time of times) {
346
+ const matchesForward = direction === 1 && (includeStart ? time >= startTime - CLIP_EVENT_EPSILON : time > startTime + CLIP_EVENT_EPSILON) && time <= endTime + CLIP_EVENT_EPSILON;
347
+ const matchesReverse = direction === -1 && (includeStart ? time <= startTime + CLIP_EVENT_EPSILON : time < startTime - CLIP_EVENT_EPSILON) && time >= endTime - CLIP_EVENT_EPSILON;
348
+ if (!matchesForward && !matchesReverse) continue;
349
+ const keyframeIndex = monitor.keyframeTimes.indexOf(time);
350
+ monitor.lastKeyframeIndex = keyframeIndex;
351
+ this.emitClipEvent(monitor, {
352
+ type: "keyframe",
353
+ clipName: monitor.clipName,
354
+ keyframeIndex,
355
+ totalKeyframes: monitor.keyframeTimes.length,
356
+ currentTime: time,
357
+ duration: monitor.duration,
358
+ iteration: monitor.iteration
359
+ });
360
+ }
361
+ }
362
+ resetClipMonitor(monitor, currentTime) {
363
+ monitor.iteration = 0;
364
+ monitor.direction = monitor.initialDirection;
365
+ monitor.lastTime = currentTime;
366
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, currentTime);
367
+ monitor.finishedPending = false;
368
+ }
369
+ syncClipMonitorTime(monitor, currentTime, emitSeek = false) {
370
+ const clamped = Math.max(0, Math.min(monitor.duration, currentTime));
371
+ monitor.lastTime = clamped;
372
+ monitor.lastKeyframeIndex = this.getKeyframeIndex(monitor.keyframeTimes, clamped);
373
+ if (emitSeek) {
374
+ this.emitClipEvent(monitor, {
375
+ type: "seek",
376
+ clipName: monitor.clipName,
377
+ currentTime: clamped,
378
+ duration: monitor.duration,
379
+ iteration: monitor.iteration
380
+ });
381
+ }
382
+ }
383
+ cleanupClipMonitor(actionId) {
384
+ const monitor = this.clipMonitors.get(actionId);
385
+ if (!monitor || monitor.cleanedUp) return;
386
+ monitor.cleanedUp = true;
387
+ try {
388
+ monitor.action.paused = true;
389
+ } catch {
390
+ }
391
+ monitor.resolveFinished();
392
+ monitor.listeners.clear();
393
+ this.clipMonitors.delete(actionId);
394
+ this.actionIdToClip.delete(actionId);
395
+ }
396
+ advanceClipMonitor(monitor, previousTime) {
397
+ if (monitor.cleanedUp || monitor.action.paused && !monitor.finishedPending) return;
398
+ const currentTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
399
+ const delta = currentTime - previousTime;
400
+ if (monitor.loopMode === "pingpong") {
401
+ const movingForward = monitor.direction === 1;
402
+ const bouncedAtEnd = movingForward && delta < -CLIP_EVENT_EPSILON;
403
+ const bouncedAtStart = !movingForward && delta > CLIP_EVENT_EPSILON;
404
+ if (bouncedAtEnd) {
405
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
406
+ monitor.direction = -1;
407
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, false);
408
+ } else if (bouncedAtStart) {
409
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
410
+ monitor.direction = 1;
411
+ monitor.iteration += 1;
412
+ this.emitClipEvent(monitor, {
413
+ type: "loop",
414
+ clipName: monitor.clipName,
415
+ iteration: monitor.iteration,
416
+ currentTime: 0,
417
+ duration: monitor.duration
418
+ });
419
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, false);
420
+ } else if (delta > CLIP_EVENT_EPSILON) {
421
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
422
+ monitor.direction = 1;
423
+ } else if (delta < -CLIP_EVENT_EPSILON) {
424
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
425
+ monitor.direction = -1;
426
+ }
427
+ } else if (monitor.direction === 1) {
428
+ const wrapped = currentTime + CLIP_EVENT_EPSILON < previousTime;
429
+ if (wrapped) {
430
+ this.emitKeyframesForRange(monitor, previousTime, monitor.duration, 1, false);
431
+ monitor.iteration += 1;
432
+ this.emitClipEvent(monitor, {
433
+ type: "loop",
434
+ clipName: monitor.clipName,
435
+ iteration: monitor.iteration,
436
+ currentTime: 0,
437
+ duration: monitor.duration
438
+ });
439
+ this.emitKeyframesForRange(monitor, 0, currentTime, 1, true);
440
+ } else if (delta > CLIP_EVENT_EPSILON) {
441
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, 1, false);
442
+ }
443
+ } else {
444
+ const wrapped = currentTime > previousTime + CLIP_EVENT_EPSILON;
445
+ if (wrapped) {
446
+ this.emitKeyframesForRange(monitor, previousTime, 0, -1, false);
447
+ monitor.iteration += 1;
448
+ this.emitClipEvent(monitor, {
449
+ type: "loop",
450
+ clipName: monitor.clipName,
451
+ iteration: monitor.iteration,
452
+ currentTime: monitor.duration,
453
+ duration: monitor.duration
454
+ });
455
+ this.emitKeyframesForRange(monitor, monitor.duration, currentTime, -1, true);
456
+ } else if (delta < -CLIP_EVENT_EPSILON) {
457
+ this.emitKeyframesForRange(monitor, previousTime, currentTime, -1, false);
458
+ }
459
+ }
460
+ this.syncClipMonitorTime(monitor, currentTime);
461
+ }
304
462
  normalizePlaybackOptions(options, defaults) {
305
463
  const clipOptions = options;
306
464
  const rawRate = options?.playbackRate ?? options?.speed ?? 1;
@@ -512,7 +670,28 @@ var BakedAnimationController = class {
512
670
  }
513
671
  update(dtSeconds) {
514
672
  if (this.animationMixer) {
673
+ const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
674
+ actionId: monitor.actionId,
675
+ previousTime: monitor.action.time
676
+ }));
515
677
  this.animationMixer.update(dtSeconds);
678
+ for (const { actionId, previousTime } of snapshots) {
679
+ const monitor = this.clipMonitors.get(actionId);
680
+ if (!monitor) continue;
681
+ this.advanceClipMonitor(monitor, previousTime);
682
+ if (monitor.finishedPending) {
683
+ const finalTime = Math.max(0, Math.min(monitor.duration, monitor.action.time));
684
+ this.syncClipMonitorTime(monitor, finalTime);
685
+ this.emitClipEvent(monitor, {
686
+ type: "completed",
687
+ clipName: monitor.clipName,
688
+ currentTime: finalTime,
689
+ duration: monitor.duration,
690
+ iteration: monitor.iteration
691
+ });
692
+ this.cleanupClipMonitor(actionId);
693
+ }
694
+ }
516
695
  }
517
696
  }
518
697
  dispose() {
@@ -532,6 +711,7 @@ var BakedAnimationController = class {
532
711
  this.clipHandles.clear();
533
712
  this.clipSources.clear();
534
713
  this.playbackState.clear();
714
+ this.clipMonitors.clear();
535
715
  }
536
716
  loadAnimationClips(clips) {
537
717
  const model = this.host.getModel();
@@ -670,6 +850,7 @@ var BakedAnimationController = class {
670
850
  }
671
851
  const action = this.animationActions.get(clipName);
672
852
  if (action) {
853
+ const actionId = this.getActionId(action);
673
854
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
674
855
  action.stop();
675
856
  if (!isBaked && this.animationMixer) {
@@ -692,9 +873,11 @@ var BakedAnimationController = class {
692
873
  }
693
874
  }
694
875
  this.animationFinishedCallbacks.delete(clipName);
876
+ if (actionId) this.cleanupClipMonitor(actionId);
695
877
  }
696
878
  const clipAction = this.clipActions.get(clipName);
697
879
  if (clipAction && clipAction !== action) {
880
+ const actionId = this.getActionId(clipAction);
698
881
  try {
699
882
  clipAction.stop();
700
883
  if (this.animationMixer) {
@@ -707,6 +890,7 @@ var BakedAnimationController = class {
707
890
  } catch {
708
891
  }
709
892
  this.clipActions.delete(clipName);
893
+ if (actionId) this.cleanupClipMonitor(actionId);
710
894
  }
711
895
  if (this.clipActions.get(clipName) === action) {
712
896
  this.clipActions.delete(clipName);
@@ -1268,6 +1452,7 @@ var BakedAnimationController = class {
1268
1452
  return null;
1269
1453
  }
1270
1454
  const clip = new AnimationClip(clipName, maxTime, tracks);
1455
+ this.setClipEventMetadata(clip, { keyframeTimes });
1271
1456
  console.log(`[Loom3] snippetToClip: Created clip "${clipName}" with ${tracks.length} tracks, duration ${maxTime.toFixed(2)}s`);
1272
1457
  return clip;
1273
1458
  }
@@ -1299,28 +1484,38 @@ var BakedAnimationController = class {
1299
1484
  this.animationClips.push(clip);
1300
1485
  }
1301
1486
  this.applyPlaybackState(action, playbackState);
1487
+ if (actionId) {
1488
+ this.cleanupClipMonitor(actionId);
1489
+ }
1302
1490
  let resolveFinished;
1303
1491
  const finishedPromise = new Promise((resolve) => {
1304
1492
  resolveFinished = resolve;
1305
1493
  });
1306
- const cleanup = () => {
1307
- try {
1308
- this.animationFinishedCallbacks.delete(clip.name);
1309
- } catch {
1310
- }
1311
- try {
1312
- action.paused = true;
1313
- } catch {
1314
- }
1494
+ const keyframeTimes = this.getClipEventMetadata(clip).keyframeTimes;
1495
+ const initialDirection = playbackState.reverse ? -1 : 1;
1496
+ const monitor = {
1497
+ action,
1498
+ actionId,
1499
+ clip,
1500
+ clipName: clip.name,
1501
+ duration: clip.duration,
1502
+ keyframeTimes,
1503
+ listeners: /* @__PURE__ */ new Set(),
1504
+ initialDirection,
1505
+ direction: initialDirection,
1506
+ iteration: 0,
1507
+ lastTime: Math.max(0, Math.min(clip.duration, action.time)),
1508
+ lastKeyframeIndex: this.getKeyframeIndex(keyframeTimes, action.time),
1509
+ loopMode: playbackState.loopMode,
1510
+ finishedPending: false,
1511
+ cleanedUp: false,
1512
+ resolveFinished
1315
1513
  };
1316
- this.animationFinishedCallbacks.set(clip.name, () => {
1317
- resolveFinished();
1318
- cleanup();
1319
- });
1320
- finishedPromise.catch(() => cleanup());
1514
+ this.clipMonitors.set(actionId, monitor);
1321
1515
  action.reset();
1322
1516
  action.time = startTime;
1323
1517
  action.play();
1518
+ this.resetClipMonitor(monitor, action.time);
1324
1519
  this.clipActions.set(clip.name, action);
1325
1520
  this.animationActions.set(clip.name, action);
1326
1521
  this.setPlaybackState(clip.name, playbackState);
@@ -1338,6 +1533,7 @@ var BakedAnimationController = class {
1338
1533
  })
1339
1534
  );
1340
1535
  action.play();
1536
+ this.resetClipMonitor(monitor, action.time);
1341
1537
  },
1342
1538
  stop: () => {
1343
1539
  action.stop();
@@ -1355,8 +1551,7 @@ var BakedAnimationController = class {
1355
1551
  this.animationActions.delete(clip.name);
1356
1552
  this.animationFinishedCallbacks.delete(clip.name);
1357
1553
  this.playbackState.delete(clip.name);
1358
- resolveFinished();
1359
- cleanup();
1554
+ this.cleanupClipMonitor(actionId);
1360
1555
  },
1361
1556
  pause: () => {
1362
1557
  action.paused = true;
@@ -1374,6 +1569,8 @@ var BakedAnimationController = class {
1374
1569
  const next = this.playbackState.get(clip.name) ?? playbackState;
1375
1570
  next.playbackRate = Number.isFinite(r) ? Math.max(0, Math.abs(r)) : 1;
1376
1571
  this.applyPlaybackState(action, next);
1572
+ monitor.direction = next.reverse ? -1 : 1;
1573
+ monitor.initialDirection = monitor.direction;
1377
1574
  this.setPlaybackState(clip.name, next);
1378
1575
  },
1379
1576
  setLoop: (mode, repeatCount) => {
@@ -1382,6 +1579,7 @@ var BakedAnimationController = class {
1382
1579
  next.loop = mode !== "once";
1383
1580
  next.repeatCount = repeatCount;
1384
1581
  this.applyPlaybackState(action, next);
1582
+ monitor.loopMode = mode;
1385
1583
  this.setPlaybackState(clip.name, next);
1386
1584
  },
1387
1585
  setTime: (t) => {
@@ -1391,9 +1589,16 @@ var BakedAnimationController = class {
1391
1589
  this.animationMixer?.update(0);
1392
1590
  } catch {
1393
1591
  }
1592
+ this.syncClipMonitorTime(monitor, clamped, true);
1394
1593
  },
1395
1594
  getTime: () => action.time,
1396
1595
  getDuration: () => clip.duration,
1596
+ subscribe: (listener) => {
1597
+ monitor.listeners.add(listener);
1598
+ return () => {
1599
+ monitor.listeners.delete(listener);
1600
+ };
1601
+ },
1397
1602
  finished: finishedPromise
1398
1603
  };
1399
1604
  this.clipHandles.set(clip.name, handle);
@@ -1417,6 +1622,7 @@ var BakedAnimationController = class {
1417
1622
  if (!this.animationMixer || !this.host.getModel()) return;
1418
1623
  for (const [clipName, action] of Array.from(this.clipActions.entries())) {
1419
1624
  if (clipName === name || clipName.startsWith(`${name}_`)) {
1625
+ const actionId = this.getActionId(action);
1420
1626
  try {
1421
1627
  action.stop();
1422
1628
  const clip = action.getClip();
@@ -1431,6 +1637,7 @@ var BakedAnimationController = class {
1431
1637
  this.clipHandles.delete(clipName);
1432
1638
  this.animationFinishedCallbacks.delete(clipName);
1433
1639
  this.playbackState.delete(clipName);
1640
+ if (actionId) this.cleanupClipMonitor(actionId);
1434
1641
  }
1435
1642
  }
1436
1643
  }
@@ -1454,6 +1661,8 @@ var BakedAnimationController = class {
1454
1661
  console.log("[Loom3] updateClipParams start", debugSnapshot());
1455
1662
  const apply = (action) => {
1456
1663
  if (!action) return;
1664
+ const actionId = this.getActionId(action);
1665
+ const monitor = actionId ? this.clipMonitors.get(actionId) : void 0;
1457
1666
  const clipName = action.getClip().name;
1458
1667
  const next = this.playbackState.get(clipName) ?? this.normalizePlaybackOptions(void 0, { loop: false, source: this.clipSources.get(clipName) ?? "clip" });
1459
1668
  try {
@@ -1472,6 +1681,10 @@ var BakedAnimationController = class {
1472
1681
  }
1473
1682
  const signedRate = next.reverse ? -next.playbackRate : next.playbackRate;
1474
1683
  action.setEffectiveTimeScale(signedRate);
1684
+ if (monitor) {
1685
+ monitor.direction = next.reverse ? -1 : 1;
1686
+ monitor.initialDirection = monitor.direction;
1687
+ }
1475
1688
  updated = true;
1476
1689
  }
1477
1690
  if (typeof params.loop === "boolean" || params.loopMode || params.repeatCount !== void 0) {
@@ -1479,6 +1692,7 @@ var BakedAnimationController = class {
1479
1692
  next.loop = next.loopMode !== "once";
1480
1693
  next.repeatCount = params.repeatCount;
1481
1694
  this.applyPlaybackState(action, next);
1695
+ if (monitor) monitor.loopMode = next.loopMode;
1482
1696
  updated = true;
1483
1697
  }
1484
1698
  this.setPlaybackState(clipName, next);
@@ -1558,6 +1772,14 @@ var BakedAnimationController = class {
1558
1772
  if (this.animationMixer && !this.mixerFinishedListenerAttached) {
1559
1773
  this.animationMixer.addEventListener("finished", (event) => {
1560
1774
  const action = event.action;
1775
+ const actionId = this.getActionId(action);
1776
+ if (actionId) {
1777
+ const monitor = this.clipMonitors.get(actionId);
1778
+ if (monitor) {
1779
+ monitor.finishedPending = true;
1780
+ return;
1781
+ }
1782
+ }
1561
1783
  const clip = action.getClip();
1562
1784
  const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1563
1785
  if (bakedRuntime) {
@@ -4264,9 +4486,11 @@ function getPreset(presetType) {
4264
4486
  return CC4_PRESET;
4265
4487
  }
4266
4488
  }
4489
+ var resolvePreset = getPreset;
4267
4490
  function getPresetWithProfile(presetType, profile) {
4268
4491
  return extendPresetWithProfile(getPreset(presetType), profile);
4269
4492
  }
4493
+ var resolvePresetWithOverrides = getPresetWithProfile;
4270
4494
 
4271
4495
  // src/engines/three/Loom3.ts
4272
4496
  var deg2rad = (d) => d * Math.PI / 180;
@@ -5985,10 +6209,11 @@ function extractProfileOverrides(config) {
5985
6209
  if (key === "annotationRegions") {
5986
6210
  const topLevelAnnotationRegions = Array.isArray(topLevelConfig.annotationRegions) ? topLevelConfig.annotationRegions : void 0;
5987
6211
  const legacyAnnotationRegions = Array.isArray(legacyNestedOverrides.annotationRegions) ? legacyNestedOverrides.annotationRegions : void 0;
5988
- const presetOverrideRegions = mergeRegionsByName(legacyAnnotationRegions, topLevelAnnotationRegions);
6212
+ const legacyRegionFallback = Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6213
+ const legacyProfileRegions = mergeRegionsByName(legacyRegionFallback, legacyAnnotationRegions);
5989
6214
  const regions = mergeRegionsByName(
5990
- presetOverrideRegions,
5991
- Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0
6215
+ legacyProfileRegions,
6216
+ topLevelAnnotationRegions
5992
6217
  );
5993
6218
  if (regions) {
5994
6219
  overrides.annotationRegions = regions.map((region) => cloneRegion(region));
@@ -6004,6 +6229,13 @@ function extractProfileOverrides(config) {
6004
6229
  }
6005
6230
  return overrides;
6006
6231
  }
6232
+ function hasCanonicalAnnotationRegionOverrides(config) {
6233
+ const topLevelConfig = config;
6234
+ if (Array.isArray(topLevelConfig.annotationRegions)) {
6235
+ return true;
6236
+ }
6237
+ return isPlainObject2(config.profile) && Array.isArray(config.profile.annotationRegions);
6238
+ }
6007
6239
  function applyCharacterProfileToPreset(config) {
6008
6240
  const presetType = config.auPresetType;
6009
6241
  if (!presetType) {
@@ -6041,7 +6273,7 @@ function extendCharacterConfigWithPreset(config) {
6041
6273
  return config;
6042
6274
  }
6043
6275
  const presetRegions = extendedPresetProfile.annotationRegions;
6044
- const mergedRegions = mergeRegionsByName(presetRegions, config.regions);
6276
+ const mergedRegions = hasCanonicalAnnotationRegionOverrides(config) ? mergeRegionsByName(config.regions, presetRegions) : mergeRegionsByName(presetRegions, config.regions);
6045
6277
  const normalizedRegions = normalizeRegionTree(
6046
6278
  mergedRegions,
6047
6279
  profileOverrides.disabledRegions
@@ -6056,6 +6288,43 @@ function extendCharacterConfigWithPreset(config) {
6056
6288
  regions: extendedRegions ?? config.regions
6057
6289
  };
6058
6290
  }
6291
+ var DEFAULT_EPSILON = 1e-4;
6292
+ var DEFAULT_YAW_WEIGHT = 0.35;
6293
+ var DEFAULT_PITCH_WEIGHT = 0.2;
6294
+ var ZERO_OFFSET = { x: 0, y: 0 };
6295
+ function clampOffset(value) {
6296
+ return THREE2.MathUtils.clamp(value, -1, 1);
6297
+ }
6298
+ function toModelLocalDirection(model, worldDirection) {
6299
+ const localDirection = worldDirection.clone();
6300
+ model.updateWorldMatrix(true, false);
6301
+ const worldQuaternion = new THREE2.Quaternion();
6302
+ model.getWorldQuaternion(worldQuaternion);
6303
+ localDirection.applyQuaternion(worldQuaternion.invert());
6304
+ return localDirection.normalize();
6305
+ }
6306
+ function computeCameraRelativeGazeOffset(model, cameraPosition, targetPosition, options = {}) {
6307
+ if (!model) {
6308
+ return ZERO_OFFSET;
6309
+ }
6310
+ const epsilon = options.epsilon ?? DEFAULT_EPSILON;
6311
+ const yawWeight = options.yawWeight ?? DEFAULT_YAW_WEIGHT;
6312
+ const pitchWeight = options.pitchWeight ?? DEFAULT_PITCH_WEIGHT;
6313
+ const worldOffset = new THREE2.Vector3().subVectors(cameraPosition, targetPosition);
6314
+ if (worldOffset.lengthSq() < epsilon) {
6315
+ return ZERO_OFFSET;
6316
+ }
6317
+ const localDirection = toModelLocalDirection(model, worldOffset.normalize());
6318
+ const yawAngle = Math.atan2(localDirection.x, localDirection.z);
6319
+ const pitchAngle = Math.atan2(
6320
+ localDirection.y,
6321
+ Math.max(Math.hypot(localDirection.x, localDirection.z), epsilon)
6322
+ );
6323
+ return {
6324
+ x: clampOffset(yawAngle / (Math.PI / 2)) * yawWeight,
6325
+ y: clampOffset(pitchAngle / (Math.PI / 3)) * pitchWeight
6326
+ };
6327
+ }
6059
6328
  var DEFAULT_HEAD_BONE_NAMES = ["CC_Base_Head", "Head", "head", "Bip01_Head"];
6060
6329
  var DEFAULT_REFERENCE_HEIGHT = 1.8;
6061
6330
  var DEFAULT_FORWARD_OFFSET = 0.08;
@@ -6071,8 +6340,8 @@ function findFaceCenter(model, options = {}) {
6071
6340
  referenceHeight = DEFAULT_REFERENCE_HEIGHT
6072
6341
  } = options;
6073
6342
  const debugInfo = [];
6074
- const boundingBox = new THREE.Box3().setFromObject(model);
6075
- const modelSize = new THREE.Vector3();
6343
+ const boundingBox = new THREE2.Box3().setFromObject(model);
6344
+ const modelSize = new THREE2.Vector3();
6076
6345
  boundingBox.getSize(modelSize);
6077
6346
  const scale = modelSize.y / referenceHeight;
6078
6347
  debugInfo.push(`Model height: ${modelSize.y.toFixed(2)}m, scale factor: ${scale.toFixed(2)}`);
@@ -6083,7 +6352,7 @@ function findFaceCenter(model, options = {}) {
6083
6352
  const objName = obj.name.toLowerCase();
6084
6353
  for (const boneName of headBoneNames) {
6085
6354
  if (objName === boneName.toLowerCase() || objName.includes(boneName.toLowerCase())) {
6086
- headPos2 = new THREE.Vector3();
6355
+ headPos2 = new THREE2.Vector3();
6087
6356
  obj.getWorldPosition(headPos2);
6088
6357
  debugInfo.push(`Found head bone "${obj.name}" at (${headPos2.x.toFixed(3)}, ${headPos2.y.toFixed(3)}, ${headPos2.z.toFixed(3)})`);
6089
6358
  return;
@@ -6102,7 +6371,7 @@ function findFaceCenter(model, options = {}) {
6102
6371
  if (!eyes2.left) {
6103
6372
  for (const name of EYE_BONE_NAMES.left) {
6104
6373
  if (objName.includes(name)) {
6105
- eyes2.left = new THREE.Vector3();
6374
+ eyes2.left = new THREE2.Vector3();
6106
6375
  obj.getWorldPosition(eyes2.left);
6107
6376
  debugInfo.push(`Found left eye "${obj.name}" at (${eyes2.left.x.toFixed(3)}, ${eyes2.left.y.toFixed(3)}, ${eyes2.left.z.toFixed(3)})`);
6108
6377
  break;
@@ -6112,7 +6381,7 @@ function findFaceCenter(model, options = {}) {
6112
6381
  if (!eyes2.right) {
6113
6382
  for (const name of EYE_BONE_NAMES.right) {
6114
6383
  if (objName.includes(name)) {
6115
- eyes2.right = new THREE.Vector3();
6384
+ eyes2.right = new THREE2.Vector3();
6116
6385
  obj.getWorldPosition(eyes2.right);
6117
6386
  debugInfo.push(`Found right eye "${obj.name}" at (${eyes2.right.x.toFixed(3)}, ${eyes2.right.y.toFixed(3)}, ${eyes2.right.z.toFixed(3)})`);
6118
6387
  break;
@@ -6124,7 +6393,7 @@ function findFaceCenter(model, options = {}) {
6124
6393
  };
6125
6394
  if (faceMeshNames && faceMeshNames.length > 0) {
6126
6395
  debugInfo.push(`Looking for face meshes: ${faceMeshNames.join(", ")}`);
6127
- const meshBox = new THREE.Box3();
6396
+ const meshBox = new THREE2.Box3();
6128
6397
  let foundMesh = false;
6129
6398
  model.traverse((obj) => {
6130
6399
  const isMesh = obj.isMesh;
@@ -6132,7 +6401,7 @@ function findFaceCenter(model, options = {}) {
6132
6401
  for (const meshName of faceMeshNames) {
6133
6402
  if (obj.name === meshName || obj.name.includes(meshName)) {
6134
6403
  const mesh = obj;
6135
- const tempBox = new THREE.Box3().setFromObject(mesh);
6404
+ const tempBox = new THREE2.Box3().setFromObject(mesh);
6136
6405
  meshBox.union(tempBox);
6137
6406
  foundMesh = true;
6138
6407
  debugInfo.push(`Found mesh: "${obj.name}"`);
@@ -6141,11 +6410,11 @@ function findFaceCenter(model, options = {}) {
6141
6410
  }
6142
6411
  });
6143
6412
  if (foundMesh && !meshBox.isEmpty()) {
6144
- const meshBoundsSize = new THREE.Vector3();
6413
+ const meshBoundsSize = new THREE2.Vector3();
6145
6414
  meshBox.getSize(meshBoundsSize);
6146
6415
  const isFullBodyMesh = meshBoundsSize.y > modelSize.y * 0.7;
6147
6416
  if (!isFullBodyMesh) {
6148
- const meshCenter = new THREE.Vector3();
6417
+ const meshCenter = new THREE2.Vector3();
6149
6418
  meshBox.getCenter(meshCenter);
6150
6419
  debugInfo.push(`Using face mesh center: (${meshCenter.x.toFixed(3)}, ${meshCenter.y.toFixed(3)}, ${meshCenter.z.toFixed(3)})`);
6151
6420
  return {
@@ -6159,7 +6428,7 @@ function findFaceCenter(model, options = {}) {
6159
6428
  }
6160
6429
  const eyes = findEyes();
6161
6430
  if (eyes.left && eyes.right) {
6162
- const eyeCenter = new THREE.Vector3().addVectors(eyes.left, eyes.right).multiplyScalar(0.5);
6431
+ const eyeCenter = new THREE2.Vector3().addVectors(eyes.left, eyes.right).multiplyScalar(0.5);
6163
6432
  debugInfo.push(`Eye center (face position): (${eyeCenter.x.toFixed(3)}, ${eyeCenter.y.toFixed(3)}, ${eyeCenter.z.toFixed(3)})`);
6164
6433
  const headPos2 = findHeadBone();
6165
6434
  return {
@@ -6184,10 +6453,10 @@ function findFaceCenter(model, options = {}) {
6184
6453
  };
6185
6454
  }
6186
6455
  debugInfo.push("No head bone found, using model center fallback");
6187
- const modelCenter = new THREE.Vector3();
6456
+ const modelCenter = new THREE2.Vector3();
6188
6457
  boundingBox.getCenter(modelCenter);
6189
6458
  const headHeight = boundingBox.min.y + modelSize.y * 0.9;
6190
- const fallbackCenter = new THREE.Vector3(modelCenter.x, headHeight, modelCenter.z);
6459
+ const fallbackCenter = new THREE2.Vector3(modelCenter.x, headHeight, modelCenter.z);
6191
6460
  debugInfo.push(`Fallback center: (${fallbackCenter.x.toFixed(3)}, ${fallbackCenter.y.toFixed(3)}, ${fallbackCenter.z.toFixed(3)})`);
6192
6461
  return {
6193
6462
  center: fallbackCenter,
@@ -6196,7 +6465,7 @@ function findFaceCenter(model, options = {}) {
6196
6465
  };
6197
6466
  }
6198
6467
  function getModelForwardDirection(model) {
6199
- const forward = new THREE.Vector3(0, 0, 1);
6468
+ const forward = new THREE2.Vector3(0, 0, 1);
6200
6469
  forward.applyQuaternion(model.quaternion);
6201
6470
  return forward.normalize();
6202
6471
  }
@@ -6213,7 +6482,7 @@ function detectFacingDirection(model, eyeBoneNames = {
6213
6482
  if (!eyes.left) {
6214
6483
  for (const name of eyeBoneNames.left) {
6215
6484
  if (objName.includes(name)) {
6216
- eyes.left = new THREE.Vector3();
6485
+ eyes.left = new THREE2.Vector3();
6217
6486
  obj.getWorldPosition(eyes.left);
6218
6487
  break;
6219
6488
  }
@@ -6222,7 +6491,7 @@ function detectFacingDirection(model, eyeBoneNames = {
6222
6491
  if (!eyes.right) {
6223
6492
  for (const name of eyeBoneNames.right) {
6224
6493
  if (objName.includes(name)) {
6225
- eyes.right = new THREE.Vector3();
6494
+ eyes.right = new THREE2.Vector3();
6226
6495
  obj.getWorldPosition(eyes.right);
6227
6496
  break;
6228
6497
  }
@@ -7138,19 +7407,19 @@ function extractBones(root) {
7138
7407
  const bones = [];
7139
7408
  const boneDepths = /* @__PURE__ */ new Map();
7140
7409
  root.traverse((obj) => {
7141
- if (obj instanceof THREE.Bone || obj.type === "Bone") {
7142
- const worldPos = new THREE.Vector3();
7410
+ if (obj instanceof THREE2.Bone || obj.type === "Bone") {
7411
+ const worldPos = new THREE2.Vector3();
7143
7412
  obj.getWorldPosition(worldPos);
7144
7413
  let depth = 0;
7145
7414
  let parent = obj.parent;
7146
7415
  while (parent) {
7147
- if (parent instanceof THREE.Bone || parent.type === "Bone") {
7416
+ if (parent instanceof THREE2.Bone || parent.type === "Bone") {
7148
7417
  depth++;
7149
7418
  }
7150
7419
  parent = parent.parent;
7151
7420
  }
7152
7421
  boneDepths.set(obj.name, depth);
7153
- const parentBone = obj.parent instanceof THREE.Bone || obj.parent?.type === "Bone" ? obj.parent.name : null;
7422
+ const parentBone = obj.parent instanceof THREE2.Bone || obj.parent?.type === "Bone" ? obj.parent.name : null;
7154
7423
  bones.push({
7155
7424
  name: obj.name,
7156
7425
  parent: parentBone,
@@ -7374,6 +7643,6 @@ async function analyzeModel(options) {
7374
7643
  };
7375
7644
  }
7376
7645
 
7377
- export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, suggestBestPreset, validateMappingConfig, validateMappings };
7646
+ export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, computeCameraRelativeGazeOffset, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, suggestBestPreset, validateMappingConfig, validateMappings };
7378
7647
  //# sourceMappingURL=index.js.map
7379
7648
  //# sourceMappingURL=index.js.map