@vctrl/hooks 0.9.5 → 0.11.1

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 (71) hide show
  1. package/CHANGELOG.md +210 -0
  2. package/README.md +202 -383
  3. package/package.json +66 -55
  4. package/project.json +55 -0
  5. package/src/index.ts +38 -0
  6. package/src/use-export-model/use-export-model.ts +113 -0
  7. package/src/use-load-model/event-system.ts +46 -0
  8. package/src/use-load-model/index.ts +15 -0
  9. package/src/use-load-model/model-context.tsx +174 -0
  10. package/src/use-load-model/state.ts +75 -0
  11. package/src/use-load-model/types.ts +284 -0
  12. package/src/use-load-model/use-load-model.ts +682 -0
  13. package/src/use-load-model/utils/calculate-referenced-bytes.ts +123 -0
  14. package/src/use-load-model/utils/index.ts +7 -0
  15. package/src/use-load-model/utils/read-directory.ts +35 -0
  16. package/src/use-load-model/utils/reconstruct-files.ts +85 -0
  17. package/src/use-load-model/utils/resolve-scene-payload.ts +151 -0
  18. package/{use-optimize-model/index.d.ts → src/use-optimize-model/index.ts} +2 -1
  19. package/src/use-optimize-model/state.ts +40 -0
  20. package/src/use-optimize-model/types.ts +37 -0
  21. package/src/use-optimize-model/use-calc-optimization-info.ts +97 -0
  22. package/src/use-optimize-model/use-optimize-model.ts +467 -0
  23. package/src/use-optimize-model/utils/index.ts +29 -0
  24. package/src/use-optimize-model/utils/texture-optimization.ts +292 -0
  25. package/src/use-optimize-model/utils/validation.ts +62 -0
  26. package/src/utils/server-communication.ts +351 -0
  27. package/tsconfig.json +21 -0
  28. package/tsconfig.lib.json +16 -0
  29. package/vite.config.ts +77 -0
  30. package/GLTFExporter-BmLF430n.cjs +0 -18
  31. package/GLTFExporter-C4hCeb4H.js +0 -1263
  32. package/index.cjs.js +0 -1
  33. package/index.d.ts +0 -3
  34. package/index.es.js +0 -12
  35. package/model-context-CQJta_vi.cjs +0 -9
  36. package/model-context-D1Q8WdpT.js +0 -3431
  37. package/use-export-model/types.d.ts +0 -19
  38. package/use-export-model/use-export-model.d.ts +0 -14
  39. package/use-export-model/utils/data-uri-to-blob.d.ts +0 -9
  40. package/use-export-model/utils/export-handlers.d.ts +0 -23
  41. package/use-export-model/utils/file-helpers.d.ts +0 -17
  42. package/use-export-model/utils/index.d.ts +0 -3
  43. package/use-export-model-AFharGg5.js +0 -77
  44. package/use-export-model-CWcKp4A3.cjs +0 -1
  45. package/use-export-model.cjs.js +0 -1
  46. package/use-export-model.es.js +0 -4
  47. package/use-load-model/event-system.d.ts +0 -7
  48. package/use-load-model/file-type-hooks/index.d.ts +0 -2
  49. package/use-load-model/file-type-hooks/use-load-binary.d.ts +0 -11
  50. package/use-load-model/file-type-hooks/use-load-gltf.d.ts +0 -5
  51. package/use-load-model/index.d.ts +0 -3
  52. package/use-load-model/loaders/create-gltf-loader.d.ts +0 -3
  53. package/use-load-model/loaders/create-usdz-loader.d.ts +0 -3
  54. package/use-load-model/loaders/index.d.ts +0 -2
  55. package/use-load-model/model-context.d.ts +0 -78
  56. package/use-load-model/state.d.ts +0 -19
  57. package/use-load-model/types.d.ts +0 -51
  58. package/use-load-model/use-load-model.d.ts +0 -74
  59. package/use-load-model/utils/array-buffer-to-base64.d.ts +0 -2
  60. package/use-load-model/utils/index.d.ts +0 -2
  61. package/use-load-model/utils/read-directory.d.ts +0 -8
  62. package/use-load-model.cjs.js +0 -1
  63. package/use-load-model.es.js +0 -8
  64. package/use-optimize-model/state.d.ts +0 -13
  65. package/use-optimize-model/types.d.ts +0 -37
  66. package/use-optimize-model/use-optimize-model.d.ts +0 -29
  67. package/use-optimize-model-Cqkp__Vi.cjs +0 -1
  68. package/use-optimize-model-DZWIb_40.js +0 -127
  69. package/use-optimize-model.cjs.js +0 -1
  70. package/use-optimize-model.es.js +0 -4
  71. /package/{use-export-model/index.d.ts → src/use-export-model/index.ts} +0 -0
@@ -0,0 +1,682 @@
1
+ /* vectreal-core | vctrl/hooks
2
+ Copyright (C) 2024 Moritz Becker
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
16
+
17
+ import { buildAssetLookupKeys, normalizeAssetUri } from '@vctrl/core'
18
+ import { ModelFileTypes, ModelLoader } from '@vctrl/core/model-loader'
19
+ import { useCallback, useMemo, useReducer, useRef } from 'react'
20
+
21
+ import eventSystem from './event-system'
22
+ import reducer, { initialState } from './state'
23
+ import {
24
+ Action,
25
+ InputFileOrDirectory,
26
+ ModelFile,
27
+ SceneDataLoadOptions,
28
+ OptimizerIntegrationReturn,
29
+ SceneLoadOptions,
30
+ SceneLoadResult,
31
+ UseLoadModelReturn
32
+ } from './types'
33
+ import {
34
+ calculateReferencedBytesFromFiles,
35
+ calculateReferencedBytesFromServerScene,
36
+ readDirectory,
37
+ resolveServerSceneDataContract,
38
+ reconstructGltfFiles
39
+ } from './utils'
40
+ import { ServerCommunicationService } from '../utils/server-communication'
41
+
42
+ import type { useOptimizeModel } from '../use-optimize-model'
43
+ import type {
44
+ OperationProgress,
45
+ SerializedSceneAssetDataMap,
46
+ ServerSceneData,
47
+ ServerScenePayload
48
+ } from '@vctrl/core'
49
+
50
+ const safeNormalizeAssetUri = (value: string): string => {
51
+ try {
52
+ return normalizeAssetUri(value)
53
+ } catch {
54
+ return value
55
+ }
56
+ }
57
+
58
+ const collectReferencedUris = (gltfJson: unknown): Set<string> => {
59
+ const referencedUris = new Set<string>()
60
+ const document = gltfJson as {
61
+ images?: Array<{ uri?: string }>
62
+ buffers?: Array<{ uri?: string }>
63
+ }
64
+
65
+ const images = Array.isArray(document.images) ? document.images : []
66
+ for (const image of images) {
67
+ if (typeof image.uri === 'string' && !image.uri.startsWith('data:')) {
68
+ referencedUris.add(image.uri)
69
+ }
70
+ }
71
+
72
+ const buffers = Array.isArray(document.buffers) ? document.buffers : []
73
+ for (const buffer of buffers) {
74
+ if (typeof buffer.uri === 'string' && !buffer.uri.startsWith('data:')) {
75
+ referencedUris.add(buffer.uri)
76
+ }
77
+ }
78
+
79
+ return referencedUris
80
+ }
81
+
82
+ const buildSceneDataFromLocalFiles = async (
83
+ gltfFile: File,
84
+ assetFiles: File[]
85
+ ): Promise<ServerSceneData> => {
86
+ const gltfJson = JSON.parse(
87
+ await gltfFile.text()
88
+ ) as ServerSceneData['gltfJson']
89
+ const referencedUris = collectReferencedUris(gltfJson)
90
+ const assetData: SerializedSceneAssetDataMap = {}
91
+ const fileLookup = new Map<string, File>()
92
+ const fileBytesCache = new Map<File, Uint8Array>()
93
+ const matchedFiles = new Set<File>()
94
+
95
+ const getFileBytes = async (file: File): Promise<Uint8Array> => {
96
+ const cached = fileBytesCache.get(file)
97
+ if (cached) {
98
+ return cached
99
+ }
100
+
101
+ const bytes = new Uint8Array(await file.arrayBuffer())
102
+ fileBytesCache.set(file, bytes)
103
+ return bytes
104
+ }
105
+
106
+ for (const assetFile of assetFiles) {
107
+ for (const key of buildAssetLookupKeys(assetFile.name)) {
108
+ fileLookup.set(key, assetFile)
109
+ }
110
+
111
+ if (assetFile.webkitRelativePath) {
112
+ for (const key of buildAssetLookupKeys(assetFile.webkitRelativePath)) {
113
+ fileLookup.set(key, assetFile)
114
+ }
115
+ }
116
+ }
117
+
118
+ const missingUris: string[] = []
119
+
120
+ for (const uri of referencedUris) {
121
+ const normalizedUri = safeNormalizeAssetUri(uri)
122
+ const basename = normalizedUri.split('/').pop() || normalizedUri
123
+
124
+ const matchedFile =
125
+ fileLookup.get(uri) ||
126
+ fileLookup.get(normalizedUri) ||
127
+ fileLookup.get(basename)
128
+
129
+ if (!matchedFile) {
130
+ missingUris.push(uri)
131
+ continue
132
+ }
133
+
134
+ matchedFiles.add(matchedFile)
135
+ const bytes = await getFileBytes(matchedFile)
136
+ assetData[normalizedUri] = {
137
+ data: Array.from(bytes),
138
+ fileName: normalizedUri,
139
+ mimeType: matchedFile.type || 'application/octet-stream'
140
+ }
141
+ }
142
+
143
+ if (missingUris.length > 0) {
144
+ throw new Error(
145
+ `Scene payload is missing required referenced assets: ${missingUris.slice(0, 5).join(', ')}`
146
+ )
147
+ }
148
+
149
+ for (const [index, assetFile] of assetFiles.entries()) {
150
+ if (matchedFiles.has(assetFile)) {
151
+ continue
152
+ }
153
+
154
+ const normalizedName = safeNormalizeAssetUri(assetFile.name)
155
+ const basename = normalizedName.split('/').pop() || normalizedName
156
+ const bytes = await getFileBytes(assetFile)
157
+ assetData[`extra-${index}-${basename}`] = {
158
+ data: Array.from(bytes),
159
+ fileName: basename,
160
+ mimeType: assetFile.type || 'application/octet-stream'
161
+ }
162
+ }
163
+
164
+ return {
165
+ gltfJson,
166
+ assetData
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Custom hook to load and manage 3D models with optional optimization integration.
172
+ *
173
+ * This hook provides a complete solution for loading 3D models (GLTF, GLB, USDZ) with
174
+ * optional integration of model optimization capabilities. The return type is conditionally
175
+ * typed based on whether an optimizer is provided.
176
+ *
177
+ * **Type Safety:**
178
+ * - When called with an optimizer: `optimizer` property is fully typed with optimization methods
179
+ * - When called without an optimizer: `optimizer` property is typed as `null`
180
+ *
181
+ * @example
182
+ * // With optimizer integration
183
+ * const optimizer = useOptimizeModel()
184
+ * const model = useLoadModel(optimizer)
185
+ * model.optimizer.applyOptimization() // ✅ Fully typed
186
+ *
187
+ * @example
188
+ * // Without optimizer
189
+ * const model = useLoadModel()
190
+ * model.optimizer // ✅ Typed as null
191
+ *
192
+ * @template T - The type of the optimizer parameter (inferred automatically)
193
+ * @param optimizer - Optional optimizer hook returned from useOptimizeModel
194
+ * @returns Model loading state and methods, with conditionally typed optimizer property
195
+ */
196
+ function useLoadModel<
197
+ T extends ReturnType<typeof useOptimizeModel> | undefined
198
+ >(optimizer?: T): UseLoadModelReturn<T extends undefined ? false : true> {
199
+ const uploadCompleteRef = useRef(false)
200
+ const loadedFileRef = useRef<ModelFile | null>(null)
201
+ const [state, dispatch] = useReducer(reducer, initialState)
202
+
203
+ // Create ModelLoader instance with progress tracking
204
+ const modelLoader = useMemo(() => {
205
+ const loader = new ModelLoader()
206
+
207
+ // Set up progress callback to update state and emit events
208
+ loader.onProgress((progress: OperationProgress) => {
209
+ dispatch({ type: 'set-progress', payload: progress.progress })
210
+ // FInal load progress is set to 100% elsewhere
211
+ if (progress.progress === 100) return
212
+ eventSystem.emit('load-progress', progress.progress)
213
+ })
214
+
215
+ return loader
216
+ }, [])
217
+
218
+ /**
219
+ * Helper function to find a specific file type from an array of files.
220
+ * Used to identify model files (GLTF, GLB, USDZ) from uploaded file lists.
221
+ */
222
+ const getFileOfType = useCallback(
223
+ (files: File[], fileType: ModelFileTypes) =>
224
+ files.find((file) => file.name.endsWith('.' + fileType)),
225
+ []
226
+ )
227
+
228
+ /**
229
+ * Updates the loading progress state and emits a progress event.
230
+ * Called during file upload and model loading operations.
231
+ */
232
+ const updateProgress = useCallback((progress: number) => {
233
+ dispatch({ type: 'set-progress', payload: progress })
234
+ eventSystem.emit('load-progress', progress)
235
+ }, [])
236
+
237
+ /**
238
+ * Resets the model loading state to initial values.
239
+ * Clears any loaded models, progress, and emits a reset event.
240
+ */
241
+ const reset = useCallback(() => {
242
+ dispatch({ type: 'reset-state' })
243
+ eventSystem.emit('load-reset')
244
+ }, [])
245
+
246
+ /**
247
+ * Loads binary model files (GLB, USDZ) into Three.js scene.
248
+ * Handles errors and updates loading state.
249
+ */
250
+ const loadBinaryModel = useCallback(
251
+ async (file: File, fileType: ModelFileTypes) => {
252
+ try {
253
+ const result = await modelLoader.loadToThreeJS(file)
254
+ const loadedFile: ModelFile = {
255
+ model: result.scene,
256
+ type: fileType,
257
+ name: file.name
258
+ }
259
+
260
+ dispatch({
261
+ type: 'set-file',
262
+ payload: loadedFile
263
+ })
264
+ loadedFileRef.current = loadedFile
265
+
266
+ dispatch({ type: 'set-file-loading', payload: false })
267
+
268
+ if (optimizer) {
269
+ await optimizer.load(result.scene)
270
+ }
271
+ } catch (error) {
272
+ console.error('Error loading binary model:', error)
273
+ dispatch({ type: 'set-file-loading', payload: false })
274
+ eventSystem.emit('load-error', error)
275
+ }
276
+ },
277
+ [modelLoader, optimizer]
278
+ )
279
+
280
+ /**
281
+ * Loads GLTF models with their associated assets (textures, bins, etc.).
282
+ * Handles the more complex GLTF format with external resources.
283
+ */
284
+ const loadGltfModel = useCallback(
285
+ async (gltfFile: File, otherFiles: File[]) => {
286
+ try {
287
+ const { sourcePackageBytes, textureBytes } =
288
+ await calculateReferencedBytesFromFiles(gltfFile, otherFiles)
289
+
290
+ const result = await modelLoader.loadGLTFWithAssetsToThreeJS(
291
+ gltfFile,
292
+ otherFiles
293
+ )
294
+
295
+ const loadedFile: ModelFile = {
296
+ model: result.scene,
297
+ type: ModelFileTypes.gltf,
298
+ name: gltfFile.name,
299
+ sourcePackageBytes,
300
+ sourceTextureBytes: textureBytes
301
+ }
302
+
303
+ dispatch({
304
+ type: 'set-file',
305
+ payload: loadedFile
306
+ })
307
+
308
+ dispatch({ type: 'set-file-loading', payload: false })
309
+
310
+ // Update the ref so loadFromServer can access it
311
+ loadedFileRef.current = loadedFile
312
+
313
+ if (optimizer) {
314
+ try {
315
+ const localSceneData = await buildSceneDataFromLocalFiles(
316
+ gltfFile,
317
+ otherFiles
318
+ )
319
+ await optimizer.loadFromServerSceneData(localSceneData)
320
+ } catch (optimizerError) {
321
+ console.warn(
322
+ 'Failed to initialize optimizer from source GLTF payload; falling back to scene import.',
323
+ optimizerError
324
+ )
325
+ await optimizer.load(result.scene)
326
+ }
327
+ }
328
+ } catch (error) {
329
+ console.error('Error loading GLTF model:', error)
330
+ console.error('GLTF file:', gltfFile.name)
331
+ console.error(
332
+ 'Asset files:',
333
+ otherFiles.map((f) => f.name)
334
+ )
335
+ dispatch({ type: 'set-file-loading', payload: false })
336
+ eventSystem.emit('load-error', error)
337
+ }
338
+ },
339
+ [modelLoader, optimizer]
340
+ )
341
+
342
+ /**
343
+ * Processes an array of files to identify and load supported 3D model formats.
344
+ * Validates that only one model file is present and emits appropriate events.
345
+ *
346
+ * Supported formats: GLTF, GLB, USDZ
347
+ * Emits events for: multiple models found, no supported files found
348
+ */
349
+ const processFiles = useCallback(
350
+ async (files: File[]) => {
351
+ if (files.length === 0) return
352
+
353
+ // Identify model files by type
354
+ const gltfFile = getFileOfType(files, ModelFileTypes.gltf)
355
+ const glbFile = getFileOfType(files, ModelFileTypes.glb)
356
+ const usdzFile = getFileOfType(files, ModelFileTypes.usdz)
357
+
358
+ const supportedFiles = [gltfFile, glbFile, usdzFile].filter(
359
+ Boolean
360
+ ) as File[]
361
+
362
+ // Emit error if multiple model files are found
363
+ if (supportedFiles.length > 1) {
364
+ eventSystem.emit('multiple-models', supportedFiles)
365
+ return
366
+ }
367
+
368
+ // Separate model file from asset files (textures, bins, etc.)
369
+ const otherFiles = files.filter(
370
+ (file) => file !== gltfFile && file !== glbFile && file !== usdzFile
371
+ )
372
+
373
+ // Mark upload as complete (progress is already emitted by ModelLoader)
374
+ const markUploadComplete = () => {
375
+ uploadCompleteRef.current = true
376
+ if (loadedFileRef.current) {
377
+ eventSystem.emit('load-complete', loadedFileRef.current)
378
+ uploadCompleteRef.current = false
379
+ }
380
+ }
381
+
382
+ // Load the appropriate model type
383
+ if (gltfFile) {
384
+ await loadGltfModel(gltfFile, otherFiles)
385
+ markUploadComplete()
386
+ } else if (glbFile) {
387
+ await loadBinaryModel(glbFile, ModelFileTypes.glb)
388
+ markUploadComplete()
389
+ } else if (usdzFile) {
390
+ await loadBinaryModel(usdzFile, ModelFileTypes.usdz)
391
+ markUploadComplete()
392
+ } else {
393
+ // No supported model files found
394
+ eventSystem.emit('not-loaded-files', files)
395
+ dispatch({ type: 'set-file-loading', payload: false })
396
+ return
397
+ }
398
+ },
399
+ [getFileOfType, loadGltfModel, loadBinaryModel]
400
+ )
401
+
402
+ /**
403
+ * Main function to load 3D models from files or directories.
404
+ * Accepts both File objects and FileSystemDirectoryHandle for folder uploads.
405
+ * Resets state, processes all files, and integrates with optimizer if available.
406
+ *
407
+ * @param filesOrDirectories - Array of File objects or FileSystemDirectoryHandle objects
408
+ */
409
+ const load = useCallback(
410
+ async (filesOrDirectories: InputFileOrDirectory): Promise<void> => {
411
+ const allFiles: File[] = []
412
+
413
+ // Emit load start event and reset state
414
+ eventSystem.emit('load-start')
415
+ dispatch({ type: 'reset-state' })
416
+ dispatch({ type: 'set-file-loading', payload: true })
417
+
418
+ updateProgress(0)
419
+ uploadCompleteRef.current = false
420
+
421
+ // Process both files and directories
422
+ for (const item of filesOrDirectories) {
423
+ if (item instanceof File) {
424
+ allFiles.push(item)
425
+ } else if ('kind' in item && item.kind === 'directory') {
426
+ // Recursively read directory contents
427
+ const directoryFiles = await readDirectory(item)
428
+ allFiles.push(...directoryFiles)
429
+ }
430
+ }
431
+
432
+ await processFiles(allFiles)
433
+ },
434
+ [processFiles, updateProgress]
435
+ )
436
+
437
+ /**
438
+ * Load a scene from the server by scene ID.
439
+ * Fetches the GLTF JSON and asset data, reconstructs files, then loads them.
440
+ *
441
+ * This function:
442
+ * 1. Emits 'server-load-start' event with the sceneId
443
+ * 2. Fetches scene data from the configured endpoint
444
+ * 3. Reconstructs GLTF and asset files from the server data
445
+ * 4. Loads the files into Three.js directly (bypassing processFiles to avoid double-loading)
446
+ * 5. Emits 'server-load-complete' event with the full result
447
+ *
448
+ * @param options - Scene loading configuration
449
+ * @returns Promise resolving to the loaded scene data with settings
450
+ */
451
+ const loadFromData = useCallback(
452
+ async (options: SceneDataLoadOptions): Promise<SceneLoadResult> => {
453
+ const { sceneData, sceneId } = options
454
+
455
+ // Update progress to 40% after data resolution
456
+ updateProgress(40)
457
+
458
+ // Reconstruct GLTF and asset files from scene data
459
+ const files = reconstructGltfFiles(sceneData)
460
+ const { sourcePackageBytes, textureBytes } =
461
+ calculateReferencedBytesFromServerScene(sceneData)
462
+
463
+ // Update progress to 60% after reconstruction
464
+ updateProgress(60)
465
+
466
+ const gltfFile = files[0] as File
467
+ const otherFiles = files.slice(1) as File[]
468
+
469
+ const result = await modelLoader.loadGLTFWithAssetsToThreeJS(
470
+ gltfFile,
471
+ otherFiles
472
+ )
473
+
474
+ const loadedFile: ModelFile = {
475
+ model: result.scene,
476
+ type: ModelFileTypes.gltf,
477
+ name: gltfFile.name,
478
+ sourcePackageBytes,
479
+ sourceTextureBytes: textureBytes
480
+ }
481
+
482
+ dispatch({
483
+ type: 'set-file',
484
+ payload: loadedFile
485
+ })
486
+
487
+ dispatch({ type: 'set-file-loading', payload: false })
488
+ loadedFileRef.current = loadedFile
489
+
490
+ if (optimizer) {
491
+ await optimizer.loadFromServerSceneData(sceneData)
492
+ }
493
+
494
+ updateProgress(100)
495
+
496
+ return {
497
+ file: loadedFile,
498
+ sceneId,
499
+ ...sceneData
500
+ }
501
+ },
502
+ [modelLoader, optimizer, updateProgress]
503
+ )
504
+
505
+ const loadFromServer = useCallback(
506
+ async (options: SceneLoadOptions): Promise<SceneLoadResult> => {
507
+ const { sceneId, serverOptions } = options
508
+
509
+ try {
510
+ // Emit server load start event
511
+ eventSystem.emit('server-load-start', sceneId)
512
+
513
+ // Reset state and set loading
514
+ dispatch({ type: 'reset-state' })
515
+ dispatch({ type: 'set-file-loading', payload: true })
516
+ updateProgress(0)
517
+
518
+ const scenePayload =
519
+ await ServerCommunicationService.loadScene<ServerScenePayload>(
520
+ sceneId,
521
+ serverOptions
522
+ )
523
+
524
+ const sceneData = resolveServerSceneDataContract(scenePayload)
525
+
526
+ const sceneLoadResult = await loadFromData({
527
+ sceneId,
528
+ sceneData
529
+ })
530
+
531
+ // Emit server load complete event
532
+ eventSystem.emit('server-load-complete', sceneLoadResult)
533
+
534
+ return sceneLoadResult
535
+ } catch (error) {
536
+ console.error('Server scene loading failed:', error)
537
+ dispatch({ type: 'set-file-loading', payload: false })
538
+
539
+ // Emit error event
540
+ eventSystem.emit('server-load-error', error)
541
+
542
+ // Re-throw for caller to handle
543
+ throw error
544
+ }
545
+ },
546
+ [loadFromData, updateProgress]
547
+ // Note: processFiles removed, we load directly to avoid uploadCompleteRef trigger
548
+ )
549
+
550
+ /**
551
+ * Creates the optimizer integration object.
552
+ * This provides additional methods for applying optimizations and
553
+ * updating the loaded model with optimized versions.
554
+ *
555
+ * Returns null if no optimizer is provided, otherwise returns
556
+ * the full optimizer interface with applyOptimization method.
557
+ */
558
+ const optimizerIntegration = useOptimizerIntegration(
559
+ optimizer,
560
+ dispatch,
561
+ state.file,
562
+ modelLoader
563
+ )
564
+
565
+ return {
566
+ ...state,
567
+ on: eventSystem.on,
568
+ off: eventSystem.off,
569
+ load,
570
+ loadFromData,
571
+ loadFromServer,
572
+ reset,
573
+ optimizer: optimizerIntegration
574
+ } as UseLoadModelReturn<T extends undefined ? false : true>
575
+ }
576
+
577
+ /**
578
+ * Hook to integrate the optimizer into the model loading process.
579
+ *
580
+ * @param instance - The optimizer instance.
581
+ * @param dispatch - The dispatch function from useReducer.
582
+ * @param file - The current model file.
583
+ * @param modelLoader - The ModelLoader instance.
584
+ * @returns An object containing optimization functions and optimizer states.
585
+ */
586
+ function useOptimizerIntegration(
587
+ instance: ReturnType<typeof useOptimizeModel> | undefined,
588
+ dispatch: React.Dispatch<Action>,
589
+ file: ModelFile | null,
590
+ modelLoader: ModelLoader
591
+ ): OptimizerIntegrationReturn<boolean> {
592
+ /**
593
+ * Applies an optimization to the current model and loads the result.
594
+ *
595
+ * This function:
596
+ * 1. Runs the provided optimization function (e.g., simplify, compress)
597
+ * 2. Retrieves the optimized model as a binary file
598
+ * 3. Loads the optimized binary into a Three.js scene
599
+ * 4. Updates the model state with the optimized version
600
+ *
601
+ * @template TOptions - The type of options for the optimization function
602
+ * @param optimizationFunction - Optional function to run optimization (e.g., instance.simplifyOptimization)
603
+ * @param options - Optional configuration for the optimization function
604
+ */
605
+ const applyOptimization = useCallback(
606
+ async <TOptions>(
607
+ optimizationFunction?:
608
+ | ((options?: TOptions) => Promise<void>)
609
+ | undefined,
610
+ options?: TOptions
611
+ ) => {
612
+ if (!instance) {
613
+ console.warn('Optimizer is not available')
614
+ return
615
+ }
616
+
617
+ try {
618
+ // Step 1: Apply the optimization with optional parameters
619
+ if (optimizationFunction) await optimizationFunction(options)
620
+
621
+ // Step 2: Get the optimized model as a binary file
622
+ const optimizedModel = await instance.getModel()
623
+ if (!optimizedModel) {
624
+ console.warn('No optimized model available after optimization')
625
+ return
626
+ }
627
+
628
+ // Step 3: Create a File object from the optimized binary data
629
+ // Ensure we pass an ArrayBufferView (Uint8Array) backed by a real ArrayBuffer
630
+ // to the File constructor to satisfy BlobPart typing and avoid SharedArrayBuffer issues.
631
+ // We create a copy using .slice() which returns a Uint8Array backed by a standard ArrayBuffer.
632
+ const optimizedBlobPart =
633
+ optimizedModel instanceof Uint8Array
634
+ ? optimizedModel.slice()
635
+ : new Uint8Array(optimizedModel as ArrayBufferLike).slice()
636
+
637
+ const optimizedFile = new File(
638
+ [optimizedBlobPart],
639
+ file?.name || 'optimized_model.glb',
640
+ {
641
+ type: 'model/gltf-binary'
642
+ }
643
+ )
644
+
645
+ // Step 4: Load the optimized file into Three.js
646
+ const result = await modelLoader.loadToThreeJS(optimizedFile)
647
+
648
+ // Step 5: Update the state with the optimized model
649
+ dispatch({
650
+ type: 'set-file',
651
+ payload: {
652
+ model: result.scene,
653
+ type: ModelFileTypes.glb,
654
+ name: optimizedFile.name
655
+ }
656
+ })
657
+ } catch (error) {
658
+ console.error('Optimization failed:', error)
659
+ // Error is logged, state remains unchanged
660
+ }
661
+ },
662
+ [instance, modelLoader, file, dispatch]
663
+ )
664
+
665
+ // Return null if no optimizer instance is provided
666
+ if (!instance) return null as OptimizerIntegrationReturn<boolean>
667
+
668
+ const isReady = instance.isReady
669
+ const isPreparing = Boolean(file?.model) && !isReady
670
+
671
+ // Return the full optimizer interface with the applyOptimization method
672
+ return {
673
+ ...instance,
674
+ isReady,
675
+ isPreparing,
676
+ applyOptimization,
677
+ reset: instance?.reset,
678
+ error: instance?.error,
679
+ loading: instance?.loading
680
+ } as OptimizerIntegrationReturn<boolean>
681
+ }
682
+ export default useLoadModel