@vctrl/hooks 0.16.2 → 0.17.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 +14 -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,292 +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 { validateServerResponse } from './validation'
|
|
18
|
-
import { ServerCommunicationService } from '../../utils/server-communication'
|
|
19
|
-
|
|
20
|
-
import type {
|
|
21
|
-
ModelOptimizer,
|
|
22
|
-
TextureCompressOptions,
|
|
23
|
-
TextureDescriptor
|
|
24
|
-
} from '@vctrl/core/model-optimizer'
|
|
25
|
-
|
|
26
|
-
const DEFAULT_TIMEOUT_MS = 20_000
|
|
27
|
-
const DEFAULT_MAX_RETRIES = 2
|
|
28
|
-
const DEFAULT_MAX_TEXTURE_UPLOAD_BYTES = 50 * 1024 * 1024
|
|
29
|
-
const DEFAULT_MAX_CONCURRENT_REQUESTS = 4
|
|
30
|
-
const TRANSIENT_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504])
|
|
31
|
-
|
|
32
|
-
const wait = async (ms: number): Promise<void> => {
|
|
33
|
-
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const resolveTextureMimeType = (response: Response): string => {
|
|
37
|
-
const contentType = response.headers.get('content-type')
|
|
38
|
-
if (!contentType) {
|
|
39
|
-
return 'application/octet-stream'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return contentType.split(';')[0]?.trim() || 'application/octet-stream'
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const resolveCanonicalTextureFileName = (response: Response): string => {
|
|
46
|
-
const responseFileName =
|
|
47
|
-
response.headers.get('X-Texture-File-Name') ||
|
|
48
|
-
response.headers.get('X-Texture-Name')
|
|
49
|
-
|
|
50
|
-
if (!responseFileName || responseFileName.trim().length === 0) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'Optimized texture response is missing a canonical file name'
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return responseFileName.trim()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const requestSingleTextureOptimization = async (
|
|
60
|
-
endpoint: string,
|
|
61
|
-
headers: HeadersInit,
|
|
62
|
-
body: ArrayBuffer,
|
|
63
|
-
requestTimeoutMs: number,
|
|
64
|
-
maxRetries: number
|
|
65
|
-
): Promise<Response> => {
|
|
66
|
-
let attempt = 0
|
|
67
|
-
|
|
68
|
-
while (true) {
|
|
69
|
-
const controller = new AbortController()
|
|
70
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs)
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const response = await fetch(endpoint, {
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers,
|
|
76
|
-
body,
|
|
77
|
-
signal: controller.signal
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
!response.ok &&
|
|
82
|
-
TRANSIENT_STATUS_CODES.has(response.status) &&
|
|
83
|
-
attempt < maxRetries
|
|
84
|
-
) {
|
|
85
|
-
attempt += 1
|
|
86
|
-
await wait(200 * attempt + Math.floor(Math.random() * 120))
|
|
87
|
-
continue
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return response
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (attempt >= maxRetries) {
|
|
93
|
-
throw error
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
attempt += 1
|
|
97
|
-
await wait(200 * attempt + Math.floor(Math.random() * 120))
|
|
98
|
-
} finally {
|
|
99
|
-
clearTimeout(timeoutId)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const optimizeTexture = async (
|
|
105
|
-
optimizer: ModelOptimizer,
|
|
106
|
-
texture: TextureDescriptor,
|
|
107
|
-
options: TextureCompressOptions,
|
|
108
|
-
serverOptions: ReturnType<
|
|
109
|
-
typeof ServerCommunicationService.createDefaultServerOptions
|
|
110
|
-
>
|
|
111
|
-
): Promise<void> => {
|
|
112
|
-
const payload = optimizer.getTexturePayload(texture.index)
|
|
113
|
-
const maxTextureUploadBytes =
|
|
114
|
-
options.maxTextureUploadBytes ?? DEFAULT_MAX_TEXTURE_UPLOAD_BYTES
|
|
115
|
-
|
|
116
|
-
if (payload.image.byteLength > maxTextureUploadBytes) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Texture ${texture.index} payload (${payload.image.byteLength} bytes) exceeds maxTextureUploadBytes (${maxTextureUploadBytes} bytes). Increase maxTextureUploadBytes to allow optimizing larger source textures.`
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const requestBytes = new Uint8Array(payload.image.byteLength)
|
|
123
|
-
requestBytes.set(payload.image)
|
|
124
|
-
const requestBody = requestBytes.buffer
|
|
125
|
-
const { serverOptions: _, ...restOptions } = options
|
|
126
|
-
const headers = ServerCommunicationService.createRequestHeaders(
|
|
127
|
-
serverOptions,
|
|
128
|
-
{
|
|
129
|
-
'Content-Type': 'application/octet-stream',
|
|
130
|
-
'X-Texture-Index': String(texture.index),
|
|
131
|
-
'X-Texture-Name': payload.fileName,
|
|
132
|
-
'X-Texture-File-Name': payload.fileName,
|
|
133
|
-
'X-Texture-Mime-Type': payload.mimeType,
|
|
134
|
-
'X-Optimize-Options': JSON.stringify(restOptions)
|
|
135
|
-
}
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
const response = await requestSingleTextureOptimization(
|
|
139
|
-
serverOptions.endpoint,
|
|
140
|
-
headers,
|
|
141
|
-
requestBody,
|
|
142
|
-
options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
143
|
-
options.maxRetries ?? DEFAULT_MAX_RETRIES
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
if (!response.ok) {
|
|
147
|
-
await ServerCommunicationService.handleServerResponseError(response)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
await validateServerResponse(response)
|
|
151
|
-
|
|
152
|
-
const responseTextureIndex = Number.parseInt(
|
|
153
|
-
response.headers.get('X-Texture-Index') || '',
|
|
154
|
-
10
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
!Number.isFinite(responseTextureIndex) ||
|
|
159
|
-
responseTextureIndex !== texture.index
|
|
160
|
-
) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`Texture identity mismatch. Expected ${texture.index}, received ${response.headers.get('X-Texture-Index')}`
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const optimizedTextureBytes = new Uint8Array(await response.arrayBuffer())
|
|
167
|
-
if (optimizedTextureBytes.byteLength === 0) {
|
|
168
|
-
throw new Error(`Empty optimized payload for texture ${texture.index}`)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const canonicalFileName = resolveCanonicalTextureFileName(response)
|
|
172
|
-
|
|
173
|
-
optimizer.replaceTexturePayload(
|
|
174
|
-
texture.index,
|
|
175
|
-
optimizedTextureBytes,
|
|
176
|
-
resolveTextureMimeType(response),
|
|
177
|
-
canonicalFileName
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Performs server-side texture optimization.
|
|
183
|
-
*/
|
|
184
|
-
export const performServerSideTextureOptimization = async (
|
|
185
|
-
optimizer: ModelOptimizer,
|
|
186
|
-
options: TextureCompressOptions
|
|
187
|
-
): Promise<void> => {
|
|
188
|
-
const serverOptions = ServerCommunicationService.createDefaultServerOptions(
|
|
189
|
-
options.serverOptions
|
|
190
|
-
)
|
|
191
|
-
const textures = optimizer.listTextureDescriptors()
|
|
192
|
-
const failures: Array<{ index: number; reason: string }> = []
|
|
193
|
-
let successCount = 0
|
|
194
|
-
const maxConcurrentRequests =
|
|
195
|
-
options.maxConcurrentRequests ?? DEFAULT_MAX_CONCURRENT_REQUESTS
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
for (
|
|
199
|
-
let start = 0;
|
|
200
|
-
start < textures.length;
|
|
201
|
-
start += maxConcurrentRequests
|
|
202
|
-
) {
|
|
203
|
-
const chunk = textures.slice(start, start + maxConcurrentRequests)
|
|
204
|
-
const settled = await Promise.allSettled(
|
|
205
|
-
chunk.map(async (texture) => {
|
|
206
|
-
await optimizeTexture(optimizer, texture, options, serverOptions)
|
|
207
|
-
return texture.index
|
|
208
|
-
})
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
for (const [index, outcome] of settled.entries()) {
|
|
212
|
-
if (outcome.status === 'fulfilled') {
|
|
213
|
-
successCount += 1
|
|
214
|
-
continue
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
failures.push({
|
|
218
|
-
index: chunk[index].index,
|
|
219
|
-
reason:
|
|
220
|
-
outcome.reason instanceof Error
|
|
221
|
-
? outcome.reason.message
|
|
222
|
-
: String(outcome.reason)
|
|
223
|
-
})
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (successCount > 0) {
|
|
228
|
-
optimizer.addAppliedOptimization('texture compression')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (failures.length > 0) {
|
|
232
|
-
const failureSummary = failures
|
|
233
|
-
.map((failure) => `#${failure.index}: ${failure.reason}`)
|
|
234
|
-
.join('; ')
|
|
235
|
-
const prefix =
|
|
236
|
-
successCount === 0
|
|
237
|
-
? 'Texture optimization failed for all textures.'
|
|
238
|
-
: `Texture optimization failed for ${failures.length} of ${textures.length} textures.`
|
|
239
|
-
|
|
240
|
-
throw new Error(`${prefix} ${failureSummary}`)
|
|
241
|
-
}
|
|
242
|
-
} catch (err) {
|
|
243
|
-
console.error('Server-side texture compression failed:', err)
|
|
244
|
-
throw new Error(
|
|
245
|
-
`Server-side texture compression failed: ${err}. Client-side texture compression is not supported in browser environments.`,
|
|
246
|
-
{ cause: err }
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Handles client-side texture optimization (throws error as not supported in browser).
|
|
253
|
-
*/
|
|
254
|
-
export const performClientSideTextureOptimization = (): never => {
|
|
255
|
-
console.warn(
|
|
256
|
-
'Client-side texture compression is not supported in browser environments. ' +
|
|
257
|
-
'Please enable server-side compression by setting serverOptions.enabled = true.'
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
throw new Error(
|
|
261
|
-
'Texture compression requires server-side processing. Enable serverOptions.enabled = true in your options.'
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Determines if server-side optimization should be used.
|
|
267
|
-
*/
|
|
268
|
-
export const shouldUseServerOptimization = (
|
|
269
|
-
options?: TextureCompressOptions
|
|
270
|
-
): boolean => {
|
|
271
|
-
return options?.serverOptions?.enabled ?? false
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Main texture optimization function that orchestrates the process.
|
|
276
|
-
*/
|
|
277
|
-
export const optimizeTextures = async (
|
|
278
|
-
optimizer: ModelOptimizer,
|
|
279
|
-
options?: TextureCompressOptions
|
|
280
|
-
): Promise<void> => {
|
|
281
|
-
if (!optimizer.hasModel()) {
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const useServer = shouldUseServerOptimization(options)
|
|
286
|
-
|
|
287
|
-
if (useServer && options) {
|
|
288
|
-
await performServerSideTextureOptimization(optimizer, options)
|
|
289
|
-
} else {
|
|
290
|
-
performClientSideTextureOptimization()
|
|
291
|
-
}
|
|
292
|
-
}
|
|
@@ -1,62 +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
|
-
/**
|
|
18
|
-
* Validates that the response contains the expected content type.
|
|
19
|
-
*/
|
|
20
|
-
export const validateResponseContentType = (response: Response): void => {
|
|
21
|
-
const contentType = response.headers.get('content-type')
|
|
22
|
-
|
|
23
|
-
if (!contentType) {
|
|
24
|
-
throw new Error('Missing content type from server response')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (contentType.includes('application/json')) {
|
|
28
|
-
throw new Error('Unexpected JSON success response from texture optimizer')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
!contentType.startsWith('image/') &&
|
|
33
|
-
!contentType.includes('application/octet-stream')
|
|
34
|
-
) {
|
|
35
|
-
throw new Error(`Unexpected content type from server: ${contentType}`)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Validates a server response and throws appropriate errors.
|
|
41
|
-
*/
|
|
42
|
-
export const validateServerResponse = async (
|
|
43
|
-
response: Response
|
|
44
|
-
): Promise<void> => {
|
|
45
|
-
// First check if the response was successful
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
return // Error handling is done elsewhere
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Check content type for successful responses
|
|
51
|
-
const contentType = response.headers.get('content-type')
|
|
52
|
-
|
|
53
|
-
if (contentType?.includes('application/json')) {
|
|
54
|
-
// Server returned JSON error despite 200 status
|
|
55
|
-
const errorData = await response.json()
|
|
56
|
-
throw new Error(
|
|
57
|
-
errorData.error || errorData.details || 'Server returned error'
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
validateResponseContentType(response)
|
|
62
|
-
}
|
|
@@ -1,351 +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 type {
|
|
18
|
-
ApiEnvelope,
|
|
19
|
-
ServerOptions,
|
|
20
|
-
TextureBinaryPayload,
|
|
21
|
-
TextureCompressOptions
|
|
22
|
-
} from '@vctrl/core'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Configuration for a server request.
|
|
26
|
-
*/
|
|
27
|
-
export interface ServerRequestConfig {
|
|
28
|
-
/** Server endpoint URL or path */
|
|
29
|
-
endpoint: string
|
|
30
|
-
/** HTTP method (GET, POST, etc.) */
|
|
31
|
-
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
32
|
-
/** Request body (FormData, JSON, or other) */
|
|
33
|
-
body?: FormData | Record<string, unknown> | string
|
|
34
|
-
/** Optional server options (headers, auth, etc.) */
|
|
35
|
-
serverOptions?: ServerOptions
|
|
36
|
-
/** Content type for JSON bodies (default: 'application/json') */
|
|
37
|
-
contentType?: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Unified server communication service for handling HTTP requests.
|
|
42
|
-
* Provides a consistent interface for API calls with built-in error handling,
|
|
43
|
-
* authentication, and response parsing.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```typescript
|
|
47
|
-
* // Simple GET request
|
|
48
|
-
* const data = await ServerCommunicationService.request<SceneData>({
|
|
49
|
-
* endpoint: '/api/load-scene',
|
|
50
|
-
* method: 'GET'
|
|
51
|
-
* })
|
|
52
|
-
*
|
|
53
|
-
* // POST with FormData
|
|
54
|
-
* const formData = new FormData()
|
|
55
|
-
* formData.append('model', file)
|
|
56
|
-
* const result = await ServerCommunicationService.request({
|
|
57
|
-
* endpoint: '/api/optimize',
|
|
58
|
-
* method: 'POST',
|
|
59
|
-
* body: formData,
|
|
60
|
-
* serverOptions: { apiKey: 'secret' }
|
|
61
|
-
* })
|
|
62
|
-
*
|
|
63
|
-
* // POST with JSON
|
|
64
|
-
* const response = await ServerCommunicationService.request({
|
|
65
|
-
* endpoint: '/api/save',
|
|
66
|
-
* method: 'POST',
|
|
67
|
-
* body: { sceneId: '123', settings: {...} },
|
|
68
|
-
* serverOptions: { headers: { 'X-Custom': 'value' } }
|
|
69
|
-
* })
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
export class ServerCommunicationService {
|
|
73
|
-
private static extractApiData<T>(response: ApiEnvelope<T> | T): T {
|
|
74
|
-
if (!response || typeof response !== 'object') {
|
|
75
|
-
return response as T
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const envelope = response as ApiEnvelope<T>
|
|
79
|
-
|
|
80
|
-
if (envelope.error) {
|
|
81
|
-
throw new Error(envelope.error)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (envelope.success === false) {
|
|
85
|
-
throw new Error('Request failed')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if ('data' in envelope) {
|
|
89
|
-
if (typeof envelope.data === 'undefined') {
|
|
90
|
-
throw new Error('Server response did not include data payload')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return envelope.data
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return response as T
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Creates default server options with required endpoint.
|
|
101
|
-
* Merges provided options with defaults.
|
|
102
|
-
*/
|
|
103
|
-
static createDefaultServerOptions(
|
|
104
|
-
serverOptions?: ServerOptions
|
|
105
|
-
): ServerOptions & Required<Pick<ServerOptions, 'endpoint'>> {
|
|
106
|
-
return {
|
|
107
|
-
endpoint: '/api/optimize-textures',
|
|
108
|
-
...serverOptions
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Creates request headers for server communication.
|
|
114
|
-
* Includes authentication token if apiKey is provided.
|
|
115
|
-
*/
|
|
116
|
-
static createRequestHeaders(
|
|
117
|
-
serverOptions?: ServerOptions,
|
|
118
|
-
additionalHeaders?: HeadersInit
|
|
119
|
-
): HeadersInit {
|
|
120
|
-
const headers: HeadersInit = {
|
|
121
|
-
...(serverOptions?.apiKey
|
|
122
|
-
? { Authorization: `Bearer ${serverOptions.apiKey}` }
|
|
123
|
-
: {}),
|
|
124
|
-
...serverOptions?.headers,
|
|
125
|
-
...additionalHeaders
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return headers
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Extracts error message from a failed response.
|
|
133
|
-
* Attempts to parse JSON error data, falls back to text.
|
|
134
|
-
*/
|
|
135
|
-
static async extractErrorMessage(response: Response): Promise<string> {
|
|
136
|
-
let errorMessage = `Server responded with ${response.status}: ${response.statusText}`
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const contentType = response.headers.get('content-type')
|
|
140
|
-
if (contentType?.includes('application/json')) {
|
|
141
|
-
const errorData = await response.json()
|
|
142
|
-
errorMessage = errorData.error || errorData.details || errorMessage
|
|
143
|
-
} else {
|
|
144
|
-
const errorText = await response.text()
|
|
145
|
-
if (errorText) errorMessage += ` - ${errorText}`
|
|
146
|
-
}
|
|
147
|
-
} catch {
|
|
148
|
-
// Fallback to basic error message if can't parse response
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return errorMessage
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Handles server response errors and throws appropriate errors.
|
|
156
|
-
*/
|
|
157
|
-
static async handleServerResponseError(response: Response): Promise<never> {
|
|
158
|
-
const errorMessage =
|
|
159
|
-
await ServerCommunicationService.extractErrorMessage(response)
|
|
160
|
-
throw new Error(errorMessage)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Prepares FormData for a single texture optimization request.
|
|
165
|
-
*/
|
|
166
|
-
static prepareTextureOptimizationFormData(
|
|
167
|
-
texture: TextureBinaryPayload,
|
|
168
|
-
options?: TextureCompressOptions
|
|
169
|
-
): FormData {
|
|
170
|
-
const requestData = new FormData()
|
|
171
|
-
|
|
172
|
-
requestData.append(
|
|
173
|
-
'texture',
|
|
174
|
-
new Blob([new Uint8Array(texture.image)], {
|
|
175
|
-
type: texture.mimeType || 'application/octet-stream'
|
|
176
|
-
}),
|
|
177
|
-
`${texture.name || `texture-${texture.index}`}`
|
|
178
|
-
)
|
|
179
|
-
requestData.append('textureIndex', String(texture.index))
|
|
180
|
-
requestData.append('textureName', texture.name)
|
|
181
|
-
requestData.append('textureMimeType', texture.mimeType)
|
|
182
|
-
|
|
183
|
-
if (options) {
|
|
184
|
-
const { serverOptions: _, ...restOptions } = options
|
|
185
|
-
requestData.append('options', JSON.stringify(restOptions))
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return requestData
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Performs a generic HTTP request with error handling.
|
|
193
|
-
* Returns parsed response data.
|
|
194
|
-
*
|
|
195
|
-
* @template T - Expected response data type
|
|
196
|
-
* @param config - Request configuration
|
|
197
|
-
* @returns Promise resolving to parsed response data
|
|
198
|
-
* @throws Error if request fails or response is not ok
|
|
199
|
-
*/
|
|
200
|
-
static async request<T = unknown>(config: ServerRequestConfig): Promise<T> {
|
|
201
|
-
const {
|
|
202
|
-
endpoint,
|
|
203
|
-
method = 'GET',
|
|
204
|
-
body,
|
|
205
|
-
serverOptions,
|
|
206
|
-
contentType = 'application/json'
|
|
207
|
-
} = config
|
|
208
|
-
|
|
209
|
-
// Prepare headers
|
|
210
|
-
const headers: HeadersInit =
|
|
211
|
-
ServerCommunicationService.createRequestHeaders(serverOptions)
|
|
212
|
-
|
|
213
|
-
// Prepare request body
|
|
214
|
-
let requestBody: FormData | string | undefined
|
|
215
|
-
|
|
216
|
-
if (body instanceof FormData) {
|
|
217
|
-
// Don't set Content-Type for FormData - browser will set it with boundary
|
|
218
|
-
requestBody = body
|
|
219
|
-
} else if (typeof body === 'object' && body !== null) {
|
|
220
|
-
// JSON body
|
|
221
|
-
;(headers as Record<string, string>)['Content-Type'] = contentType
|
|
222
|
-
requestBody = JSON.stringify(body)
|
|
223
|
-
} else if (typeof body === 'string') {
|
|
224
|
-
requestBody = body
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Perform request
|
|
228
|
-
const response = await fetch(endpoint, {
|
|
229
|
-
method,
|
|
230
|
-
headers,
|
|
231
|
-
body: requestBody
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
// Handle errors
|
|
235
|
-
if (!response.ok) {
|
|
236
|
-
await ServerCommunicationService.handleServerResponseError(response)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Parse response
|
|
240
|
-
const responseContentType = response.headers.get('content-type')
|
|
241
|
-
|
|
242
|
-
if (responseContentType?.includes('application/json')) {
|
|
243
|
-
return (await response.json()) as T
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// For binary responses (e.g., model files)
|
|
247
|
-
if (
|
|
248
|
-
responseContentType?.includes('model/') ||
|
|
249
|
-
responseContentType?.includes('application/octet-stream')
|
|
250
|
-
) {
|
|
251
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
252
|
-
return new Uint8Array(arrayBuffer) as T
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Default to text
|
|
256
|
-
return (await response.text()) as T
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Performs a GET request.
|
|
261
|
-
* Convenience method for common GET operations.
|
|
262
|
-
*/
|
|
263
|
-
static async get<T = unknown>(
|
|
264
|
-
endpoint: string,
|
|
265
|
-
serverOptions?: ServerOptions
|
|
266
|
-
): Promise<T> {
|
|
267
|
-
return ServerCommunicationService.request<T>({
|
|
268
|
-
endpoint,
|
|
269
|
-
method: 'GET',
|
|
270
|
-
serverOptions
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Performs a POST request with JSON body.
|
|
276
|
-
* Convenience method for common POST operations.
|
|
277
|
-
*/
|
|
278
|
-
static async post<T = unknown>(
|
|
279
|
-
endpoint: string,
|
|
280
|
-
body: Record<string, unknown>,
|
|
281
|
-
serverOptions?: ServerOptions
|
|
282
|
-
): Promise<T> {
|
|
283
|
-
return ServerCommunicationService.request<T>({
|
|
284
|
-
endpoint,
|
|
285
|
-
method: 'POST',
|
|
286
|
-
body,
|
|
287
|
-
serverOptions
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Performs a POST request with FormData body.
|
|
293
|
-
* Convenience method for file uploads.
|
|
294
|
-
*/
|
|
295
|
-
static async postFormData<T = unknown>(
|
|
296
|
-
endpoint: string,
|
|
297
|
-
formData: FormData,
|
|
298
|
-
serverOptions?: ServerOptions
|
|
299
|
-
): Promise<T> {
|
|
300
|
-
return ServerCommunicationService.request<T>({
|
|
301
|
-
endpoint,
|
|
302
|
-
method: 'POST',
|
|
303
|
-
body: formData,
|
|
304
|
-
serverOptions
|
|
305
|
-
})
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Fetches scene data from the server.
|
|
310
|
-
* Specialized method for loading 3D scenes with their associated settings.
|
|
311
|
-
*
|
|
312
|
-
* This method handles the vectreal-platform API format which uses:
|
|
313
|
-
* - FormData with 'action' and 'sceneId' parameters
|
|
314
|
-
* - POST request to the scene-settings endpoint
|
|
315
|
-
* - Returns scene data including GLTF JSON and asset data
|
|
316
|
-
*
|
|
317
|
-
* @param sceneId - The unique identifier of the scene to load
|
|
318
|
-
* @param serverOptions - Optional server configuration (endpoint, auth, headers)
|
|
319
|
-
* @returns Promise resolving to the scene data
|
|
320
|
-
* @throws Error if the request fails or scene doesn't exist
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* ```typescript
|
|
324
|
-
* const sceneData = await ServerCommunicationService.loadScene('abc-123', {
|
|
325
|
-
* endpoint: '/api/scenes/abc-123',
|
|
326
|
-
* apiKey: 'optional-auth-token'
|
|
327
|
-
* })
|
|
328
|
-
* ```
|
|
329
|
-
*/
|
|
330
|
-
static async loadScene<T = unknown>(
|
|
331
|
-
sceneId: string,
|
|
332
|
-
serverOptions?: ServerOptions
|
|
333
|
-
): Promise<T> {
|
|
334
|
-
const formData = new FormData()
|
|
335
|
-
formData.append('action', 'get-scene-settings')
|
|
336
|
-
formData.append('sceneId', sceneId)
|
|
337
|
-
|
|
338
|
-
const endpoint = serverOptions?.endpoint || `/api/scenes/${sceneId}`
|
|
339
|
-
|
|
340
|
-
const response = await ServerCommunicationService.request<
|
|
341
|
-
ApiEnvelope<T> | T
|
|
342
|
-
>({
|
|
343
|
-
endpoint,
|
|
344
|
-
method: 'POST',
|
|
345
|
-
body: formData,
|
|
346
|
-
serverOptions
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
return ServerCommunicationService.extractApiData(response)
|
|
350
|
-
}
|
|
351
|
-
}
|