@preference-sl/pref-viewer 2.17.0-beta.0 → 2.17.0-beta.2

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.
@@ -1,6 +1,6 @@
1
1
  import PrefViewerTask from "./pref-viewer-task.js";
2
2
  import { PREF_VIEWER_STYLES_STORAGE_KEY, PrefViewerStyles, PrefViewerStyleVariables } from "./styles.js";
3
- import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./localization/i18n.js";
3
+ import I18n from "./localization/i18n.js";
4
4
 
5
5
  /**
6
6
  * PrefViewer - Custom Web Component for immersive 2D/3D product visualization with localization, persistence, and download tooling.
@@ -26,7 +26,7 @@ import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./l
26
26
  * - config: Configuration descriptor with assets, culture/styles and optional boolean showRenderMenu/showViewCube flags.
27
27
  * - model / scene / materials / drawing: Independent asset entry points (URLs, Base64, indexed data) for each subsystem.
28
28
  * - options: JSON describing render toggles (AA, SSAO, IBL, shadows), animation menu, highlight, lighting, and misc viewer knobs.
29
- * - culture: Locale identifier (RFC-5646 style) that resolves against `SUPPORTED_LOCALES` before applying.
29
+ * - culture: Locale identifier (RFC-5646 style) that resolves against `I18n.SUPPORTED_LOCALES` before applying.
30
30
  * - mode: Switches between `2d` and `3d`, automatically hiding/showing the relevant subcomponents and menu states.
31
31
  * - show-model / show-scene: Boolean flags to toggle visibility without reloading the underlying resources.
32
32
  * - show-render-menu: Boolean flag to show or hide the render-settings menu (default: true).
@@ -95,8 +95,8 @@ export default class PrefViewer extends HTMLElement {
95
95
 
96
96
  // Localization/culture management
97
97
  #CULTURE_STORAGE_KEY = "pref-viewer/culture";
98
- #cultureInitial = DEFAULT_LOCALE;
99
- #cultureList = SUPPORTED_LOCALES;
98
+ #cultureInitial = I18n.DEFAULT_LOCALE;
99
+ #cultureList = I18n.SUPPORTED_LOCALES;
100
100
  #culture = "";
101
101
  #cultureSuppressAttribute = false;
102
102
 
@@ -117,6 +117,7 @@ export default class PrefViewer extends HTMLElement {
117
117
  #handlers = {
118
118
  on2DZoomChanged: null,
119
119
  on2DElementClick: null,
120
+ on3DElementClick: null,
120
121
  onMenuApply: null,
121
122
  onViewerHoverStart: null,
122
123
  onViewerHoverEnd: null,
@@ -135,9 +136,9 @@ export default class PrefViewer extends HTMLElement {
135
136
  this.attachShadow({ mode: "open" });
136
137
  const storedCulture = this.#cultureReadPersisted();
137
138
  if (storedCulture) {
138
- this.#culture = resolveLocale(storedCulture);
139
+ this.#culture = I18n.resolveLocale(storedCulture);
139
140
  } else {
140
- this.#culture = setLocale(this.#cultureInitial);
141
+ this.#culture = I18n.setLocale(this.#cultureInitial);
141
142
  }
142
143
 
143
144
  this.#applyStyles(this.#stylesReadPersisted(), { persist: false, applyCssVariables: false });
@@ -281,6 +282,7 @@ export default class PrefViewer extends HTMLElement {
281
282
  this.#component2D.remove();
282
283
  }
283
284
  if (this.#component3D) {
285
+ this.#component3D.removeEventListener("pref-viewer-3d-element-click", this.#handlers.on3DElementClick);
284
286
  this.#component3D.removeEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
285
287
  this.#component3D.removeEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
286
288
  this.#component3D.removeEventListener("view-cube-layout", this.#handlers.onViewCubeLayout);
@@ -355,6 +357,7 @@ export default class PrefViewer extends HTMLElement {
355
357
  * @returns {void}
356
358
  */
357
359
  #createComponent3D() {
360
+ this.#handlers.on3DElementClick = this.#on3DElementClick.bind(this);
358
361
  this.#component3D = document.createElement("pref-viewer-3d");
359
362
  this.#component3D.setAttribute("visible", "false");
360
363
  this.#component3D.setAttribute("show-view-cube", this.getAttribute("show-view-cube") === "false" ? "false" : "true");
@@ -375,6 +378,7 @@ export default class PrefViewer extends HTMLElement {
375
378
  this.#handlers.on3DSceneError = () => {
376
379
  this.#menu3D?.setApplying(false, true);
377
380
  };
381
+ this.#component3D.addEventListener("pref-viewer-3d-element-click", this.#handlers.on3DElementClick);
378
382
  this.#component3D.addEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
379
383
  this.#component3D.addEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
380
384
  this.#wrapper.appendChild(this.#component3D);
@@ -668,14 +672,14 @@ export default class PrefViewer extends HTMLElement {
668
672
  return changed;
669
673
  }
670
674
  const { reflectAttribute = false } = options;
671
- const resolved = resolveLocale(cultureId);
675
+ const resolved = I18n.resolveLocale(cultureId);
672
676
  changed = resolved !== this.#culture;
673
677
  if (reflectAttribute) {
674
678
  this.#cultureReflectAttribute(resolved);
675
679
  }
676
680
  if (changed) {
677
681
  this.#culture = resolved;
678
- setLocale(resolved);
682
+ I18n.setLocale(resolved);
679
683
  this.#menu3D?.setCulture?.(resolved);
680
684
  this.#component3D?.refreshViewCube?.();
681
685
  this.#culturePersist(resolved);
@@ -782,6 +786,7 @@ export default class PrefViewer extends HTMLElement {
782
786
 
783
787
  this.removeAttribute("loading-3d");
784
788
  this.setAttribute("loaded-3d", "");
789
+ this.#applySelectionStateTo3D();
785
790
 
786
791
  this.#dispatchPrefViewerEvent("pref-viewer-3d-loaded", detail ?? {});
787
792
  this.#dispatchPrefViewerEvent("scene-loaded", detail ?? {}); // Legacy event for backward compatibility
@@ -893,12 +898,23 @@ export default class PrefViewer extends HTMLElement {
893
898
  this.notifyElementClick(event.detail?.ids ?? []);
894
899
  }
895
900
 
901
+ /**
902
+ * Forwards semantic click ids from the encapsulated 3D component through the root event contract.
903
+ * @private
904
+ * @param {CustomEvent<{ids: string[]}>} event - Internal 3D model click event.
905
+ * @returns {void}
906
+ */
907
+ #on3DElementClick(event) {
908
+ event.stopPropagation();
909
+ this.notifyElementClick(event.detail?.ids ?? []);
910
+ }
911
+
896
912
  /**
897
913
  * Handles the custom "pref-viewer-3d-render-settings-change-requested" event emitted by the menu component.
898
914
  * Forwards the requested settings to the render-application pipeline.
899
915
  * @private
900
916
  * @param {CustomEvent} event - Menu event containing the `settings` payload.
901
- * @returns {void}
917
+ * @returns {Promise<void>}
902
918
  */
903
919
  async #onMenuApply(event) {
904
920
  event.stopPropagation();
@@ -966,7 +982,7 @@ export default class PrefViewer extends HTMLElement {
966
982
  * Processes the next task when finished.
967
983
  * @private
968
984
  * @param {object} drawing - The drawing object to process.
969
- * @returns {void}
985
+ * @returns {Promise<void>}
970
986
  */
971
987
  async #processDrawing(drawing) {
972
988
  if (!this.#component2D) {
@@ -1028,7 +1044,7 @@ export default class PrefViewer extends HTMLElement {
1028
1044
  * Dispatches loading events and processes the next task.
1029
1045
  * @private
1030
1046
  * @param {object} options - The options object to process.
1031
- * @returns {void}
1047
+ * @returns {Promise<void>}
1032
1048
  */
1033
1049
  async #processOptions(options) {
1034
1050
  if (!this.#component3D) {
@@ -1129,7 +1145,7 @@ export default class PrefViewer extends HTMLElement {
1129
1145
  * Non-string and empty-string results are filtered out.
1130
1146
  * @private
1131
1147
  * @param {Array<string|{id?: string, Id?: string}>} value - Array of id-like items to normalize.
1132
- * @returns {string[]} Deduplicated, non-empty string ids.
1148
+ * @returns {string[]} Non-empty string ids, preserving input order and duplicate occurrences.
1133
1149
  */
1134
1150
  #normalizeIdList(value) {
1135
1151
  if (!Array.isArray(value)) {
@@ -1180,11 +1196,31 @@ export default class PrefViewer extends HTMLElement {
1180
1196
  this.#dispatchPrefViewerEvent("pref-viewer-selection-state-changed", this.getSelectionState());
1181
1197
  }
1182
1198
 
1183
- /** Apply the current semantic snapshot to the 2D renderer when available. */
1199
+ /** Apply the current semantic snapshot to the 2D renderer when available. * @returns {void}
1200
+ */
1184
1201
  #applySelectionStateTo2D() {
1185
1202
  this.#component2D?.applySelectionState?.(this.getSelectionState());
1186
1203
  }
1187
1204
 
1205
+ /**
1206
+ * Applies the current semantic snapshot to the 3D renderer when available.
1207
+ * @private
1208
+ * @returns {void}
1209
+ */
1210
+ #applySelectionStateTo3D() {
1211
+ this.#component3D?.applySelectionState?.(this.getSelectionState());
1212
+ }
1213
+
1214
+ /**
1215
+ * Synchronizes the current semantic selection snapshot with both renderers.
1216
+ * @private
1217
+ * @returns {void}
1218
+ */
1219
+ #applySelectionState() {
1220
+ this.#applySelectionStateTo2D();
1221
+ this.#applySelectionStateTo3D();
1222
+ }
1223
+
1188
1224
  /**
1189
1225
  * Dispatches a `pref-viewer-3d-turnable-changed` event carrying the current turnable snapshot.
1190
1226
  * Called after `startTurnable`, `stopTurnable`, or `toggleTurnable` mutates the internal state.
@@ -1505,7 +1541,7 @@ export default class PrefViewer extends HTMLElement {
1505
1541
  */
1506
1542
  setSelectableElements(elements = []) {
1507
1543
  this.#selectionState.selectableIds = this.#normalizeIdList(elements);
1508
- this.#applySelectionStateTo2D();
1544
+ this.#applySelectionState();
1509
1545
  this.#emitSelectionStateChanged();
1510
1546
  return this.getSelectionState();
1511
1547
  }
@@ -1520,7 +1556,7 @@ export default class PrefViewer extends HTMLElement {
1520
1556
  const selectedItems = this.#normalizeSelectionItems(elements);
1521
1557
  this.#selectionState.selectedItems = selectedItems;
1522
1558
  this.#selectionState.selectedIds = selectedItems.map((item) => item.id);
1523
- this.#applySelectionStateTo2D();
1559
+ this.#applySelectionState();
1524
1560
  this.#emitSelectionStateChanged();
1525
1561
  return this.getSelectionState();
1526
1562
  }
@@ -1536,7 +1572,7 @@ export default class PrefViewer extends HTMLElement {
1536
1572
  const selectedItems = this.#normalizeSelectionItems(state.selectedItems ?? state.selectedIds ?? state.selected ?? []);
1537
1573
  this.#selectionState.selectedItems = selectedItems;
1538
1574
  this.#selectionState.selectedIds = selectedItems.map((item) => item.id);
1539
- this.#applySelectionStateTo2D();
1575
+ this.#applySelectionState();
1540
1576
  this.#emitSelectionStateChanged();
1541
1577
  return this.getSelectionState();
1542
1578
  }
@@ -1549,7 +1585,7 @@ export default class PrefViewer extends HTMLElement {
1549
1585
  clearSelectedElements() {
1550
1586
  this.#selectionState.selectedIds = [];
1551
1587
  this.#selectionState.selectedItems = [];
1552
- this.#applySelectionStateTo2D();
1588
+ this.#applySelectionState();
1553
1589
  this.#emitSelectionStateChanged();
1554
1590
  return this.getSelectionState();
1555
1591
  }
@@ -1561,7 +1597,7 @@ export default class PrefViewer extends HTMLElement {
1561
1597
  */
1562
1598
  clearSelectableElements() {
1563
1599
  this.#selectionState.selectableIds = [];
1564
- this.#applySelectionStateTo2D();
1600
+ this.#applySelectionState();
1565
1601
  this.#emitSelectionStateChanged();
1566
1602
  return this.getSelectionState();
1567
1603
  }
@@ -1577,7 +1613,7 @@ export default class PrefViewer extends HTMLElement {
1577
1613
  selectedIds: [],
1578
1614
  selectedItems: [],
1579
1615
  };
1580
- this.#applySelectionStateTo2D();
1616
+ this.#applySelectionState();
1581
1617
  this.#emitSelectionStateChanged();
1582
1618
  return this.getSelectionState();
1583
1619
  }
@@ -1643,7 +1679,7 @@ export default class PrefViewer extends HTMLElement {
1643
1679
  * Remove every SVG node previously added through appendSvgNode.
1644
1680
  * @public
1645
1681
  * @returns {void}
1646
- * */
1682
+ */
1647
1683
  removeAppendedSvgNodes() {
1648
1684
  this.#component2D?.removeAppendedSvgNodes?.();
1649
1685
  }
@@ -1,127 +1,140 @@
1
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.
2
+ * Pure lighting and hardware-scaling calculations without Babylon.js dependencies.
3
+ * @class RenderMath
4
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;
5
+ export default class RenderMath {
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
+ * @public
10
+ * @static
11
+ * @param {number} value - The value to clamp.
12
+ * @returns {number} A number in [0, 1].
13
+ */
14
+ static clamp01(value) {
15
+ if (!Number.isFinite(value)) {
16
+ return 0.5;
17
+ }
18
+ return Math.min(1, Math.max(0, value));
15
19
  }
16
- return Math.min(1, Math.max(0, value));
17
- }
18
20
 
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
- }
21
+ /**
22
+ * Linearly interpolates between two values.
23
+ * The interpolation amount is clamped to [0, 1] before use.
24
+ * @public
25
+ * @static
26
+ * @param {number} from - Start value (returned when amount ≤ 0).
27
+ * @param {number} to - End value (returned when amount ≥ 1).
28
+ * @param {number} amount - Blend factor in [0, 1].
29
+ * @returns {number} Interpolated value between `from` and `to`.
30
+ */
31
+ static lerp(from, to, amount) {
32
+ const t = RenderMath.clamp01(amount);
33
+ return from + (to - from) * t;
34
+ }
31
35
 
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
- }
36
+ /**
37
+ * Evaluates a bell-curve (half sine arc) over [0, 1].
38
+ * Returns 0 at both endpoints and 1 at the midpoint (x = 0.5).
39
+ * Used to model the intensity rise and fall of sunlight across the day.
40
+ * @private
41
+ * @static
42
+ * @param {number} x - Input in [0, 1]; clamped internally.
43
+ * @returns {number} Bell-curve output in [0, 1].
44
+ */
45
+ static #bellCurve(x) {
46
+ const clamped = RenderMath.clamp01(x);
47
+ return Math.sin(clamped * Math.PI);
48
+ }
43
49
 
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
- }
50
+ /**
51
+ * Returns a unit vector for the given (x, y, z) direction.
52
+ * Leaves the zero vector unchanged by using a divisor of 1 when its length is zero.
53
+ * @private
54
+ * @static
55
+ * @param {number} x
56
+ * @param {number} y
57
+ * @param {number} z
58
+ * @returns {{x: number, y: number, z: number}} Normalized direction vector.
59
+ */
60
+ static #normalizeDirection(x, y, z) {
61
+ const length = Math.hypot(x, y, z) || 1;
62
+ return {
63
+ x: x / length,
64
+ y: y / length,
65
+ z: z / length,
66
+ };
67
+ }
60
68
 
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
- }
69
+ /**
70
+ * Computes the Babylon.js `hardwareScalingLevel` needed to render at the requested base
71
+ * resolution on a HiDPI display.
72
+ *
73
+ * Babylon's hardware scaling is the inverse of the device pixel ratio: a level of 0.5 on a
74
+ * 2× display renders at native resolution. This helper applies the base scaling factor (which
75
+ * may already downscale for performance on low-end hardware) on top of the device pixel ratio.
76
+ *
77
+ * Both inputs are validated and fall back to 1 when zero, negative, or non-finite.
78
+ * @public
79
+ * @static
80
+ * @param {number} [baseScaling=1] - Desired base scaling level (1 = full resolution, >1 = downscaled).
81
+ * @param {number} [pixelRatio=1] - Device pixel ratio (`window.devicePixelRatio`).
82
+ * @returns {number} Hardware scaling level to pass to `engine.setHardwareScalingLevel()`.
83
+ */
84
+ static getAdaptiveHardwareScalingLevel(baseScaling = 1, pixelRatio = 1) {
85
+ const safeBaseScaling = Number.isFinite(baseScaling) && baseScaling > 0 ? baseScaling : 1;
86
+ const safePixelRatio = Number.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : 1;
87
+ return safeBaseScaling / safePixelRatio;
88
+ }
79
89
 
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);
90
+ /**
91
+ * Computes a lighting profile for a given time of day, used to drive the scene's directional
92
+ * lights, environment intensity, and tonemapping curve.
93
+ *
94
+ * The model follows a simplified solar arc: sunrise at 06:15 and sunset at 20:15 (fractional
95
+ * hours). Day factor peaks at solar noon and falls to zero outside daylight hours. Sun and moon
96
+ * directions are antipodal unit vectors derived from the sun phase angle.
97
+ * @public
98
+ * @static
99
+ * @param {number} [timeOfDay=0.5] - Normalized time in [0, 1] where 0 = midnight and 1 = next midnight.
100
+ * Values outside [0, 1] are clamped.
101
+ * @returns {{
102
+ * timeOfDay: number,
103
+ * timeHours: number,
104
+ * sunPhase: number,
105
+ * solarAzimuth: number,
106
+ * solarElevation: number,
107
+ * dayFactor: number,
108
+ * nightFactor: number,
109
+ * sunDirection: {x: number, y: number, z: number},
110
+ * moonDirection: {x: number, y: number, z: number}
111
+ * }} Lighting profile snapshot for the requested time.
112
+ */
113
+ static getLightingCycleProfile(timeOfDay = 0.5) {
114
+ const normalizedTime = RenderMath.clamp01(timeOfDay);
115
+ const timeHours = normalizedTime * 24;
116
+ const sunriseHour = 6.25;
117
+ const sunsetHour = 20.25;
118
+ const daylightProgress = RenderMath.clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
119
+ const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(RenderMath.#bellCurve(daylightProgress), 0.88) : 0;
120
+ const nightFactor = 1 - dayFactor;
121
+ const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
122
+ const solarAzimuth = RenderMath.lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
123
+ const solarElevation = RenderMath.lerp(-0.18, 1.18, solarArc);
124
+ const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
125
+ const sunDirection = RenderMath.#normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
126
+ const moonDirection = RenderMath.#normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
115
127
 
116
- return {
117
- timeOfDay: normalizedTime,
118
- timeHours,
119
- sunPhase,
120
- solarAzimuth,
121
- solarElevation,
122
- dayFactor,
123
- nightFactor,
124
- sunDirection,
125
- moonDirection,
126
- };
128
+ return {
129
+ timeOfDay: normalizedTime,
130
+ timeHours,
131
+ sunPhase,
132
+ solarAzimuth,
133
+ solarElevation,
134
+ dayFactor,
135
+ nightFactor,
136
+ sunDirection,
137
+ moonDirection,
138
+ };
139
+ }
127
140
  }
@@ -15,7 +15,7 @@ import { initDb, loadModel } from "./gltf-storage.js";
15
15
  * - Provides a unified API for consumers to obtain SVG content and metadata.
16
16
  *
17
17
  * Usage:
18
- * - Instantiate with an initial SVG state object (required).
18
+ * - Instantiate with an optional initial SVG state object.
19
19
  * Example: const resolver = new SVGResolver({ value: "", size: 0, timeStamp: null, update: true });
20
20
  * - Call getSVG(svgConfig) to resolve and update the SVG state.
21
21
  * Example: await resolver.getSVG({ storage: { url: "data:image/svg+xml;base64,..." } });
@@ -78,7 +78,7 @@ export default class SVGResolver {
78
78
  * Parse an input that may be a plain SVG string or a data URI (possibly base64).
79
79
  * @private
80
80
  * @param {string} source - Data URI or raw string to decode/check.
81
- * @returns {{value: string|null, size: number}} value when detected/decoded, and size (length of the text string).
81
+ * @returns {[string|null, number]} Decoded SVG text or null, followed by the source size used for cache comparison.
82
82
  */
83
83
  #parseSource(source) {
84
84
  let value = null;
@@ -210,14 +210,15 @@ export default class SVGResolver {
210
210
  /**
211
211
  * Resolve and prepare an SVG from multiple storage forms.
212
212
  * @public
213
- * @param {object|string} svgConfig - Configuration object. Expected shape for storage:
213
+ * @param {object} svgConfig - Configuration object. Expected shape for storage:
214
214
  * { storage: { url: "<url>" } } // remote URL or data URI (base64 or plain)
215
215
  * { storage: { db: "<db>", table: "<table>", id: "<id>" } } // IndexedDB record
216
216
  * @param {object} [svg] - Optional external SVG state object to seed/update. If provided, the resolver
217
217
  * updates this object in-place: { value: string, size: number, timeStamp: string|null, update: boolean }.
218
218
  * @returns {Promise<false|{value:string,size:number,timeStamp:string|null,update:boolean}>}
219
- * - Resolves to false when no update is required or on error.
219
+ * - Resolves to false when no update is required or the source cannot be decoded/fetched.
220
220
  * - Resolves to the internal SVG state object when an update was prepared.
221
+ * @throws {Error} Rejects on IndexedDB failures or an absent record in the requested database.
221
222
  * @description
222
223
  * - Accepts SVG (base64 or plain string) from IndexedDB (storage.db/table/id), from a data URI (base64 or plain string) (storage.url),
223
224
  * from a remote URL (storage.url) or from direct SVG string (storage.url).