@combeenation/3d-viewer 4.2.0 → 5.0.0-ar1

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 (89) hide show
  1. package/README.md +2 -2
  2. package/dist/lib-cjs/api/classes/dottedPath.js +2 -5
  3. package/dist/lib-cjs/api/classes/dottedPath.js.map +1 -1
  4. package/dist/lib-cjs/api/classes/element.js +96 -66
  5. package/dist/lib-cjs/api/classes/element.js.map +1 -1
  6. package/dist/lib-cjs/api/classes/event.js.map +1 -1
  7. package/dist/lib-cjs/api/classes/eventBroadcaster.js.map +1 -1
  8. package/dist/lib-cjs/api/classes/parameter.js +3 -2
  9. package/dist/lib-cjs/api/classes/parameter.js.map +1 -1
  10. package/dist/lib-cjs/api/classes/parameterObservable.js.map +1 -1
  11. package/dist/lib-cjs/api/classes/parameterizable.js.map +1 -1
  12. package/dist/lib-cjs/api/classes/placementAnimation.d.ts +1 -0
  13. package/dist/lib-cjs/api/classes/placementAnimation.js +2 -2
  14. package/dist/lib-cjs/api/classes/placementAnimation.js.map +1 -1
  15. package/dist/lib-cjs/api/classes/variant.js +26 -19
  16. package/dist/lib-cjs/api/classes/variant.js.map +1 -1
  17. package/dist/lib-cjs/api/classes/variantInstance.js +19 -19
  18. package/dist/lib-cjs/api/classes/variantInstance.js.map +1 -1
  19. package/dist/lib-cjs/api/classes/variantParameterizable.js.map +1 -1
  20. package/dist/lib-cjs/api/classes/viewer.d.ts +30 -0
  21. package/dist/lib-cjs/api/classes/viewer.js +117 -41
  22. package/dist/lib-cjs/api/classes/viewer.js.map +1 -1
  23. package/dist/lib-cjs/api/classes/viewerLight.js +25 -25
  24. package/dist/lib-cjs/api/classes/viewerLight.js.map +1 -1
  25. package/dist/lib-cjs/api/internal/debugViewer.js +1 -2
  26. package/dist/lib-cjs/api/internal/debugViewer.js.map +1 -1
  27. package/dist/lib-cjs/api/internal/lensRendering.js.map +1 -1
  28. package/dist/lib-cjs/api/internal/sceneSetup.js +25 -22
  29. package/dist/lib-cjs/api/internal/sceneSetup.js.map +1 -1
  30. package/dist/lib-cjs/api/manager/animationManager.js +24 -15
  31. package/dist/lib-cjs/api/manager/animationManager.js.map +1 -1
  32. package/dist/lib-cjs/api/manager/gltfExportManager.js +1 -1
  33. package/dist/lib-cjs/api/manager/gltfExportManager.js.map +1 -1
  34. package/dist/lib-cjs/api/manager/sceneManager.js +3 -3
  35. package/dist/lib-cjs/api/manager/sceneManager.js.map +1 -1
  36. package/dist/lib-cjs/api/manager/variantInstanceManager.js +10 -7
  37. package/dist/lib-cjs/api/manager/variantInstanceManager.js.map +1 -1
  38. package/dist/lib-cjs/api/store/specStorage.js.map +1 -1
  39. package/dist/lib-cjs/api/util/babylonHelper.d.ts +1 -1
  40. package/dist/lib-cjs/api/util/babylonHelper.js +12 -15
  41. package/dist/lib-cjs/api/util/babylonHelper.js.map +1 -1
  42. package/dist/lib-cjs/api/util/globalTypes.d.ts +32 -25
  43. package/dist/lib-cjs/api/util/resourceHelper.js +3 -1
  44. package/dist/lib-cjs/api/util/resourceHelper.js.map +1 -1
  45. package/dist/lib-cjs/api/util/stringHelper.d.ts +1 -1
  46. package/dist/lib-cjs/api/util/stringHelper.js +2 -2
  47. package/dist/lib-cjs/api/util/stringHelper.js.map +1 -1
  48. package/dist/lib-cjs/index.js +1 -0
  49. package/dist/lib-cjs/index.js.map +1 -1
  50. package/package.json +11 -7
  51. package/src/api/classes/animationInterface.ts +4 -6
  52. package/src/api/classes/dottedPath.ts +179 -187
  53. package/src/api/classes/element.ts +653 -621
  54. package/src/api/classes/event.ts +310 -313
  55. package/src/api/classes/eventBroadcaster.ts +47 -49
  56. package/src/api/classes/parameter.ts +394 -397
  57. package/src/api/classes/parameterObservable.ts +84 -83
  58. package/src/api/classes/parameterizable.ts +71 -73
  59. package/src/api/classes/placementAnimation.ts +130 -130
  60. package/src/api/classes/variant.ts +806 -778
  61. package/src/api/classes/variantInstance.ts +60 -52
  62. package/src/api/classes/variantParameterizable.ts +72 -67
  63. package/src/api/classes/viewer.ts +566 -490
  64. package/src/api/classes/viewerLight.ts +301 -301
  65. package/src/api/internal/debugViewer.ts +61 -53
  66. package/src/api/internal/lensRendering.ts +2 -3
  67. package/src/api/internal/sceneSetup.ts +144 -137
  68. package/src/api/manager/animationManager.ts +122 -97
  69. package/src/api/manager/gltfExportManager.ts +71 -0
  70. package/src/api/manager/sceneManager.ts +83 -86
  71. package/src/api/manager/variantInstanceManager.ts +234 -224
  72. package/src/api/store/specStorage.ts +48 -51
  73. package/src/api/util/babylonHelper.ts +329 -325
  74. package/src/api/util/globalTypes.ts +251 -246
  75. package/src/api/util/resourceHelper.ts +97 -109
  76. package/src/api/util/stringHelper.ts +13 -16
  77. package/src/buildinfo.json +2 -2
  78. package/src/commonjs.tsconfig.json +8 -11
  79. package/src/declaration.tsconfig.json +6 -8
  80. package/src/dev.ts +28 -29
  81. package/src/es6.tsconfig.json +8 -11
  82. package/src/index.ts +40 -39
  83. package/src/pagesconfig.json +4 -0
  84. package/src/tsconfig.json +30 -41
  85. package/src/tsconfig.types.json +8 -9
  86. package/src/types.d.ts +0 -1
  87. package/dist/lib-cjs/api/classes/elementParameterizable.d.ts +0 -14
  88. package/dist/lib-cjs/api/classes/elementParameterizable.js +0 -135
  89. package/dist/lib-cjs/api/classes/elementParameterizable.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { IPointerEvent } from '@babylonjs/core';
1
2
  import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
2
3
  import { PickingInfo } from '@babylonjs/core/Collisions/pickingInfo';
3
4
  import { BoundingInfo } from '@babylonjs/core/Culling/boundingInfo';
@@ -10,21 +11,22 @@ import { Vector3 } from '@babylonjs/core/Maths/math.vector';
10
11
  import { Mesh } from '@babylonjs/core/Meshes/mesh';
11
12
  import { ScreenshotTools } from '@babylonjs/core/Misc/screenshotTools';
12
13
  import { Scene } from '@babylonjs/core/scene';
14
+ import { WebXRSessionManager } from '@babylonjs/core/XR/webXRSessionManager';
13
15
  import { isString } from 'lodash-es';
14
16
  import { version } from '../../buildinfo.json';
15
17
  import { sceneSetup } from '../internal/sceneSetup';
16
18
  import { AnimationManager } from '../manager/animationManager';
19
+ import { GltfExportManager } from '../manager/gltfExportManager';
17
20
  import { SceneManager } from '../manager/sceneManager';
18
21
  import { VariantInstanceManager } from '../manager/variantInstanceManager';
19
22
  import { SpecStorage } from '../store/specStorage';
20
- import { debounce, loadJson, sleep } from '../util/resourceHelper';
23
+ import { debounce, loadJson } from '../util/resourceHelper';
21
24
  import { Event } from './event';
22
25
  import { EventBroadcaster } from './eventBroadcaster';
23
26
  import { Parameter } from './parameter';
24
27
  import { Variant } from './variant';
25
28
  import { VariantInstance } from './variantInstance';
26
29
 
27
-
28
30
  /**
29
31
  * The main exposed object. This is the entry point into the application
30
32
  *
@@ -35,491 +37,565 @@ import { VariantInstance } from './variantInstance';
35
37
  * The class does nothing on its own and needs to {@link bootstrap}
36
38
  */
37
39
  export class Viewer extends EventBroadcaster {
38
-
39
- protected _scene: Scene | null = null;
40
-
41
- protected _animationManager: AnimationManager | null = null;
42
-
43
- protected _sceneManager: SceneManager | null = null;
44
-
45
- protected _variantInstances: VariantInstanceManager | null = null;
46
-
47
- static version = version;
48
-
49
- /**
50
- * Constructor
51
- */
52
- public constructor( public readonly canvas: HTMLCanvasElement, protected structureJson: string | StructureJson ) {
53
- super();
54
- }
55
-
56
- /**
57
- * Gets the BabylonJS Scene that is attached to the instance.
58
- *
59
- * @throws Error if the `scene` has not been initialized.
60
- */
61
- get scene(): Scene {
62
- if( !this._scene ) {
63
- throw new Error( `Scene has not been initialized.` );
64
- }
65
- return this._scene;
66
- }
67
-
68
- get sceneManager(): SceneManager {
69
- if( !this._sceneManager ) {
70
- throw new Error( `Environment has not been initialized.` );
71
- }
72
- return this._sceneManager;
73
- }
74
-
75
- /**
76
- * Gets the BabylonJS Engine that is attached to the viewer.
77
- */
78
- get engine(): Engine {
79
- return this.scene.getEngine();
80
- }
81
-
82
- /**
83
- * Gets the {@link VariantInstanceManager} attached to the viewer.
84
- *
85
- * @throws Error if the {@link VariantInstanceManager} has not been initialized.
86
- */
87
- get variantInstances(): VariantInstanceManager {
88
- if( !this._variantInstances ) {
89
- throw Error( `There is no variantInstanceManager.` );
90
- }
91
- return this._variantInstances;
92
- }
93
-
94
- /**
95
- * Gets the {@link AnimationManager} attached to the viewer.
96
- *
97
- * @throws Error if the {@link AnimationManager} has not been initialized.
98
- */
99
- get animationManager(): AnimationManager {
100
- if( !this._animationManager ) {
101
- throw new Error( `There is no animationManager instance.` );
102
- }
103
- return this._animationManager;
104
- }
105
-
106
- /**
107
- * Starts the application. This will
108
- * * load the given "index" JSON file
109
- * * setup the scene with the "scene" JSON file
110
- * * create an (optional) default setup with different variant settings
111
- * * sets up resizing by attaching a debounced version of {@link resize}
112
- *
113
- * @throws Error if any of the files is not found/valid
114
- *
115
- * @emits {@link Event.BOOTSTRAP_START}
116
- * @emits {@link Event.BOOTSTRAP_END}
117
- */
118
- public async bootstrap(): Promise<Viewer> {
119
- this.broadcastEvent( Event.BOOTSTRAP_START, this );
120
- let indexJson;
121
- if( isString( this.structureJson ) ) {
122
- indexJson = await loadJson<StructureJson>( this.structureJson );
123
- } else {
124
- indexJson = this.structureJson;
125
- }
126
- if( !indexJson.scene ) {
127
- throw new Error( `No "scene" property found for bootstrapping.` );
128
- }
129
- // fill spec store
130
- SpecStorage.createFromSpec( indexJson );
131
- // load scene
132
- if( isString( indexJson.scene ) ) {
133
- const sceneJson = await loadJson<SceneJson>( indexJson.scene );
134
- indexJson.scene = sceneJson;
135
- }
136
- this._scene = await this.initScene();
137
- // create instance manager
138
- const rootVariant = await Variant.create( '_', indexJson, this );
139
- this._variantInstances = await VariantInstanceManager.create( rootVariant );
140
- // create optional default instances
141
- if( indexJson.setup ) {
142
- if( isString( indexJson.setup ) ) {
143
- const setupJson = await loadJson<SetupJson>( indexJson.setup );
144
- indexJson.setup = setupJson;
145
- }
146
- await this.createVariantInstances();
147
- }
148
- // resize handler
149
- window.addEventListener( 'resize', debounce( this.resize.bind( this ), 100 ) );
150
- // wait until scene is completely ready
151
- await this.scene.whenReadyAsync();
152
- // event broadcasting
153
- this.broadcastEvent( Event.BOOTSTRAP_END, this );
154
- // render loop
155
- this.engine.runRenderLoop( () => {
156
- this.scene.render();
157
- } );
158
- return this;
159
- }
160
-
161
- // Disabled for now to decrease bundle size.
162
- // See https://github.com/Combeenation/3d-viewer/issues/37
163
- // /**
164
- // * Enables the BabylonJS DebugLayer. If you pass options, be sure to pass an Object of type `IInspectorOptions`.
165
- // */
166
- // public async enableDebugLayer(options?: any) {
167
- // // @ts-ignore
168
- // await import(/* webpackChunkName: "debug-inspector" */'@babylonjs/inspector');
169
- // await this.scene.debugLayer.show(options);
170
- // return this;
171
- // }
172
-
173
- /**
174
- * Destroys all registered {@link VariantInstance}s that are registered
175
- */
176
- public destroyVariantInstances(): Viewer {
177
- this.variantInstances.all.forEach( variantInstance => {
178
- this.variantInstances.destroy( variantInstance.name );
179
- } );
180
- return this;
181
- }
182
-
183
- /**
184
- * Trigger a resize event for the `Engine`
185
- */
186
- public resize(): Viewer {
187
- this.engine.resize();
188
- return this;
189
- }
190
-
191
- /**
192
- * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
193
- * {@link DottedPath}s.
194
- */
195
- public async getNode( variantInstanceName: string,
196
- elementDottedPath: DottedPathArgument,
197
- nodeDottedPath: DottedPathArgument ): Promise<TransformNode> {
198
- const variantInstance = await this.variantInstances.get(variantInstanceName);
199
- return variantInstance.getNode( elementDottedPath, nodeDottedPath );
200
- }
201
-
202
- /**
203
- * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
204
- * {@link DottedPath}s.
205
- */
206
- public async getMesh( variantInstanceName: string,
207
- elementDottedPath: DottedPathArgument,
208
- meshDottedPath: DottedPathArgument ): Promise<Mesh|null> {
209
- const variantInstance = await this.variantInstances.get(variantInstanceName);
210
- return variantInstance.getMesh( elementDottedPath, meshDottedPath );
211
- }
212
-
213
- /**
214
- * Switches the camera
215
- *
216
- * @emits {@link Event.CAMERA_SWITCHED}
217
- */
218
- public switchCamera( newCamera: string, reset: boolean = true ): Viewer {
219
- const camera = this.scene.getCameraByName( newCamera );
220
- if( camera ) {
221
- const activeCamera = this.scene.activeCamera;
222
- if( activeCamera ) {
223
- activeCamera.detachControl( this.engine.getRenderingCanvas()! );
224
- }
225
- if( reset ) {
226
- camera.restoreState();
227
- }
228
- this.scene.setActiveCameraByName( newCamera );
229
- camera.attachControl( this.engine.getRenderingCanvas()! );
230
- this.broadcastEvent( Event.CAMERA_SWITCHED, camera );
231
- } else {
232
- throw new Error( `Given camera "${newCamera}" not found.` );
233
- }
234
- // TODO: put traceable observers to new camera (@see element)
235
- return this;
236
- }
237
-
238
- /**
239
- * Moves or animates the active camera to given `placement`.
240
- */
241
- public async moveActiveCameraTo( placement: string | PlacementDefinition,
242
- animation?: string | AnimationDefinition ): Promise<AnimationInterface> {
243
- return this.animationManager.animateToPlacement( this.sceneManager.activeCamera, placement, animation );
244
- }
245
-
246
- /**
247
- * Takes a sceenshot the the current scene. The result is a string containing a base64 encoded image
248
- */
249
- public screenshot( settings?: ScreenshotSettings ): Promise<string> {
250
- return new Promise( ( resolve, reject ) => {
251
- if( !this.engine ) {
252
- return reject( 'Engine is null' );
253
- }
254
- if( !this.scene ) {
255
- return reject( 'Scene is null' );
256
- }
257
- this.scene.render(); // in combination with a render target, we need to refresh the scene manually to get the latest view
258
- ScreenshotTools.CreateScreenshotUsingRenderTarget(
259
- this.engine,
260
- this.sceneManager.activeCamera,
261
- settings?.size ?? { width: this.canvas.clientWidth, height: this.canvas.clientHeight },
262
- resolve,
263
- settings?.mimeType ?? 'image/png',
264
- settings?.samples ?? 1,
265
- settings?.antialiasing ?? false,
266
- settings?.fileName ?? 'screenshot.png',
267
- settings?.renderSprites ?? false
268
- );
269
- } );
270
- }
271
-
272
- /**
273
- * Calculates the bounding box from all visible meshes on the scene.
274
- */
275
- public async calculateBoundingBox(): Promise<Mesh> {
276
- if( this.scene.meshes.length === 0 ) {
277
- throw new Error( 'There are currently no meshes on the scene.' );
278
- }
279
- this.scene.render(); // CB-6062: workaround for BoundingBox not respecting render loop
280
- const bbName = '__bounding_box__';
281
-
282
- const { max, min } = this.scene.meshes.filter( mesh => {
283
- const isEnabled = mesh.isEnabled();
284
- // ignore the existing bounding box mesh for calculating the current one
285
- const isNotBBoxMesh = bbName !== mesh.id;
286
- // ignore meshes with invalid bounding infos
287
- const hasValidBBoxInfo = mesh.getBoundingInfo().boundingSphere.radius > 0;
288
- return isEnabled && isNotBBoxMesh && hasValidBBoxInfo;
289
- } ).reduce( ( accBBoxMinMax, curMesh, idx ) => {
290
- const bBox = curMesh.getBoundingInfo().boundingBox;
291
- // use the first entry in the array as default value and get the resulting maximum/minimum values
292
- const max = ( idx === 0 ) ? bBox.maximumWorld : Vector3.Maximize( accBBoxMinMax.max, bBox.maximumWorld )
293
- const min = ( idx === 0 ) ? bBox.minimumWorld : Vector3.Minimize( accBBoxMinMax.min, bBox.minimumWorld )
294
- return { max, min }
295
- }, { max: new Vector3(), min: new Vector3() } );
296
-
297
- let boundingBox = this.scene.getMeshByName( bbName ) as Mesh;
298
- if( !boundingBox ) {
299
- boundingBox = new Mesh( bbName, this.scene );
300
- }
301
- boundingBox.setBoundingInfo( new BoundingInfo( min, max ) );
302
- return boundingBox;
303
- }
304
-
305
- public async autofocusActiveCamera( settings?: AutofocusSettings ) {
306
- // first check some preconditions
307
- const activeCamera = this.scene.activeCamera;
308
- if ( !activeCamera ) {
309
- throw new Error("No active camera found when using autofocus feature.");
310
- }
311
- if ( !( activeCamera instanceof ArcRotateCamera ) ) {
312
- const cameraClsName = activeCamera.getClassName();
313
- throw new Error( `Camera of type "${cameraClsName}" is not implemented yet to use autofocus feature.` );
314
- }
315
-
316
- // get bounding box of all visible meshes, this is the base for the autofocus algorithm
317
- const boundingBox = await this.calculateBoundingBox();
318
-
319
- // focus the helper camera and set the calculated camera data to the real camera
320
- const helperCamera = this.getFocusedHelperCamera( boundingBox, settings )
321
- await this.applyFocusedHelperCameraData( activeCamera, helperCamera, settings )
322
-
323
- // remove the helper camera
324
- helperCamera.dispose();
325
- }
326
-
327
- /**
328
- * Resets everything by calling {@link destroy} to clear all references and {@link bootstrap} to setup a clean
329
- * environment
330
- */
331
- public async reset(): Promise<Viewer> {
332
- await this.destroy();
333
- return this.bootstrap();
334
- }
335
-
336
- /**
337
- * Destroys
338
- *
339
- * * all {@link VariantInstance}s using {@link destroyVariantInstances}
340
- * * calling `dispose` on the `Engine` and `Scene`
341
- */
342
- public destroy(): Viewer {
343
- this.destroyVariantInstances();
344
- this.scene.dispose();
345
- SpecStorage.destroy();
346
- return this;
347
- }
348
-
349
- /**
350
- * Show coordinate system with given dimension (for debugging purpose).
351
- */
352
- public showWorldCoordinates( dimension: number ) {
353
- const scene = this.scene;
354
- const makeTextPlane = function( text: string, color: string, size: number ) {
355
- const dynamicTexture = new DynamicTexture( 'DynamicTexture', 50, scene, true );
356
- dynamicTexture.hasAlpha = true;
357
- dynamicTexture.drawText( text, 5, 40, 'bold 36px Arial', color, 'transparent', true );
358
- const plane = Mesh.CreatePlane( 'TextPlane', size, scene, true );
359
- plane.material = new StandardMaterial( 'TextPlaneMaterial', scene );
360
- plane.material.backFaceCulling = false;
361
- // @ts-ignore
362
- plane.material.specularColor = new Color3( 0, 0, 0 );
363
- // @ts-ignore
364
- plane.material.diffuseTexture = dynamicTexture;
365
- return plane;
366
- };
367
- const axisX = Mesh.CreateLines( 'axisX', [
368
- Vector3.Zero(),
369
- new Vector3( dimension, 0, 0 ),
370
- new Vector3( dimension * 0.95, 0.05 * dimension, 0 ),
371
- new Vector3( dimension, 0, 0 ),
372
- new Vector3( dimension * 0.95, -0.05 * dimension, 0 )
373
- ], scene );
374
- axisX.color = new Color3( 1, 0, 0 );
375
- const xChar = makeTextPlane( 'X', 'red', dimension / 10 );
376
- xChar.position = new Vector3( 0.9 * dimension, -0.05 * dimension, 0 );
377
- const axisY = Mesh.CreateLines( 'axisY', [
378
- Vector3.Zero(),
379
- new Vector3( 0, dimension, 0 ),
380
- new Vector3( -0.05 * dimension, dimension * 0.95, 0 ),
381
- new Vector3( 0, dimension, 0 ),
382
- new Vector3( 0.05 * dimension, dimension * 0.95, 0 )
383
- ], scene );
384
- axisY.color = new Color3( 0, 1, 0 );
385
- const yChar = makeTextPlane( 'Y', 'green', dimension / 10 );
386
- yChar.position = new Vector3( 0, 0.9 * dimension, -0.05 * dimension );
387
- const axisZ = Mesh.CreateLines( 'axisZ', [
388
- Vector3.Zero(),
389
- new Vector3( 0, 0, dimension ),
390
- new Vector3( 0, -0.05 * dimension, dimension * 0.95 ),
391
- new Vector3( 0, 0, dimension ),
392
- new Vector3( 0, 0.05 * dimension, dimension * 0.95 )
393
- ], scene );
394
- axisZ.color = new Color3( 0, 0, 1 );
395
- const zChar = makeTextPlane( 'Z', 'blue', dimension / 10 );
396
- zChar.position = new Vector3( 0, 0.05 * dimension, 0.9 * dimension );
397
- }
398
-
399
- /**
400
- * @emits {@link Event.SCENE_PROCESSING_START}
401
- * @emits {@link Event.SCENE_PROCESSING_END}
402
- */
403
- protected async initScene(): Promise<Scene> {
404
- const sceneJson = SpecStorage.get<SceneJson>( 'scene' );
405
- this.broadcastEvent( Event.SCENE_PROCESSING_START, sceneJson );
406
- const engine = new Engine(
407
- this.canvas as HTMLCanvasElement,
408
- sceneJson.engine?.antialiasing ?? false,
409
- sceneJson.engine?.options
410
- );
411
- const scene = await sceneSetup( engine, sceneJson );
412
- if( sceneJson.meshPicking ) {
413
- new HighlightLayer( 'default', scene );
414
- scene.onPointerPick = ( pointerEvent: PointerEvent, pickInfo: PickingInfo ) => {
415
- if( !pickInfo.hit ) {
416
- return;
417
- }
418
- const mesh = pickInfo.pickedMesh;
419
- this.broadcastEvent( Event.MESH_PICKED, mesh, mesh?.metadata.element, mesh?.metadata.variant );
420
- if( mesh?.metadata.element ) {
421
- this.broadcastEvent( Event.ELEMENT_PICKED, mesh.metadata.element );
422
- }
423
- if( mesh?.metadata.variant ) {
424
- if( mesh.metadata.variant.inheritedParameters[Parameter.HIGHLIGHT_ENABLED] ) {
425
- mesh.metadata.variant.toggleHighlight();
426
- }
427
- this.broadcastEvent( Event.VARIANT_PICKED, mesh.metadata.variant );
428
- }
429
- };
430
- }
431
- this._sceneManager = await SceneManager.create( scene );
432
- this._animationManager = await AnimationManager.create( scene );
433
- this.broadcastEvent( Event.SCENE_PROCESSING_END, scene );
434
- return scene;
435
- }
436
-
437
- /**
438
- * Batch creation of multiple {@link VariantInstance} objects with a {@link SetupJson} object passed
439
- */
440
- protected async createVariantInstances(): Promise<VariantInstance[]> {
441
- const setupJson = SpecStorage.get<SetupJson>( 'setup' );
442
- const instances = [];
443
- for( const instanceDefinition of setupJson.instances ) {
444
- if( instanceDefinition.lazy ) {
445
- this.variantInstances.register( instanceDefinition );
446
- continue;
447
- }
448
- instances.push( await this.variantInstances.create(
449
- instanceDefinition.variant,
450
- instanceDefinition.name,
451
- instanceDefinition.parameters
452
- ) );
453
- }
454
- return instances;
455
- }
456
-
457
- /**
458
- * Help function for focusing a helper camera exactly onto the given bounding box
459
- */
460
- private getFocusedHelperCamera(boundingBox: Mesh, settings?: AutofocusSettings): ArcRotateCamera {
461
- // use helper camera to get some default values and set the values of the real camera accordingly
462
- const helperCamera = new ArcRotateCamera(
463
- "__helper_camera__",
464
- 0, // camera angles will be overwritten after the target has been set
465
- 0,
466
- 0, // radius will be calculated, so we can set to 0 here
467
- Vector3.Zero(),
468
- this.scene
469
- );
470
- // this is required for automatically calculating the `lowerRadiusLimit`, so that we don't "dive" into meshes
471
- // see https://doc.babylonjs.com/divingDeeper/behaviors/cameraBehaviors#framing-behavior
472
- helperCamera.useFramingBehavior = true;
473
-
474
- // `minZ` is the camera distance beyond which the mesh will be clipped
475
- // this should be very low, but can't be zero
476
- // a good value seems to be 1% of the bounding box size (= radius), whereas the value shouldn't go above 1, which is also the default value
477
- const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
478
- helperCamera.minZ = Math.min(radius/100, 1);
479
-
480
- // set desired camera data, these won't be changed by the autofocus function!
481
- // default values should focus the element exactly from the front (= XY Plane)
482
- helperCamera.setTarget(boundingBox, true);
483
- helperCamera.alpha = ( settings?.alpha ?? - 90 ) * ( Math.PI / 180 );
484
- helperCamera.beta = ( settings?.beta ?? 90 ) * ( Math.PI / 180 );
485
-
486
- // finally zoom to the bounding box
487
- // also apply a zoom factor, this adjusts the borders around the model in the viewport
488
- helperCamera.zoomOnFactor = settings?.radiusFactor || 1;
489
- helperCamera.zoomOn( [boundingBox], true );
490
-
491
- return helperCamera;
492
- }
493
-
494
- /**
495
- * Help function for applying the relevant data of the focused helper camera to the real camera
496
- */
497
- private async applyFocusedHelperCameraData(activeCamera: ArcRotateCamera, helperCamera: ArcRotateCamera, settings?: AutofocusSettings) {
498
- // limits
499
- activeCamera.minZ = helperCamera.minZ;
500
- activeCamera.maxZ = helperCamera.maxZ;
501
- activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
502
- activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
503
-
504
- // additional settings
505
- if( settings?.adjustWheelPrecision !== false ) {
506
- activeCamera.wheelPrecision = helperCamera.wheelPrecision;
507
- }
508
- if( settings?.adjustPanningSensibility !== false ) {
509
- activeCamera.panningSensibility = helperCamera.panningSensibility;
510
- }
511
- if( settings?.adjustPinchPrecision !== false ) {
512
- activeCamera.pinchPrecision = helperCamera.pinchPrecision;
513
- }
514
-
515
- // finally move the camera
516
- // do this at last, so that all camera settings are already considered
517
- const newCameraPosition: PlacementDefinition = {
518
- alpha: helperCamera.alpha,
519
- beta: helperCamera.beta,
520
- radius: helperCamera.radius,
521
- target: helperCamera.target
522
- }
523
- await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation)
524
- }
525
- }
40
+ protected _scene: Scene | null = null;
41
+
42
+ protected _animationManager: AnimationManager | null = null;
43
+
44
+ protected _sceneManager: SceneManager | null = null;
45
+
46
+ protected _gltfExportManager: GltfExportManager | null = null;
47
+
48
+ protected _variantInstances: VariantInstanceManager | null = null;
49
+
50
+ // default value is `true` ATM for compatibility reasons
51
+ // in the future material cloning should be the edge case
52
+ protected _cloneMaterialsOnMutation: boolean = true;
53
+
54
+ static version = version;
55
+
56
+ /**
57
+ * Constructor
58
+ */
59
+ public constructor(public readonly canvas: HTMLCanvasElement, protected structureJson: string | StructureJson) {
60
+ super();
61
+ }
62
+
63
+ /**
64
+ * Gets the BabylonJS Scene that is attached to the instance.
65
+ *
66
+ * @throws Error if the `scene` has not been initialized.
67
+ */
68
+ get scene(): Scene {
69
+ if (!this._scene) {
70
+ throw new Error(`Scene has not been initialized.`);
71
+ }
72
+ return this._scene;
73
+ }
74
+
75
+ /**
76
+ * Gets the {@link SceneManager} attached to the viewer.
77
+ *
78
+ * @throws Error if the {@link SceneManager} has not been initialized.
79
+ */
80
+ get sceneManager(): SceneManager {
81
+ if (!this._sceneManager) {
82
+ throw new Error(`SceneManager has not been initialized.`);
83
+ }
84
+ return this._sceneManager;
85
+ }
86
+
87
+ /**
88
+ * Gets the {@link GltfExportManager} attached to the viewer.
89
+ *
90
+ * @throws Error if the {@link GltfExportManager} has not been initialized.
91
+ */
92
+ get gltfExportManager(): GltfExportManager {
93
+ if (!this._gltfExportManager) {
94
+ throw new Error(`GltfExportManager has not been initialized.`);
95
+ }
96
+ return this._gltfExportManager;
97
+ }
98
+
99
+ /**
100
+ * Gets the BabylonJS Engine that is attached to the viewer.
101
+ */
102
+ get engine(): Engine {
103
+ return this.scene.getEngine();
104
+ }
105
+
106
+ /**
107
+ * Gets the {@link VariantInstanceManager} attached to the viewer.
108
+ *
109
+ * @throws Error if the {@link VariantInstanceManager} has not been initialized.
110
+ */
111
+ get variantInstances(): VariantInstanceManager {
112
+ if (!this._variantInstances) {
113
+ throw Error(`There is no variantInstanceManager.`);
114
+ }
115
+ return this._variantInstances;
116
+ }
117
+
118
+ /**
119
+ * Gets the {@link AnimationManager} attached to the viewer.
120
+ *
121
+ * @throws Error if the {@link AnimationManager} has not been initialized.
122
+ */
123
+ get animationManager(): AnimationManager {
124
+ if (!this._animationManager) {
125
+ throw new Error(`There is no animationManager instance.`);
126
+ }
127
+ return this._animationManager;
128
+ }
129
+
130
+ /**
131
+ * Gets the `cloneMaterialsOnMutation` flag, as defined in the spec
132
+ */
133
+ get cloneMaterialsOnMutation(): boolean {
134
+ return this._cloneMaterialsOnMutation;
135
+ }
136
+
137
+ /**
138
+ * Starts the application. This will
139
+ * * load the given "index" JSON file
140
+ * * setup the scene with the "scene" JSON file
141
+ * * create an (optional) default setup with different variant settings
142
+ * * sets up resizing by attaching a debounced version of {@link resize}
143
+ *
144
+ * @throws Error if any of the files is not found/valid
145
+ *
146
+ * @emits {@link Event.BOOTSTRAP_START}
147
+ * @emits {@link Event.BOOTSTRAP_END}
148
+ */
149
+ public async bootstrap(): Promise<Viewer> {
150
+ this.broadcastEvent(Event.BOOTSTRAP_START, this);
151
+ let indexJson;
152
+ if (isString(this.structureJson)) {
153
+ indexJson = await loadJson<StructureJson>(this.structureJson);
154
+ } else {
155
+ indexJson = this.structureJson;
156
+ }
157
+ if (!indexJson.scene) {
158
+ throw new Error(`No "scene" property found for bootstrapping.`);
159
+ }
160
+ // fill spec store
161
+ SpecStorage.createFromSpec(indexJson);
162
+ // load scene
163
+ if (isString(indexJson.scene)) {
164
+ const sceneJson = await loadJson<SceneJson>(indexJson.scene);
165
+ indexJson.scene = sceneJson;
166
+ }
167
+ this._scene = await this.initScene();
168
+ // create instance manager
169
+ const rootVariant = await Variant.create('_', indexJson, this);
170
+ this._variantInstances = await VariantInstanceManager.create(rootVariant);
171
+ // create optional default instances
172
+ if (indexJson.setup) {
173
+ if (isString(indexJson.setup)) {
174
+ const setupJson = await loadJson<SetupJson>(indexJson.setup);
175
+ indexJson.setup = setupJson;
176
+ }
177
+ await this.createVariantInstances();
178
+ }
179
+ // create gltf export manager
180
+ this._gltfExportManager = await GltfExportManager.create(this.scene);
181
+ // resize handler
182
+ window.addEventListener('resize', debounce(this.resize.bind(this), 100));
183
+ // wait until scene is completely ready
184
+ await this.scene.whenReadyAsync();
185
+ // event broadcasting
186
+ this.broadcastEvent(Event.BOOTSTRAP_END, this);
187
+ // render loop
188
+ this.engine.runRenderLoop(() => {
189
+ this.scene.render();
190
+ });
191
+ return this;
192
+ }
193
+
194
+ /**
195
+ * Enables the BabylonJS [Inspector](https://doc.babylonjs.com/toolsAndResources/tools/inspector).\
196
+ * Due to the enormous additional package size of the inspector, this feature is only available in development builds.
197
+ */
198
+ public async enableDebugLayer(options?: IInspectorOptions) {
199
+ if (process.env.NODE_ENV?.toLowerCase().includes('dev')) {
200
+ await import(/* webpackChunkName: "debug-inspector" */ '@babylonjs/inspector');
201
+ await this.scene.debugLayer.show(options);
202
+ } else {
203
+ console.warn('BabylonJS inspector is not supported in production builds!');
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Destroys all registered {@link VariantInstance}s that are registered
209
+ */
210
+ public destroyVariantInstances(): Viewer {
211
+ this.variantInstances.all.forEach(variantInstance => {
212
+ this.variantInstances.destroy(variantInstance.name);
213
+ });
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * Trigger a resize event for the `Engine`
219
+ */
220
+ public resize(): Viewer {
221
+ this.engine.resize();
222
+ return this;
223
+ }
224
+
225
+ /**
226
+ * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
227
+ * {@link DottedPath}s.
228
+ */
229
+ public async getNode(
230
+ variantInstanceName: string,
231
+ elementDottedPath: DottedPathArgument,
232
+ nodeDottedPath: DottedPathArgument
233
+ ): Promise<TransformNode> {
234
+ const variantInstance = await this.variantInstances.get(variantInstanceName);
235
+ return variantInstance.getNode(elementDottedPath, nodeDottedPath);
236
+ }
237
+
238
+ /**
239
+ * A convenience method for directly getting a Node from a {@link VariantInstance} and an {@link Element} by its
240
+ * {@link DottedPath}s.
241
+ */
242
+ public async getMesh(
243
+ variantInstanceName: string,
244
+ elementDottedPath: DottedPathArgument,
245
+ meshDottedPath: DottedPathArgument
246
+ ): Promise<Mesh | null> {
247
+ const variantInstance = await this.variantInstances.get(variantInstanceName);
248
+ return variantInstance.getMesh(elementDottedPath, meshDottedPath);
249
+ }
250
+
251
+ /**
252
+ * Switches the camera
253
+ *
254
+ * @emits {@link Event.CAMERA_SWITCHED}
255
+ */
256
+ public switchCamera(newCamera: string, reset: boolean = true): Viewer {
257
+ const camera = this.scene.getCameraByName(newCamera);
258
+ if (camera) {
259
+ const activeCamera = this.scene.activeCamera;
260
+ if (activeCamera) {
261
+ activeCamera.detachControl(this.engine.getRenderingCanvas()!);
262
+ }
263
+ if (reset) {
264
+ camera.restoreState();
265
+ }
266
+ this.scene.setActiveCameraByName(newCamera);
267
+ camera.attachControl(this.engine.getRenderingCanvas()!);
268
+ this.broadcastEvent(Event.CAMERA_SWITCHED, camera);
269
+ } else {
270
+ throw new Error(`Given camera "${newCamera}" not found.`);
271
+ }
272
+ // TODO: put traceable observers to new camera (@see element)
273
+ return this;
274
+ }
275
+
276
+ /**
277
+ * Moves or animates the active camera to given `placement`.
278
+ */
279
+ public async moveActiveCameraTo(
280
+ placement: string | PlacementDefinition,
281
+ animation?: string | AnimationDefinition
282
+ ): Promise<AnimationInterface> {
283
+ return this.animationManager.animateToPlacement(this.sceneManager.activeCamera, placement, animation);
284
+ }
285
+
286
+ /**
287
+ * Takes a sceenshot the the current scene. The result is a string containing a base64 encoded image
288
+ */
289
+ public screenshot(settings?: ScreenshotSettings): Promise<string> {
290
+ return new Promise((resolve, reject) => {
291
+ if (!this.engine) {
292
+ return reject('Engine is null');
293
+ }
294
+ if (!this.scene) {
295
+ return reject('Scene is null');
296
+ }
297
+ this.scene.render(); // in combination with a render target, we need to refresh the scene manually to get the latest view
298
+ ScreenshotTools.CreateScreenshotUsingRenderTarget(
299
+ this.engine,
300
+ this.sceneManager.activeCamera,
301
+ settings?.size ?? { width: this.canvas.clientWidth, height: this.canvas.clientHeight },
302
+ resolve,
303
+ settings?.mimeType ?? 'image/png',
304
+ settings?.samples ?? 1,
305
+ settings?.antialiasing ?? false,
306
+ settings?.fileName ?? 'screenshot.png',
307
+ settings?.renderSprites ?? false
308
+ );
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Checks whether the browser is capable of handling XR.
314
+ */
315
+ public async isBrowserARCapable(): Promise<boolean> {
316
+ return await WebXRSessionManager.IsSessionSupportedAsync('immersive-ar');
317
+ }
318
+
319
+ /**
320
+ * Calculates the bounding box from all visible meshes on the scene.
321
+ */
322
+ public async calculateBoundingBox(): Promise<Mesh> {
323
+ if (this.scene.meshes.length === 0) {
324
+ throw new Error('There are currently no meshes on the scene.');
325
+ }
326
+ this.scene.render(); // CB-6062: workaround for BoundingBox not respecting render loop
327
+ const bbName = '__bounding_box__';
328
+
329
+ const { max, min } = this.scene.meshes
330
+ .filter(mesh => {
331
+ const isEnabled = mesh.isEnabled();
332
+ // ignore the existing bounding box mesh for calculating the current one
333
+ const isNotBBoxMesh = bbName !== mesh.id;
334
+ // ignore meshes with invalid bounding infos
335
+ const hasValidBBoxInfo = mesh.getBoundingInfo().boundingSphere.radius > 0;
336
+ return isEnabled && isNotBBoxMesh && hasValidBBoxInfo;
337
+ })
338
+ .reduce(
339
+ (accBBoxMinMax, curMesh, idx) => {
340
+ const bBox = curMesh.getBoundingInfo().boundingBox;
341
+ // use the first entry in the array as default value and get the resulting maximum/minimum values
342
+ const max = idx === 0 ? bBox.maximumWorld : Vector3.Maximize(accBBoxMinMax.max, bBox.maximumWorld);
343
+ const min = idx === 0 ? bBox.minimumWorld : Vector3.Minimize(accBBoxMinMax.min, bBox.minimumWorld);
344
+ return { max, min };
345
+ },
346
+ { max: new Vector3(), min: new Vector3() }
347
+ );
348
+
349
+ let boundingBox = this.scene.getMeshByName(bbName) as Mesh;
350
+ if (!boundingBox) {
351
+ boundingBox = new Mesh(bbName, this.scene);
352
+ }
353
+ boundingBox.setBoundingInfo(new BoundingInfo(min, max));
354
+ return boundingBox;
355
+ }
356
+
357
+ /**
358
+ * Focuses the camera to see every visible mesh in scene and tries to optimize wheel precision and panning
359
+ */
360
+ public async autofocusActiveCamera(settings?: AutofocusSettings) {
361
+ // first check some preconditions
362
+ const activeCamera = this.scene.activeCamera;
363
+ if (!activeCamera) {
364
+ throw new Error('No active camera found when using autofocus feature.');
365
+ }
366
+ if (!(activeCamera instanceof ArcRotateCamera)) {
367
+ const cameraClsName = activeCamera.getClassName();
368
+ throw new Error(`Camera of type "${cameraClsName}" is not implemented yet to use autofocus feature.`);
369
+ }
370
+
371
+ // get bounding box of all visible meshes, this is the base for the autofocus algorithm
372
+ const boundingBox = await this.calculateBoundingBox();
373
+
374
+ // focus the helper camera and set the calculated camera data to the real camera
375
+ const helperCamera = this.getFocusedHelperCamera(boundingBox, settings);
376
+ await this.applyFocusedHelperCameraData(activeCamera, helperCamera, settings);
377
+
378
+ // remove the helper camera
379
+ helperCamera.dispose();
380
+ }
381
+
382
+ /**
383
+ * Resets everything by calling {@link destroy} to clear all references and {@link bootstrap} to setup a clean
384
+ * environment
385
+ */
386
+ public async reset(): Promise<Viewer> {
387
+ await this.destroy();
388
+ return this.bootstrap();
389
+ }
390
+
391
+ /**
392
+ * Destroys
393
+ *
394
+ * * all {@link VariantInstance}s using {@link destroyVariantInstances}
395
+ * * calling `dispose` on the `Engine` and `Scene`
396
+ */
397
+ public destroy(): Viewer {
398
+ this.destroyVariantInstances();
399
+ this.scene.dispose();
400
+ SpecStorage.destroy();
401
+ return this;
402
+ }
403
+
404
+ /**
405
+ * Show coordinate system with given dimension (for debugging purpose).
406
+ */
407
+ public showWorldCoordinates(dimension: number) {
408
+ const scene = this.scene;
409
+ const makeTextPlane = function (text: string, color: string, size: number) {
410
+ const dynamicTexture = new DynamicTexture('DynamicTexture', 50, scene, true);
411
+ dynamicTexture.hasAlpha = true;
412
+ dynamicTexture.drawText(text, 5, 40, 'bold 36px Arial', color, 'transparent', true);
413
+ const plane = Mesh.CreatePlane('TextPlane', size, scene, true);
414
+ plane.material = new StandardMaterial('TextPlaneMaterial', scene);
415
+ plane.material.backFaceCulling = false;
416
+ // @ts-ignore
417
+ plane.material.specularColor = new Color3(0, 0, 0);
418
+ // @ts-ignore
419
+ plane.material.diffuseTexture = dynamicTexture;
420
+ return plane;
421
+ };
422
+ const axisX = Mesh.CreateLines(
423
+ 'axisX',
424
+ [
425
+ Vector3.Zero(),
426
+ new Vector3(dimension, 0, 0),
427
+ new Vector3(dimension * 0.95, 0.05 * dimension, 0),
428
+ new Vector3(dimension, 0, 0),
429
+ new Vector3(dimension * 0.95, -0.05 * dimension, 0),
430
+ ],
431
+ scene
432
+ );
433
+ axisX.color = new Color3(1, 0, 0);
434
+ const xChar = makeTextPlane('X', 'red', dimension / 10);
435
+ xChar.position = new Vector3(0.9 * dimension, -0.05 * dimension, 0);
436
+ const axisY = Mesh.CreateLines(
437
+ 'axisY',
438
+ [
439
+ Vector3.Zero(),
440
+ new Vector3(0, dimension, 0),
441
+ new Vector3(-0.05 * dimension, dimension * 0.95, 0),
442
+ new Vector3(0, dimension, 0),
443
+ new Vector3(0.05 * dimension, dimension * 0.95, 0),
444
+ ],
445
+ scene
446
+ );
447
+ axisY.color = new Color3(0, 1, 0);
448
+ const yChar = makeTextPlane('Y', 'green', dimension / 10);
449
+ yChar.position = new Vector3(0, 0.9 * dimension, -0.05 * dimension);
450
+ const axisZ = Mesh.CreateLines(
451
+ 'axisZ',
452
+ [
453
+ Vector3.Zero(),
454
+ new Vector3(0, 0, dimension),
455
+ new Vector3(0, -0.05 * dimension, dimension * 0.95),
456
+ new Vector3(0, 0, dimension),
457
+ new Vector3(0, 0.05 * dimension, dimension * 0.95),
458
+ ],
459
+ scene
460
+ );
461
+ axisZ.color = new Color3(0, 0, 1);
462
+ const zChar = makeTextPlane('Z', 'blue', dimension / 10);
463
+ zChar.position = new Vector3(0, 0.05 * dimension, 0.9 * dimension);
464
+ }
465
+
466
+ /**
467
+ * @emits {@link Event.SCENE_PROCESSING_START}
468
+ * @emits {@link Event.SCENE_PROCESSING_END}
469
+ */
470
+ protected async initScene(): Promise<Scene> {
471
+ const sceneJson = SpecStorage.get<SceneJson>('scene');
472
+ this.broadcastEvent(Event.SCENE_PROCESSING_START, sceneJson);
473
+ const engine = new Engine(
474
+ this.canvas as HTMLCanvasElement,
475
+ sceneJson.engine?.antialiasing ?? false,
476
+ sceneJson.engine?.options
477
+ );
478
+ const scene = await sceneSetup(engine, sceneJson);
479
+ if (sceneJson.meshPicking) {
480
+ new HighlightLayer('default', scene);
481
+ scene.onPointerPick = (pointerEvent: IPointerEvent, pickInfo: PickingInfo) => {
482
+ if (!pickInfo.hit) {
483
+ return;
484
+ }
485
+ const mesh = pickInfo.pickedMesh;
486
+ this.broadcastEvent(Event.MESH_PICKED, mesh, mesh?.metadata.element, mesh?.metadata.variant);
487
+ if (mesh?.metadata.element) {
488
+ this.broadcastEvent(Event.ELEMENT_PICKED, mesh.metadata.element);
489
+ }
490
+ if (mesh?.metadata.variant) {
491
+ if (mesh.metadata.variant.inheritedParameters[Parameter.HIGHLIGHT_ENABLED]) {
492
+ mesh.metadata.variant.toggleHighlight();
493
+ }
494
+ this.broadcastEvent(Event.VARIANT_PICKED, mesh.metadata.variant);
495
+ }
496
+ };
497
+ }
498
+ this._sceneManager = await SceneManager.create(scene);
499
+ this._animationManager = await AnimationManager.create(scene);
500
+ if (sceneJson.cloneMaterialsOnMutation !== undefined) {
501
+ this._cloneMaterialsOnMutation = sceneJson.cloneMaterialsOnMutation;
502
+ }
503
+ this.broadcastEvent(Event.SCENE_PROCESSING_END, scene);
504
+ return scene;
505
+ }
506
+
507
+ /**
508
+ * Batch creation of multiple {@link VariantInstance} objects with a {@link SetupJson} object passed
509
+ */
510
+ protected async createVariantInstances(): Promise<VariantInstance[]> {
511
+ const setupJson = SpecStorage.get<SetupJson>('setup');
512
+ const instances = [];
513
+ for (const instanceDefinition of setupJson.instances) {
514
+ if (instanceDefinition.lazy) {
515
+ this.variantInstances.register(instanceDefinition);
516
+ continue;
517
+ }
518
+ instances.push(
519
+ await this.variantInstances.create(
520
+ instanceDefinition.variant,
521
+ instanceDefinition.name,
522
+ instanceDefinition.parameters
523
+ )
524
+ );
525
+ }
526
+ return instances;
527
+ }
528
+
529
+ /**
530
+ * Help function for focusing a helper camera exactly onto the given bounding box
531
+ */
532
+ private getFocusedHelperCamera(boundingBox: Mesh, settings?: AutofocusSettings): ArcRotateCamera {
533
+ // use helper camera to get some default values and set the values of the real camera accordingly
534
+ const helperCamera = new ArcRotateCamera(
535
+ '__helper_camera__',
536
+ 0, // camera angles will be overwritten after the target has been set
537
+ 0,
538
+ 0, // radius will be calculated, so we can set to 0 here
539
+ Vector3.Zero(),
540
+ this.scene
541
+ );
542
+ // this is required for automatically calculating the `lowerRadiusLimit`, so that we don't "dive" into meshes
543
+ // see https://doc.babylonjs.com/divingDeeper/behaviors/cameraBehaviors#framing-behavior
544
+ helperCamera.useFramingBehavior = true;
545
+
546
+ // `minZ` is the camera distance beyond which the mesh will be clipped
547
+ // this should be very low, but can't be zero
548
+ // a good value seems to be 1% of the bounding box size (= radius), whereas the value shouldn't go above 1, which is also the default value
549
+ const radius = boundingBox.getBoundingInfo().boundingSphere.radius;
550
+ helperCamera.minZ = Math.min(radius / 100, 1);
551
+
552
+ // set desired camera data, these won't be changed by the autofocus function!
553
+ // default values should focus the element exactly from the front (= XY Plane)
554
+ helperCamera.setTarget(boundingBox, true);
555
+ helperCamera.alpha = (settings?.alpha ?? -90) * (Math.PI / 180);
556
+ helperCamera.beta = (settings?.beta ?? 90) * (Math.PI / 180);
557
+
558
+ // finally zoom to the bounding box
559
+ // also apply a zoom factor, this adjusts the borders around the model in the viewport
560
+ helperCamera.zoomOnFactor = settings?.radiusFactor || 1;
561
+ helperCamera.zoomOn([boundingBox], true);
562
+
563
+ return helperCamera;
564
+ }
565
+
566
+ /**
567
+ * Help function for applying the relevant data of the focused helper camera to the real camera
568
+ */
569
+ private async applyFocusedHelperCameraData(
570
+ activeCamera: ArcRotateCamera,
571
+ helperCamera: ArcRotateCamera,
572
+ settings?: AutofocusSettings
573
+ ) {
574
+ // limits
575
+ activeCamera.minZ = helperCamera.minZ;
576
+ activeCamera.maxZ = helperCamera.maxZ;
577
+ activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
578
+ activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
579
+
580
+ // additional settings
581
+ if (settings?.adjustWheelPrecision !== false) {
582
+ activeCamera.wheelPrecision = helperCamera.wheelPrecision;
583
+ }
584
+ if (settings?.adjustPanningSensibility !== false) {
585
+ activeCamera.panningSensibility = helperCamera.panningSensibility;
586
+ }
587
+ if (settings?.adjustPinchPrecision !== false) {
588
+ activeCamera.pinchPrecision = helperCamera.pinchPrecision;
589
+ }
590
+
591
+ // finally move the camera
592
+ // do this at last, so that all camera settings are already considered
593
+ const newCameraPosition: PlacementDefinition = {
594
+ alpha: helperCamera.alpha,
595
+ beta: helperCamera.beta,
596
+ radius: helperCamera.radius,
597
+ target: helperCamera.target,
598
+ };
599
+ await this.animationManager.animateToPlacement(activeCamera, newCameraPosition, settings?.animation);
600
+ }
601
+ }