@combeenation/3d-viewer 17.1.0 → 18.0.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.
Files changed (58) hide show
  1. package/dist/lib-cjs/buildinfo.json +1 -1
  2. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  3. package/dist/lib-cjs/index.d.ts +8 -0
  4. package/dist/lib-cjs/index.js +8 -0
  5. package/dist/lib-cjs/index.js.map +1 -1
  6. package/dist/lib-cjs/internal/asset-helper.d.ts +32 -0
  7. package/dist/lib-cjs/internal/asset-helper.js +105 -0
  8. package/dist/lib-cjs/internal/asset-helper.js.map +1 -0
  9. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.d.ts +18 -0
  10. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js +22 -3
  11. package/dist/lib-cjs/internal/cbn-custom-babylon-loader-plugin.js.map +1 -1
  12. package/dist/lib-cjs/internal/cloning-helper.js +1 -1
  13. package/dist/lib-cjs/internal/cloning-helper.js.map +1 -1
  14. package/dist/lib-cjs/internal/texture-parameter-helper.js +26 -7
  15. package/dist/lib-cjs/internal/texture-parameter-helper.js.map +1 -1
  16. package/dist/lib-cjs/manager/camera-manager.d.ts +23 -2
  17. package/dist/lib-cjs/manager/camera-manager.js +91 -24
  18. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  19. package/dist/lib-cjs/manager/debug-manager.d.ts +1 -1
  20. package/dist/lib-cjs/manager/debug-manager.js +3 -3
  21. package/dist/lib-cjs/manager/debug-manager.js.map +1 -1
  22. package/dist/lib-cjs/manager/dimension-line-manager.d.ts +126 -0
  23. package/dist/lib-cjs/manager/dimension-line-manager.js +138 -0
  24. package/dist/lib-cjs/manager/dimension-line-manager.js.map +1 -0
  25. package/dist/lib-cjs/manager/html-anchor-manager.d.ts +93 -0
  26. package/dist/lib-cjs/manager/html-anchor-manager.js +228 -0
  27. package/dist/lib-cjs/manager/html-anchor-manager.js.map +1 -0
  28. package/dist/lib-cjs/manager/model-manager.d.ts +11 -34
  29. package/dist/lib-cjs/manager/model-manager.js +47 -107
  30. package/dist/lib-cjs/manager/model-manager.js.map +1 -1
  31. package/dist/lib-cjs/manager/parameter-manager.d.ts +17 -12
  32. package/dist/lib-cjs/manager/parameter-manager.js +78 -69
  33. package/dist/lib-cjs/manager/parameter-manager.js.map +1 -1
  34. package/dist/lib-cjs/manager/scene-manager.d.ts +111 -5
  35. package/dist/lib-cjs/manager/scene-manager.js +276 -10
  36. package/dist/lib-cjs/manager/scene-manager.js.map +1 -1
  37. package/dist/lib-cjs/viewer-error.d.ts +1 -0
  38. package/dist/lib-cjs/viewer-error.js +1 -0
  39. package/dist/lib-cjs/viewer-error.js.map +1 -1
  40. package/dist/lib-cjs/viewer.d.ts +9 -14
  41. package/dist/lib-cjs/viewer.js +16 -38
  42. package/dist/lib-cjs/viewer.js.map +1 -1
  43. package/package.json +22 -12
  44. package/src/index.ts +8 -0
  45. package/src/internal/asset-helper.ts +115 -0
  46. package/src/internal/cbn-custom-babylon-loader-plugin.ts +30 -3
  47. package/src/internal/cloning-helper.ts +1 -1
  48. package/src/internal/texture-parameter-helper.ts +25 -8
  49. package/src/manager/camera-manager.ts +153 -39
  50. package/src/manager/debug-manager.ts +3 -3
  51. package/src/manager/dimension-line-manager.ts +255 -0
  52. package/src/manager/html-anchor-manager.ts +332 -0
  53. package/src/manager/model-manager.ts +55 -138
  54. package/src/manager/parameter-manager.ts +94 -75
  55. package/src/manager/scene-manager.ts +375 -10
  56. package/src/viewer-error.ts +1 -0
  57. package/src/viewer.ts +30 -56
  58. package/src/dev.ts +0 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@combeenation/3d-viewer",
3
- "version": "17.1.0",
3
+ "version": "18.0.0-beta2",
4
4
  "description": "Combeenation 3D Viewer",
5
5
  "homepage": "https://github.com/Combeenation/3d-viewer#readme",
6
6
  "bugs": {
@@ -41,15 +41,20 @@
41
41
  "pub-beta": "npm run dist-cjs && npm publish --tag beta",
42
42
  "pub-final": "npm run dist-cjs && npm publish",
43
43
  "pub-rc": "npm run dist-cjs && npm publish --tag rc",
44
- "replace-version": "cross-env-shell replace \"@VERSION@\" $npm_package_version dist -r"
44
+ "replace-version": "cross-env-shell replace \"@VERSION@\" $npm_package_version dist -r",
45
+ "sleep5s": "node -e \"setTimeout(() => process.exit(0), 5000)\"",
46
+ "start-asset-server": "cross-env NODE_ENV='production' webpack serve --config build/webpack.test.conf.js",
47
+ "test": "vitest",
48
+ "test-delayed": "npm run sleep5s && npm run test"
45
49
  },
46
50
  "prettier": "@combeenation/prettier-config",
47
51
  "dependencies": {
48
- "@babylonjs/core": "7.43.0",
49
- "@babylonjs/gui": "7.43.0",
50
- "@babylonjs/loaders": "7.43.0",
51
- "@babylonjs/serializers": "7.43.0",
52
+ "@babylonjs/core": "7.47.3",
53
+ "@babylonjs/gui": "7.47.3",
54
+ "@babylonjs/loaders": "7.47.3",
55
+ "@babylonjs/serializers": "7.47.3",
52
56
  "eventemitter3": "4.0.7",
57
+ "html2canvas": "1.4.1",
53
58
  "is-svg": "^5.0.0",
54
59
  "lodash-es": "4.17.21"
55
60
  },
@@ -63,6 +68,7 @@
63
68
  "cross-env": "7.0.3",
64
69
  "eslint": "8.25.0",
65
70
  "html-webpack-plugin": "5.5.0",
71
+ "jsdom": "^26.0.0",
66
72
  "prettier": "2.7.1",
67
73
  "replace": "1.2.1",
68
74
  "rimraf": "3.0.2",
@@ -71,6 +77,7 @@
71
77
  "typedoc": "0.23.15",
72
78
  "typedoc-plugin-merge-modules": "4.0.1",
73
79
  "typescript": "4.8.4",
80
+ "vitest": "2.1.8",
74
81
  "webpack": "5.74.0",
75
82
  "webpack-bundle-analyzer": "4.6.1",
76
83
  "webpack-cli": "4.10.0",
@@ -78,16 +85,19 @@
78
85
  "webpack-merge": "5.8.0"
79
86
  },
80
87
  "optionalDependencies": {
81
- "@babylonjs/gui-editor": "7.43.0",
82
- "@babylonjs/inspector": "7.43.0",
83
- "@babylonjs/materials": "7.43.0",
84
- "@babylonjs/node-editor": "7.43.0",
85
- "@babylonjs/node-geometry-editor": "7.43.0"
88
+ "@babylonjs/gui-editor": "7.47.3",
89
+ "@babylonjs/inspector": "7.47.3",
90
+ "@babylonjs/materials": "7.47.3",
91
+ "@babylonjs/node-editor": "7.47.3",
92
+ "@babylonjs/node-geometry-editor": "7.47.3"
86
93
  },
87
94
  "dependenciesComments": {
88
- "@babylonjs": "Latest version 7.43.0 comes with getters for tagged entities in asset containers, as requested by us (see [PR](https://github.com/BabylonJS/Babylon.js/pull/16029))"
95
+ "@babylonjs": "Latest version 7.47.3 added lower camera target y limit, as requested from us (https://github.com/BabylonJS/Babylon.js/pull/16125)"
89
96
  },
90
97
  "optionalDependenciesComments": {
91
98
  "@babylonjs": "Defining the inspector package (and it's dependencies) as optional is required to omit these packages in the production build of the CBN react-app (see CB-9269)"
99
+ },
100
+ "scriptsComments": {
101
+ "test-delayed": "Didn't manage to wait 5 seconds for starting up the asset server as an individual VS Code task, therefore it's solved as npm script"
92
102
  }
93
103
  }
package/src/index.ts CHANGED
@@ -19,7 +19,10 @@ export * from '@babylonjs/core/Cameras/camera';
19
19
  export * from '@babylonjs/core/Culling';
20
20
  export * from '@babylonjs/core/Debug/axesViewer';
21
21
  export * from '@babylonjs/core/Debug/debugLayer';
22
+ export * from '@babylonjs/core/Engines/abstractEngine';
22
23
  export * from '@babylonjs/core/Engines/engine';
24
+ export * from '@babylonjs/core/Engines/Extensions/engine.query';
25
+ export * from '@babylonjs/core/Engines/nullEngine';
23
26
  export * from '@babylonjs/core/Engines/thinEngine';
24
27
  export * from '@babylonjs/core/Lights';
25
28
  export * from '@babylonjs/core/Loading';
@@ -31,14 +34,17 @@ export * from '@babylonjs/core/Maths';
31
34
  export * from '@babylonjs/core/Meshes/abstractMesh';
32
35
  export * from '@babylonjs/core/Meshes/geometry';
33
36
  export * from '@babylonjs/core/Meshes/instancedMesh';
37
+ export * from '@babylonjs/core/Meshes/linesMesh';
34
38
  export * from '@babylonjs/core/Meshes/mesh';
35
39
  export * from '@babylonjs/core/Meshes/meshBuilder';
36
40
  export * from '@babylonjs/core/Meshes/transformNode';
41
+ export * from '@babylonjs/core/Misc/observable';
37
42
  export * from '@babylonjs/core/Misc/interfaces/screenshotSize';
38
43
  export * from '@babylonjs/core/Misc/screenshotTools';
39
44
  export * from '@babylonjs/core/Misc/tags';
40
45
  export * from '@babylonjs/core/Morph';
41
46
  export * from '@babylonjs/core/node';
47
+ export * from '@babylonjs/core/Rendering/boundingBoxRenderer';
42
48
  export * from '@babylonjs/core/Rendering/utilityLayerRenderer';
43
49
  export * from '@babylonjs/core/scene';
44
50
  export * from '@babylonjs/core/types';
@@ -64,9 +70,11 @@ export * from '@babylonjs/core/Materials/Node';
64
70
  export * from './viewer';
65
71
  export * from './viewer-error';
66
72
  export * from './manager/camera-manager';
73
+ export * from './manager/dimension-line-manager';
67
74
  export * from './manager/debug-manager';
68
75
  export * from './manager/event-manager';
69
76
  export * from './manager/gltf-export-manager';
77
+ export * from './manager/html-anchor-manager';
70
78
  export * from './manager/material-manager';
71
79
  export * from './manager/model-manager';
72
80
  export * from './manager/parameter-manager';
@@ -0,0 +1,115 @@
1
+ import { AssetContainer, SceneAssetSettings, SceneLoader, Viewer, ViewerError, ViewerErrorIds } from '..';
2
+ import { CbnBabylonFileData, ExtendedAssetContainer } from './cbn-custom-babylon-loader-plugin';
3
+ import { getInternalMetadataValue } from './metadata-helper';
4
+
5
+ export type AssetState = 'notLoaded' | 'loading' | 'loaded' | 'inScene';
6
+
7
+ /**
8
+ * Common base asset for model and scene asset
9
+ */
10
+ export type BaseAsset = {
11
+ name: string;
12
+ url: string;
13
+ state: AssetState;
14
+ assetContainer: AssetContainer;
15
+ };
16
+
17
+ /**
18
+ * Help function for loading asset as asset container, with some special tweaks and fixes that we need in our CBN
19
+ * environment:
20
+ * - reconstruct current environment texture and intensity, as it get overwritten by Babylon.js basic loading function
21
+ * - explicitely define plugin extension for gzipped babylon files
22
+ * - extract and return cbn custom data from babylon file
23
+ * - crop materials from asset containers and add them to scene directly, as we treat materials as a global entity,
24
+ * which can be used from all models alike
25
+ *
26
+ * This function is used by model and scene asset alike.
27
+ */
28
+ export async function loadAsset(
29
+ asset: BaseAsset,
30
+ viewer: Viewer
31
+ ): Promise<{ cbnData?: CbnBabylonFileData; sceneSettingsData?: SceneAssetSettings }> {
32
+ asset.state = 'loading';
33
+ const curEnvTexture = viewer.scene.environmentTexture;
34
+ const curEnvIntensity = viewer.scene.environmentIntensity;
35
+
36
+ // CB-9240: Babylon.js doesn't recognize gzipped babylon files (`.babylon.gz`) as such, leading to a warning
37
+ // message, therefore we overwrite the plugin extension actively for such files
38
+ let pluginExtension;
39
+ try {
40
+ // URL constructor can throw for "valid" URL, which happened to be the case for the test asset environment where
41
+ // the urls are relative (starting with ".")
42
+ const urlObj = new URL(asset.url);
43
+ if (urlObj.pathname.endsWith('.babylon.gz')) {
44
+ pluginExtension = '.babylon';
45
+ }
46
+ } catch (e) {}
47
+
48
+ let extendedAssetContainer;
49
+ try {
50
+ extendedAssetContainer = (await SceneLoader.LoadAssetContainerAsync(
51
+ '',
52
+ asset.url,
53
+ viewer.scene,
54
+ undefined,
55
+ pluginExtension
56
+ )) as ExtendedAssetContainer;
57
+ } catch (e) {
58
+ throw new ViewerError({
59
+ id: ViewerErrorIds.AssetLoadingFailed,
60
+ message: (e as Error).message,
61
+ });
62
+ }
63
+
64
+ const { cbnData, sceneSettingsData } = extendedAssetContainer;
65
+ delete extendedAssetContainer.cbnData;
66
+ delete extendedAssetContainer.sceneSettingsData;
67
+
68
+ // from here it's a basic asset container again
69
+ const assetContainer = extendedAssetContainer as AssetContainer;
70
+
71
+ // materials should be a "global" thing and not assigned to an asset container
72
+ // this is not relevant for model assets in most of the cases, since materials are cropped from babylon models on the
73
+ // CBN server anyway
74
+ assetContainer.materials.forEach(material => {
75
+ viewer.scene.addMaterial(material);
76
+ material._parentContainer = null;
77
+ viewer.parameterManager.applyParameterValuesToMaterial(material);
78
+ });
79
+ assetContainer.materials = [];
80
+
81
+ // environment texture and intensity has been overwritten by load asset container function
82
+ viewer.scene.environmentTexture = curEnvTexture;
83
+ viewer.scene.environmentIntensity = curEnvIntensity;
84
+
85
+ asset.assetContainer = assetContainer;
86
+ asset.state = 'loaded';
87
+
88
+ return { cbnData, sceneSettingsData };
89
+ }
90
+
91
+ /**
92
+ * Help function for applying parameter values and "deferred" material to asset (model or scene asset), before using it
93
+ * in the scene
94
+ */
95
+ export async function prepareAssetForScene(asset: BaseAsset, viewer: Viewer): Promise<void> {
96
+ await viewer.parameterManager.applyAllParameterValuesToModel(asset.assetContainer);
97
+ // parameter manager did his job, now apply the deferred materials to all meshes that are not explicitely hidden by
98
+ // the parameter manager
99
+ await _applyDeferredMaterialsForAllVisibleMeshes(asset, viewer);
100
+ }
101
+
102
+ /**
103
+ * Creates and assigns each "deferred" material to the corresponding mesh, if the mesh is visible.
104
+ * Model should be ready to use immediately after this function has done it's job.
105
+ */
106
+ async function _applyDeferredMaterialsForAllVisibleMeshes(asset: BaseAsset, viewer: Viewer): Promise<void> {
107
+ const setMaterialProms = asset.assetContainer.meshes.map(async mesh => {
108
+ const deferredMaterial = getInternalMetadataValue(mesh, 'deferredMaterial');
109
+ const visible = viewer.parameterManager.getNestedVisibilityParameterValueOfNode(mesh);
110
+ if (deferredMaterial && visible) {
111
+ await viewer.materialManager.setMaterialOnMesh(deferredMaterial as string, mesh);
112
+ }
113
+ });
114
+ await Promise.all(setMaterialProms);
115
+ }
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  AssetContainer,
3
- ExtendedAssetContainer,
3
+ Color4,
4
4
  ISceneLoaderPlugin,
5
5
  InstancedMesh,
6
6
  ParsedDecalConfiguration,
7
+ SceneAssetSettings,
7
8
  SceneLoader,
8
9
  ViewerError,
9
10
  ViewerErrorIds,
@@ -13,6 +14,23 @@ import { setInternalMetadataValue } from './metadata-helper';
13
14
  import { deleteAllTags, getTagsAsStrArr, setTagsAsString } from './tags-helper';
14
15
  import { isArray, isString } from 'lodash-es';
15
16
 
17
+ /**
18
+ * Contains cbn custom data, like decals or scene settings.
19
+ * This is just a temporary type, as the `loadAssetContainer` function only returns an asset container, which can be
20
+ * altered by our file loader plugin.
21
+ * After loading the model, `cbnData` is cropped and a pure asset container is available for further processing.
22
+ * @internal
23
+ */
24
+ export class ExtendedAssetContainer extends AssetContainer {
25
+ cbnData?: CbnBabylonFileData;
26
+ sceneSettingsData?: SceneAssetSettings;
27
+ }
28
+
29
+ /**
30
+ * @internal
31
+ */
32
+ export type CbnBabylonFileData = { decals?: ParsedDecalConfiguration[] };
33
+
16
34
  type DataWithMeshes = { meshes: unknown[] };
17
35
  type DataWithDecalConfigurations = { cbnData: { decals: unknown[] } };
18
36
 
@@ -59,7 +77,11 @@ export function registerCustomCbnBabylonLoaderPlugin(): void {
59
77
  load: previousLoaderPlugin.load,
60
78
  loadAssetContainer: (scene, data, rootUrl, onError): ExtendedAssetContainer => {
61
79
  const dataParsed = JSON.parse(data as string);
62
- const importedContainer = previousLoaderPlugin.loadAssetContainer(scene, data, rootUrl);
80
+ // root url parameter is left empty for the following reasons:
81
+ // - cube texture creation (for environment) breaks, as Babylon.js tries to prefix absolute paths (e.g. from
82
+ // Babylon.js server) with the root url
83
+ // - we always have absolute paths anyway => links to 3d model, material & textures assets
84
+ const importedContainer = previousLoaderPlugin.loadAssetContainer(scene, data, '');
63
85
 
64
86
  _addMissingMaterialMetadata(dataParsed, importedContainer);
65
87
  _reconstructTagsForInstancedMeshes(dataParsed, importedContainer);
@@ -73,9 +95,14 @@ export function registerCustomCbnBabylonLoaderPlugin(): void {
73
95
  onError?.((e as Error).message);
74
96
  }
75
97
 
76
- // add `cbnData` to output asset container, so that this information can be store as metadata for the model
98
+ // add `cbnData` to output asset container, so that this information can be stored as metadata for the model
99
+ // same with `sceneSettingsData`, which is used in the scene asset
77
100
  const extendedContainer = importedContainer as ExtendedAssetContainer;
78
101
  extendedContainer.cbnData = dataParsed.cbnData;
102
+ extendedContainer.sceneSettingsData = {
103
+ environmentIntensity: dataParsed.environmentIntensity,
104
+ clearColor: dataParsed.clearColor && Color4.FromArray(dataParsed.clearColor),
105
+ };
79
106
 
80
107
  return extendedContainer;
81
108
  },
@@ -186,7 +186,7 @@ function _cloneNode(
186
186
  // making deep copies of the node metadata might be an optional flag in the cloning functions in the future
187
187
 
188
188
  // ATM the assignment from clone to source is needed for reassigning instanced meshes after cloning
189
- // may be usefull for other node types in the future as well
189
+ // may be useful for other node types in the future as well
190
190
  setInternalMetadataValue(clone, 'cloneSource', node.uniqueId);
191
191
  setInternalMetadataValue(node, 'cloneTarget', clone.uniqueId);
192
192
 
@@ -73,17 +73,21 @@ export function createBuiltInTextureParameter(parameterManager: ParameterManager
73
73
  const texture = _getTextureFromParameterChannel(pbrMaterial, channel);
74
74
 
75
75
  if (texture) {
76
- // create a clone of the texture, in this way we can load the image before assigning it to the material
77
- // channel
78
- const clonedTexture = texture.clone();
79
- // update texture and await loading time right away
80
- await new Promise<void>(resolve => clonedTexture.updateURL(url, undefined, resolve));
81
-
82
- _assignTextureParameterChannel(clonedTexture, pbrMaterial, channel);
76
+ if (url) {
77
+ // create a clone of the texture, in this way we can load the image before assigning it to the material
78
+ // channel
79
+ const clonedTexture = texture.clone();
80
+ // update texture and await loading time right away
81
+ await new Promise<void>(resolve => clonedTexture.updateURL(url, undefined, resolve));
82
+
83
+ _assignTextureParameterChannel(clonedTexture, pbrMaterial, channel);
84
+ } else {
85
+ _removeFromTextureParameterChannel(pbrMaterial, channel);
86
+ }
83
87
 
84
88
  // dispose old texture
85
89
  texture.dispose();
86
- } else {
90
+ } else if (url) {
87
91
  // no texture, or wrong type => create texture from scratch
88
92
  // first we check if some settings were provided in the material definition
89
93
  const addMatSettings = await window.Cbn?.Assets.getMaterial(pbrMaterial.id);
@@ -341,6 +345,19 @@ function _assignTextureParameterChannel(
341
345
  }
342
346
  }
343
347
 
348
+ /**
349
+ * Remove runtime texture from dedicated channel in the PBR material
350
+ */
351
+ function _removeFromTextureParameterChannel(pbrMaterial: PBRMaterial, channel: ParameterTextureChannelsKeys): void {
352
+ if (channel === 'metallicRoughnessTexture') {
353
+ pbrMaterial.metallicTexture = null;
354
+ } else if (channel === 'detailmapTexture') {
355
+ pbrMaterial.detailMap.texture = null;
356
+ } else {
357
+ pbrMaterial[channel] = null;
358
+ }
359
+ }
360
+
344
361
  /**
345
362
  * Similar to `_getTextureFromParameterChannel`, whereas the input is a plain JSON object instead of a runtime material
346
363
  */
@@ -4,7 +4,6 @@ import {
4
4
  ArcRotateCamera,
5
5
  BoundingSphere,
6
6
  ExcludedGeometryList,
7
- IScreenshotSize,
8
7
  ScreenshotTools,
9
8
  Vector3,
10
9
  Viewer,
@@ -64,6 +63,11 @@ export type ScreenshotSettings = {
64
63
  autofocusScene?: boolean;
65
64
  /** Optional list of geometry to be excluded from consideration */
66
65
  exclude?: ExcludedGeometryList;
66
+ /**
67
+ * Optional list of html anchor groups (see {@link HtmlAnchorManager}), that should be excluded from the screenshot.\
68
+ * Excludes ALL html anchor groups if set to `true`.
69
+ */
70
+ excludeHtmlAnchorGroups?: string[] | true;
67
71
  /**
68
72
  * "MIME type" of the returned screenshot image, defaults to `image/png`
69
73
  *
@@ -89,11 +93,27 @@ export type ScreenshotSettings = {
89
93
  quality?: number;
90
94
  };
91
95
 
96
+ /**
97
+ * Internal data type for screenshot sizing.
98
+ * `imageWidth/Height` defines the size of the final image.
99
+ * `canvasWidth/Height` is the size of the canvas from which the screenshot is taken.
100
+ * Canvas size always has the same aspect ratio as the viewport and is larger than image size, as the final image may be
101
+ * cropped.
102
+ *
103
+ * @internal
104
+ */
105
+ export type ScreenshotSize = {
106
+ imageWidth: number;
107
+ imageHeight: number;
108
+ canvasWidth: number;
109
+ canvasHeight: number;
110
+ };
111
+
92
112
  /**
93
113
  * Manager for camera related tasks
94
114
  */
95
115
  export class CameraManager {
96
- public static readonly CAMERA_ANIMATION_NAME = '__cameraAnimation__';
116
+ public static readonly CAMERA_ANIMATION_NAME = '$cameraAnimation';
97
117
 
98
118
  /** @internal */
99
119
  public static readonly DEFAULT_CAMERA_POSITION: CameraPosition = {
@@ -112,7 +132,7 @@ export class CameraManager {
112
132
 
113
133
  protected static readonly _DEFAULT_AUTOFOCUS_RADIUS_FACTOR = 1;
114
134
  protected static readonly _DEFAULT_CAM_SPEED_MS = 250;
115
- protected static readonly _SCREENSHOT_CAMERA_NAME = '__screenshotCamera__';
135
+ protected static readonly _SCREENSHOT_CAMERA_NAME = '$screenshotCamera';
116
136
 
117
137
  /** @internal */
118
138
  public constructor(protected viewer: Viewer) {}
@@ -131,7 +151,7 @@ export class CameraManager {
131
151
  }
132
152
 
133
153
  // get bounding box of all visible meshes, this is the base for the autofocus algorithm
134
- const boundingInfo = this.viewer.calculateBoundingInfo(settings?.exclude);
154
+ const boundingInfo = this.viewer.sceneManager.calculateBoundingInfo(settings?.exclude);
135
155
  // optionally show bounding sphere for debugging purpose
136
156
  this.viewer.eventManager.fireEvent(ViewerEvent.AutofocusStart, boundingInfo.boundingSphere);
137
157
 
@@ -269,19 +289,38 @@ export class CameraManager {
269
289
  * The result is a string containing a base64 encoded image.
270
290
  */
271
291
  public async createScreenshot(settings?: ScreenshotSettings): Promise<string> {
292
+ const {
293
+ width,
294
+ height,
295
+ alpha,
296
+ beta,
297
+ radius,
298
+ target,
299
+ autofocusScene,
300
+ exclude,
301
+ excludeHtmlAnchorGroups,
302
+ fileName,
303
+ samples,
304
+ antialiasing,
305
+ renderSprites,
306
+ enableStencilBuffer,
307
+ useLayerMask,
308
+ quality,
309
+ } = settings ?? {};
310
+
272
311
  const screenshotCam = this.viewer.scene.activeCamera?.clone(
273
312
  CameraManager._SCREENSHOT_CAMERA_NAME
274
313
  ) as ArcRotateCamera;
275
- const boundingInfo = settings?.autofocusScene ? this.viewer.calculateBoundingInfo(settings.exclude) : undefined;
314
+ const boundingInfo = autofocusScene ? this.viewer.sceneManager.calculateBoundingInfo(exclude) : undefined;
276
315
 
277
- if (settings?.alpha !== undefined) {
278
- screenshotCam.alpha = settings.alpha;
316
+ if (alpha !== undefined) {
317
+ screenshotCam.alpha = alpha;
279
318
  }
280
- if (settings?.beta !== undefined) {
281
- screenshotCam.beta = settings.beta;
319
+ if (beta !== undefined) {
320
+ screenshotCam.beta = beta;
282
321
  }
283
- if (settings?.radius !== undefined) {
284
- screenshotCam.radius = settings.radius;
322
+ if (radius !== undefined) {
323
+ screenshotCam.radius = radius;
285
324
  } else if (boundingInfo) {
286
325
  // zoom out to have full scene in view if requested by `autofocusScene` flag
287
326
  const distance = this._getAutofocusZoomingDistance(boundingInfo.boundingSphere, screenshotCam);
@@ -290,44 +329,90 @@ export class CameraManager {
290
329
  }
291
330
  // `cloneAlphaBetaRadius` has to be set to true, otherwise these values get adapted by setting the new target
292
331
  // this would also be the case when setting `screenshotCam.target` directly
293
- if (settings?.target !== undefined) {
294
- screenshotCam.setTarget(settings.target, undefined, undefined, true);
332
+ if (target !== undefined) {
333
+ screenshotCam.setTarget(target, undefined, undefined, true);
295
334
  } else if (boundingInfo) {
296
335
  screenshotCam.setTarget(boundingInfo.boundingSphere.center, undefined, undefined, true);
297
336
  }
298
337
 
299
- // don't expose internal props of `IScreenshotSize` in the API (yet), as it gets rather confusing and it shouldn't
300
- // be necessary in the first place
301
- // also let Babylon.js do all the fallback handling for missing properties
302
- const size: IScreenshotSize = { width: settings?.width, height: settings?.height };
338
+ // calculate screenshot size manually as it is used for html anchor screenshot as well
339
+ const screenshotSize = this._getScreenshotSize(width, height);
340
+ const mimeType = settings?.mimeType ?? 'image/png';
341
+
342
+ const imageStr3d = await ScreenshotTools.CreateScreenshotUsingRenderTargetAsync(
343
+ this.viewer.engine,
344
+ screenshotCam,
345
+ { width: screenshotSize.imageWidth, height: screenshotSize.imageHeight },
346
+ mimeType,
347
+ samples,
348
+ antialiasing,
349
+ undefined,
350
+ renderSprites,
351
+ enableStencilBuffer,
352
+ useLayerMask,
353
+ quality,
354
+ texture => {
355
+ // NOTE: this doesn't work ATM, see https://github.com/BabylonJS/Babylon.js/pull/16081#issuecomment-2652861176
356
+ // we'll have to wait for a fix or solve the visibility in a different way (e.g. camera layer mask)
357
+ texture.renderList = this.viewer.scene.meshes.filter(
358
+ mesh => !settings?.exclude || !isNodeExcluded(mesh, settings.exclude)
359
+ );
360
+ }
361
+ );
303
362
 
304
- // for some reason the `customizeTexture` property is not exposed in the async version of this function, therefore
305
- // we create the promise here manually
306
- const imageStr = await new Promise<string>(resolve => {
307
- ScreenshotTools.CreateScreenshotUsingRenderTarget(
308
- this.viewer.engine,
363
+ let imageStr = '';
364
+ const htmlAnchorKeys =
365
+ excludeHtmlAnchorGroups === true
366
+ ? []
367
+ : this.viewer.htmlAnchorManager.getHtmlAnchorKeys(undefined, excludeHtmlAnchorGroups, true);
368
+ if (htmlAnchorKeys.length) {
369
+ // html anchors are not included in the main screenshot, as the html elements are located outside of the canvas
370
+ // the idea is to create a dedicated canvas for these elements and merge the result with the main screenshot into
371
+ // a combined canvas
372
+ const screenshotHtmlCanvas = await this.viewer.htmlAnchorManager.createScreenshotCanvas(
373
+ screenshotSize,
309
374
  screenshotCam,
310
- size,
311
- (data: string) => resolve(data),
312
- settings?.mimeType,
313
- settings?.samples,
314
- settings?.antialiasing,
315
- settings?.fileName,
316
- settings?.renderSprites,
317
- settings?.enableStencilBuffer,
318
- settings?.useLayerMask,
319
- settings?.quality,
320
- texture => {
321
- texture.renderList = this.viewer.scene.meshes.filter(
322
- mesh => !settings?.exclude || !isNodeExcluded(mesh, settings.exclude)
323
- );
324
- }
375
+ htmlAnchorKeys
325
376
  );
326
- });
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);
394
+
395
+ screenshotCombinedCanvas.remove();
396
+ } else {
397
+ imageStr = imageStr3d;
398
+ }
327
399
 
328
400
  screenshotCam.dispose();
329
401
 
330
- return imageStr;
402
+ if (fileName) {
403
+ // rebuild Babylon.js default behaviour: if a file name is given, download the screenshot instead of returning the
404
+ // data string
405
+ const link = document.createElement('a');
406
+ link.href = imageStr;
407
+ link.download = fileName;
408
+ document.body.appendChild(link);
409
+ link.click();
410
+ document.body.removeChild(link);
411
+
412
+ return '';
413
+ } else {
414
+ return imageStr;
415
+ }
331
416
  }
332
417
 
333
418
  protected static _addCameraAnimationToGroup(
@@ -373,4 +458,33 @@ export class CameraManager {
373
458
 
374
459
  return distance;
375
460
  }
461
+
462
+ protected _getScreenshotSize(width?: number, height?: number): ScreenshotSize {
463
+ const canvas = this.viewer.canvas!;
464
+ const aspectRatio = canvas.width / canvas.height;
465
+
466
+ let imageWidth = 0;
467
+ let imageHeight = 0;
468
+ if (width && height) {
469
+ imageWidth = width;
470
+ imageHeight = height;
471
+ } else if (width && !height) {
472
+ imageWidth = width;
473
+ imageHeight = width / aspectRatio;
474
+ } else if (!width && height) {
475
+ imageWidth = height * aspectRatio;
476
+ imageHeight = height;
477
+ } else {
478
+ imageWidth = canvas.width;
479
+ imageHeight = canvas.height;
480
+ }
481
+
482
+ // canvas size only differs from image size if both width and height are set
483
+ // in this case the aspect ratio differs from the canvas aspect ratio and some parts of the canvas have to be
484
+ // cropped for the screenshot
485
+ const canvasWidth = width && height ? height * aspectRatio : imageWidth;
486
+ const canvasHeight = imageHeight;
487
+
488
+ return { imageWidth, imageHeight, canvasWidth, canvasHeight };
489
+ }
376
490
  }
@@ -20,7 +20,7 @@ import {
20
20
  * Manager for debugging functionalities
21
21
  */
22
22
  export class DebugManager {
23
- protected static _BOUNDING_SPHERE_KEY = '__bounding_sphere__';
23
+ protected static _BOUNDING_SPHERE_KEY = '$boundingSphere';
24
24
 
25
25
  protected _axesViewer: AxesViewer | null = null;
26
26
  protected _showBoundingSphereForAutofocus: boolean = false;
@@ -91,7 +91,7 @@ The inspector can only be used in development builds.`);
91
91
  public showCoordinateSystem(node?: TransformNode, size?: number): void {
92
92
  // calculate the default size of not provided
93
93
  if (!size) {
94
- const sceneBoundingInfo = this.viewer.calculateBoundingInfo();
94
+ const sceneBoundingInfo = this.viewer.sceneManager.calculateBoundingInfo();
95
95
  const radius = sceneBoundingInfo.boundingSphere.radius;
96
96
 
97
97
  // takes a third of the radius from scene boundingsphere
@@ -145,7 +145,7 @@ The inspector can only be used in development builds.`);
145
145
 
146
146
  /**
147
147
  * Draws a wireframe bounding sphere on the next call of {@link CameraManager.autofocusActiveCamera}.\
148
- * This is usefull for checking which parts of the scene get centered.
148
+ * This is useful for checking which parts of the scene get centered.
149
149
  */
150
150
  public showBoundingSphereForAutofocus(): void {
151
151
  this._showBoundingSphereForAutofocus = true;