@lovelace_lol/loom3 1.0.11 → 1.0.13
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 +113 -42
- package/dist/index.cjs +25 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Loom3 provides mappings that connect [Facial Action Coding System (FACS)](https:
|
|
|
27
27
|
13. [Playback & State Control](#13-playback--state-control)
|
|
28
28
|
14. [Hair Physics](#14-hair-physics)
|
|
29
29
|
15. [Baked Animations](#15-baked-animations)
|
|
30
|
+
16. [API Reference](#16-api-reference)
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -81,7 +82,7 @@ loader.load('/character.glb', (gltf) => {
|
|
|
81
82
|
// This drives all transitions and animations
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
If you’re implementing a custom renderer, target the `
|
|
85
|
+
If you’re implementing a custom renderer, target the `Loom3` interface exported from `@lovelace_lol/loom3` (legacy alias: `LoomLarge`).
|
|
85
86
|
|
|
86
87
|
### Quick start examples
|
|
87
88
|
|
|
@@ -162,26 +163,31 @@ import { CC4_PRESET } from '@lovelace_lol/loom3';
|
|
|
162
163
|
},
|
|
163
164
|
|
|
164
165
|
boneNodes: {
|
|
165
|
-
// Logical bone name →
|
|
166
|
-
'HEAD': '
|
|
167
|
-
'JAW': '
|
|
168
|
-
'EYE_L': '
|
|
169
|
-
'EYE_R': '
|
|
170
|
-
'TONGUE': '
|
|
166
|
+
// Logical bone name → base node name used with bonePrefix
|
|
167
|
+
'HEAD': 'Head',
|
|
168
|
+
'JAW': 'JawRoot',
|
|
169
|
+
'EYE_L': 'L_Eye',
|
|
170
|
+
'EYE_R': 'R_Eye',
|
|
171
|
+
'TONGUE': 'Tongue01',
|
|
171
172
|
},
|
|
172
173
|
|
|
174
|
+
bonePrefix: 'CC_Base_',
|
|
175
|
+
suffixPattern: '_\\d+$|\\.\\d+$',
|
|
176
|
+
|
|
173
177
|
visemeKeys: [
|
|
174
178
|
// 15 viseme morph names for lip-sync
|
|
175
|
-
'
|
|
176
|
-
'
|
|
177
|
-
'
|
|
179
|
+
'EE', 'Ah', 'Oh', 'OO', 'I',
|
|
180
|
+
'U', 'W', 'L', 'F_V', 'Th',
|
|
181
|
+
'S_Z', 'B_M_P', 'K_G_H_NG', 'AE', 'R'
|
|
178
182
|
],
|
|
179
183
|
|
|
180
184
|
morphToMesh: {
|
|
181
185
|
// Routes morph categories to specific meshes
|
|
182
186
|
'face': ['CC_Base_Body'],
|
|
187
|
+
'viseme': ['CC_Base_Body', 'CC_Base_Body_1'],
|
|
183
188
|
'tongue': ['CC_Base_Tongue'],
|
|
184
|
-
'eye': ['
|
|
189
|
+
'eye': ['CC_Base_EyeOcclusion_1', 'CC_Base_EyeOcclusion_2'],
|
|
190
|
+
'hair': ['Side_part_wavy_1', 'Side_part_wavy_2'],
|
|
185
191
|
},
|
|
186
192
|
|
|
187
193
|
auMixDefaults: {
|
|
@@ -203,6 +209,25 @@ import { CC4_PRESET } from '@lovelace_lol/loom3';
|
|
|
203
209
|
}
|
|
204
210
|
```
|
|
205
211
|
|
|
212
|
+
### Name resolution and profile fields
|
|
213
|
+
|
|
214
|
+
The runtime resolves bone nodes by composing `bonePrefix + boneNodes[key] + boneSuffix`, then falling back to suffix-pattern matching when a model uses numbered exports such as `_01` or `.001`. The same prefix/suffix rules are used by validation and correction helpers, which is why `CC4_PRESET` can keep base bone names like `Head` and `JawRoot` instead of repeating the full node names everywhere.
|
|
215
|
+
|
|
216
|
+
For region and marker configs, `resolveBoneName()` treats any mapped bone name that already contains `_` or `.` as a fully qualified name and skips prefix/suffix composition.
|
|
217
|
+
|
|
218
|
+
Two caveats are worth calling out:
|
|
219
|
+
- `morphPrefix` and `morphSuffix` are part of `Profile`, but morph playback still resolves exact morph keys on the targeted meshes today. They are already used by validation and correction helpers.
|
|
220
|
+
- `leftMorphSuffixes` and `rightMorphSuffixes` are profile metadata for laterality detection in tooling, not core runtime behavior.
|
|
221
|
+
|
|
222
|
+
Other `Profile` fields that are easy to miss:
|
|
223
|
+
- `morphToMesh` routes categories such as `face`, `viseme`, `eye`, `tongue`, and `hair` to specific mesh names.
|
|
224
|
+
- `eyeMeshNodes` provides fallback eye nodes when a rig uses meshes instead of bones for eye control.
|
|
225
|
+
- `auMixDefaults` sets the default morph/bone blend weight per AU.
|
|
226
|
+
- `compositeRotations` defines the per-node pitch/yaw/roll axis layout used by the composite rotation system.
|
|
227
|
+
- `continuumPairs` and `continuumLabels` describe bidirectional AU pairs and their UI labels.
|
|
228
|
+
- `annotationRegions` defines the regions used by the marker and camera tooling.
|
|
229
|
+
- `hairPhysics` stores the mixer-driven hair defaults, including direction signs and morph target mappings.
|
|
230
|
+
|
|
206
231
|
### Passing a preset to Loom3
|
|
207
232
|
|
|
208
233
|
```typescript
|
|
@@ -319,6 +344,7 @@ Loom3 includes validation and analysis helpers so you can verify presets against
|
|
|
319
344
|
|
|
320
345
|
```typescript
|
|
321
346
|
import {
|
|
347
|
+
extractFromGLTF,
|
|
322
348
|
extractModelData,
|
|
323
349
|
analyzeModel,
|
|
324
350
|
validateMappings,
|
|
@@ -327,16 +353,41 @@ import {
|
|
|
327
353
|
} from '@lovelace_lol/loom3';
|
|
328
354
|
|
|
329
355
|
const preset = resolvePreset('cc4');
|
|
330
|
-
const modelData = extractModelData(
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
const
|
|
356
|
+
const modelData = extractModelData(model, meshes, animations);
|
|
357
|
+
const gltfData = extractFromGLTF(gltf); // Same ModelData shape, one-step GLTF wrapper
|
|
358
|
+
|
|
359
|
+
const analysis = await analyzeModel({
|
|
360
|
+
source: { type: 'gltf', gltf },
|
|
361
|
+
preset,
|
|
362
|
+
suggestCorrections: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Validate against lower-level mesh + skeleton inputs when you already have them
|
|
366
|
+
const validation = validateMappings(meshes, skeleton, preset, { suggestCorrections: true });
|
|
367
|
+
const corrections = generateMappingCorrections(meshes, skeleton, preset, { useResolvedNames: true });
|
|
334
368
|
```
|
|
335
369
|
|
|
370
|
+
If you already have a `ModelData` bundle, `analyzeModel()` is the higher-level path; `validateMappings()` and `generateMappingCorrections()` are intentionally lower-level mesh/skeleton helpers.
|
|
371
|
+
|
|
336
372
|
Use these helpers to:
|
|
337
|
-
-
|
|
338
|
-
-
|
|
339
|
-
-
|
|
373
|
+
- Extract raw model facts with `extractModelData(model, meshes?, animations?)` or `extractFromGLTF(gltf)`
|
|
374
|
+
- Validate a preset against mesh/skeleton data with `validateMappings(meshes, skeleton, preset, options)`
|
|
375
|
+
- Generate best-effort fixes with `generateMappingCorrections(meshes, skeleton, preset, options)`
|
|
376
|
+
- Run a single end-to-end pass with `analyzeModel({ source, preset, suggestCorrections })`
|
|
377
|
+
|
|
378
|
+
`validateMappings()` returns a `ValidationResult` with:
|
|
379
|
+
- `valid` and `score`
|
|
380
|
+
- `missingMorphs`, `missingBones`, `foundMorphs`, `foundBones`
|
|
381
|
+
- `missingMeshes`, `foundMeshes`, `unmappedMorphs`, `unmappedBones`, `unmappedMeshes`
|
|
382
|
+
- `warnings`
|
|
383
|
+
- optional `suggestedConfig`, `corrections`, and `unresolved` when suggestion mode is enabled
|
|
384
|
+
|
|
385
|
+
`generateMappingCorrections()` returns:
|
|
386
|
+
- `correctedConfig`
|
|
387
|
+
- `corrections`
|
|
388
|
+
- `unresolved`
|
|
389
|
+
|
|
390
|
+
`analyzeModel()` returns a `ModelAnalysisReport` containing the extracted model data, optional validation results, animation summary, `overallScore`, and a plain-language `summary`.
|
|
340
391
|
|
|
341
392
|
### Controlling mesh visibility
|
|
342
393
|
|
|
@@ -387,16 +438,16 @@ This is especially useful for:
|
|
|
387
438
|
|
|
388
439
|
## 4. Extending & Custom Presets
|
|
389
440
|
|
|
390
|
-

|
|
391
442
|
|
|
392
443
|
### Extending an existing preset
|
|
393
444
|
|
|
394
|
-
Use `
|
|
445
|
+
Use `resolveProfile` to override specific mappings while keeping the rest:
|
|
395
446
|
|
|
396
447
|
```typescript
|
|
397
|
-
import { CC4_PRESET,
|
|
448
|
+
import { CC4_PRESET, resolveProfile } from '@lovelace_lol/loom3';
|
|
398
449
|
|
|
399
|
-
const MY_PRESET =
|
|
450
|
+
const MY_PRESET = resolveProfile(CC4_PRESET, {
|
|
400
451
|
|
|
401
452
|
// Override AU12 (smile) with custom morph names
|
|
402
453
|
auToMorphs: {
|
|
@@ -420,7 +471,7 @@ const loom = new Loom3({ profile: MY_PRESET });
|
|
|
420
471
|
### Creating a preset from scratch
|
|
421
472
|
|
|
422
473
|
```typescript
|
|
423
|
-
import { Profile } from '@lovelace_lol/loom3';
|
|
474
|
+
import type { Profile } from '@lovelace_lol/loom3';
|
|
424
475
|
|
|
425
476
|
const CUSTOM_PRESET: Profile = {
|
|
426
477
|
auToMorphs: {
|
|
@@ -1075,19 +1126,21 @@ Visemes are mouth shapes used for lip-sync. Loom3 includes 15 visemes with autom
|
|
|
1075
1126
|
|
|
1076
1127
|
### The 15 visemes
|
|
1077
1128
|
|
|
1129
|
+
The `VISEME_KEYS` export uses unprefixed keys in this order.
|
|
1130
|
+
|
|
1078
1131
|
| Index | Key | Phoneme Example |
|
|
1079
1132
|
|-------|-----|-----------------|
|
|
1080
1133
|
| 0 | EE | "b**ee**" |
|
|
1081
|
-
| 1 |
|
|
1082
|
-
| 2 |
|
|
1083
|
-
| 3 |
|
|
1084
|
-
| 4 |
|
|
1085
|
-
| 5 |
|
|
1086
|
-
| 6 |
|
|
1087
|
-
| 7 |
|
|
1134
|
+
| 1 | Ah | "f**a**ther" |
|
|
1135
|
+
| 2 | Oh | "g**o**" |
|
|
1136
|
+
| 3 | OO | "t**oo**" |
|
|
1137
|
+
| 4 | I | "s**i**t" |
|
|
1138
|
+
| 5 | U | "fl**u**te" |
|
|
1139
|
+
| 6 | W | "**w**e" |
|
|
1140
|
+
| 7 | L | "**l**ip" |
|
|
1088
1141
|
| 8 | F_V | "**f**un, **v**an" |
|
|
1089
|
-
| 9 |
|
|
1090
|
-
| 10 |
|
|
1142
|
+
| 9 | Th | "**th**ink" |
|
|
1143
|
+
| 10 | S_Z | "**s**un, **z**oo" |
|
|
1091
1144
|
| 11 | B_M_P | "**b**at, **m**an, **p**op" |
|
|
1092
1145
|
| 12 | K_G_H_NG | "**k**ite, **g**o, **h**at, si**ng**" |
|
|
1093
1146
|
| 13 | AE | "c**a**t" |
|
|
@@ -1117,16 +1170,7 @@ loom.transitionViseme(3, 1.0, 80, 0);
|
|
|
1117
1170
|
|
|
1118
1171
|
### Automatic jaw coupling
|
|
1119
1172
|
|
|
1120
|
-
Each viseme has a predefined jaw opening amount. When you set a viseme, the jaw automatically opens proportionally:
|
|
1121
|
-
|
|
1122
|
-
| Viseme | Jaw Amount |
|
|
1123
|
-
|--------|------------|
|
|
1124
|
-
| EE | 0.15 |
|
|
1125
|
-
| Ah | 0.70 |
|
|
1126
|
-
| Oh | 0.50 |
|
|
1127
|
-
| B_M_P | 0.20 |
|
|
1128
|
-
|
|
1129
|
-
The `jawScale` parameter multiplies this amount:
|
|
1173
|
+
Each viseme has a predefined jaw opening amount in the preset. When you set a viseme, the jaw automatically opens proportionally, and the `jawScale` parameter multiplies that amount:
|
|
1130
1174
|
- `jawScale = 1.0`: Normal jaw opening
|
|
1131
1175
|
- `jawScale = 0.5`: Half jaw opening
|
|
1132
1176
|
- `jawScale = 0`: No jaw movement (viseme only)
|
|
@@ -1653,6 +1697,33 @@ loom.transitionAU(45, 1.0, 100); // Blink
|
|
|
1653
1697
|
|
|
1654
1698
|
---
|
|
1655
1699
|
|
|
1700
|
+
## 16. API Reference
|
|
1701
|
+
|
|
1702
|
+
This is a compact reference for the public surface exported by `@lovelace_lol/loom3`.
|
|
1703
|
+
|
|
1704
|
+
### Engine surface
|
|
1705
|
+
|
|
1706
|
+
`Loom3` (legacy alias: `LoomLarge`) covers:
|
|
1707
|
+
- Initialization and lifecycle: `onReady()`, `update()`, `start()`, `stop()`, `dispose()`
|
|
1708
|
+
- AU, morph, viseme, and continuum control: `setAU()`, `transitionAU()`, `setMorph()`, `transitionMorph()`, `setViseme()`, `transitionViseme()`, `setContinuum()`, `transitionContinuum()`
|
|
1709
|
+
- Preset state: `setProfile()`, `getProfile()`
|
|
1710
|
+
- Validation and analysis helpers: `validateMappings()`, `generateMappingCorrections()`, `extractModelData()`, `extractFromGLTF()`, `analyzeModel()`
|
|
1711
|
+
- Mixer and baked animation helpers: `loadAnimationClips()`, `getAnimationClips()`, `playAnimation()`, `playClip()`, `playSnippet()`, `snippetToClip()`
|
|
1712
|
+
- Hair physics helpers: `setHairPhysicsEnabled()`, `setHairPhysicsConfig()`, `validateHairMorphTargets()`
|
|
1713
|
+
|
|
1714
|
+
### Core types and helpers
|
|
1715
|
+
|
|
1716
|
+
- `Profile` groups the mapping tables, name-resolution fields, `morphToMesh`, `visemeKeys`, `auMixDefaults`, `compositeRotations`, `continuumPairs`, `continuumLabels`, `eyeMeshNodes`, `annotationRegions`, and `hairPhysics`.
|
|
1717
|
+
- `TransitionHandle` exposes `promise`, `pause()`, `resume()`, and `cancel()`.
|
|
1718
|
+
- `ClipHandle` exposes `clipName`, `play()`, `stop()`, `pause()`, `resume()`, optional live-update setters, `getTime()`, `getDuration()`, and `finished`.
|
|
1719
|
+
- `collectMorphMeshes()` gathers meshes that already have morph targets.
|
|
1720
|
+
- `resolvePreset()` picks a built-in preset by name.
|
|
1721
|
+
- `resolvePresetWithOverrides()` resolves a preset and merges partial overrides.
|
|
1722
|
+
- `resolveProfile()` merges a base preset with a partial profile override.
|
|
1723
|
+
- `isMixedAU()` checks whether an AU has both morph and bone support in the CC4 preset.
|
|
1724
|
+
|
|
1725
|
+
---
|
|
1726
|
+
|
|
1656
1727
|
## Resources
|
|
1657
1728
|
|
|
1658
1729
|

|
package/dist/index.cjs
CHANGED
|
@@ -4055,6 +4055,28 @@ var _Loom3 = class _Loom3 {
|
|
|
4055
4055
|
* Resolve morph key to direct targets for ultra-fast repeated access.
|
|
4056
4056
|
* Use this when you need to set the same morph many times (e.g., in animation loops).
|
|
4057
4057
|
*/
|
|
4058
|
+
resolveMorphTargetIndex(dict, key) {
|
|
4059
|
+
if (!dict) return void 0;
|
|
4060
|
+
const prefix = this.config.morphPrefix || "";
|
|
4061
|
+
const suffix = this.config.morphSuffix || "";
|
|
4062
|
+
const fullName = prefix + key + suffix;
|
|
4063
|
+
const exactIndex = dict[fullName];
|
|
4064
|
+
if (exactIndex !== void 0) {
|
|
4065
|
+
return exactIndex;
|
|
4066
|
+
}
|
|
4067
|
+
const suffixRegex = this.config.suffixPattern ? new RegExp(this.config.suffixPattern) : null;
|
|
4068
|
+
if (!suffixRegex) {
|
|
4069
|
+
return void 0;
|
|
4070
|
+
}
|
|
4071
|
+
for (const [candidate, index] of Object.entries(dict)) {
|
|
4072
|
+
if (!candidate.startsWith(fullName)) continue;
|
|
4073
|
+
const candidateSuffix = candidate.slice(fullName.length);
|
|
4074
|
+
if (candidateSuffix === "" || suffixRegex.test(candidateSuffix)) {
|
|
4075
|
+
return index;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return void 0;
|
|
4079
|
+
}
|
|
4058
4080
|
resolveMorphTargets(key, meshNames) {
|
|
4059
4081
|
const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
|
|
4060
4082
|
const cacheKey = this.getMorphKeyCacheKey(key, meshNames);
|
|
@@ -4067,7 +4089,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4067
4089
|
const dict = mesh.morphTargetDictionary;
|
|
4068
4090
|
const infl = mesh.morphTargetInfluences;
|
|
4069
4091
|
if (!dict || !infl) continue;
|
|
4070
|
-
const idx = dict
|
|
4092
|
+
const idx = this.resolveMorphTargetIndex(dict, key);
|
|
4071
4093
|
if (idx !== void 0) {
|
|
4072
4094
|
targets.push({ infl, idx });
|
|
4073
4095
|
}
|
|
@@ -4558,7 +4580,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4558
4580
|
const dict = this.faceMesh.morphTargetDictionary;
|
|
4559
4581
|
const infl = this.faceMesh.morphTargetInfluences;
|
|
4560
4582
|
if (dict && infl) {
|
|
4561
|
-
const idx = dict
|
|
4583
|
+
const idx = this.resolveMorphTargetIndex(dict, key);
|
|
4562
4584
|
if (idx !== void 0) return infl[idx] ?? 0;
|
|
4563
4585
|
}
|
|
4564
4586
|
return 0;
|
|
@@ -4567,7 +4589,7 @@ var _Loom3 = class _Loom3 {
|
|
|
4567
4589
|
const dict = mesh.morphTargetDictionary;
|
|
4568
4590
|
const infl = mesh.morphTargetInfluences;
|
|
4569
4591
|
if (!dict || !infl) continue;
|
|
4570
|
-
const idx = dict
|
|
4592
|
+
const idx = this.resolveMorphTargetIndex(dict, key);
|
|
4571
4593
|
if (idx !== void 0) return infl[idx] ?? 0;
|
|
4572
4594
|
}
|
|
4573
4595
|
return 0;
|