Skip to content

Commit

Permalink
feat: Add backward, forward and enter 360 image for navigation (#4827)
Browse files Browse the repository at this point in the history
* Renaming

* Add history

* Update Image360History.ts

* Image actions

* Fix

* Update Image360History.ts

* Refacoring

* Fix api

* Fix api

* Update reveal.api.md

* Fix api

* Update Image360UI.ts

* Diff Fixes

* Update Image360History.ts

* Update Image360History.ts

* Update Image360Action.ts

* Fix exceptions

* updated reveal.api.md file

* Update Image360ApiHelper.ts

* Update package.json

* Update package.json

* Update package.json

---------

Co-authored-by: Pramod S <[email protected]>
  • Loading branch information
nilscognite and pramodcog authored Oct 28, 2024
1 parent a14e906 commit 1cdd5c5
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 21 deletions.
3 changes: 2 additions & 1 deletion viewer/api-entry-points/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export {
Image360Annotation,
Image360AnnotationAssetFilter,
Image360AnnotationAssetQueryResult,
Image360AnnotationFilterOptions
Image360AnnotationFilterOptions,
Image360Action
} from '../packages/360-images';

export {
Expand Down
2 changes: 2 additions & 0 deletions viewer/packages/360-images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export { Image360AnnotationIntersection } from './src/annotation/Image360Annotat
export { Image360Annotation } from './src/annotation/Image360Annotation';
export { DefaultImage360Collection } from './src/collection/DefaultImage360Collection';
export { IconsOptions } from './src/icons/IconCollection';
export { Image360History } from './src/Image360History';
export { Image360Action } from './src/Image360Action';
26 changes: 26 additions & 0 deletions viewer/packages/360-images/src/Image360Action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*!
* Copyright 2024 Cognite AS
*/

/**
* 360 image action to be used by the CogniteViewer.canDo360Action and do360Action
* @beta
*/
export enum Image360Action {
/**
* When inside 360 image, forwards on the history list.
*/
Forward,
/**
* When inside 360 image, backwards on the history list.
*/
Backward,
/**
* When outside 360 image, go to current in the history list (where you exit from).
*/
Enter,
/**
* When inside 360, exit.
*/
Exit
}
69 changes: 69 additions & 0 deletions viewer/packages/360-images/src/Image360History.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*!
* Copyright 2022 Cognite AS
*/

import { Image360 } from './entity/Image360';
import { Image360Action } from './Image360Action';

export class Image360History {
private readonly _history: Image360[] = [];
private _currentIndex: number = -1; // Negative mean no current index (avoid empty because of harder logic)

public start(history: Image360): void {
if (this.isLegalIndex(this._currentIndex + 1)) {
this._history.slice(this._currentIndex + 1);
}
this._history.push(history);
this._currentIndex = this._history.length - 1;
}

private current(): Image360 | undefined {
if (!this.isLegalIndex(this._currentIndex)) {
return undefined;
}
return this._history[this._currentIndex];
}

public clear(): void {
this._history.splice(0, this._history.length);
this._currentIndex = -1;
}

private isLegalIndex(index: number): boolean {
return index >= 0 && index < this._history.length;
}

public canDoAction(action: Image360Action): boolean {
switch (action) {
case Image360Action.Forward:
return this.isLegalIndex(this._currentIndex + 1);
case Image360Action.Backward:
return this.isLegalIndex(this._currentIndex - 1);
case Image360Action.Enter:
return this.isLegalIndex(this._currentIndex);
default:
return false;
}
}

public doAction(action: Image360Action): Image360 | undefined {
let currentIndex = this._currentIndex;
switch (action) {
case Image360Action.Forward:
currentIndex++;
break;
case Image360Action.Backward:
currentIndex--;
break;
case Image360Action.Enter:
break;
default:
throw new Error(`Unknown movement ${action}`);
}
if (!this.isLegalIndex(currentIndex)) {
return undefined;
}
this._currentIndex = currentIndex;
return this.current();
}
}
68 changes: 53 additions & 15 deletions viewer/packages/api/src/api-helpers/Image360ApiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { MetricsLogger } from '@reveal/metrics';
import debounce from 'lodash/debounce';
import { Image360WithCollection } from '../public/types';
import { DEFAULT_IMAGE_360_OPACITY } from '@reveal/360-images/src/entity/Image360VisualizationBox';
import { Image360History } from '@reveal/360-images/src/Image360History';
import { Image360Action } from '@reveal/360-images/src/Image360Action';

export class Image360ApiHelper<DataSourceT extends DataSourceType> {
private readonly _image360Facade: Image360Facade<DataSourceT>;
Expand All @@ -53,6 +55,7 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
private _needsRedraw: boolean = false;
private readonly _hasEventListeners: boolean;
private readonly _inputHandler?: InputHandler;
private readonly _history = new Image360History();

private readonly _interactionState: {
currentImage360Hovered?: Image360Entity<DataSourceT>;
Expand All @@ -77,12 +80,9 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
private readonly _onBeforeSceneRenderedEvent: EventTrigger<BeforeSceneRenderedDelegate>;
private _cachedCameraManager: CameraManager | undefined;

private readonly exit360ImageOnEscapeKey = (event: KeyboardEvent) => this.exit360ImageOnEscape(event);
public readonly setHoverIconEventHandler = (event: MouseEvent): void =>
this.setHoverIconOnIntersect(event.offsetX, event.offsetY);

public readonly enter360ImageHandler = (event: PointerEventData): Promise<boolean> =>
this.enter360ImageOnIntersect(event);
private readonly onKeyPressed = (event: KeyboardEvent) => this.exit360ImageOnEscape(event);
public readonly onHover = (event: MouseEvent): void => this.setHoverIconOnIntersect(event.offsetX, event.offsetY);
public readonly onClick = (event: PointerEventData): Promise<boolean> => this.enter360ImageOnIntersect(event);

private readonly updateHoverStateOnRenderHandler = () => {
const lastOffset = this._interactionState.lastMousePosition;
Expand Down Expand Up @@ -134,9 +134,9 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
this._cachedCameraManager = activeCameraManager.innerCameraManager;
}
if (this._hasEventListeners) {
domElement.addEventListener('mousemove', this.setHoverIconEventHandler);
domElement.addEventListener('mousemove', this.onHover);
this._inputHandler = inputHandler;
this._inputHandler.on('click', this.enter360ImageHandler);
this._inputHandler.on('click', this.onClick);
}
onBeforeSceneRendered.subscribe(this.updateHoverStateOnRenderHandler);
}
Expand Down Expand Up @@ -244,7 +244,8 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {

public async enter360ImageInternal(
image360Entity: Image360Entity<DataSourceT>,
revision?: Image360RevisionEntity<DataSourceT>
revision?: Image360RevisionEntity<DataSourceT>,
updateHistory = true
): Promise<boolean> {
const revisionToEnter = revision ?? this.findRevisionIdToEnter(image360Entity);
if (revisionToEnter === this._interactionState.revisionSelectedForEntry) {
Expand Down Expand Up @@ -313,10 +314,13 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
}
this._transitionInProgress = false;
}
this._domElement.addEventListener('keydown', this.exit360ImageOnEscapeKey);
this._domElement.addEventListener('keydown', this.onKeyPressed);
this.applyFullResolutionTextures(revisionToEnter);

imageCollection.events.image360Entered.fire(image360Entity, revisionToEnter);
if (updateHistory) {
this._history.start(image360Entity);
}
return true;
}

Expand Down Expand Up @@ -481,7 +485,7 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
this._activeCameraManager.setActiveCameraManager(this._cachedCameraManager);
setCameraTarget1MeterInFrontOfCamera(this._activeCameraManager, position, rotation);
}
this._domElement.removeEventListener('keydown', this.exit360ImageOnEscapeKey);
this._domElement.removeEventListener('keydown', this.onKeyPressed);

function setCameraTarget1MeterInFrontOfCamera(manager: CameraManager, position: Vector3, rotation: Quaternion) {
manager.setCameraState({
Expand All @@ -491,15 +495,50 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
}
}

public canDoAction(action: Image360Action): boolean {
const insideImage = this._interactionState.currentImage360Entered !== undefined;
switch (action) {
case Image360Action.Exit:
return insideImage;

case Image360Action.Enter:
if (insideImage) {
return false;
}
default:
if (!insideImage) {
return false;
}
}
return this._history.canDoAction(action);
}

public async doAction(action: Image360Action): Promise<void> {
if (!this.canDoAction(action)) {
return;
}
switch (action) {
case Image360Action.Exit:
this.exit360Image();
return;
default:
const image360 = this._history.doAction(action);
if (image360 === undefined || !(image360 instanceof Image360Entity)) {
return;
}
await this.enter360ImageInternal(image360, undefined, false);
}
}

public dispose(): void {
this._onBeforeSceneRenderedEvent.unsubscribe(this.updateHoverStateOnRenderHandler);
if (this._hasEventListeners) {
this._domElement.removeEventListener('mousemove', this.setHoverIconEventHandler);
this._domElement.removeEventListener('mousemove', this.onHover);
if (this._inputHandler != undefined) {
this._inputHandler.off('click', this.enter360ImageHandler);
this._inputHandler.off('click', this.onClick);
}
}
this._domElement.removeEventListener('keydown', this.exit360ImageOnEscapeKey);
this._domElement.removeEventListener('keydown', this.onKeyPressed);

if (this._stationaryCameraManager && this._cachedCameraManager) {
if (this._activeCameraManager.innerCameraManager === this._stationaryCameraManager) {
Expand Down Expand Up @@ -590,7 +629,6 @@ export class Image360ApiHelper<DataSourceT extends DataSourceType> {
if (event.key !== 'Escape') {
return;
}

const lastEntered = this._interactionState.currentImage360Entered;
if (lastEntered !== undefined) {
const transitionOutDuration = 600;
Expand Down
35 changes: 30 additions & 5 deletions viewer/packages/api/src/public/migration/Cognite3DViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { AsyncSequencer, SequencerFunction } from '../../../../utilities/src/Asy
import { getModelAndRevisionId } from '../../utilities/utils';
import { ClassicDataSourceType, DataSourceType, isClassicIdentifier } from '@reveal/data-providers';
import assert from 'assert';
import { Image360Action } from '@reveal/360-images/src/Image360Action';

type Cognite3DViewerEvents =
| 'click'
Expand Down Expand Up @@ -924,7 +925,7 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
*/
remove360Images(...image360Entities: Image360<DataSourceT>[]): Promise<void> {
if (this._cdfSdkClient === undefined || this._image360ApiHelper === undefined) {
throw new Error(`Adding 360 image sets is only supported when connecting to Cognite Data Fusion`);
throw new Error(`Remove 360 images is only supported when connecting to Cognite Data Fusion`);
}
return this._image360ApiHelper.remove360Images(
image360Entities.map(entity => entity as Image360Entity<DataSourceT>)
Expand All @@ -946,7 +947,7 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
*/
enter360Image(image360: Image360<DataSourceT>, revision?: Image360Revision<DataSourceT>): Promise<void> {
if (this._cdfSdkClient === undefined || this._image360ApiHelper === undefined) {
throw new Error(`Adding 360 image sets is only supported when connecting to Cognite Data Fusion`);
throw new Error(`Enter 360 image is only supported when connecting to Cognite Data Fusion`);
}
return this._image360ApiHelper.enter360Image(
image360 as Image360Entity<DataSourceT>,
Expand All @@ -959,11 +960,35 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
*/
exit360Image(): void {
if (this._cdfSdkClient === undefined || this._image360ApiHelper === undefined) {
throw new Error(`Adding 360 image sets is only supported when connecting to Cognite Data Fusion`);
throw new Error(`Exit 360 image is only supported when connecting to Cognite Data Fusion`);
}
this._image360ApiHelper.exit360Image();
}

/**
* Check if a 360 image action can be done.
* @param action The action to check if can be done.
* @beta
*/
canDo360Action(action: Image360Action): boolean {
if (this._cdfSdkClient === undefined || this._image360ApiHelper === undefined) {
return false;
}
return this._image360ApiHelper.canDoAction(action);
}

/**
* Do a 360 image action.
* @param action The action to do.
* @beta
*/
async do360Action(action: Image360Action): Promise<void> {
if (this._cdfSdkClient === undefined || this._image360ApiHelper === undefined) {
throw new Error(`360 actions is only supported when connecting to Cognite Data Fusion`);
}
await this._image360ApiHelper.doAction(action);
}

/**
* Removes a model that was previously added using {@link Cognite3DViewer.addModel},
* {@link Cognite3DViewer.addCadModel} or {@link Cognite3DViewer.addPointCloudModel}
Expand Down Expand Up @@ -1698,7 +1723,7 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
if (this._image360ApiHelper === undefined) {
return false;
}
return this._image360ApiHelper.enter360ImageHandler({ offsetX: event.offsetX, offsetY: event.offsetY });
return this._image360ApiHelper.onClick({ offsetX: event.offsetX, offsetY: event.offsetY });
}

/**
Expand All @@ -1711,7 +1736,7 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
if (this._image360ApiHelper === undefined) {
return false;
}
this._image360ApiHelper.setHoverIconEventHandler(event);
this._image360ApiHelper.onHover(event);
return true;
}

Expand Down
12 changes: 12 additions & 0 deletions viewer/reveal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,15 @@ export class Cognite3DViewer<DataSourceT extends DataSourceType = ClassicDataSou
set cadBudget(budget: CadModelBudget);
// (undocumented)
get cameraManager(): CameraManager;
// @beta
canDo360Action(action: Image360Action): boolean;
get canvas(): HTMLCanvasElement;
// @beta
createCustomObjectIntersectInput(pixelCoords: THREE.Vector2): CustomObjectIntersectInput;
determineModelType(modelId: number, revisionId: number): Promise<SupportedModelTypes | ''>;
dispose(): void;
// @beta
do360Action(action: Image360Action): Promise<void>;
get domElement(): HTMLElement;
enter360Image(image360: Image360<DataSourceT>, revision?: Image360Revision<DataSourceT>): Promise<void>;
exit360Image(): void;
Expand Down Expand Up @@ -1212,6 +1216,14 @@ export interface Image360<T extends DataSourceType = ClassicDataSourceType> {
readonly transform: Matrix4;
}

// @beta
export enum Image360Action {
Backward = 1,
Enter = 2,
Exit = 3,
Forward = 0
}

// @public
export interface Image360Annotation<T extends DataSourceType = ClassicDataSourceType> {
readonly annotation: T['image360AnnotationType'];
Expand Down

0 comments on commit 1cdd5c5

Please sign in to comment.