@combeenation/3d-viewer 18.0.0-beta1 → 18.0.0-beta3

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 (39) 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 +6 -0
  4. package/dist/lib-cjs/index.js +6 -0
  5. package/dist/lib-cjs/index.js.map +1 -1
  6. package/dist/lib-cjs/internal/cloning-helper.js +1 -1
  7. package/dist/lib-cjs/internal/cloning-helper.js.map +1 -1
  8. package/dist/lib-cjs/manager/camera-manager.d.ts +23 -2
  9. package/dist/lib-cjs/manager/camera-manager.js +90 -25
  10. package/dist/lib-cjs/manager/camera-manager.js.map +1 -1
  11. package/dist/lib-cjs/manager/debug-manager.d.ts +1 -1
  12. package/dist/lib-cjs/manager/debug-manager.js +2 -2
  13. package/dist/lib-cjs/manager/debug-manager.js.map +1 -1
  14. package/dist/lib-cjs/manager/dimension-line-manager.d.ts +87 -0
  15. package/dist/lib-cjs/manager/dimension-line-manager.js +126 -0
  16. package/dist/lib-cjs/manager/dimension-line-manager.js.map +1 -0
  17. package/dist/lib-cjs/manager/html-anchor-manager.d.ts +93 -0
  18. package/dist/lib-cjs/manager/html-anchor-manager.js +228 -0
  19. package/dist/lib-cjs/manager/html-anchor-manager.js.map +1 -0
  20. package/dist/lib-cjs/manager/model-manager.d.ts +0 -1
  21. package/dist/lib-cjs/manager/model-manager.js +0 -1
  22. package/dist/lib-cjs/manager/model-manager.js.map +1 -1
  23. package/dist/lib-cjs/manager/parameter-manager.d.ts +1 -1
  24. package/dist/lib-cjs/manager/scene-manager.js +10 -3
  25. package/dist/lib-cjs/manager/scene-manager.js.map +1 -1
  26. package/dist/lib-cjs/viewer.d.ts +6 -2
  27. package/dist/lib-cjs/viewer.js +13 -1
  28. package/dist/lib-cjs/viewer.js.map +1 -1
  29. package/package.json +2 -1
  30. package/src/index.ts +6 -0
  31. package/src/internal/cloning-helper.ts +1 -1
  32. package/src/manager/camera-manager.ts +152 -40
  33. package/src/manager/debug-manager.ts +2 -2
  34. package/src/manager/dimension-line-manager.ts +210 -0
  35. package/src/manager/html-anchor-manager.ts +332 -0
  36. package/src/manager/model-manager.ts +0 -1
  37. package/src/manager/parameter-manager.ts +1 -1
  38. package/src/manager/scene-manager.ts +12 -3
  39. package/src/viewer.ts +17 -1
@@ -0,0 +1,332 @@
1
+ import {
2
+ AbstractMesh,
3
+ ArcRotateCamera,
4
+ Camera,
5
+ DimensionLineManager,
6
+ MeshBuilder,
7
+ ScreenshotSize,
8
+ StandardMaterial,
9
+ TransformNode,
10
+ Vector3,
11
+ Viewer,
12
+ Viewport,
13
+ } from '..';
14
+ import html2canvas from 'html2canvas';
15
+
16
+ export type HtmlAnchorOptions = {
17
+ /**
18
+ * Associated anchor mesh will be created underneath this parent node.\
19
+ * Can be used to make anchor position calculation easier in nested scene structures.
20
+ */
21
+ parentNode?: TransformNode;
22
+ /**
23
+ * Can be used to filter affected html anchors in {@link HtmlAnchorManager.removeAllHtmlAnchors}
24
+ */
25
+ group?: string;
26
+ /**
27
+ * `true`: html elements can be occluded by other meshes
28
+ * `false`: html elements will always be shown in front of the scene
29
+ *
30
+ * Default: `false`
31
+ */
32
+ hideIfOccluded?: boolean;
33
+ /**
34
+ * `true`: html element size is relative to camera distance, just like a "normal" 3d object
35
+ * `false`: html element size remains constant
36
+ *
37
+ * Default: `false`
38
+ */
39
+ scaleWithCameraDistance?: boolean;
40
+ /**
41
+ * Activates/deactivates `pointer-events` CSS class of anchor element.\
42
+ * Set this to `true` if the html element should be interactive (e.g. button)
43
+ *
44
+ * Default: `false`
45
+ */
46
+ enablePointerEvents?: boolean;
47
+ };
48
+
49
+ /**
50
+ * Manager for mapping html elements to 3d positions in the scene.\
51
+ * Common use cases are:
52
+ * - **Hotspot buttons**: Interactively add/remove elements in the scene or just show some info on hovering
53
+ * - **Input fields**: Interactively change the size of an element
54
+ * - **Info label**: Just shows some information on a certain position within the scene
55
+ *
56
+ * Html anchors are also used as dimension lables in the {@link DimensionLineManager}
57
+ */
58
+ export class HtmlAnchorManager {
59
+ protected static readonly _HTML_ANCHOR_KEY = '$htmlAnchor';
60
+
61
+ protected _htmlAnchors: {
62
+ [name: string]: {
63
+ parentHtmlElement: HTMLDivElement;
64
+ anchorMesh: AbstractMesh;
65
+ options?: HtmlAnchorOptions;
66
+ };
67
+ } = {};
68
+
69
+ protected _anchorMeshMaterial: StandardMaterial | null = null;
70
+
71
+ /** @internal */
72
+ public constructor(protected viewer: Viewer) {
73
+ const canvas = viewer.canvas;
74
+ if (!canvas) {
75
+ return;
76
+ }
77
+
78
+ // html element positions need to be updated after each camera render call
79
+ viewer.scene.onAfterRenderCameraObservable.add(() => {
80
+ Object.values(this._htmlAnchors).forEach(({ parentHtmlElement, anchorMesh, options }) => {
81
+ this._updateHtmlAnchor(
82
+ parentHtmlElement,
83
+ anchorMesh,
84
+ viewer.scene.activeCamera!,
85
+ canvas.width,
86
+ canvas.height,
87
+ 1,
88
+ options
89
+ );
90
+ });
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Assign a html element to a certain position in the scene.\
96
+ * The html elements 2d position will be updated on each camera render, so that the element appears like a "normal"
97
+ * mesh within the scene.
98
+ */
99
+ 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`);
103
+ return;
104
+ }
105
+
106
+ if (this._htmlAnchors[name]) {
107
+ console.warn(`Html anchor "${name}" already exists`);
108
+ return;
109
+ }
110
+
111
+ const { parentNode, enablePointerEvents } = options ?? {};
112
+
113
+ // create a parent for the input html element, which will receive the updated style and transform data
114
+ // in this way the original input element remains untouched
115
+ const parentHtmlElement = document.createElement('div');
116
+ parentHtmlElement.dataset.anchorName = name;
117
+ parentHtmlElement.style.position = 'absolute';
118
+ parentHtmlElement.style.margin = '0';
119
+ parentHtmlElement.appendChild(htmlElement);
120
+ if (!enablePointerEvents) {
121
+ parentHtmlElement.style.pointerEvents = 'none';
122
+ }
123
+ canvasParentHtmlElement.appendChild(parentHtmlElement);
124
+
125
+ // NOTE: creates a sphere with fixed size, which could be problematic in scene with "strange" dimensions
126
+ // add a property for the sphere size if required
127
+ const anchorMesh = MeshBuilder.CreateSphere(`${HtmlAnchorManager._HTML_ANCHOR_KEY}_${name}`, { diameter: 0.01 });
128
+ anchorMesh.position = position;
129
+ anchorMesh.parent = parentNode ?? null;
130
+ // anchor mesh will be invisible, we only need it for positioning and occlusion check
131
+ anchorMesh.material = this._getOrCreateAnchorMeshMaterial();
132
+ anchorMesh.occlusionType = AbstractMesh.OCCLUSION_TYPE_OPTIMISTIC;
133
+ anchorMesh.occlusionQueryAlgorithmType = AbstractMesh.OCCLUSION_ALGORITHM_TYPE_CONSERVATIVE;
134
+ // it's important that the occlusion check mesh is rendered after all "normal" meshes, which should be taken into
135
+ // account for the occlusion check
136
+ anchorMesh.renderingGroupId = 1;
137
+
138
+ this._htmlAnchors[name] = { parentHtmlElement, anchorMesh, options };
139
+ }
140
+
141
+ /**
142
+ * Remove html anchor element and disposes all associated ressources
143
+ */
144
+ public removeHtmlAnchor(name: string): void {
145
+ const htmlAnchor = this._htmlAnchors[name];
146
+ if (!htmlAnchor) {
147
+ console.warn(`Html anchor "${name}" does not exist`);
148
+ return;
149
+ }
150
+
151
+ htmlAnchor.parentHtmlElement.remove();
152
+ htmlAnchor.anchorMesh.dispose();
153
+
154
+ delete this._htmlAnchors[name];
155
+ }
156
+
157
+ /**
158
+ * Removes html anchor elements, as defined by the `groups` input
159
+ *
160
+ * @param groups if set, only html anchors which are part of these groups will be removed\
161
+ * if left `undefined`, ALL html anchors will be removed
162
+ */
163
+ public removeAllHtmlAnchors(groups?: string[]): void {
164
+ const anchorKeysToRemove = this.getHtmlAnchorKeys(
165
+ groups,
166
+ undefined,
167
+ // system groups should not be remove, as there is a dedicated workflow
168
+ // (e.g. `DimensionLineManager.removeDimensionLine`) to do this
169
+ false
170
+ );
171
+
172
+ anchorKeysToRemove.forEach(name => this.removeHtmlAnchor(name));
173
+ }
174
+
175
+ /**
176
+ * Creates a screenshot of defined html anchors and returns the associated canvas.\
177
+ * This function is used @internal from the main screenshot function.
178
+ */
179
+ public async createScreenshotCanvas(
180
+ size: ScreenshotSize,
181
+ camera: ArcRotateCamera,
182
+ htmlAnchorKeys: string[]
183
+ ): Promise<HTMLCanvasElement> {
184
+ const canvasParentHtmlElement = this.viewer.canvas?.parentElement;
185
+ if (!canvasParentHtmlElement) {
186
+ console.warn(`No parent for html anchors available`);
187
+ return new HTMLCanvasElement();
188
+ }
189
+
190
+ const htmlContainer = document.createElement('div');
191
+ htmlContainer.style.width = `${size.canvasWidth}px`;
192
+ htmlContainer.style.height = `${size.canvasHeight}px`;
193
+
194
+ // the `html2canvas` library requires the html to be present in the DOM
195
+ // that's why we create it top level and move it out of the viewport
196
+ // @reviewer: maybe there is a smoother method of doing that?
197
+ document.body.append(htmlContainer);
198
+ htmlContainer.style.position = 'absolute';
199
+ htmlContainer.style.top = `${-size.canvasHeight}px`;
200
+
201
+ htmlAnchorKeys.forEach(key => {
202
+ const { parentHtmlElement, anchorMesh, options } = this._htmlAnchors[key];
203
+
204
+ // create clones of html anchors, so that the position of the original elements remains untouched
205
+ const clonedHtmlNode = parentHtmlElement.cloneNode(true) as HTMLDivElement;
206
+ htmlContainer.append(clonedHtmlNode);
207
+
208
+ // reposition that cloned html element, so that it fits to the desired camera position
209
+ const baseScale = size.canvasHeight / this.viewer.canvas!.height;
210
+ this._updateHtmlAnchor(
211
+ clonedHtmlNode,
212
+ anchorMesh,
213
+ camera,
214
+ size.canvasWidth,
215
+ size.canvasHeight,
216
+ baseScale,
217
+ options
218
+ );
219
+ });
220
+
221
+ const screenshotHtmlCanvas = await html2canvas(htmlContainer, {
222
+ width: size.imageWidth,
223
+ height: size.imageHeight,
224
+ // apply difference of canvas and image size as offsets
225
+ x: (size.canvasWidth - size.imageWidth) / 2,
226
+ y: (size.canvasHeight - size.imageHeight) / 2,
227
+ logging: false,
228
+ backgroundColor: null,
229
+ });
230
+
231
+ htmlContainer.remove();
232
+
233
+ return screenshotHtmlCanvas;
234
+ }
235
+
236
+ /**
237
+ * Receive html anchor keys that pass the `includeGroups` and `excludeGroups` filter
238
+ * @param allowSystemGroups defines if html anchors created by the viewer should be considered (e.g. dimension line
239
+ * labels)
240
+ * @internal
241
+ */
242
+ public getHtmlAnchorKeys(includeGroups?: string[], excludeGroups?: string[], allowSystemGroups?: boolean): string[] {
243
+ const anchorKeys = Object.keys(this._htmlAnchors).filter(name => {
244
+ const group = this._htmlAnchors[name].options?.group;
245
+ const passIncludeGroupFilter =
246
+ includeGroups === undefined || (group !== undefined && includeGroups.includes(group));
247
+ const passExcludeGroupFilter =
248
+ excludeGroups === undefined || group === undefined || !excludeGroups.includes(group);
249
+ // ATM only dimension labels are system html anchors
250
+ const passSystemGroupFilter = allowSystemGroups || group !== DimensionLineManager.DIMENSION_LINE_KEY;
251
+
252
+ return passIncludeGroupFilter && passExcludeGroupFilter && passSystemGroupFilter;
253
+ });
254
+
255
+ return anchorKeys;
256
+ }
257
+
258
+ /**
259
+ * Check if input mesh is a html anchor mesh, created by this manager.\
260
+ * This check is done via the material, as all html anchor meshes have the generic `_anchorMeshMaterial` set.
261
+ * @internal
262
+ */
263
+ public isHtmlAnchorMesh(mesh: AbstractMesh): boolean {
264
+ return mesh.material === this._anchorMeshMaterial;
265
+ }
266
+
267
+ protected _updateHtmlAnchor(
268
+ parentHtmlElement: HTMLDivElement,
269
+ anchorMesh: AbstractMesh,
270
+ camera: Camera,
271
+ width: number,
272
+ height: number,
273
+ baseScale: number,
274
+ options?: HtmlAnchorOptions
275
+ ): void {
276
+ const { hideIfOccluded, scaleWithCameraDistance } = options ?? {};
277
+
278
+ const viewMatrix = camera.getViewMatrix();
279
+ const projectionMatrix = camera.getProjectionMatrix();
280
+ const transformMatrix = viewMatrix.multiply(projectionMatrix);
281
+
282
+ // convert into 2d space
283
+ const vertexScreenCoords = Vector3.Project(
284
+ Vector3.Zero(),
285
+ anchorMesh.getWorldMatrix(),
286
+ transformMatrix,
287
+ new Viewport(0, 0, width, height)
288
+ );
289
+
290
+ // base scale is used if width and height don't equal the viewer canvas, which is the case for screenshots
291
+ let scale = baseScale;
292
+ if (scaleWithCameraDistance) {
293
+ const meshWorldPos = anchorMesh.getAbsolutePosition();
294
+ // calculate camera world position manually, as there is no help function like for meshes
295
+ const camWorldMatrix = camera.computeWorldMatrix();
296
+ const camWorldPos = camWorldMatrix.getTranslation();
297
+ const distance = Vector3.Distance(meshWorldPos, camWorldPos);
298
+ const frustumSlopeY = Math.tan(camera.fov / 2);
299
+ // if the distance from camera to mesh gets larger, the html elements scaling will be decreased
300
+ // we also consider the cameras FOV, so scale 1 means, that the resulting vertical frustum of the camera
301
+ // distance equals 1
302
+ // ...this is probably too small in some scenarios, so we might have to add a general scale setting here
303
+ scale *= 1 / (distance * frustumSlopeY);
304
+ }
305
+
306
+ const elementXOffset = (parentHtmlElement.offsetWidth * scale) / 2;
307
+ const elementYOffset = (parentHtmlElement.offsetHeight * scale) / 2;
308
+ // finally move and scale the HTML element
309
+ parentHtmlElement.style.transform = `translate3d(calc(${vertexScreenCoords.x}px - 50%), calc(${vertexScreenCoords.y}px - 50%), 0px) scale(${scale})`;
310
+
311
+ // check for occlusion if desired
312
+ // we use the opacity setting, which could also be animated nicely if we want
313
+ const isInViewport =
314
+ vertexScreenCoords.x - elementXOffset > 0 &&
315
+ vertexScreenCoords.x + elementXOffset < width &&
316
+ vertexScreenCoords.y - elementYOffset > 0 &&
317
+ vertexScreenCoords.y + elementYOffset < height;
318
+ const isOccluded = hideIfOccluded && anchorMesh.isOccluded;
319
+ parentHtmlElement.style.opacity = isInViewport && !isOccluded ? '1' : '0';
320
+ }
321
+
322
+ protected _getOrCreateAnchorMeshMaterial(): StandardMaterial {
323
+ if (this._anchorMeshMaterial) {
324
+ return this._anchorMeshMaterial;
325
+ }
326
+
327
+ this._anchorMeshMaterial = new StandardMaterial('$matAnchorMesh');
328
+ this._anchorMeshMaterial.alpha = 0;
329
+ this.viewer.scene.removeMaterial(this._anchorMeshMaterial);
330
+ return this._anchorMeshMaterial;
331
+ }
332
+ }
@@ -62,7 +62,6 @@ type ModelAsset = BaseAsset & {
62
62
  export class ModelManager {
63
63
  /**
64
64
  * CAUTION: this has to be in sync with the Combeenation backend!
65
- * @internal
66
65
  */
67
66
  public static readonly CBN_FALLBACK_MODEL_ASSET_NAME = '$fallback';
68
67
 
@@ -97,7 +97,7 @@ export type ParameterObserver = (payload: ParameterObserverPayload) => Promise<v
97
97
 
98
98
  /**
99
99
  * Payload of parameter observer.\
100
- * Contains current data of parameter entry, which can be usefull for implementing the dedicated observer
100
+ * Contains current data of parameter entry, which can be useful for implementing the dedicated observer
101
101
  */
102
102
  export type ParameterObserverPayload = {
103
103
  subject: ParameterSubject;
@@ -105,7 +105,7 @@ type SceneAsset = BaseAsset & {
105
105
  * This contains creating or loading scenes with certain settings for lighting, cameras and appearance in genereal.
106
106
  */
107
107
  export class SceneManager {
108
- protected static _DEFAULT_SCENE_ASSET_NAME = '$default';
108
+ protected static _DEFAULT_SCENE_ASSET_NAME = '$defaultScene';
109
109
  protected static _DEFAULT_GROUND_PLANE_NAME = 'BackgroundPlane';
110
110
  protected static _DEFAULT_GROUND_SIZING_FACTOR = 5;
111
111
 
@@ -294,10 +294,19 @@ export class SceneManager {
294
294
  const hasInfiniteDistance = mesh.infiniteDistance;
295
295
  // ignore meshes with "BackgroundMaterial" - usually a ground or skybox
296
296
  const hasBackgroundMaterial = mesh.material instanceof BackgroundMaterial;
297
+ // ignore dummy meshes for html anchor occlusion check
298
+ const isHtmlAnchorMesh = this.viewer.htmlAnchorManager.isHtmlAnchorMesh(mesh);
297
299
  // ignore excluded meshes
298
300
  const isExcluded = excludeGeometry ? isNodeExcluded(mesh, excludeGeometry) : false;
299
301
 
300
- return isEnabled && hasValidBBoxInfo && !hasInfiniteDistance && !hasBackgroundMaterial && !isExcluded;
302
+ return (
303
+ isEnabled &&
304
+ hasValidBBoxInfo &&
305
+ !hasInfiniteDistance &&
306
+ !hasBackgroundMaterial &&
307
+ !isHtmlAnchorMesh &&
308
+ !isExcluded
309
+ );
301
310
  })
302
311
  .reduce(
303
312
  (accBBoxMinMax, curMesh, idx) => {
@@ -399,7 +408,7 @@ export class SceneManager {
399
408
  // camera
400
409
  if (cameraSettings.create) {
401
410
  const camera = new ArcRotateCamera(
402
- `${SceneManager._DEFAULT_SCENE_ASSET_NAME}.camera`,
411
+ `${SceneManager._DEFAULT_SCENE_ASSET_NAME}_camera`,
403
412
  cameraSettings.initialPosition.alpha,
404
413
  cameraSettings.initialPosition.beta,
405
414
  cameraSettings.initialPosition.radius,
package/src/viewer.ts CHANGED
@@ -5,10 +5,12 @@ import {
5
5
  CameraManager,
6
6
  DebugManager,
7
7
  DefaultSceneSettings,
8
+ DimensionLineManager,
8
9
  Engine,
9
10
  EngineOptions,
10
11
  EventManager,
11
12
  GltfExportManager,
13
+ HtmlAnchorManager,
12
14
  MaterialManager,
13
15
  ModelManager,
14
16
  NullEngine,
@@ -80,8 +82,10 @@ export class Viewer {
80
82
 
81
83
  protected _cameraManager!: CameraManager;
82
84
  protected _debugManager!: DebugManager;
85
+ protected _dimensionLineManager!: DimensionLineManager;
83
86
  protected _eventManager!: EventManager;
84
87
  protected _gltfExportManager!: GltfExportManager;
88
+ protected _htmlAnchorManager!: HtmlAnchorManager;
85
89
  protected _materialManager!: MaterialManager;
86
90
  protected _modelManager!: ModelManager;
87
91
  protected _parameterManager!: ParameterManager;
@@ -119,12 +123,18 @@ export class Viewer {
119
123
  get debugManager(): DebugManager {
120
124
  return this._debugManager;
121
125
  }
126
+ get dimensionLineManager(): DimensionLineManager {
127
+ return this._dimensionLineManager;
128
+ }
122
129
  get eventManager(): EventManager {
123
130
  return this._eventManager;
124
131
  }
125
132
  get gltfExportManager(): GltfExportManager {
126
133
  return this._gltfExportManager;
127
134
  }
135
+ get htmlAnchorManager(): HtmlAnchorManager {
136
+ return this._htmlAnchorManager;
137
+ }
128
138
  get materialManager(): MaterialManager {
129
139
  return this._materialManager;
130
140
  }
@@ -171,7 +181,7 @@ export class Viewer {
171
181
  }
172
182
 
173
183
  /**
174
- * Pause rendering can be usefull when doing internal scene processings that should not be visible to the user,
184
+ * Pause rendering can be useful when doing internal scene processings that should not be visible to the user,
175
185
  * e.g. cloning meshes for gltf export
176
186
  */
177
187
  public pauseRendering(): void {
@@ -227,14 +237,20 @@ export class Viewer {
227
237
  }
228
238
 
229
239
  this._scene = new Scene(engine);
240
+ // NOTE: rendering group id "1" is reserved for occlusion helper sphere (see `HtmlAnchorManager`)
241
+ // rendering group id "1" has the same depth buffer as 0 in order to make this work
242
+ // => use rendering group id "2" or higher for keeping meshes in foreground
243
+ this._scene.setRenderingAutoClearDepthStencil(1, false, false, false);
230
244
 
231
245
  // NOTE: order of manage seems to be a bit counter intuitive as it's sorted alphabetically
232
246
  // semantically one would start with scene manager, etc...
233
247
  // still this is a good test as the order of manager constructors should not matter
234
248
  this._cameraManager = new CameraManager(this);
235
249
  this._debugManager = new DebugManager(this);
250
+ this._dimensionLineManager = new DimensionLineManager(this);
236
251
  this._eventManager = new EventManager(this);
237
252
  this._gltfExportManager = new GltfExportManager(this);
253
+ this._htmlAnchorManager = new HtmlAnchorManager(this);
238
254
  this._materialManager = new MaterialManager(this);
239
255
  this._modelManager = new ModelManager(this);
240
256
  this._parameterManager = new ParameterManager(this);