@combeenation/3d-viewer 18.4.0-beta1 → 18.5.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.
@@ -380,7 +380,12 @@ export class CameraManager {
380
380
  // the idea is to create a dedicated canvas for these elements and merge the result with the main screenshot into
381
381
  // a combined canvas
382
382
  const screenshotHtmlCanvas = htmlAnchorKeys.length
383
- ? await this.viewer.htmlAnchorManager.createScreenshotCanvas(screenshotSize, screenshotCam, htmlAnchorKeys)
383
+ ? await this.viewer.htmlAnchorManager.createScreenshotCanvas(
384
+ screenshotSize,
385
+ screenshotCam,
386
+ htmlAnchorKeys,
387
+ excludeNodes
388
+ )
384
389
  : undefined;
385
390
 
386
391
  const screenshotCombinedCanvas = await createScreenshotCanvas(imageStr3d, screenshotSize, screenshotHtmlCanvas);
@@ -402,7 +407,7 @@ export class CameraManager {
402
407
  if (fileName) {
403
408
  // rebuild Babylon.js default behaviour: if a file name is given, download the screenshot instead of returning the
404
409
  // data string
405
- downloadFile(imageStr3d, fileName);
410
+ downloadFile(imageStr, fileName);
406
411
 
407
412
  return '';
408
413
  } else {
@@ -1,10 +1,10 @@
1
- import { Color3, LinesMesh, MeshBuilder, Tags, TransformNode, Vector3, Viewer } from '../index';
1
+ import { Color3, HtmlAnchorOptions, LinesMesh, MeshBuilder, Tags, TransformNode, Vector3, Viewer } from '../index';
2
2
  import { merge } from 'lodash-es';
3
3
 
4
4
  /**
5
5
  * Options for dimension line creation
6
6
  */
7
- export type DimensionLineOptions = {
7
+ export type DimensionLineOptions = Pick<HtmlAnchorOptions, 'occlusion' | 'scaling'> & {
8
8
  /**
9
9
  * Default: `Color3.Black()`
10
10
  */
@@ -27,20 +27,6 @@ export type DimensionLineOptions = {
27
27
  * Default: no parent node set
28
28
  */
29
29
  parentNode?: TransformNode;
30
- /**
31
- * `true`: html elements can be occluded by other meshes
32
- * `false`: html elements will always be shown in front of the scene
33
- *
34
- * Default: `false`
35
- */
36
- hideIfOccluded: boolean;
37
- /**
38
- * `true`: html element size is relative to camera distance, just like a "normal" 3d object
39
- * `false`: html element size remains constant
40
- *
41
- * Default: `false`
42
- */
43
- scaleWithCameraDistance: boolean;
44
30
  /**
45
31
  * Optional callback for label text creation.\
46
32
  * Provides calculated length from `startPoint` and `endPoint` as parameter in meters.
@@ -64,8 +50,6 @@ export class DimensionLineManager {
64
50
  protected _defaultLineOptions: DimensionLineOptions = {
65
51
  lineColor: Color3.Black(),
66
52
  closingLineHeight: 0.05,
67
- hideIfOccluded: false,
68
- scaleWithCameraDistance: false,
69
53
  labelTextCb: (lengthM): string => {
70
54
  const lengthRounded = Math.round(lengthM * 1000 * 100) / 100;
71
55
  return `${lengthRounded} mm`;
@@ -123,8 +107,8 @@ export class DimensionLineManager {
123
107
  closingLineHeight,
124
108
  closingLineDirection: closingLineDirectionIn,
125
109
  parentNode,
126
- hideIfOccluded,
127
- scaleWithCameraDistance,
110
+ occlusion,
111
+ scaling,
128
112
  labelTextCb,
129
113
  labelCssClass,
130
114
  } = resDefaultOptions;
@@ -151,7 +135,7 @@ export class DimensionLineManager {
151
135
  linesMesh.position = lineCenter;
152
136
  linesMesh.color = lineColor;
153
137
  // rendering group id 1 reserved for html anchor meshes, which are required for occlusion checking
154
- linesMesh.renderingGroupId = hideIfOccluded ? 0 : 2;
138
+ linesMesh.renderingGroupId = occlusion?.hideIfOccluded ? 0 : 2;
155
139
  // tag can be used to exclude dimension lines from autofocus or gltf export
156
140
  Tags.AddTagsTo(linesMesh, DimensionLineManager.DIMENSION_LINE_KEY);
157
141
  if (parentNode) {
@@ -185,8 +169,8 @@ export class DimensionLineManager {
185
169
  this.viewer.htmlAnchorManager.addHtmlAnchor(name, span, Vector3.Zero(), {
186
170
  parentNode: linesMesh,
187
171
  group: DimensionLineManager.DIMENSION_LINE_KEY,
188
- hideIfOccluded,
189
- scaleWithCameraDistance,
172
+ occlusion,
173
+ scaling,
190
174
  });
191
175
 
192
176
  this._dimensionLineObjs[name] = { linesMesh, htmlLabelName };
@@ -4,18 +4,68 @@ import {
4
4
  Camera,
5
5
  DimensionLineManager,
6
6
  MeshBuilder,
7
+ NodeDescription,
7
8
  Ray,
8
9
  ScreenshotSize,
9
10
  StandardMaterial,
10
- Tags,
11
11
  TransformNode,
12
12
  Vector3,
13
13
  Viewer,
14
14
  Viewport,
15
15
  } from '..';
16
16
  import { getInternalMetadataValue, setInternalMetadataValue } from '../internal/metadata-helper';
17
+ import { nodeMatchesAnyCriteria } from '../internal/node-helper';
17
18
  import html2canvas from 'html2canvas';
18
19
 
20
+ export type HtmlAnchorOcclusionOptions = {
21
+ /**
22
+ * `true`: html elements can be occluded by other meshes
23
+ * `false`: html elements will always be shown in front of the scene
24
+ *
25
+ * Default: `false`
26
+ */
27
+ hideIfOccluded: boolean;
28
+ /**
29
+ * Size of dummy mesh for occlusion check
30
+ * Smaller values result in a more precise occlusion check around the center of the anchor.
31
+ * Too small values can lead to flickering of the html element.
32
+ *
33
+ * Default: 0.01
34
+ */
35
+ anchorMeshSize?: number;
36
+ };
37
+
38
+ export type HtmlAnchorScalingOptions = {
39
+ /**
40
+ * `true`: html element size is relative to camera distance, just like a "normal" 3d object
41
+ * `false`: html element size remains constant and `referenceScale`, `minScale` and `maxScale` will have no effect
42
+ *
43
+ * Default: `false`
44
+ */
45
+ scaleWithCameraDistance: boolean;
46
+ /**
47
+ * Basis factor for converting camera distance in Babylon.js units to pixels.
48
+ * Find a suitable value for your project by trial and error, whereas larger values will make the html anchor elements
49
+ * larger as well.
50
+ *
51
+ * Default: 1
52
+ */
53
+ referenceScale?: number;
54
+ /**
55
+ * Can be used to limit the calculated scale value so that a html anchor element is still readable/clickable if the
56
+ * camera is far zoomed out.
57
+ *
58
+ * Default: undefined (no limit)
59
+ */
60
+ minScale?: number;
61
+ /**
62
+ * Max limit cap when zooming the camera in to avoid html anchor elements getting too large.
63
+ *
64
+ * Default: undefined (no limit)
65
+ */
66
+ maxScale?: number;
67
+ };
68
+
19
69
  export type HtmlAnchorOptions = {
20
70
  /**
21
71
  * Associated anchor mesh will be created underneath this parent node.\
@@ -27,19 +77,13 @@ export type HtmlAnchorOptions = {
27
77
  */
28
78
  group?: string;
29
79
  /**
30
- * `true`: html elements can be occluded by other meshes
31
- * `false`: html elements will always be shown in front of the scene
32
- *
33
- * Default: `false`
80
+ * Occlusion options, see {@link HtmlAnchorOcclusionOptions}
34
81
  */
35
- hideIfOccluded?: boolean;
82
+ occlusion?: HtmlAnchorOcclusionOptions;
36
83
  /**
37
- * `true`: html element size is relative to camera distance, just like a "normal" 3d object
38
- * `false`: html element size remains constant
39
- *
40
- * Default: `false`
84
+ * Scaling options, see {@link HtmlAnchorScalingOptions}
41
85
  */
42
- scaleWithCameraDistance?: boolean;
86
+ scaling?: HtmlAnchorScalingOptions;
43
87
  /**
44
88
  * Activates/deactivates `pointer-events` CSS class of anchor element.\
45
89
  * Set this to `true` if the html element should be interactive (e.g. button)
@@ -122,7 +166,7 @@ export class HtmlAnchorManager {
122
166
  return;
123
167
  }
124
168
 
125
- const { parentNode, enablePointerEvents } = options ?? {};
169
+ const { parentNode, occlusion, enablePointerEvents } = options ?? {};
126
170
 
127
171
  // create a parent for the input html element, which will receive the updated style and transform data
128
172
  // in this way the original input element remains untouched
@@ -141,7 +185,9 @@ export class HtmlAnchorManager {
141
185
 
142
186
  // NOTE: creates a sphere with fixed size, which could be problematic in scene with "strange" dimensions
143
187
  // add a property for the sphere size if required
144
- const anchorMesh = MeshBuilder.CreateSphere(`${HtmlAnchorManager._HTML_ANCHOR_KEY}_${name}`, { diameter: 0.01 });
188
+ const anchorMesh = MeshBuilder.CreateSphere(`${HtmlAnchorManager._HTML_ANCHOR_KEY}_${name}`, {
189
+ diameter: occlusion?.anchorMeshSize ?? 0.01,
190
+ });
145
191
  anchorMesh.position = position;
146
192
  anchorMesh.parent = parentNode ?? null;
147
193
  // anchor mesh will be invisible, we only need it for positioning and occlusion check
@@ -203,7 +249,8 @@ export class HtmlAnchorManager {
203
249
  public async createScreenshotCanvas(
204
250
  size: ScreenshotSize,
205
251
  camera: ArcRotateCamera,
206
- htmlAnchorKeys: string[]
252
+ htmlAnchorKeys: string[],
253
+ excludeNodes?: NodeDescription[]
207
254
  ): Promise<HTMLCanvasElement> {
208
255
  const canvasParentHtmlElement = this.viewer.canvas?.parentElement;
209
256
  if (!canvasParentHtmlElement) {
@@ -242,7 +289,8 @@ export class HtmlAnchorManager {
242
289
  size.canvasHeight,
243
290
  baseScale,
244
291
  true,
245
- options
292
+ options,
293
+ excludeNodes
246
294
  );
247
295
  });
248
296
 
@@ -291,9 +339,12 @@ export class HtmlAnchorManager {
291
339
  height: number,
292
340
  baseScale: number,
293
341
  useRayHitTestForOcclusionCheck: boolean,
294
- options?: HtmlAnchorOptions
342
+ options?: HtmlAnchorOptions,
343
+ excludeNodes?: NodeDescription[]
295
344
  ): void {
296
- const { hideIfOccluded, scaleWithCameraDistance } = options ?? {};
345
+ const { occlusion, scaling } = options ?? {};
346
+ const { hideIfOccluded } = occlusion ?? {};
347
+ const { scaleWithCameraDistance, referenceScale, minScale, maxScale } = scaling ?? {};
297
348
 
298
349
  const viewMatrix = camera.getViewMatrix();
299
350
  const projectionMatrix = camera.getProjectionMatrix();
@@ -307,20 +358,27 @@ export class HtmlAnchorManager {
307
358
  new Viewport(0, 0, width, height)
308
359
  );
309
360
 
361
+ const meshWorldPos = anchorMesh.getAbsolutePosition();
362
+ // calculate camera world position manually, as there is no help function like for meshes
363
+ const camWorldMatrix = camera.computeWorldMatrix();
364
+ const camWorldPos = camWorldMatrix.getTranslation();
365
+ const distance = Vector3.Distance(meshWorldPos, camWorldPos);
366
+ const camYWindow = distance * Math.tan(camera.fov / 2);
367
+
310
368
  // base scale is used if width and height don't equal the viewer canvas, which is the case for screenshots
311
369
  let scale = baseScale;
312
370
  if (scaleWithCameraDistance) {
313
- const meshWorldPos = anchorMesh.getAbsolutePosition();
314
- // calculate camera world position manually, as there is no help function like for meshes
315
- const camWorldMatrix = camera.computeWorldMatrix();
316
- const camWorldPos = camWorldMatrix.getTranslation();
317
- const distance = Vector3.Distance(meshWorldPos, camWorldPos);
318
- const frustumSlopeY = Math.tan(camera.fov / 2);
319
371
  // if the distance from camera to mesh gets larger, the html elements scaling will be decreased
320
372
  // we also consider the cameras FOV, so scale 1 means, that the resulting vertical frustum of the camera
321
373
  // distance equals 1
322
- // ...this is probably too small in some scenarios, so we might have to add a general scale setting here
323
- scale *= 1 / (distance * frustumSlopeY);
374
+ scale *= (referenceScale ?? 1) / camYWindow;
375
+ // apply scale caps
376
+ if (minScale !== undefined) {
377
+ scale = Math.max(scale, minScale);
378
+ }
379
+ if (maxScale !== undefined) {
380
+ scale = Math.min(scale, maxScale);
381
+ }
324
382
  }
325
383
 
326
384
  const elementXOffset = (parentHtmlElement.offsetWidth * scale) / 2;
@@ -350,7 +408,7 @@ export class HtmlAnchorManager {
350
408
  const ray = new Ray(camera.position, camDirection);
351
409
  const hit = this.viewer.scene.pickWithRay(
352
410
  ray,
353
- mesh => !Tags.MatchesQuery(mesh, DimensionLineManager.DIMENSION_LINE_KEY),
411
+ mesh => !nodeMatchesAnyCriteria(mesh, { isInList: excludeNodes, isDimensionLine: true }),
354
412
  false
355
413
  );
356
414
  isOccluded = hit?.pickedMesh !== anchorMesh;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  AbstractMesh,
3
- BaseTexture,
4
3
  Material,
5
4
  StandardMaterial,
6
5
  TagNamingStrategy,
6
+ TextureManager,
7
7
  Viewer,
8
8
  ViewerError,
9
9
  ViewerErrorIds,
@@ -188,9 +188,15 @@ export class MaterialManager {
188
188
 
189
189
  await this.viewer.parameterManager.applyParameterValuesToMaterial(material);
190
190
 
191
- const matReadyProms = [
192
- new Promise<void>(resolve => BaseTexture.WhenAllReady(material.getActiveTextures(), resolve)),
193
- ];
191
+ const texturesLoadProm = async (): Promise<void> => {
192
+ const errTextureName = await TextureManager.waitTexturesLoadedOrFailed(material.getActiveTextures());
193
+ if (errTextureName) {
194
+ console.warn(
195
+ `Couldn't load texture "${errTextureName}" of material "${material.id}", material still got created`
196
+ );
197
+ }
198
+ };
199
+ const matReadyProms = [texturesLoadProm()];
194
200
 
195
201
  if (mesh) {
196
202
  // this promise should only take some time on the first call of the corresponding shader (eg: PBRMaterial shader)
@@ -1,4 +1,4 @@
1
- import { BaseTexture, Viewer } from '../index';
1
+ import { BaseTexture, Texture, Viewer } from '../index';
2
2
 
3
3
  /**
4
4
  * Manager for texture related tasks, like renaming textures that are based on Combeenation image assets.\
@@ -7,6 +7,28 @@ import { BaseTexture, Viewer } from '../index';
7
7
  * @internal
8
8
  */
9
9
  export class TextureManager {
10
+ /**
11
+ * Basic textures loading check `BaseTexture.WhenAllReady` doesn't consider loading errors, which easily leads to a
12
+ * deadlock. (see CB-10769)
13
+ * This implementation listens for loading errors in the associated textures and aborts the promise accordingly.
14
+ *
15
+ * @returns (display) name of the texture that failed to load, or "" if every texture loaded successfully
16
+ */
17
+ public static async waitTexturesLoadedOrFailed(textures: BaseTexture[]): Promise<string> {
18
+ const texturesReadyProm = new Promise<string>(resolve => BaseTexture.WhenAllReady(textures, () => resolve('')));
19
+ const textureLoadErrorProm = new Promise<string>(resolve => {
20
+ const errorObserver = Texture.OnTextureLoadErrorObservable.add(errTexture => {
21
+ const activeTextureUniqueIds = textures.map(x => x.uniqueId);
22
+ if (activeTextureUniqueIds.includes(errTexture.uniqueId)) {
23
+ resolve(errTexture.displayName ?? errTexture.name);
24
+ errorObserver.remove();
25
+ }
26
+ });
27
+ });
28
+
29
+ return Promise.race([texturesReadyProm, textureLoadErrorProm]);
30
+ }
31
+
10
32
  public constructor(protected viewer: Viewer) {
11
33
  this.viewer.scene.onNewTextureAddedObservable.add(texture => this._onTextureAdded(texture));
12
34
  }