three-cad-viewer 4.3.4 → 4.3.6

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 (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
@@ -1,784 +0,0 @@
1
- import * as THREE from "three";
2
- import {
3
- CollapseState,
4
- type Theme,
5
- type ThemeInput,
6
- type ControlType,
7
- type UpDirection,
8
- type AnimationMode,
9
- type ActiveTab,
10
- type ZebraColorScheme,
11
- type ZebraMappingMode,
12
- type StudioBackground,
13
- type StudioToneMapping,
14
- type StudioTextureMapping,
15
- type Keymap,
16
- type StateChange,
17
- type StateSubscriber,
18
- type GlobalStateSubscriber,
19
- type SubscribeOptions,
20
- type RenderOptions,
21
- type ViewerOptions,
22
- type StudioOptions,
23
- } from "./types";
24
- import { logger } from "../utils/logger.js";
25
-
26
- /**
27
- * Display configuration defaults
28
- */
29
- interface DisplayDefaults {
30
- theme: Theme;
31
- cadWidth: number;
32
- treeWidth: number;
33
- treeHeight: number;
34
- height: number;
35
- pinning: boolean;
36
- glass: boolean;
37
- tools: boolean;
38
- keymap: Keymap;
39
- newTreeBehavior: boolean;
40
- measureTools: boolean;
41
- selectTool: boolean;
42
- explodeTool: boolean;
43
- zscaleTool: boolean;
44
- zebraTool: boolean;
45
- studioTool: boolean;
46
- measurementDebug: boolean;
47
- }
48
-
49
- /**
50
- * Render configuration defaults
51
- */
52
- interface RenderDefaults {
53
- ambientIntensity: number;
54
- directIntensity: number;
55
- metalness: number;
56
- roughness: number;
57
- defaultOpacity: number;
58
- edgeColor: number;
59
- normalLen: number;
60
- }
61
-
62
- /**
63
- * Viewer/view configuration defaults
64
- */
65
- interface ViewerDefaults {
66
- axes: boolean;
67
- axes0: boolean;
68
- grid: [boolean, boolean, boolean];
69
- ortho: boolean;
70
- transparent: boolean;
71
- blackEdges: boolean;
72
- collapse: CollapseState;
73
- clipIntersection: boolean;
74
- clipPlaneHelpers: boolean;
75
- clipObjectColors: boolean;
76
- clipNormal0: THREE.Vector3;
77
- clipNormal1: THREE.Vector3;
78
- clipNormal2: THREE.Vector3;
79
- clipSlider0: number;
80
- clipSlider1: number;
81
- clipSlider2: number;
82
- control: ControlType;
83
- holroyd: boolean;
84
- up: UpDirection;
85
- ticks: number;
86
- gridFontSize: number;
87
- centerGrid: boolean;
88
- position: THREE.Vector3 | null;
89
- quaternion: THREE.Quaternion | null;
90
- target: THREE.Vector3 | null;
91
- zoom: number;
92
- panSpeed: number;
93
- rotateSpeed: number;
94
- zoomSpeed: number;
95
- timeit: boolean;
96
- }
97
-
98
- /**
99
- * Zebra tool defaults
100
- */
101
- interface ZebraDefaults {
102
- zebraCount: number;
103
- zebraOpacity: number;
104
- zebraDirection: number;
105
- zebraColorScheme: ZebraColorScheme;
106
- zebraMappingMode: ZebraMappingMode;
107
- }
108
-
109
- /**
110
- * Studio mode defaults
111
- */
112
- interface StudioModeDefaults {
113
- studioEnvironment: string;
114
- studioEnvIntensity: number;
115
- studioBackground: StudioBackground;
116
- studioToneMapping: StudioToneMapping;
117
- studioExposure: number;
118
- studio4kEnvMaps: boolean;
119
- studioTextureMapping: StudioTextureMapping;
120
- studioEnvRotation: number;
121
- studioShadowIntensity: number;
122
- studioShadowSoftness: number;
123
- studioAOIntensity: number;
124
- }
125
-
126
- /**
127
- * Runtime state defaults
128
- */
129
- interface RuntimeDefaults {
130
- activeTool: string | null;
131
- animationMode: AnimationMode;
132
- animationSliderValue: number;
133
- zscaleActive: boolean;
134
- highlightedButton: string | null;
135
- activeTab: ActiveTab;
136
- }
137
-
138
- /**
139
- * Complete state shape
140
- */
141
- type StateShape = DisplayDefaults & RenderDefaults & ViewerDefaults & ZebraDefaults & StudioModeDefaults & RuntimeDefaults;
142
-
143
- /**
144
- * Keys of the state shape
145
- */
146
- type StateKey = keyof StateShape;
147
-
148
- /**
149
- * Options that can be passed to ViewerState constructor.
150
- * Accepts StateShape properties plus an index signature for runtime validation.
151
- */
152
- type ViewerStateOptions = Partial<StateShape> & { theme?: ThemeInput } & { [key: string]: unknown };
153
-
154
- /**
155
- * External notification payload - single key/change pair
156
- */
157
- type ExternalNotification = { key: string; change: StateChange<unknown> };
158
-
159
- /**
160
- * All valid state keys for runtime validation
161
- */
162
- const STATE_KEYS: ReadonlySet<string> = new Set<StateKey>([
163
- // Display
164
- "theme", "cadWidth", "treeWidth", "treeHeight", "height", "pinning", "glass", "tools",
165
- "keymap", "newTreeBehavior", "measureTools", "selectTool", "explodeTool", "zscaleTool",
166
- "zebraTool", "studioTool", "measurementDebug",
167
- // Render
168
- "ambientIntensity", "directIntensity", "metalness", "roughness", "defaultOpacity",
169
- "edgeColor", "normalLen",
170
- // Viewer
171
- "axes", "axes0", "grid", "ortho", "transparent", "blackEdges", "collapse",
172
- "clipIntersection", "clipPlaneHelpers", "clipObjectColors", "clipNormal0", "clipNormal1",
173
- "clipNormal2", "clipSlider0", "clipSlider1", "clipSlider2", "control", "holroyd", "up",
174
- "ticks", "gridFontSize", "centerGrid", "position", "quaternion", "target", "zoom",
175
- "panSpeed", "rotateSpeed", "zoomSpeed", "timeit",
176
- // Zebra
177
- "zebraCount", "zebraOpacity", "zebraDirection", "zebraColorScheme", "zebraMappingMode",
178
- // Studio
179
- "studioEnvironment", "studioEnvIntensity", "studioBackground",
180
- "studioToneMapping", "studioExposure", "studio4kEnvMaps", "studioTextureMapping",
181
- "studioEnvRotation", "studioShadowIntensity", "studioShadowSoftness", "studioAOIntensity",
182
- // Runtime
183
- "activeTool", "animationMode", "animationSliderValue", "zscaleActive", "highlightedButton",
184
- "activeTab",
185
- ]);
186
-
187
- /**
188
- * Type guard to check if a string is a valid state key
189
- */
190
- function isStateKey(key: string): key is StateKey {
191
- return STATE_KEYS.has(key);
192
- }
193
-
194
- /**
195
- * Mapping from state keys to external notification keys.
196
- * Only keys that should trigger external notifications are included.
197
- * State keys not in this map won't trigger external notifications.
198
- */
199
- const STATE_TO_NOTIFICATION_KEY: Partial<Record<StateKey, string>> = {
200
- // View settings
201
- axes: "axes",
202
- axes0: "axes0",
203
- grid: "grid",
204
- ortho: "ortho",
205
- transparent: "transparent",
206
- blackEdges: "black_edges",
207
- tools: "tools",
208
- glass: "glass",
209
- centerGrid: "center_grid",
210
- collapse: "collapse",
211
- activeTab: "tab",
212
- // Render settings
213
- ambientIntensity: "ambient_intensity",
214
- directIntensity: "direct_intensity",
215
- metalness: "metalness",
216
- roughness: "roughness",
217
- edgeColor: "default_edgecolor",
218
- defaultOpacity: "default_opacity",
219
- // Control settings
220
- zoomSpeed: "zoom_speed",
221
- panSpeed: "pan_speed",
222
- rotateSpeed: "rotate_speed",
223
- holroyd: "holroyd",
224
- // Clipping settings
225
- clipIntersection: "clip_intersection",
226
- clipObjectColors: "clip_object_colors",
227
- clipPlaneHelpers: "clip_planes",
228
- clipSlider0: "clip_slider_0",
229
- clipSlider1: "clip_slider_1",
230
- clipSlider2: "clip_slider_2",
231
- clipNormal0: "clip_normal_0",
232
- clipNormal1: "clip_normal_1",
233
- clipNormal2: "clip_normal_2",
234
- // Zebra settings
235
- zebraCount: "zebra_count",
236
- zebraOpacity: "zebra_opacity",
237
- zebraDirection: "zebra_direction",
238
- zebraColorScheme: "zebra_color_scheme",
239
- zebraMappingMode: "zebra_mapping_mode",
240
- // Studio settings
241
- studioEnvironment: "studio_environment",
242
- studioEnvIntensity: "studio_env_intensity",
243
- studioBackground: "studio_background",
244
- studioToneMapping: "studio_tone_mapping",
245
- studioExposure: "studio_exposure",
246
- studio4kEnvMaps: "studio_4k_env_maps",
247
- studioTextureMapping: "studio_texture_mapping",
248
- studioEnvRotation: "studio_env_rotation",
249
- studioShadowIntensity: "studio_shadow_intensity",
250
- studioShadowSoftness: "studio_shadow_softness",
251
- studioAOIntensity: "studio_ao_intensity",
252
- // Animation/Explode slider (shared state, mutually exclusive modes)
253
- animationSliderValue: "relative_time",
254
- };
255
-
256
- /**
257
- * Transform functions for notification values.
258
- * Converts internal state values to external notification format.
259
- */
260
- const STATE_NOTIFICATION_TRANSFORM: Partial<Record<StateKey, (v: unknown) => unknown>> = {
261
- // Slider stores 0-1000, but notifications should be 0-1
262
- animationSliderValue: (v) => (v as number) / 1000,
263
- };
264
-
265
- /**
266
- * Compare two values for equality (handles arrays)
267
- */
268
- function valuesEqual(a: unknown, b: unknown): boolean {
269
- if (a === b) return true;
270
- if (Array.isArray(a) && Array.isArray(b)) {
271
- return a.length === b.length && a.every((v, i) => v === b[i]);
272
- }
273
- return false;
274
- }
275
-
276
- /**
277
- * Resolve theme input to actual theme value
278
- */
279
- function resolveTheme(inputTheme: ThemeInput | undefined): Theme | undefined {
280
- if (inputTheme === "dark") return "dark";
281
- if (inputTheme === "light") return "light";
282
- if (
283
- inputTheme === "browser" &&
284
- typeof window !== "undefined" &&
285
- window.matchMedia("(prefers-color-scheme: dark)").matches
286
- ) {
287
- return "dark";
288
- }
289
- return undefined;
290
- }
291
-
292
- /**
293
- * Centralized state management for the viewer.
294
- *
295
- * ViewerState is the single source of truth for all viewer configuration:
296
- * - Display settings (theme, dimensions, tools enabled)
297
- * - Render settings (lighting, materials)
298
- * - View settings (camera, clipping, grid)
299
- * - Runtime state (active tool, animation)
300
- *
301
- * ## Observable Pattern
302
- * State changes can be observed via `subscribe()`:
303
- * ```typescript
304
- * const unsubscribe = state.subscribe("axes", (change) => {
305
- * console.log(`axes changed from ${change.old} to ${change.new}`);
306
- * });
307
- * ```
308
- *
309
- * ## Key Methods
310
- * - `get(key)`: Get current value
311
- * - `set(key, value)`: Set value (triggers subscribers)
312
- * - `subscribe(key, callback)`: Subscribe to changes
313
- * - `subscribeAll(callback)`: Subscribe to all changes
314
- *
315
- * ## Default Values
316
- * - `DISPLAY_DEFAULTS`: Theme, dimensions, tools
317
- * - `RENDER_DEFAULTS`: Lighting, materials
318
- * - `VIEWER_DEFAULTS`: Camera, clipping, grid
319
- *
320
- * @internal - This is an internal class used by Viewer
321
- */
322
- class ViewerState {
323
- /**
324
- * Default values for display configuration
325
- */
326
- static DISPLAY_DEFAULTS: DisplayDefaults = {
327
- theme: "light",
328
- cadWidth: 800,
329
- treeWidth: 260,
330
- treeHeight: 400,
331
- height: 600,
332
- pinning: false,
333
- glass: false,
334
- tools: true,
335
- keymap: {
336
- shift: "shiftKey", ctrl: "ctrlKey", meta: "metaKey", alt: "altKey",
337
- axes: "a", axes0: "A", grid: "g", gridxy: "G", perspective: "p", transparent: "t", blackedges: "b",
338
- reset: "R", resize: "r",
339
- iso: "5", front: "1", rear: "3", top: "8", bottom: "2", left: "4", right: "6",
340
- explode: "x", zscale: "L", distance: "D", properties: "P", select: "S", help: "h", play: " ", stop: "Escape",
341
- tree: "T", clip: "C", material: "M", zebra: "Z", studio: "s",
342
- },
343
- newTreeBehavior: true,
344
- measureTools: true,
345
- selectTool: true,
346
- explodeTool: true,
347
- zscaleTool: false,
348
- zebraTool: true,
349
- studioTool: true,
350
- measurementDebug: false,
351
- };
352
-
353
- /**
354
- * Default values for render configuration
355
- */
356
- static RENDER_DEFAULTS: RenderDefaults = {
357
- ambientIntensity: 1,
358
- directIntensity: 1.1,
359
- metalness: 0.3,
360
- roughness: 0.65,
361
- defaultOpacity: 0.5,
362
- edgeColor: 0x707070,
363
- normalLen: 0,
364
- };
365
-
366
- /**
367
- * Default values for viewer/view configuration
368
- */
369
- static VIEWER_DEFAULTS: ViewerDefaults = {
370
- axes: false,
371
- axes0: false,
372
- grid: [false, false, false],
373
- ortho: true,
374
- transparent: false,
375
- blackEdges: false,
376
- collapse: CollapseState.COLLAPSED,
377
- clipIntersection: false,
378
- clipPlaneHelpers: false,
379
- clipObjectColors: false,
380
- clipNormal0: new THREE.Vector3(-1, 0, 0),
381
- clipNormal1: new THREE.Vector3(0, -1, 0),
382
- clipNormal2: new THREE.Vector3(0, 0, -1),
383
- clipSlider0: -1,
384
- clipSlider1: -1,
385
- clipSlider2: -1,
386
- control: "orbit",
387
- holroyd: true,
388
- up: "Z",
389
- ticks: 10,
390
- gridFontSize: 10,
391
- centerGrid: false,
392
- position: null,
393
- quaternion: null,
394
- target: null,
395
- zoom: 1,
396
- panSpeed: 1.0,
397
- rotateSpeed: 1.0,
398
- zoomSpeed: 1.0,
399
- timeit: false,
400
- };
401
-
402
- /**
403
- * Zebra tool settings
404
- */
405
- static ZEBRA_DEFAULTS: ZebraDefaults = {
406
- zebraCount: 9,
407
- zebraOpacity: 1.0,
408
- zebraDirection: 0,
409
- zebraColorScheme: "blackwhite",
410
- zebraMappingMode: "reflection",
411
- };
412
-
413
- /**
414
- * Studio mode settings
415
- */
416
- static STUDIO_MODE_DEFAULTS: StudioModeDefaults = {
417
- studioEnvironment: "studio",
418
- studioEnvIntensity: 1.0,
419
- studioBackground: "environment",
420
- studioToneMapping: "neutral",
421
- studioExposure: 1.0,
422
- studio4kEnvMaps: false,
423
- studioTextureMapping: "triplanar",
424
- studioEnvRotation: 0,
425
- studioShadowIntensity: 0.5,
426
- studioShadowSoftness: 0.2,
427
- studioAOIntensity: 0.5,
428
- };
429
-
430
- /**
431
- * Runtime state (not from options, changes during execution)
432
- */
433
- static RUNTIME_DEFAULTS: RuntimeDefaults = {
434
- activeTool: null,
435
- animationMode: "none",
436
- animationSliderValue: 0,
437
- zscaleActive: false,
438
- highlightedButton: null,
439
- activeTab: "tree",
440
- };
441
-
442
- private _state: StateShape;
443
- private _listeners: Map<StateKey, StateSubscriber<unknown>[]>;
444
- private _globalListeners: GlobalStateSubscriber[];
445
- private _externalNotifyCallback: ((
446
- input: ExternalNotification | ExternalNotification[]
447
- ) => void) | null = null;
448
-
449
- /**
450
- * Create a ViewerState instance
451
- */
452
- constructor(options: ViewerStateOptions = {}) {
453
- // Start with all defaults
454
- this._state = {
455
- ...ViewerState.DISPLAY_DEFAULTS,
456
- ...ViewerState.RENDER_DEFAULTS,
457
- ...ViewerState.VIEWER_DEFAULTS,
458
- ...ViewerState.ZEBRA_DEFAULTS,
459
- ...ViewerState.STUDIO_MODE_DEFAULTS,
460
- ...ViewerState.RUNTIME_DEFAULTS,
461
- };
462
-
463
- // Handle special theme logic (browser theme detection)
464
- const resolvedTheme = resolveTheme(options.theme);
465
- if (resolvedTheme) {
466
- this._state.theme = resolvedTheme;
467
- }
468
-
469
- // Apply user options (with validation)
470
- this._applyOptions(options);
471
-
472
- this._listeners = new Map();
473
- this._globalListeners = [];
474
- }
475
-
476
- /**
477
- * Apply options to state, validating keys
478
- */
479
- private _applyOptions(options: ViewerStateOptions): void {
480
- for (const key of Object.keys(options)) {
481
- if (key === "theme") continue; // Already handled
482
- if (!isStateKey(key)) {
483
- logger.warn(`Unknown option "${key}" - ignored`);
484
- continue;
485
- }
486
- const value = options[key];
487
- if (value !== undefined && value !== null) {
488
- // Type-safe assignment using Object.assign for the single property
489
- Object.assign(this._state, { [key]: value });
490
- }
491
- }
492
- }
493
-
494
- /**
495
- * Get a state value
496
- */
497
- get<K extends StateKey>(key: K): StateShape[K] {
498
- return this._state[key];
499
- }
500
-
501
- /**
502
- * Set a state value and notify listeners
503
- */
504
- set<K extends StateKey>(key: K, value: StateShape[K], notify: boolean = true): void {
505
- const oldValue = this._state[key];
506
-
507
- // Skip if value hasn't changed
508
- if (valuesEqual(oldValue, value)) return;
509
-
510
- this._state[key] = value;
511
-
512
- if (notify) {
513
- this._notify(key, { old: oldValue, new: value });
514
- }
515
- }
516
-
517
- /**
518
- * Update multiple state values at once
519
- */
520
- private _update(updates: Partial<StateShape>, notify: boolean = true): void {
521
- const changes: Array<{ key: StateKey; change: StateChange<unknown> }> = [];
522
-
523
- for (const key of Object.keys(updates)) {
524
- if (!isStateKey(key)) continue;
525
- const value = updates[key];
526
- // Skip undefined/null, except for keys where null is a valid value (reset to default)
527
- const KEYS_WITH_VALID_NULL = ["position", "quaternion", "target"];
528
- if (value === undefined || (value === null && !KEYS_WITH_VALID_NULL.includes(key))) continue;
529
-
530
- const oldValue = this._state[key];
531
- if (!valuesEqual(oldValue, value)) {
532
- Object.assign(this._state, { [key]: value });
533
- changes.push({ key, change: { old: oldValue, new: value } });
534
- }
535
- }
536
-
537
- if (notify) {
538
- for (const { key, change } of changes) {
539
- this._notify(key, change);
540
- }
541
- }
542
- }
543
-
544
- /**
545
- * Update render state from RenderOptions.
546
- * RenderOptions types are directly compatible with StateShape.
547
- */
548
- updateRenderState(options: RenderOptions, notify: boolean = true): void {
549
- this._update(options, notify);
550
- }
551
-
552
- /**
553
- * Update viewer state from ViewerOptions.
554
- * Converts Vector3Tuple/QuaternionTuple to THREE objects.
555
- */
556
- updateViewerState(options: ViewerOptions, notify: boolean = true): void {
557
- // Extract properties that need conversion to THREE objects
558
- const { clipNormal0, clipNormal1, clipNormal2, position, quaternion, target, ...rest } = options;
559
-
560
- const converted: Partial<StateShape> = { ...rest };
561
-
562
- // Convert tuple values to THREE objects
563
- if (clipNormal0 !== undefined) {
564
- converted.clipNormal0 = new THREE.Vector3(...clipNormal0);
565
- }
566
- if (clipNormal1 !== undefined) {
567
- converted.clipNormal1 = new THREE.Vector3(...clipNormal1);
568
- }
569
- if (clipNormal2 !== undefined) {
570
- converted.clipNormal2 = new THREE.Vector3(...clipNormal2);
571
- }
572
- if (position !== undefined) {
573
- converted.position = position ? new THREE.Vector3(...position) : null;
574
- }
575
- if (quaternion !== undefined) {
576
- converted.quaternion = quaternion ? new THREE.Quaternion(...quaternion) : null;
577
- }
578
- if (target !== undefined) {
579
- converted.target = target ? new THREE.Vector3(...target) : null;
580
- }
581
-
582
- this._update(converted, notify);
583
- }
584
-
585
- /**
586
- * Update studio state from StudioOptions (shapes.studioOptions).
587
- * Maps short field names to prefixed state keys.
588
- */
589
- updateStudioState(options: StudioOptions): void {
590
- const map: Partial<StateShape> = {};
591
- if (options.environment !== undefined) map.studioEnvironment = options.environment;
592
- if (options.envIntensity !== undefined) map.studioEnvIntensity = options.envIntensity;
593
- if (options.background !== undefined) map.studioBackground = options.background;
594
- if (options.toneMapping !== undefined) map.studioToneMapping = options.toneMapping;
595
- if (options.toneMappingExposure !== undefined) map.studioExposure = options.toneMappingExposure;
596
- if (options.use4kEnvMaps !== undefined) map.studio4kEnvMaps = options.use4kEnvMaps;
597
- if (options.textureMapping !== undefined) map.studioTextureMapping = options.textureMapping;
598
- if (options.envRotation !== undefined) map.studioEnvRotation = options.envRotation;
599
- if (options.shadowIntensity !== undefined) map.studioShadowIntensity = options.shadowIntensity;
600
- if (options.shadowSoftness !== undefined) map.studioShadowSoftness = options.shadowSoftness;
601
- if (options.aoIntensity !== undefined) map.studioAOIntensity = options.aoIntensity;
602
- this._update(map, false);
603
- }
604
-
605
- /**
606
- * Get all state as a plain object (for serialization)
607
- */
608
- getAll(): StateShape {
609
- return { ...this._state };
610
- }
611
-
612
- /**
613
- * Subscribe to changes for a specific state key
614
- */
615
- subscribe<K extends StateKey>(
616
- key: K,
617
- listener: StateSubscriber<StateShape[K]>,
618
- options: SubscribeOptions = {}
619
- ): () => void {
620
- if (!this._listeners.has(key)) {
621
- this._listeners.set(key, []);
622
- }
623
- this._listeners.get(key)!.push(listener as StateSubscriber<unknown>);
624
-
625
- // Immediately invoke with current value if requested
626
- if (options.immediate) {
627
- const currentValue = this._state[key];
628
- listener({ old: undefined, new: currentValue });
629
- }
630
-
631
- // Return unsubscribe function
632
- return () => {
633
- const listeners = this._listeners.get(key);
634
- if (listeners) {
635
- const index = listeners.indexOf(listener as StateSubscriber<unknown>);
636
- if (index > -1) {
637
- listeners.splice(index, 1);
638
- }
639
- }
640
- };
641
- }
642
-
643
- /**
644
- * Subscribe to all state changes
645
- */
646
- subscribeAll(listener: GlobalStateSubscriber): () => void {
647
- this._globalListeners.push(listener);
648
-
649
- return () => {
650
- const index = this._globalListeners.indexOf(listener);
651
- if (index > -1) {
652
- this._globalListeners.splice(index, 1);
653
- }
654
- };
655
- }
656
-
657
- /**
658
- * Set a callback for external notifications (e.g., to notify external clients).
659
- * The callback receives the notification key (snake_case) and the change object.
660
- */
661
- setExternalNotifyCallback(
662
- callback: ((input: ExternalNotification | ExternalNotification[]) => void) | null
663
- ): void {
664
- this._externalNotifyCallback = callback;
665
- }
666
-
667
- /**
668
- * Get all notifiable state values in external format.
669
- * Returns a dictionary with snake_case keys and StateChange values.
670
- * Used for initial config sync to external clients.
671
- */
672
- getAllNotifiable(): Record<string, StateChange<unknown>> {
673
- const result: Record<string, StateChange<unknown>> = {};
674
- for (const [stateKey, notifyKey] of Object.entries(STATE_TO_NOTIFICATION_KEY)) {
675
- const value = this._state[stateKey as StateKey];
676
- const transform = STATE_NOTIFICATION_TRANSFORM[stateKey as StateKey];
677
- const transformedValue = transform
678
- ? transform(value)
679
- : value instanceof THREE.Vector3
680
- ? value.toArray()
681
- : value;
682
- result[notifyKey] = { old: null, new: transformedValue };
683
- }
684
- return result;
685
- }
686
-
687
- private _notify(key: StateKey, change: StateChange<unknown>): void {
688
- // Notify key-specific listeners (internal UI updates)
689
- const listeners = this._listeners.get(key);
690
- if (listeners) {
691
- for (const listener of listeners) {
692
- listener(change);
693
- }
694
- }
695
-
696
- // Notify global listeners (internal)
697
- for (const listener of this._globalListeners) {
698
- listener(key, change);
699
- }
700
-
701
- // Notify external callback if registered and key has a notification mapping
702
- if (this._externalNotifyCallback) {
703
- const notificationKey = STATE_TO_NOTIFICATION_KEY[key];
704
- if (notificationKey) {
705
- // Apply transform if defined (e.g., slider 0-1000 → relative 0-1)
706
- const transform = STATE_NOTIFICATION_TRANSFORM[key];
707
- const notifyChange = transform
708
- ? { old: change.old != null ? transform(change.old) : null, new: transform(change.new) }
709
- : change;
710
- this._externalNotifyCallback({ key: notificationKey, change: notifyChange });
711
- }
712
- }
713
- }
714
-
715
- /**
716
- * Reset state to default values
717
- */
718
- reset(options: ViewerStateOptions = {}): void {
719
- const oldState = { ...this._state };
720
-
721
- // Reset to all defaults
722
- this._state = {
723
- ...ViewerState.DISPLAY_DEFAULTS,
724
- ...ViewerState.RENDER_DEFAULTS,
725
- ...ViewerState.VIEWER_DEFAULTS,
726
- ...ViewerState.ZEBRA_DEFAULTS,
727
- ...ViewerState.STUDIO_MODE_DEFAULTS,
728
- ...ViewerState.RUNTIME_DEFAULTS,
729
- };
730
-
731
- // Handle special theme logic
732
- const resolvedTheme = resolveTheme(options.theme);
733
- if (resolvedTheme) {
734
- this._state.theme = resolvedTheme;
735
- }
736
-
737
- // Apply options
738
- this._applyOptions(options);
739
-
740
- // Notify all changes
741
- for (const key of Object.keys(this._state)) {
742
- if (!isStateKey(key)) continue;
743
- if (!valuesEqual(oldState[key], this._state[key])) {
744
- this._notify(key, { old: oldState[key], new: this._state[key] });
745
- }
746
- }
747
- }
748
-
749
- /**
750
- * Get all default values (useful for documentation/debugging)
751
- */
752
- static getDefaults(): StateShape {
753
- return {
754
- ...ViewerState.DISPLAY_DEFAULTS,
755
- ...ViewerState.RENDER_DEFAULTS,
756
- ...ViewerState.VIEWER_DEFAULTS,
757
- ...ViewerState.ZEBRA_DEFAULTS,
758
- ...ViewerState.STUDIO_MODE_DEFAULTS,
759
- ...ViewerState.RUNTIME_DEFAULTS,
760
- };
761
- }
762
-
763
- /**
764
- * Dump all state values to console, organized by category
765
- */
766
- dump(): void {
767
- const logCategory = (name: string, defaults: Partial<StateShape>): void => {
768
- logger.info(`${name}:`);
769
- for (const key of Object.keys(defaults)) {
770
- logger.info(`- ${key}`, this._state[key as StateKey]);
771
- }
772
- };
773
-
774
- logCategory("Display", ViewerState.DISPLAY_DEFAULTS);
775
- logCategory("Render", ViewerState.RENDER_DEFAULTS);
776
- logCategory("View", ViewerState.VIEWER_DEFAULTS);
777
- logCategory("Zebra", ViewerState.ZEBRA_DEFAULTS);
778
- logCategory("Studio", ViewerState.STUDIO_MODE_DEFAULTS);
779
- logCategory("Runtime", ViewerState.RUNTIME_DEFAULTS);
780
- }
781
- }
782
-
783
- export { ViewerState };
784
- export type { StateShape, StateKey, ViewerStateOptions };