@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/GLTFLoader-CygFefRr.cjs +1 -0
  3. package/GLTFLoader-NiJ7qzh1.js +1381 -0
  4. package/README.md +0 -20
  5. package/__vite-browser-external-Bn_csQuM.js +7 -0
  6. package/__vite-browser-external-CVmj8TrW.cjs +1 -0
  7. package/chunk-foFoljWS.js +23 -0
  8. package/chunk-gGpyby2o.cjs +1 -0
  9. package/index.cjs.js +1 -0
  10. package/index.d.ts +5 -0
  11. package/index.es.js +4 -0
  12. package/lib-DKn2MJ9X.js +4111 -0
  13. package/lib-DZ-MihdJ.cjs +2 -0
  14. package/model-exporter-SMSAY90N.cjs +1 -0
  15. package/model-exporter-cvguDGnn.js +6512 -0
  16. package/package.json +1 -1
  17. package/server-communication-Bj6ZxFw2.js +4659 -0
  18. package/server-communication-yXZR8eso.cjs +18 -0
  19. package/use-export-model/use-export-model.d.ts +14 -0
  20. package/use-export-model-CaijtW1c.cjs +1 -0
  21. package/use-export-model-DzVKdqnY.js +49 -0
  22. package/use-export-model.cjs.js +1 -0
  23. package/use-export-model.es.js +2 -0
  24. package/use-load-model/event-system.d.ts +7 -0
  25. package/use-load-model/index.d.ts +4 -0
  26. package/use-load-model/state.d.ts +19 -0
  27. package/use-load-model/types.d.ts +231 -0
  28. package/use-load-model/use-load-model.d.ts +30 -0
  29. package/use-load-model/utils/calculate-referenced-bytes.d.ts +8 -0
  30. package/use-load-model/utils/index.d.ts +4 -0
  31. package/use-load-model/utils/read-directory.d.ts +8 -0
  32. package/use-load-model/utils/reconstruct-files.d.ts +29 -0
  33. package/use-load-model/utils/resolve-scene-payload.d.ts +6 -0
  34. package/use-load-model-B6UXiFWh.js +517 -0
  35. package/use-load-model-CH-WPQ_s.cjs +1 -0
  36. package/use-load-model.cjs.js +1 -0
  37. package/use-load-model.es.js +18 -0
  38. package/{src/use-optimize-model/index.ts → use-optimize-model/index.d.ts} +2 -2
  39. package/use-optimize-model/state.d.ts +13 -0
  40. package/use-optimize-model/types.d.ts +39 -0
  41. package/use-optimize-model/use-calc-optimization-info.d.ts +6 -0
  42. package/use-optimize-model/use-optimize-model.d.ts +119 -0
  43. package/use-optimize-model/utils/index.d.ts +2 -0
  44. package/use-optimize-model/utils/texture-optimization.d.ts +17 -0
  45. package/use-optimize-model/utils/validation.d.ts +8 -0
  46. package/use-optimize-model-C-fzzKh6.cjs +1 -0
  47. package/use-optimize-model-Cb3WMvDZ.js +299 -0
  48. package/use-optimize-model.cjs.js +1 -0
  49. package/use-optimize-model.es.js +2 -0
  50. package/utils/server-communication.d.ts +122 -0
  51. package/project.json +0 -55
  52. package/src/index.ts +0 -38
  53. package/src/use-export-model/use-export-model.ts +0 -113
  54. package/src/use-load-model/event-system.ts +0 -46
  55. package/src/use-load-model/index.ts +0 -15
  56. package/src/use-load-model/model-context.tsx +0 -174
  57. package/src/use-load-model/state.ts +0 -75
  58. package/src/use-load-model/types.ts +0 -284
  59. package/src/use-load-model/use-load-model.ts +0 -682
  60. package/src/use-load-model/utils/calculate-referenced-bytes.ts +0 -123
  61. package/src/use-load-model/utils/index.ts +0 -7
  62. package/src/use-load-model/utils/read-directory.ts +0 -35
  63. package/src/use-load-model/utils/reconstruct-files.ts +0 -85
  64. package/src/use-load-model/utils/resolve-scene-payload.ts +0 -151
  65. package/src/use-optimize-model/state.ts +0 -40
  66. package/src/use-optimize-model/types.ts +0 -37
  67. package/src/use-optimize-model/use-calc-optimization-info.ts +0 -101
  68. package/src/use-optimize-model/use-optimize-model.ts +0 -467
  69. package/src/use-optimize-model/utils/index.ts +0 -29
  70. package/src/use-optimize-model/utils/texture-optimization.ts +0 -292
  71. package/src/use-optimize-model/utils/validation.ts +0 -62
  72. package/src/utils/server-communication.ts +0 -351
  73. package/tsconfig.json +0 -21
  74. package/tsconfig.lib.json +0 -16
  75. package/vite.config.ts +0 -77
  76. /package/{src/use-export-model/index.ts → use-export-model/index.d.ts} +0 -0
@@ -0,0 +1,119 @@
1
+ import { DedupOptions, NormalsOptions, QuantizeOptions, SimplifyOptions, TextureCompressOptions } from '../../../core/src/model-optimizer/index.ts';
2
+ import { Object3D } from 'three';
3
+ import { ServerSceneData } from '../../../core/src/index.ts';
4
+ /**
5
+ * Custom React hook for optimizing 3D models using the ModelOptimizer from @vctrl/core.
6
+ *
7
+ * This hook provides a React-friendly interface to advanced model optimization capabilities,
8
+ * including mesh simplification, deduplication, quantization, normal optimization, and texture compression.
9
+ * It manages the optimization state and provides callbacks for each optimization operation.
10
+ *
11
+ * **Features:**
12
+ * - Mesh simplification using MeshoptSimplifier
13
+ * - Geometry deduplication to remove redundant data
14
+ * - Vertex attribute quantization to reduce file size
15
+ * - Normal vector optimization
16
+ * - Texture compression (server-side only)
17
+ * - Progress tracking and error handling
18
+ * - Optimization reports with before/after metrics
19
+ *
20
+ * @example
21
+ * const optimizer = useOptimizeModel()
22
+ *
23
+ * // Load a model
24
+ * await optimizer.load(threeJsScene)
25
+ *
26
+ * // Apply optimizations
27
+ * await optimizer.simplifyOptimization({ ratio: 0.5 })
28
+ * await optimizer.quantizeOptimization({ bits: 12 })
29
+ *
30
+ * // Get optimized model
31
+ * const optimizedBinary = await optimizer.getModel()
32
+ *
33
+ * @returns Object containing optimization methods, state, and report data
34
+ */
35
+ declare const useOptimizeModel: () => {
36
+ /**
37
+ * Loads a Three.js Object3D model into the optimizer.
38
+ * Converts the scene to glTF format and generates an initial optimization report.
39
+ *
40
+ * @param model - The Three.js Object3D model to load
41
+ * @returns Promise that resolves when the model is loaded
42
+ */
43
+ load: (model: Object3D) => Promise<void>;
44
+ loadFromServerSceneData: (sceneData: ServerSceneData) => Promise<void>;
45
+ /**
46
+ * Retrieves the current model as a binary Uint8Array in glTF (.glb) format.
47
+ *
48
+ * @returns Promise that resolves with the model binary or null if no model is loaded
49
+ */
50
+ getModel: () => Promise<Uint8Array | null>;
51
+ isReady: boolean;
52
+ /**
53
+ * Exposes the underlying ModelOptimizer document instance for advanced use cases.
54
+ */
55
+ _getDocument: () => import('@gltf-transform/core').Document | null;
56
+ /**
57
+ * Resets the optimizer, clearing all model data, reports, and state.
58
+ */
59
+ reset: () => void;
60
+ /**
61
+ * Error object if any optimization operation failed, otherwise null.
62
+ */
63
+ error: Error | null;
64
+ /**
65
+ * Boolean indicating if an optimization operation is currently in progress.
66
+ */
67
+ loading: boolean;
68
+ /**
69
+ * Detailed optimization report containing metrics about the model
70
+ * (vertex count, triangle count, file size, etc.) before and after optimizations.
71
+ */
72
+ report: import('../../../core/src/index.ts').OptimizationReport | null;
73
+ /**
74
+ * Calculated optimization information including file size reduction percentages
75
+ * and comparative metrics.
76
+ */
77
+ info: import('./types').OptimizationInfo;
78
+ /**
79
+ * Simplifies the model by reducing polygon count using MeshoptSimplifier.
80
+ * Maintains visual quality while improving performance.
81
+ *
82
+ * @param options - Simplification options (ratio, error threshold)
83
+ * @returns Promise that resolves when simplification is complete
84
+ */
85
+ simplifyOptimization: (options?: SimplifyOptions) => Promise<void>;
86
+ /**
87
+ * Removes duplicate vertices, primitives, and other redundant data.
88
+ * Reduces file size without affecting visual appearance.
89
+ *
90
+ * @param options - Deduplication options
91
+ * @returns Promise that resolves when deduplication is complete
92
+ */
93
+ dedupOptimization: (options?: DedupOptions) => Promise<void>;
94
+ /**
95
+ * Quantizes vertex attributes to use fewer bits per value.
96
+ * Reduces file size with minimal visual quality loss.
97
+ *
98
+ * @param options - Quantization options (bit depth)
99
+ * @returns Promise that resolves when quantization is complete
100
+ */
101
+ quantizeOptimization: (options?: QuantizeOptions) => Promise<void>;
102
+ /**
103
+ * Optimizes normal vectors by removing, generating, or cleaning up normal data.
104
+ *
105
+ * @param options - Normal optimization options
106
+ * @returns Promise that resolves when optimization is complete
107
+ */
108
+ normalsOptimization: (options?: NormalsOptions) => Promise<void>;
109
+ /**
110
+ * Compresses textures using advanced formats (requires server-side processing).
111
+ * Note: Only works in Node.js environments with Sharp library available.
112
+ *
113
+ * @param options - Texture compression options (format, quality)
114
+ * @returns Promise that resolves when compression is complete
115
+ * @throws Error if Sharp is not available
116
+ */
117
+ texturesOptimization: (options?: TextureCompressOptions) => Promise<void>;
118
+ };
119
+ export default useOptimizeModel;
@@ -0,0 +1,2 @@
1
+ export { optimizeTextures, performClientSideTextureOptimization, performServerSideTextureOptimization, shouldUseServerOptimization } from './texture-optimization';
2
+ export { validateResponseContentType, validateServerResponse } from './validation';
@@ -0,0 +1,17 @@
1
+ import { ModelOptimizer, TextureCompressOptions } from '../../../../core/src/model-optimizer/index.ts';
2
+ /**
3
+ * Performs server-side texture optimization.
4
+ */
5
+ export declare const performServerSideTextureOptimization: (optimizer: ModelOptimizer, options: TextureCompressOptions) => Promise<void>;
6
+ /**
7
+ * Handles client-side texture optimization (throws error as not supported in browser).
8
+ */
9
+ export declare const performClientSideTextureOptimization: () => never;
10
+ /**
11
+ * Determines if server-side optimization should be used.
12
+ */
13
+ export declare const shouldUseServerOptimization: (options?: TextureCompressOptions) => boolean;
14
+ /**
15
+ * Main texture optimization function that orchestrates the process.
16
+ */
17
+ export declare const optimizeTextures: (optimizer: ModelOptimizer, options?: TextureCompressOptions) => Promise<void>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Validates that the response contains the expected content type.
3
+ */
4
+ export declare const validateResponseContentType: (response: Response) => void;
5
+ /**
6
+ * Validates a server response and throws appropriate errors.
7
+ */
8
+ export declare const validateServerResponse: (response: Response) => Promise<void>;
@@ -0,0 +1 @@
1
+ require(`./chunk-gGpyby2o.cjs`);const e=require(`./server-communication-yXZR8eso.cjs`);let t=require(`react`);var n={info:null,error:null,loading:!1,report:null},r=(e,t)=>{switch(t.type){case`LOAD_START`:return{...e,loading:!0,error:null};case`LOAD_SUCCESS`:return{...e,loading:!1,report:t.payload.report};case`LOAD_ERROR`:return{...e,loading:!1,error:t.payload};case`RESET`:return{...n};default:return e}};function i(e){return e?{verticesCount:e.stats.vertices.after??0,primitivesCount:e.stats.triangles.after??0,meshesCount:e.stats.meshes.after??0,textureAssetCount:e.stats.texturesCount.after??0,materialsCount:e.stats.materials.after??0,sceneBytes:e.optimizedSize??0}:{verticesCount:0,primitivesCount:0,meshesCount:0,textureAssetCount:0,materialsCount:0,sceneBytes:0}}var a=e=>{let n=(0,t.useRef)(null),[r,a]=(0,t.useState)(!1),o=e.report;return(0,t.useEffect)(()=>{!r&&o&&(n.current=o,a(!0))},[r,o]),{info:(0,t.useMemo)(()=>{let e=i(n.current),t=i(o);return{initial:{verticesCount:e.verticesCount,primitivesCount:e.primitivesCount,meshesCount:e.meshesCount,texturesCount:e.textureAssetCount,sceneBytes:e.sceneBytes},optimized:{verticesCount:t.verticesCount,primitivesCount:t.primitivesCount,meshesCount:t.meshesCount,texturesCount:t.textureAssetCount,sceneBytes:t.sceneBytes},improvement:{verticesCount:e.verticesCount-t.verticesCount,primitivesCount:e.primitivesCount-t.primitivesCount,meshesCount:e.meshesCount-t.meshesCount,texturesCount:e.textureAssetCount-t.textureAssetCount,sceneBytes:e.sceneBytes-t.sceneBytes}}},[o]),reset:(0,t.useCallback)(()=>{n.current=null,a(!1)},[])}},o=e=>{let t=e.headers.get(`content-type`);if(!t)throw Error(`Missing content type from server response`);if(t.includes(`application/json`))throw Error(`Unexpected JSON success response from texture optimizer`);if(!t.startsWith(`image/`)&&!t.includes(`application/octet-stream`))throw Error(`Unexpected content type from server: ${t}`)},s=async e=>{if(e.ok){if(e.headers.get(`content-type`)?.includes(`application/json`)){let t=await e.json();throw Error(t.error||t.details||`Server returned error`)}o(e)}},c=2e4,l=2,u=50*1024*1024,d=4,f=new Set([408,425,429,500,502,503,504]),p=async e=>{await new Promise(t=>setTimeout(t,e))},m=e=>{let t=e.headers.get(`content-type`);return t&&t.split(`;`)[0]?.trim()||`application/octet-stream`},h=e=>{let t=e.headers.get(`X-Texture-File-Name`)||e.headers.get(`X-Texture-Name`);if(!t||t.trim().length===0)throw Error(`Optimized texture response is missing a canonical file name`);return t.trim()},g=async(e,t,n,r,i)=>{let a=0;for(;;){let o=new AbortController,s=setTimeout(()=>o.abort(),r);try{let r=await fetch(e,{method:`POST`,headers:t,body:n,signal:o.signal});if(!r.ok&&f.has(r.status)&&a<i){a+=1,await p(200*a+Math.floor(Math.random()*120));continue}return r}catch(e){if(a>=i)throw e;a+=1,await p(200*a+Math.floor(Math.random()*120))}finally{clearTimeout(s)}}},_=async(t,n,r,i)=>{let a=t.getTexturePayload(n.index),o=r.maxTextureUploadBytes??u;if(a.image.byteLength>o)throw Error(`Texture ${n.index} payload (${a.image.byteLength} bytes) exceeds maxTextureUploadBytes (${o} bytes). Increase maxTextureUploadBytes to allow optimizing larger source textures.`);let d=new Uint8Array(a.image.byteLength);d.set(a.image);let f=d.buffer,{serverOptions:p,..._}=r,v=e.t.createRequestHeaders(i,{"Content-Type":`application/octet-stream`,"X-Texture-Index":String(n.index),"X-Texture-Name":a.fileName,"X-Texture-File-Name":a.fileName,"X-Texture-Mime-Type":a.mimeType,"X-Optimize-Options":JSON.stringify(_)}),y=await g(i.endpoint,v,f,r.requestTimeoutMs??c,r.maxRetries??l);y.ok||await e.t.handleServerResponseError(y),await s(y);let b=Number.parseInt(y.headers.get(`X-Texture-Index`)||``,10);if(!Number.isFinite(b)||b!==n.index)throw Error(`Texture identity mismatch. Expected ${n.index}, received ${y.headers.get(`X-Texture-Index`)}`);let x=new Uint8Array(await y.arrayBuffer());if(x.byteLength===0)throw Error(`Empty optimized payload for texture ${n.index}`);let S=h(y);t.replaceTexturePayload(n.index,x,m(y),S)},v=async(t,n)=>{let r=e.t.createDefaultServerOptions(n.serverOptions),i=t.listTextureDescriptors(),a=[],o=0,s=n.maxConcurrentRequests??d;try{for(let e=0;e<i.length;e+=s){let c=i.slice(e,e+s),l=await Promise.allSettled(c.map(async e=>(await _(t,e,n,r),e.index)));for(let[e,t]of l.entries()){if(t.status===`fulfilled`){o+=1;continue}a.push({index:c[e].index,reason:t.reason instanceof Error?t.reason.message:String(t.reason)})}}if(o>0&&t.addAppliedOptimization(`texture compression`),a.length>0){let e=a.map(e=>`#${e.index}: ${e.reason}`).join(`; `),t=o===0?`Texture optimization failed for all textures.`:`Texture optimization failed for ${a.length} of ${i.length} textures.`;throw Error(`${t} ${e}`)}}catch(e){throw console.error(`Server-side texture compression failed:`,e),Error(`Server-side texture compression failed: ${e}. Client-side texture compression is not supported in browser environments.`,{cause:e})}},y=()=>{throw console.warn(`Client-side texture compression is not supported in browser environments. Please enable server-side compression by setting serverOptions.enabled = true.`),Error(`Texture compression requires server-side processing. Enable serverOptions.enabled = true in your options.`)},b=e=>e?.serverOptions?.enabled??!1,x=async(e,t)=>{e.hasModel()&&(b(t)&&t?await v(e,t):y())},S=()=>{let[i,o]=(0,t.useReducer)(r,n),{report:s,error:c,loading:l}=i,{info:u,reset:d}=a(i),f=(0,t.useRef)(new e.c);(0,t.useEffect)(()=>{let e=f.current;return()=>{e.reset()}},[]);let p=(0,t.useCallback)(async e=>{o({type:`LOAD_START`});try{let t=f.current;await t.loadFromThreeJS(e),o({type:`LOAD_SUCCESS`,payload:{report:await t.getReport()}})}catch(e){o({type:`LOAD_ERROR`,payload:e}),console.error(`Error loading model:`,e)}},[]),m=(0,t.useCallback)(async t=>{o({type:`LOAD_START`});try{let n=f.current,r={};for(let n of Object.values(t.assetData??{}))r[n.fileName]=e.o(n);let i={json:t.gltfJson,resources:r};await n.loadFromJSON(i),o({type:`LOAD_SUCCESS`,payload:{report:await n.getReport()}})}catch(e){throw o({type:`LOAD_ERROR`,payload:e}),console.error(`Error loading server scene into optimizer:`,e),e}},[]),h=(0,t.useCallback)(async e=>{if(f.current.hasModel())try{await f.current.simplify(e),o({type:`LOAD_SUCCESS`,payload:{report:await f.current.getReport()}})}catch(e){console.error(`Simplification failed:`,e)}},[]),g=(0,t.useCallback)(async e=>{if(f.current.hasModel())try{await f.current.deduplicate(e),o({type:`LOAD_SUCCESS`,payload:{report:await f.current.getReport()}})}catch(e){console.error(`Deduplication failed:`,e)}},[]),_=(0,t.useCallback)(async e=>{if(f.current.hasModel())try{await f.current.quantize(e),o({type:`LOAD_SUCCESS`,payload:{report:await f.current.getReport()}})}catch(e){console.error(`Quantization failed:`,e)}},[]),v=(0,t.useCallback)(async e=>{if(f.current.hasModel())try{await f.current.optimizeNormals(e),o({type:`LOAD_SUCCESS`,payload:{report:await f.current.getReport()}})}catch(e){console.error(`Normals optimization failed:`,e)}},[]),y=(0,t.useCallback)(async e=>{try{await x(f.current,e),o({type:`LOAD_SUCCESS`,payload:{report:await f.current.getReport()}})}catch(e){throw console.error(`Texture optimization failed:`,e),e}},[]),b=(0,t.useCallback)(async()=>{if(!f.current.hasModel())return null;try{return await f.current.export()}catch(e){return console.error(`Error getting model binary:`,e),null}},[]),S=(0,t.useCallback)(()=>f.current.hasModel()?f.current.document:null,[]);return{load:p,loadFromServerSceneData:m,getModel:b,isReady:!!S()&&!l,_getDocument:S,reset:(0,t.useCallback)(()=>{f.current.reset(),o({type:`RESET`}),d()},[d]),error:c,loading:l,report:s,info:u,simplifyOptimization:h,dedupOptimization:g,quantizeOptimization:_,normalsOptimization:v,texturesOptimization:y}};Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return S}});
@@ -0,0 +1,299 @@
1
+ import { c as e, o as t, t as n } from "./server-communication-Bj6ZxFw2.js";
2
+ import { useCallback as r, useEffect as i, useMemo as a, useReducer as o, useRef as s, useState as c } from "react";
3
+ //#region packages/hooks/src/use-optimize-model/state.ts
4
+ var l = {
5
+ info: null,
6
+ error: null,
7
+ loading: !1,
8
+ report: null
9
+ }, u = (e, t) => {
10
+ switch (t.type) {
11
+ case "LOAD_START": return {
12
+ ...e,
13
+ loading: !0,
14
+ error: null
15
+ };
16
+ case "LOAD_SUCCESS": return {
17
+ ...e,
18
+ loading: !1,
19
+ report: t.payload.report
20
+ };
21
+ case "LOAD_ERROR": return {
22
+ ...e,
23
+ loading: !1,
24
+ error: t.payload
25
+ };
26
+ case "RESET": return { ...l };
27
+ default: return e;
28
+ }
29
+ };
30
+ //#endregion
31
+ //#region packages/hooks/src/use-optimize-model/use-calc-optimization-info.ts
32
+ function d(e) {
33
+ return e ? {
34
+ verticesCount: e.stats.vertices.after ?? 0,
35
+ primitivesCount: e.stats.triangles.after ?? 0,
36
+ meshesCount: e.stats.meshes.after ?? 0,
37
+ textureAssetCount: e.stats.texturesCount.after ?? 0,
38
+ materialsCount: e.stats.materials.after ?? 0,
39
+ sceneBytes: e.optimizedSize ?? 0
40
+ } : {
41
+ verticesCount: 0,
42
+ primitivesCount: 0,
43
+ meshesCount: 0,
44
+ textureAssetCount: 0,
45
+ materialsCount: 0,
46
+ sceneBytes: 0
47
+ };
48
+ }
49
+ var f = (e) => {
50
+ let t = s(null), [n, o] = c(!1), l = e.report;
51
+ return i(() => {
52
+ !n && l && (t.current = l, o(!0));
53
+ }, [n, l]), {
54
+ info: a(() => {
55
+ let e = d(t.current), n = d(l);
56
+ return {
57
+ initial: {
58
+ verticesCount: e.verticesCount,
59
+ primitivesCount: e.primitivesCount,
60
+ meshesCount: e.meshesCount,
61
+ texturesCount: e.textureAssetCount,
62
+ sceneBytes: e.sceneBytes
63
+ },
64
+ optimized: {
65
+ verticesCount: n.verticesCount,
66
+ primitivesCount: n.primitivesCount,
67
+ meshesCount: n.meshesCount,
68
+ texturesCount: n.textureAssetCount,
69
+ sceneBytes: n.sceneBytes
70
+ },
71
+ improvement: {
72
+ verticesCount: e.verticesCount - n.verticesCount,
73
+ primitivesCount: e.primitivesCount - n.primitivesCount,
74
+ meshesCount: e.meshesCount - n.meshesCount,
75
+ texturesCount: e.textureAssetCount - n.textureAssetCount,
76
+ sceneBytes: e.sceneBytes - n.sceneBytes
77
+ }
78
+ };
79
+ }, [l]),
80
+ reset: r(() => {
81
+ t.current = null, o(!1);
82
+ }, [])
83
+ };
84
+ }, p = (e) => {
85
+ let t = e.headers.get("content-type");
86
+ if (!t) throw Error("Missing content type from server response");
87
+ if (t.includes("application/json")) throw Error("Unexpected JSON success response from texture optimizer");
88
+ if (!t.startsWith("image/") && !t.includes("application/octet-stream")) throw Error(`Unexpected content type from server: ${t}`);
89
+ }, m = async (e) => {
90
+ if (e.ok) {
91
+ if (e.headers.get("content-type")?.includes("application/json")) {
92
+ let t = await e.json();
93
+ throw Error(t.error || t.details || "Server returned error");
94
+ }
95
+ p(e);
96
+ }
97
+ }, h = 2e4, g = 2, _ = 50 * 1024 * 1024, v = 4, y = new Set([
98
+ 408,
99
+ 425,
100
+ 429,
101
+ 500,
102
+ 502,
103
+ 503,
104
+ 504
105
+ ]), b = async (e) => {
106
+ await new Promise((t) => setTimeout(t, e));
107
+ }, x = (e) => {
108
+ let t = e.headers.get("content-type");
109
+ return t && t.split(";")[0]?.trim() || "application/octet-stream";
110
+ }, S = (e) => {
111
+ let t = e.headers.get("X-Texture-File-Name") || e.headers.get("X-Texture-Name");
112
+ if (!t || t.trim().length === 0) throw Error("Optimized texture response is missing a canonical file name");
113
+ return t.trim();
114
+ }, C = async (e, t, n, r, i) => {
115
+ let a = 0;
116
+ for (;;) {
117
+ let o = new AbortController(), s = setTimeout(() => o.abort(), r);
118
+ try {
119
+ let r = await fetch(e, {
120
+ method: "POST",
121
+ headers: t,
122
+ body: n,
123
+ signal: o.signal
124
+ });
125
+ if (!r.ok && y.has(r.status) && a < i) {
126
+ a += 1, await b(200 * a + Math.floor(Math.random() * 120));
127
+ continue;
128
+ }
129
+ return r;
130
+ } catch (e) {
131
+ if (a >= i) throw e;
132
+ a += 1, await b(200 * a + Math.floor(Math.random() * 120));
133
+ } finally {
134
+ clearTimeout(s);
135
+ }
136
+ }
137
+ }, w = async (e, t, r, i) => {
138
+ let a = e.getTexturePayload(t.index), o = r.maxTextureUploadBytes ?? _;
139
+ if (a.image.byteLength > o) throw Error(`Texture ${t.index} payload (${a.image.byteLength} bytes) exceeds maxTextureUploadBytes (${o} bytes). Increase maxTextureUploadBytes to allow optimizing larger source textures.`);
140
+ let s = new Uint8Array(a.image.byteLength);
141
+ s.set(a.image);
142
+ let c = s.buffer, { serverOptions: l, ...u } = r, d = n.createRequestHeaders(i, {
143
+ "Content-Type": "application/octet-stream",
144
+ "X-Texture-Index": String(t.index),
145
+ "X-Texture-Name": a.fileName,
146
+ "X-Texture-File-Name": a.fileName,
147
+ "X-Texture-Mime-Type": a.mimeType,
148
+ "X-Optimize-Options": JSON.stringify(u)
149
+ }), f = await C(i.endpoint, d, c, r.requestTimeoutMs ?? h, r.maxRetries ?? g);
150
+ f.ok || await n.handleServerResponseError(f), await m(f);
151
+ let p = Number.parseInt(f.headers.get("X-Texture-Index") || "", 10);
152
+ if (!Number.isFinite(p) || p !== t.index) throw Error(`Texture identity mismatch. Expected ${t.index}, received ${f.headers.get("X-Texture-Index")}`);
153
+ let v = new Uint8Array(await f.arrayBuffer());
154
+ if (v.byteLength === 0) throw Error(`Empty optimized payload for texture ${t.index}`);
155
+ let y = S(f);
156
+ e.replaceTexturePayload(t.index, v, x(f), y);
157
+ }, T = async (e, t) => {
158
+ let r = n.createDefaultServerOptions(t.serverOptions), i = e.listTextureDescriptors(), a = [], o = 0, s = t.maxConcurrentRequests ?? v;
159
+ try {
160
+ for (let n = 0; n < i.length; n += s) {
161
+ let c = i.slice(n, n + s), l = await Promise.allSettled(c.map(async (n) => (await w(e, n, t, r), n.index)));
162
+ for (let [e, t] of l.entries()) {
163
+ if (t.status === "fulfilled") {
164
+ o += 1;
165
+ continue;
166
+ }
167
+ a.push({
168
+ index: c[e].index,
169
+ reason: t.reason instanceof Error ? t.reason.message : String(t.reason)
170
+ });
171
+ }
172
+ }
173
+ if (o > 0 && e.addAppliedOptimization("texture compression"), a.length > 0) {
174
+ let e = a.map((e) => `#${e.index}: ${e.reason}`).join("; "), t = o === 0 ? "Texture optimization failed for all textures." : `Texture optimization failed for ${a.length} of ${i.length} textures.`;
175
+ throw Error(`${t} ${e}`);
176
+ }
177
+ } catch (e) {
178
+ throw console.error("Server-side texture compression failed:", e), Error(`Server-side texture compression failed: ${e}. Client-side texture compression is not supported in browser environments.`, { cause: e });
179
+ }
180
+ }, E = () => {
181
+ throw console.warn("Client-side texture compression is not supported in browser environments. Please enable server-side compression by setting serverOptions.enabled = true."), Error("Texture compression requires server-side processing. Enable serverOptions.enabled = true in your options.");
182
+ }, D = (e) => e?.serverOptions?.enabled ?? !1, O = async (e, t) => {
183
+ e.hasModel() && (D(t) && t ? await T(e, t) : E());
184
+ }, k = () => {
185
+ let [n, a] = o(u, l), { report: c, error: d, loading: p } = n, { info: m, reset: h } = f(n), g = s(new e());
186
+ i(() => {
187
+ let e = g.current;
188
+ return () => {
189
+ e.reset();
190
+ };
191
+ }, []);
192
+ let _ = r(async (e) => {
193
+ a({ type: "LOAD_START" });
194
+ try {
195
+ let t = g.current;
196
+ await t.loadFromThreeJS(e), a({
197
+ type: "LOAD_SUCCESS",
198
+ payload: { report: await t.getReport() }
199
+ });
200
+ } catch (e) {
201
+ a({
202
+ type: "LOAD_ERROR",
203
+ payload: e
204
+ }), console.error("Error loading model:", e);
205
+ }
206
+ }, []), v = r(async (e) => {
207
+ a({ type: "LOAD_START" });
208
+ try {
209
+ let n = g.current, r = {};
210
+ for (let n of Object.values(e.assetData ?? {})) r[n.fileName] = t(n);
211
+ let i = {
212
+ json: e.gltfJson,
213
+ resources: r
214
+ };
215
+ await n.loadFromJSON(i), a({
216
+ type: "LOAD_SUCCESS",
217
+ payload: { report: await n.getReport() }
218
+ });
219
+ } catch (e) {
220
+ throw a({
221
+ type: "LOAD_ERROR",
222
+ payload: e
223
+ }), console.error("Error loading server scene into optimizer:", e), e;
224
+ }
225
+ }, []), y = r(async (e) => {
226
+ if (g.current.hasModel()) try {
227
+ await g.current.simplify(e), a({
228
+ type: "LOAD_SUCCESS",
229
+ payload: { report: await g.current.getReport() }
230
+ });
231
+ } catch (e) {
232
+ console.error("Simplification failed:", e);
233
+ }
234
+ }, []), b = r(async (e) => {
235
+ if (g.current.hasModel()) try {
236
+ await g.current.deduplicate(e), a({
237
+ type: "LOAD_SUCCESS",
238
+ payload: { report: await g.current.getReport() }
239
+ });
240
+ } catch (e) {
241
+ console.error("Deduplication failed:", e);
242
+ }
243
+ }, []), x = r(async (e) => {
244
+ if (g.current.hasModel()) try {
245
+ await g.current.quantize(e), a({
246
+ type: "LOAD_SUCCESS",
247
+ payload: { report: await g.current.getReport() }
248
+ });
249
+ } catch (e) {
250
+ console.error("Quantization failed:", e);
251
+ }
252
+ }, []), S = r(async (e) => {
253
+ if (g.current.hasModel()) try {
254
+ await g.current.optimizeNormals(e), a({
255
+ type: "LOAD_SUCCESS",
256
+ payload: { report: await g.current.getReport() }
257
+ });
258
+ } catch (e) {
259
+ console.error("Normals optimization failed:", e);
260
+ }
261
+ }, []), C = r(async (e) => {
262
+ try {
263
+ await O(g.current, e), a({
264
+ type: "LOAD_SUCCESS",
265
+ payload: { report: await g.current.getReport() }
266
+ });
267
+ } catch (e) {
268
+ throw console.error("Texture optimization failed:", e), e;
269
+ }
270
+ }, []), w = r(async () => {
271
+ if (!g.current.hasModel()) return null;
272
+ try {
273
+ return await g.current.export();
274
+ } catch (e) {
275
+ return console.error("Error getting model binary:", e), null;
276
+ }
277
+ }, []), T = r(() => g.current.hasModel() ? g.current.document : null, []);
278
+ return {
279
+ load: _,
280
+ loadFromServerSceneData: v,
281
+ getModel: w,
282
+ isReady: !!T() && !p,
283
+ _getDocument: T,
284
+ reset: r(() => {
285
+ g.current.reset(), a({ type: "RESET" }), h();
286
+ }, [h]),
287
+ error: d,
288
+ loading: p,
289
+ report: c,
290
+ info: m,
291
+ simplifyOptimization: y,
292
+ dedupOptimization: b,
293
+ quantizeOptimization: x,
294
+ normalsOptimization: S,
295
+ texturesOptimization: C
296
+ };
297
+ };
298
+ //#endregion
299
+ export { k as t };
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`./chunk-gGpyby2o.cjs`);const e=require(`./use-optimize-model-C-fzzKh6.cjs`);exports.useOptimizeModel=e.t;
@@ -0,0 +1,2 @@
1
+ import { t as e } from "./use-optimize-model-Cb3WMvDZ.js";
2
+ export { e as useOptimizeModel };
@@ -0,0 +1,122 @@
1
+ import { ServerOptions, TextureBinaryPayload, TextureCompressOptions } from '../../../core/src/index.ts';
2
+ /**
3
+ * Configuration for a server request.
4
+ */
5
+ export interface ServerRequestConfig {
6
+ /** Server endpoint URL or path */
7
+ endpoint: string;
8
+ /** HTTP method (GET, POST, etc.) */
9
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
10
+ /** Request body (FormData, JSON, or other) */
11
+ body?: FormData | Record<string, unknown> | string;
12
+ /** Optional server options (headers, auth, etc.) */
13
+ serverOptions?: ServerOptions;
14
+ /** Content type for JSON bodies (default: 'application/json') */
15
+ contentType?: string;
16
+ }
17
+ /**
18
+ * Unified server communication service for handling HTTP requests.
19
+ * Provides a consistent interface for API calls with built-in error handling,
20
+ * authentication, and response parsing.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Simple GET request
25
+ * const data = await ServerCommunicationService.request<SceneData>({
26
+ * endpoint: '/api/load-scene',
27
+ * method: 'GET'
28
+ * })
29
+ *
30
+ * // POST with FormData
31
+ * const formData = new FormData()
32
+ * formData.append('model', file)
33
+ * const result = await ServerCommunicationService.request({
34
+ * endpoint: '/api/optimize',
35
+ * method: 'POST',
36
+ * body: formData,
37
+ * serverOptions: { apiKey: 'secret' }
38
+ * })
39
+ *
40
+ * // POST with JSON
41
+ * const response = await ServerCommunicationService.request({
42
+ * endpoint: '/api/save',
43
+ * method: 'POST',
44
+ * body: { sceneId: '123', settings: {...} },
45
+ * serverOptions: { headers: { 'X-Custom': 'value' } }
46
+ * })
47
+ * ```
48
+ */
49
+ export declare class ServerCommunicationService {
50
+ private static extractApiData;
51
+ /**
52
+ * Creates default server options with required endpoint.
53
+ * Merges provided options with defaults.
54
+ */
55
+ static createDefaultServerOptions(serverOptions?: ServerOptions): ServerOptions & Required<Pick<ServerOptions, 'endpoint'>>;
56
+ /**
57
+ * Creates request headers for server communication.
58
+ * Includes authentication token if apiKey is provided.
59
+ */
60
+ static createRequestHeaders(serverOptions?: ServerOptions, additionalHeaders?: HeadersInit): HeadersInit;
61
+ /**
62
+ * Extracts error message from a failed response.
63
+ * Attempts to parse JSON error data, falls back to text.
64
+ */
65
+ static extractErrorMessage(response: Response): Promise<string>;
66
+ /**
67
+ * Handles server response errors and throws appropriate errors.
68
+ */
69
+ static handleServerResponseError(response: Response): Promise<never>;
70
+ /**
71
+ * Prepares FormData for a single texture optimization request.
72
+ */
73
+ static prepareTextureOptimizationFormData(texture: TextureBinaryPayload, options?: TextureCompressOptions): FormData;
74
+ /**
75
+ * Performs a generic HTTP request with error handling.
76
+ * Returns parsed response data.
77
+ *
78
+ * @template T - Expected response data type
79
+ * @param config - Request configuration
80
+ * @returns Promise resolving to parsed response data
81
+ * @throws Error if request fails or response is not ok
82
+ */
83
+ static request<T = unknown>(config: ServerRequestConfig): Promise<T>;
84
+ /**
85
+ * Performs a GET request.
86
+ * Convenience method for common GET operations.
87
+ */
88
+ static get<T = unknown>(endpoint: string, serverOptions?: ServerOptions): Promise<T>;
89
+ /**
90
+ * Performs a POST request with JSON body.
91
+ * Convenience method for common POST operations.
92
+ */
93
+ static post<T = unknown>(endpoint: string, body: Record<string, unknown>, serverOptions?: ServerOptions): Promise<T>;
94
+ /**
95
+ * Performs a POST request with FormData body.
96
+ * Convenience method for file uploads.
97
+ */
98
+ static postFormData<T = unknown>(endpoint: string, formData: FormData, serverOptions?: ServerOptions): Promise<T>;
99
+ /**
100
+ * Fetches scene data from the server.
101
+ * Specialized method for loading 3D scenes with their associated settings.
102
+ *
103
+ * This method handles the vectreal-platform API format which uses:
104
+ * - FormData with 'action' and 'sceneId' parameters
105
+ * - POST request to the scene-settings endpoint
106
+ * - Returns scene data including GLTF JSON and asset data
107
+ *
108
+ * @param sceneId - The unique identifier of the scene to load
109
+ * @param serverOptions - Optional server configuration (endpoint, auth, headers)
110
+ * @returns Promise resolving to the scene data
111
+ * @throws Error if the request fails or scene doesn't exist
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const sceneData = await ServerCommunicationService.loadScene('abc-123', {
116
+ * endpoint: '/api/scenes/abc-123',
117
+ * apiKey: 'optional-auth-token'
118
+ * })
119
+ * ```
120
+ */
121
+ static loadScene<T = unknown>(sceneId: string, serverOptions?: ServerOptions): Promise<T>;
122
+ }
package/project.json DELETED
@@ -1,55 +0,0 @@
1
- {
2
- "name": "vctrl/hooks",
3
- "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "sourceRoot": "packages/hooks/src",
5
- "projectType": "library",
6
- "tags": [],
7
- "targets": {
8
- "lint": {
9
- "executor": "@nx/eslint:lint",
10
- "outputs": ["{options.outputFile}"],
11
- "options": {
12
- "fix": true,
13
- "lintFilePatterns": [
14
- "packages/hooks/**/*.{ts,tsx,js,jsx}",
15
- "packages/hooks/package.json"
16
- ]
17
- }
18
- },
19
- "publish": {
20
- "dependsOn": ["copy-md"],
21
- "executor": "nx:run-commands",
22
- "outputs": [],
23
- "options": {
24
- "command": "npm publish --access public",
25
- "cwd": "build/packages/vctrl/hooks"
26
- }
27
- },
28
- "copy-md": {
29
- "dependsOn": ["build"],
30
- "executor": "nx:run-commands",
31
- "outputs": [],
32
- "options": {
33
- "commands": [
34
- "rsync -rat packages/hooks/*.md build/packages/vctrl/hooks/"
35
- ]
36
- }
37
- },
38
- "build": {
39
- "executor": "@nx/vite:build",
40
- "outputs": ["{options.outputPath}"],
41
- "options": {
42
- "outputPath": "build/packages/vctrl/hooks",
43
- "configFile": "packages/hooks/vite.config.ts"
44
- }
45
- },
46
- "build-ci": {
47
- "executor": "@nx/vite:build",
48
- "outputs": ["{options.outputPath}"],
49
- "options": {
50
- "outputPath": "build/packages/vctrl/hooks",
51
- "configFile": "packages/hooks/vite.config.ts"
52
- }
53
- }
54
- }
55
- }