talking-head-studio 0.4.11 → 0.4.12

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 (142) hide show
  1. package/README.md +279 -193
  2. package/dist/TalkingHead.d.ts +28 -3
  3. package/dist/TalkingHead.js +21 -2
  4. package/dist/TalkingHead.web.d.ts +31 -4
  5. package/dist/TalkingHead.web.js +11 -1
  6. package/dist/TalkingHeadVisualization.d.ts +22 -0
  7. package/dist/TalkingHeadVisualization.js +30 -10
  8. package/dist/api/studioApi.d.ts +12 -1
  9. package/dist/api/studioApi.js +16 -2
  10. package/dist/contract.d.ts +14 -0
  11. package/dist/contract.js +30 -0
  12. package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
  13. package/dist/core/avatar/avatarCapabilities.js +100 -0
  14. package/dist/core/avatar/backends/gaussian.js +6 -4
  15. package/dist/core/avatar/motion.d.ts +1713 -0
  16. package/dist/core/avatar/motion.js +550 -0
  17. package/dist/core/avatar/motionRuntime.d.ts +46 -0
  18. package/dist/core/avatar/motionRuntime.js +84 -0
  19. package/dist/core/avatar/schema.d.ts +33 -5
  20. package/dist/core/avatar/visemes.d.ts +16 -1
  21. package/dist/core/avatar/visemes.js +48 -1
  22. package/dist/editor/AvatarCanvas.js +92 -1
  23. package/dist/editor/AvatarEditor.native.js +1 -0
  24. package/dist/editor/AvatarModel.js +1 -0
  25. package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
  26. package/dist/editor/FaceSqueezeEditor.js +176 -112
  27. package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
  28. package/dist/editor/FaceSqueezeEditor.web.js +30 -28
  29. package/dist/editor/RigidAccessory.js +17 -2
  30. package/dist/editor/SkinnedClothing.js +1 -0
  31. package/dist/editor/boneLockedDrag.d.ts +11 -0
  32. package/dist/editor/boneLockedDrag.js +68 -0
  33. package/dist/editor/boneSnap.web.d.ts +27 -0
  34. package/dist/editor/boneSnap.web.js +99 -0
  35. package/dist/editor/index.web.d.ts +10 -0
  36. package/dist/editor/index.web.js +26 -0
  37. package/dist/editor/sounds/haha.wav +0 -0
  38. package/dist/editor/sounds/owie.wav +0 -0
  39. package/dist/editor/sounds/stop.wav +0 -0
  40. package/dist/editor/studioTheme.d.ts +14 -14
  41. package/dist/editor/studioTheme.js +17 -14
  42. package/dist/editor/types.d.ts +1 -0
  43. package/dist/html/accessories.d.ts +7 -0
  44. package/dist/html/accessories.js +149 -0
  45. package/dist/html/motion.d.ts +1 -0
  46. package/dist/html/motion.js +189 -0
  47. package/dist/html/visemes.d.ts +7 -0
  48. package/dist/html/visemes.js +348 -0
  49. package/dist/html.d.ts +1 -1
  50. package/dist/html.js +55 -732
  51. package/dist/index.d.ts +7 -3
  52. package/dist/index.js +17 -1
  53. package/dist/index.web.d.ts +18 -1
  54. package/dist/index.web.js +36 -3
  55. package/dist/sketchfab/api.js +1 -0
  56. package/dist/sketchfab/glbInspect.d.ts +22 -0
  57. package/dist/sketchfab/glbInspect.js +58 -0
  58. package/dist/sketchfab/index.d.ts +3 -0
  59. package/dist/sketchfab/index.js +8 -1
  60. package/dist/sketchfab/inspectRemote.d.ts +13 -0
  61. package/dist/sketchfab/inspectRemote.js +77 -0
  62. package/dist/sketchfab/types.d.ts +10 -0
  63. package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
  64. package/dist/studio/AccessoryBrowserScreen.js +626 -0
  65. package/dist/studio/AccessoryPanel.d.ts +10 -0
  66. package/dist/studio/AccessoryPanel.js +396 -0
  67. package/dist/studio/AppearancePanel.d.ts +9 -0
  68. package/dist/studio/AppearancePanel.js +77 -0
  69. package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
  70. package/dist/studio/AvatarCreatorScreen.js +806 -0
  71. package/dist/studio/AvatarEditorScreen.d.ts +14 -0
  72. package/dist/studio/AvatarEditorScreen.js +510 -0
  73. package/dist/studio/AvatarGrid.d.ts +23 -0
  74. package/dist/studio/AvatarGrid.js +257 -0
  75. package/dist/studio/ColorSwatch.d.ts +8 -0
  76. package/dist/studio/ColorSwatch.js +100 -0
  77. package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
  78. package/dist/studio/CreateVoiceProfileSheet.js +242 -0
  79. package/dist/studio/DetailsPanel.d.ts +15 -0
  80. package/dist/studio/DetailsPanel.js +239 -0
  81. package/dist/studio/FilamentEditor.d.ts +2 -0
  82. package/dist/studio/FilamentEditor.js +6 -0
  83. package/dist/studio/PrecisionPanel.d.ts +2 -0
  84. package/dist/studio/PrecisionPanel.js +7 -0
  85. package/dist/studio/PublicGalleryScreen.d.ts +5 -0
  86. package/dist/studio/PublicGalleryScreen.js +358 -0
  87. package/dist/studio/SketchfabModelCard.d.ts +20 -0
  88. package/dist/studio/SketchfabModelCard.js +104 -0
  89. package/dist/studio/StudioBrowseHeader.d.ts +9 -0
  90. package/dist/studio/StudioBrowseHeader.js +28 -0
  91. package/dist/studio/StudioEmptyState.d.ts +8 -0
  92. package/dist/studio/StudioEmptyState.js +29 -0
  93. package/dist/studio/StudioFloatingAction.d.ts +13 -0
  94. package/dist/studio/StudioFloatingAction.js +42 -0
  95. package/dist/studio/StudioSectionHeader.d.ts +7 -0
  96. package/dist/studio/StudioSectionHeader.js +27 -0
  97. package/dist/studio/StudioSurfaceCard.d.ts +8 -0
  98. package/dist/studio/StudioSurfaceCard.js +20 -0
  99. package/dist/studio/VoicePanel.d.ts +15 -0
  100. package/dist/studio/VoicePanel.js +305 -0
  101. package/dist/studio/constants.d.ts +3 -0
  102. package/dist/studio/constants.js +6 -0
  103. package/dist/studio/index.d.ts +29 -0
  104. package/dist/studio/index.js +54 -0
  105. package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
  106. package/dist/studio/useSketchfabCapabilities.js +82 -0
  107. package/dist/tts/useDirectVisemeStream.js +15 -10
  108. package/dist/utils/avatarUtils.js +92 -5
  109. package/dist/utils/faceLandmarkerToShapeWeights.js +2 -4
  110. package/dist/voice/useAudioPlayer.js +17 -4
  111. package/dist/voice/useVoicePreview.js +4 -2
  112. package/dist/wardrobe/index.d.ts +1 -0
  113. package/dist/wardrobe/index.js +6 -1
  114. package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
  115. package/dist/wardrobe/useAccessoryGestures.js +94 -0
  116. package/dist/wardrobe/useAvatarWardrobeHydration.js +8 -2
  117. package/dist/wardrobe/useStudioAvatar.js +11 -2
  118. package/dist/wardrobe/wardrobeStore.d.ts +2 -0
  119. package/dist/wardrobe/wardrobeStore.js +12 -2
  120. package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
  121. package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
  122. package/dist/wgpu/WgpuAvatar.d.ts +26 -2
  123. package/dist/wgpu/WgpuAvatar.js +296 -39
  124. package/dist/wgpu/accessoryDefaults.d.ts +12 -0
  125. package/dist/wgpu/accessoryDefaults.js +19 -0
  126. package/dist/wgpu/blobShim.d.ts +2 -0
  127. package/dist/wgpu/blobShim.js +191 -0
  128. package/dist/wgpu/index.d.ts +1 -0
  129. package/dist/wgpu/index.js +4 -1
  130. package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
  131. package/dist/wgpu/loadGLTFFromUri.js +75 -0
  132. package/dist/wgpu/morphTables.js +21 -10
  133. package/dist/wgpu/motionState.d.ts +20 -0
  134. package/dist/wgpu/motionState.js +31 -0
  135. package/dist/wgpu/patchThreeForRN.d.ts +28 -0
  136. package/dist/wgpu/patchThreeForRN.js +292 -0
  137. package/dist/wgpu/scenePlacement.d.ts +5 -0
  138. package/dist/wgpu/scenePlacement.js +50 -0
  139. package/dist/wgpu/useAuthedModelUri.js +4 -2
  140. package/dist/wgpu/useNativeGLTF.d.ts +7 -0
  141. package/dist/wgpu/useNativeGLTF.js +36 -0
  142. package/package.json +97 -31
@@ -48,18 +48,21 @@ const jsx_runtime_1 = require("react/jsx-runtime");
48
48
  * Drop-in replacement: same FaceSqueezeEditorProps interface.
49
49
  * The .web.tsx stub is shared — import from FaceSqueezeEditor.web.tsx.
50
50
  */
51
+ require("../wgpu/blobShim");
51
52
  const react_1 = require("react");
52
53
  const react_native_1 = require("react-native");
53
- const native_1 = require("@react-three/fiber/native");
54
- const native_2 = require("@react-three/drei/native");
54
+ const fiber_1 = require("@react-three/fiber");
55
55
  const THREE = __importStar(require("three"));
56
- const react_native_gesture_handler_1 = require("react-native-gesture-handler");
57
- const react_native_reanimated_1 = require("react-native-reanimated");
58
- const Haptics = __importStar(require("expo-haptics"));
59
- const expo_audio_1 = require("expo-audio");
60
- const expo_asset_1 = require("expo-asset");
56
+ const avatarUtils_1 = require("../utils/avatarUtils");
57
+ const R3FWebGpuCanvas_1 = require("../wgpu/R3FWebGpuCanvas");
58
+ const patchThreeForRN_1 = require("../wgpu/patchThreeForRN");
59
+ const useNativeGLTF_1 = require("../wgpu/useNativeGLTF");
61
60
  // eslint-disable-next-line @typescript-eslint/no-require-imports
62
61
  exports.FACE_SQUEEZE_LOCAL_MODULE = require('../assets/face-squeeze-local.glb');
62
+ (0, patchThreeForRN_1.installThreeRNPatches)(false);
63
+ function emitHaptic() {
64
+ // Optional in the example path.
65
+ }
63
66
  // ---------------------------------------------------------------------------
64
67
  // Constants (identical to Filament version)
65
68
  // ---------------------------------------------------------------------------
@@ -67,17 +70,6 @@ const SPRING_DURATION_MS = 300;
67
70
  const EMOTION_HOLD_MS = 900;
68
71
  const IDLE_EMOTION_INTERVAL_MS = 30000;
69
72
  const INITIAL_IDLE_EMOTION_DELAY_MS = 1200;
70
- // eslint-disable-next-line @typescript-eslint/no-require-imports
71
- const SOUND_OWIE = require('./sounds/owie.wav');
72
- // eslint-disable-next-line @typescript-eslint/no-require-imports
73
- const SOUND_STOP = require('./sounds/stop.wav');
74
- // eslint-disable-next-line @typescript-eslint/no-require-imports
75
- const SOUND_HAHA = require('./sounds/haha.wav');
76
- const REACTION_SOUNDS = [
77
- SOUND_OWIE,
78
- SOUND_STOP,
79
- SOUND_HAHA,
80
- ];
81
73
  const EMOTION_PRESETS = {
82
74
  owie: {
83
75
  eyeBlinkLeft: 1.0, eyeBlinkRight: 1.0,
@@ -158,12 +150,56 @@ function pickIdleEmotion() {
158
150
  }
159
151
  return IDLE_EMOTIONS[0].key;
160
152
  }
153
+ function morphAliases(name) {
154
+ const out = new Set([name, name.toLowerCase()]);
155
+ const pairs = [
156
+ ['Left', '_L'],
157
+ ['Right', '_R'],
158
+ ['Left', 'Left'],
159
+ ['Right', 'Right'],
160
+ ['left', '_l'],
161
+ ['right', '_r'],
162
+ ];
163
+ for (const [needle, replacement] of pairs) {
164
+ if (!name.includes(needle))
165
+ continue;
166
+ out.add(name.replace(needle, replacement));
167
+ out.add(name.replace(needle, replacement).toLowerCase());
168
+ out.add(name.replace(needle, replacement.replace('_', '')));
169
+ out.add(name.replace(needle, replacement.replace('_', '')).toLowerCase());
170
+ }
171
+ if (name.endsWith('Left')) {
172
+ const stem = name.slice(0, -4);
173
+ out.add(`${stem}_L`);
174
+ out.add(`${stem}_l`);
175
+ out.add(`${stem}L`);
176
+ out.add(`${stem}Left`);
177
+ }
178
+ if (name.endsWith('Right')) {
179
+ const stem = name.slice(0, -5);
180
+ out.add(`${stem}_R`);
181
+ out.add(`${stem}_r`);
182
+ out.add(`${stem}R`);
183
+ out.add(`${stem}Right`);
184
+ }
185
+ return [...out];
186
+ }
187
+ function resolveMorphIndices(index, name) {
188
+ const found = new Set();
189
+ for (const alias of morphAliases(name)) {
190
+ const i = index.get(alias) ?? index.get(alias.toLowerCase());
191
+ if (i !== undefined)
192
+ found.add(i);
193
+ }
194
+ return [...found];
195
+ }
161
196
  // ---------------------------------------------------------------------------
162
197
  // Inner R3F scene — runs inside Canvas
163
198
  // ---------------------------------------------------------------------------
164
199
  function AvatarScene({ uri, morphState, onReady, }) {
165
- const { camera } = (0, native_1.useThree)();
166
- const gltf = (0, native_2.useGLTF)(uri);
200
+ const { camera } = (0, fiber_1.useThree)();
201
+ const { gltf } = (0, useNativeGLTF_1.useNativeGLTF)(uri);
202
+ const scene = gltf?.scene ?? null;
167
203
  const readyFiredRef = (0, react_1.useRef)(false);
168
204
  // Position camera to match Filament version
169
205
  (0, react_1.useLayoutEffect)(() => {
@@ -176,11 +212,11 @@ function AvatarScene({ uri, morphState, onReady, }) {
176
212
  }, [camera]);
177
213
  // Find best mesh and build morph index
178
214
  (0, react_1.useEffect)(() => {
179
- if (!gltf?.scene)
215
+ if (!scene)
180
216
  return;
181
217
  let best = null;
182
218
  let bestCount = 0;
183
- gltf.scene.traverse((node) => {
219
+ scene.traverse((node) => {
184
220
  if (!(node instanceof THREE.Mesh))
185
221
  return;
186
222
  const count = Object.keys(node.morphTargetDictionary ?? {}).length;
@@ -203,33 +239,31 @@ function AvatarScene({ uri, morphState, onReady, }) {
203
239
  readyFiredRef.current = true;
204
240
  onReady();
205
241
  }
206
- }, [gltf, morphState, onReady]);
242
+ }, [scene, morphState, onReady]);
207
243
  // Apply morphState.weights to mesh every frame
208
- (0, native_1.useFrame)(() => {
244
+ (0, fiber_1.useFrame)(() => {
209
245
  const { mesh, weights, index } = morphState.current;
210
246
  if (!mesh?.morphTargetInfluences)
211
247
  return;
212
248
  for (const [name, w] of Object.entries(weights)) {
213
- const i = index.get(name) ?? index.get(name.toLowerCase());
214
- if (i !== undefined)
249
+ for (const i of resolveMorphIndices(index, name)) {
215
250
  mesh.morphTargetInfluences[i] = w;
251
+ }
216
252
  }
217
253
  });
218
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.65 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [1, 3, 2], intensity: 1.3 }), (0, jsx_runtime_1.jsx)("primitive", { object: gltf.scene })] }));
254
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.65 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [1, 3, 2], intensity: 1.3 }), scene ? (0, jsx_runtime_1.jsx)("primitive", { object: scene }) : null] }));
219
255
  }
220
256
  // ---------------------------------------------------------------------------
221
257
  // Main component
222
258
  // ---------------------------------------------------------------------------
223
- function FaceSqueezeEditor({ onClose }) {
259
+ function FaceSqueezeEditor({ onClose, avatarModule = exports.FACE_SQUEEZE_LOCAL_MODULE, }) {
224
260
  const [localUri, setLocalUri] = (0, react_1.useState)(null);
225
261
  const [isReady, setIsReady] = (0, react_1.useState)(false);
226
262
  (0, react_1.useEffect)(() => {
227
263
  let cancelled = false;
228
- (async () => {
264
+ void (async () => {
229
265
  try {
230
- const asset = expo_asset_1.Asset.fromModule(exports.FACE_SQUEEZE_LOCAL_MODULE);
231
- await asset.downloadAsync();
232
- const uri = asset.localUri ?? asset.uri;
266
+ const uri = await (0, avatarUtils_1.resolveLocalAssetUrl)(avatarModule);
233
267
  if (!cancelled && uri)
234
268
  setLocalUri(uri);
235
269
  }
@@ -238,16 +272,13 @@ function FaceSqueezeEditor({ onClose }) {
238
272
  }
239
273
  })();
240
274
  return () => { cancelled = true; };
241
- }, []);
275
+ }, [avatarModule]);
242
276
  // Shared morph state — mutated directly, never triggers re-render
243
277
  const morphState = (0, react_1.useRef)({
244
278
  weights: {},
245
279
  index: new Map(),
246
280
  mesh: null,
247
281
  });
248
- // ---- Audio ----
249
- const reactionSoundIdx = (0, react_1.useRef)(0);
250
- const player = (0, expo_audio_1.useAudioPlayer)(REACTION_SOUNDS[0]);
251
282
  // ---- Timers ----
252
283
  const springRef = (0, react_1.useRef)(null);
253
284
  const emotionHoldTimerRef = (0, react_1.useRef)(null);
@@ -255,19 +286,17 @@ function FaceSqueezeEditor({ onClose }) {
255
286
  const lipSyncIntervalRef = (0, react_1.useRef)(null);
256
287
  // ---- View geometry (for gesture zone math) ----
257
288
  const viewSize = (0, react_1.useRef)({ width: 1, height: 1 });
258
- const viewWidthSv = (0, react_native_reanimated_1.useSharedValue)(1);
259
- const viewHeightSv = (0, react_native_reanimated_1.useSharedValue)(1);
260
- const touchStartXSv = (0, react_native_reanimated_1.useSharedValue)(0);
261
- const touchStartYSv = (0, react_native_reanimated_1.useSharedValue)(0);
262
- const touchZoneSv = (0, react_native_reanimated_1.useSharedValue)('cheek_left');
263
- const lastPanUpdateTsSv = (0, react_native_reanimated_1.useSharedValue)(0);
289
+ const touchStartRef = (0, react_1.useRef)({ x: 0, y: 0 });
290
+ const touchZoneRef = (0, react_1.useRef)('cheek_left');
291
+ const lastPanUpdateTsRef = (0, react_1.useRef)(0);
292
+ const pinchStartDistanceRef = (0, react_1.useRef)(null);
293
+ const activeTouchesRef = (0, react_1.useRef)(0);
294
+ const hasMovedRef = (0, react_1.useRef)(false);
264
295
  const onLayout = (0, react_1.useCallback)((e) => {
265
296
  const w = Math.max(1, e.nativeEvent.layout.width);
266
297
  const h = Math.max(1, e.nativeEvent.layout.height);
267
298
  viewSize.current = { width: w, height: h };
268
- viewWidthSv.value = w;
269
- viewHeightSv.value = h;
270
- }, [viewHeightSv, viewWidthSv]);
299
+ }, []);
271
300
  // ---- Weight helpers ----
272
301
  const applyWeights = (0, react_1.useCallback)((targets) => {
273
302
  for (const [name, w] of Object.entries(targets)) {
@@ -351,15 +380,8 @@ function FaceSqueezeEditor({ onClose }) {
351
380
  }, TICK_MS);
352
381
  }, [applyWeights, stopLipSync]);
353
382
  const playReaction = (0, react_1.useCallback)((durationMs = 1400) => {
354
- const idx = reactionSoundIdx.current % REACTION_SOUNDS.length;
355
- reactionSoundIdx.current += 1;
356
- try {
357
- player.replace(REACTION_SOUNDS[idx]);
358
- player.play();
359
- }
360
- catch { /* ignore */ }
361
383
  startLipSync(durationMs);
362
- }, [player, startLipSync]);
384
+ }, [startLipSync]);
363
385
  // Cleanup
364
386
  (0, react_1.useEffect)(() => () => {
365
387
  if (springRef.current !== null)
@@ -384,10 +406,9 @@ function FaceSqueezeEditor({ onClose }) {
384
406
  };
385
407
  }, [isReady, playEmotionBurst]);
386
408
  // ---- Gesture callbacks (JS side — called via runOnJS) ----
387
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
388
409
  const onPanBeginJs = (0, react_1.useCallback)((_x, _y, _zone) => {
389
410
  stopSpring();
390
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
411
+ emitHaptic();
391
412
  }, [stopSpring]);
392
413
  const onPanUpdateJs = (0, react_1.useCallback)((zone, dx, dy, isLeftHalf) => {
393
414
  const u = {};
@@ -463,7 +484,7 @@ function FaceSqueezeEditor({ onClose }) {
463
484
  }, [springToZero]);
464
485
  const onPinchBeginJs = (0, react_1.useCallback)(() => {
465
486
  stopSpring();
466
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
487
+ emitHaptic();
467
488
  }, [stopSpring]);
468
489
  const onPinchUpdateJs = (0, react_1.useCallback)((scale, delta) => {
469
490
  if (scale < 1) {
@@ -482,82 +503,125 @@ function FaceSqueezeEditor({ onClose }) {
482
503
  const relY = y / Math.max(1, viewSize.current.height);
483
504
  if (relY < 0.16 || relY > 0.52)
484
505
  return;
485
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
506
+ emitHaptic();
486
507
  playReaction();
487
508
  playEmotionBurst('owie');
488
509
  }, [playEmotionBurst, playReaction]);
489
- // ---- Gestures ----
490
- const panGesture = react_native_gesture_handler_1.Gesture.Pan()
491
- .minDistance(0)
492
- .onBegin((e) => {
493
- 'worklet';
494
- touchStartXSv.value = e.x;
495
- touchStartYSv.value = e.y;
496
- const relY = e.y / Math.max(1, viewHeightSv.value);
497
- let zone;
510
+ const getZoneForPoint = (0, react_1.useCallback)((x, y) => {
511
+ const relY = y / Math.max(1, viewSize.current.height);
498
512
  if (relY < 0.20)
499
- zone = 'brow';
500
- else if (relY < 0.38)
501
- zone = 'eye';
502
- else if (relY > 0.72)
503
- zone = 'jaw';
504
- else
505
- zone = e.x < viewWidthSv.value / 2 ? 'cheek_left' : 'cheek_right';
506
- touchZoneSv.value = zone;
507
- (0, react_native_reanimated_1.runOnJS)(onPanBeginJs)(e.x, e.y, zone);
508
- })
509
- .onUpdate((e) => {
510
- 'worklet';
513
+ return 'brow';
514
+ if (relY < 0.38)
515
+ return 'eye';
516
+ if (relY > 0.72)
517
+ return 'jaw';
518
+ return x < viewSize.current.width / 2 ? 'cheek_left' : 'cheek_right';
519
+ }, []);
520
+ const distanceBetweenTouches = (0, react_1.useCallback)((event) => {
521
+ const touches = event.nativeEvent.touches;
522
+ if (touches.length < 2)
523
+ return null;
524
+ const [a, b] = [touches[0], touches[1]];
525
+ const dx = b.pageX - a.pageX;
526
+ const dy = b.pageY - a.pageY;
527
+ return Math.hypot(dx, dy);
528
+ }, []);
529
+ const handleResponderGrant = (0, react_1.useCallback)((event) => {
530
+ activeTouchesRef.current = event.nativeEvent.touches.length;
531
+ hasMovedRef.current = false;
532
+ if (event.nativeEvent.touches.length >= 2) {
533
+ pinchStartDistanceRef.current = distanceBetweenTouches(event);
534
+ onPinchBeginJs();
535
+ return;
536
+ }
537
+ const { locationX: x, locationY: y } = event.nativeEvent;
538
+ touchStartRef.current = { x, y };
539
+ touchZoneRef.current = getZoneForPoint(x, y);
540
+ lastPanUpdateTsRef.current = 0;
541
+ onPanBeginJs(x, y, touchZoneRef.current);
542
+ }, [distanceBetweenTouches, getZoneForPoint, onPanBeginJs, onPinchBeginJs]);
543
+ const handleResponderMove = (0, react_1.useCallback)((event) => {
544
+ const touchCount = event.nativeEvent.touches.length;
545
+ activeTouchesRef.current = touchCount;
546
+ if (touchCount >= 2) {
547
+ const currentDistance = distanceBetweenTouches(event);
548
+ const startDistance = pinchStartDistanceRef.current;
549
+ if (!currentDistance || !startDistance)
550
+ return;
551
+ hasMovedRef.current = true;
552
+ const scale = currentDistance / startDistance;
553
+ onPinchUpdateJs(scale, Math.abs(1 - scale));
554
+ return;
555
+ }
511
556
  const now = Date.now();
512
- if (now - lastPanUpdateTsSv.value < 33)
557
+ if (now - lastPanUpdateTsRef.current < 33)
513
558
  return;
514
- lastPanUpdateTsSv.value = now;
515
- const w = Math.max(1, viewWidthSv.value);
516
- const h = Math.max(1, viewHeightSv.value);
517
- const dx = (e.x - touchStartXSv.value) / w;
518
- const dy = (e.y - touchStartYSv.value) / h;
519
- (0, react_native_reanimated_1.runOnJS)(onPanUpdateJs)(touchZoneSv.value, dx, dy, touchStartXSv.value < w / 2);
520
- })
521
- .onEnd(() => {
522
- 'worklet';
523
- (0, react_native_reanimated_1.runOnJS)(onPanEndJs)();
524
- });
525
- const pinchGesture = react_native_gesture_handler_1.Gesture.Pinch()
526
- .onBegin(() => {
527
- 'worklet';
528
- (0, react_native_reanimated_1.runOnJS)(onPinchBeginJs)();
529
- })
530
- .onUpdate((e) => {
531
- 'worklet';
532
- (0, react_native_reanimated_1.runOnJS)(onPinchUpdateJs)(e.scale, Math.abs(1 - e.scale));
533
- })
534
- .onEnd(() => {
535
- 'worklet';
536
- (0, react_native_reanimated_1.runOnJS)(onPinchEndJs)();
537
- });
538
- const tapGesture = react_native_gesture_handler_1.Gesture.Tap()
539
- .onEnd((e, success) => {
540
- 'worklet';
541
- if (success)
542
- (0, react_native_reanimated_1.runOnJS)(onEyeTapJs)(e.x, e.y);
543
- });
544
- const composed = react_native_gesture_handler_1.Gesture.Simultaneous(panGesture, pinchGesture, tapGesture);
559
+ lastPanUpdateTsRef.current = now;
560
+ const { locationX: x, locationY: y } = event.nativeEvent;
561
+ const w = Math.max(1, viewSize.current.width);
562
+ const h = Math.max(1, viewSize.current.height);
563
+ const dx = (x - touchStartRef.current.x) / w;
564
+ const dy = (y - touchStartRef.current.y) / h;
565
+ if (Math.abs(dx) > 0.01 || Math.abs(dy) > 0.01)
566
+ hasMovedRef.current = true;
567
+ onPanUpdateJs(touchZoneRef.current, dx, dy, touchStartRef.current.x < w / 2);
568
+ }, [distanceBetweenTouches, onPanUpdateJs, onPinchUpdateJs]);
569
+ const handleResponderRelease = (0, react_1.useCallback)((event) => {
570
+ if (activeTouchesRef.current >= 2 || pinchStartDistanceRef.current != null) {
571
+ pinchStartDistanceRef.current = null;
572
+ onPinchEndJs();
573
+ }
574
+ else {
575
+ onPanEndJs();
576
+ if (!hasMovedRef.current) {
577
+ const { locationX: x, locationY: y } = event.nativeEvent;
578
+ onEyeTapJs(x, y);
579
+ }
580
+ }
581
+ activeTouchesRef.current = 0;
582
+ hasMovedRef.current = false;
583
+ }, [onEyeTapJs, onPanEndJs, onPinchEndJs]);
584
+ const handleResponderTerminate = (0, react_1.useCallback)(() => {
585
+ if (pinchStartDistanceRef.current != null || activeTouchesRef.current >= 2) {
586
+ pinchStartDistanceRef.current = null;
587
+ onPinchEndJs();
588
+ }
589
+ else {
590
+ onPanEndJs();
591
+ }
592
+ activeTouchesRef.current = 0;
593
+ hasMovedRef.current = false;
594
+ }, [onPanEndJs, onPinchEndJs]);
545
595
  const handleReady = (0, react_1.useCallback)(() => setIsReady(true), []);
546
- return ((0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureHandlerRootView, { style: { flex: 1 }, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.scene, children: localUri ? ((0, jsx_runtime_1.jsx)(native_1.Canvas, { style: react_native_1.StyleSheet.absoluteFill, camera: { fov: 34, position: [0, 1.55, 1.05], near: 0.01, far: 50 }, gl: { antialias: true, alpha: false }, onCreated: ({ gl }) => gl.setClearColor(new THREE.Color('#0a0a0a')), children: (0, jsx_runtime_1.jsx)(AvatarScene, { uri: localUri, morphState: morphState, onReady: handleReady }) })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loading, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingText, children: "Loading face\u2026" }) })) }), (0, jsx_runtime_1.jsx)(react_native_gesture_handler_1.GestureDetector, { gesture: composed, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: react_native_1.StyleSheet.absoluteFill, onLayout: onLayout, collapsable: false }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.closeBtn, onPress: onClose, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.closeBtnText, children: "\u2715 Done squeezing" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.hint, children: "Drag to stretch \u00B7 Pinch lips \u00B7 Poke an eye \uD83D\uDC41" })] }) }));
596
+ const editorCamera = (0, react_1.useMemo)(() => ({
597
+ fov: 34,
598
+ position: [0, 1.55, 1.05],
599
+ near: 0.01,
600
+ far: 50,
601
+ }), []);
602
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.scene, children: [localUri ? ((0, jsx_runtime_1.jsx)(R3FWebGpuCanvas_1.R3FWebGpuCanvas, { style: react_native_1.StyleSheet.absoluteFill, camera: editorCamera, clearColor: "#0a0a0a", children: (0, jsx_runtime_1.jsx)(AvatarScene, { uri: localUri, morphState: morphState, onReady: handleReady }) })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loading, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingText, children: "Loading face\u2026" }) })), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, styles.touchOverlay], pointerEvents: "box-only", onLayout: onLayout, collapsable: false, onStartShouldSetResponder: () => true, onStartShouldSetResponderCapture: () => true, onMoveShouldSetResponder: () => true, onMoveShouldSetResponderCapture: () => true, onResponderGrant: handleResponderGrant, onResponderMove: handleResponderMove, onResponderRelease: handleResponderRelease, onResponderTerminate: handleResponderTerminate, onResponderTerminationRequest: () => true })] }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.closeBtn, onPress: onClose, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.closeBtnText, children: "\u2715 Done squeezing" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.hint, children: "Drag to stretch \u00B7 Pinch lips \u00B7 Poke an eye \uD83D\uDC41" })] }));
547
603
  }
548
604
  const styles = react_native_1.StyleSheet.create({
549
605
  container: { flex: 1, backgroundColor: '#0a0a0a' },
550
- scene: { flex: 1 },
606
+ scene: { flex: 1, position: 'relative' },
607
+ touchOverlay: {
608
+ zIndex: 10,
609
+ elevation: 10,
610
+ backgroundColor: 'transparent',
611
+ },
551
612
  loading: { flex: 1, alignItems: 'center', justifyContent: 'center' },
552
613
  loadingText: { color: '#97a3b5', fontSize: 14 },
553
614
  closeBtn: {
554
615
  position: 'absolute', top: 52, right: 16,
555
616
  backgroundColor: '#6c63ff', borderRadius: 20,
556
617
  paddingHorizontal: 16, paddingVertical: 8,
618
+ zIndex: 20,
619
+ elevation: 20,
557
620
  },
558
621
  closeBtnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
559
622
  hint: {
560
623
  position: 'absolute', bottom: 40, alignSelf: 'center',
561
624
  color: '#97a3b5', fontSize: 13,
625
+ zIndex: 20,
562
626
  },
563
627
  });
@@ -1,6 +1,8 @@
1
- export declare const FACE_SQUEEZE_LOCAL_MODULE: number;
1
+ /** Web builds do not bundle the native face-squeeze GLB module. */
2
+ export declare const FACE_SQUEEZE_LOCAL_MODULE: null;
2
3
  export interface FaceSqueezeEditorProps {
3
4
  onClose: () => void;
5
+ avatarModule?: unknown;
4
6
  }
5
7
  export declare function FaceSqueezeEditor({ onClose }: FaceSqueezeEditorProps): import("react/jsx-runtime").JSX.Element;
6
8
  export default FaceSqueezeEditor;
@@ -3,52 +3,54 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FACE_SQUEEZE_LOCAL_MODULE = void 0;
4
4
  exports.FaceSqueezeEditor = FaceSqueezeEditor;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
- const react_native_1 = require("react-native");
7
- // eslint-disable-next-line @typescript-eslint/no-require-imports
8
- exports.FACE_SQUEEZE_LOCAL_MODULE = require('../assets/face-squeeze-local.glb');
6
+ /** Web builds do not bundle the native face-squeeze GLB module. */
7
+ exports.FACE_SQUEEZE_LOCAL_MODULE = null;
9
8
  function FaceSqueezeEditor({ onClose }) {
10
- return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.container, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.card, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, children: "Face squeeze editor is mobile-only." }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.subtitle, children: "This placeholder keeps web builds safe while preserving native functionality." }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.closeButton, onPress: onClose, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.closeButtonText, children: "Close" }) })] }) }));
9
+ return ((0, jsx_runtime_1.jsx)("div", { style: styles.container, children: (0, jsx_runtime_1.jsxs)("div", { style: styles.card, children: [(0, jsx_runtime_1.jsx)("h2", { style: styles.title, children: "Face squeeze editor is mobile-only." }), (0, jsx_runtime_1.jsx)("p", { style: styles.subtitle, children: "This placeholder keeps web builds safe while preserving native functionality." }), (0, jsx_runtime_1.jsx)("button", { type: "button", style: styles.closeButton, onClick: onClose, children: "Close" })] }) }));
11
10
  }
12
- const styles = react_native_1.StyleSheet.create({
11
+ const styles = {
13
12
  container: {
13
+ alignItems: 'center',
14
+ backgroundColor: '#0f1115',
15
+ boxSizing: 'border-box',
16
+ display: 'flex',
14
17
  flex: 1,
15
- alignItems: "center",
16
- justifyContent: "center",
17
- backgroundColor: "#0f1115",
18
+ height: '100%',
19
+ justifyContent: 'center',
18
20
  padding: 20,
21
+ width: '100%',
19
22
  },
20
23
  card: {
21
- width: "100%",
22
- maxWidth: 460,
24
+ backgroundColor: '#171b23',
25
+ border: '1px solid #2d3340',
23
26
  borderRadius: 12,
24
- borderWidth: 1,
25
- borderColor: "#2d3340",
26
- backgroundColor: "#171b23",
27
+ boxSizing: 'border-box',
28
+ maxWidth: 460,
27
29
  padding: 16,
28
- gap: 10,
30
+ width: '100%',
29
31
  },
30
32
  title: {
31
- color: "#f1f4f8",
33
+ color: '#f1f4f8',
32
34
  fontSize: 18,
33
- fontWeight: "700",
35
+ fontWeight: 700,
36
+ margin: 0,
34
37
  },
35
38
  subtitle: {
36
- color: "#98a1b3",
39
+ color: '#98a1b3',
37
40
  fontSize: 14,
38
- lineHeight: 20,
41
+ lineHeight: '20px',
42
+ margin: '10px 0 0',
39
43
  },
40
44
  closeButton: {
41
- marginTop: 6,
42
- alignSelf: "flex-start",
45
+ backgroundColor: '#2e8f5b',
46
+ border: 0,
43
47
  borderRadius: 8,
44
- backgroundColor: "#2e8f5b",
45
- paddingHorizontal: 12,
46
- paddingVertical: 8,
47
- },
48
- closeButtonText: {
49
- color: "#ffffff",
48
+ color: '#ffffff',
49
+ cursor: 'pointer',
50
50
  fontSize: 14,
51
- fontWeight: "600",
51
+ fontWeight: 600,
52
+ marginTop: 16,
53
+ padding: '8px 12px',
52
54
  },
53
- });
55
+ };
54
56
  exports.default = FaceSqueezeEditor;
@@ -3,13 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RigidAccessory = RigidAccessory;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  // @ts-nocheck
6
+ /* eslint-disable @typescript-eslint/no-explicit-any -- Accessory attachment walks arbitrary GLTF scene nodes from Three.js loader output. */
6
7
  const drei_1 = require("@react-three/drei");
7
8
  const react_1 = require("react");
8
9
  const three_1 = require("three");
9
10
  function RigidAccessory({ assetId, url, avatarScene, attachBone, offsetPosition, offsetRotation, scale = 1, isEditing = false, onPlacementChange, }) {
10
11
  const { scene } = (0, drei_1.useGLTF)(url);
11
- // Clone uniquely for this instance
12
+ // Clone uniquely for this instance — dispose geometry/materials on replacement or unmount
12
13
  const clone = (0, react_1.useMemo)(() => scene.clone(true), [scene]);
14
+ (0, react_1.useEffect)(() => {
15
+ return () => {
16
+ clone.traverse((obj) => {
17
+ if (obj instanceof three_1.Mesh) {
18
+ obj.geometry?.dispose();
19
+ if (Array.isArray(obj.material))
20
+ obj.material.forEach((m) => m.dispose());
21
+ else
22
+ obj.material?.dispose();
23
+ }
24
+ });
25
+ };
26
+ }, [clone]);
13
27
  const [basePos, setBasePos] = (0, react_1.useState)(null);
14
28
  (0, react_1.useEffect)(() => {
15
29
  if (!avatarScene)
@@ -44,7 +58,8 @@ function RigidAccessory({ assetId, url, avatarScene, attachBone, offsetPosition,
44
58
  clone.rotation.set(...offsetRotation);
45
59
  }
46
60
  if (scale !== undefined) {
47
- clone.scale.set(scale, scale, scale);
61
+ const s = Math.max(0.001, scale);
62
+ clone.scale.set(s, s, s);
48
63
  }
49
64
  }, [basePos, offsetPosition, offsetRotation, scale, clone]);
50
65
  const handleDragEnd = () => {
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SkinnedClothing = SkinnedClothing;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  // @ts-nocheck
39
+ /* eslint-disable @typescript-eslint/no-explicit-any -- SkeletonUtils.clone and GLTF skeleton rebinding expose untyped Three.js internals. */
39
40
  const drei_1 = require("@react-three/drei");
40
41
  const react_1 = require("react");
41
42
  const THREE = __importStar(require("three"));
@@ -0,0 +1,11 @@
1
+ import * as THREE from 'three';
2
+ /**
3
+ * Converts a screen-space drag delta into a world-space translation delta,
4
+ * keeping the accessory in a plane facing the camera (bone-locked plane).
5
+ */
6
+ export declare function screenDeltaToWorldDelta(camera: THREE.PerspectiveCamera, objectWorldPos: THREE.Vector3, dx: number, dy: number, viewportWidth: number, viewportHeight: number): THREE.Vector3;
7
+ /**
8
+ * Computes the rotation angle delta between two pointer-pair angles.
9
+ * Returns delta in radians, normalized to [-π, π].
10
+ */
11
+ export declare function twoPointerAngleDelta(startAngle: number, currentAngle: number): number;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.screenDeltaToWorldDelta = screenDeltaToWorldDelta;
37
+ exports.twoPointerAngleDelta = twoPointerAngleDelta;
38
+ const THREE = __importStar(require("three"));
39
+ /**
40
+ * Converts a screen-space drag delta into a world-space translation delta,
41
+ * keeping the accessory in a plane facing the camera (bone-locked plane).
42
+ */
43
+ function screenDeltaToWorldDelta(camera, objectWorldPos, dx, dy, viewportWidth, viewportHeight) {
44
+ const distance = objectWorldPos.distanceTo(camera.position);
45
+ const fovY = (camera.fov * Math.PI) / 180;
46
+ const worldHeight = 2 * distance * Math.tan(fovY / 2);
47
+ const worldWidth = worldHeight * (viewportWidth / viewportHeight);
48
+ const right = new THREE.Vector3();
49
+ const up = new THREE.Vector3();
50
+ camera.getWorldDirection(new THREE.Vector3()); // ensure matrix is fresh
51
+ right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
52
+ up.setFromMatrixColumn(camera.matrixWorld, 1).normalize();
53
+ return right
54
+ .multiplyScalar((dx / viewportWidth) * worldWidth)
55
+ .add(up.multiplyScalar((-dy / viewportHeight) * worldHeight));
56
+ }
57
+ /**
58
+ * Computes the rotation angle delta between two pointer-pair angles.
59
+ * Returns delta in radians, normalized to [-π, π].
60
+ */
61
+ function twoPointerAngleDelta(startAngle, currentAngle) {
62
+ let delta = currentAngle - startAngle;
63
+ while (delta > Math.PI)
64
+ delta -= 2 * Math.PI;
65
+ while (delta < -Math.PI)
66
+ delta += 2 * Math.PI;
67
+ return delta;
68
+ }