diff --git a/viewer/api-entry-points/core.ts b/viewer/api-entry-points/core.ts index 69ed7aa6b64..bbd27f3cbfa 100644 --- a/viewer/api-entry-points/core.ts +++ b/viewer/api-entry-points/core.ts @@ -84,6 +84,7 @@ export { Image360Collection, Image360EnteredDelegate, Image360ExitedDelegate, + Image360IconStyle, Image360AnnotationIntersection, Image360AnnotationAppearance, Image360Annotation, diff --git a/viewer/packages/360-images/index.ts b/viewer/packages/360-images/index.ts index 0b12b7c29d7..01a353c7974 100644 --- a/viewer/packages/360-images/index.ts +++ b/viewer/packages/360-images/index.ts @@ -2,7 +2,7 @@ * Copyright 2022 Cognite AS */ -export { Image360 } from './src/entity/Image360'; +export { Image360, Image360IconStyle } from './src/entity/Image360'; export { Image360Revision } from './src/entity/Image360Revision'; export { Image360Collection, diff --git a/viewer/packages/360-images/src/entity/Image360.ts b/viewer/packages/360-images/src/entity/Image360.ts index 42caeb40e7a..144e69a2bcf 100644 --- a/viewer/packages/360-images/src/entity/Image360.ts +++ b/viewer/packages/360-images/src/entity/Image360.ts @@ -5,6 +5,18 @@ import { Image360Revision } from './Image360Revision'; import { Image360Visualization } from './Image360Visualization'; +import { Color } from 'three'; + +/** + * Image360 icon style + */ +export type Image360IconStyle = { + /** + * A color tint to apply to the 360 icon + */ + color?: Color; +}; + /** * A single 360 image "station", which may consist of several revisions * captured in approximately the same location @@ -46,4 +58,16 @@ export interface Image360 { * @returns The active revision. */ getActiveRevision(): Image360Revision; + + /** + * Get a copy of the color assigned to the icon of this entity + * + * @returns The currently assign color, or 'default' if none is assigned + */ + getIconColor(): Color | 'default'; + + /** + * Assign a color to the icon of this entity + */ + setIconColor(color: Color | 'default'): void; } diff --git a/viewer/packages/360-images/src/entity/Image360Entity.ts b/viewer/packages/360-images/src/entity/Image360Entity.ts index a68dfd781a1..d19e3140cfe 100644 --- a/viewer/packages/360-images/src/entity/Image360Entity.ts +++ b/viewer/packages/360-images/src/entity/Image360Entity.ts @@ -12,6 +12,9 @@ import { Image360VisualizationBox } from './Image360VisualizationBox'; import { ImageAnnotationObject } from '../annotation/ImageAnnotationObject'; import { Overlay3DIcon } from '@reveal/3d-overlays'; import { Image360AnnotationFilter } from '../annotation/Image360AnnotationFilter'; +import { Color } from 'three'; + +import cloneDeep from 'lodash/cloneDeep'; export class Image360Entity implements Image360 { private readonly _revisions: Image360RevisionEntity[]; @@ -20,6 +23,7 @@ export class Image360Entity implements Image360 { private readonly _image360Icon: Overlay3DIcon; private readonly _image360VisualizationBox: Image360VisualizationBox; private _activeRevision: Image360RevisionEntity; + private _iconColor: Color | 'default' = 'default'; /** * Get a copy of the model-to-world transformation matrix @@ -137,6 +141,15 @@ export class Image360Entity implements Image360 { this._image360VisualizationBox.unloadImages(); } + public getIconColor(): Color | 'default' { + return cloneDeep(this._iconColor); + } + + public setIconColor(color: Color | 'default'): void { + this._iconColor = color; + this._image360Icon.setColor(color); + } + public activateAnnotations(): void { const setAndShowAnnotations = async () => { const annotations = await this._activeRevision.getAnnotations(); diff --git a/viewer/packages/360-images/src/icons/IconCollection.ts b/viewer/packages/360-images/src/icons/IconCollection.ts index a4e72377d6e..2deea3e4f4f 100644 --- a/viewer/packages/360-images/src/icons/IconCollection.ts +++ b/viewer/packages/360-images/src/icons/IconCollection.ts @@ -83,9 +83,9 @@ export class IconCollection { spriteTexture: sharedTexture, minPixelSize: IconCollection.MinPixelSize, maxPixelSize: this._maxPixelSize, - radius: this._iconRadius + radius: this._iconRadius, + maskTexture: sharedTexture }); - iconsSprites.setPoints(points); const spriteTexture = this.createHoverIconTexture(); this._hoverSprite = this.createHoverSprite(spriteTexture); @@ -133,7 +133,11 @@ export class IconCollection { this._icons.forEach(icon => (icon.culled = true)); selectedIcons.forEach(icon => (icon.culled = false)); - iconSprites.setPoints(selectedIcons.filter(icon => icon.getVisible()).map(icon => icon.getPosition())); + const visibleIcons = selectedIcons.filter(icon => icon.getVisible()); + iconSprites.setPoints( + visibleIcons.map(icon => icon.getPosition()), + visibleIcons.map(icon => icon.getColor()) + ); }; } @@ -159,11 +163,12 @@ export class IconCollection { this._icons.forEach(icon => (icon.culled = true)); closestPoints.forEach(icon => (icon.culled = false)); + + const closestVisibleReversedPoints = closestPoints.filter(icon => icon.getVisible()).reverse(); + iconSprites.setPoints( - closestPoints - .filter(icon => icon.getVisible()) - .reverse() - .map(p => p.getPosition()) + closestVisibleReversedPoints.map(p => p.getPosition()), + closestVisibleReversedPoints.map(p => p.getColor()) ); }; } diff --git a/viewer/packages/360-images/unit-tests/Image360Entity.test.ts b/viewer/packages/360-images/unit-tests/Image360Entity.test.ts index 0ed2d24b9c9..c3b16ac5be0 100644 --- a/viewer/packages/360-images/unit-tests/Image360Entity.test.ts +++ b/viewer/packages/360-images/unit-tests/Image360Entity.test.ts @@ -12,39 +12,79 @@ import { DeviceDescriptor, SceneHandler } from '@reveal/utilities'; import { Historical360ImageSet } from '@reveal/data-providers/src/types'; import { Image360AnnotationFilter } from '../src/annotation/Image360AnnotationFilter'; +function createMockImage360(options?: { customTranslation?: THREE.Matrix4 }) { + const image360Descriptor: Historical360ImageSet = { + id: '0', + label: 'testEntity', + collectionId: '0', + collectionLabel: 'test_collection', + transform: new THREE.Matrix4(), + imageRevisions: [ + { + timestamp: undefined, + faceDescriptors: [] + } + ] + }; + + const mockSceneHandler = new Mock().setup(p => p.addCustomObject(It.IsAny())).returns(); + const mock360ImageProvider = new Mock>(); + const mock360ImageIcon = new Overlay3DIcon( + { position: new THREE.Vector3(), minPixelSize: 10, maxPixelSize: 10, iconRadius: 10 }, + {} + ); + + const testTranslation = options?.customTranslation ?? new THREE.Matrix4(); + const desktopDevice: DeviceDescriptor = { deviceType: 'desktop' }; + + return new Image360Entity( + image360Descriptor, + mockSceneHandler.object(), + mock360ImageProvider.object(), + new Image360AnnotationFilter({}), + testTranslation, + mock360ImageIcon, + desktopDevice + ); +} + describe(Image360Entity.name, () => { test('transformation should be respected', () => { - const image360Descriptor: Historical360ImageSet = { - id: '0', - label: 'testEntity', - collectionId: '0', - collectionLabel: 'test_collection', - transform: new THREE.Matrix4(), - imageRevisions: [ - { - timestamp: undefined, - faceDescriptors: [] - } - ] - }; - - const mockSceneHandler = new Mock().setup(p => p.addCustomObject(It.IsAny())).returns(); - const mock360ImageProvider = new Mock>(); - const mock360ImageIcon = new Mock().object(); - const testTranslation = new THREE.Matrix4().makeTranslation(4, 5, 6); - const desktopDevice: DeviceDescriptor = { deviceType: 'desktop' }; - - const entity = new Image360Entity( - image360Descriptor, - mockSceneHandler.object(), - mock360ImageProvider.object(), - new Image360AnnotationFilter({}), - testTranslation, - mock360ImageIcon, - desktopDevice - ); + const entity = createMockImage360({ customTranslation: testTranslation }); expect(entity.transform.equals(testTranslation)).toBeTrue(); }); + + test('set icon color is returned in getter', () => { + const entity = createMockImage360(); + + const originalColor = entity.getIconColor(); + + expect(originalColor).toBe('default'); + + const testColor = new THREE.Color(0.2, 0.3, 0.4); + entity.setIconColor(testColor); + + const gottenColor = entity.getIconColor(); + + expect(gottenColor).not.toBe('default'); + expect((gottenColor as THREE.Color).toArray()).toEqual(testColor.toArray()); + }); + + test('setting undefined icon color resets image360 icon color', () => { + const entity = createMockImage360(); + + const testColor = new THREE.Color(0.2, 0.3, 0.4); + entity.setIconColor(testColor); + + const firstColor = entity.getIconColor(); + + expect(firstColor).not.toBe('default'); + + entity.setIconColor('default'); + const secondColor = entity.getIconColor(); + + expect(secondColor).toBe('default'); + }); }); diff --git a/viewer/packages/360-images/visual-tests/Image360.VisualTest.ts b/viewer/packages/360-images/visual-tests/Image360.VisualTest.ts index a8e7e21e0fb..d3e7eeff6d4 100644 --- a/viewer/packages/360-images/visual-tests/Image360.VisualTest.ts +++ b/viewer/packages/360-images/visual-tests/Image360.VisualTest.ts @@ -36,9 +36,14 @@ export default class Image360VisualTestFixture extends StreamingVisualTestFixtur camera.near = 0.01; camera.updateProjectionMatrix(); + + camera.position.set(11.67, 4.15, -2.89); + camera.rotation.set(-0.4, 0.84, 0.3); + const desktopDevice: DeviceDescriptor = { deviceType: 'desktop' }; const { facade, entities } = await this.setup360Images(cogniteClient, sceneHandler, onBeforeRender, desktopDevice); + entities[1].setIconColor(new THREE.Color(1.0, 0.0, 1.0)); const icons = entities.map(entity => entity.icon); sceneHandler.addCustomObject(this.getOctreeVisualizationObject(icons)); diff --git a/viewer/packages/360-images/visual-tests/__image_snapshots__/Image360.VisualTest.png b/viewer/packages/360-images/visual-tests/__image_snapshots__/Image360.VisualTest.png index 5887b64172c..0941b009532 100644 Binary files a/viewer/packages/360-images/visual-tests/__image_snapshots__/Image360.VisualTest.png and b/viewer/packages/360-images/visual-tests/__image_snapshots__/Image360.VisualTest.png differ diff --git a/viewer/packages/3d-overlays/src/Overlay3DIcon.ts b/viewer/packages/3d-overlays/src/Overlay3DIcon.ts index 360d75eaa9d..3beb152e77e 100644 --- a/viewer/packages/3d-overlays/src/Overlay3DIcon.ts +++ b/viewer/packages/3d-overlays/src/Overlay3DIcon.ts @@ -36,6 +36,7 @@ export class Overlay3DIcon implements private readonly _hoverSprite?: THREE.Sprite; private readonly _content: ContentType; private readonly _raycastBoundingSphere = new Sphere(); + private readonly _defaultColor: Color; private _adaptiveScale = 1; private _visible = true; @@ -58,6 +59,7 @@ export class Overlay3DIcon implements this._hoverSprite = hoverSprite; this._content = content; this._color = color ?? this._color; + this._defaultColor = this._color; this._setAdaptiveScale = this.setupAdaptiveScaling(position); @@ -127,9 +129,13 @@ export class Overlay3DIcon implements } } - setColor(color: Color): void { - this._color = color; - this._events.parametersChange.fire({ color, visble: this.getVisible() }); + setColor(color: Color | 'default'): void { + if (color === 'default') { + this._color = this._defaultColor; + } else { + this._color = color; + } + this._events.parametersChange.fire({ color: this._color, visble: this.getVisible() }); } getColor(): Color { diff --git a/viewer/packages/3d-overlays/src/overlay3DIcon.frag b/viewer/packages/3d-overlays/src/overlay3DIcon.frag index 97fe3bad91d..f468bcc9b77 100644 --- a/viewer/packages/3d-overlays/src/overlay3DIcon.frag +++ b/viewer/packages/3d-overlays/src/overlay3DIcon.frag @@ -14,8 +14,8 @@ in vec3 vColor; out vec4 fragmentColor; void main() { - vec4 colorSample = texture(colorTexture, gl_PointCoord); - + vec4 colorSample = texture(colorTexture, gl_PointCoord); + float computedAlpha = colorSample.a; vec3 computedColor = colorSample.rgb; @@ -23,8 +23,8 @@ void main() { vec4 maskSample = texture(maskTexture, gl_PointCoord); computedAlpha = colorSample.a + (1. - colorSample.a) * maskSample.r; - computedColor = mix(vColor * maskSample.r, colorSample.rgb, colorSample.a); + computedColor = mix(colorSample.rgb, vColor, maskSample.r); #endif - + fragmentColor = vec4(computedColor * colorTint, computedAlpha * collectionOpacity); -} \ No newline at end of file +} diff --git a/viewer/reveal.api.md b/viewer/reveal.api.md index 8f0c5dfff67..948283f03b5 100644 --- a/viewer/reveal.api.md +++ b/viewer/reveal.api.md @@ -794,10 +794,12 @@ export type HtmlOverlayToolOptions = { // @public export interface Image360 { getActiveRevision(): Image360Revision; + getIconColor(): Color | 'default'; getRevisions(): Image360Revision[]; readonly id: string; readonly image360Visualization: Image360Visualization; readonly label: string | undefined; + setIconColor(color: Color | 'default'): void; readonly transform: THREE.Matrix4; } @@ -867,6 +869,11 @@ export type Image360EnteredDelegate = (image360: Image360, revision: Image360Rev // @public export type Image360ExitedDelegate = () => void; +// @public +export type Image360IconStyle = { + color?: Color; +}; + // @public export interface Image360Revision { readonly date: Date | undefined;