@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.js
CHANGED
|
@@ -5,11 +5,53 @@ var __defProp = Object.defineProperty;
|
|
|
5
5
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
6
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
7
7
|
|
|
8
|
+
// src/core/compositeAxis.ts
|
|
9
|
+
function toAUList(value) {
|
|
10
|
+
if (value === void 0) return [];
|
|
11
|
+
return Array.isArray(value) ? value : [value];
|
|
12
|
+
}
|
|
13
|
+
function findBoneBindingForNode(auToBones, auId, nodeKey) {
|
|
14
|
+
return auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
|
|
15
|
+
}
|
|
16
|
+
function getCompositeAxisValue(axisConfig, getValue) {
|
|
17
|
+
if (!axisConfig) return 0;
|
|
18
|
+
const negativeAUs = toAUList(axisConfig.negative);
|
|
19
|
+
const positiveAUs = toAUList(axisConfig.positive);
|
|
20
|
+
if (negativeAUs.length > 0 && positiveAUs.length > 0) {
|
|
21
|
+
const negativeValue = Math.max(...negativeAUs.map(getValue), 0);
|
|
22
|
+
const positiveValue = Math.max(...positiveAUs.map(getValue), 0);
|
|
23
|
+
return positiveValue - negativeValue;
|
|
24
|
+
}
|
|
25
|
+
if (axisConfig.aus.length > 1) {
|
|
26
|
+
return Math.max(...axisConfig.aus.map(getValue), 0);
|
|
27
|
+
}
|
|
28
|
+
return getValue(axisConfig.aus[0]);
|
|
29
|
+
}
|
|
30
|
+
function getCompositeAxisBinding(nodeKey, axisConfig, direction, getValue, auToBones) {
|
|
31
|
+
if (!axisConfig) return null;
|
|
32
|
+
const directionalAUs = direction < 0 ? toAUList(axisConfig.negative) : toAUList(axisConfig.positive);
|
|
33
|
+
const candidates = directionalAUs.length > 0 ? directionalAUs : axisConfig.aus;
|
|
34
|
+
const ranked = [...candidates].sort((a, b) => getValue(b) - getValue(a));
|
|
35
|
+
for (const auId of ranked) {
|
|
36
|
+
const binding = findBoneBindingForNode(auToBones, auId, nodeKey);
|
|
37
|
+
if (binding) return binding;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
8
42
|
// src/engines/three/balanceUtils.ts
|
|
9
43
|
function clampBalance(value) {
|
|
10
44
|
if (!Number.isFinite(value)) return 0;
|
|
11
45
|
return Math.max(-1, Math.min(1, value));
|
|
12
46
|
}
|
|
47
|
+
function getSideScale(balance, side) {
|
|
48
|
+
if (side !== "left" && side !== "right") return 1;
|
|
49
|
+
const clamped = clampBalance(balance);
|
|
50
|
+
if (side === "left") {
|
|
51
|
+
return clamped > 0 ? 1 - clamped : 1;
|
|
52
|
+
}
|
|
53
|
+
return clamped < 0 ? 1 + clamped : 1;
|
|
54
|
+
}
|
|
13
55
|
function resolveCurveBalance(curveId, globalBalance, balanceMap) {
|
|
14
56
|
if (balanceMap && Object.prototype.hasOwnProperty.call(balanceMap, curveId)) {
|
|
15
57
|
return clampBalance(Number(balanceMap[curveId]));
|
|
@@ -137,8 +179,22 @@ var BakedAnimationController = class {
|
|
|
137
179
|
return this.host.getMeshNamesForAU(auId) || [];
|
|
138
180
|
}
|
|
139
181
|
const facePart = config.auInfo?.[String(auId)]?.facePart;
|
|
140
|
-
if (facePart
|
|
141
|
-
|
|
182
|
+
if (facePart) {
|
|
183
|
+
const category = config.auFacePartToMeshCategory?.[facePart];
|
|
184
|
+
if (category) return config.morphToMesh?.[category] || [];
|
|
185
|
+
}
|
|
186
|
+
return config.morphToMesh?.face || [];
|
|
187
|
+
}
|
|
188
|
+
getMeshNamesForViseme(config, explicitMeshNames) {
|
|
189
|
+
if (explicitMeshNames && explicitMeshNames.length > 0) {
|
|
190
|
+
return explicitMeshNames;
|
|
191
|
+
}
|
|
192
|
+
if (typeof this.host.getMeshNamesForViseme === "function") {
|
|
193
|
+
return this.host.getMeshNamesForViseme() || [];
|
|
194
|
+
}
|
|
195
|
+
const category = config.visemeMeshCategory || (config.morphToMesh?.viseme ? "viseme" : "face");
|
|
196
|
+
const visemeMeshes = config.morphToMesh?.[category];
|
|
197
|
+
if (visemeMeshes && visemeMeshes.length > 0) return visemeMeshes;
|
|
142
198
|
return config.morphToMesh?.face || [];
|
|
143
199
|
}
|
|
144
200
|
update(dtSeconds) {
|
|
@@ -434,11 +490,12 @@ var BakedAnimationController = class {
|
|
|
434
490
|
if (isNumericAU(curveId)) {
|
|
435
491
|
const auId = Number(curveId);
|
|
436
492
|
if (isVisemeIndex(curveId)) {
|
|
493
|
+
const visemeMeshNames = this.getMeshNamesForViseme(config, meshNames);
|
|
437
494
|
const visemeKey = config.visemeKeys[auId];
|
|
438
495
|
if (typeof visemeKey === "number") {
|
|
439
|
-
this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale,
|
|
496
|
+
this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
|
|
440
497
|
} else if (visemeKey) {
|
|
441
|
-
this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale,
|
|
498
|
+
this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
|
|
442
499
|
}
|
|
443
500
|
} else {
|
|
444
501
|
const auMeshNames = this.getMeshNamesForAU(auId, config, meshNames);
|
|
@@ -518,43 +575,23 @@ var BakedAnimationController = class {
|
|
|
518
575
|
Object.keys(curves).filter(isNumericAU).map((id) => Number(id))
|
|
519
576
|
);
|
|
520
577
|
const getAxisBinding = (nodeKey, axisConfig, axisValue, t) => {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
let maxVal = sampleCurve(String(maxAU), t);
|
|
529
|
-
for (const auId2 of axisConfig.aus) {
|
|
530
|
-
const val = sampleCurve(String(auId2), t);
|
|
531
|
-
if (val > maxVal) {
|
|
532
|
-
maxVal = val;
|
|
533
|
-
maxAU = auId2;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return config.auToBones[maxAU]?.find((b) => b.node === nodeKey) ?? null;
|
|
537
|
-
}
|
|
538
|
-
const auId = axisConfig.aus[0];
|
|
539
|
-
return config.auToBones[auId]?.find((b) => b.node === nodeKey) ?? null;
|
|
578
|
+
return getCompositeAxisBinding(
|
|
579
|
+
nodeKey,
|
|
580
|
+
axisConfig,
|
|
581
|
+
axisValue,
|
|
582
|
+
(auId) => getAxisSampleForNode(auId, nodeKey, t),
|
|
583
|
+
config.auToBones
|
|
584
|
+
);
|
|
540
585
|
};
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (axisConfig.aus.length > 1) {
|
|
549
|
-
let maxVal = 0;
|
|
550
|
-
for (const auId of axisConfig.aus) {
|
|
551
|
-
const val = sampleCurve(String(auId), t);
|
|
552
|
-
if (val > maxVal) maxVal = val;
|
|
553
|
-
}
|
|
554
|
-
return maxVal;
|
|
555
|
-
}
|
|
556
|
-
return sampleCurve(String(axisConfig.aus[0]), t);
|
|
586
|
+
const getAxisSampleForNode = (auId, nodeKey, t) => {
|
|
587
|
+
const rawValue = sampleCurve(String(auId), t);
|
|
588
|
+
if (rawValue <= 1e-6) return 0;
|
|
589
|
+
const binding = config.auToBones[auId]?.find((b) => b.node === nodeKey) ?? null;
|
|
590
|
+
if (!binding?.side) return rawValue;
|
|
591
|
+
const curveBalance = resolveCurveBalance(String(auId), globalBalance, balanceMap);
|
|
592
|
+
return rawValue * getSideScale(curveBalance, binding.side);
|
|
557
593
|
};
|
|
594
|
+
const getAxisValue = (nodeKey, axisConfig, t) => getCompositeAxisValue(axisConfig, (auId) => getAxisSampleForNode(auId, nodeKey, t));
|
|
558
595
|
const autoVisemeJawHandledJaw = autoVisemeJaw && jawScale > 0 && visemeJawAmounts && options?.snippetCategory === "visemeSnippet";
|
|
559
596
|
for (const composite of compositeRotations) {
|
|
560
597
|
const nodeKey = composite.node;
|
|
@@ -575,7 +612,7 @@ var BakedAnimationController = class {
|
|
|
575
612
|
const compositeQ = new Quaternion().copy(entry.baseQuat);
|
|
576
613
|
const applyAxis = (axisConfig) => {
|
|
577
614
|
if (!axisConfig) return;
|
|
578
|
-
let axisValue = getAxisValue(axisConfig, t);
|
|
615
|
+
let axisValue = getAxisValue(nodeKey, axisConfig, t);
|
|
579
616
|
if (Math.abs(axisValue) <= 1e-6) return;
|
|
580
617
|
const binding = getAxisBinding(nodeKey, axisConfig, axisValue, t);
|
|
581
618
|
if (!binding?.maxDegrees || !binding.channel) return;
|
|
@@ -1178,24 +1215,24 @@ var AU_TO_MORPHS = {
|
|
|
1178
1215
|
center: []
|
|
1179
1216
|
},
|
|
1180
1217
|
61: {
|
|
1181
|
-
left: ["Eye_L_Look_L"
|
|
1182
|
-
right: [],
|
|
1218
|
+
left: ["Eye_L_Look_L"],
|
|
1219
|
+
right: ["Eye_R_Look_L"],
|
|
1183
1220
|
center: []
|
|
1184
1221
|
},
|
|
1185
1222
|
62: {
|
|
1186
|
-
left: [],
|
|
1187
|
-
right: ["
|
|
1223
|
+
left: ["Eye_L_Look_R"],
|
|
1224
|
+
right: ["Eye_R_Look_R"],
|
|
1188
1225
|
center: []
|
|
1189
1226
|
},
|
|
1190
1227
|
63: {
|
|
1191
|
-
left: [],
|
|
1192
|
-
right: [],
|
|
1193
|
-
center: [
|
|
1228
|
+
left: ["Eye_L_Look_Up"],
|
|
1229
|
+
right: ["Eye_R_Look_Up"],
|
|
1230
|
+
center: []
|
|
1194
1231
|
},
|
|
1195
1232
|
64: {
|
|
1196
|
-
left: [],
|
|
1197
|
-
right: [],
|
|
1198
|
-
center: [
|
|
1233
|
+
left: ["Eye_L_Look_Down"],
|
|
1234
|
+
right: ["Eye_R_Look_Down"],
|
|
1235
|
+
center: []
|
|
1199
1236
|
},
|
|
1200
1237
|
65: {
|
|
1201
1238
|
left: ["Eye_L_Look_L"],
|
|
@@ -1423,6 +1460,14 @@ var BONE_AU_TO_BINDINGS = {
|
|
|
1423
1460
|
{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 20, side: "left" },
|
|
1424
1461
|
{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 20, side: "right" }
|
|
1425
1462
|
],
|
|
1463
|
+
65: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 25, side: "left" }],
|
|
1464
|
+
66: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 25, side: "left" }],
|
|
1465
|
+
67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 20, side: "left" }],
|
|
1466
|
+
68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 20, side: "left" }],
|
|
1467
|
+
69: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 25, side: "right" }],
|
|
1468
|
+
70: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 25, side: "right" }],
|
|
1469
|
+
71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 20, side: "right" }],
|
|
1470
|
+
72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 20, side: "right" }],
|
|
1426
1471
|
// Tongue controls (optional, for rigs that expose them)
|
|
1427
1472
|
37: [{ node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 20 }],
|
|
1428
1473
|
38: [{ node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 20 }],
|
|
@@ -1510,18 +1555,18 @@ var COMPOSITE_ROTATIONS = [
|
|
|
1510
1555
|
},
|
|
1511
1556
|
{
|
|
1512
1557
|
node: "EYE_L",
|
|
1513
|
-
pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
|
|
1558
|
+
pitch: { aus: [64, 63, 68, 67], axis: "rx", negative: [64, 68], positive: [63, 67] },
|
|
1514
1559
|
// Eyes down/up
|
|
1515
|
-
yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
|
|
1560
|
+
yaw: { aus: [61, 62, 65, 66], axis: "rz", negative: [61, 65], positive: [62, 66] },
|
|
1516
1561
|
// Eyes left/right (rz for CC4)
|
|
1517
1562
|
roll: null
|
|
1518
1563
|
// Eyes don't have roll
|
|
1519
1564
|
},
|
|
1520
1565
|
{
|
|
1521
1566
|
node: "EYE_R",
|
|
1522
|
-
pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
|
|
1567
|
+
pitch: { aus: [64, 63, 72, 71], axis: "rx", negative: [64, 72], positive: [63, 71] },
|
|
1523
1568
|
// Eyes down/up
|
|
1524
|
-
yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
|
|
1569
|
+
yaw: { aus: [61, 62, 69, 70], axis: "rz", negative: [61, 69], positive: [62, 70] },
|
|
1525
1570
|
// Eyes left/right (rz for CC4)
|
|
1526
1571
|
roll: null
|
|
1527
1572
|
// Eyes don't have roll
|
|
@@ -1540,9 +1585,17 @@ var CONTINUUM_PAIRS_MAP = {
|
|
|
1540
1585
|
// Eyes horizontal - both eyes share same AUs (yaw maps to rz via COMPOSITE_ROTATIONS)
|
|
1541
1586
|
61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
|
|
1542
1587
|
62: { pairId: 61, isNegative: false, axis: "yaw", node: "EYE_L" },
|
|
1588
|
+
65: { pairId: 66, isNegative: true, axis: "yaw", node: "EYE_L" },
|
|
1589
|
+
66: { pairId: 65, isNegative: false, axis: "yaw", node: "EYE_L" },
|
|
1590
|
+
69: { pairId: 70, isNegative: true, axis: "yaw", node: "EYE_R" },
|
|
1591
|
+
70: { pairId: 69, isNegative: false, axis: "yaw", node: "EYE_R" },
|
|
1543
1592
|
// Eyes vertical (pitch)
|
|
1544
1593
|
64: { pairId: 63, isNegative: true, axis: "pitch", node: "EYE_L" },
|
|
1545
1594
|
63: { pairId: 64, isNegative: false, axis: "pitch", node: "EYE_L" },
|
|
1595
|
+
68: { pairId: 67, isNegative: true, axis: "pitch", node: "EYE_L" },
|
|
1596
|
+
67: { pairId: 68, isNegative: false, axis: "pitch", node: "EYE_L" },
|
|
1597
|
+
72: { pairId: 71, isNegative: true, axis: "pitch", node: "EYE_R" },
|
|
1598
|
+
71: { pairId: 72, isNegative: false, axis: "pitch", node: "EYE_R" },
|
|
1546
1599
|
// Head yaw (turn left/right)
|
|
1547
1600
|
51: { pairId: 52, isNegative: true, axis: "yaw", node: "HEAD" },
|
|
1548
1601
|
52: { pairId: 51, isNegative: false, axis: "yaw", node: "HEAD" },
|
|
@@ -1575,6 +1628,10 @@ var CONTINUUM_PAIRS_MAP = {
|
|
|
1575
1628
|
var CONTINUUM_LABELS = {
|
|
1576
1629
|
"61-62": "Eyes \u2014 Horizontal",
|
|
1577
1630
|
"64-63": "Eyes \u2014 Vertical",
|
|
1631
|
+
"65-66": "Left Eye \u2014 Horizontal",
|
|
1632
|
+
"68-67": "Left Eye \u2014 Vertical",
|
|
1633
|
+
"69-70": "Right Eye \u2014 Horizontal",
|
|
1634
|
+
"72-71": "Right Eye \u2014 Vertical",
|
|
1578
1635
|
"51-52": "Head \u2014 Horizontal",
|
|
1579
1636
|
"54-53": "Head \u2014 Vertical",
|
|
1580
1637
|
"55-56": "Head \u2014 Tilt",
|
|
@@ -1763,6 +1820,12 @@ var MORPH_TO_MESH = {
|
|
|
1763
1820
|
tongue: ["CC_Base_Tongue", "CC_Base_Tongue_1"],
|
|
1764
1821
|
hair: ["Side_part_wavy", "Side_part_wavy_1", "Side_part_wavy_2"]
|
|
1765
1822
|
};
|
|
1823
|
+
var AU_FACEPART_TO_MESH_CATEGORY = {
|
|
1824
|
+
Eye: "eye",
|
|
1825
|
+
Eyes: "eye",
|
|
1826
|
+
Eyelids: "eye",
|
|
1827
|
+
Tongue: "tongue"
|
|
1828
|
+
};
|
|
1766
1829
|
var CC4_HAIR_PHYSICS = {
|
|
1767
1830
|
stiffness: 7.5,
|
|
1768
1831
|
damping: 0.18,
|
|
@@ -1808,7 +1871,9 @@ var CC4_PRESET = {
|
|
|
1808
1871
|
bonePrefix: CC4_BONE_PREFIX,
|
|
1809
1872
|
suffixPattern: CC4_SUFFIX_PATTERN,
|
|
1810
1873
|
morphToMesh: MORPH_TO_MESH,
|
|
1874
|
+
auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
|
|
1811
1875
|
visemeKeys: VISEME_KEYS,
|
|
1876
|
+
visemeMeshCategory: "viseme",
|
|
1812
1877
|
visemeJawAmounts: VISEME_JAW_AMOUNTS,
|
|
1813
1878
|
auMixDefaults: AU_MIX_DEFAULTS,
|
|
1814
1879
|
auInfo: AU_INFO,
|
|
@@ -2128,9 +2193,10 @@ var HairPhysicsController = class {
|
|
|
2128
2193
|
if (meshName) {
|
|
2129
2194
|
targetMesh = this.registeredHairObjects.get(meshName);
|
|
2130
2195
|
} else {
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2196
|
+
const hairMeshNames = this.getHairMeshNames();
|
|
2197
|
+
for (const name of hairMeshNames) {
|
|
2198
|
+
const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
|
|
2199
|
+
if (mesh) {
|
|
2134
2200
|
targetMesh = mesh;
|
|
2135
2201
|
break;
|
|
2136
2202
|
}
|
|
@@ -2224,6 +2290,15 @@ var HairPhysicsController = class {
|
|
|
2224
2290
|
}
|
|
2225
2291
|
getHairMeshNames() {
|
|
2226
2292
|
if (this.cachedHairMeshNames) return this.cachedHairMeshNames;
|
|
2293
|
+
if (typeof this.host.getSelectedHairMeshNames === "function") {
|
|
2294
|
+
const selectedHairMeshNames = this.host.getSelectedHairMeshNames() || [];
|
|
2295
|
+
const resolved = selectedHairMeshNames.filter((name) => {
|
|
2296
|
+
const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
|
|
2297
|
+
return !!mesh;
|
|
2298
|
+
});
|
|
2299
|
+
this.cachedHairMeshNames = Array.from(new Set(resolved));
|
|
2300
|
+
return this.cachedHairMeshNames;
|
|
2301
|
+
}
|
|
2227
2302
|
const names = [];
|
|
2228
2303
|
this.registeredHairObjects.forEach((mesh, name) => {
|
|
2229
2304
|
const info = CC4_MESHES[name];
|
|
@@ -2239,6 +2314,18 @@ var HairPhysicsController = class {
|
|
|
2239
2314
|
this.cachedHairMeshNames = names;
|
|
2240
2315
|
return names;
|
|
2241
2316
|
}
|
|
2317
|
+
refreshMeshSelection() {
|
|
2318
|
+
this.cachedHairMeshNames = null;
|
|
2319
|
+
this.idleClipDirty = true;
|
|
2320
|
+
this.gravityClipDirty = true;
|
|
2321
|
+
this.impulseClipDirty = true;
|
|
2322
|
+
this.warnMissingHairMorphTargets();
|
|
2323
|
+
if (this.hairPhysicsEnabled) {
|
|
2324
|
+
this.startIdleClip();
|
|
2325
|
+
this.startGravityClip();
|
|
2326
|
+
this.buildImpulseClips();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2242
2329
|
supportsMixerClips() {
|
|
2243
2330
|
return typeof this.host.buildClip === "function";
|
|
2244
2331
|
}
|
|
@@ -2253,7 +2340,11 @@ var HairPhysicsController = class {
|
|
|
2253
2340
|
return;
|
|
2254
2341
|
}
|
|
2255
2342
|
const hairMeshNames = this.getHairMeshNames();
|
|
2256
|
-
if (hairMeshNames.length === 0)
|
|
2343
|
+
if (hairMeshNames.length === 0) {
|
|
2344
|
+
this.stopIdleClip();
|
|
2345
|
+
this.idleClipDirty = false;
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2257
2348
|
if (!this.idleClipDirty && this.idleClipHandle) return;
|
|
2258
2349
|
this.stopIdleClip();
|
|
2259
2350
|
const duration = Math.max(0.5, cfg.idleClipDuration);
|
|
@@ -2271,7 +2362,11 @@ var HairPhysicsController = class {
|
|
|
2271
2362
|
startGravityClip() {
|
|
2272
2363
|
if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
|
|
2273
2364
|
const hairMeshNames = this.getHairMeshNames();
|
|
2274
|
-
if (hairMeshNames.length === 0)
|
|
2365
|
+
if (hairMeshNames.length === 0) {
|
|
2366
|
+
this.stopGravityClip();
|
|
2367
|
+
this.gravityClipDirty = false;
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2275
2370
|
if (!this.gravityClipDirty && this.gravityClipHandle) return;
|
|
2276
2371
|
this.stopGravityClip();
|
|
2277
2372
|
const morphTargets = this.hairPhysicsConfig.morphTargets;
|
|
@@ -2324,7 +2419,11 @@ var HairPhysicsController = class {
|
|
|
2324
2419
|
buildImpulseClips() {
|
|
2325
2420
|
if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
|
|
2326
2421
|
const hairMeshNames = this.getHairMeshNames();
|
|
2327
|
-
if (hairMeshNames.length === 0)
|
|
2422
|
+
if (hairMeshNames.length === 0) {
|
|
2423
|
+
this.stopImpulseClips();
|
|
2424
|
+
this.impulseClipDirty = false;
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2328
2427
|
if (!this.impulseClipDirty && this.impulseClips.left && this.impulseClips.right && this.impulseClips.front) {
|
|
2329
2428
|
return;
|
|
2330
2429
|
}
|
|
@@ -2660,7 +2759,9 @@ function resolveProfile(base, override) {
|
|
|
2660
2759
|
auToBones: mergeRecord(base.auToBones, override.auToBones),
|
|
2661
2760
|
boneNodes: mergeRecord(base.boneNodes, override.boneNodes),
|
|
2662
2761
|
morphToMesh: mergeRecord(base.morphToMesh, override.morphToMesh),
|
|
2762
|
+
auFacePartToMeshCategory: base.auFacePartToMeshCategory || override.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, override.auFacePartToMeshCategory || {}) : void 0,
|
|
2663
2763
|
visemeKeys: override.visemeKeys ? [...override.visemeKeys] : [...base.visemeKeys],
|
|
2764
|
+
visemeMeshCategory: override.visemeMeshCategory ?? base.visemeMeshCategory,
|
|
2664
2765
|
auMixDefaults: base.auMixDefaults || override.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, override.auMixDefaults || {}) : void 0,
|
|
2665
2766
|
auInfo: base.auInfo || override.auInfo ? mergeRecord(base.auInfo || {}, override.auInfo || {}) : void 0,
|
|
2666
2767
|
eyeMeshNodes: override.eyeMeshNodes ?? base.eyeMeshNodes,
|
|
@@ -3498,6 +3599,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3498
3599
|
this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
|
|
3499
3600
|
this.hairPhysics = new HairPhysicsController({
|
|
3500
3601
|
getMeshByName: (name) => this.meshByName.get(name),
|
|
3602
|
+
getSelectedHairMeshNames: () => this.config.morphToMesh?.hair || [],
|
|
3501
3603
|
buildClip: (clipName, curves, options) => this.buildClip(clipName, curves, options),
|
|
3502
3604
|
cleanupSnippet: (name) => this.cleanupSnippet(name)
|
|
3503
3605
|
});
|
|
@@ -3506,6 +3608,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3506
3608
|
getMeshes: () => this.meshes,
|
|
3507
3609
|
getMeshByName: (name) => this.meshByName.get(name),
|
|
3508
3610
|
getMeshNamesForAU: (auId) => this.getMeshNamesForAU(auId),
|
|
3611
|
+
getMeshNamesForViseme: () => this.getMeshNamesForViseme(),
|
|
3509
3612
|
getBones: () => this.bones,
|
|
3510
3613
|
getConfig: () => this.config,
|
|
3511
3614
|
getCompositeRotations: () => this.compositeRotations,
|
|
@@ -3602,7 +3705,8 @@ var _Loom3 = class _Loom3 {
|
|
|
3602
3705
|
}
|
|
3603
3706
|
for (let i = 0; i < (this.config.visemeKeys || []).length; i += 1) {
|
|
3604
3707
|
const key = this.config.visemeKeys[i];
|
|
3605
|
-
const
|
|
3708
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
3709
|
+
const targets = typeof key === "number" ? this.resolveMorphTargetsByIndex(key, visemeMeshNames) : this.resolveMorphTargets(key, visemeMeshNames);
|
|
3606
3710
|
this.resolvedVisemeTargets[i] = targets;
|
|
3607
3711
|
}
|
|
3608
3712
|
}
|
|
@@ -3772,37 +3876,12 @@ var _Loom3 = class _Loom3 {
|
|
|
3772
3876
|
}
|
|
3773
3877
|
const compositeInfo = this.auToCompositeMap.get(id);
|
|
3774
3878
|
if (compositeInfo) {
|
|
3775
|
-
const storedBalance = this.auBalances[id] ?? 0;
|
|
3776
|
-
const { left: leftVal, right: rightVal } = this.computeSideValues(clamp012(v), storedBalance);
|
|
3777
3879
|
for (const nodeKey of compositeInfo.nodes) {
|
|
3778
3880
|
const config = this.compositeRotations.find((c) => c.node === nodeKey);
|
|
3779
3881
|
if (!config) continue;
|
|
3780
3882
|
const axisConfig = config[compositeInfo.axis];
|
|
3781
3883
|
if (!axisConfig) continue;
|
|
3782
|
-
|
|
3783
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
3784
|
-
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
3785
|
-
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
3786
|
-
axisValue = posValue - negValue;
|
|
3787
|
-
} else if (axisConfig.aus.length > 1) {
|
|
3788
|
-
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
3789
|
-
} else {
|
|
3790
|
-
axisValue = v;
|
|
3791
|
-
}
|
|
3792
|
-
const auBoneBindings = this.config.auToBones[id] || [];
|
|
3793
|
-
const sideByNode = /* @__PURE__ */ new Map();
|
|
3794
|
-
for (const binding of auBoneBindings) {
|
|
3795
|
-
if (binding.side === "left" || binding.side === "right") {
|
|
3796
|
-
sideByNode.set(binding.node, binding.side);
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
const side = sideByNode.get(nodeKey);
|
|
3800
|
-
if (side) {
|
|
3801
|
-
const baseValue = clamp012(v);
|
|
3802
|
-
const balanceValue = side === "left" ? leftVal : rightVal;
|
|
3803
|
-
const denom = baseValue > 0 ? baseValue : 1;
|
|
3804
|
-
axisValue = axisValue * (balanceValue / denom);
|
|
3805
|
-
}
|
|
3884
|
+
const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
|
|
3806
3885
|
this.updateBoneRotation(nodeKey, compositeInfo.axis, axisValue);
|
|
3807
3886
|
this.pendingCompositeNodes.add(nodeKey);
|
|
3808
3887
|
}
|
|
@@ -3830,11 +3909,15 @@ var _Loom3 = class _Loom3 {
|
|
|
3830
3909
|
}
|
|
3831
3910
|
}
|
|
3832
3911
|
const target = clamp012(to);
|
|
3912
|
+
if (balance !== void 0) {
|
|
3913
|
+
this.auBalances[numId] = balance;
|
|
3914
|
+
}
|
|
3915
|
+
const storedBalance = this.auBalances[numId] ?? 0;
|
|
3833
3916
|
const { left: leftKeys, right: rightKeys, center: centerKeys } = this.getAUMorphsBySide(numId);
|
|
3834
3917
|
const bindings = this.config.auToBones[numId] || [];
|
|
3835
3918
|
const mixWeight = this.isMixedAU(numId) ? this.getAUMixWeight(numId) : 1;
|
|
3836
3919
|
const base = target * mixWeight;
|
|
3837
|
-
const { left: leftVal, right: rightVal } = this.computeSideValues(base,
|
|
3920
|
+
const { left: leftVal, right: rightVal } = this.computeSideValues(base, storedBalance);
|
|
3838
3921
|
this.auValues[numId] = target;
|
|
3839
3922
|
const handles = [];
|
|
3840
3923
|
const meshNames = this.getMeshNamesForAU(numId);
|
|
@@ -3862,16 +3945,7 @@ var _Loom3 = class _Loom3 {
|
|
|
3862
3945
|
if (!config) continue;
|
|
3863
3946
|
const axisConfig = config[compositeInfo.axis];
|
|
3864
3947
|
if (!axisConfig) continue;
|
|
3865
|
-
|
|
3866
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
3867
|
-
const negValue = this.auValues[axisConfig.negative] ?? 0;
|
|
3868
|
-
const posValue = this.auValues[axisConfig.positive] ?? 0;
|
|
3869
|
-
axisValue = posValue - negValue;
|
|
3870
|
-
} else if (axisConfig.aus.length > 1) {
|
|
3871
|
-
axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
|
|
3872
|
-
} else {
|
|
3873
|
-
axisValue = target;
|
|
3874
|
-
}
|
|
3948
|
+
const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
|
|
3875
3949
|
handles.push(this.transitionBoneRotation(nodeKey, compositeInfo.axis, axisValue, durationMs));
|
|
3876
3950
|
}
|
|
3877
3951
|
}
|
|
@@ -4034,10 +4108,11 @@ var _Loom3 = class _Loom3 {
|
|
|
4034
4108
|
this.applyMorphTargets(targets, val);
|
|
4035
4109
|
} else {
|
|
4036
4110
|
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
4111
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
4037
4112
|
if (typeof morphKey === "number") {
|
|
4038
|
-
this.setMorphInfluence(morphKey, val);
|
|
4113
|
+
this.setMorphInfluence(morphKey, val, visemeMeshNames);
|
|
4039
4114
|
} else if (typeof morphKey === "string") {
|
|
4040
|
-
this.setMorph(morphKey, val);
|
|
4115
|
+
this.setMorph(morphKey, val, visemeMeshNames);
|
|
4041
4116
|
}
|
|
4042
4117
|
}
|
|
4043
4118
|
const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
|
|
@@ -4055,7 +4130,8 @@ var _Loom3 = class _Loom3 {
|
|
|
4055
4130
|
const morphKey = this.config.visemeKeys[visemeIndex];
|
|
4056
4131
|
const target = clamp012(to);
|
|
4057
4132
|
this.visemeValues[visemeIndex] = target;
|
|
4058
|
-
const
|
|
4133
|
+
const visemeMeshNames = this.getMeshNamesForViseme();
|
|
4134
|
+
const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
|
|
4059
4135
|
const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
|
|
4060
4136
|
if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
|
|
4061
4137
|
return morphHandle;
|
|
@@ -4359,6 +4435,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4359
4435
|
if (this.model) {
|
|
4360
4436
|
this.rebuildMorphTargetsCache();
|
|
4361
4437
|
}
|
|
4438
|
+
this.hairPhysics.refreshMeshSelection();
|
|
4362
4439
|
this.applyHairPhysicsProfileConfig();
|
|
4363
4440
|
}
|
|
4364
4441
|
getProfile() {
|
|
@@ -4366,20 +4443,24 @@ var _Loom3 = class _Loom3 {
|
|
|
4366
4443
|
}
|
|
4367
4444
|
/**
|
|
4368
4445
|
* Get the mesh names that should receive morph influences for a given AU.
|
|
4369
|
-
*
|
|
4446
|
+
* Routing is driven by `auFacePartToMeshCategory` in profile config.
|
|
4370
4447
|
*/
|
|
4371
4448
|
getMeshNamesForAU(auId) {
|
|
4372
4449
|
const m = this.config.morphToMesh;
|
|
4373
4450
|
const info = this.config.auInfo?.[String(auId)];
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
default:
|
|
4381
|
-
return m?.face || [];
|
|
4451
|
+
const facePart = info?.facePart;
|
|
4452
|
+
if (facePart) {
|
|
4453
|
+
const category = this.config.auFacePartToMeshCategory?.[facePart];
|
|
4454
|
+
if (category) {
|
|
4455
|
+
return m?.[category] || [];
|
|
4456
|
+
}
|
|
4382
4457
|
}
|
|
4458
|
+
return m?.face || [];
|
|
4459
|
+
}
|
|
4460
|
+
getMeshNamesForViseme() {
|
|
4461
|
+
const m = this.config.morphToMesh;
|
|
4462
|
+
const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
|
|
4463
|
+
return m?.[category] || m?.face || [];
|
|
4383
4464
|
}
|
|
4384
4465
|
// ============================================================================
|
|
4385
4466
|
// HAIR PHYSICS
|
|
@@ -4498,6 +4579,25 @@ var _Loom3 = class _Loom3 {
|
|
|
4498
4579
|
const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
|
|
4499
4580
|
return !!(hasMorphs && this.config.auToBones[id]?.length);
|
|
4500
4581
|
}
|
|
4582
|
+
getEffectiveBoneAUValue(auId, nodeKey) {
|
|
4583
|
+
const rawValue = clamp012(this.auValues[auId] ?? 0);
|
|
4584
|
+
if (rawValue <= 1e-6) return 0;
|
|
4585
|
+
const binding = this.config.auToBones[auId]?.find((candidate) => candidate.node === nodeKey) ?? null;
|
|
4586
|
+
if (!binding?.side) return rawValue;
|
|
4587
|
+
return rawValue * getSideScale(this.auBalances[auId] ?? 0, binding.side);
|
|
4588
|
+
}
|
|
4589
|
+
getCompositeAxisValueForNode(nodeKey, axisConfig) {
|
|
4590
|
+
return getCompositeAxisValue(axisConfig, (auId) => this.getEffectiveBoneAUValue(auId, nodeKey));
|
|
4591
|
+
}
|
|
4592
|
+
getCompositeAxisBindingForNode(nodeKey, axisConfig, direction) {
|
|
4593
|
+
return getCompositeAxisBinding(
|
|
4594
|
+
nodeKey,
|
|
4595
|
+
axisConfig,
|
|
4596
|
+
direction,
|
|
4597
|
+
(auId) => this.getEffectiveBoneAUValue(auId, nodeKey),
|
|
4598
|
+
this.config.auToBones
|
|
4599
|
+
);
|
|
4600
|
+
}
|
|
4501
4601
|
initBoneRotations() {
|
|
4502
4602
|
this.rotations = {};
|
|
4503
4603
|
this.pendingCompositeNodes.clear();
|
|
@@ -4573,30 +4673,10 @@ var _Loom3 = class _Loom3 {
|
|
|
4573
4673
|
if (!config) {
|
|
4574
4674
|
return;
|
|
4575
4675
|
}
|
|
4576
|
-
const getBindingForAxis = (axisConfig, direction) => {
|
|
4577
|
-
if (!axisConfig) return null;
|
|
4578
|
-
if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
|
|
4579
|
-
const auId = direction < 0 ? axisConfig.negative : axisConfig.positive;
|
|
4580
|
-
return this.config.auToBones[auId]?.find((b) => b.node === nodeKey);
|
|
4581
|
-
}
|
|
4582
|
-
if (axisConfig.aus.length > 1) {
|
|
4583
|
-
let maxAU = axisConfig.aus[0];
|
|
4584
|
-
let maxValue = this.auValues[maxAU] ?? 0;
|
|
4585
|
-
for (const auId of axisConfig.aus) {
|
|
4586
|
-
const val = this.auValues[auId] ?? 0;
|
|
4587
|
-
if (val > maxValue) {
|
|
4588
|
-
maxValue = val;
|
|
4589
|
-
maxAU = auId;
|
|
4590
|
-
}
|
|
4591
|
-
}
|
|
4592
|
-
return this.config.auToBones[maxAU]?.find((b) => b.node === nodeKey);
|
|
4593
|
-
}
|
|
4594
|
-
return this.config.auToBones[axisConfig.aus[0]]?.find((b) => b.node === nodeKey);
|
|
4595
|
-
};
|
|
4596
4676
|
const getAxis = (channel) => channel === "rx" ? X_AXIS2 : channel === "ry" ? Y_AXIS2 : Z_AXIS2;
|
|
4597
4677
|
const compositeQ = new Quaternion().copy(baseQuat);
|
|
4598
4678
|
if (config.yaw && rotState.yaw !== 0) {
|
|
4599
|
-
const binding =
|
|
4679
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.yaw, rotState.yaw);
|
|
4600
4680
|
if (binding?.maxDegrees && binding.channel) {
|
|
4601
4681
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.yaw) * binding.scale;
|
|
4602
4682
|
const axis = getAxis(binding.channel);
|
|
@@ -4605,7 +4685,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4605
4685
|
}
|
|
4606
4686
|
}
|
|
4607
4687
|
if (config.pitch && rotState.pitch !== 0) {
|
|
4608
|
-
const binding =
|
|
4688
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.pitch, rotState.pitch);
|
|
4609
4689
|
if (binding?.maxDegrees && binding.channel) {
|
|
4610
4690
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.pitch) * binding.scale;
|
|
4611
4691
|
const axis = getAxis(binding.channel);
|
|
@@ -4614,7 +4694,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4614
4694
|
}
|
|
4615
4695
|
}
|
|
4616
4696
|
if (config.roll && rotState.roll !== 0) {
|
|
4617
|
-
const binding =
|
|
4697
|
+
const binding = this.getCompositeAxisBindingForNode(nodeKey, config.roll, rotState.roll);
|
|
4618
4698
|
if (binding?.maxDegrees && binding.channel) {
|
|
4619
4699
|
const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.roll) * binding.scale;
|
|
4620
4700
|
const axis = getAxis(binding.channel);
|
|
@@ -5483,7 +5563,7 @@ function findMatches(targetNames, candidateNames, prefix, suffix, suffixPattern)
|
|
|
5483
5563
|
return { found, missing };
|
|
5484
5564
|
}
|
|
5485
5565
|
function collectAxisConfigs(axisConfigs) {
|
|
5486
|
-
return axisConfigs.filter((entry) => entry.config);
|
|
5566
|
+
return axisConfigs.filter((entry) => entry.config !== null);
|
|
5487
5567
|
}
|
|
5488
5568
|
function isEyeNodeKey(nodeKey) {
|
|
5489
5569
|
return nodeKey === "EYE_L" || nodeKey === "EYE_R";
|
|
@@ -5553,28 +5633,35 @@ function validateMappingConfig(config) {
|
|
|
5553
5633
|
);
|
|
5554
5634
|
continue;
|
|
5555
5635
|
}
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5636
|
+
for (const auId of toAUList(axisConfig.negative)) {
|
|
5637
|
+
if (!axisConfig.aus.includes(auId)) {
|
|
5638
|
+
push(
|
|
5639
|
+
"error",
|
|
5640
|
+
"COMPOSITE_AU_MISSING",
|
|
5641
|
+
`Composite axis for "${composite.node}" is missing negative AU ${auId} in aus list`,
|
|
5642
|
+
{ node: composite.node, auId }
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5563
5645
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5646
|
+
for (const auId of toAUList(axisConfig.positive)) {
|
|
5647
|
+
if (!axisConfig.aus.includes(auId)) {
|
|
5648
|
+
push(
|
|
5649
|
+
"error",
|
|
5650
|
+
"COMPOSITE_AU_MISSING",
|
|
5651
|
+
`Composite axis for "${composite.node}" is missing positive AU ${auId} in aus list`,
|
|
5652
|
+
{ node: composite.node, auId }
|
|
5653
|
+
);
|
|
5654
|
+
}
|
|
5571
5655
|
}
|
|
5572
|
-
|
|
5656
|
+
const negativeAUs = toAUList(axisConfig.negative);
|
|
5657
|
+
const positiveAUs = toAUList(axisConfig.positive);
|
|
5658
|
+
const overlappingAUs = negativeAUs.filter((auId) => positiveAUs.includes(auId));
|
|
5659
|
+
if (overlappingAUs.length > 0) {
|
|
5573
5660
|
push(
|
|
5574
5661
|
"error",
|
|
5575
5662
|
"COMPOSITE_AU_DUPLICATE",
|
|
5576
|
-
`Composite axis for "${composite.node}"
|
|
5577
|
-
{ node: composite.node, auId:
|
|
5663
|
+
`Composite axis for "${composite.node}" reuses AU ${overlappingAUs[0]} in both negative and positive groups`,
|
|
5664
|
+
{ node: composite.node, auId: overlappingAUs[0] }
|
|
5578
5665
|
);
|
|
5579
5666
|
}
|
|
5580
5667
|
}
|
|
@@ -5664,11 +5751,11 @@ function validateMappingConfig(config) {
|
|
|
5664
5751
|
);
|
|
5665
5752
|
continue;
|
|
5666
5753
|
}
|
|
5667
|
-
const expectedNeg = axisConfig.negative;
|
|
5668
|
-
const expectedPos = axisConfig.positive;
|
|
5754
|
+
const expectedNeg = toAUList(axisConfig.negative);
|
|
5755
|
+
const expectedPos = toAUList(axisConfig.positive);
|
|
5669
5756
|
const negId = info.isNegative ? Number(auIdStr) : info.pairId;
|
|
5670
5757
|
const posId = info.isNegative ? info.pairId : Number(auIdStr);
|
|
5671
|
-
if (negId
|
|
5758
|
+
if (!expectedNeg.includes(negId) || !expectedPos.includes(posId)) {
|
|
5672
5759
|
push(
|
|
5673
5760
|
"warning",
|
|
5674
5761
|
"CONTINUUM_COMPOSITE_MISMATCH",
|