@combeenation/3d-viewer 20.0.0-alpha2 → 20.0.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.
@@ -24,17 +24,47 @@ export type CameraPosition = {
24
24
  };
25
25
 
26
26
  export type AutofocusSettings = {
27
- /** Can be used to customize the margins shown around the 3d model. Defaults to 1. */
27
+ /**
28
+ * Can be used to customize the margins shown around the 3d model.\
29
+ * Default: 1
30
+ */
28
31
  radiusFactor?: number;
32
+ /** Default: true */
33
+ adjustLowerRadiusLimit?: boolean;
34
+ /** Default: true */
35
+ adjustMinZ?: boolean;
36
+ /** Default: true */
29
37
  adjustWheelPrecision?: boolean;
38
+ /** Default: true */
30
39
  adjustPanningSensibility?: boolean;
40
+ /** Default: true */
31
41
  adjustPinchPrecision?: boolean;
42
+ /**
43
+ * `true:` uses `alpha` from input or default value (45°), `false:` keeps current alpha from camera\
44
+ * Default: true
45
+ */
46
+ adjustAlpha?: boolean;
47
+ /**
48
+ * `true:` uses `beta` from input or default value (75°), `false:` keeps current beta from camera\
49
+ * Default: true
50
+ */
51
+ adjustBeta?: boolean;
52
+ /**
53
+ * `true:` uses calculated radius to fit scene bounding sphere, `false:` keeps current radius from camera\
54
+ * Default: true
55
+ */
56
+ adjustRadius?: boolean;
32
57
  /** Desired horizontal camera angle, this won't be overwritten by the autofocus function */
33
58
  alpha?: number;
34
59
  /** Desired vertical camera angle, this won't be overwritten by the autofocus function */
35
60
  beta?: number;
36
61
  /** Optional list of nodes to be excluded from camera distance calculation for autofocusing */
37
62
  excludeNodes?: NodeDescription[];
63
+ /**
64
+ * Defines if HTML anchors should be considered for the autofocus camera distance calculation.\
65
+ * Default: `true`
66
+ */
67
+ excludeHtmlAnchors?: boolean;
38
68
  durationMs?: number;
39
69
  };
40
70
 
@@ -152,6 +182,20 @@ export class CameraManager {
152
182
  * Focuses the camera to see every visible mesh in scene and tries to optimize wheel precision and panning
153
183
  */
154
184
  public async autofocusActiveCamera(settings?: AutofocusSettings): Promise<void> {
185
+ const {
186
+ adjustLowerRadiusLimit,
187
+ adjustMinZ,
188
+ adjustWheelPrecision,
189
+ adjustPanningSensibility,
190
+ adjustPinchPrecision,
191
+ adjustAlpha,
192
+ adjustBeta,
193
+ adjustRadius,
194
+ excludeNodes,
195
+ excludeHtmlAnchors,
196
+ durationMs,
197
+ } = settings ?? {};
198
+
155
199
  const activeCamera = this.viewer.scene.activeCamera;
156
200
 
157
201
  if (!(activeCamera instanceof ArcRotateCamera)) {
@@ -162,42 +206,49 @@ export class CameraManager {
162
206
  }
163
207
 
164
208
  // get bounding box of all visible meshes, this is the base for the autofocus algorithm
165
- const boundingInfo = this.viewer.sceneManager.calculateBoundingInfo(settings?.excludeNodes);
209
+ const boundingInfo = this.viewer.sceneManager.calculateBoundingInfo({ excludeNodes, excludeHtmlAnchors });
166
210
  // optionally show bounding sphere for debugging purpose
167
211
  this.viewer.eventManager.fireEvent(ViewerEvent.AutofocusStart, boundingInfo.boundingSphere);
168
212
 
169
213
  const distance = this._getAutofocusZoomingDistance(boundingInfo.boundingSphere, activeCamera);
170
- const radius = boundingInfo.boundingSphere.radius;
171
- const center = boundingInfo.boundingSphere.center;
214
+ const bBoxRadius = boundingInfo.boundingSphere.radius;
215
+ const bBoxCenter = boundingInfo.boundingSphere.center;
172
216
 
173
217
  // set lower radius limit on edge of bounding sphere to make sure that we can't dive into the meshes
174
- activeCamera.lowerRadiusLimit = radius;
218
+ if (adjustLowerRadiusLimit !== false) {
219
+ activeCamera.lowerRadiusLimit = bBoxRadius;
220
+ }
175
221
 
176
222
  // additional settings
177
223
  // constants for division are taken directly from Babylon.js repository
178
- activeCamera.minZ = Math.min(radius / CameraManager._AUTOFOCUS_CONSTANTS.minZ, 1);
179
- if (settings?.adjustWheelPrecision !== false) {
180
- activeCamera.wheelPrecision = CameraManager._AUTOFOCUS_CONSTANTS.wheelPrecision / radius;
224
+ if (adjustMinZ !== false) {
225
+ activeCamera.minZ = Math.min(bBoxRadius / CameraManager._AUTOFOCUS_CONSTANTS.minZ, 1);
181
226
  }
182
- if (settings?.adjustPanningSensibility !== false) {
183
- activeCamera.panningSensibility = CameraManager._AUTOFOCUS_CONSTANTS.panningSensibility / radius;
227
+ if (adjustWheelPrecision !== false) {
228
+ activeCamera.wheelPrecision = CameraManager._AUTOFOCUS_CONSTANTS.wheelPrecision / bBoxRadius;
184
229
  }
185
- if (settings?.adjustPinchPrecision !== false) {
186
- activeCamera.pinchPrecision = CameraManager._AUTOFOCUS_CONSTANTS.pinchPrecision / radius;
230
+ if (adjustPanningSensibility !== false) {
231
+ activeCamera.panningSensibility = CameraManager._AUTOFOCUS_CONSTANTS.panningSensibility / bBoxRadius;
232
+ }
233
+ if (adjustPinchPrecision !== false) {
234
+ activeCamera.pinchPrecision = CameraManager._AUTOFOCUS_CONSTANTS.pinchPrecision / bBoxRadius;
187
235
  }
188
236
 
189
- const radiusFactor = settings?.radiusFactor ?? CameraManager._DEFAULT_AUTOFOCUS_RADIUS_FACTOR;
190
- const alpha = settings?.alpha ?? CameraManager.DEFAULT_CAMERA_POSITION.alpha;
191
- const beta = settings?.beta ?? CameraManager.DEFAULT_CAMERA_POSITION.beta;
237
+ const adjustedAlpha = settings?.alpha ?? CameraManager.DEFAULT_CAMERA_POSITION.alpha;
238
+ const alpha = adjustAlpha !== false ? adjustedAlpha : activeCamera.alpha;
239
+ const adjustedBeta = settings?.beta ?? CameraManager.DEFAULT_CAMERA_POSITION.beta;
240
+ const beta = adjustBeta !== false ? adjustedBeta : activeCamera.beta;
241
+ const adjustedRadiusFactor = settings?.radiusFactor ?? CameraManager._DEFAULT_AUTOFOCUS_RADIUS_FACTOR;
242
+ const radius = adjustRadius !== false ? distance * adjustedRadiusFactor : activeCamera.radius;
192
243
 
193
244
  const newCameraPosition: CameraPosition = {
194
- alpha: alpha,
195
- beta: beta,
196
- radius: distance * radiusFactor,
197
- target: center,
245
+ alpha,
246
+ beta,
247
+ radius,
248
+ target: bBoxCenter,
198
249
  };
199
250
 
200
- await this.moveActiveCameraTo(newCameraPosition, settings?.durationMs);
251
+ await this.moveActiveCameraTo(newCameraPosition, durationMs);
201
252
  }
202
253
 
203
254
  /**
@@ -314,7 +365,12 @@ export class CameraManager {
314
365
  const screenshotCam = this.viewer.scene.activeCamera?.clone(
315
366
  CameraManager._SCREENSHOT_CAMERA_NAME
316
367
  ) as ArcRotateCamera;
317
- const boundingInfo = autofocusScene ? this.viewer.sceneManager.calculateBoundingInfo(excludeNodes) : undefined;
368
+ const boundingInfo = autofocusScene
369
+ ? this.viewer.sceneManager.calculateBoundingInfo({
370
+ excludeNodes,
371
+ excludeHtmlAnchors: excludeHtmlAnchorGroups === true,
372
+ })
373
+ : undefined;
318
374
 
319
375
  if (alpha !== undefined) {
320
376
  screenshotCam.alpha = alpha;
@@ -83,6 +83,16 @@ export type UpdateShadowsSettings = {
83
83
  excludeReceiverNodes?: NodeDescription[];
84
84
  };
85
85
 
86
+ export type CalculateBoundingInfoSettings = {
87
+ /** Optional list of nodes to be excluded from bounding info calculation */
88
+ excludeNodes?: NodeDescription[];
89
+ /**
90
+ * Defines if HTML anchors should be considered for the bounding info calculation.\
91
+ * Default: `true`
92
+ */
93
+ excludeHtmlAnchors?: boolean;
94
+ };
95
+
86
96
  /**
87
97
  * Additional scene settings, which are not available in a basic `AssetContainer`.
88
98
  * !!! IMPORTANT when adding a new property !!!
@@ -282,7 +292,9 @@ export class SceneManager {
282
292
  * - do not have a material of type "BackgroundMaterial"
283
293
  * - do not have an infinite distance (like sky boxes)
284
294
  */
285
- public calculateBoundingInfo(excludeNodes?: NodeDescription[]): BoundingInfo {
295
+ public calculateBoundingInfo(settings?: CalculateBoundingInfoSettings): BoundingInfo {
296
+ const { excludeNodes, excludeHtmlAnchors } = settings ?? {};
297
+
286
298
  // CB-6062: workaround for BoundingBox not respecting render loop
287
299
  this.viewer.scene.render();
288
300
 
@@ -295,7 +307,7 @@ export class SceneManager {
295
307
  hasInvalidBoundingInfo: true,
296
308
  hasInfiniteDistance: true,
297
309
  isGeneratedBackgroundMesh: true,
298
- isHtmlAnchorMesh: true,
310
+ isHtmlAnchorMesh: excludeHtmlAnchors !== false,
299
311
  });
300
312
 
301
313
  return !isExcluded;
@@ -323,7 +335,7 @@ export class SceneManager {
323
335
  * plane mesh can be used as well.
324
336
  */
325
337
  public updateGround(settings?: UpdateGroundSettings): void {
326
- const bboxInfo = this.calculateBoundingInfo(settings?.excludeNodes);
338
+ const bboxInfo = this.calculateBoundingInfo({ excludeNodes: settings?.excludeNodes });
327
339
 
328
340
  const groundMesh = settings?.groundMesh ?? this.viewer.scene.getMeshByName(SceneManager._DEFAULT_GROUND_PLANE_NAME);
329
341
  if (!groundMesh) {
@@ -4,6 +4,8 @@ import {
4
4
  Mesh,
5
5
  MorphTarget,
6
6
  MorphTargetManager,
7
+ Node,
8
+ Tags,
7
9
  TransformNode,
8
10
  Vector3,
9
11
  VertexBuffer,
@@ -13,6 +15,7 @@ import {
13
15
  export type BakeGeometrySettings = {
14
16
  keepTransformation?: boolean;
15
17
  };
18
+
16
19
  export type CreateTransformNodeSettings = {
17
20
  parentNode?: TransformNode;
18
21
  position?: Vector3;
@@ -20,6 +23,34 @@ export type CreateTransformNodeSettings = {
20
23
  scaling?: Vector3;
21
24
  };
22
25
 
26
+ /**
27
+ * Expected metadata structure for find node functions {@link ViewerUtils.findChildNodes} and
28
+ * {@link ViewerUtils.findParentNode}
29
+ */
30
+ export type MetadataObject = Record<string, unknown>;
31
+
32
+ /**
33
+ * Contains all constructor that derive from `Node`, e.g. `TransformNode`, `Mesh`
34
+ */
35
+ export type DerivedNodeType<T extends Node> = new (...args: any[]) => T;
36
+
37
+ /**
38
+ * Criteria object for find node functions {@link ViewerUtils.findChildNodes} and {@link ViewerUtils.findParentNode}.\
39
+ * All given properties must match for the criteria to pass.
40
+ *
41
+ * @param nodeName has to match exactly
42
+ * @param tagName uses algorithm of `Tags.MatchesQuery`, in most cases this is just an exact string match check
43
+ * @param metadataObject has to be an object, filter passes if all property values of the given input object match the
44
+ * checked node
45
+ * @param type nodes type has to match (e.g. Mesh)
46
+ */
47
+ export type FindNodeCriteria<T extends Node> = {
48
+ nodeName?: string;
49
+ tagName?: string;
50
+ metadataObject?: MetadataObject;
51
+ type?: DerivedNodeType<T>;
52
+ };
53
+
23
54
  export class ViewerUtils {
24
55
  /** @internal */
25
56
  public constructor(protected viewer: Viewer) {}
@@ -63,10 +94,10 @@ export class ViewerUtils {
63
94
  if (morphTargetManager?.numTargets) {
64
95
  // apply morph target vertices data to mesh geometry
65
96
  // mostly only the "PositionKind" is implemented
66
- this._bakeMorphTargetManagerIntoVertices(VertexBuffer.PositionKind, morphTargetManager, geometry);
67
- this._bakeMorphTargetManagerIntoVertices(VertexBuffer.NormalKind, morphTargetManager, geometry);
68
- this._bakeMorphTargetManagerIntoVertices(VertexBuffer.TangentKind, morphTargetManager, geometry);
69
- this._bakeMorphTargetManagerIntoVertices(VertexBuffer.UVKind, morphTargetManager, geometry);
97
+ _bakeMorphTargetManagerIntoVertices(VertexBuffer.PositionKind, morphTargetManager, geometry);
98
+ _bakeMorphTargetManagerIntoVertices(VertexBuffer.NormalKind, morphTargetManager, geometry);
99
+ _bakeMorphTargetManagerIntoVertices(VertexBuffer.TangentKind, morphTargetManager, geometry);
100
+ _bakeMorphTargetManagerIntoVertices(VertexBuffer.UVKind, morphTargetManager, geometry);
70
101
 
71
102
  // remove morph target manager with all it's morph targets
72
103
  mesh.morphTargetManager = null;
@@ -100,50 +131,103 @@ export class ViewerUtils {
100
131
  }
101
132
 
102
133
  /**
103
- * @param kind morph targets can affect various vertices kinds, whereas "position" is the most common one
104
- * still other kinds (like normals or tangents) can be affected as well and can be provided on this input
134
+ * Finds all child nodes (TransformNodes, Meshes, but also Lights, Cameras, etc...) that match a given criteria.
135
+ *
136
+ * @param node If left empty, all nodes of the scene are considered
137
+ * @param criteria If left empty, all child nodes are returned
105
138
  */
106
- protected _bakeMorphTargetManagerIntoVertices(
107
- kind: string,
108
- morphTargetManager: MorphTargetManager,
109
- geometry: Geometry
110
- ): void {
111
- const origVerticesData = geometry.getVerticesData(kind);
112
- if (!origVerticesData) {
113
- // no vertices data for this kind availabe on the mesh geometry
114
- return;
139
+ public findChildNodes<T extends Node>(node?: Node, criteria?: FindNodeCriteria<T>): T[] {
140
+ const allNodes = node ? node.getChildren(undefined, false) : [...this.viewer.scene.getNodes()];
141
+
142
+ if (!criteria) return allNodes as T[];
143
+
144
+ const foundNodes = allNodes.filter(node => _matchesFindNodeCriteria(node, criteria));
145
+ return foundNodes as T[];
146
+ }
147
+
148
+ /**
149
+ * Finds nearest node that matches a given criteria.\
150
+ * Returns `undefined` if no node could be found.
151
+ */
152
+ public findParentNode<T extends Node>(node: Node, criteria?: FindNodeCriteria<T>): T | undefined {
153
+ const parent = node.parent;
154
+ if (!parent) {
155
+ return undefined;
156
+ } else if (!criteria) {
157
+ return parent as T;
115
158
  }
116
159
 
117
- let verticesData = [...origVerticesData];
118
- for (let i = 0; i < morphTargetManager.numTargets; i++) {
119
- const target = morphTargetManager.getTarget(i);
120
- const targetVerticesData = this._getVerticesDataFromMorphTarget(kind, target);
121
- if (targetVerticesData) {
122
- // vertices data of this kind are implemented on the morph target
123
- // add the influence of this morph target to the vertices data
124
- // formula is taken from: https://doc.babylonjs.com/features/featuresDeepDive/mesh/morphTargets#basics
125
- verticesData = verticesData.map(
126
- (oldVal, idx) => oldVal + (targetVerticesData[idx] - origVerticesData[idx]) * target.influence
127
- );
128
- }
160
+ const matchesCriteria = _matchesFindNodeCriteria(parent, criteria);
161
+ if (matchesCriteria) {
162
+ return parent as T;
163
+ } else {
164
+ return this.findParentNode(parent, criteria);
129
165
  }
166
+ }
167
+ }
130
168
 
131
- // apply the updated vertices data
132
- geometry.setVerticesData(kind, verticesData);
169
+ /**
170
+ * @param kind morph targets can affect various vertices kinds, whereas "position" is the most common one
171
+ * still other kinds (like normals or tangents) can be affected as well and can be provided on this input
172
+ */
173
+ function _bakeMorphTargetManagerIntoVertices(
174
+ kind: string,
175
+ morphTargetManager: MorphTargetManager,
176
+ geometry: Geometry
177
+ ): void {
178
+ const origVerticesData = geometry.getVerticesData(kind);
179
+ if (!origVerticesData) {
180
+ // no vertices data for this kind availabe on the mesh geometry
181
+ return;
133
182
  }
134
183
 
135
- protected _getVerticesDataFromMorphTarget(kind: string, morphTarget: MorphTarget): FloatArray | null {
136
- switch (kind) {
137
- case VertexBuffer.PositionKind:
138
- return morphTarget.getPositions();
139
- case VertexBuffer.NormalKind:
140
- return morphTarget.getNormals();
141
- case VertexBuffer.TangentKind:
142
- return morphTarget.getTangents();
143
- case VertexBuffer.UVKind:
144
- return morphTarget.getUVs();
184
+ let verticesData = [...origVerticesData];
185
+ for (let i = 0; i < morphTargetManager.numTargets; i++) {
186
+ const target = morphTargetManager.getTarget(i);
187
+ const targetVerticesData = _getVerticesDataFromMorphTarget(kind, target);
188
+ if (targetVerticesData) {
189
+ // vertices data of this kind are implemented on the morph target
190
+ // add the influence of this morph target to the vertices data
191
+ // formula is taken from: https://doc.babylonjs.com/features/featuresDeepDive/mesh/morphTargets#basics
192
+ verticesData = verticesData.map(
193
+ (oldVal, idx) => oldVal + (targetVerticesData[idx] - origVerticesData[idx]) * target.influence
194
+ );
145
195
  }
196
+ }
197
+
198
+ // apply the updated vertices data
199
+ geometry.setVerticesData(kind, verticesData);
200
+ }
146
201
 
147
- return null;
202
+ function _getVerticesDataFromMorphTarget(kind: string, morphTarget: MorphTarget): FloatArray | null {
203
+ switch (kind) {
204
+ case VertexBuffer.PositionKind:
205
+ return morphTarget.getPositions();
206
+ case VertexBuffer.NormalKind:
207
+ return morphTarget.getNormals();
208
+ case VertexBuffer.TangentKind:
209
+ return morphTarget.getTangents();
210
+ case VertexBuffer.UVKind:
211
+ return morphTarget.getUVs();
148
212
  }
213
+
214
+ return null;
215
+ }
216
+
217
+ function _matchesFindNodeCriteria<T extends Node>(node: Node, criteria?: FindNodeCriteria<T>): boolean {
218
+ if (!criteria) return true;
219
+
220
+ const { nodeName, tagName, metadataObject, type } = criteria;
221
+ const nodeNameFits = !nodeName || node.name === nodeName;
222
+ const tagNameFits = !tagName || Tags.MatchesQuery(node, tagName);
223
+ const metadataFits = !metadataObject || _matchesMetadata(node.metadata, metadataObject);
224
+ const typeFits = !type || node instanceof type;
225
+
226
+ return nodeNameFits && tagNameFits && metadataFits && typeFits;
227
+ }
228
+
229
+ function _matchesMetadata(metadata: unknown, filter: MetadataObject): boolean {
230
+ if (!metadata || typeof metadata !== 'object') return false;
231
+
232
+ return Object.entries(filter).every(([key, value]) => (metadata as MetadataObject)[key] === value);
149
233
  }