@preference-sl/pref-viewer 2.14.0 → 2.15.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/package.json +1 -1
- package/src/babylonjs-controller.js +378 -127
- package/src/localization/translations.js +18 -0
- package/src/pref-viewer-3d.js +61 -1
- package/src/pref-viewer-menu-3d.js +206 -3
- package/src/pref-viewer.js +5 -1
- package/src/styles.js +117 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
2
2
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
3
|
import "@babylonjs/loaders";
|
|
4
4
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -11,6 +11,74 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
11
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
12
12
|
import { translate } from "./localization/i18n.js";
|
|
13
13
|
|
|
14
|
+
export function getAdaptiveHardwareScalingLevel(baseScaling = 1, pixelRatio = 1) {
|
|
15
|
+
const safeBaseScaling = Number.isFinite(baseScaling) && baseScaling > 0 ? baseScaling : 1;
|
|
16
|
+
const safePixelRatio = Number.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : 1;
|
|
17
|
+
return safeBaseScaling / safePixelRatio;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clamp01(value) {
|
|
21
|
+
if (!Number.isFinite(value)) {
|
|
22
|
+
return 0.5;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(1, Math.max(0, value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lerp(from, to, amount) {
|
|
28
|
+
const t = clamp01(amount);
|
|
29
|
+
return from + (to - from) * t;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function smoothstep(edge0, edge1, x) {
|
|
33
|
+
if (edge0 === edge1) {
|
|
34
|
+
return x >= edge1 ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
const t = clamp01((x - edge0) / (edge1 - edge0));
|
|
37
|
+
return t * t * (3 - 2 * t);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bellCurve(x) {
|
|
41
|
+
const clamped = clamp01(x);
|
|
42
|
+
return Math.sin(clamped * Math.PI);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeDirection(x, y, z) {
|
|
46
|
+
const length = Math.hypot(x, y, z) || 1;
|
|
47
|
+
return {
|
|
48
|
+
x: x / length,
|
|
49
|
+
y: y / length,
|
|
50
|
+
z: z / length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getLightingCycleProfile(timeOfDay = 0.5) {
|
|
55
|
+
const normalizedTime = clamp01(timeOfDay);
|
|
56
|
+
const timeHours = normalizedTime * 24;
|
|
57
|
+
const sunriseHour = 6.25;
|
|
58
|
+
const sunsetHour = 20.25;
|
|
59
|
+
const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
|
|
60
|
+
const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
|
|
61
|
+
const nightFactor = 1 - dayFactor;
|
|
62
|
+
const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
|
|
63
|
+
const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
|
|
64
|
+
const solarElevation = lerp(-0.18, 1.18, solarArc);
|
|
65
|
+
const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
|
|
66
|
+
const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
|
|
67
|
+
const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
timeOfDay: normalizedTime,
|
|
71
|
+
timeHours,
|
|
72
|
+
sunPhase,
|
|
73
|
+
solarAzimuth,
|
|
74
|
+
solarElevation,
|
|
75
|
+
dayFactor,
|
|
76
|
+
nightFactor,
|
|
77
|
+
sunDirection,
|
|
78
|
+
moonDirection,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
14
82
|
/**
|
|
15
83
|
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
16
84
|
* rebuilds camera-dependent pipelines once textures are ready, brokers XR/download interactions, and persists
|
|
@@ -85,6 +153,7 @@ export default class BabylonJSController {
|
|
|
85
153
|
ambientOcclusionEnabled: true,
|
|
86
154
|
iblEnabled: true,
|
|
87
155
|
shadowsEnabled: false,
|
|
156
|
+
lightingTimeOfDay: 0.5,
|
|
88
157
|
// Highlight settings
|
|
89
158
|
highlightEnabled: true,
|
|
90
159
|
highlightColor: "#ff6700",
|
|
@@ -103,6 +172,7 @@ export default class BabylonJSController {
|
|
|
103
172
|
#camera = null;
|
|
104
173
|
#hemiLight = null;
|
|
105
174
|
#dirLight = null;
|
|
175
|
+
#moonLight = null;
|
|
106
176
|
#cameraLight = null;
|
|
107
177
|
#shadowGen = [];
|
|
108
178
|
#XRExperience = null;
|
|
@@ -186,6 +256,18 @@ export default class BabylonJSController {
|
|
|
186
256
|
shadowBlurKernel: 16, // Shadow blur amount
|
|
187
257
|
ssaoEnabled: true, // Screen Space Ambient Occlusion
|
|
188
258
|
},
|
|
259
|
+
look: {
|
|
260
|
+
showroom: {
|
|
261
|
+
exposure: 0.92,
|
|
262
|
+
contrast: 1.02,
|
|
263
|
+
toneMappingEnabled: true,
|
|
264
|
+
toneMappingType: ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0,
|
|
265
|
+
environmentIntensity: 1.08,
|
|
266
|
+
hemiLightIntensity: 0.52,
|
|
267
|
+
dirLightIntensity: 0.56,
|
|
268
|
+
cameraLightIntensity: 0.16,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
189
271
|
};
|
|
190
272
|
|
|
191
273
|
/**
|
|
@@ -200,6 +282,7 @@ export default class BabylonJSController {
|
|
|
200
282
|
|
|
201
283
|
const debugInfo = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
202
284
|
const rendererName = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
|
|
285
|
+
const isAngleRenderer = /^ANGLE\s*\(/i.test(rendererName);
|
|
203
286
|
|
|
204
287
|
// If we have WebGL renderer info and it looks like a desktop GPU, skip mobile detection
|
|
205
288
|
// Desktop GPUs typically have "NVIDIA", "AMD", "Intel" in the name
|
|
@@ -210,6 +293,10 @@ export default class BabylonJSController {
|
|
|
210
293
|
|
|
211
294
|
// If we detect a desktop GPU, go directly to desktop detection
|
|
212
295
|
if (isDesktopGPU && !isMobileGPU) {
|
|
296
|
+
if (isAngleRenderer) {
|
|
297
|
+
console.log(`[PrefViewer] ANGLE-backed renderer detected: ${rendererName}`);
|
|
298
|
+
return this.#detectDesktopTier(gl, rendererName, true);
|
|
299
|
+
}
|
|
213
300
|
console.log(`[PrefViewer] Desktop GPU detected: ${rendererName}`);
|
|
214
301
|
return this.#detectDesktopTier(gl, rendererName);
|
|
215
302
|
}
|
|
@@ -246,7 +333,7 @@ export default class BabylonJSController {
|
|
|
246
333
|
* @param {string} rendererName - GPU renderer name
|
|
247
334
|
* @returns {string} Hardware tier
|
|
248
335
|
*/
|
|
249
|
-
#detectDesktopTier(gl, rendererName) {
|
|
336
|
+
#detectDesktopTier(gl, rendererName, conservative = false) {
|
|
250
337
|
// Check WebGL capabilities
|
|
251
338
|
const maxTextureSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) || 8192;
|
|
252
339
|
|
|
@@ -256,12 +343,14 @@ export default class BabylonJSController {
|
|
|
256
343
|
const isAmdHighEnd = /RX (6|7|8|9)[0-9]{2,}|RX Vega|Radeon Pro|WX [0-9]/i.test(rendererName);
|
|
257
344
|
const isIntelIntegrated = /Intel.*UHD|Intel.*Iris|Mesa|llvmpipe/i.test(rendererName);
|
|
258
345
|
|
|
259
|
-
let tier = 'high'; // Default to
|
|
346
|
+
let tier = conservative ? 'medium' : 'high'; // Default to conservative desktop when ANGLE is in the path
|
|
260
347
|
|
|
261
348
|
if (isIntelIntegrated || maxTextureSize < 8192) {
|
|
262
349
|
tier = 'medium';
|
|
263
|
-
} else if (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384) {
|
|
350
|
+
} else if (!conservative && (isAppleSilicon || isNvidiaHighEnd || isAmdHighEnd || maxTextureSize >= 16384)) {
|
|
264
351
|
tier = 'ultra';
|
|
352
|
+
} else if (conservative && maxTextureSize >= 16384) {
|
|
353
|
+
tier = 'high';
|
|
265
354
|
}
|
|
266
355
|
|
|
267
356
|
console.log(`[PrefViewer] Desktop tier: ${tier} (GPU: ${rendererName || 'unknown'}, MaxTexture: ${maxTextureSize})`);
|
|
@@ -364,6 +453,7 @@ export default class BabylonJSController {
|
|
|
364
453
|
antialiasingEnabled: false,
|
|
365
454
|
hardwareScaling: 2.5, // Aggressive downscaling
|
|
366
455
|
maxModelResolution: 512, // Low-res model loading
|
|
456
|
+
environmentTextureSize: 512,
|
|
367
457
|
},
|
|
368
458
|
// Mobile high-end / Tablet / Desktop low
|
|
369
459
|
medium: {
|
|
@@ -375,28 +465,31 @@ export default class BabylonJSController {
|
|
|
375
465
|
antialiasingEnabled: true,
|
|
376
466
|
hardwareScaling: 2.0,
|
|
377
467
|
maxModelResolution: 1024,
|
|
468
|
+
environmentTextureSize: 1024,
|
|
378
469
|
},
|
|
379
470
|
// Desktop mid-range / High-end mobile
|
|
380
471
|
high: {
|
|
381
472
|
shadowMapSize: 1024,
|
|
382
473
|
iblShadowResolution: 0, // 1024x1024 - good balance
|
|
383
474
|
iblSampleDirections: 4, // Quality sampling
|
|
384
|
-
shadowBlurKernel:
|
|
385
|
-
ssaoEnabled:
|
|
475
|
+
shadowBlurKernel: 8, // Sharper shadows for product clarity
|
|
476
|
+
ssaoEnabled: false,
|
|
386
477
|
antialiasingEnabled: true,
|
|
387
|
-
hardwareScaling: 1.
|
|
478
|
+
hardwareScaling: 1.0,
|
|
388
479
|
maxModelResolution: 2048,
|
|
480
|
+
environmentTextureSize: 2048,
|
|
389
481
|
},
|
|
390
482
|
// Desktop high-end (RTX, Apple Silicon, etc.)
|
|
391
483
|
ultra: {
|
|
392
484
|
shadowMapSize: 2048,
|
|
393
485
|
iblShadowResolution: 1, // 2048x2048
|
|
394
486
|
iblSampleDirections: 8, // Maximum quality
|
|
395
|
-
shadowBlurKernel:
|
|
396
|
-
ssaoEnabled:
|
|
487
|
+
shadowBlurKernel: 16, // Preserve clarity over softness
|
|
488
|
+
ssaoEnabled: false,
|
|
397
489
|
antialiasingEnabled: true,
|
|
398
490
|
hardwareScaling: 1.0, // Native resolution
|
|
399
491
|
maxModelResolution: 4096,
|
|
492
|
+
environmentTextureSize: 4096,
|
|
400
493
|
},
|
|
401
494
|
};
|
|
402
495
|
|
|
@@ -509,6 +602,11 @@ export default class BabylonJSController {
|
|
|
509
602
|
this.#settings[key] = settings[key];
|
|
510
603
|
changed = true;
|
|
511
604
|
}
|
|
605
|
+
// Handle numeric settings such as lightingTimeOfDay
|
|
606
|
+
if (typeof settings[key] === "number" && Number.isFinite(settings[key]) && this.#settings[key] !== settings[key]) {
|
|
607
|
+
this.#settings[key] = settings[key];
|
|
608
|
+
changed = true;
|
|
609
|
+
}
|
|
512
610
|
});
|
|
513
611
|
|
|
514
612
|
if (changed) {
|
|
@@ -543,6 +641,10 @@ export default class BabylonJSController {
|
|
|
543
641
|
if (typeof parsed?.[key] === "string") {
|
|
544
642
|
this.#settings[key] = parsed[key];
|
|
545
643
|
}
|
|
644
|
+
// Handle numeric settings such as lightingTimeOfDay
|
|
645
|
+
if (typeof parsed?.[key] === "number" && Number.isFinite(parsed[key])) {
|
|
646
|
+
this.#settings[key] = parsed[key];
|
|
647
|
+
}
|
|
546
648
|
});
|
|
547
649
|
} catch (error) {
|
|
548
650
|
console.warn("PrefViewer: unable to load render settings", error);
|
|
@@ -895,6 +997,7 @@ export default class BabylonJSController {
|
|
|
895
997
|
this.#camera.lowerRadiusLimit = 5;
|
|
896
998
|
this.#camera.upperRadiusLimit = 20;
|
|
897
999
|
this.#camera.metadata = { locked: false };
|
|
1000
|
+
this.#applyShowroomCameraFraming(this.#camera);
|
|
898
1001
|
this.#camera.attachControl(this.#canvas, true);
|
|
899
1002
|
this.#scene.activeCamera = this.#camera;
|
|
900
1003
|
}
|
|
@@ -916,25 +1019,11 @@ export default class BabylonJSController {
|
|
|
916
1019
|
const cameraLightName = "PrefViewerCameraLight";
|
|
917
1020
|
const dirLightName = "PrefViewerDirLight";
|
|
918
1021
|
|
|
919
|
-
const hemiLight = this.#scene.getLightByName(hemiLightName);
|
|
920
|
-
const cameraLight = this.#scene.getLightByName(cameraLightName);
|
|
921
|
-
const dirLight = this.#scene.getLightByName(dirLightName);
|
|
922
|
-
|
|
923
1022
|
let lightsChanged = false;
|
|
924
1023
|
|
|
925
1024
|
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
926
1025
|
|
|
927
1026
|
if (iblEnabled) {
|
|
928
|
-
if (hemiLight) {
|
|
929
|
-
hemiLight.dispose();
|
|
930
|
-
}
|
|
931
|
-
if (cameraLight) {
|
|
932
|
-
cameraLight.dispose();
|
|
933
|
-
}
|
|
934
|
-
if (dirLight) {
|
|
935
|
-
dirLight.dispose();
|
|
936
|
-
}
|
|
937
|
-
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
938
1027
|
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
939
1028
|
} else {
|
|
940
1029
|
// If IBL is disabled but an environment texture exists, dispose it to save resources and ensure it doesn't affect the lighting
|
|
@@ -943,27 +1032,22 @@ export default class BabylonJSController {
|
|
|
943
1032
|
this.#scene.environmentTexture = null;
|
|
944
1033
|
lightsChanged = true;
|
|
945
1034
|
}
|
|
946
|
-
|
|
947
|
-
// Add a hemispheric light for basic ambient illumination
|
|
948
|
-
if (!this.#hemiLight) {
|
|
949
|
-
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
950
|
-
this.#hemiLight.intensity = 0.6;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
954
|
-
if (!this.#dirLight) {
|
|
955
|
-
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
956
|
-
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
957
|
-
this.#dirLight.intensity = 0.6;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
961
|
-
if (!this.#cameraLight) {
|
|
962
|
-
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
963
|
-
this.#cameraLight.parent = this.#camera;
|
|
964
|
-
this.#cameraLight.intensity = 0.3;
|
|
965
|
-
}
|
|
966
1035
|
}
|
|
1036
|
+
if (!this.#hemiLight) {
|
|
1037
|
+
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
1038
|
+
}
|
|
1039
|
+
if (!this.#dirLight) {
|
|
1040
|
+
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
1041
|
+
this.#dirLight.position = new Vector3(5, 4, 5);
|
|
1042
|
+
}
|
|
1043
|
+
if (!this.#moonLight) {
|
|
1044
|
+
this.#moonLight = new DirectionalLight("PrefViewerMoonLight", new Vector3(10, -10, 8), this.#scene);
|
|
1045
|
+
}
|
|
1046
|
+
if (!this.#cameraLight) {
|
|
1047
|
+
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
1048
|
+
this.#cameraLight.parent = this.#camera;
|
|
1049
|
+
}
|
|
1050
|
+
lightsChanged = this.#applyLightingConfig() || lightsChanged;
|
|
967
1051
|
return lightsChanged;
|
|
968
1052
|
}
|
|
969
1053
|
|
|
@@ -1140,34 +1224,13 @@ export default class BabylonJSController {
|
|
|
1140
1224
|
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
1141
1225
|
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
1142
1226
|
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
1143
|
-
// FXAA
|
|
1144
|
-
defaultPipeline.fxaaEnabled =
|
|
1145
|
-
defaultPipeline.fxaa.samples = 8;
|
|
1146
|
-
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
1147
|
-
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
1148
|
-
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
1149
|
-
}
|
|
1150
|
-
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
1151
|
-
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
1152
|
-
}
|
|
1153
|
-
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
1154
|
-
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
1155
|
-
}
|
|
1227
|
+
// FXAA can soften product surfaces too much; keep MSAA only for crisper materials.
|
|
1228
|
+
defaultPipeline.fxaaEnabled = false;
|
|
1156
1229
|
|
|
1157
|
-
// Grain
|
|
1158
|
-
defaultPipeline.grainEnabled =
|
|
1159
|
-
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
1160
|
-
defaultPipeline.grain.animated = false;
|
|
1161
|
-
defaultPipeline.grain.intensity = 3;
|
|
1230
|
+
// Grain is disabled to keep product surfaces clean and avoid visible noise.
|
|
1231
|
+
defaultPipeline.grainEnabled = false;
|
|
1162
1232
|
|
|
1163
1233
|
// Configure post-processes to calculate only once instead of every frame for better performance
|
|
1164
|
-
if (defaultPipeline.fxaa?._postProcess) {
|
|
1165
|
-
defaultPipeline.fxaa._postProcess.autoClear = false;
|
|
1166
|
-
}
|
|
1167
|
-
if (defaultPipeline.grain?._postProcess) {
|
|
1168
|
-
defaultPipeline.grain._postProcess.autoClear = false;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
1234
|
this.#renderPipelines.default = defaultPipeline;
|
|
1172
1235
|
pipelineManager.update();
|
|
1173
1236
|
return true;
|
|
@@ -1201,7 +1264,8 @@ export default class BabylonJSController {
|
|
|
1201
1264
|
let hdrTexture = null;
|
|
1202
1265
|
if (this.#options.ibl?.cachedUrl) {
|
|
1203
1266
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1204
|
-
|
|
1267
|
+
const envTextureSize = this.#config.quality.environmentTextureSize || 1024;
|
|
1268
|
+
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
|
|
1205
1269
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1206
1270
|
hdrTexture = this.#hdrTexture.clone();
|
|
1207
1271
|
} else {
|
|
@@ -1218,8 +1282,10 @@ export default class BabylonJSController {
|
|
|
1218
1282
|
this.#options.ibl?.consumeCachedUrl?.(true);
|
|
1219
1283
|
}
|
|
1220
1284
|
|
|
1221
|
-
hdrTexture.level = this.#options.ibl.intensity;
|
|
1285
|
+
hdrTexture.level = this.#options.ibl.intensity ?? 1;
|
|
1222
1286
|
this.#scene.environmentTexture = hdrTexture;
|
|
1287
|
+
this.#applyShowroomLook();
|
|
1288
|
+
this.#applyLightingConfig();
|
|
1223
1289
|
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
1224
1290
|
return true;
|
|
1225
1291
|
}
|
|
@@ -1305,7 +1371,7 @@ export default class BabylonJSController {
|
|
|
1305
1371
|
});
|
|
1306
1372
|
const materialsForReceivingShadows = this.#scene.materials.filter((material) => {
|
|
1307
1373
|
if (material instanceof PBRMaterial) {
|
|
1308
|
-
material.enableSpecularAntiAliasing =
|
|
1374
|
+
material.enableSpecularAntiAliasing = true;
|
|
1309
1375
|
}
|
|
1310
1376
|
return true;
|
|
1311
1377
|
});
|
|
@@ -1530,6 +1596,184 @@ export default class BabylonJSController {
|
|
|
1530
1596
|
}
|
|
1531
1597
|
}
|
|
1532
1598
|
|
|
1599
|
+
/**
|
|
1600
|
+
* Improves texture sharpness by enabling trilinear filtering and anisotropy.
|
|
1601
|
+
* @private
|
|
1602
|
+
* @returns {void}
|
|
1603
|
+
*/
|
|
1604
|
+
#enhanceTextureQuality() {
|
|
1605
|
+
if (!this.#scene) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
1610
|
+
const maxAnisotropy = Math.max(1, Math.min(16, caps.maxAnisotropy || 1));
|
|
1611
|
+
|
|
1612
|
+
this.#scene.textures?.forEach((texture) => {
|
|
1613
|
+
if (!texture || texture.isDisposed?.()) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if ("anisotropicFilteringLevel" in texture) {
|
|
1618
|
+
texture.anisotropicFilteringLevel = maxAnisotropy;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (typeof texture.updateSamplingMode === "function") {
|
|
1622
|
+
try {
|
|
1623
|
+
texture.updateSamplingMode(Texture.TRILINEAR_SAMPLINGMODE);
|
|
1624
|
+
} catch {
|
|
1625
|
+
// Ignore texture subclasses that do not support sampling changes.
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
|
|
1633
|
+
* @private
|
|
1634
|
+
* @returns {void}
|
|
1635
|
+
*/
|
|
1636
|
+
#applyShowroomLook() {
|
|
1637
|
+
if (!this.#scene?.imageProcessingConfiguration) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
const showroom = this.#config.look?.showroom || {};
|
|
1642
|
+
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1643
|
+
|
|
1644
|
+
this.#scene.clearColor = new Color4(0.98, 0.98, 0.98, 1).toLinearSpace();
|
|
1645
|
+
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1646
|
+
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1647
|
+
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1648
|
+
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1649
|
+
imageProcessing.vignetteEnabled = false;
|
|
1650
|
+
imageProcessing.colorCurvesEnabled = false;
|
|
1651
|
+
|
|
1652
|
+
if ("environmentIntensity" in this.#scene) {
|
|
1653
|
+
this.#scene.environmentIntensity = showroom.environmentIntensity ?? 1.15;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Applies the current time-of-day cycle to the scene lighting, environment intensity, and tonemapping bias.
|
|
1659
|
+
* @private
|
|
1660
|
+
* @returns {boolean} True when a scene existed and was updated.
|
|
1661
|
+
*/
|
|
1662
|
+
#applyLightingConfig() {
|
|
1663
|
+
if (!this.#scene) {
|
|
1664
|
+
return false;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const showroom = this.#config.look?.showroom || {};
|
|
1668
|
+
const timeOfDay = clamp01(this.#settings.lightingTimeOfDay ?? 0.5);
|
|
1669
|
+
const profile = getLightingCycleProfile(timeOfDay);
|
|
1670
|
+
const daylight = profile.dayFactor;
|
|
1671
|
+
const peakLight = daylight > 0 ? Math.pow(daylight, 0.72) : 0;
|
|
1672
|
+
const twilight = 1 - peakLight;
|
|
1673
|
+
|
|
1674
|
+
if (this.#scene.imageProcessingConfiguration) {
|
|
1675
|
+
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1676
|
+
imageProcessing.exposure = lerp(0.78, showroom.exposure ?? 0.92, peakLight);
|
|
1677
|
+
imageProcessing.contrast = lerp(0.98, showroom.contrast ?? 1.02, peakLight);
|
|
1678
|
+
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
1679
|
+
imageProcessing.toneMappingType = showroom.toneMappingType ?? (ImageProcessingConfiguration?.TONEMAPPING_ACES ?? 0);
|
|
1680
|
+
imageProcessing.vignetteEnabled = false;
|
|
1681
|
+
imageProcessing.colorCurvesEnabled = false;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
this.#scene.clearColor = new Color4(
|
|
1685
|
+
lerp(0.035, 0.98, daylight),
|
|
1686
|
+
lerp(0.045, 0.98, daylight),
|
|
1687
|
+
lerp(0.085, 0.98, daylight),
|
|
1688
|
+
1,
|
|
1689
|
+
).toLinearSpace();
|
|
1690
|
+
|
|
1691
|
+
if ("environmentIntensity" in this.#scene) {
|
|
1692
|
+
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (this.#hemiLight) {
|
|
1696
|
+
this.#hemiLight.direction = new Vector3(
|
|
1697
|
+
lerp(-0.45, -0.8, peakLight),
|
|
1698
|
+
lerp(0.45, 0.95, peakLight),
|
|
1699
|
+
lerp(-0.75, -0.35, peakLight),
|
|
1700
|
+
);
|
|
1701
|
+
this.#hemiLight.intensity = lerp(0.18, showroom.hemiLightIntensity ?? 0.52, peakLight);
|
|
1702
|
+
this.#hemiLight.diffuse = new Color3(
|
|
1703
|
+
lerp(0.42, 0.92, peakLight),
|
|
1704
|
+
lerp(0.48, 0.96, peakLight),
|
|
1705
|
+
lerp(0.62, 0.98, peakLight),
|
|
1706
|
+
);
|
|
1707
|
+
this.#hemiLight.groundColor = new Color3(
|
|
1708
|
+
lerp(0.08, 0.3, twilight),
|
|
1709
|
+
lerp(0.08, 0.28, twilight),
|
|
1710
|
+
lerp(0.1, 0.22, twilight),
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (this.#dirLight) {
|
|
1715
|
+
this.#dirLight.direction = new Vector3(profile.sunDirection.x, profile.sunDirection.y, profile.sunDirection.z);
|
|
1716
|
+
this.#dirLight.intensity = lerp(0.02, showroom.dirLightIntensity ?? 0.56, peakLight);
|
|
1717
|
+
this.#dirLight.diffuse = new Color3(
|
|
1718
|
+
lerp(0.28, 1.0, peakLight),
|
|
1719
|
+
lerp(0.3, 0.97, peakLight),
|
|
1720
|
+
lerp(0.34, 0.9, peakLight),
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (this.#moonLight) {
|
|
1725
|
+
this.#moonLight.direction = new Vector3(profile.moonDirection.x, profile.moonDirection.y, profile.moonDirection.z);
|
|
1726
|
+
this.#moonLight.intensity = lerp(0.44, 0.03, peakLight);
|
|
1727
|
+
this.#moonLight.diffuse = new Color3(0.58, 0.7, 1.0);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (this.#cameraLight) {
|
|
1731
|
+
this.#cameraLight.intensity = lerp(0.06, showroom.cameraLightIntensity ?? 0.16, peakLight);
|
|
1732
|
+
this.#cameraLight.diffuse = new Color3(
|
|
1733
|
+
lerp(0.35, 1.0, peakLight),
|
|
1734
|
+
lerp(0.35, 0.97, peakLight),
|
|
1735
|
+
lerp(0.36, 0.92, peakLight),
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
this.#scene.markAllMaterialsAsDirty?.(Material.LightDirtyFlag ?? Material.TextureDirtyFlag);
|
|
1740
|
+
this.#requestRender({ frames: 2, continuousMs: this.#config.render.interactionMs });
|
|
1741
|
+
return true;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Recomputes the effective hardware scaling from the current device pixel ratio.
|
|
1746
|
+
* This keeps the 3D output crisp when the viewport changes or the browser zoom/DPR shifts.
|
|
1747
|
+
* @private
|
|
1748
|
+
* @returns {void}
|
|
1749
|
+
*/
|
|
1750
|
+
#applyViewportHardwareScaling() {
|
|
1751
|
+
if (!this.#engine) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const pixelRatio = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1756
|
+
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
1757
|
+
const scalingLevel = getAdaptiveHardwareScalingLevel(baseScaling, pixelRatio);
|
|
1758
|
+
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Keeps the showroom camera framing stable when the viewport aspect ratio changes.
|
|
1763
|
+
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
1764
|
+
* field of view on ArcRotate cameras to avoid the background appearing undersized on wide layouts.
|
|
1765
|
+
* @private
|
|
1766
|
+
* @param {Camera|null|undefined} camera - Active camera to adjust.
|
|
1767
|
+
* @returns {void}
|
|
1768
|
+
*/
|
|
1769
|
+
#applyShowroomCameraFraming(camera = this.#scene?.activeCamera) {
|
|
1770
|
+
if (!(camera instanceof ArcRotateCamera)) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
camera.fovMode = Camera?.FOVMODE_HORIZONTAL_FIXED ?? 1;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1533
1777
|
/**
|
|
1534
1778
|
* Resets pointer-picking sampling state.
|
|
1535
1779
|
* @private
|
|
@@ -1696,7 +1940,7 @@ export default class BabylonJSController {
|
|
|
1696
1940
|
}
|
|
1697
1941
|
this.#engine.dispose();
|
|
1698
1942
|
this.#engine = this.#scene = this.#camera = null;
|
|
1699
|
-
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1943
|
+
this.#hemiLight = this.#dirLight = this.#moonLight = this.#cameraLight = null;
|
|
1700
1944
|
}
|
|
1701
1945
|
|
|
1702
1946
|
/**
|
|
@@ -1931,8 +2175,11 @@ export default class BabylonJSController {
|
|
|
1931
2175
|
}
|
|
1932
2176
|
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1933
2177
|
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
2178
|
+
this.#applyShowroomCameraFraming();
|
|
1934
2179
|
this.#engine.resize();
|
|
1935
|
-
this.#
|
|
2180
|
+
const needsEnhancedBurst = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled || this.#settings.shadowsEnabled;
|
|
2181
|
+
const frames = needsEnhancedBurst ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2182
|
+
this.#requestRender({ frames, continuousMs: this.#config.render.interactionMs });
|
|
1936
2183
|
};
|
|
1937
2184
|
|
|
1938
2185
|
if (elapsed >= this.#config.resize.throttleMs) {
|
|
@@ -2084,6 +2331,7 @@ export default class BabylonJSController {
|
|
|
2084
2331
|
cameraState.setSuccess(true);
|
|
2085
2332
|
}
|
|
2086
2333
|
}
|
|
2334
|
+
this.#applyShowroomCameraFraming(camera);
|
|
2087
2335
|
this.#scene.activeCamera?.detachControl();
|
|
2088
2336
|
camera.detachControl();
|
|
2089
2337
|
if (!cameraState.locked) {
|
|
@@ -2471,6 +2719,7 @@ export default class BabylonJSController {
|
|
|
2471
2719
|
.finally(async () => {
|
|
2472
2720
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
2473
2721
|
this.#setMaxSimultaneousLights();
|
|
2722
|
+
this.#enhanceTextureQuality();
|
|
2474
2723
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
2475
2724
|
// Apply stored highlight settings so fresh page loads respect persisted state
|
|
2476
2725
|
this.#setHighlightConfig({
|
|
@@ -2736,19 +2985,13 @@ export default class BabylonJSController {
|
|
|
2736
2985
|
*/
|
|
2737
2986
|
async enable() {
|
|
2738
2987
|
this.#configureDracoCompression();
|
|
2739
|
-
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
2988
|
+
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false }, true);
|
|
2740
2989
|
this.#engine.disableUniformBuffers = true;
|
|
2741
2990
|
|
|
2742
2991
|
// OPTIMIZATION: Detect hardware tier and apply quality settings
|
|
2743
2992
|
const detectedTier = this.#detectHardwareTier();
|
|
2744
2993
|
this.#applyQualitySettings(detectedTier);
|
|
2745
2994
|
|
|
2746
|
-
// OPTIMIZATION: Apply hardware scaling from quality preset
|
|
2747
|
-
const pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
|
2748
|
-
const baseScaling = this.#config.quality.hardwareScaling || 1;
|
|
2749
|
-
const scalingLevel = pixelRatio > 1 ? baseScaling * pixelRatio : baseScaling;
|
|
2750
|
-
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
2751
|
-
|
|
2752
2995
|
// OPTIMIZATION: Initialize idle tracking
|
|
2753
2996
|
this.#state.render.lastActivityAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
2754
2997
|
|
|
@@ -2764,14 +3007,7 @@ export default class BabylonJSController {
|
|
|
2764
3007
|
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
2765
3008
|
}
|
|
2766
3009
|
|
|
2767
|
-
this.#
|
|
2768
|
-
|
|
2769
|
-
// Lowered exposure to prevent scenes from looking blown out when the DefaultRenderingPipeline (Antialiasing) is enabled.
|
|
2770
|
-
this.#scene.imageProcessingConfiguration.exposure = 0.75;
|
|
2771
|
-
this.#scene.imageProcessingConfiguration.contrast = 1.0;
|
|
2772
|
-
this.#scene.imageProcessingConfiguration.toneMappingEnabled = false;
|
|
2773
|
-
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
2774
|
-
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
3010
|
+
this.#applyShowroomLook();
|
|
2775
3011
|
|
|
2776
3012
|
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2777
3013
|
this.#scene.skipPointerMovePicking = true;
|
|
@@ -2985,6 +3221,60 @@ export default class BabylonJSController {
|
|
|
2985
3221
|
}
|
|
2986
3222
|
}
|
|
2987
3223
|
|
|
3224
|
+
/**
|
|
3225
|
+
* Applies lighting and highlight configuration without forcing a full scene reload.
|
|
3226
|
+
* @public
|
|
3227
|
+
* @param {{lightingTimeOfDay?:number, highlightColor?:string, highlightEnabled?:boolean, showAnimationMenu?:boolean}} config - Lighting/highlight configuration.
|
|
3228
|
+
* @returns {void}
|
|
3229
|
+
*/
|
|
3230
|
+
setIlluminationConfig(config = {}) {
|
|
3231
|
+
let shouldApplyLighting = false;
|
|
3232
|
+
|
|
3233
|
+
if (typeof config.lightingTimeOfDay === "number" && Number.isFinite(config.lightingTimeOfDay)) {
|
|
3234
|
+
const normalizedTime = clamp01(config.lightingTimeOfDay);
|
|
3235
|
+
if (this.#settings.lightingTimeOfDay !== normalizedTime) {
|
|
3236
|
+
this.#settings.lightingTimeOfDay = normalizedTime;
|
|
3237
|
+
shouldApplyLighting = true;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (config.showAnimationMenu !== undefined) {
|
|
3242
|
+
// Handle show-animation-menu via the 3D component's parent
|
|
3243
|
+
// This is handled in pref-viewer-3d.js attributeChangedCallback
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// Handle highlight color for animation hover
|
|
3247
|
+
if (config.highlightColor !== undefined || config.highlightEnabled !== undefined) {
|
|
3248
|
+
// Normalize highlightEnabled: accept boolean, string "true"/"false", or null
|
|
3249
|
+
let enabled = config.highlightEnabled;
|
|
3250
|
+
if (typeof enabled === "string") {
|
|
3251
|
+
enabled = enabled.toLowerCase() !== "false";
|
|
3252
|
+
} else if (enabled === null) {
|
|
3253
|
+
enabled = undefined;
|
|
3254
|
+
}
|
|
3255
|
+
// Keep #settings in sync so getRenderSettings() always reflects the real state
|
|
3256
|
+
if (enabled !== undefined) {
|
|
3257
|
+
this.#settings.highlightEnabled = enabled;
|
|
3258
|
+
}
|
|
3259
|
+
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3260
|
+
this.#settings.highlightColor = config.highlightColor;
|
|
3261
|
+
}
|
|
3262
|
+
this.#setHighlightConfig({
|
|
3263
|
+
color: config.highlightColor ?? undefined,
|
|
3264
|
+
enabled,
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
if (shouldApplyLighting && this.#scene) {
|
|
3269
|
+
this.#applyLightingConfig();
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// Persist settings unless caller explicitly requests a non-persistent update (used by playback animation).
|
|
3273
|
+
if (config.persist !== false) {
|
|
3274
|
+
this.#storeRenderSettings();
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
|
|
2988
3278
|
/**
|
|
2989
3279
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
2990
3280
|
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
@@ -3039,45 +3329,6 @@ export default class BabylonJSController {
|
|
|
3039
3329
|
return await this.#loadContainers();
|
|
3040
3330
|
}
|
|
3041
3331
|
|
|
3042
|
-
/**
|
|
3043
|
-
* Sets highlight configuration for animation hover effects.
|
|
3044
|
-
* @public
|
|
3045
|
-
* @param {{showAnimationMenu?:boolean, highlightColor?:string, highlightEnabled?:boolean}} config - Highlight configuration.
|
|
3046
|
-
* @returns {void}
|
|
3047
|
-
*/
|
|
3048
|
-
setIlluminationConfig(config = {}) {
|
|
3049
|
-
|
|
3050
|
-
if (config.showAnimationMenu !== undefined) {
|
|
3051
|
-
// Handle show-animation-menu via the 3D component's parent
|
|
3052
|
-
// This is handled in pref-viewer-3d.js attributeChangedCallback
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
// Handle highlight color for animation hover
|
|
3056
|
-
if (config.highlightColor !== undefined || config.highlightEnabled !== undefined) {
|
|
3057
|
-
// Normalize highlightEnabled: accept boolean, string "true"/"false", or null
|
|
3058
|
-
let enabled = config.highlightEnabled;
|
|
3059
|
-
if (typeof enabled === "string") {
|
|
3060
|
-
enabled = enabled.toLowerCase() !== "false";
|
|
3061
|
-
} else if (enabled === null) {
|
|
3062
|
-
enabled = undefined;
|
|
3063
|
-
}
|
|
3064
|
-
// Keep #settings in sync so getRenderSettings() always reflects the real state
|
|
3065
|
-
if (enabled !== undefined) {
|
|
3066
|
-
this.#settings.highlightEnabled = enabled;
|
|
3067
|
-
}
|
|
3068
|
-
if (config.highlightColor !== undefined && config.highlightColor !== null) {
|
|
3069
|
-
this.#settings.highlightColor = config.highlightColor;
|
|
3070
|
-
}
|
|
3071
|
-
this.#setHighlightConfig({
|
|
3072
|
-
color: config.highlightColor ?? undefined,
|
|
3073
|
-
enabled,
|
|
3074
|
-
});
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
// Persist settings
|
|
3078
|
-
this.#storeRenderSettings();
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
3332
|
/**
|
|
3082
3333
|
* Returns a snapshot of all available animations and their current state.
|
|
3083
3334
|
* @public
|