From b61bb95f54500871f39198b35773c5f391edcaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Wed, 10 Jul 2024 15:02:45 +0200 Subject: [PATCH 01/18] feat(react-components): create and delete observations --- .../CreateObservationCommand.ts | 27 ++++ .../DeleteObservationCommand.ts | 40 +++++ .../ObservationStatus.ts | 37 +++++ .../ObservationsCache.ts | 140 ++++++++++++++++++ .../ObservationsCommand.ts | 17 +++ .../ObservationsDomainObject.ts | 137 +++++++++++------ .../ObservationsTool.ts | 89 +++++++++-- .../ObservationsView.ts | 52 +++++-- .../SaveObservationsCommand.ts | 51 +++++++ .../observationsDomainObject/color.ts | 15 ++ .../observationsDomainObject/constants.ts | 7 - .../observationsDomainObject/models.ts | 32 ++-- .../observationsDomainObject/network.ts | 101 +++++++++++++ .../observationsDomainObject/types.ts | 19 +++ .../hooks/useDeleteRuleInstance.tsx | 2 +- react-components/src/utilities/FdmSDK.ts | 30 ++-- 16 files changed, 688 insertions(+), 108 deletions(-) create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/color.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/constants.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/network.ts create mode 100644 react-components/src/architecture/concrete/observationsDomainObject/types.ts diff --git a/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts new file mode 100644 index 00000000000..8256df94254 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts @@ -0,0 +1,27 @@ +import { IconType } from '@cognite/cogs.js'; +import { TranslateKey } from '../../base/utilities/TranslateKey'; +import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; +import { ButtonType } from '../../../components/Architecture/types'; +import { ObservationsTool } from './ObservationsTool'; +import { ObservationsCommand } from './ObservationsCommand'; + +export class CreateObservationCommand extends ObservationsCommand { + public override get icon(): IconType { + return 'Plus'; + } + + public override get tooltip(): TranslateKey { + return { key: 'ADD_OBSERVATION', fallback: 'Add observation. Click at a point' }; + } + + protected override invokeCore(): boolean { + const tool = this.getTool(); + if (tool === undefined) { + return false; + } + + tool.setIsCreating(!tool.isCreating); + + return true; + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts new file mode 100644 index 00000000000..ffb8f4b6b50 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts @@ -0,0 +1,40 @@ +import { IconType } from '@cognite/cogs.js'; +import { TranslateKey } from '../../base/utilities/TranslateKey'; +import { ButtonType } from '../../../components/Architecture/types'; +import { ObservationsCommand } from './ObservationsCommand'; + +export class DeleteObservationCommand extends ObservationsCommand { + public override get icon(): IconType { + return 'Delete'; + } + + public override get tooltip(): TranslateKey { + return { fallback: 'Delete observation' }; + } + + public override get buttonType(): ButtonType { + return 'ghost-destructive'; + } + + public override get shortCutKey(): string { + return 'DELETE'; + } + + public override get isEnabled(): boolean { + const observation = this.getObservationsDomainObject(); + + return observation?.getSelectedOverlay() !== undefined; + } + + protected override invokeCore(): boolean { + const observations = this.getObservationsDomainObject(); + const selectedOverlay = observations?.getSelectedOverlay(); + if (selectedOverlay === undefined) { + return false; + } + + observations?.removeObservation(selectedOverlay); + + return true; + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts new file mode 100644 index 00000000000..33a0e457c2c --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts @@ -0,0 +1,37 @@ +import { Color } from 'three'; +import { + convertToSelectedColor, + DEFAULT_OVERLAY_COLOR, + PENDING_DELETION_OVERLAY_COLOR, + PENDING_OVERLAY_COLOR +} from './color'; +import { assertNever } from '../../../utilities/assertNever'; + +export enum ObservationStatus { + Normal, + PendingCreation, + PendingDeletion +} + +export function getColorFromStatus(status: ObservationStatus, selected: boolean): Color { + const baseColor = getBaseColor(status); + + if (selected) { + return convertToSelectedColor(baseColor); + } + + return baseColor; + + function getBaseColor(status: ObservationStatus): Color { + switch (status) { + case ObservationStatus.Normal: + return DEFAULT_OVERLAY_COLOR; + case ObservationStatus.PendingCreation: + return PENDING_OVERLAY_COLOR; + case ObservationStatus.PendingDeletion: + return PENDING_DELETION_OVERLAY_COLOR; + default: + assertNever(status); + } + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts new file mode 100644 index 00000000000..6a1d8e4a456 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts @@ -0,0 +1,140 @@ +import { CDF_TO_VIEWER_TRANSFORMATION, Overlay3D, Overlay3DCollection } from '@cognite/reveal'; +import { FdmSDK } from '../../../utilities/FdmSDK'; +import { Observation, ObservationProperties } from './models'; +import { Vector3 } from 'three'; + +import { isPendingObservation, ObservationCollection, ObservationOverlay } from './types'; +import { + createObservationInstances, + deleteObservationInstances, + fetchObservations +} from './network'; +import { ObservationStatus } from './ObservationStatus'; + +/** + * A cache that takes care of loading the observations, but also buffers changes to the overlays + * list when e.g. adding or removing observations + */ +export class ObservationsCache { + private _loadedPromise: Promise; + private _fdmSdk: FdmSDK; + + private _persistedCollection = new Overlay3DCollection([]); + + private _pendingOverlaysCollection = new Overlay3DCollection([]); + + private _pendingDeletionObservations: Set> = new Set(); + + constructor(fdmSdk: FdmSDK) { + this._loadedPromise = fetchObservations(fdmSdk) + .then((data) => this.initializeCollection(data)) + .then(); + this._fdmSdk = fdmSdk; + } + + public getFinishedOriginalLoadingPromise(): Promise { + return this._loadedPromise; + } + + public getPersistedCollection(): Overlay3DCollection { + return this._persistedCollection; + } + + public getPendingCollection(): Overlay3DCollection { + return this._pendingOverlaysCollection; + } + + public getOverlaysPendingForRemoval(): Set> { + return this._pendingDeletionObservations; + } + + public getCollections(): ObservationCollection[] { + return [this._persistedCollection, this._pendingOverlaysCollection]; + } + + public addPendingObservation( + point: Vector3, + observation: ObservationProperties + ): Overlay3D { + return this._pendingOverlaysCollection.addOverlays([ + { position: point, content: observation } + ])[0]; + } + + public removeObservation(observation: ObservationOverlay): void { + if (isPendingObservation(observation)) { + this.removePendingObservation(observation); + } else { + this.markObservationForRemoval(observation); + } + } + + public async save(): Promise { + await this._loadedPromise; + await this.savePendingObservations(); + await this.removeDeletedObservations(); + } + + public removePendingObservation(observation: Overlay3D): void { + this._pendingOverlaysCollection.removeOverlays([observation]); + } + + public markObservationForRemoval(observation: Overlay3D): void { + this._pendingDeletionObservations.add(observation); + } + + public getObservationStatus(observation: ObservationOverlay): ObservationStatus { + if (isPendingObservation(observation)) { + return ObservationStatus.PendingCreation; + } else if ( + !isPendingObservation(observation) && + this._pendingDeletionObservations.has(observation) + ) { + return ObservationStatus.PendingDeletion; + } else { + return ObservationStatus.Normal; + } + } + + private async removeDeletedObservations(): Promise { + const overlaysToRemove = [...this._pendingDeletionObservations]; + + await deleteObservationInstances(this._fdmSdk, overlaysToRemove); + + this._persistedCollection.removeOverlays(overlaysToRemove); + this._pendingDeletionObservations.clear(); + } + + private async savePendingObservations(): Promise { + const overlays = this._pendingOverlaysCollection.getOverlays(); + if (overlays.length === 0) { + return; + } + + const instances = await createObservationInstances(this._fdmSdk, overlays); + + const overlayPositions = overlays.map((overlay) => overlay.getPosition()); + + this._pendingOverlaysCollection.removeAllOverlays(); + this._persistedCollection.addOverlays( + instances.map((instance, index) => ({ content: instance, position: overlayPositions[index] })) + ); + } + + private initializeCollection(observations: Observation[]) { + const observationOverlays = observations.map((observation) => { + const position = new Vector3( + observation.properties.positionX, + observation.properties.positionY, + observation.properties.positionZ + ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + + return { + position, + content: observation + }; + }); + + this._persistedCollection.addOverlays(observationOverlays); + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts new file mode 100644 index 00000000000..1ef0112bb29 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts @@ -0,0 +1,17 @@ +import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; +import { ObservationsDomainObject } from './ObservationsDomainObject'; +import { ObservationsTool } from './ObservationsTool'; + +export abstract class ObservationsCommand extends RenderTargetCommand { + protected getTool(): ObservationsTool | undefined { + if (this.activeTool instanceof ObservationsTool) { + return this.activeTool; + } + + return undefined; + } + + protected getObservationsDomainObject(): ObservationsDomainObject | undefined { + return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts index 540fa4ff125..1fb7fe65adf 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts @@ -1,30 +1,31 @@ /*! * Copyright 2024 Cognite AS */ -import { CDF_TO_VIEWER_TRANSFORMATION, type Overlay3D, Overlay3DCollection } from '@cognite/reveal'; -import { OBSERVATION_SOURCE, type ObservationProperties, type Observation } from './models'; +import { type Overlay3D } from '@cognite/reveal'; +import { type ObservationProperties } from './models'; import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; import { type ThreeView } from '../../base/views/ThreeView'; import { ObservationsView } from './ObservationsView'; import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { type FdmSDK, type InstanceFilter } from '../../../utilities/FdmSDK'; -import { Vector3 } from 'three'; -import { DEFAULT_OVERLAY_COLOR } from './constants'; +import { type FdmSDK } from '../../../utilities/FdmSDK'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { ObservationsCache } from './ObservationsCache'; +import { Vector3 } from 'three'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { ObservationCollection, ObservationOverlay } from './types'; +import { ObservationStatus } from './ObservationStatus'; export class ObservationsDomainObject extends VisualDomainObject { - private _selectedOverlay: Overlay3D | undefined; - - private readonly _collection = new Overlay3DCollection([], { - defaultOverlayColor: DEFAULT_OVERLAY_COLOR - }); + private _selectedObservation: ObservationOverlay | undefined; + private _observationsCache: ObservationsCache; constructor(fdmSdk: FdmSDK) { super(); - void fetchObservations(fdmSdk).then((observations) => { - this.initializeCollection(observations); - this.notify(Changes.geometry); - }); + + this._observationsCache = new ObservationsCache(fdmSdk); + this._observationsCache + .getFinishedOriginalLoadingPromise() + .then(() => this.notify(Changes.geometry)); } public override get typeName(): TranslateKey { @@ -35,45 +36,91 @@ export class ObservationsDomainObject extends VisualDomainObject { return new ObservationsView(); } - private initializeCollection(observations: Observation[]): void { - const observationOverlays = observations.map((observation) => { - const position = new Vector3( - observation.properties.positionX, - observation.properties.positionY, - observation.properties.positionZ - ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); - - return { - position, - content: observation - }; - }); - - this._collection.addOverlays(observationOverlays); + public override get canBeRemoved(): boolean { + return false; } - public get overlayCollection(): Overlay3DCollection { - return this._collection; + public override get hasPanelInfo(): boolean { + return true; } - public getSelectedOverlay(): Overlay3D | undefined { - return this._selectedOverlay; + public override getPanelInfo(): PanelInfo | undefined { + const info = new PanelInfo(); + const header = { fallback: 'Observation' }; + info.setHeader(header); + + info.add({ fallback: 'X', value: this._selectedObservation?.getPosition().x }); + info.add({ fallback: 'Y', value: this._selectedObservation?.getPosition().y }); + info.add({ fallback: 'Z', value: this._selectedObservation?.getPosition().z }); + + return info; } - public setSelectedOverlay(overlay: Overlay3D | undefined): void { - this._selectedOverlay = overlay; - this.notify(Changes.selected); + public get overlayCollections(): ObservationCollection[] { + return this._observationsCache.getCollections(); + } + + public addPendingObservation( + point: Vector3, + observationData: ObservationProperties + ): Overlay3D { + const pendingObservation = this._observationsCache.addPendingObservation( + point, + observationData + ); + this.setSelectedObservation(pendingObservation); + + this.notify(Changes.added); + return pendingObservation; } -} -async function fetchObservations(fdmSdk: FdmSDK): Promise { - const observationsFilter: InstanceFilter = {}; + public removeObservation(observation: ObservationOverlay): void { + this._observationsCache.removeObservation(observation); - const observationResult = await fdmSdk.filterAllInstances( - observationsFilter, - 'node', - OBSERVATION_SOURCE - ); + if (observation === this._selectedObservation) { + this.setSelectedObservation(undefined); + } - return observationResult.instances; + this.notify(Changes.geometry); + } + + public getSelectedOverlay(): ObservationOverlay | undefined { + return this._selectedObservation; + } + + public getObservationStatus(observation: ObservationOverlay): ObservationStatus { + return this._observationsCache.getObservationStatus(observation); + } + + public async save(): Promise { + if ( + this._selectedObservation !== undefined && + this._observationsCache.getObservationStatus(this._selectedObservation) !== + ObservationStatus.Normal + ) { + this.setSelectedObservation(undefined); + } + + await this._observationsCache.save(); + this.notify(Changes.geometry); + } + + public setSelectedObservation(observation: ObservationOverlay | undefined): void { + this._selectedObservation = observation; + if (this._selectedObservation === undefined) { + this.setSelectedInteractive(false); + } else { + this.setSelectedInteractive(true); + } + + this.notify(Changes.selected); + } + + public hasPendingObservations(): boolean { + return this._observationsCache.getPendingCollection().getOverlays().length !== 0; + } + + public hasPendingDeletionObservations(): boolean { + return this._observationsCache.getOverlaysPendingForRemoval().size !== 0; + } } diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts index da9a7765cd2..aed3216661b 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts @@ -6,14 +6,48 @@ import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { ObservationsDomainObject } from './ObservationsDomainObject'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { BaseCommand } from '../../base/commands/BaseCommand'; +import { CreateObservationCommand } from './CreateObservationCommand'; +import { first, sortBy } from 'lodash'; +import { isDefined } from '../../../utilities/isDefined'; +import { SaveObservationsCommand } from './SaveObservationsCommand'; +import { DeleteObservationCommand } from './DeleteObservationCommand'; +import { createEmptyObservationProperties } from './types'; export class ObservationsTool extends BaseEditTool { + private _isCreating: boolean = false; + + protected override canBeSelected(domainObject: VisualDomainObject): boolean { + return domainObject instanceof ObservationsDomainObject; + } + + public get isCreating(): boolean { + return this._isCreating; + } + + public setIsCreating(value: boolean): void { + this._isCreating = value; + if (value) { + this.renderTarget.setCrosshairCursor(); + } else { + this.renderTarget.setNavigateCursor(); + } + } + public override get icon(): IconType { return 'Location'; } public override get tooltip(): TranslateKey { - return { fallback: 'Show observations' }; + return { fallback: 'Show and edit observations' }; + } + + public override getToolbar(): Array { + return [ + new CreateObservationCommand(), + new DeleteObservationCommand(), + new SaveObservationsCommand() + ]; } public override onActivate(): void { @@ -26,10 +60,29 @@ export class ObservationsTool extends BaseEditTool { } public override onDeactivate(): void { - this.getObservationsDomainObject()?.setVisibleInteractive(false, this.renderTarget); + const domainObject = this.getObservationsDomainObject(); + domainObject?.setSelectedObservation(undefined); + domainObject?.setVisibleInteractive(false, this.renderTarget); } public override async onClick(event: PointerEvent): Promise { + if (this._isCreating) { + this.createPendingObservation(event); + return; + } + this.selectOverlayFromClick(event); + } + + public getObservationsDomainObject(): ObservationsDomainObject | undefined { + return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); + } + + public async save(): Promise { + const domainObject = this.getObservationsDomainObject(); + await domainObject?.save(); + } + + private async selectOverlayFromClick(event: PointerEvent): Promise { const intersection = await this.getIntersection(event); const domainObject = this.getIntersectedSelectableDomainObject(intersection); @@ -38,21 +91,35 @@ export class ObservationsTool extends BaseEditTool { return; } + const camera = this.renderTarget.camera; const normalizedCoords = this.getNormalizedPixelCoordinates(event); - const intersectedOverlay = domainObject.overlayCollection.intersectOverlays( - normalizedCoords, - this.renderTarget.camera + const intersectedOverlay = domainObject.overlayCollections + .map((collection) => collection.intersectOverlays(normalizedCoords, camera)) + .filter(isDefined); + + sortBy(intersectedOverlay, (overlay) => + camera.position.distanceToSquared(overlay.getPosition()) ); - domainObject.setSelectedOverlay(intersectedOverlay); + domainObject.setSelectedObservation(first(intersectedOverlay)); } - protected override canBeSelected(domainObject: VisualDomainObject): boolean { - return domainObject instanceof ObservationsDomainObject; - } + private async createPendingObservation(event: PointerEvent): Promise { + const intersection = await this.getIntersection(event); - public getObservationsDomainObject(): ObservationsDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); + if (intersection === undefined) { + return; + } + + const domainObject = this.getObservationsDomainObject(); + const pendingOverlay = domainObject?.addPendingObservation( + intersection.point, + createEmptyObservationProperties(intersection.point) + ); + domainObject?.setSelectedObservation(pendingOverlay); + this.renderTarget.invalidate(); + + this.setIsCreating(false); } } diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts index 441d03a4f57..e3f64e7819b 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts @@ -7,32 +7,45 @@ import { GroupThreeView } from '../../base/views/GroupThreeView'; import { type CustomObjectIntersectInput, type CustomObjectIntersection } from '@cognite/reveal'; import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { DEFAULT_OVERLAY_COLOR, SELECTED_OVERLAY_COLOR } from './constants'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { isDefined } from '../../../utilities/isDefined'; +import { first, sortBy } from 'lodash'; +import { ObservationOverlay } from './types'; +import { getColorFromStatus } from './ObservationStatus'; export class ObservationsView extends GroupThreeView { protected override calculateBoundingBox(): Box3 { - return this.domainObject.overlayCollection - .getOverlays() + return this.domainObject.overlayCollections + .flatMap((collection) => collection.getOverlays()) .reduce((box, overlay) => box.expandByPoint(overlay.getPosition()), new Box3()); } protected override addChildren(): void { - this.addChild(this.domainObject.overlayCollection); + this.domainObject.overlayCollections.forEach((collection) => this.addChild(collection)); } public override update(change: DomainObjectChange): void { super.update(change); - if (change.isChanged(Changes.selected)) { - this.domainObject.overlayCollection.getOverlays().forEach((overlay) => { - overlay.setColor(DEFAULT_OVERLAY_COLOR); - }); - const overlay = this.domainObject.getSelectedOverlay(); - overlay?.setColor(SELECTED_OVERLAY_COLOR); + if (change.isChanged(Changes.selected, Changes.geometry)) { + const selectedOverlay = this.domainObject.getSelectedOverlay(); + const overlayCollections = this.domainObject.overlayCollections; + + overlayCollections.forEach((collection) => { + const overlays = collection.getOverlays(); + overlays.forEach((overlay) => { + const isSelected = selectedOverlay === overlay; + const observationStatus = this.domainObject.getObservationStatus(overlay); - this.renderTarget.invalidate(); + const color = getColorFromStatus(observationStatus, isSelected); + if (!color.equals(overlay.getColor())) { + overlay.setColor(color); + } + }); + }); } + + this.renderTarget.invalidate(); } public override intersectIfCloser( @@ -41,16 +54,23 @@ export class ObservationsView extends GroupThreeView { ): undefined | CustomObjectIntersection { const { domainObject } = this; - const intersection = this.domainObject.overlayCollection.intersectOverlays( - intersectInput.normalizedCoords, - intersectInput.camera + const intersections = this.domainObject.overlayCollections + .map((collection) => + collection.intersectOverlays(intersectInput.normalizedCoords, intersectInput.camera) + ) + .filter(isDefined); + + const closestIntersection = first( + sortBy(intersections, (intersection) => + this.renderTarget.camera.position.distanceToSquared(intersection.getPosition()) + ) ); - if (intersection === undefined) { + if (closestIntersection === undefined) { return undefined; } - const point = intersection.getPosition(); + const point = closestIntersection.getPosition(); const distanceToCamera = point.distanceTo(intersectInput.raycaster.ray.origin); if (closestDistance !== undefined && closestDistance < distanceToCamera) { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts new file mode 100644 index 00000000000..bf861459c10 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts @@ -0,0 +1,51 @@ +import { IconType, toast } from '@cognite/cogs.js'; +import { ButtonType } from '../../../components/Architecture/types'; +import { TranslateKey } from '../../base/utilities/TranslateKey'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { ObservationsCommand } from './ObservationsCommand'; + +export class SaveObservationsCommand extends ObservationsCommand { + public override get icon(): IconType { + return 'Save'; + } + + public override get tooltip(): TranslateKey { + return { fallback: 'Publish observation changes' }; + } + + public override get buttonType(): ButtonType { + return 'primary'; + } + + public override get isEnabled(): boolean { + const observation = this.getObservationsDomainObject(); + + return ( + observation !== undefined && + (observation.hasPendingObservations() || observation.hasPendingDeletionObservations()) + ); + } + + protected override invokeCore(): boolean { + const tool = this.getTool(); + if (tool === undefined) { + return false; + } + + void tool + .save() + .then(() => { + toast.success({ fallback: 'Successfully published changes' }.fallback); + + const observation = this.getObservationsDomainObject(); + observation?.notify(Changes.geometry); + this.renderTarget.commandsController.update(); + }) + .catch((e) => { + toast.error({ fallback: 'Unable to publish observation changes: ' + e }.fallback); + throw e; + }); + + return true; + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/color.ts b/react-components/src/architecture/concrete/observationsDomainObject/color.ts new file mode 100644 index 00000000000..bea19793e10 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/color.ts @@ -0,0 +1,15 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { Color } from 'three'; + +export const DEFAULT_OVERLAY_COLOR = new Color('#3333AA'); +export const PENDING_OVERLAY_COLOR = new Color('#33AA33'); +export const PENDING_DELETION_OVERLAY_COLOR = new Color('#AA3333'); + +export function convertToSelectedColor(color: Color): Color { + const hsl = { h: 0, s: 0, l: 0 }; + color.getHSL(hsl); + hsl.l = Math.sqrt(hsl.l); + return new Color().setHSL(hsl.h, hsl.s, hsl.l); +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/constants.ts b/react-components/src/architecture/concrete/observationsDomainObject/constants.ts deleted file mode 100644 index bab3e2c3073..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { Color } from 'three'; - -export const DEFAULT_OVERLAY_COLOR = new Color('lightblue'); -export const SELECTED_OVERLAY_COLOR = new Color('red'); diff --git a/react-components/src/architecture/concrete/observationsDomainObject/models.ts b/react-components/src/architecture/concrete/observationsDomainObject/models.ts index f402f148667..7afba0f2571 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/models.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/models.ts @@ -5,41 +5,41 @@ import { type DmsUniqueIdentifier, type FdmNode, type Source } from '../../../ut export type ObservationProperties = { // "ID as the node appears in the Source system" - sourceId: string; + sourceId?: string; // "Name of the source system node comes from" - source: string; + source?: string; // "Title or name of the node" - title: string; + title?: string; // "Long description of the node" - description: string; + description?: string; // "Text based labels for generic use" - labels: string[]; + labels?: string[]; // "Visibility of node (PUBLIC, PRIVATE, PROTECTED)" - visibility: string; + visibility?: string; // "Who created this node?" - createdBy: string; + createdBy?: string; // "Who was the last person to update this node?" - updatedBy: string; + updatedBy?: string; // "Is this item archived, and therefore hidden from most UIs?" - isArchived: boolean; + isArchived?: boolean; // "The status of the observation (draft, completed, sent)" - status: string; + status?: string; // "External ID of the associated CDF Asset" - asset: DmsUniqueIdentifier; + asset?: DmsUniqueIdentifier; // "List of associated files" - files: DmsUniqueIdentifier[]; + files?: DmsUniqueIdentifier[]; // "description of how the observation was troubleshooted" - troubleshooting: string; + troubleshooting?: string; // "Priority of the observation (Urgent, High ...)" - priority: string; + priority?: string; // "The observation type (Malfunction report, Maintenance request, etc.)" - type: string; + type?: string; // "3D position" positionX: number; positionY: number; positionZ: number; // "Comments" - comments: [CommentProperties]; + comments?: CommentProperties[]; }; export type CommentProperties = { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/network.ts b/react-components/src/architecture/concrete/observationsDomainObject/network.ts new file mode 100644 index 00000000000..5117a27b4e2 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/network.ts @@ -0,0 +1,101 @@ +import { chunk } from 'lodash'; +import { + CreateInstanceItem, + DmsUniqueIdentifier, + FdmSDK, + InstanceFilter +} from '../../../utilities/FdmSDK'; +import { Observation, OBSERVATION_SOURCE, ObservationProperties } from './models'; +import { Overlay3D } from '@cognite/reveal'; + +import { v4 as uuid } from 'uuid'; + +export async function fetchObservations(fdmSdk: FdmSDK): Promise { + const observationsFilter: InstanceFilter = {}; + + const observationResult = await fdmSdk.filterAllInstances( + observationsFilter, + 'node', + OBSERVATION_SOURCE + ); + + return observationResult.instances; +} + +export async function createObservationInstances( + fdmSdk: FdmSDK, + observationOverlays: Overlay3D[] +): Promise { + const chunks = chunk(observationOverlays, 100); + const resultPromises = chunks.map(async (chunk) => { + const payloads = chunk.map(createObservationInstancePayload); + const instanceResults = await fdmSdk.createInstance(payloads); + return instanceResults.items; + }); + + const createResults = (await Promise.all(resultPromises)).flat(); + + return await fetchObservationsWithIds(fdmSdk, createResults); +} + +async function fetchObservationsWithIds( + fdmSdk: FdmSDK, + identifiers: DmsUniqueIdentifier[] +): Promise { + return ( + await fdmSdk.filterInstances( + { + and: [ + { + in: { + property: ['node', 'externalId'], + values: identifiers.map((identifier) => identifier.externalId) + } + }, + { + in: { + property: ['node', 'space'], + values: identifiers.map((identifier) => identifier.space) + } + } + ] + }, + 'node', + OBSERVATION_SOURCE + ) + ).instances.map((observation) => observation); +} + +function createObservationInstancePayload( + overlay: Overlay3D +): CreateInstanceItem { + const id = uuid(); + return { + instanceType: 'node' as const, + externalId: uuid(), + space: 'observations', + sources: [ + { + source: OBSERVATION_SOURCE, + properties: { + ...overlay.getContent(), + type: 'simple', + sourceId: id + } + } + ] + }; +} + +export async function deleteObservationInstances( + fdmSdk: FdmSDK, + observations: Overlay3D[] +): Promise { + await fdmSdk.deleteInstances( + observations.map((observation) => ({ + instanceType: 'node', + externalId: observation.getContent().externalId, + space: observation.getContent().space + })) + ); +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/types.ts b/react-components/src/architecture/concrete/observationsDomainObject/types.ts new file mode 100644 index 00000000000..d388f2285c4 --- /dev/null +++ b/react-components/src/architecture/concrete/observationsDomainObject/types.ts @@ -0,0 +1,19 @@ +import { CDF_TO_VIEWER_TRANSFORMATION, Overlay3D, Overlay3DCollection } from '@cognite/reveal'; +import { Observation, ObservationProperties } from './models'; +import { Vector3 } from 'three'; + +export type ObservationOverlay = Overlay3D | Overlay3D; +export type ObservationCollection = + | Overlay3DCollection + | Overlay3DCollection; + +export function isPendingObservation( + observationOverlay: ObservationOverlay +): observationOverlay is Overlay3D { + return (observationOverlay.getContent() as ObservationProperties).positionX !== undefined; +} + +export function createEmptyObservationProperties(point: Vector3): ObservationProperties { + const cdfPosition = point.clone().applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; +} diff --git a/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx b/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx index 06b07c33faa..ba0326cc04d 100644 --- a/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx +++ b/react-components/src/components/RuleBasedOutputs/hooks/useDeleteRuleInstance.tsx @@ -21,7 +21,7 @@ export const useDeleteRuleInstance = (): (( return { items: [] }; } - const resultFromSavingRule = await fdmSdk.deleteInstance([ + const resultFromSavingRule = await fdmSdk.deleteInstances([ { instanceType: 'node', space: RULE_BASED_OUTPUTS_VIEW.space, diff --git a/react-components/src/utilities/FdmSDK.ts b/react-components/src/utilities/FdmSDK.ts index afaaeade356..5492775c9b4 100644 --- a/react-components/src/utilities/FdmSDK.ts +++ b/react-components/src/utilities/FdmSDK.ts @@ -80,7 +80,7 @@ type QuerySelect = { }; export type EdgeItem> = { - instanceType: string; + instanceType: 'edge'; version: number; type: DmsUniqueIdentifier; space: string; @@ -93,7 +93,7 @@ export type EdgeItem> = { }; export type NodeItem> = { - instanceType: InstanceType; + instanceType: 'node'; version: number; space: string; externalId: string; @@ -114,6 +114,13 @@ export type FdmNode> = { properties: PropertyType; }; +export type CreateInstanceItem> = { + instanceType: InstanceType; + externalId: string; + space: string; + sources: { source: Source; properties: PropertyType }[]; +}; + type InspectionOperations = | { involvedContainers: Record; involvedViews?: Record } | { involvedContainers?: Record; involvedViews: Record }; @@ -311,7 +318,7 @@ export class FdmSDK { public async filterInstances>( filter: InstanceFilter | undefined, instanceType: InstanceType, - source?: Source, + source: Source, cursor?: string ): Promise<{ instances: Array | FdmNode>; @@ -322,7 +329,7 @@ export class FdmSDK { public async filterInstances>( filter: InstanceFilter | undefined, instanceType: 'node', - source?: Source, + source: Source, cursor?: string ): Promise<{ instances: Array>; nextCursor?: string }>; @@ -330,7 +337,7 @@ export class FdmSDK { public async filterInstances>( filter: InstanceFilter | undefined, instanceType: 'edge', - source?: Source, + source: Source, cursor?: string ): Promise<{ instances: Array>; nextCursor?: string }>; @@ -435,12 +442,7 @@ export class FdmSDK { } public async createInstance( - queries: Array<{ - instanceType: InstanceType; - externalId: string; - space: string; - sources: [{ source: Source; properties: any }]; - }> + queries: Array> ): Promise> { const data: any = { items: queries, @@ -480,13 +482,17 @@ export class FdmSDK { throw new Error(`Failed to edit instances. Status: ${result.status}`); } - public async deleteInstance( + public async deleteInstances( queries: Array<{ instanceType: InstanceType; externalId: string; space: string; }> ): Promise> { + if (queries.length === 0) { + return { items: [] }; + } + const data: any = { items: queries }; From 46419078ab8078dd8bb93c5d5b48d2b01e215aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Wed, 10 Jul 2024 15:42:14 +0200 Subject: [PATCH 02/18] chore: lint fix --- .../CreateObservationCommand.ts | 10 +++--- .../DeleteObservationCommand.ts | 9 ++++-- .../ObservationStatus.ts | 5 ++- .../ObservationsCache.ts | 31 +++++++++++-------- .../ObservationsCommand.ts | 3 ++ .../ObservationsDomainObject.ts | 12 +++---- .../ObservationsTool.ts | 6 ++-- .../ObservationsView.ts | 6 ++-- .../SaveObservationsCommand.ts | 9 ++++-- .../observationsDomainObject/network.ts | 19 +++++++----- .../observationsDomainObject/types.ts | 13 ++++++-- react-components/src/utilities/FdmSDK.ts | 2 +- 12 files changed, 77 insertions(+), 48 deletions(-) diff --git a/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts index 8256df94254..143b3c03fbb 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts @@ -1,8 +1,8 @@ -import { IconType } from '@cognite/cogs.js'; -import { TranslateKey } from '../../base/utilities/TranslateKey'; -import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { ButtonType } from '../../../components/Architecture/types'; -import { ObservationsTool } from './ObservationsTool'; +/*! + * Copyright 2024 Cognite AS + */ +import { type IconType } from '@cognite/cogs.js'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { ObservationsCommand } from './ObservationsCommand'; export class CreateObservationCommand extends ObservationsCommand { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts index ffb8f4b6b50..37c42ab5ccc 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts @@ -1,6 +1,9 @@ -import { IconType } from '@cognite/cogs.js'; -import { TranslateKey } from '../../base/utilities/TranslateKey'; -import { ButtonType } from '../../../components/Architecture/types'; +/*! + * Copyright 2024 Cognite AS + */ +import { type IconType } from '@cognite/cogs.js'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { type ButtonType } from '../../../components/Architecture/types'; import { ObservationsCommand } from './ObservationsCommand'; export class DeleteObservationCommand extends ObservationsCommand { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts index 33a0e457c2c..814a9f264b1 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts @@ -1,4 +1,7 @@ -import { Color } from 'three'; +/*! + * Copyright 2024 Cognite AS + */ +import { type Color } from 'three'; import { convertToSelectedColor, DEFAULT_OVERLAY_COLOR, diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts index 6a1d8e4a456..ba008ff9966 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts @@ -1,9 +1,12 @@ -import { CDF_TO_VIEWER_TRANSFORMATION, Overlay3D, Overlay3DCollection } from '@cognite/reveal'; -import { FdmSDK } from '../../../utilities/FdmSDK'; -import { Observation, ObservationProperties } from './models'; +/*! + * Copyright 2024 Cognite AS + */ +import { CDF_TO_VIEWER_TRANSFORMATION, type Overlay3D, Overlay3DCollection } from '@cognite/reveal'; +import { type FdmSDK } from '../../../utilities/FdmSDK'; +import { type Observation, type ObservationProperties } from './models'; import { Vector3 } from 'three'; -import { isPendingObservation, ObservationCollection, ObservationOverlay } from './types'; +import { isPendingObservation, type ObservationCollection, type ObservationOverlay } from './types'; import { createObservationInstances, deleteObservationInstances, @@ -16,24 +19,26 @@ import { ObservationStatus } from './ObservationStatus'; * list when e.g. adding or removing observations */ export class ObservationsCache { - private _loadedPromise: Promise; - private _fdmSdk: FdmSDK; + private readonly _loadedPromise: Promise; + private readonly _fdmSdk: FdmSDK; - private _persistedCollection = new Overlay3DCollection([]); + private readonly _persistedCollection = new Overlay3DCollection([]); - private _pendingOverlaysCollection = new Overlay3DCollection([]); + private readonly _pendingOverlaysCollection = new Overlay3DCollection([]); - private _pendingDeletionObservations: Set> = new Set(); + private readonly _pendingDeletionObservations = new Set>(); constructor(fdmSdk: FdmSDK) { this._loadedPromise = fetchObservations(fdmSdk) - .then((data) => this.initializeCollection(data)) + .then((data) => { + this.initializeCollection(data); + }) .then(); this._fdmSdk = fdmSdk; } - public getFinishedOriginalLoadingPromise(): Promise { - return this._loadedPromise; + public async getFinishedOriginalLoadingPromise(): Promise { + await this._loadedPromise; } public getPersistedCollection(): Overlay3DCollection { @@ -121,7 +126,7 @@ export class ObservationsCache { ); } - private initializeCollection(observations: Observation[]) { + private initializeCollection(observations: Observation[]): void { const observationOverlays = observations.map((observation) => { const position = new Vector3( observation.properties.positionX, diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts index 1ef0112bb29..01202238e63 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts @@ -1,3 +1,6 @@ +/*! + * Copyright 2024 Cognite AS + */ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; import { ObservationsDomainObject } from './ObservationsDomainObject'; import { ObservationsTool } from './ObservationsTool'; diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts index 1fb7fe65adf..2961f8d0932 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts @@ -10,22 +10,22 @@ import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { type FdmSDK } from '../../../utilities/FdmSDK'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { ObservationsCache } from './ObservationsCache'; -import { Vector3 } from 'three'; +import { type Vector3 } from 'three'; import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; -import { ObservationCollection, ObservationOverlay } from './types'; +import { type ObservationCollection, type ObservationOverlay } from './types'; import { ObservationStatus } from './ObservationStatus'; export class ObservationsDomainObject extends VisualDomainObject { private _selectedObservation: ObservationOverlay | undefined; - private _observationsCache: ObservationsCache; + private readonly _observationsCache: ObservationsCache; constructor(fdmSdk: FdmSDK) { super(); this._observationsCache = new ObservationsCache(fdmSdk); - this._observationsCache - .getFinishedOriginalLoadingPromise() - .then(() => this.notify(Changes.geometry)); + void this._observationsCache.getFinishedOriginalLoadingPromise().then(() => { + this.notify(Changes.geometry); + }); } public override get typeName(): TranslateKey { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts index aed3216661b..59a8ffb2d2c 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts @@ -6,7 +6,7 @@ import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { ObservationsDomainObject } from './ObservationsDomainObject'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { BaseCommand } from '../../base/commands/BaseCommand'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; import { CreateObservationCommand } from './CreateObservationCommand'; import { first, sortBy } from 'lodash'; import { isDefined } from '../../../utilities/isDefined'; @@ -67,10 +67,10 @@ export class ObservationsTool extends BaseEditTool { public override async onClick(event: PointerEvent): Promise { if (this._isCreating) { - this.createPendingObservation(event); + await this.createPendingObservation(event); return; } - this.selectOverlayFromClick(event); + await this.selectOverlayFromClick(event); } public getObservationsDomainObject(): ObservationsDomainObject | undefined { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts index e3f64e7819b..6afe25da918 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts @@ -10,7 +10,7 @@ import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; import { isDefined } from '../../../utilities/isDefined'; import { first, sortBy } from 'lodash'; -import { ObservationOverlay } from './types'; +import { type ObservationOverlay } from './types'; import { getColorFromStatus } from './ObservationStatus'; export class ObservationsView extends GroupThreeView { @@ -21,7 +21,9 @@ export class ObservationsView extends GroupThreeView { } protected override addChildren(): void { - this.domainObject.overlayCollections.forEach((collection) => this.addChild(collection)); + this.domainObject.overlayCollections.forEach((collection) => { + this.addChild(collection); + }); } public override update(change: DomainObjectChange): void { diff --git a/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts b/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts index bf861459c10..ef6f8d1c8a9 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts @@ -1,6 +1,9 @@ -import { IconType, toast } from '@cognite/cogs.js'; -import { ButtonType } from '../../../components/Architecture/types'; -import { TranslateKey } from '../../base/utilities/TranslateKey'; +/*! + * Copyright 2024 Cognite AS + */ +import { type IconType, toast } from '@cognite/cogs.js'; +import { type ButtonType } from '../../../components/Architecture/types'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { ObservationsCommand } from './ObservationsCommand'; diff --git a/react-components/src/architecture/concrete/observationsDomainObject/network.ts b/react-components/src/architecture/concrete/observationsDomainObject/network.ts index 5117a27b4e2..e95e236c45f 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/network.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/network.ts @@ -1,12 +1,15 @@ +/*! + * Copyright 2024 Cognite AS + */ import { chunk } from 'lodash'; import { - CreateInstanceItem, - DmsUniqueIdentifier, - FdmSDK, - InstanceFilter + type CreateInstanceItem, + type DmsUniqueIdentifier, + type FdmSDK, + type InstanceFilter } from '../../../utilities/FdmSDK'; -import { Observation, OBSERVATION_SOURCE, ObservationProperties } from './models'; -import { Overlay3D } from '@cognite/reveal'; +import { type Observation, OBSERVATION_SOURCE, type ObservationProperties } from './models'; +import { type Overlay3D } from '@cognite/reveal'; import { v4 as uuid } from 'uuid'; @@ -24,7 +27,7 @@ export async function fetchObservations(fdmSdk: FdmSDK): Promise export async function createObservationInstances( fdmSdk: FdmSDK, - observationOverlays: Overlay3D[] + observationOverlays: Array> ): Promise { const chunks = chunk(observationOverlays, 100); const resultPromises = chunks.map(async (chunk) => { @@ -89,7 +92,7 @@ function createObservationInstancePayload( export async function deleteObservationInstances( fdmSdk: FdmSDK, - observations: Overlay3D[] + observations: Array> ): Promise { await fdmSdk.deleteInstances( observations.map((observation) => ({ diff --git a/react-components/src/architecture/concrete/observationsDomainObject/types.ts b/react-components/src/architecture/concrete/observationsDomainObject/types.ts index d388f2285c4..678438246e7 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/types.ts +++ b/react-components/src/architecture/concrete/observationsDomainObject/types.ts @@ -1,6 +1,13 @@ -import { CDF_TO_VIEWER_TRANSFORMATION, Overlay3D, Overlay3DCollection } from '@cognite/reveal'; -import { Observation, ObservationProperties } from './models'; -import { Vector3 } from 'three'; +/*! + * Copyright 2024 Cognite AS + */ +import { + CDF_TO_VIEWER_TRANSFORMATION, + type Overlay3D, + type Overlay3DCollection +} from '@cognite/reveal'; +import { type Observation, type ObservationProperties } from './models'; +import { type Vector3 } from 'three'; export type ObservationOverlay = Overlay3D | Overlay3D; export type ObservationCollection = diff --git a/react-components/src/utilities/FdmSDK.ts b/react-components/src/utilities/FdmSDK.ts index 5492775c9b4..34796381f04 100644 --- a/react-components/src/utilities/FdmSDK.ts +++ b/react-components/src/utilities/FdmSDK.ts @@ -118,7 +118,7 @@ export type CreateInstanceItem> = { instanceType: InstanceType; externalId: string; space: string; - sources: { source: Source; properties: PropertyType }[]; + sources: Array<{ source: Source; properties: PropertyType }>; }; type InspectionOperations = From 142fbc48cbfba364291a5f5336cfe7a3b0a20ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Thu, 11 Jul 2024 09:31:24 +0200 Subject: [PATCH 03/18] chore: Make source argument required --- react-components/src/utilities/FdmSDK.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/react-components/src/utilities/FdmSDK.ts b/react-components/src/utilities/FdmSDK.ts index 34796381f04..fee52ce6d6d 100644 --- a/react-components/src/utilities/FdmSDK.ts +++ b/react-components/src/utilities/FdmSDK.ts @@ -381,28 +381,28 @@ export class FdmSDK { public async filterAllInstances>( filter: InstanceFilter, instanceType: InstanceType, - source?: Source + source: Source ): Promise<{ instances: Array | FdmNode> }>; // eslint-disable-next-line no-dupe-class-members public async filterAllInstances>( filter: InstanceFilter, instanceType: 'edge', - source?: Source + source: Source ): Promise<{ instances: Array> }>; // eslint-disable-next-line no-dupe-class-members public async filterAllInstances>( filter: InstanceFilter, instanceType: 'node', - source?: Source + source: Source ): Promise<{ instances: Array> }>; // eslint-disable-next-line no-dupe-class-members public async filterAllInstances>( filter: InstanceFilter | undefined, instanceType: InstanceType, - source?: Source + source: Source ): Promise<{ instances: Array | FdmNode> }> { filter = makeSureNonEmptyFilterForRequest(filter); From 96ca80c5b8cc6ddbfcf1c36cfec446921637902e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Fri, 12 Jul 2024 17:26:52 +0200 Subject: [PATCH 04/18] chore: rewrite according to review --- .../base/domainObjectsHelpers/Changes.ts | 2 +- .../concrete/config/StoryBookConfig.ts | 2 +- .../CreateObservationCommand.ts | 0 .../DeleteObservationCommand.ts | 9 +- .../observations/ObservationsCache.ts | 58 +++++++ .../ObservationsCommand.ts | 0 .../observations/ObservationsDomainObject.ts | 156 ++++++++++++++++++ .../ObservationsTool.ts | 48 ++++-- .../concrete/observations/ObservationsView.ts | 142 ++++++++++++++++ .../SaveObservationsCommand.ts | 0 .../color.ts} | 28 ++-- .../models.ts | 2 +- .../network.ts | 21 ++- .../concrete/observations/types.ts | 38 +++++ .../ObservationsCache.ts | 145 ---------------- .../ObservationsDomainObject.ts | 126 -------------- .../ObservationsView.ts | 99 ----------- .../observationsDomainObject/color.ts | 15 -- .../observationsDomainObject/types.ts | 26 --- .../components/Architecture/RevealButtons.tsx | 2 +- .../utilities/getAddModelOptionsFromUrl.ts | 4 +- 21 files changed, 460 insertions(+), 463 deletions(-) rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/CreateObservationCommand.ts (100%) rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/DeleteObservationCommand.ts (75%) create mode 100644 react-components/src/architecture/concrete/observations/ObservationsCache.ts rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/ObservationsCommand.ts (100%) create mode 100644 react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/ObservationsTool.ts (76%) create mode 100644 react-components/src/architecture/concrete/observations/ObservationsView.ts rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/SaveObservationsCommand.ts (100%) rename react-components/src/architecture/concrete/{observationsDomainObject/ObservationStatus.ts => observations/color.ts} (57%) rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/models.ts (96%) rename react-components/src/architecture/concrete/{observationsDomainObject => observations}/network.ts (81%) create mode 100644 react-components/src/architecture/concrete/observations/types.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/color.ts delete mode 100644 react-components/src/architecture/concrete/observationsDomainObject/types.ts diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts index 5347f8a2256..3a7cf9c1134 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts @@ -9,7 +9,7 @@ export class Changes { public static readonly expanded: symbol = Symbol('expanded'); public static readonly selected: symbol = Symbol('selected'); public static readonly focus: symbol = Symbol('focus'); - public static readonly clipping: symbol = Symbol('visibleState'); + public static readonly clipping: symbol = Symbol('clipping'); // Domain object Fields changed public static readonly naming: symbol = Symbol('naming'); diff --git a/react-components/src/architecture/concrete/config/StoryBookConfig.ts b/react-components/src/architecture/concrete/config/StoryBookConfig.ts index 1cfc959da63..99eae4d9f66 100644 --- a/react-components/src/architecture/concrete/config/StoryBookConfig.ts +++ b/react-components/src/architecture/concrete/config/StoryBookConfig.ts @@ -20,7 +20,7 @@ import { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetr import { MeasurementTool } from '../measurements/MeasurementTool'; import { ClipTool } from '../clipping/ClipTool'; import { KeyboardSpeedCommand } from '../../base/concreteCommands/KeyboardSpeedCommand'; -import { ObservationsTool } from '../observationsDomainObject/ObservationsTool'; +import { ObservationsTool } from '../observations/ObservationsTool'; export class StoryBookConfig extends BaseRevealConfig { // ================================================== diff --git a/react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts b/react-components/src/architecture/concrete/observations/CreateObservationCommand.ts similarity index 100% rename from react-components/src/architecture/concrete/observationsDomainObject/CreateObservationCommand.ts rename to react-components/src/architecture/concrete/observations/CreateObservationCommand.ts diff --git a/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts b/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts similarity index 75% rename from react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts rename to react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts index 37c42ab5ccc..ce46d1c348a 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/DeleteObservationCommand.ts +++ b/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts @@ -26,17 +26,18 @@ export class DeleteObservationCommand extends ObservationsCommand { public override get isEnabled(): boolean { const observation = this.getObservationsDomainObject(); - return observation?.getSelectedOverlay() !== undefined; + return observation?.getSelectedObservation() !== undefined; } protected override invokeCore(): boolean { const observations = this.getObservationsDomainObject(); - const selectedOverlay = observations?.getSelectedOverlay(); - if (selectedOverlay === undefined) { + const selectedOverlay = observations?.getSelectedObservation(); + if (observations === undefined || selectedOverlay === undefined) { return false; } - observations?.removeObservation(selectedOverlay); + observations.removeObservation(selectedOverlay); + observations.setSelectedObservation(undefined); return true; } diff --git a/react-components/src/architecture/concrete/observations/ObservationsCache.ts b/react-components/src/architecture/concrete/observations/ObservationsCache.ts new file mode 100644 index 00000000000..222c404095e --- /dev/null +++ b/react-components/src/architecture/concrete/observations/ObservationsCache.ts @@ -0,0 +1,58 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type FdmSDK } from '../../../utilities/FdmSDK'; +import { type ObservationFdmNode, type ObservationProperties } from './models'; + +import { Observation } from './types'; +import { + createObservationInstances, + deleteObservationInstances, + fetchObservations +} from './network'; +import { isDefined } from '../../../utilities/isDefined'; + +/** + * A cache that takes care of loading the observations, but also buffers changes to the overlays + * list when e.g. adding or removing observations + */ +export class ObservationsCache { + private readonly _loadedPromise: Promise; + private readonly _fdmSdk: FdmSDK; + + constructor(fdmSdk: FdmSDK) { + this._loadedPromise = fetchObservations(fdmSdk); + this._fdmSdk = fdmSdk; + } + + public async getFinishedOriginalLoadingPromise(): Promise { + return await this._loadedPromise; + } + + public async deleteObservations(observations: Observation[]): Promise { + if (observations.length === 0) { + return; + } + + console.log('Thinking to delete', observations); + const observationData = observations + .map((observation) => observation.fdmMetadata) + .filter(isDefined); + + console.log('Deleting ', observationData); + + await deleteObservationInstances(this._fdmSdk, observationData); + } + + public async saveObservations( + observations: ObservationProperties[] + ): Promise { + if (observations.length === 0) { + return []; + } + + console.log('Saving ', observations); + + return await createObservationInstances(this._fdmSdk, observations); + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts b/react-components/src/architecture/concrete/observations/ObservationsCommand.ts similarity index 100% rename from react-components/src/architecture/concrete/observationsDomainObject/ObservationsCommand.ts rename to react-components/src/architecture/concrete/observations/ObservationsCommand.ts diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts new file mode 100644 index 00000000000..a542308ee70 --- /dev/null +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -0,0 +1,156 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { type ThreeView } from '../../base/views/ThreeView'; +import { ObservationsView } from './ObservationsView'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { type FdmSDK } from '../../../utilities/FdmSDK'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { ObservationsCache } from './ObservationsCache'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { Observation, ObservationStatus } from './types'; +import { partition, remove } from 'lodash'; +import { ObservationProperties } from './models'; + +export class ObservationsDomainObject extends VisualDomainObject { + private _selectedObservation: Observation | undefined; + private readonly _observationsCache: ObservationsCache; + + private _observations: Observation[] = []; + + constructor(fdmSdk: FdmSDK) { + super(); + + this._observationsCache = new ObservationsCache(fdmSdk); + void this._observationsCache.getFinishedOriginalLoadingPromise().then((observations) => { + this._observations = observations.map((observation) => ({ + fdmMetadata: observation, + properties: observation.properties, + status: ObservationStatus.Default + })); + this.notify(Changes.geometry); + }); + } + + public override get typeName(): TranslateKey { + return { fallback: ObservationsDomainObject.name }; + } + + protected override createThreeView(): ThreeView | undefined { + return new ObservationsView(); + } + + public override get canBeRemoved(): boolean { + return false; + } + + public override get hasPanelInfo(): boolean { + return true; + } + + public override getPanelInfo(): PanelInfo | undefined { + const info = new PanelInfo(); + const header = { fallback: 'Observation' }; + info.setHeader(header); + + info.add({ fallback: 'X', value: this._selectedObservation?.properties.positionX }); + info.add({ fallback: 'Y', value: this._selectedObservation?.properties.positionY }); + info.add({ fallback: 'Z', value: this._selectedObservation?.properties.positionZ }); + + return info; + } + + public addPendingObservation(observationData: ObservationProperties): Observation { + const newObservation = { + properties: observationData, + status: ObservationStatus.PendingCreation + }; + + this._observations.push(newObservation); + + this.notify(Changes.geometry); + + return newObservation; + } + + public removeObservation(observation: Observation): void { + if (observation.status === ObservationStatus.PendingCreation) { + remove(this._observations, observation); + } else if (this._observations.includes(observation)) { + observation.status = ObservationStatus.PendingDeletion; + } + + this.notify(Changes.geometry); + } + + public getObservations(): Observation[] { + return this._observations; + } + + public getSelectedObservation(): Observation | undefined { + return this._selectedObservation; + } + + public async save(): Promise { + if ( + this._selectedObservation !== undefined && + (this._selectedObservation.status === ObservationStatus.PendingCreation || + this._selectedObservation.status === ObservationStatus.PendingDeletion) + ) { + this.setSelectedObservation(undefined); + } + + const [toRemove, toKeep] = partition( + this._observations, + (observation) => observation.status === ObservationStatus.PendingDeletion + ); + + const deletePromise = this._observationsCache.deleteObservations(toRemove); + + const observationsToCreate = this._observations.filter( + (obs) => obs.status === ObservationStatus.PendingCreation + ); + const newObservations = await this._observationsCache.saveObservations( + observationsToCreate.map((obs) => obs.properties) + ); + + this._observations = toKeep.concat( + newObservations.map((observation) => ({ + status: ObservationStatus.Default, + fdmMetadata: observation, + properties: observation.properties + })) + ); + + await deletePromise; + + this.notify(Changes.geometry); + } + + public setSelectedObservation(observation: Observation | undefined): void { + this._selectedObservation = observation; + if (this._selectedObservation === undefined) { + this.setSelectedInteractive(false); + } else { + this.setSelectedInteractive(true); + } + + this.notify(Changes.selected); + } + + public hasPendingObservations(): boolean { + return ( + this._observations.find( + (observation) => observation.status === ObservationStatus.PendingCreation + ) !== undefined + ); + } + + public hasPendingDeletionObservations(): boolean { + return ( + this._observations.find((obs) => obs.status === ObservationStatus.PendingDeletion) !== + undefined + ); + } +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts similarity index 76% rename from react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts rename to react-components/src/architecture/concrete/observations/ObservationsTool.ts index 59a8ffb2d2c..027d28a3bb7 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -12,7 +12,10 @@ import { first, sortBy } from 'lodash'; import { isDefined } from '../../../utilities/isDefined'; import { SaveObservationsCommand } from './SaveObservationsCommand'; import { DeleteObservationCommand } from './DeleteObservationCommand'; -import { createEmptyObservationProperties } from './types'; +import { createEmptyObservationProperties, isObservationIntersection } from './types'; +import { ObservationsView } from './ObservationsView'; +import { CustomObjectIntersectInput } from '@cognite/reveal'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; export class ObservationsTool extends BaseEditTool { private _isCreating: boolean = false; @@ -67,8 +70,7 @@ export class ObservationsTool extends BaseEditTool { public override async onClick(event: PointerEvent): Promise { if (this._isCreating) { - await this.createPendingObservation(event); - return; + return this.createPendingObservation(event); } await this.selectOverlayFromClick(event); } @@ -83,26 +85,40 @@ export class ObservationsTool extends BaseEditTool { } private async selectOverlayFromClick(event: PointerEvent): Promise { - const intersection = await this.getIntersection(event); + const camera = this.renderTarget.camera; + const normalizedCoords = this.getNormalizedPixelCoordinates(event); + const domainObject = this.getObservationsDomainObject(); - const domainObject = this.getIntersectedSelectableDomainObject(intersection); - if (!(domainObject instanceof ObservationsDomainObject)) { - await super.onClick(event); + if (domainObject === undefined) { return; } - const camera = this.renderTarget.camera; - const normalizedCoords = this.getNormalizedPixelCoordinates(event); + const threeView = [...domainObject.views.getByType(ObservationsView)][0]; - const intersectedOverlay = domainObject.overlayCollections - .map((collection) => collection.intersectOverlays(normalizedCoords, camera)) - .filter(isDefined); + if (threeView === undefined) { + return; + } - sortBy(intersectedOverlay, (overlay) => - camera.position.distanceToSquared(overlay.getPosition()) + const clippingPlanes = this.renderTarget.viewer.getGlobalClippingPlanes(); + + const intersectionInput = new CustomObjectIntersectInput( + normalizedCoords, + camera, + clippingPlanes ); - domainObject.setSelectedObservation(first(intersectedOverlay)); + const intersection = threeView.intersectIfCloser(intersectionInput, undefined); + + if (intersection === undefined) { + return; + } + + if (!isObservationIntersection(intersection)) { + await super.onClick(event); + return; + } + + domainObject.setSelectedObservation(intersection.userData.getContent()); } private async createPendingObservation(event: PointerEvent): Promise { @@ -114,11 +130,9 @@ export class ObservationsTool extends BaseEditTool { const domainObject = this.getObservationsDomainObject(); const pendingOverlay = domainObject?.addPendingObservation( - intersection.point, createEmptyObservationProperties(intersection.point) ); domainObject?.setSelectedObservation(pendingOverlay); - this.renderTarget.invalidate(); this.setIsCreating(false); } diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts new file mode 100644 index 00000000000..8109de5a42d --- /dev/null +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -0,0 +1,142 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { Box3, Vector3 } from 'three'; +import { type ObservationsDomainObject } from './ObservationsDomainObject'; +import { GroupThreeView } from '../../base/views/GroupThreeView'; +import { + CDF_TO_VIEWER_TRANSFORMATION, + Overlay3DCollection, + OverlayInfo, + type CustomObjectIntersectInput, + type CustomObjectIntersection +} from '@cognite/reveal'; +import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { Observation, ObservationIntersection, observationMarker } from './types'; +import { ClosestGeometryFinder } from '../../base/utilities/geometry/ClosestGeometryFinder'; +import { getColorFromStatus } from './color'; + +type ObservationCollection = Overlay3DCollection; + +export class ObservationsView extends GroupThreeView { + private _overlayCollection: ObservationCollection = new Overlay3DCollection([]); + + protected override calculateBoundingBox(): Box3 { + return this._overlayCollection + .getOverlays() + .reduce((box, overlay) => box.expandByPoint(overlay.getPosition()), new Box3()); + } + + protected override addChildren(): void { + const observations = this.domainObject.getObservations(); + + const selectedObservation = this.domainObject.getSelectedObservation(); + const overlayInfos = createObservationOverlays(observations, selectedObservation); + this._overlayCollection.removeAllOverlays(); + this._overlayCollection.addOverlays(overlayInfos); + + this.addChild(this._overlayCollection); + } + + public override update(change: DomainObjectChange): void { + super.update(change); + + if (change.isChanged(Changes.geometry)) { + this.clearMemory(); + this.invalidateRenderTarget(); + this.invalidateBoundingBox(); + } else if (change.isChanged(Changes.selected)) { + this.resetColors(); + this.invalidateRenderTarget(); + } + } + + private resetColors() { + const selectedObservation = this.domainObject.getSelectedObservation(); + this._overlayCollection.getOverlays().forEach((overlay) => { + const oldColor = overlay.getColor(); + const newColor = getColorFromStatus( + overlay.getContent().status, + overlay.getContent() === selectedObservation + ); + + if (oldColor.equals(newColor)) { + return; + } + + overlay.setColor(newColor); + }); + } + + public override intersectIfCloser( + intersectInput: CustomObjectIntersectInput, + closestDistance: number | undefined + ): undefined | CustomObjectIntersection { + const { domainObject } = this; + + const closestFinder = new ClosestGeometryFinder( + intersectInput.raycaster.ray.origin + ); + + if (closestDistance !== undefined) { + closestFinder.minDistance = closestDistance; + } + + const intersectedOverlay = this._overlayCollection.intersectOverlays( + intersectInput.normalizedCoords, + intersectInput.camera + ); + + if (intersectedOverlay === undefined || !intersectedOverlay.getVisible()) { + return undefined; + } + + const point = intersectedOverlay.getPosition(); + + if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { + return undefined; + } + + if (!closestFinder.isClosest(point)) { + return undefined; + } + + const customObjectIntersection: ObservationIntersection = { + type: 'customObject', + marker: observationMarker, + point, + distanceToCamera: closestFinder.minDistance, + customObject: this, + domainObject, + userData: intersectedOverlay + }; + + closestFinder.setClosestGeometry(customObjectIntersection); + return closestFinder.getClosestGeometry(); + } + + public getOverlays(): ObservationCollection { + return this._overlayCollection; + } +} + +function createObservationOverlays( + observations: Observation[], + selectedObservation: Observation | undefined +): OverlayInfo[] { + return observations.map((observation) => ({ + position: extractObservationPosition(observation), + content: observation, + color: getColorFromStatus(observation.status, observation === selectedObservation) + })); +} + +function extractObservationPosition(observation: Observation): Vector3 { + return new Vector3( + observation.properties.positionX, + observation.properties.positionY, + observation.properties.positionZ + ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts b/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts similarity index 100% rename from react-components/src/architecture/concrete/observationsDomainObject/SaveObservationsCommand.ts rename to react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts b/react-components/src/architecture/concrete/observations/color.ts similarity index 57% rename from react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts rename to react-components/src/architecture/concrete/observations/color.ts index 814a9f264b1..5562f1ec183 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationStatus.ts +++ b/react-components/src/architecture/concrete/observations/color.ts @@ -1,19 +1,19 @@ /*! * Copyright 2024 Cognite AS */ -import { type Color } from 'three'; -import { - convertToSelectedColor, - DEFAULT_OVERLAY_COLOR, - PENDING_DELETION_OVERLAY_COLOR, - PENDING_OVERLAY_COLOR -} from './color'; +import { Color } from 'three'; import { assertNever } from '../../../utilities/assertNever'; +import { ObservationStatus } from './types'; -export enum ObservationStatus { - Normal, - PendingCreation, - PendingDeletion +export const DEFAULT_OVERLAY_COLOR = new Color('#3333AA'); +export const PENDING_OVERLAY_COLOR = new Color('#33AA33'); +export const PENDING_DELETION_OVERLAY_COLOR = new Color('#AA3333'); + +export function convertToSelectedColor(color: Color): Color { + const hsl = { h: 0, s: 0, l: 0 }; + color.getHSL(hsl); + hsl.l = Math.sqrt(hsl.l); + return new Color().setHSL(hsl.h, hsl.s, hsl.l); } export function getColorFromStatus(status: ObservationStatus, selected: boolean): Color { @@ -27,12 +27,12 @@ export function getColorFromStatus(status: ObservationStatus, selected: boolean) function getBaseColor(status: ObservationStatus): Color { switch (status) { - case ObservationStatus.Normal: + case ObservationStatus.Default: return DEFAULT_OVERLAY_COLOR; - case ObservationStatus.PendingCreation: - return PENDING_OVERLAY_COLOR; case ObservationStatus.PendingDeletion: return PENDING_DELETION_OVERLAY_COLOR; + case ObservationStatus.PendingCreation: + return PENDING_OVERLAY_COLOR; default: assertNever(status); } diff --git a/react-components/src/architecture/concrete/observationsDomainObject/models.ts b/react-components/src/architecture/concrete/observations/models.ts similarity index 96% rename from react-components/src/architecture/concrete/observationsDomainObject/models.ts rename to react-components/src/architecture/concrete/observations/models.ts index 7afba0f2571..139bb37f811 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/models.ts +++ b/react-components/src/architecture/concrete/observations/models.ts @@ -47,7 +47,7 @@ export type CommentProperties = { text: string; }; -export type Observation = FdmNode; +export type ObservationFdmNode = FdmNode; export const OBSERVATION_SOURCE: Source = { type: 'view', diff --git a/react-components/src/architecture/concrete/observationsDomainObject/network.ts b/react-components/src/architecture/concrete/observations/network.ts similarity index 81% rename from react-components/src/architecture/concrete/observationsDomainObject/network.ts rename to react-components/src/architecture/concrete/observations/network.ts index e95e236c45f..8808e4c255d 100644 --- a/react-components/src/architecture/concrete/observationsDomainObject/network.ts +++ b/react-components/src/architecture/concrete/observations/network.ts @@ -8,12 +8,11 @@ import { type FdmSDK, type InstanceFilter } from '../../../utilities/FdmSDK'; -import { type Observation, OBSERVATION_SOURCE, type ObservationProperties } from './models'; -import { type Overlay3D } from '@cognite/reveal'; +import { type ObservationFdmNode, OBSERVATION_SOURCE, type ObservationProperties } from './models'; import { v4 as uuid } from 'uuid'; -export async function fetchObservations(fdmSdk: FdmSDK): Promise { +export async function fetchObservations(fdmSdk: FdmSDK): Promise { const observationsFilter: InstanceFilter = {}; const observationResult = await fdmSdk.filterAllInstances( @@ -27,8 +26,8 @@ export async function fetchObservations(fdmSdk: FdmSDK): Promise export async function createObservationInstances( fdmSdk: FdmSDK, - observationOverlays: Array> -): Promise { + observationOverlays: Array +): Promise { const chunks = chunk(observationOverlays, 100); const resultPromises = chunks.map(async (chunk) => { const payloads = chunk.map(createObservationInstancePayload); @@ -44,7 +43,7 @@ export async function createObservationInstances( async function fetchObservationsWithIds( fdmSdk: FdmSDK, identifiers: DmsUniqueIdentifier[] -): Promise { +): Promise { return ( await fdmSdk.filterInstances( { @@ -70,7 +69,7 @@ async function fetchObservationsWithIds( } function createObservationInstancePayload( - overlay: Overlay3D + observation: ObservationProperties ): CreateInstanceItem { const id = uuid(); return { @@ -81,7 +80,7 @@ function createObservationInstancePayload( { source: OBSERVATION_SOURCE, properties: { - ...overlay.getContent(), + ...observation, type: 'simple', sourceId: id } @@ -92,13 +91,13 @@ function createObservationInstancePayload( export async function deleteObservationInstances( fdmSdk: FdmSDK, - observations: Array> + observations: ObservationFdmNode[] ): Promise { await fdmSdk.deleteInstances( observations.map((observation) => ({ instanceType: 'node', - externalId: observation.getContent().externalId, - space: observation.getContent().space + externalId: observation.externalId, + space: observation.space })) ); } diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts new file mode 100644 index 00000000000..43eacdbc260 --- /dev/null +++ b/react-components/src/architecture/concrete/observations/types.ts @@ -0,0 +1,38 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { CDF_TO_VIEWER_TRANSFORMATION, CustomObjectIntersection, Overlay3D } from '@cognite/reveal'; +import { type ObservationProperties } from './models'; +import { type Vector3 } from 'three'; +import { FdmNode } from '../../../utilities/FdmSDK'; +import { DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; + +export enum ObservationStatus { + Default, + PendingDeletion, + PendingCreation +} + +export type Observation = { + properties: ObservationProperties; + fdmMetadata?: FdmNode; + status: ObservationStatus; +}; + +export function createEmptyObservationProperties(point: Vector3): ObservationProperties { + const cdfPosition = point.clone().applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; +} + +export const observationMarker = Symbol('observationSymbol'); + +export type ObservationIntersection = Omit & { + marker: typeof observationMarker; + userData: Overlay3D; +}; + +export function isObservationIntersection( + objectIntersection: CustomObjectIntersection +): objectIntersection is ObservationIntersection { + return (objectIntersection as ObservationIntersection).marker === observationMarker; +} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts deleted file mode 100644 index ba008ff9966..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsCache.ts +++ /dev/null @@ -1,145 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { CDF_TO_VIEWER_TRANSFORMATION, type Overlay3D, Overlay3DCollection } from '@cognite/reveal'; -import { type FdmSDK } from '../../../utilities/FdmSDK'; -import { type Observation, type ObservationProperties } from './models'; -import { Vector3 } from 'three'; - -import { isPendingObservation, type ObservationCollection, type ObservationOverlay } from './types'; -import { - createObservationInstances, - deleteObservationInstances, - fetchObservations -} from './network'; -import { ObservationStatus } from './ObservationStatus'; - -/** - * A cache that takes care of loading the observations, but also buffers changes to the overlays - * list when e.g. adding or removing observations - */ -export class ObservationsCache { - private readonly _loadedPromise: Promise; - private readonly _fdmSdk: FdmSDK; - - private readonly _persistedCollection = new Overlay3DCollection([]); - - private readonly _pendingOverlaysCollection = new Overlay3DCollection([]); - - private readonly _pendingDeletionObservations = new Set>(); - - constructor(fdmSdk: FdmSDK) { - this._loadedPromise = fetchObservations(fdmSdk) - .then((data) => { - this.initializeCollection(data); - }) - .then(); - this._fdmSdk = fdmSdk; - } - - public async getFinishedOriginalLoadingPromise(): Promise { - await this._loadedPromise; - } - - public getPersistedCollection(): Overlay3DCollection { - return this._persistedCollection; - } - - public getPendingCollection(): Overlay3DCollection { - return this._pendingOverlaysCollection; - } - - public getOverlaysPendingForRemoval(): Set> { - return this._pendingDeletionObservations; - } - - public getCollections(): ObservationCollection[] { - return [this._persistedCollection, this._pendingOverlaysCollection]; - } - - public addPendingObservation( - point: Vector3, - observation: ObservationProperties - ): Overlay3D { - return this._pendingOverlaysCollection.addOverlays([ - { position: point, content: observation } - ])[0]; - } - - public removeObservation(observation: ObservationOverlay): void { - if (isPendingObservation(observation)) { - this.removePendingObservation(observation); - } else { - this.markObservationForRemoval(observation); - } - } - - public async save(): Promise { - await this._loadedPromise; - await this.savePendingObservations(); - await this.removeDeletedObservations(); - } - - public removePendingObservation(observation: Overlay3D): void { - this._pendingOverlaysCollection.removeOverlays([observation]); - } - - public markObservationForRemoval(observation: Overlay3D): void { - this._pendingDeletionObservations.add(observation); - } - - public getObservationStatus(observation: ObservationOverlay): ObservationStatus { - if (isPendingObservation(observation)) { - return ObservationStatus.PendingCreation; - } else if ( - !isPendingObservation(observation) && - this._pendingDeletionObservations.has(observation) - ) { - return ObservationStatus.PendingDeletion; - } else { - return ObservationStatus.Normal; - } - } - - private async removeDeletedObservations(): Promise { - const overlaysToRemove = [...this._pendingDeletionObservations]; - - await deleteObservationInstances(this._fdmSdk, overlaysToRemove); - - this._persistedCollection.removeOverlays(overlaysToRemove); - this._pendingDeletionObservations.clear(); - } - - private async savePendingObservations(): Promise { - const overlays = this._pendingOverlaysCollection.getOverlays(); - if (overlays.length === 0) { - return; - } - - const instances = await createObservationInstances(this._fdmSdk, overlays); - - const overlayPositions = overlays.map((overlay) => overlay.getPosition()); - - this._pendingOverlaysCollection.removeAllOverlays(); - this._persistedCollection.addOverlays( - instances.map((instance, index) => ({ content: instance, position: overlayPositions[index] })) - ); - } - - private initializeCollection(observations: Observation[]): void { - const observationOverlays = observations.map((observation) => { - const position = new Vector3( - observation.properties.positionX, - observation.properties.positionY, - observation.properties.positionZ - ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); - - return { - position, - content: observation - }; - }); - - this._persistedCollection.addOverlays(observationOverlays); - } -} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts deleted file mode 100644 index 2961f8d0932..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsDomainObject.ts +++ /dev/null @@ -1,126 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { type Overlay3D } from '@cognite/reveal'; -import { type ObservationProperties } from './models'; -import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { type ThreeView } from '../../base/views/ThreeView'; -import { ObservationsView } from './ObservationsView'; -import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { type FdmSDK } from '../../../utilities/FdmSDK'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { ObservationsCache } from './ObservationsCache'; -import { type Vector3 } from 'three'; -import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; -import { type ObservationCollection, type ObservationOverlay } from './types'; -import { ObservationStatus } from './ObservationStatus'; - -export class ObservationsDomainObject extends VisualDomainObject { - private _selectedObservation: ObservationOverlay | undefined; - private readonly _observationsCache: ObservationsCache; - - constructor(fdmSdk: FdmSDK) { - super(); - - this._observationsCache = new ObservationsCache(fdmSdk); - void this._observationsCache.getFinishedOriginalLoadingPromise().then(() => { - this.notify(Changes.geometry); - }); - } - - public override get typeName(): TranslateKey { - return { fallback: ObservationsDomainObject.name }; - } - - protected override createThreeView(): ThreeView | undefined { - return new ObservationsView(); - } - - public override get canBeRemoved(): boolean { - return false; - } - - public override get hasPanelInfo(): boolean { - return true; - } - - public override getPanelInfo(): PanelInfo | undefined { - const info = new PanelInfo(); - const header = { fallback: 'Observation' }; - info.setHeader(header); - - info.add({ fallback: 'X', value: this._selectedObservation?.getPosition().x }); - info.add({ fallback: 'Y', value: this._selectedObservation?.getPosition().y }); - info.add({ fallback: 'Z', value: this._selectedObservation?.getPosition().z }); - - return info; - } - - public get overlayCollections(): ObservationCollection[] { - return this._observationsCache.getCollections(); - } - - public addPendingObservation( - point: Vector3, - observationData: ObservationProperties - ): Overlay3D { - const pendingObservation = this._observationsCache.addPendingObservation( - point, - observationData - ); - this.setSelectedObservation(pendingObservation); - - this.notify(Changes.added); - return pendingObservation; - } - - public removeObservation(observation: ObservationOverlay): void { - this._observationsCache.removeObservation(observation); - - if (observation === this._selectedObservation) { - this.setSelectedObservation(undefined); - } - - this.notify(Changes.geometry); - } - - public getSelectedOverlay(): ObservationOverlay | undefined { - return this._selectedObservation; - } - - public getObservationStatus(observation: ObservationOverlay): ObservationStatus { - return this._observationsCache.getObservationStatus(observation); - } - - public async save(): Promise { - if ( - this._selectedObservation !== undefined && - this._observationsCache.getObservationStatus(this._selectedObservation) !== - ObservationStatus.Normal - ) { - this.setSelectedObservation(undefined); - } - - await this._observationsCache.save(); - this.notify(Changes.geometry); - } - - public setSelectedObservation(observation: ObservationOverlay | undefined): void { - this._selectedObservation = observation; - if (this._selectedObservation === undefined) { - this.setSelectedInteractive(false); - } else { - this.setSelectedInteractive(true); - } - - this.notify(Changes.selected); - } - - public hasPendingObservations(): boolean { - return this._observationsCache.getPendingCollection().getOverlays().length !== 0; - } - - public hasPendingDeletionObservations(): boolean { - return this._observationsCache.getOverlaysPendingForRemoval().size !== 0; - } -} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts b/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts deleted file mode 100644 index 6afe25da918..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/ObservationsView.ts +++ /dev/null @@ -1,99 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { Box3 } from 'three'; -import { type ObservationsDomainObject } from './ObservationsDomainObject'; -import { GroupThreeView } from '../../base/views/GroupThreeView'; -import { type CustomObjectIntersectInput, type CustomObjectIntersection } from '@cognite/reveal'; -import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { isDefined } from '../../../utilities/isDefined'; -import { first, sortBy } from 'lodash'; -import { type ObservationOverlay } from './types'; -import { getColorFromStatus } from './ObservationStatus'; - -export class ObservationsView extends GroupThreeView { - protected override calculateBoundingBox(): Box3 { - return this.domainObject.overlayCollections - .flatMap((collection) => collection.getOverlays()) - .reduce((box, overlay) => box.expandByPoint(overlay.getPosition()), new Box3()); - } - - protected override addChildren(): void { - this.domainObject.overlayCollections.forEach((collection) => { - this.addChild(collection); - }); - } - - public override update(change: DomainObjectChange): void { - super.update(change); - - if (change.isChanged(Changes.selected, Changes.geometry)) { - const selectedOverlay = this.domainObject.getSelectedOverlay(); - const overlayCollections = this.domainObject.overlayCollections; - - overlayCollections.forEach((collection) => { - const overlays = collection.getOverlays(); - overlays.forEach((overlay) => { - const isSelected = selectedOverlay === overlay; - const observationStatus = this.domainObject.getObservationStatus(overlay); - - const color = getColorFromStatus(observationStatus, isSelected); - if (!color.equals(overlay.getColor())) { - overlay.setColor(color); - } - }); - }); - } - - this.renderTarget.invalidate(); - } - - public override intersectIfCloser( - intersectInput: CustomObjectIntersectInput, - closestDistance: number | undefined - ): undefined | CustomObjectIntersection { - const { domainObject } = this; - - const intersections = this.domainObject.overlayCollections - .map((collection) => - collection.intersectOverlays(intersectInput.normalizedCoords, intersectInput.camera) - ) - .filter(isDefined); - - const closestIntersection = first( - sortBy(intersections, (intersection) => - this.renderTarget.camera.position.distanceToSquared(intersection.getPosition()) - ) - ); - - if (closestIntersection === undefined) { - return undefined; - } - - const point = closestIntersection.getPosition(); - - const distanceToCamera = point.distanceTo(intersectInput.raycaster.ray.origin); - if (closestDistance !== undefined && closestDistance < distanceToCamera) { - return undefined; - } - - if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { - return undefined; - } - - const customObjectIntersection: DomainObjectIntersection = { - type: 'customObject', - point, - distanceToCamera, - customObject: this, - domainObject - }; - - if (this.shouldPickBoundingBox) { - customObjectIntersection.boundingBox = this.boundingBox; - } - return customObjectIntersection; - } -} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/color.ts b/react-components/src/architecture/concrete/observationsDomainObject/color.ts deleted file mode 100644 index bea19793e10..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/color.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { Color } from 'three'; - -export const DEFAULT_OVERLAY_COLOR = new Color('#3333AA'); -export const PENDING_OVERLAY_COLOR = new Color('#33AA33'); -export const PENDING_DELETION_OVERLAY_COLOR = new Color('#AA3333'); - -export function convertToSelectedColor(color: Color): Color { - const hsl = { h: 0, s: 0, l: 0 }; - color.getHSL(hsl); - hsl.l = Math.sqrt(hsl.l); - return new Color().setHSL(hsl.h, hsl.s, hsl.l); -} diff --git a/react-components/src/architecture/concrete/observationsDomainObject/types.ts b/react-components/src/architecture/concrete/observationsDomainObject/types.ts deleted file mode 100644 index 678438246e7..00000000000 --- a/react-components/src/architecture/concrete/observationsDomainObject/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { - CDF_TO_VIEWER_TRANSFORMATION, - type Overlay3D, - type Overlay3DCollection -} from '@cognite/reveal'; -import { type Observation, type ObservationProperties } from './models'; -import { type Vector3 } from 'three'; - -export type ObservationOverlay = Overlay3D | Overlay3D; -export type ObservationCollection = - | Overlay3DCollection - | Overlay3DCollection; - -export function isPendingObservation( - observationOverlay: ObservationOverlay -): observationOverlay is Overlay3D { - return (observationOverlay.getContent() as ObservationProperties).positionX !== undefined; -} - -export function createEmptyObservationProperties(point: Vector3): ObservationProperties { - const cdfPosition = point.clone().applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); - return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; -} diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 5566e8f8f5f..aaa08579235 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -10,9 +10,9 @@ import { SetFlexibleControlsTypeCommand } from '../../architecture/base/concrete import { SetAxisVisibleCommand } from '../../architecture/concrete/axis/SetAxisVisibleCommand'; import { ClipTool } from '../../architecture/concrete/clipping/ClipTool'; import { MeasurementTool } from '../../architecture/concrete/measurements/MeasurementTool'; -import { ObservationsTool } from '../../architecture/concrete/observationsDomainObject/ObservationsTool'; import { KeyboardSpeedCommand } from '../../architecture/base/concreteCommands/KeyboardSpeedCommand'; import { createCommandButton } from './CommandButton'; +import { ObservationsTool } from '../../architecture/concrete/observations/ObservationsTool'; export class RevealButtons { static FitView = (): ReactElement => createCommandButton(() => new FitViewCommand()); diff --git a/react-components/stories/utilities/getAddModelOptionsFromUrl.ts b/react-components/stories/utilities/getAddModelOptionsFromUrl.ts index 9c1b4d7686f..a2ec110e27c 100644 --- a/react-components/stories/utilities/getAddModelOptionsFromUrl.ts +++ b/react-components/stories/utilities/getAddModelOptionsFromUrl.ts @@ -16,8 +16,8 @@ export function getAddModelOptionsFromUrl(localModelUrlFallback: string): AddMod } return { - modelId: -1, - revisionId: -1, + modelId: 3544114490298106, + revisionId: 6405404576933316, localPath: modelUrl ?? localModelUrlFallback }; } From ff01008d93fbc2076759b321daf2ecb935b6578d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Fri, 12 Jul 2024 17:28:45 +0200 Subject: [PATCH 05/18] chore: lint fix --- .../observations/ObservationsCache.ts | 7 +--- .../observations/ObservationsDomainObject.ts | 4 +- .../concrete/observations/ObservationsTool.ts | 6 +-- .../concrete/observations/ObservationsView.ts | 42 +++++++++---------- .../concrete/observations/network.ts | 2 +- .../concrete/observations/types.ts | 10 +++-- 6 files changed, 34 insertions(+), 37 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsCache.ts b/react-components/src/architecture/concrete/observations/ObservationsCache.ts index 222c404095e..743b2b8e451 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsCache.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsCache.ts @@ -4,7 +4,7 @@ import { type FdmSDK } from '../../../utilities/FdmSDK'; import { type ObservationFdmNode, type ObservationProperties } from './models'; -import { Observation } from './types'; +import { type Observation } from './types'; import { createObservationInstances, deleteObservationInstances, @@ -34,13 +34,10 @@ export class ObservationsCache { return; } - console.log('Thinking to delete', observations); const observationData = observations .map((observation) => observation.fdmMetadata) .filter(isDefined); - console.log('Deleting ', observationData); - await deleteObservationInstances(this._fdmSdk, observationData); } @@ -51,8 +48,6 @@ export class ObservationsCache { return []; } - console.log('Saving ', observations); - return await createObservationInstances(this._fdmSdk, observations); } } diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index a542308ee70..04cf4f80a44 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -9,9 +9,9 @@ import { type FdmSDK } from '../../../utilities/FdmSDK'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { ObservationsCache } from './ObservationsCache'; import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; -import { Observation, ObservationStatus } from './types'; +import { type Observation, ObservationStatus } from './types'; import { partition, remove } from 'lodash'; -import { ObservationProperties } from './models'; +import { type ObservationProperties } from './models'; export class ObservationsDomainObject extends VisualDomainObject { private _selectedObservation: Observation | undefined; diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 027d28a3bb7..8a3ad157f08 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -8,14 +8,11 @@ import { BaseEditTool } from '../../base/commands/BaseEditTool'; import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; import { type BaseCommand } from '../../base/commands/BaseCommand'; import { CreateObservationCommand } from './CreateObservationCommand'; -import { first, sortBy } from 'lodash'; -import { isDefined } from '../../../utilities/isDefined'; import { SaveObservationsCommand } from './SaveObservationsCommand'; import { DeleteObservationCommand } from './DeleteObservationCommand'; import { createEmptyObservationProperties, isObservationIntersection } from './types'; import { ObservationsView } from './ObservationsView'; import { CustomObjectIntersectInput } from '@cognite/reveal'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; export class ObservationsTool extends BaseEditTool { private _isCreating: boolean = false; @@ -70,7 +67,8 @@ export class ObservationsTool extends BaseEditTool { public override async onClick(event: PointerEvent): Promise { if (this._isCreating) { - return this.createPendingObservation(event); + await this.createPendingObservation(event); + return; } await this.selectOverlayFromClick(event); } diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index 8109de5a42d..27aa783d2b7 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -7,21 +7,21 @@ import { GroupThreeView } from '../../base/views/GroupThreeView'; import { CDF_TO_VIEWER_TRANSFORMATION, Overlay3DCollection, - OverlayInfo, + type OverlayInfo, type CustomObjectIntersectInput, type CustomObjectIntersection } from '@cognite/reveal'; import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { Observation, ObservationIntersection, observationMarker } from './types'; +import { type Observation, type ObservationIntersection, observationMarker } from './types'; import { ClosestGeometryFinder } from '../../base/utilities/geometry/ClosestGeometryFinder'; import { getColorFromStatus } from './color'; type ObservationCollection = Overlay3DCollection; export class ObservationsView extends GroupThreeView { - private _overlayCollection: ObservationCollection = new Overlay3DCollection([]); + private readonly _overlayCollection: ObservationCollection = new Overlay3DCollection([]); protected override calculateBoundingBox(): Box3 { return this._overlayCollection @@ -53,23 +53,6 @@ export class ObservationsView extends GroupThreeView { } } - private resetColors() { - const selectedObservation = this.domainObject.getSelectedObservation(); - this._overlayCollection.getOverlays().forEach((overlay) => { - const oldColor = overlay.getColor(); - const newColor = getColorFromStatus( - overlay.getContent().status, - overlay.getContent() === selectedObservation - ); - - if (oldColor.equals(newColor)) { - return; - } - - overlay.setColor(newColor); - }); - } - public override intersectIfCloser( intersectInput: CustomObjectIntersectInput, closestDistance: number | undefined @@ -120,12 +103,29 @@ export class ObservationsView extends GroupThreeView { public getOverlays(): ObservationCollection { return this._overlayCollection; } + + private resetColors(): void { + const selectedObservation = this.domainObject.getSelectedObservation(); + this._overlayCollection.getOverlays().forEach((overlay) => { + const oldColor = overlay.getColor(); + const newColor = getColorFromStatus( + overlay.getContent().status, + overlay.getContent() === selectedObservation + ); + + if (oldColor.equals(newColor)) { + return; + } + + overlay.setColor(newColor); + }); + } } function createObservationOverlays( observations: Observation[], selectedObservation: Observation | undefined -): OverlayInfo[] { +): Array> { return observations.map((observation) => ({ position: extractObservationPosition(observation), content: observation, diff --git a/react-components/src/architecture/concrete/observations/network.ts b/react-components/src/architecture/concrete/observations/network.ts index 8808e4c255d..98eb4d17a9a 100644 --- a/react-components/src/architecture/concrete/observations/network.ts +++ b/react-components/src/architecture/concrete/observations/network.ts @@ -26,7 +26,7 @@ export async function fetchObservations(fdmSdk: FdmSDK): Promise + observationOverlays: ObservationProperties[] ): Promise { const chunks = chunk(observationOverlays, 100); const resultPromises = chunks.map(async (chunk) => { diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts index 43eacdbc260..4a6b4670d3b 100644 --- a/react-components/src/architecture/concrete/observations/types.ts +++ b/react-components/src/architecture/concrete/observations/types.ts @@ -1,11 +1,15 @@ /*! * Copyright 2024 Cognite AS */ -import { CDF_TO_VIEWER_TRANSFORMATION, CustomObjectIntersection, Overlay3D } from '@cognite/reveal'; +import { + CDF_TO_VIEWER_TRANSFORMATION, + type CustomObjectIntersection, + type Overlay3D +} from '@cognite/reveal'; import { type ObservationProperties } from './models'; import { type Vector3 } from 'three'; -import { FdmNode } from '../../../utilities/FdmSDK'; -import { DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { type FdmNode } from '../../../utilities/FdmSDK'; +import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; export enum ObservationStatus { Default, From e086ad70bb3fd167f09f8a411cb230533f8c3d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 09:48:01 +0200 Subject: [PATCH 06/18] fix: logic related to removing/adding pending/created observations --- .../observations/ObservationsDomainObject.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index 04cf4f80a44..faafb7d5c27 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -101,7 +101,7 @@ export class ObservationsDomainObject extends VisualDomainObject { this.setSelectedObservation(undefined); } - const [toRemove, toKeep] = partition( + const [toRemove, notToRemove] = partition( this._observations, (observation) => observation.status === ObservationStatus.PendingDeletion ); @@ -115,13 +115,15 @@ export class ObservationsDomainObject extends VisualDomainObject { observationsToCreate.map((obs) => obs.properties) ); - this._observations = toKeep.concat( - newObservations.map((observation) => ({ - status: ObservationStatus.Default, - fdmMetadata: observation, - properties: observation.properties - })) - ); + this._observations = notToRemove + .filter((observation) => observation.status === ObservationStatus.Default) + .concat( + newObservations.map((observation) => ({ + status: ObservationStatus.Default, + fdmMetadata: observation, + properties: observation.properties + })) + ); await deletePromise; From 7b85b2694c61e8237875281ceb35ca40840e75a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 09:49:52 +0200 Subject: [PATCH 07/18] chore: don't select interactive --- .../concrete/observations/ObservationsDomainObject.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index faafb7d5c27..dcd77336b4d 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -132,11 +132,6 @@ export class ObservationsDomainObject extends VisualDomainObject { public setSelectedObservation(observation: Observation | undefined): void { this._selectedObservation = observation; - if (this._selectedObservation === undefined) { - this.setSelectedInteractive(false); - } else { - this.setSelectedInteractive(true); - } this.notify(Changes.selected); } From d6e7034a7309ef38465394ed94363abb14a5ff11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 09:50:02 +0200 Subject: [PATCH 08/18] chore: simplify pending-queries --- .../concrete/observations/ObservationsDomainObject.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index dcd77336b4d..af6762bcbe2 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -137,17 +137,14 @@ export class ObservationsDomainObject extends VisualDomainObject { } public hasPendingObservations(): boolean { - return ( - this._observations.find( - (observation) => observation.status === ObservationStatus.PendingCreation - ) !== undefined + return this._observations.some( + (observation) => observation.status === ObservationStatus.PendingCreation ); } public hasPendingDeletionObservations(): boolean { - return ( - this._observations.find((obs) => obs.status === ObservationStatus.PendingDeletion) !== - undefined + return this._observations.some( + (observations) => observations.status === ObservationStatus.PendingDeletion ); } } From b5786501134d6e6acdda282c366a6c808aa3fed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 09:50:46 +0200 Subject: [PATCH 09/18] feat: handle clipping --- .../concrete/observations/ObservationsTool.ts | 1 - .../concrete/observations/ObservationsView.ts | 29 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 8a3ad157f08..55bd8e67439 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -62,7 +62,6 @@ export class ObservationsTool extends BaseEditTool { public override onDeactivate(): void { const domainObject = this.getObservationsDomainObject(); domainObject?.setSelectedObservation(undefined); - domainObject?.setVisibleInteractive(false, this.renderTarget); } public override async onClick(event: PointerEvent): Promise { diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index 27aa783d2b7..9a07b7c7a7f 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -1,7 +1,7 @@ /*! * Copyright 2024 Cognite AS */ -import { Box3, Vector3 } from 'three'; +import { Box3, Plane, Vector3 } from 'three'; import { type ObservationsDomainObject } from './ObservationsDomainObject'; import { GroupThreeView } from '../../base/views/GroupThreeView'; import { @@ -26,7 +26,10 @@ export class ObservationsView extends GroupThreeView { protected override calculateBoundingBox(): Box3 { return this._overlayCollection .getOverlays() - .reduce((box, overlay) => box.expandByPoint(overlay.getPosition()), new Box3()); + .reduce( + (box, overlay) => (overlay.getVisible() ? box.expandByPoint(overlay.getPosition()) : box), + new Box3() + ); } protected override addChildren(): void { @@ -35,7 +38,9 @@ export class ObservationsView extends GroupThreeView { const selectedObservation = this.domainObject.getSelectedObservation(); const overlayInfos = createObservationOverlays(observations, selectedObservation); this._overlayCollection.removeAllOverlays(); + this._overlayCollection.addOverlays(overlayInfos); + this.updateClipping(); this.addChild(this._overlayCollection); } @@ -47,8 +52,10 @@ export class ObservationsView extends GroupThreeView { this.clearMemory(); this.invalidateRenderTarget(); this.invalidateBoundingBox(); + } else if (change.isChanged(Changes.clipping)) { + this.updateClipping(); } else if (change.isChanged(Changes.selected)) { - this.resetColors(); + this.updateColors(); this.invalidateRenderTarget(); } } @@ -104,7 +111,17 @@ export class ObservationsView extends GroupThreeView { return this._overlayCollection; } - private resetColors(): void { + private updateClipping(): void { + const clippingPlanes = this.renderTarget.getGlobalClippingPlanes(); + this._overlayCollection.getOverlays().forEach((overlay) => { + const isVisible = !isClipped(overlay.getPosition(), clippingPlanes); + if (isVisible !== overlay.getVisible()) { + overlay.setVisible(isVisible); + } + }); + } + + private updateColors(): void { const selectedObservation = this.domainObject.getSelectedObservation(); this._overlayCollection.getOverlays().forEach((overlay) => { const oldColor = overlay.getColor(); @@ -140,3 +157,7 @@ function extractObservationPosition(observation: Observation): Vector3 { observation.properties.positionZ ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); } + +function isClipped(point: Vector3, planes: Plane[]): boolean { + return planes.some((plane) => plane.distanceToPoint(point) < 0); +} From c82151e6ff570867357723c32367a32feaf34788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 09:59:37 +0200 Subject: [PATCH 10/18] chore: simplify overlay selection code --- .../concrete/observations/ObservationsTool.ts | 32 ++----------------- .../concrete/observations/types.ts | 10 ++++-- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 55bd8e67439..95735a1bd98 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -82,40 +82,14 @@ export class ObservationsTool extends BaseEditTool { } private async selectOverlayFromClick(event: PointerEvent): Promise { - const camera = this.renderTarget.camera; - const normalizedCoords = this.getNormalizedPixelCoordinates(event); - const domainObject = this.getObservationsDomainObject(); - - if (domainObject === undefined) { - return; - } - - const threeView = [...domainObject.views.getByType(ObservationsView)][0]; - - if (threeView === undefined) { - return; - } - - const clippingPlanes = this.renderTarget.viewer.getGlobalClippingPlanes(); - - const intersectionInput = new CustomObjectIntersectInput( - normalizedCoords, - camera, - clippingPlanes - ); - - const intersection = threeView.intersectIfCloser(intersectionInput, undefined); - - if (intersection === undefined) { - return; - } + const intersection = await this.getIntersection(event); - if (!isObservationIntersection(intersection)) { + if (intersection === undefined || !isObservationIntersection(intersection)) { await super.onClick(event); return; } - domainObject.setSelectedObservation(intersection.userData.getContent()); + intersection.domainObject.setSelectedObservation(intersection.userData.getContent()); } private async createPendingObservation(event: PointerEvent): Promise { diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts index 4a6b4670d3b..bf2bc1801a9 100644 --- a/react-components/src/architecture/concrete/observations/types.ts +++ b/react-components/src/architecture/concrete/observations/types.ts @@ -2,6 +2,7 @@ * Copyright 2024 Cognite AS */ import { + AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION, type CustomObjectIntersection, type Overlay3D @@ -10,6 +11,7 @@ import { type ObservationProperties } from './models'; import { type Vector3 } from 'three'; import { type FdmNode } from '../../../utilities/FdmSDK'; import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { ObservationsDomainObject } from './ObservationsDomainObject'; export enum ObservationStatus { Default, @@ -30,13 +32,17 @@ export function createEmptyObservationProperties(point: Vector3): ObservationPro export const observationMarker = Symbol('observationSymbol'); -export type ObservationIntersection = Omit & { +export type ObservationIntersection = Omit< + DomainObjectIntersection, + 'userData' | 'domainObject' +> & { marker: typeof observationMarker; + domainObject: ObservationsDomainObject; userData: Overlay3D; }; export function isObservationIntersection( - objectIntersection: CustomObjectIntersection + objectIntersection: AnyIntersection ): objectIntersection is ObservationIntersection { return (objectIntersection as ObservationIntersection).marker === observationMarker; } From ee20745fa2d00517f5df18b3d7ee476412ea8f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 10:08:22 +0200 Subject: [PATCH 11/18] chore: only save through SaveCommand --- .../architecture/concrete/observations/ObservationsTool.ts | 5 ----- .../concrete/observations/SaveObservationsCommand.ts | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 95735a1bd98..8baed4c25ba 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -76,11 +76,6 @@ export class ObservationsTool extends BaseEditTool { return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); } - public async save(): Promise { - const domainObject = this.getObservationsDomainObject(); - await domainObject?.save(); - } - private async selectOverlayFromClick(event: PointerEvent): Promise { const intersection = await this.getIntersection(event); diff --git a/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts b/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts index ef6f8d1c8a9..379495e2531 100644 --- a/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts +++ b/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts @@ -35,8 +35,10 @@ export class SaveObservationsCommand extends ObservationsCommand { return false; } - void tool - .save() + const domainObject = this.getObservationsDomainObject(); + + void domainObject + ?.save() .then(() => { toast.success({ fallback: 'Successfully published changes' }.fallback); From 0655249799a8eb32bef981c450449073a766a792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 10:36:33 +0200 Subject: [PATCH 12/18] chore: small refactor, increase type-safety of intersection --- .../concrete/observations/ObservationsTool.ts | 7 ++--- .../concrete/observations/ObservationsView.ts | 14 ++++----- .../concrete/observations/types.ts | 29 ++++++++++++++----- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 8baed4c25ba..8fe347d2911 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -80,18 +80,17 @@ export class ObservationsTool extends BaseEditTool { const intersection = await this.getIntersection(event); if (intersection === undefined || !isObservationIntersection(intersection)) { - await super.onClick(event); - return; + return await super.onClick(event); } - intersection.domainObject.setSelectedObservation(intersection.userData.getContent()); + intersection.domainObject.setSelectedObservation(intersection.userData); } private async createPendingObservation(event: PointerEvent): Promise { const intersection = await this.getIntersection(event); if (intersection === undefined) { - return; + return await super.onClick(event); } const domainObject = this.getObservationsDomainObject(); diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index 9a07b7c7a7f..9425d3f9a3d 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -14,7 +14,7 @@ import { import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { type Observation, type ObservationIntersection, observationMarker } from './types'; +import { createObservationIntersection, type Observation } from './types'; import { ClosestGeometryFinder } from '../../base/utilities/geometry/ClosestGeometryFinder'; import { getColorFromStatus } from './color'; @@ -93,15 +93,13 @@ export class ObservationsView extends GroupThreeView { return undefined; } - const customObjectIntersection: ObservationIntersection = { - type: 'customObject', - marker: observationMarker, + const customObjectIntersection = createObservationIntersection( point, - distanceToCamera: closestFinder.minDistance, - customObject: this, + closestFinder.minDistance, + this, domainObject, - userData: intersectedOverlay - }; + intersectedOverlay.getContent() + ); closestFinder.setClosestGeometry(customObjectIntersection); return closestFinder.getClosestGeometry(); diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts index bf2bc1801a9..e2ddce377a7 100644 --- a/react-components/src/architecture/concrete/observations/types.ts +++ b/react-components/src/architecture/concrete/observations/types.ts @@ -1,12 +1,7 @@ /*! * Copyright 2024 Cognite AS */ -import { - AnyIntersection, - CDF_TO_VIEWER_TRANSFORMATION, - type CustomObjectIntersection, - type Overlay3D -} from '@cognite/reveal'; +import { AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION, ICustomObject } from '@cognite/reveal'; import { type ObservationProperties } from './models'; import { type Vector3 } from 'three'; import { type FdmNode } from '../../../utilities/FdmSDK'; @@ -30,7 +25,7 @@ export function createEmptyObservationProperties(point: Vector3): ObservationPro return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; } -export const observationMarker = Symbol('observationSymbol'); +const observationMarker = Symbol('observationSymbol'); export type ObservationIntersection = Omit< DomainObjectIntersection, @@ -38,9 +33,27 @@ export type ObservationIntersection = Omit< > & { marker: typeof observationMarker; domainObject: ObservationsDomainObject; - userData: Overlay3D; + userData: Observation; }; +export function createObservationIntersection( + point: Vector3, + distanceToCamera: number, + customObject: ICustomObject, + domainObject: ObservationsDomainObject, + overlay: Observation +): ObservationIntersection { + return { + type: 'customObject', + marker: observationMarker, + point, + distanceToCamera, + customObject, + domainObject, + userData: overlay + }; +} + export function isObservationIntersection( objectIntersection: AnyIntersection ): objectIntersection is ObservationIntersection { From 6f6816b8cdace7360bdb4260d13f3dd8218c7103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 10:39:18 +0200 Subject: [PATCH 13/18] chore: lint fix --- .../concrete/observations/ObservationsTool.ts | 8 ++++---- .../concrete/observations/ObservationsView.ts | 2 +- .../src/architecture/concrete/observations/types.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index 8fe347d2911..faf0e42ad8c 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -11,8 +11,6 @@ import { CreateObservationCommand } from './CreateObservationCommand'; import { SaveObservationsCommand } from './SaveObservationsCommand'; import { DeleteObservationCommand } from './DeleteObservationCommand'; import { createEmptyObservationProperties, isObservationIntersection } from './types'; -import { ObservationsView } from './ObservationsView'; -import { CustomObjectIntersectInput } from '@cognite/reveal'; export class ObservationsTool extends BaseEditTool { private _isCreating: boolean = false; @@ -80,7 +78,8 @@ export class ObservationsTool extends BaseEditTool { const intersection = await this.getIntersection(event); if (intersection === undefined || !isObservationIntersection(intersection)) { - return await super.onClick(event); + await super.onClick(event); + return; } intersection.domainObject.setSelectedObservation(intersection.userData); @@ -90,7 +89,8 @@ export class ObservationsTool extends BaseEditTool { const intersection = await this.getIntersection(event); if (intersection === undefined) { - return await super.onClick(event); + await super.onClick(event); + return; } const domainObject = this.getObservationsDomainObject(); diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index 9425d3f9a3d..2817fb87d22 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -1,7 +1,7 @@ /*! * Copyright 2024 Cognite AS */ -import { Box3, Plane, Vector3 } from 'three'; +import { Box3, type Plane, Vector3 } from 'three'; import { type ObservationsDomainObject } from './ObservationsDomainObject'; import { GroupThreeView } from '../../base/views/GroupThreeView'; import { diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts index e2ddce377a7..a7271501ec5 100644 --- a/react-components/src/architecture/concrete/observations/types.ts +++ b/react-components/src/architecture/concrete/observations/types.ts @@ -1,12 +1,16 @@ /*! * Copyright 2024 Cognite AS */ -import { AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION, ICustomObject } from '@cognite/reveal'; +import { + type AnyIntersection, + CDF_TO_VIEWER_TRANSFORMATION, + type ICustomObject +} from '@cognite/reveal'; import { type ObservationProperties } from './models'; import { type Vector3 } from 'three'; import { type FdmNode } from '../../../utilities/FdmSDK'; import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; -import { ObservationsDomainObject } from './ObservationsDomainObject'; +import { type ObservationsDomainObject } from './ObservationsDomainObject'; export enum ObservationStatus { Default, From fbf4dee2adcc00bf85f133180156f2098233a190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 10:39:34 +0200 Subject: [PATCH 14/18] chore: revert unintended changes to util function --- .../stories/utilities/getAddModelOptionsFromUrl.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/react-components/stories/utilities/getAddModelOptionsFromUrl.ts b/react-components/stories/utilities/getAddModelOptionsFromUrl.ts index a2ec110e27c..9c1b4d7686f 100644 --- a/react-components/stories/utilities/getAddModelOptionsFromUrl.ts +++ b/react-components/stories/utilities/getAddModelOptionsFromUrl.ts @@ -16,8 +16,8 @@ export function getAddModelOptionsFromUrl(localModelUrlFallback: string): AddMod } return { - modelId: 3544114490298106, - revisionId: 6405404576933316, + modelId: -1, + revisionId: -1, localPath: modelUrl ?? localModelUrlFallback }; } From 9bdc575fb4cb03de166e5e73e90a8e0750d01919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Mon, 15 Jul 2024 13:29:17 +0200 Subject: [PATCH 15/18] chore: don't store fdmSdk in cache --- .../concrete/observations/ObservationsCache.ts | 9 ++++----- .../concrete/observations/ObservationsDomainObject.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsCache.ts b/react-components/src/architecture/concrete/observations/ObservationsCache.ts index 743b2b8e451..c5f6b05d26c 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsCache.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsCache.ts @@ -18,18 +18,16 @@ import { isDefined } from '../../../utilities/isDefined'; */ export class ObservationsCache { private readonly _loadedPromise: Promise; - private readonly _fdmSdk: FdmSDK; constructor(fdmSdk: FdmSDK) { this._loadedPromise = fetchObservations(fdmSdk); - this._fdmSdk = fdmSdk; } public async getFinishedOriginalLoadingPromise(): Promise { return await this._loadedPromise; } - public async deleteObservations(observations: Observation[]): Promise { + public async deleteObservations(fdmSdk: FdmSDK, observations: Observation[]): Promise { if (observations.length === 0) { return; } @@ -38,16 +36,17 @@ export class ObservationsCache { .map((observation) => observation.fdmMetadata) .filter(isDefined); - await deleteObservationInstances(this._fdmSdk, observationData); + await deleteObservationInstances(fdmSdk, observationData); } public async saveObservations( + fdmSdk: FdmSDK, observations: ObservationProperties[] ): Promise { if (observations.length === 0) { return []; } - return await createObservationInstances(this._fdmSdk, observations); + return await createObservationInstances(fdmSdk, observations); } } diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index af6762bcbe2..34f69e72cf8 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -93,6 +93,12 @@ export class ObservationsDomainObject extends VisualDomainObject { } public async save(): Promise { + const fdmSdk = this.rootDomainObject?.renderTarget.fdmSdk; + + if (fdmSdk === undefined) { + return fdmSdk; + } + if ( this._selectedObservation !== undefined && (this._selectedObservation.status === ObservationStatus.PendingCreation || @@ -106,12 +112,13 @@ export class ObservationsDomainObject extends VisualDomainObject { (observation) => observation.status === ObservationStatus.PendingDeletion ); - const deletePromise = this._observationsCache.deleteObservations(toRemove); + const deletePromise = this._observationsCache.deleteObservations(fdmSdk, toRemove); const observationsToCreate = this._observations.filter( (obs) => obs.status === ObservationStatus.PendingCreation ); const newObservations = await this._observationsCache.saveObservations( + fdmSdk, observationsToCreate.map((obs) => obs.properties) ); From b71810f552a70d4b47b54300a94de88bbad747e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Tue, 16 Jul 2024 09:34:06 +0200 Subject: [PATCH 16/18] chore: use more getters --- .../concrete/observations/DeleteObservationCommand.ts | 4 ++-- .../concrete/observations/ObservationsDomainObject.ts | 4 ++-- .../architecture/concrete/observations/ObservationsView.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts b/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts index ce46d1c348a..b1cabaa2716 100644 --- a/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts +++ b/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts @@ -26,12 +26,12 @@ export class DeleteObservationCommand extends ObservationsCommand { public override get isEnabled(): boolean { const observation = this.getObservationsDomainObject(); - return observation?.getSelectedObservation() !== undefined; + return observation?.selectedObservation !== undefined; } protected override invokeCore(): boolean { const observations = this.getObservationsDomainObject(); - const selectedOverlay = observations?.getSelectedObservation(); + const selectedOverlay = observations?.selectedObservation; if (observations === undefined || selectedOverlay === undefined) { return false; } diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts index 34f69e72cf8..21530203a68 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts @@ -84,11 +84,11 @@ export class ObservationsDomainObject extends VisualDomainObject { this.notify(Changes.geometry); } - public getObservations(): Observation[] { + public get observations(): Observation[] { return this._observations; } - public getSelectedObservation(): Observation | undefined { + public get selectedObservation(): Observation | undefined { return this._selectedObservation; } diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index 2817fb87d22..e7e5069388c 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -33,9 +33,9 @@ export class ObservationsView extends GroupThreeView { } protected override addChildren(): void { - const observations = this.domainObject.getObservations(); + const observations = this.domainObject.observations; - const selectedObservation = this.domainObject.getSelectedObservation(); + const selectedObservation = this.domainObject.selectedObservation; const overlayInfos = createObservationOverlays(observations, selectedObservation); this._overlayCollection.removeAllOverlays(); From 67003beec7efbfe5bb1b321d70cd93974cc9ec90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Tue, 16 Jul 2024 10:00:35 +0200 Subject: [PATCH 17/18] chore: move public methods --- .../concrete/observations/ObservationsTool.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts index faf0e42ad8c..e3c63ef7ec1 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsTool.ts @@ -19,19 +19,6 @@ export class ObservationsTool extends BaseEditTool { return domainObject instanceof ObservationsDomainObject; } - public get isCreating(): boolean { - return this._isCreating; - } - - public setIsCreating(value: boolean): void { - this._isCreating = value; - if (value) { - this.renderTarget.setCrosshairCursor(); - } else { - this.renderTarget.setNavigateCursor(); - } - } - public override get icon(): IconType { return 'Location'; } @@ -74,6 +61,19 @@ export class ObservationsTool extends BaseEditTool { return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); } + public get isCreating(): boolean { + return this._isCreating; + } + + public setIsCreating(value: boolean): void { + this._isCreating = value; + if (value) { + this.renderTarget.setCrosshairCursor(); + } else { + this.renderTarget.setNavigateCursor(); + } + } + private async selectOverlayFromClick(event: PointerEvent): Promise { const intersection = await this.getIntersection(event); From 60d4e8b0514aca67a791a832f0f318f068f35020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= Date: Tue, 16 Jul 2024 10:00:52 +0200 Subject: [PATCH 18/18] chore: simplify bounding box computation, invalidate stuff, skip clip check --- .../concrete/observations/ObservationsView.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/observations/ObservationsView.ts index e7e5069388c..58cdc9698df 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/observations/ObservationsView.ts @@ -24,12 +24,13 @@ export class ObservationsView extends GroupThreeView { private readonly _overlayCollection: ObservationCollection = new Overlay3DCollection([]); protected override calculateBoundingBox(): Box3 { - return this._overlayCollection - .getOverlays() - .reduce( - (box, overlay) => (overlay.getVisible() ? box.expandByPoint(overlay.getPosition()) : box), - new Box3() - ); + const boundingBox = new Box3().makeEmpty(); + for (const overlay of this._overlayCollection.getOverlays()) { + if (overlay.getVisible()) { + boundingBox.expandByPoint(overlay.getPosition()); + } + } + return boundingBox; } protected override addChildren(): void { @@ -54,6 +55,8 @@ export class ObservationsView extends GroupThreeView { this.invalidateBoundingBox(); } else if (change.isChanged(Changes.clipping)) { this.updateClipping(); + this.invalidateRenderTarget(); + this.invalidateBoundingBox(); } else if (change.isChanged(Changes.selected)) { this.updateColors(); this.invalidateRenderTarget(); @@ -85,10 +88,6 @@ export class ObservationsView extends GroupThreeView { const point = intersectedOverlay.getPosition(); - if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { - return undefined; - } - if (!closestFinder.isClosest(point)) { return undefined; } @@ -120,7 +119,7 @@ export class ObservationsView extends GroupThreeView { } private updateColors(): void { - const selectedObservation = this.domainObject.getSelectedObservation(); + const selectedObservation = this.domainObject.selectedObservation; this._overlayCollection.getOverlays().forEach((overlay) => { const oldColor = overlay.getColor(); const newColor = getColorFromStatus(