@preference-sl/pref-viewer 2.14.0 → 2.16.0-beta.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 +1 -1
- package/src/babylonjs-controller.js +378 -127
- package/src/localization/translations.js +18 -0
- package/src/pref-viewer-3d.js +69 -2
- package/src/pref-viewer-menu-3d.js +206 -3
- package/src/pref-viewer.js +5 -1
- package/src/styles.js +117 -0
|
@@ -8,6 +8,8 @@ export const translations = {
|
|
|
8
8
|
actions: {
|
|
9
9
|
apply: "Apply",
|
|
10
10
|
applying: "Applying...",
|
|
11
|
+
play: "Play",
|
|
12
|
+
pause: "Pause",
|
|
11
13
|
},
|
|
12
14
|
status: {
|
|
13
15
|
error: "Couldn't apply the changes.",
|
|
@@ -37,6 +39,13 @@ export const translations = {
|
|
|
37
39
|
label: "Highlight Color",
|
|
38
40
|
helper: "Color for hover highlight on animations.",
|
|
39
41
|
},
|
|
42
|
+
lightingTimeOfDay: {
|
|
43
|
+
label: "Time of day",
|
|
44
|
+
helper: "Slide from night to day and back to night with a smooth sun and moon cycle.",
|
|
45
|
+
startLabel: "Night",
|
|
46
|
+
midLabel: "Day",
|
|
47
|
+
endLabel: "Night",
|
|
48
|
+
},
|
|
40
49
|
},
|
|
41
50
|
},
|
|
42
51
|
downloadDialog: {
|
|
@@ -66,6 +75,8 @@ export const translations = {
|
|
|
66
75
|
actions: {
|
|
67
76
|
apply: "Aplicar",
|
|
68
77
|
applying: "Aplicando...",
|
|
78
|
+
play: "Reproducir",
|
|
79
|
+
pause: "Pausar",
|
|
69
80
|
},
|
|
70
81
|
status: {
|
|
71
82
|
error: "No se pudo aplicar los cambios.",
|
|
@@ -95,6 +106,13 @@ export const translations = {
|
|
|
95
106
|
label: "Color de resaltado",
|
|
96
107
|
helper: "Color del resaltado al pasar sobre animaciones.",
|
|
97
108
|
},
|
|
109
|
+
lightingTimeOfDay: {
|
|
110
|
+
label: "Hora del día",
|
|
111
|
+
helper: "Desliza de noche a día y vuelve a noche con un ciclo suave de sol y luna.",
|
|
112
|
+
startLabel: "Noche",
|
|
113
|
+
midLabel: "Día",
|
|
114
|
+
endLabel: "Noche",
|
|
115
|
+
},
|
|
98
116
|
},
|
|
99
117
|
},
|
|
100
118
|
downloadDialog: {
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -229,6 +229,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
229
229
|
outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
|
|
230
230
|
},
|
|
231
231
|
ibl: new IBLData(),
|
|
232
|
+
lighting: {
|
|
233
|
+
timeOfDay: 0.5,
|
|
234
|
+
},
|
|
232
235
|
},
|
|
233
236
|
};
|
|
234
237
|
}
|
|
@@ -426,6 +429,38 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
426
429
|
return needUpdate;
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Resolves incoming lighting settings (time-of-day slider) and marks the option as pending when changed.
|
|
434
|
+
* Supports both nested `options.lighting.timeOfDay` and flat `options.lightingTimeOfDay` payloads so the menu
|
|
435
|
+
* and external API can share the same controller path.
|
|
436
|
+
* @private
|
|
437
|
+
* @param {object} options - Options payload containing lighting state.
|
|
438
|
+
* @returns {boolean} True when lighting differs from the cached state, otherwise false.
|
|
439
|
+
*/
|
|
440
|
+
#checkNeedToUpdateLighting(options) {
|
|
441
|
+
if (!options) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const lightingState = this.#data.options.lighting;
|
|
446
|
+
const incomingTime = typeof options.lightingTimeOfDay === "number"
|
|
447
|
+
? options.lightingTimeOfDay
|
|
448
|
+
: typeof options.lighting?.timeOfDay === "number"
|
|
449
|
+
? options.lighting.timeOfDay
|
|
450
|
+
: undefined;
|
|
451
|
+
|
|
452
|
+
if (!Number.isFinite(incomingTime)) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const normalizedTime = Math.min(1, Math.max(0, incomingTime));
|
|
457
|
+
const controllerTime = this.#babylonJSController?.getRenderSettings?.()?.lightingTimeOfDay;
|
|
458
|
+
const currentTime = Number.isFinite(controllerTime) ? controllerTime : lightingState.timeOfDay;
|
|
459
|
+
const needUpdate = normalizedTime !== currentTime;
|
|
460
|
+
lightingState.timeOfDay = normalizedTime;
|
|
461
|
+
return needUpdate;
|
|
462
|
+
}
|
|
463
|
+
|
|
429
464
|
/**
|
|
430
465
|
* Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
|
|
431
466
|
* Used internally when a loading operation starts.
|
|
@@ -608,14 +643,29 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
608
643
|
this.#checkNeedToUpdateContainers(config);
|
|
609
644
|
|
|
610
645
|
// Options
|
|
646
|
+
let needUpdateLighting = false;
|
|
647
|
+
const iblOptions = config.scene?.ibl
|
|
648
|
+
? { ...(config.options ?? {}), ibl: { ...(config.options?.ibl ?? {}), ...config.scene.ibl } }
|
|
649
|
+
: config.options;
|
|
650
|
+
|
|
611
651
|
if (config.options) {
|
|
612
652
|
this.#checkNeedToUpdateCamera(config.options);
|
|
613
653
|
this.#checkNeedToUpdateMaterials(config.options);
|
|
614
|
-
|
|
654
|
+
needUpdateLighting = this.#checkNeedToUpdateLighting(config.options);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (iblOptions) {
|
|
658
|
+
await this.#checkNeedToUpdateIBL(iblOptions);
|
|
615
659
|
}
|
|
616
660
|
|
|
617
661
|
const loadDetail = await this.#babylonJSController.load(forceReload);
|
|
618
662
|
|
|
663
|
+
if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
|
|
664
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
665
|
+
lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
619
669
|
// Apply initial render settings if provided in options
|
|
620
670
|
if (config.options?.render) {
|
|
621
671
|
await this.applyRenderSettings(config.options.render);
|
|
@@ -649,6 +699,13 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
649
699
|
if (needUpdateIBL) {
|
|
650
700
|
someSetted = someSetted || (await this.#babylonJSController.setIBLOptions());
|
|
651
701
|
}
|
|
702
|
+
const needUpdateLighting = this.#checkNeedToUpdateLighting(options);
|
|
703
|
+
if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
|
|
704
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
705
|
+
lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
|
|
706
|
+
});
|
|
707
|
+
someSetted = true;
|
|
708
|
+
}
|
|
652
709
|
|
|
653
710
|
// Apply render settings if provided
|
|
654
711
|
if (options.render) {
|
|
@@ -910,7 +967,17 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
910
967
|
}
|
|
911
968
|
|
|
912
969
|
// Exclude highlight settings from the reload — they are handled above without requiring a scene reload
|
|
913
|
-
|
|
970
|
+
if (settings.lightingTimeOfDay !== undefined && typeof this.#babylonJSController.setIlluminationConfig === "function") {
|
|
971
|
+
const lightingTimeOfDay = Number(settings.lightingTimeOfDay);
|
|
972
|
+
if (Number.isFinite(lightingTimeOfDay)) {
|
|
973
|
+
this.#babylonJSController.setIlluminationConfig({
|
|
974
|
+
lightingTimeOfDay,
|
|
975
|
+
persist: settings.persist !== false,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const { highlightEnabled: _he, highlightColor: _hc, lightingTimeOfDay: _lt, lighting: _lighting, persist: _persist, ...reloadSettings } = settings;
|
|
914
981
|
const scheduled = this.#babylonJSController.scheduleRenderSettingsReload(reloadSettings);
|
|
915
982
|
if (!scheduled.changed) {
|
|
916
983
|
return { changed: false, success: true };
|
|
@@ -34,6 +34,14 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
34
34
|
colorPicker: null,
|
|
35
35
|
colorSlider: null,
|
|
36
36
|
colorPickerPreview: null,
|
|
37
|
+
lightingPlayButton: null,
|
|
38
|
+
lightingSliderWrapper: null,
|
|
39
|
+
lightingSliderLabel: null,
|
|
40
|
+
lightingSliderStart: null,
|
|
41
|
+
lightingSliderMid: null,
|
|
42
|
+
lightingSliderEnd: null,
|
|
43
|
+
lightingSlider: null,
|
|
44
|
+
lightingValue: null,
|
|
37
45
|
panel: null,
|
|
38
46
|
pendingLabel: null,
|
|
39
47
|
status: null,
|
|
@@ -48,6 +56,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
48
56
|
#handles = {
|
|
49
57
|
onApplyClick: null,
|
|
50
58
|
onColorSliderInput: null,
|
|
59
|
+
onLightingSliderInput: null,
|
|
60
|
+
onLightingPlayClick: null,
|
|
51
61
|
onHostLeave: null,
|
|
52
62
|
onPanelEnter: null,
|
|
53
63
|
onSwitchChange: null,
|
|
@@ -65,6 +75,11 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
65
75
|
#statusTimeout = null;
|
|
66
76
|
#isApplying = false;
|
|
67
77
|
#isEnabled = true;
|
|
78
|
+
#isLightingPlaying = false;
|
|
79
|
+
#lightingAnimationFrame = null;
|
|
80
|
+
#lightingPlaybackStart = 0;
|
|
81
|
+
#lightingPlaybackBase = 0.5;
|
|
82
|
+
#lightingPlaybackDurationMs = 28000;
|
|
68
83
|
|
|
69
84
|
#culture = DEFAULT_LOCALE;
|
|
70
85
|
#texts = {};
|
|
@@ -79,7 +94,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
79
94
|
* @returns {string[]} Array of attribute names to observe.
|
|
80
95
|
*/
|
|
81
96
|
static get observedAttributes() {
|
|
82
|
-
return ["culture", "show-illumination", "illumination-color"];
|
|
97
|
+
return ["culture", "show-illumination", "illumination-color", "lighting-time-of-day"];
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
/**
|
|
@@ -107,6 +122,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
107
122
|
// Update color picker preview if exists
|
|
108
123
|
this.#updateColorPickerPreview(value);
|
|
109
124
|
}
|
|
125
|
+
} else if (name === "lighting-time-of-day") {
|
|
126
|
+
const parsed = Number(value);
|
|
127
|
+
if (Number.isFinite(parsed)) {
|
|
128
|
+
this.#draftSettings.lightingTimeOfDay = this.#clampLightingTime(parsed);
|
|
129
|
+
this.#updateLightingSliderUI(this.#draftSettings.lightingTimeOfDay);
|
|
130
|
+
}
|
|
110
131
|
}
|
|
111
132
|
}
|
|
112
133
|
|
|
@@ -134,6 +155,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
134
155
|
*/
|
|
135
156
|
disconnectedCallback() {
|
|
136
157
|
this.#detachEvents();
|
|
158
|
+
this.#stopLightingPlayback();
|
|
137
159
|
if (this.#statusTimeout) {
|
|
138
160
|
clearTimeout(this.#statusTimeout);
|
|
139
161
|
this.#statusTimeout = null;
|
|
@@ -167,6 +189,21 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
167
189
|
</div>
|
|
168
190
|
<input type="range" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#draftSettings.highlightColor || '#ff6700')}">
|
|
169
191
|
</div>
|
|
192
|
+
<div class="menu-lighting-slider" data-setting="lightingTimeOfDay">
|
|
193
|
+
<div class="lighting-slider-row">
|
|
194
|
+
<span class="lighting-slider-label">${this.#texts.switches?.lightingTimeOfDay?.label || "Time of day"}</span>
|
|
195
|
+
<span class="lighting-slider-value">${PrefViewerMenu3D.#formatLightingTime(this.#draftSettings.lightingTimeOfDay ?? 0.5)}</span>
|
|
196
|
+
</div>
|
|
197
|
+
<input type="range" class="lighting-slider" min="0" max="1000" step="1" value="${Math.round((this.#draftSettings.lightingTimeOfDay ?? 0.5) * 1000)}">
|
|
198
|
+
<div class="lighting-slider-markers" aria-hidden="true">
|
|
199
|
+
<span class="lighting-slider-start">${this.#texts.switches?.lightingTimeOfDay?.startLabel || "Night"}</span>
|
|
200
|
+
<span class="lighting-slider-mid">${this.#texts.switches?.lightingTimeOfDay?.midLabel || "Day"}</span>
|
|
201
|
+
<span class="lighting-slider-end">${this.#texts.switches?.lightingTimeOfDay?.endLabel || "Night"}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="lighting-slider-actions">
|
|
204
|
+
<button type="button" class="lighting-play-button">${this.#texts.actions?.play || "Play"}</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
170
207
|
<div class="menu-footer">
|
|
171
208
|
<span class="menu-pending" aria-live="polite">${this.#texts.pendingLabel}</span>
|
|
172
209
|
<span class="menu-status" aria-live="assertive"></span>
|
|
@@ -227,6 +264,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
227
264
|
actions: {
|
|
228
265
|
apply: data.actions?.apply || "",
|
|
229
266
|
applying: data.actions?.applying || "",
|
|
267
|
+
play: data.actions?.play || "",
|
|
268
|
+
pause: data.actions?.pause || "",
|
|
230
269
|
},
|
|
231
270
|
status: {
|
|
232
271
|
error: data.status?.error || "",
|
|
@@ -258,6 +297,23 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
258
297
|
if (this.#elements.applyButton) {
|
|
259
298
|
this.#elements.applyButton.textContent = this.#isApplying ? this.#texts.actions.applying : this.#texts.actions.apply;
|
|
260
299
|
}
|
|
300
|
+
if (this.#elements.lightingPlayButton) {
|
|
301
|
+
this.#elements.lightingPlayButton.textContent = this.#isLightingPlaying ? (this.#texts.actions.pause || "Pause") : (this.#texts.actions.play || "Play");
|
|
302
|
+
this.#elements.lightingPlayButton.setAttribute("aria-pressed", this.#isLightingPlaying ? "true" : "false");
|
|
303
|
+
}
|
|
304
|
+
if (this.#elements.lightingSliderLabel) {
|
|
305
|
+
this.#elements.lightingSliderLabel.textContent = this.#texts.switches?.lightingTimeOfDay?.label || "Time of day";
|
|
306
|
+
}
|
|
307
|
+
if (this.#elements.lightingSliderStart) {
|
|
308
|
+
this.#elements.lightingSliderStart.textContent = this.#texts.switches?.lightingTimeOfDay?.startLabel || "Night";
|
|
309
|
+
}
|
|
310
|
+
if (this.#elements.lightingSliderMid) {
|
|
311
|
+
this.#elements.lightingSliderMid.textContent = this.#texts.switches?.lightingTimeOfDay?.midLabel || "Day";
|
|
312
|
+
}
|
|
313
|
+
if (this.#elements.lightingSliderEnd) {
|
|
314
|
+
this.#elements.lightingSliderEnd.textContent = this.#texts.switches?.lightingTimeOfDay?.endLabel || "Night";
|
|
315
|
+
}
|
|
316
|
+
this.#updateLightingPlayButtonUI();
|
|
261
317
|
Object.entries(this.#elements.switchCopyNodes).forEach(([key, nodes]) => {
|
|
262
318
|
if (!nodes) {
|
|
263
319
|
return;
|
|
@@ -303,6 +359,14 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
303
359
|
this.#elements.colorPicker = this.querySelector(".menu-color-picker");
|
|
304
360
|
this.#elements.colorSlider = this.querySelector(".color-slider");
|
|
305
361
|
this.#elements.colorPickerPreview = this.querySelector(".color-preview");
|
|
362
|
+
this.#elements.lightingPlayButton = this.querySelector(".lighting-play-button");
|
|
363
|
+
this.#elements.lightingSliderWrapper = this.querySelector(".menu-lighting-slider");
|
|
364
|
+
this.#elements.lightingSliderLabel = this.querySelector(".lighting-slider-label");
|
|
365
|
+
this.#elements.lightingSliderStart = this.querySelector(".lighting-slider-start");
|
|
366
|
+
this.#elements.lightingSliderMid = this.querySelector(".lighting-slider-mid");
|
|
367
|
+
this.#elements.lightingSliderEnd = this.querySelector(".lighting-slider-end");
|
|
368
|
+
this.#elements.lightingSlider = this.querySelector(".lighting-slider");
|
|
369
|
+
this.#elements.lightingValue = this.querySelector(".lighting-slider-value");
|
|
306
370
|
|
|
307
371
|
this.#MENU_SWITCHES.forEach((key) => {
|
|
308
372
|
this.#elements.switches[key] = this.querySelector(`input[data-setting="${key}"]`);
|
|
@@ -335,6 +399,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
335
399
|
this.#handles.onSwitchChange = (event) => this.#handleSwitchChange(event);
|
|
336
400
|
this.#handles.onApplyClick = () => this.#emitApply();
|
|
337
401
|
this.#handles.onColorSliderInput = (event) => this.#handleColorSliderInput(event);
|
|
402
|
+
this.#handles.onLightingSliderInput = (event) => this.#handleLightingSliderInput(event);
|
|
403
|
+
this.#handles.onLightingPlayClick = () => this.#toggleLightingPlayback();
|
|
338
404
|
|
|
339
405
|
this.#elements.toggleButton.addEventListener("mouseenter", this.#handles.onToggleEnter);
|
|
340
406
|
this.#elements.toggleButton.addEventListener("focus", this.#handles.onToggleEnter);
|
|
@@ -343,6 +409,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
343
409
|
Object.values(this.#elements.switches).forEach((input) => input?.addEventListener("change", this.#handles.onSwitchChange));
|
|
344
410
|
this.#elements.applyButton?.addEventListener("click", this.#handles.onApplyClick);
|
|
345
411
|
this.#elements.colorSlider?.addEventListener("input", this.#handles.onColorSliderInput);
|
|
412
|
+
this.#elements.lightingSlider?.addEventListener("input", this.#handles.onLightingSliderInput);
|
|
413
|
+
this.#elements.lightingPlayButton?.addEventListener("click", this.#handles.onLightingPlayClick);
|
|
346
414
|
}
|
|
347
415
|
|
|
348
416
|
/**
|
|
@@ -362,6 +430,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
362
430
|
this.#elements.applyButton?.removeEventListener("click", this.#handles.onApplyClick);
|
|
363
431
|
|
|
364
432
|
this.#elements.colorSlider?.removeEventListener("input", this.#handles.onColorSliderInput);
|
|
433
|
+
this.#elements.lightingSlider?.removeEventListener("input", this.#handles.onLightingSliderInput);
|
|
434
|
+
this.#elements.lightingPlayButton?.removeEventListener("click", this.#handles.onLightingPlayClick);
|
|
365
435
|
}
|
|
366
436
|
|
|
367
437
|
/**
|
|
@@ -423,6 +493,100 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
423
493
|
this.#emitApply(true);
|
|
424
494
|
}
|
|
425
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Handles lighting cycle slider input by updating the draft state and applying it immediately.
|
|
498
|
+
* @private
|
|
499
|
+
* @param {Event} event - Input event from the time-of-day slider.
|
|
500
|
+
* @returns {void}
|
|
501
|
+
*/
|
|
502
|
+
#handleLightingSliderInput(event) {
|
|
503
|
+
const rawValue = parseInt(event.target.value, 10);
|
|
504
|
+
if (!Number.isFinite(rawValue)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const normalized = this.#clampLightingTime(rawValue / 1000);
|
|
508
|
+
this.#draftSettings.lightingTimeOfDay = normalized;
|
|
509
|
+
if (this.#isLightingPlaying) {
|
|
510
|
+
this.#lightingPlaybackBase = normalized;
|
|
511
|
+
this.#lightingPlaybackStart = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
512
|
+
}
|
|
513
|
+
this.#updateLightingSliderUI(normalized);
|
|
514
|
+
this.#updatePendingState();
|
|
515
|
+
this.#emitApply(true);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Toggles the continuous lighting playback loop.
|
|
520
|
+
* @private
|
|
521
|
+
* @returns {void}
|
|
522
|
+
*/
|
|
523
|
+
#toggleLightingPlayback() {
|
|
524
|
+
if (this.#isLightingPlaying) {
|
|
525
|
+
this.#stopLightingPlayback(true);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
this.#startLightingPlayback();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Starts the continuous day/night playback loop.
|
|
533
|
+
* @private
|
|
534
|
+
* @returns {void}
|
|
535
|
+
*/
|
|
536
|
+
#startLightingPlayback() {
|
|
537
|
+
if (this.#isLightingPlaying) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.#isLightingPlaying = true;
|
|
541
|
+
this.#lightingPlaybackBase = this.#draftSettings.lightingTimeOfDay ?? 0.5;
|
|
542
|
+
this.#lightingPlaybackStart = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
543
|
+
this.#updateLightingPlayButtonUI();
|
|
544
|
+
const tick = () => {
|
|
545
|
+
if (!this.#isLightingPlaying) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
549
|
+
const elapsed = now - this.#lightingPlaybackStart;
|
|
550
|
+
const cycle = (this.#lightingPlaybackBase + (elapsed / this.#lightingPlaybackDurationMs)) % 1;
|
|
551
|
+
this.#draftSettings.lightingTimeOfDay = cycle;
|
|
552
|
+
this.#updateLightingSliderUI(cycle);
|
|
553
|
+
this.#emitApply(true, { persist: false });
|
|
554
|
+
this.#lightingAnimationFrame = requestAnimationFrame(tick);
|
|
555
|
+
};
|
|
556
|
+
this.#lightingAnimationFrame = requestAnimationFrame(tick);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Stops the continuous day/night playback loop.
|
|
561
|
+
* @private
|
|
562
|
+
* @returns {void}
|
|
563
|
+
*/
|
|
564
|
+
#stopLightingPlayback(persist = false) {
|
|
565
|
+
this.#isLightingPlaying = false;
|
|
566
|
+
if (this.#lightingAnimationFrame !== null) {
|
|
567
|
+
cancelAnimationFrame(this.#lightingAnimationFrame);
|
|
568
|
+
this.#lightingAnimationFrame = null;
|
|
569
|
+
}
|
|
570
|
+
if (persist) {
|
|
571
|
+
this.#emitApply(true, { persist: true });
|
|
572
|
+
}
|
|
573
|
+
this.#updateLightingPlayButtonUI();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Refreshes the play button label and pressed state.
|
|
578
|
+
* @private
|
|
579
|
+
* @returns {void}
|
|
580
|
+
*/
|
|
581
|
+
#updateLightingPlayButtonUI() {
|
|
582
|
+
if (!this.#elements.lightingPlayButton) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.#elements.lightingSliderWrapper?.toggleAttribute("data-playing", this.#isLightingPlaying);
|
|
586
|
+
this.#elements.lightingPlayButton.textContent = this.#isLightingPlaying ? (this.#texts.actions.pause || "Pause") : (this.#texts.actions.play || "Play");
|
|
587
|
+
this.#elements.lightingPlayButton.setAttribute("aria-pressed", this.#isLightingPlaying ? "true" : "false");
|
|
588
|
+
}
|
|
589
|
+
|
|
426
590
|
static #hueToHex(hue) {
|
|
427
591
|
const h = ((hue % 360) + 360) % 360;
|
|
428
592
|
const a = Math.min(0.5, 1 - 0.5);
|
|
@@ -445,6 +609,21 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
445
609
|
return Math.round(h * 60);
|
|
446
610
|
}
|
|
447
611
|
|
|
612
|
+
static #formatLightingTime(value) {
|
|
613
|
+
const normalized = Math.min(1, Math.max(0, Number(value) || 0));
|
|
614
|
+
const minutes = Math.round(normalized * 24 * 60) % (24 * 60);
|
|
615
|
+
const hours = Math.floor(minutes / 60);
|
|
616
|
+
const mins = minutes % 60;
|
|
617
|
+
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#clampLightingTime(value) {
|
|
621
|
+
if (!Number.isFinite(value)) {
|
|
622
|
+
return 0.5;
|
|
623
|
+
}
|
|
624
|
+
return Math.min(1, Math.max(0, value));
|
|
625
|
+
}
|
|
626
|
+
|
|
448
627
|
/**
|
|
449
628
|
* Updates the color picker UI (input value, preview, selected button).
|
|
450
629
|
* @private
|
|
@@ -474,18 +653,33 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
474
653
|
}
|
|
475
654
|
}
|
|
476
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Updates the lighting slider value and formatted time label.
|
|
658
|
+
* @private
|
|
659
|
+
* @param {number} value - Normalized 0..1 lighting cycle value.
|
|
660
|
+
* @returns {void}
|
|
661
|
+
*/
|
|
662
|
+
#updateLightingSliderUI(value) {
|
|
663
|
+
if (this.#elements.lightingSlider) {
|
|
664
|
+
this.#elements.lightingSlider.value = Math.round(this.#clampLightingTime(value) * 1000);
|
|
665
|
+
}
|
|
666
|
+
if (this.#elements.lightingValue) {
|
|
667
|
+
this.#elements.lightingValue.textContent = PrefViewerMenu3D.#formatLightingTime(value);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
477
671
|
/**
|
|
478
672
|
* Emits the apply event with the current draft settings when changes are pending.
|
|
479
673
|
* @private
|
|
480
674
|
* @returns {void}
|
|
481
675
|
*/
|
|
482
|
-
#emitApply(force = false) {
|
|
676
|
+
#emitApply(force = false, extraSettings = {}) {
|
|
483
677
|
// For illumination/color changes, we apply immediately regardless of pending state
|
|
484
678
|
if (!force && this.#isApplying) {
|
|
485
679
|
return;
|
|
486
680
|
}
|
|
487
681
|
|
|
488
|
-
const detail = { settings: { ...this.#draftSettings } };
|
|
682
|
+
const detail = { settings: { ...this.#draftSettings, ...extraSettings } };
|
|
489
683
|
const customEventOptions = {
|
|
490
684
|
bubbles: true,
|
|
491
685
|
cancelable: true,
|
|
@@ -535,6 +729,9 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
535
729
|
*/
|
|
536
730
|
#getPendingKeys() {
|
|
537
731
|
return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => {
|
|
732
|
+
if (key === "lightingTimeOfDay") {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
538
735
|
if (this.#elements.switches[key]?.disabled) {
|
|
539
736
|
return false;
|
|
540
737
|
}
|
|
@@ -564,6 +761,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
564
761
|
}
|
|
565
762
|
wrapper.toggleAttribute("data-pending", pendingKeys.includes(key));
|
|
566
763
|
});
|
|
764
|
+
this.#elements.lightingSliderWrapper?.toggleAttribute("data-pending", pendingKeys.includes("lightingTimeOfDay"));
|
|
567
765
|
}
|
|
568
766
|
|
|
569
767
|
/**
|
|
@@ -649,6 +847,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
649
847
|
if (!this.#isEnabled) {
|
|
650
848
|
this.removeAttribute("data-viewer-hover");
|
|
651
849
|
this.#closeMenu();
|
|
850
|
+
this.#stopLightingPlayback();
|
|
652
851
|
}
|
|
653
852
|
}
|
|
654
853
|
|
|
@@ -668,10 +867,14 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
668
867
|
if (this.#draftSettings.highlightColor) {
|
|
669
868
|
this.#updateColorPickerUI(this.#draftSettings.highlightColor);
|
|
670
869
|
}
|
|
870
|
+
if (this.#draftSettings.lightingTimeOfDay !== undefined) {
|
|
871
|
+
this.#updateLightingSliderUI(this.#draftSettings.lightingTimeOfDay);
|
|
872
|
+
}
|
|
671
873
|
// Update illumination color picker if present
|
|
672
874
|
if (this.#draftSettings.illuminationColor) {
|
|
673
875
|
this.#updateColorPickerUI(this.#draftSettings.illuminationColor);
|
|
674
876
|
}
|
|
877
|
+
this.#updateLightingPlayButtonUI();
|
|
675
878
|
|
|
676
879
|
this.setApplying(false);
|
|
677
880
|
this.#setStatusMessage("");
|
package/src/pref-viewer.js
CHANGED
|
@@ -486,7 +486,11 @@ export default class PrefViewer extends HTMLElement {
|
|
|
486
486
|
if (!this.#component3D) {
|
|
487
487
|
return;
|
|
488
488
|
}
|
|
489
|
-
|
|
489
|
+
const { highlightEnabled: _he, highlightColor: _hc, lightingTimeOfDay: _lt, lighting: _lighting, persist: _persist, ...reloadSettings } = settings ?? {};
|
|
490
|
+
const hasReloadSettings = Object.keys(reloadSettings).length > 0;
|
|
491
|
+
if (hasReloadSettings) {
|
|
492
|
+
this.#menu3D?.setApplying(true);
|
|
493
|
+
}
|
|
490
494
|
try {
|
|
491
495
|
const result = await this.#component3D.applyRenderSettings(settings);
|
|
492
496
|
if (!result?.changed) {
|
package/src/styles.js
CHANGED
|
@@ -482,6 +482,123 @@ export const PrefViewerMenu3DStyles = `
|
|
|
482
482
|
opacity: 0.5;
|
|
483
483
|
pointer-events: none;
|
|
484
484
|
}
|
|
485
|
+
|
|
486
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider {
|
|
487
|
+
display: flex;
|
|
488
|
+
flex-direction: column;
|
|
489
|
+
gap: 8px;
|
|
490
|
+
padding: var(--panel-spacing) 0;
|
|
491
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider[data-pending] .lighting-slider-label::after {
|
|
495
|
+
content: "•";
|
|
496
|
+
margin-left: 8px;
|
|
497
|
+
color: var(--accent-color);
|
|
498
|
+
font-size: 1.1em;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-row {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
justify-content: space-between;
|
|
505
|
+
gap: 12px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-row>.lighting-slider-label {
|
|
509
|
+
font-size: var(--font-size-medium);
|
|
510
|
+
font-weight: var(--font-weight-medium);
|
|
511
|
+
color: var(--text-color);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-row>.lighting-slider-value {
|
|
515
|
+
font-size: var(--font-size-small);
|
|
516
|
+
color: var(--muted-color);
|
|
517
|
+
font-variant-numeric: tabular-nums;
|
|
518
|
+
letter-spacing: 0.06em;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider {
|
|
522
|
+
-webkit-appearance: none;
|
|
523
|
+
appearance: none;
|
|
524
|
+
width: 100%;
|
|
525
|
+
height: 10px;
|
|
526
|
+
border-radius: 999px;
|
|
527
|
+
cursor: pointer;
|
|
528
|
+
background:
|
|
529
|
+
linear-gradient(to right,
|
|
530
|
+
rgba(255, 214, 120, 0.95) 0%,
|
|
531
|
+
rgba(255, 179, 71, 0.98) 20%,
|
|
532
|
+
rgba(255, 244, 214, 0.95) 50%,
|
|
533
|
+
rgba(118, 165, 255, 0.92) 80%,
|
|
534
|
+
rgba(56, 66, 115, 0.95) 100%);
|
|
535
|
+
outline: none;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider::-webkit-slider-thumb {
|
|
539
|
+
-webkit-appearance: none;
|
|
540
|
+
width: 18px;
|
|
541
|
+
height: 18px;
|
|
542
|
+
border-radius: 50%;
|
|
543
|
+
background: #ffffff;
|
|
544
|
+
border: 2px solid rgba(0, 0, 0, 0.24);
|
|
545
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
|
|
546
|
+
cursor: pointer;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider::-moz-range-thumb {
|
|
550
|
+
width: 18px;
|
|
551
|
+
height: 18px;
|
|
552
|
+
border-radius: 50%;
|
|
553
|
+
background: #ffffff;
|
|
554
|
+
border: 2px solid rgba(0, 0, 0, 0.24);
|
|
555
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
|
|
556
|
+
cursor: pointer;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-markers {
|
|
560
|
+
display: flex;
|
|
561
|
+
justify-content: space-between;
|
|
562
|
+
font-size: var(--font-size-small);
|
|
563
|
+
color: var(--muted-color);
|
|
564
|
+
letter-spacing: 0.02em;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-actions {
|
|
568
|
+
display: flex;
|
|
569
|
+
justify-content: flex-end;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-actions>.lighting-play-button {
|
|
573
|
+
appearance: none;
|
|
574
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
575
|
+
border-radius: 999px;
|
|
576
|
+
background: rgba(255, 255, 255, 0.08);
|
|
577
|
+
color: var(--text-color);
|
|
578
|
+
padding: 6px 12px;
|
|
579
|
+
font-size: var(--font-size-small);
|
|
580
|
+
font-weight: var(--font-weight-medium);
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-actions>.lighting-play-button:hover,
|
|
586
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider>.lighting-slider-actions>.lighting-play-button:focus-visible {
|
|
587
|
+
background: rgba(255, 255, 255, 0.14);
|
|
588
|
+
border-color: rgba(255, 255, 255, 0.28);
|
|
589
|
+
transform: translateY(-1px);
|
|
590
|
+
outline: none;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-lighting-slider[data-playing] .lighting-slider {
|
|
594
|
+
background:
|
|
595
|
+
linear-gradient(to right,
|
|
596
|
+
rgba(255, 220, 132, 0.96) 0%,
|
|
597
|
+
rgba(255, 187, 88, 0.98) 18%,
|
|
598
|
+
rgba(255, 247, 226, 0.96) 50%,
|
|
599
|
+
rgba(112, 154, 255, 0.92) 82%,
|
|
600
|
+
rgba(45, 55, 102, 0.98) 100%);
|
|
601
|
+
}
|
|
485
602
|
`;
|
|
486
603
|
|
|
487
604
|
export const PrefViewer3DAnimationMenuStyles = `
|