@preference-sl/pref-viewer 2.16.0-beta.9 → 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.9",
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,104 +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
- const PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY = "prefViewerSimplifiedTransmission";
55
-
56
- export function markPrefViewerSimplifiedTransmissionMaterial(mat) {
57
- if (!mat) return mat;
58
- mat.metadata = {
59
- ...(mat.metadata ?? {}),
60
- [PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY]: true,
61
- };
62
- return mat;
63
- }
64
-
65
- export function isPrefViewerTransparentRenderOrderMaterial(mat) {
66
- if (!mat) return false;
67
- const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
68
- const isSimplifiedTransmission = mat.metadata?.[PREF_VIEWER_SIMPLIFIED_TRANSMISSION_METADATA_KEY] === true;
69
- const alpha = typeof mat.alpha === "number" ? mat.alpha : 1;
70
- const isAlphaBlended =
71
- alpha < 1 ||
72
- mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHABLEND ||
73
- mat.transparencyMode === PBRMaterial.PBRMATERIAL_ALPHATESTANDBLEND ||
74
- mat.needAlphaBlending?.() === true ||
75
- mat.needAlphaTesting?.() === true;
76
-
77
- return Boolean(isTransmissive || isSimplifiedTransmission || isAlphaBlended);
78
- }
79
-
80
- export function isPrefViewerPhysicalGlassMaterial(mat) {
81
- return Boolean(mat?.subSurface?.isRefractionEnabled || mat?.subSurface?.isTranslucencyEnabled);
82
- }
83
-
84
- export function getLightingCycleProfile(timeOfDay = 0.5) {
85
- const normalizedTime = clamp01(timeOfDay);
86
- const timeHours = normalizedTime * 24;
87
- const sunriseHour = 6.25;
88
- const sunsetHour = 20.25;
89
- const daylightProgress = clamp01((timeHours - sunriseHour) / (sunsetHour - sunriseHour));
90
- const dayFactor = timeHours >= sunriseHour && timeHours <= sunsetHour ? Math.pow(bellCurve(daylightProgress), 0.88) : 0;
91
- const nightFactor = 1 - dayFactor;
92
- const solarArc = dayFactor > 0 ? Math.pow(dayFactor, 0.72) : 0;
93
- const solarAzimuth = lerp(-Math.PI * 0.82, Math.PI * 0.82, daylightProgress);
94
- const solarElevation = lerp(-0.18, 1.18, solarArc);
95
- const sunPhase = normalizedTime * Math.PI * 2 - Math.PI / 2;
96
- const sunDirection = normalizeDirection(-Math.cos(sunPhase), -Math.sin(sunPhase), -0.35);
97
- const moonDirection = normalizeDirection(-sunDirection.x, -sunDirection.y, -sunDirection.z);
98
-
99
- return {
100
- timeOfDay: normalizedTime,
101
- timeHours,
102
- sunPhase,
103
- solarAzimuth,
104
- solarElevation,
105
- dayFactor,
106
- nightFactor,
107
- sunDirection,
108
- moonDirection,
109
- };
110
- }
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";
111
20
 
112
21
  /**
113
22
  * BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
@@ -130,9 +39,10 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
130
39
  *
131
40
  * Runtime Flow
132
41
  * 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
133
- * 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.
134
44
  * 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
135
- * fetches sources, rebuilds containers, and reattaches pipelines.
45
+ * fetches sources, rebuilds containers, reattaches pipelines, and starts the render loop.
136
46
  * 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
137
47
  * `reloadWithCurrentSettings()` to apply the staged settings.
138
48
  * 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
@@ -149,10 +59,13 @@ export function getLightingCycleProfile(timeOfDay = 0.5) {
149
59
  * - getRenderSettings() / scheduleRenderSettingsReload(settings)
150
60
  * - setContainerVisibility(name, show)
151
61
  * - setMaterialOptions() / setCameraOptions() / setIBLOptions()
62
+ * - setIlluminationConfig(config)
63
+ * - getAnimations() / playAnimation(name, action) / setAnimationProgress(name, progress) / setAnimationLoop(name, loop)
152
64
  *
153
65
  * Key Invariants
154
- * - Asset containers must expose `setPendingWithCurrentStorage`/`setPending` before calling load/reload; the controller
155
- * 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.
156
69
  * - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
157
70
  * only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
158
71
  * - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
@@ -186,7 +99,7 @@ export default class BabylonJSController {
186
99
  lightingTimeOfDay: 0.5,
187
100
  // Highlight settings
188
101
  highlightEnabled: true,
189
- highlightColor: "#ff6700",
102
+ highlightColor: PrefViewerStyleVariables.colorPrimary,
190
103
  };
191
104
 
192
105
  // Canvas HTML element
@@ -208,7 +121,7 @@ export default class BabylonJSController {
208
121
  #XRExperience = null;
209
122
  #canvasResizeObserver = null;
210
123
 
211
- #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
212
125
 
213
126
  #containers = {};
214
127
  #options = {};
@@ -261,30 +174,30 @@ export default class BabylonJSController {
261
174
  // Runtime configuration constants (tuning knobs, not per-frame state).
262
175
  #config = {
263
176
  pointerPicking: {
264
- movePickIntervalMs: 50, // cap expensive scene.pick calls to ~20 Hz while moving the pointer
265
- 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
266
179
  },
267
180
  render: {
268
181
  burstFramesBase: 2,
269
- 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
270
183
  interactionMs: 250,
271
184
  animationMs: 200,
272
185
  idleThrottleMs: 1000 / 8, // Reduced from 15 FPS to 8 FPS for idle - reduces GPU/CPU load
273
186
  idleThresholdMs: 3000, // Time before going to true idle (no renders)
274
187
  },
275
188
  resize: {
276
- 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
277
190
  },
278
191
  // Hardware-adaptive quality settings (applied based on GPU capabilities)
279
192
  quality: {
280
193
  // Settings for different hardware tiers: 'low', 'medium', 'high'
281
194
  // Applied automatically based on GPU detected capabilities
282
- tier: 'medium', // default, will be updated after hardware detection
283
- shadowMapSize: 1024, // Standard shadow generator resolution
284
- iblShadowResolution: 1, // IBL shadow resolution exponent (0=1024, 1=2048, 2=4096)
285
- iblSampleDirections: 4, // IBL shadow quality
286
- shadowBlurKernel: 16, // Shadow blur amount
287
- 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
288
201
  },
289
202
  look: {
290
203
  showroom: {
@@ -408,23 +321,23 @@ export default class BabylonJSController {
408
321
  // High-end mobile patterns (recent flagship devices)
409
322
  const highEndPatterns = [
410
323
  /iPhone (1[4-9]|SE)/i, // iPhone 14+
411
- /iPad Pro/i, // iPad Pro
412
- /SM-S91[0-9]/i, // Samsung S21+
413
- /SM-S9[0-9]/i, // Samsung S20+
414
- /Pixel [7-9]/i, // Pixel 7+
415
- /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+
416
329
  /Xiaomi (1[2-4]|Mi 1[1-4])/i, // Xiaomi 12+
417
- /Huawei (P50|Mate 4|)/i, // Huawei P50+
330
+ /Huawei (P50|Mate 4|)/i, // Huawei P50+
418
331
  ];
419
332
 
420
333
  // Mid-range patterns
421
334
  const midRangePatterns = [
422
- /iPhone (1[0-3]|SE)/i, // iPhone 10-13
335
+ /iPhone (1[0-3]|SE)/i, // iPhone 10-13
423
336
  /iPad Air/i, // iPad Air
424
- /SM-A[0-9]/i, // Samsung A series
425
- /Redmi Note/i, // Redmi Note
426
- /Pixel [4-6]/i, // Pixel 4-6
427
- /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
428
341
  ];
429
342
 
430
343
  const isHighEnd = highEndPatterns.some(p => p.test(userAgent));
@@ -476,13 +389,13 @@ export default class BabylonJSController {
476
389
  // Mobile low-end (old phones, budget devices)
477
390
  low: {
478
391
  shadowMapSize: 256, // Very small for speed
479
- iblShadowResolution: -1, // Disable IBL shadows
392
+ iblShadowResolution: -1, // Disable IBL shadows
480
393
  iblSampleDirections: 1, // Minimal sampling
481
394
  shadowBlurKernel: 0, // No blur
482
395
  ssaoEnabled: false,
483
396
  antialiasingEnabled: false,
484
- hardwareScaling: 2.5, // Aggressive downscaling
485
- maxModelResolution: 512, // Low-res model loading
397
+ hardwareScaling: 2.5, // Aggressive downscaling
398
+ maxModelResolution: 512, // Low-res model loading
486
399
  environmentTextureSize: 512,
487
400
  },
488
401
  // Mobile high-end / Tablet / Desktop low
@@ -500,7 +413,7 @@ export default class BabylonJSController {
500
413
  // Desktop mid-range / High-end mobile
501
414
  high: {
502
415
  shadowMapSize: 1024,
503
- iblShadowResolution: 0, // 1024x1024 - good balance
416
+ iblShadowResolution: 0, // 1024x1024 - good balance
504
417
  iblSampleDirections: 4, // Quality sampling
505
418
  shadowBlurKernel: 8, // Sharper shadows for product clarity
506
419
  ssaoEnabled: false,
@@ -512,9 +425,9 @@ export default class BabylonJSController {
512
425
  // Desktop high-end (RTX, Apple Silicon, etc.)
513
426
  ultra: {
514
427
  shadowMapSize: 2048,
515
- iblShadowResolution: 1, // 2048x2048
428
+ iblShadowResolution: 1, // 2048x2048
516
429
  iblSampleDirections: 8, // Maximum quality
517
- shadowBlurKernel: 16, // Preserve clarity over softness
430
+ shadowBlurKernel: 16, // Preserve clarity over softness
518
431
  ssaoEnabled: false,
519
432
  antialiasingEnabled: true,
520
433
  hardwareScaling: 1.0, // Native resolution
@@ -1657,82 +1570,7 @@ export default class BabylonJSController {
1657
1570
  }
1658
1571
  });
1659
1572
  }
1660
-
1661
- /**
1662
- * Fixes cross-GLTF transparency ordering. Detects transparent/transmissive meshes
1663
- * (alpha-blended, alpha-tested, simplified environment transmission, or
1664
- * KHR_materials_transmission) across all loaded containers and moves them to
1665
- * renderingGroupId=1 with depth-write disabled.
1666
- * This ensures opaque scene geometry (e.g. lamp cups behind a model window) is
1667
- * written to the depth buffer first (group 0) so the glass layer composites on top
1668
- * without occluding it.
1669
- * @private
1670
- * @returns {void}
1671
- */
1672
- #applyTransmittanceRenderOrder() {
1673
- if (!this.#scene) return;
1674
-
1675
- const envMeshIds = new Set(
1676
- this.#containers.environment?.assetContainer?.meshes.map((m) => m.uniqueId) ?? []
1677
- );
1678
- const modelMeshIds = new Set(
1679
- this.#containers.model?.assetContainer?.meshes.map((m) => m.uniqueId) ?? []
1680
- );
1681
-
1682
- // Map each material to the model meshes using it — needed to detect shared materials
1683
- // before modifying an env mesh's material (must clone to avoid breaking model glass).
1684
- const matToModelMeshes = new Map();
1685
- this.#scene.meshes.forEach((mesh) => {
1686
- if (!modelMeshIds.has(mesh.uniqueId) || !mesh.material) return;
1687
- if (!matToModelMeshes.has(mesh.material)) matToModelMeshes.set(mesh.material, new Set());
1688
- matToModelMeshes.get(mesh.material).add(mesh.uniqueId);
1689
- });
1690
-
1691
- this.#scene.meshes.forEach((mesh) => {
1692
- const mat = mesh.material;
1693
- if (!mat) return;
1694
- const isTransmissive = isPrefViewerPhysicalGlassMaterial(mat);
1695
- const isTransparentLike = isPrefViewerTransparentRenderOrderMaterial(mat);
1696
- if (!isTransparentLike) return;
1697
-
1698
- if (envMeshIds.has(mesh.uniqueId)) {
1699
- // Environment mesh carrying a transmissive material introduced by a material swap
1700
- // (initial-load materials are pre-simplified in #assetContainer_simplifyTransmissiveMaterials).
1701
- // Clone if shared with a model mesh so the model's physical glass is not affected.
1702
- let targetMat = mat;
1703
- if (matToModelMeshes.has(mat)) {
1704
- targetMat = mat.clone(`${mat.name}_env_simplified`);
1705
- mesh.material = targetMat;
1706
- }
1707
- if (isTransmissive) {
1708
- this.#simplifyTransmissiveMaterial(targetMat);
1709
- }
1710
- mesh.renderingGroupId = 1;
1711
- targetMat.disableDepthWrite = true;
1712
- targetMat.alphaIndex = 0;
1713
- } else if (modelMeshIds.has(mesh.uniqueId)) {
1714
- // Model transparent materials render after all group-0 geometry.
1715
- mesh.renderingGroupId = 1;
1716
- mat.disableDepthWrite = true;
1717
-
1718
- // Physical model glass keeps transmission, but strips volume absorption (tintColor)
1719
- // and roughness because those blur/darken transmitted geometry. Do not apply this
1720
- // tuning to plain alpha-blended model materials: render-order and physical glass
1721
- // material tuning are separate concerns.
1722
- if (isTransmissive && mat.subSurface) {
1723
- mat.subSurface.tintColor = Color3.White();
1724
- mat.roughness = 0;
1725
- }
1726
- mat.alphaIndex = 1;
1727
- } else {
1728
- // Unknown container (future-proof safe default).
1729
- mesh.renderingGroupId = 1;
1730
- mat.disableDepthWrite = true;
1731
- mat.alphaIndex = 1;
1732
- }
1733
- });
1734
- }
1735
-
1573
+
1736
1574
  /**
1737
1575
  * Applies the showroom tonemapping curve and lighting bias used by the 3D viewer.
1738
1576
  * @private
@@ -2863,7 +2701,6 @@ export default class BabylonJSController {
2863
2701
  this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
2864
2702
  this.#setMaxSimultaneousLights();
2865
2703
  this.#enhanceTextureQuality();
2866
- this.#applyTransmittanceRenderOrder();
2867
2704
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
2868
2705
  // Apply stored highlight settings so fresh page loads respect persisted state
2869
2706
  this.#setHighlightConfig({
@@ -3346,7 +3183,6 @@ export default class BabylonJSController {
3346
3183
  setMaterialOptions() {
3347
3184
  this.#stopRender();
3348
3185
  const materialsOptionsSetted = this.#setOptions_Materials();
3349
- this.#applyTransmittanceRenderOrder();
3350
3186
  this.#startRender();
3351
3187
  return materialsOptionsSetted;
3352
3188
  }
@@ -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) {