@preference-sl/pref-viewer 2.16.0-beta.9 → 2.16.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/package.json +6 -6
- package/src/babylonjs-controller.js +44 -208
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/pref-viewer-3d.js +24 -8
- package/src/pref-viewer-menu-3d.js +133 -260
- package/src/pref-viewer.js +3 -3
- package/src/render-math.js +127 -0
- package/src/styles.js +71 -162
|
@@ -3,34 +3,32 @@ import BabylonJSController from "./babylonjs-controller.js";
|
|
|
3
3
|
import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* PrefViewerMenu3D - Localized floating control
|
|
6
|
+
* PrefViewerMenu3D - Localized floating control panel for Babylon.js render settings.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Renders a gear-icon toggle button that appears on viewer hover and opens a panel with:
|
|
9
|
+
* - Boolean switches for every boolean render setting (AA, SSAO, IBL, shadows, highlight, etc.).
|
|
10
|
+
* - A hue slider for the highlight color.
|
|
11
|
+
* - A time-of-day slider with play/pause continuous lighting-cycle animation.
|
|
12
|
+
*
|
|
13
|
+
* Every interaction emits `pref-viewer-menu-3d-apply` immediately; there is no Apply button or
|
|
14
|
+
* draft/pending state. Switch changes emit the full settings snapshot, while highlight color and
|
|
15
|
+
* lighting changes emit partial payloads to avoid unnecessary scene reloads.
|
|
15
16
|
*
|
|
16
17
|
* Public API:
|
|
17
|
-
* - `setSettings(settings)`:
|
|
18
|
-
* - `setApplying(isApplying
|
|
19
|
-
* - `setViewerHover(isHovering)`:
|
|
20
|
-
* - `setEnabled(isEnabled)`: hides the menu
|
|
21
|
-
* - `setSwitchEnabled(settingKey, isEnabled)`: enables/disables a
|
|
22
|
-
* - `setCulture(cultureId)`:
|
|
18
|
+
* - `setSettings(settings)`: syncs the UI with the latest render settings snapshot.
|
|
19
|
+
* - `setApplying(isApplying)`: locks interactions while an async apply operation is running.
|
|
20
|
+
* - `setViewerHover(isHovering)`: drives panel visibility from the 3D viewport hover state.
|
|
21
|
+
* - `setEnabled(isEnabled)`: hides the menu when 3D mode is unavailable.
|
|
22
|
+
* - `setSwitchEnabled(settingKey, isEnabled)`: enables/disables a specific toggle at runtime.
|
|
23
|
+
* - `setCulture(cultureId)`: switches the display locale.
|
|
23
24
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* - Exposes a single `culture` observed attribute so hosts can drive localization declaratively.
|
|
25
|
+
* Observed attributes: `culture`, `show-illumination`, `illumination-color`, `lighting-time-of-day`.
|
|
26
|
+
* The illumination attributes are mirrored for compatibility with hosts that still set them declaratively.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* - Uses ARIA labels/live regions for the toggle button, dialog panel, pending indicator, and status slot.
|
|
28
|
+
* Events emitted: `pref-viewer-menu-3d-apply` - `{ detail: { settings: object } }`.
|
|
30
29
|
*/
|
|
31
30
|
export default class PrefViewerMenu3D extends HTMLElement {
|
|
32
31
|
#elements = {
|
|
33
|
-
applyButton: null,
|
|
34
32
|
colorPicker: null,
|
|
35
33
|
colorSlider: null,
|
|
36
34
|
colorPickerPreview: null,
|
|
@@ -43,8 +41,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
43
41
|
lightingSlider: null,
|
|
44
42
|
lightingValue: null,
|
|
45
43
|
panel: null,
|
|
46
|
-
pendingLabel: null,
|
|
47
|
-
status: null,
|
|
48
44
|
subtitle: null,
|
|
49
45
|
switches: {},
|
|
50
46
|
switchCopyNodes: {},
|
|
@@ -54,7 +50,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
54
50
|
};
|
|
55
51
|
|
|
56
52
|
#handles = {
|
|
57
|
-
onApplyClick: null,
|
|
58
53
|
onColorSliderInput: null,
|
|
59
54
|
onLightingSliderInput: null,
|
|
60
55
|
onLightingPlayClick: null,
|
|
@@ -65,25 +60,28 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
65
60
|
};
|
|
66
61
|
|
|
67
62
|
#DEFAULT_RENDER_SETTINGS = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
68
|
-
// Only include boolean settings as switches
|
|
63
|
+
// Only include boolean settings as switches; scalar settings are handled by sliders.
|
|
69
64
|
#MENU_SWITCHES = Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter(key =>
|
|
70
65
|
typeof this.#DEFAULT_RENDER_SETTINGS[key] === "boolean"
|
|
71
66
|
);
|
|
72
|
-
#
|
|
73
|
-
#draftSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
|
|
67
|
+
#settings = { ...this.#DEFAULT_RENDER_SETTINGS };
|
|
74
68
|
#switchAvailability = {};
|
|
75
|
-
#statusTimeout = null;
|
|
76
69
|
#isApplying = false;
|
|
77
70
|
#isEnabled = true;
|
|
78
71
|
#isLightingPlaying = false;
|
|
79
72
|
#lightingAnimationFrame = null;
|
|
80
73
|
#lightingPlaybackStart = 0;
|
|
81
74
|
#lightingPlaybackBase = 0.5;
|
|
82
|
-
#lightingPlaybackDurationMs =
|
|
75
|
+
#lightingPlaybackDurationMs = 15000;
|
|
83
76
|
|
|
84
77
|
#culture = DEFAULT_LOCALE;
|
|
85
78
|
#texts = {};
|
|
86
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Creates the menu custom element.
|
|
82
|
+
* Rendering is deferred until connectedCallback() so attributes can be set before the UI is built.
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
87
85
|
constructor() {
|
|
88
86
|
super();
|
|
89
87
|
}
|
|
@@ -98,35 +96,30 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
/**
|
|
101
|
-
* Reacts to observed attribute mutations
|
|
99
|
+
* Reacts to observed attribute mutations and mirrors them into the internal settings/UI state.
|
|
102
100
|
* @public
|
|
103
|
-
* @param {
|
|
104
|
-
* @param {
|
|
105
|
-
* @param {
|
|
101
|
+
* @param {"culture"|"show-illumination"|"illumination-color"|"lighting-time-of-day"} name - Attribute name that changed.
|
|
102
|
+
* @param {string|null} _old - Previous attribute value (unused).
|
|
103
|
+
* @param {string|null} value - New attribute value.
|
|
106
104
|
* @returns {void}
|
|
107
105
|
*/
|
|
108
106
|
attributeChangedCallback(name, _old, value) {
|
|
109
107
|
if (name === "culture") {
|
|
110
108
|
this.#setCultureInternal(value || DEFAULT_LOCALE);
|
|
111
|
-
} else if (name === "show-illumination"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const switchEl = this.#elements.switches?.illuminationEnabled;
|
|
117
|
-
if (switchEl) {
|
|
118
|
-
switchEl.checked = this.#draftSettings.illuminationEnabled;
|
|
119
|
-
}
|
|
120
|
-
} else if (name === "illumination-color") {
|
|
121
|
-
this.#draftSettings.illuminationColor = value;
|
|
122
|
-
// Update color picker preview if exists
|
|
123
|
-
this.#updateColorPickerPreview(value);
|
|
109
|
+
} else if (name === "show-illumination") {
|
|
110
|
+
this.#settings.illuminationEnabled = value === "true" || value === true;
|
|
111
|
+
const switchEl = this.#elements.switches?.illuminationEnabled;
|
|
112
|
+
if (switchEl) {
|
|
113
|
+
switchEl.checked = this.#settings.illuminationEnabled;
|
|
124
114
|
}
|
|
115
|
+
} else if (name === "illumination-color") {
|
|
116
|
+
this.#settings.illuminationColor = value;
|
|
117
|
+
this.#updateColorPickerPreview(value);
|
|
125
118
|
} else if (name === "lighting-time-of-day") {
|
|
126
119
|
const parsed = Number(value);
|
|
127
120
|
if (Number.isFinite(parsed)) {
|
|
128
|
-
this.#
|
|
129
|
-
this.#updateLightingSliderUI(this.#
|
|
121
|
+
this.#settings.lightingTimeOfDay = this.#clampLightingTime(parsed);
|
|
122
|
+
this.#updateLightingSliderUI(this.#settings.lightingTimeOfDay);
|
|
130
123
|
}
|
|
131
124
|
}
|
|
132
125
|
}
|
|
@@ -143,27 +136,23 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
143
136
|
this.#createDOMElements();
|
|
144
137
|
this.#cacheElements();
|
|
145
138
|
this.#attachEvents();
|
|
146
|
-
this.setSettings(this.#
|
|
139
|
+
this.setSettings(this.#settings);
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
|
|
150
143
|
/**
|
|
151
144
|
* Lifecycle hook invoked when the element is removed from the DOM.
|
|
152
|
-
* Cleans up listeners and
|
|
145
|
+
* Cleans up listeners and stops lighting playback to avoid leaks.
|
|
153
146
|
* @public
|
|
154
147
|
* @returns {void}
|
|
155
148
|
*/
|
|
156
149
|
disconnectedCallback() {
|
|
157
150
|
this.#detachEvents();
|
|
158
151
|
this.#stopLightingPlayback();
|
|
159
|
-
if (this.#statusTimeout) {
|
|
160
|
-
clearTimeout(this.#statusTimeout);
|
|
161
|
-
this.#statusTimeout = null;
|
|
162
|
-
}
|
|
163
152
|
}
|
|
164
153
|
|
|
165
154
|
/**
|
|
166
|
-
* Builds the
|
|
155
|
+
* Builds the internal DOM structure for the toggle button, panel, switches, color slider, and lighting controls.
|
|
167
156
|
* @private
|
|
168
157
|
* @returns {void}
|
|
169
158
|
*/
|
|
@@ -180,34 +169,31 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
180
169
|
<p class="menu-subtitle">${this.#texts.subtitle}</p>
|
|
181
170
|
</div>
|
|
182
171
|
<div class="menu-switches">
|
|
183
|
-
${this.#MENU_SWITCHES.map((key) => this.#renderSwitchRow(key, this.#texts)).join("")}
|
|
172
|
+
${this.#MENU_SWITCHES.map((key) => this.#renderSwitchRow(key, this.#texts.features)).join("")}
|
|
184
173
|
</div>
|
|
185
174
|
<div class="menu-color-picker" data-setting="highlightColor">
|
|
186
175
|
<div class="color-slider-row">
|
|
187
|
-
<span class="color-picker-label">${this.#texts.
|
|
188
|
-
<span class="color-preview" style="background-color: ${this.#
|
|
176
|
+
<span class="color-picker-label">${this.#texts.features?.highlightColor?.label || "Highlight Color"}</span>
|
|
177
|
+
<span class="color-preview" style="background-color: ${this.#settings.highlightColor || '#ff6700'}"></span>
|
|
189
178
|
</div>
|
|
190
|
-
<input type="range" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#
|
|
179
|
+
<input type="range" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#settings.highlightColor || '#ff6700')}">
|
|
191
180
|
</div>
|
|
192
181
|
<div class="menu-lighting-slider" data-setting="lightingTimeOfDay">
|
|
193
182
|
<div class="lighting-slider-row">
|
|
194
|
-
<span class="lighting-slider-label">${this.#texts.
|
|
195
|
-
<span class="lighting-slider-value">${PrefViewerMenu3D.#formatLightingTime(this.#
|
|
183
|
+
<span class="lighting-slider-label">${this.#texts.features?.lightingTimeOfDay?.label || "Time of day"}</span>
|
|
184
|
+
<span class="lighting-slider-value">${PrefViewerMenu3D.#formatLightingTime(this.#settings.lightingTimeOfDay ?? 0.5)}</span>
|
|
196
185
|
</div>
|
|
197
|
-
<input type="range" class="lighting-slider" min="0" max="1000" step="1" value="${Math.round((this.#
|
|
186
|
+
<input type="range" class="lighting-slider" min="0" max="1000" step="1" value="${Math.round((this.#settings.lightingTimeOfDay ?? 0.5) * 1000)}">
|
|
198
187
|
<div class="lighting-slider-markers" aria-hidden="true">
|
|
199
|
-
<span class="lighting-slider-start">${this.#texts.
|
|
200
|
-
<span class="lighting-slider-mid">${this.#texts.
|
|
201
|
-
<span class="lighting-slider-end">${this.#texts.
|
|
188
|
+
<span class="lighting-slider-start">${this.#texts.features?.lightingTimeOfDay?.startLabel || "Night"}</span>
|
|
189
|
+
<span class="lighting-slider-mid">${this.#texts.features?.lightingTimeOfDay?.midLabel || "Day"}</span>
|
|
190
|
+
<span class="lighting-slider-end">${this.#texts.features?.lightingTimeOfDay?.endLabel || "Night"}</span>
|
|
202
191
|
</div>
|
|
203
192
|
<div class="lighting-slider-actions">
|
|
204
193
|
<button type="button" class="lighting-play-button">${this.#texts.actions?.play || "Play"}</button>
|
|
205
194
|
</div>
|
|
206
195
|
</div>
|
|
207
|
-
<div class="menu-footer">
|
|
208
|
-
<span class="menu-pending" aria-live="polite">${this.#texts.pendingLabel}</span>
|
|
209
|
-
<span class="menu-status" aria-live="assertive"></span>
|
|
210
|
-
</div>
|
|
196
|
+
<div class="menu-footer"></div>
|
|
211
197
|
</div>
|
|
212
198
|
`;
|
|
213
199
|
const style = document.createElement("style");
|
|
@@ -216,14 +202,14 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
216
202
|
}
|
|
217
203
|
|
|
218
204
|
/**
|
|
219
|
-
* Generates the HTML fragment for a single render-setting switch.
|
|
205
|
+
* Generates the HTML fragment for a single boolean render-setting switch.
|
|
220
206
|
* @private
|
|
221
|
-
* @param {string} key - Render setting
|
|
222
|
-
* @param {
|
|
207
|
+
* @param {string} key - Render setting key from BabylonJSController.DEFAULT_RENDER_SETTINGS.
|
|
208
|
+
* @param {Record<string, {label?: string, helper?: string}>} copy - Locale-aware feature copy used for labels.
|
|
223
209
|
* @returns {string} HTML string representing the switch row.
|
|
224
210
|
*/
|
|
225
211
|
#renderSwitchRow(key, copy) {
|
|
226
|
-
const switchCopy = copy
|
|
212
|
+
const switchCopy = copy?.[key] ?? {};
|
|
227
213
|
const label = switchCopy.label ?? key;
|
|
228
214
|
const helper = switchCopy.helper ?? "";
|
|
229
215
|
return `
|
|
@@ -243,7 +229,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
243
229
|
/**
|
|
244
230
|
* Pulls the full translation namespace for the menu using the active culture.
|
|
245
231
|
* @private
|
|
246
|
-
* @returns {object} Resolved translation object or an empty fallback.
|
|
232
|
+
* @returns {object} Resolved `prefViewer.menu3d` translation object or an empty fallback.
|
|
247
233
|
*/
|
|
248
234
|
#getMenuNamespace() {
|
|
249
235
|
const namespace = translate("prefViewer.menu3d", { locale: this.#culture });
|
|
@@ -253,24 +239,18 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
253
239
|
/**
|
|
254
240
|
* Normalizes locale data into a predictable shape consumed by the UI helpers.
|
|
255
241
|
* @private
|
|
256
|
-
* @returns {object} Structured copy bundle for titles, actions,
|
|
242
|
+
* @returns {{title: string, subtitle: string, actions: {play: string, pause: string}, features: object}} Structured copy bundle for titles, actions, and feature labels.
|
|
257
243
|
*/
|
|
258
244
|
#getMenuTexts() {
|
|
259
245
|
const data = this.#getMenuNamespace();
|
|
260
246
|
return {
|
|
261
247
|
title: data.title || "",
|
|
262
248
|
subtitle: data.subtitle || "",
|
|
263
|
-
pendingLabel: data.pendingLabel || "",
|
|
264
249
|
actions: {
|
|
265
|
-
apply: data.actions?.apply || "",
|
|
266
|
-
applying: data.actions?.applying || "",
|
|
267
250
|
play: data.actions?.play || "",
|
|
268
251
|
pause: data.actions?.pause || "",
|
|
269
252
|
},
|
|
270
|
-
|
|
271
|
-
error: data.status?.error || "",
|
|
272
|
-
},
|
|
273
|
-
switches: data.switches || {},
|
|
253
|
+
features: data.features || {},
|
|
274
254
|
};
|
|
275
255
|
}
|
|
276
256
|
|
|
@@ -291,34 +271,28 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
291
271
|
if (this.#elements.subtitle) {
|
|
292
272
|
this.#elements.subtitle.textContent = this.#texts.subtitle;
|
|
293
273
|
}
|
|
294
|
-
if (this.#elements.pendingLabel) {
|
|
295
|
-
this.#elements.pendingLabel.textContent = this.#texts.pendingLabel;
|
|
296
|
-
}
|
|
297
|
-
if (this.#elements.applyButton) {
|
|
298
|
-
this.#elements.applyButton.textContent = this.#isApplying ? this.#texts.actions.applying : this.#texts.actions.apply;
|
|
299
|
-
}
|
|
300
274
|
if (this.#elements.lightingPlayButton) {
|
|
301
275
|
this.#elements.lightingPlayButton.textContent = this.#isLightingPlaying ? (this.#texts.actions.pause || "Pause") : (this.#texts.actions.play || "Play");
|
|
302
276
|
this.#elements.lightingPlayButton.setAttribute("aria-pressed", this.#isLightingPlaying ? "true" : "false");
|
|
303
277
|
}
|
|
304
278
|
if (this.#elements.lightingSliderLabel) {
|
|
305
|
-
this.#elements.lightingSliderLabel.textContent = this.#texts.
|
|
279
|
+
this.#elements.lightingSliderLabel.textContent = this.#texts.features?.lightingTimeOfDay?.label || "Time of day";
|
|
306
280
|
}
|
|
307
281
|
if (this.#elements.lightingSliderStart) {
|
|
308
|
-
this.#elements.lightingSliderStart.textContent = this.#texts.
|
|
282
|
+
this.#elements.lightingSliderStart.textContent = this.#texts.features?.lightingTimeOfDay?.startLabel || "Night";
|
|
309
283
|
}
|
|
310
284
|
if (this.#elements.lightingSliderMid) {
|
|
311
|
-
this.#elements.lightingSliderMid.textContent = this.#texts.
|
|
285
|
+
this.#elements.lightingSliderMid.textContent = this.#texts.features?.lightingTimeOfDay?.midLabel || "Day";
|
|
312
286
|
}
|
|
313
287
|
if (this.#elements.lightingSliderEnd) {
|
|
314
|
-
this.#elements.lightingSliderEnd.textContent = this.#texts.
|
|
288
|
+
this.#elements.lightingSliderEnd.textContent = this.#texts.features?.lightingTimeOfDay?.endLabel || "Night";
|
|
315
289
|
}
|
|
316
290
|
this.#updateLightingPlayButtonUI();
|
|
317
291
|
Object.entries(this.#elements.switchCopyNodes).forEach(([key, nodes]) => {
|
|
318
292
|
if (!nodes) {
|
|
319
293
|
return;
|
|
320
294
|
}
|
|
321
|
-
const switchCopy = this.#texts.
|
|
295
|
+
const switchCopy = this.#texts.features?.[key] ?? {};
|
|
322
296
|
nodes.title && (nodes.title.textContent = switchCopy.label ?? key);
|
|
323
297
|
nodes.helper && (nodes.helper.textContent = switchCopy.helper ?? "");
|
|
324
298
|
});
|
|
@@ -342,7 +316,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
342
316
|
}
|
|
343
317
|
|
|
344
318
|
/**
|
|
345
|
-
* Caches references to frequently used DOM nodes for
|
|
319
|
+
* Caches references to frequently used DOM nodes for event binding and UI synchronization.
|
|
346
320
|
* @private
|
|
347
321
|
* @returns {void}
|
|
348
322
|
*/
|
|
@@ -351,10 +325,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
351
325
|
this.#elements.panel = this.querySelector(".menu-panel");
|
|
352
326
|
this.#elements.title = this.querySelector(".menu-title");
|
|
353
327
|
this.#elements.subtitle = this.querySelector(".menu-subtitle");
|
|
354
|
-
this.#elements.applyButton = this.querySelector(".menu-apply");
|
|
355
|
-
this.#elements.pendingLabel = this.querySelector(".menu-pending");
|
|
356
|
-
this.#elements.status = this.querySelector(".menu-status");
|
|
357
|
-
|
|
358
328
|
// Cache color picker elements
|
|
359
329
|
this.#elements.colorPicker = this.querySelector(".menu-color-picker");
|
|
360
330
|
this.#elements.colorSlider = this.querySelector(".color-slider");
|
|
@@ -380,7 +350,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
380
350
|
}
|
|
381
351
|
|
|
382
352
|
/**
|
|
383
|
-
* Attaches all interactive event listeners for hover, focus, switch
|
|
353
|
+
* Attaches all interactive event listeners for hover, focus, switch, color, and lighting controls.
|
|
384
354
|
* @private
|
|
385
355
|
* @returns {void}
|
|
386
356
|
*/
|
|
@@ -397,7 +367,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
397
367
|
this.#closeMenu();
|
|
398
368
|
};
|
|
399
369
|
this.#handles.onSwitchChange = (event) => this.#handleSwitchChange(event);
|
|
400
|
-
this.#handles.onApplyClick = () => this.#emitApply();
|
|
401
370
|
this.#handles.onColorSliderInput = (event) => this.#handleColorSliderInput(event);
|
|
402
371
|
this.#handles.onLightingSliderInput = (event) => this.#handleLightingSliderInput(event);
|
|
403
372
|
this.#handles.onLightingPlayClick = () => this.#toggleLightingPlayback();
|
|
@@ -407,7 +376,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
407
376
|
this.#elements.panel.addEventListener("mouseenter", this.#handles.onPanelEnter);
|
|
408
377
|
this.addEventListener("mouseleave", this.#handles.onHostLeave);
|
|
409
378
|
Object.values(this.#elements.switches).forEach((input) => input?.addEventListener("change", this.#handles.onSwitchChange));
|
|
410
|
-
this.#elements.applyButton?.addEventListener("click", this.#handles.onApplyClick);
|
|
411
379
|
this.#elements.colorSlider?.addEventListener("input", this.#handles.onColorSliderInput);
|
|
412
380
|
this.#elements.lightingSlider?.addEventListener("input", this.#handles.onLightingSliderInput);
|
|
413
381
|
this.#elements.lightingPlayButton?.addEventListener("click", this.#handles.onLightingPlayClick);
|
|
@@ -427,8 +395,6 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
427
395
|
this.#elements.panel?.removeEventListener("mouseenter", this.#handles.onPanelEnter);
|
|
428
396
|
this.removeEventListener("mouseleave", this.#handles.onHostLeave);
|
|
429
397
|
Object.values(this.#elements.switches).forEach((input) => input?.removeEventListener("change", this.#handles.onSwitchChange));
|
|
430
|
-
this.#elements.applyButton?.removeEventListener("click", this.#handles.onApplyClick);
|
|
431
|
-
|
|
432
398
|
this.#elements.colorSlider?.removeEventListener("input", this.#handles.onColorSliderInput);
|
|
433
399
|
this.#elements.lightingSlider?.removeEventListener("input", this.#handles.onLightingSliderInput);
|
|
434
400
|
this.#elements.lightingPlayButton?.removeEventListener("click", this.#handles.onLightingPlayClick);
|
|
@@ -456,7 +422,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
456
422
|
}
|
|
457
423
|
|
|
458
424
|
/**
|
|
459
|
-
* Handles switch changes by updating
|
|
425
|
+
* Handles switch changes by updating settings and emitting a full settings snapshot.
|
|
460
426
|
* @private
|
|
461
427
|
* @param {Event} event - Change event triggered by a switch input.
|
|
462
428
|
* @returns {void}
|
|
@@ -469,32 +435,28 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
469
435
|
if (!key) {
|
|
470
436
|
return;
|
|
471
437
|
}
|
|
472
|
-
this.#
|
|
473
|
-
this.#
|
|
474
|
-
|
|
475
|
-
// For highlight toggle, apply immediately without waiting for user to click Apply
|
|
476
|
-
if (key === "highlightEnabled") {
|
|
477
|
-
this.#emitApply(true);
|
|
478
|
-
}
|
|
438
|
+
this.#settings[key] = event.currentTarget.checked;
|
|
439
|
+
this.#emitApply();
|
|
479
440
|
}
|
|
480
441
|
|
|
481
442
|
/**
|
|
482
|
-
* Handles
|
|
443
|
+
* Handles hue slider input by converting the selected hue to a highlight color.
|
|
444
|
+
* Emits a partial payload so highlight color changes apply without scheduling a render-settings reload.
|
|
483
445
|
* @private
|
|
484
|
-
* @param {Event} event -
|
|
446
|
+
* @param {Event} event - Input event from the hue slider.
|
|
485
447
|
* @returns {void}
|
|
486
448
|
*/
|
|
487
449
|
#handleColorSliderInput(event) {
|
|
488
450
|
const hue = parseInt(event.target.value, 10);
|
|
489
451
|
const color = PrefViewerMenu3D.#hueToHex(hue);
|
|
490
|
-
this.#
|
|
452
|
+
this.#settings.highlightColor = color;
|
|
491
453
|
this.#updateColorPickerPreview(color);
|
|
492
|
-
this.#
|
|
493
|
-
this.#emitApply(true);
|
|
454
|
+
this.#emitApply({ highlightColor: color }, { partial: true });
|
|
494
455
|
}
|
|
495
456
|
|
|
496
457
|
/**
|
|
497
|
-
* Handles lighting cycle slider input by updating the
|
|
458
|
+
* Handles lighting cycle slider input by updating the normalized time-of-day value.
|
|
459
|
+
* Emits a partial payload so lighting changes apply immediately without scheduling a render-settings reload.
|
|
498
460
|
* @private
|
|
499
461
|
* @param {Event} event - Input event from the time-of-day slider.
|
|
500
462
|
* @returns {void}
|
|
@@ -505,14 +467,13 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
505
467
|
return;
|
|
506
468
|
}
|
|
507
469
|
const normalized = this.#clampLightingTime(rawValue / 1000);
|
|
508
|
-
this.#
|
|
470
|
+
this.#settings.lightingTimeOfDay = normalized;
|
|
509
471
|
if (this.#isLightingPlaying) {
|
|
510
472
|
this.#lightingPlaybackBase = normalized;
|
|
511
473
|
this.#lightingPlaybackStart = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
512
474
|
}
|
|
513
475
|
this.#updateLightingSliderUI(normalized);
|
|
514
|
-
this.#
|
|
515
|
-
this.#emitApply(true);
|
|
476
|
+
this.#emitApply({ lightingTimeOfDay: normalized }, { partial: true });
|
|
516
477
|
}
|
|
517
478
|
|
|
518
479
|
/**
|
|
@@ -530,6 +491,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
530
491
|
|
|
531
492
|
/**
|
|
532
493
|
* Starts the continuous day/night playback loop.
|
|
494
|
+
* The playback speed is controlled by `#lightingPlaybackDurationMs`, which represents one full 0..1 cycle.
|
|
533
495
|
* @private
|
|
534
496
|
* @returns {void}
|
|
535
497
|
*/
|
|
@@ -538,7 +500,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
538
500
|
return;
|
|
539
501
|
}
|
|
540
502
|
this.#isLightingPlaying = true;
|
|
541
|
-
this.#lightingPlaybackBase = this.#
|
|
503
|
+
this.#lightingPlaybackBase = this.#settings.lightingTimeOfDay ?? 0.5;
|
|
542
504
|
this.#lightingPlaybackStart = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
543
505
|
this.#updateLightingPlayButtonUI();
|
|
544
506
|
const tick = () => {
|
|
@@ -548,9 +510,9 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
548
510
|
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
549
511
|
const elapsed = now - this.#lightingPlaybackStart;
|
|
550
512
|
const cycle = (this.#lightingPlaybackBase + (elapsed / this.#lightingPlaybackDurationMs)) % 1;
|
|
551
|
-
this.#
|
|
513
|
+
this.#settings.lightingTimeOfDay = cycle;
|
|
552
514
|
this.#updateLightingSliderUI(cycle);
|
|
553
|
-
this.#emitApply(
|
|
515
|
+
this.#emitApply({ lightingTimeOfDay: cycle, persist: false }, { partial: true });
|
|
554
516
|
this.#lightingAnimationFrame = requestAnimationFrame(tick);
|
|
555
517
|
};
|
|
556
518
|
this.#lightingAnimationFrame = requestAnimationFrame(tick);
|
|
@@ -559,6 +521,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
559
521
|
/**
|
|
560
522
|
* Stops the continuous day/night playback loop.
|
|
561
523
|
* @private
|
|
524
|
+
* @param {boolean} [persist=false] - True to emit a final apply event that should be persisted by the host.
|
|
562
525
|
* @returns {void}
|
|
563
526
|
*/
|
|
564
527
|
#stopLightingPlayback(persist = false) {
|
|
@@ -568,7 +531,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
568
531
|
this.#lightingAnimationFrame = null;
|
|
569
532
|
}
|
|
570
533
|
if (persist) {
|
|
571
|
-
this.#emitApply(true, {
|
|
534
|
+
this.#emitApply({ lightingTimeOfDay: this.#settings.lightingTimeOfDay, persist: true }, { partial: true });
|
|
572
535
|
}
|
|
573
536
|
this.#updateLightingPlayButtonUI();
|
|
574
537
|
}
|
|
@@ -587,6 +550,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
587
550
|
this.#elements.lightingPlayButton.setAttribute("aria-pressed", this.#isLightingPlaying ? "true" : "false");
|
|
588
551
|
}
|
|
589
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Converts a hue angle into a saturated hex color.
|
|
555
|
+
* @private
|
|
556
|
+
* @param {number} hue - Hue angle in degrees. Values outside 0..359 wrap around.
|
|
557
|
+
* @returns {string} Hex color string in "#rrggbb" format.
|
|
558
|
+
*/
|
|
590
559
|
static #hueToHex(hue) {
|
|
591
560
|
const h = ((hue % 360) + 360) % 360;
|
|
592
561
|
const a = Math.min(0.5, 1 - 0.5);
|
|
@@ -597,6 +566,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
597
566
|
return '#' + [f(0), f(8), f(4)].map((v) => Math.round(v * 255).toString(16).padStart(2, '0')).join('');
|
|
598
567
|
}
|
|
599
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Extracts the hue angle from a hex color string.
|
|
571
|
+
* @private
|
|
572
|
+
* @param {string} hex - Hex color string in "#rrggbb" format.
|
|
573
|
+
* @returns {number} Hue angle in degrees, or the default orange hue when the value is invalid.
|
|
574
|
+
*/
|
|
600
575
|
static #hexToHue(hex) {
|
|
601
576
|
if (!hex || hex.length < 7) return 24;
|
|
602
577
|
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
@@ -609,6 +584,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
609
584
|
return Math.round(h * 60);
|
|
610
585
|
}
|
|
611
586
|
|
|
587
|
+
/**
|
|
588
|
+
* Formats a normalized lighting-cycle value as a 24-hour clock label.
|
|
589
|
+
* @private
|
|
590
|
+
* @param {number} value - Normalized time-of-day value between 0 and 1.
|
|
591
|
+
* @returns {string} Time label formatted as "HH:mm".
|
|
592
|
+
*/
|
|
612
593
|
static #formatLightingTime(value) {
|
|
613
594
|
const normalized = Math.min(1, Math.max(0, Number(value) || 0));
|
|
614
595
|
const minutes = Math.round(normalized * 24 * 60) % (24 * 60);
|
|
@@ -617,6 +598,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
617
598
|
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
618
599
|
}
|
|
619
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Clamps a lighting-cycle value to the supported normalized range.
|
|
603
|
+
* @private
|
|
604
|
+
* @param {number} value - Value to clamp.
|
|
605
|
+
* @returns {number} Clamped value between 0 and 1, or 0.5 when the input is invalid.
|
|
606
|
+
*/
|
|
620
607
|
#clampLightingTime(value) {
|
|
621
608
|
if (!Number.isFinite(value)) {
|
|
622
609
|
return 0.5;
|
|
@@ -625,7 +612,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
625
612
|
}
|
|
626
613
|
|
|
627
614
|
/**
|
|
628
|
-
* Updates the color picker UI
|
|
615
|
+
* Updates the color picker UI, including the hue slider and color preview.
|
|
629
616
|
* @private
|
|
630
617
|
* @param {string} color - Selected color.
|
|
631
618
|
* @returns {void}
|
|
@@ -669,28 +656,28 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
669
656
|
}
|
|
670
657
|
|
|
671
658
|
/**
|
|
672
|
-
* Emits the apply event with the current
|
|
659
|
+
* Emits the apply event with the current settings immediately unless the menu is locked.
|
|
673
660
|
* @private
|
|
661
|
+
* @param {object} [extraSettings={}] - Additional fields merged into the emitted settings payload, or the complete payload when `options.partial` is true.
|
|
662
|
+
* @param {object} [options={}] - Event payload options.
|
|
663
|
+
* @param {boolean} [options.partial=false] - True to emit only `extraSettings` instead of the full settings snapshot.
|
|
674
664
|
* @returns {void}
|
|
675
665
|
*/
|
|
676
|
-
#emitApply(
|
|
677
|
-
|
|
678
|
-
if (!force && this.#isApplying) {
|
|
666
|
+
#emitApply(extraSettings = {}, options = {}) {
|
|
667
|
+
if (this.#isApplying) {
|
|
679
668
|
return;
|
|
680
669
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const customEventOptions = {
|
|
670
|
+
const detail = { settings: options.partial ? { ...extraSettings } : { ...this.#settings, ...extraSettings } };
|
|
671
|
+
this.dispatchEvent(new CustomEvent("pref-viewer-menu-3d-apply", {
|
|
684
672
|
bubbles: true,
|
|
685
673
|
cancelable: true,
|
|
686
674
|
composed: true,
|
|
687
|
-
detail
|
|
688
|
-
};
|
|
689
|
-
this.dispatchEvent(new CustomEvent("pref-viewer-menu-3d-apply", customEventOptions));
|
|
675
|
+
detail,
|
|
676
|
+
}));
|
|
690
677
|
}
|
|
691
678
|
|
|
692
679
|
/**
|
|
693
|
-
* Synchronizes checkbox states with the
|
|
680
|
+
* Synchronizes checkbox states with the current settings snapshot.
|
|
694
681
|
* @private
|
|
695
682
|
* @returns {void}
|
|
696
683
|
*/
|
|
@@ -698,102 +685,13 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
698
685
|
Object.entries(this.#elements.switches).forEach(([key, input]) => {
|
|
699
686
|
if (input) {
|
|
700
687
|
const isEnabled = this.#switchAvailability[key] !== false;
|
|
701
|
-
input.checked = Boolean(this.#
|
|
688
|
+
input.checked = Boolean(this.#settings[key]);
|
|
702
689
|
input.disabled = !isEnabled;
|
|
703
690
|
this.#elements.switchWrappers[key]?.toggleAttribute("data-disabled", !isEnabled);
|
|
704
691
|
}
|
|
705
692
|
});
|
|
706
693
|
}
|
|
707
694
|
|
|
708
|
-
/**
|
|
709
|
-
* Toggles pending labels, apply button state, and per-switch markers based on outstanding changes.
|
|
710
|
-
* @private
|
|
711
|
-
* @returns {void}
|
|
712
|
-
*/
|
|
713
|
-
#updatePendingState() {
|
|
714
|
-
const pendingKeys = this.#getPendingKeys();
|
|
715
|
-
const pending = pendingKeys.length > 0;
|
|
716
|
-
if (this.#elements.pendingLabel) {
|
|
717
|
-
this.#elements.pendingLabel.hidden = !pending;
|
|
718
|
-
}
|
|
719
|
-
if (!this.#isApplying && this.#elements.applyButton) {
|
|
720
|
-
this.#elements.applyButton.disabled = !pending;
|
|
721
|
-
}
|
|
722
|
-
this.#updateSwitchPendingIndicators(pendingKeys);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Calculates which render keys differ between draft and applied snapshots.
|
|
727
|
-
* @private
|
|
728
|
-
* @returns {string[]} Keys currently pending application.
|
|
729
|
-
*/
|
|
730
|
-
#getPendingKeys() {
|
|
731
|
-
return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => {
|
|
732
|
-
if (key === "lightingTimeOfDay") {
|
|
733
|
-
return false;
|
|
734
|
-
}
|
|
735
|
-
if (this.#elements.switches[key]?.disabled) {
|
|
736
|
-
return false;
|
|
737
|
-
}
|
|
738
|
-
return this.#draftSettings[key] !== this.#appliedSettings[key];
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Convenience helper to check if any pending changes exist.
|
|
744
|
-
* @private
|
|
745
|
-
* @returns {boolean}
|
|
746
|
-
*/
|
|
747
|
-
#hasPendingChanges() {
|
|
748
|
-
return this.#getPendingKeys().length > 0;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Adds or removes the `data-pending` attribute on switch wrappers to visually flag unsaved toggles.
|
|
753
|
-
* @private
|
|
754
|
-
* @param {string[]} pendingKeys - Keys currently pending.
|
|
755
|
-
* @returns {void}
|
|
756
|
-
*/
|
|
757
|
-
#updateSwitchPendingIndicators(pendingKeys = []) {
|
|
758
|
-
Object.entries(this.#elements.switchWrappers).forEach(([key, wrapper]) => {
|
|
759
|
-
if (!wrapper) {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
wrapper.toggleAttribute("data-pending", pendingKeys.includes(key));
|
|
763
|
-
});
|
|
764
|
-
this.#elements.lightingSliderWrapper?.toggleAttribute("data-pending", pendingKeys.includes("lightingTimeOfDay"));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Shows a transient status message in the footer and schedules it to auto-clear.
|
|
769
|
-
* @private
|
|
770
|
-
* @param {string} message - The status text to display.
|
|
771
|
-
* @returns {void}
|
|
772
|
-
*/
|
|
773
|
-
#setStatusMessage(message) {
|
|
774
|
-
if (!this.#elements.status) {
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
this.#elements.status.textContent = message;
|
|
778
|
-
if (message) {
|
|
779
|
-
this.setAttribute("data-status", "visible");
|
|
780
|
-
} else {
|
|
781
|
-
this.removeAttribute("data-status");
|
|
782
|
-
}
|
|
783
|
-
if (this.#statusTimeout) {
|
|
784
|
-
clearTimeout(this.#statusTimeout);
|
|
785
|
-
this.#statusTimeout = null;
|
|
786
|
-
}
|
|
787
|
-
if (message) {
|
|
788
|
-
const schedule = typeof window !== "undefined" ? window.setTimeout : setTimeout;
|
|
789
|
-
this.#statusTimeout = schedule(() => {
|
|
790
|
-
this.#elements.status.textContent = "";
|
|
791
|
-
this.removeAttribute("data-status");
|
|
792
|
-
this.#statusTimeout = null;
|
|
793
|
-
}, 4000);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
695
|
/**
|
|
798
696
|
* ---------------------------
|
|
799
697
|
* Public methods
|
|
@@ -801,24 +699,14 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
801
699
|
*/
|
|
802
700
|
|
|
803
701
|
/**
|
|
804
|
-
* Locks or unlocks the menu while changes are applied
|
|
702
|
+
* Locks or unlocks the menu while changes are applied.
|
|
805
703
|
* @public
|
|
806
704
|
* @param {boolean} isApplying - Whether an apply operation is currently running.
|
|
807
|
-
* @param {boolean} [hasError=false] - Optional flag to show an error message.
|
|
808
705
|
* @returns {void}
|
|
809
706
|
*/
|
|
810
|
-
setApplying(isApplying
|
|
707
|
+
setApplying(isApplying) {
|
|
811
708
|
this.#isApplying = isApplying;
|
|
812
709
|
this.toggleAttribute("data-applying", isApplying);
|
|
813
|
-
if (this.#elements.applyButton) {
|
|
814
|
-
this.#elements.applyButton.textContent = isApplying ? this.#texts.actions.applying : this.#texts.actions.apply;
|
|
815
|
-
this.#elements.applyButton.disabled = isApplying || !this.#hasPendingChanges();
|
|
816
|
-
}
|
|
817
|
-
if (hasError) {
|
|
818
|
-
this.#setStatusMessage(this.#texts.status.error);
|
|
819
|
-
} else if (!isApplying) {
|
|
820
|
-
this.#setStatusMessage("");
|
|
821
|
-
}
|
|
822
710
|
}
|
|
823
711
|
|
|
824
712
|
/**
|
|
@@ -852,32 +740,22 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
852
740
|
}
|
|
853
741
|
|
|
854
742
|
/**
|
|
855
|
-
* Applies a new
|
|
743
|
+
* Applies a new render settings snapshot to the UI and clears the applying state.
|
|
856
744
|
* @public
|
|
857
|
-
* @param {object} settings - Render settings snapshot from PrefViewer.
|
|
745
|
+
* @param {object} settings - Render settings snapshot from PrefViewer or BabylonJSController.
|
|
858
746
|
* @returns {void}
|
|
859
747
|
*/
|
|
860
748
|
setSettings(settings = {}) {
|
|
861
|
-
this.#
|
|
862
|
-
this.#draftSettings = { ...this.#appliedSettings };
|
|
749
|
+
this.#settings = { ...this.#DEFAULT_RENDER_SETTINGS, ...settings };
|
|
863
750
|
this.#updateSwitches();
|
|
864
|
-
this.#
|
|
865
|
-
|
|
866
|
-
// Sync color slider with current highlightColor
|
|
867
|
-
if (this.#draftSettings.highlightColor) {
|
|
868
|
-
this.#updateColorPickerUI(this.#draftSettings.highlightColor);
|
|
869
|
-
}
|
|
870
|
-
if (this.#draftSettings.lightingTimeOfDay !== undefined) {
|
|
871
|
-
this.#updateLightingSliderUI(this.#draftSettings.lightingTimeOfDay);
|
|
751
|
+
if (this.#settings.highlightColor) {
|
|
752
|
+
this.#updateColorPickerUI(this.#settings.highlightColor);
|
|
872
753
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
this.#updateColorPickerUI(this.#draftSettings.illuminationColor);
|
|
754
|
+
if (this.#settings.lightingTimeOfDay !== undefined) {
|
|
755
|
+
this.#updateLightingSliderUI(this.#settings.lightingTimeOfDay);
|
|
876
756
|
}
|
|
877
757
|
this.#updateLightingPlayButtonUI();
|
|
878
|
-
|
|
879
758
|
this.setApplying(false);
|
|
880
|
-
this.#setStatusMessage("");
|
|
881
759
|
}
|
|
882
760
|
|
|
883
761
|
/**
|
|
@@ -909,12 +787,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
909
787
|
if (!settingKey || !this.#MENU_SWITCHES.includes(settingKey)) {
|
|
910
788
|
return;
|
|
911
789
|
}
|
|
912
|
-
|
|
913
|
-
this.#switchAvailability[settingKey] = enabled;
|
|
914
|
-
if (!enabled) {
|
|
915
|
-
this.#draftSettings[settingKey] = this.#appliedSettings[settingKey];
|
|
916
|
-
}
|
|
790
|
+
this.#switchAvailability[settingKey] = Boolean(isEnabled);
|
|
917
791
|
this.#updateSwitches();
|
|
918
|
-
this.#updatePendingState();
|
|
919
792
|
}
|
|
920
793
|
}
|