@combeenation/3d-viewer 6.3.0 → 6.4.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.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Combeenation 3D Viewer",
5
5
  "homepage": "https://github.com/Combeenation/3d-viewer#readme",
6
6
  "bugs": {
@@ -8,6 +8,7 @@ import { SpecStorage } from '../store/specStorage';
8
8
  import { backgroundDomeName, envHelperMetadataName } from '../util/babylonHelper';
9
9
  import { debounce, loadJavascript, loadJson } from '../util/resourceHelper';
10
10
  import { getCustomCbnBabylonLoaderPlugin } from '../util/sceneLoaderHelper';
11
+ import { replaceDots } from '../util/stringHelper';
11
12
  import { isMeshIncludedInExclusionList } from '../util/structureHelper';
12
13
  import { AnimationInterface } from './animationInterface';
13
14
  import { Event } from './event';
@@ -79,6 +80,10 @@ export class Viewer extends EventBroadcaster {
79
80
  * This data is most likely coming from babylon assets from the Combeenation system but other sources are also valid.
80
81
  */
81
82
  public static generateSpec(genData: SpecGenerationData[]): StructureJson {
83
+ // dots in the variant name indicate inheritance, but this should not be the case for an auto-generated spec
84
+ // therefore dots are exchanged with slashes
85
+ const safeGenData: SpecGenerationData[] = genData.map(data => ({ ...data, name: replaceDots(data.name) }));
86
+
82
87
  const spec: StructureJson = {
83
88
  // common scene settings as suggested in the viewer docs
84
89
  scene: {
@@ -98,7 +103,7 @@ export class Viewer extends EventBroadcaster {
98
103
  // create one instance for each input entry
99
104
  // name and variant are named accordingly from the input, instance will be hidden by default and lazy loading
100
105
  // is activated
101
- instances: genData.map(instanceData => ({
106
+ instances: safeGenData.map(instanceData => ({
102
107
  name: instanceData.name,
103
108
  variant: instanceData.name,
104
109
  lazy: true,
@@ -110,7 +115,7 @@ export class Viewer extends EventBroadcaster {
110
115
  // variants definition is also mapped to the input array, using the name as key and url as glTF source
111
116
  // no elements are created here, since this should be done automatically from the system
112
117
  // => create one element which contains all root nodes of the imported 3d file (GLB or Babylon)
113
- variants: genData.reduce((accVariants, curVariant) => {
118
+ variants: safeGenData.reduce((accVariants, curVariant) => {
114
119
  accVariants[curVariant.name] = {
115
120
  glTF: curVariant.url,
116
121
  };
@@ -362,7 +362,7 @@ const changeEnvironment = function (scene: Scene, envDef: EnvironmentDefinition)
362
362
  * Sets a material by a given material id as material property on the given node.
363
363
  *
364
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).
365
+ * [material asset](https://docs.combeenation.com/docs/howto-create-and-use-babylon-and-material-asset).
366
366
  * This of course only works if the viewer is used inside a Combeenation configurator.
367
367
  *
368
368
  * Furthermore this function also defers the material creation if the node is not visible yet to improve network &
@@ -409,7 +409,8 @@ const setMaterial = function (
409
409
  };
410
410
 
411
411
  /**
412
- * Gets the Material either defined in the given {@link Variant}, scene or via {@link createMaterialFromCbnAssets}.
412
+ * Gets the Material either from the given {@link Variant}, the given scene or tries to create it from an Combeenation
413
+ * material asset when inside a Combeenation configurator.
413
414
  */
414
415
  const getOrCreateMaterial = function (scene: Scene, materialId: string, variant?: Variant): Material {
415
416
  let chosenMaterial: Material | undefined | null;
@@ -431,17 +432,29 @@ const getOrCreateMaterial = function (scene: Scene, materialId: string, variant?
431
432
  * Waits until the materials textures are loaded and sets the material on the node if there is no newer "set material"
432
433
  * request
433
434
  */
434
- const applyMaterialAfterTexturesLoaded = function (material: Material, node: AbstractMesh) {
435
+ const applyMaterialAfterTexturesLoaded = async function (material: Material, node: AbstractMesh) {
435
436
  // set current material id as last valid id, in this case all previously set materials on the node will be invalidated
436
437
  injectNodeMetadata(node, { [materialWaitingToBeSetMetadataName]: material.id }, false);
437
438
 
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
- });
439
+ const promTexturesReady = new Promise<void>(resolve =>
440
+ BaseTexture.WhenAllReady(material.getActiveTextures(), resolve)
441
+ );
442
+ // this promise should only take some time on the first call of the corresponding shader (eg: PBRMaterial shader)
443
+ // on each other call of the same material/shader type there should be not be a waiting time, or at maximum one frame
444
+ // https://forum.babylonjs.com/t/mesh-flickering-when-setting-material-initially/37312
445
+ const promMaterialCompiled = material.forceCompilationAsync(node);
446
+
447
+ // material needs to fulfill 2 criterias before it's ready to use
448
+ // - textures need to be "ready" => downloaded
449
+ // - dedicated shader needs to be compiled
450
+ // if this would not be the case you can see some "flickering" when setting the material
451
+ await Promise.all([promTexturesReady, promMaterialCompiled]);
452
+
453
+ // textures ready, now check if the material is still up-to-date
454
+ if (material.id === node.metadata[materialWaitingToBeSetMetadataName]) {
455
+ node.material = material;
456
+ delete node.metadata[materialWaitingToBeSetMetadataName];
457
+ }
445
458
  };
446
459
 
447
460
  /**
@@ -2,6 +2,7 @@ import { applyMaterialAfterTexturesLoaded, getOrCreateMaterial, injectNodeMetada
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';
5
+ import { Observer } from '@babylonjs/core/Misc/observable';
5
6
  import { AssetContainer } from '@babylonjs/core/assetContainer';
6
7
  //! overload DOM API Node due to name-clash with BJS
7
8
  import { Node as BjsNode } from '@babylonjs/core/node';
@@ -9,6 +10,11 @@ import { Scene } from '@babylonjs/core/scene';
9
10
  import { Nullable } from '@babylonjs/core/types';
10
11
  import has from 'lodash-es/has';
11
12
 
13
+ // map for keeping track of active "node enable" observers
14
+ const enableObserverMap: {
15
+ [concerningNodeId: string]: { currNodeId: string; observer: Nullable<Observer<boolean>> }[];
16
+ } = {};
17
+
12
18
  export const missingMaterialMetadataName = 'missingMaterial';
13
19
 
14
20
  /**
@@ -41,12 +47,18 @@ export const getCustomCbnBabylonLoaderPlugin = function (previousLoaderPlugin: I
41
47
  /**
42
48
  * Return an observer to be applied to meshes in order to post-load missing materials
43
49
  * upon set enabled/visible.
44
- * @param targetMeshOrInstance AbstractMesh the observer will be applied to
50
+ *
45
51
  * @param concerningMesh Mesh to look for missing materials on, and create/apply to (when found).
46
52
  * @returns observer
47
53
  */
48
- export const getMaterialPostLoadObserver = function (targetMeshOrInstance: AbstractMesh, concerningMesh: Mesh) {
49
- return (eventData: any, eventState: any) => {
54
+ export const getMaterialPostLoadObserver = function (concerningMesh: Mesh) {
55
+ return async () => {
56
+ const scene = concerningMesh.getScene();
57
+
58
+ // can't check `isEnabled` immediatly, since the enabled state of parents and childs is not synced yet
59
+ // postpone to "when scene ready" to ensure a correct parent-child enable relation
60
+ await scene.whenReadyAsync();
61
+
50
62
  const hasBeenEnabled = concerningMesh.isEnabled(true);
51
63
  const materialMissing = has(concerningMesh.metadata, missingMaterialMetadataName);
52
64
  if (!hasBeenEnabled || !materialMissing) return;
@@ -57,6 +69,15 @@ export const getMaterialPostLoadObserver = function (targetMeshOrInstance: Abstr
57
69
  applyMaterialAfterTexturesLoaded(material, concerningMesh);
58
70
  // since the material is there now, we do not need the related metadata tag anymore
59
71
  delete concerningMesh.metadata[missingMaterialMetadataName];
72
+
73
+ // remove all "enable" observers that were assigned to the concerning mesh
74
+ // the mesh got visible and therefore the observers are not needed anymore
75
+ enableObserverMap[concerningMesh.id].forEach(entry => {
76
+ const currNode = scene.getMeshById(entry.currNodeId);
77
+ currNode?.onEnabledStateChangedObservable.remove(entry.observer);
78
+ });
79
+ // also remove from the local observer map
80
+ delete enableObserverMap[concerningMesh.id];
60
81
  };
61
82
  };
62
83
 
@@ -98,30 +119,23 @@ export const addMissingMaterialObserver = function (node: BjsNode) {
98
119
  // for each of our AbstractMeshes, set an observer on the AbstractMesh itself and all of its parents.
99
120
  let currNode: Nullable<BjsNode> = node;
100
121
  while (currNode) {
101
- // Note HAR: Using `addOnce` could be wrong in certain situations.
102
- // E.g.:
103
- // * 2 meshes `parentMesh` & `parentMesh.concerningMesh`
104
- // * `concerningMesh` is having the missing material flag
105
- // * Both `parentMesh` & `concerningMesh` are disabled
106
- // * `parentMesh` is enabled -> material should **not** be created as `concerningMesh` is still disabled
107
- // * `parentMesh` is disabled
108
- // * `concerningMesh` is enabled -> material should **not** be created as `concerningMesh` is still invisible
109
- // because its `parentMesh` is ATM disabled
110
- // * `parentMesh` & `concerningMesh` are both enable
111
- // -> material should be created as `concerningMesh` is now actually visible but it isn't, as all observers were
112
- // only fired once 🔥
113
- //
114
- // However: Using `add` instead of `addOnce` requires rather complicated manual clean up work...
122
+ const callback = getMaterialPostLoadObserver(concerningNode);
123
+ const observer = currNode.onEnabledStateChangedObservable.add(callback);
124
+
125
+ // store the observer in a local map to keep track of the active "enable" observers
126
+ // observers will be removed when the concerning node gets enabled
127
+ if (!enableObserverMap[concerningNode.id]) {
128
+ enableObserverMap[concerningNode.id] = [];
129
+ }
130
+ enableObserverMap[concerningNode.id].push({ currNodeId: currNode.id, observer });
115
131
 
116
- // add observer. needed only once per node, hence addOnce()
117
- node.onEnabledStateChangedObservable.addOnce(getMaterialPostLoadObserver(currNode as AbstractMesh, concerningNode));
118
- // console.log('## observer set on: ' + meshOrInstance.name);
119
132
  currNode = currNode.parent;
120
133
  }
121
134
  };
122
135
 
123
136
  /**
124
137
  * Look up the provided materials (see library import) and create and return one if found.
138
+ *
125
139
  * @param materialId BabylonJs material-id. E.g. 'concrete".
126
140
  * @param scene BabylonJs scene
127
141
  * @returns PBRMaterial | null
@@ -20,4 +20,11 @@ const camelToSnakeCase = function (str: string): string {
20
20
  .toLowerCase();
21
21
  };
22
22
 
23
- export { uuidv4, camelToSnakeCase };
23
+ /**
24
+ * Replaces all dots from the input string with a desired character ('/' by default)
25
+ */
26
+ const replaceDots = function (str: string, replaceChar = '/'): string {
27
+ return str.split('.').join(replaceChar);
28
+ };
29
+
30
+ export { uuidv4, camelToSnakeCase, replaceDots };
package/src/dev.ts CHANGED
@@ -12,12 +12,10 @@ import { set } from 'lodash-es';
12
12
  const loadingElement = document.getElementById('loading') as HTMLDivElement;
13
13
 
14
14
  Emitter.on(Event.BOOTSTRAP_START, () => {
15
- Emitter.on(Event.LOADING_START, () => {
16
- loadingElement!.style.display = 'block';
17
- });
18
- Emitter.on(Event.VARIANT_CREATED, () => {
19
- loadingElement!.style.display = 'none';
20
- });
15
+ loadingElement!.style.display = 'block';
16
+ });
17
+ Emitter.on(Event.BOOTSTRAP_END, () => {
18
+ loadingElement!.style.display = 'none';
21
19
  });
22
20
 
23
21
  document.addEventListener('DOMContentLoaded', main);