@preference-sl/pref-viewer 2.16.0-beta.1 → 2.16.0-beta.10

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.
@@ -66,7 +66,6 @@ export default class PrefViewer extends HTMLElement {
66
66
  #cultureList = SUPPORTED_LOCALES;
67
67
  #culture = "";
68
68
  #cultureSuppressAttribute = false;
69
- #suppressVisibilityAttributeCallback = false;
70
69
 
71
70
  // References to internal components
72
71
  #wrapper = null;
@@ -121,9 +120,6 @@ export default class PrefViewer extends HTMLElement {
121
120
  * @returns {void}
122
121
  */
123
122
  attributeChangedCallback(name, _old, value) {
124
- if (this.#suppressVisibilityAttributeCallback && (name === "show-model" || name === "show-scene")) {
125
- return;
126
- }
127
123
  switch (name) {
128
124
  case "config":
129
125
  this.loadConfig(value);
@@ -480,10 +476,10 @@ export default class PrefViewer extends HTMLElement {
480
476
  }
481
477
 
482
478
  /**
483
- * Applies render toggles via PrefViewer3D, showing progress in the menu and resyncing on completion.
484
- * When no changes occur, the menu is simply refreshed to match the controller snapshot.
479
+ * Applies render settings via PrefViewer3D.
480
+ * Reload-capable toggles lock the menu while they run; partial highlight/lighting updates are forwarded immediately.
485
481
  * @private
486
- * @param {object} settings - Partial render settings (AA, SSAO, IBL, shadows) requested by the menu.
482
+ * @param {object} settings - Partial render settings requested by the menu.
487
483
  * @returns {Promise<void>}
488
484
  */
489
485
  async #applyRenderSettings(settings) {
@@ -631,47 +627,20 @@ export default class PrefViewer extends HTMLElement {
631
627
  /**
632
628
  * Handles the completion of a 3D loading operation.
633
629
  * Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
634
- * Dispatches granular container events ("model-loaded", "environment-loaded", "materials-loaded")
635
- * for each successfully loaded container before firing "scene-loaded".
636
- * The independent model path may additionally emit "model-first-painted" when the first painted
637
- * frame is acknowledged by the Babylon controller.
638
630
  * @private
639
631
  * @param {object} [detail] - Optional details to include in the event.
640
- * @param {string[]} [detail.loadedContainers] - Names of containers that loaded successfully.
641
632
  * @returns {void}
642
633
  */
643
634
  #on3DLoaded(detail) {
644
635
  this.#isLoaded = true;
645
636
  this.#isLoading = false;
646
-
637
+
647
638
  this.#menu3DSyncSettings();
648
639
  this.#menu3DUpdateSwitchAvailability();
649
640
 
650
641
  this.removeAttribute("loading-3d");
651
642
  this.setAttribute("loaded-3d", "");
652
643
 
653
- // Dispatch granular container events before the aggregate "scene-loaded".
654
- // This lets consumers react as soon as individual assets are ready without
655
- // waiting for the full scene-loaded signal.
656
- const granularEventMap = {
657
- model: "model-loaded",
658
- environment: "environment-loaded",
659
- materials: "materials-loaded",
660
- };
661
- if (Array.isArray(detail?.loadedContainers)) {
662
- const granularEventOptions = {
663
- bubbles: true,
664
- cancelable: false,
665
- composed: true,
666
- };
667
- detail.loadedContainers.forEach((containerName) => {
668
- const eventName = granularEventMap[containerName];
669
- if (eventName) {
670
- this.dispatchEvent(new CustomEvent(eventName, granularEventOptions));
671
- }
672
- });
673
- }
674
-
675
644
  const customEventOptions = {
676
645
  bubbles: true,
677
646
  cancelable: true,
@@ -1152,78 +1121,6 @@ export default class PrefViewer extends HTMLElement {
1152
1121
  this.#addTaskToQueue(model, "model");
1153
1122
  }
1154
1123
 
1155
- /**
1156
- * Loads the model container in parallel with any ongoing scene load, bypassing the task queue.
1157
- * Dispatches "model-loading", "model-first-painted", and "model-loaded" events directly.
1158
- * @public
1159
- * @param {object|string} model - Model payload with storage pointer.
1160
- * @returns {Promise<void>}
1161
- */
1162
- async loadModelParallel(model) {
1163
- model = typeof model === "string" ? JSON.parse(model) : model;
1164
- if (!model || !this.#component3D) return;
1165
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1166
- this.dispatchEvent(new CustomEvent("model-loading", eventOptions));
1167
- try {
1168
- const detail = await this.#component3D.loadModelIndependent(model);
1169
- if (detail?.success) {
1170
- this.dispatchEvent(new CustomEvent("model-loaded", { ...eventOptions, detail }));
1171
- } else {
1172
- this.dispatchEvent(new CustomEvent("model-error", { ...eventOptions, detail }));
1173
- }
1174
- } catch (error) {
1175
- this.dispatchEvent(new CustomEvent("model-error", { ...eventOptions, detail: { error } }));
1176
- }
1177
- }
1178
-
1179
- /**
1180
- * Loads the environment/scene container in parallel with any ongoing model load, bypassing the
1181
- * task queue. Dispatches "scene-loading" and "scene-loaded" (or "scene-error") events directly.
1182
- * @public
1183
- * @param {object|string} scene - Scene payload: { storage, ibl?, visible? }
1184
- * @returns {Promise<void>}
1185
- */
1186
- async loadSceneParallel(scene) {
1187
- scene = typeof scene === "string" ? JSON.parse(scene) : scene;
1188
- if (!scene || !this.#component3D) return;
1189
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1190
- this.dispatchEvent(new CustomEvent("scene-loading", eventOptions));
1191
- try {
1192
- const detail = await this.#component3D.loadSceneIndependent(scene);
1193
- if (detail?.success) {
1194
- this.dispatchEvent(new CustomEvent("scene-loaded", { ...eventOptions, detail }));
1195
- } else {
1196
- this.dispatchEvent(new CustomEvent("scene-error", { ...eventOptions, detail }));
1197
- }
1198
- } catch (error) {
1199
- this.dispatchEvent(new CustomEvent("scene-error", { ...eventOptions, detail: { error } }));
1200
- }
1201
- }
1202
-
1203
- /**
1204
- * Loads the materials container in parallel with any ongoing model or environment load,
1205
- * bypassing the task queue. Dispatches "materials-loading" and "materials-loaded" events.
1206
- * @public
1207
- * @param {object|string} materials - Materials payload: { storage }
1208
- * @returns {Promise<void>}
1209
- */
1210
- async loadMaterialsParallel(materials) {
1211
- materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1212
- if (!materials || !this.#component3D) return;
1213
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1214
- this.dispatchEvent(new CustomEvent("materials-loading", eventOptions));
1215
- try {
1216
- const detail = await this.#component3D.loadMaterialsIndependent(materials);
1217
- if (detail?.success) {
1218
- this.dispatchEvent(new CustomEvent("materials-loaded", { ...eventOptions, detail }));
1219
- } else {
1220
- this.dispatchEvent(new CustomEvent("materials-error", { ...eventOptions, detail }));
1221
- }
1222
- } catch (error) {
1223
- this.dispatchEvent(new CustomEvent("materials-error", { ...eventOptions, detail: { error } }));
1224
- }
1225
- }
1226
-
1227
1124
  /**
1228
1125
  * Loads a scene/environment object or JSON string and adds it to the task queue.
1229
1126
  * @public
@@ -1368,31 +1265,6 @@ export default class PrefViewer extends HTMLElement {
1368
1265
  this.#addTaskToQueue(config, "visibility");
1369
1266
  }
1370
1267
 
1371
- /**
1372
- * Reflects the current visibility of a container without feeding the task queue.
1373
- * Used by the Babylon controller to keep host attributes in sync with renderer state.
1374
- * @public
1375
- * @param {string} name - Container name ("model" or "environment").
1376
- * @param {boolean} isVisible - Whether the container should be reflected as visible.
1377
- * @returns {void}
1378
- */
1379
- syncVisibility(name, isVisible) {
1380
- const attrName = name === "model" ? "show-model" : name === "environment" ? "show-scene" : null;
1381
- if (!attrName) {
1382
- return;
1383
- }
1384
- const nextValue = isVisible ? "true" : "false";
1385
- if (this.getAttribute(attrName) === nextValue) {
1386
- return;
1387
- }
1388
- this.#suppressVisibilityAttributeCallback = true;
1389
- try {
1390
- this.setAttribute(attrName, nextValue);
1391
- } finally {
1392
- this.#suppressVisibilityAttributeCallback = false;
1393
- }
1394
- }
1395
-
1396
1268
  /**
1397
1269
  * Centers the 2D drawing view in the viewer.
1398
1270
  * @public
@@ -1647,22 +1519,4 @@ export default class PrefViewer extends HTMLElement {
1647
1519
  get isMode3D() {
1648
1520
  return this.#mode === "3d";
1649
1521
  }
1650
-
1651
- /**
1652
- * Forces the 3D camera to frame the current model, when supported.
1653
- * @public
1654
- * @returns {boolean}
1655
- */
1656
- focusModel() {
1657
- return this.#component3D?.focusModel?.() ?? false;
1658
- }
1659
-
1660
- /**
1661
- * Exposes a debug snapshot of the 3D runtime when available.
1662
- * @public
1663
- * @returns {object|null}
1664
- */
1665
- getDebugState() {
1666
- return this.#component3D?.getDebugState?.() ?? null;
1667
- }
1668
1522
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pure math utilities used by the lighting cycle and hardware scaling logic.
3
+ * No Babylon.js imports — safe to use in Node/SSR environments and in tests without mocks.
4
+ */
5
+
6
+ /**
7
+ * Clamps a number to the [0, 1] range.
8
+ * Returns 0.5 for non-finite inputs (NaN, ±Infinity) so callers get a safe mid-point fallback.
9
+ * @param {number} value - The value to clamp.
10
+ * @returns {number} A number in [0, 1].
11
+ */
12
+ export function clamp01(value) {
13
+ if (!Number.isFinite(value)) {
14
+ return 0.5;
15
+ }
16
+ return Math.min(1, Math.max(0, value));
17
+ }
18
+
19
+ /**
20
+ * Linearly interpolates between two values.
21
+ * The interpolation amount is clamped to [0, 1] before use.
22
+ * @param {number} from - Start value (returned when amount ≤ 0).
23
+ * @param {number} to - End value (returned when amount ≥ 1).
24
+ * @param {number} amount - Blend factor in [0, 1].
25
+ * @returns {number} Interpolated value between `from` and `to`.
26
+ */
27
+ export function lerp(from, to, amount) {
28
+ const t = clamp01(amount);
29
+ return from + (to - from) * t;
30
+ }
31
+
32
+ /**
33
+ * Evaluates a bell-curve (half sine arc) over [0, 1].
34
+ * Returns 0 at both endpoints and 1 at the midpoint (x = 0.5).
35
+ * Used to model the intensity rise and fall of sunlight across the day.
36
+ * @param {number} x - Input in [0, 1]; clamped internally.
37
+ * @returns {number} Bell-curve output in [0, 1].
38
+ */
39
+ function bellCurve(x) {
40
+ const clamped = clamp01(x);
41
+ return Math.sin(clamped * Math.PI);
42
+ }
43
+
44
+ /**
45
+ * Returns a unit vector for the given (x, y, z) direction.
46
+ * Falls back to the original vector with length 1 when the input is the zero vector.
47
+ * @param {number} x
48
+ * @param {number} y
49
+ * @param {number} z
50
+ * @returns {{x: number, y: number, z: number}} Normalized direction vector.
51
+ */
52
+ function normalizeDirection(x, y, z) {
53
+ const length = Math.hypot(x, y, z) || 1;
54
+ return {
55
+ x: x / length,
56
+ y: y / length,
57
+ z: z / length,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Computes the Babylon.js `hardwareScalingLevel` needed to render at the requested base
63
+ * resolution on a HiDPI display.
64
+ *
65
+ * Babylon's hardware scaling is the inverse of the device pixel ratio: a level of 0.5 on a
66
+ * 2× display renders at native resolution. This helper applies the base scaling factor (which
67
+ * may already downscale for performance on low-end hardware) on top of the device pixel ratio.
68
+ *
69
+ * Both inputs are validated and fall back to 1 when zero, negative, or non-finite.
70
+ * @param {number} [baseScaling=1] - Desired base scaling level (1 = full resolution, >1 = downscaled).
71
+ * @param {number} [pixelRatio=1] - Device pixel ratio (`window.devicePixelRatio`).
72
+ * @returns {number} Hardware scaling level to pass to `engine.setHardwareScalingLevel()`.
73
+ */
74
+ export function getAdaptiveHardwareScalingLevel(baseScaling = 1, pixelRatio = 1) {
75
+ const safeBaseScaling = Number.isFinite(baseScaling) && baseScaling > 0 ? baseScaling : 1;
76
+ const safePixelRatio = Number.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : 1;
77
+ return safeBaseScaling / safePixelRatio;
78
+ }
79
+
80
+ /**
81
+ * Computes a lighting profile for a given time of day, used to drive the scene's directional
82
+ * lights, environment intensity, and tonemapping curve.
83
+ *
84
+ * The model follows a simplified solar arc: sunrise at 06:15 and sunset at 20:15 (fractional
85
+ * hours). Day factor peaks at solar noon and falls to zero outside daylight hours. Sun and moon
86
+ * directions are antipodal unit vectors derived from the sun phase angle.
87
+ * @param {number} [timeOfDay=0.5] - Normalized time in [0, 1] where 0 = midnight and 1 = next midnight.
88
+ * Values outside [0, 1] are clamped.
89
+ * @returns {{
90
+ * timeOfDay: number,
91
+ * timeHours: number,
92
+ * sunPhase: number,
93
+ * solarAzimuth: number,
94
+ * solarElevation: number,
95
+ * dayFactor: number,
96
+ * nightFactor: number,
97
+ * sunDirection: {x: number, y: number, z: number},
98
+ * moonDirection: {x: number, y: number, z: number}
99
+ * }} Lighting profile snapshot for the requested time.
100
+ */
101
+ export function getLightingCycleProfile(timeOfDay = 0.5) {
102
+ const normalizedTime = clamp01(timeOfDay);
103
+ const timeHours = normalizedTime * 24;
104
+ const sunriseHour = 6.25;
105
+ const sunsetHour = 20.25;
106
+ const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
107
+ const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
108
+ const nightFactor = 1 - dayFactor;
109
+ const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
110
+ const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
111
+ const solarElevation = lerp(-0.18, 1.18, solarArc);
112
+ const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
113
+ const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
114
+ const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
115
+
116
+ return {
117
+ timeOfDay: normalizedTime,
118
+ timeHours,
119
+ sunPhase,
120
+ solarAzimuth,
121
+ solarElevation,
122
+ dayFactor,
123
+ nightFactor,
124
+ sunDirection,
125
+ moonDirection,
126
+ };
127
+ }