@combeenation/3d-viewer 6.2.0 → 6.3.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": "@combeenation/3d-viewer",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Combeenation 3D Viewer",
5
5
  "homepage": "https://github.com/Combeenation/3d-viewer#readme",
6
6
  "bugs": {
@@ -24,8 +24,8 @@
24
24
  "src"
25
25
  ],
26
26
  "scripts": {
27
- "clean-dist": "rimraf dist",
28
27
  "bundle-analyzer": "npm run generate-profile && webpack-bundle-analyzer dist/webpack-stats.json dist/lib-full",
28
+ "clean-dist": "rimraf dist",
29
29
  "dev": "cross-env NODE_ENV='dev' webpack serve --config build/webpack.conf.js --progress --hot",
30
30
  "dist-cjs": "npm run clean-dist && npm run lint && tsc --project commonjs.tsconfig.json && npm run replace-version",
31
31
  "dist-es6": "npm run clean-dist && npm run lint && tsc --project es6.tsconfig.json && npm run replace-version",
@@ -441,7 +441,7 @@ export class Element extends VariantParameterizable {
441
441
  this._parameterObservers.set(Parameter.MATERIAL, [
442
442
  async (element: Element, oldValue: ParameterValue, newValue: ParameterValue) => {
443
443
  const materialName = newValue.toString();
444
- element.nodes.forEach(node => {
444
+ for (const node of element.nodes) {
445
445
  assertTransformNode(node, (node: AbstractMesh) => {
446
446
  if (node instanceof InstancedMesh) {
447
447
  throw new Error(
@@ -451,8 +451,8 @@ export class Element extends VariantParameterizable {
451
451
  );
452
452
  }
453
453
  });
454
- setMaterial(element.variant, node, materialName);
455
- });
454
+ setMaterial(element.variant.viewer.scene, node, materialName, true, element.variant);
455
+ }
456
456
  },
457
457
  ]);
458
458
  this._parameterObservers.set(Parameter.MATERIAL_COLOR, [
@@ -1,6 +1,5 @@
1
1
  import { deactivateTransformNode, getDottedPathForNode, injectNodeMetadata } from '../util/babylonHelper';
2
2
  import { loadJson, mergeMaps } from '../util/resourceHelper';
3
- import { createMaterialFromCbnAssets } from '../util/sceneLoaderHelper';
4
3
  import { DottedPath } from './dottedPath';
5
4
  import { Element } from './element';
6
5
  import { Event } from './event';
@@ -421,31 +420,6 @@ export class Variant extends Parameterizable {
421
420
  return element.getMesh(meshDottedPath);
422
421
  }
423
422
 
424
- /**
425
- * Gets the Material defined in one of the variants glTFs by its id.
426
- */
427
- public getOrCreateMaterial(id: string): Material {
428
- const scene = this.viewer.scene;
429
-
430
- for (const material of this.inheritedMaterials) {
431
- if (material.id === id) {
432
- return material;
433
- }
434
- }
435
- // fallback to dynamically created materials on scene
436
- for (const material of scene.materials) {
437
- if (material.id === id) {
438
- return material;
439
- }
440
- }
441
- const cbnAssetMaterial = createMaterialFromCbnAssets(id, scene);
442
- if (cbnAssetMaterial) {
443
- return cbnAssetMaterial;
444
- }
445
-
446
- throw new Error(`Material with id "${id}" does not exist for variant "${this.id}".`);
447
- }
448
-
449
423
  /**
450
424
  * Creates a living clone of this {@link Variant}. Will clone all parent {@link Variant}s in tree.
451
425
  *
@@ -65,6 +65,15 @@ export class Viewer extends EventBroadcaster {
65
65
 
66
66
  static version = buildInfo.version;
67
67
 
68
+ // these are constants for calculating the camera settings, depending on the bounding box size of the meshes to zoom
69
+ // the algorithms and constant values are directly taken from the BabylonJS repository
70
+ private static readonly _autofocusConstants = {
71
+ minZ: 100,
72
+ wheelPrecision: 100,
73
+ panningSensibility: 5000,
74
+ pinchPrecision: 200,
75
+ };
76
+
68
77
  /**
69
78
  * Help function for automatically creating a valid Spec from a list of variant names and dedicated GLB/Babylon URLs.
70
79
  * This data is most likely coming from babylon assets from the Combeenation system but other sources are also valid.
@@ -461,12 +470,40 @@ export class Viewer extends EventBroadcaster {
461
470
  // get bounding box of all visible meshes, this is the base for the autofocus algorithm
462
471
  const boundingBox = await this.calculateBoundingBox(exclude);
463
472
 
464
- // focus the helper camera and set the calculated camera data to the real camera
465
- const helperCamera = this.getFocusedHelperCamera(boundingBox, settings);
466
- await this.applyFocusedHelperCameraData(activeCamera, helperCamera, settings);
473
+ const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
474
+ const center = boundingBox.getBoundingInfo().boundingSphere.center;
475
+ const diameter = radius * 2;
476
+
477
+ // set lower radius limit on edge of bounding sphere to make sure that we can't dive into the meshes
478
+ activeCamera.lowerRadiusLimit = radius;
467
479
 
468
- // remove the helper camera
469
- helperCamera.dispose();
480
+ // additional settings
481
+ // constants for division are taken directly from BabylonJS repository
482
+ activeCamera.minZ = Math.min(radius / Viewer._autofocusConstants.minZ, 1);
483
+ if (settings?.adjustWheelPrecision !== false) {
484
+ activeCamera.wheelPrecision = Viewer._autofocusConstants.wheelPrecision / radius;
485
+ }
486
+ if (settings?.adjustPanningSensibility !== false) {
487
+ activeCamera.panningSensibility = Viewer._autofocusConstants.panningSensibility / diameter;
488
+ }
489
+ if (settings?.adjustPinchPrecision !== false) {
490
+ activeCamera.pinchPrecision = Viewer._autofocusConstants.pinchPrecision / radius;
491
+ }
492
+
493
+ const radiusFactor = settings?.radiusFactor ?? 1;
494
+ const alpha = (settings?.alpha ?? -90) * (Math.PI / 180);
495
+ const beta = (settings?.beta ?? 90) * (Math.PI / 180);
496
+
497
+ const newCameraPosition = {
498
+ alpha: alpha,
499
+ beta: beta,
500
+ // this calculation is a bit "simplified", as it doesn't consider the viewport ratio or the frustum angle
501
+ // but it's also done this way in the BabylonJs repository, so it should be fine for us
502
+ radius: diameter * radiusFactor,
503
+ target: center,
504
+ };
505
+
506
+ await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation);
470
507
  }
471
508
 
472
509
  /**
@@ -629,7 +666,10 @@ export class Viewer extends EventBroadcaster {
629
666
  const setupJson = SpecStorage.get<SetupJson>('setup');
630
667
  const instances: VariantInstance[] = [];
631
668
  for (const instanceDefinition of setupJson.instances) {
632
- if (instanceDefinition.lazy) {
669
+ // don't create the instance right away if `lazy` is set, register it for later creation (on first usage) instead
670
+ // however if the variant should be `visible` by default, `lazy` loading doesn't make sense and should therefore
671
+ // be overruled by the `visible` flag
672
+ if (instanceDefinition.lazy && instanceDefinition.parameters?.visible !== true) {
633
673
  this.variantInstances.register(instanceDefinition);
634
674
  continue;
635
675
  }
@@ -643,78 +683,4 @@ export class Viewer extends EventBroadcaster {
643
683
  }
644
684
  return instances;
645
685
  }
646
-
647
- /**
648
- * Help function for focusing a helper camera exactly onto the given bounding box
649
- */
650
- private getFocusedHelperCamera(boundingBox: Mesh, settings?: AutofocusSettings): ArcRotateCamera {
651
- // use helper camera to get some default values and set the values of the real camera accordingly
652
- const helperCamera = new ArcRotateCamera(
653
- '__helper_camera__',
654
- 0, // camera angles will be overwritten after the target has been set
655
- 0,
656
- 0, // radius will be calculated, so we can set to 0 here
657
- Vector3.Zero(),
658
- this.scene
659
- );
660
- // this is required for automatically calculating the `lowerRadiusLimit`, so that we don't "dive" into meshes
661
- // see https://doc.babylonjs.com/divingDeeper/behaviors/cameraBehaviors#framing-behavior
662
- helperCamera.useFramingBehavior = true;
663
-
664
- // `minZ` is the camera distance beyond which the mesh will be clipped
665
- // this should be very low, but can't be zero
666
- // a good value seems to be 1% of the bounding box size (= radius), whereas the value shouldn't go above 1, which is
667
- // also the default value
668
- const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
669
- helperCamera.minZ = Math.min(radius / 100, 1);
670
-
671
- // set desired camera data, these won't be changed by the autofocus function!
672
- // default values should focus the element exactly from the front (= XY Plane)
673
- helperCamera.setTarget(boundingBox, true);
674
- helperCamera.alpha = (settings?.alpha ?? -90) * (Math.PI / 180);
675
- helperCamera.beta = (settings?.beta ?? 90) * (Math.PI / 180);
676
-
677
- // finally zoom to the bounding box
678
- // also apply a zoom factor, this adjusts the borders around the model in the viewport
679
- helperCamera.zoomOnFactor = settings?.radiusFactor || 1;
680
- helperCamera.zoomOn([boundingBox], true);
681
-
682
- return helperCamera;
683
- }
684
-
685
- /**
686
- * Help function for applying the relevant data of the focused helper camera to the real camera
687
- */
688
- private async applyFocusedHelperCameraData(
689
- activeCamera: ArcRotateCamera,
690
- helperCamera: ArcRotateCamera,
691
- settings?: AutofocusSettings
692
- ) {
693
- // limits
694
- activeCamera.minZ = helperCamera.minZ;
695
- activeCamera.maxZ = helperCamera.maxZ;
696
- activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
697
- activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
698
-
699
- // additional settings
700
- if (settings?.adjustWheelPrecision !== false) {
701
- activeCamera.wheelPrecision = helperCamera.wheelPrecision;
702
- }
703
- if (settings?.adjustPanningSensibility !== false) {
704
- activeCamera.panningSensibility = helperCamera.panningSensibility;
705
- }
706
- if (settings?.adjustPinchPrecision !== false) {
707
- activeCamera.pinchPrecision = helperCamera.pinchPrecision;
708
- }
709
-
710
- // finally move the camera
711
- // do this at last, so that all camera settings are already considered
712
- const newCameraPosition: PlacementDefinition = {
713
- alpha: helperCamera.alpha,
714
- beta: helperCamera.beta,
715
- radius: helperCamera.radius,
716
- target: helperCamera.target,
717
- };
718
- await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation);
719
- }
720
686
  }
@@ -121,7 +121,7 @@ const sceneSetup = async function (engine: Engine, sceneJson: SceneJson): Promis
121
121
  cameras.push(camera);
122
122
  }
123
123
  // grounds
124
- const groundDefinitions = get(sceneJson.scene, 'grounds') as GroundDefinitions;
124
+ const groundDefinitions = get(sceneJson.scene, 'grounds', {}) as GroundDefinitions;
125
125
  if (!isEmpty(groundDefinitions)) {
126
126
  for (const groundName in groundDefinitions) {
127
127
  await processGround(scene, groundName, groundDefinitions[groundName]);
@@ -1,6 +1,10 @@
1
1
  import { DottedPath } from '../classes/dottedPath';
2
2
  import { defaultEnvHelperColor, defaultSceneClearColor } from '../internal/sceneSetup';
3
- import { addMissingMaterialObserver, missingMaterialMetadataName } from './sceneLoaderHelper';
3
+ import {
4
+ addMissingMaterialObserver,
5
+ createMaterialFromCbnAssets,
6
+ missingMaterialMetadataName,
7
+ } from './sceneLoaderHelper';
4
8
  import { EnvironmentHelper } from '@babylonjs/core/Helpers/environmentHelper';
5
9
  import { PhotoDome } from '@babylonjs/core/Helpers/photoDome';
6
10
  import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
@@ -27,6 +31,7 @@ import { cloneDeep, get, has, merge } from 'lodash-es';
27
31
 
28
32
  const backgroundDomeName = 'BackgroundDome_ViewerGenerated';
29
33
  const envHelperMetadataName = 'viewerEnvHelper';
34
+ const materialWaitingToBeSetMetadataName = 'materialWaitingToBeSet';
30
35
 
31
36
  /**
32
37
  * @param node
@@ -354,35 +359,91 @@ const changeEnvironment = function (scene: Scene, envDef: EnvironmentDefinition)
354
359
  };
355
360
 
356
361
  /**
357
- * @param node
358
- * @param materialName
359
- * @param deep
362
+ * Sets a material by a given material id as material property on the given node.
363
+ *
364
+ * If the material is not already available in the scene, the viewer tries to create a material based on a Combeenation
365
+ * [material asset](https://doc.combeenation.com/docs/howto-create-and-use-babylon-and-material-asset).
366
+ * This of course only works if the viewer is used inside a Combeenation configurator.
367
+ *
368
+ * Furthermore this function also defers the material creation if the node is not visible yet to improve network &
369
+ * viewer bootstrap performance as textures are automatically being lazy loaded only when they are actually visible in
370
+ * the scene.
371
+ *
372
+ * Finally the material will not be applied before all its textures have been loaded. In this way "flickering" effects
373
+ * will be avoided, since the material would be incomplete without its loaded textures.
360
374
  */
361
- const setMaterial = function (variant: Variant, node: TransformNode, materialName: string, deep: boolean = true) {
375
+ const setMaterial = function (
376
+ scene: Scene,
377
+ node: TransformNode,
378
+ materialId: string,
379
+ deep: boolean = true,
380
+ variant?: Variant
381
+ ) {
362
382
  if (node instanceof AbstractMesh) {
363
- const materialExists = variant.viewer.scene.getMaterialById(materialName);
383
+ const materialExists = scene.getMaterialById(materialId);
364
384
  const hasMissingMaterial = has(node.metadata, missingMaterialMetadataName);
365
385
  const deferMaterialCreation = !materialExists && !node.isEnabled();
366
-
367
386
  if (deferMaterialCreation) {
368
- injectNodeMetadata(node, { [missingMaterialMetadataName]: materialName }, false);
369
-
370
- // If it already had the missing material flag before, there already exists an observer...
387
+ // do not set the material
388
+ injectNodeMetadata(node, { [missingMaterialMetadataName]: materialId }, false);
389
+ // if it already had the missing material flag before, there already exists an observer...
371
390
  if (!hasMissingMaterial) {
372
391
  addMissingMaterialObserver(node);
373
392
  }
374
393
  } else {
375
- node.material = variant.getOrCreateMaterial(materialName);
394
+ // create material an apply it when textures have been loaded
395
+ const material = getOrCreateMaterial(scene, materialId, variant);
396
+ applyMaterialAfterTexturesLoaded(material, node);
397
+
376
398
  if (hasMissingMaterial) {
377
399
  delete node.metadata[missingMaterialMetadataName];
378
400
  }
379
401
  }
380
402
  }
403
+ // recursively set materials on children (if desired)
381
404
  if (deep) {
382
- node.getChildTransformNodes(true).forEach(child => setMaterial(variant, child, materialName, deep));
405
+ for (const child of node.getChildTransformNodes(true)) {
406
+ setMaterial(scene, child, materialId, deep, variant);
407
+ }
383
408
  }
384
409
  };
385
410
 
411
+ /**
412
+ * Gets the Material either defined in the given {@link Variant}, scene or via {@link createMaterialFromCbnAssets}.
413
+ */
414
+ const getOrCreateMaterial = function (scene: Scene, materialId: string, variant?: Variant): Material {
415
+ let chosenMaterial: Material | undefined | null;
416
+ chosenMaterial = variant?.inheritedMaterials.find(mat => mat.id === materialId);
417
+ chosenMaterial = chosenMaterial || scene.materials.find(mat => mat.id === materialId);
418
+ chosenMaterial = chosenMaterial || createMaterialFromCbnAssets(materialId, scene);
419
+ if (chosenMaterial) {
420
+ return chosenMaterial as Material;
421
+ }
422
+ // reject when material was not found
423
+ let rejectMessage = `Material with id "${materialId}" does not exist on scene.`;
424
+ if (variant) {
425
+ rejectMessage = `Material with id "${materialId}" does not exist for variant "${variant.id}".`;
426
+ }
427
+ throw new Error(rejectMessage);
428
+ };
429
+
430
+ /**
431
+ * Waits until the materials textures are loaded and sets the material on the node if there is no newer "set material"
432
+ * request
433
+ */
434
+ const applyMaterialAfterTexturesLoaded = function (material: Material, node: AbstractMesh) {
435
+ // set current material id as last valid id, in this case all previously set materials on the node will be invalidated
436
+ injectNodeMetadata(node, { [materialWaitingToBeSetMetadataName]: material.id }, false);
437
+
438
+ BaseTexture.WhenAllReady(material.getActiveTextures(), () => {
439
+ // textures ready, now check if the material is still up-to-date
440
+ if (material.id === node.metadata[materialWaitingToBeSetMetadataName]) {
441
+ node.material = material;
442
+ delete node.metadata[materialWaitingToBeSetMetadataName];
443
+ }
444
+ });
445
+ };
446
+
386
447
  /**
387
448
  * !!! Warning !!!
388
449
  * This function is not public API. Whilst it can help solving certain problems, it only works reliably in well defined
@@ -638,6 +699,8 @@ export {
638
699
  cloneTransformNode,
639
700
  cloneNodeWithParents,
640
701
  cloneTransformNodeMaterial,
702
+ getOrCreateMaterial,
703
+ applyMaterialAfterTexturesLoaded,
641
704
  injectNodeMetadata,
642
705
  assertTransformNode,
643
706
  activateTransformNode,
@@ -1,4 +1,4 @@
1
- import { injectNodeMetadata } from './babylonHelper';
1
+ import { applyMaterialAfterTexturesLoaded, getOrCreateMaterial, injectNodeMetadata } from './babylonHelper';
2
2
  import { ISceneLoaderPlugin } from '@babylonjs/core/Loading/sceneLoader';
3
3
  import { Material } from '@babylonjs/core/Materials/material';
4
4
  import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
@@ -52,10 +52,9 @@ export const getMaterialPostLoadObserver = function (targetMeshOrInstance: Abstr
52
52
  if (!hasBeenEnabled || !materialMissing) return;
53
53
  // get id of missing material
54
54
  const missingMatId = concerningMesh.metadata[missingMaterialMetadataName];
55
- // try to find material on the scene
56
- const existingMat = concerningMesh.getScene().getMaterialById(missingMatId);
57
- // assign either existing material or freshly created one
58
- concerningMesh.material = existingMat || createMaterialFromCbnAssets(missingMatId, concerningMesh.getScene());
55
+ // get material and apply it on the concerning mesh after all textures have been loaded
56
+ const material = getOrCreateMaterial(concerningMesh.getScene(), missingMatId);
57
+ applyMaterialAfterTexturesLoaded(material, concerningMesh);
59
58
  // since the material is there now, we do not need the related metadata tag anymore
60
59
  delete concerningMesh.metadata[missingMaterialMetadataName];
61
60
  };
package/src/dev.ts CHANGED
@@ -25,6 +25,7 @@ document.addEventListener('DOMContentLoaded', main);
25
25
  window.Cbn = {
26
26
  Assets: {
27
27
  getMaterial(materialId: string) {
28
+ //! this creates a new function on the object that uses the imported function of the same name..
28
29
  const material = getMaterial(materialId);
29
30
  if (material) return material;
30
31