@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.
- package/README.md +184 -22
- package/index.d.ts +11 -1
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +12 -163
- package/src/babylonjs-animation-opening-menu.js +8 -1
- package/src/babylonjs-animation-opening.js +21 -1
- package/src/babylonjs-animation-overlay-controller.js +145 -0
- package/src/babylonjs-camera-framing.js +171 -0
- package/src/babylonjs-camera-zoom-controller.js +272 -0
- package/src/babylonjs-controller.js +640 -378
- package/src/babylonjs-selection-controller.js +215 -0
- package/src/babylonjs-view-cube-controller.js +810 -0
- package/src/file-storage.js +13 -0
- package/src/gltf-storage.js +97 -0
- package/src/localization/i18n.js +147 -74
- package/src/localization/translations.js +18 -0
- package/src/material-utils.js +67 -60
- package/src/panzoom-controller.js +14 -1
- package/src/pref-viewer-2d.js +16 -4
- package/src/pref-viewer-3d-data.js +166 -0
- package/src/pref-viewer-3d.js +40 -5
- package/src/pref-viewer-dialog.js +7 -1
- package/src/pref-viewer-menu-3d.js +57 -37
- package/src/pref-viewer.js +155 -44
- package/src/render-math.js +130 -117
- package/src/styles.js +94 -3
- package/src/svg-resolver.js +5 -4
package/src/render-math.js
CHANGED
|
@@ -1,127 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure
|
|
3
|
-
*
|
|
2
|
+
* Pure lighting and hardware-scaling calculations without Babylon.js dependencies.
|
|
3
|
+
* @class RenderMath
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 {
|
package/src/svg-resolver.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
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
|
|
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).
|