talking-head-studio 0.4.10 → 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.
- package/README.md +299 -337
- package/dist/TalkingHead.d.ts +44 -28
- package/dist/TalkingHead.js +21 -2
- package/dist/TalkingHead.web.d.ts +37 -4
- package/dist/TalkingHead.web.js +28 -8
- package/dist/TalkingHeadVisualization.d.ts +22 -0
- package/dist/TalkingHeadVisualization.js +30 -10
- package/dist/api/studioApi.d.ts +12 -1
- package/dist/api/studioApi.js +41 -28
- package/dist/appearance/apply.js +2 -3
- package/dist/appearance/matchers.js +1 -2
- package/dist/appearance/schema.js +1 -2
- package/dist/contract.d.ts +14 -0
- package/dist/contract.js +30 -0
- package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
- package/dist/core/avatar/avatarCapabilities.js +100 -0
- package/dist/core/avatar/backend.d.ts +130 -0
- package/dist/core/avatar/backend.js +4 -0
- package/dist/core/avatar/backends/gaussian.d.ts +49 -0
- package/dist/core/avatar/backends/gaussian.js +293 -0
- package/dist/core/avatar/backends/index.d.ts +3 -0
- package/dist/core/avatar/backends/index.js +7 -0
- package/dist/core/avatar/backends/morphTarget.d.ts +39 -0
- package/dist/core/avatar/backends/morphTarget.js +179 -0
- package/dist/core/avatar/faceControls.d.ts +40 -0
- package/dist/core/avatar/faceControls.js +138 -0
- package/dist/core/avatar/motion.d.ts +1713 -0
- package/dist/core/avatar/motion.js +550 -0
- package/dist/core/avatar/motionRuntime.d.ts +46 -0
- package/dist/core/avatar/motionRuntime.js +84 -0
- package/dist/core/avatar/schema.d.ts +78 -0
- package/dist/core/avatar/schema.js +134 -0
- package/dist/core/avatar/visemes.d.ts +47 -1
- package/dist/core/avatar/visemes.js +114 -1
- package/dist/editor/AvatarCanvas.js +93 -3
- package/dist/editor/AvatarEditor.native.js +19 -9
- package/dist/editor/AvatarModel.js +2 -2
- package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
- package/dist/editor/FaceSqueezeEditor.js +195 -121
- package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
- package/dist/editor/FaceSqueezeEditor.web.js +32 -30
- package/dist/editor/RigidAccessory.js +18 -4
- package/dist/editor/SkinnedClothing.js +19 -9
- package/dist/editor/boneLockedDrag.d.ts +11 -0
- package/dist/editor/boneLockedDrag.js +68 -0
- package/dist/editor/boneSnap.js +22 -12
- package/dist/editor/boneSnap.web.d.ts +27 -0
- package/dist/editor/boneSnap.web.js +99 -0
- package/dist/editor/index.web.d.ts +10 -0
- package/dist/editor/index.web.js +26 -0
- package/dist/editor/sounds/haha.wav +0 -0
- package/dist/editor/sounds/owie.wav +0 -0
- package/dist/editor/sounds/stop.wav +0 -0
- package/dist/editor/studioTheme.d.ts +14 -14
- package/dist/editor/studioTheme.js +19 -16
- package/dist/editor/types.d.ts +1 -0
- package/dist/html/accessories.d.ts +7 -0
- package/dist/html/accessories.js +149 -0
- package/dist/html/motion.d.ts +1 -0
- package/dist/html/motion.js +189 -0
- package/dist/html/visemes.d.ts +7 -0
- package/dist/html/visemes.js +348 -0
- package/dist/html.d.ts +1 -1
- package/dist/html.js +56 -734
- package/dist/index.d.ts +19 -1
- package/dist/index.js +44 -5
- package/dist/index.web.d.ts +18 -1
- package/dist/index.web.js +36 -3
- package/dist/platform/api/types.d.ts +10 -0
- package/dist/platform/api/types.js +2 -0
- package/dist/platform/marketplace/types.d.ts +32 -0
- package/dist/platform/marketplace/types.js +2 -0
- package/dist/platform/sdk/unity.d.ts +27 -0
- package/dist/platform/sdk/unity.js +2 -0
- package/dist/platform/sdk/unreal.d.ts +23 -0
- package/dist/platform/sdk/unreal.js +2 -0
- package/dist/platform/sdk/web.d.ts +16 -0
- package/dist/platform/sdk/web.js +2 -0
- package/dist/sketchfab/api.js +5 -5
- package/dist/sketchfab/glbInspect.d.ts +22 -0
- package/dist/sketchfab/glbInspect.js +58 -0
- package/dist/sketchfab/index.d.ts +3 -0
- package/dist/sketchfab/index.js +8 -1
- package/dist/sketchfab/inspectRemote.d.ts +13 -0
- package/dist/sketchfab/inspectRemote.js +77 -0
- package/dist/sketchfab/types.d.ts +10 -0
- package/dist/sketchfab/useSketchfabSearch.js +1 -2
- package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
- package/dist/studio/AccessoryBrowserScreen.js +626 -0
- package/dist/studio/AccessoryPanel.d.ts +10 -0
- package/dist/studio/AccessoryPanel.js +396 -0
- package/dist/studio/AppearancePanel.d.ts +9 -0
- package/dist/studio/AppearancePanel.js +77 -0
- package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
- package/dist/studio/AvatarCreatorScreen.js +806 -0
- package/dist/studio/AvatarEditorScreen.d.ts +14 -0
- package/dist/studio/AvatarEditorScreen.js +510 -0
- package/dist/studio/AvatarGrid.d.ts +23 -0
- package/dist/studio/AvatarGrid.js +257 -0
- package/dist/studio/ColorSwatch.d.ts +8 -0
- package/dist/studio/ColorSwatch.js +100 -0
- package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
- package/dist/studio/CreateVoiceProfileSheet.js +242 -0
- package/dist/studio/DetailsPanel.d.ts +15 -0
- package/dist/studio/DetailsPanel.js +239 -0
- package/dist/studio/FilamentEditor.d.ts +2 -0
- package/dist/studio/FilamentEditor.js +6 -0
- package/dist/studio/PrecisionPanel.d.ts +2 -0
- package/dist/studio/PrecisionPanel.js +7 -0
- package/dist/studio/PublicGalleryScreen.d.ts +5 -0
- package/dist/studio/PublicGalleryScreen.js +358 -0
- package/dist/studio/SketchfabModelCard.d.ts +20 -0
- package/dist/studio/SketchfabModelCard.js +104 -0
- package/dist/studio/StudioBrowseHeader.d.ts +9 -0
- package/dist/studio/StudioBrowseHeader.js +28 -0
- package/dist/studio/StudioEmptyState.d.ts +8 -0
- package/dist/studio/StudioEmptyState.js +29 -0
- package/dist/studio/StudioFloatingAction.d.ts +13 -0
- package/dist/studio/StudioFloatingAction.js +42 -0
- package/dist/studio/StudioSectionHeader.d.ts +7 -0
- package/dist/studio/StudioSectionHeader.js +27 -0
- package/dist/studio/StudioSurfaceCard.d.ts +8 -0
- package/dist/studio/StudioSurfaceCard.js +20 -0
- package/dist/studio/VoicePanel.d.ts +15 -0
- package/dist/studio/VoicePanel.js +305 -0
- package/dist/studio/constants.d.ts +3 -0
- package/dist/studio/constants.js +6 -0
- package/dist/studio/index.d.ts +29 -0
- package/dist/studio/index.js +54 -0
- package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
- package/dist/studio/useSketchfabCapabilities.js +82 -0
- package/dist/tts/useDirectVisemeStream.d.ts +2 -6
- package/dist/tts/useDirectVisemeStream.js +16 -12
- package/dist/tts/useMotionMarkers.d.ts +0 -1
- package/dist/tts/useMotionMarkers.js +1 -2
- package/dist/utils/avatarUtils.js +94 -8
- package/dist/utils/faceLandmarkerToShapeWeights.js +21 -14
- package/dist/voice/convertToWav.js +1 -2
- package/dist/voice/index.d.ts +3 -0
- package/dist/voice/index.js +6 -1
- package/dist/voice/useAudioPlayer.js +18 -6
- package/dist/voice/useAudioRecording.js +1 -2
- package/dist/voice/useFaceControls.d.ts +14 -0
- package/dist/voice/useFaceControls.js +81 -0
- package/dist/voice/useVoicePreview.d.ts +7 -0
- package/dist/voice/useVoicePreview.js +83 -0
- package/dist/wardrobe/index.d.ts +3 -0
- package/dist/wardrobe/index.js +8 -1
- package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
- package/dist/wardrobe/useAccessoryGestures.js +94 -0
- package/dist/wardrobe/useAvatarWardrobeHydration.js +9 -4
- package/dist/wardrobe/useStudioAvatar.d.ts +29 -0
- package/dist/wardrobe/useStudioAvatar.js +186 -0
- package/dist/wardrobe/wardrobeStore.d.ts +2 -0
- package/dist/wardrobe/wardrobeStore.js +12 -2
- package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
- package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
- package/dist/wgpu/WgpuAvatar.d.ts +26 -2
- package/dist/wgpu/WgpuAvatar.js +313 -46
- package/dist/wgpu/accessoryDefaults.d.ts +12 -0
- package/dist/wgpu/accessoryDefaults.js +19 -0
- package/dist/wgpu/blobShim.d.ts +2 -0
- package/dist/wgpu/blobShim.js +191 -0
- package/dist/wgpu/index.d.ts +1 -0
- package/dist/wgpu/index.js +4 -1
- package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
- package/dist/wgpu/loadGLTFFromUri.js +75 -0
- package/dist/wgpu/morphTables.js +21 -10
- package/dist/wgpu/motionState.d.ts +20 -0
- package/dist/wgpu/motionState.js +31 -0
- package/dist/wgpu/patchThreeForRN.d.ts +28 -0
- package/dist/wgpu/patchThreeForRN.js +292 -0
- package/dist/wgpu/scenePlacement.d.ts +5 -0
- package/dist/wgpu/scenePlacement.js +50 -0
- package/dist/wgpu/useAuthedModelUri.js +22 -11
- package/dist/wgpu/useNativeGLTF.d.ts +7 -0
- package/dist/wgpu/useNativeGLTF.js +36 -0
- package/package.json +102 -32
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/voice/useFaceControls.ts
|
|
3
|
+
// Derive a canonical FaceControl (pose + expression) from a TalkingHead viseme schedule.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.useFaceControlsFromVisemes = useFaceControlsFromVisemes;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const morphTables_1 = require("../wgpu/morphTables");
|
|
8
|
+
const faceControls_1 = require("../core/avatar/faceControls");
|
|
9
|
+
function sampleExpressionAtTime(schedule, nowMs) {
|
|
10
|
+
const neutral = (0, faceControls_1.createNeutralExpression)();
|
|
11
|
+
if (!schedule.cues || schedule.cues.length === 0)
|
|
12
|
+
return neutral;
|
|
13
|
+
const startedAt = schedule.startedAtMs ?? nowMs;
|
|
14
|
+
const t = Math.max(0, nowMs - startedAt);
|
|
15
|
+
const cues = schedule.cues.filter((cue) => t >= cue.startMs && t <= cue.endMs);
|
|
16
|
+
if (cues.length === 0)
|
|
17
|
+
return neutral;
|
|
18
|
+
let expr = neutral;
|
|
19
|
+
for (const cue of cues) {
|
|
20
|
+
const rhubarbKey = cue.viseme;
|
|
21
|
+
const oculusViseme = (morphTables_1.RHUBARB_TO_VISEME[rhubarbKey] ?? 'sil');
|
|
22
|
+
const w = typeof cue.weight === 'number' ? cue.weight : 1;
|
|
23
|
+
expr = (0, faceControls_1.applyVisemeToExpression)(expr, oculusViseme, w);
|
|
24
|
+
}
|
|
25
|
+
return expr;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* useFaceControlsFromVisemes
|
|
29
|
+
*
|
|
30
|
+
* Given a TalkingHead viseme schedule (Rhubarb shape cues + startedAtMs),
|
|
31
|
+
* produce a time-varying FaceControl object (neutral head pose + ExpressionState).
|
|
32
|
+
*
|
|
33
|
+
* This hook is intentionally simple: it drives expression purely from the
|
|
34
|
+
* viseme cues and keeps head pose neutral (0 yaw/pitch/roll). Downstream
|
|
35
|
+
* backends can layer motion / camera moves on top.
|
|
36
|
+
*/
|
|
37
|
+
function useFaceControlsFromVisemes(schedule) {
|
|
38
|
+
const [expr, setExpr] = (0, react_1.useState)(() => (0, faceControls_1.createNeutralExpression)());
|
|
39
|
+
const rafRef = (0, react_1.useRef)(null);
|
|
40
|
+
const scheduleRef = (0, react_1.useRef)(schedule);
|
|
41
|
+
(0, react_1.useEffect)(() => {
|
|
42
|
+
scheduleRef.current = schedule;
|
|
43
|
+
}, [schedule]);
|
|
44
|
+
(0, react_1.useEffect)(() => {
|
|
45
|
+
if (rafRef.current != null) {
|
|
46
|
+
cancelAnimationFrame(rafRef.current);
|
|
47
|
+
rafRef.current = null;
|
|
48
|
+
}
|
|
49
|
+
if (!schedule || !schedule.cues || schedule.cues.length === 0) {
|
|
50
|
+
setExpr((0, faceControls_1.createNeutralExpression)());
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const lastEnd = Math.max(...schedule.cues.map((c) => c.endMs ?? 0));
|
|
54
|
+
const loop = () => {
|
|
55
|
+
const s = scheduleRef.current;
|
|
56
|
+
if (!s || !s.cues || s.cues.length === 0) {
|
|
57
|
+
setExpr((0, faceControls_1.createNeutralExpression)());
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const nextExpr = sampleExpressionAtTime(s, now);
|
|
62
|
+
setExpr(nextExpr);
|
|
63
|
+
const startedAt = s.startedAtMs ?? now;
|
|
64
|
+
const t = Math.max(0, now - startedAt);
|
|
65
|
+
if (t <= lastEnd + 200) {
|
|
66
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
rafRef.current = requestAnimationFrame(loop);
|
|
70
|
+
return () => {
|
|
71
|
+
if (rafRef.current != null) {
|
|
72
|
+
cancelAnimationFrame(rafRef.current);
|
|
73
|
+
rafRef.current = null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}, [schedule]);
|
|
77
|
+
return {
|
|
78
|
+
pose: { yaw: 0, pitch: 0, roll: 0 },
|
|
79
|
+
expr,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function configureVoicePreviewBaseUrl(url: string): void;
|
|
2
|
+
export declare function useVoicePreview(): {
|
|
3
|
+
activeVoiceId: string | null;
|
|
4
|
+
loadingVoiceId: string | null;
|
|
5
|
+
previewVoice: (voiceId: string) => Promise<void>;
|
|
6
|
+
stopPreview: () => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureVoicePreviewBaseUrl = configureVoicePreviewBaseUrl;
|
|
4
|
+
exports.useVoicePreview = useVoicePreview;
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const expo_audio_1 = require("expo-audio");
|
|
7
|
+
const studioApi_1 = require("../api/studioApi");
|
|
8
|
+
let _baseUrl = process.env.EXPO_PUBLIC_BACKEND_URL ?? '';
|
|
9
|
+
function configureVoicePreviewBaseUrl(url) {
|
|
10
|
+
_baseUrl = url;
|
|
11
|
+
}
|
|
12
|
+
async function fetchFirstSampleUrl(voiceId) {
|
|
13
|
+
const samples = await (0, studioApi_1.getVoiceProfileSamples)(voiceId);
|
|
14
|
+
if (!samples || samples.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return `${_baseUrl}/samples/${samples[0].id}`;
|
|
17
|
+
}
|
|
18
|
+
function useVoicePreview() {
|
|
19
|
+
const [emptyPlayer] = (0, react_1.useState)(() => (0, expo_audio_1.createAudioPlayer)(null));
|
|
20
|
+
const playerRef = (0, react_1.useRef)(null);
|
|
21
|
+
const [player, setPlayer] = (0, react_1.useState)(null);
|
|
22
|
+
const [activeVoiceId, setActiveVoiceId] = (0, react_1.useState)(null);
|
|
23
|
+
const [loadingVoiceId, setLoadingVoiceId] = (0, react_1.useState)(null);
|
|
24
|
+
const status = (0, expo_audio_1.useAudioPlayerStatus)(player ?? emptyPlayer);
|
|
25
|
+
(0, react_1.useEffect)(() => {
|
|
26
|
+
if (status?.didJustFinish) {
|
|
27
|
+
setActiveVoiceId(null);
|
|
28
|
+
}
|
|
29
|
+
}, [status?.didJustFinish]);
|
|
30
|
+
const stopPreview = (0, react_1.useCallback)(() => {
|
|
31
|
+
if (playerRef.current) {
|
|
32
|
+
playerRef.current.pause();
|
|
33
|
+
playerRef.current.remove();
|
|
34
|
+
playerRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
setPlayer(null);
|
|
37
|
+
setActiveVoiceId(null);
|
|
38
|
+
setLoadingVoiceId(null);
|
|
39
|
+
}, []);
|
|
40
|
+
const previewVoice = (0, react_1.useCallback)(async (voiceId) => {
|
|
41
|
+
if (!voiceId || loadingVoiceId === voiceId)
|
|
42
|
+
return;
|
|
43
|
+
if (activeVoiceId === voiceId) {
|
|
44
|
+
stopPreview();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setLoadingVoiceId(voiceId);
|
|
48
|
+
stopPreview();
|
|
49
|
+
try {
|
|
50
|
+
const sampleUrl = await fetchFirstSampleUrl(voiceId);
|
|
51
|
+
if (!sampleUrl) {
|
|
52
|
+
setLoadingVoiceId(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await (0, expo_audio_1.setAudioModeAsync)({
|
|
56
|
+
playsInSilentMode: true,
|
|
57
|
+
allowsRecording: false,
|
|
58
|
+
shouldPlayInBackground: false,
|
|
59
|
+
interruptionMode: 'mixWithOthers',
|
|
60
|
+
});
|
|
61
|
+
const newPlayer = (0, expo_audio_1.createAudioPlayer)(sampleUrl);
|
|
62
|
+
playerRef.current = newPlayer;
|
|
63
|
+
setPlayer(newPlayer);
|
|
64
|
+
newPlayer.play();
|
|
65
|
+
setLoadingVoiceId(null);
|
|
66
|
+
setActiveVoiceId(voiceId);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error('[useVoicePreview] Failed to play sample:', err);
|
|
70
|
+
setLoadingVoiceId(null);
|
|
71
|
+
setActiveVoiceId(null);
|
|
72
|
+
}
|
|
73
|
+
}, [activeVoiceId, loadingVoiceId, stopPreview]);
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
return () => {
|
|
76
|
+
playerRef.current?.pause();
|
|
77
|
+
playerRef.current?.remove();
|
|
78
|
+
playerRef.current = null;
|
|
79
|
+
emptyPlayer.remove();
|
|
80
|
+
};
|
|
81
|
+
}, [emptyPlayer]);
|
|
82
|
+
return { activeVoiceId, loadingVoiceId, previewVoice, stopPreview };
|
|
83
|
+
}
|
package/dist/wardrobe/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export * from './wardrobeStore';
|
|
2
2
|
export { useAvatarWardrobeHydration } from './useAvatarWardrobeHydration';
|
|
3
|
+
export { useStudioAvatar } from './useStudioAvatar';
|
|
4
|
+
export type { UseStudioAvatarOptions, UseStudioAvatarResult } from './useStudioAvatar';
|
|
5
|
+
export { useAccessoryGestures, applyDragTranslate, applyPinchScale, applyRotation, } from './useAccessoryGestures';
|
package/dist/wardrobe/index.js
CHANGED
|
@@ -14,7 +14,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.useAvatarWardrobeHydration = void 0;
|
|
17
|
+
exports.applyRotation = exports.applyPinchScale = exports.applyDragTranslate = exports.useAccessoryGestures = exports.useStudioAvatar = exports.useAvatarWardrobeHydration = void 0;
|
|
18
18
|
__exportStar(require("./wardrobeStore"), exports);
|
|
19
19
|
var useAvatarWardrobeHydration_1 = require("./useAvatarWardrobeHydration");
|
|
20
20
|
Object.defineProperty(exports, "useAvatarWardrobeHydration", { enumerable: true, get: function () { return useAvatarWardrobeHydration_1.useAvatarWardrobeHydration; } });
|
|
21
|
+
var useStudioAvatar_1 = require("./useStudioAvatar");
|
|
22
|
+
Object.defineProperty(exports, "useStudioAvatar", { enumerable: true, get: function () { return useStudioAvatar_1.useStudioAvatar; } });
|
|
23
|
+
var useAccessoryGestures_1 = require("./useAccessoryGestures");
|
|
24
|
+
Object.defineProperty(exports, "useAccessoryGestures", { enumerable: true, get: function () { return useAccessoryGestures_1.useAccessoryGestures; } });
|
|
25
|
+
Object.defineProperty(exports, "applyDragTranslate", { enumerable: true, get: function () { return useAccessoryGestures_1.applyDragTranslate; } });
|
|
26
|
+
Object.defineProperty(exports, "applyPinchScale", { enumerable: true, get: function () { return useAccessoryGestures_1.applyPinchScale; } });
|
|
27
|
+
Object.defineProperty(exports, "applyRotation", { enumerable: true, get: function () { return useAccessoryGestures_1.applyRotation; } });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AssetPlacement } from './wardrobeStore';
|
|
2
|
+
/**
|
|
3
|
+
* Translate a position by screen-space drag deltas.
|
|
4
|
+
* Z is intentionally locked — drag stays in the XY plane to avoid depth
|
|
5
|
+
* artifacts from single-finger gestures.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyDragTranslate(startPos: [number, number, number], translationX: number, translationY: number, sensitivity?: number): [number, number, number];
|
|
8
|
+
/**
|
|
9
|
+
* Scale a [x, y, z] triple by a pinch factor, clamped to [0.1, 10].
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyPinchScale(startScale: [number, number, number], scaleFactor: number): [number, number, number];
|
|
12
|
+
/**
|
|
13
|
+
* Accumulate Euler rotation from gesture deltas (radians).
|
|
14
|
+
* deltaX maps to Y-axis (yaw), deltaY maps to X-axis (pitch).
|
|
15
|
+
*/
|
|
16
|
+
export declare function applyRotation(startRot: [number, number, number], deltaX: number, deltaY: number): [number, number, number];
|
|
17
|
+
export declare function useAccessoryGestures(): {
|
|
18
|
+
commitTransform: (id: string, placement: AssetPlacement) => void;
|
|
19
|
+
selectAccessory: (id: string) => Promise<void>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
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.applyDragTranslate = applyDragTranslate;
|
|
37
|
+
exports.applyPinchScale = applyPinchScale;
|
|
38
|
+
exports.applyRotation = applyRotation;
|
|
39
|
+
exports.useAccessoryGestures = useAccessoryGestures;
|
|
40
|
+
const Haptics = __importStar(require("expo-haptics"));
|
|
41
|
+
const wardrobeStore_1 = require("./wardrobeStore");
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Pure math helpers — 'worklet' annotated so they can run on the UI thread
|
|
44
|
+
// via react-native-reanimated gesture handlers.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Translate a position by screen-space drag deltas.
|
|
48
|
+
* Z is intentionally locked — drag stays in the XY plane to avoid depth
|
|
49
|
+
* artifacts from single-finger gestures.
|
|
50
|
+
*/
|
|
51
|
+
function applyDragTranslate(startPos, translationX, translationY, sensitivity = 0.01) {
|
|
52
|
+
return [
|
|
53
|
+
startPos[0] + translationX * sensitivity,
|
|
54
|
+
startPos[1] - translationY * sensitivity,
|
|
55
|
+
startPos[2],
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Scale a [x, y, z] triple by a pinch factor, clamped to [0.1, 10].
|
|
60
|
+
*/
|
|
61
|
+
function applyPinchScale(startScale, scaleFactor) {
|
|
62
|
+
const clamp = (v) => Math.max(0.1, Math.min(v, 10));
|
|
63
|
+
return [
|
|
64
|
+
clamp(startScale[0] * scaleFactor),
|
|
65
|
+
clamp(startScale[1] * scaleFactor),
|
|
66
|
+
clamp(startScale[2] * scaleFactor),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Accumulate Euler rotation from gesture deltas (radians).
|
|
71
|
+
* deltaX maps to Y-axis (yaw), deltaY maps to X-axis (pitch).
|
|
72
|
+
*/
|
|
73
|
+
function applyRotation(startRot, deltaX, deltaY) {
|
|
74
|
+
return [
|
|
75
|
+
startRot[0] + deltaY,
|
|
76
|
+
startRot[1] + deltaX,
|
|
77
|
+
startRot[2],
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Hook
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function useAccessoryGestures() {
|
|
84
|
+
const setActiveAsset = (0, wardrobeStore_1.useWardrobeStore)((state) => state.setActiveAsset);
|
|
85
|
+
const setPlacement = (0, wardrobeStore_1.useWardrobeStore)((state) => state.setPlacement);
|
|
86
|
+
const commitTransform = (id, placement) => {
|
|
87
|
+
setPlacement(id, placement);
|
|
88
|
+
};
|
|
89
|
+
const selectAccessory = async (id) => {
|
|
90
|
+
setActiveAsset(id);
|
|
91
|
+
await Haptics.selectionAsync();
|
|
92
|
+
};
|
|
93
|
+
return { commitTransform, selectAccessory };
|
|
94
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useAvatarWardrobeHydration =
|
|
3
|
+
exports.useAvatarWardrobeHydration = useAvatarWardrobeHydration;
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const studioApi_1 = require("../api/studioApi");
|
|
6
6
|
const wardrobeStore_1 = require("./wardrobeStore");
|
|
7
7
|
function useAvatarWardrobeHydration({ avatarId, accessories, }) {
|
|
8
8
|
const hydrateFromApi = (0, wardrobeStore_1.useWardrobeStore)((s) => s.hydrateFromApi);
|
|
9
|
+
const accessoriesRef = (0, react_1.useRef)(accessories ?? []);
|
|
9
10
|
const accessorySignature = (0, react_1.useMemo)(() => JSON.stringify(accessories ?? []), [accessories]);
|
|
10
11
|
(0, react_1.useEffect)(() => {
|
|
11
|
-
|
|
12
|
+
accessoriesRef.current = accessories ?? [];
|
|
13
|
+
}, [accessories]);
|
|
14
|
+
(0, react_1.useEffect)(() => {
|
|
15
|
+
const nextAccessories = accessoriesRef.current;
|
|
12
16
|
if (!avatarId || nextAccessories.length === 0) {
|
|
13
17
|
hydrateFromApi([], {});
|
|
14
18
|
return;
|
|
@@ -25,11 +29,12 @@ function useAvatarWardrobeHydration({ avatarId, accessories, }) {
|
|
|
25
29
|
hydrateFromApi(nextAccessories, lookup);
|
|
26
30
|
})
|
|
27
31
|
.catch((err) => {
|
|
32
|
+
if (cancelled)
|
|
33
|
+
return;
|
|
28
34
|
console.error('[useAvatarWardrobeHydration] Failed to hydrate wardrobe:', err);
|
|
29
35
|
});
|
|
30
36
|
return () => {
|
|
31
37
|
cancelled = true;
|
|
32
38
|
};
|
|
33
|
-
}, [accessorySignature, avatarId, hydrateFromApi
|
|
39
|
+
}, [accessorySignature, avatarId, hydrateFromApi]);
|
|
34
40
|
}
|
|
35
|
-
exports.useAvatarWardrobeHydration = useAvatarWardrobeHydration;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Avatar } from '../api/types';
|
|
2
|
+
export interface UseStudioAvatarOptions {
|
|
3
|
+
avatarId: string;
|
|
4
|
+
}
|
|
5
|
+
export interface UseStudioAvatarResult {
|
|
6
|
+
avatar: Avatar | null;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
name: string;
|
|
10
|
+
setName: (name: string) => void;
|
|
11
|
+
description: string;
|
|
12
|
+
setDescription: (desc: string) => void;
|
|
13
|
+
isPublic: boolean;
|
|
14
|
+
setIsPublic: (v: boolean) => void;
|
|
15
|
+
hairColor: string;
|
|
16
|
+
setHairColor: (color: string) => void;
|
|
17
|
+
skinColor: string;
|
|
18
|
+
setSkinColor: (color: string) => void;
|
|
19
|
+
eyeColor: string;
|
|
20
|
+
setEyeColor: (color: string) => void;
|
|
21
|
+
voiceProfileId: string | null;
|
|
22
|
+
setVoiceProfileId: (id: string | null) => void;
|
|
23
|
+
isDirty: boolean;
|
|
24
|
+
saving: boolean;
|
|
25
|
+
save: () => Promise<void>;
|
|
26
|
+
revert: () => void;
|
|
27
|
+
reload: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function useStudioAvatar({ avatarId }: UseStudioAvatarOptions): UseStudioAvatarResult;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useStudioAvatar = useStudioAvatar;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const studioApi_1 = require("../api/studioApi");
|
|
6
|
+
const wardrobeStore_1 = require("./wardrobeStore");
|
|
7
|
+
const DEFAULT_HAIR_COLOR = '#4a3728';
|
|
8
|
+
const DEFAULT_SKIN_COLOR = '#f5d0b0';
|
|
9
|
+
const DEFAULT_EYE_COLOR = '#5b7553';
|
|
10
|
+
function extractHairColor(a) {
|
|
11
|
+
return a?.appearance?.hairColor ?? DEFAULT_HAIR_COLOR;
|
|
12
|
+
}
|
|
13
|
+
function extractSkinColor(a) {
|
|
14
|
+
return a?.appearance?.skinColor ?? DEFAULT_SKIN_COLOR;
|
|
15
|
+
}
|
|
16
|
+
function extractEyeColor(a) {
|
|
17
|
+
return a?.appearance?.eyeColor ?? DEFAULT_EYE_COLOR;
|
|
18
|
+
}
|
|
19
|
+
function useStudioAvatar({ avatarId }) {
|
|
20
|
+
const [avatar, setAvatar] = (0, react_1.useState)(null);
|
|
21
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
22
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
23
|
+
const [saving, setSaving] = (0, react_1.useState)(false);
|
|
24
|
+
const [name, setName] = (0, react_1.useState)('');
|
|
25
|
+
const [description, setDescription] = (0, react_1.useState)('');
|
|
26
|
+
const [isPublic, setIsPublic] = (0, react_1.useState)(false);
|
|
27
|
+
const [hairColor, setHairColor] = (0, react_1.useState)(DEFAULT_HAIR_COLOR);
|
|
28
|
+
const [skinColor, setSkinColor] = (0, react_1.useState)(DEFAULT_SKIN_COLOR);
|
|
29
|
+
const [eyeColor, setEyeColor] = (0, react_1.useState)(DEFAULT_EYE_COLOR);
|
|
30
|
+
const [voiceProfileId, setVoiceProfileId] = (0, react_1.useState)(null);
|
|
31
|
+
const populateFromAvatar = (0, react_1.useCallback)((a) => {
|
|
32
|
+
setName(a.name);
|
|
33
|
+
setDescription(a.description ?? '');
|
|
34
|
+
setIsPublic(a.is_public);
|
|
35
|
+
setHairColor(extractHairColor(a));
|
|
36
|
+
setSkinColor(extractSkinColor(a));
|
|
37
|
+
setEyeColor(extractEyeColor(a));
|
|
38
|
+
setVoiceProfileId(a.default_voice_profile_id);
|
|
39
|
+
}, []);
|
|
40
|
+
const fetchAvatar = (0, react_1.useCallback)(async () => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
try {
|
|
44
|
+
const a = await (0, studioApi_1.getAvatar)(avatarId);
|
|
45
|
+
setAvatar(a);
|
|
46
|
+
populateFromAvatar(a);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : 'Failed to load avatar');
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setLoading(false);
|
|
53
|
+
}
|
|
54
|
+
}, [avatarId, populateFromAvatar]);
|
|
55
|
+
(0, react_1.useEffect)(() => {
|
|
56
|
+
void fetchAvatar();
|
|
57
|
+
}, [fetchAvatar]);
|
|
58
|
+
const equipped = (0, wardrobeStore_1.useWardrobeStore)((s) => s.equipped);
|
|
59
|
+
const placements = (0, wardrobeStore_1.useWardrobeStore)((s) => s.placements);
|
|
60
|
+
const isDirty = (0, react_1.useMemo)(() => {
|
|
61
|
+
if (!avatar)
|
|
62
|
+
return false;
|
|
63
|
+
if (name !== avatar.name)
|
|
64
|
+
return true;
|
|
65
|
+
if (description !== (avatar.description ?? ''))
|
|
66
|
+
return true;
|
|
67
|
+
if (isPublic !== avatar.is_public)
|
|
68
|
+
return true;
|
|
69
|
+
if (hairColor !== extractHairColor(avatar))
|
|
70
|
+
return true;
|
|
71
|
+
if (skinColor !== extractSkinColor(avatar))
|
|
72
|
+
return true;
|
|
73
|
+
if (eyeColor !== extractEyeColor(avatar))
|
|
74
|
+
return true;
|
|
75
|
+
if (voiceProfileId !== avatar.default_voice_profile_id)
|
|
76
|
+
return true;
|
|
77
|
+
const serialized = Object.values(equipped).map((asset) => {
|
|
78
|
+
const placement = placements[asset.id];
|
|
79
|
+
return {
|
|
80
|
+
asset_id: asset.id,
|
|
81
|
+
bone: placement?.bone ?? 'Head',
|
|
82
|
+
position: placement?.position ?? [0, 0, 0],
|
|
83
|
+
rotation: placement?.rotation ?? [0, 0, 0],
|
|
84
|
+
scale: placement?.scale ?? 1,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const serverAccs = avatar.appearance?.equippedAccessories ?? [];
|
|
88
|
+
if (serialized.length !== serverAccs.length)
|
|
89
|
+
return true;
|
|
90
|
+
for (const acc of serialized) {
|
|
91
|
+
const serverMatch = serverAccs.find((sa) => sa.asset_id === acc.asset_id);
|
|
92
|
+
if (!serverMatch)
|
|
93
|
+
return true;
|
|
94
|
+
if (serverMatch.bone !== acc.bone)
|
|
95
|
+
return true;
|
|
96
|
+
if (serverMatch.scale !== acc.scale)
|
|
97
|
+
return true;
|
|
98
|
+
if (serverMatch.position?.join(',') !== acc.position?.join(','))
|
|
99
|
+
return true;
|
|
100
|
+
if (serverMatch.rotation?.join(',') !== acc.rotation?.join(','))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}, [avatar, name, description, isPublic, hairColor, skinColor, eyeColor, voiceProfileId, equipped, placements]);
|
|
105
|
+
const save = (0, react_1.useCallback)(async () => {
|
|
106
|
+
if (!avatar)
|
|
107
|
+
return;
|
|
108
|
+
setSaving(true);
|
|
109
|
+
setError(null);
|
|
110
|
+
try {
|
|
111
|
+
const update = {};
|
|
112
|
+
if (name !== avatar.name)
|
|
113
|
+
update.name = name;
|
|
114
|
+
if (description !== (avatar.description ?? ''))
|
|
115
|
+
update.description = description;
|
|
116
|
+
if (isPublic !== avatar.is_public)
|
|
117
|
+
update.is_public = isPublic;
|
|
118
|
+
const equippedAccessories = wardrobeStore_1.useWardrobeStore.getState().serializeForApi();
|
|
119
|
+
const serverAccs = avatar.appearance?.equippedAccessories ?? [];
|
|
120
|
+
const accessoriesChanged = equippedAccessories.length !== serverAccs.length ||
|
|
121
|
+
equippedAccessories.some((acc) => {
|
|
122
|
+
const serverMatch = serverAccs.find((sa) => sa.asset_id === acc.asset_id);
|
|
123
|
+
if (!serverMatch)
|
|
124
|
+
return true;
|
|
125
|
+
if (serverMatch.bone !== acc.bone)
|
|
126
|
+
return true;
|
|
127
|
+
if (serverMatch.scale !== acc.scale)
|
|
128
|
+
return true;
|
|
129
|
+
if (serverMatch.position?.join(',') !== acc.position?.join(','))
|
|
130
|
+
return true;
|
|
131
|
+
if (serverMatch.rotation?.join(',') !== acc.rotation?.join(','))
|
|
132
|
+
return true;
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
if (hairColor !== extractHairColor(avatar) ||
|
|
136
|
+
skinColor !== extractSkinColor(avatar) ||
|
|
137
|
+
eyeColor !== extractEyeColor(avatar) ||
|
|
138
|
+
accessoriesChanged) {
|
|
139
|
+
const appearance = {
|
|
140
|
+
version: 1,
|
|
141
|
+
...(hairColor ? { hairColor } : {}),
|
|
142
|
+
...(skinColor ? { skinColor } : {}),
|
|
143
|
+
...(eyeColor ? { eyeColor } : {}),
|
|
144
|
+
equippedAccessories,
|
|
145
|
+
};
|
|
146
|
+
update.appearance = appearance;
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(update).length > 0) {
|
|
149
|
+
await (0, studioApi_1.updateAvatar)(avatarId, update);
|
|
150
|
+
}
|
|
151
|
+
const serverVoice = avatar.default_voice_profile_id;
|
|
152
|
+
if (voiceProfileId !== serverVoice) {
|
|
153
|
+
if (voiceProfileId) {
|
|
154
|
+
await (0, studioApi_1.setDefaultVoice)(avatarId, voiceProfileId);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await (0, studioApi_1.removeDefaultVoice)(avatarId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await fetchAvatar();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
setError(err instanceof Error ? err.message : 'Failed to save avatar');
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
setSaving(false);
|
|
167
|
+
}
|
|
168
|
+
}, [avatar, avatarId, name, description, isPublic, hairColor, skinColor, eyeColor, voiceProfileId, fetchAvatar]);
|
|
169
|
+
const revert = (0, react_1.useCallback)(() => {
|
|
170
|
+
if (avatar)
|
|
171
|
+
populateFromAvatar(avatar);
|
|
172
|
+
}, [avatar, populateFromAvatar]);
|
|
173
|
+
return {
|
|
174
|
+
avatar, loading, error,
|
|
175
|
+
name, setName,
|
|
176
|
+
description, setDescription,
|
|
177
|
+
isPublic, setIsPublic,
|
|
178
|
+
hairColor, setHairColor,
|
|
179
|
+
skinColor, setSkinColor,
|
|
180
|
+
eyeColor, setEyeColor,
|
|
181
|
+
voiceProfileId, setVoiceProfileId,
|
|
182
|
+
isDirty, saving,
|
|
183
|
+
save, revert,
|
|
184
|
+
reload: fetchAvatar,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -17,6 +17,8 @@ export interface WardrobeState {
|
|
|
17
17
|
gizmoMode: GizmoMode;
|
|
18
18
|
/** ID of the avatar currently synced to avoid redundant fetches */
|
|
19
19
|
syncedAvatarId: string | null;
|
|
20
|
+
/** ID of the avatar currently being fetched, used to ignore stale sync results. */
|
|
21
|
+
syncingAvatarId: string | null;
|
|
20
22
|
equip: (slot: string, asset: WearableAsset) => void;
|
|
21
23
|
unequip: (slot: string) => void;
|
|
22
24
|
setPlacement: (assetId: string, placement: AssetPlacement) => void;
|
|
@@ -22,12 +22,12 @@ exports.useWardrobeStore = (0, zustand_1.create)()((set, get) => ({
|
|
|
22
22
|
activeAssetId: null,
|
|
23
23
|
gizmoMode: 'translate',
|
|
24
24
|
syncedAvatarId: null,
|
|
25
|
+
syncingAvatarId: null,
|
|
25
26
|
equip: (slot, asset) => set((state) => ({
|
|
26
27
|
equipped: { ...state.equipped, [slot]: asset },
|
|
27
28
|
})),
|
|
28
29
|
unequip: (slot) => set((state) => {
|
|
29
30
|
const removed = state.equipped[slot];
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
31
31
|
const { [slot]: _removed, ...restEquipped } = state.equipped;
|
|
32
32
|
// Clean up placement data for the removed asset
|
|
33
33
|
const placements = { ...state.placements };
|
|
@@ -70,18 +70,26 @@ exports.useWardrobeStore = (0, zustand_1.create)()((set, get) => ({
|
|
|
70
70
|
return;
|
|
71
71
|
if (get().syncedAvatarId === avatarId)
|
|
72
72
|
return;
|
|
73
|
+
if (get().syncingAvatarId === avatarId)
|
|
74
|
+
return;
|
|
73
75
|
get().reset();
|
|
76
|
+
set({ syncingAvatarId: avatarId });
|
|
74
77
|
try {
|
|
75
78
|
const [avatar, allAssets] = await Promise.all([(0, studioApi_1.getAvatar)(avatarId), (0, studioApi_1.listAssets)()]);
|
|
79
|
+
if (get().syncingAvatarId !== avatarId)
|
|
80
|
+
return;
|
|
76
81
|
const lookup = {};
|
|
77
82
|
for (const a of allAssets)
|
|
78
83
|
lookup[a.id] = a;
|
|
79
84
|
const accessories = avatar.appearance?.equippedAccessories ?? [];
|
|
80
85
|
get().hydrateFromApi(accessories, lookup);
|
|
81
|
-
set({ syncedAvatarId: avatarId });
|
|
86
|
+
set({ syncedAvatarId: avatarId, syncingAvatarId: null });
|
|
82
87
|
}
|
|
83
88
|
catch (e) {
|
|
84
89
|
console.warn('[WardrobeStore] Failed to load avatar accessories:', e);
|
|
90
|
+
if (get().syncingAvatarId === avatarId) {
|
|
91
|
+
set({ syncingAvatarId: null });
|
|
92
|
+
}
|
|
85
93
|
}
|
|
86
94
|
},
|
|
87
95
|
serializeForApi: () => {
|
|
@@ -102,5 +110,7 @@ exports.useWardrobeStore = (0, zustand_1.create)()((set, get) => ({
|
|
|
102
110
|
placements: {},
|
|
103
111
|
activeAssetId: null,
|
|
104
112
|
gizmoMode: 'translate',
|
|
113
|
+
syncedAvatarId: null,
|
|
114
|
+
syncingAvatarId: null,
|
|
105
115
|
}),
|
|
106
116
|
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import type { RootState } from '@react-three/fiber';
|
|
5
|
+
type CameraProps = THREE.Camera | Record<string, unknown>;
|
|
6
|
+
interface R3FWebGpuCanvasProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
camera?: CameraProps;
|
|
10
|
+
clearColor?: string;
|
|
11
|
+
onCreated?: (state: RootState) => void;
|
|
12
|
+
onError?: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function R3FWebGpuCanvas({ children, style, camera, clearColor, onCreated, onError, }: R3FWebGpuCanvasProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|