@combeenation/3d-viewer 17.1.0 → 18.0.0-beta2

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.
Files changed (58) hide show
  1. package/dist/lib-cjs/buildinfo.json +1 -1
  2. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  3. package/dist/lib-cjs/index.d.ts +8 -0
  4. package/dist/lib-cjs/index.js +8 -0
  5. package/dist/lib-cjs/index.js.map +1 -1
  6. package/dist/lib-cjs/internal/asset-helper.d.ts +32 -0
  7. package/dist/lib-cjs/internal/asset-helper.js +105 -0
  8. package/dist/lib-cjs/internal/asset-helper.js.map +1 -0
  9. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.d.ts +18 -0
  10. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js +22 -3
  11. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js.map +1 -1
  12. package/dist/lib-cjs/internal/cloning-helper.js +1 -1
  13. package/dist/lib-cjs/internal/cloning-helper.js.map +1 -1
  14. package/dist/lib-cjs/internal/texture-parameter-helper.js +26 -7
  15. package/dist/lib-cjs/internal/texture-parameter-helper.js.map +1 -1
  16. package/dist/lib-cjs/manager/camera-manager.d.ts +23 -2
  17. package/dist/lib-cjs/manager/camera-manager.js +91 -24
  18. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  19. package/dist/lib-cjs/manager/debug-manager.d.ts +1 -1
  20. package/dist/lib-cjs/manager/debug-manager.js +3 -3
  21. package/dist/lib-cjs/manager/debug-manager.js.map +1 -1
  22. package/dist/lib-cjs/manager/dimension-line-manager.d.ts +126 -0
  23. package/dist/lib-cjs/manager/dimension-line-manager.js +138 -0
  24. package/dist/lib-cjs/manager/dimension-line-manager.js.map +1 -0
  25. package/dist/lib-cjs/manager/html-anchor-manager.d.ts +93 -0
  26. package/dist/lib-cjs/manager/html-anchor-manager.js +228 -0
  27. package/dist/lib-cjs/manager/html-anchor-manager.js.map +1 -0
  28. package/dist/lib-cjs/manager/model-manager.d.ts +11 -34
  29. package/dist/lib-cjs/manager/model-manager.js +47 -107
  30. package/dist/lib-cjs/manager/model-manager.js.map +1 -1
  31. package/dist/lib-cjs/manager/parameter-manager.d.ts +17 -12
  32. package/dist/lib-cjs/manager/parameter-manager.js +78 -69
  33. package/dist/lib-cjs/manager/parameter-manager.js.map +1 -1
  34. package/dist/lib-cjs/manager/scene-manager.d.ts +111 -5
  35. package/dist/lib-cjs/manager/scene-manager.js +276 -10
  36. package/dist/lib-cjs/manager/scene-manager.js.map +1 -1
  37. package/dist/lib-cjs/viewer-error.d.ts +1 -0
  38. package/dist/lib-cjs/viewer-error.js +1 -0
  39. package/dist/lib-cjs/viewer-error.js.map +1 -1
  40. package/dist/lib-cjs/viewer.d.ts +9 -14
  41. package/dist/lib-cjs/viewer.js +16 -38
  42. package/dist/lib-cjs/viewer.js.map +1 -1
  43. package/package.json +22 -12
  44. package/src/index.ts +8 -0
  45. package/src/internal/asset-helper.ts +115 -0
  46. package/src/internal/cbn-custom-babylon-loader-plugin.ts +30 -3
  47. package/src/internal/cloning-helper.ts +1 -1
  48. package/src/internal/texture-parameter-helper.ts +25 -8
  49. package/src/manager/camera-manager.ts +153 -39
  50. package/src/manager/debug-manager.ts +3 -3
  51. package/src/manager/dimension-line-manager.ts +255 -0
  52. package/src/manager/html-anchor-manager.ts +332 -0
  53. package/src/manager/model-manager.ts +55 -138
  54. package/src/manager/parameter-manager.ts +94 -75
  55. package/src/manager/scene-manager.ts +375 -10
  56. package/src/viewer-error.ts +1 -0
  57. package/src/viewer.ts +30 -56
  58. package/src/dev.ts +0 -47
@@ -1,33 +1,17 @@
1
1
  import {
2
2
  AssetContainer,
3
- BuiltInParameter,
4
3
  DecalConfiguration,
5
4
  MaterialManager,
6
5
  MeshBuilder,
7
- ParameterManager,
8
- SceneLoader,
9
6
  TransformNode,
10
7
  Viewer,
11
8
  ViewerError,
12
9
  ViewerErrorIds,
13
10
  } from '../index';
11
+ import { BaseAsset, loadAsset, prepareAssetForScene } from '../internal/asset-helper';
14
12
  import { cloneModelAssetContainer } from '../internal/cloning-helper';
15
- import { getInternalMetadataValue } from '../internal/metadata-helper';
16
13
  import { isArray } from 'lodash-es';
17
14
 
18
- /**
19
- * Contains cbn custom data, like decals.
20
- * This is just a temporary type, as the `loadAssetContainer` function only returns an asset container, which can be
21
- * altered by our file loader plugin.
22
- * After loading the model, `cbnData` is cropped and a pure asset container is available for further processing.
23
- *
24
- * @internal
25
- */
26
- export class ExtendedAssetContainer extends AssetContainer {
27
- cbnData?: CbnBabylonFileData;
28
- }
29
-
30
- type CbnBabylonFileData = { decals?: ParsedDecalConfiguration[] };
31
15
  export type ParsedDecalConfiguration = DecalConfiguration & { materialId?: string; tags?: string };
32
16
 
33
17
  export type ModelAssetDefinition = {
@@ -62,20 +46,14 @@ export type ModelCloneOptions = {
62
46
  tagNamingStrategy?: TagNamingStrategy;
63
47
  };
64
48
 
65
- type Model = {
66
- name: string;
67
- url: string;
68
- state: ModelAssetState;
69
- assetContainer: AssetContainer;
70
- cbnBabylonFileData?: CbnBabylonFileData;
49
+ type ModelAsset = BaseAsset & {
50
+ decals?: ParsedDecalConfiguration[];
71
51
  isClone: boolean;
72
52
  // only set for "instantiated" clones
73
53
  sourceModelName?: string;
74
54
  visibilityCallId?: number;
75
55
  };
76
56
 
77
- type ModelAssetState = 'notLoaded' | 'loading' | 'loaded' | 'inScene';
78
-
79
57
  /**
80
58
  * Manager for handling 3d models.\
81
59
  * Responsible for loading models and handling their visibility.\
@@ -84,12 +62,11 @@ type ModelAssetState = 'notLoaded' | 'loading' | 'loaded' | 'inScene';
84
62
  export class ModelManager {
85
63
  /**
86
64
  * CAUTION: this has to be in sync with the Combeenation backend!
87
- * @internal
88
65
  */
89
66
  public static readonly CBN_FALLBACK_MODEL_ASSET_NAME = '$fallback';
90
67
 
91
- protected _modelAssets: { [name: string]: Model } = {};
92
- protected _fallbackModelAsset: Model = {
68
+ protected _modelAssets: { [name: string]: ModelAsset } = {};
69
+ protected _fallbackModelAsset: ModelAsset = {
93
70
  name: ModelManager.CBN_FALLBACK_MODEL_ASSET_NAME,
94
71
  url: '',
95
72
  state: 'loaded',
@@ -113,6 +90,7 @@ export class ModelManager {
113
90
  return inputStr.split('/').join('.');
114
91
  }
115
92
 
93
+ /** @internal */
116
94
  public constructor(protected viewer: Viewer) {}
117
95
 
118
96
  /**
@@ -144,11 +122,11 @@ export class ModelManager {
144
122
  for (const { name, url } of modelAssets) {
145
123
  const existingModel = this._modelAssets[name];
146
124
  if (existingModel) {
147
- console.warn(`Model ${name} is already registered`);
125
+ console.warn(`Model "${name}" is already registered`);
148
126
  return;
149
127
  }
150
128
 
151
- const model: Model = { name, url, state: 'notLoaded', assetContainer: new AssetContainer(), isClone: false };
129
+ const model: ModelAsset = { name, url, state: 'notLoaded', assetContainer: new AssetContainer(), isClone: false };
152
130
  this._modelAssets[name] = model;
153
131
  }
154
132
  }
@@ -170,7 +148,7 @@ export class ModelManager {
170
148
  }
171
149
 
172
150
  if (model.state !== 'notLoaded') {
173
- console.warn(`Model ${name} is already loaded or currently loading`);
151
+ console.warn(`Model "${name}" is already loaded or currently loading`);
174
152
  return;
175
153
  }
176
154
 
@@ -187,14 +165,14 @@ export class ModelManager {
187
165
  public async setModelVisibility(
188
166
  modelVisibility: ModelVisibilityEntry | ModelVisibilityEntry[]
189
167
  ): Promise<ModelVisibilityEntry[]> {
190
- const showModels: Model[] = [];
191
- const hideModels: Model[] = [];
168
+ const modelsToShow: ModelAsset[] = [];
169
+ const modelsToHide: ModelAsset[] = [];
192
170
 
193
171
  if (!isArray(modelVisibility)) {
194
172
  modelVisibility = [modelVisibility];
195
173
  }
196
174
 
197
- for (const entry of modelVisibility) {
175
+ const loadModelProms = modelVisibility.map(async entry => {
198
176
  const model = this._getModel(entry.name);
199
177
  if (!model) {
200
178
  throw new ViewerError({
@@ -206,8 +184,10 @@ export class ModelManager {
206
184
  // there can be multiple "setModelVisibility" calls while the model is loading
207
185
  // loading can be awaited, but it has to be ensured, that the last call of "setModelVisibility" has priority
208
186
  // therefore we store the id of the call in the model
209
- const curVisibilityCallId = (model.visibilityCallId ?? 0) + 1;
210
- model.visibilityCallId = curVisibilityCallId;
187
+ model.visibilityCallId = (model.visibilityCallId ?? 0) + 1;
188
+ const curVisibilityCallId = model.visibilityCallId;
189
+ let showModel = false;
190
+ let hideModel = false;
211
191
 
212
192
  if (entry.visible) {
213
193
  if (model.state === 'notLoaded') {
@@ -215,45 +195,51 @@ export class ModelManager {
215
195
 
216
196
  // check if this is still the latest visibility call
217
197
  if (model.visibilityCallId === curVisibilityCallId) {
218
- showModels.push(model);
198
+ showModel = true;
219
199
  }
220
200
  } else if (model.state === 'loading') {
221
201
  await this._loadModelPromises[model.name];
222
202
 
223
203
  if (model.visibilityCallId === curVisibilityCallId) {
224
- showModels.push(model);
204
+ showModel = true;
225
205
  }
226
206
  } else if (model.state === 'loaded') {
227
- showModels.push(model);
207
+ showModel = true;
228
208
  }
229
209
  } else {
230
210
  if (model.state === 'loading') {
231
211
  await this._loadModelPromises[model.name];
232
212
 
233
213
  if (model.visibilityCallId === curVisibilityCallId) {
234
- hideModels.push(model);
214
+ hideModel = true;
235
215
  }
236
216
  } else if (model.state === 'inScene') {
237
- hideModels.push(model);
217
+ hideModel = true;
238
218
  }
239
219
  }
240
- }
241
-
242
- for (const showModel of showModels) {
243
- await this._prepareModelForScene(showModel);
244
- }
245
220
 
246
- for (const showModel of showModels) {
247
- await this._showModel(showModel, true);
248
- }
249
-
250
- for (const hideModel of hideModels) {
251
- this._hideModel(hideModel);
252
- }
221
+ if (showModel) {
222
+ await this._prepareModelForScene(model);
223
+ modelsToShow.push(model);
224
+ } else if (hideModel) {
225
+ modelsToHide.push(model);
226
+ }
227
+ });
228
+ // model loading and preparation (e.g. create materials) is done simultaniously for all models to save time
229
+ // model (`ModelManager._loadModelPromises`) and material queues (`MaterialManager._createMaterialPromises`) are
230
+ // responsible for correct loading state handling of these instances
231
+ await Promise.all(loadModelProms);
253
232
 
254
233
  const returnVal: ModelVisibilityEntry[] = [];
255
- showModels.forEach(showModel => returnVal.push({ name: showModel.name, visible: true }));
256
- hideModels.forEach(hideModel => returnVal.push({ name: hideModel.name, visible: false }));
234
+ modelsToShow.forEach(model => {
235
+ // show model is not async, as preparation has already been done
236
+ this._showModel(model, true);
237
+ returnVal.push({ name: model.name, visible: true });
238
+ });
239
+ modelsToHide.forEach(model => {
240
+ this._hideModel(model);
241
+ returnVal.push({ name: model.name, visible: false });
242
+ });
257
243
 
258
244
  return returnVal;
259
245
  }
@@ -294,7 +280,7 @@ export class ModelManager {
294
280
  await this._loadModelPromises[sourceModel.name];
295
281
  }
296
282
 
297
- const clonedModel: Model = {
283
+ const clonedModel: ModelAsset = {
298
284
  name: newModelName,
299
285
  url: sourceModel.url,
300
286
  state: 'loaded',
@@ -400,14 +386,14 @@ export class ModelManager {
400
386
  await this._prepareModelForScene(model);
401
387
  }
402
388
 
403
- return model.cbnBabylonFileData?.decals ?? [];
389
+ return model.decals ?? [];
404
390
  }
405
391
 
406
392
  /**
407
393
  * Get model by name
408
394
  */
409
- protected _getModel(name: string): Model | undefined {
410
- if (name === '$fallback') {
395
+ protected _getModel(name: string): ModelAsset | undefined {
396
+ if (name === ModelManager.CBN_FALLBACK_MODEL_ASSET_NAME) {
411
397
  return this._fallbackModelAsset;
412
398
  }
413
399
 
@@ -420,67 +406,16 @@ export class ModelManager {
420
406
  /**
421
407
  * Load model into scene, but don't show it immediately
422
408
  */
423
- protected async _loadModel(model: Model): Promise<void> {
409
+ protected async _loadModel(model: ModelAsset): Promise<void> {
424
410
  const loadModelPromise = async (): Promise<void> => {
425
- model.state = 'loading';
426
- const curEnvTexture = this.viewer.scene.environmentTexture;
427
- const curEnvIntensity = this.viewer.scene.environmentIntensity;
428
-
429
- // CB-9240: Babylon.js doesn't recognize gzipped babylon files (`.babylon.gz`) as such, leading to a warning
430
- // message, therefore we overwrite the plugin extension actively for such files
431
- let pluginExtension;
432
- try {
433
- // URL constructor can throw for "valid" URL, which happened to be the case for the test asset environment where
434
- // the urls are relative (starting with ".")
435
- const urlObj = new URL(model.url);
436
- if (urlObj.pathname.endsWith('.babylon.gz')) {
437
- pluginExtension = '.babylon';
438
- }
439
- } catch (e) {}
440
-
441
- let assetContainer;
442
- try {
443
- const fullContainer = (await SceneLoader.LoadAssetContainerAsync(
444
- '',
445
- model.url,
446
- this.viewer.scene,
447
- undefined,
448
- pluginExtension
449
- )) as ExtendedAssetContainer;
450
-
451
- // crop and store custom cbn data from .babylon file
452
- model.cbnBabylonFileData = fullContainer.cbnData;
453
- delete fullContainer.cbnData;
454
-
455
- // from here it's a basic asset container again
456
- assetContainer = fullContainer as AssetContainer;
457
- } catch (e) {
458
- throw new ViewerError({
459
- id: ViewerErrorIds.AssetLoadingFailed,
460
- message: (e as Error).message,
461
- });
462
- }
411
+ const { cbnData } = await loadAsset(model, this.viewer);
412
+
413
+ model.decals = cbnData?.decals;
463
414
 
464
415
  // remove all lights and cameras from the asset, as this data should be handled globally in the scene instead of
465
416
  // being in a model context
466
- [...assetContainer.lights].forEach(light => light.dispose());
467
- [...assetContainer.cameras].forEach(camera => camera.dispose());
468
-
469
- // materials should be a "global" thing and not assigned to an asset container
470
- // this is not relevant in most of the cases, since materials are cropped from babylon models on the CBN server
471
- // anyway
472
- assetContainer.materials.forEach(material => {
473
- this.viewer.scene.addMaterial(material);
474
- material._parentContainer = null;
475
- });
476
- assetContainer.materials = [];
477
-
478
- // environment texture and intensity has been overwritten by load asset container function
479
- this.viewer.scene.environmentTexture = curEnvTexture;
480
- this.viewer.scene.environmentIntensity = curEnvIntensity;
481
-
482
- model.assetContainer = assetContainer;
483
- model.state = 'loaded';
417
+ [...model.assetContainer.lights].forEach(light => light.dispose());
418
+ [...model.assetContainer.cameras].forEach(camera => camera.dispose());
484
419
 
485
420
  delete this._loadModelPromises[model.name];
486
421
  };
@@ -494,7 +429,7 @@ export class ModelManager {
494
429
  * This is typically done before the model is added to the scene to avoid changes on the visible model, which ensures
495
430
  * a smooth model switch behaviour.
496
431
  */
497
- protected async _prepareModelForScene(model: Model): Promise<void> {
432
+ protected async _prepareModelForScene(model: ModelAsset): Promise<void> {
498
433
  if (model.sourceModelName) {
499
434
  // source model has to be prepared for scene as well in "instantiate" mode, because materials would not be
500
435
  // loaded otherwise
@@ -504,10 +439,7 @@ export class ModelManager {
504
439
  await this._prepareModelForScene(sourceModel);
505
440
  }
506
441
 
507
- await this.viewer.parameterManager.applyAllParameterValuesToModel(model.assetContainer);
508
- // parameter manager did his job, now apply the deferred materials to all meshes that are not explicitely hidden by
509
- // the parameter manager
510
- await this._applyDeferredMaterialsForAllVisibleMeshes(model);
442
+ await prepareAssetForScene(model, this.viewer);
511
443
  }
512
444
 
513
445
  /**
@@ -517,7 +449,7 @@ export class ModelManager {
517
449
  * @param skipPreparation optionally skip applying parameter values to model before showing it
518
450
  * (see {@link _prepareModelForScene})
519
451
  */
520
- protected async _showModel(model: Model, skipPreparation?: boolean): Promise<void> {
452
+ protected async _showModel(model: ModelAsset, skipPreparation?: boolean): Promise<void> {
521
453
  if (!skipPreparation) {
522
454
  await this._prepareModelForScene(model);
523
455
  }
@@ -529,23 +461,8 @@ export class ModelManager {
529
461
  /**
530
462
  * Remove assets of asset container from scene
531
463
  */
532
- protected _hideModel(model: Model): void {
464
+ protected _hideModel(model: ModelAsset): void {
533
465
  model.assetContainer.removeFromScene();
534
466
  model.state = 'loaded';
535
467
  }
536
-
537
- /**
538
- * Creates and assigns each "deferred" material to the corresponding mesh, if the mesh is visible.
539
- * Model should be ready to use immediately after this function has done it's job.
540
- */
541
- protected async _applyDeferredMaterialsForAllVisibleMeshes(model: Model): Promise<void> {
542
- for (const mesh of model.assetContainer.meshes) {
543
- const deferredMaterial = getInternalMetadataValue(mesh, 'deferredMaterial');
544
- const rawVisibleValue = this.viewer.parameterManager.getParameterValueOfNode(mesh, BuiltInParameter.Visible);
545
- const visible = rawVisibleValue === undefined || ParameterManager.parseBoolean(rawVisibleValue) === true;
546
- if (deferredMaterial && visible) {
547
- await this.viewer.materialManager.setMaterialOnMesh(deferredMaterial as string, mesh);
548
- }
549
- }
550
- }
551
468
  }
@@ -97,7 +97,7 @@ export type ParameterObserver = (payload: ParameterObserverPayload) => Promise<v
97
97
 
98
98
  /**
99
99
  * Payload of parameter observer.\
100
- * Contains current data of parameter entry, which can be usefull for implementing the dedicated observer
100
+ * Contains current data of parameter entry, which can be useful for implementing the dedicated observer
101
101
  */
102
102
  export type ParameterObserverPayload = {
103
103
  subject: ParameterSubject;
@@ -258,6 +258,7 @@ export class ParameterManager {
258
258
  protected _parameterEntries: ParameterEntry[] = [];
259
259
  protected _parameterObserver: { [parameterName: ParameterName]: ParameterObserver } = {};
260
260
 
261
+ /** @internal */
261
262
  public constructor(protected viewer: Viewer) {
262
263
  this._addBuiltInParameterObservers();
263
264
  }
@@ -459,49 +460,28 @@ export class ParameterManager {
459
460
  }
460
461
 
461
462
  /**
462
- * @returns Desired parameter value of a certain node.
463
- * Tags are considered as well but have lower priority than node parameters, as these are more specific.
464
- *
465
- * @internal
466
- */
467
- public getParameterValueOfNode(node: TransformNode, parameterName: string): ParameterValue | undefined {
468
- const nodeParamValue = this.getParameterValue({ nodeName: node.name }, parameterName);
469
- if (nodeParamValue !== undefined) {
470
- return nodeParamValue;
471
- }
472
-
473
- const tags = getTagsAsStrArr(node);
474
- const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
475
- // NOTE: it is possible that values are available for multiple tags
476
- // in this case the resulting parameter value is quite "random" as the last tag in the tag string of the node has
477
- // priority
478
- const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
479
- return accValue ?? tagParamValue;
480
- }, undefined);
481
-
482
- return tagParamValue;
483
- }
484
-
485
- /**
486
- * @returns Desired parameter value of a certain material.
487
- * Tags are considered as well but have lower priority than material parameters, as these are more specific.
488
- * Unused ATM, but added for consistency as counter part for {@link getParameterValueOfNode}
463
+ * Retrieves visibility state of node, considering pending parameter values and node hierarchy.\
464
+ * `node.isEnabled()` alone is insufficient here, as visibility observers for the desired node or one of it's parents
465
+ * may be called in the same cycle, overwriting the visibility state again.
489
466
  *
490
467
  * @internal
491
468
  */
492
- public getParameterValueOfMaterial(material: Material, parameterName: string): ParameterValue | undefined {
493
- const materialParamValue = this.getParameterValue({ materialName: material.name }, parameterName);
494
- if (materialParamValue !== undefined) {
495
- return materialParamValue;
469
+ public getNestedVisibilityParameterValueOfNode(node: TransformNode): boolean {
470
+ let curNode: TransformNode | null = node;
471
+ let visibleByParameter: boolean | undefined = undefined;
472
+ while (curNode && visibleByParameter !== false) {
473
+ const curNodeVisibleByParameter = this._getParameterValueOfNode(curNode, BuiltInParameter.Visible);
474
+ if (curNodeVisibleByParameter !== undefined) {
475
+ visibleByParameter = curNodeVisibleByParameter as boolean;
476
+ }
477
+ curNode = curNode.parent as TransformNode;
496
478
  }
497
479
 
498
- const tags = getTagsAsStrArr(material);
499
- const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
500
- const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
501
- return accValue ?? tagParamValue;
502
- }, undefined);
480
+ // check state of nested visibility parameter value and fall back to basic node enabled state
481
+ const visible =
482
+ visibleByParameter !== undefined ? ParameterManager.parseBoolean(visibleByParameter) : node.isEnabled();
503
483
 
504
- return tagParamValue;
484
+ return visible;
505
485
  }
506
486
 
507
487
  /**
@@ -511,33 +491,40 @@ export class ParameterManager {
511
491
  this.setParameterObserver(BuiltInParameter.Visible, async ({ nodes, newValue }) => {
512
492
  const visible = ParameterManager.parseBoolean(newValue);
513
493
 
514
- for (const node of nodes) {
515
- if (visible) {
494
+ const observerProms = nodes.map(async node => {
495
+ const visibleNested = this.getNestedVisibilityParameterValueOfNode(node);
496
+ if (visibleNested) {
516
497
  // if a mesh gets visible by this operation we have to activate the assigned material which is stored in the
517
498
  // internal metadata
518
499
  // => consider child meshes as well (CB-10143)
519
500
  const activatedNodes = [node, ...node.getChildMeshes(false)];
520
- for (const activatedNode of activatedNodes) {
521
- const deferredMaterial = getInternalMetadataValue(activatedNode, 'deferredMaterial');
522
- if (deferredMaterial) {
501
+ const setMaterialProms = activatedNodes.map(async an => {
502
+ const anVisibleNested = this.getNestedVisibilityParameterValueOfNode(an);
503
+ const anDeferredMaterial = getInternalMetadataValue(an, 'deferredMaterial');
504
+ // skip applying material if it's already set via the parameter
505
+ const anMaterialParamValue = this._getParameterValueOfNode(an, BuiltInParameter.Material) as
506
+ | string
507
+ | undefined;
508
+ if (anVisibleNested && anDeferredMaterial && !anMaterialParamValue) {
523
509
  await this.viewer.materialManager.setMaterialOnMesh(
524
- deferredMaterial as string,
510
+ anDeferredMaterial,
525
511
  // this cast is fine, as deferred material can only be set on meshes
526
- activatedNode as AbstractMesh
512
+ an as AbstractMesh
527
513
  );
528
514
  }
529
- }
530
-
531
- node.setEnabled(true);
532
- } else {
533
- node.setEnabled(false);
515
+ });
516
+ await Promise.all(setMaterialProms);
534
517
  }
535
- }
518
+
519
+ // set enabled state anyway
520
+ node.setEnabled(visible);
521
+ });
522
+ await Promise.all(observerProms);
536
523
  });
537
524
  this.setParameterObserver(BuiltInParameter.Material, async ({ nodes, newValue }) => {
538
525
  const material = ParameterManager.parseString(newValue);
539
526
 
540
- for (const node of nodes) {
527
+ const observerProms = nodes.map(async node => {
541
528
  if (!(node instanceof AbstractMesh)) {
542
529
  throw new ViewerError({
543
530
  id: ViewerErrorIds.InvalidParameterSubject,
@@ -545,28 +532,15 @@ export class ParameterManager {
545
532
  });
546
533
  }
547
534
 
548
- // NOTE: don't use node.isEnabled() as visibility observer is probably called in same cycle but later
549
- // however the parameter value is already correct at this stage
550
- // we have to go through all parents as well, because a parent may have been set to false, which also disables
551
- // this child node
552
- let curNode: TransformNode | null = node;
553
- let visibleByParameter: boolean | undefined = undefined;
554
- while (curNode && visibleByParameter !== false) {
555
- const curNodeVisibleByParameter = this.getParameterValueOfNode(curNode, BuiltInParameter.Visible);
556
- if (curNodeVisibleByParameter !== undefined) {
557
- visibleByParameter = curNodeVisibleByParameter as boolean;
558
- }
559
- curNode = curNode.parent as TransformNode;
560
- }
561
-
562
- const visible =
563
- visibleByParameter !== undefined ? ParameterManager.parseBoolean(visibleByParameter) : node.isEnabled();
535
+ // only set material if the mesh is visible, or gets visible by the parameter update
536
+ const visible = this.getNestedVisibilityParameterValueOfNode(node);
564
537
  if (visible) {
565
538
  await this.viewer.materialManager.setMaterialOnMesh(material, node);
566
539
  } else {
567
540
  setInternalMetadataValue(node, 'deferredMaterial', material);
568
541
  }
569
- }
542
+ });
543
+ await Promise.all(observerProms);
570
544
  });
571
545
  this.setParameterObserver(BuiltInParameter.Position, async ({ nodes, newValue }) => {
572
546
  const position = ParameterManager.parseVector(newValue);
@@ -710,12 +684,15 @@ export class ParameterManager {
710
684
  const tagParamEntries = parameterEntries.filter(entry => isTagParameterSubject(entry.subject));
711
685
  const nonTagParamEntries = parameterEntries.filter(entry => !isTagParameterSubject(entry.subject));
712
686
 
713
- for (const entry of tagParamEntries) {
714
- await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
715
- }
716
- for (const entry of nonTagParamEntries) {
717
- await this._applyParameterValue(entry.subject, entry.parameterName, assetContainer);
718
- }
687
+ const tagParamProms = tagParamEntries.map(entry =>
688
+ this._applyParameterValue(entry.subject, entry.parameterName, assetContainer)
689
+ );
690
+ await Promise.all(tagParamProms);
691
+
692
+ const nonTagParamProms = nonTagParamEntries.map(entry =>
693
+ this._applyParameterValue(entry.subject, entry.parameterName, assetContainer)
694
+ );
695
+ await Promise.all(nonTagParamProms);
719
696
  }
720
697
 
721
698
  /**
@@ -811,4 +788,46 @@ export class ParameterManager {
811
788
 
812
789
  return entries;
813
790
  }
791
+
792
+ /**
793
+ * @returns Desired parameter value of a certain node.\
794
+ * Tags are considered as well but have lower priority than node parameters, as these are more specific.
795
+ */
796
+ protected _getParameterValueOfNode(node: TransformNode, parameterName: string): ParameterValue | undefined {
797
+ const nodeParamValue = this.getParameterValue({ nodeName: node.name }, parameterName);
798
+ if (nodeParamValue !== undefined) {
799
+ return nodeParamValue;
800
+ }
801
+
802
+ const tags = getTagsAsStrArr(node);
803
+ const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
804
+ // NOTE: it is possible that values are available for multiple tags
805
+ // in this case the resulting parameter value is quite "random" as the last tag in the tag string of the node has
806
+ // priority
807
+ const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
808
+ return accValue ?? tagParamValue;
809
+ }, undefined);
810
+
811
+ return tagParamValue;
812
+ }
813
+
814
+ /**
815
+ * @returns Desired parameter value of a certain material.\
816
+ * Tags are considered as well but have lower priority than material parameters, as these are more specific.\
817
+ * Unused ATM, but added for consistency as counter part for {@link _getParameterValueOfNode}
818
+ */
819
+ protected _getParameterValueOfMaterial(material: Material, parameterName: string): ParameterValue | undefined {
820
+ const materialParamValue = this.getParameterValue({ materialName: material.name }, parameterName);
821
+ if (materialParamValue !== undefined) {
822
+ return materialParamValue;
823
+ }
824
+
825
+ const tags = getTagsAsStrArr(material);
826
+ const tagParamValue = tags.reduce<ParameterValue | undefined>((accValue, curTag) => {
827
+ const tagParamValue = this.getParameterValue({ tagName: curTag }, parameterName);
828
+ return accValue ?? tagParamValue;
829
+ }, undefined);
830
+
831
+ return tagParamValue;
832
+ }
814
833
  }