@combeenation/3d-viewer 4.0.1-alpha1 → 4.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.
@@ -276,7 +276,7 @@ export class Viewer extends EventBroadcaster {
276
276
  if( this.scene.meshes.length === 0 ) {
277
277
  throw new Error( 'There are currently no meshes on the scene.' );
278
278
  }
279
- this.scene.render(); // XXX: workaround for BoundingBox not respecting render loop
279
+ this.scene.render(); // CB-6062: workaround for BoundingBox not respecting render loop
280
280
  const bbName = '__bounding_box__';
281
281
 
282
282
  const { max, min } = this.scene.meshes.filter( mesh => {
@@ -302,57 +302,26 @@ export class Viewer extends EventBroadcaster {
302
302
  return boundingBox;
303
303
  }
304
304
 
305
- /**
306
- * Focuses the camera to see every visible mesh in scene and tries to optimize wheel precision and panning.
307
- */
308
305
  public async autofocusActiveCamera( settings?: AutofocusSettings ) {
306
+ // first check some preconditions
309
307
  const activeCamera = this.scene.activeCamera;
310
- if( !activeCamera ) {
311
- throw new Error( 'No active camera found when using autofocus feature.' );
308
+ if ( !activeCamera ) {
309
+ throw new Error("No active camera found when using autofocus feature.");
312
310
  }
313
- if( activeCamera instanceof ArcRotateCamera ) {
314
- // calculate some values
315
- const boundingBox = await this.calculateBoundingBox();
316
- const size = boundingBox.getBoundingInfo().maximum.subtract( boundingBox.getBoundingInfo().minimum );
317
- let radius = size.length() * (settings?.radiusFactor ?? 1.5);
318
- if( !isFinite( radius ) ) {
319
- radius = 1;
320
- }
321
- // use helper camera to get some default values
322
- const helperCamera = new ArcRotateCamera(
323
- '__helper_camera__',
324
- Math.PI / -2,
325
- Math.PI / 2,
326
- radius,
327
- Vector3.Zero(),
328
- this.scene
329
- );
330
- helperCamera.useFramingBehavior = true;
331
- helperCamera.setTarget( boundingBox );
332
- // translate values from helper to active camera
333
- activeCamera.setTarget( Mesh.Center( [boundingBox] ) );
334
- activeCamera.alpha = helperCamera.alpha;
335
- activeCamera.beta = helperCamera.beta;
336
- activeCamera.minZ = helperCamera.minZ;
337
- activeCamera.maxZ = helperCamera.maxZ;
338
- activeCamera.radius = helperCamera.radius;
339
- activeCamera.lowerRadiusLimit = helperCamera.lowerRadiusLimit;
340
- activeCamera.upperRadiusLimit = helperCamera.upperRadiusLimit;
341
- if( settings?.adjustWheelPrecision !== false ) {
342
- activeCamera.wheelPrecision = helperCamera.wheelPrecision;
343
- }
344
- if( settings?.adjustPanningSensibility !== false ) {
345
- activeCamera.panningSensibility = helperCamera.panningSensibility;
346
- }
347
- if( settings?.adjustPinchPrecision !== false ) {
348
- activeCamera.pinchPrecision = helperCamera.pinchPrecision;
349
- }
350
- // remove the helper camera
351
- helperCamera.dispose();
352
- } else {
311
+ if ( !( activeCamera instanceof ArcRotateCamera ) ) {
353
312
  const cameraClsName = activeCamera.getClassName();
354
313
  throw new Error( `Camera of type "${cameraClsName}" is not implemented yet to use autofocus feature.` );
355
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();
356
325
  }
357
326
 
358
327
  /**
@@ -485,4 +454,72 @@ export class Viewer extends EventBroadcaster {
485
454
  return instances;
486
455
  }
487
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
+ }
488
525
  }
@@ -14,8 +14,7 @@ import {
14
14
  enableNodeWithParents,
15
15
  getRootNode,
16
16
  injectNodeMetadata,
17
- moveTransformNode,
18
- rotateTransformNode
17
+ transformTransformNode
19
18
  } from './../util/babylonHelper';
20
19
  import { DottedPath } from './dottedPath';
21
20
  import { Parameter } from './parameter';
@@ -128,37 +127,42 @@ export class ViewerLight extends VariantParameterizable {
128
127
  set( light.light, 'intensity', Parameter.parseNumber( newValue ) );
129
128
  }
130
129
  ] );
131
- this._parameterObservers.set( Parameter.POSITION, [
130
+ this._parameterObservers.set( Parameter.SCALING, [
132
131
  async ( light: ViewerLight, oldValue: ParameterValue, newValue: ParameterValue ) => {
133
132
  // we have to deal just with root nodes here due to relative impacts in a node tree
134
133
  const rootNode = getRootNode( light.light );
135
134
  if( rootNode instanceof TransformNode ) {
136
- moveTransformNode( rootNode, Parameter.parseVector( newValue ) );
135
+ transformTransformNode( rootNode, {
136
+ scaling: Parameter.parseScaling( newValue ),
137
+ position: Parameter.parseVector( light.inheritedParameters[Parameter.POSITION] || '(0, 0, 0)' ),
138
+ rotation: Parameter.parseRotation( light.inheritedParameters[Parameter.ROTATION] || '(0, 0, 0)' )
139
+ } );
137
140
  }
138
141
  }
139
142
  ] );
140
- this._parameterObservers.set( Parameter.ROTATION, [
143
+ this._parameterObservers.set( Parameter.POSITION, [
141
144
  async ( light: ViewerLight, oldValue: ParameterValue, newValue: ParameterValue ) => {
142
- // The current implementation (rotating around coordinates 0,0,0) implicitly mutates the position of a node.
143
- // Since a user expects the rotation after the positioning, we have to manually fire the position observers.
144
- // Without calling these observers, the pivot and the position of a node is initially the same before rotating,
145
- // so there is no rotation happening at all.
146
- if( Parameter.POSITION in light.inheritedParameters ) {
147
- await light.commitParameter( Parameter.POSITION, light.inheritedParameters[Parameter.POSITION] );
148
- }
149
145
  // we have to deal just with root nodes here due to relative impacts in a node tree
150
146
  const rootNode = getRootNode( light.light );
151
147
  if( rootNode instanceof TransformNode ) {
152
- rotateTransformNode( rootNode, Parameter.parseRotation( newValue ) );
148
+ transformTransformNode( rootNode, {
149
+ scaling: Parameter.parseVector( light.inheritedParameters[Parameter.SCALING] || '(1, 1, 1)' ),
150
+ position: Parameter.parseVector( newValue ),
151
+ rotation: Parameter.parseRotation( light.inheritedParameters[Parameter.ROTATION] || '(0, 0, 0)' )
152
+ } );
153
153
  }
154
154
  }
155
155
  ] );
156
- this._parameterObservers.set( Parameter.SCALING, [
156
+ this._parameterObservers.set( Parameter.ROTATION, [
157
157
  async ( light: ViewerLight, oldValue: ParameterValue, newValue: ParameterValue ) => {
158
158
  // we have to deal just with root nodes here due to relative impacts in a node tree
159
159
  const rootNode = getRootNode( light.light );
160
160
  if( rootNode instanceof TransformNode ) {
161
- rootNode.scaling = Parameter.parseScaling( newValue );
161
+ transformTransformNode( rootNode, {
162
+ scaling: Parameter.parseVector( light.inheritedParameters[Parameter.SCALING] || '(1, 1, 1)' ),
163
+ position: Parameter.parseVector( light.inheritedParameters[Parameter.POSITION] || '(0, 0, 0)' ),
164
+ rotation: Parameter.parseRotation( newValue )
165
+ } );
162
166
  }
163
167
  }
164
168
  ] );
@@ -216,13 +216,24 @@ const disableNodeWithParents = function( node: Node ) {
216
216
  };
217
217
 
218
218
  /**
219
- * Attention: this function mutates the position of a node. Keep in mind that there are dependencies to other
220
- * functions moving nodes around.
219
+ * Applies a {@link TransformationDefinition} consecutively to ensure dependencies in positioning etc.
221
220
  * @param node
222
- * @param rotation
221
+ * @param transformation
223
222
  */
224
- const rotateTransformNode = function( node: TransformNode, rotation: Vector3 ): TransformNode {
225
- // remember absolute rotation and reset it before translating
223
+ const transformTransformNode = function( node: TransformNode, transformation: TransformationDefinition ) {
224
+ // scaling
225
+ if( !has( node.metadata, 'scaling.initial' ) ) {
226
+ injectNodeMetadata( node, { 'scaling.initial': node.scaling }, false );
227
+ }
228
+ const initialScaling = get( node.metadata, 'scaling.initial' ) as Vector3;
229
+ node.scaling = initialScaling.multiply( transformation.scaling );
230
+ // position
231
+ if( !has( node.metadata, 'position.initial' ) ) {
232
+ injectNodeMetadata( node, { 'position.initial': node.absolutePosition.clone() }, false );
233
+ }
234
+ const initialPosition = get( node.metadata, 'position.initial' ) as Vector3;
235
+ node.setAbsolutePosition( initialPosition.add( transformation.position ).multiply( transformation.scaling ) );
236
+ // rotation
226
237
  if( !has( node.metadata, 'rotation.initial' ) ) {
227
238
  let rotationQuaternion = node.rotationQuaternion;
228
239
  if( !rotationQuaternion ) {
@@ -230,43 +241,12 @@ const rotateTransformNode = function( node: TransformNode, rotation: Vector3 ):
230
241
  }
231
242
  injectNodeMetadata( node, { 'rotation.initial': rotationQuaternion.asArray() }, false );
232
243
  }
233
- if( !has( node.metadata, 'rotation.position' ) || get( node.metadata, 'position.dirty' ) ) {
234
- let rotationPosition = node.absolutePosition.clone();
235
- if( has( node.metadata, 'rotation.offset' ) ) {
236
- rotationPosition = rotationPosition.subtract( get( node.metadata, 'rotation.offset' ) as Vector3 );
237
- }
238
- injectNodeMetadata( node, { 'rotation.position': rotationPosition }, false );
239
- injectNodeMetadata( node, { 'position.dirty': false }, false );
240
- }
241
- node.setAbsolutePosition( get( node.metadata, 'rotation.position' ) );
242
- node.rotationQuaternion = Quaternion.FromArray( get( node.metadata, 'rotation.initial' ) as [] );
243
- node.rotateAround( Vector3.Zero(), Axis.X, rotation.x );
244
- node.rotateAround( Vector3.Zero(), Axis.Y, rotation.y );
245
- node.rotateAround( Vector3.Zero(), Axis.Z, rotation.z );
244
+ const initialRotationQuaternion = Quaternion.FromArray( get( node.metadata, 'rotation.initial' ) as [] );
245
+ node.rotationQuaternion = initialRotationQuaternion;
246
+ node.rotateAround( Vector3.Zero(), Axis.X, transformation.rotation.x );
247
+ node.rotateAround( Vector3.Zero(), Axis.Y, transformation.rotation.y );
248
+ node.rotateAround( Vector3.Zero(), Axis.Z, transformation.rotation.z );
246
249
  node.computeWorldMatrix( true );
247
- const rotationOffset = node.absolutePosition.subtract( get( node.metadata, 'rotation.position' ) as Vector3 );
248
- injectNodeMetadata( node, { 'rotation.offset': rotationOffset }, false );
249
- return node;
250
- };
251
-
252
- /**
253
- * Attention: this function mutates the position of a node. Keep in mind that there are dependencies to other
254
- * functions moving nodes around.
255
- * @param node
256
- * @param distance
257
- */
258
- const moveTransformNode = function( node: TransformNode, distance: Vector3 ): TransformNode {
259
- // remember absolute position and reset it before translating
260
- if( !has( node.metadata, 'position.initial' ) ) {
261
- injectNodeMetadata( node, { 'position.initial': node.absolutePosition.clone() }, false );
262
- }
263
- let position = get( node.metadata, 'position.initial' ) as Vector3;
264
- if( has( node.metadata, 'rotation.offset' ) ) {
265
- position = position.add( get( node.metadata, 'rotation.offset' ) as Vector3 );
266
- }
267
- node.setAbsolutePosition( position.add( distance ) );
268
- injectNodeMetadata( node, { 'position.dirty': true }, false );
269
- return node;
270
250
  };
271
251
 
272
252
  /**
@@ -538,8 +518,7 @@ export {
538
518
  deactivateTransformNode,
539
519
  enableNodeWithParents,
540
520
  disableNodeWithParents,
541
- rotateTransformNode,
542
- moveTransformNode,
521
+ transformTransformNode,
543
522
  setMaterial,
544
523
  setSourceNodeMaterial,
545
524
  setMaterialColor,
@@ -79,6 +79,12 @@ type ElementDefinition = {
79
79
  paintables?: PaintableDefinitions
80
80
  };
81
81
 
82
+ type TransformationDefinition = {
83
+ scaling: Vector3,
84
+ position: Vector3,
85
+ rotation: Vector3
86
+ };
87
+
82
88
  type StructureJson = {
83
89
  /**
84
90
  * `scene` describes the visualisation of the Babylon `scene` such as the incidence of light and camera position. If a
@@ -227,7 +233,13 @@ type AutofocusSettings = {
227
233
  radiusFactor?: number,
228
234
  adjustWheelPrecision?: boolean,
229
235
  adjustPanningSensibility?: boolean,
230
- adjustPinchPrecision?: boolean
236
+ adjustPinchPrecision?: boolean,
237
+ /** Desired horizontal camera angle, this won't be overwritten by the autofocus function */
238
+ alpha?: number;
239
+ /** Desired vertical camera angle, this won't be overwritten by the autofocus function */
240
+ beta?: number;
241
+ /** Optional animation for the focusing camera movement */
242
+ animation?: string | AnimationDefinition
231
243
  };
232
244
 
233
245
  type LightDefinitions = {
@@ -40,11 +40,15 @@
40
40
  },
41
41
  {
42
42
  "title": "ViewerLights",
43
- "source": "./doc/ViewerLights.md"
43
+ "source": "./doc/documentation/ViewerLights.md"
44
44
  },
45
45
  {
46
46
  "title": "Paintables",
47
47
  "source": "./doc/documentation/Paintables.md"
48
+ },
49
+ {
50
+ "title": "Transformation Parameters",
51
+ "source": "./doc/documentation/Transformation-Parameters.md"
48
52
  }
49
53
  ]
50
54
  },