Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose Overlay3DCollection #4598

Merged
merged 11 commits into from
Jun 21, 2024
9 changes: 8 additions & 1 deletion viewer/api-entry-points/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,11 @@ export {
Image360AnnotationFilterOptions
} from '../packages/360-images';

export { OverlayCollection, OverlayInfo, Overlay3D, DefaultOverlay3DContentType } from '../packages/3d-overlays';
export {
OverlayCollection,
Overlay3DCollection,
Overlay3DCollectionOptions,
OverlayInfo,
Overlay3D,
DefaultOverlay3DContentType
} from '../packages/3d-overlays';
3 changes: 2 additions & 1 deletion viewer/packages/3d-overlays/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/*!
* Copyright 2023 Cognite AS
*/
export { Overlay3DCollection } from './src/Overlay3DCollection';
export { Overlay3DCollection, Overlay3DCollectionOptions } from './src/Overlay3DCollection';
export { OverlayPointsObject, OverlayPointsParameters } from './src/OverlayPointsObject';
export { Overlay3DIcon } from './src/Overlay3DIcon';
export { Overlay3D } from './src/Overlay3D';
export { OverlayCollection, OverlayInfo, DefaultOverlay3DContentType } from './src/OverlayCollection';
export { IconOctree } from './src/IconOctree';
export { TextOverlay } from './src/TextOverlay';
7 changes: 4 additions & 3 deletions viewer/packages/3d-overlays/src/IconOctree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import pullAll from 'lodash/pullAll';
import { Node, PointOctant, PointOctree } from 'sparse-octree';
import { Box3, Matrix4, Vector3 } from 'three';
import { Overlay3DIcon } from './Overlay3DIcon';
import { DefaultOverlay3DContentType } from './OverlayCollection';

type NodeMetadata = {
icon: Overlay3DIcon;
level: number;
};

export class IconOctree extends PointOctree<Overlay3DIcon> {
export class IconOctree<ContentType = DefaultOverlay3DContentType> extends PointOctree<Overlay3DIcon<ContentType>> {
private readonly _nodeCenters: Map<Node, NodeMetadata>;

public static getMinimalOctreeBoundsFromIcons(icons: Overlay3DIcon[]): Box3 {
public static getMinimalOctreeBoundsFromIcons<ContentType>(icons: Overlay3DIcon<ContentType>[]): Box3 {
return new Box3().setFromPoints(icons.map(icon => icon.getPosition()));
}

constructor(icons: Overlay3DIcon[], bounds: Box3, maxLeafSize: number) {
constructor(icons: Overlay3DIcon<ContentType>[], bounds: Box3, maxLeafSize: number) {
super(bounds.min, bounds.max, 0, maxLeafSize);
icons.forEach(icon => this.set(icon.getPosition(), icon));
this.filterEmptyLeaves();
Expand Down
66 changes: 58 additions & 8 deletions viewer/packages/3d-overlays/src/Overlay3DCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,39 @@
* Copyright 2023 Cognite AS
*/

import { CanvasTexture, Color, Texture, Object3D, type Camera } from 'three';
import { CanvasTexture, Color, Texture, Object3D, Camera, Vector2, Raycaster } from 'three';
import { Overlay3DIcon } from './Overlay3DIcon';
import { Overlay3D } from './Overlay3D';
import { OverlayPointsObject } from './OverlayPointsObject';
import { IconOctree } from './IconOctree';
import { DefaultOverlay3DContentType, OverlayCollection, OverlayInfo } from './OverlayCollection';
import minBy from 'lodash/minBy';

/**
* Constructor options for the Overlay3DCollection
*/
export type Overlay3DCollectionOptions = {
/**
* The texture to display as icons in this collection
*/
overlayTexture?: Texture;
/**
* A texture mask for marking what pixels are transparent in the supplied overlayTexture
*/
overlayTextureMask?: Texture;
/**
* The maximum display size of each icon in pixels
*/
maxPointSize?: number;
/**
* The default color to apply to overlay icons without a color on their own
*/
defaultOverlayColor?: Color;
};

/**
* A collection of overlay icons with associated data
*/
export class Overlay3DCollection<MetadataType = DefaultOverlay3DContentType>
extends Object3D
implements OverlayCollection<MetadataType>
Expand All @@ -33,9 +52,10 @@ export class Overlay3DCollection<MetadataType = DefaultOverlay3DContentType>
private readonly _iconRadius = 0.4;
private _overlays: Overlay3DIcon<MetadataType>[];
//@ts-ignore Will be removed when clustering is added.
private _octree: IconOctree;
private _octree: IconOctree<MetadataType>;
private readonly _rayCaster = new Raycaster();

constructor(overlayInfos?: OverlayInfo<MetadataType>[], options?: Overlay3DCollectionOptions) {
constructor(overlayInfos: OverlayInfo<MetadataType>[], options?: Overlay3DCollectionOptions) {
super();

this.defaultOverlayColor = options?.defaultOverlayColor ?? this.defaultOverlayColor;
Expand All @@ -62,14 +82,23 @@ export class Overlay3DCollection<MetadataType = DefaultOverlay3DContentType>
this._octree = this.rebuildOctree();
}

/**
* Set whether this collection is visible or not
*/
setVisibility(visibility: boolean): void {
this._overlayPoints.visible = visibility;
}

/**
* Get the overlay icons contained in this collection
*/
getOverlays(): Overlay3D<MetadataType>[] {
return this._overlays;
}

/**
* Add more overlays into this collection
*/
public addOverlays(overlayInfos: OverlayInfo<MetadataType>[]): Overlay3D<MetadataType>[] {
if (overlayInfos.length + this._overlays.length > this.DefaultMaxPoints)
throw new Error('Cannot add more than ' + this.DefaultMaxPoints + ' points');
Expand All @@ -83,33 +112,51 @@ export class Overlay3DCollection<MetadataType = DefaultOverlay3DContentType>
return newIcons;
}

sortOverlaysRelativeToCamera(camera: Camera): void {
private sortOverlaysRelativeToCamera(camera: Camera): void {
this._overlays = this._overlays.sort((a, b) => {
return b.getPosition().distanceToSquared(camera.position) - a.getPosition().distanceToSquared(camera.position);
});

this.updatePointsObject();
}

/**
* Remove the listed overlays from this collection
*/
public removeOverlays(overlays: Overlay3D<MetadataType>[]): void {
this._overlays = this._overlays.filter(overlay => !overlays.includes(overlay));

this.updatePointsObject();
this._octree = this.rebuildOctree();
}

/**
* Clean up all icons in this collection
*/
public removeAllOverlays(): void {
this._overlays = [];

this.updatePointsObject();
this._octree = this.rebuildOctree();
}

private rebuildOctree(): IconOctree {
const icons = this._overlays as Overlay3DIcon[];
/**
* Run intersection on icons in this collection. Returns the closest hit
*/
public intersectOverlays(normalizedCoordinates: Vector2, camera: Camera): Overlay3D<MetadataType> | undefined {
this._rayCaster.setFromCamera(normalizedCoordinates.clone(), camera);

const intersections = this._overlays.filter(icon => {
return icon.getVisible() && icon.intersect(this._rayCaster.ray) !== null;
});

return minBy(intersections, a => a.getPosition().clone().sub(this._rayCaster.ray.origin).length());
}

private rebuildOctree(): IconOctree<MetadataType> {
const icons = this._overlays;
const octreeBounds = IconOctree.getMinimalOctreeBoundsFromIcons(icons);

return new IconOctree(icons, octreeBounds, 2);
return new IconOctree<MetadataType>(icons, octreeBounds, 2);
}

private updatePointsObject(): void {
Expand Down Expand Up @@ -142,6 +189,9 @@ export class Overlay3DCollection<MetadataType = DefaultOverlay3DContentType>
});
}

/**
* Dispose this collection and icons with all associated resources
*/
public dispose(): void {
this._overlays.forEach(overlay => overlay.dispose());

Expand Down
78 changes: 78 additions & 0 deletions viewer/packages/3d-overlays/src/TextOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*!
* Copyright 2024 Cognite AS
*/
export class TextOverlay {
private readonly _textOverlay: HTMLElement;
private readonly _defaultTextOverlayToCursorOffset = 20;
private _textOverlayVisible = false;

constructor(domElement: HTMLElement) {
this._textOverlay = this.createTextOverlay('', this._defaultTextOverlayToCursorOffset);
domElement.appendChild(this._textOverlay);
}

get textOverlay(): HTMLElement {
return this._textOverlay;
}

/**
* Sets whether text overlay is visible.
* Default is false.
*/
setTextOverlayEnabled(visible: boolean): void {
this._textOverlayVisible = visible;
this._textOverlay.style.opacity = visible ? '1' : '0';
}

/**
* Resets (i.e. hides) text overlay before recalculating position/visibility
*/
reset(): void {
this._textOverlay.style.opacity = '0';
}

/**
* Gets whether text overlay is visible.
*/
getTextOverlayEnabled(): boolean {
return this._textOverlayVisible;
}

dispose(): void {
this._textOverlay.remove();
}

public positionTextOverlay(event: MouseEvent): void {
const { _textOverlay, _textOverlayVisible } = this;
_textOverlay.style.left = `${event.offsetX}px`;
_textOverlay.style.top = `${event.offsetY}px`;
_textOverlay.style.opacity = _textOverlayVisible ? '1' : '0';
}

private createTextOverlay(text: string, horizontalOffset: number): HTMLElement {
const textOverlay = document.createElement('div');
textOverlay.innerText = text;
textOverlay.setAttribute('class', 'text-overlay');
textOverlay.style.cssText = `
position: absolute;

/* Anchor to the center of the element and ignore events */
transform: translate(${horizontalOffset}px, -50%);
touch-action: none;
user-select: none;

padding: 7px;
max-width: 200px;
color: #fff;
background: #232323da;
border-radius: 5px;
border: '#ffffff22 solid 2px;
opacity: 0;
transition: opacity 0.3s;
opacity: 0;
z-index: 10;
`;

return textOverlay;
}
}
Loading
Loading