From bd358c31b85855ed2a15d2fa97c27091bb147976 Mon Sep 17 00:00:00 2001 From: Nils Petter Fremming <35219649+nilscognite@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:21:33 +0200 Subject: [PATCH] (feat) Adding unit to measurments + fixing picking on the measurment views + Renaming (#4563) * Generalize more on Commands * Update MeasureBoxDomainObject.ts * Update CommandButton.tsx * Update and fix react stuff * Make implement line intersection * Add comment * Add a simple unit system * The final touch. Clean up some command issues and made it simpler * Smaller adjustments --- react-components/package.json | 4 +- .../architecture/base/commands/BaseCommand.ts | 35 ++++-- .../architecture/base/commands/BaseTool.ts | 4 - .../base/commands/NavigationTool.ts | 4 +- .../CopyToClipboardCommand.ts | 57 ++++++++++ .../DeleteDomainObjectCommand.ts | 42 +++++++ .../base/concreteCommands/FitViewCommand.ts | 4 +- .../SetFlexibleControlsTypeCommand.ts | 9 +- .../ToogleMetricUnitsCommand.ts | 35 ++++++ .../base/domainObjects/DomainObject.ts | 15 ++- .../base/domainObjects/RootDomainObject.ts | 10 ++ .../base/domainObjectsHelpers/Changes.ts | 1 + .../base/domainObjectsHelpers/PanelInfo.ts | 45 ++------ .../base/domainObjectsHelpers/Quantity.ts | 11 ++ .../reactUpdaters/DomainObjectPanelUpdater.ts | 2 +- .../base/renderTarget/UnitSystem.ts | 80 ++++++++++++++ .../base/utilities/TranslateKey.ts | 9 ++ .../geometry/ClosestGeometryFinder.ts | 62 +++++++++++ .../concrete/axis/SetAxisVisibleCommand.ts | 8 +- .../boxDomainObject/MeasureBoxDomainObject.ts | 35 +++--- .../boxDomainObject/MeasureBoxView.ts | 32 ++++-- .../MeasureLineDomainObject.ts | 14 +-- .../boxDomainObject/MeasureLineView.ts | 83 +++++++++++++- .../concrete/boxDomainObject/MeasureType.ts | 4 +- .../boxDomainObject/MeasurementTool.ts | 9 +- .../boxDomainObject/SetCropBoxCommand.ts | 8 +- .../SetMeasurmentTypeCommand.ts | 9 +- .../ShowMeasurmentsOnTopCommand.ts | 8 +- .../ExampleDomainObject.ts | 13 ++- .../exampleDomainObject/ExampleTool.ts | 6 +- .../commands/DeleteAllExamplesCommand.ts | 4 +- .../commands/ResetAllExamplesCommand.ts | 4 +- .../commands/ShowAllExamplesCommand.ts | 8 +- .../SetTerrainVisibleCommand.ts | 4 +- .../UpdateTerrainCommand.ts | 4 +- .../components/Architecture/CommandButton.tsx | 47 ++++++-- .../Architecture/DomainObjectPanel.tsx | 104 ++++++++---------- .../components/Architecture/RevealButtons.tsx | 14 +-- .../src/components/Architecture/Toolbar.tsx | 19 +--- .../RevealToolbar/SetFlexibleControlsType.tsx | 3 +- react-components/yarn.lock | 12 +- 41 files changed, 633 insertions(+), 248 deletions(-) create mode 100644 react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts create mode 100644 react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts create mode 100644 react-components/src/architecture/base/concreteCommands/ToogleMetricUnitsCommand.ts create mode 100644 react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts create mode 100644 react-components/src/architecture/base/renderTarget/UnitSystem.ts create mode 100644 react-components/src/architecture/base/utilities/TranslateKey.ts create mode 100644 react-components/src/architecture/base/utilities/geometry/ClosestGeometryFinder.ts diff --git a/react-components/package.json b/react-components/package.json index 57dd5426459..035e6fd43c9 100644 --- a/react-components/package.json +++ b/react-components/package.json @@ -29,7 +29,7 @@ "sort-keys": "cdf-i18n-utils-cli sort-local-keys --namespace reveal-react-components --path ./src/common/i18n" }, "peerDependencies": { - "@cognite/cogs.js": ">=9", + "@cognite/cogs.js": ">=9.84.3", "@cognite/reveal": "4.14.5", "react": ">=18", "react-dom": ">=18", @@ -43,7 +43,7 @@ "devDependencies": { "@cognite/cdf-i18n-utils": "^0.7.5", "@cognite/cdf-utilities": "^3.6.0", - "@cognite/cogs.js": "^9.3.0", + "@cognite/cogs.js": "^9.84.3", "@cognite/reveal": "^4.14.5", "@cognite/sdk": "^9.13.0", "@playwright/test": "^1.43.1", diff --git a/react-components/src/architecture/base/commands/BaseCommand.ts b/react-components/src/architecture/base/commands/BaseCommand.ts index 04ee863126d..a01bd891b0d 100644 --- a/react-components/src/architecture/base/commands/BaseCommand.ts +++ b/react-components/src/architecture/base/commands/BaseCommand.ts @@ -2,31 +2,43 @@ * Copyright 2024 Cognite AS */ +import { type TranslateKey } from '../utilities/TranslateKey'; import { clear, remove } from '../utilities/extensions/arrayExtensions'; type UpdateDelegate = (command: BaseCommand) => void; -export type Tooltip = { - key: string; - fallback?: string; -}; - /** * Base class for all command and tools. Thses are object that can do a * user interaction with the system. It also have enough information to * generate the UI for the command. */ + export abstract class BaseCommand { + private static _counter: number = 0; // Counter for the unique index + // ================================================== // INSTANCE FIELDS // ================================================== private readonly _listeners: UpdateDelegate[] = []; + // Unique index for the command, used by in React to force rerender + // when the command changes for a button. + public readonly _uniqueIndex: number; + + public get uniqueIndex(): number { + return this._uniqueIndex; + } + // ================================================== // VIRTUAL METHODS (To be override) // ================================================= + constructor() { + BaseCommand._counter++; + this._uniqueIndex = BaseCommand._counter; + } + public get name(): string { return this.tooltip.fallback ?? this.tooltip.key; } @@ -35,7 +47,7 @@ export abstract class BaseCommand { return undefined; } - public get tooltip(): Tooltip { + public get tooltip(): TranslateKey { return { key: '' }; } @@ -51,11 +63,18 @@ export abstract class BaseCommand { return this.isEnabled; } - public get isCheckable(): boolean { + public get isChecked(): boolean { return false; } - public get isChecked(): boolean { + /** + * Gets a value indicating whether the command has data, for instance a reference + * to a specific domain object. Then the command cannot be reused or shared in the user interface. + * These command will not be added to the commandsController for updating, so update will + * not be done automatically. Typically used when the command is created for a specific domain object + * in the DomainObjectPanel. + */ + public get hasData(): boolean { return false; } diff --git a/react-components/src/architecture/base/commands/BaseTool.ts b/react-components/src/architecture/base/commands/BaseTool.ts index 6e5aced0ce0..3c29a71d546 100644 --- a/react-components/src/architecture/base/commands/BaseTool.ts +++ b/react-components/src/architecture/base/commands/BaseTool.ts @@ -30,10 +30,6 @@ export abstract class BaseTool extends RenderTargetCommand { // OVERRIDES // ================================================= - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { return this.renderTarget.commandsController.activeTool === this; } diff --git a/react-components/src/architecture/base/commands/NavigationTool.ts b/react-components/src/architecture/base/commands/NavigationTool.ts index fcd2b0266c3..d3bc621240c 100644 --- a/react-components/src/architecture/base/commands/NavigationTool.ts +++ b/react-components/src/architecture/base/commands/NavigationTool.ts @@ -4,8 +4,8 @@ */ import { BaseTool } from './BaseTool'; -import { type Tooltip } from './BaseCommand'; import { type IFlexibleCameraManager } from '@cognite/reveal'; +import { type TranslateKey } from '../utilities/TranslateKey'; /** * Represents a tool navigation tool used for camera manipulation. @@ -32,7 +32,7 @@ export class NavigationTool extends BaseTool { return 'Grab'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'NAVIGATION', fallback: 'Navigation' }; } diff --git a/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts b/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts new file mode 100644 index 00000000000..430f25e6274 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts @@ -0,0 +1,57 @@ +/*! + * Copyright 2024 Cognite AS + * BaseTool: Base class for the tool are used to interact with the render target. + */ + +import { BaseCommand } from '../commands/BaseCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; + +type GetStringDelegate = () => string; + +export class CopyToClipboardCommand extends BaseCommand { + private readonly _getString: GetStringDelegate; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(getString: GetStringDelegate) { + super(); + this._getString = getString; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'COPY_TO_CLIPBOARD', fallback: 'Copy to clipboard' }; + } + + public override get icon(): string { + return 'Copy'; + } + + public override get isEnabled(): boolean { + return this._getString !== undefined; + } + + public override get hasData(): boolean { + return true; + } + + protected override invokeCore(): boolean { + if (this._getString === undefined) { + return false; + } + navigator.clipboard + .writeText(this._getString()) + .then((_result) => { + return true; + }) + .catch((error) => { + console.error(error); + }); + return true; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts b/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts new file mode 100644 index 00000000000..76293d36f48 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts @@ -0,0 +1,42 @@ +/*! + * Copyright 2024 Cognite AS + * BaseTool: Base class for the tool are used to interact with the render target. + */ + +import { BaseCommand } from '../commands/BaseCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; +import { type DomainObject } from '../domainObjects/DomainObject'; + +export class DeleteDomainObjectCommand extends BaseCommand { + private readonly _domainObject: DomainObject | undefined = undefined; + public constructor(domainObject: DomainObject) { + super(); + this._domainObject = domainObject; + } + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'DELETE', fallback: 'Delete' }; + } + + public override get icon(): string { + return 'Delete'; + } + + public override get isEnabled(): boolean { + return this._domainObject !== undefined && this._domainObject.canBeRemoved; + } + + public override get hasData(): boolean { + return true; + } + + protected override invokeCore(): boolean { + if (this._domainObject === undefined) { + return false; + } + return this._domainObject.removeInteractive(); + } +} diff --git a/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts b/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts index a3b1cfa64cb..7301eb47b85 100644 --- a/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts @@ -3,14 +3,14 @@ */ import { RenderTargetCommand } from '../commands/RenderTargetCommand'; -import { type Tooltip } from '../commands/BaseCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; export class FitViewCommand extends RenderTargetCommand { public override get icon(): string { return 'ExpandAlternative'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'FIT_VIEW_TOOLTIP', fallback: 'Fit view' }; } diff --git a/react-components/src/architecture/base/concreteCommands/SetFlexibleControlsTypeCommand.ts b/react-components/src/architecture/base/concreteCommands/SetFlexibleControlsTypeCommand.ts index 7c4056a1735..2dfbfa7b45f 100644 --- a/react-components/src/architecture/base/concreteCommands/SetFlexibleControlsTypeCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/SetFlexibleControlsTypeCommand.ts @@ -5,7 +5,8 @@ import { RenderTargetCommand } from '../commands/RenderTargetCommand'; import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { FlexibleControlsType } from '@cognite/reveal'; -import { type BaseCommand, type Tooltip } from '../commands/BaseCommand'; +import { type BaseCommand } from '../commands/BaseCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; export class SetFlexibleControlsTypeCommand extends RenderTargetCommand { private readonly _controlsType: FlexibleControlsType; @@ -49,7 +50,7 @@ export class SetFlexibleControlsTypeCommand extends RenderTargetCommand { } } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { switch (this._controlsType) { case FlexibleControlsType.FirstPerson: return { key: 'CONTROLS_TYPE_FIRST_PERSON', fallback: 'Fly' }; @@ -62,10 +63,6 @@ export class SetFlexibleControlsTypeCommand extends RenderTargetCommand { } } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { const { renderTarget } = this; const { flexibleCameraManager } = renderTarget; diff --git a/react-components/src/architecture/base/concreteCommands/ToogleMetricUnitsCommand.ts b/react-components/src/architecture/base/concreteCommands/ToogleMetricUnitsCommand.ts new file mode 100644 index 00000000000..531a2035bc9 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/ToogleMetricUnitsCommand.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2024 Cognite AS + * BaseTool: Base class for the tool are used to interact with the render target. + */ + +import { RenderTargetCommand } from '../commands/RenderTargetCommand'; +import { Changes } from '../domainObjectsHelpers/Changes'; +import { type TranslateKey } from '../utilities/TranslateKey'; + +export class ToogleMetricUnitsCommand extends RenderTargetCommand { + // ================================================== + // OVERRIDES of BaseCommand + // ================================================== + + public override get icon(): string { + return 'RulerAlternative'; + } + + public override get tooltip(): TranslateKey { + return { key: 'TOGGLE_METRIC_UNITS', fallback: 'm/ft' }; // Note: m/ft do not need to be translated! + } + + public override get isChecked(): boolean { + const { renderTarget } = this; + return renderTarget.rootDomainObject.unitSystem.isMetric; + } + + protected override invokeCore(): boolean { + const { renderTarget } = this; + const unitSystem = renderTarget.rootDomainObject.unitSystem; + unitSystem.isMetric = !unitSystem.isMetric; + renderTarget.rootDomainObject.notifyRecursive(Changes.unit); + return true; + } +} diff --git a/react-components/src/architecture/base/domainObjects/DomainObject.ts b/react-components/src/architecture/base/domainObjects/DomainObject.ts index 91c47deba95..484cb58c1f2 100644 --- a/react-components/src/architecture/base/domainObjects/DomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/DomainObject.ts @@ -256,10 +256,13 @@ export abstract class DomainObject { ) ) { if (this.root instanceof RootDomainObject) { + // Update all the command buttons (in the toolbars). + // This goes fast and will not slow the system down. CommandsUpdater.update(this.root.renderTarget); } } if (this.hasPanelInfo) { + // Update the DomainObjectPanel if any DomainObjectPanelUpdater.notify(this, change); } } @@ -461,9 +464,16 @@ export abstract class DomainObject { } public get root(): DomainObject { + // Returns the root of the hierarcy, regardless what it is return this.parent === undefined ? this : this.parent.root; } + public get rootDomainObject(): RootDomainObject | undefined { + // Returns a RootDomainObject only if the root is a RootDomainObject, otherwise undefined + const root = this.root; + return root instanceof RootDomainObject ? root : undefined; + } + public get hasParent(): boolean { return this._parent !== undefined; } @@ -692,9 +702,9 @@ export abstract class DomainObject { return true; } - public removeInteractive(checkCanBeDeleted = true): void { + public removeInteractive(checkCanBeDeleted = true): boolean { if (checkCanBeDeleted && !this.canBeRemoved) { - return; + return false; } for (const child of this.children) { child.removeInteractive(false); // If parent can be removed, so the children also @@ -703,6 +713,7 @@ export abstract class DomainObject { this.notify(Changes.deleted); this.remove(); parent?.notify(Changes.childDeleted); + return true; } public sortChildrenByName(): void { diff --git a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts index 14972f804da..70e336bd2af 100644 --- a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts @@ -3,10 +3,20 @@ */ import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; +import { UnitSystem } from '../renderTarget/UnitSystem'; import { DomainObject } from './DomainObject'; export class RootDomainObject extends DomainObject { + // ================================================== + // INSTANCE FIELDS + // ================================================== + private readonly _renderTarget: RevealRenderTarget; + public readonly unitSystem = new UnitSystem(); + + // ================================================== + // INSTANCE PROPERTIES + // ================================================== public get renderTarget(): RevealRenderTarget { return this._renderTarget; diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts index 883275159eb..bf42d4d713b 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts @@ -16,6 +16,7 @@ export class Changes { public static readonly icon: symbol = Symbol('icon'); public static readonly colorMap: symbol = Symbol('colorMap'); public static readonly renderStyle: symbol = Symbol('renderStyle'); + public static readonly unit: symbol = Symbol('unit'); // Something in the geometry changed public static readonly geometry: symbol = Symbol('geometry'); diff --git a/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts b/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts index c1897bcc4a2..11e3c7b4dfe 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts @@ -2,21 +2,15 @@ * Copyright 2024 Cognite AS */ -export enum NumberType { - Unitless, - Length, - Area, - Volume, - Degrees -} +import { type TranslateKey } from '../utilities/TranslateKey'; +import { Quantity } from './Quantity'; type PanelItemProps = { - key?: string; + key: string; fallback?: string; icon?: string; value?: number; - numberType?: NumberType; - decimals?: number; + quantity?: Quantity; }; export class PanelInfo { @@ -37,7 +31,7 @@ export class PanelItem { public key?: string; public fallback?: string; - constructor(props: PanelItemProps) { + constructor(props: TranslateKey) { this.key = props.key; this.fallback = props.fallback; } @@ -46,37 +40,12 @@ export class PanelItem { export class NumberPanelItem extends PanelItem { public icon: string | undefined = undefined; public value: number; - public numberType: NumberType; - public decimals: number; + public quantity: Quantity; constructor(props: PanelItemProps) { super(props); this.icon = props.icon; this.value = props.value ?? 0; - this.numberType = props.numberType ?? NumberType.Unitless; - this.decimals = props.decimals ?? 2; - } - - public get valueAsString(): string { - return this.value.toFixed(this.decimals); - } - - public get unit(): string { - return getUnit(this.numberType); - } -} - -function getUnit(numberType: NumberType): string { - switch (numberType) { - case NumberType.Unitless: - return ''; - case NumberType.Length: - return 'm'; - case NumberType.Area: - return 'm²'; - case NumberType.Volume: - return 'm³'; - case NumberType.Degrees: - return '°'; + this.quantity = props.quantity ?? Quantity.Unitless; } } diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts b/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts new file mode 100644 index 00000000000..f0a90367961 --- /dev/null +++ b/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts @@ -0,0 +1,11 @@ +/*! + * Copyright 2024 Cognite AS + */ + +export enum Quantity { + Unitless, + Length, + Area, + Volume, + Degrees +} diff --git a/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts b/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts index 6463ff1c495..46138a22ec7 100644 --- a/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts +++ b/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts @@ -43,7 +43,7 @@ export class DomainObjectPanelUpdater { if (change.isChanged(Changes.deleted)) { this.hide(); } - if (change.isChanged(Changes.selected, Changes.geometry, Changes.naming)) { + if (change.isChanged(Changes.selected, Changes.geometry, Changes.naming, Changes.unit)) { this.update(domainObject); } } else { diff --git a/react-components/src/architecture/base/renderTarget/UnitSystem.ts b/react-components/src/architecture/base/renderTarget/UnitSystem.ts new file mode 100644 index 00000000000..b1be8ad2ca2 --- /dev/null +++ b/react-components/src/architecture/base/renderTarget/UnitSystem.ts @@ -0,0 +1,80 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Quantity } from '../domainObjectsHelpers/Quantity'; + +const METER_TO_FT = 1 / 0.3048; + +/** + * Represents a unit system that handles conversions and formatting of values based on the system's metric setting. + * This is a very basic unit system, but could be extended in the future to cover all use cases. + * Todo: Should use CogniteClient.units to get the unit and conversion factors + */ +export class UnitSystem { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + public isMetric: boolean = true; + + // ================================================== + // INSTANCE METHODS: Convert number + // ================================================== + + public convertToUnit(value: number, quantity: Quantity): number { + if (!this.isMetric) { + switch (quantity) { + case Quantity.Length: + return value * METER_TO_FT; + case Quantity.Area: + return value * METER_TO_FT * METER_TO_FT; + case Quantity.Volume: + return value * METER_TO_FT * METER_TO_FT * METER_TO_FT; + } + } + return value; + } + + // ================================================== + // INSTANCE METHODS: Convert number to string + // ================================================== + + public toString(value: number, quantity: Quantity): string { + const fractionDigits = this.getFractionDigits(quantity); + const convertedValue = this.convertToUnit(value, quantity); + return convertedValue.toFixed(fractionDigits); + } + + public toStringWithUnit(value: number, quantity: Quantity): string { + return `${this.toString(value, quantity)} ${this.getUnit(quantity)}`; + } + + // ================================================== + // INSTANCE METHODS: Getters + // ================================================== + + public getUnit(quantity: Quantity): string { + switch (quantity) { + case Quantity.Unitless: + return ''; + case Quantity.Length: + return this.isMetric ? 'm' : 'ft'; + case Quantity.Area: + return this.isMetric ? 'm²' : 'ft²'; + case Quantity.Volume: + return this.isMetric ? 'm³' : 'ft³'; + case Quantity.Degrees: + return '°'; + } + } + + private getFractionDigits(quantity: Quantity): number { + switch (quantity) { + case Quantity.Degrees: + return 1; + default: + return 2; + } + } +} diff --git a/react-components/src/architecture/base/utilities/TranslateKey.ts b/react-components/src/architecture/base/utilities/TranslateKey.ts new file mode 100644 index 00000000000..54e05e2dc3e --- /dev/null +++ b/react-components/src/architecture/base/utilities/TranslateKey.ts @@ -0,0 +1,9 @@ +/*! + * Copyright 2024 Cognite AS + */ +export type TranslateDelegate = (key: string, fallback?: string) => string; + +export type TranslateKey = { + key: string; + fallback?: string; +}; diff --git a/react-components/src/architecture/base/utilities/geometry/ClosestGeometryFinder.ts b/react-components/src/architecture/base/utilities/geometry/ClosestGeometryFinder.ts new file mode 100644 index 00000000000..153a4280c96 --- /dev/null +++ b/react-components/src/architecture/base/utilities/geometry/ClosestGeometryFinder.ts @@ -0,0 +1,62 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type Vector3 } from 'three'; + +export class ClosestGeometryFinder { + private closestGeometry: T | undefined = undefined; + private readonly origin: Vector3; + private minDistanceSquared = Number.MAX_VALUE; + + public constructor(origin: Vector3) { + this.origin = origin; + this.clear(); + } + + public set minDistance(value: number) { + this.minDistanceSquared = value * value; + } + + public get minDistance(): number { + return Math.sqrt(this.minDistanceSquared); + } + + public getClosestGeometry(): T | undefined { + return this.closestGeometry; + } + + public setClosestGeometry(geometry: T): void { + this.closestGeometry = geometry; + } + + public isClosest(point: Vector3): boolean { + const distanceSquared = point.distanceToSquared(this.origin); + if (distanceSquared > this.minDistanceSquared) { + return false; + } + this.minDistanceSquared = distanceSquared; + return true; + } + + public clear(): void { + this.closestGeometry = undefined; + this.minDistanceSquared = Number.MAX_VALUE; + } + + public add(point: Vector3, geometry: T): boolean { + if (!this.isClosest(point)) { + return false; + } + this.setClosestGeometry(geometry); + return true; + } + + public addLazy(point: Vector3, geometryCreator: () => T): boolean { + if (!this.isClosest(point)) { + return false; + } + this.setClosestGeometry(geometryCreator()); + return true; + } +} diff --git a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts index 80882314e15..4e15bc94b67 100644 --- a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts +++ b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts @@ -4,7 +4,7 @@ */ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { AxisDomainObject } from './AxisDomainObject'; export class SetAxisVisibleCommand extends RenderTargetCommand { @@ -12,7 +12,7 @@ export class SetAxisVisibleCommand extends RenderTargetCommand { // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'SHOW_OR_HIDE_AXIS', fallback: 'Show or hide axis' }; } @@ -24,10 +24,6 @@ export class SetAxisVisibleCommand extends RenderTargetCommand { return true; } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { const { renderTarget, rootDomainObject } = this; diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts index 155991050d6..2817436b7a9 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts @@ -15,13 +15,13 @@ import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; import { type BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; import { MeasureBoxDragger } from './MeasureBoxDragger'; import { MeasureDomainObject } from './MeasureDomainObject'; -import { NumberType, PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; import { radToDeg } from 'three/src/math/MathUtils.js'; import { type CreateDraggerProps } from '../../base/domainObjects/VisualDomainObject'; import { Range3 } from '../../base/utilities/geometry/Range3'; -import { RootDomainObject } from '../../base/domainObjects/RootDomainObject'; import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; export const MIN_BOX_SIZE = 0.01; @@ -107,31 +107,36 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { break; } if (isFinished || isValidSize(this.size.x)) { - add('MEASUREMENTS_LENGTH', 'Length', this.size.x, NumberType.Length); + add('MEASUREMENTS_LENGTH', 'Length', this.size.x, Quantity.Length); } if (measureType !== MeasureType.VerticalArea && (isFinished || isValidSize(this.size.y))) { - add('MEASUREMENTS_DEPTH', 'Depth', this.size.y, NumberType.Length); + add('MEASUREMENTS_DEPTH', 'Depth', this.size.y, Quantity.Length); } if (measureType !== MeasureType.HorizontalArea && (isFinished || isValidSize(this.size.z))) { - add('MEASUREMENTS_HEIGHT', 'Height', this.size.z, NumberType.Length); + add('MEASUREMENTS_HEIGHT', 'Height', this.size.z, Quantity.Length); } if (measureType !== MeasureType.Volume && (isFinished || this.hasArea)) { - add('MEASUREMENTS_AREA', 'Area', this.area, NumberType.Area); + add('MEASUREMENTS_AREA', 'Area', this.area, Quantity.Area); } if (measureType === MeasureType.Volume && (isFinished || this.hasHorizontalArea)) { - add('MEASUREMENTS_HORIZONTAL_AREA', 'Horizontal area', this.horizontalArea, NumberType.Area); + add('MEASUREMENTS_HORIZONTAL_AREA', 'Horizontal area', this.horizontalArea, Quantity.Area); } if (measureType === MeasureType.Volume && (isFinished || this.hasVolume)) { - add('MEASUREMENTS_VOLUME', 'Volume', this.volume, NumberType.Volume); + add('MEASUREMENTS_VOLUME', 'Volume', this.volume, Quantity.Volume); } // I forgot to add text for rotation angle before the deadline, so I used a icon instead. if (this.zRotation !== 0 && isFinished) { - info.add({ icon: 'Angle', value: radToDeg(this.zRotation), numberType: NumberType.Degrees }); + info.add({ + key: '', + icon: 'Angle', + value: radToDeg(this.zRotation), + quantity: Quantity.Degrees + }); } return info; - function add(key: string, fallback: string, value: number, numberType: NumberType): void { - info.add({ key, fallback, value, numberType }); + function add(key: string, fallback: string, value: number, quantity: Quantity): void { + info.add({ key, fallback, value, quantity }); } } @@ -249,16 +254,16 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } public get isUseAsCropBox(): boolean { - const root = this.root as RootDomainObject; - if (!(root instanceof RootDomainObject)) { + const root = this.rootDomainObject; + if (root === undefined) { return false; } return root.renderTarget.isGlobalCropBox(this); } public setUseAsCropBox(use: boolean): void { - const root = this.root as RootDomainObject; - if (!(root instanceof RootDomainObject)) { + const root = this.rootDomainObject; + if (root === undefined) { return; } if (!use) { diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts index 8fde9669ae9..5f693aecc3e 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts @@ -46,6 +46,7 @@ import { radToDeg } from 'three/src/math/MathUtils.js'; import { Range1 } from '../../base/utilities/geometry/Range1'; import { MeasureType } from './MeasureType'; import { type MeasureRenderStyle } from './MeasureRenderStyle'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; const RELATIVE_RESIZE_RADIUS = 0.15; const RELATIVE_ROTATION_RADIUS = new Range1(0.6, 0.75); @@ -80,7 +81,15 @@ export class MeasureBoxView extends GroupThreeView { public override update(change: DomainObjectChange): void { super.update(change); - if (change.isChanged(Changes.selected, Changes.focus, Changes.renderStyle, Changes.color)) { + if ( + change.isChanged( + Changes.selected, + Changes.focus, + Changes.renderStyle, + Changes.color, + Changes.unit + ) + ) { this.removeChildren(); this.invalidateBoundingBox(); this.invalidateRenderTarget(); @@ -121,7 +130,7 @@ export class MeasureBoxView extends GroupThreeView { intersectInput: CustomObjectIntersectInput, closestDistance: number | undefined ): undefined | CustomObjectIntersection { - const { domainObject } = this; + const { domainObject, style } = this; if (domainObject.focusType === FocusType.Pending) { return undefined; // Should never be picked } @@ -135,7 +144,7 @@ export class MeasureBoxView extends GroupThreeView { return undefined; } const distanceToCamera = point.distanceTo(ray.origin); - if (closestDistance !== undefined && closestDistance < distanceToCamera) { + if (style.depthTest && closestDistance !== undefined && closestDistance < distanceToCamera) { return undefined; } if (!intersectInput.isVisible(point)) { @@ -230,12 +239,16 @@ export class MeasureBoxView extends GroupThreeView { return undefined; } const { domainObject } = this; + const { rootDomainObject } = domainObject; + if (rootDomainObject === undefined) { + return undefined; + } const degrees = radToDeg(domainObject.zRotation); - const text = degrees.toFixed(1); - if (text === '0.0') { + if (degrees === 0) { return undefined; // Not show when about 0 } - const sprite = createSprite(text + '°', this.style, spriteHeight); + const text = rootDomainObject.unitSystem.toStringWithUnit(degrees, Quantity.Degrees); + const sprite = createSprite(text, this.style, spriteHeight); if (sprite === undefined) { return undefined; } @@ -330,6 +343,10 @@ export class MeasureBoxView extends GroupThreeView { private addLabels(matrix: Matrix4): void { const { domainObject, style } = this; + const { rootDomainObject } = domainObject; + if (rootDomainObject === undefined) { + return undefined; + } const spriteHeight = this.getTextHeight(style.relativeTextSize); clear(this._sprites); for (let index = 0; index < 3; index++) { @@ -338,7 +355,8 @@ export class MeasureBoxView extends GroupThreeView { this._sprites.push(undefined); continue; } - const sprite = createSprite(size.toFixed(2), style, spriteHeight); + const text = rootDomainObject.unitSystem.toStringWithUnit(size, Quantity.Length); + const sprite = createSprite(text, style, spriteHeight); if (sprite === undefined) { this._sprites.push(undefined); continue; diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts index 5fc700de2a8..746e6ab3fd8 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts @@ -14,9 +14,10 @@ import { horizontalDistanceTo, verticalDistanceTo } from '../../base/utilities/extensions/vectorExtensions'; -import { NumberType, PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; export class MeasureLineDomainObject extends MeasureDomainObject { // ================================================== @@ -76,7 +77,7 @@ export class MeasureLineDomainObject extends MeasureDomainObject { 'MEASUREMENTS_HORIZONTAL_AREA', 'Horizontal area', this.getHorizontalArea(), - NumberType.Area + Quantity.Area ); } break; @@ -86,13 +87,8 @@ export class MeasureLineDomainObject extends MeasureDomainObject { } return info; - function add( - key: string, - fallback: string, - value: number, - numberType = NumberType.Length - ): void { - info.add({ key, fallback, value, numberType }); + function add(key: string, fallback: string, value: number, quantity = Quantity.Length): void { + info.add({ key, fallback, value, quantity }); } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts index 2b536e4ceff..53b24136ff9 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts @@ -35,6 +35,10 @@ import { createSpriteWithText } from '../../base/utilities/sprites/createSprite' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; import { MeasureRenderStyle } from './MeasureRenderStyle'; +import { DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { ClosestGeometryFinder } from '../../base/utilities/geometry/ClosestGeometryFinder'; +import { square } from '../../base/utilities/extensions/mathExtensions'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; const CYLINDER_DEFAULT_AXIS = new Vector3(0, 1, 0); const RENDER_ORDER = 100; @@ -58,7 +62,15 @@ export class MeasureLineView extends GroupThreeView { public override update(change: DomainObjectChange): void { super.update(change); - if (change.isChanged(Changes.selected, Changes.focus, Changes.renderStyle, Changes.color)) { + if ( + change.isChanged( + Changes.selected, + Changes.focus, + Changes.renderStyle, + Changes.color, + Changes.unit + ) + ) { this.removeChildren(); this.invalidateBoundingBox(); this.invalidateRenderTarget(); @@ -82,7 +94,59 @@ export class MeasureLineView extends GroupThreeView { if (this.domainObject.focusType === FocusType.Pending) { return undefined; // Should never be picked } - return super.intersectIfCloser(intersectInput, closestDistance); + // Implement the intersection logic here, because of bug in tree.js + const { domainObject, style } = this; + const radius = getRadius(domainObject, style); + if (radius <= 0) { + return; + } + const { points } = domainObject; + const { length } = points; + if (length < 2) { + return undefined; + } + // Just allocate all needed objects once + const prevPoint = new Vector3(); + const thisPoint = new Vector3(); + const intersection = new Vector3(); + + const radiusSquared = square(1.5 * radius); // Add 50% more to make it easier to pick + const ray = intersectInput.raycaster.ray; + const closestFinder = new ClosestGeometryFinder(ray.origin); + + // TODO: The line below will case a tiny bug. The best is that the main intersection algorithm + // in the vieweer intersects the objects with depthTest == false first, before any other object and + // returns out if any of those objects are intersected. Same for Boxes. Now the intersection is somewhat arbitrarly + // if style.depthTest == false. Then the last one added to the viewer will be picked first, + // regardless of the distance to the mouse. + if (style.depthTest && closestDistance !== undefined) { + closestFinder.minDistance = closestDistance; + } + const loopLength = domainObject.measureType === MeasureType.Polygon ? length + 1 : length; + for (let i = 0; i < loopLength; i++) { + thisPoint.copy(points[i % length]); + thisPoint.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + + if (i === 0) { + prevPoint.copy(thisPoint); + continue; + } + const distanceSq = ray.distanceSqToSegment(prevPoint, thisPoint, undefined, intersection); + if (distanceSq > radiusSquared || !closestFinder.isClosest(intersection)) { + prevPoint.copy(thisPoint); + continue; + } + const objectIntersection: DomainObjectIntersection = { + type: 'customObject', + point: intersection, + distanceToCamera: closestFinder.minDistance, + customObject: this, + domainObject + }; + closestFinder.setClosestGeometry(objectIntersection); + prevPoint.copy(thisPoint); + } + return closestFinder.getClosestGeometry(); } // ================================================== @@ -91,7 +155,7 @@ export class MeasureLineView extends GroupThreeView { private createPipe(): Mesh | undefined { const { domainObject, style } = this; - const radius = domainObject.isSelected ? style.selectedPipeRadius : style.pipeRadius; + const radius = getRadius(domainObject, style); if (radius <= 0) { return; } @@ -188,7 +252,10 @@ export class MeasureLineView extends GroupThreeView { private addLabels(): void { const { domainObject, style } = this; - const { points } = domainObject; + const { points, rootDomainObject } = domainObject; + if (rootDomainObject === undefined) { + return; + } const { length } = points; if (length < 2) { return; @@ -206,7 +273,9 @@ export class MeasureLineView extends GroupThreeView { center.copy(point1).add(point2).divideScalar(2); center.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); - const sprite = createSprite(distance.toFixed(2), style, spriteHeight); + + const text = rootDomainObject.unitSystem.toStringWithUnit(distance, Quantity.Length); + const sprite = createSprite(text, style, spriteHeight); if (sprite === undefined) { continue; } @@ -284,3 +353,7 @@ function adjustLabel( point.y += (1.1 * spriteHeight) / 2 + style.pipeRadius; } } + +function getRadius(domainObject: MeasureLineDomainObject, style: MeasureLineRenderStyle): number { + return domainObject.isSelected ? style.selectedPipeRadius : style.pipeRadius; +} diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts index c51b84d27b5..ef8b55de138 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts @@ -2,7 +2,7 @@ * Copyright 2024 Cognite AS */ -import { type Tooltip } from '../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export enum MeasureType { None, @@ -52,7 +52,7 @@ export function getNameByMeasureType(measureType: MeasureType): string { } } -export function getTooltipByMeasureType(measureType: MeasureType): Tooltip { +export function getTooltipByMeasureType(measureType: MeasureType): TranslateKey { switch (measureType) { case MeasureType.Line: return { diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts index a316bc01e8d..23e0a41c104 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts @@ -4,7 +4,7 @@ import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; import { type AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; -import { type BaseCommand, type Tooltip } from '../../base/commands/BaseCommand'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; import { isDomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; @@ -23,6 +23,8 @@ import { SetMeasurmentTypeCommand } from './SetMeasurmentTypeCommand'; import { PopupStyle } from '../../base/domainObjectsHelpers/PopupStyle'; import { type RootDomainObject } from '../../base/domainObjects/RootDomainObject'; import { CommandsUpdater } from '../../base/reactUpdaters/CommandsUpdater'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { ToogleMetricUnitsCommand } from '../../base/concreteCommands/ToogleMetricUnitsCommand'; export class MeasurementTool extends BaseEditTool { // ================================================== @@ -40,7 +42,7 @@ export class MeasurementTool extends BaseEditTool { return 'Ruler'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'MEASUREMENTS', fallback: 'Measurements' }; } @@ -53,6 +55,7 @@ export class MeasurementTool extends BaseEditTool { new SetMeasurmentTypeCommand(MeasureType.VerticalArea), new SetMeasurmentTypeCommand(MeasureType.Volume), undefined, // Separator + new ToogleMetricUnitsCommand(), new ShowMeasurmentsOnTopCommand() ]; } @@ -181,7 +184,6 @@ export class MeasurementTool extends BaseEditTool { const intersection = await this.getIntersection(event); if (intersection === undefined) { // Click in the "air" - await super.onClick(event); return; } const measurment = this.getMeasurement(intersection); @@ -194,7 +196,6 @@ export class MeasurementTool extends BaseEditTool { if (creator === undefined) { const creator = (this._creator = createCreator(this.measureType)); if (creator === undefined) { - await super.onClick(event); return; } if (creator.addPoint(ray, intersection)) { diff --git a/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts index 64007891a36..f2de2d5eb85 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts @@ -4,7 +4,7 @@ */ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; import { MeasureType } from './MeasureType'; @@ -15,7 +15,7 @@ export class SetCropBoxCommand extends RenderTargetCommand { // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'CROP_BOX', fallback: 'Set as crop box' }; } @@ -30,10 +30,6 @@ export class SetCropBoxCommand extends RenderTargetCommand { return this.getMeasureBoxDomainObject() !== undefined; } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { return this.renderTarget.isGlobalCropBoxActive; } diff --git a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts index 8a1cb259ad5..5a91ddb3009 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts @@ -4,9 +4,10 @@ */ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { type BaseCommand, type Tooltip } from '../../base/commands/BaseCommand'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; import { MeasureType, getIconByMeasureType, getTooltipByMeasureType } from './MeasureType'; import { MeasurementTool } from './MeasurementTool'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class SetMeasurmentTypeCommand extends RenderTargetCommand { private readonly _measureType: MeasureType; @@ -28,7 +29,7 @@ export class SetMeasurmentTypeCommand extends RenderTargetCommand { return getIconByMeasureType(this._measureType); } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return getTooltipByMeasureType(this._measureType); } @@ -36,10 +37,6 @@ export class SetMeasurmentTypeCommand extends RenderTargetCommand { return this.measurementTool !== undefined; } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { const { measurementTool } = this; if (measurementTool === undefined) { diff --git a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts index 9d66341e6c5..cba24af00d3 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts @@ -4,8 +4,8 @@ */ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../base/commands/BaseCommand'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { MeasureDomainObject } from './MeasureDomainObject'; export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { @@ -13,7 +13,7 @@ export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'MEASUREMENTS_SHOW_ON_TOP', fallback: 'Show all measurements on top' }; } @@ -26,10 +26,6 @@ export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { return domainObject !== undefined; } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { return !this.getDepthTest(); } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts index a2fb3b0f25a..e26e4244c43 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts @@ -6,7 +6,7 @@ import { ExampleRenderStyle } from './ExampleRenderStyle'; import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; import { type ThreeView } from '../../base/views/ThreeView'; import { ExampleView } from './ExampleView'; -import { NumberType, PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; import { type CreateDraggerProps, VisualDomainObject @@ -15,6 +15,7 @@ import { Vector3 } from 'three'; import { PopupStyle } from '../../base/domainObjectsHelpers/PopupStyle'; import { type BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; import { ExampleDragger } from './ExampleDragger'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; export class ExampleDomainObject extends VisualDomainObject { // ================================================== @@ -62,13 +63,13 @@ export class ExampleDomainObject extends VisualDomainObject { public override getPanelInfo(): PanelInfo | undefined { const info = new PanelInfo(); info.setHeader('NAME', this.name); - add('XCORDINATE', 'X coordinate', this.center.x, NumberType.Length); - add('YCORDINATE', 'Y coordinate', this.center.y, NumberType.Length); - add('ZCORDINATE', 'Z coordinate', this.center.z, NumberType.Length); + add('XCORDINATE', 'X coordinate', this.center.x, Quantity.Length); + add('YCORDINATE', 'Y coordinate', this.center.y, Quantity.Length); + add('ZCORDINATE', 'Z coordinate', this.center.z, Quantity.Length); return info; - function add(key: string, fallback: string, value: number, numberType: NumberType): void { - info.add({ key, fallback, value, numberType }); + function add(key: string, fallback: string, value: number, quantity: Quantity): void { + info.add({ key, fallback, value, quantity }); } } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts index b9f847338f4..b50190a282e 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts @@ -4,7 +4,7 @@ import { ExampleDomainObject } from './ExampleDomainObject'; import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; -import { type BaseCommand, type Tooltip } from '../../base/commands/BaseCommand'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { ResetAllExamplesCommand } from './commands/ResetAllExamplesCommand'; @@ -13,6 +13,8 @@ import { ShowAllExamplesCommand } from './commands/ShowAllExamplesCommand'; import { clamp } from 'lodash'; import { type DomainObject } from '../../base/domainObjects/DomainObject'; import { type HSL } from 'three'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; + export class ExampleTool extends BaseEditTool { // ================================================== // OVERRIDES of BaseCommand @@ -22,7 +24,7 @@ export class ExampleTool extends BaseEditTool { return 'Circle'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'EXAMPLE_EDIT', fallback: 'Create or edit a single point' }; } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts index e6729404595..d2ab8ffda47 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts @@ -4,7 +4,7 @@ */ import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; import { ExampleDomainObject } from '../ExampleDomainObject'; export class DeleteAllExamplesCommand extends RenderTargetCommand { @@ -12,7 +12,7 @@ export class DeleteAllExamplesCommand extends RenderTargetCommand { // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'EXAMPLES_DELETE', fallback: 'Remove all examples' }; } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts index 11606735822..9279f8ddf5f 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts @@ -4,16 +4,16 @@ */ import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../../base/commands/BaseCommand'; import { ExampleDomainObject } from '../ExampleDomainObject'; import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; export class ResetAllExamplesCommand extends RenderTargetCommand { // ================================================== // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'EXAMPLES_RESET', fallback: 'Reset all examples' }; } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts index ec288b355f3..9a16e75b228 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts @@ -4,7 +4,7 @@ */ import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; -import { type Tooltip } from '../../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; import { ExampleDomainObject } from '../ExampleDomainObject'; export class ShowAllExamplesCommand extends RenderTargetCommand { @@ -12,7 +12,7 @@ export class ShowAllExamplesCommand extends RenderTargetCommand { // OVERRIDES // ================================================== - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'EXAMPLES_SHOW', fallback: 'Show or hide all examples' }; } @@ -24,10 +24,6 @@ export class ShowAllExamplesCommand extends RenderTargetCommand { return this.getFirst() !== undefined; } - public override get isCheckable(): boolean { - return true; - } - public override get isChecked(): boolean { const domainObject = this.getFirst(); if (domainObject === undefined) { diff --git a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts index 0e164f4e97f..08869ee34e0 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts @@ -8,7 +8,7 @@ import { Vector3 } from 'three'; import { Range3 } from '../../base/utilities/geometry/Range3'; import { createFractalRegularGrid2 } from './geometry/createFractalRegularGrid2'; import { DEFAULT_TERRAIN_NAME, TerrainDomainObject } from './TerrainDomainObject'; -import { type Tooltip } from '../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class SetTerrainVisibleCommand extends RenderTargetCommand { // ================================================== @@ -19,7 +19,7 @@ export class SetTerrainVisibleCommand extends RenderTargetCommand { return 'EyeShow'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'UNKNOWN', fallback: 'Set terrain visible. Create it if not done' }; } diff --git a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts index 1358fb736ef..fe8ef24a2ef 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts @@ -9,7 +9,7 @@ import { Range3 } from '../../base/utilities/geometry/Range3'; import { createFractalRegularGrid2 } from './geometry/createFractalRegularGrid2'; import { DEFAULT_TERRAIN_NAME, TerrainDomainObject } from './TerrainDomainObject'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type Tooltip } from '../../base/commands/BaseCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class UpdateTerrainCommand extends RenderTargetCommand { // ================================================== @@ -20,7 +20,7 @@ export class UpdateTerrainCommand extends RenderTargetCommand { return 'Refresh'; } - public override get tooltip(): Tooltip { + public override get tooltip(): TranslateKey { return { key: 'UNKNOWN', fallback: 'Change the visible terrain' }; } diff --git a/react-components/src/components/Architecture/CommandButton.tsx b/react-components/src/components/Architecture/CommandButton.tsx index 22ad3fb44a4..921af49e30b 100644 --- a/react-components/src/components/Architecture/CommandButton.tsx +++ b/react-components/src/components/Architecture/CommandButton.tsx @@ -4,13 +4,29 @@ import { type ReactElement, useState, useEffect, useMemo } from 'react'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; -import { Button, Tooltip as CogsTooltip, type IconType } from '@cognite/cogs.js'; +import { Button, Tooltip as CogsTooltip, Divider, type IconType } from '@cognite/cogs.js'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget'; import { RenderTargetCommand } from '../../architecture/base/commands/RenderTargetCommand'; -export const CreateButton = (command: BaseCommand, isHorizontal = false): ReactElement => { +export const CommandButtons = ({ + commands, + isHorizontal = false +}: { + commands: Array; + isHorizontal: boolean; +}): ReactElement => { + return ( + <> + {commands.map( + (command, index): ReactElement => addCommandButton(command, isHorizontal, index) + )} + + ); +}; + +export const CreateCommandButton = (command: BaseCommand, isHorizontal = false): ReactElement => { return ; }; @@ -28,6 +44,7 @@ export const CommandButton = ({ const [isChecked, setChecked] = useState(false); const [isEnabled, setEnabled] = useState(true); const [isVisible, setVisible] = useState(true); + const [uniqueIndex, setUniqueIndex] = useState(0); const [icon, setIcon] = useState('Copy'); useEffect(() => { @@ -35,6 +52,7 @@ export const CommandButton = ({ setChecked(command.isChecked); setEnabled(command.isEnabled); setVisible(command.isVisible); + setUniqueIndex(command._uniqueIndex); setIcon(command.icon as IconType); } update(newCommand); @@ -54,6 +72,7 @@ export const CommandButton = ({ - - - )} - - - - - + - {info.items.map((item, _i) => addTextWithNumber(item))} + {info.items.map((item, _i) => addTextWithNumber(item, unitSystem))}
); - function addTextWithNumber(item: NumberPanelItem): ReactElement { + function addTextWithNumber(item: NumberPanelItem, unitSystem: UnitSystem): ReactElement { const icon = item.icon as IconType; - const { key, fallback, unit } = item; + const { key, fallback, quantity, value } = item; return ( {key !== undefined && {t(key, fallback)}} - {icon !== undefined && ( - - - - )} + {icon !== undefined && } <> - {item.valueAsString} + {unitSystem.toString(value, quantity)} - {unit} + {unitSystem.getUnit(quantity)} ); } +}; - async function copyTextToClipboard(info: PanelInfo): Promise { - let text = ''; - { - const { header } = info; - if (header !== undefined) { - const { key, fallback } = header; - if (key !== undefined) { - text += `${t(key, fallback)}\n`; - } - } - } - for (const item of info.items) { - const { key, fallback, unit } = item; +function toString(info: PanelInfo, translate: TranslateDelegate, unitSystem: UnitSystem): string { + let text = ''; + { + const { header } = info; + if (header !== undefined) { + const { key, fallback } = header; if (key !== undefined) { - text += `${t(key, fallback)}: `; + text += `${translate(key, fallback)}\n`; } - text += `${item.valueAsString} ${unit}\n`; } - await navigator.clipboard.writeText(text); } -}; + for (const item of info.items) { + const { key, fallback, quantity, value } = item; + if (key !== undefined) { + text += `${translate(key, fallback)}: `; + } + text += `${unitSystem.toStringWithUnit(value, quantity)}\n`; + } + return text; +} const NumberTh = styled.th` text-align: right; @@ -155,7 +143,7 @@ const PaddedTh = styled.th` size: small; `; -const Container = styled.div` +const Container = withSuppressRevealEvents(styled.div` zindex: 1000px; position: absolute; display: block; @@ -164,4 +152,4 @@ const Container = styled.div` overflow: hidden; background-color: white; box-shadow: 0px 1px 8px #4f52681a; -`; +`); diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 2b321ea34ef..ff1aa096803 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -4,7 +4,7 @@ import { type ReactElement } from 'react'; import { NavigationTool } from '../../architecture/base/commands/NavigationTool'; -import { CreateButton } from './CommandButton'; +import { CreateCommandButton } from './CommandButton'; import { MeasurementTool } from '../../architecture/concrete/boxDomainObject/MeasurementTool'; import { FitViewCommand } from '../../architecture/base/concreteCommands/FitViewCommand'; import { FlexibleControlsType } from '@cognite/reveal'; @@ -12,14 +12,14 @@ import { SetFlexibleControlsTypeCommand } from '../../architecture/base/concrete import { SetAxisVisibleCommand } from '../../architecture/concrete/axis/SetAxisVisibleCommand'; export class RevealButtons { - static FitView = (): ReactElement => CreateButton(new FitViewCommand()); - static NavigationTool = (): ReactElement => CreateButton(new NavigationTool()); - static SetAxisVisible = (): ReactElement => CreateButton(new SetAxisVisibleCommand()); - static Measurement = (): ReactElement => CreateButton(new MeasurementTool()); + static FitView = (): ReactElement => CreateCommandButton(new FitViewCommand()); + static NavigationTool = (): ReactElement => CreateCommandButton(new NavigationTool()); + static SetAxisVisible = (): ReactElement => CreateCommandButton(new SetAxisVisibleCommand()); + static Measurement = (): ReactElement => CreateCommandButton(new MeasurementTool()); static SetFlexibleControlsTypeOrbit = (): ReactElement => - CreateButton(new SetFlexibleControlsTypeCommand(FlexibleControlsType.Orbit)); + CreateCommandButton(new SetFlexibleControlsTypeCommand(FlexibleControlsType.Orbit)); static SetFlexibleControlsTypeFirstPerson = (): ReactElement => - CreateButton(new SetFlexibleControlsTypeCommand(FlexibleControlsType.FirstPerson)); + CreateCommandButton(new SetFlexibleControlsTypeCommand(FlexibleControlsType.FirstPerson)); } diff --git a/react-components/src/components/Architecture/Toolbar.tsx b/react-components/src/components/Architecture/Toolbar.tsx index 10b929ec25a..dc6c441ed28 100644 --- a/react-components/src/components/Architecture/Toolbar.tsx +++ b/react-components/src/components/Architecture/Toolbar.tsx @@ -1,11 +1,11 @@ /*! * Copyright 2024 Cognite AS */ -import { Divider, ToolBar } from '@cognite/cogs.js'; +import { ToolBar } from '@cognite/cogs.js'; import styled from 'styled-components'; import { useState, type ReactElement } from 'react'; import { withSuppressRevealEvents } from '../../higher-order-components/withSuppressRevealEvents'; -import { CommandButton } from './CommandButton'; +import { CommandButtons } from './CommandButton'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; import { ActiveToolUpdater } from '../../architecture/base/reactUpdaters/ActiveToolUpdater'; @@ -69,24 +69,12 @@ const CreateToolToolbar = ( flexFlow: style.flexFlow // Padding is not used here }}> - <>{commands.map((command, index): ReactElement => addCommand(command, style, index))} + ); }; -function addCommand( - command: BaseCommand | undefined, - style: PopupStyle, - index: number -): ReactElement { - if (command === undefined) { - const direction = style.isHorizontalDivider ? 'horizontal' : 'vertical'; - return ; - } - return ; -} - const Container = styled.div` zindex: 1000px; position: absolute; @@ -94,3 +82,4 @@ const Container = styled.div` `; const MyCustomToolbar = styled(withSuppressRevealEvents(ToolBar))``; +export { CommandButtons }; diff --git a/react-components/src/components/RevealToolbar/SetFlexibleControlsType.tsx b/react-components/src/components/RevealToolbar/SetFlexibleControlsType.tsx index 6dcb2a637d0..eff98682371 100644 --- a/react-components/src/components/RevealToolbar/SetFlexibleControlsType.tsx +++ b/react-components/src/components/RevealToolbar/SetFlexibleControlsType.tsx @@ -14,14 +14,13 @@ import { import { useTranslation } from '../i18n/I18n'; import styled from 'styled-components'; +import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; type CustomSettingsProps = { includeOrbitInCenter?: boolean; orientation?: 'horizontal' | 'vertical'; }; -type TranslateDelegate = (key: string, fallback?: string) => string; - type ControlTypeSelectionProps = { selectedControlsType: FlexibleControlsType; setSelectedControlsType: (controlsType: FlexibleControlsType) => void; diff --git a/react-components/yarn.lock b/react-components/yarn.lock index 3300d2975cd..c530593fe25 100644 --- a/react-components/yarn.lock +++ b/react-components/yarn.lock @@ -1693,9 +1693,9 @@ __metadata: languageName: node linkType: hard -"@cognite/cogs.js@npm:^9.3.0": - version: 9.84.2 - resolution: "@cognite/cogs.js@npm:9.84.2" +"@cognite/cogs.js@npm:^9.84.3": + version: 9.84.3 + resolution: "@cognite/cogs.js@npm:9.84.3" dependencies: "@emotion/react": "npm:11.10.6" "@emotion/styled": "npm:11.10.6" @@ -1736,7 +1736,7 @@ __metadata: "@types/react": ^18.0.0 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/4fdd6f11534e63ea7205b18f34ca448972309a7cba499a9eb2098b99d3ffe95f0b68ef9c93e172e5d30ea26c2bbe417705d05a0dd69d4cfd0bb98c16a268925a + checksum: 10/bc36fb3b01026068fee222c99fd422e72c8b3e37d4b92155ca3b4a22a46db0df820cbbc0e5ea9befac1adba0e7018caa07197550eee14c5b914f4a3123cfd2df languageName: node linkType: hard @@ -1746,7 +1746,7 @@ __metadata: dependencies: "@cognite/cdf-i18n-utils": "npm:^0.7.5" "@cognite/cdf-utilities": "npm:^3.6.0" - "@cognite/cogs.js": "npm:^9.3.0" + "@cognite/cogs.js": "npm:^9.84.3" "@cognite/reveal": "npm:^4.14.5" "@cognite/sdk": "npm:^9.13.0" "@playwright/test": "npm:^1.43.1" @@ -1801,7 +1801,7 @@ __metadata: vite-plugin-externalize-deps: "npm:^0.8.0" vitest: "npm:^1.5.3" peerDependencies: - "@cognite/cogs.js": ">=9" + "@cognite/cogs.js": ">=9.84.3" "@cognite/reveal": 4.14.5 react: ">=18" react-dom: ">=18"