@preference-sl/pref-viewer 2.12.0 → 2.13.0-beta.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.12.0",
3
+ "version": "2.13.0-beta.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": "^8.39.2",
39
- "@babylonjs/loaders": "^8.39.2",
40
- "@babylonjs/serializers": "^8.39.2",
38
+ "@babylonjs/core": "^8.46.2",
39
+ "@babylonjs/loaders": "^8.46.2",
40
+ "@babylonjs/serializers": "^8.46.2",
41
41
  "@panzoom/panzoom": "^4.6.0",
42
- "babylonjs-gltf2interface": "^8.39.2",
42
+ "babylonjs-gltf2interface": "^8.46.2",
43
43
  "buffer": "^6.0.3",
44
44
  "idb": "^8.0.3",
45
45
  "is-svg": "^6.1.0",
@@ -1,4 +1,4 @@
1
- import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } 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";
@@ -44,7 +44,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
44
44
  * - load(): Reloads every pending asset container, re-applies options, and resolves with the loading summary { success, error }.
45
45
  * - setCameraOptions(): Applies the pending camera selection, reinstalls dependent pipelines, and restarts rendering safely.
46
46
  * - setMaterialOptions(): Re-applies all configured material overrides across visible containers and restarts rendering.
47
- * - setIBLOptions(): Pushes pending HDR/IBL updates, refreshes dependent effects, and resumes the render loop.
47
+ * - setIBLOptions(): Pushes pending HDR/IBL updates, refreshes dependent effects, resumes the render loop, and reports whether anything changed.
48
48
  * - setContainerVisibility(name, show): Toggles model/environment containers, syncing wall/floor helpers and component attributes.
49
49
  * - downloadGLB(content): Exports the selected scope (scene/model/environment) into a time-stamped GLB and triggers the download.
50
50
  * - downloadGLTF(content): Generates a glTF + BIN + textures ZIP for the requested scope, adding metadata comments for traceability.
@@ -62,9 +62,11 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
62
62
  * - #initializeVisualImprovements(): Reinstalls the default rendering pipeline (MSAA/FXAA/grain).
63
63
  * - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
64
64
  * - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
65
+ * - #addMeshToShadowGenerator(shadowGenerator, mesh): Adds eligible meshes to the provided shadow generator while honoring GLTF metadata.
65
66
  * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
66
67
  * - #initializeDefaultLightShadows(): Configures soft shadows when no HDR environment exists.
67
68
  * - #initializeEnvironmentShadows(): Rebuilds environment-provided shadow generators.
69
+ * - #ensureMeshesReceiveShadows(): Ensures every non-root mesh receives shadows unless metadata opts out.
68
70
  * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
69
71
  * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
70
72
  * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
@@ -284,7 +286,7 @@ export default class BabylonJSController {
284
286
  * @returns {void}
285
287
  */
286
288
  #createCamera() {
287
- this.#camera = new ArcRotateCamera("camera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
289
+ this.#camera = new ArcRotateCamera("defaultCamera", (3 * Math.PI) / 2, Math.PI * 0.47, 10, Vector3.Zero(), this.#scene);
288
290
  this.#camera.upperBetaLimit = Math.PI * 0.48;
289
291
  this.#camera.lowerBetaLimit = Math.PI * 0.25;
290
292
  this.#camera.lowerRadiusLimit = 5;
@@ -564,13 +566,18 @@ export default class BabylonJSController {
564
566
 
565
567
  this.#scene.meshes.forEach((mesh) => {
566
568
  const isRootMesh = mesh.id.startsWith("__root__");
567
- const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
568
- if (isRootMesh || isHDRIMesh) {
569
+ if (isRootMesh) {
569
570
  return false;
570
571
  }
571
- iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
572
- iblShadowsRenderPipeline.updateSceneBounds();
573
- mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
572
+
573
+ const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
574
+ const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
575
+ const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
576
+
577
+ if (meshGenerateShadows) {
578
+ iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
579
+ iblShadowsRenderPipeline.updateSceneBounds();
580
+ }
574
581
  });
575
582
 
576
583
  this.#scene.materials.forEach((material) => {
@@ -590,6 +597,29 @@ export default class BabylonJSController {
590
597
  }
591
598
  }
592
599
 
600
+ /**
601
+ * Adds the mesh to the provided shadow generator when it is eligible to cast shadows.
602
+ * Skips hidden Babylon root nodes, ignores HDRI domes, and honors the `castShadows` GLTF extra flag.
603
+ * @private
604
+ * @param {ShadowGenerator} shadowGenerator - Target generator that should receive the mesh.
605
+ * @param {AbstractMesh} mesh - Mesh to evaluate for shadow casting.
606
+ * @returns {boolean} True when the mesh is registered as a caster, otherwise false.
607
+ */
608
+ #addMeshToShadowGenerator(shadowGenerator, mesh) {
609
+ const isRootMesh = mesh.id.startsWith("__root__");
610
+ if (isRootMesh) {
611
+ return false;
612
+ }
613
+ const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
614
+ const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
615
+ const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
616
+ if (meshGenerateShadows) {
617
+ shadowGenerator.addShadowCaster(mesh, true);
618
+ return true;
619
+ }
620
+ return false;
621
+ }
622
+
593
623
  /**
594
624
  * Configures soft shadows for the built-in directional light used when no HDR environment is present.
595
625
  * @private
@@ -608,15 +638,7 @@ export default class BabylonJSController {
608
638
  shadowGenerator.bias = 0.0005;
609
639
  shadowGenerator.normalBias = 0.02;
610
640
  shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
611
- this.#scene.meshes.forEach((mesh) => {
612
- if (mesh.id.startsWith("__root__")) {
613
- return false;
614
- }
615
- if (mesh.name?.toLowerCase() !== "hdri") {
616
- shadowGenerator.addShadowCaster(mesh, true);
617
- }
618
- mesh.receiveShadows = true;
619
- });
641
+ this.#scene.meshes.forEach((mesh) => this.#addMeshToShadowGenerator(shadowGenerator, mesh));
620
642
  this.#shadowGen.push(shadowGenerator);
621
643
  }
622
644
 
@@ -648,18 +670,28 @@ export default class BabylonJSController {
648
670
  shadowGenerator.bias = 0.0005;
649
671
  shadowGenerator.normalBias = 0.02;
650
672
  shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
651
- this.#scene.meshes.forEach((mesh) => {
652
- if (mesh.id.startsWith("__root__")) {
653
- return false;
654
- }
655
- if (mesh.name?.toLowerCase() !== "hdri") {
656
- shadowGenerator.addShadowCaster(mesh, true);
657
- }
658
- });
673
+ this.#scene.meshes.forEach((mesh) => this.#addMeshToShadowGenerator(shadowGenerator, mesh));
659
674
  this.#shadowGen.push(shadowGenerator);
660
675
  });
661
676
  }
662
677
 
678
+ /**
679
+ * Marks every non-root mesh as shadow receiving, unless GLTF metadata explicitly disables it.
680
+ * Ensures standard shadow generators work even when extras are missing, while still honoring `receiveShadows` overrides.
681
+ * @private
682
+ * @returns {void}
683
+ */
684
+ #ensureMeshesReceiveShadows() {
685
+ this.#scene.meshes.forEach((mesh) => {
686
+ const isRootMesh = mesh.id.startsWith("__root__");
687
+ if (isRootMesh) {
688
+ return false;
689
+ }
690
+ const extrasReceiveShadows = mesh.metadata?.gltf?.extras?.receiveShadows;
691
+ mesh.receiveShadows = typeof extrasReceiveShadows === "boolean" ? extrasReceiveShadows : true; // Not necessary for IBL shadows, but yes for standard shadows
692
+ });
693
+ }
694
+
663
695
  /**
664
696
  * Initializes shadows for the Babylon.js scene.
665
697
  * @private
@@ -672,6 +704,9 @@ export default class BabylonJSController {
672
704
  if (!this.#settings.shadowsEnabled) {
673
705
  return false;
674
706
  }
707
+
708
+ this.#ensureMeshesReceiveShadows();
709
+
675
710
  if (this.#scene.environmentTexture) {
676
711
  if (this.#options.ibl.shadows) {
677
712
  if (this.#scene.environmentTexture.isReady()) {
@@ -834,12 +869,23 @@ export default class BabylonJSController {
834
869
  * @returns {void|false} Returns false if there is no active camera; otherwise, void.
835
870
  */
836
871
  #onMouseWheel(event, pickInfo) {
837
- if (!this.#scene?.activeCamera) {
872
+ const camera = this.#scene?.activeCamera;
873
+ if (!camera) {
838
874
  return false;
839
875
  }
840
- //this.#scene.activeCamera.target = pickInfo.hit ? pickInfo.pickedPoint.clone() : this.#scene.activeCamera.target;
841
- if (!this.#scene.activeCamera.metadata?.locked) {
842
- this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
876
+ if (!camera.metadata?.locked) {
877
+ if (camera instanceof ArcRotateCamera) {
878
+ camera.wheelPrecision = camera.wheelPrecision || 3.0;
879
+ camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
880
+ } else if (camera instanceof FreeCamera || camera instanceof UniversalCamera) {
881
+ camera.wheelPrecision = camera.wheelPrecision || 3.0;
882
+ const zoomSpeed = -event.deltaY * camera.wheelPrecision * 0.001;
883
+
884
+ const target = camera.target || Vector3.Zero();
885
+ const direction = target.subtract(camera.position).normalize();
886
+ const movementVector = direction.scale(zoomSpeed);
887
+ camera.position = camera.position.add(movementVector);
888
+ }
843
889
  }
844
890
  event.preventDefault();
845
891
  }
@@ -995,7 +1041,8 @@ export default class BabylonJSController {
995
1041
  }
996
1042
  }
997
1043
  this.#scene.activeCamera?.detachControl();
998
- if (!cameraState.locked && cameraState.value !== null) {
1044
+ camera.detachControl();
1045
+ if (!cameraState.locked) {
999
1046
  camera.attachControl(this.#canvas, true);
1000
1047
  }
1001
1048
  this.#scene.activeCamera = camera;
@@ -1210,6 +1257,9 @@ export default class BabylonJSController {
1210
1257
  compileMaterials: true,
1211
1258
  loadAllMaterials: true,
1212
1259
  loadOnlyMaterials: container.state.name === "materials",
1260
+ extensionOptions: {
1261
+ ExtrasAsMetadata: { enabled: true },
1262
+ },
1213
1263
  },
1214
1264
  },
1215
1265
  };
@@ -1288,7 +1338,6 @@ export default class BabylonJSController {
1288
1338
  this.#setMaxSimultaneousLights();
1289
1339
  this.#loadCameraDepentEffects();
1290
1340
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
1291
- this.#forceReflectionsInModelGlasses();
1292
1341
  this.#startRender();
1293
1342
  });
1294
1343
  return detail;
@@ -1382,28 +1431,6 @@ export default class BabylonJSController {
1382
1431
  }
1383
1432
  }
1384
1433
 
1385
- /**
1386
- * Configures realistic glass material properties for all materials whose names include "Glass".
1387
- * @private
1388
- * @param {AssetContainer} [assetContainer] - The asset container with materials to process. If undefined, uses the model container.
1389
- * @returns {void}
1390
- * @note This method assumes that glass materials are named with the substring "Glass".
1391
- */
1392
- #forceReflectionsInModelGlasses(assetContainer) {
1393
- if (assetContainer === undefined) {
1394
- assetContainer = this.#containers.model.assetContainer;
1395
- }
1396
- if (!this.#scene || !assetContainer) {
1397
- return;
1398
- }
1399
- assetContainer.materials?.forEach(material => {
1400
- if (material && material.name && material.name.includes("Glass")) {
1401
- material.metallic = 0.25;
1402
- material.environmentIntensity = 1.0;
1403
- }
1404
- });
1405
- }
1406
-
1407
1434
  /**
1408
1435
  * Translates a node along the scene's vertical (Y) axis by the provided value.
1409
1436
  * @private
@@ -138,7 +138,8 @@ export class MaterialData {
138
138
  * - Access status via isPending, isSuccess getters.
139
139
  */
140
140
  export class CameraData {
141
- constructor(name = "", value = null, locked = true) {
141
+ defaultLocked = true;
142
+ constructor(name = "", value = null, locked = this.defaultLocked) {
142
143
  this.name = name;
143
144
  this.value = value;
144
145
  this.locked = locked;
@@ -161,7 +162,7 @@ export class CameraData {
161
162
  this.update.success = false;
162
163
  }
163
164
  }
164
- setPending(value = undefined, locked = true) {
165
+ setPending(value = undefined, locked = this.defaultLocked) {
165
166
  this.reset();
166
167
  if (value === undefined) {
167
168
  return;