@combeenation/3d-viewer 18.0.0 → 18.1.0-beta2

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.
@@ -12,6 +12,7 @@ import {
12
12
  ViewerEvent,
13
13
  } from '../index';
14
14
  import { isNodeExcluded } from '../internal/geometry-helper';
15
+ import { createScreenshotCanvas, trimCanvas } from '../internal/screenshot-helper';
15
16
 
16
17
  export type CameraPosition = {
17
18
  alpha: number;
@@ -68,6 +69,11 @@ export type ScreenshotSettings = {
68
69
  * Excludes ALL html anchor groups if set to `true`.
69
70
  */
70
71
  excludeHtmlAnchorGroups?: string[] | true;
72
+ /**
73
+ * Removes all bounding transparent pixels, so that final image size exactly fits the scene content.\
74
+ * As a result the final image size might be smaller than the values defined on the `width` and `height` input.
75
+ */
76
+ cropTransparentArea?: boolean;
71
77
  /**
72
78
  * "MIME type" of the returned screenshot image, defaults to `image/png`
73
79
  *
@@ -79,7 +85,10 @@ export type ScreenshotSettings = {
79
85
  mimeType?: string;
80
86
  /** If file name is given, the screenshot image will be downloaded and the base64 string will NOT be returned! */
81
87
  fileName?: string;
82
- /** Expert settings to tweak the screenshot image, see [Babylon.js](https://doc.babylonjs.com/typedoc/functions/BABYLON.CreateScreenshotUsingRenderTarget) docs for further information */
88
+ /**
89
+ * Expert settings to tweak the screenshot image, see [Babylon.js](https://doc.babylonjs.com/typedoc/functions/BABYLON.CreateScreenshotUsingRenderTarget) docs for further information.\
90
+ * Defaults to `8` for improved screenshot quality.
91
+ */
83
92
  samples?: number;
84
93
  /** Expert settings to tweak the screenshot image, see [Babylon.js](https://doc.babylonjs.com/typedoc/functions/BABYLON.CreateScreenshotUsingRenderTarget) docs for further information */
85
94
  antialiasing?: boolean;
@@ -299,6 +308,7 @@ export class CameraManager {
299
308
  autofocusScene,
300
309
  exclude,
301
310
  excludeHtmlAnchorGroups,
311
+ cropTransparentArea,
302
312
  fileName,
303
313
  samples,
304
314
  antialiasing,
@@ -344,7 +354,8 @@ export class CameraManager {
344
354
  screenshotCam,
345
355
  { width: screenshotSize.imageWidth, height: screenshotSize.imageHeight },
346
356
  mimeType,
347
- samples,
357
+ // default to "8", since Babylon.js default "1" is really ugly
358
+ samples ?? 8,
348
359
  antialiasing,
349
360
  undefined,
350
361
  renderSprites,
@@ -365,32 +376,22 @@ export class CameraManager {
365
376
  excludeHtmlAnchorGroups === true
366
377
  ? []
367
378
  : this.viewer.htmlAnchorManager.getHtmlAnchorKeys(undefined, excludeHtmlAnchorGroups, true);
368
- if (htmlAnchorKeys.length) {
379
+ if (htmlAnchorKeys.length || cropTransparentArea) {
369
380
  // html anchors are not included in the main screenshot, as the html elements are located outside of the canvas
370
381
  // the idea is to create a dedicated canvas for these elements and merge the result with the main screenshot into
371
382
  // a combined canvas
372
- const screenshotHtmlCanvas = await this.viewer.htmlAnchorManager.createScreenshotCanvas(
373
- screenshotSize,
374
- screenshotCam,
375
- htmlAnchorKeys
376
- );
377
-
378
- // convert the main screenshot into an image, so that it can be drawn onto a canvas as well
379
- const screenshot3dImg = new Image();
380
- screenshot3dImg.src = imageStr3d;
381
- await new Promise(resolve => {
382
- screenshot3dImg.onload = resolve;
383
- });
384
-
385
- const screenshotCombinedCanvas = document.createElement('canvas');
386
- screenshotCombinedCanvas.width = screenshotSize.imageWidth;
387
- screenshotCombinedCanvas.height = screenshotSize.imageHeight;
388
-
389
- // draw main and html screenshot on a new canvas and get the base64 string from it
390
- const context = screenshotCombinedCanvas.getContext('2d')!;
391
- context.drawImage(screenshot3dImg, 0, 0, screenshotSize.imageWidth, screenshotSize.imageHeight);
392
- context.drawImage(screenshotHtmlCanvas, 0, 0, screenshotSize.imageWidth, screenshotSize.imageHeight);
393
- imageStr = screenshotCombinedCanvas.toDataURL(mimeType);
383
+ const screenshotHtmlCanvas = htmlAnchorKeys.length
384
+ ? await this.viewer.htmlAnchorManager.createScreenshotCanvas(screenshotSize, screenshotCam, htmlAnchorKeys)
385
+ : undefined;
386
+
387
+ const screenshotCombinedCanvas = await createScreenshotCanvas(imageStr3d, screenshotSize, screenshotHtmlCanvas);
388
+ if (cropTransparentArea) {
389
+ const screenshotTrimmedCanvas = trimCanvas(screenshotCombinedCanvas);
390
+ imageStr = screenshotTrimmedCanvas.toDataURL(mimeType);
391
+ screenshotTrimmedCanvas.remove();
392
+ } else {
393
+ imageStr = screenshotCombinedCanvas.toDataURL(mimeType);
394
+ }
394
395
 
395
396
  screenshotCombinedCanvas.remove();
396
397
  } else {
@@ -4,8 +4,10 @@ import {
4
4
  Camera,
5
5
  DimensionLineManager,
6
6
  MeshBuilder,
7
+ Ray,
7
8
  ScreenshotSize,
8
9
  StandardMaterial,
10
+ Tags,
9
11
  TransformNode,
10
12
  Vector3,
11
13
  Viewer,
@@ -82,9 +84,13 @@ export class HtmlAnchorManager {
82
84
  parentHtmlElement,
83
85
  anchorMesh,
84
86
  viewer.scene.activeCamera!,
85
- canvas.width,
86
- canvas.height,
87
+ // it's import to use `clientWidth` and `clientHeight` as it makes a difference on mobile devices that have
88
+ // hardware scaling activated
89
+ // => `width`/`height` return the upscaled values, but the basis html element for the anchors remains unscaled
90
+ canvas.clientWidth,
91
+ canvas.clientHeight,
87
92
  1,
93
+ false,
88
94
  options
89
95
  );
90
96
  });
@@ -97,9 +103,7 @@ export class HtmlAnchorManager {
97
103
  * mesh within the scene.
98
104
  */
99
105
  public addHtmlAnchor(name: string, htmlElement: HTMLElement, position: Vector3, options?: HtmlAnchorOptions): void {
100
- const canvasParentHtmlElement = this.viewer.canvas?.parentElement;
101
- if (!canvasParentHtmlElement) {
102
- console.warn(`No parent for desired html anchor available`);
106
+ if (!this.viewer.canvas) {
103
107
  return;
104
108
  }
105
109
 
@@ -120,7 +124,10 @@ export class HtmlAnchorManager {
120
124
  if (!enablePointerEvents) {
121
125
  parentHtmlElement.style.pointerEvents = 'none';
122
126
  }
123
- canvasParentHtmlElement.appendChild(parentHtmlElement);
127
+ // it's important to insert the html anchor elements right after the viewer canvas
128
+ // otherwise the elements could be in front of other sibling nodes, like the viewer control loading mask
129
+ // => see CB-10496
130
+ this.viewer.canvas.insertAdjacentElement('afterend', parentHtmlElement);
124
131
 
125
132
  // NOTE: creates a sphere with fixed size, which could be problematic in scene with "strange" dimensions
126
133
  // add a property for the sphere size if required
@@ -206,7 +213,10 @@ export class HtmlAnchorManager {
206
213
  htmlContainer.append(clonedHtmlNode);
207
214
 
208
215
  // reposition that cloned html element, so that it fits to the desired camera position
209
- const baseScale = size.canvasHeight / this.viewer.canvas!.height;
216
+ // `clientHeight`: makes difference on mobile devices that have hardware scaling activated
217
+ // => `height` returns the upscaled value, but the basis html element for the anchors remains
218
+ // unscaled
219
+ const baseScale = size.canvasHeight / this.viewer.canvas!.clientHeight;
210
220
  this._updateHtmlAnchor(
211
221
  clonedHtmlNode,
212
222
  anchorMesh,
@@ -214,6 +224,7 @@ export class HtmlAnchorManager {
214
224
  size.canvasWidth,
215
225
  size.canvasHeight,
216
226
  baseScale,
227
+ true,
217
228
  options
218
229
  );
219
230
  });
@@ -271,6 +282,7 @@ export class HtmlAnchorManager {
271
282
  width: number,
272
283
  height: number,
273
284
  baseScale: number,
285
+ useRayHitTestForOcclusionCheck: boolean,
274
286
  options?: HtmlAnchorOptions
275
287
  ): void {
276
288
  const { hideIfOccluded, scaleWithCameraDistance } = options ?? {};
@@ -315,7 +327,31 @@ export class HtmlAnchorManager {
315
327
  vertexScreenCoords.x + elementXOffset < width &&
316
328
  vertexScreenCoords.y - elementYOffset > 0 &&
317
329
  vertexScreenCoords.y + elementYOffset < height;
318
- const isOccluded = hideIfOccluded && anchorMesh.isOccluded;
330
+
331
+ let isOccluded = false;
332
+ if (hideIfOccluded) {
333
+ if (useRayHitTestForOcclusionCheck) {
334
+ // ray test has to be used for synchronous occlusion checks
335
+ // this is the case for screenshots, where the screenshot camera might be on a different position than the scene
336
+ // camera
337
+ // default occlusion query is too slow and there is no way to know when the occlusion check is finished, so we
338
+ // fall back to this alternative technology
339
+ // however, this tech has higher performance impact, so we don't use it for the cyclic occlusion check
340
+ const meshCenter = anchorMesh.getBoundingInfo().boundingBox.centerWorld;
341
+ const camDirection = meshCenter.subtract(camera.position);
342
+ const ray = new Ray(camera.position, camDirection);
343
+ const hit = this.viewer.scene.pickWithRay(
344
+ ray,
345
+ mesh => !Tags.MatchesQuery(mesh, DimensionLineManager.DIMENSION_LINE_KEY),
346
+ false
347
+ );
348
+ isOccluded = hit?.pickedMesh !== anchorMesh;
349
+ } else {
350
+ // use default occlusion check from GPU
351
+ isOccluded = anchorMesh.isOccluded;
352
+ }
353
+ }
354
+
319
355
  parentHtmlElement.style.opacity = isInViewport && !isOccluded ? '1' : '0';
320
356
  }
321
357