@preference-sl/pref-viewer 2.16.2 → 2.17.0-beta.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.
@@ -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
  }
package/src/styles.js CHANGED
@@ -150,6 +150,94 @@ export const PrefViewer3DStyles = `
150
150
  }
151
151
  `;
152
152
 
153
+ export const PrefViewerViewCubeStyles = `
154
+ .pref-viewer-view-cube {
155
+ --cube-fill-color: #eef0f3;
156
+ --cube-border-color: #78838f;
157
+ --cube-border-width: 1.0px;
158
+ --cube-hover-color: var(--pref-viewer-color-primary, ${PrefViewerStyleVariables.colorPrimary});
159
+ --cube-hover-opacity: 0.50;
160
+ --cube-active-color: var(--pref-viewer-color-primary, ${PrefViewerStyleVariables.colorPrimary});
161
+ --cube-active-opacity: 0.15;
162
+ --cube-text-font-family: ${PrefViewerStyleVariables.fontFamily};
163
+ --cube-text-font-size: 9px;
164
+ --cube-text-color: #29323b;
165
+ --cube-text-font-weight: 600;
166
+ --cube-text-letter-spacing: 1.0px;
167
+ }
168
+
169
+ .pref-viewer-view-cube[hidden] {
170
+ display: none;
171
+ }
172
+
173
+ .pref-viewer-view-cube svg {
174
+ width: 100%;
175
+ height: 100%;
176
+ overflow: visible;
177
+ pointer-events: none;
178
+ }
179
+
180
+ .pref-viewer-view-cube polygon {
181
+ stroke-width: var(--cube-border-width);
182
+ }
183
+
184
+ .pref-viewer-view-cube .cube-shell {
185
+ fill: var(--cube-fill-color);
186
+ stroke: var(--cube-border-color);
187
+ pointer-events: none;
188
+ }
189
+
190
+ .pref-viewer-view-cube polygon[data-region] {
191
+ fill: transparent;
192
+ stroke: transparent;
193
+ pointer-events: visiblePainted;
194
+ cursor: pointer;
195
+ }
196
+
197
+ .pref-viewer-view-cube[data-locked="true"] polygon {
198
+ cursor: default;
199
+ }
200
+
201
+ .pref-viewer-view-cube polygon[data-current] {
202
+ fill: var(--cube-active-color);
203
+ fill-opacity: var(--cube-active-opacity);
204
+ stroke: transparent;
205
+ }
206
+
207
+ .pref-viewer-view-cube polygon[data-hover] {
208
+ fill: var(--cube-hover-color);
209
+ fill-opacity: var(--cube-hover-opacity);
210
+ stroke: transparent;
211
+ }
212
+
213
+ .pref-viewer-view-cube .hover-outline {
214
+ display: none;
215
+ fill: none;
216
+ stroke: var(--cube-hover-color);
217
+ stroke-opacity: 1;
218
+ stroke-width: var(--cube-border-width);
219
+ pointer-events: none;
220
+ }
221
+
222
+ .pref-viewer-view-cube .hover-outline[data-hover] {
223
+ display: inline;
224
+ }
225
+
226
+ .pref-viewer-view-cube text {
227
+ -webkit-user-select: none;
228
+ user-select: none;
229
+ pointer-events: none;
230
+ fill: var(--cube-text-color);
231
+ font-family: var(--cube-text-font-family);
232
+ font-size: var(--cube-text-font-size);
233
+ font-weight: var(--cube-text-font-weight);
234
+ letter-spacing: var(--cube-text-letter-spacing);
235
+ text-transform: uppercase;
236
+ text-anchor: middle;
237
+ dominant-baseline: middle;
238
+ }
239
+ `;
240
+
153
241
  export const PrefViewerMenu3DStyles = `
154
242
  pref-viewer-menu-3d {
155
243
  --font-family: ${PrefViewerStyleVariables.fontFamily};
@@ -203,7 +291,7 @@ export const PrefViewerMenu3DStyles = `
203
291
  --slider-bar-border: 1px solid rgba(255, 255, 255, 0.5);
204
292
 
205
293
  position: absolute;
206
- top: var(--toggle-margin);
294
+ top: var(--pref-viewer-render-menu-top, var(--toggle-margin));
207
295
  right: var(--toggle-margin);
208
296
  z-index: 1200;
209
297
  /* The closed panel keeps its place in the flex column (opacity:0, not display:none), so
@@ -214,6 +302,8 @@ export const PrefViewerMenu3DStyles = `
214
302
  pointer-events: none;
215
303
 
216
304
  font-family: var(--font-family);
305
+ -webkit-user-select: none;
306
+ user-select: none;
217
307
  }
218
308
 
219
309
  pref-viewer-menu-3d>.menu-wrapper {
@@ -256,7 +346,7 @@ export const PrefViewerMenu3DStyles = `
256
346
 
257
347
  pref-viewer-menu-3d>.menu-wrapper>.menu-panel {
258
348
  width: var(--panel-width);
259
- max-height: 70vh;
349
+ max-height: min(70vh, calc(var(--pref-viewer-viewport-height, 100vh) - var(--pref-viewer-render-menu-top, var(--toggle-margin)) - 84px));
260
350
  overflow-y: auto;
261
351
  border-radius: var(--panel-radius);
262
352
  background: var(--panel-bg-color);
@@ -820,7 +910,8 @@ export const PrefViewerDialogStyles = `
820
910
  justify-content: center;
821
911
  background-color: var(--dialog-backdrop-color);
822
912
  position: relative;
823
- z-index: 1000;
913
+ /* Cover all viewer overlays, including the 3D render menu (1200). */
914
+ z-index: 1300;
824
915
  }
825
916
 
826
917
  pref-viewer-dialog>.dialog-wrapper {
@@ -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).