@vctrl/hooks 0.16.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/GLTFLoader-CygFefRr.cjs +1 -0
- package/GLTFLoader-NiJ7qzh1.js +1381 -0
- package/README.md +0 -20
- package/__vite-browser-external-Bn_csQuM.js +7 -0
- package/__vite-browser-external-CVmj8TrW.cjs +1 -0
- package/chunk-foFoljWS.js +23 -0
- package/chunk-gGpyby2o.cjs +1 -0
- package/index.cjs.js +1 -0
- package/index.d.ts +5 -0
- package/index.es.js +4 -0
- package/lib-DKn2MJ9X.js +4111 -0
- package/lib-DZ-MihdJ.cjs +2 -0
- package/model-exporter-SMSAY90N.cjs +1 -0
- package/model-exporter-cvguDGnn.js +6512 -0
- package/package.json +1 -1
- package/server-communication-Bj6ZxFw2.js +4659 -0
- package/server-communication-yXZR8eso.cjs +18 -0
- package/use-export-model/use-export-model.d.ts +14 -0
- package/use-export-model-CaijtW1c.cjs +1 -0
- package/use-export-model-DzVKdqnY.js +49 -0
- package/use-export-model.cjs.js +1 -0
- package/use-export-model.es.js +2 -0
- package/use-load-model/event-system.d.ts +7 -0
- package/use-load-model/index.d.ts +4 -0
- package/use-load-model/state.d.ts +19 -0
- package/use-load-model/types.d.ts +231 -0
- package/use-load-model/use-load-model.d.ts +30 -0
- package/use-load-model/utils/calculate-referenced-bytes.d.ts +8 -0
- package/use-load-model/utils/index.d.ts +4 -0
- package/use-load-model/utils/read-directory.d.ts +8 -0
- package/use-load-model/utils/reconstruct-files.d.ts +29 -0
- package/use-load-model/utils/resolve-scene-payload.d.ts +6 -0
- package/use-load-model-B6UXiFWh.js +517 -0
- package/use-load-model-CH-WPQ_s.cjs +1 -0
- package/use-load-model.cjs.js +1 -0
- package/use-load-model.es.js +18 -0
- package/{src/use-optimize-model/index.ts → use-optimize-model/index.d.ts} +2 -2
- package/use-optimize-model/state.d.ts +13 -0
- package/use-optimize-model/types.d.ts +39 -0
- package/use-optimize-model/use-calc-optimization-info.d.ts +6 -0
- package/use-optimize-model/use-optimize-model.d.ts +119 -0
- package/use-optimize-model/utils/index.d.ts +2 -0
- package/use-optimize-model/utils/texture-optimization.d.ts +17 -0
- package/use-optimize-model/utils/validation.d.ts +8 -0
- package/use-optimize-model-C-fzzKh6.cjs +1 -0
- package/use-optimize-model-Cb3WMvDZ.js +299 -0
- package/use-optimize-model.cjs.js +1 -0
- package/use-optimize-model.es.js +2 -0
- package/utils/server-communication.d.ts +122 -0
- package/project.json +0 -55
- package/src/index.ts +0 -38
- package/src/use-export-model/use-export-model.ts +0 -113
- package/src/use-load-model/event-system.ts +0 -46
- package/src/use-load-model/index.ts +0 -15
- package/src/use-load-model/model-context.tsx +0 -174
- package/src/use-load-model/state.ts +0 -75
- package/src/use-load-model/types.ts +0 -284
- package/src/use-load-model/use-load-model.ts +0 -682
- package/src/use-load-model/utils/calculate-referenced-bytes.ts +0 -123
- package/src/use-load-model/utils/index.ts +0 -7
- package/src/use-load-model/utils/read-directory.ts +0 -35
- package/src/use-load-model/utils/reconstruct-files.ts +0 -85
- package/src/use-load-model/utils/resolve-scene-payload.ts +0 -151
- package/src/use-optimize-model/state.ts +0 -40
- package/src/use-optimize-model/types.ts +0 -37
- package/src/use-optimize-model/use-calc-optimization-info.ts +0 -101
- package/src/use-optimize-model/use-optimize-model.ts +0 -467
- package/src/use-optimize-model/utils/index.ts +0 -29
- package/src/use-optimize-model/utils/texture-optimization.ts +0 -292
- package/src/use-optimize-model/utils/validation.ts +0 -62
- package/src/utils/server-communication.ts +0 -351
- package/tsconfig.json +0 -21
- package/tsconfig.lib.json +0 -16
- package/vite.config.ts +0 -77
- /package/{src/use-export-model/index.ts → use-export-model/index.d.ts} +0 -0
|
@@ -1,682 +0,0 @@
|
|
|
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
|