@combeenation/3d-viewer 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +111 -111
  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 +149 -149
  7. package/dist/lib-cjs/api/classes/element.js +669 -669
  8. package/dist/lib-cjs/api/classes/event.d.ts +342 -342
  9. package/dist/lib-cjs/api/classes/event.js +365 -365
  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/parameter.d.ts +339 -339
  13. package/dist/lib-cjs/api/classes/parameter.js +464 -464
  14. package/dist/lib-cjs/api/classes/parameterObservable.d.ts +36 -36
  15. package/dist/lib-cjs/api/classes/parameterObservable.js +97 -97
  16. package/dist/lib-cjs/api/classes/parameterizable.d.ts +15 -15
  17. package/dist/lib-cjs/api/classes/parameterizable.js +102 -102
  18. package/dist/lib-cjs/api/classes/placementAnimation.d.ts +45 -45
  19. package/dist/lib-cjs/api/classes/placementAnimation.js +176 -176
  20. package/dist/lib-cjs/api/classes/variant.d.ts +238 -234
  21. package/dist/lib-cjs/api/classes/variant.js +841 -828
  22. package/dist/lib-cjs/api/classes/variant.js.map +1 -1
  23. package/dist/lib-cjs/api/classes/variantInstance.d.ts +44 -44
  24. package/dist/lib-cjs/api/classes/variantInstance.js +105 -105
  25. package/dist/lib-cjs/api/classes/variantParameterizable.d.ts +17 -17
  26. package/dist/lib-cjs/api/classes/variantParameterizable.js +88 -88
  27. package/dist/lib-cjs/api/classes/viewer.d.ts +192 -187
  28. package/dist/lib-cjs/api/classes/viewer.js +639 -594
  29. package/dist/lib-cjs/api/classes/viewer.js.map +1 -1
  30. package/dist/lib-cjs/api/classes/viewerLight.d.ts +66 -66
  31. package/dist/lib-cjs/api/classes/viewerLight.js +348 -348
  32. package/dist/lib-cjs/api/internal/lensRendering.d.ts +8 -8
  33. package/dist/lib-cjs/api/internal/lensRendering.js +11 -11
  34. package/dist/lib-cjs/api/internal/sceneSetup.d.ts +13 -13
  35. package/dist/lib-cjs/api/internal/sceneSetup.js +226 -226
  36. package/dist/lib-cjs/api/manager/animationManager.d.ts +30 -30
  37. package/dist/lib-cjs/api/manager/animationManager.js +126 -126
  38. package/dist/lib-cjs/api/manager/gltfExportManager.d.ts +78 -78
  39. package/dist/lib-cjs/api/manager/gltfExportManager.js +241 -241
  40. package/dist/lib-cjs/api/manager/sceneManager.d.ts +33 -33
  41. package/dist/lib-cjs/api/manager/sceneManager.js +130 -130
  42. package/dist/lib-cjs/api/manager/textureLoadManager.d.ts +22 -22
  43. package/dist/lib-cjs/api/manager/textureLoadManager.js +97 -97
  44. package/dist/lib-cjs/api/manager/variantInstanceManager.d.ts +92 -92
  45. package/dist/lib-cjs/api/manager/variantInstanceManager.js +260 -260
  46. package/dist/lib-cjs/api/store/specStorage.d.ts +24 -24
  47. package/dist/lib-cjs/api/store/specStorage.js +50 -50
  48. package/dist/lib-cjs/api/util/babylonHelper.d.ts +187 -187
  49. package/dist/lib-cjs/api/util/babylonHelper.js +596 -596
  50. package/dist/lib-cjs/api/util/globalTypes.d.ts +387 -383
  51. package/dist/lib-cjs/api/util/globalTypes.js +1 -1
  52. package/dist/lib-cjs/api/util/resourceHelper.d.ts +58 -58
  53. package/dist/lib-cjs/api/util/resourceHelper.js +203 -203
  54. package/dist/lib-cjs/api/util/sceneLoaderHelper.d.ts +42 -42
  55. package/dist/lib-cjs/api/util/sceneLoaderHelper.js +139 -139
  56. package/dist/lib-cjs/api/util/stringHelper.d.ts +9 -9
  57. package/dist/lib-cjs/api/util/stringHelper.js +25 -25
  58. package/dist/lib-cjs/api/util/structureHelper.d.ts +9 -9
  59. package/dist/lib-cjs/api/util/structureHelper.js +48 -48
  60. package/dist/lib-cjs/buildinfo.json +3 -3
  61. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  62. package/dist/lib-cjs/index.d.ts +51 -51
  63. package/dist/lib-cjs/index.js +110 -110
  64. package/package.json +81 -81
  65. package/src/api/classes/animationInterface.ts +10 -10
  66. package/src/api/classes/dottedPath.ts +181 -181
  67. package/src/api/classes/element.ts +717 -717
  68. package/src/api/classes/event.ts +385 -385
  69. package/src/api/classes/eventBroadcaster.ts +52 -52
  70. package/src/api/classes/parameter.ts +497 -497
  71. package/src/api/classes/parameterObservable.ts +100 -100
  72. package/src/api/classes/parameterizable.ts +87 -87
  73. package/src/api/classes/placementAnimation.ts +162 -162
  74. package/src/api/classes/variant.ts +910 -896
  75. package/src/api/classes/variantInstance.ts +97 -97
  76. package/src/api/classes/variantParameterizable.ts +85 -85
  77. package/src/api/classes/viewer.ts +720 -672
  78. package/src/api/classes/viewerLight.ts +339 -339
  79. package/src/api/internal/debugViewer.ts +90 -90
  80. package/src/api/internal/lensRendering.ts +9 -9
  81. package/src/api/internal/sceneSetup.ts +205 -205
  82. package/src/api/manager/animationManager.ts +143 -143
  83. package/src/api/manager/gltfExportManager.ts +236 -236
  84. package/src/api/manager/sceneManager.ts +132 -132
  85. package/src/api/manager/textureLoadManager.ts +95 -95
  86. package/src/api/manager/variantInstanceManager.ts +265 -265
  87. package/src/api/store/specStorage.ts +51 -51
  88. package/src/api/util/babylonHelper.ts +663 -663
  89. package/src/api/util/globalTypes.ts +437 -432
  90. package/src/api/util/resourceHelper.ts +191 -191
  91. package/src/api/util/sceneLoaderHelper.ts +137 -137
  92. package/src/api/util/stringHelper.ts +23 -23
  93. package/src/api/util/structureHelper.ts +49 -49
  94. package/src/buildinfo.json +3 -3
  95. package/src/dev.ts +61 -61
  96. package/src/index.ts +96 -96
  97. package/src/types.d.ts +28 -28
@@ -1,672 +1,720 @@
1
- import buildInfo from '../../buildinfo.json';
2
- import { sceneSetup } from '../internal/sceneSetup';
3
- import { AnimationManager } from '../manager/animationManager';
4
- import { GltfExportManager } from '../manager/gltfExportManager';
5
- import { SceneManager } from '../manager/sceneManager';
6
- import { VariantInstanceManager } from '../manager/variantInstanceManager';
7
- import { SpecStorage } from '../store/specStorage';
8
- import { backgroundDomeName, envHelperMetadataName } from '../util/babylonHelper';
9
- import { debounce, loadJavascript, loadJson } from '../util/resourceHelper';
10
- import { getCustomCbnBabylonLoaderPlugin } from '../util/sceneLoaderHelper';
11
- import { isMeshIncludedInExclusionList } from '../util/structureHelper';
12
- import { AnimationInterface } from './animationInterface';
13
- import { Event } from './event';
14
- import { EventBroadcaster } from './eventBroadcaster';
15
- import { Parameter } from './parameter';
16
- import { Variant } from './variant';
17
- import { VariantInstance } from './variantInstance';
18
- import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
19
- import { PickingInfo } from '@babylonjs/core/Collisions/pickingInfo';
20
- import { BoundingInfo } from '@babylonjs/core/Culling/boundingInfo';
21
- import { DebugLayer } from '@babylonjs/core/Debug/debugLayer';
22
- import { Engine } from '@babylonjs/core/Engines/engine';
23
- import { IPointerEvent } from '@babylonjs/core/Events/deviceInputEvents';
24
- import { EnvironmentHelper } from '@babylonjs/core/Helpers/environmentHelper';
25
- import { PhotoDome } from '@babylonjs/core/Helpers/photoDome';
26
- import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
27
- import { ISceneLoaderPlugin, SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
28
- import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
29
- import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
30
- import { Color3 } from '@babylonjs/core/Maths/math.color';
31
- import { Vector3 } from '@babylonjs/core/Maths/math.vector';
32
- import { Mesh } from '@babylonjs/core/Meshes/mesh';
33
- import { ScreenshotTools } from '@babylonjs/core/Misc/screenshotTools';
34
- import { WebXRSessionManager } from '@babylonjs/core/XR/webXRSessionManager';
35
- import { Scene } from '@babylonjs/core/scene';
36
- import { isString } from 'lodash-es';
37
-
38
- /**
39
- * The main exposed object. This is the entry point into the application
40
- *
41
- * ```js
42
- * const canvas = document.getElementById( 'babylon-canvas' );
43
- * const viewer = Viewer( canvas, '/path/to/index.json' );
44
- * ```
45
- * The class does nothing on its own and needs to {@link bootstrap}
46
- */
47
- export class Viewer extends EventBroadcaster {
48
- protected _scene: Scene | null = null;
49
-
50
- protected _animationManager: AnimationManager | null = null;
51
-
52
- protected _sceneManager: SceneManager | null = null;
53
-
54
- protected _gltfExportManager: GltfExportManager | null = null;
55
-
56
- protected _variantInstances: VariantInstanceManager | null = null;
57
-
58
- // default value is `true` ATM for compatibility reasons
59
- // in the future material cloning should be the edge case
60
- protected _cloneMaterialsOnMutation: boolean = true;
61
-
62
- protected _isRenderLoopPaused: boolean = false;
63
-
64
- protected _inspectorLoaded: boolean = false;
65
-
66
- static version = buildInfo.version;
67
-
68
- /**
69
- * Constructor
70
- */
71
- public constructor(public readonly canvas: HTMLCanvasElement, protected structureJson: string | StructureJson) {
72
- super();
73
- }
74
-
75
- /**
76
- * Gets the BabylonJS Scene that is attached to the instance.
77
- *
78
- * @throws Error if the `scene` has not been initialized.
79
- */
80
- get scene(): Scene {
81
- if (!this._scene) {
82
- throw new Error(`Scene has not been initialized.`);
83
- }
84
- return this._scene;
85
- }
86
-
87
- /**
88
- * Gets the {@link SceneManager} attached to the viewer.
89
- *
90
- * @throws Error if the {@link SceneManager} has not been initialized.
91
- */
92
- get sceneManager(): SceneManager {
93
- if (!this._sceneManager) {
94
- throw new Error(`SceneManager has not been initialized.`);
95
- }
96
- return this._sceneManager;
97
- }
98
-
99
- /**
100
- * Gets the {@link GltfExportManager} attached to the viewer.
101
- *
102
- * @throws Error if the {@link GltfExportManager} has not been initialized.
103
- */
104
- get gltfExportManager(): GltfExportManager {
105
- if (!this._gltfExportManager) {
106
- throw new Error(`GltfExportManager has not been initialized.`);
107
- }
108
- return this._gltfExportManager;
109
- }
110
-
111
- /**
112
- * Gets the BabylonJS Engine that is attached to the viewer.
113
- */
114
- get engine(): Engine {
115
- return this.scene.getEngine();
116
- }
117
-
118
- /**
119
- * Gets the {@link VariantInstanceManager} attached to the viewer.
120
- *
121
- * @throws Error if the {@link VariantInstanceManager} has not been initialized.
122
- */
123
- get variantInstances(): VariantInstanceManager {
124
- if (!this._variantInstances) {
125
- throw Error(`There is no variantInstanceManager.`);
126
- }
127
- return this._variantInstances;
128
- }
129
-
130
- /**
131
- * Gets the {@link AnimationManager} attached to the viewer.
132
- *
133
- * @throws Error if the {@link AnimationManager} has not been initialized.
134
- */
135
- get animationManager(): AnimationManager {
136
- if (!this._animationManager) {
137
- throw new Error(`There is no animationManager instance.`);
138
- }
139
- return this._animationManager;
140
- }
141
-
142
- /**
143
- * Gets the `cloneMaterialsOnMutation` flag, as defined in the spec
144
- */
145
- get cloneMaterialsOnMutation(): boolean {
146
- return this._cloneMaterialsOnMutation;
147
- }
148
-
149
- /**
150
- * Starts the application. This will
151
- * * load the given "index" JSON file
152
- * * setup the scene with the "scene" JSON file
153
- * * create an (optional) default setup with different variant settings
154
- * * sets up resizing by attaching a debounced version of {@link resize}
155
- *
156
- * @throws Error if any of the files is not found/valid
157
- *
158
- * @emits {@link Event.BOOTSTRAP_START}
159
- * @emits {@link Event.BOOTSTRAP_END}
160
- */
161
- public async bootstrap(): Promise<Viewer> {
162
- this.broadcastEvent(Event.BOOTSTRAP_START, this);
163
-
164
- let indexJson;
165
- if (isString(this.structureJson)) {
166
- indexJson = await loadJson<StructureJson>(this.structureJson);
167
- } else {
168
- indexJson = this.structureJson;
169
- }
170
- if (!indexJson.scene) {
171
- throw new Error(`No "scene" property found for bootstrapping.`);
172
- }
173
- // fill spec store
174
- SpecStorage.createFromSpec(indexJson);
175
-
176
- this.initCbnBabylonLoaderPlugin();
177
-
178
- // load scene
179
- if (isString(indexJson.scene)) {
180
- const sceneJson = await loadJson<SceneJson>(indexJson.scene);
181
- indexJson.scene = sceneJson;
182
- }
183
- this._scene = await this.initScene();
184
-
185
- // create instance manager
186
- const rootVariant = await Variant.create('_', indexJson, this);
187
- this._variantInstances = await VariantInstanceManager.create(rootVariant);
188
- // create optional default instances
189
- if (indexJson.setup) {
190
- if (isString(indexJson.setup)) {
191
- const setupJson = await loadJson<SetupJson>(indexJson.setup);
192
- indexJson.setup = setupJson;
193
- }
194
- await this.createVariantInstances();
195
- }
196
- // create gltf export manager
197
- this._gltfExportManager = await GltfExportManager.create(this);
198
- // resize handler
199
- window.addEventListener('resize', debounce(this.resize.bind(this), 100));
200
- // wait until scene is completely ready
201
- await this.scene.whenReadyAsync();
202
-
203
- // event broadcasting
204
- this.broadcastEvent(Event.BOOTSTRAP_END, this);
205
- // render loop
206
- this.engine.runRenderLoop(() => {
207
- if (!this._isRenderLoopPaused) this.scene.render();
208
- });
209
- return this;
210
- }
211
-
212
- /**
213
- * Enables the BabylonJS [Inspector](https://doc.babylonjs.com/toolsAndResources/tools/inspector).\
214
- * Due to the additional size of the inspector, the CDN version is used instead of shipping the required code with
215
- * the viewer.\
216
- * This means that the code will be downloaded only when needed and calling `enableDebugLayer` can take a little while
217
- * depending on your internet connection etc.
218
- */
219
- public async enableDebugLayer(options?: IInspectorOptions) {
220
- if (!this._inspectorLoaded) {
221
- // CDN version of inspector requires the BabylonJS core to be available as CDN module as well
222
- await loadJavascript('https://cdn.jsdelivr.net/npm/babylonjs@5.6.0/babylon.min.js');
223
-
224
- DebugLayer.InspectorURL =
225
- 'https://cdn.jsdelivr.net/npm/babylonjs-inspector@5.6.0/babylon.inspector.bundle.max.min.js';
226
-
227
- this._inspectorLoaded = true;
228
- }
229
-
230
- await this.scene.debugLayer.show(options);
231
- }
232
-
233
- /**
234
- * Destroys all registered {@link VariantInstance}s that are registered
235
- */
236
- public destroyVariantInstances(): Viewer {
237
- this.variantInstances.all.forEach(variantInstance => {
238
- this.variantInstances.destroy(variantInstance.name);
239
- });
240
- return this;
241
- }
242
-
243
- /**
244
- * Trigger a resize event for the `Engine`
245
- */
246
- public resize(): Viewer {
247
- this.engine.resize();
248
- return this;
249
- }
250
-
251
- /**
252
- * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
253
- * {@link DottedPath}s.
254
- */
255
- public async getNode(
256
- variantInstanceName: string,
257
- elementDottedPath: DottedPathArgument,
258
- nodeDottedPath: DottedPathArgument
259
- ): Promise<TransformNode> {
260
- const variantInstance = await this.variantInstances.get(variantInstanceName);
261
- return variantInstance.getNode(elementDottedPath, nodeDottedPath);
262
- }
263
-
264
- /**
265
- * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
266
- * {@link DottedPath}s.
267
- */
268
- public async getMesh(
269
- variantInstanceName: string,
270
- elementDottedPath: DottedPathArgument,
271
- meshDottedPath: DottedPathArgument
272
- ): Promise<Mesh | null> {
273
- const variantInstance = await this.variantInstances.get(variantInstanceName);
274
- return variantInstance.getMesh(elementDottedPath, meshDottedPath);
275
- }
276
-
277
- /**
278
- * Switches the camera
279
- *
280
- * @emits {@link Event.CAMERA_SWITCHED}
281
- */
282
- public switchCamera(newCamera: string, reset: boolean = true): Viewer {
283
- const camera = this.scene.getCameraByName(newCamera);
284
- if (camera) {
285
- const activeCamera = this.scene.activeCamera;
286
- if (activeCamera) {
287
- activeCamera.detachControl(this.engine.getRenderingCanvas()!);
288
- }
289
- if (reset) {
290
- camera.restoreState();
291
- }
292
- this.scene.setActiveCameraByName(newCamera);
293
- camera.attachControl(this.engine.getRenderingCanvas()!);
294
- this.broadcastEvent(Event.CAMERA_SWITCHED, camera);
295
- } else {
296
- throw new Error(`Given camera "${newCamera}" not found.`);
297
- }
298
- // TODO: put traceable observers to new camera (@see element)
299
- return this;
300
- }
301
-
302
- /**
303
- * Moves or animates the active camera to given `placement`.
304
- */
305
- public async moveActiveCameraTo(
306
- placement: string | PlacementDefinition,
307
- animation?: string | AnimationDefinition
308
- ): Promise<AnimationInterface> {
309
- return this.animationManager.animateToPlacement(this.sceneManager.activeCamera, placement, animation);
310
- }
311
-
312
- /**
313
- * Takes a sceenshot the the current scene. The result is a string containing a base64 encoded image
314
- */
315
- public screenshot(settings?: ScreenshotSettings): Promise<string> {
316
- return new Promise((resolve, reject) => {
317
- if (!this.engine) {
318
- return reject('Engine is null');
319
- }
320
- if (!this.scene) {
321
- return reject('Scene is null');
322
- }
323
- this.scene.render(); // in combination with a render target, we need to refresh the scene manually to get the latest view
324
- ScreenshotTools.CreateScreenshotUsingRenderTarget(
325
- this.engine,
326
- this.sceneManager.activeCamera,
327
- settings?.size ?? { width: this.canvas.clientWidth, height: this.canvas.clientHeight },
328
- resolve,
329
- settings?.mimeType ?? 'image/png',
330
- settings?.samples ?? 1,
331
- settings?.antialiasing ?? false,
332
- settings?.fileName ?? 'screenshot.png',
333
- settings?.renderSprites ?? false
334
- );
335
- });
336
- }
337
-
338
- /**
339
- * Checks whether the browser is capable of handling XR.
340
- */
341
- public async isBrowserARCapable(): Promise<boolean> {
342
- return await WebXRSessionManager.IsSessionSupportedAsync('immersive-ar');
343
- }
344
-
345
- /**
346
- * Calculates the bounding box from all visible meshes on the scene.
347
- */
348
- public async calculateBoundingBox(excludeGeometry?: ExcludedGeometryList): Promise<Mesh> {
349
- if (this.scene.meshes.length === 0) {
350
- throw new Error('There are currently no meshes on the scene.');
351
- }
352
- this.scene.render(); // CB-6062: workaround for BoundingBox not respecting render loop
353
- const bbName = '__bounding_box__';
354
-
355
- const { max, min } = this.scene.meshes
356
- .filter(mesh => {
357
- const isEnabled = mesh.isEnabled();
358
- // ignore the existing bounding box mesh for calculating the current one
359
- const isNotBBoxMesh = bbName !== mesh.id;
360
- // ignore meshes with invalid bounding infos
361
- const hasValidBBoxInfo = mesh.getBoundingInfo().boundingSphere.radius > 0;
362
- // ignore excluded meshes
363
- const isExcluded = excludeGeometry ? isMeshIncludedInExclusionList(mesh as Mesh, excludeGeometry) : false;
364
- return isEnabled && isNotBBoxMesh && hasValidBBoxInfo && !isExcluded;
365
- })
366
- .reduce(
367
- (accBBoxMinMax, curMesh, idx) => {
368
- const bBox = curMesh.getBoundingInfo().boundingBox;
369
- // use the first entry in the array as default value and get the resulting maximum/minimum values
370
- const max = idx === 0 ? bBox.maximumWorld : Vector3.Maximize(accBBoxMinMax.max, bBox.maximumWorld);
371
- const min = idx === 0 ? bBox.minimumWorld : Vector3.Minimize(accBBoxMinMax.min, bBox.minimumWorld);
372
- return { max, min };
373
- },
374
- { max: new Vector3(), min: new Vector3() }
375
- );
376
-
377
- let boundingBox = this.scene.getMeshByName(bbName) as Mesh;
378
- if (!boundingBox) {
379
- boundingBox = new Mesh(bbName, this.scene);
380
- }
381
- boundingBox.setBoundingInfo(new BoundingInfo(min, max));
382
- return boundingBox;
383
- }
384
-
385
- /**
386
- * Focuses the camera to see every visible mesh in scene and tries to optimize wheel precision and panning
387
- */
388
- public async autofocusActiveCamera(settings?: AutofocusSettings) {
389
- // first check some preconditions
390
- const activeCamera = this.scene.activeCamera;
391
- if (!activeCamera) {
392
- throw new Error('No active camera found when using autofocus feature.');
393
- }
394
- if (!(activeCamera instanceof ArcRotateCamera)) {
395
- const cameraClsName = activeCamera.getClassName();
396
- throw new Error(`Camera of type "${cameraClsName}" is not implemented yet to use autofocus feature.`);
397
- }
398
-
399
- let exclude = settings?.exclude || [];
400
-
401
- // Exclude shown photo dome or environment helper from bounding box calculation
402
- const photoDome = this.scene.getNodeByName(backgroundDomeName) as undefined | PhotoDome;
403
- const photoDomeMeshes = photoDome?.getChildMeshes();
404
- if (photoDomeMeshes?.length) {
405
- exclude = [...exclude, ...photoDomeMeshes];
406
- }
407
-
408
- const envHelper = this.scene.metadata?.[envHelperMetadataName] as undefined | EnvironmentHelper;
409
- if (envHelper?.rootMesh) {
410
- exclude = [...exclude, envHelper.rootMesh];
411
- }
412
-
413
- // get bounding box of all visible meshes, this is the base for the autofocus algorithm
414
- const boundingBox = await this.calculateBoundingBox(exclude);
415
-
416
- // focus the helper camera and set the calculated camera data to the real camera
417
- const helperCamera = this.getFocusedHelperCamera(boundingBox, settings);
418
- await this.applyFocusedHelperCameraData(activeCamera, helperCamera, settings);
419
-
420
- // remove the helper camera
421
- helperCamera.dispose();
422
- }
423
-
424
- /**
425
- * Resets everything by calling {@link destroy} to clear all references and {@link bootstrap} to setup a clean
426
- * environment
427
- */
428
- public async reset(): Promise<Viewer> {
429
- await this.destroy();
430
- return this.bootstrap();
431
- }
432
-
433
- /**
434
- * Destroys
435
- *
436
- * * all {@link VariantInstance}s using {@link destroyVariantInstances}
437
- * * calling `dispose` on the `Engine` and `Scene`
438
- */
439
- public destroy(): Viewer {
440
- this.destroyVariantInstances();
441
- this.scene.dispose();
442
- SpecStorage.destroy();
443
- return this;
444
- }
445
-
446
- /**
447
- * Show coordinate system with given dimension (for debugging purpose).
448
- */
449
- public showWorldCoordinates(dimension: number) {
450
- const scene = this.scene;
451
- const makeTextPlane = function (text: string, color: string, size: number) {
452
- const dynamicTexture = new DynamicTexture('DynamicTexture', 50, scene, true);
453
- dynamicTexture.hasAlpha = true;
454
- dynamicTexture.drawText(text, 5, 40, 'bold 36px Arial', color, 'transparent', true);
455
- const plane = Mesh.CreatePlane('TextPlane', size, scene, true);
456
- plane.material = new StandardMaterial('TextPlaneMaterial', scene);
457
- plane.material.backFaceCulling = false;
458
- (plane.material as StandardMaterial).specularColor = new Color3(0, 0, 0);
459
- (plane.material as StandardMaterial).diffuseTexture = dynamicTexture;
460
- return plane;
461
- };
462
-
463
- const axisX = Mesh.CreateLines(
464
- 'axisX',
465
- [
466
- Vector3.Zero(),
467
- new Vector3(dimension, 0, 0),
468
- new Vector3(dimension * 0.95, 0.05 * dimension, 0),
469
- new Vector3(dimension, 0, 0),
470
- new Vector3(dimension * 0.95, -0.05 * dimension, 0),
471
- ],
472
- scene,
473
- false
474
- );
475
- axisX.color = new Color3(1, 0, 0);
476
- const xChar = makeTextPlane('X', 'red', dimension / 10);
477
- xChar.position = new Vector3(0.9 * dimension, -0.05 * dimension, 0);
478
- const axisY = Mesh.CreateLines(
479
- 'axisY',
480
- [
481
- Vector3.Zero(),
482
- new Vector3(0, dimension, 0),
483
- new Vector3(-0.05 * dimension, dimension * 0.95, 0),
484
- new Vector3(0, dimension, 0),
485
- new Vector3(0.05 * dimension, dimension * 0.95, 0),
486
- ],
487
- scene,
488
- false
489
- );
490
- axisY.color = new Color3(0, 1, 0);
491
- const yChar = makeTextPlane('Y', 'green', dimension / 10);
492
- yChar.position = new Vector3(0, 0.9 * dimension, -0.05 * dimension);
493
- const axisZ = Mesh.CreateLines(
494
- 'axisZ',
495
- [
496
- Vector3.Zero(),
497
- new Vector3(0, 0, dimension),
498
- new Vector3(0, -0.05 * dimension, dimension * 0.95),
499
- new Vector3(0, 0, dimension),
500
- new Vector3(0, 0.05 * dimension, dimension * 0.95),
501
- ],
502
- scene,
503
- false
504
- );
505
- axisZ.color = new Color3(0, 0, 1);
506
- const zChar = makeTextPlane('Z', 'blue', dimension / 10);
507
- zChar.position = new Vector3(0, 0.05 * dimension, 0.9 * dimension);
508
- }
509
-
510
- /**
511
- * Pause render loop.
512
- */
513
- public pauseRendering() {
514
- this._isRenderLoopPaused = true;
515
- }
516
-
517
- /**
518
- * Resume render loop when paused.
519
- */
520
- public resumeRendering() {
521
- this._isRenderLoopPaused = false;
522
- }
523
-
524
- /**
525
- * @emits {@link Event.SCENE_PROCESSING_START}
526
- * @emits {@link Event.SCENE_PROCESSING_END}
527
- */
528
- protected async initScene(): Promise<Scene> {
529
- const sceneJson = SpecStorage.get<SceneJson>('scene');
530
- this.broadcastEvent(Event.SCENE_PROCESSING_START, sceneJson);
531
- const engine = new Engine(
532
- this.canvas as HTMLCanvasElement,
533
- sceneJson.engine?.antialiasing ?? false,
534
- sceneJson.engine?.options
535
- );
536
- const scene = await sceneSetup(engine, sceneJson);
537
- if (sceneJson.meshPicking) {
538
- new HighlightLayer('default', scene);
539
- scene.onPointerPick = (pointerEvent: IPointerEvent, pickInfo: PickingInfo) => {
540
- if (!pickInfo.hit) {
541
- return;
542
- }
543
- const mesh = pickInfo.pickedMesh;
544
- this.broadcastEvent(Event.MESH_PICKED, mesh, mesh?.metadata.element, mesh?.metadata.variant);
545
- if (mesh?.metadata.element) {
546
- this.broadcastEvent(Event.ELEMENT_PICKED, mesh.metadata.element);
547
- }
548
- if (mesh?.metadata.variant) {
549
- if (mesh.metadata.variant.inheritedParameters[Parameter.HIGHLIGHT_ENABLED]) {
550
- mesh.metadata.variant.toggleHighlight();
551
- }
552
- this.broadcastEvent(Event.VARIANT_PICKED, mesh.metadata.variant);
553
- }
554
- };
555
- }
556
- this._sceneManager = await SceneManager.create(scene);
557
- this._animationManager = await AnimationManager.create(scene);
558
- if (sceneJson.cloneMaterialsOnMutation !== undefined) {
559
- this._cloneMaterialsOnMutation = sceneJson.cloneMaterialsOnMutation;
560
- }
561
- this.broadcastEvent(Event.SCENE_PROCESSING_END, scene);
562
- return scene;
563
- }
564
-
565
- /**
566
- * Register custom file loader for babylon files which adds "missing-material" metadata to meshes which reference
567
- * materials that are not present in the `materials` section of the given babylon file.
568
- *
569
- * Required for babylon files & materials loaded from "Combeenation configurator assets".
570
- */
571
- protected initCbnBabylonLoaderPlugin() {
572
- const previousLoaderPlugin = SceneLoader.GetPluginForExtension('babylon') as ISceneLoaderPlugin;
573
- const customLoaderPlugin = getCustomCbnBabylonLoaderPlugin(previousLoaderPlugin);
574
- SceneLoader.RegisterPlugin(customLoaderPlugin);
575
- }
576
-
577
- /**
578
- * Batch creation of multiple {@link VariantInstance} objects with a {@link SetupJson} object passed
579
- */
580
- protected async createVariantInstances(): Promise<VariantInstance[]> {
581
- const setupJson = SpecStorage.get<SetupJson>('setup');
582
- const instances: VariantInstance[] = [];
583
- for (const instanceDefinition of setupJson.instances) {
584
- if (instanceDefinition.lazy) {
585
- this.variantInstances.register(instanceDefinition);
586
- continue;
587
- }
588
- instances.push(
589
- await this.variantInstances.create(
590
- instanceDefinition.variant,
591
- instanceDefinition.name,
592
- instanceDefinition.parameters
593
- )
594
- );
595
- }
596
- return instances;
597
- }
598
-
599
- /**
600
- * Help function for focusing a helper camera exactly onto the given bounding box
601
- */
602
- private getFocusedHelperCamera(boundingBox: Mesh, settings?: AutofocusSettings): ArcRotateCamera {
603
- // use helper camera to get some default values and set the values of the real camera accordingly
604
- const helperCamera = new ArcRotateCamera(
605
- '__helper_camera__',
606
- 0, // camera angles will be overwritten after the target has been set
607
- 0,
608
- 0, // radius will be calculated, so we can set to 0 here
609
- Vector3.Zero(),
610
- this.scene
611
- );
612
- // this is required for automatically calculating the `lowerRadiusLimit`, so that we don't "dive" into meshes
613
- // see https://doc.babylonjs.com/divingDeeper/behaviors/cameraBehaviors#framing-behavior
614
- helperCamera.useFramingBehavior = true;
615
-
616
- // `minZ` is the camera distance beyond which the mesh will be clipped
617
- // this should be very low, but can't be zero
618
- // a good value seems to be 1% of the bounding box size (= radius), whereas the value shouldn't go above 1, which is
619
- // also the default value
620
- const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
621
- helperCamera.minZ = Math.min(radius / 100, 1);
622
-
623
- // set desired camera data, these won't be changed by the autofocus function!
624
- // default values should focus the element exactly from the front (= XY Plane)
625
- helperCamera.setTarget(boundingBox, true);
626
- helperCamera.alpha = (settings?.alpha ?? -90) * (Math.PI / 180);
627
- helperCamera.beta = (settings?.beta ?? 90) * (Math.PI / 180);
628
-
629
- // finally zoom to the bounding box
630
- // also apply a zoom factor, this adjusts the borders around the model in the viewport
631
- helperCamera.zoomOnFactor = settings?.radiusFactor || 1;
632
- helperCamera.zoomOn([boundingBox], true);
633
-
634
- return helperCamera;
635
- }
636
-
637
- /**
638
- * Help function for applying the relevant data of the focused helper camera to the real camera
639
- */
640
- private async applyFocusedHelperCameraData(
641
- activeCamera: ArcRotateCamera,
642
- helperCamera: ArcRotateCamera,
643
- settings?: AutofocusSettings
644
- ) {
645
- // limits
646
- activeCamera.minZ = helperCamera.minZ;
647
- activeCamera.maxZ = helperCamera.maxZ;
648
- activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
649
- activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
650
-
651
- // additional settings
652
- if (settings?.adjustWheelPrecision !== false) {
653
- activeCamera.wheelPrecision = helperCamera.wheelPrecision;
654
- }
655
- if (settings?.adjustPanningSensibility !== false) {
656
- activeCamera.panningSensibility = helperCamera.panningSensibility;
657
- }
658
- if (settings?.adjustPinchPrecision !== false) {
659
- activeCamera.pinchPrecision = helperCamera.pinchPrecision;
660
- }
661
-
662
- // finally move the camera
663
- // do this at last, so that all camera settings are already considered
664
- const newCameraPosition: PlacementDefinition = {
665
- alpha: helperCamera.alpha,
666
- beta: helperCamera.beta,
667
- radius: helperCamera.radius,
668
- target: helperCamera.target,
669
- };
670
- await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation);
671
- }
672
- }
1
+ import buildInfo from '../../buildinfo.json';
2
+ import { sceneSetup } from '../internal/sceneSetup';
3
+ import { AnimationManager } from '../manager/animationManager';
4
+ import { GltfExportManager } from '../manager/gltfExportManager';
5
+ import { SceneManager } from '../manager/sceneManager';
6
+ import { VariantInstanceManager } from '../manager/variantInstanceManager';
7
+ import { SpecStorage } from '../store/specStorage';
8
+ import { backgroundDomeName, envHelperMetadataName } from '../util/babylonHelper';
9
+ import { debounce, loadJavascript, loadJson } from '../util/resourceHelper';
10
+ import { getCustomCbnBabylonLoaderPlugin } from '../util/sceneLoaderHelper';
11
+ import { isMeshIncludedInExclusionList } from '../util/structureHelper';
12
+ import { AnimationInterface } from './animationInterface';
13
+ import { Event } from './event';
14
+ import { EventBroadcaster } from './eventBroadcaster';
15
+ import { Parameter } from './parameter';
16
+ import { Variant } from './variant';
17
+ import { VariantInstance } from './variantInstance';
18
+ import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
19
+ import { PickingInfo } from '@babylonjs/core/Collisions/pickingInfo';
20
+ import { BoundingInfo } from '@babylonjs/core/Culling/boundingInfo';
21
+ import { DebugLayer } from '@babylonjs/core/Debug/debugLayer';
22
+ import { Engine } from '@babylonjs/core/Engines/engine';
23
+ import { IPointerEvent } from '@babylonjs/core/Events/deviceInputEvents';
24
+ import { EnvironmentHelper } from '@babylonjs/core/Helpers/environmentHelper';
25
+ import { PhotoDome } from '@babylonjs/core/Helpers/photoDome';
26
+ import { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer';
27
+ import { ISceneLoaderPlugin, SceneLoader } from '@babylonjs/core/Loading/sceneLoader';
28
+ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
29
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
30
+ import { Color3 } from '@babylonjs/core/Maths/math.color';
31
+ import { Vector3 } from '@babylonjs/core/Maths/math.vector';
32
+ import { Mesh } from '@babylonjs/core/Meshes/mesh';
33
+ import { ScreenshotTools } from '@babylonjs/core/Misc/screenshotTools';
34
+ import { WebXRSessionManager } from '@babylonjs/core/XR/webXRSessionManager';
35
+ import { Scene } from '@babylonjs/core/scene';
36
+ import { isString } from 'lodash-es';
37
+
38
+ /**
39
+ * The main exposed object. This is the entry point into the application
40
+ *
41
+ * ```js
42
+ * const canvas = document.getElementById( 'babylon-canvas' );
43
+ * const viewer = Viewer( canvas, '/path/to/index.json' );
44
+ * ```
45
+ * The class does nothing on its own and needs to {@link bootstrap}
46
+ */
47
+ export class Viewer extends EventBroadcaster {
48
+ protected _scene: Scene | null = null;
49
+
50
+ protected _animationManager: AnimationManager | null = null;
51
+
52
+ protected _sceneManager: SceneManager | null = null;
53
+
54
+ protected _gltfExportManager: GltfExportManager | null = null;
55
+
56
+ protected _variantInstances: VariantInstanceManager | null = null;
57
+
58
+ // default value is `true` ATM for compatibility reasons
59
+ // in the future material cloning should be the edge case
60
+ protected _cloneMaterialsOnMutation: boolean = true;
61
+
62
+ protected _isRenderLoopPaused: boolean = false;
63
+
64
+ protected _inspectorLoaded: boolean = false;
65
+
66
+ static version = buildInfo.version;
67
+
68
+ /**
69
+ * Help function for automatically creating a valid Spec from a list of variant names and dedicated GLB/Babylon URLs.
70
+ * This data is most likely coming from babylon assets from the Combeenation system but other sources are also valid.
71
+ */
72
+ public static generateSpec(genData: SpecGenerationData[]): StructureJson {
73
+ const spec: StructureJson = {
74
+ // common scene settings as suggested in the viewer docs
75
+ scene: {
76
+ engine: {
77
+ antialiasing: true,
78
+ options: {
79
+ preserveDrawingBuffer: true,
80
+ stencil: true,
81
+ xrCompatible: false,
82
+ },
83
+ },
84
+ scene: {
85
+ globals: {},
86
+ },
87
+ },
88
+ setup: {
89
+ // create one instance for each input entry
90
+ // name and variant are named accordingly from the input, instance will be hidden by default and lazy loading
91
+ // is activated
92
+ instances: genData.map(instanceData => ({
93
+ name: instanceData.name,
94
+ variant: instanceData.name,
95
+ lazy: true,
96
+ parameters: {
97
+ [Parameter.VISIBLE]: false,
98
+ },
99
+ })),
100
+ },
101
+ // variants definition is also mapped to the input array, using the name as key and url as glTF source
102
+ // no elements are created here, since this should be done automatically from the system
103
+ // => create one element which contains all root nodes of the imported 3d file (GLB or Babylon)
104
+ variants: genData.reduce((accVariants, curVariant) => {
105
+ accVariants[curVariant.name] = {
106
+ glTF: curVariant.url,
107
+ };
108
+
109
+ return accVariants;
110
+ }, {} as { [id: string]: StructureJson }),
111
+ };
112
+
113
+ return spec;
114
+ }
115
+
116
+ /**
117
+ * Constructor
118
+ */
119
+ public constructor(public readonly canvas: HTMLCanvasElement, protected structureJson: string | StructureJson) {
120
+ super();
121
+ }
122
+
123
+ /**
124
+ * Gets the BabylonJS Scene that is attached to the instance.
125
+ *
126
+ * @throws Error if the `scene` has not been initialized.
127
+ */
128
+ get scene(): Scene {
129
+ if (!this._scene) {
130
+ throw new Error(`Scene has not been initialized.`);
131
+ }
132
+ return this._scene;
133
+ }
134
+
135
+ /**
136
+ * Gets the {@link SceneManager} attached to the viewer.
137
+ *
138
+ * @throws Error if the {@link SceneManager} has not been initialized.
139
+ */
140
+ get sceneManager(): SceneManager {
141
+ if (!this._sceneManager) {
142
+ throw new Error(`SceneManager has not been initialized.`);
143
+ }
144
+ return this._sceneManager;
145
+ }
146
+
147
+ /**
148
+ * Gets the {@link GltfExportManager} attached to the viewer.
149
+ *
150
+ * @throws Error if the {@link GltfExportManager} has not been initialized.
151
+ */
152
+ get gltfExportManager(): GltfExportManager {
153
+ if (!this._gltfExportManager) {
154
+ throw new Error(`GltfExportManager has not been initialized.`);
155
+ }
156
+ return this._gltfExportManager;
157
+ }
158
+
159
+ /**
160
+ * Gets the BabylonJS Engine that is attached to the viewer.
161
+ */
162
+ get engine(): Engine {
163
+ return this.scene.getEngine();
164
+ }
165
+
166
+ /**
167
+ * Gets the {@link VariantInstanceManager} attached to the viewer.
168
+ *
169
+ * @throws Error if the {@link VariantInstanceManager} has not been initialized.
170
+ */
171
+ get variantInstances(): VariantInstanceManager {
172
+ if (!this._variantInstances) {
173
+ throw Error(`There is no variantInstanceManager.`);
174
+ }
175
+ return this._variantInstances;
176
+ }
177
+
178
+ /**
179
+ * Gets the {@link AnimationManager} attached to the viewer.
180
+ *
181
+ * @throws Error if the {@link AnimationManager} has not been initialized.
182
+ */
183
+ get animationManager(): AnimationManager {
184
+ if (!this._animationManager) {
185
+ throw new Error(`There is no animationManager instance.`);
186
+ }
187
+ return this._animationManager;
188
+ }
189
+
190
+ /**
191
+ * Gets the `cloneMaterialsOnMutation` flag, as defined in the spec
192
+ */
193
+ get cloneMaterialsOnMutation(): boolean {
194
+ return this._cloneMaterialsOnMutation;
195
+ }
196
+
197
+ /**
198
+ * Starts the application. This will
199
+ * * load the given "index" JSON file
200
+ * * setup the scene with the "scene" JSON file
201
+ * * create an (optional) default setup with different variant settings
202
+ * * sets up resizing by attaching a debounced version of {@link resize}
203
+ *
204
+ * @throws Error if any of the files is not found/valid
205
+ *
206
+ * @emits {@link Event.BOOTSTRAP_START}
207
+ * @emits {@link Event.BOOTSTRAP_END}
208
+ */
209
+ public async bootstrap(): Promise<Viewer> {
210
+ this.broadcastEvent(Event.BOOTSTRAP_START, this);
211
+
212
+ let indexJson;
213
+ if (isString(this.structureJson)) {
214
+ indexJson = await loadJson<StructureJson>(this.structureJson);
215
+ } else {
216
+ indexJson = this.structureJson;
217
+ }
218
+ if (!indexJson.scene) {
219
+ throw new Error(`No "scene" property found for bootstrapping.`);
220
+ }
221
+ // fill spec store
222
+ SpecStorage.createFromSpec(indexJson);
223
+
224
+ this.initCbnBabylonLoaderPlugin();
225
+
226
+ // load scene
227
+ if (isString(indexJson.scene)) {
228
+ const sceneJson = await loadJson<SceneJson>(indexJson.scene);
229
+ indexJson.scene = sceneJson;
230
+ }
231
+ this._scene = await this.initScene();
232
+
233
+ // create instance manager
234
+ const rootVariant = await Variant.create('_', indexJson, this);
235
+ this._variantInstances = await VariantInstanceManager.create(rootVariant);
236
+ // create optional default instances
237
+ if (indexJson.setup) {
238
+ if (isString(indexJson.setup)) {
239
+ const setupJson = await loadJson<SetupJson>(indexJson.setup);
240
+ indexJson.setup = setupJson;
241
+ }
242
+ await this.createVariantInstances();
243
+ }
244
+ // create gltf export manager
245
+ this._gltfExportManager = await GltfExportManager.create(this);
246
+ // resize handler
247
+ window.addEventListener('resize', debounce(this.resize.bind(this), 100));
248
+ // wait until scene is completely ready
249
+ await this.scene.whenReadyAsync();
250
+
251
+ // event broadcasting
252
+ this.broadcastEvent(Event.BOOTSTRAP_END, this);
253
+ // render loop
254
+ this.engine.runRenderLoop(() => {
255
+ if (!this._isRenderLoopPaused) this.scene.render();
256
+ });
257
+ return this;
258
+ }
259
+
260
+ /**
261
+ * Enables the BabylonJS [Inspector](https://doc.babylonjs.com/toolsAndResources/tools/inspector).\
262
+ * Due to the additional size of the inspector, the CDN version is used instead of shipping the required code with
263
+ * the viewer.\
264
+ * This means that the code will be downloaded only when needed and calling `enableDebugLayer` can take a little while
265
+ * depending on your internet connection etc.
266
+ */
267
+ public async enableDebugLayer(options?: IInspectorOptions) {
268
+ if (!this._inspectorLoaded) {
269
+ // CDN version of inspector requires the BabylonJS core to be available as CDN module as well
270
+ await loadJavascript('https://cdn.jsdelivr.net/npm/babylonjs@5.6.0/babylon.min.js');
271
+
272
+ DebugLayer.InspectorURL =
273
+ 'https://cdn.jsdelivr.net/npm/babylonjs-inspector@5.6.0/babylon.inspector.bundle.max.min.js';
274
+
275
+ this._inspectorLoaded = true;
276
+ }
277
+
278
+ await this.scene.debugLayer.show(options);
279
+ }
280
+
281
+ /**
282
+ * Destroys all registered {@link VariantInstance}s that are registered
283
+ */
284
+ public destroyVariantInstances(): Viewer {
285
+ this.variantInstances.all.forEach(variantInstance => {
286
+ this.variantInstances.destroy(variantInstance.name);
287
+ });
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Trigger a resize event for the `Engine`
293
+ */
294
+ public resize(): Viewer {
295
+ this.engine.resize();
296
+ return this;
297
+ }
298
+
299
+ /**
300
+ * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
301
+ * {@link DottedPath}s.
302
+ */
303
+ public async getNode(
304
+ variantInstanceName: string,
305
+ elementDottedPath: DottedPathArgument,
306
+ nodeDottedPath: DottedPathArgument
307
+ ): Promise<TransformNode> {
308
+ const variantInstance = await this.variantInstances.get(variantInstanceName);
309
+ return variantInstance.getNode(elementDottedPath, nodeDottedPath);
310
+ }
311
+
312
+ /**
313
+ * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
314
+ * {@link DottedPath}s.
315
+ */
316
+ public async getMesh(
317
+ variantInstanceName: string,
318
+ elementDottedPath: DottedPathArgument,
319
+ meshDottedPath: DottedPathArgument
320
+ ): Promise<Mesh | null> {
321
+ const variantInstance = await this.variantInstances.get(variantInstanceName);
322
+ return variantInstance.getMesh(elementDottedPath, meshDottedPath);
323
+ }
324
+
325
+ /**
326
+ * Switches the camera
327
+ *
328
+ * @emits {@link Event.CAMERA_SWITCHED}
329
+ */
330
+ public switchCamera(newCamera: string, reset: boolean = true): Viewer {
331
+ const camera = this.scene.getCameraByName(newCamera);
332
+ if (camera) {
333
+ const activeCamera = this.scene.activeCamera;
334
+ if (activeCamera) {
335
+ activeCamera.detachControl(this.engine.getRenderingCanvas()!);
336
+ }
337
+ if (reset) {
338
+ camera.restoreState();
339
+ }
340
+ this.scene.setActiveCameraByName(newCamera);
341
+ camera.attachControl(this.engine.getRenderingCanvas()!);
342
+ this.broadcastEvent(Event.CAMERA_SWITCHED, camera);
343
+ } else {
344
+ throw new Error(`Given camera "${newCamera}" not found.`);
345
+ }
346
+ // TODO: put traceable observers to new camera (@see element)
347
+ return this;
348
+ }
349
+
350
+ /**
351
+ * Moves or animates the active camera to given `placement`.
352
+ */
353
+ public async moveActiveCameraTo(
354
+ placement: string | PlacementDefinition,
355
+ animation?: string | AnimationDefinition
356
+ ): Promise<AnimationInterface> {
357
+ return this.animationManager.animateToPlacement(this.sceneManager.activeCamera, placement, animation);
358
+ }
359
+
360
+ /**
361
+ * Takes a sceenshot the the current scene. The result is a string containing a base64 encoded image
362
+ */
363
+ public screenshot(settings?: ScreenshotSettings): Promise<string> {
364
+ return new Promise((resolve, reject) => {
365
+ if (!this.engine) {
366
+ return reject('Engine is null');
367
+ }
368
+ if (!this.scene) {
369
+ return reject('Scene is null');
370
+ }
371
+ this.scene.render(); // in combination with a render target, we need to refresh the scene manually to get the latest view
372
+ ScreenshotTools.CreateScreenshotUsingRenderTarget(
373
+ this.engine,
374
+ this.sceneManager.activeCamera,
375
+ settings?.size ?? { width: this.canvas.clientWidth, height: this.canvas.clientHeight },
376
+ resolve,
377
+ settings?.mimeType ?? 'image/png',
378
+ settings?.samples ?? 1,
379
+ settings?.antialiasing ?? false,
380
+ settings?.fileName ?? 'screenshot.png',
381
+ settings?.renderSprites ?? false
382
+ );
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Checks whether the browser is capable of handling XR.
388
+ */
389
+ public async isBrowserARCapable(): Promise<boolean> {
390
+ return await WebXRSessionManager.IsSessionSupportedAsync('immersive-ar');
391
+ }
392
+
393
+ /**
394
+ * Calculates the bounding box from all visible meshes on the scene.
395
+ */
396
+ public async calculateBoundingBox(excludeGeometry?: ExcludedGeometryList): Promise<Mesh> {
397
+ if (this.scene.meshes.length === 0) {
398
+ throw new Error('There are currently no meshes on the scene.');
399
+ }
400
+ this.scene.render(); // CB-6062: workaround for BoundingBox not respecting render loop
401
+ const bbName = '__bounding_box__';
402
+
403
+ const { max, min } = this.scene.meshes
404
+ .filter(mesh => {
405
+ const isEnabled = mesh.isEnabled();
406
+ // ignore the existing bounding box mesh for calculating the current one
407
+ const isNotBBoxMesh = bbName !== mesh.id;
408
+ // ignore meshes with invalid bounding infos
409
+ const hasValidBBoxInfo = mesh.getBoundingInfo().boundingSphere.radius > 0;
410
+ // ignore excluded meshes
411
+ const isExcluded = excludeGeometry ? isMeshIncludedInExclusionList(mesh as Mesh, excludeGeometry) : false;
412
+ return isEnabled && isNotBBoxMesh && hasValidBBoxInfo && !isExcluded;
413
+ })
414
+ .reduce(
415
+ (accBBoxMinMax, curMesh, idx) => {
416
+ const bBox = curMesh.getBoundingInfo().boundingBox;
417
+ // use the first entry in the array as default value and get the resulting maximum/minimum values
418
+ const max = idx === 0 ? bBox.maximumWorld : Vector3.Maximize(accBBoxMinMax.max, bBox.maximumWorld);
419
+ const min = idx === 0 ? bBox.minimumWorld : Vector3.Minimize(accBBoxMinMax.min, bBox.minimumWorld);
420
+ return { max, min };
421
+ },
422
+ { max: new Vector3(), min: new Vector3() }
423
+ );
424
+
425
+ let boundingBox = this.scene.getMeshByName(bbName) as Mesh;
426
+ if (!boundingBox) {
427
+ boundingBox = new Mesh(bbName, this.scene);
428
+ }
429
+ boundingBox.setBoundingInfo(new BoundingInfo(min, max));
430
+ return boundingBox;
431
+ }
432
+
433
+ /**
434
+ * Focuses the camera to see every visible mesh in scene and tries to optimize wheel precision and panning
435
+ */
436
+ public async autofocusActiveCamera(settings?: AutofocusSettings) {
437
+ // first check some preconditions
438
+ const activeCamera = this.scene.activeCamera;
439
+ if (!activeCamera) {
440
+ throw new Error('No active camera found when using autofocus feature.');
441
+ }
442
+ if (!(activeCamera instanceof ArcRotateCamera)) {
443
+ const cameraClsName = activeCamera.getClassName();
444
+ throw new Error(`Camera of type "${cameraClsName}" is not implemented yet to use autofocus feature.`);
445
+ }
446
+
447
+ let exclude = settings?.exclude || [];
448
+
449
+ // Exclude shown photo dome or environment helper from bounding box calculation
450
+ const photoDome = this.scene.getNodeByName(backgroundDomeName) as undefined | PhotoDome;
451
+ const photoDomeMeshes = photoDome?.getChildMeshes();
452
+ if (photoDomeMeshes?.length) {
453
+ exclude = [...exclude, ...photoDomeMeshes];
454
+ }
455
+
456
+ const envHelper = this.scene.metadata?.[envHelperMetadataName] as undefined | EnvironmentHelper;
457
+ if (envHelper?.rootMesh) {
458
+ exclude = [...exclude, envHelper.rootMesh];
459
+ }
460
+
461
+ // get bounding box of all visible meshes, this is the base for the autofocus algorithm
462
+ const boundingBox = await this.calculateBoundingBox(exclude);
463
+
464
+ // focus the helper camera and set the calculated camera data to the real camera
465
+ const helperCamera = this.getFocusedHelperCamera(boundingBox, settings);
466
+ await this.applyFocusedHelperCameraData(activeCamera, helperCamera, settings);
467
+
468
+ // remove the helper camera
469
+ helperCamera.dispose();
470
+ }
471
+
472
+ /**
473
+ * Resets everything by calling {@link destroy} to clear all references and {@link bootstrap} to setup a clean
474
+ * environment
475
+ */
476
+ public async reset(): Promise<Viewer> {
477
+ await this.destroy();
478
+ return this.bootstrap();
479
+ }
480
+
481
+ /**
482
+ * Destroys
483
+ *
484
+ * * all {@link VariantInstance}s using {@link destroyVariantInstances}
485
+ * * calling `dispose` on the `Engine` and `Scene`
486
+ */
487
+ public destroy(): Viewer {
488
+ this.destroyVariantInstances();
489
+ this.scene.dispose();
490
+ SpecStorage.destroy();
491
+ return this;
492
+ }
493
+
494
+ /**
495
+ * Show coordinate system with given dimension (for debugging purpose).
496
+ */
497
+ public showWorldCoordinates(dimension: number) {
498
+ const scene = this.scene;
499
+ const makeTextPlane = function (text: string, color: string, size: number) {
500
+ const dynamicTexture = new DynamicTexture('DynamicTexture', 50, scene, true);
501
+ dynamicTexture.hasAlpha = true;
502
+ dynamicTexture.drawText(text, 5, 40, 'bold 36px Arial', color, 'transparent', true);
503
+ const plane = Mesh.CreatePlane('TextPlane', size, scene, true);
504
+ plane.material = new StandardMaterial('TextPlaneMaterial', scene);
505
+ plane.material.backFaceCulling = false;
506
+ (plane.material as StandardMaterial).specularColor = new Color3(0, 0, 0);
507
+ (plane.material as StandardMaterial).diffuseTexture = dynamicTexture;
508
+ return plane;
509
+ };
510
+
511
+ const axisX = Mesh.CreateLines(
512
+ 'axisX',
513
+ [
514
+ Vector3.Zero(),
515
+ new Vector3(dimension, 0, 0),
516
+ new Vector3(dimension * 0.95, 0.05 * dimension, 0),
517
+ new Vector3(dimension, 0, 0),
518
+ new Vector3(dimension * 0.95, -0.05 * dimension, 0),
519
+ ],
520
+ scene,
521
+ false
522
+ );
523
+ axisX.color = new Color3(1, 0, 0);
524
+ const xChar = makeTextPlane('X', 'red', dimension / 10);
525
+ xChar.position = new Vector3(0.9 * dimension, -0.05 * dimension, 0);
526
+ const axisY = Mesh.CreateLines(
527
+ 'axisY',
528
+ [
529
+ Vector3.Zero(),
530
+ new Vector3(0, dimension, 0),
531
+ new Vector3(-0.05 * dimension, dimension * 0.95, 0),
532
+ new Vector3(0, dimension, 0),
533
+ new Vector3(0.05 * dimension, dimension * 0.95, 0),
534
+ ],
535
+ scene,
536
+ false
537
+ );
538
+ axisY.color = new Color3(0, 1, 0);
539
+ const yChar = makeTextPlane('Y', 'green', dimension / 10);
540
+ yChar.position = new Vector3(0, 0.9 * dimension, -0.05 * dimension);
541
+ const axisZ = Mesh.CreateLines(
542
+ 'axisZ',
543
+ [
544
+ Vector3.Zero(),
545
+ new Vector3(0, 0, dimension),
546
+ new Vector3(0, -0.05 * dimension, dimension * 0.95),
547
+ new Vector3(0, 0, dimension),
548
+ new Vector3(0, 0.05 * dimension, dimension * 0.95),
549
+ ],
550
+ scene,
551
+ false
552
+ );
553
+ axisZ.color = new Color3(0, 0, 1);
554
+ const zChar = makeTextPlane('Z', 'blue', dimension / 10);
555
+ zChar.position = new Vector3(0, 0.05 * dimension, 0.9 * dimension);
556
+ }
557
+
558
+ /**
559
+ * Pause render loop.
560
+ */
561
+ public pauseRendering() {
562
+ this._isRenderLoopPaused = true;
563
+ }
564
+
565
+ /**
566
+ * Resume render loop when paused.
567
+ */
568
+ public resumeRendering() {
569
+ this._isRenderLoopPaused = false;
570
+ }
571
+
572
+ /**
573
+ * @emits {@link Event.SCENE_PROCESSING_START}
574
+ * @emits {@link Event.SCENE_PROCESSING_END}
575
+ */
576
+ protected async initScene(): Promise<Scene> {
577
+ const sceneJson = SpecStorage.get<SceneJson>('scene');
578
+ this.broadcastEvent(Event.SCENE_PROCESSING_START, sceneJson);
579
+ const engine = new Engine(
580
+ this.canvas as HTMLCanvasElement,
581
+ sceneJson.engine?.antialiasing ?? false,
582
+ sceneJson.engine?.options
583
+ );
584
+ const scene = await sceneSetup(engine, sceneJson);
585
+ if (sceneJson.meshPicking) {
586
+ new HighlightLayer('default', scene);
587
+ scene.onPointerPick = (pointerEvent: IPointerEvent, pickInfo: PickingInfo) => {
588
+ if (!pickInfo.hit) {
589
+ return;
590
+ }
591
+ const mesh = pickInfo.pickedMesh;
592
+ this.broadcastEvent(Event.MESH_PICKED, mesh, mesh?.metadata.element, mesh?.metadata.variant);
593
+ if (mesh?.metadata.element) {
594
+ this.broadcastEvent(Event.ELEMENT_PICKED, mesh.metadata.element);
595
+ }
596
+ if (mesh?.metadata.variant) {
597
+ if (mesh.metadata.variant.inheritedParameters[Parameter.HIGHLIGHT_ENABLED]) {
598
+ mesh.metadata.variant.toggleHighlight();
599
+ }
600
+ this.broadcastEvent(Event.VARIANT_PICKED, mesh.metadata.variant);
601
+ }
602
+ };
603
+ }
604
+ this._sceneManager = await SceneManager.create(scene);
605
+ this._animationManager = await AnimationManager.create(scene);
606
+ if (sceneJson.cloneMaterialsOnMutation !== undefined) {
607
+ this._cloneMaterialsOnMutation = sceneJson.cloneMaterialsOnMutation;
608
+ }
609
+ this.broadcastEvent(Event.SCENE_PROCESSING_END, scene);
610
+ return scene;
611
+ }
612
+
613
+ /**
614
+ * Register custom file loader for babylon files which adds "missing-material" metadata to meshes which reference
615
+ * materials that are not present in the `materials` section of the given babylon file.
616
+ *
617
+ * Required for babylon files & materials loaded from "Combeenation configurator assets".
618
+ */
619
+ protected initCbnBabylonLoaderPlugin() {
620
+ const previousLoaderPlugin = SceneLoader.GetPluginForExtension('babylon') as ISceneLoaderPlugin;
621
+ const customLoaderPlugin = getCustomCbnBabylonLoaderPlugin(previousLoaderPlugin);
622
+ SceneLoader.RegisterPlugin(customLoaderPlugin);
623
+ }
624
+
625
+ /**
626
+ * Batch creation of multiple {@link VariantInstance} objects with a {@link SetupJson} object passed
627
+ */
628
+ protected async createVariantInstances(): Promise<VariantInstance[]> {
629
+ const setupJson = SpecStorage.get<SetupJson>('setup');
630
+ const instances: VariantInstance[] = [];
631
+ for (const instanceDefinition of setupJson.instances) {
632
+ if (instanceDefinition.lazy) {
633
+ this.variantInstances.register(instanceDefinition);
634
+ continue;
635
+ }
636
+ instances.push(
637
+ await this.variantInstances.create(
638
+ instanceDefinition.variant,
639
+ instanceDefinition.name,
640
+ instanceDefinition.parameters
641
+ )
642
+ );
643
+ }
644
+ return instances;
645
+ }
646
+
647
+ /**
648
+ * Help function for focusing a helper camera exactly onto the given bounding box
649
+ */
650
+ private getFocusedHelperCamera(boundingBox: Mesh, settings?: AutofocusSettings): ArcRotateCamera {
651
+ // use helper camera to get some default values and set the values of the real camera accordingly
652
+ const helperCamera = new ArcRotateCamera(
653
+ '__helper_camera__',
654
+ 0, // camera angles will be overwritten after the target has been set
655
+ 0,
656
+ 0, // radius will be calculated, so we can set to 0 here
657
+ Vector3.Zero(),
658
+ this.scene
659
+ );
660
+ // this is required for automatically calculating the `lowerRadiusLimit`, so that we don't "dive" into meshes
661
+ // see https://doc.babylonjs.com/divingDeeper/behaviors/cameraBehaviors#framing-behavior
662
+ helperCamera.useFramingBehavior = true;
663
+
664
+ // `minZ` is the camera distance beyond which the mesh will be clipped
665
+ // this should be very low, but can't be zero
666
+ // a good value seems to be 1% of the bounding box size (= radius), whereas the value shouldn't go above 1, which is
667
+ // also the default value
668
+ const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
669
+ helperCamera.minZ = Math.min(radius / 100, 1);
670
+
671
+ // set desired camera data, these won't be changed by the autofocus function!
672
+ // default values should focus the element exactly from the front (= XY Plane)
673
+ helperCamera.setTarget(boundingBox, true);
674
+ helperCamera.alpha = (settings?.alpha ?? -90) * (Math.PI / 180);
675
+ helperCamera.beta = (settings?.beta ?? 90) * (Math.PI / 180);
676
+
677
+ // finally zoom to the bounding box
678
+ // also apply a zoom factor, this adjusts the borders around the model in the viewport
679
+ helperCamera.zoomOnFactor = settings?.radiusFactor || 1;
680
+ helperCamera.zoomOn([boundingBox], true);
681
+
682
+ return helperCamera;
683
+ }
684
+
685
+ /**
686
+ * Help function for applying the relevant data of the focused helper camera to the real camera
687
+ */
688
+ private async applyFocusedHelperCameraData(
689
+ activeCamera: ArcRotateCamera,
690
+ helperCamera: ArcRotateCamera,
691
+ settings?: AutofocusSettings
692
+ ) {
693
+ // limits
694
+ activeCamera.minZ = helperCamera.minZ;
695
+ activeCamera.maxZ = helperCamera.maxZ;
696
+ activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
697
+ activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
698
+
699
+ // additional settings
700
+ if (settings?.adjustWheelPrecision !== false) {
701
+ activeCamera.wheelPrecision = helperCamera.wheelPrecision;
702
+ }
703
+ if (settings?.adjustPanningSensibility !== false) {
704
+ activeCamera.panningSensibility = helperCamera.panningSensibility;
705
+ }
706
+ if (settings?.adjustPinchPrecision !== false) {
707
+ activeCamera.pinchPrecision = helperCamera.pinchPrecision;
708
+ }
709
+
710
+ // finally move the camera
711
+ // do this at last, so that all camera settings are already considered
712
+ const newCameraPosition: PlacementDefinition = {
713
+ alpha: helperCamera.alpha,
714
+ beta: helperCamera.beta,
715
+ radius: helperCamera.radius,
716
+ target: helperCamera.target,
717
+ };
718
+ await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation);
719
+ }
720
+ }