loomlarge 0.2.1 → 1.0.1

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
@@ -241,119 +241,121 @@ var VISEME_KEYS = [
241
241
  ];
242
242
  var BONE_AU_TO_BINDINGS = {
243
243
  // Head turn and tilt (M51-M56) - use HEAD bone only (NECK should not rotate with head)
244
- // Three.js Y rotation: positive = counter-clockwise from above = head turns LEFT (character POV)
245
244
  51: [
246
- { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
245
+ { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30, axis: "yaw" }
247
246
  // Head turn left
248
247
  ],
249
248
  52: [
250
- { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30 }
249
+ { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30, axis: "yaw" }
251
250
  // Head turn right
252
251
  ],
253
252
  53: [
254
- { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20 }
253
+ { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20, axis: "pitch" }
255
254
  // Head up
256
255
  ],
257
256
  54: [
258
- { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20 }
257
+ { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20, axis: "pitch" }
259
258
  // Head down
260
259
  ],
261
260
  55: [
262
- { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15 }
261
+ { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15, axis: "roll" }
263
262
  // Head tilt left
264
263
  ],
265
264
  56: [
266
- { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
265
+ { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15, axis: "roll" }
267
266
  // Head tilt right
268
267
  ],
269
268
  // Eyes horizontal (yaw) - CC4 rigs use rz for horizontal eye rotation
270
269
  61: [
271
- { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
270
+ { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" },
272
271
  // Eyes look left
273
- { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32 }
272
+ { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" }
274
273
  ],
275
274
  62: [
276
- { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32 },
275
+ { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" },
277
276
  // Eyes look right
278
- { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32 }
277
+ { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" }
279
278
  ],
280
279
  63: [
281
- { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
282
- { node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32 }
280
+ { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32, axis: "pitch" },
281
+ // Eyes Up
282
+ { node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32, axis: "pitch" }
283
283
  ],
284
284
  64: [
285
- { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
286
- { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32 }
285
+ { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32, axis: "pitch" },
286
+ // Eyes Down
287
+ { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32, axis: "pitch" }
287
288
  ],
288
289
  // Single-eye (Left) — horizontal (rz for CC4) and vertical (rx)
289
- 65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15 }],
290
- 66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15 }],
291
- 67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12 }],
292
- 68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12 }],
290
+ 65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15, axis: "yaw" }],
291
+ 66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15, axis: "yaw" }],
292
+ 67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12, axis: "pitch" }],
293
+ // Left Eye Up
294
+ 68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12, axis: "pitch" }],
295
+ // Left Eye Down
293
296
  // Single-eye (Right) — horizontal (rz for CC4) and vertical (rx)
294
- 69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15 }],
295
- 70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15 }],
296
- 71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12 }],
297
- 72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12 }],
297
+ 69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15, axis: "yaw" }],
298
+ 70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15, axis: "yaw" }],
299
+ 71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12, axis: "pitch" }],
300
+ // Right Eye Up
301
+ 72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12, axis: "pitch" }],
302
+ // Right Eye Down
298
303
  // Jaw / Mouth
299
304
  8: [
300
305
  // Lips Toward Each Other - slight jaw open helps sell the lip press
301
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8 }
302
- // Small downward rotation (jaw opening slightly)
306
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8, axis: "roll" }
303
307
  ],
304
308
  25: [
305
309
  // Lips Part — small jaw open
306
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
307
- // 73% of 8
310
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84, axis: "roll" }
308
311
  ],
309
312
  26: [
310
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
311
- // 73% of 20
313
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28, axis: "roll" }
312
314
  ],
313
315
  27: [
314
316
  // Mouth Stretch — larger jaw open
315
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
316
- // 73% of 25
317
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32, axis: "roll" }
317
318
  ],
318
319
  29: [
319
320
  { node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
320
- // Negative for forward thrust
321
+ // Translation - no axis needed
321
322
  ],
322
323
  30: [
323
324
  // Jaw Left
324
- { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5 }
325
+ { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5, axis: "yaw" }
325
326
  ],
326
327
  35: [
327
328
  // Jaw Right
328
- { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5 }
329
+ { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5, axis: "yaw" }
329
330
  ],
330
331
  // Tongue
331
332
  19: [
332
333
  { node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
334
+ // Translation - no axis needed
333
335
  ],
334
336
  37: [
335
337
  // Tongue Up
336
- { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45 }
338
+ { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45, axis: "pitch" }
337
339
  ],
338
340
  38: [
339
341
  // Tongue Down
340
- { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45 }
342
+ { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45, axis: "pitch" }
341
343
  ],
342
344
  39: [
343
345
  // Tongue Left
344
- { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10 }
346
+ { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10, axis: "yaw" }
345
347
  ],
346
348
  40: [
347
349
  // Tongue Right
348
- { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10 }
350
+ { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10, axis: "yaw" }
349
351
  ],
350
352
  41: [
351
353
  // Tongue Tilt Left
352
- { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20 }
354
+ { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20, axis: "roll" }
353
355
  ],
354
356
  42: [
355
357
  // Tongue Tilt Right
356
- { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
358
+ { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20, axis: "roll" }
357
359
  ]
358
360
  };
359
361
  var isMixedAU = (id) => !!(AU_TO_MORPHS[id]?.length && BONE_AU_TO_BINDINGS[id]?.length);
@@ -795,8 +797,9 @@ var _LoomLargeThree = class _LoomLargeThree {
795
797
  if (bindings) {
796
798
  for (const binding of bindings) {
797
799
  if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
798
- const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
799
- this.updateBoneRotation(binding.node, axis, v * binding.scale, binding.maxDegrees ?? 0);
800
+ if (binding.axis) {
801
+ this.updateBoneRotation(binding.node, binding.axis, v * binding.scale, binding.maxDegrees ?? 0);
802
+ }
800
803
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
801
804
  if (binding.maxUnits !== void 0) {
802
805
  this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
@@ -834,8 +837,9 @@ var _LoomLargeThree = class _LoomLargeThree {
834
837
  }
835
838
  for (const binding of bindings) {
836
839
  if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
837
- const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
838
- handles.push(this.transitionBoneRotation(binding.node, axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
840
+ if (binding.axis) {
841
+ handles.push(this.transitionBoneRotation(binding.node, binding.axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
842
+ }
839
843
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
840
844
  if (binding.maxUnits !== void 0) {
841
845
  handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
@@ -847,11 +851,16 @@ var _LoomLargeThree = class _LoomLargeThree {
847
851
  getAU(id) {
848
852
  return this.auValues[id] ?? 0;
849
853
  }
850
- // ============================================================================
851
- // MORPH CONTROL
852
- // ============================================================================
853
- setMorph(key, v, meshNames) {
854
+ setMorph(key, v, meshNamesOrTargets) {
854
855
  const val = clamp01(v);
856
+ if (Array.isArray(meshNamesOrTargets) && meshNamesOrTargets.length > 0 && typeof meshNamesOrTargets[0] === "object" && "infl" in meshNamesOrTargets[0]) {
857
+ const targets2 = meshNamesOrTargets;
858
+ for (const target of targets2) {
859
+ target.infl[target.idx] = val;
860
+ }
861
+ return;
862
+ }
863
+ const meshNames = meshNamesOrTargets;
855
864
  const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
856
865
  const cached = this.morphCache.get(key);
857
866
  if (cached) {
@@ -890,11 +899,54 @@ var _LoomLargeThree = class _LoomLargeThree {
890
899
  this.morphCache.set(key, targets);
891
900
  }
892
901
  }
902
+ /**
903
+ * Resolve morph key to direct targets for ultra-fast repeated access.
904
+ * Use this when you need to set the same morph many times (e.g., in animation loops).
905
+ */
906
+ resolveMorphTargets(key, meshNames) {
907
+ const cached = this.morphCache.get(key);
908
+ if (cached) return cached;
909
+ const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
910
+ const targets = [];
911
+ if (targetMeshes.length) {
912
+ for (const name of targetMeshes) {
913
+ const mesh = this.meshByName.get(name);
914
+ if (!mesh) continue;
915
+ const dict = mesh.morphTargetDictionary;
916
+ const infl = mesh.morphTargetInfluences;
917
+ if (!dict || !infl) continue;
918
+ const idx = dict[key];
919
+ if (idx !== void 0) {
920
+ targets.push({ infl, idx });
921
+ }
922
+ }
923
+ } else {
924
+ for (const mesh of this.meshes) {
925
+ const dict = mesh.morphTargetDictionary;
926
+ const infl = mesh.morphTargetInfluences;
927
+ if (!dict || !infl) continue;
928
+ const idx = dict[key];
929
+ if (idx !== void 0) {
930
+ targets.push({ infl, idx });
931
+ }
932
+ }
933
+ }
934
+ if (targets.length > 0) {
935
+ this.morphCache.set(key, targets);
936
+ }
937
+ return targets;
938
+ }
893
939
  transitionMorph(key, to, durationMs = 120, meshNames) {
894
940
  const transitionKey = `morph_${key}`;
895
941
  const from = this.getMorphValue(key);
896
942
  const target = clamp01(to);
897
- return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.setMorph(key, value, meshNames));
943
+ const targets = this.resolveMorphTargets(key, meshNames);
944
+ return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => {
945
+ const val = clamp01(value);
946
+ for (const t of targets) {
947
+ t.infl[t.idx] = val;
948
+ }
949
+ });
898
950
  }
899
951
  // ============================================================================
900
952
  // VISEME CONTROL
@@ -997,6 +1049,33 @@ var _LoomLargeThree = class _LoomLargeThree {
997
1049
  });
998
1050
  return result;
999
1051
  }
1052
+ /** Get all morph targets grouped by mesh name */
1053
+ getMorphTargets() {
1054
+ const result = {};
1055
+ for (const mesh of this.meshes) {
1056
+ const dict = mesh.morphTargetDictionary;
1057
+ if (dict) {
1058
+ result[mesh.name] = Object.keys(dict).sort();
1059
+ }
1060
+ }
1061
+ return result;
1062
+ }
1063
+ /** Get all resolved bone names and their current transforms */
1064
+ getBones() {
1065
+ const result = {};
1066
+ for (const name of Object.keys(this.bones)) {
1067
+ const entry = this.bones[name];
1068
+ if (entry) {
1069
+ const pos = entry.obj.position;
1070
+ const rot = entry.obj.rotation;
1071
+ result[name] = {
1072
+ position: [pos.x, pos.y, pos.z],
1073
+ rotation: [rot.x * 180 / Math.PI, rot.y * 180 / Math.PI, rot.z * 180 / Math.PI]
1074
+ };
1075
+ }
1076
+ }
1077
+ return result;
1078
+ }
1000
1079
  setMeshVisible(meshName, visible) {
1001
1080
  if (!this.model) return;
1002
1081
  this.model.traverse((obj) => {
@@ -1005,6 +1084,70 @@ var _LoomLargeThree = class _LoomLargeThree {
1005
1084
  }
1006
1085
  });
1007
1086
  }
1087
+ /** Get material config for a mesh */
1088
+ getMeshMaterialConfig(meshName) {
1089
+ if (!this.model) return null;
1090
+ let result = null;
1091
+ this.model.traverse((obj) => {
1092
+ if (obj.isMesh && obj.name === meshName) {
1093
+ const mat = obj.material;
1094
+ if (mat) {
1095
+ let blendingName = "Normal";
1096
+ for (const [name, value] of Object.entries(_LoomLargeThree.BLENDING_MODES)) {
1097
+ if (mat.blending === value) {
1098
+ blendingName = name;
1099
+ break;
1100
+ }
1101
+ }
1102
+ result = {
1103
+ renderOrder: obj.renderOrder,
1104
+ transparent: mat.transparent,
1105
+ opacity: mat.opacity,
1106
+ depthWrite: mat.depthWrite,
1107
+ depthTest: mat.depthTest,
1108
+ blending: blendingName
1109
+ };
1110
+ }
1111
+ }
1112
+ });
1113
+ return result;
1114
+ }
1115
+ /** Set material config for a mesh */
1116
+ setMeshMaterialConfig(meshName, config) {
1117
+ if (!this.model) return;
1118
+ this.model.traverse((obj) => {
1119
+ if (obj.isMesh && obj.name === meshName) {
1120
+ const mat = obj.material;
1121
+ if (config.renderOrder !== void 0) {
1122
+ obj.renderOrder = config.renderOrder;
1123
+ }
1124
+ if (mat) {
1125
+ if (config.opacity !== void 0) {
1126
+ mat.opacity = config.opacity;
1127
+ if (config.opacity < 1 && config.transparent === void 0) {
1128
+ mat.transparent = true;
1129
+ }
1130
+ }
1131
+ if (config.transparent !== void 0) {
1132
+ mat.transparent = config.transparent;
1133
+ }
1134
+ if (config.depthWrite !== void 0) {
1135
+ mat.depthWrite = config.depthWrite;
1136
+ }
1137
+ if (config.depthTest !== void 0) {
1138
+ mat.depthTest = config.depthTest;
1139
+ }
1140
+ if (config.blending !== void 0) {
1141
+ const blendValue = _LoomLargeThree.BLENDING_MODES[config.blending];
1142
+ if (blendValue !== void 0) {
1143
+ mat.blending = blendValue;
1144
+ }
1145
+ }
1146
+ mat.needsUpdate = true;
1147
+ }
1148
+ }
1149
+ });
1150
+ }
1008
1151
  // ============================================================================
1009
1152
  // CONFIGURATION
1010
1153
  // ============================================================================
@@ -1196,6 +1339,13 @@ var _LoomLargeThree = class _LoomLargeThree {
1196
1339
  if (typeof settings.depthTest === "boolean") {
1197
1340
  obj.material.depthTest = settings.depthTest;
1198
1341
  }
1342
+ if (typeof settings.blending === "string") {
1343
+ const blendValue = _LoomLargeThree.BLENDING_MODES[settings.blending];
1344
+ if (blendValue !== void 0) {
1345
+ obj.material.blending = blendValue;
1346
+ }
1347
+ }
1348
+ obj.material.needsUpdate = true;
1199
1349
  }
1200
1350
  });
1201
1351
  }
@@ -1219,6 +1369,19 @@ __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
1219
1369
  0.4
1220
1370
  ]);
1221
1371
  __publicField(_LoomLargeThree, "JAW_MAX_DEGREES", 28);
1372
+ /** Blending mode options for Three.js materials */
1373
+ __publicField(_LoomLargeThree, "BLENDING_MODES", {
1374
+ "Normal": 1,
1375
+ // THREE.NormalBlending
1376
+ "Additive": 2,
1377
+ // THREE.AdditiveBlending
1378
+ "Subtractive": 3,
1379
+ // THREE.SubtractiveBlending
1380
+ "Multiply": 4,
1381
+ // THREE.MultiplyBlending
1382
+ "None": 0
1383
+ // THREE.NoBlending
1384
+ });
1222
1385
  var LoomLargeThree = _LoomLargeThree;
1223
1386
  function collectMorphMeshes(root) {
1224
1387
  const meshes = [];
@@ -1232,6 +1395,20 @@ function collectMorphMeshes(root) {
1232
1395
  return meshes;
1233
1396
  }
1234
1397
 
1398
+ // src/mappings/types.ts
1399
+ var BLENDING_MODES = {
1400
+ "Normal": 1,
1401
+ // THREE.NormalBlending
1402
+ "Additive": 2,
1403
+ // THREE.AdditiveBlending
1404
+ "Subtractive": 3,
1405
+ // THREE.SubtractiveBlending
1406
+ "Multiply": 4,
1407
+ // THREE.MultiplyBlending
1408
+ "None": 0
1409
+ // THREE.NoBlending
1410
+ };
1411
+
1235
1412
  // src/physics/HairPhysics.ts
1236
1413
  var DEFAULT_HAIR_PHYSICS_CONFIG = {
1237
1414
  mass: 1,
@@ -1356,6 +1533,6 @@ var HairPhysics = class {
1356
1533
  }
1357
1534
  };
1358
1535
 
1359
- export { AU_INFO, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, HairPhysics, LoomLargeThree, MORPH_TO_MESH, VISEME_KEYS, collectMorphMeshes, LoomLargeThree as default, hasLeftRightMorphs, isMixedAU };
1536
+ export { AU_INFO, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, HairPhysics, LoomLargeThree, MORPH_TO_MESH, VISEME_KEYS, collectMorphMeshes, LoomLargeThree as default, hasLeftRightMorphs, isMixedAU };
1360
1537
  //# sourceMappingURL=index.js.map
1361
1538
  //# sourceMappingURL=index.js.map