@preference-sl/pref-viewer 2.16.0-beta.9 → 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.
@@ -10,14 +10,15 @@ import FileStorage from "./file-storage.js";
10
10
  * - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
11
11
  * - Manages internal state for containers (model, environment, materials) and options (camera, materials, IBL, render toggles).
12
12
  * - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
13
- * - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can persistently toggle AA/SSAO/IBL/shadows.
13
+ * - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can toggle AA/SSAO/IBL/shadows,
14
+ * hover highlight, and time-of-day lighting.
14
15
  * - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
15
16
  * - Emits custom events for loading, loaded, option-setting, and render-setting workflows.
16
17
  *
17
18
  * Usage:
18
19
  * - Use as a custom HTML element: <pref-viewer-3d ...>
19
20
  * - Configure via attributes (config, options, show-model, show-scene, visible).
20
- * - Call getRenderSettings()/applyRenderSettings() to synchronize UI toggles with BabylonJSController persistence.
21
+ * - Call getRenderSettings()/applyRenderSettings() to synchronize UI controls with BabylonJSController settings.
21
22
  * - Control viewer state and assets via public methods.
22
23
  *
23
24
  * Observed Attributes:
@@ -30,12 +31,19 @@ import FileStorage from "./file-storage.js";
30
31
  * - hide(): Hides the 3D viewer component.
31
32
  * - load(config): Loads the provided configuration into the viewer.
32
33
  * - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
33
- * - getRenderSettings(): Returns the current anti-aliasing, SSAO, IBL, and shadow flags.
34
- * - applyRenderSettings(settings): Persists incoming render toggles, reloads if needed, and reports the result.
34
+ * - getRenderSettings(): Returns the current render settings snapshot, including highlight and lighting values.
35
+ * - applyRenderSettings(settings): Applies render settings, reloading only when a setting requires it.
35
36
  * - showModel(): Shows the 3D model.
36
37
  * - hideModel(): Hides the 3D model.
37
38
  * - showEnvironment(): Shows the 3D environment/scene.
38
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.
39
47
  * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
40
48
  * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
41
49
  * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
@@ -56,14 +64,16 @@ import FileStorage from "./file-storage.js";
56
64
  * - #checkNeedToUpdateCamera(options): Detects camera option changes and marks them pending.
57
65
  * - #checkNeedToUpdateMaterials(options): Resolves material overrides that require updates.
58
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.
59
68
  * - #resetUpdateFlags(): Clears pending/success flags after loads or option-setting flows.
60
69
  * - #onLoading/#onLoaded/#onSettingOptions/#onSetOptions(): Dispatch lifecycle events and synchronize attributes.
61
70
  *
62
71
  * Events:
63
- * - "scene-loading": Dispatched when a loading operation starts.
64
- * - "scene-loaded": Dispatched when a loading operation completes.
65
- * - "scene-setting-options": Dispatched when viewer options are being set.
66
- * - "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.
67
77
  *
68
78
  * Notes:
69
79
  * - Internal state and heavy initialization are handled in connectedCallback.
@@ -97,7 +107,7 @@ export default class PrefViewer3D extends HTMLElement {
97
107
  * @returns {string[]} Array of attribute names that trigger attributeChangedCallback.
98
108
  */
99
109
  static get observedAttributes() {
100
- return ["show-model", "show-scene", "visible", "show-animation-menu", "highlight-color", "highlight-enabled"];
110
+ return ["show-model", "show-scene", "visible"];
101
111
  }
102
112
 
103
113
  /**
@@ -137,18 +147,6 @@ export default class PrefViewer3D extends HTMLElement {
137
147
  this.hide();
138
148
  }
139
149
  break;
140
- case "show-animation-menu":
141
- case "highlight-color":
142
- case "highlight-enabled":
143
- // Pass these to BabylonJSController if initialized
144
- if (this.#babylonJSController && typeof this.#babylonJSController.setIlluminationConfig === "function") {
145
- this.#babylonJSController.setIlluminationConfig({
146
- showAnimationMenu: this.getAttribute("show-animation-menu"),
147
- highlightColor: this.getAttribute("highlight-color"),
148
- highlightEnabled: this.getAttribute("highlight-enabled"),
149
- });
150
- }
151
- break;
152
150
  }
153
151
  }
154
152
 
@@ -229,8 +227,10 @@ export default class PrefViewer3D extends HTMLElement {
229
227
  outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
230
228
  },
231
229
  ibl: new IBLData(),
232
- lighting: {
233
- timeOfDay: 0.5,
230
+ render: {
231
+ showAnimationMenu: false,
232
+ lightingTimeOfDay: 0.5,
233
+ showLightingTimeOfDay: false,
234
234
  },
235
235
  },
236
236
  };
@@ -245,15 +245,6 @@ export default class PrefViewer3D extends HTMLElement {
245
245
  this.#babylonJSController = new BabylonJSController(this.#canvas, this.#data.containers, this.#data.options);
246
246
  this.#babylonJSController.enable();
247
247
 
248
- // Apply highlight attributes that were set before the controller was ready
249
- const highlightColor = this.getAttribute("highlight-color");
250
- const highlightEnabled = this.getAttribute("highlight-enabled");
251
- if (highlightColor !== null || highlightEnabled !== null) {
252
- this.#babylonJSController.setIlluminationConfig({
253
- highlightColor: highlightColor ?? undefined,
254
- highlightEnabled: highlightEnabled ?? undefined,
255
- });
256
- }
257
248
  }
258
249
 
259
250
  /**
@@ -425,29 +416,37 @@ export default class PrefViewer3D extends HTMLElement {
425
416
  iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
426
417
  }
427
418
  }
428
-
419
+
429
420
  return needUpdate;
430
421
  }
431
422
 
432
423
  /**
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.
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.
436
427
  * @private
437
- * @param {object} options - Options payload containing lighting state.
438
- * @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.
439
432
  */
440
433
  #checkNeedToUpdateLighting(options) {
441
434
  if (!options) {
442
435
  return false;
443
436
  }
444
437
 
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;
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;
451
450
 
452
451
  if (!Number.isFinite(incomingTime)) {
453
452
  return false;
@@ -455,9 +454,9 @@ export default class PrefViewer3D extends HTMLElement {
455
454
 
456
455
  const normalizedTime = Math.min(1, Math.max(0, incomingTime));
457
456
  const controllerTime = this.#babylonJSController?.getRenderSettings?.()?.lightingTimeOfDay;
458
- const currentTime = Number.isFinite(controllerTime) ? controllerTime : lightingState.timeOfDay;
457
+ const currentTime = Number.isFinite(controllerTime) ? controllerTime : dataTime;
459
458
  const needUpdate = normalizedTime !== currentTime;
460
- lightingState.timeOfDay = normalizedTime;
459
+ this.#data.options.render.lightingTimeOfDay = normalizedTime;
461
460
  return needUpdate;
462
461
  }
463
462
 
@@ -472,8 +471,10 @@ export default class PrefViewer3D extends HTMLElement {
472
471
  bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
473
472
  cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
474
473
  composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
474
+
475
475
  };
476
- 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
477
478
 
478
479
  this.removeAttribute("loaded");
479
480
  this.setAttribute("loading", "");
@@ -521,7 +522,8 @@ export default class PrefViewer3D extends HTMLElement {
521
522
  composed: false,
522
523
  detail: detail,
523
524
  };
524
- 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
525
527
 
526
528
  this.removeAttribute("loading");
527
529
  this.setAttribute("loaded", "");
@@ -544,7 +546,8 @@ export default class PrefViewer3D extends HTMLElement {
544
546
  cancelable: true,
545
547
  composed: false,
546
548
  };
547
- 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
548
551
 
549
552
  this.removeAttribute("loaded");
550
553
  this.setAttribute("loading", "");
@@ -586,7 +589,8 @@ export default class PrefViewer3D extends HTMLElement {
586
589
  composed: false,
587
590
  detail: detail,
588
591
  };
589
- 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
590
594
 
591
595
  this.removeAttribute("loading");
592
596
  this.setAttribute("loaded", "");
@@ -643,10 +647,15 @@ export default class PrefViewer3D extends HTMLElement {
643
647
  this.#checkNeedToUpdateContainers(config);
644
648
 
645
649
  // Options
646
- let needUpdateLighting = false;
647
650
  const iblOptions = config.scene?.ibl
648
- ? { ...(config.options ?? {}), ibl: { ...(config.options?.ibl ?? {}), ...config.scene.ibl } }
649
- : 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;
650
659
 
651
660
  if (config.options) {
652
661
  this.#checkNeedToUpdateCamera(config.options);
@@ -654,16 +663,15 @@ export default class PrefViewer3D extends HTMLElement {
654
663
  needUpdateLighting = this.#checkNeedToUpdateLighting(config.options);
655
664
  }
656
665
 
657
- if (iblOptions) {
658
- await this.#checkNeedToUpdateIBL(iblOptions);
659
- }
660
-
661
- const loadDetail = await this.#babylonJSController.load(forceReload);
662
-
663
666
  if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
664
- this.#babylonJSController.setIlluminationConfig({
665
- lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
666
- });
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);
667
675
  }
668
676
 
669
677
  // Apply initial render settings if provided in options
@@ -671,6 +679,8 @@ export default class PrefViewer3D extends HTMLElement {
671
679
  await this.applyRenderSettings(config.options.render);
672
680
  }
673
681
 
682
+ const loadDetail = await this.#babylonJSController.load(forceReload);
683
+
674
684
  return { ...loadDetail, load: this.#onLoaded() };
675
685
  }
676
686
 
@@ -701,17 +711,22 @@ export default class PrefViewer3D extends HTMLElement {
701
711
  }
702
712
  const needUpdateLighting = this.#checkNeedToUpdateLighting(options);
703
713
  if (needUpdateLighting && typeof this.#babylonJSController.setIlluminationConfig === "function") {
704
- this.#babylonJSController.setIlluminationConfig({
705
- lightingTimeOfDay: this.#data.options.lighting.timeOfDay,
706
- });
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);
707
722
  someSetted = true;
708
723
  }
709
-
724
+
710
725
  // Apply render settings if provided
711
726
  if (options.render) {
712
727
  await this.applyRenderSettings(options.render);
713
728
  }
714
-
729
+
715
730
  const detail = this.#onSetOptions();
716
731
  return { success: someSetted, detail: detail };
717
732
  }
@@ -874,8 +889,9 @@ export default class PrefViewer3D extends HTMLElement {
874
889
 
875
890
  /**
876
891
  * Returns the current render settings used by the BabylonJSController.
892
+ * The snapshot is suitable for hydrating PrefViewerMenu3D controls.
877
893
  * @public
878
- * @returns {object|null} A shallow copy of the render settings or null if unavailable.
894
+ * @returns {object|null} A shallow copy of the render settings, including quality toggles, highlight state, and lighting time, or null if unavailable.
879
895
  */
880
896
  getRenderSettings() {
881
897
  if (!this.#babylonJSController) {
@@ -944,40 +960,95 @@ export default class PrefViewer3D extends HTMLElement {
944
960
  }
945
961
 
946
962
  /**
947
- * Applies render settings that require reloading the Babylon.js scene.
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.
948
988
  * @public
949
- * @param {object} settings - Partial settings for anti-aliasing, SSAO, IBL, and shadows.
950
- * @returns {Promise<object>} Resolves with the reload summary.
989
+ * @returns {object|null} Debug information, or null when the controller is not initialized.
990
+ */
991
+ getDebugInfo() {
992
+ return this.#babylonJSController?.getDebugInfo?.() ?? null;
993
+ }
994
+ /**
995
+ * Applies render settings received from PrefViewerMenu3D or another UI.
996
+ * Highlight and time-of-day lighting values are pushed directly to BabylonJSController; AA/SSAO/IBL/shadow changes
997
+ * are scheduled through the render-settings reload path when needed.
998
+ * @public
999
+ * @param {object} settings - Partial render settings payload.
1000
+ * @param {boolean} [settings.antiAliasingEnabled] - Enables or disables anti-aliasing.
1001
+ * @param {boolean} [settings.ambientOcclusionEnabled] - Enables or disables SSAO.
1002
+ * @param {boolean} [settings.iblEnabled] - Enables or disables image-based lighting.
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.
1005
+ * @param {boolean|string} [settings.highlightEnabled] - Enables or disables hover highlight; string "false" is normalized to false.
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.
1008
+ * @param {number} [settings.lightingTimeOfDay] - Normalized 0..1 time-of-day value.
1009
+ * @param {boolean} [settings.persist=true] - False for transient lighting updates such as playback frames.
1010
+ * @returns {Promise<{changed: boolean, success: boolean, detail?: object, controller?: object}>} Resolves with the apply/reload summary.
951
1011
  */
952
1012
  async applyRenderSettings(settings) {
953
1013
  if (!this.#babylonJSController) {
954
1014
  return { changed: false, success: false };
955
1015
  }
956
1016
 
957
- // Handle highlight settings separately for immediate effect without reload
958
- if (settings.highlightEnabled !== undefined || settings.highlightColor !== undefined) {
959
- if (typeof this.#babylonJSController.setIlluminationConfig === "function") {
960
- // Convert string "false" to boolean false
961
- const highlightEnabled = settings.highlightEnabled === "false" ? false : settings.highlightEnabled;
962
- this.#babylonJSController.setIlluminationConfig({
963
- highlightEnabled: highlightEnabled,
964
- highlightColor: settings.highlightColor,
965
- });
966
- }
1017
+ const illuminationSettings = {};
1018
+ if (settings.showAnimationMenu !== undefined) {
1019
+ illuminationSettings.showAnimationMenu = settings.showAnimationMenu;
967
1020
  }
968
-
969
- // Exclude highlight settings from the reload — they are handled above without requiring a scene reload
970
- 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) {
971
1031
  const lightingTimeOfDay = Number(settings.lightingTimeOfDay);
972
1032
  if (Number.isFinite(lightingTimeOfDay)) {
973
- this.#babylonJSController.setIlluminationConfig({
974
- lightingTimeOfDay,
975
- persist: settings.persist !== false,
976
- });
1033
+ illuminationSettings.lightingTimeOfDay = lightingTimeOfDay;
977
1034
  }
978
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
+ }
979
1042
 
980
- 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;
981
1052
  const scheduled = this.#babylonJSController.scheduleRenderSettingsReload(reloadSettings);
982
1053
  if (!scheduled.changed) {
983
1054
  return { changed: false, success: true };