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

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 (45) 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 +2 -0
  4. package/dist/lib-cjs/index.js +2 -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/texture-parameter-helper.js +26 -7
  13. package/dist/lib-cjs/internal/texture-parameter-helper.js.map +1 -1
  14. package/dist/lib-cjs/manager/camera-manager.js +4 -2
  15. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  16. package/dist/lib-cjs/manager/debug-manager.js +1 -1
  17. package/dist/lib-cjs/manager/debug-manager.js.map +1 -1
  18. package/dist/lib-cjs/manager/model-manager.d.ts +11 -33
  19. package/dist/lib-cjs/manager/model-manager.js +47 -106
  20. package/dist/lib-cjs/manager/model-manager.js.map +1 -1
  21. package/dist/lib-cjs/manager/parameter-manager.d.ts +16 -11
  22. package/dist/lib-cjs/manager/parameter-manager.js +78 -69
  23. package/dist/lib-cjs/manager/parameter-manager.js.map +1 -1
  24. package/dist/lib-cjs/manager/scene-manager.d.ts +111 -5
  25. package/dist/lib-cjs/manager/scene-manager.js +269 -10
  26. package/dist/lib-cjs/manager/scene-manager.js.map +1 -1
  27. package/dist/lib-cjs/viewer-error.d.ts +1 -0
  28. package/dist/lib-cjs/viewer-error.js +1 -0
  29. package/dist/lib-cjs/viewer-error.js.map +1 -1
  30. package/dist/lib-cjs/viewer.d.ts +4 -13
  31. package/dist/lib-cjs/viewer.js +3 -37
  32. package/dist/lib-cjs/viewer.js.map +1 -1
  33. package/package.json +21 -12
  34. package/src/index.ts +2 -0
  35. package/src/internal/asset-helper.ts +115 -0
  36. package/src/internal/cbn-custom-babylon-loader-plugin.ts +30 -3
  37. package/src/internal/texture-parameter-helper.ts +25 -8
  38. package/src/manager/camera-manager.ts +4 -2
  39. package/src/manager/debug-manager.ts +1 -1
  40. package/src/manager/model-manager.ts +55 -137
  41. package/src/manager/parameter-manager.ts +93 -74
  42. package/src/manager/scene-manager.ts +366 -10
  43. package/src/viewer-error.ts +1 -0
  44. package/src/viewer.ts +13 -55
  45. 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.\
@@ -88,8 +66,8 @@ export class ModelManager {
88
66
  */
89
67
  public static readonly CBN_FALLBACK_MODEL_ASSET_NAME = '$fallback';
90
68
 
91
- protected _modelAssets: { [name: string]: Model } = {};
92
- protected _fallbackModelAsset: Model = {
69
+ protected _modelAssets: { [name: string]: ModelAsset } = {};
70
+ protected _fallbackModelAsset: ModelAsset = {
93
71
  name: ModelManager.CBN_FALLBACK_MODEL_ASSET_NAME,
94
72
  url: '',
95
73
  state: 'loaded',
@@ -113,6 +91,7 @@ export class ModelManager {
113
91
  return inputStr.split('/').join('.');
114
92
  }
115
93
 
94
+ /** @internal */
116
95
  public constructor(protected viewer: Viewer) {}
117
96
 
118
97
  /**
@@ -144,11 +123,11 @@ export class ModelManager {
144
123
  for (const { name, url } of modelAssets) {
145
124
  const existingModel = this._modelAssets[name];
146
125
  if (existingModel) {
147
- console.warn(`Model ${name} is already registered`);
126
+ console.warn(`Model "${name}" is already registered`);
148
127
  return;
149
128
  }
150
129
 
151
- const model: Model = { name, url, state: 'notLoaded', assetContainer: new AssetContainer(), isClone: false };
130
+ const model: ModelAsset = { name, url, state: 'notLoaded', assetContainer: new AssetContainer(), isClone: false };
152
131
  this._modelAssets[name] = model;
153
132
  }
154
133
  }
@@ -170,7 +149,7 @@ export class ModelManager {
170
149
  }
171
150
 
172
151
  if (model.state !== 'notLoaded') {
173
- console.warn(`Model ${name} is already loaded or currently loading`);
152
+ console.warn(`Model "${name}" is already loaded or currently loading`);
174
153
  return;
175
154
  }
176
155
 
@@ -187,14 +166,14 @@ export class ModelManager {
187
166
  public async setModelVisibility(
188
167
  modelVisibility: ModelVisibilityEntry | ModelVisibilityEntry[]
189
168
  ): Promise<ModelVisibilityEntry[]> {
190
- const showModels: Model[] = [];
191
- const hideModels: Model[] = [];
169
+ const modelsToShow: ModelAsset[] = [];
170
+ const modelsToHide: ModelAsset[] = [];
192
171
 
193
172
  if (!isArray(modelVisibility)) {
194
173
  modelVisibility = [modelVisibility];
195
174
  }
196
175
 
197
- for (const entry of modelVisibility) {
176
+ const loadModelProms = modelVisibility.map(async entry => {
198
177
  const model = this._getModel(entry.name);
199
178
  if (!model) {
200
179
  throw new ViewerError({
@@ -206,8 +185,10 @@ export class ModelManager {
206
185
  // there can be multiple "setModelVisibility" calls while the model is loading
207
186
  // loading can be awaited, but it has to be ensured, that the last call of "setModelVisibility" has priority
208
187
  // therefore we store the id of the call in the model
209
- const curVisibilityCallId = (model.visibilityCallId ?? 0) + 1;
210
- model.visibilityCallId = curVisibilityCallId;
188
+ model.visibilityCallId = (model.visibilityCallId ?? 0) + 1;
189
+ const curVisibilityCallId = model.visibilityCallId;
190
+ let showModel = false;
191
+ let hideModel = false;
211
192
 
212
193
  if (entry.visible) {
213
194
  if (model.state === 'notLoaded') {
@@ -215,45 +196,51 @@ export class ModelManager {
215
196
 
216
197
  // check if this is still the latest visibility call
217
198
  if (model.visibilityCallId === curVisibilityCallId) {
218
- showModels.push(model);
199
+ showModel = true;
219
200
  }
220
201
  } else if (model.state === 'loading') {
221
202
  await this._loadModelPromises[model.name];
222
203
 
223
204
  if (model.visibilityCallId === curVisibilityCallId) {
224
- showModels.push(model);
205
+ showModel = true;
225
206
  }
226
207
  } else if (model.state === 'loaded') {
227
- showModels.push(model);
208
+ showModel = true;
228
209
  }
229
210
  } else {
230
211
  if (model.state === 'loading') {
231
212
  await this._loadModelPromises[model.name];
232
213
 
233
214
  if (model.visibilityCallId === curVisibilityCallId) {
234
- hideModels.push(model);
215
+ hideModel = true;
235
216
  }
236
217
  } else if (model.state === 'inScene') {
237
- hideModels.push(model);
218
+ hideModel = true;
238
219
  }
239
220
  }
240
- }
241
-
242
- for (const showModel of showModels) {
243
- await this._prepareModelForScene(showModel);
244
- }
245
221
 
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
- }
222
+ if (showModel) {
223
+ await this._prepareModelForScene(model);
224
+ modelsToShow.push(model);
225
+ } else if (hideModel) {
226
+ modelsToHide.push(model);
227
+ }
228
+ });
229
+ // model loading and preparation (e.g. create materials) is done simultaniously for all models to save time
230
+ // model (`ModelManager._loadModelPromises`) and material queues (`MaterialManager._createMaterialPromises`) are
231
+ // responsible for correct loading state handling of these instances
232
+ await Promise.all(loadModelProms);
253
233
 
254
234
  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 }));
235
+ modelsToShow.forEach(model => {
236
+ // show model is not async, as preparation has already been done
237
+ this._showModel(model, true);
238
+ returnVal.push({ name: model.name, visible: true });
239
+ });
240
+ modelsToHide.forEach(model => {
241
+ this._hideModel(model);
242
+ returnVal.push({ name: model.name, visible: false });
243
+ });
257
244
 
258
245
  return returnVal;
259
246
  }
@@ -294,7 +281,7 @@ export class ModelManager {
294
281
  await this._loadModelPromises[sourceModel.name];
295
282
  }
296
283
 
297
- const clonedModel: Model = {
284
+ const clonedModel: ModelAsset = {
298
285
  name: newModelName,
299
286
  url: sourceModel.url,
300
287
  state: 'loaded',
@@ -400,14 +387,14 @@ export class ModelManager {
400
387
  await this._prepareModelForScene(model);
401
388
  }
402
389
 
403
- return model.cbnBabylonFileData?.decals ?? [];
390
+ return model.decals ?? [];
404
391
  }
405
392
 
406
393
  /**
407
394
  * Get model by name
408
395
  */
409
- protected _getModel(name: string): Model | undefined {
410
- if (name === '$fallback') {
396
+ protected _getModel(name: string): ModelAsset | undefined {
397
+ if (name === ModelManager.CBN_FALLBACK_MODEL_ASSET_NAME) {
411
398
  return this._fallbackModelAsset;
412
399
  }
413
400
 
@@ -420,67 +407,16 @@ export class ModelManager {
420
407
  /**
421
408
  * Load model into scene, but don't show it immediately
422
409
  */
423
- protected async _loadModel(model: Model): Promise<void> {
410
+ protected async _loadModel(model: ModelAsset): Promise<void> {
424
411
  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
- }
412
+ const { cbnData } = await loadAsset(model, this.viewer);
413
+
414
+ model.decals = cbnData?.decals;
463
415
 
464
416
  // remove all lights and cameras from the asset, as this data should be handled globally in the scene instead of
465
417
  // 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';
418
+ [...model.assetContainer.lights].forEach(light => light.dispose());
419
+ [...model.assetContainer.cameras].forEach(camera => camera.dispose());
484
420
 
485
421
  delete this._loadModelPromises[model.name];
486
422
  };
@@ -494,7 +430,7 @@ export class ModelManager {
494
430
  * This is typically done before the model is added to the scene to avoid changes on the visible model, which ensures
495
431
  * a smooth model switch behaviour.
496
432
  */
497
- protected async _prepareModelForScene(model: Model): Promise<void> {
433
+ protected async _prepareModelForScene(model: ModelAsset): Promise<void> {
498
434
  if (model.sourceModelName) {
499
435
  // source model has to be prepared for scene as well in "instantiate" mode, because materials would not be
500
436
  // loaded otherwise
@@ -504,10 +440,7 @@ export class ModelManager {
504
440
  await this._prepareModelForScene(sourceModel);
505
441
  }
506
442
 
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);
443
+ await prepareAssetForScene(model, this.viewer);
511
444
  }
512
445
 
513
446
  /**
@@ -517,7 +450,7 @@ export class ModelManager {
517
450
  * @param skipPreparation optionally skip applying parameter values to model before showing it
518
451
  * (see {@link _prepareModelForScene})
519
452
  */
520
- protected async _showModel(model: Model, skipPreparation?: boolean): Promise<void> {
453
+ protected async _showModel(model: ModelAsset, skipPreparation?: boolean): Promise<void> {
521
454
  if (!skipPreparation) {
522
455
  await this._prepareModelForScene(model);
523
456
  }
@@ -529,23 +462,8 @@ export class ModelManager {
529
462
  /**
530
463
  * Remove assets of asset container from scene
531
464
  */
532
- protected _hideModel(model: Model): void {
465
+ protected _hideModel(model: ModelAsset): void {
533
466
  model.assetContainer.removeFromScene();
534
467
  model.state = 'loaded';
535
468
  }
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
469
  }
@@ -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
  }