@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.
- package/LICENSE.md +13 -0
- package/README.md +463 -0
- package/package.json +7 -7
- package/src/babylonjs-controller.js +122 -847
- package/src/file-storage.js +5 -126
- package/src/gltf-resolver.js +24 -58
- package/src/localization/translations.js +5 -17
- package/src/material-utils.js +72 -0
- package/src/pref-viewer-3d.js +24 -112
- package/src/pref-viewer-menu-3d.js +133 -260
- package/src/pref-viewer.js +4 -150
- package/src/render-math.js +127 -0
- package/src/styles.js +71 -163
- package/Readme.md +0 -8
package/src/pref-viewer.js
CHANGED
|
@@ -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
|
|
484
|
-
*
|
|
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
|
|
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
|
+
}
|