@preference-sl/pref-viewer 2.16.0 → 2.16.1

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.
@@ -25,9 +25,6 @@ import FileStorage from "./file-storage.js";
25
25
  * - show-model: Show or hide the 3D model ("true"/"false").
26
26
  * - show-scene: Show or hide the 3D environment ("true"/"false").
27
27
  * - visible: Show or hide the entire viewer ("true"/"false").
28
- * - show-animation-menu: Show or hide animation controls in the 3D controller.
29
- * - highlight-color: Hex color used for hover highlight on animated meshes.
30
- * - highlight-enabled: Enable or disable hover highlight ("true"/"false").
31
28
  *
32
29
  * Public Methods:
33
30
  * - show(): Shows the 3D viewer component.
@@ -40,6 +37,13 @@ import FileStorage from "./file-storage.js";
40
37
  * - hideModel(): Hides the 3D model.
41
38
  * - showEnvironment(): Shows the 3D environment/scene.
42
39
  * - hideEnvironment(): Hides the 3D environment/scene.
40
+ * - isIBLAvailable(): Reports whether a valid IBL environment map is currently available.
41
+ * - isAnimationMenuEnabled(): Reports whether the animation control menu is currently enabled.
42
+ * - getDebugInfo(): Returns a diagnostic snapshot of the underlying Babylon.js runtime.
43
+ * - getAnimations(): Returns a snapshot of all available animations and their current state.
44
+ * - playAnimation(name, action): Controls a named animation by applying the specified action.
45
+ * - setAnimationProgress(name, progress): Seeks a named animation to a normalized progress position (0–1).
46
+ * - setAnimationLoop(name, loop): Enables or disables loop mode for a named animation.
43
47
  * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
44
48
  * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
45
49
  * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
@@ -60,14 +64,16 @@ import FileStorage from "./file-storage.js";
60
64
  * - #checkNeedToUpdateCamera(options): Detects camera option changes and marks them pending.
61
65
  * - #checkNeedToUpdateMaterials(options): Resolves material overrides that require updates.
62
66
  * - #checkNeedToUpdateIBL(options): Fetches HDR URLs/timestamps and enqueues IBL updates when needed.
67
+ * - #checkNeedToUpdateLighting(options): Resolves time-of-day changes and marks lighting as pending.
63
68
  * - #resetUpdateFlags(): Clears pending/success flags after loads or option-setting flows.
64
69
  * - #onLoading/#onLoaded/#onSettingOptions/#onSetOptions(): Dispatch lifecycle events and synchronize attributes.
65
70
  *
66
71
  * Events:
67
- * - "scene-loading": Dispatched when a loading operation starts.
68
- * - "scene-loaded": Dispatched when a loading operation completes.
69
- * - "scene-setting-options": Dispatched when viewer options are being set.
70
- * - "scene-set-options": Dispatched when viewer options have been set.
72
+ * - "pref-viewer-3d-loading": Dispatched when a loading operation starts. Legacy alias: "scene-loading".
73
+ * - "pref-viewer-3d-loaded": Dispatched when a loading operation completes. Legacy alias: "scene-loaded".
74
+ * - "pref-viewer-3d-options-applying": Dispatched when viewer options are being set. Legacy alias: "scene-setting-options".
75
+ * - "pref-viewer-3d-options-applied": Dispatched when viewer options have been set. Legacy alias: "scene-set-options".
76
+ * - "pref-viewer-3d-animation-changed": Dispatched by OpeningAnimation instances via the pref-viewer-3d host when an animation state, progress, or loop value changes.
71
77
  *
72
78
  * Notes:
73
79
  * - Internal state and heavy initialization are handled in connectedCallback.
@@ -101,7 +107,7 @@ export default class PrefViewer3D extends HTMLElement {
101
107
  * @returns {string[]} Array of attribute names that trigger attributeChangedCallback.
102
108
  */
103
109
  static get observedAttributes() {
104
- return ["show-model", "show-scene", "visible", "show-animation-menu", "highlight-color", "highlight-enabled"];
110
+ return ["show-model", "show-scene", "visible"];
105
111
  }
106
112
 
107
113
  /**
@@ -114,7 +120,6 @@ export default class PrefViewer3D extends HTMLElement {
114
120
  * - "show-model": shows or hides the model according to the attribute value ("true"/"false").
115
121
  * - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
116
122
  * - "visible": shows or hides the component according to the attribute value ("true"/"false").
117
- * - "show-animation-menu", "highlight-color", and "highlight-enabled": forward runtime UI/highlight settings to BabylonJSController.
118
123
  */
119
124
  attributeChangedCallback(name, _old, value) {
120
125
  switch (name) {
@@ -142,18 +147,6 @@ export default class PrefViewer3D extends HTMLElement {
142
147
  this.hide();
143
148
  }
144
149
  break;
145
- case "show-animation-menu":
146
- case "highlight-color":
147
- case "highlight-enabled":
148
- // Pass these to BabylonJSController if initialized
149
- if (this.#babylonJSController && typeof this.#babylonJSController.setIlluminationConfig === "function") {
150
- this.#babylonJSController.setIlluminationConfig({
151
- showAnimationMenu: this.getAttribute("show-animation-menu"),
152
- highlightColor: this.getAttribute("highlight-color"),
153
- highlightEnabled: this.getAttribute("highlight-enabled"),
154
- });
155
- }
156
- break;
157
150
  }
158
151
  }
159
152
 
@@ -234,8 +227,10 @@ export default class PrefViewer3D extends HTMLElement {
234
227
  outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
235
228
  },
236
229
  ibl: new IBLData(),
237
- lighting: {
238
- timeOfDay: 0.5,
230
+ render: {
231
+ showAnimationMenu: false,
232
+ lightingTimeOfDay: 0.5,
233
+ showLightingTimeOfDay: false,
239
234
  },
240
235
  },
241
236
  };
@@ -250,15 +245,6 @@ export default class PrefViewer3D extends HTMLElement {
250
245
  this.#babylonJSController = new BabylonJSController(this.#canvas, this.#data.containers, this.#data.options);
251
246
  this.#babylonJSController.enable();
252
247
 
253
- // Apply highlight attributes that were set before the controller was ready
254
- const highlightColor = this.getAttribute("highlight-color");
255
- const highlightEnabled = this.getAttribute("highlight-enabled");
256
- if (highlightColor !== null || highlightEnabled !== null) {
257
- this.#babylonJSController.setIlluminationConfig({
258
- highlightColor: highlightColor ?? undefined,
259
- highlightEnabled: highlightEnabled ?? undefined,
260
- });
261
- }
262
248
  }
263
249
 
264
250
  /**
@@ -430,29 +416,37 @@ export default class PrefViewer3D extends HTMLElement {
430
416
  iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
431
417
  }
432
418
  }
433
-
419
+
434
420
  return needUpdate;
435
421
  }
436
422
 
437
423
  /**
438
- * Resolves incoming lighting settings (time-of-day slider) and marks the option as pending when changed.
439
- * Supports both nested `options.lighting.timeOfDay` and flat `options.lightingTimeOfDay` payloads so the menu
440
- * and external API can share the same controller path.
424
+ * Checks whether any lighting-related value in `options.render` differs from the cached state.
425
+ * `options.render.showLightingTimeOfDay` updates slider visibility, while
426
+ * `options.render.lightingTimeOfDay` updates the normalized 0..1 lighting value.
441
427
  * @private
442
- * @param {object} options - Options payload containing lighting state.
443
- * @returns {boolean} True when lighting differs from the cached state, otherwise false.
428
+ * @param {object} options - Options payload containing a `render` sub-object.
429
+ * @param {boolean|string} [options.render.showLightingTimeOfDay] - Whether the time-of-day slider should be visible.
430
+ * @param {number} [options.render.lightingTimeOfDay] - Desired time-of-day, normalized to 0..1.
431
+ * @returns {boolean} True when either the visibility flag or the time value differs from the cached state.
444
432
  */
445
433
  #checkNeedToUpdateLighting(options) {
446
434
  if (!options) {
447
435
  return false;
448
436
  }
449
437
 
450
- const lightingState = this.#data.options.lighting;
451
- const incomingTime = typeof options.lightingTimeOfDay === "number"
452
- ? options.lightingTimeOfDay
453
- : typeof options.lighting?.timeOfDay === "number"
454
- ? options.lighting.timeOfDay
455
- : undefined;
438
+ const incomingTimeEnabled = options.render?.showLightingTimeOfDay;
439
+ if (incomingTimeEnabled !== undefined) {
440
+ const dataTimeEnabled = this.#data.options.render.showLightingTimeOfDay;
441
+ const needUpdate = dataTimeEnabled !== incomingTimeEnabled;
442
+ this.#data.options.render.showLightingTimeOfDay = incomingTimeEnabled;
443
+ if (needUpdate) {
444
+ return true;
445
+ }
446
+ }
447
+
448
+ const dataTime = this.#data.options.render.lightingTimeOfDay;
449
+ const incomingTime = typeof options.render?.lightingTimeOfDay === "number" ? options.render.lightingTimeOfDay : undefined;
456
450
 
457
451
  if (!Number.isFinite(incomingTime)) {
458
452
  return false;
@@ -460,9 +454,9 @@ export default class PrefViewer3D extends HTMLElement {
460
454
 
461
455
  const normalizedTime = Math.min(1, Math.max(0, incomingTime));
462
456
  const controllerTime = this.#babylonJSController?.getRenderSettings?.()?.lightingTimeOfDay;
463
- const currentTime = Number.isFinite(controllerTime) ? controllerTime : lightingState.timeOfDay;
457
+ const currentTime = Number.isFinite(controllerTime) ? controllerTime : dataTime;
464
458
  const needUpdate = normalizedTime !== currentTime;
465
- lightingState.timeOfDay = normalizedTime;
459
+ this.#data.options.render.lightingTimeOfDay = normalizedTime;
466
460
  return needUpdate;
467
461
  }
468
462
 
@@ -477,8 +471,10 @@ export default class PrefViewer3D extends HTMLElement {
477
471
  bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
478
472
  cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
479
473
  composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
474
+
480
475
  };
481
- this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
476
+ this.dispatchEvent(new CustomEvent("pref-viewer-3d-loading", customEventOptions));
477
+ this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions)); // Legacy alias for backward compatibility
482
478
 
483
479
  this.removeAttribute("loaded");
484
480
  this.setAttribute("loading", "");
@@ -526,7 +522,8 @@ export default class PrefViewer3D extends HTMLElement {
526
522
  composed: false,
527
523
  detail: detail,
528
524
  };
529
- this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
525
+ this.dispatchEvent(new CustomEvent("pref-viewer-3d-loaded", customEventOptions));
526
+ this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions)); // Legacy alias for backward compatibility
530
527
 
531
528
  this.removeAttribute("loading");
532
529
  this.setAttribute("loaded", "");
@@ -549,7 +546,8 @@ export default class PrefViewer3D extends HTMLElement {
549
546
  cancelable: true,
550
547
  composed: false,
551
548
  };
552
- this.dispatchEvent(new CustomEvent("scene-setting-options", customEventOptions));
549
+ this.dispatchEvent(new CustomEvent("pref-viewer-3d-options-applying", customEventOptions));
550
+ this.dispatchEvent(new CustomEvent("scene-setting-options", customEventOptions)); // Legacy alias for backward compatibility
553
551
 
554
552
  this.removeAttribute("loaded");
555
553
  this.setAttribute("loading", "");
@@ -591,7 +589,8 @@ export default class PrefViewer3D extends HTMLElement {
591
589
  composed: false,
592
590
  detail: detail,
593
591
  };
594
- this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions));
592
+ this.dispatchEvent(new CustomEvent("pref-viewer-3d-options-applied", customEventOptions));
593
+ this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions)); // Legacy alias for backward compatibility
595
594
 
596
595
  this.removeAttribute("loading");
597
596
  this.setAttribute("loaded", "");
@@ -648,10 +647,15 @@ export default class PrefViewer3D extends HTMLElement {
648
647
  this.#checkNeedToUpdateContainers(config);
649
648
 
650
649
  // Options
651
- let needUpdateLighting = false;
652
650
  const iblOptions = config.scene?.ibl
653
- ? { ...(config.options ?? {}), ibl: { ...(config.options?.ibl ?? {}), ...config.scene.ibl } }
654
- : config.options;
651
+ ? { ...(config.options ?? {}), ibl: { ...(config.options?.ibl ?? {}), ...config.scene.ibl } }
652
+ : config.options;
653
+
654
+ if (iblOptions) {
655
+ await this.#checkNeedToUpdateIBL(iblOptions);
656
+ }
657
+
658
+ let needUpdateLighting = false;
655
659
 
656
660
  if (config.options) {
657
661
  this.#checkNeedToUpdateCamera(config.options);
@@ -659,16 +663,15 @@ export default class PrefViewer3D extends HTMLElement {
659
663
  needUpdateLighting = this.#checkNeedToUpdateLighting(config.options);
660
664
  }
661
665
 
662
- if (iblOptions) {
663
- await this.#checkNeedToUpdateIBL(iblOptions);
664
- }
665
-
666
- const loadDetail = await this.#babylonJSController.load(forceReload);
667
-
668
666
  if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
669
- this.#babylonJSController.setIlluminationConfig({
670
- lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
671
- });
667
+ const lightingConfig = {};
668
+ if (config.options?.render?.lightingTimeOfDay !== undefined) {
669
+ lightingConfig.lightingTimeOfDay = config.options.render.lightingTimeOfDay;
670
+ }
671
+ if (config.options?.render?.showLightingTimeOfDay !== undefined) {
672
+ lightingConfig.showLightingTimeOfDay = config.options.render.showLightingTimeOfDay;
673
+ }
674
+ this.#babylonJSController.setIlluminationConfig(lightingConfig);
672
675
  }
673
676
 
674
677
  // Apply initial render settings if provided in options
@@ -676,6 +679,8 @@ export default class PrefViewer3D extends HTMLElement {
676
679
  await this.applyRenderSettings(config.options.render);
677
680
  }
678
681
 
682
+ const loadDetail = await this.#babylonJSController.load(forceReload);
683
+
679
684
  return { ...loadDetail, load: this.#onLoaded() };
680
685
  }
681
686
 
@@ -706,17 +711,22 @@ export default class PrefViewer3D extends HTMLElement {
706
711
  }
707
712
  const needUpdateLighting = this.#checkNeedToUpdateLighting(options);
708
713
  if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
709
- this.#babylonJSController.setIlluminationConfig({
710
- lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
711
- });
714
+ const lightingConfig = {};
715
+ if (options.render?.lightingTimeOfDay !== undefined) {
716
+ lightingConfig.lightingTimeOfDay = options.render.lightingTimeOfDay;
717
+ }
718
+ if (options.render?.showLightingTimeOfDay !== undefined) {
719
+ lightingConfig.showLightingTimeOfDay = options.render.showLightingTimeOfDay;
720
+ }
721
+ this.#babylonJSController.setIlluminationConfig(lightingConfig);
712
722
  someSetted = true;
713
723
  }
714
-
724
+
715
725
  // Apply render settings if provided
716
726
  if (options.render) {
717
727
  await this.applyRenderSettings(options.render);
718
728
  }
719
-
729
+
720
730
  const detail = this.#onSetOptions();
721
731
  return { success: someSetted, detail: detail };
722
732
  }
@@ -949,6 +959,38 @@ export default class PrefViewer3D extends HTMLElement {
949
959
  return typeof cachedUrl === "string" ? cachedUrl.length > 0 : Boolean(cachedUrl);
950
960
  }
951
961
 
962
+ /**
963
+ * Reports whether the animation control menu is currently enabled.
964
+ * @public
965
+ * @returns {boolean} True when the animation menu option is active; otherwise false.
966
+ */
967
+ isAnimationMenuEnabled() {
968
+ const renderSettings = this.getRenderSettings?.() ?? null;
969
+ if (renderSettings?.showAnimationMenu !== undefined) {
970
+ return renderSettings.showAnimationMenu === true;
971
+ }
972
+ return false;
973
+ }
974
+
975
+ /**
976
+ * Reports whether the lighting time-of-day control should be visible and interactive.
977
+ * Requires both `showLightingTimeOfDay === true` and a finite `lightingTimeOfDay` value.
978
+ * @public
979
+ * @returns {boolean} True when the time-of-day slider is explicitly visible and has a valid value; otherwise false.
980
+ */
981
+ isLightingTimeOfDayAvailable() {
982
+ const renderSettings = this.getRenderSettings?.() ?? null;
983
+ return renderSettings?.showLightingTimeOfDay === true && Number.isFinite(renderSettings.lightingTimeOfDay);
984
+ }
985
+
986
+ /**
987
+ * Returns a diagnostic snapshot of the underlying Babylon.js runtime.
988
+ * @public
989
+ * @returns {object|null} Debug information, or null when the controller is not initialized.
990
+ */
991
+ getDebugInfo() {
992
+ return this.#babylonJSController?.getDebugInfo?.() ?? null;
993
+ }
952
994
  /**
953
995
  * Applies render settings received from PrefViewerMenu3D or another UI.
954
996
  * Highlight and time-of-day lighting values are pushed directly to BabylonJSController; AA/SSAO/IBL/shadow changes
@@ -959,8 +1001,10 @@ export default class PrefViewer3D extends HTMLElement {
959
1001
  * @param {boolean} [settings.ambientOcclusionEnabled] - Enables or disables SSAO.
960
1002
  * @param {boolean} [settings.iblEnabled] - Enables or disables image-based lighting.
961
1003
  * @param {boolean} [settings.shadowsEnabled] - Enables or disables scene shadows.
1004
+ * @param {boolean|string} [settings.showAnimationMenu] - Enables/disables the animation controls when animations are available.
962
1005
  * @param {boolean|string} [settings.highlightEnabled] - Enables or disables hover highlight; string "false" is normalized to false.
963
1006
  * @param {string} [settings.highlightColor] - Hex color used for hover highlight.
1007
+ * @param {boolean|string} [settings.showLightingTimeOfDay] - Shows/hides the lighting time-of-day control.
964
1008
  * @param {number} [settings.lightingTimeOfDay] - Normalized 0..1 time-of-day value.
965
1009
  * @param {boolean} [settings.persist=true] - False for transient lighting updates such as playback frames.
966
1010
  * @returns {Promise<{changed: boolean, success: boolean, detail?: object, controller?: object}>} Resolves with the apply/reload summary.
@@ -970,30 +1014,41 @@ export default class PrefViewer3D extends HTMLElement {
970
1014
  return { changed: false, success: false };
971
1015
  }
972
1016
 
973
- // Handle highlight settings separately for immediate effect without reload
974
- if (settings.highlightEnabled !== undefined || settings.highlightColor !== undefined) {
975
- if (typeof this.#babylonJSController.setIlluminationConfig === "function") {
976
- // Convert string "false" to boolean false
977
- const highlightEnabled = settings.highlightEnabled === "false" ? false : settings.highlightEnabled;
978
- this.#babylonJSController.setIlluminationConfig({
979
- highlightEnabled: highlightEnabled,
980
- highlightColor: settings.highlightColor,
981
- });
982
- }
1017
+ const illuminationSettings = {};
1018
+ if (settings.showAnimationMenu !== undefined) {
1019
+ illuminationSettings.showAnimationMenu = settings.showAnimationMenu;
983
1020
  }
984
-
985
- // Exclude highlight settings from the reload — they are handled above without requiring a scene reload
986
- if (settings.lightingTimeOfDay !== undefined && typeof this.#babylonJSController.setIlluminationConfig === "function") {
1021
+ if (settings.highlightEnabled !== undefined) {
1022
+ illuminationSettings.highlightEnabled = settings.highlightEnabled;
1023
+ }
1024
+ if (settings.highlightColor !== undefined) {
1025
+ illuminationSettings.highlightColor = settings.highlightColor;
1026
+ }
1027
+ if (settings.showLightingTimeOfDay !== undefined) {
1028
+ illuminationSettings.showLightingTimeOfDay = settings.showLightingTimeOfDay;
1029
+ }
1030
+ if (settings.lightingTimeOfDay !== undefined) {
987
1031
  const lightingTimeOfDay = Number(settings.lightingTimeOfDay);
988
1032
  if (Number.isFinite(lightingTimeOfDay)) {
989
- this.#babylonJSController.setIlluminationConfig({
990
- lightingTimeOfDay,
991
- persist: settings.persist !== false,
992
- });
1033
+ illuminationSettings.lightingTimeOfDay = lightingTimeOfDay;
993
1034
  }
994
1035
  }
1036
+ if (settings.persist !== undefined) {
1037
+ illuminationSettings.persist = settings.persist;
1038
+ }
1039
+ if (Object.keys(illuminationSettings).length > 0 && typeof this.#babylonJSController.setIlluminationConfig === "function") {
1040
+ this.#babylonJSController.setIlluminationConfig(illuminationSettings);
1041
+ }
995
1042
 
996
- const { highlightEnabled: _he, highlightColor: _hc, lightingTimeOfDay: _lt, lighting: _lighting, persist: _persist, ...reloadSettings } = settings;
1043
+ const {
1044
+ showAnimationMenu: _sam,
1045
+ highlightEnabled: _he,
1046
+ highlightColor: _hc,
1047
+ showLightingTimeOfDay: _slt,
1048
+ lightingTimeOfDay: _lt,
1049
+ persist: _persist,
1050
+ ...reloadSettings
1051
+ } = settings;
997
1052
  const scheduled = this.#babylonJSController.scheduleRenderSettingsReload(reloadSettings);
998
1053
  if (!scheduled.changed) {
999
1054
  return { changed: false, success: true };
@@ -10,7 +10,7 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
10
10
  * - A hue slider for the highlight color.
11
11
  * - A time-of-day slider with play/pause continuous lighting-cycle animation.
12
12
  *
13
- * Every interaction emits `pref-viewer-menu-3d-apply` immediately; there is no Apply button or
13
+ * Every interaction emits `pref-viewer-3d-render-settings-change-requested` immediately; there is no Apply button or
14
14
  * draft/pending state. Switch changes emit the full settings snapshot, while highlight color and
15
15
  * lighting changes emit partial payloads to avoid unnecessary scene reloads.
16
16
  *
@@ -19,13 +19,13 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
19
19
  * - `setApplying(isApplying)`: locks interactions while an async apply operation is running.
20
20
  * - `setViewerHover(isHovering)`: drives panel visibility from the 3D viewport hover state.
21
21
  * - `setEnabled(isEnabled)`: hides the menu when 3D mode is unavailable.
22
- * - `setSwitchEnabled(settingKey, isEnabled)`: enables/disables a specific toggle at runtime.
22
+ * - `setElementEnabled(settingKey, isEnabled)`: enables/disables a specific switch or slider at runtime.
23
+ * - `setElementVisible(settingKey, isVisible)`: shows/hides a specific switch or slider at runtime.
23
24
  * - `setCulture(cultureId)`: switches the display locale.
24
25
  *
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.
26
+ * Observed attributes: `culture`. Runtime render values are synchronized through `setSettings()`.
27
27
  *
28
- * Events emitted: `pref-viewer-menu-3d-apply` - `{ detail: { settings: object } }`.
28
+ * Events emitted: `pref-viewer-3d-render-settings-change-requested` - `{ detail: { settings: object } }`.
29
29
  */
30
30
  export default class PrefViewerMenu3D extends HTMLElement {
31
31
  #elements = {
@@ -41,6 +41,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
41
41
  lightingSlider: null,
42
42
  lightingValue: null,
43
43
  panel: null,
44
+ sliders: {},
45
+ sliderWrappers: {},
44
46
  subtitle: null,
45
47
  switches: {},
46
48
  switchCopyNodes: {},
@@ -65,7 +67,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
65
67
  typeof this.#DEFAULT_RENDER_SETTINGS[key] === "boolean"
66
68
  );
67
69
  #settings = { ...this.#DEFAULT_RENDER_SETTINGS };
68
- #switchAvailability = {};
70
+ #elementAvailability = {};
71
+ #elementVisibility = {};
69
72
  #isApplying = false;
70
73
  #isEnabled = true;
71
74
  #isLightingPlaying = false;
@@ -92,35 +95,22 @@ export default class PrefViewerMenu3D extends HTMLElement {
92
95
  * @returns {string[]} Array of attribute names to observe.
93
96
  */
94
97
  static get observedAttributes() {
95
- return ["culture", "show-illumination", "illumination-color", "lighting-time-of-day"];
98
+ return ["culture"];
96
99
  }
97
100
 
98
101
  /**
99
102
  * Reacts to observed attribute mutations and mirrors them into the internal settings/UI state.
100
103
  * @public
101
- * @param {"culture"|"show-illumination"|"illumination-color"|"lighting-time-of-day"} name - Attribute name that changed.
104
+ * @param {"culture"} name - Attribute name that changed.
102
105
  * @param {string|null} _old - Previous attribute value (unused).
103
106
  * @param {string|null} value - New attribute value.
104
107
  * @returns {void}
105
108
  */
106
109
  attributeChangedCallback(name, _old, value) {
107
- if (name === "culture") {
108
- this.#setCultureInternal(value || DEFAULT_LOCALE);
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;
114
- }
115
- } else if (name === "illumination-color") {
116
- this.#settings.illuminationColor = value;
117
- this.#updateColorPickerPreview(value);
118
- } else if (name === "lighting-time-of-day") {
119
- const parsed = Number(value);
120
- if (Number.isFinite(parsed)) {
121
- this.#settings.lightingTimeOfDay = this.#clampLightingTime(parsed);
122
- this.#updateLightingSliderUI(this.#settings.lightingTimeOfDay);
123
- }
110
+ switch (name) {
111
+ case "culture":
112
+ this.#setCultureInternal(value || DEFAULT_LOCALE);
113
+ break;
124
114
  }
125
115
  }
126
116
 
@@ -171,19 +161,19 @@ export default class PrefViewerMenu3D extends HTMLElement {
171
161
  <div class="menu-switches">
172
162
  ${this.#MENU_SWITCHES.map((key) => this.#renderSwitchRow(key, this.#texts.features)).join("")}
173
163
  </div>
174
- <div class="menu-color-picker" data-setting="highlightColor">
164
+ <div class="menu-color-picker menu-slider" data-setting="highlightColor">
175
165
  <div class="color-slider-row">
176
166
  <span class="color-picker-label">${this.#texts.features?.highlightColor?.label || "Highlight Color"}</span>
177
167
  <span class="color-preview" style="background-color: ${this.#settings.highlightColor || '#ff6700'}"></span>
178
168
  </div>
179
- <input type="range" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#settings.highlightColor || '#ff6700')}">
169
+ <input type="range" data-setting="highlightColor" class="color-slider" min="0" max="359" value="${PrefViewerMenu3D.#hexToHue(this.#settings.highlightColor || '#ff6700')}">
180
170
  </div>
181
- <div class="menu-lighting-slider" data-setting="lightingTimeOfDay">
171
+ <div class="menu-lighting-slider menu-slider" data-setting="lightingTimeOfDay">
182
172
  <div class="lighting-slider-row">
183
- <span class="lighting-slider-label">${this.#texts.features?.lightingTimeOfDay?.label || "Time of day"}</span>
173
+ <span class="lighting-slider-label menu-slider-label">${this.#texts.features?.lightingTimeOfDay?.label || "Time of day"}</span>
184
174
  <span class="lighting-slider-value">${PrefViewerMenu3D.#formatLightingTime(this.#settings.lightingTimeOfDay ?? 0.5)}</span>
185
175
  </div>
186
- <input type="range" class="lighting-slider" min="0" max="1000" step="1" value="${Math.round((this.#settings.lightingTimeOfDay ?? 0.5) * 1000)}">
176
+ <input type="range" data-setting="lightingTimeOfDay" class="lighting-slider" min="0" max="1000" step="1" value="${Math.round((this.#settings.lightingTimeOfDay ?? 0.5) * 1000)}">
187
177
  <div class="lighting-slider-markers" aria-hidden="true">
188
178
  <span class="lighting-slider-start">${this.#texts.features?.lightingTimeOfDay?.startLabel || "Night"}</span>
189
179
  <span class="lighting-slider-mid">${this.#texts.features?.lightingTimeOfDay?.midLabel || "Day"}</span>
@@ -325,7 +315,8 @@ export default class PrefViewerMenu3D extends HTMLElement {
325
315
  this.#elements.panel = this.querySelector(".menu-panel");
326
316
  this.#elements.title = this.querySelector(".menu-title");
327
317
  this.#elements.subtitle = this.querySelector(".menu-subtitle");
328
- // Cache color picker elements
318
+
319
+ // Cache slider elements
329
320
  this.#elements.colorPicker = this.querySelector(".menu-color-picker");
330
321
  this.#elements.colorSlider = this.querySelector(".color-slider");
331
322
  this.#elements.colorPickerPreview = this.querySelector(".color-preview");
@@ -337,6 +328,11 @@ export default class PrefViewerMenu3D extends HTMLElement {
337
328
  this.#elements.lightingSliderEnd = this.querySelector(".lighting-slider-end");
338
329
  this.#elements.lightingSlider = this.querySelector(".lighting-slider");
339
330
  this.#elements.lightingValue = this.querySelector(".lighting-slider-value");
331
+
332
+ this.#elements.sliders["highlightColor"] = this.querySelector("input[data-setting='highlightColor']");
333
+ this.#elements.sliderWrappers["highlightColor"] = this.querySelector(".menu-slider[data-setting='highlightColor']");
334
+ this.#elements.sliders["lightingTimeOfDay"] = this.querySelector("input[data-setting='lightingTimeOfDay']");
335
+ this.#elements.sliderWrappers["lightingTimeOfDay"] = this.querySelector(".menu-slider[data-setting='lightingTimeOfDay']");
340
336
 
341
337
  this.#MENU_SWITCHES.forEach((key) => {
342
338
  this.#elements.switches[key] = this.querySelector(`input[data-setting="${key}"]`);
@@ -668,12 +664,13 @@ export default class PrefViewerMenu3D extends HTMLElement {
668
664
  return;
669
665
  }
670
666
  const detail = { settings: options.partial ? { ...extraSettings } : { ...this.#settings, ...extraSettings } };
671
- this.dispatchEvent(new CustomEvent("pref-viewer-menu-3d-apply", {
667
+ const eventOptions = {
672
668
  bubbles: true,
673
669
  cancelable: true,
674
670
  composed: true,
675
671
  detail,
676
- }));
672
+ };
673
+ this.dispatchEvent(new CustomEvent("pref-viewer-3d-render-settings-change-requested", eventOptions));
677
674
  }
678
675
 
679
676
  /**
@@ -684,10 +681,29 @@ export default class PrefViewerMenu3D extends HTMLElement {
684
681
  #updateSwitches() {
685
682
  Object.entries(this.#elements.switches).forEach(([key, input]) => {
686
683
  if (input) {
687
- const isEnabled = this.#switchAvailability[key] !== false;
684
+ const isEnabled = this.#elementAvailability[key] !== false;
685
+ const isVisible = this.#elementVisibility[key] !== false;
688
686
  input.checked = Boolean(this.#settings[key]);
689
687
  input.disabled = !isEnabled;
690
688
  this.#elements.switchWrappers[key]?.toggleAttribute("data-disabled", !isEnabled);
689
+ this.#elements.switchWrappers[key]?.toggleAttribute("data-hidden", !isVisible);
690
+ }
691
+ });
692
+ }
693
+
694
+ /**
695
+ * Synchronizes slider states with the current settings snapshot.
696
+ * @private
697
+ * @returns {void}
698
+ */
699
+ #updateSliders() {
700
+ Object.entries(this.#elements.sliders).forEach(([key, input]) => {
701
+ if (input) {
702
+ const isEnabled = this.#elementAvailability[key] !== false;
703
+ const isVisible = this.#elementVisibility[key] !== false;
704
+ input.disabled = !isEnabled;
705
+ this.#elements.sliderWrappers[key]?.toggleAttribute("data-disabled", !isEnabled);
706
+ this.#elements.sliderWrappers[key]?.toggleAttribute("data-hidden", !isVisible);
691
707
  }
692
708
  });
693
709
  }
@@ -754,6 +770,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
754
770
  if (this.#settings.lightingTimeOfDay !== undefined) {
755
771
  this.#updateLightingSliderUI(this.#settings.lightingTimeOfDay);
756
772
  }
773
+ this.#updateSliders();
757
774
  this.#updateLightingPlayButtonUI();
758
775
  this.setApplying(false);
759
776
  }
@@ -777,17 +794,44 @@ export default class PrefViewerMenu3D extends HTMLElement {
777
794
  }
778
795
 
779
796
  /**
780
- * Enables or disables a specific render-setting switch in the menu.
797
+ * Enables or disables a specific render-setting switch or slider in the menu.
781
798
  * @public
782
- * @param {string} settingKey - Switch key (e.g. "iblEnabled").
799
+ * @param {string} settingKey - Setting key (e.g. "iblEnabled", "highlightColor", or "lightingTimeOfDay").
783
800
  * @param {boolean} [isEnabled=true] - True to allow interaction, false to disable it.
784
801
  * @returns {void}
785
802
  */
786
- setSwitchEnabled(settingKey, isEnabled = true) {
787
- if (!settingKey || !this.#MENU_SWITCHES.includes(settingKey)) {
803
+ setElementEnabled(settingKey, isEnabled = true) {
804
+ if (!settingKey) {
788
805
  return;
789
806
  }
790
- this.#switchAvailability[settingKey] = Boolean(isEnabled);
791
- this.#updateSwitches();
807
+
808
+ if (this.#MENU_SWITCHES.includes(settingKey)) {
809
+ this.#elementAvailability[settingKey] = Boolean(isEnabled);
810
+ this.#updateSwitches();
811
+ } else if (this.#elements.sliders[settingKey]) {
812
+ this.#elementAvailability[settingKey] = Boolean(isEnabled);
813
+ this.#updateSliders();
814
+ }
815
+ }
816
+
817
+ /**
818
+ * Shows or hides a specific render-setting switch or slider in the menu.
819
+ * @public
820
+ * @param {string} settingKey - Setting key (e.g. "iblEnabled", "highlightColor", or "lightingTimeOfDay").
821
+ * @param {boolean} [isVisible=true] - True to show the switch, false to hide it.
822
+ * @returns {void}
823
+ */
824
+ setElementVisible(settingKey, isVisible = true) {
825
+ if (!settingKey) {
826
+ return;
827
+ }
828
+
829
+ if (this.#MENU_SWITCHES.includes(settingKey)) {
830
+ this.#elementVisibility[settingKey] = Boolean(isVisible);
831
+ this.#updateSwitches();
832
+ } else if (this.#elements.sliders[settingKey]) {
833
+ this.#elementVisibility[settingKey] = Boolean(isVisible);
834
+ this.#updateSliders();
835
+ }
792
836
  }
793
837
  }