@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.
- package/CHANGELOG.md +210 -0
- package/README.md +202 -383
- package/package.json +66 -55
- package/project.json +55 -0
- package/src/index.ts +38 -0
- package/src/use-export-model/use-export-model.ts +113 -0
- package/src/use-load-model/event-system.ts +46 -0
- package/src/use-load-model/index.ts +15 -0
- package/src/use-load-model/model-context.tsx +174 -0
- package/src/use-load-model/state.ts +75 -0
- package/src/use-load-model/types.ts +284 -0
- package/src/use-load-model/use-load-model.ts +682 -0
- package/src/use-load-model/utils/calculate-referenced-bytes.ts +123 -0
- package/src/use-load-model/utils/index.ts +7 -0
- package/src/use-load-model/utils/read-directory.ts +35 -0
- package/src/use-load-model/utils/reconstruct-files.ts +85 -0
- package/src/use-load-model/utils/resolve-scene-payload.ts +151 -0
- package/{use-optimize-model/index.d.ts → src/use-optimize-model/index.ts} +2 -1
- package/src/use-optimize-model/state.ts +40 -0
- package/src/use-optimize-model/types.ts +37 -0
- package/src/use-optimize-model/use-calc-optimization-info.ts +97 -0
- package/src/use-optimize-model/use-optimize-model.ts +467 -0
- package/src/use-optimize-model/utils/index.ts +29 -0
- package/src/use-optimize-model/utils/texture-optimization.ts +292 -0
- package/src/use-optimize-model/utils/validation.ts +62 -0
- package/src/utils/server-communication.ts +351 -0
- package/tsconfig.json +21 -0
- package/tsconfig.lib.json +16 -0
- package/vite.config.ts +77 -0
- package/GLTFExporter-BmLF430n.cjs +0 -18
- package/GLTFExporter-C4hCeb4H.js +0 -1263
- package/index.cjs.js +0 -1
- package/index.d.ts +0 -3
- package/index.es.js +0 -12
- package/model-context-CQJta_vi.cjs +0 -9
- package/model-context-D1Q8WdpT.js +0 -3431
- package/use-export-model/types.d.ts +0 -19
- package/use-export-model/use-export-model.d.ts +0 -14
- package/use-export-model/utils/data-uri-to-blob.d.ts +0 -9
- package/use-export-model/utils/export-handlers.d.ts +0 -23
- package/use-export-model/utils/file-helpers.d.ts +0 -17
- package/use-export-model/utils/index.d.ts +0 -3
- package/use-export-model-AFharGg5.js +0 -77
- package/use-export-model-CWcKp4A3.cjs +0 -1
- package/use-export-model.cjs.js +0 -1
- package/use-export-model.es.js +0 -4
- package/use-load-model/event-system.d.ts +0 -7
- package/use-load-model/file-type-hooks/index.d.ts +0 -2
- package/use-load-model/file-type-hooks/use-load-binary.d.ts +0 -11
- package/use-load-model/file-type-hooks/use-load-gltf.d.ts +0 -5
- package/use-load-model/index.d.ts +0 -3
- package/use-load-model/loaders/create-gltf-loader.d.ts +0 -3
- package/use-load-model/loaders/create-usdz-loader.d.ts +0 -3
- package/use-load-model/loaders/index.d.ts +0 -2
- package/use-load-model/model-context.d.ts +0 -78
- package/use-load-model/state.d.ts +0 -19
- package/use-load-model/types.d.ts +0 -51
- package/use-load-model/use-load-model.d.ts +0 -74
- package/use-load-model/utils/array-buffer-to-base64.d.ts +0 -2
- package/use-load-model/utils/index.d.ts +0 -2
- package/use-load-model/utils/read-directory.d.ts +0 -8
- package/use-load-model.cjs.js +0 -1
- package/use-load-model.es.js +0 -8
- package/use-optimize-model/state.d.ts +0 -13
- package/use-optimize-model/types.d.ts +0 -37
- package/use-optimize-model/use-optimize-model.d.ts +0 -29
- package/use-optimize-model-Cqkp__Vi.cjs +0 -1
- package/use-optimize-model-DZWIb_40.js +0 -127
- package/use-optimize-model.cjs.js +0 -1
- package/use-optimize-model.es.js +0 -4
- /package/{use-export-model/index.d.ts → src/use-export-model/index.ts} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAssetLookupKeys,
|
|
3
|
+
getSerializedAssetByteSize,
|
|
4
|
+
normalizeAssetUri
|
|
5
|
+
} from '@vctrl/core'
|
|
6
|
+
|
|
7
|
+
import type { ServerSceneData } from '../types'
|
|
8
|
+
|
|
9
|
+
type ReferencedBytes = {
|
|
10
|
+
sourcePackageBytes: number
|
|
11
|
+
textureBytes: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const resolveSizeFromAssets = (
|
|
15
|
+
uri: string,
|
|
16
|
+
assets: Array<{ fileName: string; size: number }>
|
|
17
|
+
) => {
|
|
18
|
+
const normalizedUri = normalizeAssetUri(uri)
|
|
19
|
+
const uriBasename = normalizedUri.split('/').pop() || normalizedUri
|
|
20
|
+
|
|
21
|
+
for (const asset of assets) {
|
|
22
|
+
const assetKeys = buildAssetLookupKeys(asset.fileName)
|
|
23
|
+
if (assetKeys.has(normalizedUri) || assetKeys.has(uriBasename)) {
|
|
24
|
+
return asset.size
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const collectReferencedUris = (gltfJson: unknown) => {
|
|
32
|
+
const bufferUris = new Set<string>()
|
|
33
|
+
const imageUris = new Set<string>()
|
|
34
|
+
const document = gltfJson as {
|
|
35
|
+
images?: Array<{ uri?: string }>
|
|
36
|
+
buffers?: Array<{ uri?: string }>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const images = Array.isArray(document.images)
|
|
40
|
+
? (document.images as Array<{ uri?: string }>)
|
|
41
|
+
: []
|
|
42
|
+
for (const image of images) {
|
|
43
|
+
if (typeof image.uri === 'string' && !image.uri.startsWith('data:')) {
|
|
44
|
+
imageUris.add(image.uri)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const buffers = Array.isArray(document.buffers)
|
|
49
|
+
? (document.buffers as Array<{ uri?: string }>)
|
|
50
|
+
: []
|
|
51
|
+
for (const buffer of buffers) {
|
|
52
|
+
if (typeof buffer.uri === 'string' && !buffer.uri.startsWith('data:')) {
|
|
53
|
+
bufferUris.add(buffer.uri)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { bufferUris, imageUris }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function calculateReferencedBytesFromFiles(
|
|
61
|
+
gltfFile: File,
|
|
62
|
+
assetFiles: File[]
|
|
63
|
+
): Promise<ReferencedBytes> {
|
|
64
|
+
const gltfText = await gltfFile.text()
|
|
65
|
+
let gltfJson: Record<string, unknown>
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
gltfJson = JSON.parse(gltfText) as Record<string, unknown>
|
|
69
|
+
} catch {
|
|
70
|
+
return {
|
|
71
|
+
sourcePackageBytes: gltfFile.size,
|
|
72
|
+
textureBytes: 0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const assets = assetFiles.map((file) => ({
|
|
77
|
+
fileName: file.name,
|
|
78
|
+
size: file.size
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
const { bufferUris, imageUris } = collectReferencedUris(gltfJson)
|
|
82
|
+
|
|
83
|
+
const bufferBytes = Array.from(bufferUris).reduce(
|
|
84
|
+
(total, uri) => total + resolveSizeFromAssets(uri, assets),
|
|
85
|
+
0
|
|
86
|
+
)
|
|
87
|
+
const textureBytes = Array.from(imageUris).reduce(
|
|
88
|
+
(total, uri) => total + resolveSizeFromAssets(uri, assets),
|
|
89
|
+
0
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sourcePackageBytes: gltfFile.size + bufferBytes + textureBytes,
|
|
94
|
+
textureBytes
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function calculateReferencedBytesFromServerScene(
|
|
99
|
+
data: ServerSceneData
|
|
100
|
+
): ReferencedBytes {
|
|
101
|
+
const gltfJson = data.gltfJson
|
|
102
|
+
const assets = Object.values(data.assetData || {}).map((asset) => ({
|
|
103
|
+
fileName: asset.fileName,
|
|
104
|
+
size: getSerializedAssetByteSize(asset.data)
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
const gltfSize = new TextEncoder().encode(JSON.stringify(gltfJson)).byteLength
|
|
108
|
+
const { bufferUris, imageUris } = collectReferencedUris(gltfJson)
|
|
109
|
+
|
|
110
|
+
const bufferBytes = Array.from(bufferUris).reduce(
|
|
111
|
+
(total, uri) => total + resolveSizeFromAssets(uri, assets),
|
|
112
|
+
0
|
|
113
|
+
)
|
|
114
|
+
const textureBytes = Array.from(imageUris).reduce(
|
|
115
|
+
(total, uri) => total + resolveSizeFromAssets(uri, assets),
|
|
116
|
+
0
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
sourcePackageBytes: gltfSize + bufferBytes + textureBytes,
|
|
121
|
+
textureBytes
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as readDirectory } from './read-directory'
|
|
2
|
+
export { reconstructGltfFiles } from './reconstruct-files'
|
|
3
|
+
export {
|
|
4
|
+
calculateReferencedBytesFromFiles,
|
|
5
|
+
calculateReferencedBytesFromServerScene
|
|
6
|
+
} from './calculate-referenced-bytes'
|
|
7
|
+
export { resolveServerSceneDataContract } from './resolve-scene-payload'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively reads a directory
|
|
3
|
+
*
|
|
4
|
+
* @param directoryHandle - The directory handle to read
|
|
5
|
+
* @returns - An array of file objects
|
|
6
|
+
*/
|
|
7
|
+
async function readDirectory(
|
|
8
|
+
directoryHandle: FileSystemDirectoryHandle
|
|
9
|
+
): Promise<File[]> {
|
|
10
|
+
const files: File[] = []
|
|
11
|
+
|
|
12
|
+
async function* getFilesRecursively(
|
|
13
|
+
entry: FileSystemDirectoryHandle
|
|
14
|
+
): AsyncGenerator<File> {
|
|
15
|
+
const asyncEntries = entry as unknown as AsyncIterable<
|
|
16
|
+
[string, FileSystemHandle]
|
|
17
|
+
>
|
|
18
|
+
|
|
19
|
+
for await (const [, handle] of asyncEntries) {
|
|
20
|
+
if (handle.kind === 'file') {
|
|
21
|
+
yield await (handle as FileSystemFileHandle).getFile()
|
|
22
|
+
} else if (handle.kind === 'directory') {
|
|
23
|
+
yield* getFilesRecursively(handle as FileSystemDirectoryHandle)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for await (const file of getFilesRecursively(directoryHandle)) {
|
|
29
|
+
files.push(file)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return files
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default readDirectory
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { toSerializedAssetBytes } from '@vctrl/core'
|
|
18
|
+
|
|
19
|
+
import type { InputFileOrDirectory, ServerSceneData } from '../types'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reconstructs GLTF and asset files from scene data received from the server.
|
|
23
|
+
*
|
|
24
|
+
* This function converts the server's scene data format (GLTF JSON + binary assets)
|
|
25
|
+
* back into File objects that can be loaded by the model loader. This is the reverse
|
|
26
|
+
* operation of what happens when a scene is saved to the server.
|
|
27
|
+
*
|
|
28
|
+
* The function:
|
|
29
|
+
* 1. Converts binary asset data (stored as number arrays) back into File objects
|
|
30
|
+
* 2. Creates a GLTF file from the JSON structure
|
|
31
|
+
* 3. Returns an array of files compatible with the model loading system
|
|
32
|
+
*
|
|
33
|
+
* @param data - Scene data from the server containing GLTF JSON and assets
|
|
34
|
+
* @returns Array of File objects ready to be loaded (GLTF file + asset files)
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Load scene data from server
|
|
39
|
+
* const sceneData = await ServerCommunicationService.loadScene('scene-123')
|
|
40
|
+
*
|
|
41
|
+
* // Reconstruct files
|
|
42
|
+
* const files = reconstructGltfFiles(sceneData)
|
|
43
|
+
*
|
|
44
|
+
* // Load into the viewer
|
|
45
|
+
* await modelLoader.load(files)
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function reconstructGltfFiles(
|
|
49
|
+
data: ServerSceneData
|
|
50
|
+
): InputFileOrDirectory {
|
|
51
|
+
const assetFiles: File[] = []
|
|
52
|
+
|
|
53
|
+
// Convert asset data back to File objects
|
|
54
|
+
if (data.assetData && typeof data.assetData === 'object') {
|
|
55
|
+
for (const [, assetInfo] of Object.entries(data.assetData)) {
|
|
56
|
+
const { fileName, mimeType } = assetInfo
|
|
57
|
+
const uint8Array = toSerializedAssetBytes(assetInfo)
|
|
58
|
+
|
|
59
|
+
// Create a Blob from the binary data
|
|
60
|
+
const blobBytes = new Uint8Array(uint8Array.byteLength)
|
|
61
|
+
blobBytes.set(uint8Array)
|
|
62
|
+
const blob = new Blob([blobBytes], { type: mimeType })
|
|
63
|
+
|
|
64
|
+
// Create a File object
|
|
65
|
+
const file = new File([blob], fileName, { type: mimeType })
|
|
66
|
+
|
|
67
|
+
assetFiles.push(file)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create GLTF file from JSON structure
|
|
72
|
+
const gltfJsonString = JSON.stringify(data.gltfJson)
|
|
73
|
+
const gltfBlob = new Blob([gltfJsonString], { type: 'model/gltf+json' })
|
|
74
|
+
|
|
75
|
+
// Determine filename from metadata or use default
|
|
76
|
+
const sceneName = data.meta?.name
|
|
77
|
+
const gltfFileName = sceneName ? `${sceneName}.gltf` : 'scene.gltf'
|
|
78
|
+
|
|
79
|
+
const gltfFile = new File([gltfBlob], gltfFileName, {
|
|
80
|
+
type: 'model/gltf+json'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Return GLTF file first, followed by all asset files
|
|
84
|
+
return [gltfFile, ...assetFiles]
|
|
85
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAssetLookupKeys,
|
|
3
|
+
isValidBase64,
|
|
4
|
+
normalizeAssetUri
|
|
5
|
+
} from '@vctrl/core'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
SceneSettings,
|
|
9
|
+
SerializedSceneAssetDataMap,
|
|
10
|
+
ServerSceneData,
|
|
11
|
+
ServerScenePayload
|
|
12
|
+
} from '@vctrl/core'
|
|
13
|
+
|
|
14
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toSceneSettings(payload: ServerScenePayload): SceneSettings {
|
|
19
|
+
const nestedSettings = isRecord(payload.settings)
|
|
20
|
+
? (payload.settings as SceneSettings)
|
|
21
|
+
: {}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...nestedSettings,
|
|
25
|
+
bounds: payload.bounds ?? nestedSettings.bounds,
|
|
26
|
+
camera: payload.camera ?? nestedSettings.camera,
|
|
27
|
+
controls: payload.controls ?? nestedSettings.controls,
|
|
28
|
+
environment: payload.environment ?? nestedSettings.environment,
|
|
29
|
+
shadows: payload.shadows ?? nestedSettings.shadows
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectReferencedUris(gltfJson: unknown): Set<string> {
|
|
34
|
+
const referencedUris = new Set<string>()
|
|
35
|
+
const document = gltfJson as {
|
|
36
|
+
images?: Array<{ uri?: string }>
|
|
37
|
+
buffers?: Array<{ uri?: string }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const images = Array.isArray(document.images)
|
|
41
|
+
? (document.images as Array<{ uri?: string }>)
|
|
42
|
+
: []
|
|
43
|
+
for (const image of images) {
|
|
44
|
+
if (typeof image.uri === 'string' && !image.uri.startsWith('data:')) {
|
|
45
|
+
referencedUris.add(image.uri)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const buffers = Array.isArray(document.buffers)
|
|
50
|
+
? (document.buffers as Array<{ uri?: string }>)
|
|
51
|
+
: []
|
|
52
|
+
for (const buffer of buffers) {
|
|
53
|
+
if (typeof buffer.uri === 'string' && !buffer.uri.startsWith('data:')) {
|
|
54
|
+
referencedUris.add(buffer.uri)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return referencedUris
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateAssetDataMap(assetData: SerializedSceneAssetDataMap): void {
|
|
62
|
+
for (const [assetId, asset] of Object.entries(assetData)) {
|
|
63
|
+
if (!asset || typeof asset !== 'object') {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Scene payload contains invalid asset entry for ${assetId}`
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
typeof asset.fileName !== 'string' ||
|
|
71
|
+
asset.fileName.trim().length === 0
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Scene payload asset ${assetId} is missing a valid fileName`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
typeof asset.mimeType !== 'string' ||
|
|
80
|
+
asset.mimeType.trim().length === 0
|
|
81
|
+
) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Scene payload asset ${assetId} is missing a valid mimeType`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof asset.data === 'string' && asset.encoding === 'base64') {
|
|
88
|
+
if (!isValidBase64(asset.data)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Scene payload asset ${asset.fileName} has invalid base64 data`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateReferencedAssets(
|
|
98
|
+
gltfJson: ServerSceneData['gltfJson'],
|
|
99
|
+
assetData: SerializedSceneAssetDataMap
|
|
100
|
+
): void {
|
|
101
|
+
const referencedUris = collectReferencedUris(gltfJson)
|
|
102
|
+
|
|
103
|
+
if (referencedUris.size === 0) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lookup = new Set<string>()
|
|
108
|
+
for (const asset of Object.values(assetData)) {
|
|
109
|
+
for (const key of buildAssetLookupKeys(asset.fileName)) {
|
|
110
|
+
lookup.add(key)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const missingUris: string[] = []
|
|
115
|
+
for (const uri of referencedUris) {
|
|
116
|
+
const normalized = normalizeAssetUri(uri)
|
|
117
|
+
const basename = normalized.split('/').pop() || normalized
|
|
118
|
+
if (!lookup.has(uri) && !lookup.has(normalized) && !lookup.has(basename)) {
|
|
119
|
+
missingUris.push(uri)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (missingUris.length > 0) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Scene payload is missing required referenced assets: ${missingUris.slice(0, 5).join(', ')}`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolves raw scene payload from the API into normalized scene data used by hooks.
|
|
132
|
+
* This keeps payload-shape adaptation in one explicit contract boundary.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveServerSceneDataContract(
|
|
135
|
+
payload: ServerScenePayload
|
|
136
|
+
): ServerSceneData {
|
|
137
|
+
if (!isRecord(payload.gltfJson)) {
|
|
138
|
+
throw new Error('Scene payload is missing a valid glTF document')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const assetData = payload.assetData ?? {}
|
|
142
|
+
validateAssetDataMap(assetData)
|
|
143
|
+
validateReferencedAssets(payload.gltfJson, assetData)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
meta: payload.meta,
|
|
147
|
+
gltfJson: payload.gltfJson,
|
|
148
|
+
assetData,
|
|
149
|
+
...toSceneSettings(payload)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { default as useOptimizeModel } from './use-optimize-model'
|
|
1
|
+
export { default as useOptimizeModel } from './use-optimize-model'
|
|
2
|
+
export * from './types'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Action, OptimizationState } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initial state for the reducer.
|
|
5
|
+
*/
|
|
6
|
+
export const initialState: OptimizationState = {
|
|
7
|
+
info: null,
|
|
8
|
+
error: null,
|
|
9
|
+
loading: false,
|
|
10
|
+
report: null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reducer function to manage state transitions.
|
|
15
|
+
*
|
|
16
|
+
* @param state - Current state.
|
|
17
|
+
* @param action - Action to perform.
|
|
18
|
+
* @returns New state after applying the action.
|
|
19
|
+
*/
|
|
20
|
+
export const reducer = (
|
|
21
|
+
state: OptimizationState,
|
|
22
|
+
action: Action
|
|
23
|
+
): OptimizationState => {
|
|
24
|
+
switch (action.type) {
|
|
25
|
+
case 'LOAD_START':
|
|
26
|
+
return { ...state, loading: true, error: null }
|
|
27
|
+
case 'LOAD_SUCCESS':
|
|
28
|
+
return {
|
|
29
|
+
...state,
|
|
30
|
+
loading: false,
|
|
31
|
+
report: action.payload.report
|
|
32
|
+
}
|
|
33
|
+
case 'LOAD_ERROR':
|
|
34
|
+
return { ...state, loading: false, error: action.payload }
|
|
35
|
+
case 'RESET':
|
|
36
|
+
return { ...initialState }
|
|
37
|
+
default:
|
|
38
|
+
return state
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { OptimizationReport } from '@vctrl/core/model-optimizer'
|
|
2
|
+
|
|
3
|
+
interface ModelTotals {
|
|
4
|
+
verticesCount: number
|
|
5
|
+
primitivesCount: number
|
|
6
|
+
texturesCount: number
|
|
7
|
+
meshesCount: number
|
|
8
|
+
sceneBytes: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OptimizationInfo {
|
|
12
|
+
initial: ModelTotals
|
|
13
|
+
optimized: ModelTotals
|
|
14
|
+
improvement: ModelTotals
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Interface representing the state of the model optimizer.
|
|
19
|
+
*/
|
|
20
|
+
export interface OptimizationState {
|
|
21
|
+
report: OptimizationReport | null
|
|
22
|
+
info: OptimizationInfo | null
|
|
23
|
+
error: Error | null
|
|
24
|
+
loading: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Types of actions for the reducer.
|
|
29
|
+
*/
|
|
30
|
+
export type Action =
|
|
31
|
+
| { type: 'LOAD_START' }
|
|
32
|
+
| {
|
|
33
|
+
type: 'LOAD_SUCCESS'
|
|
34
|
+
payload: { report: OptimizationReport }
|
|
35
|
+
}
|
|
36
|
+
| { type: 'LOAD_ERROR'; payload: Error }
|
|
37
|
+
| { type: 'RESET' }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { OptimizationInfo, OptimizationState } from './types'
|
|
4
|
+
|
|
5
|
+
import type { OptimizationReport } from '@vctrl/core/model-optimizer'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper function to safely get stats from optimization report.
|
|
9
|
+
*/
|
|
10
|
+
function getStatsFromReport(report: OptimizationReport | null): {
|
|
11
|
+
vertices: number
|
|
12
|
+
primitives: number
|
|
13
|
+
meshes: number
|
|
14
|
+
textures: number
|
|
15
|
+
materials: number
|
|
16
|
+
totalSize: number
|
|
17
|
+
} {
|
|
18
|
+
if (!report) {
|
|
19
|
+
return {
|
|
20
|
+
vertices: 0,
|
|
21
|
+
primitives: 0,
|
|
22
|
+
meshes: 0,
|
|
23
|
+
textures: 0,
|
|
24
|
+
materials: 0,
|
|
25
|
+
totalSize: 0
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
vertices: report.stats.vertices.after || 0,
|
|
31
|
+
primitives: report.stats.triangles.after || 0,
|
|
32
|
+
meshes: report.stats.meshes.after || 0,
|
|
33
|
+
textures: report.stats.textures.after || 0,
|
|
34
|
+
materials: report.stats.materials.after || 0,
|
|
35
|
+
totalSize: report.optimizedSize || 0
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OptimizationInfoData {
|
|
40
|
+
info: OptimizationInfo
|
|
41
|
+
reset: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const useCalcOptimizationInfo = (
|
|
45
|
+
state: OptimizationState
|
|
46
|
+
): OptimizationInfoData => {
|
|
47
|
+
const initialReport = useRef<OptimizationReport | null>(null)
|
|
48
|
+
const [initialCaptured, setInitialCaptured] = useState<boolean>(false)
|
|
49
|
+
const report = state.report
|
|
50
|
+
|
|
51
|
+
// Capture initial state
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!initialCaptured && report) {
|
|
54
|
+
initialReport.current = report
|
|
55
|
+
setInitialCaptured(true)
|
|
56
|
+
}
|
|
57
|
+
}, [initialCaptured, report])
|
|
58
|
+
|
|
59
|
+
const info = useMemo(() => {
|
|
60
|
+
const initial = getStatsFromReport(initialReport.current)
|
|
61
|
+
const current = getStatsFromReport(report)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
initial: {
|
|
65
|
+
verticesCount: initial.vertices,
|
|
66
|
+
primitivesCount: initial.primitives,
|
|
67
|
+
meshesCount: initial.meshes,
|
|
68
|
+
texturesCount: initial.textures,
|
|
69
|
+
sceneBytes: initial.totalSize
|
|
70
|
+
},
|
|
71
|
+
optimized: {
|
|
72
|
+
verticesCount: current.vertices,
|
|
73
|
+
primitivesCount: current.primitives,
|
|
74
|
+
meshesCount: current.meshes,
|
|
75
|
+
texturesCount: current.textures,
|
|
76
|
+
sceneBytes: current.totalSize
|
|
77
|
+
},
|
|
78
|
+
improvement: {
|
|
79
|
+
verticesCount: initial.vertices - current.vertices,
|
|
80
|
+
primitivesCount: initial.primitives - current.primitives,
|
|
81
|
+
meshesCount: initial.meshes - current.meshes,
|
|
82
|
+
texturesCount: initial.textures - current.textures,
|
|
83
|
+
sceneBytes: initial.totalSize - current.totalSize
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, [report])
|
|
87
|
+
|
|
88
|
+
const reset = useCallback(() => {
|
|
89
|
+
initialReport.current = null
|
|
90
|
+
setInitialCaptured(false)
|
|
91
|
+
}, [])
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
info,
|
|
95
|
+
reset
|
|
96
|
+
}
|
|
97
|
+
}
|