@lovelace_lol/loom3 1.0.4 → 1.0.7

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
@@ -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]));
@@ -533,43 +575,23 @@ var BakedAnimationController = class {
533
575
  Object.keys(curves).filter(isNumericAU).map((id) => Number(id))
534
576
  );
535
577
  const getAxisBinding = (nodeKey, axisConfig, axisValue, t) => {
536
- if (!axisConfig) return null;
537
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
538
- const auId2 = axisValue < 0 ? axisConfig.negative : axisConfig.positive;
539
- return config.auToBones[auId2]?.find((b) => b.node === nodeKey) ?? null;
540
- }
541
- if (axisConfig.aus.length > 1) {
542
- let maxAU = axisConfig.aus[0];
543
- let maxVal = sampleCurve(String(maxAU), t);
544
- for (const auId2 of axisConfig.aus) {
545
- const val = sampleCurve(String(auId2), t);
546
- if (val > maxVal) {
547
- maxVal = val;
548
- maxAU = auId2;
549
- }
550
- }
551
- return config.auToBones[maxAU]?.find((b) => b.node === nodeKey) ?? null;
552
- }
553
- const auId = axisConfig.aus[0];
554
- 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
+ );
555
585
  };
556
- const getAxisValue = (axisConfig, t) => {
557
- if (!axisConfig) return 0;
558
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
559
- const posValue = sampleCurve(String(axisConfig.positive), t);
560
- const negValue = sampleCurve(String(axisConfig.negative), t);
561
- return posValue - negValue;
562
- }
563
- if (axisConfig.aus.length > 1) {
564
- let maxVal = 0;
565
- for (const auId of axisConfig.aus) {
566
- const val = sampleCurve(String(auId), t);
567
- if (val > maxVal) maxVal = val;
568
- }
569
- return maxVal;
570
- }
571
- 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);
572
593
  };
594
+ const getAxisValue = (nodeKey, axisConfig, t) => getCompositeAxisValue(axisConfig, (auId) => getAxisSampleForNode(auId, nodeKey, t));
573
595
  const autoVisemeJawHandledJaw = autoVisemeJaw && jawScale > 0 && visemeJawAmounts && options?.snippetCategory === "visemeSnippet";
574
596
  for (const composite of compositeRotations) {
575
597
  const nodeKey = composite.node;
@@ -590,7 +612,7 @@ var BakedAnimationController = class {
590
612
  const compositeQ = new Quaternion().copy(entry.baseQuat);
591
613
  const applyAxis = (axisConfig) => {
592
614
  if (!axisConfig) return;
593
- let axisValue = getAxisValue(axisConfig, t);
615
+ let axisValue = getAxisValue(nodeKey, axisConfig, t);
594
616
  if (Math.abs(axisValue) <= 1e-6) return;
595
617
  const binding = getAxisBinding(nodeKey, axisConfig, axisValue, t);
596
618
  if (!binding?.maxDegrees || !binding.channel) return;
@@ -1193,24 +1215,24 @@ var AU_TO_MORPHS = {
1193
1215
  center: []
1194
1216
  },
1195
1217
  61: {
1196
- left: ["Eye_L_Look_L", "Eye_R_Look_L"],
1197
- right: [],
1218
+ left: ["Eye_L_Look_L"],
1219
+ right: ["Eye_R_Look_L"],
1198
1220
  center: []
1199
1221
  },
1200
1222
  62: {
1201
- left: [],
1202
- right: ["Eye_L_Look_R", "Eye_R_Look_R"],
1223
+ left: ["Eye_L_Look_R"],
1224
+ right: ["Eye_R_Look_R"],
1203
1225
  center: []
1204
1226
  },
1205
1227
  63: {
1206
- left: [],
1207
- right: [],
1208
- center: ["Eye_L_Look_Up", "Eye_R_Look_Up"]
1228
+ left: ["Eye_L_Look_Up"],
1229
+ right: ["Eye_R_Look_Up"],
1230
+ center: []
1209
1231
  },
1210
1232
  64: {
1211
- left: [],
1212
- right: [],
1213
- center: ["Eye_L_Look_Down", "Eye_R_Look_Down"]
1233
+ left: ["Eye_L_Look_Down"],
1234
+ right: ["Eye_R_Look_Down"],
1235
+ center: []
1214
1236
  },
1215
1237
  65: {
1216
1238
  left: ["Eye_L_Look_L"],
@@ -1438,6 +1460,14 @@ var BONE_AU_TO_BINDINGS = {
1438
1460
  { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 20, side: "left" },
1439
1461
  { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 20, side: "right" }
1440
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" }],
1441
1471
  // Tongue controls (optional, for rigs that expose them)
1442
1472
  37: [{ node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 20 }],
1443
1473
  38: [{ node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 20 }],
@@ -1525,18 +1555,18 @@ var COMPOSITE_ROTATIONS = [
1525
1555
  },
1526
1556
  {
1527
1557
  node: "EYE_L",
1528
- 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] },
1529
1559
  // Eyes down/up
1530
- 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] },
1531
1561
  // Eyes left/right (rz for CC4)
1532
1562
  roll: null
1533
1563
  // Eyes don't have roll
1534
1564
  },
1535
1565
  {
1536
1566
  node: "EYE_R",
1537
- 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] },
1538
1568
  // Eyes down/up
1539
- 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] },
1540
1570
  // Eyes left/right (rz for CC4)
1541
1571
  roll: null
1542
1572
  // Eyes don't have roll
@@ -1555,9 +1585,17 @@ var CONTINUUM_PAIRS_MAP = {
1555
1585
  // Eyes horizontal - both eyes share same AUs (yaw maps to rz via COMPOSITE_ROTATIONS)
1556
1586
  61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
1557
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" },
1558
1592
  // Eyes vertical (pitch)
1559
1593
  64: { pairId: 63, isNegative: true, axis: "pitch", node: "EYE_L" },
1560
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" },
1561
1599
  // Head yaw (turn left/right)
1562
1600
  51: { pairId: 52, isNegative: true, axis: "yaw", node: "HEAD" },
1563
1601
  52: { pairId: 51, isNegative: false, axis: "yaw", node: "HEAD" },
@@ -1590,6 +1628,10 @@ var CONTINUUM_PAIRS_MAP = {
1590
1628
  var CONTINUUM_LABELS = {
1591
1629
  "61-62": "Eyes \u2014 Horizontal",
1592
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",
1593
1635
  "51-52": "Head \u2014 Horizontal",
1594
1636
  "54-53": "Head \u2014 Vertical",
1595
1637
  "55-56": "Head \u2014 Tilt",
@@ -3834,37 +3876,12 @@ var _Loom3 = class _Loom3 {
3834
3876
  }
3835
3877
  const compositeInfo = this.auToCompositeMap.get(id);
3836
3878
  if (compositeInfo) {
3837
- const storedBalance = this.auBalances[id] ?? 0;
3838
- const { left: leftVal, right: rightVal } = this.computeSideValues(clamp012(v), storedBalance);
3839
3879
  for (const nodeKey of compositeInfo.nodes) {
3840
3880
  const config = this.compositeRotations.find((c) => c.node === nodeKey);
3841
3881
  if (!config) continue;
3842
3882
  const axisConfig = config[compositeInfo.axis];
3843
3883
  if (!axisConfig) continue;
3844
- let axisValue;
3845
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
3846
- const negValue = this.auValues[axisConfig.negative] ?? 0;
3847
- const posValue = this.auValues[axisConfig.positive] ?? 0;
3848
- axisValue = posValue - negValue;
3849
- } else if (axisConfig.aus.length > 1) {
3850
- axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
3851
- } else {
3852
- axisValue = v;
3853
- }
3854
- const auBoneBindings = this.config.auToBones[id] || [];
3855
- const sideByNode = /* @__PURE__ */ new Map();
3856
- for (const binding of auBoneBindings) {
3857
- if (binding.side === "left" || binding.side === "right") {
3858
- sideByNode.set(binding.node, binding.side);
3859
- }
3860
- }
3861
- const side = sideByNode.get(nodeKey);
3862
- if (side) {
3863
- const baseValue = clamp012(v);
3864
- const balanceValue = side === "left" ? leftVal : rightVal;
3865
- const denom = baseValue > 0 ? baseValue : 1;
3866
- axisValue = axisValue * (balanceValue / denom);
3867
- }
3884
+ const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
3868
3885
  this.updateBoneRotation(nodeKey, compositeInfo.axis, axisValue);
3869
3886
  this.pendingCompositeNodes.add(nodeKey);
3870
3887
  }
@@ -3892,11 +3909,15 @@ var _Loom3 = class _Loom3 {
3892
3909
  }
3893
3910
  }
3894
3911
  const target = clamp012(to);
3912
+ if (balance !== void 0) {
3913
+ this.auBalances[numId] = balance;
3914
+ }
3915
+ const storedBalance = this.auBalances[numId] ?? 0;
3895
3916
  const { left: leftKeys, right: rightKeys, center: centerKeys } = this.getAUMorphsBySide(numId);
3896
3917
  const bindings = this.config.auToBones[numId] || [];
3897
3918
  const mixWeight = this.isMixedAU(numId) ? this.getAUMixWeight(numId) : 1;
3898
3919
  const base = target * mixWeight;
3899
- const { left: leftVal, right: rightVal } = this.computeSideValues(base, balance);
3920
+ const { left: leftVal, right: rightVal } = this.computeSideValues(base, storedBalance);
3900
3921
  this.auValues[numId] = target;
3901
3922
  const handles = [];
3902
3923
  const meshNames = this.getMeshNamesForAU(numId);
@@ -3924,16 +3945,7 @@ var _Loom3 = class _Loom3 {
3924
3945
  if (!config) continue;
3925
3946
  const axisConfig = config[compositeInfo.axis];
3926
3947
  if (!axisConfig) continue;
3927
- let axisValue;
3928
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
3929
- const negValue = this.auValues[axisConfig.negative] ?? 0;
3930
- const posValue = this.auValues[axisConfig.positive] ?? 0;
3931
- axisValue = posValue - negValue;
3932
- } else if (axisConfig.aus.length > 1) {
3933
- axisValue = Math.max(...axisConfig.aus.map((auId) => this.auValues[auId] ?? 0));
3934
- } else {
3935
- axisValue = target;
3936
- }
3948
+ const axisValue = this.getCompositeAxisValueForNode(nodeKey, axisConfig);
3937
3949
  handles.push(this.transitionBoneRotation(nodeKey, compositeInfo.axis, axisValue, durationMs));
3938
3950
  }
3939
3951
  }
@@ -4567,6 +4579,25 @@ var _Loom3 = class _Loom3 {
4567
4579
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
4568
4580
  return !!(hasMorphs && this.config.auToBones[id]?.length);
4569
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
+ }
4570
4601
  initBoneRotations() {
4571
4602
  this.rotations = {};
4572
4603
  this.pendingCompositeNodes.clear();
@@ -4642,30 +4673,10 @@ var _Loom3 = class _Loom3 {
4642
4673
  if (!config) {
4643
4674
  return;
4644
4675
  }
4645
- const getBindingForAxis = (axisConfig, direction) => {
4646
- if (!axisConfig) return null;
4647
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0) {
4648
- const auId = direction < 0 ? axisConfig.negative : axisConfig.positive;
4649
- return this.config.auToBones[auId]?.find((b) => b.node === nodeKey);
4650
- }
4651
- if (axisConfig.aus.length > 1) {
4652
- let maxAU = axisConfig.aus[0];
4653
- let maxValue = this.auValues[maxAU] ?? 0;
4654
- for (const auId of axisConfig.aus) {
4655
- const val = this.auValues[auId] ?? 0;
4656
- if (val > maxValue) {
4657
- maxValue = val;
4658
- maxAU = auId;
4659
- }
4660
- }
4661
- return this.config.auToBones[maxAU]?.find((b) => b.node === nodeKey);
4662
- }
4663
- return this.config.auToBones[axisConfig.aus[0]]?.find((b) => b.node === nodeKey);
4664
- };
4665
4676
  const getAxis = (channel) => channel === "rx" ? X_AXIS2 : channel === "ry" ? Y_AXIS2 : Z_AXIS2;
4666
4677
  const compositeQ = new Quaternion().copy(baseQuat);
4667
4678
  if (config.yaw && rotState.yaw !== 0) {
4668
- const binding = getBindingForAxis(config.yaw, rotState.yaw);
4679
+ const binding = this.getCompositeAxisBindingForNode(nodeKey, config.yaw, rotState.yaw);
4669
4680
  if (binding?.maxDegrees && binding.channel) {
4670
4681
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.yaw) * binding.scale;
4671
4682
  const axis = getAxis(binding.channel);
@@ -4674,7 +4685,7 @@ var _Loom3 = class _Loom3 {
4674
4685
  }
4675
4686
  }
4676
4687
  if (config.pitch && rotState.pitch !== 0) {
4677
- const binding = getBindingForAxis(config.pitch, rotState.pitch);
4688
+ const binding = this.getCompositeAxisBindingForNode(nodeKey, config.pitch, rotState.pitch);
4678
4689
  if (binding?.maxDegrees && binding.channel) {
4679
4690
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.pitch) * binding.scale;
4680
4691
  const axis = getAxis(binding.channel);
@@ -4683,7 +4694,7 @@ var _Loom3 = class _Loom3 {
4683
4694
  }
4684
4695
  }
4685
4696
  if (config.roll && rotState.roll !== 0) {
4686
- const binding = getBindingForAxis(config.roll, rotState.roll);
4697
+ const binding = this.getCompositeAxisBindingForNode(nodeKey, config.roll, rotState.roll);
4687
4698
  if (binding?.maxDegrees && binding.channel) {
4688
4699
  const radians = deg2rad(binding.maxDegrees) * Math.abs(rotState.roll) * binding.scale;
4689
4700
  const axis = getAxis(binding.channel);
@@ -5552,7 +5563,7 @@ function findMatches(targetNames, candidateNames, prefix, suffix, suffixPattern)
5552
5563
  return { found, missing };
5553
5564
  }
5554
5565
  function collectAxisConfigs(axisConfigs) {
5555
- return axisConfigs.filter((entry) => entry.config);
5566
+ return axisConfigs.filter((entry) => entry.config !== null);
5556
5567
  }
5557
5568
  function isEyeNodeKey(nodeKey) {
5558
5569
  return nodeKey === "EYE_L" || nodeKey === "EYE_R";
@@ -5622,28 +5633,35 @@ function validateMappingConfig(config) {
5622
5633
  );
5623
5634
  continue;
5624
5635
  }
5625
- if (axisConfig.negative !== void 0 && !axisConfig.aus.includes(axisConfig.negative)) {
5626
- push(
5627
- "error",
5628
- "COMPOSITE_AU_MISSING",
5629
- `Composite axis for "${composite.node}" is missing negative AU ${axisConfig.negative} in aus list`,
5630
- { node: composite.node, auId: axisConfig.negative }
5631
- );
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
+ }
5632
5645
  }
5633
- if (axisConfig.positive !== void 0 && !axisConfig.aus.includes(axisConfig.positive)) {
5634
- push(
5635
- "error",
5636
- "COMPOSITE_AU_MISSING",
5637
- `Composite axis for "${composite.node}" is missing positive AU ${axisConfig.positive} in aus list`,
5638
- { node: composite.node, auId: axisConfig.positive }
5639
- );
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
+ }
5640
5655
  }
5641
- if (axisConfig.negative !== void 0 && axisConfig.positive !== void 0 && axisConfig.negative === axisConfig.positive) {
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) {
5642
5660
  push(
5643
5661
  "error",
5644
5662
  "COMPOSITE_AU_DUPLICATE",
5645
- `Composite axis for "${composite.node}" has identical negative/positive AU ${axisConfig.negative}`,
5646
- { node: composite.node, auId: axisConfig.negative }
5663
+ `Composite axis for "${composite.node}" reuses AU ${overlappingAUs[0]} in both negative and positive groups`,
5664
+ { node: composite.node, auId: overlappingAUs[0] }
5647
5665
  );
5648
5666
  }
5649
5667
  }
@@ -5733,11 +5751,11 @@ function validateMappingConfig(config) {
5733
5751
  );
5734
5752
  continue;
5735
5753
  }
5736
- const expectedNeg = axisConfig.negative;
5737
- const expectedPos = axisConfig.positive;
5754
+ const expectedNeg = toAUList(axisConfig.negative);
5755
+ const expectedPos = toAUList(axisConfig.positive);
5738
5756
  const negId = info.isNegative ? Number(auIdStr) : info.pairId;
5739
5757
  const posId = info.isNegative ? info.pairId : Number(auIdStr);
5740
- if (negId !== expectedNeg || posId !== expectedPos) {
5758
+ if (!expectedNeg.includes(negId) || !expectedPos.includes(posId)) {
5741
5759
  push(
5742
5760
  "warning",
5743
5761
  "CONTINUUM_COMPOSITE_MISMATCH",