three-cad-viewer 4.3.5 → 4.3.7

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/dist/three-cad-viewer.esm.js +8 -5
  2. package/dist/three-cad-viewer.esm.js.map +1 -1
  3. package/dist/three-cad-viewer.esm.min.js +1 -1
  4. package/dist/three-cad-viewer.js +8 -5
  5. package/dist/three-cad-viewer.min.js +1 -1
  6. package/package.json +2 -3
  7. package/src/_version.ts +0 -1
  8. package/src/camera/camera.ts +0 -445
  9. package/src/camera/controls/CADOrbitControls.ts +0 -241
  10. package/src/camera/controls/CADTrackballControls.ts +0 -598
  11. package/src/camera/controls.ts +0 -380
  12. package/src/core/patches.ts +0 -16
  13. package/src/core/studio-manager.ts +0 -652
  14. package/src/core/types.ts +0 -892
  15. package/src/core/viewer-state.ts +0 -784
  16. package/src/core/viewer.ts +0 -4821
  17. package/src/index.ts +0 -151
  18. package/src/rendering/environment.ts +0 -840
  19. package/src/rendering/light-detection.ts +0 -327
  20. package/src/rendering/material-factory.ts +0 -735
  21. package/src/rendering/material-presets.ts +0 -289
  22. package/src/rendering/raycast.ts +0 -291
  23. package/src/rendering/room-environment.ts +0 -192
  24. package/src/rendering/studio-composer.ts +0 -577
  25. package/src/rendering/studio-floor.ts +0 -108
  26. package/src/rendering/texture-cache.ts +0 -324
  27. package/src/rendering/tree-model.ts +0 -542
  28. package/src/rendering/triplanar.ts +0 -329
  29. package/src/scene/animation.ts +0 -343
  30. package/src/scene/axes.ts +0 -108
  31. package/src/scene/bbox.ts +0 -223
  32. package/src/scene/clipping.ts +0 -650
  33. package/src/scene/grid.ts +0 -864
  34. package/src/scene/nestedgroup.ts +0 -1448
  35. package/src/scene/objectgroup.ts +0 -866
  36. package/src/scene/orientation.ts +0 -259
  37. package/src/scene/render-shape.ts +0 -634
  38. package/src/tools/cad_tools/measure.ts +0 -811
  39. package/src/tools/cad_tools/select.ts +0 -100
  40. package/src/tools/cad_tools/tools.ts +0 -231
  41. package/src/tools/cad_tools/ui.ts +0 -454
  42. package/src/tools/cad_tools/zebra.ts +0 -369
  43. package/src/types/html.d.ts +0 -5
  44. package/src/types/n8ao.d.ts +0 -28
  45. package/src/types/three-augmentation.d.ts +0 -60
  46. package/src/ui/display.ts +0 -3295
  47. package/src/ui/index.html +0 -505
  48. package/src/ui/info.ts +0 -177
  49. package/src/ui/slider.ts +0 -206
  50. package/src/ui/toolbar.ts +0 -347
  51. package/src/ui/treeview.ts +0 -945
  52. package/src/utils/decode-instances.ts +0 -233
  53. package/src/utils/font.ts +0 -60
  54. package/src/utils/gpu-tracker.ts +0 -265
  55. package/src/utils/logger.ts +0 -92
  56. package/src/utils/sizeof.ts +0 -116
  57. package/src/utils/timer.ts +0 -69
  58. 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 };