@combeenation/3d-viewer 18.0.0-beta1 → 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 (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 +126 -0
  15. package/dist/lib-cjs/manager/dimension-line-manager.js +138 -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 +9 -2
  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 +255 -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 +11 -2
  39. package/src/viewer.ts +17 -1
@@ -0,0 +1,255 @@
1
+ import { Color3, LinesMesh, MeshBuilder, Tags, TransformNode, Vector3, Viewer } from '../index';
2
+ import { merge } from 'lodash-es';
3
+
4
+ /**
5
+ * General options that are used for each dimension line
6
+ */
7
+ export type DefaultDimensionLineOptions = {
8
+ /**
9
+ * Default: `Color3.Black()`
10
+ */
11
+ lineColor: Color3;
12
+ /**
13
+ * Default: 50mm
14
+ */
15
+ closingLineHeight: number;
16
+ /**
17
+ * Height of dimension label
18
+ *
19
+ * Default: 24px
20
+ */
21
+ labelHeight: number;
22
+ /**
23
+ * Corner radius of dimension label
24
+ *
25
+ * Default: 1/2 of `labelHeight`, to create a half circle
26
+ */
27
+ borderRadius?: number;
28
+ /**
29
+ * Text padding in x direction
30
+ *
31
+ * Default: 1/2 of `labelHeight`
32
+ */
33
+ textPadding?: number;
34
+ /**
35
+ * Default: Arial
36
+ */
37
+ fontFamily: string;
38
+ /**
39
+ * Text color of dimension label
40
+ *
41
+ * Default: black
42
+ */
43
+ textColor: string;
44
+ /**
45
+ * Color of border
46
+ *
47
+ * Default: black
48
+ */
49
+ borderColor: string;
50
+ /**
51
+ * Background color of dimension label
52
+ *
53
+ * Default: white
54
+ */
55
+ backgroundColor: string;
56
+ /**
57
+ * `true`: html elements can be occluded by other meshes
58
+ * `false`: html elements will always be shown in front of the scene
59
+ *
60
+ * Default: `false`
61
+ */
62
+ hideIfOccluded?: boolean;
63
+ /**
64
+ * `true`: html element size is relative to camera distance, just like a "normal" 3d object
65
+ * `false`: html element size remains constant
66
+ *
67
+ * Default: `false`
68
+ */
69
+ scaleWithCameraDistance?: boolean;
70
+ };
71
+
72
+ /**
73
+ * Specific options for each dimension line
74
+ */
75
+ export type DimensionLineOptions = Partial<DefaultDimensionLineOptions> & {
76
+ /**
77
+ * Optional parent node of dimension line group, can be used for easier calculation of start and end point
78
+ *
79
+ * Default: no parent node set
80
+ */
81
+ parentNode?: TransformNode;
82
+ /**
83
+ * Optional direction of the closing lines on start and end.\
84
+ * Use built-in vector functions like `Vector3.Left()` or `Vector3.Forward()` for simple directions of use a custom
85
+ * normal vector like `Vector3(1, 0.5 ,0)`.
86
+ *
87
+ * Default: x or y direction is taken, depending on the normal vector between start and end point
88
+ */
89
+ closingLineDirection?: Vector3;
90
+ /**
91
+ * Optional label text
92
+ *
93
+ * Default: dimension line size is calculated from `startPoint` and `endPoint` and suffixed with "mm" (e.g. "1200 mm")
94
+ */
95
+ labelText?: string;
96
+ };
97
+
98
+ /**
99
+ * Manager for dimension lines, which can be used to display certain dimensions like object sizes in the scene
100
+ */
101
+ export class DimensionLineManager {
102
+ public static readonly DIMENSION_LINE_KEY = '$dimLine';
103
+
104
+ protected _defaultLineOptions: DefaultDimensionLineOptions = {
105
+ lineColor: Color3.Black(),
106
+ closingLineHeight: 50,
107
+ labelHeight: 24,
108
+ fontFamily: 'Arial',
109
+ textColor: 'black',
110
+ borderColor: 'black',
111
+ backgroundColor: 'white',
112
+ hideIfOccluded: false,
113
+ scaleWithCameraDistance: false,
114
+ };
115
+
116
+ // map with all dimension lines that are currently visible in the scene
117
+ // holds data of all disposable objects for a clean up
118
+ protected _dimensionLineObjs: {
119
+ [name: string]: {
120
+ linesMesh: LinesMesh;
121
+ htmlLabelName: string;
122
+ };
123
+ } = {};
124
+
125
+ /** @internal */
126
+ public constructor(protected viewer: Viewer) {}
127
+
128
+ /**
129
+ * Define dimension line options that are used for each line by default.\
130
+ * Options can be overwritten by each line individually.
131
+ */
132
+ public setDefaultDimensionLineOptions(defaultLineOptions: Partial<DefaultDimensionLineOptions>): void {
133
+ merge(this._defaultLineOptions, defaultLineOptions);
134
+ }
135
+
136
+ /**
137
+ * Create dimension line between two points
138
+ */
139
+ public addDimensionLine(name: string, startPoint: Vector3, endPoint: Vector3, options?: DimensionLineOptions): void {
140
+ if (this._dimensionLineObjs[name]) {
141
+ console.warn(`Dimension line "${name}" already exists`);
142
+ return;
143
+ }
144
+
145
+ const {
146
+ parentNode,
147
+ closingLineDirection: closingLineDirectionIn,
148
+ labelText: labelTextIn,
149
+ ...inputDefaultOptions
150
+ } = options ?? {};
151
+
152
+ const resDefaultOptions = { ...this._defaultLineOptions };
153
+ merge(resDefaultOptions, inputDefaultOptions);
154
+ const {
155
+ lineColor,
156
+ closingLineHeight,
157
+ labelHeight,
158
+ textPadding,
159
+ borderRadius,
160
+ fontFamily,
161
+ textColor,
162
+ borderColor,
163
+ backgroundColor,
164
+ hideIfOccluded,
165
+ scaleWithCameraDistance,
166
+ } = resDefaultOptions;
167
+
168
+ let closingLineDirection = closingLineDirectionIn;
169
+ if (!closingLineDirection) {
170
+ // evaluate closing line direction if not given
171
+ const normal = endPoint.subtract(startPoint);
172
+ const yIsMainDirection = Math.abs(normal.y) > Math.abs(normal.x) && Math.abs(normal.y) > Math.abs(normal.z);
173
+ closingLineDirection = yIsMainDirection ? Vector3.Right() : Vector3.Up();
174
+ }
175
+
176
+ // calculate and create main and closing lines
177
+ const lineCenter = Vector3.Center(startPoint, endPoint);
178
+ const relStartPt = startPoint.subtract(lineCenter);
179
+ const relEndPt = endPoint.subtract(lineCenter);
180
+ const closingLineOffsetVector = closingLineDirection.normalizeToNew().scale(closingLineHeight / 2 / 1000);
181
+ const closingLineStartPts = [relStartPt.add(closingLineOffsetVector), relStartPt.subtract(closingLineOffsetVector)];
182
+ const closingLineEndPts = [relEndPt.add(closingLineOffsetVector), relEndPt.subtract(closingLineOffsetVector)];
183
+
184
+ const linesMesh = MeshBuilder.CreateLineSystem(`${DimensionLineManager.DIMENSION_LINE_KEY}_${name}`, {
185
+ lines: [[relStartPt, relEndPt], closingLineStartPts, closingLineEndPts],
186
+ });
187
+ linesMesh.position = lineCenter;
188
+ linesMesh.color = lineColor;
189
+ // rendering group id 1 reserved for html anchor meshes, which are required for occlusion checking
190
+ linesMesh.renderingGroupId = hideIfOccluded ? 0 : 2;
191
+ // tag can be used to exclude dimension lines from autofocus or gltf export
192
+ Tags.AddTagsTo(linesMesh, DimensionLineManager.DIMENSION_LINE_KEY);
193
+ if (parentNode) {
194
+ linesMesh.parent = parentNode;
195
+ }
196
+
197
+ let labelText = labelTextIn;
198
+ if (labelText === undefined) {
199
+ // create default text if not explicitely overwritten by the function
200
+ const length = Vector3.Distance(startPoint, endPoint);
201
+ const lengthRounded = Math.round(length * 1000 * 100) / 100;
202
+ labelText = `${lengthRounded} mm`;
203
+ }
204
+
205
+ // create default html element
206
+ const span = document.createElement('span');
207
+ span.textContent = labelText;
208
+ span.style.display = 'block';
209
+ span.style.height = `${labelHeight}px`;
210
+ span.style.lineHeight = `${labelHeight}px`;
211
+ span.style.fontFamily = fontFamily;
212
+ span.style.fontSize = `${(labelHeight * 2) / 3}px`;
213
+ span.style.border = `1px solid ${borderColor}`;
214
+ span.style.paddingLeft = `${textPadding ?? labelHeight / 2}px`;
215
+ span.style.paddingRight = `${textPadding ?? labelHeight / 2}px`;
216
+ span.style.borderRadius = `${borderRadius ?? labelHeight / 2}px`;
217
+ span.style.color = textColor;
218
+ span.style.background = backgroundColor;
219
+
220
+ const htmlLabelName = `${DimensionLineManager.DIMENSION_LINE_KEY}_${name}`;
221
+ this.viewer.htmlAnchorManager.addHtmlAnchor(name, span, Vector3.Zero(), {
222
+ parentNode: linesMesh,
223
+ group: DimensionLineManager.DIMENSION_LINE_KEY,
224
+ hideIfOccluded,
225
+ scaleWithCameraDistance,
226
+ });
227
+
228
+ this._dimensionLineObjs[name] = { linesMesh, htmlLabelName };
229
+ }
230
+
231
+ /**
232
+ * Remove dimension line and disposes all associated ressources
233
+ */
234
+ public removeDimensionLine(name: string): void {
235
+ const dimensionLine = this._dimensionLineObjs[name];
236
+ if (!dimensionLine) {
237
+ console.warn(`Dimension line "${name}" does not exist`);
238
+ return;
239
+ }
240
+
241
+ this.viewer.htmlAnchorManager.removeHtmlAnchor(name);
242
+ dimensionLine.linesMesh.dispose(false, true);
243
+
244
+ delete this._dimensionLineObjs[name];
245
+ }
246
+
247
+ /**
248
+ * Remove all dimension lines that have been created with the `DimensionLineManager`
249
+ */
250
+ public removeAllDimensionLines(): void {
251
+ Object.keys(this._dimensionLineObjs).forEach(dimLineName => {
252
+ this.removeDimensionLine(dimLineName);
253
+ });
254
+ }
255
+ }
@@ -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;
@@ -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,