diff --git a/react-components/src/architecture/base/commands/BaseCommand.ts b/react-components/src/architecture/base/commands/BaseCommand.ts index 02dbf816681..d9b274c1a95 100644 --- a/react-components/src/architecture/base/commands/BaseCommand.ts +++ b/react-components/src/architecture/base/commands/BaseCommand.ts @@ -34,13 +34,13 @@ export abstract class BaseCommand { // CONSTRUCTOR // ================================================== - constructor() { + public constructor() { BaseCommand._counter++; this._uniqueId = BaseCommand._counter; } // ================================================== - // VIRTUAL METHODS (To be override) + // VIRTUAL METHODS (To be overridden) // ================================================= public get name(): string { @@ -79,6 +79,15 @@ export abstract class BaseCommand { return this.isEnabled; } + /** + * Gets a value indicating whether the command can be toggled on or off. + * Override this property if the command can be toggled. + * You must also override isChecked to get the toggle state. + */ + public get isToggle(): boolean { + return false; + } + public get isChecked(): boolean { return false; } @@ -137,6 +146,10 @@ export abstract class BaseCommand { } } + // ================================================== + // INSTANCE METHODS: Others + // ================================================== + public getLabel(translate: TranslateDelegate): string { const { key, fallback } = this.tooltip; if (key === undefined) { diff --git a/react-components/src/architecture/base/commands/BaseOptionCommand.ts b/react-components/src/architecture/base/commands/BaseOptionCommand.ts index 7aaed2c49c7..9ee5ab825ad 100644 --- a/react-components/src/architecture/base/commands/BaseOptionCommand.ts +++ b/react-components/src/architecture/base/commands/BaseOptionCommand.ts @@ -3,46 +3,53 @@ */ import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; -import { BaseCommand } from './BaseCommand'; +import { type BaseCommand } from './BaseCommand'; import { RenderTargetCommand } from './RenderTargetCommand'; /** - * Base class for all command and tools. These are object that can do a - * user interaction with the system. It also have enough information to - * generate the UI for the command. + * Base class for all option like commands. Override createOptions to add options + * or use add method to add them in. */ -export abstract class BaseOptionCommand extends BaseCommand { +export abstract class BaseOptionCommand extends RenderTargetCommand { private _options: BaseCommand[] | undefined = undefined; + // ================================================== + // OVERRIDES + // ================================================== + + public override attach(renderTarget: RevealRenderTarget): void { + this._renderTarget = renderTarget; + for (const option of this.options) { + if (option instanceof RenderTargetCommand) { + option.attach(renderTarget); + } + } + } // ================================================== // VIRTUAL METHODS // ================================================== - public createOptions(): BaseCommand[] { - return []; // Override this to add options + protected createOptions(): BaseCommand[] { + return []; // Override this to add options or use the add method } // ================================================== // INSTANCE METHODS // ================================================== - public getOrCreateOptions(renderTarget: RevealRenderTarget): BaseCommand[] { + public get options(): BaseCommand[] { if (this._options === undefined) { this._options = this.createOptions(); - for (const option of this._options) { - if (option instanceof RenderTargetCommand) { - option.attach(renderTarget); - } - } } return this._options; } public get selectedOption(): BaseCommand | undefined { - if (this._options === undefined) { - return undefined; - } - return this._options.find((option) => option.isChecked); + return this.options.find((option) => option.isChecked); + } + + protected add(command: BaseCommand): void { + this.options.push(command); } } diff --git a/react-components/src/architecture/base/commands/BaseSliderCommand.ts b/react-components/src/architecture/base/commands/BaseSliderCommand.ts new file mode 100644 index 00000000000..dd7f4c3f33c --- /dev/null +++ b/react-components/src/architecture/base/commands/BaseSliderCommand.ts @@ -0,0 +1,34 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { RenderTargetCommand } from './RenderTargetCommand'; + +export abstract class BaseSliderCommand extends RenderTargetCommand { + // ================================================== + // INSTANCE FIELDS + // ================================================= + + readonly min: number; + readonly max: number; + readonly step: number; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + protected constructor(min: number, max: number, step: number) { + super(); + this.min = min; + this.max = max; + this.step = step; + } + + // ================================================== + // VIRTUAL METHODS (To be overridden) + // ================================================= + + public abstract get value(): number; + + public abstract set value(value: number); +} diff --git a/react-components/src/architecture/base/commands/RenderTargetCommand.ts b/react-components/src/architecture/base/commands/RenderTargetCommand.ts index eb49cecd226..c9df95aa879 100644 --- a/react-components/src/architecture/base/commands/RenderTargetCommand.ts +++ b/react-components/src/architecture/base/commands/RenderTargetCommand.ts @@ -65,14 +65,14 @@ export abstract class RenderTargetCommand extends BaseCommand { return activeTool.undoManager; } - // ================================================== - // INSTANCE METHODS - // ================================================== - public attach(renderTarget: RevealRenderTarget): void { this._renderTarget = renderTarget; } + // ================================================== + // INSTANCE METHODS + // ================================================== + public addTransaction(transaction: Transaction | undefined): void { if (transaction === undefined) { return; diff --git a/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts b/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts index 7bf9f076068..0f55cbfd509 100644 --- a/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts +++ b/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts @@ -18,6 +18,10 @@ export abstract class ShowAllDomainObjectsCommand extends InstanceCommand { return 'EyeShow'; } + public override get isToggle(): boolean { + return true; + } + public override get isChecked(): boolean { return this.isAnyVisible(); } diff --git a/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts b/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts index e062b4f99ae..88916053686 100644 --- a/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts +++ b/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts @@ -15,6 +15,10 @@ export abstract class ShowDomainObjectsOnTopCommand extends InstanceCommand { return 'Flag'; } + public override get isToggle(): boolean { + return true; + } + public override get isChecked(): boolean { return !this.getDepthTest(); } diff --git a/react-components/src/architecture/base/concreteCommands/KeyboardSpeedCommand.ts b/react-components/src/architecture/base/concreteCommands/KeyboardSpeedCommand.ts index 88915de486d..acc2b7b9ba0 100644 --- a/react-components/src/architecture/base/concreteCommands/KeyboardSpeedCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/KeyboardSpeedCommand.ts @@ -2,34 +2,36 @@ * Copyright 2024 Cognite AS */ -import { type BaseCommand } from '../commands/BaseCommand'; import { BaseOptionCommand } from '../commands/BaseOptionCommand'; import { RenderTargetCommand } from '../commands/RenderTargetCommand'; import { type TranslateKey } from '../utilities/TranslateKey'; -const KEYBOARD_SPEED_VALUES = [0.5, 1, 2, 5, 10, 20]; +const DEFAULT_OPTIONS = [0.5, 1, 2, 5, 10, 20]; export class KeyboardSpeedCommand extends BaseOptionCommand { // ================================================== - // OVERRIDES + // CONSTRUCTOR // ================================================== - public override get tooltip(): TranslateKey { - return { key: 'FLY_SPEED', fallback: 'Set fly speed on the camera' }; + constructor(supportedTypes = DEFAULT_OPTIONS) { + super(); + for (const value of supportedTypes) { + this.add(new OptionCommand(value)); + } } - public override createOptions(): BaseCommand[] { - const options: BaseCommand[] = []; - for (const value of KEYBOARD_SPEED_VALUES) { - options.push(new OptionCommand(value)); - } - return options; + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'FLY_SPEED', fallback: 'Set camera fly speed' }; } } // Note: This is not exported, as it is only used internally class OptionCommand extends RenderTargetCommand { - private readonly _value; + private readonly _value: number; public constructor(value: number) { super(); diff --git a/react-components/src/architecture/base/concreteCommands/SetPointColorTypeCommand.ts b/react-components/src/architecture/base/concreteCommands/SetPointColorTypeCommand.ts new file mode 100644 index 00000000000..77855e41882 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/SetPointColorTypeCommand.ts @@ -0,0 +1,97 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { PointColorType } from '@cognite/reveal'; +import { BaseOptionCommand } from '../commands/BaseOptionCommand'; +import { RenderTargetCommand } from '../commands/RenderTargetCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; + +const DEFAULT_OPTIONS: PointColorType[] = [ + PointColorType.Rgb, + PointColorType.Classification, + PointColorType.Depth, + PointColorType.Height, + PointColorType.Intensity +]; + +export class SetPointColorTypeCommand extends BaseOptionCommand { + // ================================================== + // CONSTRUCTOR + // ================================================== + + constructor(supportedTypes = DEFAULT_OPTIONS) { + super(); + for (const value of supportedTypes) { + this.add(new OptionCommand(value)); + } + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'POINT_COLOR', fallback: 'Point color' }; + } + + public override get isEnabled(): boolean { + return this.renderTarget.getPointClouds().next().value !== undefined; + } +} + +// Note: This is not exported, as it is only used internally + +class OptionCommand extends RenderTargetCommand { + private readonly _value: PointColorType; + + public constructor(value: PointColorType) { + super(); + this._value = value; + } + + public override get tooltip(): TranslateKey { + return getTranslateKey(this._value); + } + + public override get isChecked(): boolean { + // Let the first PointCloud decide the color type + const pointCloud = this.renderTarget.getPointClouds().next().value; + if (pointCloud === undefined) { + return false; + } + return pointCloud.pointColorType === this._value; + } + + public override invokeCore(): boolean { + for (const pointCloud of this.renderTarget.getPointClouds()) { + pointCloud.pointColorType = this._value; + } + return true; + } +} + +// ================================================== +// PRIVATE FUNCTIONS +// ================================================== + +function getTranslateKey(type: PointColorType): TranslateKey { + switch (type) { + case PointColorType.Rgb: + return { key: 'RGB', fallback: 'RGB' }; + case PointColorType.Depth: + return { key: 'DEPTH', fallback: 'Depth' }; + case PointColorType.Height: + return { key: 'HEIGHT', fallback: 'Height' }; + case PointColorType.Classification: + return { key: 'CLASSIFICATION', fallback: 'Classification' }; + case PointColorType.Intensity: + return { key: 'INTENSITY', fallback: 'Intensity' }; + case PointColorType.LevelOfDetail: + return { key: 'LEVEL_OF_DETAIL', fallback: 'LevelOfDetail' }; + case PointColorType.PointIndex: + return { key: 'POINT_INDEX', fallback: 'PointIndex' }; + default: + return { key: 'UNKNOWN', fallback: 'Unknown' }; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/SetPointShapeCommand.ts b/react-components/src/architecture/base/concreteCommands/SetPointShapeCommand.ts new file mode 100644 index 00000000000..e2fdb16cf9b --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/SetPointShapeCommand.ts @@ -0,0 +1,81 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { PointShape } from '@cognite/reveal'; +import { BaseOptionCommand } from '../commands/BaseOptionCommand'; +import { RenderTargetCommand } from '../commands/RenderTargetCommand'; +import { type TranslateKey } from '../utilities/TranslateKey'; + +const DEFAULT_OPTIONS: PointShape[] = [PointShape.Circle, PointShape.Square, PointShape.Paraboloid]; + +export class SetPointShapeCommand extends BaseOptionCommand { + // ================================================== + // CONSTRUCTOR + // ================================================== + + constructor(supportedTypes = DEFAULT_OPTIONS) { + super(); + for (const value of supportedTypes) { + this.add(new OptionCommand(value)); + } + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'POINT_SHAPE', fallback: 'Point shape' }; + } + + public override get isEnabled(): boolean { + return this.renderTarget.getPointClouds().next().value !== undefined; + } +} + +// Note: This is not exported, as it is only used internally + +class OptionCommand extends RenderTargetCommand { + private readonly _value: PointShape; + + public constructor(value: PointShape) { + super(); + this._value = value; + } + + public override get tooltip(): TranslateKey { + return getTranslateKey(this._value); + } + + public override get isChecked(): boolean { + // Let the first PointCloud decide the color type + const pointCloud = this.renderTarget.getPointClouds().next().value; + if (pointCloud === undefined) { + return false; + } + return pointCloud.pointSizeType === this._value; + } + + public override invokeCore(): boolean { + for (const pointCloud of this.renderTarget.getPointClouds()) { + pointCloud.pointShape = this._value; + } + return true; + } +} + +// ================================================== +// PRIVATE FUNCTIONS +// ================================================== + +function getTranslateKey(type: PointShape): TranslateKey { + switch (type) { + case PointShape.Circle: + return { key: 'CIRCLE', fallback: 'Circle' }; + case PointShape.Square: + return { key: 'SQUARE', fallback: 'Square' }; + case PointShape.Paraboloid: + return { key: 'PARABOLOID', fallback: 'Paraboloid' }; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/SetPointSizeCommand.ts b/react-components/src/architecture/base/concreteCommands/SetPointSizeCommand.ts new file mode 100644 index 00000000000..765edc4ad7e --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/SetPointSizeCommand.ts @@ -0,0 +1,45 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type TranslateKey } from '../utilities/TranslateKey'; +import { BaseSliderCommand } from '../commands/BaseSliderCommand'; + +const DEFAULT_POINT_SIZE = 2; +const MIN_POINT_SIZE = 0.0; +const MAX_POINT_SIZE = 4; // Default seems be be 2, but the user probably wants lower values +const STEP_POINT_SIZE = 0.1; + +export class SetPointSizeCommand extends BaseSliderCommand { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor() { + super(MIN_POINT_SIZE, MAX_POINT_SIZE, STEP_POINT_SIZE); + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'POINT_SIZE', fallback: 'Point size' }; + } + + public override get isEnabled(): boolean { + return this.renderTarget.getPointClouds().next().value !== undefined; + } + + public override get value(): number { + // Let the first PointCloud decide the point size + const pointCloud = this.renderTarget.getPointClouds().next().value; + return pointCloud?.pointSize ?? DEFAULT_POINT_SIZE; + } + + public override set value(value: number) { + for (const pointCloud of this.renderTarget.getPointClouds()) { + pointCloud.pointSize = value; + } + } +} diff --git a/react-components/src/architecture/base/concreteCommands/SetQualityCommand.ts b/react-components/src/architecture/base/concreteCommands/SetQualityCommand.ts new file mode 100644 index 00000000000..c8e4df7caf9 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/SetQualityCommand.ts @@ -0,0 +1,104 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type TranslateKey } from '../utilities/TranslateKey'; +import { type QualitySettings } from '../../../components/RevealToolbar/SettingsContainer/types'; +import { type Cognite3DViewer } from '@cognite/reveal'; +import { RenderTargetCommand } from '../commands/RenderTargetCommand'; + +export class SetQualityCommand extends RenderTargetCommand { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + private readonly _highQualityFactor: number; + private _lowQualitySettings: QualitySettings | undefined; + private _highQualitySettings: QualitySettings | undefined; + + // ================================================== + // INSTANCE PROPERTIES + // ================================================== + + private get lowQualitySettings(): QualitySettings { + if (this._lowQualitySettings === undefined) { + this._lowQualitySettings = getDefaultSettings(this.renderTarget.viewer); + } + return this._lowQualitySettings; + } + + private get highQualitySettings(): QualitySettings { + if (this._highQualitySettings === undefined) { + const settings = this.lowQualitySettings; + this._highQualitySettings = { + cadBudget: { + maximumRenderCost: this._highQualityFactor * settings.cadBudget.maximumRenderCost, + highDetailProximityThreshold: settings.cadBudget.highDetailProximityThreshold + }, + pointCloudBudget: { + numberOfPoints: this._highQualityFactor * settings.pointCloudBudget.numberOfPoints + }, + resolutionOptions: { + maxRenderResolution: Infinity, + movingCameraResolutionFactor: 1 + } + }; + } + return this._highQualitySettings; + } + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(highQualityFactor = 3) { + super(); + this._highQualityFactor = highQualityFactor; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'HIGH_FIDELITY', fallback: 'High Fidelity' }; + } + + public override get isToggle(): boolean { + return true; + } + + public override get isChecked(): boolean { + return !this.isLowQuality(this.renderTarget.viewer); + } + + protected override invokeCore(): boolean { + const { viewer } = this.renderTarget; + const settings = this.isLowQuality(viewer) ? this.highQualitySettings : this.lowQualitySettings; + viewer.cadBudget = settings.cadBudget; + viewer.pointCloudBudget = settings.pointCloudBudget; + viewer.setResolutionOptions(settings.resolutionOptions); + return true; + } + + private isLowQuality(viewer: Cognite3DViewer): boolean { + const settings = this.lowQualitySettings; + return ( + viewer.cadBudget.maximumRenderCost <= settings.cadBudget.maximumRenderCost && + viewer.pointCloudBudget.numberOfPoints <= settings.pointCloudBudget.numberOfPoints + ); + } +} + +function getDefaultSettings(viewer: Cognite3DViewer): QualitySettings { + const settings: QualitySettings = { + cadBudget: { ...viewer.cadBudget }, + pointCloudBudget: { ...viewer.pointCloudBudget }, + // This should be fetched from the viewer, but cannot for some unknown reason + resolutionOptions: { + maxRenderResolution: 1.4e6, + movingCameraResolutionFactor: 1 + } + }; + return settings; +} diff --git a/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts b/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts new file mode 100644 index 00000000000..e9b250f1f56 --- /dev/null +++ b/react-components/src/architecture/base/concreteCommands/SettingsCommand.ts @@ -0,0 +1,52 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; +import { type TranslateKey } from '../utilities/TranslateKey'; +import { type BaseCommand } from '../commands/BaseCommand'; +import { RenderTargetCommand } from '../commands/RenderTargetCommand'; + +export class SettingsCommand extends RenderTargetCommand { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + private readonly _commands: BaseCommand[] = []; + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'SETTINGS_TOOLTIP', fallback: 'Settings' }; + } + + public override get icon(): string | undefined { + return 'Settings'; + } + + public override attach(renderTarget: RevealRenderTarget): void { + super.attach(renderTarget); + for (const command of this._commands) { + if (command instanceof RenderTargetCommand) { + command.attach(renderTarget); + } + } + } + // ================================================== + // INSTANCE METHODS + // ================================================== + + public add(command: BaseCommand): void { + if (this._commands.find((c) => c.equals(command)) !== undefined) { + console.error('Duplicated command given: ' + command.name); + return; + } + this._commands.push(command); + } + + public get commands(): BaseCommand[] { + return this._commands; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/ToggleMetricUnitsCommand.ts b/react-components/src/architecture/base/concreteCommands/ToggleMetricUnitsCommand.ts index ccac8ad91db..c933c788c70 100644 --- a/react-components/src/architecture/base/concreteCommands/ToggleMetricUnitsCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/ToggleMetricUnitsCommand.ts @@ -19,6 +19,10 @@ export class ToggleMetricUnitsCommand extends RenderTargetCommand { return { key: 'TOGGLE_METRIC_UNITS', fallback: 'm/ft' }; // Note: m/ft do not need to be translated! } + public override get isToggle(): boolean { + return true; + } + public override get isChecked(): boolean { return this.rootDomainObject.unitSystem.isMetric; } diff --git a/react-components/src/architecture/base/concreteCommands/UndoCommand.ts b/react-components/src/architecture/base/concreteCommands/UndoCommand.ts index ae3668ad2c3..56eac50393c 100644 --- a/react-components/src/architecture/base/concreteCommands/UndoCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/UndoCommand.ts @@ -12,7 +12,7 @@ export class UndoCommand extends RenderTargetCommand { // ================================================== public override get icon(): string { - return 'Refresh'; // Should be 'Restore ' but it doesn't exist + return 'Refresh'; // Should be 'Restore' but it doesn't exist } public override get tooltip(): TranslateKey { diff --git a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts index c5c583d4460..5cb692006ba 100644 --- a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts +++ b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts @@ -8,7 +8,8 @@ import { isFlexibleCameraManager, type Cognite3DViewer, type IFlexibleCameraManager, - CDF_TO_VIEWER_TRANSFORMATION + CDF_TO_VIEWER_TRANSFORMATION, + CognitePointCloudModel } from '@cognite/reveal'; import { Vector3, @@ -148,6 +149,14 @@ export class RevealRenderTarget { return this.viewer.getSceneBoundingBox(); } + public *getPointClouds(): Generator { + for (const model of this.viewer.models) { + if (model instanceof CognitePointCloudModel) { + yield model; + } + } + } + // ================================================== // INSTANCE METHODS // ================================================== diff --git a/react-components/src/architecture/base/undo/DomainObjectTransaction.ts b/react-components/src/architecture/base/undo/DomainObjectTransaction.ts index 91d37d31a0d..f0e63857349 100644 --- a/react-components/src/architecture/base/undo/DomainObjectTransaction.ts +++ b/react-components/src/architecture/base/undo/DomainObjectTransaction.ts @@ -34,7 +34,7 @@ export class DomainObjectTransaction extends Transaction { } // ================================================== - // VIRTUAL METHODS (To be override) + // VIRTUAL METHODS (To be overridden) // ================================================= protected override undoCore( diff --git a/react-components/src/architecture/base/undo/Transaction.ts b/react-components/src/architecture/base/undo/Transaction.ts index c18a7ba3c1e..0caafb4687e 100644 --- a/react-components/src/architecture/base/undo/Transaction.ts +++ b/react-components/src/architecture/base/undo/Transaction.ts @@ -33,7 +33,7 @@ export abstract class Transaction { } // ================================================== - // VIRTUAL METHODS (To be override) + // VIRTUAL METHODS (To be overridden) // ================================================= protected abstract undoCore( diff --git a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts index a85e4dfc38c..3e84bb44cda 100644 --- a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts +++ b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts @@ -23,6 +23,10 @@ export class SetAxisVisibleCommand extends RenderTargetCommand { return true; } + public override get isToggle(): boolean { + return true; + } + public override get isChecked(): boolean { const { renderTarget, rootDomainObject } = this; diff --git a/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts b/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts index 44e927d42fc..24dec061358 100644 --- a/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts +++ b/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts @@ -40,6 +40,10 @@ export class ApplyClipCommand extends RenderTargetCommand { return false; } + public override get isToggle(): boolean { + return true; + } + public override get isChecked(): boolean { return this.renderTarget.isGlobalClippingActive; } diff --git a/react-components/src/architecture/concrete/config/StoryBookConfig.ts b/react-components/src/architecture/concrete/config/StoryBookConfig.ts index 643c20f1cc4..6fc8d4d1848 100644 --- a/react-components/src/architecture/concrete/config/StoryBookConfig.ts +++ b/react-components/src/architecture/concrete/config/StoryBookConfig.ts @@ -21,6 +21,11 @@ import { MeasurementTool } from '../measurements/MeasurementTool'; import { ClipTool } from '../clipping/ClipTool'; import { KeyboardSpeedCommand } from '../../base/concreteCommands/KeyboardSpeedCommand'; import { ObservationsTool } from '../observations/ObservationsTool'; +import { SettingsCommand } from '../../base/concreteCommands/SettingsCommand'; +import { SetQualityCommand } from '../../base/concreteCommands/SetQualityCommand'; +import { SetPointSizeCommand } from '../../base/concreteCommands/SetPointSizeCommand'; +import { SetPointColorTypeCommand } from '../../base/concreteCommands/SetPointColorTypeCommand'; +import { SetPointShapeCommand } from '../../base/concreteCommands/SetPointShapeCommand'; export class StoryBookConfig extends BaseRevealConfig { // ================================================== @@ -32,6 +37,12 @@ export class StoryBookConfig extends BaseRevealConfig { } public override createMainToolbar(): Array { + const settings = new SettingsCommand(); + settings.add(new SetQualityCommand()); + settings.add(new SetPointSizeCommand()); + settings.add(new SetPointColorTypeCommand()); + settings.add(new SetPointShapeCommand()); + return [ new SetFlexibleControlsTypeCommand(FlexibleControlsType.Orbit), new SetFlexibleControlsTypeCommand(FlexibleControlsType.FirstPerson), @@ -47,7 +58,9 @@ export class StoryBookConfig extends BaseRevealConfig { new ObservationsTool(), undefined, new SetTerrainVisibleCommand(), - new UpdateTerrainCommand() + new UpdateTerrainCommand(), + undefined, + settings ]; } diff --git a/react-components/src/components/Architecture/CommandButton.tsx b/react-components/src/components/Architecture/CommandButton.tsx index a7ef18a2551..57ccad1287f 100644 --- a/react-components/src/components/Architecture/CommandButton.tsx +++ b/react-components/src/components/Architecture/CommandButton.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import { type ReactElement, useState, useEffect, useMemo, useCallback } from 'react'; @@ -12,10 +12,12 @@ import { LabelWithShortcut } from './LabelWithShortcut'; export const CommandButton = ({ inputCommand, - isHorizontal = false + isHorizontal = false, + usedInSettings = false }: { inputCommand: BaseCommand; isHorizontal: boolean; + usedInSettings?: boolean; }): ReactElement => { const renderTarget = useRenderTarget(); const { t } = useTranslation(); @@ -47,12 +49,13 @@ export const CommandButton = ({ return <>; } const placement = getTooltipPlacement(isHorizontal); - const tooltip = command.getLabel(t); + const label = command.getLabel(t); const shortcut = command.getShortCutKeys(); return ( } + content={} + disabled={usedInSettings || label === undefined} appendTo={document.body} placement={placement}> ); }; diff --git a/react-components/src/components/Architecture/CommandButtons.tsx b/react-components/src/components/Architecture/CommandButtons.tsx index 84e20378de7..32c6909a699 100644 --- a/react-components/src/components/Architecture/CommandButtons.tsx +++ b/react-components/src/components/Architecture/CommandButtons.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import { useMemo, type ReactElement } from 'react'; @@ -8,12 +8,26 @@ import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { OptionButton } from './OptionButton'; import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; import { CommandButton } from './CommandButton'; +import { SettingsButton } from './SettingsButton'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; -export function createButton(command: BaseCommand, isHorizontal = false): ReactElement { - if (command instanceof BaseOptionCommand) { +export function createButton( + command: BaseCommand, + isHorizontal = false, + usedInSettings = false +): ReactElement { + if (command instanceof SettingsCommand) { + return ; + } else if (command instanceof BaseOptionCommand) { return ; } else { - return ; + return ( + + ); } } diff --git a/react-components/src/components/Architecture/LabelWithShortcut.tsx b/react-components/src/components/Architecture/LabelWithShortcut.tsx index 0a7e8c5dde7..ac4faf14f88 100644 --- a/react-components/src/components/Architecture/LabelWithShortcut.tsx +++ b/react-components/src/components/Architecture/LabelWithShortcut.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import React from 'react'; @@ -8,11 +8,14 @@ import styled from 'styled-components'; import { Shortcut } from '@cognite/cogs.js'; type LabelWithShortcutProps = { - label: string; + label?: string; shortcut?: string; }; export const LabelWithShortcut: React.FC = ({ label, shortcut }) => { + if (label === undefined) { + return <>; + } return ( diff --git a/react-components/src/components/Architecture/OptionButton.tsx b/react-components/src/components/Architecture/OptionButton.tsx index f28b9f2bba4..c77f0590d48 100644 --- a/react-components/src/components/Architecture/OptionButton.tsx +++ b/react-components/src/components/Architecture/OptionButton.tsx @@ -1,9 +1,9 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ -import { useCallback, useEffect, useMemo, useState, type ReactElement } from 'react'; -import { Button, Dropdown, Menu, Tooltip as CogsTooltip, type IconType } from '@cognite/cogs.js'; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'; +import { Button, Dropdown, Menu, Tooltip as CogsTooltip } from '@cognite/cogs.js'; import { useTranslation } from '../i18n/I18n'; import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; import { useRenderTarget } from '../RevealCanvas/ViewerContext'; @@ -12,17 +12,21 @@ import { getButtonType, getDefaultCommand, getFlexDirection, - getIcon, - getTooltipPlacement + getTooltipPlacement, + getIcon } from './utilities'; import { LabelWithShortcut } from './LabelWithShortcut'; +import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; +import { useClickOutside } from './useClickOutside'; export const OptionButton = ({ inputCommand, - isHorizontal = false + isHorizontal = false, + usedInSettings = false }: { inputCommand: BaseOptionCommand; isHorizontal: boolean; + usedInSettings?: boolean; }): ReactElement => { const renderTarget = useRenderTarget(); const { t } = useTranslation(); @@ -32,13 +36,21 @@ export const OptionButton = ({ const [isEnabled, setEnabled] = useState(true); const [isVisible, setVisible] = useState(true); const [uniqueId, setUniqueId] = useState(0); - const [icon, setIcon] = useState(undefined); + + const postAction = (): void => { + setOpen(false); + renderTarget.domElement.focus(); + }; + + const menuRef = useRef(null); + useClickOutside(menuRef, () => { + postAction(); + }); const update = useCallback((command: BaseCommand) => { setEnabled(command.isEnabled); setVisible(command.isVisible); setUniqueId(command.uniqueId); - setIcon(getIcon(command)); }, []); useEffect(() => { @@ -56,67 +68,74 @@ export const OptionButton = ({ return <>; } const placement = getTooltipPlacement(isHorizontal); - const tooltip = command.getLabel(t); + const label = usedInSettings ? undefined : command.getLabel(t); const shortcut = command.getShortCutKeys(); const flexDirection = getFlexDirection(isHorizontal); - const options = command.getOrCreateOptions(renderTarget); + const options = command.options; const selectedLabel = command.selectedOption?.getLabel(t); return ( - } - appendTo={document.body} - placement={placement}> - + } + disabled={usedInSettings || label === undefined} appendTo={document.body} - onClickOutside={() => { - setOpen(false); - renderTarget.domElement.focus(); - }} - content={ - + + {options.map((command, _index): ReactElement => { + return createMenuItem(command, t, postAction); + })} + + }> + - - + {selectedLabel} + + + + ); }; + +export function createMenuItem( + command: BaseCommand, + t: TranslateDelegate, + postAction: () => void +): ReactElement { + return ( + { + command.invoke(); + postAction(); + }}> + {command.getLabel(t)} + + ); +} diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 431aa6fa1cb..8541f38f9e1 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 Cognite AS + * Copyright 2024 Cognite AS */ import { type ReactElement } from 'react'; @@ -13,8 +13,15 @@ import { MeasurementTool } from '../../architecture/concrete/measurements/Measur import { KeyboardSpeedCommand } from '../../architecture/base/concreteCommands/KeyboardSpeedCommand'; import { ObservationsTool } from '../../architecture/concrete/observations/ObservationsTool'; import { createButtonFromCommandConstructor } from './CommandButtons'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { SetPointColorTypeCommand } from '../../architecture/base/concreteCommands/SetPointColorTypeCommand'; +import { SetPointShapeCommand } from '../../architecture/base/concreteCommands/SetPointShapeCommand'; +import { SetPointSizeCommand } from '../../architecture/base/concreteCommands/SetPointSizeCommand'; +import { SetQualityCommand } from '../../architecture/base/concreteCommands/SetQualityCommand'; export class RevealButtons { + static Settings = (): ReactElement => createButtonFromCommandConstructor(() => createSettings()); + static FitView = (): ReactElement => createButtonFromCommandConstructor(() => new FitViewCommand()); @@ -46,3 +53,12 @@ export class RevealButtons { static KeyboardSpeed = (): ReactElement => createButtonFromCommandConstructor(() => new KeyboardSpeedCommand()); } + +function createSettings(): SettingsCommand { + const settings = new SettingsCommand(); + settings.add(new SetQualityCommand()); + settings.add(new SetPointSizeCommand()); + settings.add(new SetPointColorTypeCommand()); + settings.add(new SetPointShapeCommand()); + return settings; +} diff --git a/react-components/src/components/Architecture/SettingsButton.tsx b/react-components/src/components/Architecture/SettingsButton.tsx new file mode 100644 index 00000000000..742c8b8efc8 --- /dev/null +++ b/react-components/src/components/Architecture/SettingsButton.tsx @@ -0,0 +1,220 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { useCallback, useEffect, useMemo, useState, type ReactElement } from 'react'; +import { + Button, + Dropdown, + Menu, + Tooltip as CogsTooltip, + type IconType, + Slider +} from '@cognite/cogs.js'; +import { useTranslation } from '../i18n/I18n'; +import { type BaseCommand } from '../../architecture/base/commands/BaseCommand'; +import { useRenderTarget } from '../RevealCanvas/ViewerContext'; +import { + getButtonType, + getDefaultCommand, + getFlexDirection, + getTooltipPlacement, + getIcon +} from './utilities'; +import { LabelWithShortcut } from './LabelWithShortcut'; +import { type TranslateDelegate } from '../../architecture/base/utilities/TranslateKey'; +import styled from 'styled-components'; +import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; +import { createButton } from './CommandButtons'; +import { BaseOptionCommand } from '../../architecture/base/commands/BaseOptionCommand'; +import { OptionButton } from './OptionButton'; +import { BaseSliderCommand } from '../../architecture/base/commands/BaseSliderCommand'; + +export const SettingsButton = ({ + inputCommand, + isHorizontal = false +}: { + inputCommand: SettingsCommand; + isHorizontal: boolean; +}): ReactElement => { + const renderTarget = useRenderTarget(); + const { t } = useTranslation(); + const command = useMemo(() => getDefaultCommand(inputCommand, renderTarget), []); + + const [isOpen, setOpen] = useState(false); + const [isEnabled, setEnabled] = useState(true); + const [isVisible, setVisible] = useState(true); + const [uniqueId, setUniqueId] = useState(0); + const [icon, setIcon] = useState(undefined); + + const update = useCallback((command: BaseCommand) => { + setEnabled(command.isEnabled); + setVisible(command.isVisible); + setUniqueId(command.uniqueId); + setIcon(getIcon(command)); + }, []); + + useEffect(() => { + update(command); + command.addEventListener(update); + return () => { + command.removeEventListener(update); + }; + }, [command]); + + if (!(command instanceof SettingsCommand)) { + return <>; + } + if (!isVisible) { + return <>; + } + const placement = getTooltipPlacement(isHorizontal); + const label = command.getLabel(t); + const shortcut = command.getShortCutKeys(); + const flexDirection = getFlexDirection(isHorizontal); + const commands = command.commands; + + return ( + } + disabled={label === undefined} + appendTo={document.body} + placement={placement}> + + {commands.map((command, _index): ReactElement | undefined => { + return createMenuItem(command, t); + })} + + }> +