@vctrl/hooks 0.25.0 → 1.0.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 (46) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +110 -67
  3. package/index.cjs +1 -0
  4. package/index.d.ts +2 -1
  5. package/index.es.js +4 -4
  6. package/package.json +18 -10
  7. package/{chunk-CMqjfN_6.cjs → rolldown-runtime-BocRIvOZ.cjs} +1 -1
  8. package/use-export-model/use-export-model.d.ts +1 -1
  9. package/use-export-model-CORoAKbf.cjs +1 -0
  10. package/{use-export-model-efFsSWe-.js → use-export-model-DXesLypI.js} +1 -1
  11. package/use-export-model.cjs +1 -0
  12. package/use-export-model.es.js +1 -1
  13. package/use-load-model/error-helpers.d.ts +5 -0
  14. package/use-load-model/file-loaders.d.ts +11 -12
  15. package/use-load-model/index.d.ts +1 -1
  16. package/use-load-model/load-context.d.ts +17 -0
  17. package/use-load-model/optimizer-ingest.d.ts +8 -0
  18. package/use-load-model/optimizer-integration.d.ts +3 -4
  19. package/use-load-model/scene-loaders.d.ts +15 -0
  20. package/use-load-model/state.d.ts +14 -14
  21. package/use-load-model/types.d.ts +154 -131
  22. package/use-load-model/use-load-model.d.ts +12 -8
  23. package/use-load-model/utils/index.d.ts +1 -1
  24. package/use-load-model/utils/reconstruct-files.d.ts +27 -5
  25. package/use-load-model/utils/resolve-scene-payload.d.ts +9 -0
  26. package/use-load-model-CRDVzrPC.js +635 -0
  27. package/use-load-model-CZCi6bPM.cjs +1 -0
  28. package/use-load-model.cjs +1 -0
  29. package/use-load-model.es.js +1 -1
  30. package/use-optimize-model/index.d.ts +1 -0
  31. package/use-optimize-model/types.d.ts +3 -1
  32. package/use-optimize-model/use-optimize-model.d.ts +11 -6
  33. package/use-optimize-model-Fwew6qD8.cjs +1 -0
  34. package/{use-optimize-model-D16-4Zwm.js → use-optimize-model-sNXkORNg.js} +94 -76
  35. package/use-optimize-model.cjs +1 -0
  36. package/use-optimize-model.es.js +2 -2
  37. package/utils/server-communication.d.ts +0 -5
  38. package/index.cjs.js +0 -1
  39. package/use-export-model-yQWV3O3H.cjs +0 -1
  40. package/use-export-model.cjs.js +0 -1
  41. package/use-load-model/event-system.d.ts +0 -7
  42. package/use-load-model-B8oqou4I.cjs +0 -1
  43. package/use-load-model-C88r8-HE.js +0 -585
  44. package/use-load-model.cjs.js +0 -1
  45. package/use-optimize-model-CD5zqTLK.cjs +0 -1
  46. package/use-optimize-model.cjs.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0](https://github.com/Vectreal/vectreal-platform/compare/hooks-v0.25.1...hooks-v1.0.0) (2026-09-25)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **core:** `OptimizationStats` fields are renamed. `vertices` -> `verticesCount`, `triangles` -> `primitivesCount`, `materials` -> `materialsCount`, `textures` -> `textureBytes`, `meshes` -> `meshBytes`. `texturesCount`, `meshesCount` and `textureResolutions` are unchanged.
9
+ * **publisher:** `@vctrl/core` makes `ModelOptimizer.ensureModelLoaded` private (`hasModel()` answers the same question) and widens `ExportResult` to include `USDZExportResult`, which an exhaustive consumer switch will notice. `@vctrl/hooks` drops `info` from `OptimizationState`; `useOptimizeModel` still returns it, derived.
10
+ * **hooks:** `ShadowsProps` is a plain interface rather than a union discriminated on `type`, so a consumer still passing `type: 'accumulative'` is passing an unknown property.
11
+ * **publisher:** `@vctrl/viewer` reads `ao` off the shadow options directly rather than only when they were tagged accumulative, so a consumer passing `{ enabled: true, ao: true }` gains the ambient-occlusion pass and its per-frame cost.
12
+ * **publisher:** `@vctrl/core` removes `GridProps`, `ShadowType`, `ShadowTypePropBase`, `ContactShadowProps`, `AccumulativeShadowsProps`, `ExportOptions`, `ExportModelOptions`, `ModifiedTextureResources` and `OptimizationStats.nodes`; `exportThreeJSGLB` takes only the object; `optimizeAll` compresses textures unless `textures: false`. `@vctrl/viewer` removes the `gridOptions` prop, `SceneGrid` and `defaultGridOptions`, and renames `defaultShadowOptions` to `defaultShadowsOptions`. `@vctrl/hooks` replaces `load(files)`, `loadFromData` and `loadFromServer` with a single `load(source)`, replaces `isFileLoading` with a `status` union, and removes the `on`/`off` event bus along with `EventTypes`, `EventHandler`, `LoadData`, `SceneLoadOptions` and `SceneDataLoadOptions`.
13
+
14
+ ### Features
15
+
16
+ * **core,hooks:** the loader reads six formats and resolves every reference through one rule ([#882](https://github.com/Vectreal/vectreal-platform/issues/882)) ([7b52540](https://github.com/Vectreal/vectreal-platform/commit/7b52540233c8258da852be6d3d4374c119e09b16))
17
+ * **scene:** a saved scene stores each asset under the name its glTF uses ([#883](https://github.com/Vectreal/vectreal-platform/issues/883)) ([b0f98cc](https://github.com/Vectreal/vectreal-platform/commit/b0f98cc965d67d8afe77cae1ef432adb54b572ac))
18
+ * **viewer:** play glTF animation clips ([11ff088](https://github.com/Vectreal/vectreal-platform/commit/11ff0880eb32ef0bda03196fe74de4955452d2f1))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **convert:** the page describes the model that is on the stage ([#891](https://github.com/Vectreal/vectreal-platform/issues/891)) ([c2376d9](https://github.com/Vectreal/vectreal-platform/commit/c2376d974f6e7ec16ba6984836fc069059ff2153))
24
+ * **core:** count meshes instead of weighing them ([#783](https://github.com/Vectreal/vectreal-platform/issues/783)) ([9000982](https://github.com/Vectreal/vectreal-platform/commit/900098243213a7e8eb862a77f34ef113c21acb19))
25
+ * **core:** the loader stops claiming it can read USDZ ([#890](https://github.com/Vectreal/vectreal-platform/issues/890)) ([2a3770c](https://github.com/Vectreal/vectreal-platform/commit/2a3770c7039638cf8dee8f80a4da141c00e7a0c3))
26
+ * **embed:** serve embeds the published GLB instead of the editor's assets ([#734](https://github.com/Vectreal/vectreal-platform/issues/734)) ([e5a0bf5](https://github.com/Vectreal/vectreal-platform/commit/e5a0bf5a77d1288abe08303ec5b547416a0ffd32))
27
+ * **hooks:** keep a failed upload from hiding a failed scene load ([5e9676f](https://github.com/Vectreal/vectreal-platform/commit/5e9676f01f61aa53ab74ec3e3fb2d78283fe7045))
28
+ * **publisher:** a failed load costs nothing ([90cf7be](https://github.com/Vectreal/vectreal-platform/commit/90cf7be0f81dbc9e897192c4333f85ec06cf7d25))
29
+ * **publisher:** apply a saved scene once, not on every revalidation ([d903e6d](https://github.com/Vectreal/vectreal-platform/commit/d903e6db95d6876f1c369042fffc8cb5dd6ae860))
30
+ * **publisher:** one answer to which scene is open ([cbcc863](https://github.com/Vectreal/vectreal-platform/commit/cbcc8632e8137e9aba446293367d6d90f1796146))
31
+ * signup return path, search leak, field surfaces and quick wins ([#897](https://github.com/Vectreal/vectreal-platform/issues/897)) ([c3111d8](https://github.com/Vectreal/vectreal-platform/commit/c3111d84b3980d1daf0b7aaa3f4169693051c853))
32
+
33
+
34
+ ### Code Refactoring
35
+
36
+ * **core:** name every metric field by its unit ([#784](https://github.com/Vectreal/vectreal-platform/issues/784)) ([2f7c7d3](https://github.com/Vectreal/vectreal-platform/commit/2f7c7d377a7bfb51199663ad1648895867ebc447))
37
+ * **publisher:** leave each write to the one place that owns it ([0b04d08](https://github.com/Vectreal/vectreal-platform/commit/0b04d08075d455d3a963be1553d2f2e52d5c61dc))
38
+
39
+
40
+ ### Dependencies
41
+
42
+ * The following workspace dependencies were updated
43
+ * dependencies
44
+ * @vctrl/core bumped to 1.0.0
45
+
46
+ ## [0.25.1](https://github.com/Vectreal/vectreal-platform/compare/hooks-v0.25.0...hooks-v0.25.1) (2026-08-08)
47
+
48
+
49
+ ### Bug Fixes
50
+
51
+ * **core:** keep the pristine baseline when syncing the worker's output ([a2e5654](https://github.com/Vectreal/vectreal-platform/commit/a2e5654f6a2acd8dc95cb2e9dace3c32436fc826))
52
+ * **publisher:** stale scene state across navigation and nonsensical optimization stats ([4152ab1](https://github.com/Vectreal/vectreal-platform/commit/4152ab146770f86930a53b892900978a163ae167))
53
+
54
+
55
+ ### Dependencies
56
+
57
+ * The following workspace dependencies were updated
58
+ * dependencies
59
+ * @vctrl/core bumped to 0.25.1
60
+
3
61
  ## [0.25.0](https://github.com/Vectreal/vectreal-platform/compare/hooks-v0.24.1...hooks-v0.25.0) (2026-08-04)
4
62
 
5
63
 
package/README.md CHANGED
@@ -20,7 +20,7 @@ pnpm add @vctrl/hooks
20
20
 
21
21
  | Hook | Import path | Description |
22
22
  | ------------------ | --------------------------------- | ------------------------------------------------------------------------------- |
23
- | `useLoadModel` | `@vctrl/hooks/use-load-model` | Load and parse GLTF, GLB, and USDZ files from file lists or dropped directories |
23
+ | `useLoadModel` | `@vctrl/hooks/use-load-model` | Load and parse every format `@vctrl/core/model-formats` marks importable, from a file list, a dropped folder or a saved scene. `supportedFileTypes` on the return value is that list |
24
24
  | `useOptimizeModel` | `@vctrl/hooks/use-optimize-model` | Run glTF-Transform optimizations on loaded models |
25
25
  | `useExportModel` | `@vctrl/hooks/use-export-model` | Export the current scene to GLB or glTF |
26
26
 
@@ -31,18 +31,19 @@ pnpm add @vctrl/hooks
31
31
  Loads 3D files and exposes the parsed Three.js `Object3D` scene.
32
32
 
33
33
  ```tsx
34
- import { ModelProvider, useLoadModel } from '@vctrl/hooks/use-load-model'
34
+ import { useLoadModel } from '@vctrl/hooks/use-load-model'
35
35
 
36
36
  function Uploader() {
37
- const { load, file, isFileLoading } = useLoadModel()
37
+ const { load, status, file, error } = useLoadModel()
38
38
 
39
39
  const handleDrop = (e: React.DragEvent) => {
40
40
  e.preventDefault()
41
- void load(Array.from(e.dataTransfer.files))
41
+ void load({ kind: 'files', files: Array.from(e.dataTransfer.files) })
42
42
  }
43
43
 
44
- if (isFileLoading) return <p>Loading...</p>
45
- if (file?.model) return <p>Model loaded: {file.name}</p>
44
+ if (status === 'loading') return <p>Loading...</p>
45
+ if (status === 'error') return <p>{error.message}</p>
46
+ if (status === 'ready') return <p>Model loaded: {file.name}</p>
46
47
 
47
48
  return (
48
49
  <div onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
@@ -50,16 +51,70 @@ function Uploader() {
50
51
  </div>
51
52
  )
52
53
  }
54
+ ```
53
55
 
54
- export default function App() {
55
- return (
56
- <ModelProvider>
57
- <Uploader />
58
- </ModelProvider>
59
- )
60
- }
56
+ `useLoadModel` holds its own model. To share one across a tree, mount
57
+ `ModelProvider` and read it with `useModelContext`; see below.
58
+
59
+ ### One entry point, one state
60
+
61
+ `load` handles every way a model arrives, and the hook's state is a
62
+ discriminated union on `status`. `status === 'ready'` and a non-null `file` are
63
+ the same fact, so there is no separate loading flag to fall out of step with
64
+ what is on screen.
65
+
66
+ `load` never rejects. It resolves to the terminal state, and for every source
67
+ but `files` that is also the state you are rendering from.
68
+
69
+ A rejected `files` load is the exception, on purpose: when a model was already
70
+ on screen it stays exactly as it was, so dropping the wrong file does not cost
71
+ the user their scene, and the failure is reported only through the resolved
72
+ value. With nothing on screen there is nothing to protect, and the state is the
73
+ error as usual. Branch on what `load` returns to react to an upload that did
74
+ not take; `reset()` is how you clear a model deliberately.
75
+
76
+ A newer `load` supersedes an older one, so a slow response can never overwrite
77
+ the load that replaced it. The superseded call still resolves, with the state it
78
+ would have produced, so a caller acting on the resolved value rather than on the
79
+ rendered state asks `stillCurrent()` before writing anything down - and asks it
80
+ again after any further await, because a drop can land during an encode as
81
+ easily as during a parse. `reset()` claims a token too, so a load in flight when
82
+ the user clears the model also reports `false`.
83
+
84
+ Resolving is not the same moment as reaching the screen. With an optimizer
85
+ attached, the loaders publish the parsed model first and then await its ingest,
86
+ so `load` resolves one ingest after the viewer changed. A caller that adopts on
87
+ the resolved value therefore keeps printing the previous model's name and byte
88
+ figures around the new one for as long as that ingest takes, and
89
+ `stillCurrent()` cannot close the window, because during it the new load
90
+ genuinely is the current one. Pass `onPublish` to adopt when the stage changes
91
+ instead:
92
+
93
+ ```tsx
94
+ await load({ kind: 'files', files }, {
95
+ onPublish: (published) => adopt(published)
96
+ })
61
97
  ```
62
98
 
99
+ It receives the same `LoadOutcome` the promise resolves to, carrying the same
100
+ `stillCurrent()`. A load superseded before it published never calls it at all,
101
+ so there is nothing to re-check on entry; ask again only after an await of your
102
+ own, for the reason above. Where there is no ingest to wait on (no optimizer
103
+ attached, or a scene source that has none), publishing and resolving are the
104
+ same moment and `onPublish` gains nothing.
105
+
106
+ ### Sources
107
+
108
+ | `kind` | Fields | Use |
109
+ | ------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
110
+ | `files` | `files: (File \| FileSystemDirectoryHandle)[]` | A user upload: a single model, or a bundle with the siblings it refers to. A dropped folder arrives here as plain `File`s; the handle branch is for a caller that opened `showDirectoryPicker()` itself |
111
+ | `scene-data` | `sceneData: ServerScenePayload`, `sceneId?`, `parseMode?` | A payload you already hold. Binary assets may be referenced rather than inlined; the loader fetches them |
112
+ | `server` | `sceneId: string`, `serverOptions?`, `parseMode?` | Fetch a scene from an API endpoint by id |
113
+
114
+ `parseMode: 'direct'` parses the glTF JSON with its assets in memory and skips
115
+ the optimizer: the read-only fast path for viewers. The default reconstructs
116
+ files so the optimizer can ingest exactly what the viewer renders.
117
+
63
118
  ### Context and direct usage
64
119
 
65
120
  `useLoadModel` can be used in two ways:
@@ -69,52 +124,27 @@ export default function App() {
69
124
 
70
125
  ### Return values
71
126
 
72
- | Value | Type | Description |
73
- | -------------------------- | ------------------------------------ | ----------------------------------------------------------- |
74
- | `file` | `ModelFile \| null` | Loaded file metadata and Three.js model |
75
- | `isFileLoading` | `boolean` | True while loading and parsing |
76
- | `progress` | `number` | Progress value from 0 to 100 |
77
- | `load(filesOrDirectories)` | `Promise<void>` | Load files or dropped `FileSystemDirectoryHandle` entries |
78
- | `loadFromData(options)` | `Promise<SceneLoadResult>` | Load already-resolved server scene payload |
79
- | `loadFromServer(options)` | `Promise<SceneLoadResult>` | Fetch and load scene from an API endpoint |
80
- | `reset` | `() => void` | Clear the current model |
81
- | `on` / `off` | Event helpers | Subscribe and unsubscribe to loader lifecycle events |
82
- | `optimizer` | `OptimizerIntegrationReturn \| null` | Populated when the hook is called with `useOptimizeModel()` |
83
-
84
- ### `useLoadModel` events
85
-
86
- `on` and `off` support these typed events:
87
-
88
- | Event | Payload |
89
- | ---------------------- | ------------------- |
90
- | `multiple-models` | `File[]` |
91
- | `not-loaded-files` | `File[]` |
92
- | `load-start` | `null` |
93
- | `load-progress` | `number` |
94
- | `load-complete` | `ModelFile \| null` |
95
- | `load-reset` | `null` |
96
- | `load-error` | `Error \| unknown` |
97
- | `server-load-start` | `string` |
98
- | `server-load-complete` | `SceneLoadResult` |
99
- | `server-load-error` | `Error \| unknown` |
100
-
101
- ### Scene loading option types
102
-
103
- `loadFromServer(options)` uses:
104
-
105
- | Field | Type | Description |
106
- | --------------- | --------------- | --------------------------------------------- |
107
- | `sceneId` | `string` | Scene identifier to fetch |
108
- | `serverOptions` | `ServerOptions` | Endpoint, auth, and header configuration |
109
- | `applySettings` | `boolean` | Whether scene settings are applied after load |
110
-
111
- `loadFromData(options)` uses:
112
-
113
- | Field | Type | Description |
114
- | --------------- | --------------------- | -------------------------------------------------------------- |
115
- | `sceneId` | `string \| undefined` | Optional scene identifier |
116
- | `sceneData` | `ServerSceneData` | Already-resolved payload containing glTF, settings, and assets |
117
- | `applySettings` | `boolean` | Whether scene settings are applied after load |
127
+ | Value | Type | Description |
128
+ | -------------- | ------------------------------------------------- | ----------------------------------------------------------------- |
129
+ | `status` | `'empty' \| 'loading' \| 'ready' \| 'error'` | The load, as one value |
130
+ | `file` | `ModelFile \| null` | Loaded file metadata and Three.js model. Non-null exactly when `status` is `'ready'` |
131
+ | `error` | `StructuredLoadError \| null` | Non-null exactly when `status` is `'error'` |
132
+ | `sceneData` | `ServerSceneData \| undefined` | The resolved payload, for scene sources |
133
+ | `progress` | `number` | Progress value from 0 to 100 |
134
+ | `source` | `'files' \| 'scene-data' \| 'server' \| null` | What the current state came from |
135
+ | `load(source, options?)` | `Promise<LoadOutcome>` | Load a model; resolves to the terminal state plus `stillCurrent()`. `options.onPublish` fires earlier, when the model reaches the screen |
136
+ | `supportedFileTypes` | `ModelFileTypes[]` | Every format the loader accepts, from `@vctrl/core/model-formats` |
137
+ | `reset` | `() => void` | Clear the current model and retire any load in flight |
138
+ | `optimizer` | `OptimizerIntegrationReturn<true> \| null` | Populated when the hook is called with `useOptimizeModel()` |
139
+
140
+ ### Error codes
141
+
142
+ `error.code` is one of `unsupported_format`, `multiple_models`,
143
+ `binary_load_failed`, `gltf_load_failed`, `missing_assets`,
144
+ `server_load_failed`, `not_found`, `quota_exceeded`, `unknown`. Optimizer
145
+ ingest is deliberately not among them: the model is already on screen by then,
146
+ so a failure there costs the optimize step and is reported on
147
+ `optimizer.error`.
118
148
 
119
149
  ---
120
150
 
@@ -200,20 +230,28 @@ function ExportButton({ file }: { file: ModelFile | null }) {
200
230
 
201
231
  ### Methods
202
232
 
203
- | Method | Description |
204
- | -------------------------------------------------------------- | ------------------------------------------------------------------- |
205
- | `handleThreeGltfExport(file, binary)` | Export a loaded Three.js model to `.glb` or a zipped `.gltf` bundle |
206
- | `handleDocumentGltfExport(document, file, binary?, download?)` | Export from a glTF-Transform `Document` |
233
+ | Method | Description |
234
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------ |
235
+ | `handleThreeGltfExport(file, binary)` | Export a loaded Three.js model to `.glb` or a zipped `.gltf` bundle |
236
+ | `handleThreeUsdzExport(file)` | Export a loaded Three.js model to `.usdz` for AR QuickLook |
237
+ | `handleDocumentGltfExport(document, file, binary?, download?)` | Export from a glTF-Transform `Document` |
238
+ | `handleDocumentGlbDracoExport(document, file)` | Export a `Document` to `.glb` with Draco geometry compression |
207
239
 
208
- `binary = true` writes `.glb`; `binary = false` writes a zipped `.gltf` package.
240
+ `binary = true` writes `.glb`; `binary = false` writes a zipped `.gltf` package. Pass
241
+ `download = false` to `handleDocumentGltfExport` to get the `GLTFExportResult` back
242
+ instead of saving a file.
209
243
 
210
244
  ---
211
245
 
212
246
  ## Additional exports
213
247
 
214
- - `reconstructGltfFiles` from `@vctrl/hooks`
248
+ - `reconstructGltfFiles` and `createBrowserTextureEncoder` from `@vctrl/hooks`
215
249
  - `ModelProvider` and `useModelContext` from `@vctrl/hooks/use-load-model`
216
- - Shared types such as `ModelFile`, `SceneLoadResult`, and `ServerSceneData`
250
+ - Shared types such as `ModelFile`, `SceneLoadResult`, `ServerSceneData`, and `OptimizerIntegrationReturn`
251
+
252
+ `createBrowserTextureEncoder()` returns the `OffscreenCanvas` encoder the hook injects
253
+ into `texturesOptimization`. Pass it as `ModelOptimizer#compressTextures`'s
254
+ `encoder` option when driving the optimizer directly.
217
255
 
218
256
  ---
219
257
 
@@ -222,7 +260,12 @@ function ExportButton({ file }: { file: ModelFile | null }) {
222
260
  | Package | Version |
223
261
  | ------- | -------------- |
224
262
  | `react` | `^18 \|\| ^19` |
225
- | `three` | `^0.177` |
263
+ | `three` | see below |
264
+
265
+ Install `three` yourself and declare it explicitly in your own `package.json`, so your
266
+ project resolves exactly one copy: Three.js uses global singletons internally and
267
+ duplicate instances produce subtle rendering bugs. Let your package manager resolve the
268
+ version against the declared peer range rather than pinning one from this document.
226
269
 
227
270
  ---
228
271
 
package/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-load-model-CZCi6bPM.cjs"),t=require("./use-export-model-CORoAKbf.cjs"),n=require("./use-optimize-model-Fwew6qD8.cjs");exports.createBrowserTextureEncoder=n.n,exports.reconstructGltfFiles=e.r,exports.useExportModel=t.t,exports.useLoadModel=e.t,exports.useOptimizeModel=n.t;
package/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { default as useLoadModel } from './use-load-model/use-load-model';
2
2
  export { default as useExportModel } from './use-export-model/use-export-model';
3
3
  export { default as useOptimizeModel } from './use-optimize-model/use-optimize-model';
4
4
  export { reconstructGltfFiles } from './use-load-model/utils/reconstruct-files';
5
- export type { EventTypes, EventHandler, ModelFile, LoadData, UseLoadModelReturn, SceneDataLoadOptions, SceneLoadOptions, ServerSceneData, SceneLoadResult } from './use-load-model/types';
5
+ export { createBrowserTextureEncoder } from './use-optimize-model/utils';
6
+ export type { LoadOptions, LoadOutcome, ModelFile, ModelSource, ModelSourceKind, ModelState, OptimizerIntegrationReturn, UseLoadModelReturn, ServerSceneData, SceneLoadResult } from './use-load-model/types';
package/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { r as e, t } from "./use-load-model-C88r8-HE.js";
2
- import { t as n } from "./use-export-model-efFsSWe-.js";
3
- import { t as r } from "./use-optimize-model-D16-4Zwm.js";
4
- export { e as reconstructGltfFiles, n as useExportModel, t as useLoadModel, r as useOptimizeModel };
1
+ import { r as e, t } from "./use-load-model-CRDVzrPC.js";
2
+ import { t as n } from "./use-export-model-DXesLypI.js";
3
+ import { n as r, t as i } from "./use-optimize-model-sNXkORNg.js";
4
+ export { r as createBrowserTextureEncoder, e as reconstructGltfFiles, n as useExportModel, t as useLoadModel, i as useOptimizeModel };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.25.0",
2
+ "version": "1.0.0",
3
3
  "name": "@vctrl/hooks",
4
4
  "description": "vctrl/hooks is a React hooks package designed to simplify 3D model loading and management within React applications. It's part of the vectreal-core ecosystem and is primarily used in the vctrl/viewer React component and the official website application.",
5
5
  "bugs": {
@@ -29,40 +29,48 @@
29
29
  },
30
30
  "license": "AGPL-3.0-only",
31
31
  "types": "./index.d.ts",
32
- "main": "./index.cjs.js",
32
+ "main": "./index.cjs",
33
33
  "module": "./index.es.js",
34
34
  "type": "module",
35
35
  "publishConfig": {
36
- "access": "public"
36
+ "access": "public",
37
+ "directory": "../../build/packages/vctrl/hooks"
37
38
  },
38
39
  "exports": {
39
40
  ".": {
40
41
  "types": "./index.d.ts",
41
42
  "import": "./index.es.js",
42
- "require": "./index.cjs.js"
43
+ "require": "./index.cjs"
43
44
  },
44
45
  "./use-load-model": {
45
46
  "types": "./use-load-model/index.d.ts",
46
47
  "import": "./use-load-model.es.js",
47
- "require": "./use-load-model.cjs.js"
48
+ "require": "./use-load-model.cjs"
48
49
  },
49
50
  "./use-export-model": {
50
51
  "types": "./use-export-model/index.d.ts",
51
52
  "import": "./use-export-model.es.js",
52
- "require": "./use-export-model.cjs.js"
53
+ "require": "./use-export-model.cjs"
53
54
  },
54
55
  "./use-optimize-model": {
55
56
  "types": "./use-optimize-model/index.d.ts",
56
57
  "import": "./use-optimize-model.es.js",
57
- "require": "./use-optimize-model.cjs.js"
58
+ "require": "./use-optimize-model.cjs"
58
59
  }
59
60
  },
60
61
  "peerDependencies": {
61
62
  "react": "^18.0.0 || ^19.0.0",
62
- "three": "^0.177.0"
63
+ "three": "^0.185.1"
63
64
  },
64
65
  "dependencies": {
65
- "@vctrl/core": "^0.25.0",
66
- "file-saver": "^2.0.5"
66
+ "@gltf-transform/core": "^4.4.2",
67
+ "file-saver": "^2.0.5",
68
+ "@vctrl/core": "^1.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@nx/vite": "23.1.1",
72
+ "@vitejs/plugin-react": "^6.0.5",
73
+ "vite": "^8.2.1",
74
+ "vite-plugin-dts": "5.0.3"
67
75
  }
68
76
  }
@@ -1 +1 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return s}});
@@ -9,7 +9,7 @@ import { ModelFile } from '../use-load-model';
9
9
  */
10
10
  declare const useExportModel: (onSaved?: () => void, onError?: (error: Error) => void) => {
11
11
  handleThreeGltfExport: (file: ModelFile | null, binary: boolean) => Promise<void>;
12
- handleDocumentGltfExport: (jsonDocument: Document, file: ModelFile | null, binary?: boolean, download?: boolean) => Promise<import('../../../core/src/model-exporter/index.ts').GLTFExportResult | undefined>;
12
+ handleDocumentGltfExport: (jsonDocument: Document, file: ModelFile | null, binary?: boolean, download?: boolean) => Promise<import('../../../core/src/index.ts').GLTFExportResult | undefined>;
13
13
  handleThreeUsdzExport: (file: ModelFile | null) => Promise<void>;
14
14
  handleDocumentGlbDracoExport: (jsonDocument: Document, file: ModelFile | null) => Promise<void>;
15
15
  };
@@ -0,0 +1 @@
1
+ const e=require("./rolldown-runtime-BocRIvOZ.cjs");let t=require("react"),n=require("@vctrl/core/model-exporter"),r=require("file-saver");r=e.t(r,1);var i=(e,i)=>{let a=(0,t.useRef)(new n.ModelExporter),o=(0,t.useCallback)(async(t,n)=>{let o=t?.model;if(!o){console.error(`Scene not initialized`);return}try{let i=a.current,s=t.name.replace(/\.[^/.]+$/,``);if(n){let e=await i.exportThreeJSGLB(o);if(!(e.data instanceof Uint8Array))throw Error(`Expected GLB export to return Uint8Array`);r.default.saveAs(new Blob([new Uint8Array(e.data)]),`${s}.glb`)}else{let e=await i.exportThreeJSGLTF(o),t=await i.createZIPArchive(e,s);r.default.saveAs(new Blob([new Uint8Array(t)]),`${s}.zip`)}e&&e()}catch(e){console.error(`Export failed:`,e),i&&i(e)}},[e,i]),s=(0,t.useCallback)(async t=>{let n=t?.model;if(!n){console.error(`Scene not initialized`);return}try{let i=a.current,o=t.name.replace(/\.[^/.]+$/,``),s=await i.exportThreeJSUSDZ(n);r.default.saveAs(new Blob([new Uint8Array(s.data)]),`${o}.usdz`),e&&e()}catch(e){console.error(`USDZ export failed:`,e),i&&i(e)}},[e,i]),c=(0,t.useCallback)(async(t,n)=>{try{let i=a.current,o=n?.name.replace(/\.[^/.]+$/,``)||`model`,s=await i.exportDocumentGLBDraco(t);r.default.saveAs(new Blob([new Uint8Array(s.data)]),`${o}.glb`),e&&e()}catch(e){console.error(`Draco GLB export failed:`,e),i&&i(e)}},[e,i]);return{handleThreeGltfExport:o,handleDocumentGltfExport:(0,t.useCallback)(async function(t,n,o=!1,s=!0){try{let i=a.current,c=n?.name.replace(/\.[^/.]+$/,``)||`model`;if(o){let e=await i.exportDocumentGLB(t);r.default.saveAs(new Blob([new Uint8Array(e.data)]),`${c}.glb`)}else{let e=await i.exportDocumentGLTF(t);if(!s)return e;let n=await i.createZIPArchive(e,c);r.default.saveAs(new Blob([new Uint8Array(n)]),`${c}.zip`)}e&&e()}catch(e){console.error(`Export failed:`,e),i&&i(e)}},[i,e]),handleThreeUsdzExport:s,handleDocumentGlbDracoExport:c}};Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return i}});
@@ -12,7 +12,7 @@ var i = (i, a) => {
12
12
  try {
13
13
  let a = o.current, s = e.name.replace(/\.[^/.]+$/, "");
14
14
  if (t) {
15
- let e = await a.exportThreeJSGLB(n, {});
15
+ let e = await a.exportThreeJSGLB(n);
16
16
  if (!(e.data instanceof Uint8Array)) throw Error("Expected GLB export to return Uint8Array");
17
17
  r.saveAs(new Blob([new Uint8Array(e.data)]), `${s}.glb`);
18
18
  } else {
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./use-export-model-CORoAKbf.cjs");exports.useExportModel=e.t;
@@ -1,2 +1,2 @@
1
- import { t as e } from "./use-export-model-efFsSWe-.js";
1
+ import { t as e } from "./use-export-model-DXesLypI.js";
2
2
  export { e as useExportModel };
@@ -1,4 +1,9 @@
1
1
  import { StructuredLoadError } from './types';
2
+ /**
3
+ * A failure that already describes itself. The loaders throw these deliberately,
4
+ * so normalizing one again would flatten its code and message into "unknown".
5
+ */
6
+ export declare const isStructuredLoadError: (error: unknown) => error is StructuredLoadError;
2
7
  export declare const createStructuredLoadError: ({ code, message, recoverable, source, cause, context }: StructuredLoadError) => StructuredLoadError;
3
8
  export declare const toErrorMessage: (error: unknown) => string;
4
9
  export declare const normalizeLocalLoadError: (error: unknown, code: StructuredLoadError["code"], context?: Record<string, unknown>) => StructuredLoadError;
@@ -1,12 +1,11 @@
1
- import { ModelFileTypes, ModelLoader } from '../../../core/src/model-loader/index.ts';
2
- import { default as React } from 'react';
3
- import { Action, ModelFile } from './types';
4
- import { useOptimizeModel } from '../use-optimize-model';
5
- type Optimizer = ReturnType<typeof useOptimizeModel> | undefined;
6
- type Dispatch = React.Dispatch<Action>;
7
- export declare const loadBinaryModel: (file: File, fileType: ModelFileTypes, modelLoader: ModelLoader, optimizer: Optimizer, dispatch: Dispatch) => Promise<ModelFile | null>;
8
- export declare const loadGltfModel: (gltfFile: File, otherFiles: File[], modelLoader: ModelLoader, optimizer: Optimizer, dispatch: Dispatch, calculateReferencedBytesFromFiles: (gltfFile: File, otherFiles: File[]) => Promise<{
9
- sourcePackageBytes: number;
10
- textureBytes: number;
11
- }>) => Promise<ModelFile | null>;
12
- export {};
1
+ import { InputFileOrDirectory, LoadedModel } from './types';
2
+ import { LoadContext } from './load-context';
3
+ /**
4
+ * Loads a model the user picked: a single file, or a bundle with the siblings
5
+ * it refers to.
6
+ *
7
+ * Throws a `StructuredLoadError` for every rejection, including "nothing usable
8
+ * in here" and "more than one model in here". Those used to be silent returns,
9
+ * which left the caller waiting on a load that would never finish.
10
+ */
11
+ export declare const loadModelFromFiles: (filesOrDirectories: InputFileOrDirectory, ctx: LoadContext) => Promise<LoadedModel>;
@@ -1,5 +1,5 @@
1
1
  export { default as useLoadModel } from './use-load-model';
2
- export { type EventHandler, type EventTypes, type InputFileOrDirectory, type LoadData, type ModelFile, type SceneDataLoadOptions, type SceneLoadOptions, type SceneLoadResult, type StructuredLoadError, type UseLoadModelReturn, type ViewerLoadErrorCode, type ServerSceneData } from './types';
2
+ export { type InputFileOrDirectory, type LoadedModel, type LoadOptions, type LoadOutcome, type ModelFile, type ModelSource, type ModelSourceKind, type ModelState, type OptimizerIntegrationReturn, type SceneLoadResult, type ServerSceneData, type StructuredLoadError, type UseLoadModelReturn, type ViewerLoadErrorCode } from './types';
3
3
  export { reconstructGltfFiles } from './utils/reconstruct-files';
4
4
  export { fetchManifestAssetData, type FetchManifestAssetsOptions } from './utils/fetch-manifest-assets';
5
5
  export * from './model-context';
@@ -0,0 +1,17 @@
1
+ import { LoadedModel } from './types';
2
+ import { useOptimizeModel } from '../use-optimize-model';
3
+ import { ModelLoader } from '../../../core/src/model-loader/index.ts';
4
+ export type Optimizer = ReturnType<typeof useOptimizeModel> | undefined;
5
+ /**
6
+ * What every per-source loader needs, and nothing else.
7
+ *
8
+ * `publish` exists because a load has two useful moments: the model is parsed
9
+ * and can be rendered, and the optimizer has finished ingesting it. Publishing
10
+ * the first means the viewer never waits on the second.
11
+ */
12
+ export interface LoadContext {
13
+ modelLoader: ModelLoader;
14
+ optimizer: Optimizer;
15
+ publish: (loaded: LoadedModel) => void;
16
+ onProgress: (progress: number) => void;
17
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Optimizer ingest is not part of "is the model on screen".
3
+ *
4
+ * The model is always published before this runs, so a failure here must not
5
+ * turn a visible scene into an error state. It costs the optimize step, which
6
+ * `optimizer.error` and `optimizer.isPreparing` already report.
7
+ */
8
+ export declare const ingestIntoOptimizer: (ingest: () => Promise<void>, fallback?: () => Promise<void>) => Promise<void>;
@@ -1,6 +1,5 @@
1
1
  import { ModelLoader } from '../../../core/src/model-loader/index.ts';
2
- import { default as React } from 'react';
3
- import { Action, ModelFile, OptimizerIntegrationReturn } from './types';
2
+ import { ModelFile, OptimizerIntegrationReturn } from './types';
4
3
  import { useOptimizeModel } from '../use-optimize-model';
5
4
  /**
6
5
  * Integrates the optimizer into the model loading process.
@@ -9,8 +8,8 @@ import { useOptimizeModel } from '../use-optimize-model';
9
8
  * 1. Runs an optional optimization function
10
9
  * 2. Exports the optimized model as GLB
11
10
  * 3. Reloads it into Three.js
12
- * 4. Updates the model state
11
+ * 4. Swaps it into the loaded model, leaving the load state itself untouched
13
12
  *
14
13
  * Returns `null` when no optimizer instance is provided.
15
14
  */
16
- export declare function useOptimizerIntegration(instance: ReturnType<typeof useOptimizeModel> | undefined, dispatch: React.Dispatch<Action>, file: ModelFile | null, modelLoader: ModelLoader): OptimizerIntegrationReturn<boolean>;
15
+ export declare function useOptimizerIntegration(instance: ReturnType<typeof useOptimizeModel> | undefined, replaceModel: (file: ModelFile) => void, file: ModelFile | null, modelLoader: ModelLoader): OptimizerIntegrationReturn<boolean>;
@@ -0,0 +1,15 @@
1
+ import { LoadedModel, ModelSource } from './types';
2
+ import { LoadContext } from './load-context';
3
+ type SceneDataSource = Extract<ModelSource, {
4
+ kind: 'scene-data';
5
+ }>;
6
+ type ServerSource = Extract<ModelSource, {
7
+ kind: 'server';
8
+ }>;
9
+ export declare const loadModelFromSceneData: (source: SceneDataSource, ctx: LoadContext, assetHeaders?: HeadersInit) => Promise<LoadedModel>;
10
+ /**
11
+ * Fetches a scene by id and loads it. Prefers the manifest endpoint and falls
12
+ * back to the legacy form-post contract for scenes the manifest cannot serve.
13
+ */
14
+ export declare const loadModelFromServer: (source: ServerSource, ctx: LoadContext) => Promise<LoadedModel>;
15
+ export {};
@@ -1,19 +1,19 @@
1
- import { Action, LoadData } from './types';
1
+ import { ModelFileTypes } from '../../../core/src/model-loader/index.ts';
2
+ import { LoadedModel, ModelSourceKind, ModelState, StructuredLoadError } from './types';
2
3
  /**
3
- * Initial state for the useReadModelFiles hook
4
+ * Model formats the loader accepts, reported on `useLoadModel`'s return.
4
5
  *
5
- * @property file - Currently loaded file
6
- * @property isFileLoading - Flag indicating if a file is currently being loaded
7
- * @property progress - Current loading progress (0-100)
8
- * @property supportedFileTypes - List of supported file types
6
+ * It was a hand-maintained array restating the three ids - one of the nine
7
+ * independent statements of a set nothing kept in agreement. It is now read
8
+ * from the owner. The export stays because it is part of `@vctrl/hooks`'
9
+ * published return type, and no app code reads it.
9
10
  */
10
- export declare const initialState: LoadData;
11
+ export declare const supportedFileTypes: ModelFileTypes[];
11
12
  /**
12
- * Reducer function for the useReadModelFiles hook
13
- *
14
- * @param state - The current state of the reducer
15
- * @param action - The action to be performed
16
- * @returns The updated state
13
+ * The four states a load can be in. Building them here rather than spreading
14
+ * partial updates is what keeps `status`, `file` and `error` from drifting apart.
17
15
  */
18
- declare function reducer(state: LoadData, action: Action): LoadData;
19
- export default reducer;
16
+ export declare const emptyModelState: ModelState;
17
+ export declare const loadingModelState: (source: ModelSourceKind, progress?: number) => ModelState;
18
+ export declare const readyModelState: (source: ModelSourceKind, loaded: LoadedModel) => ModelState;
19
+ export declare const errorModelState: (source: ModelSourceKind, error: StructuredLoadError) => ModelState;