@call-me-sensei/toonlab 0.2.0 → 0.3.0

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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,441 @@
1
+ // Engine-neutral pose, clip sampler, and object-rig adapters. A game can feed
2
+ // imported animation samplers, generated keyframes, or fully procedural
3
+ // harmonic clips through the same controller contract.
4
+
5
+ const plain = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
6
+ const finite = (value, fallback = 0) => Number.isFinite(Number(value)) ? Number(value) : fallback;
7
+ const clamp01 = (value) => Math.min(Math.max(finite(value), 0), 1);
8
+ const vector = (value, fallback, size = 3) => Array.isArray(value) && value.length >= size
9
+ ? value.slice(0, size).map((entry, index) => finite(entry, fallback[index]))
10
+ : [...fallback];
11
+
12
+ const IDENTITY_TRANSFORM = Object.freeze({
13
+ position: Object.freeze([0, 0, 0]),
14
+ rotation: Object.freeze([0, 0, 0]),
15
+ scale: Object.freeze([1, 1, 1]),
16
+ });
17
+
18
+ export function normalizeMotionTransform(value = {}) {
19
+ const source = plain(value) ? value : {};
20
+ return {
21
+ position: vector(source.position ?? source.p, IDENTITY_TRANSFORM.position),
22
+ rotation: vector(source.rotation ?? source.r, IDENTITY_TRANSFORM.rotation),
23
+ scale: vector(source.scale ?? source.s, IDENTITY_TRANSFORM.scale),
24
+ };
25
+ }
26
+
27
+ export function createEmptyMotionPose() {
28
+ return { bones: {}, root: normalizeMotionTransform() };
29
+ }
30
+
31
+ export function normalizeMotionPose(value = {}) {
32
+ const source = plain(value) ? value : {};
33
+ return {
34
+ bones: Object.fromEntries(Object.entries(plain(source.bones) ? source.bones : {})
35
+ .map(([role, transform]) => [role, normalizeMotionTransform(transform)])),
36
+ root: normalizeMotionTransform(source.root),
37
+ };
38
+ }
39
+
40
+ function lerpAngle(a, b, t) {
41
+ let delta = (b - a) % (Math.PI * 2);
42
+ if (delta > Math.PI) delta -= Math.PI * 2;
43
+ if (delta < -Math.PI) delta += Math.PI * 2;
44
+ return a + delta * t;
45
+ }
46
+
47
+ function blendTransform(a, b, t) {
48
+ return {
49
+ position: a.position.map((value, index) => value + (b.position[index] - value) * t),
50
+ rotation: a.rotation.map((value, index) => lerpAngle(value, b.rotation[index], t)),
51
+ scale: a.scale.map((value, index) => value + (b.scale[index] - value) * t),
52
+ };
53
+ }
54
+
55
+ /** Blends two sparse poses while treating absent roles as identity transforms. */
56
+ export function blendMotionPoses(from, to, amount) {
57
+ const a = normalizeMotionPose(from);
58
+ const b = normalizeMotionPose(to);
59
+ const t = clamp01(amount);
60
+ const roles = new Set([...Object.keys(a.bones), ...Object.keys(b.bones)]);
61
+ return {
62
+ bones: Object.fromEntries([...roles].map((role) => [role, blendTransform(
63
+ a.bones[role] ?? normalizeMotionTransform(),
64
+ b.bones[role] ?? normalizeMotionTransform(),
65
+ t,
66
+ )])),
67
+ root: blendTransform(a.root, b.root, t),
68
+ };
69
+ }
70
+
71
+ /** Weighted normalized blend for arbitrary blend-space fan-in. */
72
+ export function blendMotionPoseList(entries = []) {
73
+ const active = entries.filter((entry) => entry?.pose && finite(entry.weight, 0) > 0);
74
+ if (active.length === 0) return createEmptyMotionPose();
75
+ let output = normalizeMotionPose(active[0].pose);
76
+ let total = finite(active[0].weight, 1);
77
+ for (let index = 1; index < active.length; index += 1) {
78
+ const weight = finite(active[index].weight, 0);
79
+ output = blendMotionPoses(output, active[index].pose, weight / (total + weight));
80
+ total += weight;
81
+ }
82
+ return output;
83
+ }
84
+
85
+ function maskMatches(role, mask) {
86
+ if (!Array.isArray(mask) || mask.length === 0) return true;
87
+ return mask.some((pattern) => {
88
+ if (pattern === '*') return true;
89
+ if (pattern.endsWith('*')) return role.startsWith(pattern.slice(0, -1));
90
+ return role === pattern;
91
+ });
92
+ }
93
+
94
+ /** Applies an additive or override layer with an arbitrary role mask. */
95
+ export function layerMotionPose(base, layer, { mode = 'additive', weight = 1, mask = [] } = {}) {
96
+ const output = normalizeMotionPose(base);
97
+ const source = normalizeMotionPose(layer);
98
+ const amount = clamp01(weight);
99
+ for (const [role, transform] of Object.entries(source.bones)) {
100
+ if (!maskMatches(role, mask)) continue;
101
+ const target = output.bones[role] ?? normalizeMotionTransform();
102
+ if (mode === 'override') {
103
+ output.bones[role] = blendTransform(target, transform, amount);
104
+ } else {
105
+ output.bones[role] = {
106
+ position: target.position.map((value, index) => value + transform.position[index] * amount),
107
+ rotation: target.rotation.map((value, index) => value + transform.rotation[index] * amount),
108
+ scale: target.scale.map((value, index) => value * (1 + (transform.scale[index] - 1) * amount)),
109
+ };
110
+ }
111
+ }
112
+ if (maskMatches('root', mask)) {
113
+ output.root = mode === 'override'
114
+ ? blendTransform(output.root, source.root, amount)
115
+ : {
116
+ position: output.root.position.map((value, index) => value + source.root.position[index] * amount),
117
+ rotation: output.root.rotation.map((value, index) => value + source.root.rotation[index] * amount),
118
+ scale: output.root.scale.map((value, index) => value * (1 + (source.root.scale[index] - 1) * amount)),
119
+ };
120
+ }
121
+ return output;
122
+ }
123
+
124
+ function normalizeKey(value, index, duration) {
125
+ const source = plain(value) ? value : {};
126
+ return {
127
+ time: Math.min(Math.max(finite(source.time ?? source.at, duration ? index / Math.max(duration, 1) : index), 0), duration),
128
+ transform: normalizeMotionTransform(source),
129
+ };
130
+ }
131
+
132
+ function sampleTrack(track, time) {
133
+ if (track.length === 0) return normalizeMotionTransform();
134
+ if (track.length === 1 || time <= track[0].time) return normalizeMotionTransform(track[0].transform);
135
+ if (time >= track.at(-1).time) return normalizeMotionTransform(track.at(-1).transform);
136
+ let upper = 1;
137
+ while (upper < track.length && track[upper].time < time) upper += 1;
138
+ const a = track[upper - 1];
139
+ const b = track[upper];
140
+ return blendTransform(a.transform, b.transform, (time - a.time) / Math.max(b.time - a.time, 1e-6));
141
+ }
142
+
143
+ function normalizeClipEvents(events = [], duration = 1) {
144
+ return (Array.isArray(events) ? events : []).map((entry, index) => {
145
+ const source = plain(entry) ? entry : { name: entry };
146
+ const rawTime = finite(source.time ?? source.at, 0);
147
+ return {
148
+ id: String(source.id ?? `event-${index}`),
149
+ name: String(source.name ?? source.type ?? 'event'),
150
+ time: Math.min(Math.max(source.normalized ? rawTime * duration : rawTime, 0), duration),
151
+ once: Boolean(source.once),
152
+ payload: plain(source.payload) ? { ...source.payload } : {},
153
+ };
154
+ }).sort((a, b) => a.time - b.time);
155
+ }
156
+
157
+ /**
158
+ * Creates a serializable keyframe sampler. Tracks are keyed by semantic bone
159
+ * role, so a missing role on a target skeleton is harmless.
160
+ */
161
+ export function createKeyframeMotionClip(definition = {}) {
162
+ const duration = Math.max(0.001, finite(definition.duration, 1));
163
+ const tracks = Object.fromEntries(Object.entries(plain(definition.tracks) ? definition.tracks : {})
164
+ .map(([role, values]) => [role, (Array.isArray(values) ? values : [])
165
+ .map((entry, index) => normalizeKey(entry, index, duration))
166
+ .sort((a, b) => a.time - b.time)]));
167
+ const rootTrack = (Array.isArray(definition.root) ? definition.root : [])
168
+ .map((entry, index) => normalizeKey(entry, index, duration))
169
+ .sort((a, b) => a.time - b.time);
170
+ const events = normalizeClipEvents(definition.events, duration);
171
+ return {
172
+ id: String(definition.id ?? 'keyframe-clip'),
173
+ duration,
174
+ events,
175
+ sample(time, { loop = true } = {}) {
176
+ const local = loop
177
+ ? ((finite(time) % duration) + duration) % duration
178
+ : Math.min(Math.max(finite(time), 0), duration);
179
+ return {
180
+ bones: Object.fromEntries(Object.entries(tracks).map(([role, track]) => [role, sampleTrack(track, local)])),
181
+ root: sampleTrack(rootTrack, local),
182
+ };
183
+ },
184
+ dispose() {},
185
+ };
186
+ }
187
+
188
+ function sampleHarmonics(channels, time, base) {
189
+ const output = [...base];
190
+ for (const channel of Array.isArray(channels) ? channels : []) {
191
+ const axis = Math.min(Math.max(Math.round(finite(channel.axis, 0)), 0), 2);
192
+ const wave = channel.wave === 'triangle'
193
+ ? (2 / Math.PI) * Math.asin(Math.sin((finite(channel.frequency, 1) * time + finite(channel.phase, 0)) * Math.PI * 2))
194
+ : channel.wave === 'square'
195
+ ? Math.sign(Math.sin((finite(channel.frequency, 1) * time + finite(channel.phase, 0)) * Math.PI * 2))
196
+ : Math.sin((finite(channel.frequency, 1) * time + finite(channel.phase, 0)) * Math.PI * 2);
197
+ output[axis] += finite(channel.offset, 0) + finite(channel.amplitude, 0) * wave;
198
+ }
199
+ return output;
200
+ }
201
+
202
+ function sampleHarmonicTransform(definition, time) {
203
+ const source = plain(definition) ? definition : {};
204
+ return {
205
+ position: sampleHarmonics(source.position, time, [0, 0, 0]),
206
+ rotation: sampleHarmonics(source.rotation, time, [0, 0, 0]),
207
+ scale: sampleHarmonics(source.scale, time, [1, 1, 1]),
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Infinite procedural clip primitive: every role may have any number of sine,
213
+ * triangle, or square channels. It is useful for generated prototypes and is
214
+ * not tied to a fixed animation-name catalog.
215
+ */
216
+ export function createHarmonicMotionClip(definition = {}) {
217
+ const duration = Math.max(0.001, finite(definition.duration, 1));
218
+ const channels = plain(definition.channels) ? definition.channels : {};
219
+ return {
220
+ id: String(definition.id ?? 'harmonic-clip'),
221
+ duration,
222
+ events: normalizeClipEvents(definition.events, duration),
223
+ sample(time, { loop = true } = {}) {
224
+ const local = loop
225
+ ? ((finite(time) % duration) + duration) % duration
226
+ : Math.min(Math.max(finite(time), 0), duration);
227
+ return {
228
+ bones: Object.fromEntries(Object.entries(channels)
229
+ .filter(([role]) => role !== 'root')
230
+ .map(([role, track]) => [role, sampleHarmonicTransform(track, local)])),
231
+ root: sampleHarmonicTransform(channels.root, local),
232
+ };
233
+ },
234
+ dispose() {},
235
+ };
236
+ }
237
+
238
+ function trackTarget(trackName) {
239
+ const match = String(trackName).match(/^(.*)\.(position|quaternion|rotation|scale)$/);
240
+ if (!match) return null;
241
+ const path = match[1];
242
+ const bracket = path.match(/\[([^\]]+)\]$/);
243
+ const bone = bracket?.[1] ?? path.split(/[/.]/).filter(Boolean).at(-1) ?? path;
244
+ return { bone, property: match[2] };
245
+ }
246
+
247
+ function quaternionToEuler(value) {
248
+ const [x, y, z, w] = value;
249
+ const test = 2 * (w * y - z * x);
250
+ return [
251
+ Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)),
252
+ Math.asin(Math.min(Math.max(test, -1), 1)),
253
+ Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)),
254
+ ];
255
+ }
256
+
257
+ function relativeQuaternion(value, reference) {
258
+ const [ax, ay, az, aw] = reference;
259
+ const [bx, by, bz, bw] = value;
260
+ // inverse(reference) * value; normalized animation quaternions make the
261
+ // conjugate the inverse.
262
+ return [
263
+ aw * bx - ax * bw - ay * bz + az * by,
264
+ aw * by + ax * bz - ay * bw - az * bx,
265
+ aw * bz - ax * by + ay * bx - az * bw,
266
+ aw * bw + ax * bx + ay * by + az * bz,
267
+ ];
268
+ }
269
+
270
+ /**
271
+ * Adapts a Three.js AnimationClip without coupling the graph to an
272
+ * AnimationMixer. Track names are mapped to semantic roles, evaluated into
273
+ * first-frame-relative transforms, then blended like any generated clip.
274
+ */
275
+ export function createThreeAnimationClipSampler(animationClip, {
276
+ events = [],
277
+ roleMap = {},
278
+ rootRole = 'root',
279
+ reference = 'firstFrame',
280
+ } = {}) {
281
+ if (!animationClip || !Array.isArray(animationClip.tracks)) {
282
+ throw new TypeError('createThreeAnimationClipSampler requires a Three.js AnimationClip.');
283
+ }
284
+ const duration = Math.max(0.001, finite(animationClip.duration, 1));
285
+ const roleByBone = new Map(Object.entries(roleMap).map(([role, bone]) => [String(bone), role]));
286
+ const tracks = animationClip.tracks.map((track) => {
287
+ const target = trackTarget(track.name);
288
+ if (!target || typeof track.createInterpolant !== 'function') return null;
289
+ const interpolant = track.createInterpolant();
290
+ return {
291
+ ...target,
292
+ role: roleByBone.get(target.bone) ?? target.bone,
293
+ interpolant,
294
+ initial: Array.from(interpolant.evaluate(0)),
295
+ };
296
+ }).filter(Boolean);
297
+ const explicitReference = plain(reference) ? normalizeMotionPose(reference) : null;
298
+
299
+ function relativeValue(track, sampled) {
300
+ const referenceTransform = track.role === rootRole
301
+ ? explicitReference?.root
302
+ : explicitReference?.bones?.[track.role];
303
+ if (track.property === 'quaternion') {
304
+ const base = referenceTransform?.rotation;
305
+ return base
306
+ ? quaternionToEuler(sampled).map((value, index) => value - base[index])
307
+ : quaternionToEuler(reference === 'none' ? sampled : relativeQuaternion(sampled, track.initial));
308
+ }
309
+ if (track.property === 'rotation') {
310
+ const base = referenceTransform?.rotation ?? (reference === 'none' ? [0, 0, 0] : track.initial);
311
+ return sampled.map((value, index) => value - finite(base[index], 0));
312
+ }
313
+ if (track.property === 'position') {
314
+ const base = referenceTransform?.position ?? (reference === 'none' ? [0, 0, 0] : track.initial);
315
+ return sampled.map((value, index) => value - finite(base[index], 0));
316
+ }
317
+ const base = referenceTransform?.scale ?? (reference === 'none' ? [1, 1, 1] : track.initial);
318
+ return sampled.map((value, index) => value / Math.max(Math.abs(finite(base[index], 1)), 1e-6));
319
+ }
320
+
321
+ return {
322
+ id: String(animationClip.name || animationClip.uuid || 'three-animation-clip'),
323
+ duration,
324
+ events: normalizeClipEvents(events, duration),
325
+ sample(time, { loop = true } = {}) {
326
+ const local = loop
327
+ ? ((finite(time) % duration) + duration) % duration
328
+ : Math.min(Math.max(finite(time), 0), duration);
329
+ const pose = createEmptyMotionPose();
330
+ for (const track of tracks) {
331
+ const sampled = Array.from(track.interpolant.evaluate(local));
332
+ const target = track.role === rootRole ? pose.root : (pose.bones[track.role] ??= normalizeMotionTransform());
333
+ const property = track.property === 'quaternion' ? 'rotation' : track.property;
334
+ target[property] = relativeValue(track, sampled);
335
+ }
336
+ return pose;
337
+ },
338
+ dispose() {},
339
+ };
340
+ }
341
+
342
+ function findNamed(root, name) {
343
+ if (!root || !name) return null;
344
+ if (root.name === name) return root;
345
+ if (typeof root.getObjectByName === 'function') return root.getObjectByName(name) ?? null;
346
+ let result = null;
347
+ root.traverse?.((object) => {
348
+ if (!result && object.name === name) result = object;
349
+ });
350
+ return result;
351
+ }
352
+
353
+ function readVector(target, fallback) {
354
+ return target && ['x', 'y', 'z'].every((key) => Number.isFinite(target[key]))
355
+ ? [target.x, target.y, target.z]
356
+ : [...fallback];
357
+ }
358
+
359
+ function writeVector(target, values) {
360
+ if (!target) return;
361
+ if (typeof target.set === 'function') target.set(...values);
362
+ else [target.x, target.y, target.z] = values;
363
+ }
364
+
365
+ /**
366
+ * Adapts a Three.js-like object hierarchy to semantic role poses. Role values
367
+ * may be objects or names. Missing roles are counted and ignored by design.
368
+ */
369
+ export function createObjectMotionRig(root, { roles = {}, missingBonePolicy = 'ignore' } = {}) {
370
+ const bindings = {};
371
+ const reference = {};
372
+ const missingBones = new Set();
373
+ for (const [role, target] of Object.entries(roles)) {
374
+ const object = typeof target === 'string' ? findNamed(root, target) : target;
375
+ if (!object) {
376
+ missingBones.add(role);
377
+ if (missingBonePolicy === 'error') throw new Error(`Motion rig is missing role "${role}".`);
378
+ continue;
379
+ }
380
+ bindings[role] = object;
381
+ reference[role] = {
382
+ position: readVector(object.position, [0, 0, 0]),
383
+ rotation: readVector(object.rotation, [0, 0, 0]),
384
+ scale: readVector(object.scale, [1, 1, 1]),
385
+ };
386
+ }
387
+ const rootReference = {
388
+ position: readVector(root?.position, [0, 0, 0]),
389
+ rotation: readVector(root?.rotation, [0, 0, 0]),
390
+ scale: readVector(root?.scale, [1, 1, 1]),
391
+ };
392
+ const worldOffset = [0, 0, 0];
393
+ let yawOffset = 0;
394
+ let disposed = false;
395
+
396
+ function applyPose(value) {
397
+ if (disposed) return;
398
+ const pose = normalizeMotionPose(value);
399
+ for (const [role, object] of Object.entries(bindings)) {
400
+ const base = reference[role];
401
+ const transform = pose.bones[role] ?? normalizeMotionTransform();
402
+ writeVector(object.position, base.position.map((entry, index) => entry + transform.position[index]));
403
+ writeVector(object.rotation, base.rotation.map((entry, index) => entry + transform.rotation[index]));
404
+ writeVector(object.scale, base.scale.map((entry, index) => entry * transform.scale[index]));
405
+ }
406
+ for (const role of Object.keys(pose.bones)) {
407
+ if (!bindings[role]) missingBones.add(role);
408
+ }
409
+ if (root) {
410
+ writeVector(root.position, rootReference.position.map((entry, index) => entry + worldOffset[index] + pose.root.position[index]));
411
+ writeVector(root.rotation, rootReference.rotation.map((entry, index) => entry + pose.root.rotation[index] + (index === 1 ? yawOffset : 0)));
412
+ writeVector(root.scale, rootReference.scale.map((entry, index) => entry * pose.root.scale[index]));
413
+ }
414
+ }
415
+
416
+ return {
417
+ root,
418
+ roles: bindings,
419
+ applyPose,
420
+ applyRootMotion(delta = [0, 0, 0], yaw = 0) {
421
+ for (let index = 0; index < 3; index += 1) worldOffset[index] += finite(delta[index], 0);
422
+ yawOffset += finite(yaw, 0);
423
+ },
424
+ reset() {
425
+ worldOffset.fill(0);
426
+ yawOffset = 0;
427
+ applyPose(createEmptyMotionPose());
428
+ },
429
+ stats() {
430
+ return {
431
+ boundBoneCount: Object.keys(bindings).length,
432
+ missingBoneCount: missingBones.size,
433
+ missingBones: [...missingBones],
434
+ rootOffset: [...worldOffset],
435
+ };
436
+ },
437
+ dispose() {
438
+ disposed = true;
439
+ },
440
+ };
441
+ }