@lovelace_lol/loom3 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +241 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -33
- package/dist/index.d.ts +35 -33
- package/dist/index.js +241 -154
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -26,11 +26,53 @@ 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;
|
|
27
27
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
28
28
|
|
|
29
|
+
// src/core/compositeAxis.ts
|
|
30
|
+
function toAUList(value) {
|
|
31
|
+
if (value === void 0) return [];
|
|
32
|
+
return Array.isArray(value) ? value : [value];
|
|
33
|
+
}
|
|
34
|
+
function findBoneBindingForNode(auToBones, auId, nodeKey) {
|
|
35
|
+
return auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
|
|
36
|
+
}
|
|
37
|
+
function getCompositeAxisValue(axisConfig, getValue) {
|
|
38
|
+
if (!axisConfig) return 0;
|
|
39
|
+
const negativeAUs = toAUList(axisConfig.negative);
|
|
40
|
+
const positiveAUs = toAUList(axisConfig.positive);
|
|
41
|
+
if (negativeAUs.length > 0 && positiveAUs.length > 0) {
|
|
42
|
+
const negativeValue = Math.max(...negativeAUs.map(getValue), 0);
|
|
43
|
+
const positiveValue = Math.max(...positiveAUs.map(getValue), 0);
|
|
44
|
+
return positiveValue - negativeValue;
|
|
45
|
+
}
|
|
46
|
+
if (axisConfig.aus.length > 1) {
|
|
47
|
+
return Math.max(...axisConfig.aus.map(getValue), 0);
|
|
48
|
+
}
|
|
49
|
+
return getValue(axisConfig.aus[0]);
|
|
50
|
+
}
|
|
51
|
+
function getCompositeAxisBinding(nodeKey, axisConfig, direction, getValue, auToBones) {
|
|
52
|
+
if (!axisConfig) return null;
|
|
53
|
+
const directionalAUs = direction < 0 ? toAUList(axisConfig.negative) : toAUList(axisConfig.positive);
|
|
54
|
+
const candidates = directionalAUs.length > 0 ? directionalAUs : axisConfig.aus;
|
|
55
|
+
const ranked = [...candidates].sort((a, b) => getValue(b) - getValue(a));
|
|
56
|
+
for (const auId of ranked) {
|
|
57
|
+
const binding = findBoneBindingForNode(auToBones, auId, nodeKey);
|
|
58
|
+
if (binding) return binding;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
// src/engines/three/balanceUtils.ts
|
|
30
64
|
function clampBalance(value) {
|
|
31
65
|
if (!Number.isFinite(value)) return 0;
|
|
32
66
|
return Math.max(-1, Math.min(1, value));
|
|
33
67
|
}
|
|
68
|
+
function getSideScale(balance, side) {
|
|
69
|
+
if (side !== "left" && side !== "right") return 1;
|
|
70
|
+
const clamped = clampBalance(balance);
|
|
71
|
+
if (side === "left") {
|
|
72
|
+
return clamped > 0 ? 1 - clamped : 1;
|
|
73
|
+
}
|
|
74
|
+
return clamped < 0 ? 1 + clamped : 1;
|
|
75
|
+
}
|
|
34
76
|
function resolveCurveBalance(curveId, globalBalance, balanceMap) {
|
|
35
77
|
if (balanceMap && Object.prototype.hasOwnProperty.call(balanceMap, curveId)) {
|
|
36
78
|
return clampBalance(Number(balanceMap[curveId]));
|
|
@@ -158,8 +200,22 @@ var BakedAnimationController = class {
|
|
|
158
200
|
return this.host.getMeshNamesForAU(auId) || [];
|
|
159
201
|
}
|
|
160
202
|
const facePart = config.auInfo?.[String(auId)]?.facePart;
|
|
161
|
-
if (facePart
|
|
162
|
-
|
|
203
|
+
if (facePart) {
|
|
204
|
+
const category = config.auFacePartToMeshCategory?.[facePart];
|
|
205
|
+
if (category) return config.morphToMesh?.[category] || [];
|
|
206
|
+
}
|
|
207
|
+
return config.morphToMesh?.face || [];
|
|
208
|
+
}
|
|
209
|
+
getMeshNamesForViseme(config, explicitMeshNames) {
|
|
210
|
+
if (explicitMeshNames && explicitMeshNames.length > 0) {
|
|
211
|
+
return explicitMeshNames;
|
|
212
|
+
}
|
|
213
|
+
if (typeof this.host.getMeshNamesForViseme === "function") {
|
|
214
|
+
return this.host.getMeshNamesForViseme() || [];
|
|
215
|
+
}
|
|
216
|
+
const category = config.visemeMeshCategory || (config.morphToMesh?.viseme ? "viseme" : "face");
|
|
217
|
+
const visemeMeshes = config.morphToMesh?.[category];
|
|
218
|
+
if (visemeMeshes && visemeMeshes.length > 0) return visemeMeshes;
|
|
163
219
|
return config.morphToMesh?.face || [];
|
|
164
220
|
}
|
|
165
221
|
update(dtSeconds) {
|
|
@@ -455,11 +511,12 @@ var BakedAnimationController = class {
|
|
|
455
511
|
if (isNumericAU(curveId)) {
|
|
456
512
|
const auId = Number(curveId);
|
|
457
513
|
if (isVisemeIndex(curveId)) {
|
|
514
|
+
const visemeMeshNames = this.getMeshNamesForViseme(config, meshNames);
|
|
458
515
|
const visemeKey = config.visemeKeys[auId];
|
|
459
516
|
if (typeof visemeKey === "number") {
|
|
460
|
-
this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale,
|
|
517
|
+
this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
|
|
461
518
|
} else if (visemeKey) {
|
|
462
|
-
this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale,
|
|
519
|
+
this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
|
|
463
520
|
}
|
|
464
521
|
} else {
|
|
465
522
|
const auMeshNames = this.getMeshNamesForAU(auId, config, meshNames);
|
|
@@ -539,43 +596,23 @@ var BakedAnimationController = class {
|
|
|
539
596
|
Object.keys(curves).filter(isNumericAU).map((id) => Number(id))
|
|
540
597
|
);
|
|
541
598
|
const getAxisBinding = (nodeKey, axisConfig, axisValue, t) => {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
let maxVal = sampleCurve(String(maxAU), t);
|
|
550
|
-
for (const auId2 of axisConfig.aus) {
|
|
551
|
-
const val = sampleCurve(String(auId2), t);
|
|
552
|
-
if (val > maxVal) {
|
|
553
|
-
maxVal = val;
|
|
554
|
-
maxAU = auId2;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return config.auToBones[maxAU]?.find((b) => b.node === nodeKey) ?? null;
|
|
558
|
-
}
|
|
559
|
-
const auId = axisConfig.aus[0];
|
|
560
|
-
return config.auToBones[auId]?.find((b) => b.node === nodeKey) ?? null;
|
|
599
|
+
return getCompositeAxisBinding(
|
|
600
|
+
nodeKey,
|
|
601
|
+
axisConfig,
|
|
602
|
+
axisValue,
|
|
603
|
+
(auId) => getAxisSampleForNode(auId, nodeKey, t),
|
|
604
|
+
config.auToBones
|
|
605
|
+
);
|
|
561
606
|
};
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (axisConfig.aus.length > 1) {
|
|
570
|
-
let maxVal = 0;
|
|
571
|
-
for (const auId of axisConfig.aus) {
|
|
572
|
-
const val = sampleCurve(String(auId), t);
|
|
573
|
-
if (val > maxVal) maxVal = val;
|
|
574
|
-
}
|
|
575
|
-
return maxVal;
|
|
576
|
-
}
|
|
577
|
-
return sampleCurve(String(axisConfig.aus[0]), t);
|
|
607
|
+
const getAxisSampleForNode = (auId, nodeKey, t) => {
|
|
608
|
+
const rawValue = sampleCurve(String(auId), t);
|
|
609
|
+
if (rawValue <= 1e-6) return 0;
|
|
610
|
+
const binding = config.auToBones[auId]?.find((b) => b.node === nodeKey) ?? null;
|
|
611
|
+
if (!binding?.side) return rawValue;
|
|
612
|
+
const curveBalance = resolveCurveBalance(String(auId), globalBalance, balanceMap);
|
|
613
|
+
return rawValue * getSideScale(curveBalance, binding.side);
|
|
578
614
|
};
|
|
615
|
+
const getAxisValue = (nodeKey, axisConfig, t) => getCompositeAxisValue(axisConfig, (auId) => getAxisSampleForNode(auId, nodeKey, t));
|
|
579
616
|
const autoVisemeJawHandledJaw = autoVisemeJaw && jawScale > 0 && visemeJawAmounts && options?.snippetCategory === "visemeSnippet";
|
|
580
617
|
for (const composite of compositeRotations) {
|
|
581
618
|
const nodeKey = composite.node;
|
|
@@ -596,7 +633,7 @@ var BakedAnimationController = class {
|
|
|
596
633
|
const compositeQ = new THREE.Quaternion().copy(entry.baseQuat);
|
|
597
634
|
const applyAxis = (axisConfig) => {
|
|
598
635
|
if (!axisConfig) return;
|
|
599
|
-
let axisValue = getAxisValue(axisConfig, t);
|
|
636
|
+
let axisValue = getAxisValue(nodeKey, axisConfig, t);
|
|
600
637
|
if (Math.abs(axisValue) <= 1e-6) return;
|
|
601
638
|
const binding = getAxisBinding(nodeKey, axisConfig, axisValue, t);
|
|
602
639
|
if (!binding?.maxDegrees || !binding.channel) return;
|
|
@@ -1199,24 +1236,24 @@ var AU_TO_MORPHS = {
|
|
|
1199
1236
|
center: []
|
|
1200
1237
|
},
|
|
1201
1238
|
61: {
|
|
1202
|
-
left: ["Eye_L_Look_L"
|
|
1203
|
-
right: [],
|
|
1239
|
+
left: ["Eye_L_Look_L"],
|
|
1240
|
+
right: ["Eye_R_Look_L"],
|
|
1204
1241
|
center: []
|
|
1205
1242
|
},
|
|
1206
1243
|
62: {
|
|
1207
|
-
left: [],
|
|
1208
|
-
right: ["
|
|
1244
|
+
left: ["Eye_L_Look_R"],
|
|
1245
|
+
right: ["Eye_R_Look_R"],
|
|
1209
1246
|
center: []
|
|
1210
1247
|
},
|
|
1211
1248
|
63: {
|
|
1212
|
-
left: [],
|
|
1213
|
-
right: [],
|
|
1214
|
-
center: [
|
|
1249
|
+
left: ["Eye_L_Look_Up"],
|
|
1250
|
+
right: ["Eye_R_Look_Up"],
|
|
1251
|
+
center: []
|
|
1215
1252
|
},
|
|
1216
1253
|
64: {
|
|
1217
|
-
left: [],
|
|
1218
|
-
right: [],
|
|
1219
|
-
center: [
|
|
1254
|
+
left: ["Eye_L_Look_Down"],
|
|
1255
|
+
right: ["Eye_R_Look_Down"],
|
|
1256
|
+
center: []
|
|
1220
1257
|
},
|
|
1221
1258
|
65: {
|
|
1222
1259
|
left: ["Eye_L_Look_L"],
|
|
@@ -1444,6 +1481,14 @@ var BONE_AU_TO_BINDINGS = {
|
|
|
1444
1481
|
{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 20, side: "left" },
|
|
1445
1482
|
{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 20, side: "right" }
|
|
1446
1483
|
],
|
|
1484
|
+
65: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 25, side: "left" }],
|
|
1485
|
+
66: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 25, side: "left" }],
|
|
1486
|
+
67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 20, side: "left" }],
|
|
1487
|
+
68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 20, side: "left" }],
|
|
1488
|
+
69: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 25, side: "right" }],
|
|
1489
|
+
70: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 25, side: "right" }],
|
|
1490
|
+
71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 20, side: "right" }],
|
|
1491
|
+
72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 20, side: "right" }],
|
|
1447
1492
|
// Tongue controls (optional, for rigs that expose them)
|
|
1448
1493
|
37: [{ node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 20 }],
|
|
1449
1494
|
38: [{ node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 20 }],
|
|
@@ -1531,18 +1576,18 @@ var COMPOSITE_ROTATIONS = [
|
|
|
1531
1576
|
},
|
|
1532
1577
|
{
|
|
1533
1578
|
node: "EYE_L",
|
|
1534
|
-
pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
|
|
1579
|
+
pitch: { aus: [64, 63, 68, 67], axis: "rx", negative: [64, 68], positive: [63, 67] },
|
|
1535
1580
|
// Eyes down/up
|
|
1536
|
-
yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
|
|
1581
|
+
yaw: { aus: [61, 62, 65, 66], axis: "rz", negative: [61, 65], positive: [62, 66] },
|
|
1537
1582
|
// Eyes left/right (rz for CC4)
|
|
1538
1583
|
roll: null
|
|
1539
1584
|
// Eyes don't have roll
|
|
1540
1585
|
},
|
|
1541
1586
|
{
|
|
1542
1587
|
node: "EYE_R",
|
|
1543
|
-
pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
|
|
1588
|
+
pitch: { aus: [64, 63, 72, 71], axis: "rx", negative: [64, 72], positive: [63, 71] },
|
|
1544
1589
|
// Eyes down/up
|
|
1545
|
-
yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
|
|
1590
|
+
yaw: { aus: [61, 62, 69, 70], axis: "rz", negative: [61, 69], positive: [62, 70] },
|
|
1546
1591
|
// Eyes left/right (rz for CC4)
|
|
1547
1592
|
roll: null
|
|
1548
1593
|
// Eyes don't have roll
|
|
@@ -1561,9 +1606,17 @@ var CONTINUUM_PAIRS_MAP = {
|
|
|
1561
1606
|
// Eyes horizontal - both eyes share same AUs (yaw maps to rz via COMPOSITE_ROTATIONS)
|
|
1562
1607
|
61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
|
|
1563
1608
|
62: { pairId: 61, isNegative: false, axis: "yaw", node: "EYE_L" },
|
|
1609
|
+
65: { pairId: 66, isNegative: true, axis: "yaw", node: "EYE_L" },
|
|
1610
|
+
66: { pairId: 65, isNegative: false, axis: "yaw", node: "EYE_L" },
|
|
1611
|
+
69: { pairId: 70, isNegative: true, axis: "yaw", node: "EYE_R" },
|
|
1612
|
+
70: { pairId: 69, isNegative: false, axis: "yaw", node: "EYE_R" },
|
|
1564
1613
|
// Eyes vertical (pitch)
|
|
1565
1614
|
64: { pairId: 63, isNegative: true, axis: "pitch", node: "EYE_L" },
|
|
1566
1615
|
63: { pairId: 64, isNegative: false, axis: "pitch", node: "EYE_L" },
|
|
1616
|
+
68: { pairId: 67, isNegative: true, axis: "pitch", node: "EYE_L" },
|
|
1617
|
+
67: { pairId: 68, isNegative: false, axis: "pitch", node: "EYE_L" },
|
|
1618
|
+
72: { pairId: 71, isNegative: true, axis: "pitch", node: "EYE_R" },
|
|
1619
|
+
71: { pairId: 72, isNegative: false, axis: "pitch", node: "EYE_R" },
|
|
1567
1620
|
// Head yaw (turn left/right)
|
|
1568
1621
|
51: { pairId: 52, isNegative: true, axis: "yaw", node: "HEAD" },
|
|
1569
1622
|
52: { pairId: 51, isNegative: false, axis: "yaw", node: "HEAD" },
|
|
@@ -1596,6 +1649,10 @@ var CONTINUUM_PAIRS_MAP = {
|
|
|
1596
1649
|
var CONTINUUM_LABELS = {
|
|
1597
1650
|
"61-62": "Eyes \u2014 Horizontal",
|
|
1598
1651
|
"64-63": "Eyes \u2014 Vertical",
|
|
1652
|
+
"65-66": "Left Eye \u2014 Horizontal",
|
|
1653
|
+
"68-67": "Left Eye \u2014 Vertical",
|
|
1654
|
+
"69-70": "Right Eye \u2014 Horizontal",
|
|
1655
|
+
"72-71": "Right Eye \u2014 Vertical",
|
|
1599
1656
|
"51-52": "Head \u2014 Horizontal",
|
|
1600
1657
|
"54-53": "Head \u2014 Vertical",
|
|
1601
1658
|
"55-56": "Head \u2014 Tilt",
|
|
@@ -1784,6 +1841,12 @@ var MORPH_TO_MESH = {
|
|
|
1784
1841
|
tongue: ["CC_Base_Tongue", "CC_Base_Tongue_1"],
|
|
1785
1842
|
hair: ["Side_part_wavy", "Side_part_wavy_1", "Side_part_wavy_2"]
|
|
1786
1843
|
};
|
|
1844
|
+
var AU_FACEPART_TO_MESH_CATEGORY = {
|
|
1845
|
+
Eye: "eye",
|
|
1846
|
+
Eyes: "eye",
|
|
1847
|
+
Eyelids: "eye",
|
|
1848
|
+
Tongue: "tongue"
|
|
1849
|
+
};
|
|
1787
1850
|
var CC4_HAIR_PHYSICS = {
|
|
1788
1851
|
stiffness: 7.5,
|
|
1789
1852
|
damping: 0.18,
|
|
@@ -1829,7 +1892,9 @@ var CC4_PRESET = {
|
|
|
1829
1892
|
bonePrefix: CC4_BONE_PREFIX,
|
|
1830
1893
|
suffixPattern: CC4_SUFFIX_PATTERN,
|
|
1831
1894
|
morphToMesh: MORPH_TO_MESH,
|
|
1895
|
+
auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
|
|
1832
1896
|
visemeKeys: VISEME_KEYS,
|
|
1897
|
+
visemeMeshCategory: "viseme",
|
|
1833
1898
|
visemeJawAmounts: VISEME_JAW_AMOUNTS,
|
|
1834
1899
|
auMixDefaults: AU_MIX_DEFAULTS,
|
|
1835
1900
|
auInfo: AU_INFO,
|
|
@@ -2149,9 +2214,10 @@ var HairPhysicsController = class {
|
|
|
2149
2214
|
if (meshName) {
|
|
2150
2215
|
targetMesh = this.registeredHairObjects.get(meshName);
|
|
2151
2216
|
} else {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2217
|
+
const hairMeshNames = this.getHairMeshNames();
|
|
2218
|
+
for (const name of hairMeshNames) {
|
|
2219
|
+
const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
|
|
2220
|
+
if (mesh) {
|
|
2155
2221
|
targetMesh = mesh;
|
|
2156
2222
|
break;
|
|
2157
2223
|
}
|
|
@@ -2245,6 +2311,15 @@ var HairPhysicsController = class {
|
|
|
2245
2311
|
}
|
|
2246
2312
|
getHairMeshNames() {
|
|
2247
2313
|
if (this.cachedHairMeshNames) return this.cachedHairMeshNames;
|
|
2314
|
+
if (typeof this.host.getSelectedHairMeshNames === "function") {
|
|
2315
|
+
const selectedHairMeshNames = this.host.getSelectedHairMeshNames() || [];
|
|
2316
|
+
const resolved = selectedHairMeshNames.filter((name) => {
|
|
2317
|
+
const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
|
|
2318
|
+
return !!mesh;
|
|
2319
|
+
});
|
|
2320
|
+
this.cachedHairMeshNames = Array.from(new Set(resolved));
|
|
2321
|
+
return this.cachedHairMeshNames;
|
|
2322
|
+
}
|
|
2248
2323
|
const names = [];
|
|
2249
2324
|
this.registeredHairObjects.forEach((mesh, name) => {
|
|
2250
2325
|
const info = CC4_MESHES[name];
|
|
@@ -2260,6 +2335,18 @@ var HairPhysicsController = class {
|
|
|
2260
2335
|
this.cachedHairMeshNames = names;
|
|
2261
2336
|
return names;
|
|
2262
2337
|
}
|
|
2338
|
+
refreshMeshSelection() {
|
|
2339
|
+
this.cachedHairMeshNames = null;
|
|
2340
|
+
this.idleClipDirty = true;
|
|
2341
|
+
this.gravityClipDirty = true;
|
|
2342
|
+
this.impulseClipDirty = true;
|
|
2343
|
+
this.warnMissingHairMorphTargets();
|
|
2344
|
+
if (this.hairPhysicsEnabled) {
|
|
2345
|
+
this.startIdleClip();
|
|
2346
|
+
this.startGravityClip();
|
|
2347
|
+
this.buildImpulseClips();
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2263
2350
|
supportsMixerClips() {
|
|
2264
2351
|
return typeof this.host.buildClip === "function";
|
|
2265
2352
|
}
|
|
@@ -2274,7 +2361,11 @@ var HairPhysicsController = class {
|
|
|
2274
2361
|
return;
|
|
2275
2362
|
}
|
|
2276
2363
|
const hairMeshNames = this.getHairMeshNames();
|
|
2277
|
-
if (hairMeshNames.length === 0)
|
|
2364
|
+
if (hairMeshNames.length === 0) {
|
|
2365
|
+
this.stopIdleClip();
|
|
2366
|
+
this.idleClipDirty = false;
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2278
2369
|
if (!this.idleClipDirty && this.idleClipHandle) return;
|
|
2279
2370
|
this.stopIdleClip();
|
|
2280
2371
|
const duration = Math.max(0.5, cfg.idleClipDuration);
|
|
@@ -2292,7 +2383,11 @@ var HairPhysicsController = class {
|
|
|
2292
2383
|
startGravityClip() {
|
|
2293
2384
|
if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
|
|
2294
2385
|
const hairMeshNames = this.getHairMeshNames();
|
|
2295
|
-
if (hairMeshNames.length === 0)
|
|
2386
|
+
if (hairMeshNames.length === 0) {
|
|
2387
|
+
this.stopGravityClip();
|
|
2388
|
+
this.gravityClipDirty = false;
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2296
2391
|
if (!this.gravityClipDirty && this.gravityClipHandle) return;
|
|
2297
2392
|
this.stopGravityClip();
|
|
2298
2393
|
const morphTargets = this.hairPhysicsConfig.morphTargets;
|
|
@@ -2345,7 +2440,11 @@ var HairPhysicsController = class {
|
|
|
2345
2440
|
buildImpulseClips() {
|
|
2346
2441
|
if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
|
|
2347
2442
|
const hairMeshNames = this.getHairMeshNames();
|
|
2348
|
-
if (hairMeshNames.length === 0)
|
|
2443
|
+
if (hairMeshNames.length === 0) {
|
|
2444
|
+
this.stopImpulseClips();
|
|
2445
|
+
this.impulseClipDirty = false;
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2349
2448
|
if (!this.impulseClipDirty && this.impulseClips.left && this.impulseClips.right && this.impulseClips.front) {
|
|
2350
2449
|
return;
|
|
2351
2450
|
}
|
|
@@ -2681,7 +2780,9 @@ function resolveProfile(base, override) {
|
|
|
2681
2780
|
auToBones: mergeRecord(base.auToBones, override.auToBones),
|
|
2682
2781
|
boneNodes: mergeRecord(base.boneNodes, override.boneNodes),
|
|
2683
2782
|
morphToMesh: mergeRecord(base.morphToMesh, override.morphToMesh),
|
|
2783
|
+
auFacePartToMeshCategory: base.auFacePartToMeshCategory || override.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, override.auFacePartToMeshCategory || {}) : void 0,
|
|
2684
2784
|
visemeKeys: override.visemeKeys ? [...override.visemeKeys] : [...base.visemeKeys],
|
|
2785
|
+
visemeMeshCategory: override.visemeMeshCategory ?? base.visemeMeshCategory,
|
|
2685
2786
|
auMixDefaults: base.auMixDefaults || override.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, override.auMixDefaults || {}) : void 0,
|
|
2686
2787
|
auInfo: base.auInfo || override.auInfo ? mergeRecord(base.auInfo || {}, override.auInfo || {}) : void 0,
|
|
2687
2788
|
eyeMeshNodes: override.eyeMeshNodes ?? base.eyeMeshNodes,
|
|
@@ -3519,6 +3620,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3519
3620
|
this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
|
|
3520
3621
|
this.hairPhysics = new HairPhysicsController({
|
|
3521
3622
|
getMeshByName: (name) => this.meshByName.get(name),
|
|
3623
|
+
getSelectedHairMeshNames: () => this.config.morphToMesh?.hair || [],
|
|
3522
3624
|
buildClip: (clipName, curves, options) => this.buildClip(clipName, curves, options),
|
|
3523
3625
|
cleanupSnippet: (name) => this.cleanupSnippet(name)
|
|
3524
3626
|
});
|
|
@@ -3527,6 +3629,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3527
3629
|
getMeshes: () => this.meshes,
|
|
3528
3630
|
getMeshByName: (name) => this.meshByName.get(name),
|
|
3529
3631
|
getMeshNamesForAU: (auId) => this.getMeshNamesForAU(auId),
|
|
3632
|
+
getMeshNamesForViseme: () => this.getMeshNamesForViseme(),
|
|
3530
3633
|
getBones: () => this.bones,
|
|
3531
3634
|
getConfig: () => this.config,
|
|
3532
3635
|
getCompositeRotations: () => this.compositeRotations,
|
|
@@ -3623,7 +3726,8 @@ var _Loom3 = class _Loom3 {
|
|
|
3623
3726
|
}
|
|
3624
3727
|
for (let i = 0; i < (this.config.visemeKeys || []).length; i += 1) {
|
|
3625
3728
|
const key = this.config.visemeKeys[i];
|
|
3626
|
-
const
|
|
3729
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
3730
|
+
const targets = typeof key === "number" ? this.resolveMorphTargetsByIndex(key, visemeMeshNames) : this.resolveMorphTargets(key, visemeMeshNames);
|
|
3627
3731
|
this.resolvedVisemeTargets[i] = targets;
|
|
3628
3732
|
}
|
|
3629
3733
|
}
|
|
@@ -3793,37 +3897,12 @@ var _Loom3 = class _Loom3 {
|
|
|
3793
3897
|
}
|
|
3794
3898
|
const compositeInfo = this.auToCompositeMap.get(id);
|
|
3795
3899
|
if (compositeInfo) {
|
|
3796
|
-
const storedBalance = this.auBalances[id] ?? 0;
|
|
3797
|
-
const { left: leftVal, right: rightVal } = this.computeSideValues(clamp012(v), storedBalance);
|
|
3798
3900
|
for (const nodeKey of compositeInfo.nodes) {
|
|
3799
3901
|
const config = this.compositeRotations.find((c) => c.node === nodeKey);
|
|
3800
3902
|
if (!config) continue;
|
|
3801
3903
|
const axisConfig = config[compositeInfo.axis];
|
|
3802
3904
|
if (!axisConfig) continue;
|
|
3803
|
-
|
|
3804
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
3805
|
-
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
3806
|
-
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
3807
|
-
axisValue = posValue - negValue;
|
|
3808
|
-
} else if (axisConfig.aus.length > 1) {
|
|
3809
|
-
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
3810
|
-
} else {
|
|
3811
|
-
axisValue = v;
|
|
3812
|
-
}
|
|
3813
|
-
const auBoneBindings = this.config.auToBones[id] || [];
|
|
3814
|
-
const sideByNode = /* @__PURE__ */ new Map();
|
|
3815
|
-
for (const binding of auBoneBindings) {
|
|
3816
|
-
if (binding.side === "left" || binding.side === "right") {
|
|
3817
|
-
sideByNode.set(binding.node, binding.side);
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
const side = sideByNode.get(nodeKey);
|
|
3821
|
-
if (side) {
|
|
3822
|
-
const baseValue = clamp012(v);
|
|
3823
|
-
const balanceValue = side === "left" ? leftVal : rightVal;
|
|
3824
|
-
const denom = baseValue > 0 ? baseValue : 1;
|
|
3825
|
-
axisValue = axisValue * (balanceValue / denom);
|
|
3826
|
-
}
|
|
3905
|
+
const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
|
|
3827
3906
|
this.updateBoneRotation(nodeKey, compositeInfo.axis, axisValue);
|
|
3828
3907
|
this.pendingCompositeNodes.add(nodeKey);
|
|
3829
3908
|
}
|
|
@@ -3851,11 +3930,15 @@ var _Loom3 = class _Loom3 {
|
|
|
3851
3930
|
}
|
|
3852
3931
|
}
|
|
3853
3932
|
const target = clamp012(to);
|
|
3933
|
+
if (balance !== void 0) {
|
|
3934
|
+
this.auBalances[numId] = balance;
|
|
3935
|
+
}
|
|
3936
|
+
const storedBalance = this.auBalances[numId] ?? 0;
|
|
3854
3937
|
const { left: leftKeys, right: rightKeys, center: centerKeys } = this.getAUMorphsBySide(numId);
|
|
3855
3938
|
const bindings = this.config.auToBones[numId] || [];
|
|
3856
3939
|
const mixWeight = this.isMixedAU(numId) ? this.getAUMixWeight(numId) : 1;
|
|
3857
3940
|
const base = target * mixWeight;
|
|
3858
|
-
const { left: leftVal, right: rightVal } = this.computeSideValues(base,
|
|
3941
|
+
const { left: leftVal, right: rightVal } = this.computeSideValues(base, storedBalance);
|
|
3859
3942
|
this.auValues[numId] = target;
|
|
3860
3943
|
const handles = [];
|
|
3861
3944
|
const meshNames = this.getMeshNamesForAU(numId);
|
|
@@ -3883,16 +3966,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3883
3966
|
if (!config) continue;
|
|
3884
3967
|
const axisConfig = config[compositeInfo.axis];
|
|
3885
3968
|
if (!axisConfig) continue;
|
|
3886
|
-
|
|
3887
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
3888
|
-
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
3889
|
-
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
3890
|
-
axisValue = posValue - negValue;
|
|
3891
|
-
} else if (axisConfig.aus.length > 1) {
|
|
3892
|
-
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
3893
|
-
} else {
|
|
3894
|
-
axisValue = target;
|
|
3895
|
-
}
|
|
3969
|
+
const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
|
|
3896
3970
|
handles.push(this.transitionBoneRotation(nodeKey, compositeInfo.axis, axisValue, durationMs));
|
|
3897
3971
|
}
|
|
3898
3972
|
}
|
|
@@ -4055,10 +4129,11 @@ var _Loom3 = class _Loom3 {
|
|
|
4055
4129
|
this.applyMorphTargets(targets, val);
|
|
4056
4130
|
} else {
|
|
4057
4131
|
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
4132
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
4058
4133
|
if (typeof morphKey === "number") {
|
|
4059
|
-
this.setMorphInfluence(morphKey, val);
|
|
4134
|
+
this.setMorphInfluence(morphKey, val, visemeMeshNames);
|
|
4060
4135
|
} else if (typeof morphKey === "string") {
|
|
4061
|
-
this.setMorph(morphKey, val);
|
|
4136
|
+
this.setMorph(morphKey, val, visemeMeshNames);
|
|
4062
4137
|
}
|
|
4063
4138
|
}
|
|
4064
4139
|
const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
|
|
@@ -4076,7 +4151,8 @@ var _Loom3 = class _Loom3 {
|
|
|
4076
4151
|
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
4077
4152
|
const target = clamp012(to);
|
|
4078
4153
|
this.visemeValues[visemeIndex] = target;
|
|
4079
|
-
const
|
|
4154
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
4155
|
+
const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
|
|
4080
4156
|
const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
|
|
4081
4157
|
if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
|
|
4082
4158
|
return morphHandle;
|
|
@@ -4380,6 +4456,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4380
4456
|
if (this.model) {
|
|
4381
4457
|
this.rebuildMorphTargetsCache();
|
|
4382
4458
|
}
|
|
4459
|
+
this.hairPhysics.refreshMeshSelection();
|
|
4383
4460
|
this.applyHairPhysicsProfileConfig();
|
|
4384
4461
|
}
|
|
4385
4462
|
getProfile() {
|
|
@@ -4387,20 +4464,24 @@ var _Loom3 = class _Loom3 {
|
|
|
4387
4464
|
}
|
|
4388
4465
|
/**
|
|
4389
4466
|
* Get the mesh names that should receive morph influences for a given AU.
|
|
4390
|
-
*
|
|
4467
|
+
* Routing is driven by `auFacePartToMeshCategory` in profile config.
|
|
4391
4468
|
*/
|
|
4392
4469
|
getMeshNamesForAU(auId) {
|
|
4393
4470
|
const m = this.config.morphToMesh;
|
|
4394
4471
|
const info = this.config.auInfo?.[String(auId)];
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
default:
|
|
4402
|
-
return m?.face || [];
|
|
4472
|
+
const facePart = info?.facePart;
|
|
4473
|
+
if (facePart) {
|
|
4474
|
+
const category = this.config.auFacePartToMeshCategory?.[facePart];
|
|
4475
|
+
if (category) {
|
|
4476
|
+
return m?.[category] || [];
|
|
4477
|
+
}
|
|
4403
4478
|
}
|
|
4479
|
+
return m?.face || [];
|
|
4480
|
+
}
|
|
4481
|
+
getMeshNamesForViseme() {
|
|
4482
|
+
const m = this.config.morphToMesh;
|
|
4483
|
+
const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
|
|
4484
|
+
return m?.[category] || m?.face || [];
|
|
4404
4485
|
}
|
|
4405
4486
|
// ============================================================================
|
|
4406
4487
|
// HAIR PHYSICS
|
|
@@ -4519,6 +4600,25 @@ var _Loom3 = class _Loom3 {
|
|
|
4519
4600
|
const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
|
|
4520
4601
|
return !!(hasMorphs && this.config.auToBones[id]?.length);
|
|
4521
4602
|
}
|
|
4603
|
+
getEffectiveBoneAUValue(auId, nodeKey) {
|
|
4604
|
+
const rawValue = clamp012(this.auValues[auId] ?? 0);
|
|
4605
|
+
if (rawValue <= 1e-6) return 0;
|
|
4606
|
+
const binding = this.config.auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
|
|
4607
|
+
if (!binding?.side) return rawValue;
|
|
4608
|
+
return rawValue * getSideScale(this.auBalances[auId] ?? 0, binding.side);
|
|
4609
|
+
}
|
|
4610
|
+
getCompositeAxisValueForNode(nodeKey, axisConfig) {
|
|
4611
|
+
return getCompositeAxisValue(axisConfig, (auId) => this.getEffectiveBoneAUValue(auId, nodeKey));
|
|
4612
|
+
}
|
|
4613
|
+
getCompositeAxisBindingForNode(nodeKey, axisConfig, direction) {
|
|
4614
|
+
return getCompositeAxisBinding(
|
|
4615
|
+
nodeKey,
|
|
4616
|
+
axisConfig,
|
|
4617
|
+
direction,
|
|
4618
|
+
(auId) => this.getEffectiveBoneAUValue(auId, nodeKey),
|
|
4619
|
+
this.config.auToBones
|
|
4620
|
+
);
|
|
4621
|
+
}
|
|
4522
4622
|
initBoneRotations() {
|
|
4523
4623
|
this.rotations = {};
|
|
4524
4624
|
this.pendingCompositeNodes.clear();
|
|
@@ -4594,30 +4694,10 @@ var _Loom3 = class _Loom3 {
|
|
|
4594
4694
|
if (!config) {
|
|
4595
4695
|
return;
|
|
4596
4696
|
}
|
|
4597
|
-
const getBindingForAxis = (axisConfig, direction) => {
|
|
4598
|
-
if (!axisConfig) return null;
|
|
4599
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
4600
|
-
const auId = direction < 0 ? axisConfig.negative : axisConfig.positive;
|
|
4601
|
-
return this.config.auToBones[auId]?.find((b) => b.node === nodeKey);
|
|
4602
|
-
}
|
|
4603
|
-
if (axisConfig.aus.length > 1) {
|
|
4604
|
-
let maxAU = axisConfig.aus[0];
|
|
4605
|
-
let maxValue = this.auValues[maxAU] ?? 0;
|
|
4606
|
-
for (const auId of axisConfig.aus) {
|
|
4607
|
-
const val = this.auValues[auId] ?? 0;
|
|
4608
|
-
if (val > maxValue) {
|
|
4609
|
-
maxValue = val;
|
|
4610
|
-
maxAU = auId;
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
return this.config.auToBones[maxAU]?.find((b) => b.node === nodeKey);
|
|
4614
|
-
}
|
|
4615
|
-
return this.config.auToBones[axisConfig.aus[0]]?.find((b) => b.node === nodeKey);
|
|
4616
|
-
};
|
|
4617
4697
|
const getAxis = (channel) => channel === "rx" ? X_AXIS2 : channel === "ry" ? Y_AXIS2 : Z_AXIS2;
|
|
4618
4698
|
const compositeQ = new THREE.Quaternion().copy(baseQuat);
|
|
4619
4699
|
if (config.yaw && rotState.yaw !== 0) {
|
|
4620
|
-
const binding =
|
|
4700
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.yaw, rotState.yaw);
|
|
4621
4701
|
if (binding?.maxDegrees && binding.channel) {
|
|
4622
4702
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.yaw) * binding.scale;
|
|
4623
4703
|
const axis = getAxis(binding.channel);
|
|
@@ -4626,7 +4706,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4626
4706
|
}
|
|
4627
4707
|
}
|
|
4628
4708
|
if (config.pitch && rotState.pitch !== 0) {
|
|
4629
|
-
const binding =
|
|
4709
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.pitch, rotState.pitch);
|
|
4630
4710
|
if (binding?.maxDegrees && binding.channel) {
|
|
4631
4711
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.pitch) * binding.scale;
|
|
4632
4712
|
const axis = getAxis(binding.channel);
|
|
@@ -4635,7 +4715,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4635
4715
|
}
|
|
4636
4716
|
}
|
|
4637
4717
|
if (config.roll && rotState.roll !== 0) {
|
|
4638
|
-
const binding =
|
|
4718
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.roll, rotState.roll);
|
|
4639
4719
|
if (binding?.maxDegrees && binding.channel) {
|
|
4640
4720
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.roll) * binding.scale;
|
|
4641
4721
|
const axis = getAxis(binding.channel);
|
|
@@ -5504,7 +5584,7 @@ function findMatches(targetNames, candidateNames, prefix, suffix, suffixPattern)
|
|
|
5504
5584
|
return { found, missing };
|
|
5505
5585
|
}
|
|
5506
5586
|
function collectAxisConfigs(axisConfigs) {
|
|
5507
|
-
return axisConfigs.filter((entry) => entry.config);
|
|
5587
|
+
return axisConfigs.filter((entry) => entry.config !== null);
|
|
5508
5588
|
}
|
|
5509
5589
|
function isEyeNodeKey(nodeKey) {
|
|
5510
5590
|
return nodeKey === "EYE_L" || nodeKey === "EYE_R";
|
|
@@ -5574,28 +5654,35 @@ function validateMappingConfig(config) {
|
|
|
5574
5654
|
);
|
|
5575
5655
|
continue;
|
|
5576
5656
|
}
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5657
|
+
for (const auId of toAUList(axisConfig.negative)) {
|
|
5658
|
+
if (!axisConfig.aus.includes(auId)) {
|
|
5659
|
+
push(
|
|
5660
|
+
"error",
|
|
5661
|
+
"COMPOSITE_AU_MISSING",
|
|
5662
|
+
`Composite axis for "${composite.node}" is missing negative AU ${auId} in aus list`,
|
|
5663
|
+
{ node: composite.node, auId }
|
|
5664
|
+
);
|
|
5665
|
+
}
|
|
5584
5666
|
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5667
|
+
for (const auId of toAUList(axisConfig.positive)) {
|
|
5668
|
+
if (!axisConfig.aus.includes(auId)) {
|
|
5669
|
+
push(
|
|
5670
|
+
"error",
|
|
5671
|
+
"COMPOSITE_AU_MISSING",
|
|
5672
|
+
`Composite axis for "${composite.node}" is missing positive AU ${auId} in aus list`,
|
|
5673
|
+
{ node: composite.node, auId }
|
|
5674
|
+
);
|
|
5675
|
+
}
|
|
5592
5676
|
}
|
|
5593
|
-
|
|
5677
|
+
const negativeAUs = toAUList(axisConfig.negative);
|
|
5678
|
+
const positiveAUs = toAUList(axisConfig.positive);
|
|
5679
|
+
const overlappingAUs = negativeAUs.filter((auId) => positiveAUs.includes(auId));
|
|
5680
|
+
if (overlappingAUs.length > 0) {
|
|
5594
5681
|
push(
|
|
5595
5682
|
"error",
|
|
5596
5683
|
"COMPOSITE_AU_DUPLICATE",
|
|
5597
|
-
`Composite axis for "${composite.node}"
|
|
5598
|
-
{ node: composite.node, auId:
|
|
5684
|
+
`Composite axis for "${composite.node}" reuses AU ${overlappingAUs[0]} in both negative and positive groups`,
|
|
5685
|
+
{ node: composite.node, auId: overlappingAUs[0] }
|
|
5599
5686
|
);
|
|
5600
5687
|
}
|
|
5601
5688
|
}
|
|
@@ -5685,11 +5772,11 @@ function validateMappingConfig(config) {
|
|
|
5685
5772
|
);
|
|
5686
5773
|
continue;
|
|
5687
5774
|
}
|
|
5688
|
-
const expectedNeg = axisConfig.negative;
|
|
5689
|
-
const expectedPos = axisConfig.positive;
|
|
5775
|
+
const expectedNeg = toAUList(axisConfig.negative);
|
|
5776
|
+
const expectedPos = toAUList(axisConfig.positive);
|
|
5690
5777
|
const negId = info.isNegative ? Number(auIdStr) : info.pairId;
|
|
5691
5778
|
const posId = info.isNegative ? info.pairId : Number(auIdStr);
|
|
5692
|
-
if (negId
|
|
5779
|
+
if (!expectedNeg.includes(negId) || !expectedPos.includes(posId)) {
|
|
5693
5780
|
push(
|
|
5694
5781
|
"warning",
|
|
5695
5782
|
"CONTINUUM_COMPOSITE_MISMATCH",
|