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.
- package/dist/scene/clipping.d.ts +6 -0
- package/dist/three-cad-viewer.esm.js +20 -5
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +1 -1
- package/dist/three-cad-viewer.js +20 -5
- package/dist/three-cad-viewer.min.js +1 -1
- package/package.json +2 -3
- package/src/_version.ts +0 -1
- package/src/camera/camera.ts +0 -445
- package/src/camera/controls/CADOrbitControls.ts +0 -241
- package/src/camera/controls/CADTrackballControls.ts +0 -598
- package/src/camera/controls.ts +0 -380
- package/src/core/patches.ts +0 -16
- package/src/core/studio-manager.ts +0 -652
- package/src/core/types.ts +0 -892
- package/src/core/viewer-state.ts +0 -784
- package/src/core/viewer.ts +0 -4821
- package/src/index.ts +0 -151
- package/src/rendering/environment.ts +0 -840
- package/src/rendering/light-detection.ts +0 -327
- package/src/rendering/material-factory.ts +0 -735
- package/src/rendering/material-presets.ts +0 -289
- package/src/rendering/raycast.ts +0 -291
- package/src/rendering/room-environment.ts +0 -192
- package/src/rendering/studio-composer.ts +0 -577
- package/src/rendering/studio-floor.ts +0 -108
- package/src/rendering/texture-cache.ts +0 -324
- package/src/rendering/tree-model.ts +0 -542
- package/src/rendering/triplanar.ts +0 -329
- package/src/scene/animation.ts +0 -343
- package/src/scene/axes.ts +0 -108
- package/src/scene/bbox.ts +0 -223
- package/src/scene/clipping.ts +0 -640
- package/src/scene/grid.ts +0 -864
- package/src/scene/nestedgroup.ts +0 -1444
- package/src/scene/objectgroup.ts +0 -866
- package/src/scene/orientation.ts +0 -259
- package/src/scene/render-shape.ts +0 -634
- package/src/tools/cad_tools/measure.ts +0 -811
- package/src/tools/cad_tools/select.ts +0 -100
- package/src/tools/cad_tools/tools.ts +0 -231
- package/src/tools/cad_tools/ui.ts +0 -454
- package/src/tools/cad_tools/zebra.ts +0 -369
- package/src/types/html.d.ts +0 -5
- package/src/types/n8ao.d.ts +0 -28
- package/src/types/three-augmentation.d.ts +0 -60
- package/src/ui/display.ts +0 -3295
- package/src/ui/index.html +0 -505
- package/src/ui/info.ts +0 -177
- package/src/ui/slider.ts +0 -206
- package/src/ui/toolbar.ts +0 -347
- package/src/ui/treeview.ts +0 -945
- package/src/utils/decode-instances.ts +0 -233
- package/src/utils/font.ts +0 -60
- package/src/utils/gpu-tracker.ts +0 -265
- package/src/utils/logger.ts +0 -92
- package/src/utils/sizeof.ts +0 -116
- package/src/utils/timer.ts +0 -69
- 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
|
-
"<" + before[k].slice(0, -3) + ">",
|
|
3242
|
-
"<_" + after[k]!.slice(0, -3) + ">",
|
|
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 };
|