@preference-sl/pref-viewer 2.16.0-beta.8 → 2.16.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.16.0-beta.8",
3
+ "version": "2.16.0",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -35,11 +35,11 @@
35
35
  "index.d.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@babylonjs/core": "9.3.3",
39
- "@babylonjs/loaders": "9.3.3",
40
- "@babylonjs/serializers": "9.3.3",
41
- "@panzoom/panzoom": "^4.6.0",
42
- "babylonjs-gltf2interface": "9.3.3",
38
+ "@babylonjs/core": "^9.11.0",
39
+ "@babylonjs/loaders": "^9.11.0",
40
+ "@babylonjs/serializers": "^9.11.0",
41
+ "@panzoom/panzoom": "^4.6.2",
42
+ "babylonjs-gltf2interface": "^9.11.0",
43
43
  "buffer": "^6.0.3",
44
44
  "idb": "^8.0.3",
45
45
  "is-svg": "^6.1.0",
@@ -10,74 +10,13 @@ import { MaterialData } from "./pref-viewer-3d-data.js";
10
10
  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
+ import { PrefViewerStyleVariables } from "./styles.js";
14
+ import { clamp01, lerp, getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
15
+ import { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
13
16
 
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
- }
17
+ // Re-export so existing consumers (tests, other modules) can keep importing from this file.
18
+ export { getAdaptiveHardwareScalingLevel, getLightingCycleProfile } from "./render-math.js";
19
+ export { markPrefViewerSimplifiedTransmissionMaterial, isPrefViewerPhysicalGlassMaterial, isPrefViewerTransparentRenderOrderMaterial } from "./material-utils.js";
81
20
 
82
21
  /**
83
22
  * BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
@@ -100,9 +39,10 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
100
39
  *
101
40
  * Runtime Flow
102
41
  * 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
103
- * 2. Call `enable()` to configure Draco, spin up the engine/scene, attach interaction + XR hooks, and start the render loop.
42
+ * 2. Call `enable()` to configure Draco, spin up the engine/scene, and attach interaction + XR hooks. The render loop
43
+ * does not start here; it starts automatically after the first `load()` completes.
104
44
  * 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
105
- * fetches sources, rebuilds containers, and reattaches pipelines.
45
+ * fetches sources, rebuilds containers, reattaches pipelines, and starts the render loop.
106
46
  * 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
107
47
  * `reloadWithCurrentSettings()` to apply the staged settings.
108
48
  * 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
@@ -119,10 +59,13 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
119
59
  * - getRenderSettings() / scheduleRenderSettingsReload(settings)
120
60
  * - setContainerVisibility(name, show)
121
61
  * - setMaterialOptions() / setCameraOptions() / setIBLOptions()
62
+ * - setIlluminationConfig(config)
63
+ * - getAnimations() / playAnimation(name, action) / setAnimationProgress(name, progress) / setAnimationLoop(name, loop)
122
64
  *
123
65
  * Key Invariants
124
- * - Asset containers must expose `setPendingWithCurrentStorage`/`setPending` before calling load/reload; the controller
125
- * reads those flags to resolve fresh sources and avoids touching the DOM until data is ready.
66
+ * - Asset containers must expose `setPendingWithCurrentStorage` before calling load/reload; the controller reads that
67
+ * flag to resolve fresh sources. Option objects (camera, materials, IBL) expose `setPendingWithCurrent` for the same
68
+ * purpose. Neither set of flags touches the DOM — they only gate resolver calls.
126
69
  * - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
127
70
  * only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
128
71
  * - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
@@ -156,7 +99,7 @@ export default class BabylonJSController {
156
99
  lightingTimeOfDay: 0.5,
157
100
  // Highlight settings
158
101
  highlightEnabled: true,
159
- highlightColor: "#ff6700",
102
+ highlightColor: PrefViewerStyleVariables.colorPrimary,
160
103
  };
161
104
 
162
105
  // Canvas HTML element
@@ -178,7 +121,7 @@ export default class BabylonJSController {
178
121
  #XRExperience = null;
179
122
  #canvasResizeObserver = null;
180
123
 
181
- #hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
124
+ #hdrTexture = null; // Reusable in-memory HDR source cloned into scene.environmentTexture across reloads
182
125
 
183
126
  #containers = {};
184
127
  #options = {};
@@ -231,30 +174,30 @@ export default class BabylonJSController {
231
174
  // Runtime configuration constants (tuning knobs, not per-frame state).
232
175
  #config = {
233
176
  pointerPicking: {
234
- movePickIntervalMs: 50, // cap expensive scene.pick calls to ~20 Hz while moving the pointer
235
- movePickMinDistancePx: 2, // skip picks for sub-pixel jitter
177
+ movePickIntervalMs: 50, // Cap expensive scene.pick calls to ~20 Hz while moving the pointer
178
+ movePickMinDistancePx: 2, // Skip picks for sub-pixel jitter
236
179
  },
237
180
  render: {
238
181
  burstFramesBase: 2,
239
- burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
182
+ burstFramesEnhanced: 32, // When AA/SSAO/IBL is enabled, more frames are needed to reach stable output
240
183
  interactionMs: 250,
241
184
  animationMs: 200,
242
185
  idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
243
186
  idleThresholdMs: 3000, // Time before going to true idle (no renders)
244
187
  },
245
188
  resize: {
246
- throttleMs: 50, // cap resize work to ~20 Hz while dragging/resizing containers
189
+ throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
247
190
  },
248
191
  // Hardware-adaptive quality settings (applied based on GPU capabilities)
249
192
  quality: {
250
193
  // Settings for different hardware tiers: 'low', 'medium', 'high'
251
194
  // Applied automatically based on GPU detected capabilities
252
- tier: 'medium', // default, will be updated after hardware detection
253
- shadowMapSize: 1024, // Standard shadow generator resolution
254
- iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
255
- iblSampleDirections: 4, // IBL shadow quality
256
- shadowBlurKernel: 16, // Shadow blur amount
257
- ssaoEnabled: true, // Screen Space Ambient Occlusion
195
+ tier: 'medium', // Default, will be updated after hardware detection
196
+ shadowMapSize: 1024, // Standard shadow generator resolution
197
+ iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
198
+ iblSampleDirections: 4, // IBL shadow quality
199
+ shadowBlurKernel: 16, // Shadow blur amount
200
+ ssaoEnabled: true, // Screen Space Ambient Occlusion
258
201
  },
259
202
  look: {
260
203
  showroom: {
@@ -378,23 +321,23 @@ export default class BabylonJSController {
378
321
  // High-end mobile patterns (recent flagship devices)
379
322
  const highEndPatterns = [
380
323
  /iPhone (1[4-9]|SE)/i, // iPhone 14+
381
- /iPad Pro/i, // iPad Pro
382
- /SM-S91[0-9]/i, // Samsung S21+
383
- /SM-S9[0-9]/i, // Samsung S20+
384
- /Pixel [7-9]/i, // Pixel 7+
385
- /OnePlus [8-9|10|11]/i, // OnePlus 8+
324
+ /iPad Pro/i, // iPad Pro
325
+ /SM-S91[0-9]/i, // Samsung S21+
326
+ /SM-S9[0-9]/i, // Samsung S20+
327
+ /Pixel [7-9]/i, // Pixel 7+
328
+ /OnePlus [8-9|10|11]/i, // OnePlus 8+
386
329
  /Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
387
- /Huawei (P50|Mate 4|)/i, // Huawei P50+
330
+ /Huawei (P50|Mate 4|)/i, // Huawei P50+
388
331
  ];
389
332
 
390
333
  // Mid-range patterns
391
334
  const midRangePatterns = [
392
- /iPhone (1[0-3]|SE)/i, // iPhone 10-13
335
+ /iPhone (1[0-3]|SE)/i, // iPhone 10-13
393
336
  /iPad Air/i, // iPad Air
394
- /SM-A[0-9]/i, // Samsung A series
395
- /Redmi Note/i, // Redmi Note
396
- /Pixel [4-6]/i, // Pixel 4-6
397
- /Moto G/i, // Moto G
337
+ /SM-A[0-9]/i, // Samsung A series
338
+ /Redmi Note/i, // Redmi Note
339
+ /Pixel [4-6]/i, // Pixel 4-6
340
+ /Moto G/i, // Moto G
398
341
  ];
399
342
 
400
343
  const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
@@ -446,13 +389,13 @@ export default class BabylonJSController {
446
389
  // Mobile low-end (old phones, budget devices)
447
390
  low: {
448
391
  shadowMapSize: 256, // Very small for speed
449
- iblShadowResolution: -1, // Disable IBL shadows
392
+ iblShadowResolution: -1, // Disable IBL shadows
450
393
  iblSampleDirections: 1, // Minimal sampling
451
394
  shadowBlurKernel: 0, // No blur
452
395
  ssaoEnabled: false,
453
396
  antialiasingEnabled: false,
454
- hardwareScaling: 2.5, // Aggressive downscaling
455
- maxModelResolution: 512, // Low-res model loading
397
+ hardwareScaling: 2.5, // Aggressive downscaling
398
+ maxModelResolution: 512, // Low-res model loading
456
399
  environmentTextureSize: 512,
457
400
  },
458
401
  // Mobile high-end / Tablet / Desktop low
@@ -470,7 +413,7 @@ export default class BabylonJSController {
470
413
  // Desktop mid-range / High-end mobile
471
414
  high: {
472
415
  shadowMapSize: 1024,
473
- iblShadowResolution: 0, // 1024x1024 - good balance
416
+ iblShadowResolution: 0, // 1024x1024 - good balance
474
417
  iblSampleDirections: 4, // Quality sampling
475
418
  shadowBlurKernel: 8, // Sharper shadows for product clarity
476
419
  ssaoEnabled: false,
@@ -482,9 +425,9 @@ export default class BabylonJSController {
482
425
  // Desktop high-end (RTX, Apple Silicon, etc.)
483
426
  ultra: {
484
427
  shadowMapSize: 2048,
485
- iblShadowResolution: 1, // 2048x2048
428
+ iblShadowResolution: 1, // 2048x2048
486
429
  iblSampleDirections: 8, // Maximum quality
487
- shadowBlurKernel: 16, // Preserve clarity over softness
430
+ shadowBlurKernel: 16, // Preserve clarity over softness
488
431
  ssaoEnabled: false,
489
432
  antialiasingEnabled: true,
490
433
  hardwareScaling: 1.0, // Native resolution
@@ -1627,7 +1570,7 @@ export default class BabylonJSController {
1627
1570
  }
1628
1571
  });
1629
1572
  }
1630
-
1573
+
1631
1574
  /**
1632
1575
  * Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
1633
1576
  * @private
@@ -2522,6 +2465,40 @@ export default class BabylonJSController {
2522
2465
  assetContainer.lights = [];
2523
2466
  }
2524
2467
 
2468
+ /**
2469
+ * Strips KHR_materials_transmission, KHR_materials_ior and KHR_materials_volume from
2470
+ * environment-container (scene) materials, converting them to simple alpha blending.
2471
+ * Babylon.js cannot render two physical-transmission materials (from separate GLTF
2472
+ * containers) through each other: the scene decorative glass (lamp cups, etc.) is
2473
+ * simplified so the model's physical glass keeps full correct transmission.
2474
+ * @private
2475
+ * @param {AssetContainer} assetContainer
2476
+ * @returns {void}
2477
+ */
2478
+ #assetContainer_simplifyTransmissiveMaterials(assetContainer) {
2479
+ assetContainer.materials?.forEach((mat) => {
2480
+ if (!(mat instanceof PBRMaterial)) return;
2481
+ if (!mat.subSurface?.isRefractionEnabled && !mat.subSurface?.isTranslucencyEnabled) return;
2482
+ this.#simplifyTransmissiveMaterial(mat);
2483
+ });
2484
+ }
2485
+
2486
+ /**
2487
+ * Converts a single PBRMaterial from physical KHR_materials_transmission to simple alpha
2488
+ * blending. Shared helper used by both the at-load simplification and the post-swap pass.
2489
+ * @private
2490
+ * @param {PBRMaterial} mat
2491
+ * @returns {void}
2492
+ */
2493
+ #simplifyTransmissiveMaterial(mat) {
2494
+ const intensity = mat.subSurface.refractionIntensity ?? 1.0;
2495
+ mat.subSurface.isRefractionEnabled = false;
2496
+ mat.subSurface.isTranslucencyEnabled = false;
2497
+ mat.alpha = Math.max(0.28, 1.0 - intensity * 0.65);
2498
+ mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
2499
+ markPrefViewerSimplifiedTransmissionMaterial(mat);
2500
+ }
2501
+
2525
2502
  /**
2526
2503
  * Assigns unique ids to every imported camera so Babylon.js does not reuse stale SSAO effects between reloads.
2527
2504
  * @private
@@ -2695,6 +2672,9 @@ export default class BabylonJSController {
2695
2672
  if (container.state.name === "model" || container.state.name === "environment") {
2696
2673
  this.#assetContainer_retagCameras(assetContainer);
2697
2674
  }
2675
+ if (container.state.name === "environment") {
2676
+ this.#assetContainer_simplifyTransmissiveMaterials(assetContainer);
2677
+ }
2698
2678
  this.#replaceContainer(container, assetContainer);
2699
2679
  container.state.setSuccess(true);
2700
2680
  } else {
@@ -3384,4 +3364,4 @@ export default class BabylonJSController {
3384
3364
  this.#requestRender({ frames: 2 });
3385
3365
  }
3386
3366
  }
3387
- }
3367
+ }
@@ -3,18 +3,12 @@ export const translations = {
3
3
  prefViewer: {
4
4
  menu3d: {
5
5
  title: "3D render settings",
6
- subtitle: "Toggle visual effects on or off.",
7
- pendingLabel: "Pending changes",
6
+ subtitle: "Modify the visual effects.",
8
7
  actions: {
9
- apply: "Apply",
10
- applying: "Applying...",
11
8
  play: "Play",
12
9
  pause: "Pause",
13
10
  },
14
- status: {
15
- error: "Couldn't apply the changes.",
16
- },
17
- switches: {
11
+ features: {
18
12
  antiAliasingEnabled: {
19
13
  label: "Antialiasing",
20
14
  helper: "Softens jagged edges and lines.",
@@ -70,18 +64,12 @@ export const translations = {
70
64
  prefViewer: {
71
65
  menu3d: {
72
66
  title: "Ajustes de render 3D",
73
- subtitle: "Activa o desactiva los efectos visuales.",
74
- pendingLabel: "Pendiente de aplicar",
67
+ subtitle: "Modifica los efectos visuales.",
75
68
  actions: {
76
- apply: "Aplicar",
77
- applying: "Aplicando...",
78
69
  play: "Reproducir",
79
70
  pause: "Pausar",
80
71
  },
81
- status: {
82
- error: "No se pudo aplicar los cambios.",
83
- },
84
- switches: {
72
+ features: {
85
73
  antiAliasingEnabled: {
86
74
  label: "Antialiasing",
87
75
  helper: "Suaviza aristas y líneas.",
@@ -100,7 +88,7 @@ export const translations = {
100
88
  },
101
89
  highlightEnabled: {
102
90
  label: "Resaltado",
103
- helper: "Resaltar elementos animados al pasar el ratón.",
91
+ helper: "Resalta elementos animados al pasar el ratón.",
104
92
  },
105
93
  highlightColor: {
106
94
  label: "Color de resaltado",
@@ -0,0 +1,72 @@
1
+ import { PBRMaterial } from "@babylonjs/core";
2
+
3
+ /**
4
+ * Utilities for classifying and tagging PBR materials by their transparency/transmission mode.
5
+ * Used by BabylonJSController at load time and by pref-viewer-3d for render-order decisions.
6
+ */
7
+
8
+ const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
9
+
10
+ /**
11
+ * Tags a PBR material as having been converted from physical KHR_materials_transmission to simple
12
+ * alpha blending. The tag is stored in `mat.metadata` under an internal key so that downstream
13
+ * render-order passes can still identify these materials as transparent candidates even after
14
+ * their `subSurface.isRefractionEnabled` flag has been cleared.
15
+ *
16
+ * Returns the same material instance (or the original value when `mat` is falsy) so calls can be
17
+ * chained.
18
+ * @param {import("@babylonjs/core").PBRMaterial|null|undefined} mat - Material to tag.
19
+ * @returns {import("@babylonjs/core").PBRMaterial|null|undefined} The same `mat` value that was passed in.
20
+ */
21
+ export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
22
+ if (!mat) {
23
+ return mat;
24
+ }
25
+
26
+ mat.metadata = {
27
+ ...(mat.metadata ?? {}),
28
+ [PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
29
+ };
30
+ return mat;
31
+ }
32
+
33
+ /**
34
+ * Returns true when the material uses Babylon.js physical glass rendering — i.e. either
35
+ * KHR_materials_transmission (refraction) or KHR_materials_translucency is active.
36
+ *
37
+ * This is intentionally narrower than `isPrefViewerTransparentRenderOrderMaterial`: it only
38
+ * matches full physical transmission, not plain alpha blending or simplified environment glass.
39
+ * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
40
+ * @returns {boolean}
41
+ */
42
+ export function isPrefViewerPhysicalGlassMaterial(mat) {
43
+ return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
44
+ }
45
+
46
+ /**
47
+ * Returns true when a material should be placed in the transparent rendering group (renderingGroupId=1).
48
+ *
49
+ * A material qualifies when any of the following is true:
50
+ * - It is a physical glass material (KHR_materials_transmission / translucency).
51
+ * - It has been tagged as simplified environment transmission via `markPrefViewerSimplifiedTransmissionMaterial`.
52
+ * - Its effective alpha is below 1, its `transparencyMode` is alpha-blend or alpha-test-and-blend,
53
+ * or it self-reports as needing blending/testing through the Babylon.js material API.
54
+ * @param {import("@babylonjs/core").Material|null|undefined} mat - Material to test.
55
+ * @returns {boolean}
56
+ */
57
+ export function isPrefViewerTransparentRenderOrderMaterial(mat) {
58
+ if (!mat) {
59
+ return false;
60
+ }
61
+ const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
62
+ const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
63
+ const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
64
+ const isAlphaBlended =
65
+ alpha < 1 ||
66
+ mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
67
+ mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
68
+ mat.needAlphaBlending?.() === true ||
69
+ mat.needAlphaTesting?.() === true;
70
+
71
+ return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
72
+ }
@@ -10,28 +10,32 @@ import FileStorage from "./file-storage.js";
10
10
  * - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
11
11
  * - Manages internal state for containers (model, environment, materials) and options (camera, materials, IBL, render toggles).
12
12
  * - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
13
- * - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can persistently toggle AA/SSAO/IBL/shadows.
13
+ * - Exposes getters/setters for render settings so PrefViewerMenu3D or other UIs can toggle AA/SSAO/IBL/shadows,
14
+ * hover highlight, and time-of-day lighting.
14
15
  * - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
15
16
  * - Emits custom events for loading, loaded, option-setting, and render-setting workflows.
16
17
  *
17
18
  * Usage:
18
19
  * - Use as a custom HTML element: <pref-viewer-3d ...>
19
20
  * - Configure via attributes (config, options, show-model, show-scene, visible).
20
- * - Call getRenderSettings()/applyRenderSettings() to synchronize UI toggles with BabylonJSController persistence.
21
+ * - Call getRenderSettings()/applyRenderSettings() to synchronize UI controls with BabylonJSController settings.
21
22
  * - Control viewer state and assets via public methods.
22
23
  *
23
24
  * Observed Attributes:
24
25
  * - show-model: Show or hide the 3D model ("true"/"false").
25
26
  * - show-scene: Show or hide the 3D environment ("true"/"false").
26
27
  * - visible: Show or hide the entire viewer ("true"/"false").
28
+ * - show-animation-menu: Show or hide animation controls in the 3D controller.
29
+ * - highlight-color: Hex color used for hover highlight on animated meshes.
30
+ * - highlight-enabled: Enable or disable hover highlight ("true"/"false").
27
31
  *
28
32
  * Public Methods:
29
33
  * - show(): Shows the 3D viewer component.
30
34
  * - hide(): Hides the 3D viewer component.
31
35
  * - load(config): Loads the provided configuration into the viewer.
32
36
  * - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
33
- * - getRenderSettings(): Returns the current anti-aliasing, SSAO, IBL, and shadow flags.
34
- * - applyRenderSettings(settings): Persists incoming render toggles, reloads if needed, and reports the result.
37
+ * - getRenderSettings(): Returns the current render settings snapshot, including highlight and lighting values.
38
+ * - applyRenderSettings(settings): Applies render settings, reloading only when a setting requires it.
35
39
  * - showModel(): Shows the 3D model.
36
40
  * - hideModel(): Hides the 3D model.
37
41
  * - showEnvironment(): Shows the 3D environment/scene.
@@ -110,6 +114,7 @@ export default class PrefViewer3D extends HTMLElement {
110
114
  * - "show-model": shows or hides the model according to the attribute value ("true"/"false").
111
115
  * - "show-scene": shows or hides the scene according to the attribute value ("true"/"false").
112
116
  * - "visible": shows or hides the component according to the attribute value ("true"/"false").
117
+ * - "show-animation-menu", "highlight-color", and "highlight-enabled": forward runtime UI/highlight settings to BabylonJSController.
113
118
  */
114
119
  attributeChangedCallback(name, _old, value) {
115
120
  switch (name) {
@@ -874,8 +879,9 @@ export default class PrefViewer3D extends HTMLElement {
874
879
 
875
880
  /**
876
881
  * Returns the current render settings used by the BabylonJSController.
882
+ * The snapshot is suitable for hydrating PrefViewerMenu3D controls.
877
883
  * @public
878
- * @returns {object|null} A shallow copy of the render settings or null if unavailable.
884
+ * @returns {object|null} A shallow copy of the render settings, including quality toggles, highlight state, and lighting time, or null if unavailable.
879
885
  */
880
886
  getRenderSettings() {
881
887
  if (!this.#babylonJSController) {
@@ -944,10 +950,20 @@ export default class PrefViewer3D extends HTMLElement {
944
950
  }
945
951
 
946
952
  /**
947
- * Applies render settings that require reloading the Babylon.js scene.
953
+ * Applies render settings received from PrefViewerMenu3D or another UI.
954
+ * Highlight and time-of-day lighting values are pushed directly to BabylonJSController; AA/SSAO/IBL/shadow changes
955
+ * are scheduled through the render-settings reload path when needed.
948
956
  * @public
949
- * @param {object} settings - Partial settings for anti-aliasing, SSAO, IBL, and shadows.
950
- * @returns {Promise<object>} Resolves with the reload summary.
957
+ * @param {object} settings - Partial render settings payload.
958
+ * @param {boolean} [settings.antiAliasingEnabled] - Enables or disables anti-aliasing.
959
+ * @param {boolean} [settings.ambientOcclusionEnabled] - Enables or disables SSAO.
960
+ * @param {boolean} [settings.iblEnabled] - Enables or disables image-based lighting.
961
+ * @param {boolean} [settings.shadowsEnabled] - Enables or disables scene shadows.
962
+ * @param {boolean|string} [settings.highlightEnabled] - Enables or disables hover highlight; string "false" is normalized to false.
963
+ * @param {string} [settings.highlightColor] - Hex color used for hover highlight.
964
+ * @param {number} [settings.lightingTimeOfDay] - Normalized 0..1 time-of-day value.
965
+ * @param {boolean} [settings.persist=true] - False for transient lighting updates such as playback frames.
966
+ * @returns {Promise<{changed: boolean, success: boolean, detail?: object, controller?: object}>} Resolves with the apply/reload summary.
951
967
  */
952
968
  async applyRenderSettings(settings) {
953
969
  if (!this.#babylonJSController) {