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
package/src/ui/display.ts DELETED
@@ -1,3295 +0,0 @@
1
- // =============================================================================
2
- // IMPORTS & HELPERS
3
- // =============================================================================
4
-
5
- import * as THREE from "three";
6
- import { KeyMapper, EventListenerManager } from "../utils/utils.js";
7
- import type { KeyMappingConfig } from "../utils/utils.js";
8
- import { Slider } from "./slider.js";
9
- import { Toolbar, Button, ClickButton, Ellipsis } from "./toolbar.js";
10
- import { ToolTypes } from "../tools/cad_tools/tools.js";
11
- import type {
12
- MeasurementPanelElements,
13
- FilterDropdownElements,
14
- } from "../tools/cad_tools/tools.js";
15
- import { FilterByDropDownMenu } from "../tools/cad_tools/ui.js";
16
- import { Info } from "./info.js";
17
- import type { Viewer } from "../core/viewer.js";
18
- import type { ObjectGroup } from "../scene/objectgroup.js";
19
- import type { ViewerState } from "../core/viewer-state.js";
20
- import { isClipIndex, CollapseState } from "../core/types.js";
21
- import type { Vector3Tuple } from "three";
22
- import type { ActiveTab, ThemeInput, ClipIndex } from "../core/types.js";
23
- import type { CameraDirection } from "../camera/camera.js";
24
- import { applyTriplanarMapping } from "../rendering/triplanar.js";
25
-
26
- import template from "./index.html";
27
-
28
- function TEMPLATE(id: string): string {
29
- const shift = KeyMapper.getshortcuts("shift");
30
- const ctrl = KeyMapper.getshortcuts("ctrl");
31
- const meta = KeyMapper.getshortcuts("meta");
32
- const alt = KeyMapper.getshortcuts("alt");
33
- const html = template
34
- .replaceAll("{{id}}", id)
35
- .replaceAll("{{shift}}", shift)
36
- .replaceAll("{{ctrl}}", ctrl)
37
- .replaceAll("{{meta}}", meta)
38
- .replaceAll("{{alt}}", alt);
39
- return html;
40
- }
41
-
42
- function px(val: number): string {
43
- return `${val}px`;
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Material Editor Parameter Definitions
48
- // ---------------------------------------------------------------------------
49
-
50
- interface MatEditorParam {
51
- key: string;
52
- label: string;
53
- min: number;
54
- max: number;
55
- step: number;
56
- group: string;
57
- infinity?: boolean;
58
- }
59
-
60
- const MAT_EDITOR_PARAMS: MatEditorParam[] = [
61
- { key: "metalness", label: "Metallic", min: 0, max: 1, step: 0.01, group: "PBR Core" },
62
- { key: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01, group: "PBR Core" },
63
- { key: "clearcoat", label: "Clearcoat", min: 0, max: 1, step: 0.01, group: "Clearcoat" },
64
- { key: "clearcoatRoughness", label: "Clearcoat Rough.", min: 0, max: 1, step: 0.01, group: "Clearcoat" },
65
- { key: "transmission", label: "Transmission", min: 0, max: 1, step: 0.01, group: "Transmission" },
66
- { key: "ior", label: "IOR", min: 1.0, max: 2.5, step: 0.01, group: "Transmission" },
67
- { key: "thickness", label: "Thickness", min: 0, max: 10, step: 0.1, group: "Transmission" },
68
- { key: "attenuationDistance", label: "Atten. Distance", min: 0, max: 100, step: 0.5, group: "Transmission", infinity: true },
69
- { key: "sheen", label: "Sheen", min: 0, max: 1, step: 0.01, group: "Sheen" },
70
- { key: "sheenRoughness", label: "Sheen Roughness", min: 0, max: 1, step: 0.01, group: "Sheen" },
71
- { key: "specularIntensity", label: "Specular Intensity", min: 0, max: 2, step: 0.01, group: "Specular" },
72
- { key: "anisotropy", label: "Anisotropy", min: 0, max: 1, step: 0.01, group: "Anisotropy" },
73
- { key: "anisotropyRotation", label: "Anisotropy Rotation", min: 0, max: 6.28, step: 0.01, group: "Anisotropy" },
74
- { key: "emissiveIntensity", label: "Emissive Intensity", min: 0, max: 5, step: 0.1, group: "Emissive" },
75
- ];
76
-
77
- function _formatMatValue(value: number, step: number): string {
78
- const decimals = step < 0.1 ? 2 : 1;
79
- return value.toFixed(decimals);
80
- }
81
-
82
- const buttons = ["plane", "play", "pause", "stop"];
83
-
84
- const listeners = new EventListenerManager();
85
-
86
- // =============================================================================
87
- // INTERFACES
88
- // =============================================================================
89
-
90
- /**
91
- * Options for Display constructor
92
- */
93
- export interface DisplayOptions {
94
- measureTools: boolean;
95
- measurementDebug: boolean;
96
- selectTool: boolean;
97
- explodeTool: boolean;
98
- zscaleTool: boolean;
99
- zebraTool: boolean;
100
- studioTool: boolean;
101
- glass: boolean;
102
- tools: boolean;
103
- cadWidth: number;
104
- height: number;
105
- treeWidth: number;
106
- treeHeight?: number;
107
- theme: ThemeInput;
108
- pinning: boolean;
109
- canvas?: HTMLCanvasElement;
110
- gl?: WebGLRenderingContext | WebGL2RenderingContext;
111
- }
112
-
113
- /**
114
- * Options for setSizes method
115
- */
116
- interface SizeOptions {
117
- cadWidth?: number;
118
- height?: number;
119
- treeWidth?: number;
120
- treeHeight?: number;
121
- glass?: boolean;
122
- tools?: boolean;
123
- }
124
-
125
- /**
126
- * Options for canvas capture
127
- */
128
- interface CaptureOptions {
129
- taskId: string;
130
- render: () => void;
131
- onComplete?: () => void;
132
- }
133
-
134
- /**
135
- * Result of canvas capture
136
- */
137
- interface CaptureResult {
138
- task: string;
139
- dataUrl: string | ArrayBuffer | null;
140
- }
141
-
142
- /**
143
- * Stored event for cleanup
144
- */
145
- type StoredEvent = [string, string, EventListener];
146
-
147
- // =============================================================================
148
- // DISPLAY CLASS
149
- // =============================================================================
150
-
151
- /**
152
- * Main entry point for three-cad-viewer. Creates the UI and manages the viewer.
153
- *
154
- * Display handles:
155
- * - DOM structure (toolbar, tree view, tabs, sliders)
156
- * - User interaction (button clicks, slider changes)
157
- * - State subscriptions (UI updates when viewer state changes)
158
- * - Theme management (light/dark/browser preference)
159
- *
160
- * ## Usage
161
- * ```typescript
162
- * import { Display } from 'three-cad-viewer';
163
- *
164
- * const container = document.getElementById('viewer');
165
- * const display = new Display(container, {
166
- * cadWidth: 800,
167
- * height: 600,
168
- * theme: 'light'
169
- * });
170
- *
171
- * // Load and render CAD shapes
172
- * display.render(shapesData, states, renderOptions);
173
- *
174
- * // Access viewer for programmatic control
175
- * display.viewer.setAxes(true);
176
- *
177
- * // Cleanup when done
178
- * display.dispose();
179
- * ```
180
- *
181
- * ## Options
182
- * @see DisplayOptions for all configuration options
183
- *
184
- * @public
185
- */
186
- class Display {
187
- // DOM Elements - all initialized in constructor from template
188
- container!: HTMLElement;
189
- cadBody!: HTMLElement;
190
- cadView!: HTMLElement;
191
- cadTree!: HTMLElement;
192
- cadTreeScrollContainer!: HTMLElement;
193
- cadTreeToggles!: HTMLElement;
194
- cadClipToggles!: HTMLElement;
195
- cadMaterialToggles!: HTMLElement;
196
- cadZebraToggles!: HTMLElement;
197
- cadStudioToggles!: HTMLElement;
198
- cadClip!: HTMLElement;
199
- cadMaterial!: HTMLElement;
200
- cadZebra!: HTMLElement;
201
- cadStudio!: HTMLElement;
202
- private _spinnerEl: HTMLElement | null = null;
203
- private _warningBannerEl: HTMLElement | null = null;
204
- private _warningBannerTimer: ReturnType<typeof setTimeout> | null = null;
205
- private _spinnerCount: number = 0;
206
- // Material editor state
207
- private _matEditorPath: string | null = null;
208
- private _matEditorClones: Map<string, { original: THREE.MeshPhysicalMaterial; clone: THREE.MeshPhysicalMaterial }> = new Map();
209
- private _savedMatEditorChanges: Map<string, Record<string, number>> = new Map();
210
- private _matEditorDragAbort: AbortController | null = null;
211
- private _matEditorInputAbort: AbortController | null = null;
212
- cadInfo!: HTMLElement;
213
- cadAnim!: HTMLElement;
214
- private _animWasVisible: boolean = false;
215
- cadTools!: HTMLElement;
216
- cadHelp!: HTMLElement;
217
- tabTree!: HTMLElement;
218
- tabClip!: HTMLElement;
219
- tabMaterial!: HTMLElement;
220
- tabZebra!: HTMLElement;
221
- tabStudio!: HTMLElement;
222
- tickValueElement!: HTMLElement;
223
- tickInfoElement!: HTMLElement;
224
- distanceMeasurementPanel!: HTMLElement;
225
- propertiesMeasurementPanel!: HTMLElement;
226
- planeLabels!: HTMLElement[];
227
- animationSlider: HTMLInputElement | null;
228
-
229
- // Grouped UI elements for CAD tools (implements DisplayLike interface)
230
- measurementPanels: MeasurementPanelElements;
231
- filterDropdown: FilterDropdownElements;
232
-
233
- // Toolbar
234
- cadTool: Toolbar;
235
- clickButtons: Record<string, ClickButton>;
236
- buttons: Record<string, Button>;
237
-
238
- // Sliders
239
- clipSliders: Slider[] | null;
240
- ambientlightSlider: Slider | undefined;
241
- directionallightSlider: Slider | undefined;
242
- metalnessSlider: Slider | undefined;
243
- roughnessSlider: Slider | undefined;
244
- zebraCountSlider: Slider | undefined;
245
- zebraOpacitySlider: Slider | undefined;
246
- zebraDirectionSlider: Slider | undefined;
247
- studioEnvIntensitySlider: Slider | undefined;
248
- studioExposureSlider: Slider | undefined;
249
- studioEnvRotationSlider: Slider | undefined;
250
- studioShadowIntensitySlider: Slider | undefined;
251
- studioShadowSoftnessSlider: Slider | undefined;
252
- studioAOIntensitySlider: Slider | undefined;
253
-
254
- // State - set in setupUI() which is called at end of Viewer constructor
255
- viewer!: Viewer;
256
- state!: ViewerState;
257
- measureTools: boolean;
258
- measurementDebug: boolean;
259
- selectTool: boolean;
260
- explodeTool: boolean;
261
- zscaleTool: boolean;
262
- zScale: number;
263
- glass: boolean;
264
- tools: boolean;
265
- cadWidth: number;
266
- height: number;
267
- treeWidth: number;
268
- theme: ThemeInput;
269
- lastPlaneState: boolean;
270
- help_shown: boolean;
271
- info_shown: boolean;
272
- tools_shown: boolean;
273
-
274
- // Info panel
275
- _info: Info;
276
-
277
- // Events and subscriptions
278
- _events: StoredEvent[];
279
- _unsubscribers: (() => void)[];
280
-
281
- // Filter dropdown
282
- shapeFilterDropDownMenu: FilterByDropDownMenu;
283
-
284
- // Media query for theme
285
- mediaQuery: MediaQueryList | undefined;
286
-
287
- // ---------------------------------------------------------------------------
288
- // Constructor & Toolbar Setup
289
- // ---------------------------------------------------------------------------
290
-
291
- /**
292
- * Create Display.
293
- * @param container - the DOM element that should contain the Display
294
- * @param options - display options
295
- * @public
296
- */
297
- constructor(container: HTMLElement, options: DisplayOptions) {
298
- this.container = container;
299
- this.container.setAttribute("tabindex", "0");
300
- this.container.style.outline = "none";
301
- this.container.innerHTML = TEMPLATE(this.container.id);
302
-
303
- this.cadBody = this.getElement("tcv_cad_body");
304
-
305
- this.measureTools = options.measureTools;
306
- this.measurementDebug = options.measurementDebug;
307
- this.selectTool = options.selectTool;
308
- this.explodeTool = options.explodeTool;
309
- this.zscaleTool = options.zscaleTool;
310
- this.zScale = 1.0;
311
-
312
- this.cadTool = new Toolbar(
313
- this.getElement("tcv_cad_toolbar"),
314
- container.id,
315
- {
316
- getVisibleWidth: () =>
317
- this.glass ? this.cadWidth : this.cadWidth + this.treeWidth,
318
- getWidthThreshold: () => this._widthThreshold(),
319
- features: {
320
- measureTools: this.measureTools,
321
- selectTool: this.selectTool,
322
- explodeTool: this.explodeTool,
323
- },
324
- },
325
- );
326
- this.cadView = this.getElement("tcv_cad_view");
327
- this.distanceMeasurementPanel = this.getElement(
328
- "tcv_distance_measurement_panel",
329
- );
330
- this.propertiesMeasurementPanel = this.getElement(
331
- "tcv_properties_measurement_panel",
332
- );
333
-
334
- // Initialize grouped UI elements for CAD tools
335
- this.measurementPanels = {
336
- distancePanel: this.distanceMeasurementPanel,
337
- propertiesPanel: this.propertiesMeasurementPanel,
338
- };
339
- this.filterDropdown = {
340
- container: this.getElement("tcv_shape_filter"),
341
- dropdown: this.getElement("tcv_filter_dropdown"),
342
- icon: this.getElement("tcv_filter_icon"),
343
- value: this.getElement("tcv_filter_value"),
344
- content: this.getElement("tcv_filter_content"),
345
- options: {
346
- none: this.getElement("tvc_filter_none"),
347
- vertex: this.getElement("tvc_filter_vertex"),
348
- edge: this.getElement("tvc_filter_edge"),
349
- face: this.getElement("tvc_filter_face"),
350
- solid: this.getElement("tvc_filter_solid"),
351
- },
352
- };
353
-
354
- this.cadTree = this.getElement("tcv_cad_tree_container");
355
- this.cadTreeScrollContainer = this.getElement("tcv_box_content");
356
- this.cadTreeToggles = this.getElement("tcv_toggles_tree");
357
- this.cadClipToggles = this.getElement("tcv_toggles_clip");
358
- this.cadMaterialToggles = this.getElement("tcv_toggles_material");
359
- this.cadZebraToggles = this.getElement("tcv_toggles_zebra");
360
- this.cadStudioToggles = this.getElement("tcv_toggles_studio");
361
- this.cadClip = this.getElement("tcv_cad_clip_container");
362
- this.cadMaterial = this.getElement("tcv_cad_material_container");
363
- this.cadZebra = this.getElement("tcv_cad_zebra_container");
364
- this.cadStudio = this.getElement("tcv_cad_studio_container");
365
- this._spinnerEl = this.container.querySelector(".tcv_studio_spinner") as HTMLElement | null;
366
- this._warningBannerEl = this.container.querySelector(".tcv_warning_banner") as HTMLElement | null;
367
- this.container.addEventListener("tcv-material-warnings", ((e: CustomEvent<string[]>) => {
368
- this._showWarningBanner(
369
- `Unresolved material tag(s): ${e.detail.map(t => `"${t}"`).join(", ")}`,
370
- );
371
- }) as EventListener);
372
- this.tabTree = this.getElement("tcv_tab_tree");
373
- this.tabClip = this.getElement("tcv_tab_clip");
374
- this.tabZebra = this.getElement("tcv_tab_zebra");
375
- this.tabMaterial = this.getElement("tcv_tab_material");
376
- this.tabStudio = this.getElement("tcv_tab_studio");
377
- this.cadInfo = this.getElement("tcv_cad_info_container");
378
- this._info = new Info(this.cadInfo);
379
- this.tickValueElement = this.getElement("tcv_tick_size_value");
380
- this.tickInfoElement = this.getElement("tcv_tick_size");
381
- this.cadAnim = this.getElement("tcv_cad_animation");
382
- this.cadTools = this.getElement("tcv_cad_tools");
383
- if (!options.zebraTool) {
384
- this.tabZebra.style.display = "none";
385
- }
386
- if (options.studioTool === false) {
387
- this.tabStudio.style.display = "none";
388
- }
389
- this.cadHelp = this.getElement("tcv_cad_help");
390
- listeners.add(this.cadHelp, "contextmenu", (e) => {
391
- e.preventDefault();
392
- e.stopPropagation();
393
- return false;
394
- });
395
- this.planeLabels = [];
396
- for (let i = 1; i < 4; i++) {
397
- this.planeLabels.push(this.getElement(`tcv_lbl_norm_plane${i}`));
398
- }
399
- // viewer and state are set in setupUI(), called at end of Viewer constructor
400
- this.glass = options.glass;
401
- this.tools = options.tools;
402
- this.cadWidth = options.cadWidth;
403
- this.height = options.height;
404
- this.treeWidth = options.treeWidth;
405
- this._events = [];
406
- this._unsubscribers = [];
407
-
408
- this.setSizes(options);
409
-
410
- // Note: activeTab is managed by ViewerState, not stored locally
411
- this.cadTree.style.display = "block";
412
- this.cadClip.style.display = "none";
413
- this.cadMaterial.style.display = "none";
414
- this.cadZebra.style.display = "none";
415
- this.cadStudio.style.display = "none";
416
- this.clipSliders = null;
417
-
418
- // Note: activeTool is managed by ViewerState, not stored locally
419
-
420
- this.lastPlaneState = false;
421
- this.help_shown = true;
422
- this.info_shown = !this.glass;
423
- this.tools_shown = true;
424
-
425
- const theme = options.theme;
426
- this.theme = theme;
427
-
428
- this.setButtonBackground();
429
-
430
- this.clickButtons = {};
431
- this.buttons = {};
432
-
433
- this.clickButtons["axes"] = new ClickButton(
434
- theme,
435
- "axes",
436
- "Show axes",
437
- this.setAxes,
438
- );
439
- this.cadTool.addButton(this.clickButtons["axes"], -1);
440
- this.cadTool.addEllipsis(new Ellipsis(0, this.cadTool.maximize));
441
- this.clickButtons["axes0"] = new ClickButton(
442
- theme,
443
- "axes0",
444
- "Show axes at origin (0,0,0)",
445
- this.setAxes0,
446
- );
447
- this.cadTool.addButton(this.clickButtons["axes0"], 0);
448
- this.clickButtons["grid"] = new ClickButton(
449
- theme,
450
- "grid",
451
- "Show grid",
452
- this.setGrid,
453
- false,
454
- ["xy", "xz", "yz"],
455
- );
456
- this.cadTool.addButton(this.clickButtons["grid"], 0);
457
- this.cadTool.addSeparator();
458
- this.clickButtons["perspective"] = new ClickButton(
459
- theme,
460
- "perspective",
461
- "Use perspective camera",
462
- this.setOrtho,
463
- );
464
- this.cadTool.addButton(this.clickButtons["perspective"], -1);
465
- this.cadTool.addEllipsis(new Ellipsis(1, this.cadTool.maximize));
466
- this.clickButtons["transparent"] = new ClickButton(
467
- theme,
468
- "transparent",
469
- "Show transparent faces",
470
- this.setTransparent,
471
- );
472
- this.cadTool.addButton(this.clickButtons["transparent"], 1);
473
- this.clickButtons["blackedges"] = new ClickButton(
474
- theme,
475
- "blackedges",
476
- "Show black edges",
477
- this.setBlackEdges,
478
- );
479
- this.cadTool.addButton(this.clickButtons["blackedges"], 1);
480
- this.cadTool.addSeparator();
481
-
482
- this.buttons["reset"] = new Button(
483
- theme,
484
- "reset",
485
- "Reset view",
486
- this.reset,
487
- );
488
- this.cadTool.addButton(this.buttons["reset"], -1);
489
- this.cadTool.addEllipsis(new Ellipsis(2, this.cadTool.maximize));
490
- this.buttons["resize"] = new Button(
491
- theme,
492
- "resize",
493
- "Resize object",
494
- this.resize,
495
- );
496
- this.cadTool.addButton(this.buttons["resize"], 2);
497
-
498
- this.buttons["iso"] = new Button(
499
- theme,
500
- "iso",
501
- "Switch to iso view",
502
- this.setView,
503
- );
504
- this.cadTool.addButton(this.buttons["iso"], 2);
505
- this.buttons["front"] = new Button(
506
- theme,
507
- "front",
508
- "Switch to front view",
509
- this.setView,
510
- );
511
- this.cadTool.addButton(this.buttons["front"], 2);
512
- this.buttons["rear"] = new Button(
513
- theme,
514
- "rear",
515
- "Switch to back view",
516
- this.setView,
517
- );
518
- this.cadTool.addButton(this.buttons["rear"], 2);
519
- this.buttons["top"] = new Button(
520
- theme,
521
- "top",
522
- "Switch to top view",
523
- this.setView,
524
- );
525
- this.cadTool.addButton(this.buttons["top"], 2);
526
- this.buttons["bottom"] = new Button(
527
- theme,
528
- "bottom",
529
- "Switch to bottom view",
530
- this.setView,
531
- );
532
- this.cadTool.addButton(this.buttons["bottom"], 2);
533
- this.buttons["left"] = new Button(
534
- theme,
535
- "left",
536
- "Switch to left view",
537
- this.setView,
538
- );
539
- this.cadTool.addButton(this.buttons["left"], 2);
540
- this.buttons["right"] = new Button(
541
- theme,
542
- "right",
543
- "Switch to right view",
544
- this.setView,
545
- );
546
- this.cadTool.addButton(this.buttons["right"], 2);
547
-
548
- this.cadTool.addSeparator();
549
-
550
- this.clickButtons["explode"] = new ClickButton(
551
- theme,
552
- "explode",
553
- "Explode tool",
554
- this.setExplode,
555
- );
556
- if (this.explodeTool && !this.zscaleTool) {
557
- this.cadTool.addButton(this.clickButtons["explode"], -1);
558
- }
559
-
560
- this.clickButtons["zscale"] = new ClickButton(
561
- theme,
562
- "zscale",
563
- "Scale along the Z-axis",
564
- this.setZScale,
565
- );
566
- if (this.zscaleTool && !this.explodeTool) {
567
- this.cadTool.addButton(this.clickButtons["zscale"], -1);
568
- this.showZScale(false);
569
- const el = this.getInputElement("tcv_zscale_slider");
570
- listeners.add(el, "change", (e) => {
571
- if (!(e.target instanceof HTMLInputElement)) return;
572
- this.zScale = parseInt(e.target.value);
573
- this.viewer.setZscaleValue(parseInt(e.target.value));
574
- });
575
- }
576
-
577
- this.clickButtons["distance"] = new ClickButton(
578
- theme,
579
- "distance",
580
- "Measure distance between shapes",
581
- this.setTool,
582
- );
583
- this.cadTool.addButton(this.clickButtons["distance"], 3);
584
- const count =
585
- (this.measureTools ? 2 : 0) +
586
- (this.explodeTool ? 1 : 0) +
587
- (this.selectTool ? 1 : 0) +
588
- (this.zscaleTool ? 1 : 0);
589
- if (count > 1) {
590
- this.cadTool.addEllipsis(new Ellipsis(3, this.cadTool.maximize));
591
- }
592
-
593
- this.clickButtons["properties"] = new ClickButton(
594
- theme,
595
- "properties",
596
- "Show shape properties",
597
- this.setTool,
598
- );
599
- this.cadTool.addButton(this.clickButtons["properties"], 3);
600
-
601
- this.clickButtons["select"] = new ClickButton(
602
- theme,
603
- "select",
604
- "Copy shape IDs to clipboard",
605
- this.setTool,
606
- );
607
- this.cadTool.addButton(this.clickButtons["select"], 3);
608
-
609
- this.cadTool.defineGroup([
610
- this.clickButtons["explode"],
611
- this.clickButtons["distance"],
612
- this.clickButtons["properties"],
613
- this.clickButtons["select"],
614
- ]);
615
-
616
- listeners.add(document, "keydown", (e) => {
617
- if (e instanceof KeyboardEvent && e.key === "Escape" && this.help_shown) {
618
- e.preventDefault();
619
- this.showHelp(false);
620
- }
621
- });
622
-
623
- this.cadTool.addSeparator();
624
-
625
- this.buttons["pin"] = new Button(
626
- theme,
627
- "pin",
628
- "Pin viewer as png",
629
- this.pinAsPng,
630
- );
631
- this.buttons["pin"].alignRight();
632
- this.cadTool.addButton(this.buttons["pin"], -1);
633
- this.shapeFilterDropDownMenu = new FilterByDropDownMenu(this);
634
-
635
- this.showPinning(options.pinning);
636
-
637
- this.buttons["help"] = new Button(theme, "help", "Help", this.toggleHelp);
638
- this.cadTool.addButton(this.buttons["help"], -1);
639
-
640
- // Initialize animation slider (will be set up in setupUI)
641
- this.animationSlider = null;
642
- }
643
-
644
- // ---------------------------------------------------------------------------
645
- // Private Helpers
646
- // ---------------------------------------------------------------------------
647
-
648
- setButtonBackground(): void {
649
- for (const btn of buttons) {
650
- const elements = this.container.getElementsByClassName(`tcv_${btn}`);
651
- for (let i = 0; i < elements.length; i++) {
652
- const el = elements[i];
653
- el.classList.add(`tcv_button_${btn}`);
654
- }
655
- }
656
- }
657
-
658
- /**
659
- * Calculate the width threshold for toolbar collapse.
660
- * @returns The threshold width in pixels.
661
- */
662
- private _widthThreshold(): number {
663
- let threshold = 770;
664
- if (!this.state.get("pinning")) threshold -= 30;
665
- if (!this.state.get("selectTool")) threshold -= 30;
666
- if (!this.state.get("explodeTool") && !this.state.get("zscaleTool"))
667
- threshold -= 30;
668
- return threshold;
669
- }
670
-
671
- /**
672
- * Update toolbar collapse state based on available width.
673
- * Maximizes toolbar if width is sufficient, minimizes otherwise.
674
- * @param availableWidth - The available width in pixels.
675
- */
676
- updateToolbarCollapse(availableWidth: number): void {
677
- if (availableWidth >= this._widthThreshold()) {
678
- this.cadTool.maximize();
679
- } else {
680
- this.cadTool.minimize();
681
- }
682
- }
683
-
684
- private setupCheckEvent(
685
- name: string,
686
- fn: EventListener,
687
- flag?: boolean,
688
- ): void {
689
- const el = this.getInputElement(name);
690
- listeners.add(el, "change", fn);
691
- if (flag !== undefined) {
692
- el.checked = flag;
693
- }
694
- this._events.push(["change", name, fn]);
695
- }
696
-
697
- private setupClickEvent(name: string, fn: EventListener): void {
698
- const el = this.getElement(name);
699
- listeners.add(el, "click", fn);
700
- this._events.push(["click", name, fn]);
701
- }
702
-
703
- private setupRadioEvent(name: string, fn: EventListener): void {
704
- const el = this.getElement(name);
705
- listeners.add(el, "change", fn);
706
- this._events.push(["change", name, fn]);
707
- }
708
-
709
- /**
710
- * Wire a select element's change event
711
- */
712
- private setupSelectEvent(name: string, fn: EventListener): void {
713
- const el = this.getElement(name);
714
- listeners.add(el, "change", fn);
715
- this._events.push(["change", name, fn]);
716
- }
717
-
718
- /**
719
- * Get a DOM element by class name (internal use only).
720
- * @param name - Name of the DOM element class
721
- * @returns The DOM element
722
- */
723
- private getElement(name: string): HTMLElement {
724
- const el = this.container?.getElementsByClassName(name)[0];
725
- // In browser DOM, getElementsByClassName always returns HTMLElement subclasses
726
- if (el instanceof HTMLElement) return el;
727
- // Return a dummy element to satisfy type checker - callers handle missing elements gracefully
728
- return document.createElement("div");
729
- }
730
-
731
- /**
732
- * Get an input element by class name (internal use only).
733
- * @param name - Name of the DOM element class
734
- * @returns The input element
735
- */
736
- private getInputElement(name: string): HTMLInputElement {
737
- const el = this.container?.getElementsByClassName(name)[0];
738
- if (el instanceof HTMLInputElement) return el;
739
- // Return a dummy element to satisfy type checker - callers handle missing elements gracefully
740
- return document.createElement("input");
741
- }
742
-
743
- // ---------------------------------------------------------------------------
744
- // Disposal & UI Layout
745
- // ---------------------------------------------------------------------------
746
-
747
- /**
748
- * Dispose of all resources. Call when done with the viewer.
749
- *
750
- * Disposes:
751
- * - All state subscriptions
752
- * - Event listeners
753
- * - Toolbar and buttons
754
- * - Sliders
755
- * - DOM content
756
- *
757
- * After dispose(), the Display instance should not be used.
758
- *
759
- * @public
760
- */
761
- dispose(): void {
762
- // Unsubscribe from all state subscriptions first (prevents callbacks to disposed UI)
763
- if (this._unsubscribers) {
764
- for (const unsubscribe of this._unsubscribers) {
765
- unsubscribe();
766
- }
767
- this._unsubscribers = [];
768
- }
769
-
770
- listeners.dispose();
771
-
772
- // Dispose toolbar and all its buttons/ellipses
773
- this.cadTool.dispose();
774
-
775
- // Dispose sliders
776
- if (this.clipSliders) {
777
- for (const slider of this.clipSliders) {
778
- slider.dispose();
779
- }
780
- }
781
- this.ambientlightSlider?.dispose();
782
- this.directionallightSlider?.dispose();
783
- this.metalnessSlider?.dispose();
784
- this.roughnessSlider?.dispose();
785
- this.zebraCountSlider?.dispose();
786
- this.zebraOpacitySlider?.dispose();
787
- this.zebraDirectionSlider?.dispose();
788
- this.studioEnvIntensitySlider?.dispose();
789
- this.studioExposureSlider?.dispose();
790
- this.studioEnvRotationSlider?.dispose();
791
- this.studioAOIntensitySlider?.dispose();
792
-
793
- // Clean up material editor
794
- this._matEditorDragAbort?.abort();
795
- this.disposeMatEditorClones();
796
-
797
- // Clear DOM content (elements remain valid until Display is GC'd)
798
- this.cadTree.innerHTML = "";
799
- const attachedCanvas = this.cadView.querySelector("canvas");
800
- if (attachedCanvas && attachedCanvas.parentElement === this.cadView) {
801
- this.cadView.removeChild(attachedCanvas);
802
- }
803
- this.container.innerHTML = "";
804
- }
805
-
806
- // ---------------------------------------------------------------------------
807
- // Info Panel Methods
808
- // ---------------------------------------------------------------------------
809
-
810
- /**
811
- * Add HTML content to the info panel.
812
- * @param html - The HTML string to add.
813
- */
814
- addInfoHtml(html: string): void {
815
- this._info.addHtml(html);
816
- }
817
-
818
- /**
819
- * Display the ready message with viewer version and control mode.
820
- * @param version - Viewer version string.
821
- * @param control - Control mode name (e.g., "orbit", "trackball").
822
- */
823
- showReadyMessage(version: string, control: string): void {
824
- this._info.readyMsg(version, control);
825
- }
826
-
827
- /**
828
- * Display camera target center information.
829
- * @param center - The center coordinates [x, y, z].
830
- */
831
- showCenterInfo(center: Vector3Tuple): void {
832
- this._info.centerInfo(center);
833
- }
834
-
835
- /**
836
- * Display bounding box information for a selected object.
837
- * @param path - The object's path in the tree.
838
- * @param name - The object's name.
839
- * @param bb - The bounding box to display.
840
- */
841
- showBoundingBoxInfo(path: string, name: string, bb: THREE.Box3): void {
842
- this._info.bbInfo(path, name, bb);
843
- }
844
-
845
- // ---------------------------------------------------------------------------
846
- // Canvas Capture
847
- // ---------------------------------------------------------------------------
848
-
849
- /**
850
- * Capture the canvas as a data URL.
851
- * @param options - Capture options.
852
- * @returns Promise resolving to task ID and data URL.
853
- */
854
- captureCanvas(options: CaptureOptions): Promise<CaptureResult> {
855
- const { taskId, render, onComplete } = options;
856
- const canvas = this.getCanvas();
857
- if (!(canvas instanceof HTMLCanvasElement)) {
858
- return Promise.reject(new Error("Canvas element not found"));
859
- }
860
-
861
- // Render the scene
862
- render();
863
-
864
- return new Promise((resolve) => {
865
- canvas.toBlob((blob) => {
866
- const reader = new FileReader();
867
- reader.addEventListener(
868
- "load",
869
- () => {
870
- resolve({ task: taskId, dataUrl: reader.result });
871
- if (onComplete) {
872
- onComplete();
873
- }
874
- },
875
- { once: true },
876
- );
877
- reader.readAsDataURL(blob!);
878
- });
879
- });
880
- }
881
-
882
- /**
883
- * Set the width and height of the different UI elements (tree, canvas and info box).
884
- * @param options - Size options
885
- * @public
886
- */
887
- setSizes(options: SizeOptions): void {
888
- if (options.cadWidth) {
889
- this.cadWidth = options.cadWidth;
890
- this.cadView.style.width = px(options.cadWidth);
891
- }
892
- if (options.height) {
893
- this.height = options.height;
894
- this.cadView.style.height = px(options.height);
895
- }
896
-
897
- if (options.treeWidth) {
898
- this.treeWidth = options.treeWidth;
899
- this.cadTree.parentElement!.parentElement!.style.width = px(
900
- options.treeWidth,
901
- );
902
- this.cadInfo.parentElement!.parentElement!.style.width = px(
903
- options.treeWidth,
904
- );
905
- }
906
- if (!options.glass && options.treeHeight) {
907
- this.cadTree.parentElement!.parentElement!.style.height = px(
908
- options.treeHeight,
909
- );
910
- this.cadInfo.parentElement!.parentElement!.style.height = px(
911
- this.height - options.treeHeight - 4,
912
- );
913
- }
914
-
915
- if (options.tools && !options.glass) {
916
- this.cadTool.container.style.width = px(
917
- this.treeWidth + this.cadWidth + 4,
918
- );
919
- this.cadBody.style.width = px(this.treeWidth + this.cadWidth + 4);
920
- } else {
921
- this.cadTool.container.style.width = px(this.cadWidth + 2);
922
- this.cadBody.style.width = px(this.cadWidth + 2);
923
- }
924
-
925
- this.cadBody.style.height = px(this.height + 4);
926
- }
927
-
928
- // ---------------------------------------------------------------------------
929
- // UI Setup & State Subscriptions
930
- // ---------------------------------------------------------------------------
931
-
932
- /**
933
- * Set up the UI and attach the canvas element.
934
- * Called by Viewer constructor, not intended for direct use.
935
- * @param viewer - The viewer instance for this UI.
936
- * @param canvasElement - The Three.js renderer canvas to attach.
937
- * @internal
938
- */
939
- setupUI(viewer: Viewer, canvasElement: HTMLCanvasElement): void {
940
- this.viewer = viewer;
941
- this.state = viewer.state;
942
-
943
- // Attach the canvas element to the CAD view
944
- this.attachCanvas(canvasElement);
945
-
946
- // Theme
947
- if (this.theme === "browser") {
948
- this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
949
- listeners.add(this.mediaQuery, "change", (event) => {
950
- if (event instanceof MediaQueryListEvent && event.matches) {
951
- this.setTheme("dark");
952
- } else {
953
- this.setTheme("light");
954
- }
955
- });
956
- }
957
-
958
- this.setupClickEvent("tcv_expand_root", this.handleCollapseNodes);
959
- this.setupClickEvent("tcv_collapse_singles", this.handleCollapseNodes);
960
- this.setupClickEvent("tcv_collapse_all", this.handleCollapseNodes);
961
- this.setupClickEvent("tcv_expand", this.handleCollapseNodes);
962
-
963
- this.setupClickEvent("tcv_clip_reset", this.handleClipReset);
964
- this.setupClickEvent("tcv_material_reset", this.handleMaterialReset);
965
- this.setupClickEvent("tcv_zebra_reset", this.handleZebraReset);
966
-
967
- this.setupClickEvent("tcv_toggle_info", this.toggleInfo);
968
- this.setupClickEvent("tcv_toggle_tools_wrapper", this.toggleToolsPanel);
969
-
970
- this.help_shown = true;
971
- this.info_shown = !this.glass;
972
-
973
- const tabs = [
974
- "tcv_tab_tree",
975
- "tcv_tab_clip",
976
- "tcv_tab_zebra",
977
- "tcv_tab_material",
978
- "tcv_tab_studio",
979
- ];
980
- tabs.forEach((name) => {
981
- this.setupClickEvent(name, this.selectTab);
982
- });
983
-
984
- this.clipSliders = [];
985
- for (let i = 1; i < 4; i++) {
986
- this.clipSliders.push(
987
- new Slider(`plane${i}`, 0, 100, this.container, {
988
- handler: this.refreshPlane,
989
- notifyCallback: (change, notify) =>
990
- this.viewer.checkChanges(change, notify),
991
- onSetSlider: this.refreshPlane,
992
- }),
993
- );
994
- }
995
-
996
- const viewerReadyCheck = () => this.viewer.ready;
997
-
998
- this.ambientlightSlider = new Slider(
999
- "ambientlight",
1000
- 0,
1001
- 400,
1002
- this.container,
1003
- {
1004
- handler: this.viewer.setAmbientLight,
1005
- percentage: true,
1006
- isReadyCheck: viewerReadyCheck,
1007
- },
1008
- );
1009
- this.directionallightSlider = new Slider(
1010
- "pointlight",
1011
- 0,
1012
- 400,
1013
- this.container,
1014
- {
1015
- handler: this.viewer.setDirectLight,
1016
- percentage: true,
1017
- isReadyCheck: viewerReadyCheck,
1018
- },
1019
- );
1020
- this.metalnessSlider = new Slider("metalness", 0, 100, this.container, {
1021
- handler: this.viewer.setMetalness,
1022
- percentage: true,
1023
- isReadyCheck: viewerReadyCheck,
1024
- });
1025
- this.roughnessSlider = new Slider("roughness", 0, 100, this.container, {
1026
- handler: this.viewer.setRoughness,
1027
- percentage: true,
1028
- isReadyCheck: viewerReadyCheck,
1029
- });
1030
-
1031
- this.zebraCountSlider = new Slider("zebra_count", 2, 50, this.container, {
1032
- handler: this.viewer.setZebraCount,
1033
- isReadyCheck: viewerReadyCheck,
1034
- });
1035
- this.zebraOpacitySlider = new Slider(
1036
- "zebra_opacity",
1037
- 0.0,
1038
- 1.0,
1039
- this.container,
1040
- {
1041
- handler: this.viewer.setZebraOpacity,
1042
- isReadyCheck: viewerReadyCheck,
1043
- },
1044
- );
1045
- this.zebraDirectionSlider = new Slider(
1046
- "zebra_direction",
1047
- 0,
1048
- 90,
1049
- this.container,
1050
- {
1051
- handler: this.viewer.setZebraDirection,
1052
- isReadyCheck: viewerReadyCheck,
1053
- },
1054
- );
1055
-
1056
- this.getInputElement("tcv_zebra_color1").checked = true;
1057
- this.getInputElement("tcv_zebra_mapping1").checked = true;
1058
-
1059
- this.setupCheckEvent(
1060
- "tcv_clip_plane_helpers",
1061
- this.setClipPlaneHelpers,
1062
- false,
1063
- );
1064
- this.setupCheckEvent(
1065
- "tcv_clip_intersection",
1066
- this.setClipIntersection,
1067
- false,
1068
- );
1069
- this.setupCheckEvent("tcv_clip_caps", this.setObjectColorCaps, false);
1070
-
1071
- for (let i = 1; i < 4; i++) {
1072
- this.setupClickEvent(
1073
- `tcv_btn_norm_plane${i}`,
1074
- this.setClipNormalFromPosition,
1075
- );
1076
- }
1077
-
1078
- [1, 2, 3].forEach((id) => {
1079
- this.setupRadioEvent(`tcv_zebra_color${id}`, this.setZebraColorScheme);
1080
- });
1081
-
1082
- [1, 2].forEach((id) => {
1083
- this.setupRadioEvent(`tcv_zebra_mapping${id}`, this.setZebraMappingMode);
1084
- });
1085
-
1086
- // Studio tab controls
1087
- this.studioEnvIntensitySlider = new Slider(
1088
- "studio_env_intensity",
1089
- 0,
1090
- 300,
1091
- this.container,
1092
- {
1093
- handler: this.handleStudioEnvIntensity,
1094
- percentage: true,
1095
- isReadyCheck: viewerReadyCheck,
1096
- },
1097
- );
1098
- this.studioExposureSlider = new Slider(
1099
- "studio_exposure",
1100
- 0,
1101
- 300,
1102
- this.container,
1103
- {
1104
- handler: this.handleStudioExposure,
1105
- percentage: true,
1106
- isReadyCheck: viewerReadyCheck,
1107
- },
1108
- );
1109
- this.studioEnvRotationSlider = new Slider(
1110
- "studio_env_rotation",
1111
- 0,
1112
- 360,
1113
- this.container,
1114
- {
1115
- handler: this.handleStudioEnvRotation,
1116
- isReadyCheck: viewerReadyCheck,
1117
- },
1118
- );
1119
-
1120
- this.studioShadowIntensitySlider = new Slider(
1121
- "studio_shadow_intensity",
1122
- 0,
1123
- 100,
1124
- this.container,
1125
- {
1126
- handler: this.handleStudioShadowIntensity,
1127
- percentage: true,
1128
- isReadyCheck: viewerReadyCheck,
1129
- },
1130
- );
1131
- this.studioShadowSoftnessSlider = new Slider(
1132
- "studio_shadow_softness",
1133
- 0,
1134
- 100,
1135
- this.container,
1136
- {
1137
- handler: this.handleStudioShadowSoftness,
1138
- percentage: true,
1139
- isReadyCheck: viewerReadyCheck,
1140
- },
1141
- );
1142
- this.studioAOIntensitySlider = new Slider(
1143
- "studio_ao_intensity",
1144
- 0,
1145
- 30,
1146
- this.container,
1147
- {
1148
- handler: this.handleStudioAOIntensity,
1149
- isReadyCheck: viewerReadyCheck,
1150
- },
1151
- );
1152
- this.setupCheckEvent(
1153
- "tcv_studio_4k_env_maps",
1154
- this.handleStudio4kEnvMaps,
1155
- false,
1156
- );
1157
-
1158
- this.setupSelectEvent(
1159
- "tcv_studio_environment",
1160
- this.handleStudioEnvironment,
1161
- );
1162
- this.setupSelectEvent(
1163
- "tcv_studio_background",
1164
- this.handleStudioBackground,
1165
- );
1166
- this.setupSelectEvent(
1167
- "tcv_studio_tone_mapping",
1168
- this.handleStudioToneMapping,
1169
- );
1170
- this.setupSelectEvent(
1171
- "tcv_studio_texture_mapping",
1172
- this.handleStudioTextureMapping,
1173
- );
1174
-
1175
- this.setupClickEvent("tcv_studio_reset", this.handleStudioReset);
1176
- this.setupClickEvent("tcv_mat_editor_toggle", this.handleMatEditorToggle);
1177
- this.setupClickEvent("tcv_mat_editor_close", () => this.closeMatEditor());
1178
- this.setupClickEvent("tcv_mat_editor_reset", this.handleMatEditorReset);
1179
- this._initMatEditorDrag();
1180
-
1181
- this.setupClickEvent("tcv_play", this.controlAnimation);
1182
- this.setupClickEvent("tcv_pause", this.controlAnimation);
1183
- this.setupClickEvent("tcv_stop", this.controlAnimation);
1184
- const animSlider = this.container.getElementsByClassName(
1185
- "tcv_animation_slider",
1186
- )[0];
1187
- this.animationSlider =
1188
- animSlider instanceof HTMLInputElement ? animSlider : null;
1189
- // Initial value synced via subscription with immediate:true
1190
- if (this.animationSlider) {
1191
- listeners.add(this.animationSlider, "input", this.animationChange);
1192
- }
1193
- // Animation control starts hidden (state default is false)
1194
-
1195
- this.showHelp(false);
1196
- this.showDistancePanel(false);
1197
- this.showPropertiesPanel(false);
1198
-
1199
- this.showMeasureTools(this.measureTools);
1200
- this.showSelectTool(this.selectTool);
1201
- this.showExplodeTool(this.explodeTool);
1202
- this.showZScaleTool(this.zscaleTool);
1203
-
1204
- // Subscribe to state changes
1205
- this.subscribeToStateChanges();
1206
-
1207
- // Focus handling for keyboard shortcuts
1208
- listeners.add(this.container, "mousedown", () => this.container.focus());
1209
- listeners.add(this.container, "keydown", this._handleKeyboardShortcut);
1210
- }
1211
-
1212
- /**
1213
- * Subscribe to ViewerState changes to keep UI in sync.
1214
- * Stores unsubscribe functions for cleanup in dispose().
1215
- * @internal
1216
- */
1217
- private subscribeToStateChanges(): void {
1218
- const state = this.viewer.state;
1219
- this._unsubscribers = [];
1220
-
1221
- // Helper to subscribe and track unsubscribe function with type inference
1222
- const sub = <K extends Parameters<typeof state.subscribe>[0]>(
1223
- key: K,
1224
- callback: Parameters<typeof state.subscribe<K>>[1],
1225
- options?: { immediate?: boolean },
1226
- ) => {
1227
- this._unsubscribers.push(state.subscribe(key, callback, options));
1228
- };
1229
-
1230
- // Subscribe to individual state keys that affect UI
1231
- sub("axes", (change) => {
1232
- this.clickButtons["axes"]?.set(change.new);
1233
- });
1234
-
1235
- sub("axes0", (change) => {
1236
- this.clickButtons["axes0"]?.set(change.new);
1237
- });
1238
-
1239
- sub("ortho", (change) => {
1240
- this.clickButtons["perspective"]?.set(!change.new);
1241
- });
1242
-
1243
- sub("transparent", (change) => {
1244
- this.clickButtons["transparent"]?.set(change.new);
1245
- });
1246
-
1247
- sub("blackEdges", (change) => {
1248
- this.clickButtons["blackedges"]?.set(change.new);
1249
- });
1250
-
1251
- sub(
1252
- "grid",
1253
- (change) => {
1254
- const gridButton = this.clickButtons["grid"];
1255
- if (gridButton) {
1256
- const grid = change.new;
1257
- // Update main button state (true if any grid is visible)
1258
- gridButton.set(grid.some((g) => g));
1259
- // Update individual checkboxes
1260
- if (gridButton.checkElems) {
1261
- gridButton.checkElems["xy"].checked = grid[0];
1262
- gridButton.checkElems["xz"].checked = grid[1];
1263
- gridButton.checkElems["yz"].checked = grid[2];
1264
- }
1265
- }
1266
- },
1267
- { immediate: true },
1268
- );
1269
-
1270
- sub("tools", (change) => {
1271
- this.showTools(change.new);
1272
- const animationMode = this.state.get("animationMode");
1273
- this.cadAnim.style.display =
1274
- change.new && animationMode !== "none" ? "block" : "none";
1275
- });
1276
-
1277
- sub("glass", (change) => {
1278
- this.glassMode(change.new);
1279
- });
1280
-
1281
- sub("theme", (change) => {
1282
- this.setTheme(change.new);
1283
- });
1284
-
1285
- sub("clipIntersection", (change) => {
1286
- this.getInputElement("tcv_clip_intersection").checked = change.new;
1287
- });
1288
-
1289
- sub(
1290
- "clipPlaneHelpers",
1291
- (change) => {
1292
- this.checkElement("tcv_clip_plane_helpers", change.new);
1293
- },
1294
- { immediate: true },
1295
- );
1296
-
1297
- sub("clipObjectColors", (change) => {
1298
- this.getInputElement("tcv_clip_caps").checked = change.new;
1299
- });
1300
-
1301
- // Clip slider subscriptions - handle runtime changes
1302
- // Initial sync happens via syncClipSlidersFromState() after limits are set
1303
- sub("clipSlider0", (change) => {
1304
- if (change.new !== -1) {
1305
- this.clipSliders?.[0]?.setValueFromState(change.new);
1306
- }
1307
- });
1308
- sub("clipSlider1", (change) => {
1309
- if (change.new !== -1) {
1310
- this.clipSliders?.[1]?.setValueFromState(change.new);
1311
- }
1312
- });
1313
- sub("clipSlider2", (change) => {
1314
- if (change.new !== -1) {
1315
- this.clipSliders?.[2]?.setValueFromState(change.new);
1316
- }
1317
- });
1318
-
1319
- // Material slider subscriptions (state stores 0-1, sliders display 0-100 or 0-400)
1320
- // Use immediate:true to sync sliders with initial state values
1321
- sub(
1322
- "ambientIntensity",
1323
- (change) => {
1324
- this.ambientlightSlider?.setValueFromState(change.new * 100);
1325
- },
1326
- { immediate: true },
1327
- );
1328
- sub(
1329
- "directIntensity",
1330
- (change) => {
1331
- this.directionallightSlider?.setValueFromState(change.new * 100);
1332
- },
1333
- { immediate: true },
1334
- );
1335
- sub(
1336
- "metalness",
1337
- (change) => {
1338
- this.metalnessSlider?.setValueFromState(change.new * 100);
1339
- },
1340
- { immediate: true },
1341
- );
1342
- sub(
1343
- "roughness",
1344
- (change) => {
1345
- this.roughnessSlider?.setValueFromState(change.new * 100);
1346
- },
1347
- { immediate: true },
1348
- );
1349
-
1350
- // Zebra slider subscriptions
1351
- sub("zebraCount", (change) => {
1352
- this.zebraCountSlider?.setValueFromState(change.new);
1353
- });
1354
- sub("zebraOpacity", (change) => {
1355
- this.zebraOpacitySlider?.setValueFromState(change.new);
1356
- });
1357
- sub("zebraDirection", (change) => {
1358
- this.zebraDirectionSlider?.setValueFromState(change.new);
1359
- });
1360
-
1361
- // Zebra radio button subscriptions
1362
- sub("zebraColorScheme", (change) => {
1363
- this.setZebraColorSchemeSelect(change.new);
1364
- });
1365
- sub("zebraMappingMode", (change) => {
1366
- this.setZebraMappingModeSelect(change.new);
1367
- });
1368
-
1369
- // Studio tab subscriptions
1370
- sub("studioEnvironment", (change) => {
1371
- this._syncEnvDropdown(change.new);
1372
- this._update4kCheckboxEnabled(change.new);
1373
- });
1374
- sub(
1375
- "studioEnvIntensity",
1376
- (change) => {
1377
- this.studioEnvIntensitySlider?.setValueFromState(change.new * 100);
1378
- },
1379
- { immediate: true },
1380
- );
1381
- sub(
1382
- "studioEnvRotation",
1383
- (change) => {
1384
- this.studioEnvRotationSlider?.setValueFromState(change.new);
1385
- },
1386
- { immediate: true },
1387
- );
1388
- sub("studioBackground", (change) => {
1389
- const el = this.container.querySelector(".tcv_studio_background");
1390
- if (el instanceof HTMLSelectElement) el.value = change.new;
1391
- });
1392
- sub("studioToneMapping", (change) => {
1393
- const el = this.container.querySelector(".tcv_studio_tone_mapping");
1394
- if (el instanceof HTMLSelectElement) el.value = change.new;
1395
- });
1396
- sub("studioTextureMapping", (change) => {
1397
- const el = this.container.querySelector(".tcv_studio_texture_mapping");
1398
- if (el instanceof HTMLSelectElement) el.value = change.new;
1399
- });
1400
- sub(
1401
- "studioExposure",
1402
- (change) => {
1403
- this.studioExposureSlider?.setValueFromState(change.new * 100);
1404
- },
1405
- { immediate: true },
1406
- );
1407
- sub(
1408
- "studioShadowIntensity",
1409
- (change) => {
1410
- this.studioShadowIntensitySlider?.setValueFromState(change.new * 100);
1411
- },
1412
- { immediate: true },
1413
- );
1414
- sub(
1415
- "studioShadowSoftness",
1416
- (change) => {
1417
- this.studioShadowSoftnessSlider?.setValueFromState(change.new * 100);
1418
- },
1419
- { immediate: true },
1420
- );
1421
- sub(
1422
- "studioAOIntensity",
1423
- (change) => {
1424
- this.studioAOIntensitySlider?.setValueFromState(change.new * 10);
1425
- },
1426
- { immediate: true },
1427
- );
1428
- sub("studio4kEnvMaps", (change) => {
1429
- this.getInputElement("tcv_studio_4k_env_maps").checked = change.new;
1430
- });
1431
-
1432
- // Animation/Explode mode subscription - controls slider visibility, label, and explode button
1433
- sub(
1434
- "animationMode",
1435
- (change) => {
1436
- const mode = change.new;
1437
- const toolsEnabled = this.state.get("tools");
1438
- // Show/hide slider control (only when tools panel is enabled)
1439
- this.cadAnim.style.display =
1440
- toolsEnabled && mode !== "none" ? "block" : "none";
1441
- // Set label: "A" for animation, "E" for explode
1442
- this.getElement("tcv_animation_label").innerHTML =
1443
- mode === "explode" ? "E" : "A";
1444
- // Update explode button state
1445
- this.clickButtons["explode"]?.set(mode === "explode");
1446
- },
1447
- { immediate: true },
1448
- );
1449
- sub(
1450
- "animationSliderValue",
1451
- (change) => {
1452
- if (this.animationSlider) {
1453
- this.animationSlider.value = String(change.new);
1454
- }
1455
- },
1456
- { immediate: true },
1457
- );
1458
-
1459
- // ZScale toolbar button subscription
1460
- sub("zscaleActive", (change) => {
1461
- this.clickButtons["zscale"]?.set(change.new);
1462
- });
1463
-
1464
- // Camera button highlight subscription
1465
- sub("highlightedButton", (change) => {
1466
- // Clear all highlights first
1467
- const buttonNames = [
1468
- "front",
1469
- "rear",
1470
- "top",
1471
- "bottom",
1472
- "left",
1473
- "right",
1474
- "iso",
1475
- ] as const;
1476
- buttonNames.forEach((btn) => {
1477
- this.buttons[btn]?.highlight(false);
1478
- });
1479
- // Highlight the new button if set
1480
- if (change.new && change.new in this.buttons) {
1481
- this.buttons[change.new]?.highlight(true);
1482
- }
1483
- });
1484
-
1485
- // Active tool subscription
1486
- sub("activeTool", (change) => {
1487
- // Deactivate old tool button
1488
- if (change.old && change.old in this.clickButtons) {
1489
- this.clickButtons[change.old]?.set(false);
1490
- }
1491
- // Activate new tool button
1492
- if (change.new && change.new in this.clickButtons) {
1493
- this.clickButtons[change.new]?.set(true);
1494
- }
1495
- });
1496
-
1497
- // Active tab subscription
1498
- sub("activeTab", (change) => {
1499
- this.switchToTab(change.new, change.old ?? undefined);
1500
- });
1501
- }
1502
-
1503
- /**
1504
- * Initialize UI elements from current state.
1505
- * Called once during initialization. Subsequent updates happen via state subscriptions.
1506
- * @internal
1507
- */
1508
- updateUI(): void {
1509
- const state = this.viewer.state;
1510
- const axes = state.get("axes");
1511
- const axes0 = state.get("axes0");
1512
- const ortho = state.get("ortho");
1513
- const transparent = state.get("transparent");
1514
- const blackEdges = state.get("blackEdges");
1515
- const tools = state.get("tools");
1516
- const glass = state.get("glass");
1517
- const clipPlaneHelpers = state.get("clipPlaneHelpers");
1518
-
1519
- if (typeof axes === "boolean") this.clickButtons["axes"].set(axes);
1520
- if (typeof axes0 === "boolean") this.clickButtons["axes0"].set(axes0);
1521
- if (typeof ortho === "boolean")
1522
- this.clickButtons["perspective"].set(!ortho);
1523
- if (typeof transparent === "boolean")
1524
- this.clickButtons["transparent"].set(transparent);
1525
- if (typeof blackEdges === "boolean")
1526
- this.clickButtons["blackedges"].set(blackEdges);
1527
-
1528
- if (typeof tools === "boolean") this.showTools(tools);
1529
- if (typeof glass === "boolean") this.glassMode(glass);
1530
- const width = this.glass ? this.cadWidth : this.cadWidth + this.treeWidth;
1531
- this.updateToolbarCollapse(width);
1532
-
1533
- // Initialize lastPlaneState from options (used for tab switching)
1534
- this.lastPlaneState =
1535
- typeof clipPlaneHelpers === "boolean" ? clipPlaneHelpers : false;
1536
-
1537
- // Sync material, zebra, and studio sliders with current state values
1538
- this.syncMaterialSlidersFromState();
1539
- this.syncZebraSlidersFromState();
1540
- this.syncStudioSlidersFromState();
1541
- }
1542
-
1543
- // ---------------------------------------------------------------------------
1544
- // DOM Management
1545
- // ---------------------------------------------------------------------------
1546
-
1547
- /**
1548
- * Check or uncheck a checkbox
1549
- * @param name - name of the check box, see getElement
1550
- * @param flag - whether to check or uncheck
1551
- */
1552
- checkElement(name: string, flag: boolean): void {
1553
- this.getInputElement(name).checked = flag;
1554
- }
1555
-
1556
- /**
1557
- * Attach the canvas element to the CAD view container.
1558
- * @param canvasElement - The canvas to attach.
1559
- */
1560
- private attachCanvas(canvasElement: HTMLCanvasElement): void {
1561
- // If the canvas is already attached elsewhere
1562
- // do not re-parent it into this display.
1563
- if (canvasElement.parentElement && canvasElement.parentElement !== this.cadView) {
1564
- listeners.add(canvasElement, "click", () => {
1565
- if (this.help_shown) {
1566
- this.showHelp(false);
1567
- }
1568
- });
1569
- return;
1570
- }
1571
-
1572
- const existingCanvas = this.cadView.querySelector("canvas");
1573
- if (existingCanvas) {
1574
- this.cadView.replaceChild(canvasElement, existingCanvas);
1575
- } else {
1576
- this.cadView.appendChild(canvasElement);
1577
- }
1578
- listeners.add(canvasElement, "click", () => {
1579
- if (this.help_shown) {
1580
- this.showHelp(false);
1581
- }
1582
- });
1583
- }
1584
-
1585
- /**
1586
- * Get the DOM canvas element
1587
- */
1588
- getCanvas(): Element {
1589
- const localCanvas = this.cadView.querySelector("canvas");
1590
- if (localCanvas) return localCanvas;
1591
- return this.viewer.renderer.domElement;
1592
- }
1593
-
1594
- /**
1595
- * Clear the Cad tree
1596
- */
1597
- clearCadTree(): void {
1598
- this.cadTree.innerHTML = "";
1599
- }
1600
-
1601
- /**
1602
- * Add the Cad tree and other UI elements like Clipping
1603
- * @param cadTree - the DOM element that contains the cadTree
1604
- */
1605
- addCadTree(cadTree: HTMLElement): void {
1606
- this.cadTree.appendChild(cadTree);
1607
- }
1608
-
1609
- // ---------------------------------------------------------------------------
1610
- // Toolbar Button Handlers: View Settings
1611
- // ---------------------------------------------------------------------------
1612
-
1613
- /**
1614
- * Checkbox Handler for setting the axes parameter
1615
- */
1616
- setAxes = (_name: string, flag: boolean): void => {
1617
- this.viewer.setAxes(flag);
1618
- };
1619
-
1620
- /**
1621
- * Checkbox Handler for setting the grid parameter
1622
- */
1623
- setGrid = (name: string, flag: boolean): void => {
1624
- this.viewer.setGrid(name, flag);
1625
- };
1626
-
1627
- /**
1628
- * Checkbox Handler for setting the axes0 parameter
1629
- */
1630
- setAxes0 = (_name: string, flag: boolean): void => {
1631
- this.viewer.setAxes0(flag);
1632
- };
1633
-
1634
- /**
1635
- * Checkbox Handler for setting the ortho parameter
1636
- */
1637
- setOrtho = (_name: string, flag: boolean): void => {
1638
- this.viewer.switchCamera(!flag);
1639
- };
1640
-
1641
- /**
1642
- * Checkbox Handler for setting the transparent parameter
1643
- */
1644
- setTransparent = (_name: string, flag: boolean): void => {
1645
- this.viewer.setTransparent(flag);
1646
- };
1647
-
1648
- /**
1649
- * Checkbox Handler for setting the black edges parameter
1650
- */
1651
- setBlackEdges = (_name: string, flag: boolean): void => {
1652
- this.viewer.setBlackEdges(flag);
1653
- };
1654
-
1655
- // ---------------------------------------------------------------------------
1656
- // Toolbar Button Handlers: Tools
1657
- // ---------------------------------------------------------------------------
1658
-
1659
- /**
1660
- * Handler for the explode button
1661
- */
1662
- setExplode = (_name: string, flag: boolean): void => {
1663
- this.viewer.setExplode(flag);
1664
- };
1665
-
1666
- /**
1667
- * Show or hide the Explode checkbox
1668
- */
1669
- showExplode = (flag: boolean): void => {
1670
- const el = this.getElement("tcv_explode_widget");
1671
- el.style.display = flag ? "inline-block" : "none";
1672
- };
1673
-
1674
- /**
1675
- * Checkbox Handler for setting the zscale mode
1676
- */
1677
- setZScale = (_name: string, flag: boolean): void => {
1678
- this.showZScale(flag);
1679
- this.viewer.nestedGroup.setZScale(1);
1680
- this.viewer.update(true);
1681
- this.getInputElement("tcv_zscale_slider").value = "1";
1682
- };
1683
-
1684
- /**
1685
- * Show or hide the ZScale slider
1686
- */
1687
- showZScale = (flag: boolean): void => {
1688
- const el = this.getElement("tcv_cad_zscale");
1689
- el.style.display = flag ? "inline-block" : "none";
1690
- };
1691
-
1692
- /**
1693
- * Checkbox Handler for setting the tools mode.
1694
- * Delegates state mutations to Viewer.activateTool() to maintain unidirectional data flow.
1695
- */
1696
- setTool = (name: string, flag: boolean): void => {
1697
- // Block tool activation while Studio mode is active
1698
- if (flag && this.viewer.isStudioActive) {
1699
- return;
1700
- }
1701
- this.viewer.toggleAnimationLoop(flag);
1702
- const activeTool = this.state.get("activeTool");
1703
- const currentTool = typeof activeTool === "string" ? activeTool : "";
1704
-
1705
- if (flag) {
1706
- // Delegate state mutations to Viewer
1707
- this.viewer.activateTool(name, true);
1708
-
1709
- if (
1710
- ["distance", "properties", "angle", "select"].includes(name) &&
1711
- !["distance", "properties", "angle", "select"].includes(currentTool)
1712
- ) {
1713
- this.viewer.toggleGroup(true);
1714
- this.viewer.toggleTab(true);
1715
- }
1716
- this.viewer.setRaycastMode(flag);
1717
- this.shapeFilterDropDownMenu.setRaycaster(this.viewer.raycaster!);
1718
-
1719
- if (name === "distance") {
1720
- this.viewer.cadTools.enable(ToolTypes.DISTANCE);
1721
- this.viewer.checkChanges({ activeTool: ToolTypes.DISTANCE });
1722
- } else if (name === "properties") {
1723
- this.viewer.cadTools.enable(ToolTypes.PROPERTIES);
1724
- this.viewer.checkChanges({ activeTool: ToolTypes.PROPERTIES });
1725
- } else if (name === "select") {
1726
- this.viewer.cadTools.enable(ToolTypes.SELECT);
1727
- this.viewer.checkChanges({ activeTool: ToolTypes.SELECT });
1728
- }
1729
- } else {
1730
- if (currentTool === name || name === "explode") {
1731
- this.viewer.toggleGroup(false);
1732
- this.viewer.toggleTab(false);
1733
- }
1734
- if (name === "distance") {
1735
- this.viewer.cadTools.disable();
1736
- } else if (name === "properties") {
1737
- this.viewer.cadTools.disable();
1738
- } else if (name === "select") {
1739
- this.viewer.cadTools.disable();
1740
- }
1741
- this.viewer.checkChanges({ activeTool: ToolTypes.NONE });
1742
- this.viewer.clearSelection();
1743
-
1744
- // Delegate state mutations to Viewer
1745
- this.viewer.activateTool(name, false);
1746
-
1747
- this.viewer.setRaycastMode(flag);
1748
- }
1749
- this.viewer.setPickHandler(!flag);
1750
- this.shapeFilterDropDownMenu.show(flag);
1751
- };
1752
-
1753
- /**
1754
- * Show or hide the CAD tools (UI update only).
1755
- * This method only updates the visual state - it does not modify ViewerState.
1756
- */
1757
- showTools = (flag: boolean): void => {
1758
- this.tools = flag;
1759
- const tb = this.getElement("tcv_cad_toolbar");
1760
- const cn = this.getElement("tcv_cad_navigation");
1761
- const tickInfo = this.tickInfoElement;
1762
- if (flag) {
1763
- tb.style.height = "38px";
1764
- tb.style.display = "flex";
1765
- cn.style.height = "38px";
1766
- cn.style.display = "block";
1767
-
1768
- // Tick size badge belongs to the tools UI. Restore it only when tools are visible and at least one grid is visible.
1769
- if (tickInfo) {
1770
- if (this.viewer?.ready) {
1771
- tickInfo.style.display = this.viewer.rendered.gridHelper.getVisible()
1772
- ? "block"
1773
- : "none";
1774
- } else {
1775
- tickInfo.style.display = "none";
1776
- }
1777
- }
1778
- } else {
1779
- tb.style.height = "0px";
1780
- tb.style.display = "none";
1781
- cn.style.height = "0px";
1782
- cn.style.display = "none";
1783
-
1784
- if (tickInfo) {
1785
- tickInfo.style.display = "none";
1786
- }
1787
- }
1788
- };
1789
-
1790
- /**
1791
- * Show or hides measurement tools, measurement tools needs a backend to be used.
1792
- */
1793
- showMeasureTools = (flag: boolean): void => {
1794
- this.clickButtons["distance"].show(flag);
1795
- this.clickButtons["properties"].show(flag);
1796
- };
1797
-
1798
- /**
1799
- * Show or hides select tool
1800
- */
1801
- showSelectTool = (flag: boolean): void => {
1802
- this.clickButtons["select"].show(flag);
1803
- };
1804
-
1805
- /**
1806
- * Show or hides explode tool
1807
- */
1808
- showExplodeTool = (flag: boolean): void => {
1809
- this.clickButtons["explode"].show(flag);
1810
- };
1811
-
1812
- /**
1813
- * Show or hides ZScale tool
1814
- */
1815
- showZScaleTool = (flag: boolean): void => {
1816
- this.clickButtons["zscale"].show(flag);
1817
- if (!flag) {
1818
- this.showZScale(false);
1819
- }
1820
- };
1821
-
1822
- /**
1823
- * Deactivate any running tool and hide tool buttons for Studio mode.
1824
- * Called when entering Studio mode.
1825
- * @internal
1826
- */
1827
- private _deactivateToolsForStudio(): void {
1828
- // If a tool is currently active, deactivate it cleanly
1829
- const activeTool = this.state.get("activeTool");
1830
- if (activeTool && ["distance", "properties", "angle", "select"].includes(activeTool)) {
1831
- this.clickButtons[activeTool]?.set(false);
1832
- this.setTool(activeTool, false);
1833
- // setTool→toggleTab(false) silently sets activeTab to "tree" (no notification).
1834
- // Restore to "studio" so the next tab click correctly detects Studio as oldTab.
1835
- this.state.set("activeTab", "studio", false);
1836
- }
1837
-
1838
- // Hide tool buttons
1839
- this.showMeasureTools(false);
1840
- this.showSelectTool(false);
1841
- }
1842
-
1843
- /**
1844
- * Restore tool button visibility after leaving Studio mode.
1845
- * Respects original feature flags (measureTools, selectTool).
1846
- * Does not auto-activate any tool.
1847
- * @internal
1848
- */
1849
- private _restoreToolsAfterStudio(): void {
1850
- this.showMeasureTools(this.measureTools);
1851
- this.showSelectTool(this.selectTool);
1852
- }
1853
-
1854
- // ---------------------------------------------------------------------------
1855
- // Clipping Handlers
1856
- // ---------------------------------------------------------------------------
1857
-
1858
- /**
1859
- * Checkbox Handler for setting the clip planes parameter
1860
- */
1861
- setClipPlaneHelpers = (e: Event): void => {
1862
- if (!(e.target instanceof HTMLInputElement)) return;
1863
- const flag = e.target.checked;
1864
- this.lastPlaneState = flag;
1865
- this.viewer.setClipPlaneHelpers(flag);
1866
- };
1867
-
1868
- /**
1869
- * Checkbox Handler for setting the clip intersection parameter
1870
- */
1871
- setClipIntersection = (e: Event): void => {
1872
- if (!(e.target instanceof HTMLInputElement)) return;
1873
- this.viewer.setClipIntersection(e.target.checked);
1874
- };
1875
-
1876
- /**
1877
- * Checkbox Handler for toggling the clip caps
1878
- */
1879
- setObjectColorCaps = (e: Event): void => {
1880
- if (!(e.target instanceof HTMLInputElement)) return;
1881
- this.viewer.setClipObjectColorCaps(e.target.checked);
1882
- };
1883
-
1884
- /**
1885
- * Set the normal at index to the current viewing direction
1886
- */
1887
- setClipNormalFromPosition = (e: Event): void => {
1888
- if (!(e.target instanceof HTMLElement)) return;
1889
- const uiIndex = parseInt(e.target.classList[0].slice(-1));
1890
- const index = uiIndex - 1;
1891
- if (!isClipIndex(index)) return;
1892
- this.viewer.setClipNormalFromPosition(index);
1893
- };
1894
-
1895
- /**
1896
- * Handler to set the label of a clipping normal widget
1897
- */
1898
- setNormalLabel = (
1899
- index: ClipIndex,
1900
- normal: [number, number, number],
1901
- ): void => {
1902
- this.planeLabels[index].innerHTML = `N=(${normal[0].toFixed(
1903
- 2,
1904
- )}, ${normal[1].toFixed(2)}, ${normal[2].toFixed(2)})`;
1905
- };
1906
-
1907
- // ---------------------------------------------------------------------------
1908
- // View Control Handlers
1909
- // ---------------------------------------------------------------------------
1910
-
1911
- /**
1912
- * Handler to reset position, zoom and up of the camera
1913
- */
1914
- reset = (): void => {
1915
- this.viewer.reset();
1916
- this.viewer.state.set("highlightedButton", null);
1917
- };
1918
-
1919
- /**
1920
- * Handler to reset zoom of the camera
1921
- */
1922
- resize = (): void => {
1923
- this.viewer.resize();
1924
- };
1925
-
1926
- /**
1927
- * Handler to set camera to a predefined position.
1928
- * Called by Button callback which passes the button name as string.
1929
- */
1930
- setView = (direction: string, focus: boolean = false): void => {
1931
- // Button names match CameraDirection values: "iso", "front", "rear", "left", "right", "top", "bottom"
1932
- this.viewer.presetCamera(direction as CameraDirection);
1933
- if (focus) {
1934
- this.viewer.centerVisibleObjects();
1935
- }
1936
- this.viewer.state.set("highlightedButton", direction);
1937
- this.viewer.keepHighlight = true;
1938
- this.viewer.update(true, false);
1939
- };
1940
-
1941
- /**
1942
- * Show/hide pinning button
1943
- */
1944
- showPinning(flag: boolean): void {
1945
- this.buttons["pin"].show(flag);
1946
- }
1947
-
1948
- /**
1949
- * Pin screenshot of canvas as PNG
1950
- */
1951
- pinAsPng = (_name: string, _shift: boolean): void => {
1952
- this.viewer.pinAsPng();
1953
- };
1954
-
1955
- // ---------------------------------------------------------------------------
1956
- // Tab Navigation & Tree Control
1957
- // ---------------------------------------------------------------------------
1958
-
1959
- /**
1960
- * Handler to activate a UI tab (tree / clipping / material / zebra)
1961
- */
1962
- selectTab = (e: Event): void => {
1963
- if (!(e.target instanceof HTMLElement)) return;
1964
- const tab = e.target.className.split(" ")[0];
1965
- const tabName = tab.slice(8);
1966
- if (
1967
- tabName === "clip" ||
1968
- tabName === "tree" ||
1969
- tabName === "zebra" ||
1970
- tabName === "material" ||
1971
- tabName === "studio"
1972
- ) {
1973
- this.viewer.setActiveTab(tabName);
1974
- }
1975
- };
1976
-
1977
- /**
1978
- * Switch to a tab (internal, called by activeTab subscription).
1979
- */
1980
- private switchToTab(newTab: ActiveTab, oldTab?: ActiveTab): void {
1981
- if (!["clip", "tree", "zebra", "material", "studio"].includes(newTab)) {
1982
- return;
1983
- }
1984
-
1985
- // --- Leave the OLD mode FIRST (before _updateVisibility runs) ---
1986
- // This ensures all state (clipping, materials, etc.) is restored
1987
- // before the new tab's controls are activated.
1988
- if (oldTab === "zebra" && newTab !== "zebra") {
1989
- this.viewer.enableZebraTool(false);
1990
- }
1991
- if (oldTab === "studio" && newTab !== "studio") {
1992
- this.closeMatEditor();
1993
- this._saveMatEditorChanges();
1994
- this.disposeMatEditorClones();
1995
- this.viewer.leaveStudioMode();
1996
- this._hideWarningBanner();
1997
- // Restore tool button visibility based on feature flags
1998
- this._restoreToolsAfterStudio();
1999
- }
2000
-
2001
- const _updateVisibility = (
2002
- showTree: boolean,
2003
- showClip: boolean,
2004
- showZebra: boolean,
2005
- showMaterial: boolean,
2006
- showStudio: boolean,
2007
- ) => {
2008
- this.cadTree.style.display = showTree ? "block" : "none";
2009
- this.cadTreeToggles.style.display = showTree ? "block" : "none";
2010
- this.cadClipToggles.style.display = showClip ? "block" : "none";
2011
- this.cadClip.style.display = showClip ? "block" : "none";
2012
- this.cadZebraToggles.style.display = showZebra ? "block" : "none";
2013
- this.cadZebra.style.display = showZebra ? "block" : "none";
2014
- this.cadMaterialToggles.style.display = showMaterial ? "block" : "none";
2015
- this.cadMaterial.style.display = showMaterial ? "block" : "none";
2016
- this.cadStudioToggles.style.display = showStudio ? "block" : "none";
2017
- this.cadStudio.style.display = showStudio ? "block" : "none";
2018
-
2019
- this.viewer.clipping.setVisible(showClip);
2020
- this.viewer.setLocalClipping(showClip);
2021
- if (!showClip) {
2022
- this.viewer.setClipPlaneHelpers(false);
2023
- }
2024
- // NOTE: zebra and studio leave calls removed from here --
2025
- // they now run above, before _updateVisibility is called.
2026
- };
2027
-
2028
- if (newTab === "tree") {
2029
- _updateVisibility(true, false, false, false, false);
2030
- this.viewer.nestedGroup.setBackVisible(false);
2031
- // Lazy-rendered tree nodes may be stale if the tree was rebuilt
2032
- // while this tab was hidden (display:none → getBoundingClientRect
2033
- // returns zero, so update() rendered nothing). Kick it now.
2034
- this.viewer.treeview?.update();
2035
- } else if (newTab === "clip") {
2036
- _updateVisibility(false, true, false, false, false);
2037
- this.viewer.nestedGroup.setBackVisible(true);
2038
- const clipIntersection = this.viewer.state.get("clipIntersection");
2039
- if (typeof clipIntersection === "boolean") {
2040
- this.viewer.setClipIntersection(clipIntersection);
2041
- }
2042
- this.viewer.setClipPlaneHelpers(this.lastPlaneState);
2043
- this.viewer.update(true, false);
2044
- } else if (newTab === "zebra") {
2045
- _updateVisibility(false, false, true, false, false);
2046
- this.viewer.enableZebraTool(true);
2047
- } else if (newTab === "material") {
2048
- _updateVisibility(false, false, false, true, false);
2049
- this.viewer.nestedGroup.setBackVisible(false);
2050
- } else if (newTab === "studio") {
2051
- _updateVisibility(false, false, false, false, true);
2052
- this.viewer.nestedGroup.setBackVisible(false);
2053
-
2054
- // Disable any active tool before entering Studio mode
2055
- this._deactivateToolsForStudio();
2056
-
2057
- this._showSpinner();
2058
- this.viewer.enterStudioMode().finally(() => {
2059
- this._hideSpinner();
2060
- this._reapplyMatEditorChanges();
2061
- this.syncStudioSlidersFromState();
2062
- this.viewer.update(true, false);
2063
- });
2064
- }
2065
-
2066
- // Update tab styling
2067
- [this.tabTree, this.tabClip, this.tabZebra, this.tabMaterial, this.tabStudio].forEach(
2068
- (tabEl) => {
2069
- tabEl.classList.add("tcv_tab-unselected");
2070
- tabEl.classList.remove("tcv_tab-selected");
2071
- },
2072
- );
2073
-
2074
- this.viewer.checkChanges({ tab: newTab });
2075
- if (newTab === "tree") {
2076
- this.tabTree.classList.add("tcv_tab-selected");
2077
- this.tabTree.classList.remove("tcv_tab-unselected");
2078
- } else if (newTab === "clip") {
2079
- this.tabClip.classList.add("tcv_tab-selected");
2080
- this.tabClip.classList.remove("tcv_tab-unselected");
2081
- } else if (newTab === "zebra") {
2082
- this.tabZebra.classList.add("tcv_tab-selected");
2083
- this.tabZebra.classList.remove("tcv_tab-unselected");
2084
- } else if (newTab === "material") {
2085
- this.tabMaterial.classList.add("tcv_tab-selected");
2086
- this.tabMaterial.classList.remove("tcv_tab-unselected");
2087
- } else if (newTab === "studio") {
2088
- this.tabStudio.classList.add("tcv_tab-selected");
2089
- this.tabStudio.classList.remove("tcv_tab-unselected");
2090
- }
2091
- }
2092
-
2093
- /** Show the toolbar spinner (ref-counted for overlapping async ops). */
2094
- private _showSpinner(): void {
2095
- this._spinnerCount++;
2096
- if (this._spinnerEl) this._spinnerEl.style.display = "block";
2097
- }
2098
-
2099
- /** Hide the toolbar spinner (only when all pending ops complete). */
2100
- private _hideSpinner(): void {
2101
- this._spinnerCount = Math.max(0, this._spinnerCount - 1);
2102
- if (this._spinnerCount === 0 && this._spinnerEl) {
2103
- this._spinnerEl.style.display = "none";
2104
- }
2105
- }
2106
-
2107
- /** Show a warning banner in the viewport. Auto-hides after 8 seconds. */
2108
- private _showWarningBanner(message: string): void {
2109
- if (!this._warningBannerEl) return;
2110
- this._warningBannerEl.textContent = message;
2111
- this._warningBannerEl.style.display = "block";
2112
- if (this._warningBannerTimer) clearTimeout(this._warningBannerTimer);
2113
- this._warningBannerTimer = setTimeout(() => this._hideWarningBanner(), 8000);
2114
- }
2115
-
2116
- /** Hide the warning banner. */
2117
- private _hideWarningBanner(): void {
2118
- if (this._warningBannerEl) this._warningBannerEl.style.display = "none";
2119
- if (this._warningBannerTimer) {
2120
- clearTimeout(this._warningBannerTimer);
2121
- this._warningBannerTimer = null;
2122
- }
2123
- }
2124
-
2125
- /**
2126
- * Toggle visibility of the clipping tab
2127
- */
2128
- toggleClippingTab = (flag: boolean): void => {
2129
- if (flag) {
2130
- this.tabClip.removeAttribute("disabled");
2131
- } else {
2132
- this.tabClip.setAttribute("disabled", "true");
2133
- }
2134
- this.tabClip.classList.toggle("tcv_tab-disabled", !flag);
2135
- };
2136
-
2137
- /**
2138
- * Collapse nodes handler (event handler)
2139
- * Translates button codes to CollapseState and calls viewer
2140
- */
2141
- handleCollapseNodes = (e: Event): void => {
2142
- if (!(e.target instanceof HTMLInputElement)) return;
2143
- const buttonCode = e.target.value;
2144
- const stateMap: Record<string, CollapseState> = {
2145
- "1": CollapseState.LEAVES,
2146
- R: CollapseState.ROOT,
2147
- C: CollapseState.COLLAPSED,
2148
- E: CollapseState.EXPANDED,
2149
- };
2150
- const state = stateMap[buttonCode];
2151
- if (state !== undefined) {
2152
- this.viewer.collapseNodes(state);
2153
- }
2154
- };
2155
-
2156
- // ---------------------------------------------------------------------------
2157
- // Clip Reset Handler
2158
- // ---------------------------------------------------------------------------
2159
-
2160
- /**
2161
- * Reset clip planes to default normals and slider positions
2162
- */
2163
- handleClipReset = (_e: Event): void => {
2164
- this.viewer.resetClip();
2165
- };
2166
-
2167
- // ---------------------------------------------------------------------------
2168
- // Material Handlers
2169
- // ---------------------------------------------------------------------------
2170
-
2171
- /**
2172
- * Reset material values to original values
2173
- */
2174
- handleMaterialReset = (_e: Event): void => {
2175
- this.viewer.resetMaterial();
2176
- };
2177
-
2178
- /**
2179
- * Reset zebra tool to default settings
2180
- */
2181
- handleZebraReset = (_e: Event): void => {
2182
- this.viewer.resetZebra();
2183
- };
2184
-
2185
- // ---------------------------------------------------------------------------
2186
- // Studio Tab Handlers
2187
- // ---------------------------------------------------------------------------
2188
-
2189
- /**
2190
- * Handler for Studio environment dropdown change
2191
- */
2192
- handleStudioEnvironment = (e: Event): void => {
2193
- if (!(e.target instanceof HTMLSelectElement)) return;
2194
- this._showSpinner();
2195
- this.state.set("studioEnvironment", e.target.value);
2196
- this.container.addEventListener("tcv-studio-ready", () => this._hideSpinner(), { once: true });
2197
- };
2198
-
2199
- /**
2200
- * Handler for Studio env intensity slider change.
2201
- * Slider range 0-200 with percentage=true, so value arrives as 0-2.
2202
- */
2203
- handleStudioEnvIntensity = (value: number): void => {
2204
- this.state.set("studioEnvIntensity", value);
2205
- };
2206
-
2207
- handleStudioEnvRotation = (value: number): void => {
2208
- this.state.set("studioEnvRotation", value);
2209
- };
2210
-
2211
- /**
2212
- * Handler for Studio background dropdown change.
2213
- * Validates against the StudioBackground union before setting state.
2214
- */
2215
- handleStudioBackground = (e: Event): void => {
2216
- if (!(e.target instanceof HTMLSelectElement)) return;
2217
- const value = e.target.value;
2218
- if (value === "grey" || value === "darkgrey" || value === "white" || value === "gradient" || value === "gradient-dark" || value === "environment" || value === "transparent") {
2219
- this.state.set("studioBackground", value);
2220
- }
2221
- };
2222
-
2223
- /**
2224
- * Handler for Studio tone mapping dropdown change.
2225
- * Validates against the StudioToneMapping union before setting state.
2226
- */
2227
- handleStudioToneMapping = (e: Event): void => {
2228
- if (!(e.target instanceof HTMLSelectElement)) return;
2229
- const value = e.target.value;
2230
- if (value === "neutral" || value === "ACES" || value === "none") {
2231
- this.state.set("studioToneMapping", value);
2232
- }
2233
- };
2234
-
2235
- /**
2236
- * Handler for Studio exposure slider change.
2237
- * Slider range 0-200 with percentage=true, so value arrives as 0-2.
2238
- */
2239
- handleStudioTextureMapping = (e: Event): void => {
2240
- if (!(e.target instanceof HTMLSelectElement)) return;
2241
- const value = e.target.value;
2242
- if (value === "triplanar" || value === "parametric") {
2243
- this._showSpinner();
2244
- this.state.set("studioTextureMapping", value);
2245
- this.container.addEventListener("tcv-studio-ready", () => this._hideSpinner(), { once: true });
2246
- }
2247
- };
2248
-
2249
- handleStudioExposure = (value: number): void => {
2250
- this.state.set("studioExposure", value);
2251
- };
2252
-
2253
- handleStudioShadowIntensity = (value: number): void => {
2254
- this.state.set("studioShadowIntensity", value);
2255
- };
2256
-
2257
- handleStudioShadowSoftness = (value: number): void => {
2258
- this.state.set("studioShadowSoftness", value);
2259
- };
2260
-
2261
- /**
2262
- * Handler for Studio AO intensity slider change.
2263
- * Slider range 0-30, divided by 10 → state gets 0-3.0.
2264
- * A value of 0 disables AO.
2265
- */
2266
- handleStudioAOIntensity = (value: number): void => {
2267
- this.state.set("studioAOIntensity", value / 10);
2268
- };
2269
-
2270
- /**
2271
- * Handler for Studio 4K env maps checkbox change.
2272
- * Shows a "Loading…" indicator while the new resolution downloads.
2273
- */
2274
- handleStudio4kEnvMaps = (e: Event): void => {
2275
- if (!(e.target instanceof HTMLInputElement)) return;
2276
- this._showSpinner();
2277
- this.state.set("studio4kEnvMaps", e.target.checked);
2278
- this.container.addEventListener("tcv-studio-ready", () => this._hideSpinner(), { once: true });
2279
- };
2280
-
2281
- /**
2282
- * Reset Studio tab values to defaults.
2283
- * Delegates to viewer.resetStudio() (same pattern as handleMaterialReset -> resetMaterial()).
2284
- */
2285
- handleStudioReset = (_e: Event): void => {
2286
- this.viewer.resetStudio();
2287
- };
2288
-
2289
- // ---------------------------------------------------------------------------
2290
- // Material Editor
2291
- // ---------------------------------------------------------------------------
2292
-
2293
- handleMatEditorToggle = (_e: Event): void => {
2294
- const dialog = this.container.querySelector(".tcv_mat_editor") as HTMLElement;
2295
- if (!dialog) return;
2296
-
2297
- // Toggle off if visible
2298
- if (dialog.style.display !== "none") {
2299
- this.closeMatEditor();
2300
- return;
2301
- }
2302
-
2303
- const result = this.viewer.getSelectedObjectGroup();
2304
- if (!result || !result.object.front) {
2305
- this._showMatEditorHint(dialog);
2306
- return;
2307
- }
2308
-
2309
- this.openMatEditor(result.object, result.path);
2310
- };
2311
-
2312
- private _showMatEditorHint(dialog: HTMLElement): void {
2313
- const pathEl = dialog.querySelector(".tcv_mat_editor_path") as HTMLElement;
2314
- if (pathEl) pathEl.style.display = "none";
2315
- const resetBtn = dialog.querySelector(".tcv_mat_editor_reset") as HTMLElement;
2316
- if (resetBtn) resetBtn.style.display = "none";
2317
- const content = dialog.querySelector(".tcv_mat_editor_content") as HTMLElement;
2318
- if (content) {
2319
- content.innerHTML = "";
2320
- const hint = document.createElement("div");
2321
- hint.style.padding = "12px 6px";
2322
- hint.style.textAlign = "center";
2323
- hint.style.opacity = "0.7";
2324
- hint.textContent = "Select object by double-click first";
2325
- content.appendChild(hint);
2326
- }
2327
- dialog.style.display = "";
2328
- }
2329
-
2330
- private openMatEditor(object: ObjectGroup, objectPath: string): void {
2331
- const dialog = this.container.querySelector(".tcv_mat_editor") as HTMLElement;
2332
- if (!dialog || !object.front) return;
2333
-
2334
- this._matEditorPath = objectPath;
2335
-
2336
- // Clone material on first edit so changes are per-object (reuse existing clone)
2337
- let mat: THREE.MeshPhysicalMaterial;
2338
- let originalMat: THREE.MeshPhysicalMaterial;
2339
- const existing = this._matEditorClones.get(objectPath);
2340
- if (existing && object.front.material === existing.clone) {
2341
- mat = existing.clone;
2342
- originalMat = existing.original;
2343
- } else {
2344
- const currentMat = object.front.material;
2345
- if (!(currentMat instanceof THREE.MeshPhysicalMaterial)) return;
2346
- originalMat = currentMat;
2347
- mat = currentMat.clone();
2348
- // Preserve triplanar mapping if the original material uses it
2349
- if (currentMat.customProgramCacheKey() === "triplanar" && object.shapeGeometry) {
2350
- applyTriplanarMapping(mat, object.shapeGeometry);
2351
- }
2352
- object.front.material = mat;
2353
- this._matEditorClones.set(objectPath, { original: originalMat, clone: mat });
2354
- }
2355
-
2356
- // Restore elements that _showMatEditorHint may have hidden
2357
- const resetBtn = dialog.querySelector(".tcv_mat_editor_reset") as HTMLElement;
2358
- if (resetBtn) resetBtn.style.display = "";
2359
-
2360
- // Update path display
2361
- const pathEl = dialog.querySelector(".tcv_mat_editor_path") as HTMLElement;
2362
- if (pathEl) {
2363
- pathEl.style.display = "";
2364
- const shortName = objectPath.split("/").pop() || objectPath;
2365
- pathEl.textContent = `${shortName} — ${object.materialTag || "(default)"}`;
2366
- pathEl.title = objectPath;
2367
- }
2368
-
2369
- // Mark toggle active
2370
- const toggleBtn = this.container.querySelector(".tcv_mat_editor_toggle") as HTMLElement;
2371
- if (toggleBtn) toggleBtn.classList.add("tcv_active");
2372
-
2373
- // Populate content — abort previous input listeners before rebuilding
2374
- this._matEditorInputAbort?.abort();
2375
- this._matEditorInputAbort = new AbortController();
2376
-
2377
- const content = dialog.querySelector(".tcv_mat_editor_content") as HTMLElement;
2378
- if (!content) return;
2379
- content.innerHTML = "";
2380
- this._buildMatEditorContent(content, mat, originalMat);
2381
-
2382
- dialog.style.display = "";
2383
- }
2384
-
2385
- closeMatEditor(): void {
2386
- this._matEditorInputAbort?.abort();
2387
- this._matEditorInputAbort = null;
2388
-
2389
- const dialog = this.container.querySelector(".tcv_mat_editor") as HTMLElement;
2390
- if (dialog) {
2391
- const content = dialog.querySelector(".tcv_mat_editor_content") as HTMLElement;
2392
- if (content) content.innerHTML = "";
2393
- dialog.style.display = "none";
2394
- }
2395
- const toggleBtn = this.container.querySelector(".tcv_mat_editor_toggle") as HTMLElement;
2396
- if (toggleBtn) toggleBtn.classList.remove("tcv_active");
2397
- this._matEditorPath = null;
2398
- }
2399
-
2400
- /** Dispose all cloned materials, restoring originals first (call on Studio mode exit) */
2401
- disposeMatEditorClones(): void {
2402
- this._matEditorInputAbort?.abort();
2403
- this._matEditorInputAbort = null;
2404
-
2405
- let groups: Record<string, THREE.Object3D> | null = null;
2406
- try { groups = this.viewer.rendered.nestedGroup.groups; } catch { /* not rendered */ }
2407
-
2408
- for (const [path, { original, clone }] of this._matEditorClones.entries()) {
2409
- // Restore original material on mesh before disposing the clone
2410
- if (groups) {
2411
- const group = groups[path] as ObjectGroup | undefined;
2412
- if (group?.front?.material === clone) {
2413
- group.front.material = original;
2414
- }
2415
- }
2416
- clone.dispose();
2417
- }
2418
- this._matEditorClones.clear();
2419
- }
2420
-
2421
- /** Save material editor property deltas so they survive a Studio mode leave/enter cycle. */
2422
- private _saveMatEditorChanges(): void {
2423
- this._savedMatEditorChanges.clear();
2424
- for (const [path, { original, clone }] of this._matEditorClones.entries()) {
2425
- const changes: Record<string, number> = {};
2426
- for (const param of MAT_EDITOR_PARAMS) {
2427
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2428
- const origVal = (original as any)[param.key] as number;
2429
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2430
- const cloneVal = (clone as any)[param.key] as number;
2431
- if (origVal !== cloneVal) {
2432
- changes[param.key] = cloneVal;
2433
- }
2434
- }
2435
- if (Object.keys(changes).length > 0) {
2436
- this._savedMatEditorChanges.set(path, changes);
2437
- }
2438
- }
2439
- }
2440
-
2441
- /** Reapply saved material editor changes after re-entering Studio mode. */
2442
- private _reapplyMatEditorChanges(): void {
2443
- if (this._savedMatEditorChanges.size === 0) return;
2444
- let groups: Record<string, THREE.Object3D>;
2445
- try { groups = this.viewer.rendered.nestedGroup.groups; } catch { return; }
2446
-
2447
- for (const [path, changes] of this._savedMatEditorChanges.entries()) {
2448
- const group = groups[path] as ObjectGroup | undefined;
2449
- if (!group?.front?.material) continue;
2450
- const currentMat = group.front.material;
2451
- if (!(currentMat instanceof THREE.MeshPhysicalMaterial)) continue;
2452
-
2453
- const clone = currentMat.clone();
2454
- if (currentMat.customProgramCacheKey() === "triplanar" && group.shapeGeometry) {
2455
- applyTriplanarMapping(clone, group.shapeGeometry);
2456
- }
2457
- for (const [key, value] of Object.entries(changes)) {
2458
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2459
- (clone as any)[key] = value;
2460
- }
2461
- group.front.material = clone;
2462
- this._matEditorClones.set(path, { original: currentMat as THREE.MeshPhysicalMaterial, clone });
2463
- }
2464
- this._savedMatEditorChanges.clear();
2465
- }
2466
-
2467
- /**
2468
- * Called by viewer.ts when the selected object changes.
2469
- * Updates or closes the material editor if it's open.
2470
- */
2471
- onSelectionChanged(newObjectId: string | null): void {
2472
- if (!this._matEditorPath) return; // Editor not open
2473
- if (newObjectId === this._matEditorPath) return; // Same object
2474
- this.closeMatEditor();
2475
- // Immediately reopen for the new object if one was selected
2476
- if (newObjectId) {
2477
- const result = this.viewer.getSelectedObjectGroup();
2478
- if (result?.object.front) {
2479
- this.openMatEditor(result.object, result.path);
2480
- }
2481
- }
2482
- }
2483
-
2484
- handleMatEditorReset = (_e: Event): void => {
2485
- if (!this._matEditorPath) return;
2486
- const result = this.viewer.getSelectedObjectGroup();
2487
- if (!result?.object.front) return;
2488
-
2489
- const entry = this._matEditorClones.get(this._matEditorPath);
2490
- if (entry) {
2491
- // Restore original material and dispose the clone
2492
- result.object.front.material = entry.original;
2493
- entry.clone.dispose();
2494
- this._matEditorClones.delete(this._matEditorPath);
2495
- }
2496
-
2497
- // Reopen to rebuild UI with fresh clone from original
2498
- this.closeMatEditor();
2499
- this.openMatEditor(result.object, result.path);
2500
- this.viewer.update(true, false);
2501
- };
2502
-
2503
- /** Make the material editor dialog draggable by its titlebar */
2504
- private _initMatEditorDrag(): void {
2505
- const dialog = this.container.querySelector(".tcv_mat_editor") as HTMLElement;
2506
- const titlebar = this.container.querySelector(".tcv_mat_editor_titlebar") as HTMLElement;
2507
- if (!dialog || !titlebar) return;
2508
-
2509
- this._matEditorDragAbort = new AbortController();
2510
- const { signal } = this._matEditorDragAbort;
2511
-
2512
- let dragging = false;
2513
- let startX = 0;
2514
- let startY = 0;
2515
- let origLeft = 0;
2516
- let origTop = 0;
2517
-
2518
- titlebar.addEventListener("mousedown", (e: MouseEvent) => {
2519
- // Don't drag if clicking buttons
2520
- if ((e.target as HTMLElement).tagName === "INPUT") return;
2521
- dragging = true;
2522
- startX = e.clientX;
2523
- startY = e.clientY;
2524
- const rect = dialog.getBoundingClientRect();
2525
- const parentRect = dialog.offsetParent?.getBoundingClientRect() ?? { left: 0, top: 0 };
2526
- origLeft = rect.left - parentRect.left;
2527
- origTop = rect.top - parentRect.top;
2528
- // Switch from right-positioning to left-positioning for drag
2529
- dialog.style.right = "auto";
2530
- dialog.style.left = origLeft + "px";
2531
- dialog.style.top = origTop + "px";
2532
- e.preventDefault();
2533
- }, { signal });
2534
-
2535
- document.addEventListener("mousemove", (e: MouseEvent) => {
2536
- if (!dragging) return;
2537
- const dx = e.clientX - startX;
2538
- const dy = e.clientY - startY;
2539
- dialog.style.left = (origLeft + dx) + "px";
2540
- dialog.style.top = (origTop + dy) + "px";
2541
- }, { signal });
2542
-
2543
- document.addEventListener("mouseup", () => {
2544
- dragging = false;
2545
- }, { signal });
2546
- }
2547
-
2548
- private _buildMatEditorContent(
2549
- content: HTMLElement,
2550
- material: THREE.MeshPhysicalMaterial,
2551
- originalMat: THREE.MeshPhysicalMaterial,
2552
- ): void {
2553
- let currentGroup = "";
2554
- for (const param of MAT_EDITOR_PARAMS) {
2555
- if (param.group !== currentGroup) {
2556
- currentGroup = param.group;
2557
- const section = document.createElement("div");
2558
- section.className = "tcv_mat_editor_section";
2559
- section.textContent = currentGroup;
2560
- content.appendChild(section);
2561
- }
2562
-
2563
- let currentValue = // eslint-disable-next-line @typescript-eslint/no-explicit-any
2564
- (material as any)[param.key] as number;
2565
- const isInfinity = param.infinity === true && (currentValue === Infinity || currentValue == null);
2566
- if (isInfinity) currentValue = param.max;
2567
-
2568
- this._buildMatEditorRow(content, param, currentValue ?? 0, isInfinity, originalMat);
2569
- }
2570
- }
2571
-
2572
- private _buildMatEditorRow(
2573
- container: HTMLElement,
2574
- param: MatEditorParam,
2575
- value: number,
2576
- isInfinity: boolean,
2577
- originalMat: THREE.MeshPhysicalMaterial,
2578
- ): void {
2579
- const row = document.createElement("div");
2580
- row.className = "tcv_mat_editor_row";
2581
-
2582
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2583
- const origValue = (originalMat as any)[param.key] as number;
2584
-
2585
- const isChanged = (v: number) => param.infinity
2586
- ? (v >= param.max) !== (origValue === Infinity || origValue == null)
2587
- || (v < param.max && Math.abs(v - origValue) > param.step * 0.5)
2588
- : Math.abs(v - origValue) > param.step * 0.5;
2589
-
2590
- const label = document.createElement("label");
2591
- label.className = "tcv_mat_editor_label";
2592
- if (isChanged(value)) label.classList.add("tcv_mat_editor_changed");
2593
- label.textContent = param.label;
2594
- label.title = `${param.key} (${param.min}–${param.max})`;
2595
-
2596
- const sliderGroup = document.createElement("div");
2597
- sliderGroup.className = "tcv_mat_editor_slider_group";
2598
-
2599
- const slider = document.createElement("input");
2600
- slider.type = "range";
2601
- slider.className = "tcv_clip_slider";
2602
- slider.min = String(param.min);
2603
- slider.max = String(param.max);
2604
- slider.step = String(param.step);
2605
- slider.value = String(value);
2606
-
2607
- const valueDisplay = document.createElement("input");
2608
- valueDisplay.className = "tcv_clip_input";
2609
- valueDisplay.readOnly = true;
2610
- valueDisplay.value = isInfinity ? "\u221E" : _formatMatValue(value, param.step);
2611
-
2612
- slider.addEventListener("input", () => {
2613
- const newValue = parseFloat(slider.value);
2614
- const result = this.viewer.getSelectedObjectGroup();
2615
- if (!result?.object.front) return;
2616
- const mat = result.object.front.material;
2617
-
2618
- if (param.infinity && newValue >= param.max) {
2619
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2620
- (mat as any)[param.key] = Infinity;
2621
- valueDisplay.value = "\u221E";
2622
- } else {
2623
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2624
- (mat as any)[param.key] = newValue;
2625
- valueDisplay.value = _formatMatValue(newValue, param.step);
2626
- }
2627
-
2628
- label.classList.toggle("tcv_mat_editor_changed", isChanged(newValue));
2629
-
2630
- this.viewer.update(true, false);
2631
- }, { signal: this._matEditorInputAbort!.signal });
2632
-
2633
- sliderGroup.appendChild(slider);
2634
- sliderGroup.appendChild(valueDisplay);
2635
- row.appendChild(label);
2636
- row.appendChild(sliderGroup);
2637
- container.appendChild(row);
2638
- }
2639
-
2640
- // ---------------------------------------------------------------------------
2641
- // Zebra Tool Handlers
2642
- // ---------------------------------------------------------------------------
2643
-
2644
- /**
2645
- * Set zebra stripe count in the UI
2646
- */
2647
- setZebraCount = (val: number): void => {
2648
- this.zebraCountSlider!.setValue(val);
2649
- };
2650
-
2651
- /**
2652
- * Set zebra stripe opacity in the UI
2653
- */
2654
- setZebraOpacity = (val: number): void => {
2655
- this.zebraOpacitySlider!.setValue(val);
2656
- };
2657
-
2658
- /**
2659
- * Set zebra stripe direction in the UI
2660
- */
2661
- setZebraDirection = (val: number): void => {
2662
- this.zebraDirectionSlider!.setValue(val);
2663
- };
2664
-
2665
- /**
2666
- * Handler for setting the zebra color scheme
2667
- */
2668
- setZebraColorScheme = (e: Event): void => {
2669
- if (!(e.target instanceof HTMLInputElement)) return;
2670
- const value = e.target.value;
2671
- if (
2672
- value === "blackwhite" ||
2673
- value === "colorful" ||
2674
- value === "grayscale"
2675
- ) {
2676
- this.viewer.setZebraColorScheme(value);
2677
- this.setZebraColorSchemeSelect(value);
2678
- }
2679
- };
2680
-
2681
- /**
2682
- * Set zebra color scheme radio button in the UI
2683
- */
2684
- setZebraColorSchemeSelect = (value: string): void => {
2685
- const el = this.container.querySelector(
2686
- `input[name="zebra_color_group"][value="${value}"]`,
2687
- );
2688
- if (el instanceof HTMLInputElement) el.checked = true;
2689
- };
2690
-
2691
- /**
2692
- * Handler for setting the zebra mapping mode
2693
- */
2694
- setZebraMappingMode = (e: Event): void => {
2695
- if (!(e.target instanceof HTMLInputElement)) return;
2696
- const value = e.target.value;
2697
- if (value === "reflection" || value === "normal") {
2698
- this.viewer.setZebraMappingMode(value);
2699
- this.setZebraMappingModeSelect(value);
2700
- }
2701
- };
2702
-
2703
- /**
2704
- * Set zebra mapping mode radio button in the UI
2705
- */
2706
- setZebraMappingModeSelect = (value: string): void => {
2707
- const el = this.container.querySelector(
2708
- `input[name="zebra_mapping_group"][value="${value}"]`,
2709
- );
2710
- if (el instanceof HTMLInputElement) el.checked = true;
2711
- };
2712
-
2713
- // ---------------------------------------------------------------------------
2714
- // Slider & Animation Control
2715
- // ---------------------------------------------------------------------------
2716
-
2717
- /**
2718
- * Set minimum and maximum of the clipping sliders
2719
- */
2720
- setSliderLimits(limit: number): void {
2721
- for (let i = 0; i < 3; i++) {
2722
- this.clipSliders![i].setLimits(limit);
2723
- }
2724
- }
2725
-
2726
- /**
2727
- * Sync clip slider UI from current state values.
2728
- * Called after setSliderLimits to apply initial values with correct limits.
2729
- */
2730
- syncClipSlidersFromState(): void {
2731
- const state = this.viewer.state;
2732
- const values = [
2733
- state.get("clipSlider0"),
2734
- state.get("clipSlider1"),
2735
- state.get("clipSlider2"),
2736
- ];
2737
- for (let i = 0; i < 3; i++) {
2738
- if (values[i] !== -1) {
2739
- this.clipSliders![i].setValueFromState(values[i]);
2740
- }
2741
- }
2742
- }
2743
-
2744
- /**
2745
- * Sync material slider UI from current state values.
2746
- * Called from updateUI after render options are applied to state.
2747
- * State stores values in 0-1 range, sliders display 0-100 (or 0-400 for lights).
2748
- */
2749
- syncMaterialSlidersFromState(): void {
2750
- const state = this.viewer.state;
2751
- this.ambientlightSlider?.setValueFromState(state.get("ambientIntensity") * 100);
2752
- this.directionallightSlider?.setValueFromState(state.get("directIntensity") * 100);
2753
- this.metalnessSlider?.setValueFromState(state.get("metalness") * 100);
2754
- this.roughnessSlider?.setValueFromState(state.get("roughness") * 100);
2755
- }
2756
-
2757
- /**
2758
- * Sync zebra slider UI from current state values.
2759
- * Called from updateUI after viewer options are applied to state.
2760
- */
2761
- syncZebraSlidersFromState(): void {
2762
- const state = this.viewer.state;
2763
- this.zebraCountSlider?.setValueFromState(state.get("zebraCount"));
2764
- this.zebraOpacitySlider?.setValueFromState(state.get("zebraOpacity"));
2765
- this.zebraDirectionSlider?.setValueFromState(state.get("zebraDirection"));
2766
- this.setZebraColorSchemeSelect(state.get("zebraColorScheme"));
2767
- this.setZebraMappingModeSelect(state.get("zebraMappingMode"));
2768
- }
2769
-
2770
- /**
2771
- * Sync Studio slider/control UI from current state values.
2772
- */
2773
- syncStudioSlidersFromState(): void {
2774
- const state = this.viewer.state;
2775
- this.studioEnvIntensitySlider?.setValueFromState(state.get("studioEnvIntensity") * 100);
2776
- this.studioExposureSlider?.setValueFromState(state.get("studioExposure") * 100);
2777
- this.studioEnvRotationSlider?.setValueFromState(state.get("studioEnvRotation"));
2778
- this.studioShadowIntensitySlider?.setValueFromState(state.get("studioShadowIntensity") * 100);
2779
- this.studioShadowSoftnessSlider?.setValueFromState(state.get("studioShadowSoftness") * 100);
2780
- this.studioAOIntensitySlider?.setValueFromState(state.get("studioAOIntensity") * 10);
2781
- this.getInputElement("tcv_studio_4k_env_maps").checked = state.get("studio4kEnvMaps");
2782
- this._syncEnvDropdown(state.get("studioEnvironment"));
2783
- const bgEl = this.container.querySelector(".tcv_studio_background");
2784
- if (bgEl instanceof HTMLSelectElement) bgEl.value = state.get("studioBackground");
2785
- const tmEl = this.container.querySelector(".tcv_studio_tone_mapping");
2786
- if (tmEl instanceof HTMLSelectElement) tmEl.value = state.get("studioToneMapping");
2787
- const txmEl = this.container.querySelector(".tcv_studio_texture_mapping");
2788
- if (txmEl instanceof HTMLSelectElement) txmEl.value = state.get("studioTextureMapping");
2789
- this._update4kCheckboxEnabled(state.get("studioEnvironment"));
2790
- }
2791
-
2792
- /**
2793
- * Ensure the environment dropdown can display a custom HDR URL.
2794
- * If envName isn't already an option, adds a "Custom HDR" entry.
2795
- * Removes stale custom entries when switching back to a built-in preset.
2796
- */
2797
- private _syncEnvDropdown(envName: string): void {
2798
- const el = this.container.querySelector(".tcv_studio_environment");
2799
- if (!(el instanceof HTMLSelectElement)) return;
2800
-
2801
- // Check if the value matches a built-in option
2802
- const isBuiltin = Array.from(el.options).some(
2803
- (opt) => !opt.hasAttribute("data-custom") && opt.value === envName,
2804
- );
2805
-
2806
- if (isBuiltin) {
2807
- el.value = envName;
2808
- } else {
2809
- // Add or update a "Custom" optgroup with the custom HDR entry
2810
- const label = envName.split("/").pop()?.replace(/\.hdr$/i, "") || "Custom HDR";
2811
- let customGroup = el.querySelector("optgroup[data-custom]") as HTMLOptGroupElement | null;
2812
- if (customGroup) {
2813
- const opt = customGroup.querySelector("option")!;
2814
- opt.value = envName;
2815
- opt.textContent = label;
2816
- } else {
2817
- customGroup = document.createElement("optgroup");
2818
- customGroup.label = "Custom";
2819
- customGroup.setAttribute("data-custom", "true");
2820
- const opt = document.createElement("option");
2821
- opt.value = envName;
2822
- opt.textContent = label;
2823
- customGroup.appendChild(opt);
2824
- el.appendChild(customGroup);
2825
- }
2826
- el.value = envName;
2827
- }
2828
- }
2829
-
2830
- /**
2831
- * Enable/disable the 4K checkbox based on whether the current environment
2832
- * is a Poly Haven preset (resolution-switchable) or a custom URL / "studio".
2833
- */
2834
- private _update4kCheckboxEnabled(envName: string): void {
2835
- const cb = this.container.querySelector(".tcv_studio_4k_env_maps") as HTMLInputElement | null;
2836
- if (!cb) return;
2837
- const isPreset = this.viewer.envManager.isPreset(envName);
2838
- cb.disabled = !isPreset;
2839
- if (!isPreset) {
2840
- cb.title = "4K switching is only available for built-in Poly Haven presets";
2841
- } else {
2842
- cb.title = "";
2843
- }
2844
- }
2845
-
2846
- /**
2847
- * Refresh clipping plane position
2848
- */
2849
- refreshPlane = (uiIndex: number, value: string): void => {
2850
- const index = uiIndex - 1;
2851
- if (!isClipIndex(index)) return;
2852
- this.viewer.refreshPlane(index, parseFloat(value));
2853
- };
2854
-
2855
- /**
2856
- * Handle animation control by button name
2857
- */
2858
- controlAnimationByName(btn: string): void {
2859
- this.viewer.controlAnimation(btn);
2860
-
2861
- const currentTime = this.viewer.animation.getRelativeTime();
2862
- this.viewer.state.set("animationSliderValue", 1000 * currentTime);
2863
- if (btn == "play") {
2864
- this.viewer.bboxNeedsUpdate = true;
2865
- } else if (btn == "stop") {
2866
- this.viewer.bboxNeedsUpdate = false;
2867
- if (this.viewer.lastBbox != null) {
2868
- this.viewer.lastBbox.needsUpdate = true;
2869
- }
2870
- } else {
2871
- this.viewer.bboxNeedsUpdate = !this.viewer.bboxNeedsUpdate;
2872
- }
2873
- }
2874
-
2875
- /**
2876
- * Handler for the animation control buttons
2877
- */
2878
- controlAnimation = (e: Event): void => {
2879
- if (!(e.target instanceof HTMLElement)) return;
2880
- const btn = e.target.className.split(" ")[0].slice(4);
2881
- this.controlAnimationByName(btn);
2882
- };
2883
-
2884
- /**
2885
- * Handler for the animation slider
2886
- */
2887
- animationChange = (e: Event): void => {
2888
- if (!(e.target instanceof HTMLInputElement)) return;
2889
- this.viewer.setRelativeTime(e.target.valueAsNumber / 1000);
2890
- if (this.viewer.lastBbox != null) {
2891
- this.viewer.lastBbox.needsUpdate = true;
2892
- }
2893
- };
2894
-
2895
- // ---------------------------------------------------------------------------
2896
- // Keyboard Shortcuts
2897
- // ---------------------------------------------------------------------------
2898
-
2899
- /**
2900
- * Handle keyboard shortcut events on the container.
2901
- */
2902
- private _handleKeyboardShortcut = (e: Event): void => {
2903
- if (!(e instanceof KeyboardEvent)) return;
2904
-
2905
- // ESC closes the material editor (works regardless of focus target)
2906
- if (e.key === "Escape") {
2907
- const matDialog = this.container.querySelector(".tcv_mat_editor") as HTMLElement;
2908
- if (matDialog && matDialog.style.display !== "none") {
2909
- e.preventDefault();
2910
- this.closeMatEditor();
2911
- return;
2912
- }
2913
- }
2914
-
2915
- // Skip if modifier keys are held (avoid conflicts with modifier-based mouse actions)
2916
- if (e.ctrlKey || e.altKey || e.metaKey) return;
2917
-
2918
- // Skip if target is a text-entry input element (but allow buttons/checkboxes)
2919
- const target = e.target;
2920
- if (
2921
- (target instanceof HTMLInputElement &&
2922
- target.type !== "button" && target.type !== "checkbox") ||
2923
- target instanceof HTMLTextAreaElement ||
2924
- target instanceof HTMLSelectElement
2925
- ) {
2926
- return;
2927
- }
2928
-
2929
- const action = KeyMapper.getActionForKey(e.key);
2930
- if (action) {
2931
- const result = this._dispatchAction(action);
2932
- if (result !== "propagate") {
2933
- e.preventDefault();
2934
- e.stopPropagation();
2935
- }
2936
- }
2937
- };
2938
-
2939
- /**
2940
- * Dispatch a keyboard shortcut action.
2941
- * Returns "propagate" if the event should not be suppressed.
2942
- */
2943
- private _dispatchAction(action: string): string | void {
2944
- // Toggle buttons
2945
- const toggleActions = [
2946
- "axes", "axes0", "grid", "perspective", "transparent", "blackedges",
2947
- "explode", "zscale", "distance", "properties", "select",
2948
- ];
2949
- if (toggleActions.includes(action)) {
2950
- this._toggleClickButton(action);
2951
- return;
2952
- }
2953
-
2954
- // Grid XY only
2955
- if (action === "gridxy") {
2956
- const grid = this.state.get("grid");
2957
- const xyOnly = grid[0] && !grid[1] && !grid[2];
2958
- // Hide all grids first
2959
- this.viewer.setGrid("grid", false);
2960
- // If not already in XY-only state, turn XY on
2961
- if (!xyOnly) {
2962
- this.viewer.setGrid("grid-xy", true);
2963
- }
2964
- return;
2965
- }
2966
-
2967
- // Execute buttons
2968
- switch (action) {
2969
- case "reset":
2970
- this.reset();
2971
- return;
2972
- case "resize":
2973
- this.resize();
2974
- return;
2975
- case "iso":
2976
- case "front":
2977
- case "rear":
2978
- case "top":
2979
- case "bottom":
2980
- case "left":
2981
- case "right":
2982
- this.setView(action);
2983
- return;
2984
- case "help":
2985
- this.toggleHelp();
2986
- return;
2987
- case "play": {
2988
- const mode = this.state.get("animationMode");
2989
- if (mode === "animation" || mode === "explode") {
2990
- const clipAction = this.viewer.clipAction;
2991
- if (clipAction && clipAction.isRunning()) {
2992
- this.controlAnimationByName("pause");
2993
- } else {
2994
- this.controlAnimationByName("play");
2995
- }
2996
- }
2997
- return;
2998
- }
2999
- case "stop": {
3000
- if (this.help_shown) {
3001
- this.showHelp(false);
3002
- this.container.focus();
3003
- return;
3004
- }
3005
- // When a tool is active, let ESC propagate to the raycaster
3006
- // for shape deselection
3007
- if (this.state.get("activeTool")) return "propagate";
3008
- const stopMode = this.state.get("animationMode");
3009
- if (stopMode === "explode" || stopMode === "animation") {
3010
- this.controlAnimationByName("stop");
3011
- }
3012
- return;
3013
- }
3014
- case "tree":
3015
- case "clip":
3016
- case "zebra":
3017
- case "material":
3018
- case "studio":
3019
- this.viewer.setActiveTab(action);
3020
- return;
3021
- }
3022
- }
3023
-
3024
- /**
3025
- * Programmatically toggle a ClickButton by name.
3026
- */
3027
- private _toggleClickButton(name: string): void {
3028
- const button = this.clickButtons[name];
3029
- if (!button) return;
3030
-
3031
- // Skip if button is hidden
3032
- if (button.html.style.display === "none") return;
3033
-
3034
- if (!button.state) {
3035
- button.clearGroup();
3036
- }
3037
- button.set(!button.state);
3038
- button.action(button.name, button.state);
3039
- }
3040
-
3041
- /**
3042
- * Update tooltips with keyboard shortcut suffixes.
3043
- */
3044
- updateTooltips(): void {
3045
- const shortcuts = KeyMapper.getActionShortcuts();
3046
-
3047
- for (const [action, key] of Object.entries(shortcuts)) {
3048
- // Check clickButtons and buttons
3049
- const button = this.clickButtons[action] || this.buttons[action];
3050
- if (button) {
3051
- const baseTooltip = button.html.getAttribute("data-base-tooltip");
3052
- if (baseTooltip) {
3053
- button.html.setAttribute("data-tooltip", `${baseTooltip} › ${key}`);
3054
- }
3055
- }
3056
- }
3057
-
3058
- // Update tab titles
3059
- const tabMap: Record<string, { el: HTMLElement | undefined; label: string }> = {
3060
- tree: { el: this.tabTree, label: "Navigation Tree" },
3061
- clip: { el: this.tabClip, label: "Clipping Tool" },
3062
- zebra: { el: this.tabZebra, label: "Zebra Tool" },
3063
- material: { el: this.tabMaterial, label: "Material Selection" },
3064
- studio: { el: this.tabStudio, label: "Studio Mode" },
3065
- };
3066
- for (const [action, key] of Object.entries(shortcuts)) {
3067
- const entry = tabMap[action];
3068
- if (entry?.el?.parentElement) {
3069
- entry.el.parentElement.setAttribute("data-tooltip", `${entry.label} › ${key}`);
3070
- }
3071
- }
3072
- }
3073
-
3074
- // ---------------------------------------------------------------------------
3075
- // Help & Info Panels
3076
- // ---------------------------------------------------------------------------
3077
-
3078
- /**
3079
- * Show or hide help dialog
3080
- */
3081
- showHelp = (flag: boolean): void => {
3082
- this.cadHelp.style.display = flag ? "block" : "none";
3083
- this.help_shown = flag;
3084
- };
3085
-
3086
- /**
3087
- * Toggle help dialog visibility
3088
- */
3089
- toggleHelp = (): void => {
3090
- this.showHelp(!this.help_shown);
3091
- };
3092
-
3093
- /**
3094
- * Replace container content with a static image
3095
- */
3096
- replaceWithImage(image: HTMLImageElement): void {
3097
- while (this.container.firstChild) {
3098
- this.container.removeChild(this.container.firstChild);
3099
- }
3100
- this.container.appendChild(image);
3101
- }
3102
-
3103
- /**
3104
- * Show or hide the distance measurement panel
3105
- */
3106
- showDistancePanel = (flag: boolean): void => {
3107
- this.distanceMeasurementPanel.style.display = flag ? "block" : "none";
3108
- };
3109
-
3110
- /**
3111
- * Show or hide the properties measurement panel
3112
- */
3113
- showPropertiesPanel = (flag: boolean): void => {
3114
- this.propertiesMeasurementPanel.style.display = flag ? "block" : "none";
3115
- };
3116
-
3117
- /**
3118
- * Show or hide info dialog
3119
- */
3120
- showInfo = (flag: boolean): void => {
3121
- const infoContainer = this.cadInfo.parentNode?.parentNode;
3122
- if (infoContainer instanceof HTMLElement) {
3123
- infoContainer.style.display = flag ? "block" : "none";
3124
- }
3125
- this.getElement("tcv_toggle_info").innerHTML = flag ? "\u25BE" : "\u25B8";
3126
- this.info_shown = flag;
3127
- };
3128
-
3129
- /**
3130
- * Toggle info dialog visibility
3131
- */
3132
- toggleInfo = (): void => {
3133
- this.showInfo(!this.info_shown);
3134
- };
3135
-
3136
- /**
3137
- * Show or hide tools panel (tabs + content) in glass mode.
3138
- * Also toggles the orientation marker and animation/explode slider.
3139
- */
3140
- showToolsPanel = (flag: boolean): void => {
3141
- const cadTree = this.getElement("tcv_cad_tree");
3142
- cadTree.style.display = flag ? "" : "none";
3143
- this.getElement("tcv_toggle_tools").innerHTML = flag ? "\u25BE" : "\u25B8";
3144
- this.tools_shown = flag;
3145
-
3146
- // Toggle orientation marker
3147
- if (this.viewer.ready) {
3148
- this.viewer.rendered.orientationMarker.setVisible(flag);
3149
- this.viewer.update(true, false);
3150
- }
3151
-
3152
- // Toggle animation/explode slider (only if it was visible before hiding)
3153
- if (!flag) {
3154
- this._animWasVisible = this.cadAnim.style.display !== "none";
3155
- this.cadAnim.style.display = "none";
3156
- } else if (this._animWasVisible) {
3157
- this.cadAnim.style.display = "block";
3158
- }
3159
- };
3160
-
3161
- /**
3162
- * Toggle tools panel visibility
3163
- */
3164
- toggleToolsPanel = (): void => {
3165
- this.showToolsPanel(!this.tools_shown);
3166
- };
3167
-
3168
- // ---------------------------------------------------------------------------
3169
- // Theme & Glass Mode
3170
- // ---------------------------------------------------------------------------
3171
-
3172
- /**
3173
- * Auto collapse tree nodes when cad width < 600
3174
- */
3175
- autoCollapse(): void {
3176
- if (this.cadWidth < 600 && this.glass) {
3177
- console.info("Small view, collapsing tree");
3178
- this.viewer.collapseNodes(CollapseState.COLLAPSED);
3179
- }
3180
- }
3181
-
3182
- /**
3183
- * Enable/disable glass mode (UI update only).
3184
- */
3185
- glassMode(flag: boolean): void {
3186
- const stateTreeHeight = this.state?.get("treeHeight");
3187
- const treeHeight =
3188
- typeof stateTreeHeight === "number"
3189
- ? stateTreeHeight
3190
- : Math.round((this.height * 2) / 3);
3191
- const cadTree = this.getElement("tcv_cad_tree");
3192
- if (flag) {
3193
- cadTree.classList.add("tcv_cad_tree_glass");
3194
- cadTree.style.height = "";
3195
- cadTree.style.maxHeight = px(treeHeight - 18);
3196
-
3197
- this.getElement("tcv_cad_info").classList.add("tcv_cad_info_glass");
3198
- this.getElement("tcv_cad_view").classList.add("tcv_cad_view_glass");
3199
-
3200
- this.getElement("tcv_toggle_info_wrapper").style.display = "block";
3201
- this.getElement("tcv_toggle_tools_wrapper").style.display = "block";
3202
-
3203
- this.showInfo(false);
3204
- this.showToolsPanel(true);
3205
- this.glass = true;
3206
- this.autoCollapse();
3207
- } else {
3208
- cadTree.classList.remove("tcv_cad_tree_glass");
3209
- cadTree.style.maxHeight = "";
3210
- cadTree.style.height = px(treeHeight);
3211
- this.getElement("tcv_cad_info").classList.remove("tcv_cad_info_glass");
3212
- this.getElement("tcv_cad_view").classList.remove("tcv_cad_view_glass");
3213
-
3214
- this.getElement("tcv_toggle_info_wrapper").style.display = "none";
3215
- this.getElement("tcv_toggle_tools_wrapper").style.display = "none";
3216
-
3217
- this.showInfo(true);
3218
- this.showToolsPanel(true);
3219
- this.glass = false;
3220
- }
3221
- const options: SizeOptions = {
3222
- cadWidth: this.cadWidth,
3223
- glass: this.glass,
3224
- height: this.height,
3225
- treeHeight: treeHeight,
3226
- tools: this.tools,
3227
- treeWidth: flag ? 0 : this.treeWidth,
3228
- };
3229
- this.setSizes(options);
3230
- }
3231
-
3232
- /**
3233
- * Update help dialog with new key mappings
3234
- */
3235
- updateHelp(before: KeyMappingConfig, after: Partial<KeyMappingConfig>): void {
3236
- const help = this.getElement("tcv_cad_help_layout");
3237
- const keys = Object.keys(before) as (keyof KeyMappingConfig)[];
3238
- for (const k of keys) {
3239
- if (before[k] && after[k]) {
3240
- help.innerHTML = help.innerHTML.replaceAll(
3241
- "&lt;" + before[k].slice(0, -3) + "&gt;",
3242
- "&lt;_" + after[k]!.slice(0, -3) + "&gt;",
3243
- );
3244
- }
3245
- }
3246
- help.innerHTML = help.innerHTML.replaceAll("_shift", "shift");
3247
- help.innerHTML = help.innerHTML.replaceAll("_ctrl", "ctrl");
3248
- help.innerHTML = help.innerHTML.replaceAll("_alt", "alt");
3249
- help.innerHTML = help.innerHTML.replaceAll("_meta", "meta");
3250
- }
3251
-
3252
- /**
3253
- * Set the UI theme.
3254
- * @param theme - "light", "dark", or "browser" for auto-detection
3255
- * @returns The resolved theme ("light" or "dark")
3256
- * @public
3257
- */
3258
- setTheme(theme: ThemeInput): string {
3259
- if (
3260
- theme === "dark" ||
3261
- (theme === "browser" &&
3262
- window.matchMedia("(prefers-color-scheme: dark)").matches)
3263
- ) {
3264
- this.container.setAttribute("data-theme", "dark");
3265
- document.body.setAttribute("data-theme", "dark");
3266
- if (this.viewer.ready) {
3267
- this.viewer.orientationMarker.changeTheme("dark");
3268
- this.viewer.gridHelper.clearCache();
3269
- this.viewer.gridHelper.update(
3270
- this.viewer.getCameraZoom(),
3271
- true,
3272
- "dark",
3273
- );
3274
- }
3275
- this.viewer.update(true);
3276
- return "dark";
3277
- } else {
3278
- this.container.setAttribute("data-theme", "light");
3279
- document.body.setAttribute("data-theme", "light");
3280
- if (this.viewer.ready) {
3281
- this.viewer.orientationMarker.changeTheme("light");
3282
- this.viewer.gridHelper.clearCache();
3283
- this.viewer.gridHelper.update(
3284
- this.viewer.getCameraZoom(),
3285
- true,
3286
- "light",
3287
- );
3288
- }
3289
- this.viewer.update(true);
3290
- return "light";
3291
- }
3292
- }
3293
- }
3294
-
3295
- export { Display };