@combeenation/3d-viewer 12.4.1 → 12.4.3

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 (114) hide show
  1. package/README.md +9 -9
  2. package/dist/lib-cjs/api/classes/animationInterface.d.ts +8 -8
  3. package/dist/lib-cjs/api/classes/animationInterface.js +2 -2
  4. package/dist/lib-cjs/api/classes/dottedPath.d.ts +79 -79
  5. package/dist/lib-cjs/api/classes/dottedPath.js +166 -166
  6. package/dist/lib-cjs/api/classes/element.d.ts +153 -153
  7. package/dist/lib-cjs/api/classes/element.js +702 -702
  8. package/dist/lib-cjs/api/classes/event.d.ts +401 -401
  9. package/dist/lib-cjs/api/classes/event.js +424 -424
  10. package/dist/lib-cjs/api/classes/eventBroadcaster.d.ts +26 -26
  11. package/dist/lib-cjs/api/classes/eventBroadcaster.js +49 -49
  12. package/dist/lib-cjs/api/classes/fuzzyMap.d.ts +7 -7
  13. package/dist/lib-cjs/api/classes/fuzzyMap.js +21 -21
  14. package/dist/lib-cjs/api/classes/parameter.d.ts +410 -410
  15. package/dist/lib-cjs/api/classes/parameter.js +642 -642
  16. package/dist/lib-cjs/api/classes/parameterObservable.d.ts +36 -36
  17. package/dist/lib-cjs/api/classes/parameterObservable.js +72 -72
  18. package/dist/lib-cjs/api/classes/parameterizable.d.ts +15 -15
  19. package/dist/lib-cjs/api/classes/parameterizable.js +102 -102
  20. package/dist/lib-cjs/api/classes/placementAnimation.d.ts +45 -45
  21. package/dist/lib-cjs/api/classes/placementAnimation.js +176 -176
  22. package/dist/lib-cjs/api/classes/variant.d.ts +261 -261
  23. package/dist/lib-cjs/api/classes/variant.js +872 -872
  24. package/dist/lib-cjs/api/classes/variantInstance.d.ts +53 -53
  25. package/dist/lib-cjs/api/classes/variantInstance.js +125 -125
  26. package/dist/lib-cjs/api/classes/variantParameterizable.d.ts +17 -17
  27. package/dist/lib-cjs/api/classes/variantParameterizable.js +86 -86
  28. package/dist/lib-cjs/api/classes/viewer.d.ts +215 -215
  29. package/dist/lib-cjs/api/classes/viewer.js +708 -708
  30. package/dist/lib-cjs/api/classes/viewerError.d.ts +43 -43
  31. package/dist/lib-cjs/api/classes/viewerError.js +55 -55
  32. package/dist/lib-cjs/api/classes/viewerLight.d.ts +66 -66
  33. package/dist/lib-cjs/api/classes/viewerLight.js +344 -344
  34. package/dist/lib-cjs/api/internal/lensRendering.d.ts +8 -8
  35. package/dist/lib-cjs/api/internal/lensRendering.js +11 -11
  36. package/dist/lib-cjs/api/internal/sceneSetup.d.ts +13 -13
  37. package/dist/lib-cjs/api/internal/sceneSetup.js +227 -227
  38. package/dist/lib-cjs/api/manager/animationManager.d.ts +30 -30
  39. package/dist/lib-cjs/api/manager/animationManager.js +126 -126
  40. package/dist/lib-cjs/api/manager/gltfExportManager.d.ts +80 -80
  41. package/dist/lib-cjs/api/manager/gltfExportManager.js +300 -299
  42. package/dist/lib-cjs/api/manager/gltfExportManager.js.map +1 -1
  43. package/dist/lib-cjs/api/manager/sceneManager.d.ts +33 -33
  44. package/dist/lib-cjs/api/manager/sceneManager.js +128 -128
  45. package/dist/lib-cjs/api/manager/tagManager.d.ts +118 -118
  46. package/dist/lib-cjs/api/manager/tagManager.js +530 -530
  47. package/dist/lib-cjs/api/manager/textureLoadManager.d.ts +22 -22
  48. package/dist/lib-cjs/api/manager/textureLoadManager.js +107 -107
  49. package/dist/lib-cjs/api/manager/variantInstanceManager.d.ts +106 -106
  50. package/dist/lib-cjs/api/manager/variantInstanceManager.js +290 -290
  51. package/dist/lib-cjs/api/store/specStorage.d.ts +32 -32
  52. package/dist/lib-cjs/api/store/specStorage.js +65 -65
  53. package/dist/lib-cjs/api/util/babylonHelper.d.ts +238 -238
  54. package/dist/lib-cjs/api/util/babylonHelper.js +825 -825
  55. package/dist/lib-cjs/api/util/debugHelper.d.ts +9 -9
  56. package/dist/lib-cjs/api/util/debugHelper.js +93 -93
  57. package/dist/lib-cjs/api/util/deviceHelper.d.ts +9 -9
  58. package/dist/lib-cjs/api/util/deviceHelper.js +28 -28
  59. package/dist/lib-cjs/api/util/geometryHelper.d.ts +17 -17
  60. package/dist/lib-cjs/api/util/geometryHelper.js +112 -112
  61. package/dist/lib-cjs/api/util/globalTypes.d.ts +490 -490
  62. package/dist/lib-cjs/api/util/globalTypes.js +1 -1
  63. package/dist/lib-cjs/api/util/resourceHelper.d.ts +58 -58
  64. package/dist/lib-cjs/api/util/resourceHelper.js +214 -214
  65. package/dist/lib-cjs/api/util/sceneLoaderHelper.d.ts +58 -58
  66. package/dist/lib-cjs/api/util/sceneLoaderHelper.js +228 -228
  67. package/dist/lib-cjs/api/util/stringHelper.d.ts +13 -13
  68. package/dist/lib-cjs/api/util/stringHelper.js +32 -32
  69. package/dist/lib-cjs/api/util/structureHelper.d.ts +9 -9
  70. package/dist/lib-cjs/api/util/structureHelper.js +57 -57
  71. package/dist/lib-cjs/buildinfo.json +3 -3
  72. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  73. package/dist/lib-cjs/index.d.ts +63 -63
  74. package/dist/lib-cjs/index.js +128 -128
  75. package/package.json +93 -92
  76. package/src/api/classes/animationInterface.ts +10 -10
  77. package/src/api/classes/dottedPath.ts +181 -181
  78. package/src/api/classes/element.ts +766 -766
  79. package/src/api/classes/event.ts +457 -457
  80. package/src/api/classes/eventBroadcaster.ts +52 -52
  81. package/src/api/classes/fuzzyMap.ts +21 -21
  82. package/src/api/classes/parameter.ts +686 -686
  83. package/src/api/classes/parameterObservable.ts +73 -73
  84. package/src/api/classes/parameterizable.ts +87 -87
  85. package/src/api/classes/placementAnimation.ts +162 -162
  86. package/src/api/classes/variant.ts +965 -965
  87. package/src/api/classes/variantInstance.ts +123 -123
  88. package/src/api/classes/variantParameterizable.ts +83 -83
  89. package/src/api/classes/viewer.ts +751 -751
  90. package/src/api/classes/viewerError.ts +63 -63
  91. package/src/api/classes/viewerLight.ts +335 -335
  92. package/src/api/internal/debugViewer.ts +90 -90
  93. package/src/api/internal/lensRendering.ts +9 -9
  94. package/src/api/internal/sceneSetup.ts +208 -208
  95. package/src/api/manager/animationManager.ts +143 -143
  96. package/src/api/manager/gltfExportManager.ts +337 -334
  97. package/src/api/manager/sceneManager.ts +134 -134
  98. package/src/api/manager/tagManager.ts +572 -572
  99. package/src/api/manager/textureLoadManager.ts +107 -107
  100. package/src/api/manager/variantInstanceManager.ts +306 -306
  101. package/src/api/store/specStorage.ts +68 -68
  102. package/src/api/util/babylonHelper.ts +915 -915
  103. package/src/api/util/debugHelper.ts +121 -121
  104. package/src/api/util/deviceHelper.ts +31 -31
  105. package/src/api/util/geometryHelper.ts +142 -142
  106. package/src/api/util/globalTypes.ts +566 -566
  107. package/src/api/util/resourceHelper.ts +201 -201
  108. package/src/api/util/sceneLoaderHelper.ts +247 -247
  109. package/src/api/util/stringHelper.ts +30 -30
  110. package/src/api/util/structureHelper.ts +62 -62
  111. package/src/buildinfo.json +3 -3
  112. package/src/dev.ts +70 -70
  113. package/src/index.ts +116 -116
  114. package/src/types.d.ts +49 -49
@@ -1,915 +1,915 @@
1
- import { DottedPath } from '../classes/dottedPath';
2
- import { Event, emitter } from '../classes/event';
3
- import { defaultEnvHelperColor, defaultSceneClearColor } from '../internal/sceneSetup';
4
- import {
5
- addMissingMaterialObserver,
6
- createMaterialFromCbnAssets,
7
- missingMaterialMetadataName,
8
- } from './sceneLoaderHelper';
9
- import { EnvironmentHelper } from '@babylonjs/core/Helpers/environmentHelper';
10
- import { PhotoDome } from '@babylonjs/core/Helpers/photoDome';
11
- import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
12
- import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
13
- import { Light } from '@babylonjs/core/Lights/light';
14
- import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
15
- import { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture';
16
- import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';
17
- import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
18
- import { Texture } from '@babylonjs/core/Materials/Textures/texture';
19
- import { Material } from '@babylonjs/core/Materials/material';
20
- import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
21
- import { Axis } from '@babylonjs/core/Maths/math.axis';
22
- import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
23
- import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
24
- import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
25
- import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
26
- import { Mesh } from '@babylonjs/core/Meshes/mesh';
27
- import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
28
- import { Observable } from '@babylonjs/core/Misc/observable';
29
- import { Tags } from '@babylonjs/core/Misc/tags';
30
- import { Tools } from '@babylonjs/core/Misc/tools';
31
- import { Node } from '@babylonjs/core/node';
32
- import { Scene } from '@babylonjs/core/scene';
33
- import { Nullable } from '@babylonjs/core/types';
34
- import { get, has, merge, uniq } from 'lodash-es';
35
-
36
- const backgroundDomeName = 'BackgroundDome_ViewerGenerated';
37
- const envHelperMetadataName = 'viewerEnvHelper';
38
- const materialWaitingToBeSetMetadataName = 'materialWaitingToBeSet';
39
-
40
- // Holds pending `createMaterialFromCbnAssets` promises for dedicated material ids
41
- // In this case we can make sure that a single material is not requested multiple times if used in various nodes
42
- const getMaterialPromises: Map<string, Promise<Material | null>> = new Map();
43
-
44
- /**
45
- * @param node
46
- * @return Node
47
- */
48
- const getRootNode = function (node: Node): Node {
49
- let _node = node;
50
- while (_node.parent) {
51
- _node = _node.parent;
52
- }
53
- return _node;
54
- };
55
-
56
- /**
57
- * @param nodes
58
- * @param predicate
59
- * @return Map<DottedPath, T>
60
- */
61
- const mapToDottedNodes = function <T>(nodes: Node[], predicate?: (node: Node) => boolean): Map<DottedPath, T> {
62
- const map = new Map<DottedPath, T>();
63
- const addNodes = function (_node: Node) {
64
- if (predicate && predicate(_node)) {
65
- map.set(_node.metadata.dottedPath, _node as any);
66
- }
67
- _node.getChildren().forEach(child => {
68
- addNodes(child);
69
- });
70
- };
71
- nodes.forEach(node => {
72
- addNodes(node);
73
- });
74
- return map;
75
- };
76
-
77
- /**
78
- * @param node
79
- * @return DottedPath
80
- */
81
- const getDottedPathForNode = function (node: Node): DottedPath {
82
- const dottedPath = DottedPath.create(node.name);
83
- let _parent = node;
84
- while (_parent.parent) {
85
- _parent = _parent.parent;
86
- dottedPath.unshiftPart(_parent.name);
87
- }
88
- return dottedPath;
89
- };
90
-
91
- /**
92
- * @param node
93
- * @param nodeNamingStrategy
94
- * @param predicate
95
- * @param deep
96
- * @return TransformNode | null
97
- */
98
- const cloneTransformNode = function (
99
- node: TransformNode,
100
- nodeNamingStrategy: NodeNamingStrategy,
101
- predicate?: (node: TransformNode) => boolean,
102
- deep: boolean = true
103
- ): TransformNode | null {
104
- if (predicate && !predicate(node)) {
105
- return null;
106
- }
107
- const newName = nodeNamingStrategy.handler(node, nodeNamingStrategy.payload);
108
- const clone = node.clone(newName, node.parent, true);
109
- if (clone) {
110
- clone.id = newName;
111
- injectMetadata(clone, { cloneSource: node }, false);
112
- injectMetadata(node, { cloneTarget: clone }, false);
113
- // if the cloned node is of type InstancedMesh, due to a bug(?),
114
- // the InstancedMesh.isEnabled state may have changed after cloning.
115
- // in that case, set the clone's enabled state to the original's state
116
- if (node instanceof InstancedMesh) {
117
- clone.setEnabled(node.isEnabled(false));
118
-
119
- // tags are not cloned for instanced meshes by default
120
- if (Tags.HasTags(node)) {
121
- Tags.AddTagsTo(clone, Tags.GetTags(node, true));
122
- }
123
- }
124
- }
125
- if (deep) {
126
- const children = node.getChildTransformNodes(true);
127
- children.forEach(child => {
128
- const clonedChild = cloneTransformNode(child, nodeNamingStrategy, predicate, deep);
129
- if (clonedChild) {
130
- clonedChild.parent = clone;
131
- }
132
- });
133
- }
134
- return clone;
135
- };
136
-
137
- /**
138
- * @param node
139
- */
140
- const cloneNodeWithParents = function (node: Node | null): Node | null {
141
- let clone = null;
142
- if (node instanceof TransformNode) {
143
- clone = node.clone(node.name, cloneNodeWithParents(node.parent) as Nullable<Node>, true);
144
- } else if (node instanceof Light) {
145
- clone = node.clone(node.name, cloneNodeWithParents(node.parent) as Nullable<Node>);
146
- } else if (node) {
147
- throw new Error(`Cloning of "${node?.constructor.name}" is not implemented (yet).`);
148
- }
149
- return clone;
150
- };
151
-
152
- /**
153
- * @param node
154
- * @param deep
155
- * @param prefix
156
- * @return TransformNode
157
- */
158
- const cloneTransformNodeMaterial = function (
159
- node: TransformNode,
160
- prefix: DottedPathArgument = '',
161
- deep: boolean = true
162
- ): TransformNode {
163
- if (node instanceof AbstractMesh && node.material) {
164
- const newMatName = DottedPath.create(prefix).addParts([node.material.name, 'clone', node.uniqueId.toString()]);
165
- node.material = node.material.clone(newMatName.path);
166
- }
167
- if (deep) {
168
- const children = node.getChildTransformNodes(true);
169
- children.forEach(child => cloneTransformNodeMaterial(child, prefix, deep));
170
- }
171
- return node;
172
- };
173
-
174
- /**
175
- * @param object
176
- * @param deep
177
- * @param metadata
178
- */
179
- const injectMetadata = function (object: Node | Material | BaseTexture, metadata: {}, deep: boolean = true) {
180
- object.metadata = merge({}, object.metadata, metadata);
181
- if (deep && object instanceof TransformNode) {
182
- const children = object.getChildTransformNodes(true);
183
- children.forEach(child => injectMetadata(child, metadata, deep));
184
- }
185
- };
186
-
187
- /**
188
- * @param node
189
- * @param assertCallable
190
- * @param callableParameters
191
- * @param deep
192
- */
193
- const assertTransformNode = function (
194
- node: TransformNode,
195
- assertCallable: (node: TransformNode, ...args: any[]) => void,
196
- callableParameters: any[] = [],
197
- deep: boolean = true
198
- ) {
199
- assertCallable(node, ...callableParameters);
200
- if (deep) {
201
- const children = node.getChildTransformNodes(true);
202
- children.forEach(child => assertTransformNode(child, assertCallable, callableParameters, deep));
203
- }
204
- };
205
-
206
- /**
207
- * @param node
208
- * @param parameter
209
- */
210
- const assertMeshCapability = function (node: TransformNode, parameter: string) {
211
- if (!(node instanceof AbstractMesh)) {
212
- const cls = node.getClassName();
213
- throw new Error(
214
- `Changing parameter "${parameter}" of a(n) ${cls} is not supported. Tried to change node "${node.id}".`
215
- );
216
- }
217
- if (node instanceof InstancedMesh) {
218
- throw new Error(
219
- `Changing parameter "${parameter}" of an InstancedMesh is not supported. Tried to change node "${node.id}".`
220
- );
221
- }
222
- };
223
-
224
- /**
225
- * @param node
226
- * @param deep
227
- */
228
- const activateTransformNode = function (node: TransformNode, deep: boolean = true) {
229
- node.setEnabled(true);
230
- /*
231
- if( node instanceof AbstractMesh ) {
232
- node.visibility = 1;
233
- node.isPickable = true;
234
- }
235
- */
236
- if (deep) {
237
- node.getChildTransformNodes(true).forEach(child => activateTransformNode(child, deep));
238
- }
239
- };
240
-
241
- /**
242
- * @param node
243
- * @param deep
244
- */
245
- const deactivateTransformNode = function (node: TransformNode, deep: boolean = true) {
246
- node.setEnabled(false);
247
- /*
248
- if( node instanceof AbstractMesh ) {
249
- node.visibility = 0;
250
- node.isPickable = false;
251
- }
252
- */
253
- if (deep) {
254
- node.getChildTransformNodes(true).forEach(child => deactivateTransformNode(child, deep));
255
- }
256
- };
257
-
258
- /**
259
- * @param node
260
- */
261
- const enableNodeWithParents = function (node: Node) {
262
- node.setEnabled(true);
263
- if (node.parent) {
264
- enableNodeWithParents(node.parent);
265
- }
266
- };
267
-
268
- /**
269
- * @param node
270
- */
271
- const disableNodeWithParents = function (node: Node) {
272
- node.setEnabled(false);
273
- if (node.parent) {
274
- disableNodeWithParents(node.parent);
275
- }
276
- };
277
-
278
- /**
279
- * Applies a {@link TransformationDefinition} consecutively to ensure dependencies in positioning etc.
280
- * @param node
281
- * @param transformation
282
- */
283
- const transformTransformNode = function (node: TransformNode, transformation: TransformationDefinition) {
284
- // scaling
285
- if (!has(node.metadata, 'scaling.initial')) {
286
- injectMetadata(node, { 'scaling.initial': node.scaling }, false);
287
- }
288
- const initialScaling = get(node.metadata, 'scaling.initial') as Vector3;
289
- node.scaling = initialScaling.multiply(transformation.scaling);
290
- // position
291
- if (!has(node.metadata, 'position.initial')) {
292
- injectMetadata(node, { 'position.initial': node.absolutePosition.clone() }, false);
293
- }
294
- const initialPosition = get(node.metadata, 'position.initial') as Vector3;
295
- node.setAbsolutePosition(initialPosition.add(transformation.position).multiply(transformation.scaling));
296
- // rotation
297
- if (!has(node.metadata, 'rotation.initial')) {
298
- let rotationQuaternion = node.rotationQuaternion;
299
- if (!rotationQuaternion) {
300
- rotationQuaternion = Quaternion.RotationYawPitchRoll(node.rotation.x, node.rotation.y, node.rotation.z);
301
- }
302
- injectMetadata(node, { 'rotation.initial': rotationQuaternion.asArray() }, false);
303
- }
304
- const initialRotationQuaternion = Quaternion.FromArray(get(node.metadata, 'rotation.initial') as []);
305
- node.rotationQuaternion = initialRotationQuaternion;
306
- node.rotateAround(Vector3.Zero(), Axis.X, transformation.rotation.x);
307
- node.rotateAround(Vector3.Zero(), Axis.Y, transformation.rotation.y);
308
- node.rotateAround(Vector3.Zero(), Axis.Z, transformation.rotation.z);
309
- node.computeWorldMatrix(true);
310
- };
311
-
312
- /**
313
- * Apply changes of environment (background texture, etc.) consecutively in order to avoid dependency related issues.
314
- * @param scene
315
- * @param envDef
316
- */
317
- const changeEnvironment = function (scene: Scene, envDef: EnvironmentDefinition) {
318
- // issue warning if both background texture and usedefault are set
319
- if (envDef.environmentUseDefault && envDef.environmentBackground) {
320
- console.warn(
321
- `!!! Warning !!! Spec parameter 'environment.usedefault' is being overruled by 'environment.background'.`
322
- );
323
- }
324
-
325
- const useDefaultEnv = envDef.environmentUseDefault && !envDef.environmentBackground;
326
-
327
- // 1) set ENVIRONMENT_COLOR
328
- const clearColorBefore = scene.clearColor.toString();
329
- scene.clearColor = envDef.environmentColor
330
- ? Color4.FromColor3(envDef.environmentColor)
331
- : useDefaultEnv
332
- ? defaultEnvHelperColor
333
- : defaultSceneClearColor;
334
- const clearColorChanged = clearColorBefore !== scene.clearColor.toString();
335
-
336
- // 2) dispose existing & set new ENVIRONMENT (==texture)
337
- const curEnvTexture = scene.environmentTexture as Nullable<CubeTexture>;
338
- const envTextureChanged = envDef.environment !== curEnvTexture?.url;
339
-
340
- if (curEnvTexture && (!envDef.environment || envTextureChanged)) {
341
- curEnvTexture.dispose();
342
- }
343
-
344
- const rotation = envDef.environmentRotation !== undefined ? Tools.ToRadians(envDef.environmentRotation) : 0;
345
- if (envDef.environment && envTextureChanged) {
346
- const envTexture = CubeTexture.CreateFromPrefilteredData(envDef.environment, scene);
347
- envTexture.rotationY = rotation;
348
- scene.environmentTexture = envTexture;
349
- } else if (curEnvTexture && curEnvTexture.rotationY !== rotation) {
350
- curEnvTexture.rotationY = rotation;
351
- }
352
-
353
- // 3) dispose existing & set new ENVIRONMENT_BACKGROUND
354
- const curDome = scene.getNodeByName(backgroundDomeName) as undefined | PhotoDome;
355
- const backgroundUrlChanged = envDef.environmentBackground !== curDome?.texture.url;
356
-
357
- if (curDome && (!envDef.environmentBackground || backgroundUrlChanged)) {
358
- curDome.dispose();
359
- }
360
-
361
- const rotationPhotoDome = -1 * rotation;
362
- if (envDef.environmentBackground && backgroundUrlChanged) {
363
- const textureUrl = envDef.environmentBackground;
364
- const dome = new PhotoDome(backgroundDomeName, textureUrl, { resolution: 32, size: 450 }, scene);
365
- // Rotate submesh to get them in line with environment texture
366
- dome.getChildMeshes(true)[0].rotation.y = Tools.ToRadians(90);
367
- dome.rotation.y = rotationPhotoDome;
368
- } else if (curDome && curDome.rotation.y !== rotationPhotoDome) {
369
- curDome.rotation.y = rotationPhotoDome;
370
- }
371
-
372
- // 4) dispose existing & set new ENVIRONMENT_USEDEFAULT (only if no background is set)
373
- const curEnvHelper = scene.metadata?.[envHelperMetadataName] as undefined | EnvironmentHelper;
374
-
375
- if (curEnvHelper && !useDefaultEnv) {
376
- curEnvHelper.dispose();
377
- delete scene.metadata[envHelperMetadataName];
378
- }
379
-
380
- const envHelperMainColor = Color3.FromArray(scene.clearColor.asArray());
381
- if (useDefaultEnv && !curEnvHelper) {
382
- const envHelper = scene.createDefaultEnvironment({ sizeAuto: true });
383
- envHelper?.setMainColor(envHelperMainColor);
384
- if (envHelper) {
385
- scene.metadata = merge({}, scene.metadata, { [envHelperMetadataName]: envHelper });
386
- }
387
- } else if (curEnvHelper && useDefaultEnv && clearColorChanged) {
388
- curEnvHelper.setMainColor(envHelperMainColor);
389
- }
390
-
391
- // 5) set ENVIRONMENT_INTENSITY
392
- if (envDef.environmentIntensity !== undefined) {
393
- scene.environmentIntensity = envDef.environmentIntensity;
394
- }
395
- };
396
-
397
- /**
398
- * Sets a material by a given material id as material property on the given node.
399
- *
400
- * If the material is not already available in the scene, the viewer tries to create a material based on a Combeenation
401
- * [material asset](https://docs.combeenation.com/docs/howto-create-and-use-babylon-and-material-asset).
402
- * This of course only works if the viewer is used inside a Combeenation configurator.
403
- *
404
- * Furthermore this function also defers the material creation if the node is not visible yet to improve network &
405
- * viewer bootstrap performance as textures are automatically being lazy loaded only when they are actually visible in
406
- * the scene.
407
- *
408
- * Finally the material will not be applied before all its textures have been loaded. In this way "flickering" effects
409
- * will be avoided, since the material would be incomplete without its loaded textures.
410
- */
411
- const setMaterial = function (node: TransformNode, materialId: string, deep: boolean = true, variant?: Variant) {
412
- if (node instanceof AbstractMesh) {
413
- const materialExists = node.getScene().getMaterialById(materialId);
414
- const hasMissingMaterial = has(node.metadata, missingMaterialMetadataName);
415
- const deferMaterialCreation = !materialExists && !node.isEnabled();
416
- if (deferMaterialCreation) {
417
- // do not set the material
418
- injectMetadata(node, { [missingMaterialMetadataName]: materialId }, false);
419
- // if it already had the missing material flag before, there already exists an observer...
420
- if (!hasMissingMaterial) {
421
- addMissingMaterialObserver(node);
422
- }
423
- } else {
424
- // create material and apply it when textures have been loaded
425
- getOrCreateMaterial(node.getScene(), materialId, variant).then(material => {
426
- applyMaterial(material, node).then(() => emitter.emit(Event.MESH_MATERIAL_APPLIED, node, material));
427
-
428
- if (hasMissingMaterial) {
429
- delete node.metadata[missingMaterialMetadataName];
430
- }
431
- });
432
- }
433
- }
434
- // recursively set materials on children (if desired)
435
- if (deep) {
436
- for (const child of node.getChildTransformNodes(true)) {
437
- setMaterial(child, materialId, deep, variant);
438
- }
439
- }
440
- };
441
-
442
- /**
443
- * Removes material from promise map once it has been added to the scene, because we can be sure that the material will
444
- * be taken from the scene directly, the next time it is requested
445
- */
446
- const onNewMaterialAddedCb = (material: Material) => {
447
- if (getMaterialPromises.has(material.id)) {
448
- getMaterialPromises.delete(material.id);
449
- }
450
- };
451
-
452
- /**
453
- * Responsible for adding the `onNewMaterialAddedCb` only once
454
- */
455
- const registerOnNewMaterialAddedCbOnce = (scene: Scene) => {
456
- const observerAlreadyAssigned = scene.onNewMaterialAddedObservable.observers.find(
457
- observer => observer.callback === onNewMaterialAddedCb
458
- );
459
-
460
- if (!observerAlreadyAssigned) {
461
- scene.onNewMaterialAddedObservable.add(onNewMaterialAddedCb);
462
- }
463
- };
464
-
465
- /**
466
- * Gets the Material either from the given {@link Variant}, the given scene or tries to create it from an Combeenation
467
- * material asset when inside a Combeenation configurator.
468
- */
469
- const getOrCreateMaterial = async function (scene: Scene, materialId: string, variant?: Variant): Promise<Material> {
470
- registerOnNewMaterialAddedCbOnce(scene);
471
-
472
- let chosenMaterial: Material | undefined | null;
473
- chosenMaterial = variant?.inheritedMaterials.find(mat => mat.id === materialId);
474
- chosenMaterial = chosenMaterial || scene.materials.find(mat => mat.id === materialId);
475
-
476
- if (!chosenMaterial) {
477
- // material not available yet, request it from Combeenation assets
478
- if (getMaterialPromises.has(materialId)) {
479
- // request is already pending, just wait for the result
480
- chosenMaterial = await getMaterialPromises.get(materialId);
481
- } else {
482
- // request not pending, call the dedicated function
483
- const getMaterialProm = createMaterialFromCbnAssets(materialId, scene);
484
- // store the promise in a global map, so that subsequent requests can reference it
485
- getMaterialPromises.set(materialId, getMaterialProm);
486
- chosenMaterial = await getMaterialProm;
487
- }
488
- }
489
-
490
- if (chosenMaterial) {
491
- return chosenMaterial as Material;
492
- }
493
- // reject when material was not found
494
- let rejectMessage = `Material with id "${materialId}" does not exist on scene.`;
495
- if (variant) {
496
- rejectMessage = `Material with id "${materialId}" does not exist for variant "${variant.id}".`;
497
- }
498
- throw new Error(rejectMessage);
499
- };
500
-
501
- /**
502
- * Waits until the materials textures are loaded and shaders are compiled.
503
- * Then sets the material on the node if there is no newer "set material" request
504
- */
505
- const applyMaterial = async function (material: Material, node: AbstractMesh) {
506
- // set current material id as last valid id, in this case all previously set materials on the node will be invalidated
507
- injectMetadata(node, { [materialWaitingToBeSetMetadataName]: material.id }, false);
508
-
509
- const promTexturesReady = new Promise<void>(resolve =>
510
- BaseTexture.WhenAllReady(material.getActiveTextures(), resolve)
511
- );
512
- // this promise should only take some time on the first call of the corresponding shader (eg: PBRMaterial shader)
513
- // on each other call of the same material/shader type there should be not be a waiting time, or at maximum one frame
514
- // https://forum.babylonjs.com/t/mesh-flickering-when-setting-material-initially/37312
515
- const promMaterialCompiled = material.forceCompilationAsync(node);
516
-
517
- // material needs to fulfill 2 criterias before it's ready to use
518
- // - textures need to be "ready" => downloaded
519
- // - dedicated shader needs to be compiled
520
- // if this would not be the case you can see some "flickering" when setting the material
521
- await Promise.all([promTexturesReady, promMaterialCompiled]);
522
-
523
- // textures ready, now check if the material is still up-to-date
524
- if (material.id === node.metadata?.[materialWaitingToBeSetMetadataName]) {
525
- node.material = material;
526
- delete node.metadata[materialWaitingToBeSetMetadataName];
527
- }
528
- };
529
-
530
- /**
531
- * !!! Warning !!!
532
- * This function is not public API. Whilst it can help solving certain problems, it only works reliably in well defined
533
- * situations and can cause unwanted side effects under some conditions. Use carefully at your own risk!
534
- *
535
- * See https://combeenation.myjetbrains.com/youtrack/issue/CB-5906 for further details regarding this warning.
536
- *
537
- * Set material of an instanced meshes source mesh.
538
- * Changes the material of all instanced meshes which have the same source mesh.
539
- *
540
- * @param node
541
- * @param material
542
- * @param deep
543
- *
544
- * @ignore
545
- */
546
- const setSourceNodeMaterial = function (node: TransformNode, material: Material, deep: boolean = true) {
547
- const warn = ` You're using "setSourceNodeMaterial" which is not public API.
548
- Whilst it can help solving certain problems, it only works reliably in well defined situations and can cause unwanted side effects under some conditions.
549
- Use carefully at your own risk!`;
550
- console.warn(`!!! Warning !!!\n${warn}`);
551
-
552
- if (node instanceof InstancedMesh) {
553
- node.sourceMesh.material = material;
554
- }
555
- if (deep) {
556
- node.getChildTransformNodes(true).forEach(child => setSourceNodeMaterial(child, material, deep));
557
- }
558
- };
559
-
560
- /**
561
- * @param node
562
- * @param color
563
- * @param deep
564
- */
565
- const setMaterialColor = function (node: TransformNode, color: Color3, deep: boolean = true) {
566
- if (node instanceof AbstractMesh && node.material) {
567
- const materialCls = node.material.getClassName();
568
- switch (materialCls) {
569
- case 'PBRMaterial':
570
- (node.material as PBRMaterial).albedoColor = color.toLinearSpace();
571
- break;
572
- case 'StandardMaterial':
573
- (node.material as StandardMaterial).diffuseColor = color;
574
- break;
575
- default:
576
- throw new Error(`Setting color for material of instance "${materialCls}" not implemented (yet).`);
577
- }
578
- setMaterial(node, node.material.id, false);
579
- }
580
- if (deep) {
581
- node.getChildTransformNodes(true).forEach(child => setMaterialColor(child, color, deep));
582
- }
583
- };
584
-
585
- /**
586
- * @param node
587
- * @param texture
588
- * @param deep
589
- */
590
- const setMaterialTexture = function (node: TransformNode, texture: Texture, deep: boolean = true) {
591
- if (node instanceof AbstractMesh && node.material) {
592
- const materialCls = node.material.getClassName();
593
- switch (materialCls) {
594
- case 'PBRMaterial':
595
- (node.material as PBRMaterial).albedoTexture = texture;
596
- break;
597
- case 'StandardMaterial':
598
- (node.material as StandardMaterial).diffuseTexture = texture;
599
- break;
600
- default:
601
- throw new Error(`Setting texture for material of instance "${materialCls}" not implemented (yet).`);
602
- }
603
- setMaterial(node, node.material.id, false);
604
- }
605
- if (deep) {
606
- node.getChildTransformNodes(true).forEach(child => setMaterialTexture(child, texture, deep));
607
- }
608
- };
609
-
610
- /**
611
- * @param node
612
- * @param metallness
613
- * @param deep
614
- */
615
- const setMaterialMetallness = function (node: TransformNode, metallness: number, deep: boolean = true) {
616
- if (node instanceof AbstractMesh && node.material) {
617
- const materialCls = node.material.getClassName();
618
- switch (materialCls) {
619
- case 'PBRMaterial':
620
- (node.material as PBRMaterial).metallic = metallness;
621
- break;
622
- default:
623
- throw new Error(`Setting metallness for material of instance "${materialCls}" not implemented (yet).`);
624
- }
625
- setMaterial(node, node.material.id, false);
626
- }
627
- if (deep) {
628
- node.getChildTransformNodes(true).forEach(child => setMaterialMetallness(child, metallness, deep));
629
- }
630
- };
631
-
632
- /**
633
- * @param node
634
- * @param roughness
635
- * @param deep
636
- */
637
- const setMaterialRoughness = function (node: TransformNode, roughness: number, deep: boolean = true) {
638
- if (node instanceof AbstractMesh && node.material) {
639
- const materialCls = node.material.getClassName();
640
- switch (materialCls) {
641
- case 'PBRMaterial':
642
- (node.material as PBRMaterial).roughness = roughness;
643
- break;
644
- case 'StandardMaterial':
645
- (node.material as StandardMaterial).roughness = roughness;
646
- break;
647
- default:
648
- throw new Error(`Setting roughness for material of instance "${materialCls}" not implemented (yet).`);
649
- }
650
- setMaterial(node, node.material.id, false);
651
- }
652
- if (deep) {
653
- node.getChildTransformNodes(true).forEach(child => setMaterialRoughness(child, roughness, deep));
654
- }
655
- };
656
-
657
- /**
658
- * @param node
659
- * @param layer
660
- * @param color
661
- * @param deep
662
- */
663
- const addToHighlightLayer = function (layer: HighlightLayer, color: Color3, node: TransformNode, deep: boolean = true) {
664
- if (node instanceof AbstractMesh) {
665
- layer.addMesh(node as Mesh, color);
666
- }
667
- if (deep) {
668
- node.getChildTransformNodes(true).forEach(child => addToHighlightLayer(layer, color, child, deep));
669
- }
670
- };
671
-
672
- /**
673
- * @param node
674
- * @param layer
675
- * @param deep
676
- */
677
- const removeFromHighlightLayer = function (layer: HighlightLayer, node: TransformNode, deep: boolean = true) {
678
- if (node instanceof AbstractMesh) {
679
- layer.removeMesh(node as Mesh);
680
- }
681
- if (deep) {
682
- node.getChildTransformNodes(true).forEach(child => removeFromHighlightLayer(layer, child, deep));
683
- }
684
- };
685
-
686
- /**
687
- * @param node
688
- * @param receiveShadows
689
- * @param deep
690
- */
691
- const setReceiveShadows = function (node: TransformNode, receiveShadows: boolean, deep: boolean = true) {
692
- if (node instanceof AbstractMesh) {
693
- node.receiveShadows = receiveShadows;
694
- }
695
- if (deep) {
696
- node.getChildTransformNodes(true).forEach(child => setReceiveShadows(child, receiveShadows, deep));
697
- }
698
- };
699
-
700
- /**
701
- * @param node
702
- * @param generator
703
- * @param deep
704
- */
705
- const addToShadowGenerator = function (generator: ShadowGenerator, node: TransformNode, deep: boolean = true) {
706
- if (node instanceof AbstractMesh) {
707
- // We have to remove the node because there's no duplicate check in babylon
708
- generator.removeShadowCaster(node, false);
709
- generator.addShadowCaster(node, false);
710
- }
711
- if (deep) {
712
- node.getChildTransformNodes(true).forEach(child => addToShadowGenerator(generator, child, deep));
713
- }
714
- };
715
-
716
- /**
717
- * @param node
718
- * @param generator
719
- * @param deep
720
- */
721
- const removeFromShadowGenerator = function (generator: ShadowGenerator, node: TransformNode, deep: boolean = true) {
722
- if (node instanceof AbstractMesh) {
723
- generator.removeShadowCaster(node, false);
724
- }
725
- if (deep) {
726
- node.getChildTransformNodes(true).forEach(child => removeFromShadowGenerator(generator, child, deep));
727
- }
728
- };
729
-
730
- /**
731
- * https://forum.babylonjs.com/t/get-mesh-bounding-box-position-and-size-in-2d-screen-coordinates/1058/3
732
- * @param mesh
733
- * @param scene
734
- * @param canvas
735
- */
736
- const getClientRectFromMesh = function (mesh: AbstractMesh, scene: Scene, canvas: HTMLCanvasElement): ClientRect {
737
- // get bounding box of the mesh
738
- const meshVectors = mesh.getBoundingInfo().boundingBox.vectors;
739
- // get the matrix and viewport needed to project the vectors onto the screen
740
- const worldMatrix = mesh.getWorldMatrix();
741
- const transformMatrix = scene.getTransformMatrix();
742
- const viewport = scene.activeCamera!.viewport;
743
- // loop though all the vectors and project them against the current camera viewport to get a set of coordinates
744
- const coordinates = meshVectors.map(vector => {
745
- const projection = Vector3.Project(vector, worldMatrix, transformMatrix, viewport);
746
- projection.x = projection.x * canvas.clientWidth;
747
- projection.y = projection.y * canvas.clientHeight;
748
- return projection;
749
- });
750
- // get the min and max for all the coordinates so we can calculate the largest possible screen size
751
- const maxX = Math.max(...coordinates.map(o => o.x));
752
- const minX = Math.min(...coordinates.map(o => o.x));
753
- const maxY = Math.max(...coordinates.map(o => o.y));
754
- const minY = Math.min(...coordinates.map(o => o.y));
755
- // return a ClientRect from this
756
- return {
757
- width: maxX - minX,
758
- height: maxY - minY,
759
- left: minX,
760
- top: minY,
761
- right: maxX,
762
- bottom: maxY,
763
- } as ClientRect;
764
- };
765
-
766
- type LoadableBaseTexture = BaseTexture & {
767
- url: string;
768
- onLoadObservable: Observable<BaseTexture>;
769
- };
770
-
771
- /**
772
- * This type guard checks whether the given `BaseTextures` is any of its subtypes which comes with an url and
773
- * `onLoadObservable`.
774
- *
775
- * !!! Timing of when this function is called is important !!!
776
- * See the following for more details: https://forum.babylonjs.com/t/basetexture-whenallready-returns-too-early/34501/6
777
- */
778
- const isLoadableTexture = function (texture: BaseTexture): texture is LoadableBaseTexture {
779
- const castedTexture = texture as LoadableBaseTexture;
780
- return !!castedTexture.url && !!castedTexture.onLoadObservable;
781
- };
782
-
783
- /**
784
- * @param objects
785
- * @param tagMapping
786
- */
787
- const mapTags = function (objects: object[], tagMapping: TagMapping): string[] {
788
- let affectedTags: string[] = [];
789
- for (const object of objects) {
790
- const oldTags = Object.keys(Tags.GetTags(object, false) ?? {});
791
- const mappedTags = oldTags.map(t => tagMapping[t] || t);
792
- Tags.RemoveTagsFrom(object, oldTags.join(' '));
793
- Tags.AddTagsTo(object, mappedTags.join(' '));
794
- affectedTags = [...affectedTags, ...mappedTags];
795
- }
796
- return uniq(affectedTags);
797
- };
798
-
799
- /**
800
- * Gets an array of ids for nodeIds that exist in both TransformNode arrays.
801
- * @param nodes1
802
- * @param nodes2
803
- * @param predicate
804
- */
805
- const intersectingNodeNames = function (nodes1: Node[], nodes2: Node[], predicate?: (node: Node) => boolean): string[] {
806
- const nodeNames1 = nodes1.filter(n => !predicate || predicate(n)).map(n => n.name);
807
- const nodesNames2 = nodes2.filter(n => !predicate || predicate(n)).map(n => n.name);
808
- return nodeNames1.filter(n1 => nodesNames2.includes(n1));
809
- };
810
-
811
- /**
812
- * Gets an array of ids for duplicate nodeIds in given node array.
813
- * @param nodes
814
- * @param predicate
815
- */
816
- const duplicateNodeNames = function (nodes: Node[], predicate?: (node: Node) => boolean): string[] {
817
- const nodeNames = nodes.filter(n => !predicate || predicate(n)).map(n => n.name);
818
- return nodeNames.filter((v, i, self) => self.indexOf(v) !== i);
819
- };
820
-
821
- /**
822
- * Reports duplicate nodeIds.
823
- * @param nodeNames
824
- */
825
- const reportDuplicateNodeNames = function (nodeNames: string[]): void {
826
- if (nodeNames.length > 0) {
827
- const warn = `There are duplicate nodeNames: ${nodeNames.join(', ')}`;
828
- console.warn(`!!! Warning !!!\n${warn}`);
829
- window.Cbn?.Assets.reportDuplicateNodeName(nodeNames);
830
- }
831
- };
832
-
833
- const drawPaintableOnMaterial = function (
834
- material: Material,
835
- imageSource: HTMLImageElement,
836
- scene: Scene,
837
- options?: PaintableOptions
838
- ) {
839
- // always take width and height from image source, scaling is done with uvScale properties
840
- const widthAndHeight = {
841
- width: imageSource.width,
842
- height: imageSource.height,
843
- };
844
-
845
- const texture = new DynamicTexture(`${material.id}.paintable_texture`, widthAndHeight, scene);
846
-
847
- // draw image on texture
848
- const ctx = texture.getContext();
849
- ctx.drawImage(imageSource, 0, 0);
850
- texture.update();
851
-
852
- // apply settings from paintable options to tweak position and scaling of image on the texture
853
- texture.uScale = options?.uScale ?? texture.uScale;
854
- texture.vScale = options?.vScale ?? texture.vScale;
855
- texture.uOffset = options?.uOffset ?? texture.uOffset;
856
- texture.vOffset = options?.vOffset ?? texture.vOffset;
857
-
858
- // wrap mode is preferred, as it will always show the texture, no matter which position offset is currently chosen
859
- // clamp mode requires more knowledge (and patience) when adjusting the uv scale and offset values
860
- texture.wrapU = Texture.WRAP_ADDRESSMODE;
861
- texture.wrapV = Texture.WRAP_ADDRESSMODE;
862
-
863
- // apply the paintable texture on the dedicated material type
864
- const materialCls = material.getClassName();
865
- switch (materialCls) {
866
- case 'PBRMaterial':
867
- (material as PBRMaterial).albedoTexture = texture;
868
- break;
869
- case 'StandardMaterial':
870
- (material as StandardMaterial).diffuseTexture = texture;
871
- break;
872
- default:
873
- throw new Error(`Setting paintable texture for material of instance "${materialCls}" not implemented (yet).`);
874
- }
875
- };
876
-
877
- export {
878
- getRootNode,
879
- isLoadableTexture,
880
- mapToDottedNodes,
881
- getDottedPathForNode,
882
- cloneTransformNode,
883
- cloneNodeWithParents,
884
- cloneTransformNodeMaterial,
885
- getOrCreateMaterial,
886
- applyMaterial,
887
- injectMetadata,
888
- assertTransformNode,
889
- assertMeshCapability,
890
- activateTransformNode,
891
- deactivateTransformNode,
892
- enableNodeWithParents,
893
- disableNodeWithParents,
894
- transformTransformNode,
895
- setMaterial,
896
- setSourceNodeMaterial,
897
- setMaterialColor,
898
- setMaterialTexture,
899
- setMaterialMetallness,
900
- setMaterialRoughness,
901
- addToHighlightLayer,
902
- removeFromHighlightLayer,
903
- setReceiveShadows,
904
- addToShadowGenerator,
905
- removeFromShadowGenerator,
906
- getClientRectFromMesh,
907
- changeEnvironment,
908
- backgroundDomeName,
909
- envHelperMetadataName,
910
- mapTags,
911
- intersectingNodeNames,
912
- duplicateNodeNames,
913
- reportDuplicateNodeNames,
914
- drawPaintableOnMaterial,
915
- };
1
+ import { DottedPath } from '../classes/dottedPath';
2
+ import { Event, emitter } from '../classes/event';
3
+ import { defaultEnvHelperColor, defaultSceneClearColor } from '../internal/sceneSetup';
4
+ import {
5
+ addMissingMaterialObserver,
6
+ createMaterialFromCbnAssets,
7
+ missingMaterialMetadataName,
8
+ } from './sceneLoaderHelper';
9
+ import { EnvironmentHelper } from '@babylonjs/core/Helpers/environmentHelper';
10
+ import { PhotoDome } from '@babylonjs/core/Helpers/photoDome';
11
+ import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
12
+ import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
13
+ import { Light } from '@babylonjs/core/Lights/light';
14
+ import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
15
+ import { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture';
16
+ import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';
17
+ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
18
+ import { Texture } from '@babylonjs/core/Materials/Textures/texture';
19
+ import { Material } from '@babylonjs/core/Materials/material';
20
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
21
+ import { Axis } from '@babylonjs/core/Maths/math.axis';
22
+ import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
23
+ import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector';
24
+ import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
25
+ import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
26
+ import { Mesh } from '@babylonjs/core/Meshes/mesh';
27
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
28
+ import { Observable } from '@babylonjs/core/Misc/observable';
29
+ import { Tags } from '@babylonjs/core/Misc/tags';
30
+ import { Tools } from '@babylonjs/core/Misc/tools';
31
+ import { Node } from '@babylonjs/core/node';
32
+ import { Scene } from '@babylonjs/core/scene';
33
+ import { Nullable } from '@babylonjs/core/types';
34
+ import { get, has, merge, uniq } from 'lodash-es';
35
+
36
+ const backgroundDomeName = 'BackgroundDome_ViewerGenerated';
37
+ const envHelperMetadataName = 'viewerEnvHelper';
38
+ const materialWaitingToBeSetMetadataName = 'materialWaitingToBeSet';
39
+
40
+ // Holds pending `createMaterialFromCbnAssets` promises for dedicated material ids
41
+ // In this case we can make sure that a single material is not requested multiple times if used in various nodes
42
+ const getMaterialPromises: Map<string, Promise<Material | null>> = new Map();
43
+
44
+ /**
45
+ * @param node
46
+ * @return Node
47
+ */
48
+ const getRootNode = function (node: Node): Node {
49
+ let _node = node;
50
+ while (_node.parent) {
51
+ _node = _node.parent;
52
+ }
53
+ return _node;
54
+ };
55
+
56
+ /**
57
+ * @param nodes
58
+ * @param predicate
59
+ * @return Map<DottedPath, T>
60
+ */
61
+ const mapToDottedNodes = function <T>(nodes: Node[], predicate?: (node: Node) => boolean): Map<DottedPath, T> {
62
+ const map = new Map<DottedPath, T>();
63
+ const addNodes = function (_node: Node) {
64
+ if (predicate && predicate(_node)) {
65
+ map.set(_node.metadata.dottedPath, _node as any);
66
+ }
67
+ _node.getChildren().forEach(child => {
68
+ addNodes(child);
69
+ });
70
+ };
71
+ nodes.forEach(node => {
72
+ addNodes(node);
73
+ });
74
+ return map;
75
+ };
76
+
77
+ /**
78
+ * @param node
79
+ * @return DottedPath
80
+ */
81
+ const getDottedPathForNode = function (node: Node): DottedPath {
82
+ const dottedPath = DottedPath.create(node.name);
83
+ let _parent = node;
84
+ while (_parent.parent) {
85
+ _parent = _parent.parent;
86
+ dottedPath.unshiftPart(_parent.name);
87
+ }
88
+ return dottedPath;
89
+ };
90
+
91
+ /**
92
+ * @param node
93
+ * @param nodeNamingStrategy
94
+ * @param predicate
95
+ * @param deep
96
+ * @return TransformNode | null
97
+ */
98
+ const cloneTransformNode = function (
99
+ node: TransformNode,
100
+ nodeNamingStrategy: NodeNamingStrategy,
101
+ predicate?: (node: TransformNode) => boolean,
102
+ deep: boolean = true
103
+ ): TransformNode | null {
104
+ if (predicate && !predicate(node)) {
105
+ return null;
106
+ }
107
+ const newName = nodeNamingStrategy.handler(node, nodeNamingStrategy.payload);
108
+ const clone = node.clone(newName, node.parent, true);
109
+ if (clone) {
110
+ clone.id = newName;
111
+ injectMetadata(clone, { cloneSource: node }, false);
112
+ injectMetadata(node, { cloneTarget: clone }, false);
113
+ // if the cloned node is of type InstancedMesh, due to a bug(?),
114
+ // the InstancedMesh.isEnabled state may have changed after cloning.
115
+ // in that case, set the clone's enabled state to the original's state
116
+ if (node instanceof InstancedMesh) {
117
+ clone.setEnabled(node.isEnabled(false));
118
+
119
+ // tags are not cloned for instanced meshes by default
120
+ if (Tags.HasTags(node)) {
121
+ Tags.AddTagsTo(clone, Tags.GetTags(node, true));
122
+ }
123
+ }
124
+ }
125
+ if (deep) {
126
+ const children = node.getChildTransformNodes(true);
127
+ children.forEach(child => {
128
+ const clonedChild = cloneTransformNode(child, nodeNamingStrategy, predicate, deep);
129
+ if (clonedChild) {
130
+ clonedChild.parent = clone;
131
+ }
132
+ });
133
+ }
134
+ return clone;
135
+ };
136
+
137
+ /**
138
+ * @param node
139
+ */
140
+ const cloneNodeWithParents = function (node: Node | null): Node | null {
141
+ let clone = null;
142
+ if (node instanceof TransformNode) {
143
+ clone = node.clone(node.name, cloneNodeWithParents(node.parent) as Nullable<Node>, true);
144
+ } else if (node instanceof Light) {
145
+ clone = node.clone(node.name, cloneNodeWithParents(node.parent) as Nullable<Node>);
146
+ } else if (node) {
147
+ throw new Error(`Cloning of "${node?.constructor.name}" is not implemented (yet).`);
148
+ }
149
+ return clone;
150
+ };
151
+
152
+ /**
153
+ * @param node
154
+ * @param deep
155
+ * @param prefix
156
+ * @return TransformNode
157
+ */
158
+ const cloneTransformNodeMaterial = function (
159
+ node: TransformNode,
160
+ prefix: DottedPathArgument = '',
161
+ deep: boolean = true
162
+ ): TransformNode {
163
+ if (node instanceof AbstractMesh && node.material) {
164
+ const newMatName = DottedPath.create(prefix).addParts([node.material.name, 'clone', node.uniqueId.toString()]);
165
+ node.material = node.material.clone(newMatName.path);
166
+ }
167
+ if (deep) {
168
+ const children = node.getChildTransformNodes(true);
169
+ children.forEach(child => cloneTransformNodeMaterial(child, prefix, deep));
170
+ }
171
+ return node;
172
+ };
173
+
174
+ /**
175
+ * @param object
176
+ * @param deep
177
+ * @param metadata
178
+ */
179
+ const injectMetadata = function (object: Node | Material | BaseTexture, metadata: {}, deep: boolean = true) {
180
+ object.metadata = merge({}, object.metadata, metadata);
181
+ if (deep && object instanceof TransformNode) {
182
+ const children = object.getChildTransformNodes(true);
183
+ children.forEach(child => injectMetadata(child, metadata, deep));
184
+ }
185
+ };
186
+
187
+ /**
188
+ * @param node
189
+ * @param assertCallable
190
+ * @param callableParameters
191
+ * @param deep
192
+ */
193
+ const assertTransformNode = function (
194
+ node: TransformNode,
195
+ assertCallable: (node: TransformNode, ...args: any[]) => void,
196
+ callableParameters: any[] = [],
197
+ deep: boolean = true
198
+ ) {
199
+ assertCallable(node, ...callableParameters);
200
+ if (deep) {
201
+ const children = node.getChildTransformNodes(true);
202
+ children.forEach(child => assertTransformNode(child, assertCallable, callableParameters, deep));
203
+ }
204
+ };
205
+
206
+ /**
207
+ * @param node
208
+ * @param parameter
209
+ */
210
+ const assertMeshCapability = function (node: TransformNode, parameter: string) {
211
+ if (!(node instanceof AbstractMesh)) {
212
+ const cls = node.getClassName();
213
+ throw new Error(
214
+ `Changing parameter "${parameter}" of a(n) ${cls} is not supported. Tried to change node "${node.id}".`
215
+ );
216
+ }
217
+ if (node instanceof InstancedMesh) {
218
+ throw new Error(
219
+ `Changing parameter "${parameter}" of an InstancedMesh is not supported. Tried to change node "${node.id}".`
220
+ );
221
+ }
222
+ };
223
+
224
+ /**
225
+ * @param node
226
+ * @param deep
227
+ */
228
+ const activateTransformNode = function (node: TransformNode, deep: boolean = true) {
229
+ node.setEnabled(true);
230
+ /*
231
+ if( node instanceof AbstractMesh ) {
232
+ node.visibility = 1;
233
+ node.isPickable = true;
234
+ }
235
+ */
236
+ if (deep) {
237
+ node.getChildTransformNodes(true).forEach(child => activateTransformNode(child, deep));
238
+ }
239
+ };
240
+
241
+ /**
242
+ * @param node
243
+ * @param deep
244
+ */
245
+ const deactivateTransformNode = function (node: TransformNode, deep: boolean = true) {
246
+ node.setEnabled(false);
247
+ /*
248
+ if( node instanceof AbstractMesh ) {
249
+ node.visibility = 0;
250
+ node.isPickable = false;
251
+ }
252
+ */
253
+ if (deep) {
254
+ node.getChildTransformNodes(true).forEach(child => deactivateTransformNode(child, deep));
255
+ }
256
+ };
257
+
258
+ /**
259
+ * @param node
260
+ */
261
+ const enableNodeWithParents = function (node: Node) {
262
+ node.setEnabled(true);
263
+ if (node.parent) {
264
+ enableNodeWithParents(node.parent);
265
+ }
266
+ };
267
+
268
+ /**
269
+ * @param node
270
+ */
271
+ const disableNodeWithParents = function (node: Node) {
272
+ node.setEnabled(false);
273
+ if (node.parent) {
274
+ disableNodeWithParents(node.parent);
275
+ }
276
+ };
277
+
278
+ /**
279
+ * Applies a {@link TransformationDefinition} consecutively to ensure dependencies in positioning etc.
280
+ * @param node
281
+ * @param transformation
282
+ */
283
+ const transformTransformNode = function (node: TransformNode, transformation: TransformationDefinition) {
284
+ // scaling
285
+ if (!has(node.metadata, 'scaling.initial')) {
286
+ injectMetadata(node, { 'scaling.initial': node.scaling }, false);
287
+ }
288
+ const initialScaling = get(node.metadata, 'scaling.initial') as Vector3;
289
+ node.scaling = initialScaling.multiply(transformation.scaling);
290
+ // position
291
+ if (!has(node.metadata, 'position.initial')) {
292
+ injectMetadata(node, { 'position.initial': node.absolutePosition.clone() }, false);
293
+ }
294
+ const initialPosition = get(node.metadata, 'position.initial') as Vector3;
295
+ node.setAbsolutePosition(initialPosition.add(transformation.position).multiply(transformation.scaling));
296
+ // rotation
297
+ if (!has(node.metadata, 'rotation.initial')) {
298
+ let rotationQuaternion = node.rotationQuaternion;
299
+ if (!rotationQuaternion) {
300
+ rotationQuaternion = Quaternion.RotationYawPitchRoll(node.rotation.x, node.rotation.y, node.rotation.z);
301
+ }
302
+ injectMetadata(node, { 'rotation.initial': rotationQuaternion.asArray() }, false);
303
+ }
304
+ const initialRotationQuaternion = Quaternion.FromArray(get(node.metadata, 'rotation.initial') as []);
305
+ node.rotationQuaternion = initialRotationQuaternion;
306
+ node.rotateAround(Vector3.Zero(), Axis.X, transformation.rotation.x);
307
+ node.rotateAround(Vector3.Zero(), Axis.Y, transformation.rotation.y);
308
+ node.rotateAround(Vector3.Zero(), Axis.Z, transformation.rotation.z);
309
+ node.computeWorldMatrix(true);
310
+ };
311
+
312
+ /**
313
+ * Apply changes of environment (background texture, etc.) consecutively in order to avoid dependency related issues.
314
+ * @param scene
315
+ * @param envDef
316
+ */
317
+ const changeEnvironment = function (scene: Scene, envDef: EnvironmentDefinition) {
318
+ // issue warning if both background texture and usedefault are set
319
+ if (envDef.environmentUseDefault && envDef.environmentBackground) {
320
+ console.warn(
321
+ `!!! Warning !!! Spec parameter 'environment.usedefault' is being overruled by 'environment.background'.`
322
+ );
323
+ }
324
+
325
+ const useDefaultEnv = envDef.environmentUseDefault && !envDef.environmentBackground;
326
+
327
+ // 1) set ENVIRONMENT_COLOR
328
+ const clearColorBefore = scene.clearColor.toString();
329
+ scene.clearColor = envDef.environmentColor
330
+ ? Color4.FromColor3(envDef.environmentColor)
331
+ : useDefaultEnv
332
+ ? defaultEnvHelperColor
333
+ : defaultSceneClearColor;
334
+ const clearColorChanged = clearColorBefore !== scene.clearColor.toString();
335
+
336
+ // 2) dispose existing & set new ENVIRONMENT (==texture)
337
+ const curEnvTexture = scene.environmentTexture as Nullable<CubeTexture>;
338
+ const envTextureChanged = envDef.environment !== curEnvTexture?.url;
339
+
340
+ if (curEnvTexture && (!envDef.environment || envTextureChanged)) {
341
+ curEnvTexture.dispose();
342
+ }
343
+
344
+ const rotation = envDef.environmentRotation !== undefined ? Tools.ToRadians(envDef.environmentRotation) : 0;
345
+ if (envDef.environment && envTextureChanged) {
346
+ const envTexture = CubeTexture.CreateFromPrefilteredData(envDef.environment, scene);
347
+ envTexture.rotationY = rotation;
348
+ scene.environmentTexture = envTexture;
349
+ } else if (curEnvTexture && curEnvTexture.rotationY !== rotation) {
350
+ curEnvTexture.rotationY = rotation;
351
+ }
352
+
353
+ // 3) dispose existing & set new ENVIRONMENT_BACKGROUND
354
+ const curDome = scene.getNodeByName(backgroundDomeName) as undefined | PhotoDome;
355
+ const backgroundUrlChanged = envDef.environmentBackground !== curDome?.texture.url;
356
+
357
+ if (curDome && (!envDef.environmentBackground || backgroundUrlChanged)) {
358
+ curDome.dispose();
359
+ }
360
+
361
+ const rotationPhotoDome = -1 * rotation;
362
+ if (envDef.environmentBackground && backgroundUrlChanged) {
363
+ const textureUrl = envDef.environmentBackground;
364
+ const dome = new PhotoDome(backgroundDomeName, textureUrl, { resolution: 32, size: 450 }, scene);
365
+ // Rotate submesh to get them in line with environment texture
366
+ dome.getChildMeshes(true)[0].rotation.y = Tools.ToRadians(90);
367
+ dome.rotation.y = rotationPhotoDome;
368
+ } else if (curDome && curDome.rotation.y !== rotationPhotoDome) {
369
+ curDome.rotation.y = rotationPhotoDome;
370
+ }
371
+
372
+ // 4) dispose existing & set new ENVIRONMENT_USEDEFAULT (only if no background is set)
373
+ const curEnvHelper = scene.metadata?.[envHelperMetadataName] as undefined | EnvironmentHelper;
374
+
375
+ if (curEnvHelper && !useDefaultEnv) {
376
+ curEnvHelper.dispose();
377
+ delete scene.metadata[envHelperMetadataName];
378
+ }
379
+
380
+ const envHelperMainColor = Color3.FromArray(scene.clearColor.asArray());
381
+ if (useDefaultEnv && !curEnvHelper) {
382
+ const envHelper = scene.createDefaultEnvironment({ sizeAuto: true });
383
+ envHelper?.setMainColor(envHelperMainColor);
384
+ if (envHelper) {
385
+ scene.metadata = merge({}, scene.metadata, { [envHelperMetadataName]: envHelper });
386
+ }
387
+ } else if (curEnvHelper && useDefaultEnv && clearColorChanged) {
388
+ curEnvHelper.setMainColor(envHelperMainColor);
389
+ }
390
+
391
+ // 5) set ENVIRONMENT_INTENSITY
392
+ if (envDef.environmentIntensity !== undefined) {
393
+ scene.environmentIntensity = envDef.environmentIntensity;
394
+ }
395
+ };
396
+
397
+ /**
398
+ * Sets a material by a given material id as material property on the given node.
399
+ *
400
+ * If the material is not already available in the scene, the viewer tries to create a material based on a Combeenation
401
+ * [material asset](https://docs.combeenation.com/docs/howto-create-and-use-babylon-and-material-asset).
402
+ * This of course only works if the viewer is used inside a Combeenation configurator.
403
+ *
404
+ * Furthermore this function also defers the material creation if the node is not visible yet to improve network &
405
+ * viewer bootstrap performance as textures are automatically being lazy loaded only when they are actually visible in
406
+ * the scene.
407
+ *
408
+ * Finally the material will not be applied before all its textures have been loaded. In this way "flickering" effects
409
+ * will be avoided, since the material would be incomplete without its loaded textures.
410
+ */
411
+ const setMaterial = function (node: TransformNode, materialId: string, deep: boolean = true, variant?: Variant) {
412
+ if (node instanceof AbstractMesh) {
413
+ const materialExists = node.getScene().getMaterialById(materialId);
414
+ const hasMissingMaterial = has(node.metadata, missingMaterialMetadataName);
415
+ const deferMaterialCreation = !materialExists && !node.isEnabled();
416
+ if (deferMaterialCreation) {
417
+ // do not set the material
418
+ injectMetadata(node, { [missingMaterialMetadataName]: materialId }, false);
419
+ // if it already had the missing material flag before, there already exists an observer...
420
+ if (!hasMissingMaterial) {
421
+ addMissingMaterialObserver(node);
422
+ }
423
+ } else {
424
+ // create material and apply it when textures have been loaded
425
+ getOrCreateMaterial(node.getScene(), materialId, variant).then(material => {
426
+ applyMaterial(material, node).then(() => emitter.emit(Event.MESH_MATERIAL_APPLIED, node, material));
427
+
428
+ if (hasMissingMaterial) {
429
+ delete node.metadata[missingMaterialMetadataName];
430
+ }
431
+ });
432
+ }
433
+ }
434
+ // recursively set materials on children (if desired)
435
+ if (deep) {
436
+ for (const child of node.getChildTransformNodes(true)) {
437
+ setMaterial(child, materialId, deep, variant);
438
+ }
439
+ }
440
+ };
441
+
442
+ /**
443
+ * Removes material from promise map once it has been added to the scene, because we can be sure that the material will
444
+ * be taken from the scene directly, the next time it is requested
445
+ */
446
+ const onNewMaterialAddedCb = (material: Material) => {
447
+ if (getMaterialPromises.has(material.id)) {
448
+ getMaterialPromises.delete(material.id);
449
+ }
450
+ };
451
+
452
+ /**
453
+ * Responsible for adding the `onNewMaterialAddedCb` only once
454
+ */
455
+ const registerOnNewMaterialAddedCbOnce = (scene: Scene) => {
456
+ const observerAlreadyAssigned = scene.onNewMaterialAddedObservable.observers.find(
457
+ observer => observer.callback === onNewMaterialAddedCb
458
+ );
459
+
460
+ if (!observerAlreadyAssigned) {
461
+ scene.onNewMaterialAddedObservable.add(onNewMaterialAddedCb);
462
+ }
463
+ };
464
+
465
+ /**
466
+ * Gets the Material either from the given {@link Variant}, the given scene or tries to create it from an Combeenation
467
+ * material asset when inside a Combeenation configurator.
468
+ */
469
+ const getOrCreateMaterial = async function (scene: Scene, materialId: string, variant?: Variant): Promise<Material> {
470
+ registerOnNewMaterialAddedCbOnce(scene);
471
+
472
+ let chosenMaterial: Material | undefined | null;
473
+ chosenMaterial = variant?.inheritedMaterials.find(mat => mat.id === materialId);
474
+ chosenMaterial = chosenMaterial || scene.materials.find(mat => mat.id === materialId);
475
+
476
+ if (!chosenMaterial) {
477
+ // material not available yet, request it from Combeenation assets
478
+ if (getMaterialPromises.has(materialId)) {
479
+ // request is already pending, just wait for the result
480
+ chosenMaterial = await getMaterialPromises.get(materialId);
481
+ } else {
482
+ // request not pending, call the dedicated function
483
+ const getMaterialProm = createMaterialFromCbnAssets(materialId, scene);
484
+ // store the promise in a global map, so that subsequent requests can reference it
485
+ getMaterialPromises.set(materialId, getMaterialProm);
486
+ chosenMaterial = await getMaterialProm;
487
+ }
488
+ }
489
+
490
+ if (chosenMaterial) {
491
+ return chosenMaterial as Material;
492
+ }
493
+ // reject when material was not found
494
+ let rejectMessage = `Material with id "${materialId}" does not exist on scene.`;
495
+ if (variant) {
496
+ rejectMessage = `Material with id "${materialId}" does not exist for variant "${variant.id}".`;
497
+ }
498
+ throw new Error(rejectMessage);
499
+ };
500
+
501
+ /**
502
+ * Waits until the materials textures are loaded and shaders are compiled.
503
+ * Then sets the material on the node if there is no newer "set material" request
504
+ */
505
+ const applyMaterial = async function (material: Material, node: AbstractMesh) {
506
+ // set current material id as last valid id, in this case all previously set materials on the node will be invalidated
507
+ injectMetadata(node, { [materialWaitingToBeSetMetadataName]: material.id }, false);
508
+
509
+ const promTexturesReady = new Promise<void>(resolve =>
510
+ BaseTexture.WhenAllReady(material.getActiveTextures(), resolve)
511
+ );
512
+ // this promise should only take some time on the first call of the corresponding shader (eg: PBRMaterial shader)
513
+ // on each other call of the same material/shader type there should be not be a waiting time, or at maximum one frame
514
+ // https://forum.babylonjs.com/t/mesh-flickering-when-setting-material-initially/37312
515
+ const promMaterialCompiled = material.forceCompilationAsync(node);
516
+
517
+ // material needs to fulfill 2 criterias before it's ready to use
518
+ // - textures need to be "ready" => downloaded
519
+ // - dedicated shader needs to be compiled
520
+ // if this would not be the case you can see some "flickering" when setting the material
521
+ await Promise.all([promTexturesReady, promMaterialCompiled]);
522
+
523
+ // textures ready, now check if the material is still up-to-date
524
+ if (material.id === node.metadata?.[materialWaitingToBeSetMetadataName]) {
525
+ node.material = material;
526
+ delete node.metadata[materialWaitingToBeSetMetadataName];
527
+ }
528
+ };
529
+
530
+ /**
531
+ * !!! Warning !!!
532
+ * This function is not public API. Whilst it can help solving certain problems, it only works reliably in well defined
533
+ * situations and can cause unwanted side effects under some conditions. Use carefully at your own risk!
534
+ *
535
+ * See https://combeenation.myjetbrains.com/youtrack/issue/CB-5906 for further details regarding this warning.
536
+ *
537
+ * Set material of an instanced meshes source mesh.
538
+ * Changes the material of all instanced meshes which have the same source mesh.
539
+ *
540
+ * @param node
541
+ * @param material
542
+ * @param deep
543
+ *
544
+ * @ignore
545
+ */
546
+ const setSourceNodeMaterial = function (node: TransformNode, material: Material, deep: boolean = true) {
547
+ const warn = ` You're using "setSourceNodeMaterial" which is not public API.
548
+ Whilst it can help solving certain problems, it only works reliably in well defined situations and can cause unwanted side effects under some conditions.
549
+ Use carefully at your own risk!`;
550
+ console.warn(`!!! Warning !!!\n${warn}`);
551
+
552
+ if (node instanceof InstancedMesh) {
553
+ node.sourceMesh.material = material;
554
+ }
555
+ if (deep) {
556
+ node.getChildTransformNodes(true).forEach(child => setSourceNodeMaterial(child, material, deep));
557
+ }
558
+ };
559
+
560
+ /**
561
+ * @param node
562
+ * @param color
563
+ * @param deep
564
+ */
565
+ const setMaterialColor = function (node: TransformNode, color: Color3, deep: boolean = true) {
566
+ if (node instanceof AbstractMesh && node.material) {
567
+ const materialCls = node.material.getClassName();
568
+ switch (materialCls) {
569
+ case 'PBRMaterial':
570
+ (node.material as PBRMaterial).albedoColor = color.toLinearSpace();
571
+ break;
572
+ case 'StandardMaterial':
573
+ (node.material as StandardMaterial).diffuseColor = color;
574
+ break;
575
+ default:
576
+ throw new Error(`Setting color for material of instance "${materialCls}" not implemented (yet).`);
577
+ }
578
+ setMaterial(node, node.material.id, false);
579
+ }
580
+ if (deep) {
581
+ node.getChildTransformNodes(true).forEach(child => setMaterialColor(child, color, deep));
582
+ }
583
+ };
584
+
585
+ /**
586
+ * @param node
587
+ * @param texture
588
+ * @param deep
589
+ */
590
+ const setMaterialTexture = function (node: TransformNode, texture: Texture, deep: boolean = true) {
591
+ if (node instanceof AbstractMesh && node.material) {
592
+ const materialCls = node.material.getClassName();
593
+ switch (materialCls) {
594
+ case 'PBRMaterial':
595
+ (node.material as PBRMaterial).albedoTexture = texture;
596
+ break;
597
+ case 'StandardMaterial':
598
+ (node.material as StandardMaterial).diffuseTexture = texture;
599
+ break;
600
+ default:
601
+ throw new Error(`Setting texture for material of instance "${materialCls}" not implemented (yet).`);
602
+ }
603
+ setMaterial(node, node.material.id, false);
604
+ }
605
+ if (deep) {
606
+ node.getChildTransformNodes(true).forEach(child => setMaterialTexture(child, texture, deep));
607
+ }
608
+ };
609
+
610
+ /**
611
+ * @param node
612
+ * @param metallness
613
+ * @param deep
614
+ */
615
+ const setMaterialMetallness = function (node: TransformNode, metallness: number, deep: boolean = true) {
616
+ if (node instanceof AbstractMesh && node.material) {
617
+ const materialCls = node.material.getClassName();
618
+ switch (materialCls) {
619
+ case 'PBRMaterial':
620
+ (node.material as PBRMaterial).metallic = metallness;
621
+ break;
622
+ default:
623
+ throw new Error(`Setting metallness for material of instance "${materialCls}" not implemented (yet).`);
624
+ }
625
+ setMaterial(node, node.material.id, false);
626
+ }
627
+ if (deep) {
628
+ node.getChildTransformNodes(true).forEach(child => setMaterialMetallness(child, metallness, deep));
629
+ }
630
+ };
631
+
632
+ /**
633
+ * @param node
634
+ * @param roughness
635
+ * @param deep
636
+ */
637
+ const setMaterialRoughness = function (node: TransformNode, roughness: number, deep: boolean = true) {
638
+ if (node instanceof AbstractMesh && node.material) {
639
+ const materialCls = node.material.getClassName();
640
+ switch (materialCls) {
641
+ case 'PBRMaterial':
642
+ (node.material as PBRMaterial).roughness = roughness;
643
+ break;
644
+ case 'StandardMaterial':
645
+ (node.material as StandardMaterial).roughness = roughness;
646
+ break;
647
+ default:
648
+ throw new Error(`Setting roughness for material of instance "${materialCls}" not implemented (yet).`);
649
+ }
650
+ setMaterial(node, node.material.id, false);
651
+ }
652
+ if (deep) {
653
+ node.getChildTransformNodes(true).forEach(child => setMaterialRoughness(child, roughness, deep));
654
+ }
655
+ };
656
+
657
+ /**
658
+ * @param node
659
+ * @param layer
660
+ * @param color
661
+ * @param deep
662
+ */
663
+ const addToHighlightLayer = function (layer: HighlightLayer, color: Color3, node: TransformNode, deep: boolean = true) {
664
+ if (node instanceof AbstractMesh) {
665
+ layer.addMesh(node as Mesh, color);
666
+ }
667
+ if (deep) {
668
+ node.getChildTransformNodes(true).forEach(child => addToHighlightLayer(layer, color, child, deep));
669
+ }
670
+ };
671
+
672
+ /**
673
+ * @param node
674
+ * @param layer
675
+ * @param deep
676
+ */
677
+ const removeFromHighlightLayer = function (layer: HighlightLayer, node: TransformNode, deep: boolean = true) {
678
+ if (node instanceof AbstractMesh) {
679
+ layer.removeMesh(node as Mesh);
680
+ }
681
+ if (deep) {
682
+ node.getChildTransformNodes(true).forEach(child => removeFromHighlightLayer(layer, child, deep));
683
+ }
684
+ };
685
+
686
+ /**
687
+ * @param node
688
+ * @param receiveShadows
689
+ * @param deep
690
+ */
691
+ const setReceiveShadows = function (node: TransformNode, receiveShadows: boolean, deep: boolean = true) {
692
+ if (node instanceof AbstractMesh) {
693
+ node.receiveShadows = receiveShadows;
694
+ }
695
+ if (deep) {
696
+ node.getChildTransformNodes(true).forEach(child => setReceiveShadows(child, receiveShadows, deep));
697
+ }
698
+ };
699
+
700
+ /**
701
+ * @param node
702
+ * @param generator
703
+ * @param deep
704
+ */
705
+ const addToShadowGenerator = function (generator: ShadowGenerator, node: TransformNode, deep: boolean = true) {
706
+ if (node instanceof AbstractMesh) {
707
+ // We have to remove the node because there's no duplicate check in babylon
708
+ generator.removeShadowCaster(node, false);
709
+ generator.addShadowCaster(node, false);
710
+ }
711
+ if (deep) {
712
+ node.getChildTransformNodes(true).forEach(child => addToShadowGenerator(generator, child, deep));
713
+ }
714
+ };
715
+
716
+ /**
717
+ * @param node
718
+ * @param generator
719
+ * @param deep
720
+ */
721
+ const removeFromShadowGenerator = function (generator: ShadowGenerator, node: TransformNode, deep: boolean = true) {
722
+ if (node instanceof AbstractMesh) {
723
+ generator.removeShadowCaster(node, false);
724
+ }
725
+ if (deep) {
726
+ node.getChildTransformNodes(true).forEach(child => removeFromShadowGenerator(generator, child, deep));
727
+ }
728
+ };
729
+
730
+ /**
731
+ * https://forum.babylonjs.com/t/get-mesh-bounding-box-position-and-size-in-2d-screen-coordinates/1058/3
732
+ * @param mesh
733
+ * @param scene
734
+ * @param canvas
735
+ */
736
+ const getClientRectFromMesh = function (mesh: AbstractMesh, scene: Scene, canvas: HTMLCanvasElement): ClientRect {
737
+ // get bounding box of the mesh
738
+ const meshVectors = mesh.getBoundingInfo().boundingBox.vectors;
739
+ // get the matrix and viewport needed to project the vectors onto the screen
740
+ const worldMatrix = mesh.getWorldMatrix();
741
+ const transformMatrix = scene.getTransformMatrix();
742
+ const viewport = scene.activeCamera!.viewport;
743
+ // loop though all the vectors and project them against the current camera viewport to get a set of coordinates
744
+ const coordinates = meshVectors.map(vector => {
745
+ const projection = Vector3.Project(vector, worldMatrix, transformMatrix, viewport);
746
+ projection.x = projection.x * canvas.clientWidth;
747
+ projection.y = projection.y * canvas.clientHeight;
748
+ return projection;
749
+ });
750
+ // get the min and max for all the coordinates so we can calculate the largest possible screen size
751
+ const maxX = Math.max(...coordinates.map(o => o.x));
752
+ const minX = Math.min(...coordinates.map(o => o.x));
753
+ const maxY = Math.max(...coordinates.map(o => o.y));
754
+ const minY = Math.min(...coordinates.map(o => o.y));
755
+ // return a ClientRect from this
756
+ return {
757
+ width: maxX - minX,
758
+ height: maxY - minY,
759
+ left: minX,
760
+ top: minY,
761
+ right: maxX,
762
+ bottom: maxY,
763
+ } as ClientRect;
764
+ };
765
+
766
+ type LoadableBaseTexture = BaseTexture & {
767
+ url: string;
768
+ onLoadObservable: Observable<BaseTexture>;
769
+ };
770
+
771
+ /**
772
+ * This type guard checks whether the given `BaseTextures` is any of its subtypes which comes with an url and
773
+ * `onLoadObservable`.
774
+ *
775
+ * !!! Timing of when this function is called is important !!!
776
+ * See the following for more details: https://forum.babylonjs.com/t/basetexture-whenallready-returns-too-early/34501/6
777
+ */
778
+ const isLoadableTexture = function (texture: BaseTexture): texture is LoadableBaseTexture {
779
+ const castedTexture = texture as LoadableBaseTexture;
780
+ return !!castedTexture.url && !!castedTexture.onLoadObservable;
781
+ };
782
+
783
+ /**
784
+ * @param objects
785
+ * @param tagMapping
786
+ */
787
+ const mapTags = function (objects: object[], tagMapping: TagMapping): string[] {
788
+ let affectedTags: string[] = [];
789
+ for (const object of objects) {
790
+ const oldTags = Object.keys(Tags.GetTags(object, false) ?? {});
791
+ const mappedTags = oldTags.map(t => tagMapping[t] || t);
792
+ Tags.RemoveTagsFrom(object, oldTags.join(' '));
793
+ Tags.AddTagsTo(object, mappedTags.join(' '));
794
+ affectedTags = [...affectedTags, ...mappedTags];
795
+ }
796
+ return uniq(affectedTags);
797
+ };
798
+
799
+ /**
800
+ * Gets an array of ids for nodeIds that exist in both TransformNode arrays.
801
+ * @param nodes1
802
+ * @param nodes2
803
+ * @param predicate
804
+ */
805
+ const intersectingNodeNames = function (nodes1: Node[], nodes2: Node[], predicate?: (node: Node) => boolean): string[] {
806
+ const nodeNames1 = nodes1.filter(n => !predicate || predicate(n)).map(n => n.name);
807
+ const nodesNames2 = nodes2.filter(n => !predicate || predicate(n)).map(n => n.name);
808
+ return nodeNames1.filter(n1 => nodesNames2.includes(n1));
809
+ };
810
+
811
+ /**
812
+ * Gets an array of ids for duplicate nodeIds in given node array.
813
+ * @param nodes
814
+ * @param predicate
815
+ */
816
+ const duplicateNodeNames = function (nodes: Node[], predicate?: (node: Node) => boolean): string[] {
817
+ const nodeNames = nodes.filter(n => !predicate || predicate(n)).map(n => n.name);
818
+ return nodeNames.filter((v, i, self) => self.indexOf(v) !== i);
819
+ };
820
+
821
+ /**
822
+ * Reports duplicate nodeIds.
823
+ * @param nodeNames
824
+ */
825
+ const reportDuplicateNodeNames = function (nodeNames: string[]): void {
826
+ if (nodeNames.length > 0) {
827
+ const warn = `There are duplicate nodeNames: ${nodeNames.join(', ')}`;
828
+ console.warn(`!!! Warning !!!\n${warn}`);
829
+ window.Cbn?.Assets.reportDuplicateNodeName(nodeNames);
830
+ }
831
+ };
832
+
833
+ const drawPaintableOnMaterial = function (
834
+ material: Material,
835
+ imageSource: HTMLImageElement,
836
+ scene: Scene,
837
+ options?: PaintableOptions
838
+ ) {
839
+ // always take width and height from image source, scaling is done with uvScale properties
840
+ const widthAndHeight = {
841
+ width: imageSource.width,
842
+ height: imageSource.height,
843
+ };
844
+
845
+ const texture = new DynamicTexture(`${material.id}.paintable_texture`, widthAndHeight, scene);
846
+
847
+ // draw image on texture
848
+ const ctx = texture.getContext();
849
+ ctx.drawImage(imageSource, 0, 0);
850
+ texture.update();
851
+
852
+ // apply settings from paintable options to tweak position and scaling of image on the texture
853
+ texture.uScale = options?.uScale ?? texture.uScale;
854
+ texture.vScale = options?.vScale ?? texture.vScale;
855
+ texture.uOffset = options?.uOffset ?? texture.uOffset;
856
+ texture.vOffset = options?.vOffset ?? texture.vOffset;
857
+
858
+ // wrap mode is preferred, as it will always show the texture, no matter which position offset is currently chosen
859
+ // clamp mode requires more knowledge (and patience) when adjusting the uv scale and offset values
860
+ texture.wrapU = Texture.WRAP_ADDRESSMODE;
861
+ texture.wrapV = Texture.WRAP_ADDRESSMODE;
862
+
863
+ // apply the paintable texture on the dedicated material type
864
+ const materialCls = material.getClassName();
865
+ switch (materialCls) {
866
+ case 'PBRMaterial':
867
+ (material as PBRMaterial).albedoTexture = texture;
868
+ break;
869
+ case 'StandardMaterial':
870
+ (material as StandardMaterial).diffuseTexture = texture;
871
+ break;
872
+ default:
873
+ throw new Error(`Setting paintable texture for material of instance "${materialCls}" not implemented (yet).`);
874
+ }
875
+ };
876
+
877
+ export {
878
+ getRootNode,
879
+ isLoadableTexture,
880
+ mapToDottedNodes,
881
+ getDottedPathForNode,
882
+ cloneTransformNode,
883
+ cloneNodeWithParents,
884
+ cloneTransformNodeMaterial,
885
+ getOrCreateMaterial,
886
+ applyMaterial,
887
+ injectMetadata,
888
+ assertTransformNode,
889
+ assertMeshCapability,
890
+ activateTransformNode,
891
+ deactivateTransformNode,
892
+ enableNodeWithParents,
893
+ disableNodeWithParents,
894
+ transformTransformNode,
895
+ setMaterial,
896
+ setSourceNodeMaterial,
897
+ setMaterialColor,
898
+ setMaterialTexture,
899
+ setMaterialMetallness,
900
+ setMaterialRoughness,
901
+ addToHighlightLayer,
902
+ removeFromHighlightLayer,
903
+ setReceiveShadows,
904
+ addToShadowGenerator,
905
+ removeFromShadowGenerator,
906
+ getClientRectFromMesh,
907
+ changeEnvironment,
908
+ backgroundDomeName,
909
+ envHelperMetadataName,
910
+ mapTags,
911
+ intersectingNodeNames,
912
+ duplicateNodeNames,
913
+ reportDuplicateNodeNames,
914
+ drawPaintableOnMaterial,
915
+ };