@lovelace_lol/embody 0.0.7 → 0.0.8

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.
Files changed (127) hide show
  1. package/README.md +541 -2394
  2. package/dist/index.d.ts +2 -100
  3. package/dist/index.js +7714 -12951
  4. package/dist/index.js.map +1 -1
  5. package/dist/three/annotations/DOMControls.d.ts +73 -0
  6. package/dist/three/annotations/DPthree.d.ts +113 -0
  7. package/dist/three/annotations/DPthree3DMarkers.d.ts +319 -0
  8. package/dist/three/annotations/DPthreeCameraController.d.ts +414 -0
  9. package/dist/three/annotations/DPthreeHTMLMarkers.d.ts +131 -0
  10. package/dist/three/annotations/DPthreeMarkers.d.ts +52 -0
  11. package/dist/three/annotations/adapter.d.ts +157 -0
  12. package/dist/three/annotations/animationFrameTaskScheduler.d.ts +18 -0
  13. package/dist/three/annotations/annotationAnchorModel.d.ts +16 -0
  14. package/dist/three/annotations/annotationCameraCore.d.ts +5 -0
  15. package/dist/three/annotations/boneResolutionProfile.d.ts +7 -0
  16. package/dist/three/annotations/index.d.ts +20 -0
  17. package/dist/three/annotations/loadFrame.d.ts +2 -0
  18. package/dist/three/annotations/markerSurfaceRaycast.d.ts +11 -0
  19. package/dist/three/annotations/markerVisibilityLifecycle.d.ts +21 -0
  20. package/dist/three/annotations/modelRenderPreparation.d.ts +6 -0
  21. package/dist/three/annotations/runtimeAnnotationPreview.d.ts +15 -0
  22. package/dist/three/annotations/runtimeAnnotationPreviewSide.d.ts +10 -0
  23. package/dist/three/annotations/runtimeAnnotationSidePreview.d.ts +5 -0
  24. package/dist/three/annotations/types.d.ts +262 -0
  25. package/dist/three/gaze.d.ts +122 -0
  26. package/dist/three/index.d.ts +147 -0
  27. package/dist/three/model.d.ts +31 -0
  28. package/dist/three/modelSkinning.d.ts +3 -0
  29. package/dist/three/reference-pose.d.ts +73 -0
  30. package/dist/three/scene.d.ts +253 -0
  31. package/dist/three/sceneRenderingBinding.d.ts +22 -0
  32. package/dist/three.js +7696 -8936
  33. package/dist/three.js.map +1 -1
  34. package/dist/wasm/README.md +551 -62
  35. package/dist/wasm/auto.d.ts +1 -0
  36. package/dist/wasm/embody_wasm.d.ts +388 -52
  37. package/dist/wasm/embody_wasm.js +1514 -119
  38. package/dist/wasm/embody_wasm_bg.wasm +0 -0
  39. package/dist/wasm/embody_wasm_bg.wasm.d.ts +115 -40
  40. package/dist/wasm/index.d.ts +126 -0
  41. package/dist/wasm.d.ts +1 -43
  42. package/dist/wasm.js +1043 -239
  43. package/dist/wasm.js.map +1 -1
  44. package/package.json +34 -39
  45. package/service-worker/character-asset-service-worker.js +295 -0
  46. package/dist/authoring/boneAuthoring.d.ts +0 -132
  47. package/dist/camera/annotationCameraAngles.d.ts +0 -28
  48. package/dist/camera/cameraRelativeGaze.d.ts +0 -15
  49. package/dist/camera/rustAnnotationCameraCore.d.ts +0 -136
  50. package/dist/cljs.cjs +0 -362
  51. package/dist/cljs.cjs.map +0 -1
  52. package/dist/cljs.d.ts +0 -5
  53. package/dist/cljs.js +0 -359
  54. package/dist/cljs.js.map +0 -1
  55. package/dist/core/TsClipCompiler.d.ts +0 -51
  56. package/dist/core/WasmRuntimeCore.d.ts +0 -41
  57. package/dist/core/compositeAxis.d.ts +0 -4
  58. package/dist/core/contracts/abi.d.ts +0 -70
  59. package/dist/core/contracts/clip.d.ts +0 -44
  60. package/dist/core/contracts/frame.d.ts +0 -27
  61. package/dist/core/contracts/host.d.ts +0 -28
  62. package/dist/core/contracts/ids.d.ts +0 -15
  63. package/dist/core/contracts/index.d.ts +0 -10
  64. package/dist/core/contracts/model.d.ts +0 -31
  65. package/dist/core/contracts/primitives.d.ts +0 -17
  66. package/dist/core/contracts/skeletonFit.d.ts +0 -41
  67. package/dist/core/index.d.ts +0 -6
  68. package/dist/core/types.d.ts +0 -472
  69. package/dist/core.cjs +0 -343
  70. package/dist/core.cjs.map +0 -1
  71. package/dist/core.js +0 -323
  72. package/dist/core.js.map +0 -1
  73. package/dist/engines/three/Embody.d.ts +0 -372
  74. package/dist/engines/three/ThreeAnimationRuntime.d.ts +0 -203
  75. package/dist/engines/three/ThreeClipAdapter.d.ts +0 -24
  76. package/dist/engines/three/ThreeFrameApplier.d.ts +0 -68
  77. package/dist/engines/three/ThreeModelInspector.d.ts +0 -54
  78. package/dist/engines/three/bakedClipPartitioning.d.ts +0 -16
  79. package/dist/engines/three/balanceUtils.d.ts +0 -3
  80. package/dist/engines/three/characterHost.d.ts +0 -80
  81. package/dist/engines/three/characterScene.d.ts +0 -98
  82. package/dist/engines/three/hair/HairPhysicsController.d.ts +0 -124
  83. package/dist/engines/three/index.d.ts +0 -20
  84. package/dist/engines/three/modelLoader.d.ts +0 -61
  85. package/dist/engines/three/profileTestScene.d.ts +0 -52
  86. package/dist/engines/three/types.d.ts +0 -20
  87. package/dist/hair/appearance.d.ts +0 -29
  88. package/dist/index.cjs +0 -14871
  89. package/dist/index.cjs.map +0 -1
  90. package/dist/interfaces/Animation.d.ts +0 -315
  91. package/dist/interfaces/EmbodyRuntime.d.ts +0 -90
  92. package/dist/interfaces/Hair.d.ts +0 -181
  93. package/dist/interfaces/HairPhysics.d.ts +0 -90
  94. package/dist/interfaces/index.d.ts +0 -11
  95. package/dist/mappings/extendPresetWithProfile.d.ts +0 -17
  96. package/dist/mappings/types.d.ts +0 -309
  97. package/dist/mappings/visemeSystem.d.ts +0 -26
  98. package/dist/physics/HairPhysics.d.ts +0 -86
  99. package/dist/physics/RustHairPhysics.d.ts +0 -18
  100. package/dist/physics/index.d.ts +0 -6
  101. package/dist/preset.cjs +0 -2005
  102. package/dist/preset.cjs.map +0 -1
  103. package/dist/preset.js +0 -2003
  104. package/dist/preset.js.map +0 -1
  105. package/dist/presets/bettaFish.d.ts +0 -328
  106. package/dist/presets/cc4.d.ts +0 -145
  107. package/dist/presets/index.d.ts +0 -30
  108. package/dist/presets/registry.d.ts +0 -57
  109. package/dist/profiles/resolveProfileConfig.d.ts +0 -36
  110. package/dist/profiles/rustResolvedProfile.d.ts +0 -31
  111. package/dist/profiles/types.d.ts +0 -118
  112. package/dist/regions/normalizeRegionTree.d.ts +0 -6
  113. package/dist/regions/regionMapping.d.ts +0 -17
  114. package/dist/regions/types.d.ts +0 -122
  115. package/dist/skeletonTemplates/humanoidSkeletonTemplates.d.ts +0 -32
  116. package/dist/test/setupWasm.d.ts +0 -1
  117. package/dist/three.cjs +0 -9550
  118. package/dist/three.cjs.map +0 -1
  119. package/dist/validation/analyzeModel.d.ts +0 -68
  120. package/dist/validation/extractModelData.d.ts +0 -90
  121. package/dist/validation/generateMappingCorrections.d.ts +0 -32
  122. package/dist/validation/geometryHelpers.d.ts +0 -66
  123. package/dist/validation/types.d.ts +0 -10
  124. package/dist/validation/validateMappings.d.ts +0 -77
  125. package/dist/wasm.cjs +0 -302
  126. package/dist/wasm.cjs.map +0 -1
  127. package/dist/wasmTypes.d.ts +0 -123
@@ -1,72 +1,561 @@
1
- # Embody Wasm Core
2
-
3
- This crate provides the Rust/Wasm artifact used by the host-neutral Embody core.
4
- The npm build now produces the generated Wasm package under `dist/wasm` and
5
- the public `@lovelace_lol/embody/wasm` entrypoint initializes it explicitly.
6
-
7
- The current Rust surface focuses on batched numeric operations that match the
8
- core JS/Wasm boundary shape:
9
-
10
- - inputs are number slices / typed arrays
11
- - outputs are packed numeric buffers
12
- - no Three.js, Babylon, Unity, or host objects cross the boundary
13
-
14
- Skeleton fitting, fit metadata, and automatic skin binding should follow the
15
- same descriptor boundary. Rust can own host-neutral math over mesh descriptors,
16
- template skeleton descriptors, rest-pose transforms, bone envelopes, topology,
17
- inverse bind data, normals, scale, and manual fit adjustments. Host adapters
18
- must still own real mesh, material, GLB, skinning, and engine-object mutation.
19
- The TypeScript core contract exposes `TemplateSkeletonFitMetadata` so hosts can
20
- persist template id, source character id, fit transform, height/spans, status,
21
- vertical axis/anchor, confidence, and manual adjustments separately from actual
22
- skin weights. Both TypeScript and Rust/Wasm expose helpers that compose a
23
- solved fit transform with a user's manual scale and xyz offset.
24
-
25
- Automatic skin binding is not a per-frame Wasm target by default. It should be
26
- treated as research/offline-capable work until fixtures prove acceptable
27
- deformation quality for clothing, hands, face, hair, non-standard poses, and
28
- other hard cases. The runtime package should expose stable metadata and helper
29
- math first, then only move weight generation into Rust/Wasm if benchmarks and
30
- visual tests justify doing it in the browser.
31
-
32
- `validate_generated_template_fit_metadata(template_id, source_character_id,
33
- metadata)` validates persisted generated-template fit metadata without treating
34
- it as imported rig or skinning data. The packed metadata layout is:
1
+ # Embody
2
+
3
+ Explicit profiles may be empty: `RuntimeCore.configure_with_profile('{}', modelJson)`
4
+ loads a static or not-yet-mapped model without selecting an embedded preset.
5
+ Missing mappings remain an authoring/validation concern; malformed JSON still
6
+ fails configuration. Existing mapped profiles retain their runtime behavior.
7
+
8
+ Embody is a Rust/Wasm character animation core for facial action units,
9
+ visemes, bone controls, baked animation clips, and hair motion. Three.js is a
10
+ host adapter, not the runtime implementation.
11
+
12
+ ## Architecture
35
13
 
36
14
  ```text
37
- [
38
- schema_version,
39
- metadata_kind,
40
- template_lookup_status,
41
- fit_status,
42
- vertical_axis,
43
- vertical_anchor,
44
- fit_scale,
45
- translate_x, translate_y, translate_z,
46
- solver_confidence,
47
- manual_scale_multiplier,
48
- manual_offset_x, manual_offset_y, manual_offset_z
49
- ]
15
+ src/ Rust source (the Wasm crate) — hair, clips, profiles, runtime
16
+ assets/presets/ Embedded typed character profiles
17
+ assets/templates/ Embedded humanoid skeleton templates
18
+ three/ Thin Three.js inspection/application adapter only
19
+ wasm/ Generated-Wasm JS loader + ABI constants
20
+ index.ts Package re-exports only
21
+ ```
22
+
23
+ There is no parallel TypeScript hair/physics/runtime core. Hair idle, impulse,
24
+ gravity curves and the spring solver live in `src/hair_curves.rs` /
25
+ `src/hair.rs` and are exported through Wasm. Hosts schedule the resulting
26
+ ClipIR on their mixer; they must not reimplement hair sampling in TS.
27
+
28
+ The Rust core owns:
29
+
30
+ - preset and profile parsing, typed merge rules, validation, and corrections
31
+ - AU, viseme, morph, bone, continuum, and live control state
32
+ - compiling AU/viseme/named-morph snippets into concrete ClipIR
33
+ (`morphTarget` / `boneTransform` tracks) for the host mixer to schedule
34
+ - mesh-category routing and material profile data
35
+ - hair curves, hair physics, and appearance normalization
36
+ - annotation camera/marker math and humanoid template fitting
37
+ - profile-aware screen-space gaze geometry for eye/head AU trajectories
38
+ - renderer-neutral model analysis and packed live frame generation
39
+
40
+ The host animation library (Three `AnimationMixer`, Unity Animator, etc.) owns
41
+ clip playback, lerping, blending, looping, seeking, and crossfades. Rust must
42
+ not sample or lerp clips on the hot path.
43
+
44
+ An AU animation drives its profile's mapped bones and morphs through the same
45
+ Rust evaluation used by live AU controls. For example, speech can send an AU
46
+ 103 curve and separate viseme shape curves; the profile decides which bones
47
+ and morphs AU 103 moves. Hosts must not replace that AU with a hardcoded jaw
48
+ rotation or read `auToBones` to construct their own bone tracks. When LipSync
49
+ already supplies a jaw AU curve, its existing `autoVisemeJaw: false` option
50
+ keeps automatic viseme jaw synthesis from replacing that curve; it does not
51
+ disable the AU's mapped morphs or the viseme shapes.
52
+
53
+ The TypeScript adapter is intentionally limited to operations that require
54
+ Three.js objects: scene traversal, ClipIR ↔ `AnimationClip` conversion, frame
55
+ application, material writes, model loading/disposal, and default scene
56
+ construction. The annotation adapter also owns camera controls, marker scene
57
+ objects, labels, selection, and their browser lifecycle; applications provide
58
+ the scene/camera/DOM inputs and render controls through the public API.
59
+
60
+ `ThreeFrameApplier.applyMeshMaterialConfigs(model, profile.meshes)` replays named
61
+ mesh visibility and material overrides when a host binds or replaces a profile.
62
+ It applies explicit `visible: false` and `visible: true` even without a material
63
+ entry, and restores render order, opacity, transparency, depth flags and blending
64
+ from `material`. Omitted fields leave the model's current values unchanged.
65
+ Polymer calls this adapter during model binding and profile replacement; hosts
66
+ do not need a separate mesh-visibility replay loop.
67
+
68
+ ### Annotation runtime
69
+
70
+ `DPthreeCameraController` and the `DPthree` convenience facade from
71
+ `@lovelace_lol/embody/three` own the complete camera and annotation runtime.
72
+ `DPthree3DMarkers`, `DPthreeHTMLMarkers`, and the legacy `DPthreeMarkers` adapter
73
+ are available for hosts that need a marker-only layer. Rust owns camera
74
+ framing/flights/orbits, viewport clipping, curves, visibility factors and
75
+ endpoint separation. The Three adapter owns native model inspection,
76
+ raycasting, labels, DOM controls, marker expansion/solo/style state and disposal.
77
+
78
+ Applications pass `AnnotationCharacterConfig`; storage, agency, editor and
79
+ character-asset metadata remain host concerns. `loadRegions` defaults to
80
+ `resolveAnnotationCharacterConfig`, which resolves annotation and AU mapping
81
+ fields from an Embody preset without serializing unrelated host metadata.
82
+ An optional `resolveCharacterConfig` callback supports host profile intake.
83
+ `prepareRegionsAndMarkersForReveal` accepts an already-resolved profile and
84
+ prepares native surface queries while the model pose is still stable.
85
+ Initial queries yield to a browser paint between meshes once a 4 ms slice is
86
+ spent. Each native mesh raycast is indivisible; a large mesh can still cause a
87
+ frame drop, and slicing does not reduce total raycast CPU time. Interactive
88
+ annotation changes retain synchronous queries so a marker samples one pose.
89
+ Replacement, clearing and disposal cancel stale loads before scene commits.
90
+
91
+ For first reveal, call `controller.prepareModelForRender(model, { signal })`
92
+ from the existing `beforeReveal` callback (or directly before attaching the
93
+ model). It compiles the model's visible surfaces, then uses the controller's
94
+ existing render loop for one complete scene preparation draw. No host warmup
95
+ queue, shadow proxies or texture preparation loop is needed. The promise rejects
96
+ on failure or cancellation; retain borrowed model/scene/renderer resources until
97
+ it settles, even after `controller.dispose()`. This prepares visible variants,
98
+ not every future hidden material or animation state. See the
99
+ [model readiness contract](docs/SCENE_READINESS.md#model-readiness).
100
+
101
+ `createMarkerVisibilityLifecycle` owns automatic reveal/hide timers;
102
+ `createRuntimeAnnotationPreviewLifecycle` owns temporary authoring previews.
103
+ React hosts forward user events and subscribe to controller state. They do not
104
+ need local copies of the camera, marker, placement or timer algorithms.
105
+ The convenience facade delegates to one controller-owned marker layer.
106
+
107
+ ## Runtime Use
108
+
109
+ Embody is an ES module package. Use `import` / dynamic `import()`; CommonJS
110
+ `require()` is not a supported runtime API. Package subpaths remain resolvable
111
+ through Node for tooling that only needs to locate an exported file.
112
+
113
+ Initialize the Wasm module before constructing a runtime:
114
+
115
+ ```ts
116
+ import { initEmbodyCore } from '@lovelace_lol/embody/wasm';
117
+ import {
118
+ AnimationMixer,
119
+ } from 'three';
120
+ import {
121
+ ThreeFrameApplier,
122
+ ThreeModelInspector,
123
+ createAnimationClipFromClipIR,
124
+ } from '@lovelace_lol/embody/three';
125
+
126
+ const wasm = await initEmbodyCore();
127
+ const inspector = new ThreeModelInspector();
128
+ const applier = new ThreeFrameApplier();
129
+ const inspection = inspector.inspectModel(model, { profile });
130
+ const runtime = new wasm.RuntimeCore(0);
131
+ const mixer = new AnimationMixer(model);
132
+
133
+ applier.setBindings(inspection);
134
+ runtime.configure_with_preset(
135
+ 'cc4',
136
+ JSON.stringify(profileOverrides ?? {}),
137
+ JSON.stringify(inspection.descriptor),
138
+ );
139
+
140
+ const clipIR = JSON.parse(runtime.build_clip(
141
+ 'smile',
142
+ JSON.stringify(curves),
143
+ JSON.stringify({ intensityScale: 1 }),
144
+ ));
145
+ const clip = createAnimationClipFromClipIR(clipIR, inspection);
146
+ mixer.clipAction(clip).play();
147
+
148
+ function update(dtSeconds: number) {
149
+ // Live AU/viseme packed frames, then host mixer owns clip lerp.
150
+ applier.applyPackedMorphFrameDelta(runtime.evaluate_morph_frame_delta());
151
+ applier.applyPackedBoneFrameDelta(runtime.evaluate_bone_frame_delta());
152
+ mixer.update(dtSeconds);
153
+ }
154
+ ```
155
+
156
+ Application-facing JavaScript APIs belong in the host package. Polymer owns
157
+ the CLJS character host used by LoomLarge and calls the Wasm exports directly.
158
+
159
+ ### Direct viseme snapshots
160
+
161
+ `RuntimeCore.get_viseme(index)` and `get_viseme_jaw_scale(index)` read the
162
+ canonical direct viseme controls without changing a frame or sampling a host
163
+ mixer. Unset, cleared, or out-of-range slots return `0` and `1` respectively.
164
+ Hosts can snapshot these values before a temporary mouth-shape preview and
165
+ restore them with the existing setters; a zero value releases its live override.
166
+
167
+ ### Preparing a default scene
168
+
169
+ `createDefaultCharacterSceneRuntime(container, { rendering: { preference: 'auto' } })`
170
+ returns a stable scene, camera, lighting controller and `rendering` settings
171
+ controller immediately. `ready` reports the initial attempt; failure retains the
172
+ controller for recovery through `setSettings`. `getSettings`, `getSnapshot` and
173
+ `subscribe` expose the requested preference, actual backend, status and errors.
174
+ Live changes prepare and replace only backend resources, preserving the character,
175
+ scene, camera, lighting and marker state. `DPthreeCameraController({ rendering,
176
+ scene, camera, domElement: container })` retargets its existing loop and controls;
177
+ hosts persist settings and display status without recreating the scene.
178
+
179
+ Auto tries native WebGPU then falls back only on acquisition/initialization
180
+ failure. Explicit WebGPU remains strict; material/preparation errors retain the
181
+ previous usable renderer. Device loss is observable and recoverable through the
182
+ same controller. `acquireRenderer()` leases protect captures and model mutations;
183
+ release them before requesting a switch. `releaseSceneResources()` drains rendering
184
+ before detaching and freeing borrowed resources, including after device loss or
185
+ scene disposal. Await `runtime.dispose()` before freeing resources on scene teardown.
186
+
187
+ The existing async factory remains available and rejects after cleanup on initial
188
+ failure. Its successful handles expose the same controller and dynamic backend /
189
+ renderer getters. The synchronous factory stays WebGL-specific. See the owning
190
+ contract for latest-request handling, capture/model leases and teardown ordering.
191
+
192
+ Three runtime and types are checked at 0.184.0, with a >=0.184.0 runtime peer.
193
+ Initial scene readiness does not prepare subsequently added models or establish
194
+ first-frame/visual parity. See [renderer selection and scene readiness](docs/SCENE_READINESS.md)
195
+ for ownership, version rationale and remaining real-browser acceptance.
196
+
197
+ ### Bilateral snippet channels
198
+
199
+ AU balance uses the character's left/right: `-1` drives the left side,
200
+ `0` drives both, and `1` drives the right. `RuntimeCore.get_au_balance(id)`
201
+ returns the stored live balance, clamped by the existing setters to [-1, 1],
202
+ with neutral zero for an unset id or after `clear()`. Hosts can save it alongside
203
+ `get_au(id)` when serializing a manual pose.
204
+
205
+ Legacy `build_clip` curve maps use
206
+ `options.balanceMap[auId]`, falling back to `options.balance` and then zero.
207
+ For `build_typed_clip`, an AU target's explicit `balance` takes precedence:
208
+
209
+ ```js
210
+ const channels = [
211
+ { target: { type: 'au', id: 43, balance: -1 }, keyframes: leftWink },
212
+ { target: { type: 'au', id: 43, balance: 1 }, keyframes: rightWink },
213
+ ];
214
+ ```
215
+
216
+ Each channel retains its own keyframe times and intensity scale. Typed AU and
217
+ viseme ids belong to separate namespaces, regardless of `snippetCategory`.
218
+ Duplicate typed channels for the same AU or viseme and concrete morph target
219
+ combine by maximum, including intersections between authored linear curves.
220
+ Independent endpoint-balanced channels keep their own inherited or explicit
221
+ starts; an inactive opposite-side contribution cannot replace an active
222
+ channel's anchor. Composite bone rotations take the maximum AU contribution
223
+ after each channel's balance. Legacy curves and distinct semantic ids that
224
+ share a morph retain their existing separate tracks and host blending.
225
+
226
+ A duplicate typed group with multiple enabled contributions to the same morph
227
+ cannot also contain an inherited start: compilation rejects that combination
228
+ with an error identifying the AU/viseme and destination. Author one curve per
229
+ side or explicit starting values. Previously, duplicate ids silently discarded
230
+ all but the last channel. Inheritance on supported morph tracks is captured
231
+ when the Three clip is constructed. Hosts must construct it again to capture
232
+ a fresh pose; this does not change cached host replay or generated bone
233
+ inheritance behavior.
234
+
235
+ ### Gaze geometry contract
236
+
237
+ `solve_profile_screen_space_gaze` and `solve_profile_viewer_space_gaze` return
238
+ 14 floats: combined target XY, eye target XY, head target XY, total yaw/pitch,
239
+ camera yaw/pitch, eye-to-camera distance, and viewer world XYZ. XY outputs are
240
+ signed AU intensities in [-1, 1]; positive Y is up and horizontal direction is
241
+ subject-relative. Angular values are degrees.
242
+
243
+ Pass current world camera position/quaternion, character eye midpoint, and
244
+ model quaternion. The viewer solver takes normalized image XY (+Y up) and
245
+ depth behind the virtual camera; depth and all positions must share scene
246
+ units. Convert physical webcam estimates before calling and supply the actual
247
+ source FOV/aspect. Zero viewer XY extends the character-eye-to-camera bearing,
248
+ so the character looks at the viewer even when scene framing puts its eyes
249
+ away from image center. Offsets retain the camera's right/up axes; when the
250
+ camera coincides with the eyes, its local +Z supplies the fallback bearing.
251
+ The depth guard is 0.2–10 scene units. Mouse and webcam can share this contract
252
+ when they use the same calibration.
253
+
254
+ Ordinary head following participates toward the common target from calibrated
255
+ model-neutral +Z: `head_follow_fraction` is a share of the target yaw/pitch,
256
+ defaulting to 0.35 for a non-finite value. Zero prefers neutral, one follows the
257
+ full target, and eye overflow can recruit more head movement within its limits.
258
+ The explicit `lock_head_to_camera` flag instead prefers the camera bearing while
259
+ eyes can reach; a zero follow fraction alone does not select that policy. When
260
+ eyes are disabled the head carries the target regardless of follow or lock.
261
+ This removes the previous implicit camera bias, which could point the head
262
+ opposite the selected target when the camera was off-center. Hosts that intend
263
+ camera-facing behavior must request the explicit viewer-solver lock; Polymer's
264
+ `headFollowEyes: false` maps to zero participation, not an implicit lock.
265
+ Eye angles are solved in the
266
+ rotated head frame, using yaw then pitch to match runtime composition. Limits
267
+ and AU normalization are directional and include each binding's scale. Missing
268
+ or morph-only mappings contribute no inferred angular capacity.
269
+
270
+ The profile must describe the active rig with calibrated semantic axes:
271
+ model +Z is forward, +Y is up. This ABI does not include the skeleton's rest
272
+ frames or optical axes, per-eye origins, current animated head pose, or
273
+ webcam-to-display calibration. It solves an endpoint in the assumed calibrated
274
+ basis; it does not provide binocular convergence or compensate intermediate
275
+ head motion. Shared eye commands use the smaller mapped eye capacity; unequal
276
+ left/right ranges need separate calibration. Hosts must compose the requested
277
+ rotations without averaging independent yaw/pitch clips together.
278
+
279
+ For import-time inspection, `captureModelReferencePose` provides an explicit,
280
+ immutable transform and morph reference that hosts can reuse after playback.
281
+ `extendModelReferencePose` explicitly adds appended skeletons while preserving
282
+ the captured parent transforms. It does not infer a bind pose or perform retargeting. See
283
+ [Animation reference poses](docs/ANIMATION_REFERENCE_POSES.md) for the contract,
284
+ step/linear clip conversion, and remaining import work.
285
+
286
+ ## Presets And Profiles
287
+
288
+ ### Full humanoid skeleton and body authoring
289
+
290
+ `humanoid.getSpecification` exposes all 55 VRM 1.0 anatomical roles and their
291
+ parent/required rules. CC4 maps all 55 to distinct authored bones and supplies
292
+ 58 body controls for fingers, wrists, shoulders, feet, whole body motion, the
293
+ spine, head, eyes and jaw. `profile.setHumanoidRoleBinding` edits or clears a role and
294
+ retargets its existing actuators. The runtime keeps the same bone/morph evaluator;
295
+ other rigs must author their own calibrated axes/ranges and optional muscle
296
+ morph targets. See [Body controls](docs/body-controls.md) for migration, validation
297
+ and role-assignment contracts.
298
+
299
+
300
+ `cc4` and `fish` are embedded in the Wasm binary. `skeletal` is accepted as a
301
+ host-level alias for `fish`; exact custom profiles use
302
+ `RuntimeCore.configure_with_profile` and are never silently merged with CC4.
303
+
304
+ ```ts
305
+ const ids = wasm.list_presets();
306
+ const cc4 = JSON.parse(wasm.get_preset_json('cc4'));
307
+ const merged = JSON.parse(wasm.merge_embedded_preset('cc4', JSON.stringify(overrides)));
308
+ ```
309
+
310
+ Host-neutral profile authoring operations use one JSON request entry point:
311
+
312
+ ```ts
313
+ const result = JSON.parse(wasm.embody_request(JSON.stringify({
314
+ op: 'bone.applyAxisUpdate',
315
+ payload: { profile, boneName: 'CC_Base_Head', axis: 'yaw', update },
316
+ })));
50
317
  ```
51
318
 
52
- The validator returns a bitmask. `0` means valid; nonzero values identify
53
- unsupported versions, non-generated metadata kinds, unknown templates, invalid
54
- axis/anchor/status codes, non-finite transform fields, invalid confidence, or
55
- invalid ids.
319
+ This boundary keeps ClojureScript and other hosts independent of Rust struct
320
+ layouts while ensuring profile semantics execute in Rust.
321
+
322
+ Speech articulation data belongs to the rig preset. The CC4 profile keeps two
323
+ ordered tables beside its canonical `visemeSlots`: `visemeJawAmounts` stores
324
+ the jaw opening for each viseme index, and `visemeTongueTargets` stores the AU
325
+ target map for that same index. Empty tongue maps mean that viseme has no
326
+ tongue overlay. Hosts pass these resolved tables to their lip-sync planner;
327
+ they should not recreate rig-specific jaw or tongue values in application code.
328
+ The live runtime also compiles both tables: `setViseme` applies the viseme morph,
329
+ the mapped jaw opening, and the mapped tongue AUs from the same preset entry.
330
+
331
+ ## Model Analysis
332
+
333
+ Three.js models are reduced to a renderer-neutral descriptor by
334
+ `ThreeModelInspector`. Rust consumes that descriptor for extraction and
335
+ validation:
336
+
337
+ ```ts
338
+ const clips = serializeAnimationClips(model, animations, inspection);
339
+ const report = JSON.parse(wasm.analyze_model_descriptor(
340
+ JSON.stringify(inspection.descriptor),
341
+ JSON.stringify(clips),
342
+ JSON.stringify(profile),
343
+ JSON.stringify({ suggestCorrections: true }),
344
+ ));
345
+ ```
346
+
347
+ ## Pose-aware focus
348
+
349
+ `ThreeGazeFocus` from `@lovelace_lol/embody/three` applies a focus constraint
350
+ inside an animation runtime. It has no clock or target-selection policy:
351
+ call `restore()` before evaluating the base animation, then `apply(request,
352
+ controls)` after it. Restoration compares quaternion orientations independently
353
+ of Float32 sample norms, preserving the exact authored sample and any newer
354
+ authored orientation. Repeated applications between mixer ticks do not accumulate
355
+ the constraint's previous correction. Controls are the current positions of existing motor
356
+ tracks. The optional sampled `controls.headIntensity` and `controls.eyeIntensity`
357
+ override the matching request fields, so the host can animate configuration
358
+ changes on its existing motor tracks. Both mean contribution in [0, 1]: zero
359
+ preserves the evaluated base, one applies the full solved excursion, and legacy
360
+ values above one normalize to one. Intermediate eye contribution proportionally
361
+ reduces the correction; it does not shrink anatomical limits. Reduced eye
362
+ contribution deliberately leaves focus error. These controls do not amplify
363
+ input displacement or control response speed.
364
+
365
+ During travel, the remaining sampled bearing rotates the requested finite
366
+ target. At the endpoint that rotation becomes identity, reaching the exact
367
+ world target continuously. There is no settled-threshold switch between targets
368
+ constructed from the authored and tracking-shifted eye origins; a larger head
369
+ turn must not introduce a final eye snap.
370
+
371
+ Pass `{ referencePose }` as the constructor's third argument, reusing the
372
+ reference captured at import. Without it, construction captures the current
373
+ pose, so callers must construct before playback. The head solves its bounded
374
+ gaze excursion in that reference hierarchy under the model's current scene
375
+ transform, then composes the excursion onto the evaluated authored head pose.
376
+ Head and neck gestures retain their motion instead of being cancelled or
377
+ reversed by tracking. Each eye then solves toward the same finite target from
378
+ its own origin in the final animated hierarchy. Full eye contribution retains
379
+ binocular focus wherever the combined authored motion and gaze are reachable;
380
+ otherwise diagnostics report the remaining angular error and limits. Authored
381
+ gestures are not suppressed to force a target into range. The signed limits
382
+ bound the tracking excursion; they do not clamp the final authored pose plus
383
+ that excursion against a neutral-reference joint range. Authored motion can
384
+ therefore take the combined pose beyond that range or the eyes' reach.
385
+
386
+ `readControlState({worldTarget, headIntensity, eyeIntensity})` provides initial
387
+ motor bearings without changing the rig. It inverts existing focus contributions
388
+ against their saved base, and seeds zero added head excursion when enabling on
389
+ a purely authored pose. When replacing legacy AU controls, also provide the
390
+ evaluated local `headBaseQuaternion` and/or `eyeBaseQuaternions` (a map keyed by
391
+ bone name), with those legacy controls excluded. `headSeedLimited` and
392
+ `eyeSeedLimited` report poses that cannot be represented by the bounded motor
393
+ bearings. Parallel legacy eyes cannot both be preserved exactly by a shared
394
+ finite target; inverse eye contribution avoids double attenuation but does not
395
+ remove that convergence residual. This helper supplies no handoff transition
396
+ clock; the host owns activation policy and movement timing.
56
397
 
57
- Automatic skin binding and weight generation are intentionally tracked as a
58
- separate research problem. See `docs/HUMANOID_TEMPLATE_SKINNING_TDR.md`.
398
+ The optional `profile.gazeCalibration` supplies bone-local
399
+ `head`, `leftEye`, and `rightEye` optical axes and `modelUnitsPerMeter`.
400
+ Without explicit optical axes, signed yaw/pitch bindings determine the optical
401
+ frame. Unresolvable joints remain untouched and are reported in diagnostics.
59
402
 
60
- Build locally:
403
+ `solve_profile_viewer_space_gaze_scaled` adds `world_units_per_meter` as its
404
+ last argument. Multiply the authored units-per-meter calibration by the
405
+ presentation's uniform root scale. The original viewer solver keeps its
406
+ existing scale-one behavior. Neither solver infers physical webcam/display
407
+ placement from face landmarks.
61
408
 
62
- ```sh
63
- cargo test --manifest-path rust/embody-wasm/Cargo.toml
64
- cargo build --manifest-path rust/embody-wasm/Cargo.toml --target wasm32-unknown-unknown --release
409
+ `solve_profile_tracking_gaze` maps normalized display-right/up controls to the
410
+ profile's full signed head AU rotation range. Pass `head_follow_fraction = 1`
411
+ for full participation: on an aligned display axis, an input endpoint of +/-1
412
+ requests the same head rotation as the corresponding AU at intensity 1. Binding
413
+ scale and asymmetric `maxDegrees` remain authoritative. Camera distance and FOV
414
+ do not reduce this range. Intermediate inputs interpolate between the bounded
415
+ camera bearing at zero and the corresponding signed endpoint; camera roll
416
+ rotates the display axes into the model frame. With head tracking disabled,
417
+ the target spans the eye AU range instead.
418
+
419
+ The signature is `(profile_json, tracking_target, camera_position,
420
+ camera_quaternion, gaze_origin, model_quaternion, eyes_enabled, head_enabled,
421
+ head_follow_fraction, lock_head_to_camera, world_units_per_meter)`. It returns
422
+ the same 14-float layout as the existing physical projection solvers. Target Z
423
+ is depth in meters, clamped to 0.2–10 before scene scaling; it sets a finite
424
+ binocular focus distance without attenuating the angular range. X/Y clamp to
425
+ [-1, 1], nonfinite coordinates become zero, invalid positive depth/scale use
426
+ 0.8/1 respectively, and absent AU bindings supply no angular capacity.
427
+
428
+ This is an input-range mapping, not a physical reconstruction of the viewer's
429
+ position. The existing screen/viewer projection APIs retain their contracts.
430
+ Consumers must select this entry point and request full head participation to
431
+ use it; adding the export alone does not change LoomLarge tracking. Eye focus
432
+ still uses the resulting finite target and reports residual error when reduced
433
+ strength, authored motion, or rig limits make that target unreachable.
434
+
435
+ ## npm releases and PR previews
436
+
437
+ Production consumers install an exact published `@lovelace_lol/embody` version
438
+ from public npm without read credentials and commit their lockfile. The GitHub
439
+ source repository stays private. A stable release contains the compiled JS,
440
+ TypeScript declarations, and Wasm; consumers do not build Embody from Git.
441
+
442
+ The `Verify and publish package` workflow publishes a stable npm release after
443
+ a push to `main`, or a manual run on `main` with the `ref` input left empty.
444
+ It chooses the next patch version, records the source commit in `gitHead`, and
445
+ builds and validates once. Stable version selection reads public metadata from
446
+ `registry.npmjs.org` anonymously; redirects and failed lookups stop the release.
447
+ Metadata reads never transmit publishing credentials. The separate
448
+ publish job first requires `npm access get status` to report the existing package
449
+ as public, then uses the `npm` environment's `NPM_KEY` to publish the packed
450
+ output with public access and creates its Git tag and GitHub release. Stable
451
+ releases are serialized through version selection and publication. Provenance
452
+ remains disabled for the private source repository. Retrying a published commit
453
+ reuses its existing version, and an older unpublished commit cannot replace a
454
+ newer source release.
455
+
456
+ PRs, including drafts, manual runs with an explicit `ref`, and `publish-pkg-pr-new`
457
+ repository dispatches run full verification and publish immutable `pkg.pr.new`
458
+ previews for testing linked upstream changes. An explicit ref remains a preview
459
+ request even when it names `main`; it does not publish to npm. Manual and
460
+ repository dispatch requests may include `upstream_pr` to identify their linked PR.
461
+
462
+ After publication, the producing workflow uploads a JSON notification record
463
+ without a dispatch credential. The separate `Notify package publication`
464
+ `workflow_run` receiver checks out `main`, treats the artifact only as data, and
465
+ validates the successful run attempt, repository, workflow, and source commit.
466
+ Stable records must identify that run's main commit; linked previews must match
467
+ the current open PR head targeting main. The receiver then sends
468
+ `upstream-package-published` to `meekmachine/LoomLarge` using
469
+ `CROSS_REPO_CI_TOKEN` from the `release-coordination` environment. That token must
470
+ not be stored as a repository secret or exposed to a PR build/helper.
471
+
472
+ Stable notifications contain the exact source SHA and npm version. Preview
473
+ notifications contain the full-SHA URL and upstream PR number, or `null` for an
474
+ unlinked manual preview. The receiver checks the canonical preview URL once and
475
+ fails visibly if it is unavailable. LoomLarge owns downstream dependency updates;
476
+ Embody does not edit Polymer or application branches. Missing credentials and
477
+ failed notifications fail the receiver visibly. Fork PRs do not send
478
+ notifications or receive npm/dispatch secrets.
479
+
480
+ GitHub activates the receiver after its workflow is present on `main`. The
481
+ release-stack PR's first preview remains usable by its immutable URL before
482
+ that bootstrap merge; subsequent successful publications notify automatically.
483
+
484
+ Before enabling stable releases, the existing npm package must have public
485
+ visibility and `NPM_KEY` must permit publishing it. No npm read token is required.
486
+ Configure both the `npm` and `release-coordination` GitHub
487
+ environments to allow deployments from `main` only. Store `CROSS_REPO_CI_TOKEN`
488
+ only in `release-coordination`, with permission to dispatch to LoomLarge. These
489
+ account, environment protection, and secret settings are prerequisites; source
490
+ changes do not configure them. Existing versions are never republished or
491
+ overwritten.
492
+
493
+ ## Development
494
+
495
+ ```bash
496
+ npm ci
65
497
  npm run build
66
- node scripts/smoke/package-exports.mjs
498
+ npm test
499
+ npm run test:package
500
+ npm run check:generated
67
501
  ```
68
502
 
69
- The npm build uses `wasm-pack` to generate JS glue and the `.wasm` binary after
70
- `tsup` writes the TypeScript package artifacts. Consumers should call
71
- `initEmbodyCore()` from `@lovelace_lol/embody/wasm` before using direct Wasm
72
- exports.
503
+ `npm run build` creates the JS adapter bundles, declarations, generated
504
+ wasm-bindgen glue, and `.wasm` binary in `dist/`. Generated output is not source
505
+ and is not committed. Package checks consume that existing build, so run the
506
+ build once before `npm run test:package`.
507
+
508
+ The public Wasm loader returns `EmbodyCore`, derived directly from wasm-bindgen's
509
+ generated declarations, including `RuntimeCore` constructors, methods, and
510
+ numeric helpers. Instance types can also be imported with `import type` from
511
+ `@lovelace_lol/embody/wasm`. Rust exports remain the source of truth for these
512
+ signatures; JSON payload contents are still encoded as strings.
513
+
514
+ Run the build before `npm test` or `npm run typecheck` in a clean checkout, and
515
+ rebuild after changing Rust exports. The build generates the Wasm bindings before
516
+ TypeScript declarations. Neither typechecking nor package verification rebuilds
517
+ the package; CI checks and publishes that same build.
518
+
519
+ ## License
520
+
521
+ MIT. See [LICENSE](LICENSE) and [AUTHORS.md](AUTHORS.md).
522
+
523
+ ### Semantic poses and generated motion
524
+
525
+ `RuntimeCore.get_semantic_pose_catalog_json()` returns the configured profile's
526
+ motion identifiers, direction labels, VRM roles, resolved bone/morph names and
527
+ per-direction model support and available sides. VRM names anatomy; profile motion IDs name authored
528
+ actions. Hosts should send only supported directions to generation prompts,
529
+ along with their bounds: intensity and morph strength `0..1`, bilateral balance
530
+ `-1` character-left, `0` both, `1` character-right. Amounts are normalized authored
531
+ motion, not radians or world-space rotation.
532
+
533
+ `capture_semantic_pose_json()` returns `{version:1,controls:[{controlId,positive,
534
+ negative?}]}`. Each direction stores intensity, balance and morphStrength
535
+ independently, preserving opposed action values. These are manual runtime
536
+ controls; capturing an arbitrary mixer/baked pose would require pose inference.
537
+ `validate_semantic_pose_json()` fills omitted settings without mutation.
538
+ `apply_semantic_pose_json()` validates the complete input before replacing Body
539
+ actions, preserving unrelated facial state. Either direction may be omitted;
540
+ a control requires at least one. Known missing outputs remain portable to partial
541
+ rigs; unknown IDs, duplicate actions and invalid bounds fail. Applying a semantic pose releases prior direct overrides on its exact mapped
542
+ mesh/target IDs, preserving unrelated facial overrides and same-named targets on
543
+ unmapped meshes.
544
+
545
+ `build_semantic_clip(name, animationJson, optionsJson)` compiles
546
+ `{version:1,durationSeconds,tracks:[{controlId,direction,balance?,morphStrength?,
547
+ keyframes:[{time,intensity}]}]}` through the same AU/bone/morph evaluator.
548
+ Direction is `positive` or `negative`; time is seconds. Unsupported directions,
549
+ unknown controls, duplicate actions and non-increasing/out-of-range keys fail.
550
+ `options.faceCurves` accepts additional non-Body AU curves so face and body compile
551
+ together, including overlapping mapped bones. Compilation preserves live state;
552
+ per-track morph strength changes only the compiled output. The host mixer owns
553
+ playback and interpolation, as with existing snippets.
554
+
555
+ Catalog `availableSides` identifies actual mapped left/right/center outputs.
556
+ Generated animations reject a negative balance without a left output or positive
557
+ balance without a right output; balance zero may drive the remaining side of a
558
+ partial rig. Stored static poses retain their portability across missing outputs.
559
+ `evaluate_semantic_body_bone_frame()` returns exact Body-owned properties including
560
+ neutral values, allowing an explicit semantic pose/reset to replace old raw bone
561
+ edits without touching unrelated properties.
@@ -0,0 +1 @@
1
+ export * from './index';