three-cad-viewer 4.1.2 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +12 -5
- package/dist/camera/camera.d.ts +14 -2
- package/dist/core/studio-manager.d.ts +91 -0
- package/dist/core/types.d.ts +260 -9
- package/dist/core/viewer-state.d.ts +28 -2
- package/dist/core/viewer.d.ts +200 -6
- package/dist/index.d.ts +7 -2
- package/dist/rendering/environment.d.ts +239 -0
- package/dist/rendering/light-detection.d.ts +44 -0
- package/dist/rendering/material-factory.d.ts +77 -2
- package/dist/rendering/material-presets.d.ts +32 -0
- package/dist/rendering/room-environment.d.ts +13 -0
- package/dist/rendering/studio-composer.d.ts +130 -0
- package/dist/rendering/studio-floor.d.ts +53 -0
- package/dist/rendering/texture-cache.d.ts +142 -0
- package/dist/rendering/triplanar.d.ts +37 -0
- package/dist/scene/animation.d.ts +1 -1
- package/dist/scene/clipping.d.ts +31 -0
- package/dist/scene/nestedgroup.d.ts +64 -27
- package/dist/scene/objectgroup.d.ts +47 -0
- package/dist/three-cad-viewer.css +339 -29
- package/dist/three-cad-viewer.esm.js +27567 -11874
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +10 -4
- package/dist/three-cad-viewer.js +27486 -11787
- package/dist/three-cad-viewer.min.js +10 -4
- package/dist/ui/display.d.ts +147 -0
- package/dist/utils/decode-instances.d.ts +60 -0
- package/dist/utils/utils.d.ts +10 -0
- package/package.json +4 -2
- package/src/_version.ts +1 -1
- package/src/camera/camera.ts +27 -10
- package/src/core/studio-manager.ts +682 -0
- package/src/core/types.ts +328 -9
- package/src/core/viewer-state.ts +84 -4
- package/src/core/viewer.ts +453 -22
- package/src/index.ts +25 -1
- package/src/rendering/environment.ts +840 -0
- package/src/rendering/light-detection.ts +327 -0
- package/src/rendering/material-factory.ts +456 -2
- package/src/rendering/material-presets.ts +303 -0
- package/src/rendering/raycast.ts +2 -2
- package/src/rendering/room-environment.ts +192 -0
- package/src/rendering/studio-composer.ts +577 -0
- package/src/rendering/studio-floor.ts +108 -0
- package/src/rendering/texture-cache.ts +1020 -0
- package/src/rendering/triplanar.ts +329 -0
- package/src/scene/animation.ts +3 -2
- package/src/scene/clipping.ts +59 -0
- package/src/scene/nestedgroup.ts +399 -0
- package/src/scene/objectgroup.ts +186 -11
- package/src/scene/orientation.ts +12 -0
- package/src/scene/render-shape.ts +55 -21
- package/src/types/n8ao.d.ts +28 -0
- package/src/ui/display.ts +1032 -27
- package/src/ui/index.html +181 -44
- package/src/utils/decode-instances.ts +233 -0
- package/src/utils/utils.ts +33 -20
package/src/ui/display.ts
CHANGED
|
@@ -15,11 +15,13 @@ import type {
|
|
|
15
15
|
import { FilterByDropDownMenu } from "../tools/cad_tools/ui.js";
|
|
16
16
|
import { Info } from "./info.js";
|
|
17
17
|
import type { Viewer } from "../core/viewer.js";
|
|
18
|
+
import type { ObjectGroup } from "../scene/objectgroup.js";
|
|
18
19
|
import type { ViewerState } from "../core/viewer-state.js";
|
|
19
20
|
import { isClipIndex, CollapseState } from "../core/types.js";
|
|
20
21
|
import type { Vector3Tuple } from "three";
|
|
21
22
|
import type { ActiveTab, ThemeInput, ClipIndex } from "../core/types.js";
|
|
22
23
|
import type { CameraDirection } from "../camera/camera.js";
|
|
24
|
+
import { applyTriplanarMapping } from "../rendering/triplanar.js";
|
|
23
25
|
|
|
24
26
|
import template from "./index.html";
|
|
25
27
|
|
|
@@ -41,6 +43,42 @@ function px(val: number): string {
|
|
|
41
43
|
return `${val}px`;
|
|
42
44
|
}
|
|
43
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
|
+
|
|
44
82
|
const buttons = ["plane", "play", "pause", "stop"];
|
|
45
83
|
|
|
46
84
|
const listeners = new EventListenerManager();
|
|
@@ -59,6 +97,7 @@ export interface DisplayOptions {
|
|
|
59
97
|
explodeTool: boolean;
|
|
60
98
|
zscaleTool: boolean;
|
|
61
99
|
zebraTool: boolean;
|
|
100
|
+
studioTool: boolean;
|
|
62
101
|
glass: boolean;
|
|
63
102
|
tools: boolean;
|
|
64
103
|
cadWidth: number;
|
|
@@ -67,6 +106,8 @@ export interface DisplayOptions {
|
|
|
67
106
|
treeHeight?: number;
|
|
68
107
|
theme: ThemeInput;
|
|
69
108
|
pinning: boolean;
|
|
109
|
+
canvas?: HTMLCanvasElement;
|
|
110
|
+
gl?: WebGLRenderingContext | WebGL2RenderingContext;
|
|
70
111
|
}
|
|
71
112
|
|
|
72
113
|
/**
|
|
@@ -150,17 +191,34 @@ class Display {
|
|
|
150
191
|
cadTree!: HTMLElement;
|
|
151
192
|
cadTreeScrollContainer!: HTMLElement;
|
|
152
193
|
cadTreeToggles!: HTMLElement;
|
|
194
|
+
cadClipToggles!: HTMLElement;
|
|
195
|
+
cadMaterialToggles!: HTMLElement;
|
|
196
|
+
cadZebraToggles!: HTMLElement;
|
|
197
|
+
cadStudioToggles!: HTMLElement;
|
|
153
198
|
cadClip!: HTMLElement;
|
|
154
199
|
cadMaterial!: HTMLElement;
|
|
155
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;
|
|
156
212
|
cadInfo!: HTMLElement;
|
|
157
213
|
cadAnim!: HTMLElement;
|
|
214
|
+
private _animWasVisible: boolean = false;
|
|
158
215
|
cadTools!: HTMLElement;
|
|
159
216
|
cadHelp!: HTMLElement;
|
|
160
217
|
tabTree!: HTMLElement;
|
|
161
218
|
tabClip!: HTMLElement;
|
|
162
219
|
tabMaterial!: HTMLElement;
|
|
163
220
|
tabZebra!: HTMLElement;
|
|
221
|
+
tabStudio!: HTMLElement;
|
|
164
222
|
tickValueElement!: HTMLElement;
|
|
165
223
|
tickInfoElement!: HTMLElement;
|
|
166
224
|
distanceMeasurementPanel!: HTMLElement;
|
|
@@ -186,6 +244,12 @@ class Display {
|
|
|
186
244
|
zebraCountSlider: Slider | undefined;
|
|
187
245
|
zebraOpacitySlider: Slider | undefined;
|
|
188
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;
|
|
189
253
|
|
|
190
254
|
// State - set in setupUI() which is called at end of Viewer constructor
|
|
191
255
|
viewer!: Viewer;
|
|
@@ -205,6 +269,7 @@ class Display {
|
|
|
205
269
|
lastPlaneState: boolean;
|
|
206
270
|
help_shown: boolean;
|
|
207
271
|
info_shown: boolean;
|
|
272
|
+
tools_shown: boolean;
|
|
208
273
|
|
|
209
274
|
// Info panel
|
|
210
275
|
_info: Info;
|
|
@@ -288,14 +353,27 @@ class Display {
|
|
|
288
353
|
|
|
289
354
|
this.cadTree = this.getElement("tcv_cad_tree_container");
|
|
290
355
|
this.cadTreeScrollContainer = this.getElement("tcv_box_content");
|
|
291
|
-
this.cadTreeToggles = this.getElement("
|
|
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");
|
|
292
361
|
this.cadClip = this.getElement("tcv_cad_clip_container");
|
|
293
362
|
this.cadMaterial = this.getElement("tcv_cad_material_container");
|
|
294
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);
|
|
295
372
|
this.tabTree = this.getElement("tcv_tab_tree");
|
|
296
373
|
this.tabClip = this.getElement("tcv_tab_clip");
|
|
297
|
-
this.tabMaterial = this.getElement("tcv_tab_material");
|
|
298
374
|
this.tabZebra = this.getElement("tcv_tab_zebra");
|
|
375
|
+
this.tabMaterial = this.getElement("tcv_tab_material");
|
|
376
|
+
this.tabStudio = this.getElement("tcv_tab_studio");
|
|
299
377
|
this.cadInfo = this.getElement("tcv_cad_info_container");
|
|
300
378
|
this._info = new Info(this.cadInfo);
|
|
301
379
|
this.tickValueElement = this.getElement("tcv_tick_size_value");
|
|
@@ -305,6 +383,9 @@ class Display {
|
|
|
305
383
|
if (!options.zebraTool) {
|
|
306
384
|
this.tabZebra.style.display = "none";
|
|
307
385
|
}
|
|
386
|
+
if (options.studioTool === false) {
|
|
387
|
+
this.tabStudio.style.display = "none";
|
|
388
|
+
}
|
|
308
389
|
this.cadHelp = this.getElement("tcv_cad_help");
|
|
309
390
|
listeners.add(this.cadHelp, "contextmenu", (e) => {
|
|
310
391
|
e.preventDefault();
|
|
@@ -331,6 +412,7 @@ class Display {
|
|
|
331
412
|
this.cadClip.style.display = "none";
|
|
332
413
|
this.cadMaterial.style.display = "none";
|
|
333
414
|
this.cadZebra.style.display = "none";
|
|
415
|
+
this.cadStudio.style.display = "none";
|
|
334
416
|
this.clipSliders = null;
|
|
335
417
|
|
|
336
418
|
// Note: activeTool is managed by ViewerState, not stored locally
|
|
@@ -338,6 +420,7 @@ class Display {
|
|
|
338
420
|
this.lastPlaneState = false;
|
|
339
421
|
this.help_shown = true;
|
|
340
422
|
this.info_shown = !this.glass;
|
|
423
|
+
this.tools_shown = true;
|
|
341
424
|
|
|
342
425
|
const theme = options.theme;
|
|
343
426
|
this.theme = theme;
|
|
@@ -623,6 +706,15 @@ class Display {
|
|
|
623
706
|
this._events.push(["change", name, fn]);
|
|
624
707
|
}
|
|
625
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
|
+
|
|
626
718
|
/**
|
|
627
719
|
* Get a DOM element by class name (internal use only).
|
|
628
720
|
* @param name - Name of the DOM element class
|
|
@@ -693,10 +785,21 @@ class Display {
|
|
|
693
785
|
this.zebraCountSlider?.dispose();
|
|
694
786
|
this.zebraOpacitySlider?.dispose();
|
|
695
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();
|
|
696
796
|
|
|
697
797
|
// Clear DOM content (elements remain valid until Display is GC'd)
|
|
698
798
|
this.cadTree.innerHTML = "";
|
|
699
|
-
this.cadView.
|
|
799
|
+
const attachedCanvas = this.cadView.querySelector("canvas");
|
|
800
|
+
if (attachedCanvas && attachedCanvas.parentElement === this.cadView) {
|
|
801
|
+
this.cadView.removeChild(attachedCanvas);
|
|
802
|
+
}
|
|
700
803
|
this.container.innerHTML = "";
|
|
701
804
|
}
|
|
702
805
|
|
|
@@ -790,6 +893,7 @@ class Display {
|
|
|
790
893
|
this.height = options.height;
|
|
791
894
|
this.cadView.style.height = px(options.height);
|
|
792
895
|
}
|
|
896
|
+
|
|
793
897
|
if (options.treeWidth) {
|
|
794
898
|
this.treeWidth = options.treeWidth;
|
|
795
899
|
this.cadTree.parentElement!.parentElement!.style.width = px(
|
|
@@ -856,9 +960,12 @@ class Display {
|
|
|
856
960
|
this.setupClickEvent("tcv_collapse_all", this.handleCollapseNodes);
|
|
857
961
|
this.setupClickEvent("tcv_expand", this.handleCollapseNodes);
|
|
858
962
|
|
|
963
|
+
this.setupClickEvent("tcv_clip_reset", this.handleClipReset);
|
|
859
964
|
this.setupClickEvent("tcv_material_reset", this.handleMaterialReset);
|
|
965
|
+
this.setupClickEvent("tcv_zebra_reset", this.handleZebraReset);
|
|
860
966
|
|
|
861
967
|
this.setupClickEvent("tcv_toggle_info", this.toggleInfo);
|
|
968
|
+
this.setupClickEvent("tcv_toggle_tools_wrapper", this.toggleToolsPanel);
|
|
862
969
|
|
|
863
970
|
this.help_shown = true;
|
|
864
971
|
this.info_shown = !this.glass;
|
|
@@ -866,8 +973,9 @@ class Display {
|
|
|
866
973
|
const tabs = [
|
|
867
974
|
"tcv_tab_tree",
|
|
868
975
|
"tcv_tab_clip",
|
|
869
|
-
"tcv_tab_material",
|
|
870
976
|
"tcv_tab_zebra",
|
|
977
|
+
"tcv_tab_material",
|
|
978
|
+
"tcv_tab_studio",
|
|
871
979
|
];
|
|
872
980
|
tabs.forEach((name) => {
|
|
873
981
|
this.setupClickEvent(name, this.selectTab);
|
|
@@ -975,6 +1083,101 @@ class Display {
|
|
|
975
1083
|
this.setupRadioEvent(`tcv_zebra_mapping${id}`, this.setZebraMappingMode);
|
|
976
1084
|
});
|
|
977
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
|
+
|
|
978
1181
|
this.setupClickEvent("tcv_play", this.controlAnimation);
|
|
979
1182
|
this.setupClickEvent("tcv_pause", this.controlAnimation);
|
|
980
1183
|
this.setupClickEvent("tcv_stop", this.controlAnimation);
|
|
@@ -1066,6 +1269,9 @@ class Display {
|
|
|
1066
1269
|
|
|
1067
1270
|
sub("tools", (change) => {
|
|
1068
1271
|
this.showTools(change.new);
|
|
1272
|
+
const animationMode = this.state.get("animationMode");
|
|
1273
|
+
this.cadAnim.style.display =
|
|
1274
|
+
change.new && animationMode !== "none" ? "block" : "none";
|
|
1069
1275
|
});
|
|
1070
1276
|
|
|
1071
1277
|
sub("glass", (change) => {
|
|
@@ -1160,13 +1366,78 @@ class Display {
|
|
|
1160
1366
|
this.setZebraMappingModeSelect(change.new);
|
|
1161
1367
|
});
|
|
1162
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
|
+
|
|
1163
1432
|
// Animation/Explode mode subscription - controls slider visibility, label, and explode button
|
|
1164
1433
|
sub(
|
|
1165
1434
|
"animationMode",
|
|
1166
1435
|
(change) => {
|
|
1167
1436
|
const mode = change.new;
|
|
1168
|
-
|
|
1169
|
-
|
|
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";
|
|
1170
1441
|
// Set label: "A" for animation, "E" for explode
|
|
1171
1442
|
this.getElement("tcv_animation_label").innerHTML =
|
|
1172
1443
|
mode === "explode" ? "E" : "A";
|
|
@@ -1263,9 +1534,10 @@ class Display {
|
|
|
1263
1534
|
this.lastPlaneState =
|
|
1264
1535
|
typeof clipPlaneHelpers === "boolean" ? clipPlaneHelpers : false;
|
|
1265
1536
|
|
|
1266
|
-
// Sync material and
|
|
1537
|
+
// Sync material, zebra, and studio sliders with current state values
|
|
1267
1538
|
this.syncMaterialSlidersFromState();
|
|
1268
1539
|
this.syncZebraSlidersFromState();
|
|
1540
|
+
this.syncStudioSlidersFromState();
|
|
1269
1541
|
}
|
|
1270
1542
|
|
|
1271
1543
|
// ---------------------------------------------------------------------------
|
|
@@ -1286,6 +1558,17 @@ class Display {
|
|
|
1286
1558
|
* @param canvasElement - The canvas to attach.
|
|
1287
1559
|
*/
|
|
1288
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
|
+
|
|
1289
1572
|
const existingCanvas = this.cadView.querySelector("canvas");
|
|
1290
1573
|
if (existingCanvas) {
|
|
1291
1574
|
this.cadView.replaceChild(canvasElement, existingCanvas);
|
|
@@ -1303,7 +1586,9 @@ class Display {
|
|
|
1303
1586
|
* Get the DOM canvas element
|
|
1304
1587
|
*/
|
|
1305
1588
|
getCanvas(): Element {
|
|
1306
|
-
|
|
1589
|
+
const localCanvas = this.cadView.querySelector("canvas");
|
|
1590
|
+
if (localCanvas) return localCanvas;
|
|
1591
|
+
return this.viewer.renderer.domElement;
|
|
1307
1592
|
}
|
|
1308
1593
|
|
|
1309
1594
|
/**
|
|
@@ -1409,6 +1694,10 @@ class Display {
|
|
|
1409
1694
|
* Delegates state mutations to Viewer.activateTool() to maintain unidirectional data flow.
|
|
1410
1695
|
*/
|
|
1411
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
|
+
}
|
|
1412
1701
|
this.viewer.toggleAnimationLoop(flag);
|
|
1413
1702
|
const activeTool = this.state.get("activeTool");
|
|
1414
1703
|
const currentTool = typeof activeTool === "string" ? activeTool : "";
|
|
@@ -1469,16 +1758,32 @@ class Display {
|
|
|
1469
1758
|
this.tools = flag;
|
|
1470
1759
|
const tb = this.getElement("tcv_cad_toolbar");
|
|
1471
1760
|
const cn = this.getElement("tcv_cad_navigation");
|
|
1761
|
+
const tickInfo = this.tickInfoElement;
|
|
1472
1762
|
if (flag) {
|
|
1473
1763
|
tb.style.height = "38px";
|
|
1474
1764
|
tb.style.display = "flex";
|
|
1475
1765
|
cn.style.height = "38px";
|
|
1476
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
|
+
}
|
|
1477
1778
|
} else {
|
|
1478
1779
|
tb.style.height = "0px";
|
|
1479
1780
|
tb.style.display = "none";
|
|
1480
1781
|
cn.style.height = "0px";
|
|
1481
1782
|
cn.style.display = "none";
|
|
1783
|
+
|
|
1784
|
+
if (tickInfo) {
|
|
1785
|
+
tickInfo.style.display = "none";
|
|
1786
|
+
}
|
|
1482
1787
|
}
|
|
1483
1788
|
};
|
|
1484
1789
|
|
|
@@ -1514,6 +1819,38 @@ class Display {
|
|
|
1514
1819
|
}
|
|
1515
1820
|
};
|
|
1516
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
|
+
|
|
1517
1854
|
// ---------------------------------------------------------------------------
|
|
1518
1855
|
// Clipping Handlers
|
|
1519
1856
|
// ---------------------------------------------------------------------------
|
|
@@ -1629,8 +1966,9 @@ class Display {
|
|
|
1629
1966
|
if (
|
|
1630
1967
|
tabName === "clip" ||
|
|
1631
1968
|
tabName === "tree" ||
|
|
1969
|
+
tabName === "zebra" ||
|
|
1632
1970
|
tabName === "material" ||
|
|
1633
|
-
tabName === "
|
|
1971
|
+
tabName === "studio"
|
|
1634
1972
|
) {
|
|
1635
1973
|
this.viewer.setActiveTab(tabName);
|
|
1636
1974
|
}
|
|
@@ -1640,41 +1978,62 @@ class Display {
|
|
|
1640
1978
|
* Switch to a tab (internal, called by activeTab subscription).
|
|
1641
1979
|
*/
|
|
1642
1980
|
private switchToTab(newTab: ActiveTab, oldTab?: ActiveTab): void {
|
|
1643
|
-
if (!["clip", "tree", "material", "
|
|
1981
|
+
if (!["clip", "tree", "zebra", "material", "studio"].includes(newTab)) {
|
|
1644
1982
|
return;
|
|
1645
1983
|
}
|
|
1646
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
|
+
|
|
1647
2001
|
const _updateVisibility = (
|
|
1648
2002
|
showTree: boolean,
|
|
1649
2003
|
showClip: boolean,
|
|
1650
|
-
showMaterial: boolean,
|
|
1651
2004
|
showZebra: boolean,
|
|
2005
|
+
showMaterial: boolean,
|
|
2006
|
+
showStudio: boolean,
|
|
1652
2007
|
) => {
|
|
1653
2008
|
this.cadTree.style.display = showTree ? "block" : "none";
|
|
1654
2009
|
this.cadTreeToggles.style.display = showTree ? "block" : "none";
|
|
2010
|
+
this.cadClipToggles.style.display = showClip ? "block" : "none";
|
|
1655
2011
|
this.cadClip.style.display = showClip ? "block" : "none";
|
|
1656
|
-
this.
|
|
2012
|
+
this.cadZebraToggles.style.display = showZebra ? "block" : "none";
|
|
1657
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";
|
|
1658
2018
|
|
|
1659
2019
|
this.viewer.clipping.setVisible(showClip);
|
|
1660
2020
|
this.viewer.setLocalClipping(showClip);
|
|
1661
2021
|
if (!showClip) {
|
|
1662
2022
|
this.viewer.setClipPlaneHelpers(false);
|
|
1663
2023
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
}
|
|
2024
|
+
// NOTE: zebra and studio leave calls removed from here --
|
|
2025
|
+
// they now run above, before _updateVisibility is called.
|
|
1667
2026
|
};
|
|
1668
2027
|
|
|
1669
2028
|
if (newTab === "tree") {
|
|
1670
|
-
_updateVisibility(true, false, false, false);
|
|
2029
|
+
_updateVisibility(true, false, false, false, false);
|
|
1671
2030
|
this.viewer.nestedGroup.setBackVisible(false);
|
|
1672
2031
|
// Lazy-rendered tree nodes may be stale if the tree was rebuilt
|
|
1673
2032
|
// while this tab was hidden (display:none → getBoundingClientRect
|
|
1674
2033
|
// returns zero, so update() rendered nothing). Kick it now.
|
|
1675
2034
|
this.viewer.treeview?.update();
|
|
1676
2035
|
} else if (newTab === "clip") {
|
|
1677
|
-
_updateVisibility(false, true, false, false);
|
|
2036
|
+
_updateVisibility(false, true, false, false, false);
|
|
1678
2037
|
this.viewer.nestedGroup.setBackVisible(true);
|
|
1679
2038
|
const clipIntersection = this.viewer.state.get("clipIntersection");
|
|
1680
2039
|
if (typeof clipIntersection === "boolean") {
|
|
@@ -1682,16 +2041,30 @@ class Display {
|
|
|
1682
2041
|
}
|
|
1683
2042
|
this.viewer.setClipPlaneHelpers(this.lastPlaneState);
|
|
1684
2043
|
this.viewer.update(true, false);
|
|
1685
|
-
} else if (newTab === "material") {
|
|
1686
|
-
_updateVisibility(false, false, true, false);
|
|
1687
|
-
this.viewer.nestedGroup.setBackVisible(false);
|
|
1688
2044
|
} else if (newTab === "zebra") {
|
|
1689
|
-
_updateVisibility(false, false, false,
|
|
2045
|
+
_updateVisibility(false, false, true, false, false);
|
|
1690
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
|
+
});
|
|
1691
2064
|
}
|
|
1692
2065
|
|
|
1693
2066
|
// Update tab styling
|
|
1694
|
-
[this.tabTree, this.tabClip, this.tabMaterial, this.
|
|
2067
|
+
[this.tabTree, this.tabClip, this.tabZebra, this.tabMaterial, this.tabStudio].forEach(
|
|
1695
2068
|
(tabEl) => {
|
|
1696
2069
|
tabEl.classList.add("tcv_tab-unselected");
|
|
1697
2070
|
tabEl.classList.remove("tcv_tab-selected");
|
|
@@ -1705,12 +2078,47 @@ class Display {
|
|
|
1705
2078
|
} else if (newTab === "clip") {
|
|
1706
2079
|
this.tabClip.classList.add("tcv_tab-selected");
|
|
1707
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");
|
|
1708
2084
|
} else if (newTab === "material") {
|
|
1709
2085
|
this.tabMaterial.classList.add("tcv_tab-selected");
|
|
1710
2086
|
this.tabMaterial.classList.remove("tcv_tab-unselected");
|
|
1711
|
-
} else if (newTab === "
|
|
1712
|
-
this.
|
|
1713
|
-
this.
|
|
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;
|
|
1714
2122
|
}
|
|
1715
2123
|
}
|
|
1716
2124
|
|
|
@@ -1745,6 +2153,17 @@ class Display {
|
|
|
1745
2153
|
}
|
|
1746
2154
|
};
|
|
1747
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
|
+
|
|
1748
2167
|
// ---------------------------------------------------------------------------
|
|
1749
2168
|
// Material Handlers
|
|
1750
2169
|
// ---------------------------------------------------------------------------
|
|
@@ -1756,6 +2175,468 @@ class Display {
|
|
|
1756
2175
|
this.viewer.resetMaterial();
|
|
1757
2176
|
};
|
|
1758
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
|
+
|
|
1759
2640
|
// ---------------------------------------------------------------------------
|
|
1760
2641
|
// Zebra Tool Handlers
|
|
1761
2642
|
// ---------------------------------------------------------------------------
|
|
@@ -1886,6 +2767,82 @@ class Display {
|
|
|
1886
2767
|
this.setZebraMappingModeSelect(state.get("zebraMappingMode"));
|
|
1887
2768
|
}
|
|
1888
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
|
+
|
|
1889
2846
|
/**
|
|
1890
2847
|
* Refresh clipping plane position
|
|
1891
2848
|
*/
|
|
@@ -1945,6 +2902,16 @@ class Display {
|
|
|
1945
2902
|
private _handleKeyboardShortcut = (e: Event): void => {
|
|
1946
2903
|
if (!(e instanceof KeyboardEvent)) return;
|
|
1947
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
|
+
|
|
1948
2915
|
// Skip if modifier keys are held (avoid conflicts with modifier-based mouse actions)
|
|
1949
2916
|
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
|
1950
2917
|
|
|
@@ -2046,8 +3013,9 @@ class Display {
|
|
|
2046
3013
|
}
|
|
2047
3014
|
case "tree":
|
|
2048
3015
|
case "clip":
|
|
2049
|
-
case "material":
|
|
2050
3016
|
case "zebra":
|
|
3017
|
+
case "material":
|
|
3018
|
+
case "studio":
|
|
2051
3019
|
this.viewer.setActiveTab(action);
|
|
2052
3020
|
return;
|
|
2053
3021
|
}
|
|
@@ -2091,8 +3059,9 @@ class Display {
|
|
|
2091
3059
|
const tabMap: Record<string, { el: HTMLElement | undefined; label: string }> = {
|
|
2092
3060
|
tree: { el: this.tabTree, label: "Navigation Tree" },
|
|
2093
3061
|
clip: { el: this.tabClip, label: "Clipping Tool" },
|
|
2094
|
-
material: { el: this.tabMaterial, label: "Material Selection" },
|
|
2095
3062
|
zebra: { el: this.tabZebra, label: "Zebra Tool" },
|
|
3063
|
+
material: { el: this.tabMaterial, label: "Material Selection" },
|
|
3064
|
+
studio: { el: this.tabStudio, label: "Studio Mode" },
|
|
2096
3065
|
};
|
|
2097
3066
|
for (const [action, key] of Object.entries(shortcuts)) {
|
|
2098
3067
|
const entry = tabMap[action];
|
|
@@ -2164,6 +3133,38 @@ class Display {
|
|
|
2164
3133
|
this.showInfo(!this.info_shown);
|
|
2165
3134
|
};
|
|
2166
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
|
+
|
|
2167
3168
|
// ---------------------------------------------------------------------------
|
|
2168
3169
|
// Theme & Glass Mode
|
|
2169
3170
|
// ---------------------------------------------------------------------------
|
|
@@ -2197,8 +3198,10 @@ class Display {
|
|
|
2197
3198
|
this.getElement("tcv_cad_view").classList.add("tcv_cad_view_glass");
|
|
2198
3199
|
|
|
2199
3200
|
this.getElement("tcv_toggle_info_wrapper").style.display = "block";
|
|
3201
|
+
this.getElement("tcv_toggle_tools_wrapper").style.display = "block";
|
|
2200
3202
|
|
|
2201
3203
|
this.showInfo(false);
|
|
3204
|
+
this.showToolsPanel(true);
|
|
2202
3205
|
this.glass = true;
|
|
2203
3206
|
this.autoCollapse();
|
|
2204
3207
|
} else {
|
|
@@ -2209,8 +3212,10 @@ class Display {
|
|
|
2209
3212
|
this.getElement("tcv_cad_view").classList.remove("tcv_cad_view_glass");
|
|
2210
3213
|
|
|
2211
3214
|
this.getElement("tcv_toggle_info_wrapper").style.display = "none";
|
|
3215
|
+
this.getElement("tcv_toggle_tools_wrapper").style.display = "none";
|
|
2212
3216
|
|
|
2213
3217
|
this.showInfo(true);
|
|
3218
|
+
this.showToolsPanel(true);
|
|
2214
3219
|
this.glass = false;
|
|
2215
3220
|
}
|
|
2216
3221
|
const options: SizeOptions = {
|