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.
Files changed (178) hide show
  1. package/README.md +299 -337
  2. package/dist/TalkingHead.d.ts +44 -28
  3. package/dist/TalkingHead.js +21 -2
  4. package/dist/TalkingHead.web.d.ts +37 -4
  5. package/dist/TalkingHead.web.js +28 -8
  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 +41 -28
  10. package/dist/appearance/apply.js +2 -3
  11. package/dist/appearance/matchers.js +1 -2
  12. package/dist/appearance/schema.js +1 -2
  13. package/dist/contract.d.ts +14 -0
  14. package/dist/contract.js +30 -0
  15. package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
  16. package/dist/core/avatar/avatarCapabilities.js +100 -0
  17. package/dist/core/avatar/backend.d.ts +130 -0
  18. package/dist/core/avatar/backend.js +4 -0
  19. package/dist/core/avatar/backends/gaussian.d.ts +49 -0
  20. package/dist/core/avatar/backends/gaussian.js +293 -0
  21. package/dist/core/avatar/backends/index.d.ts +3 -0
  22. package/dist/core/avatar/backends/index.js +7 -0
  23. package/dist/core/avatar/backends/morphTarget.d.ts +39 -0
  24. package/dist/core/avatar/backends/morphTarget.js +179 -0
  25. package/dist/core/avatar/faceControls.d.ts +40 -0
  26. package/dist/core/avatar/faceControls.js +138 -0
  27. package/dist/core/avatar/motion.d.ts +1713 -0
  28. package/dist/core/avatar/motion.js +550 -0
  29. package/dist/core/avatar/motionRuntime.d.ts +46 -0
  30. package/dist/core/avatar/motionRuntime.js +84 -0
  31. package/dist/core/avatar/schema.d.ts +78 -0
  32. package/dist/core/avatar/schema.js +134 -0
  33. package/dist/core/avatar/visemes.d.ts +47 -1
  34. package/dist/core/avatar/visemes.js +114 -1
  35. package/dist/editor/AvatarCanvas.js +93 -3
  36. package/dist/editor/AvatarEditor.native.js +19 -9
  37. package/dist/editor/AvatarModel.js +2 -2
  38. package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
  39. package/dist/editor/FaceSqueezeEditor.js +195 -121
  40. package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
  41. package/dist/editor/FaceSqueezeEditor.web.js +32 -30
  42. package/dist/editor/RigidAccessory.js +18 -4
  43. package/dist/editor/SkinnedClothing.js +19 -9
  44. package/dist/editor/boneLockedDrag.d.ts +11 -0
  45. package/dist/editor/boneLockedDrag.js +68 -0
  46. package/dist/editor/boneSnap.js +22 -12
  47. package/dist/editor/boneSnap.web.d.ts +27 -0
  48. package/dist/editor/boneSnap.web.js +99 -0
  49. package/dist/editor/index.web.d.ts +10 -0
  50. package/dist/editor/index.web.js +26 -0
  51. package/dist/editor/sounds/haha.wav +0 -0
  52. package/dist/editor/sounds/owie.wav +0 -0
  53. package/dist/editor/sounds/stop.wav +0 -0
  54. package/dist/editor/studioTheme.d.ts +14 -14
  55. package/dist/editor/studioTheme.js +19 -16
  56. package/dist/editor/types.d.ts +1 -0
  57. package/dist/html/accessories.d.ts +7 -0
  58. package/dist/html/accessories.js +149 -0
  59. package/dist/html/motion.d.ts +1 -0
  60. package/dist/html/motion.js +189 -0
  61. package/dist/html/visemes.d.ts +7 -0
  62. package/dist/html/visemes.js +348 -0
  63. package/dist/html.d.ts +1 -1
  64. package/dist/html.js +56 -734
  65. package/dist/index.d.ts +19 -1
  66. package/dist/index.js +44 -5
  67. package/dist/index.web.d.ts +18 -1
  68. package/dist/index.web.js +36 -3
  69. package/dist/platform/api/types.d.ts +10 -0
  70. package/dist/platform/api/types.js +2 -0
  71. package/dist/platform/marketplace/types.d.ts +32 -0
  72. package/dist/platform/marketplace/types.js +2 -0
  73. package/dist/platform/sdk/unity.d.ts +27 -0
  74. package/dist/platform/sdk/unity.js +2 -0
  75. package/dist/platform/sdk/unreal.d.ts +23 -0
  76. package/dist/platform/sdk/unreal.js +2 -0
  77. package/dist/platform/sdk/web.d.ts +16 -0
  78. package/dist/platform/sdk/web.js +2 -0
  79. package/dist/sketchfab/api.js +5 -5
  80. package/dist/sketchfab/glbInspect.d.ts +22 -0
  81. package/dist/sketchfab/glbInspect.js +58 -0
  82. package/dist/sketchfab/index.d.ts +3 -0
  83. package/dist/sketchfab/index.js +8 -1
  84. package/dist/sketchfab/inspectRemote.d.ts +13 -0
  85. package/dist/sketchfab/inspectRemote.js +77 -0
  86. package/dist/sketchfab/types.d.ts +10 -0
  87. package/dist/sketchfab/useSketchfabSearch.js +1 -2
  88. package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
  89. package/dist/studio/AccessoryBrowserScreen.js +626 -0
  90. package/dist/studio/AccessoryPanel.d.ts +10 -0
  91. package/dist/studio/AccessoryPanel.js +396 -0
  92. package/dist/studio/AppearancePanel.d.ts +9 -0
  93. package/dist/studio/AppearancePanel.js +77 -0
  94. package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
  95. package/dist/studio/AvatarCreatorScreen.js +806 -0
  96. package/dist/studio/AvatarEditorScreen.d.ts +14 -0
  97. package/dist/studio/AvatarEditorScreen.js +510 -0
  98. package/dist/studio/AvatarGrid.d.ts +23 -0
  99. package/dist/studio/AvatarGrid.js +257 -0
  100. package/dist/studio/ColorSwatch.d.ts +8 -0
  101. package/dist/studio/ColorSwatch.js +100 -0
  102. package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
  103. package/dist/studio/CreateVoiceProfileSheet.js +242 -0
  104. package/dist/studio/DetailsPanel.d.ts +15 -0
  105. package/dist/studio/DetailsPanel.js +239 -0
  106. package/dist/studio/FilamentEditor.d.ts +2 -0
  107. package/dist/studio/FilamentEditor.js +6 -0
  108. package/dist/studio/PrecisionPanel.d.ts +2 -0
  109. package/dist/studio/PrecisionPanel.js +7 -0
  110. package/dist/studio/PublicGalleryScreen.d.ts +5 -0
  111. package/dist/studio/PublicGalleryScreen.js +358 -0
  112. package/dist/studio/SketchfabModelCard.d.ts +20 -0
  113. package/dist/studio/SketchfabModelCard.js +104 -0
  114. package/dist/studio/StudioBrowseHeader.d.ts +9 -0
  115. package/dist/studio/StudioBrowseHeader.js +28 -0
  116. package/dist/studio/StudioEmptyState.d.ts +8 -0
  117. package/dist/studio/StudioEmptyState.js +29 -0
  118. package/dist/studio/StudioFloatingAction.d.ts +13 -0
  119. package/dist/studio/StudioFloatingAction.js +42 -0
  120. package/dist/studio/StudioSectionHeader.d.ts +7 -0
  121. package/dist/studio/StudioSectionHeader.js +27 -0
  122. package/dist/studio/StudioSurfaceCard.d.ts +8 -0
  123. package/dist/studio/StudioSurfaceCard.js +20 -0
  124. package/dist/studio/VoicePanel.d.ts +15 -0
  125. package/dist/studio/VoicePanel.js +305 -0
  126. package/dist/studio/constants.d.ts +3 -0
  127. package/dist/studio/constants.js +6 -0
  128. package/dist/studio/index.d.ts +29 -0
  129. package/dist/studio/index.js +54 -0
  130. package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
  131. package/dist/studio/useSketchfabCapabilities.js +82 -0
  132. package/dist/tts/useDirectVisemeStream.d.ts +2 -6
  133. package/dist/tts/useDirectVisemeStream.js +16 -12
  134. package/dist/tts/useMotionMarkers.d.ts +0 -1
  135. package/dist/tts/useMotionMarkers.js +1 -2
  136. package/dist/utils/avatarUtils.js +94 -8
  137. package/dist/utils/faceLandmarkerToShapeWeights.js +21 -14
  138. package/dist/voice/convertToWav.js +1 -2
  139. package/dist/voice/index.d.ts +3 -0
  140. package/dist/voice/index.js +6 -1
  141. package/dist/voice/useAudioPlayer.js +18 -6
  142. package/dist/voice/useAudioRecording.js +1 -2
  143. package/dist/voice/useFaceControls.d.ts +14 -0
  144. package/dist/voice/useFaceControls.js +81 -0
  145. package/dist/voice/useVoicePreview.d.ts +7 -0
  146. package/dist/voice/useVoicePreview.js +83 -0
  147. package/dist/wardrobe/index.d.ts +3 -0
  148. package/dist/wardrobe/index.js +8 -1
  149. package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
  150. package/dist/wardrobe/useAccessoryGestures.js +94 -0
  151. package/dist/wardrobe/useAvatarWardrobeHydration.js +9 -4
  152. package/dist/wardrobe/useStudioAvatar.d.ts +29 -0
  153. package/dist/wardrobe/useStudioAvatar.js +186 -0
  154. package/dist/wardrobe/wardrobeStore.d.ts +2 -0
  155. package/dist/wardrobe/wardrobeStore.js +12 -2
  156. package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
  157. package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
  158. package/dist/wgpu/WgpuAvatar.d.ts +26 -2
  159. package/dist/wgpu/WgpuAvatar.js +313 -46
  160. package/dist/wgpu/accessoryDefaults.d.ts +12 -0
  161. package/dist/wgpu/accessoryDefaults.js +19 -0
  162. package/dist/wgpu/blobShim.d.ts +2 -0
  163. package/dist/wgpu/blobShim.js +191 -0
  164. package/dist/wgpu/index.d.ts +1 -0
  165. package/dist/wgpu/index.js +4 -1
  166. package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
  167. package/dist/wgpu/loadGLTFFromUri.js +75 -0
  168. package/dist/wgpu/morphTables.js +21 -10
  169. package/dist/wgpu/motionState.d.ts +20 -0
  170. package/dist/wgpu/motionState.js +31 -0
  171. package/dist/wgpu/patchThreeForRN.d.ts +28 -0
  172. package/dist/wgpu/patchThreeForRN.js +292 -0
  173. package/dist/wgpu/scenePlacement.d.ts +5 -0
  174. package/dist/wgpu/scenePlacement.js +50 -0
  175. package/dist/wgpu/useAuthedModelUri.js +22 -11
  176. package/dist/wgpu/useNativeGLTF.d.ts +7 -0
  177. package/dist/wgpu/useNativeGLTF.js +36 -0
  178. package/package.json +102 -32
@@ -0,0 +1,626 @@
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.AccessoryBrowserScreen = AccessoryBrowserScreen;
37
+ const jsx_runtime_1 = require("react/jsx-runtime");
38
+ const react_1 = require("react");
39
+ const react_native_1 = require("react-native");
40
+ const flash_list_1 = require("@shopify/flash-list");
41
+ const expo_image_1 = require("expo-image");
42
+ const bottom_sheet_1 = __importStar(require("@gorhom/bottom-sheet"));
43
+ const Haptics = __importStar(require("expo-haptics"));
44
+ const LegacyFileSystem = __importStar(require("expo-file-system/legacy"));
45
+ const expo_blur_1 = require("expo-blur");
46
+ const vector_icons_1 = require("@expo/vector-icons");
47
+ const api_1 = require("../api");
48
+ const wardrobe_1 = require("../wardrobe");
49
+ const StudioBrowseHeader_1 = require("./StudioBrowseHeader");
50
+ const StudioEmptyState_1 = require("./StudioEmptyState");
51
+ const SketchfabModelCard_1 = require("./SketchfabModelCard");
52
+ const constants_1 = require("./constants");
53
+ const editor_1 = require("../editor");
54
+ const COLORS = editor_1.studioTheme.colors;
55
+ const SKETCHFAB_API = 'https://api.sketchfab.com/v3';
56
+ const DEFAULT_QUERY = 'accessories hat glasses helmet';
57
+ const ACCESSORY_CATEGORIES = [
58
+ { id: 'hair', label: 'Hair', icon: 'cut', type: 'skinned', defaultQuery: 'anime hair 3d model' },
59
+ { id: 'hat', label: 'Hats', icon: 'school', type: 'rigid', attach_bone: 'Head', defaultQuery: 'hat cap 3d model' },
60
+ { id: 'glasses', label: 'Glasses', icon: 'glasses', type: 'rigid', attach_bone: 'Head', defaultQuery: 'glasses eyewear 3d model' },
61
+ { id: 'necklace', label: 'Necklaces', icon: 'link', type: 'rigid', attach_bone: 'Neck', defaultQuery: 'necklace pendant 3d model' },
62
+ { id: 'handheld', label: 'Handheld', icon: 'hand-left', type: 'rigid', attach_bone: 'RightHand', defaultQuery: 'sword weapon prop 3d model' },
63
+ { id: 'wings', label: 'Wings', icon: 'leaf', type: 'rigid', attach_bone: 'Spine2', defaultQuery: 'wings 3d model fantasy' },
64
+ { id: 'tail', label: 'Tail', icon: 'flash', type: 'rigid', attach_bone: 'Hips', defaultQuery: 'tail 3d model character' },
65
+ { id: 'cape', label: 'Cape', icon: 'shield', type: 'skinned', attach_bone: 'Spine2', defaultQuery: 'cape cloak 3d model' },
66
+ { id: 'top', label: 'Tops', icon: 'shirt', type: 'skinned', defaultQuery: 'shirt jacket clothing 3d model' },
67
+ { id: 'bottom', label: 'Bottoms', icon: 'cube', type: 'skinned', defaultQuery: 'pants skirt clothing 3d model' },
68
+ { id: 'footwear', label: 'Footwear', icon: 'footsteps', type: 'skinned', defaultQuery: 'shoes boots footwear 3d model' },
69
+ ];
70
+ const DEFAULT_CATEGORY = ACCESSORY_CATEGORIES[0];
71
+ function formatCount(n) {
72
+ if (n >= 1000000)
73
+ return `${(n / 1000000).toFixed(1)}M`;
74
+ if (n >= 1000)
75
+ return `${(n / 1000).toFixed(1)}k`;
76
+ return String(n);
77
+ }
78
+ function pickThumbnail(thumbnails, targetWidth = 600) {
79
+ const images = thumbnails?.images;
80
+ if (!images || images.length === 0)
81
+ return null;
82
+ let best = images[0];
83
+ let bestDiff = Math.abs(best.width - targetWidth);
84
+ for (let i = 1; i < images.length; i++) {
85
+ const diff = Math.abs(images[i].width - targetWidth);
86
+ if (diff < bestDiff) {
87
+ best = images[i];
88
+ bestDiff = diff;
89
+ }
90
+ }
91
+ return best.url;
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Backdrop component
95
+ // ---------------------------------------------------------------------------
96
+ const renderBackdrop = (props) => ((0, jsx_runtime_1.jsx)(bottom_sheet_1.BottomSheetBackdrop, { ...props, disappearsOnIndex: -1, appearsOnIndex: 0, pressBehavior: "close", opacity: 0.6 }));
97
+ // ---------------------------------------------------------------------------
98
+ // Main screen
99
+ // ---------------------------------------------------------------------------
100
+ function AccessoryBrowserScreen({ avatarId, sketchfabApiKey, onDone, }) {
101
+ const bottomSheetRef = (0, react_1.useRef)(null);
102
+ const equip = (0, wardrobe_1.useWardrobeStore)((s) => s.equip);
103
+ // Search state
104
+ const [searchText, setSearchText] = (0, react_1.useState)('');
105
+ const [activeCategory, setActiveCategory] = (0, react_1.useState)(DEFAULT_CATEGORY);
106
+ const [query, setQuery] = (0, react_1.useState)(DEFAULT_CATEGORY.defaultQuery);
107
+ // Data state
108
+ const [models, setModels] = (0, react_1.useState)([]);
109
+ const [loading, setLoading] = (0, react_1.useState)(true);
110
+ const [loadingMore, setLoadingMore] = (0, react_1.useState)(false);
111
+ const [error, setError] = (0, react_1.useState)(null);
112
+ const [nextCursor, setNextCursor] = (0, react_1.useState)(null);
113
+ // Bottom sheet state
114
+ const [selected, setSelected] = (0, react_1.useState)(null);
115
+ // Import state
116
+ const [importing, setImporting] = (0, react_1.useState)(false);
117
+ const [importPhase, setImportPhase] = (0, react_1.useState)(null);
118
+ const [importProgress, setImportProgress] = (0, react_1.useState)(0);
119
+ // Debounce ref
120
+ const debounceRef = (0, react_1.useRef)(null);
121
+ // Bottom sheet snap points
122
+ const snapPoints = (0, react_1.useMemo)(() => ['55%', '80%'], []);
123
+ // ------ Sketchfab search ------------------------------------------------
124
+ const searchSketchfab = (0, react_1.useCallback)(async (q, cursor) => {
125
+ const effectiveQuery = q.trim() || DEFAULT_QUERY;
126
+ const params = new URLSearchParams({
127
+ type: 'models',
128
+ downloadable: 'true',
129
+ sort_by: '-likeCount',
130
+ count: String(constants_1.RESULTS_PER_PAGE),
131
+ q: effectiveQuery,
132
+ });
133
+ if (cursor) {
134
+ params.set('cursor', cursor);
135
+ }
136
+ const res = await fetch(`${SKETCHFAB_API}/search?${params.toString()}`, {
137
+ headers: { Authorization: `Token ${sketchfabApiKey}` },
138
+ });
139
+ if (!res.ok) {
140
+ throw new Error(`Sketchfab API error: ${res.status}`);
141
+ }
142
+ return (await res.json());
143
+ }, [sketchfabApiKey]);
144
+ const filterBySize = (0, react_1.useCallback)((results) => {
145
+ return results.filter((m) => {
146
+ const size = m.archives?.glb?.size ?? m.archives?.gltf?.size;
147
+ return !size || size <= constants_1.MAX_ASSET_SIZE;
148
+ });
149
+ }, []);
150
+ const loadInitial = (0, react_1.useCallback)(async (q) => {
151
+ try {
152
+ setLoading(true);
153
+ setError(null);
154
+ setModels([]);
155
+ setNextCursor(null);
156
+ const data = await searchSketchfab(q);
157
+ setModels(filterBySize(data.results));
158
+ setNextCursor(data.cursors?.next ?? null);
159
+ }
160
+ catch (err) {
161
+ setError(err instanceof Error ? err.message : 'Failed to search Sketchfab');
162
+ }
163
+ finally {
164
+ setLoading(false);
165
+ }
166
+ }, [searchSketchfab, filterBySize]);
167
+ const loadMore = (0, react_1.useCallback)(async () => {
168
+ if (!nextCursor || loadingMore)
169
+ return;
170
+ try {
171
+ setLoadingMore(true);
172
+ const data = await searchSketchfab(query, nextCursor);
173
+ setModels((prev) => [...prev, ...filterBySize(data.results)]);
174
+ setNextCursor(data.cursors?.next ?? null);
175
+ }
176
+ catch (err) {
177
+ react_native_1.Alert.alert('Error', err instanceof Error ? err.message : 'Failed to load more results');
178
+ }
179
+ finally {
180
+ setLoadingMore(false);
181
+ }
182
+ }, [nextCursor, loadingMore, query, searchSketchfab, filterBySize]);
183
+ // Initial load
184
+ (0, react_1.useEffect)(() => {
185
+ void loadInitial(query);
186
+ }, [query, loadInitial]);
187
+ // ------ Debounced text search -------------------------------------------
188
+ const handleSearchChange = (0, react_1.useCallback)((text) => {
189
+ setSearchText(text);
190
+ if (debounceRef.current) {
191
+ clearTimeout(debounceRef.current);
192
+ }
193
+ debounceRef.current = setTimeout(() => {
194
+ const trimmed = text.trim();
195
+ const nextQuery = trimmed ? `${trimmed} accessory 3d model` : activeCategory.defaultQuery;
196
+ setQuery(nextQuery);
197
+ }, 400);
198
+ }, [activeCategory]);
199
+ (0, react_1.useEffect)(() => {
200
+ return () => {
201
+ if (debounceRef.current)
202
+ clearTimeout(debounceRef.current);
203
+ };
204
+ }, []);
205
+ // ------ Filter chips ----------------------------------------------------
206
+ const handleCategoryPress = (0, react_1.useCallback)((cat) => {
207
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
208
+ setActiveCategory(cat);
209
+ setSearchText('');
210
+ setQuery(cat.defaultQuery);
211
+ if (debounceRef.current) {
212
+ clearTimeout(debounceRef.current);
213
+ debounceRef.current = null;
214
+ }
215
+ }, []);
216
+ // ------ Card tap --------------------------------------------------------
217
+ const handleCardPress = (0, react_1.useCallback)((model) => {
218
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
219
+ setSelected(model);
220
+ setImportPhase(null);
221
+ setImportProgress(0);
222
+ bottomSheetRef.current?.snapToIndex(0);
223
+ }, []);
224
+ const handleSheetChange = (0, react_1.useCallback)((index) => {
225
+ if (index === -1) {
226
+ setSelected(null);
227
+ setImportPhase(null);
228
+ setImportProgress(0);
229
+ }
230
+ }, []);
231
+ // ------ Import flow -----------------------------------------------------
232
+ const handleImport = (0, react_1.useCallback)(async (model) => {
233
+ if (importing)
234
+ return;
235
+ void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
236
+ try {
237
+ setImporting(true);
238
+ setImportPhase('downloading');
239
+ setImportProgress(0);
240
+ // 1. Get signed download URL
241
+ const downloadRes = await fetch(`${SKETCHFAB_API}/models/${model.uid}/download`, {
242
+ headers: { Authorization: `Token ${sketchfabApiKey}` },
243
+ });
244
+ if (!downloadRes.ok) {
245
+ throw new Error('Failed to get download URL');
246
+ }
247
+ const downloadData = await downloadRes.json();
248
+ // 2. Check size before downloading (Max 50MB)
249
+ const MAX_SIZE = constants_1.MAX_ASSET_SIZE;
250
+ const skfSize = downloadData?.glb?.size ?? downloadData?.gltf?.size;
251
+ if (skfSize && skfSize > MAX_SIZE) {
252
+ const mb = (skfSize / (1024 * 1024)).toFixed(1);
253
+ throw new Error(`Model is too large (${mb}MB). Maximum size is 50MB.`);
254
+ }
255
+ const glbUrl = downloadData?.glb?.url ?? downloadData?.gltf?.url;
256
+ if (!glbUrl)
257
+ throw new Error('No download URL found');
258
+ // HEAD request to verify content-length (fallback)
259
+ let tooLargeError = null;
260
+ try {
261
+ const headRes = await fetch(glbUrl, { method: 'HEAD' });
262
+ const contentLength = headRes.headers.get('content-length');
263
+ if (contentLength && parseInt(contentLength, 10) > MAX_SIZE) {
264
+ const mb = (parseInt(contentLength, 10) / (1024 * 1024)).toFixed(1);
265
+ tooLargeError = new Error(`Model is too large (${mb}MB). Maximum size is 50MB.`);
266
+ }
267
+ }
268
+ catch (e) {
269
+ console.warn('[AccessoryBrowser] HEAD size check failed:', e);
270
+ }
271
+ if (tooLargeError)
272
+ throw tooLargeError;
273
+ // 3. Download GLB to disk
274
+ const tmpPath = `${LegacyFileSystem.cacheDirectory}sketchfab_accessory.glb`;
275
+ const download = LegacyFileSystem.createDownloadResumable(glbUrl, tmpPath, {}, (progress) => {
276
+ const pct = progress.totalBytesWritten /
277
+ progress.totalBytesExpectedToWrite;
278
+ setImportProgress(pct);
279
+ });
280
+ const result = await download.downloadAsync();
281
+ if (!result)
282
+ throw new Error('Download failed');
283
+ // 3. Upload as asset
284
+ setImportPhase('uploading');
285
+ setImportProgress(0);
286
+ await LegacyFileSystem.getInfoAsync(result.uri);
287
+ const asset = await (0, api_1.uploadAsset)(result.uri, {
288
+ name: model.name,
289
+ category: activeCategory.id,
290
+ type: activeCategory.type,
291
+ slot: model.uid,
292
+ attach_bone: activeCategory.attach_bone,
293
+ });
294
+ // 4. AI placement suggestion
295
+ if (avatarId) {
296
+ setImportPhase('placing');
297
+ try {
298
+ const suggestion = await (0, api_1.suggestPlacement)(asset.id, avatarId);
299
+ equip(asset.slot, asset);
300
+ const setPlacement = wardrobe_1.useWardrobeStore.getState().setPlacement;
301
+ setPlacement(asset.id, {
302
+ bone: suggestion.bone,
303
+ position: suggestion.position,
304
+ rotation: suggestion.rotation,
305
+ scale: 1,
306
+ });
307
+ }
308
+ catch {
309
+ equip(asset.slot, asset);
310
+ }
311
+ }
312
+ else {
313
+ equip(asset.slot, asset);
314
+ }
315
+ // 5. Cleanup temp file
316
+ await LegacyFileSystem.deleteAsync(tmpPath, { idempotent: true });
317
+ // 6. Close sheet and navigate back
318
+ bottomSheetRef.current?.close();
319
+ void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
320
+ onDone?.();
321
+ }
322
+ catch (err) {
323
+ react_native_1.Alert.alert('Import Failed', err instanceof Error ? err.message : 'Something went wrong during import.');
324
+ }
325
+ finally {
326
+ setImporting(false);
327
+ setImportPhase(null);
328
+ setImportProgress(0);
329
+ }
330
+ }, [importing, sketchfabApiKey, avatarId, equip, activeCategory, onDone]);
331
+ // ------ Render helpers --------------------------------------------------
332
+ const renderItem = (0, react_1.useCallback)(({ item, index }) => ((0, jsx_runtime_1.jsx)(SketchfabModelCard_1.SketchfabModelCard, { model: item, index: index, onPress: handleCardPress })), [handleCardPress]);
333
+ const keyExtractor = (0, react_1.useCallback)((item) => item.uid, []);
334
+ const renderFooter = (0, react_1.useCallback)(() => {
335
+ if (loadingMore) {
336
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.footerContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: COLORS.accent, size: "small" }) }));
337
+ }
338
+ if (nextCursor) {
339
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.footerContainer, children: (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: styles.loadMoreButton, activeOpacity: 0.7, onPress: loadMore, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadMoreText, children: "Load More" }) }) }));
340
+ }
341
+ return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.gridBottomSpacer });
342
+ }, [loadingMore, nextCursor, loadMore]);
343
+ // ------ Import button label ---------------------------------------------
344
+ const importButtonLabel = (0, react_1.useMemo)(() => {
345
+ if (importPhase === 'downloading') {
346
+ return `Downloading\u2026 ${Math.round(importProgress * 100)}%`;
347
+ }
348
+ if (importPhase === 'uploading') {
349
+ return 'Uploading\u2026';
350
+ }
351
+ if (importPhase === 'placing') {
352
+ return 'Auto-placing\u2026';
353
+ }
354
+ return 'Import Accessory';
355
+ }, [importPhase, importProgress]);
356
+ // ------ Selected model details ------------------------------------------
357
+ const selectedThumbnail = selected
358
+ ? pickThumbnail(selected.thumbnails, 600)
359
+ : null;
360
+ // ------ Header: search bar + chips --------------------------------------
361
+ const ListHeader = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsxs)(StudioBrowseHeader_1.StudioBrowseHeader, { eyebrow: "Wardrobe", title: "Browse accessories", subtitle: "Search props, hair, and wearables, then place them directly on the active avatar.", children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.searchContainer, children: [(0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: "search", size: 18, color: COLORS.textMuted, style: styles.searchIcon }), (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: styles.searchInput, placeholder: "Search accessories...", placeholderTextColor: COLORS.textMuted, value: searchText, onChangeText: handleSearchChange, returnKeyType: "search", autoCorrect: false, autoCapitalize: "none" }), searchText.length > 0 && ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: () => handleSearchChange(''), hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, children: (0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: "close-circle", size: 18, color: COLORS.textMuted }) }))] }), (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.chipScroll, children: ACCESSORY_CATEGORIES.map((cat) => {
362
+ const isActive = activeCategory.id === cat.id;
363
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.TouchableOpacity, { style: [styles.chip, isActive && styles.chipActive], activeOpacity: 0.7, onPress: () => handleCategoryPress(cat), children: [(0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: cat.icon, size: 13, color: isActive ? '#fff' : COLORS.textSecondary, style: { marginRight: 4 } }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
364
+ styles.chipText,
365
+ isActive && styles.chipTextActive,
366
+ ], children: cat.label })] }, cat.id));
367
+ }) })] })), [searchText, activeCategory, handleSearchChange, handleCategoryPress]);
368
+ // ------ Loading state ---------------------------------------------------
369
+ if (loading) {
370
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [ListHeader, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.centered, children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: COLORS.accent, size: "large" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingText, children: "Searching accessories..." })] })] }));
371
+ }
372
+ // ------ Error state -----------------------------------------------------
373
+ if (error && models.length === 0) {
374
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [ListHeader, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.centered, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.errorText, children: error }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: () => loadInitial(query), style: styles.retryButton, activeOpacity: 0.7, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.retryButtonText, children: "Retry" }) })] })] }));
375
+ }
376
+ // ------ Empty state -----------------------------------------------------
377
+ if (!loading && models.length === 0) {
378
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [ListHeader, (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.centered, children: (0, jsx_runtime_1.jsx)(StudioEmptyState_1.StudioEmptyState, { icon: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.emptyTitle, children: "WARDROBE" }), title: "No accessories found", subtitle: "Try a different search term or filter." }) })] }));
379
+ }
380
+ // ------ Main grid -------------------------------------------------------
381
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
382
+ const AnyFlashList = flash_list_1.FlashList;
383
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [(0, jsx_runtime_1.jsx)(AnyFlashList, { style: { flex: 1 }, data: models, keyExtractor: keyExtractor, renderItem: renderItem, numColumns: 2, estimatedItemSize: 200, contentContainerStyle: styles.gridContent, showsVerticalScrollIndicator: false, ListHeaderComponent: ListHeader, ListFooterComponent: renderFooter }), (0, jsx_runtime_1.jsx)(bottom_sheet_1.default, { ref: bottomSheetRef, index: -1, snapPoints: snapPoints, enablePanDownToClose: true, onChange: handleSheetChange, backdropComponent: renderBackdrop, backgroundComponent: ((props) => ((0, jsx_runtime_1.jsx)(expo_blur_1.BlurView, { ...props, tint: "systemThickMaterialDark", intensity: 90, style: [
384
+ props.style,
385
+ styles.sheetBackground,
386
+ { backgroundColor: 'rgba(15, 19, 26, 0.4)' },
387
+ ] }))), handleIndicatorStyle: styles.sheetHandle, children: (0, jsx_runtime_1.jsx)(bottom_sheet_1.BottomSheetScrollView, { contentContainerStyle: styles.sheetContent, children: selected ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedThumbnail ? ((0, jsx_runtime_1.jsx)(expo_image_1.Image, { source: { uri: selectedThumbnail }, style: styles.sheetThumbnail, contentFit: "cover", placeholder: { blurhash: constants_1.BLURHASH }, transition: 300 })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.sheetThumbnailPlaceholder, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sheetPlaceholderText, children: "3D" }) })), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sheetName, children: selected.name }), selected.description ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sheetDescription, numberOfLines: 4, children: selected.description })) : null, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statsRow, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statItem, children: [(0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: "heart", size: 14, color: COLORS.textSecondary }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.statText, children: formatCount(selected.likeCount) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statItem, children: [(0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: "eye", size: 14, color: COLORS.textSecondary }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.statText, children: formatCount(selected.viewCount) })] }), selected.animationCount > 0 && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statItem, children: [(0, jsx_runtime_1.jsx)(vector_icons_1.Ionicons, { name: "film", size: 14, color: COLORS.textSecondary }), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.statText, children: [selected.animationCount, " anim"] })] }))] }), (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.sheetMeta, children: ["by", ' ', selected.user.displayName || selected.user.username, selected.license
388
+ ? ` \u00b7 ${selected.license.label}`
389
+ : ''] }), importPhase === 'downloading' ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.progressContainer, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.progressTrack, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
390
+ styles.progressFill,
391
+ { width: `${Math.round(importProgress * 100)}%` },
392
+ ] }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.progressText, children: importButtonLabel })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [
393
+ styles.importButton,
394
+ importing && styles.importButtonDisabled,
395
+ ], activeOpacity: 0.8, onPress: () => handleImport(selected), disabled: importing, children: importing ? ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.importButtonInner, children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: "#fff", size: "small" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.importButtonText, children: importButtonLabel })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.importButtonText, children: "Import Accessory" })) }))] })) : null }) })] }));
396
+ }
397
+ // ---------------------------------------------------------------------------
398
+ // Styles
399
+ // ---------------------------------------------------------------------------
400
+ const styles = react_native_1.StyleSheet.create({
401
+ container: {
402
+ flex: 1,
403
+ backgroundColor: COLORS.background,
404
+ },
405
+ centered: {
406
+ flex: 1,
407
+ alignItems: 'center',
408
+ justifyContent: 'center',
409
+ paddingHorizontal: 32,
410
+ gap: 8,
411
+ },
412
+ searchContainer: {
413
+ flexDirection: 'row',
414
+ alignItems: 'center',
415
+ backgroundColor: COLORS.surface,
416
+ borderRadius: 14,
417
+ borderWidth: 1,
418
+ borderColor: COLORS.border,
419
+ marginHorizontal: 12,
420
+ paddingHorizontal: 14,
421
+ height: 46,
422
+ gap: 8,
423
+ },
424
+ searchIcon: {
425
+ marginRight: 2,
426
+ },
427
+ searchInput: {
428
+ flex: 1,
429
+ color: COLORS.textPrimary,
430
+ fontSize: 15,
431
+ paddingVertical: 0,
432
+ },
433
+ chipScroll: {
434
+ paddingHorizontal: 12,
435
+ gap: 8,
436
+ },
437
+ chip: {
438
+ flexDirection: 'row',
439
+ alignItems: 'center',
440
+ paddingHorizontal: 14,
441
+ paddingVertical: 7,
442
+ borderRadius: 10,
443
+ backgroundColor: COLORS.surface,
444
+ borderWidth: 1,
445
+ borderColor: COLORS.border,
446
+ },
447
+ chipActive: {
448
+ backgroundColor: COLORS.accent,
449
+ borderColor: COLORS.accent,
450
+ },
451
+ chipText: {
452
+ color: COLORS.textSecondary,
453
+ fontSize: 13,
454
+ fontWeight: '500',
455
+ },
456
+ chipTextActive: {
457
+ color: '#fff',
458
+ fontWeight: '600',
459
+ },
460
+ gridContent: {
461
+ paddingHorizontal: 12,
462
+ },
463
+ gridBottomSpacer: {
464
+ height: 32,
465
+ },
466
+ footerContainer: {
467
+ paddingVertical: 20,
468
+ alignItems: 'center',
469
+ },
470
+ loadMoreButton: {
471
+ paddingHorizontal: 28,
472
+ paddingVertical: 12,
473
+ backgroundColor: COLORS.surface,
474
+ borderRadius: 12,
475
+ borderWidth: 1,
476
+ borderColor: COLORS.border,
477
+ },
478
+ loadMoreText: {
479
+ color: COLORS.textPrimary,
480
+ fontSize: 14,
481
+ fontWeight: '600',
482
+ },
483
+ loadingText: {
484
+ color: COLORS.textSecondary,
485
+ fontSize: 13,
486
+ marginTop: 8,
487
+ },
488
+ errorText: {
489
+ color: COLORS.danger,
490
+ fontSize: 14,
491
+ fontWeight: '500',
492
+ textAlign: 'center',
493
+ marginBottom: 8,
494
+ },
495
+ retryButton: {
496
+ paddingHorizontal: 24,
497
+ paddingVertical: 10,
498
+ backgroundColor: COLORS.surfaceSoft,
499
+ borderRadius: 10,
500
+ borderWidth: 1,
501
+ borderColor: COLORS.border,
502
+ },
503
+ retryButtonText: {
504
+ color: COLORS.textPrimary,
505
+ fontSize: 13,
506
+ fontWeight: '600',
507
+ },
508
+ emptyTitle: {
509
+ color: COLORS.textSecondary,
510
+ fontSize: 16,
511
+ fontWeight: '600',
512
+ textAlign: 'center',
513
+ },
514
+ sheetBackground: {
515
+ backgroundColor: COLORS.surface,
516
+ borderTopLeftRadius: 24,
517
+ borderTopRightRadius: 24,
518
+ },
519
+ sheetHandle: {
520
+ backgroundColor: COLORS.border,
521
+ width: 40,
522
+ },
523
+ sheetContent: {
524
+ paddingHorizontal: 24,
525
+ paddingTop: 8,
526
+ paddingBottom: 40,
527
+ gap: 12,
528
+ },
529
+ sheetThumbnail: {
530
+ width: '100%',
531
+ aspectRatio: 4 / 3,
532
+ borderRadius: 16,
533
+ backgroundColor: COLORS.backgroundAlt,
534
+ },
535
+ sheetThumbnailPlaceholder: {
536
+ width: '100%',
537
+ aspectRatio: 4 / 3,
538
+ borderRadius: 16,
539
+ backgroundColor: COLORS.surfaceSoft,
540
+ alignItems: 'center',
541
+ justifyContent: 'center',
542
+ },
543
+ sheetPlaceholderText: {
544
+ color: COLORS.textMuted,
545
+ fontSize: 32,
546
+ fontWeight: '700',
547
+ letterSpacing: 3,
548
+ },
549
+ sheetName: {
550
+ color: COLORS.textPrimary,
551
+ fontSize: 22,
552
+ fontWeight: '700',
553
+ letterSpacing: -0.3,
554
+ },
555
+ sheetDescription: {
556
+ color: COLORS.textSecondary,
557
+ fontSize: 14,
558
+ lineHeight: 20,
559
+ },
560
+ statsRow: {
561
+ flexDirection: 'row',
562
+ alignItems: 'center',
563
+ gap: 16,
564
+ },
565
+ statItem: {
566
+ flexDirection: 'row',
567
+ alignItems: 'center',
568
+ gap: 4,
569
+ },
570
+ statText: {
571
+ color: COLORS.textSecondary,
572
+ fontSize: 13,
573
+ },
574
+ sheetMeta: {
575
+ color: COLORS.textMuted,
576
+ fontSize: 12,
577
+ },
578
+ importButton: {
579
+ marginTop: 8,
580
+ backgroundColor: COLORS.accent,
581
+ borderRadius: 14,
582
+ paddingVertical: 16,
583
+ alignItems: 'center',
584
+ shadowColor: COLORS.accent,
585
+ shadowOffset: { width: 0, height: 4 },
586
+ shadowOpacity: 0.4,
587
+ shadowRadius: 12,
588
+ elevation: 8,
589
+ },
590
+ importButtonDisabled: {
591
+ opacity: 0.7,
592
+ },
593
+ importButtonInner: {
594
+ flexDirection: 'row',
595
+ alignItems: 'center',
596
+ gap: 8,
597
+ },
598
+ importButtonText: {
599
+ color: '#fff',
600
+ fontSize: 16,
601
+ fontWeight: '700',
602
+ letterSpacing: 0.3,
603
+ },
604
+ progressContainer: {
605
+ marginTop: 8,
606
+ gap: 8,
607
+ alignItems: 'center',
608
+ },
609
+ progressTrack: {
610
+ width: '100%',
611
+ height: 8,
612
+ borderRadius: 4,
613
+ backgroundColor: COLORS.border,
614
+ overflow: 'hidden',
615
+ },
616
+ progressFill: {
617
+ height: '100%',
618
+ borderRadius: 4,
619
+ backgroundColor: COLORS.accent,
620
+ },
621
+ progressText: {
622
+ color: COLORS.textSecondary,
623
+ fontSize: 13,
624
+ fontWeight: '500',
625
+ },
626
+ });
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { WgpuAvatarRef } from '../wgpu/WgpuAvatar';
3
+ export interface AccessoryPanelProps {
4
+ avatarId: string;
5
+ onPositionAsset: (assetId: string) => void;
6
+ onBrowseAccessories?: () => void;
7
+ /** Ref to the live avatar — used to trigger the blink reaction on snap-on. */
8
+ avatarRef?: React.RefObject<WgpuAvatarRef | null>;
9
+ }
10
+ export default function AccessoryPanel({ avatarId, onPositionAsset, onBrowseAccessories, avatarRef }: AccessoryPanelProps): import("react/jsx-runtime").JSX.Element;