three-cad-viewer 4.1.2 → 4.2.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 (58) hide show
  1. package/Readme.md +12 -5
  2. package/dist/camera/camera.d.ts +14 -2
  3. package/dist/core/studio-manager.d.ts +91 -0
  4. package/dist/core/types.d.ts +260 -9
  5. package/dist/core/viewer-state.d.ts +28 -2
  6. package/dist/core/viewer.d.ts +200 -6
  7. package/dist/index.d.ts +7 -2
  8. package/dist/rendering/environment.d.ts +239 -0
  9. package/dist/rendering/light-detection.d.ts +44 -0
  10. package/dist/rendering/material-factory.d.ts +77 -2
  11. package/dist/rendering/material-presets.d.ts +32 -0
  12. package/dist/rendering/room-environment.d.ts +13 -0
  13. package/dist/rendering/studio-composer.d.ts +130 -0
  14. package/dist/rendering/studio-floor.d.ts +53 -0
  15. package/dist/rendering/texture-cache.d.ts +142 -0
  16. package/dist/rendering/triplanar.d.ts +37 -0
  17. package/dist/scene/animation.d.ts +1 -1
  18. package/dist/scene/clipping.d.ts +31 -0
  19. package/dist/scene/nestedgroup.d.ts +64 -27
  20. package/dist/scene/objectgroup.d.ts +47 -0
  21. package/dist/three-cad-viewer.css +339 -29
  22. package/dist/three-cad-viewer.esm.js +27567 -11874
  23. package/dist/three-cad-viewer.esm.js.map +1 -1
  24. package/dist/three-cad-viewer.esm.min.js +10 -4
  25. package/dist/three-cad-viewer.js +27486 -11787
  26. package/dist/three-cad-viewer.min.js +10 -4
  27. package/dist/ui/display.d.ts +147 -0
  28. package/dist/utils/decode-instances.d.ts +60 -0
  29. package/dist/utils/utils.d.ts +10 -0
  30. package/package.json +4 -2
  31. package/src/_version.ts +1 -1
  32. package/src/camera/camera.ts +27 -10
  33. package/src/core/studio-manager.ts +682 -0
  34. package/src/core/types.ts +328 -9
  35. package/src/core/viewer-state.ts +84 -4
  36. package/src/core/viewer.ts +453 -22
  37. package/src/index.ts +25 -1
  38. package/src/rendering/environment.ts +840 -0
  39. package/src/rendering/light-detection.ts +327 -0
  40. package/src/rendering/material-factory.ts +456 -2
  41. package/src/rendering/material-presets.ts +303 -0
  42. package/src/rendering/raycast.ts +2 -2
  43. package/src/rendering/room-environment.ts +192 -0
  44. package/src/rendering/studio-composer.ts +577 -0
  45. package/src/rendering/studio-floor.ts +108 -0
  46. package/src/rendering/texture-cache.ts +1020 -0
  47. package/src/rendering/triplanar.ts +329 -0
  48. package/src/scene/animation.ts +3 -2
  49. package/src/scene/clipping.ts +59 -0
  50. package/src/scene/nestedgroup.ts +399 -0
  51. package/src/scene/objectgroup.ts +186 -11
  52. package/src/scene/orientation.ts +12 -0
  53. package/src/scene/render-shape.ts +55 -21
  54. package/src/types/n8ao.d.ts +28 -0
  55. package/src/ui/display.ts +1032 -27
  56. package/src/ui/index.html +181 -44
  57. package/src/utils/decode-instances.ts +233 -0
  58. package/src/utils/utils.ts +33 -20
package/src/ui/index.html CHANGED
@@ -3,14 +3,18 @@
3
3
 
4
4
  <div class="tcv_cad_body">
5
5
  <div class="tcv_cad_navigation">
6
+ <div class="tcv_toggle_tools_wrapper">
7
+ <span class='tcv_toggle_tools'></span><span class="tcv_tools_label">Tools</span>
8
+ </div>
6
9
  <div class="tcv_cad_tree tcv_round">
7
10
  <div class="tcv_tabnav">
8
11
  <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_tree tcv_tab tcv_tab-left tcv_tab-selected' value="Tree" type="button" style="width: 100%" /></span>
9
12
  <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_clip tcv_tab tcv_tab-right tcv_tab-unselected' value="Clip" type="button" style="width: 100%" /></span>
10
- <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_material tcv_tab tcv_tab-right tcv_tab-unselected' value="Material" type="button" style="width: 100%" /></span>
11
13
  <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_zebra tcv_tab tcv_tab-right tcv_tab-unselected' value="Zebra" type="button" style="width: 100%" /></span>
14
+ <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_material tcv_tab tcv_tab-right tcv_tab-unselected' value="Material" type="button" style="width: 100%" /></span>
15
+ <span class="tcv_tooltip" style="flex: 1"><input class='tcv_tab_studio tcv_tab tcv_tab-right tcv_tab-unselected' value="Studio" type="button" style="width: 100%" /></span>
12
16
  </div>
13
- <div class="tcv_cad_tree_toggles">
17
+ <div class="tcv_cad_tree_toggles tcv_toggles_tree">
14
18
  <span class="tcv_tooltip" data-tooltip="Collpase nodes with a single leaf">
15
19
  <input class='tcv_collapse_singles tcv_btn tcv_small_btn' value="1" type="button" />
16
20
  </span>
@@ -24,6 +28,30 @@
24
28
  <input class='tcv_expand tcv_btn tcv_small_btn' value="E" type="button" />
25
29
  </span>
26
30
  </div>
31
+ <div class="tcv_cad_tree_toggles tcv_toggles_clip" style="display: none;">
32
+ <span class="tcv_tooltip" data-tooltip="Reset to default clip planes">
33
+ <input class='tcv_clip_reset tcv_btn tcv_small_btn' value="R" type="button" />
34
+ </span>
35
+ </div>
36
+ <div class="tcv_cad_tree_toggles tcv_toggles_material" style="display: none;">
37
+ <span class="tcv_tooltip" data-tooltip="Reset to original values">
38
+ <input class='tcv_material_reset tcv_btn tcv_small_btn' value="R" type="button" />
39
+ </span>
40
+ </div>
41
+ <div class="tcv_cad_tree_toggles tcv_toggles_zebra" style="display: none;">
42
+ <span class="tcv_tooltip" data-tooltip="Reset to default zebra settings">
43
+ <input class='tcv_zebra_reset tcv_btn tcv_small_btn' value="R" type="button" />
44
+ </span>
45
+ </div>
46
+ <div class="tcv_cad_tree_toggles tcv_toggles_studio" style="display: none;">
47
+ <div class="tcv_studio_spinner"></div>
48
+ <span class="tcv_tooltip" data-tooltip="Edit material of selected object">
49
+ <input class='tcv_mat_editor_toggle tcv_btn tcv_small_btn' value="E" type="button" />
50
+ </span>
51
+ <span class="tcv_tooltip" data-tooltip="Reset to default values">
52
+ <input class='tcv_studio_reset tcv_btn tcv_small_btn' value="R" type="button" />
53
+ </span>
54
+ </div>
27
55
  <div class="tcv_box_content tcv_mac-scrollbar tcv_scroller">
28
56
  <div class="tcv_cad_tree_container"></div>
29
57
  <div class="tcv_cad_clip_container">
@@ -84,81 +112,62 @@
84
112
  </div>
85
113
  </div>
86
114
  <div class="tcv_cad_material_container">
87
- <div class="tcv_cad_tree_toggles">
88
- <span class="tcv_tooltip" data-tooltip="Reset to original values">
89
- <input class='tcv_material_reset tcv_btn tcv_small_btn' value="R" type="button" />
90
- </span>
91
- </div>
92
- <div class="tcv_material_ambientlight tcv_label">
93
- Ambient light intensity (%)
94
- </div>
95
- <div class="tcv_slider_group">
96
- <div>
115
+ <div class="tcv_studio_row">
116
+ <span class="tcv_label">Ambient Light</span>
117
+ <div class="tcv_studio_slider_group">
97
118
  <input type="range" min="0" max="20" value="1"
98
119
  class="tcv_sld_value_ambientlight tcv_clip_slider">
99
120
  <input value=1 class="tcv_inp_value_ambientlight tcv_clip_input"></input>
100
121
  </div>
101
122
  </div>
102
- <div class="tcv_material_pointlight tcv_label">
103
- Directional light intensity (%)
104
- </div>
105
- <div class="tcv_slider_group">
106
- <div>
123
+ <div class="tcv_studio_row">
124
+ <span class="tcv_label">Direct Light</span>
125
+ <div class="tcv_studio_slider_group">
107
126
  <input type="range" min="0" max="40" value="1"
108
127
  class="tcv_sld_value_pointlight tcv_clip_slider">
109
128
  <input value=1 class="tcv_inp_value_pointlight tcv_clip_input"></input>
110
129
  </div>
111
130
  </div>
112
- <div class="tcv_material_metalness tcv_label">
113
- Metalness (%)
114
- </div>
115
- <div class="tcv_slider_group">
116
- <div>
131
+ <div class="tcv_studio_row">
132
+ <span class="tcv_label">Metalness</span>
133
+ <div class="tcv_studio_slider_group">
117
134
  <input type="range" min="0" max="100" value="40"
118
135
  class="tcv_sld_value_metalness tcv_clip_slider">
119
136
  <input value=40 class="tcv_inp_value_metalness tcv_clip_input"></input>
120
137
  </div>
121
138
  </div>
122
- <div class="tcv_material_roughness tcv_label">
123
- Roughness (%)
124
- </div>
125
- <div class="tcv_slider_group">
126
- <div>
139
+ <div class="tcv_studio_row">
140
+ <span class="tcv_label">Roughness</span>
141
+ <div class="tcv_studio_slider_group">
127
142
  <input type="range" min="0" max="100" value="40"
128
143
  class="tcv_sld_value_roughness tcv_clip_slider">
129
144
  <input value=40 class="tcv_inp_value_roughness tcv_clip_input"></input>
130
145
  </div>
131
146
  </div>
132
147
  <div class="tcv_material_info">
133
- Note: This is not a full material renderer.
148
+ Control material for CAD view, values in %
134
149
  </div>
135
150
  </div>
136
151
  <div class="tcv_cad_zebra_container">
137
- <div class="tcv_zebra_stripe_count tcv_label">
138
- Stripe Count
139
- </div>
140
- <div class="tcv_slider_group">
141
- <div>
152
+ <div class="tcv_studio_row">
153
+ <span class="tcv_label">Stripe Count</span>
154
+ <div class="tcv_studio_slider_group">
142
155
  <input type="range" min="2" max="50" value="10"
143
156
  class="tcv_sld_value_zebra_count tcv_clip_slider">
144
157
  <input value=10 class="tcv_inp_value_zebra_count tcv_clip_input"></input>
145
158
  </div>
146
- </div>
147
- <div class="tcv_zebra_stripe_opacity tcv_label">
148
- Stripe Opacity
149
159
  </div>
150
- <div class="tcv_slider_group">
151
- <div>
160
+ <div class="tcv_studio_row">
161
+ <span class="tcv_label">Stripe Opacity</span>
162
+ <div class="tcv_studio_slider_group">
152
163
  <input type="range" min="0.0" max="1.0" step="0.01" value="1.0"
153
164
  class="tcv_sld_value_zebra_opacity tcv_clip_slider">
154
165
  <input value=1.0 class="tcv_inp_value_zebra_opacity tcv_clip_input"></input>
155
166
  </div>
156
- </div>
157
- <div class="tcv_zebra_stripe_direction tcv_label">
158
- Stripe Direction
159
167
  </div>
160
- <div class="tcv_slider_group">
161
- <div>
168
+ <div class="tcv_studio_row">
169
+ <span class="tcv_label">Direction</span>
170
+ <div class="tcv_studio_slider_group">
162
171
  <input type="range" min="0" max="90" step="0.5" value="0"
163
172
  class="tcv_sld_value_zebra_direction tcv_clip_slider">
164
173
  <input value=0 class="tcv_inp_value_zebra_direction tcv_clip_input"></input>
@@ -186,7 +195,122 @@
186
195
  <label for="zebra_normal" class="tcv_radio_label">Normal</label>
187
196
  </span>
188
197
  </div>
189
- </div>
198
+ </div>
199
+ </div>
200
+ <div class="tcv_cad_studio_container">
201
+ <!-- Group 1: Environment -->
202
+ <div class="tcv_studio_row">
203
+ <span class="tcv_label" title="HDR environment map for lighting and reflections">Environment</span>
204
+ <select class="tcv_studio_environment tcv_studio_select">
205
+ <optgroup label="Studio">
206
+ <option value="studio">Procedural Studio</option>
207
+ <option value="studio_small_08" title="Poly Haven: studio_small_08.hdr (soft light, neutral, backlight)">Soft Light</option>
208
+ <option value="studio_small_03" title="Poly Haven: studio_small_03.hdr (high-contrast, softbox + ceiling lamp, crisp)">High Contrast Studio</option>
209
+ <option value="white_studio_05" title="Poly Haven: white_studio_05.hdr (white, product, bright, neutral lighting)">Bright Neutral</option>
210
+ <option value="white_studio_03" title="Poly Haven: white_studio_03.hdr (white, softbox, reflection, clean)">Clean Softbox</option>
211
+ <option value="photo_studio_01" title="Poly Haven: photo_studio_01.hdr (lighting setup, spotlights)">Spotlit Setup</option>
212
+ <option value="studio_small_09" title="Poly Haven: studio_small_09.hdr (product lighting, controlled, soft reflections)">Controlled Light</option>
213
+ <option value="cyclorama_hard_light" title="Poly Haven: cyclorama_hard_light.hdr (cyclorama, hard light, contrast)">Hard Contrast Light</option>
214
+ </optgroup>
215
+ <optgroup label="Outdoor">
216
+ <option value="canary_wharf" title="Poly Haven: canary_wharf.hdr (urban, city, overcast)">Urban Overcast</option>
217
+ <option value="kiara_1_dawn" title="Poly Haven: kiara_1_dawn.hdr (dawn, warm, nature, sunrise)">Outdoor Warm</option>
218
+ <option value="empty_warehouse_01" title="Poly Haven: empty_warehouse_01.hdr (warehouse, neutral, big space)">Neutral Industrial</option>
219
+ <option value="san_giuseppe_bridge" title="Poly Haven: san_giuseppe_bridge.hdr (bridge, outdoor, GPUOpen reference)">San Giuseppe Bridge</option>
220
+ </optgroup>
221
+ </select>
222
+ </div>
223
+ <div class="tcv_studio_checks tcv_studio_4k_row">
224
+ <input class='tcv_studio_4k_env_maps tcv_check' type="checkbox" title="Download 4K environment maps (sharper reflections, slower)" />
225
+ <span class="tcv_label" title="Download 4K environment maps (sharper reflections, slower)">Use 4K maps</span>
226
+ </div>
227
+ <div class="tcv_studio_row">
228
+ <span class="tcv_label" title="Brightness of the environment lighting">Env Intensity</span>
229
+ <div class="tcv_studio_slider_group">
230
+ <input type="range" min="0" max="300" step="1" value="100"
231
+ class="tcv_sld_value_studio_env_intensity tcv_clip_slider">
232
+ <input value=100 class="tcv_inp_value_studio_env_intensity tcv_clip_input"></input>
233
+ </div>
234
+ </div>
235
+ <div class="tcv_studio_row">
236
+ <span class="tcv_label" title="Rotate the environment map around the vertical axis">Env Rotation</span>
237
+ <div class="tcv_studio_slider_group">
238
+ <input type="range" min="0" max="360" step="1" value="0"
239
+ class="tcv_sld_value_studio_env_rotation tcv_clip_slider">
240
+ <input value=0 class="tcv_inp_value_studio_env_rotation tcv_clip_input"></input>
241
+ </div>
242
+ </div>
243
+
244
+ <div class="tcv_studio_group_spacer"></div>
245
+
246
+ <!-- Group 2: Appearance -->
247
+ <div class="tcv_studio_row">
248
+ <span class="tcv_label" title="Scene background style">Background</span>
249
+ <select class="tcv_studio_background tcv_studio_select">
250
+ <option value="environment">Environment</option>
251
+ <option value="transparent">Transparent</option>
252
+ <option value="gradient">Gradient Grey</option>
253
+ <option value="gradient-dark">Gradient Dark Grey</option>
254
+ <option value="white">Solid White</option>
255
+ <option value="grey">Solid Grey</option>
256
+ <option value="darkgrey">Solid Dark Grey</option>
257
+ </select>
258
+ </div>
259
+ <div class="tcv_studio_row">
260
+ <span class="tcv_label" title="Tone mapping algorithm for HDR to display conversion">Tone Mapping</span>
261
+ <select class="tcv_studio_tone_mapping tcv_studio_select">
262
+ <option value="neutral">PBR Neutral</option>
263
+ <option value="ACES">ACES Filmic</option>
264
+ <option value="none">Linear (none)</option>
265
+ </select>
266
+ </div>
267
+ <div class="tcv_studio_row">
268
+ <span class="tcv_label" title="Overall brightness of the rendered image">Exposure</span>
269
+ <div class="tcv_studio_slider_group">
270
+ <input type="range" min="0" max="300" step="1" value="100"
271
+ class="tcv_sld_value_studio_exposure tcv_clip_slider">
272
+ <input value=100 class="tcv_inp_value_studio_exposure tcv_clip_input"></input>
273
+ </div>
274
+ </div>
275
+
276
+ <div class="tcv_studio_group_spacer"></div>
277
+
278
+ <!-- Group 3: Shadows & AO -->
279
+ <div class="tcv_studio_row">
280
+ <span class="tcv_label" title="Darkness of directional shadows on the floor and between objects">Shadow Intensity</span>
281
+ <div class="tcv_studio_slider_group">
282
+ <input type="range" min="0" max="100" step="1" value="50"
283
+ class="tcv_sld_value_studio_shadow_intensity tcv_clip_slider">
284
+ <input value=50 class="tcv_inp_value_studio_shadow_intensity tcv_clip_input"></input>
285
+ </div>
286
+ </div>
287
+ <div class="tcv_studio_row">
288
+ <span class="tcv_label" title="Blur amount for shadow edges (sharp to soft)">Shadow Softness</span>
289
+ <div class="tcv_studio_slider_group">
290
+ <input type="range" min="0" max="100" step="1" value="20"
291
+ class="tcv_sld_value_studio_shadow_softness tcv_clip_slider">
292
+ <input value=20 class="tcv_inp_value_studio_shadow_softness tcv_clip_input"></input>
293
+ </div>
294
+ </div>
295
+ <div class="tcv_studio_row">
296
+ <span class="tcv_label" title="Screen-space ambient occlusion — darkens crevices and contact areas">AO Intensity</span>
297
+ <div class="tcv_studio_slider_group">
298
+ <input type="range" min="0" max="30" value="5"
299
+ class="tcv_sld_value_studio_ao_intensity tcv_clip_slider">
300
+ <input value=5 class="tcv_inp_value_studio_ao_intensity tcv_clip_input"></input>
301
+ </div>
302
+ </div>
303
+
304
+ <div class="tcv_studio_group_spacer"></div>
305
+
306
+ <!-- Group 4: Textures -->
307
+ <div class="tcv_studio_row">
308
+ <span class="tcv_label" title="How textures are projected onto surfaces without UV coordinates">Texture Mapping</span>
309
+ <select class="tcv_studio_texture_mapping tcv_studio_select">
310
+ <option value="triplanar">Triplanar</option>
311
+ <option value="parametric">Parametric UV</option>
312
+ </select>
313
+ </div>
190
314
  </div>
191
315
  </div>
192
316
  </div>
@@ -206,10 +330,23 @@
206
330
  </div>
207
331
 
208
332
  <div class="tcv_cad_view">
333
+ <div class="tcv_warning_banner" style="display: none;"></div>
209
334
  <div class="tcv_distance_measurement_panel tcv_panel tcv_round">
210
335
  <div class="tcv_measure_header">Distance</div>
211
336
  </div>
212
337
 
338
+ <div class="tcv_mat_editor tcv_round" style="display: none;">
339
+ <div class="tcv_mat_editor_titlebar">
340
+ <span class="tcv_mat_editor_title">Material Editor</span>
341
+ <span class="tcv_mat_editor_titlebar_btns">
342
+ <input class="tcv_mat_editor_reset tcv_btn tcv_small_btn" value="R" type="button" title="Reset to original material" />
343
+ <input class="tcv_mat_editor_close tcv_btn tcv_small_btn" value="X" type="button" title="Close" />
344
+ </span>
345
+ </div>
346
+ <div class="tcv_mat_editor_path"></div>
347
+ <div class="tcv_mat_editor_content tcv_mac-scrollbar tcv_scroller"></div>
348
+ </div>
349
+
213
350
  <div class="tcv_properties_measurement_panel tcv_panel tcv_round">
214
351
  <div class="tcv_measure_header">Properties</div>
215
352
  <div class="tcv_measure_subheader">Shape</div>
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Decode the compressed/instanced shape format.
3
+ *
4
+ * The instanced format wraps a standard Shapes object:
5
+ * ```
6
+ * { instances: [ { vertices: {shape,dtype,buffer,codec}, ... }, ... ],
7
+ * shapes: { version, parts, loc, name, id, bb, ... } }
8
+ * ```
9
+ *
10
+ * Each instance contains base64-encoded geometry buffers. Parts reference
11
+ * instances via `"shape": { "ref": N }`. After decoding, the result is a
12
+ * standard Shapes tree with TypedArrays — identical to the existing format.
13
+ */
14
+ import type { Shape, Shapes } from "../core/types.js";
15
+
16
+ // =============================================================================
17
+ // Types for the encoded format
18
+ // =============================================================================
19
+
20
+ /** A single base64-encoded buffer entry. */
21
+ interface EncodedBuffer {
22
+ shape: number[];
23
+ dtype: "float32" | "int32" | "uint32";
24
+ buffer: string;
25
+ codec: "b64";
26
+ }
27
+
28
+ /** An encoded geometry instance (all 9 buffer fields + optional uvs). */
29
+ interface EncodedInstance {
30
+ vertices: EncodedBuffer;
31
+ triangles: EncodedBuffer;
32
+ normals: EncodedBuffer;
33
+ edges: EncodedBuffer;
34
+ obj_vertices: EncodedBuffer;
35
+ face_types: EncodedBuffer;
36
+ edge_types: EncodedBuffer;
37
+ triangles_per_face: EncodedBuffer;
38
+ segments_per_edge: EncodedBuffer;
39
+ uvs?: EncodedBuffer;
40
+ }
41
+
42
+ /** Top-level structure of the instanced format. */
43
+ interface InstancedData {
44
+ instances: EncodedInstance[];
45
+ shapes: Shapes;
46
+ }
47
+
48
+ /** Shape reference (before resolution). */
49
+ interface ShapeRef {
50
+ ref: number;
51
+ }
52
+
53
+ // =============================================================================
54
+ // Decoding
55
+ // =============================================================================
56
+
57
+ /** Maximum decoded buffer size (512 MB). */
58
+ const MAX_BUFFER_BYTES = 512 * 1024 * 1024;
59
+
60
+ /** Decode a base64 string to a Uint8Array. */
61
+ function fromBase64(b64: string): Uint8Array {
62
+ const binary = atob(b64);
63
+ if (binary.length > MAX_BUFFER_BYTES) {
64
+ throw new Error(
65
+ `Decoded buffer size ${binary.length} bytes exceeds maximum of ${MAX_BUFFER_BYTES} bytes`,
66
+ );
67
+ }
68
+ const bytes = new Uint8Array(binary.length);
69
+ for (let i = 0; i < binary.length; i++) {
70
+ bytes[i] = binary.charCodeAt(i);
71
+ }
72
+ return bytes;
73
+ }
74
+
75
+ /** Decode a single encoded buffer entry to a typed array. */
76
+ function decodeBuffer(buf: EncodedBuffer): Float32Array | Uint32Array {
77
+ const bytes = fromBase64(buf.buffer);
78
+ const arrayBuffer = bytes.buffer.slice(
79
+ bytes.byteOffset,
80
+ bytes.byteOffset + bytes.byteLength,
81
+ );
82
+ switch (buf.dtype) {
83
+ case "float32":
84
+ return new Float32Array(arrayBuffer);
85
+ case "int32":
86
+ case "uint32":
87
+ return new Uint32Array(arrayBuffer);
88
+ default:
89
+ throw new Error(`Unknown dtype: ${buf.dtype}`);
90
+ }
91
+ }
92
+
93
+ /** Decode all buffer fields of an instance into a Shape object. */
94
+ function decodeInstance(inst: EncodedInstance): Shape {
95
+ const shape: Shape = {
96
+ vertices: decodeBuffer(inst.vertices) as Float32Array,
97
+ triangles: decodeBuffer(inst.triangles) as Uint32Array,
98
+ normals: decodeBuffer(inst.normals) as Float32Array,
99
+ edges: decodeBuffer(inst.edges) as Float32Array,
100
+ obj_vertices: decodeBuffer(inst.obj_vertices) as Float32Array,
101
+ face_types: decodeBuffer(inst.face_types) as Uint32Array,
102
+ edge_types: decodeBuffer(inst.edge_types) as Uint32Array,
103
+ triangles_per_face: decodeBuffer(inst.triangles_per_face) as Uint32Array,
104
+ segments_per_edge: decodeBuffer(inst.segments_per_edge) as Uint32Array,
105
+ };
106
+ if (inst.uvs) {
107
+ shape.uvs = decodeBuffer(inst.uvs) as Float32Array;
108
+ }
109
+ return shape;
110
+ }
111
+
112
+ /** Check if a value is an encoded buffer (inline base64 field). */
113
+ function isEncodedBuffer(val: unknown): val is EncodedBuffer {
114
+ return (
115
+ typeof val === "object" &&
116
+ val !== null &&
117
+ "buffer" in val &&
118
+ "dtype" in val &&
119
+ "codec" in val &&
120
+ (val as EncodedBuffer).codec === "b64"
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Decode any inline encoded buffers on a shape object.
126
+ * Mutates the shape in place, replacing EncodedBuffer fields with TypedArrays.
127
+ */
128
+ function decodeInlineShapeBuffers(shape: Record<string, unknown>): void {
129
+ for (const key of Object.keys(shape)) {
130
+ if (isEncodedBuffer(shape[key])) {
131
+ shape[key] = decodeBuffer(shape[key] as EncodedBuffer);
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Walk the shapes tree and decode any inline encoded buffers found in
138
+ * part.shape objects. This handles edge/vertex-only objects that embed
139
+ * encoded buffers directly (not via instance refs).
140
+ */
141
+ function decodeInlineBuffers(shapes: Shapes): void {
142
+ if (shapes.parts) {
143
+ for (const part of shapes.parts) {
144
+ if (
145
+ part.shape != null &&
146
+ typeof part.shape === "object" &&
147
+ !isShapeRef(part.shape as unknown)
148
+ ) {
149
+ decodeInlineShapeBuffers(part.shape as unknown as Record<string, unknown>);
150
+ }
151
+ if (part.parts) {
152
+ decodeInlineBuffers(part);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ /** Check if a shape field is an unresolved reference. */
159
+ function isShapeRef(shape: unknown): shape is ShapeRef {
160
+ return (
161
+ typeof shape === "object" &&
162
+ shape !== null &&
163
+ "ref" in shape &&
164
+ typeof (shape as ShapeRef).ref === "number"
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Recursively walk the shapes tree and replace { ref: N } entries
170
+ * with the corresponding decoded instance.
171
+ *
172
+ * Before decoding, `part.shape` may be `{ ref: N }` which doesn't match
173
+ * the `Shape` type. We use `unknown` casts since this operates on raw
174
+ * (pre-typed) data that is being transformed into the proper type.
175
+ */
176
+ function resolveRefs(shapes: Shapes, decoded: Shape[]): void {
177
+ if (shapes.parts) {
178
+ for (const part of shapes.parts) {
179
+ if (isShapeRef(part.shape as unknown)) {
180
+ const ref = (part.shape as unknown as ShapeRef).ref;
181
+ if (ref < 0 || ref >= decoded.length) {
182
+ throw new Error(
183
+ `Shape ref ${ref} is out of bounds (${decoded.length} instances decoded)`,
184
+ );
185
+ }
186
+ part.shape = decoded[ref];
187
+ }
188
+ if (part.parts) {
189
+ resolveRefs(part, decoded);
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // =============================================================================
196
+ // Public API
197
+ // =============================================================================
198
+
199
+ /**
200
+ * Type guard: check if data is in the instanced format.
201
+ * Detected by the presence of an `instances` array and a `shapes` object.
202
+ */
203
+ function isInstancedFormat(data: unknown): data is InstancedData {
204
+ return (
205
+ typeof data === "object" &&
206
+ data !== null &&
207
+ "instances" in data &&
208
+ Array.isArray((data as InstancedData).instances) &&
209
+ "shapes" in data &&
210
+ typeof (data as InstancedData).shapes === "object"
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Decode the instanced format into a standard Shapes object.
216
+ *
217
+ * 1. Decode all instance buffers from base64 → TypedArrays
218
+ * 2. Walk the shapes tree and replace { ref: N } with decoded instances
219
+ * 3. Return the unwrapped Shapes object
220
+ */
221
+ function decodeInstancedFormat(data: InstancedData): Shapes {
222
+ // Decode all instances
223
+ const decoded = data.instances.map(decodeInstance);
224
+
225
+ // Resolve all shape references
226
+ const shapes = data.shapes;
227
+ resolveRefs(shapes, decoded);
228
+
229
+ return shapes;
230
+ }
231
+
232
+ export { isInstancedFormat, decodeInstancedFormat, decodeInlineBuffers };
233
+ export type { InstancedData, EncodedBuffer, EncodedInstance };
@@ -110,12 +110,9 @@ interface Disposable {
110
110
  dispose: () => void;
111
111
  }
112
112
 
113
- function isDisposable(value: unknown): value is Disposable {
114
- return value !== null && typeof value === "object" && "dispose" in value && typeof (value as Disposable).dispose === "function";
115
- }
116
-
117
113
  interface MaterialLike {
118
114
  dispose: () => void;
115
+ // MeshStandardMaterial texture maps
119
116
  map?: Disposable | null;
120
117
  normalMap?: Disposable | null;
121
118
  roughnessMap?: Disposable | null;
@@ -124,29 +121,45 @@ interface MaterialLike {
124
121
  emissiveMap?: Disposable | null;
125
122
  alphaMap?: Disposable | null;
126
123
  bumpMap?: Disposable | null;
124
+ // MeshPhysicalMaterial additional texture maps
125
+ transmissionMap?: Disposable | null;
126
+ clearcoatMap?: Disposable | null;
127
+ clearcoatRoughnessMap?: Disposable | null;
128
+ clearcoatNormalMap?: Disposable | null;
129
+ thicknessMap?: Disposable | null;
130
+ specularIntensityMap?: Disposable | null;
131
+ specularColorMap?: Disposable | null;
132
+ sheenColorMap?: Disposable | null;
133
+ sheenRoughnessMap?: Disposable | null;
134
+ anisotropyMap?: Disposable | null;
127
135
  }
128
136
 
137
+ /** All texture map property names on MaterialLike (for iteration) */
138
+ const MATERIAL_TEXTURE_KEYS: readonly (keyof Omit<MaterialLike, "dispose">)[] = [
139
+ // MeshStandardMaterial
140
+ "map", "normalMap", "roughnessMap", "metalnessMap",
141
+ "aoMap", "emissiveMap", "alphaMap", "bumpMap",
142
+ // MeshPhysicalMaterial
143
+ "transmissionMap", "clearcoatMap", "clearcoatRoughnessMap", "clearcoatNormalMap",
144
+ "thicknessMap", "specularIntensityMap", "specularColorMap",
145
+ "sheenColorMap", "sheenRoughnessMap", "anisotropyMap",
146
+ ];
147
+
129
148
  /**
130
- * Dispose a material and its associated textures.
149
+ * Dispose a material and detach its texture references.
150
+ *
151
+ * Texture GPU resources are NOT freed here -- TextureCache is the sole owner
152
+ * of loaded textures and is responsible for calling texture.dispose().
153
+ * This function only nulls out the material's texture map references to
154
+ * break the association, then disposes the material itself.
131
155
  */
132
156
  function disposeMaterial(material: MaterialLike | null | undefined): void {
133
157
  if (!material) return;
134
158
 
135
- // Dispose all texture properties
136
- const textures = [
137
- material.map,
138
- material.normalMap,
139
- material.roughnessMap,
140
- material.metalnessMap,
141
- material.aoMap,
142
- material.emissiveMap,
143
- material.alphaMap,
144
- material.bumpMap,
145
- ];
146
- for (const texture of textures) {
147
- if (isDisposable(texture)) {
148
- gpuTracker.untrack("texture", texture);
149
- texture.dispose();
159
+ // Detach all texture references (do NOT dispose -- TextureCache owns them)
160
+ for (const key of MATERIAL_TEXTURE_KEYS) {
161
+ if (material[key]) {
162
+ (material as unknown as Record<string, unknown>)[key] = null;
150
163
  }
151
164
  }
152
165