From 14f2d903c7a60afa0b08c4f4d73eccfdff9f4d33 Mon Sep 17 00:00:00 2001 From: Nils Petter Fremming <35219649+nilscognite@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:18:57 +0200 Subject: [PATCH] react-component(feat): Add crop box, slicing and tool, generalize box concept (#4594) * First commit * Generalized box and lines * More generalize * Further work * Fix crop box * Some fixes * Move files again * Move files * Reuse code * adjustments * After add constrains * Fixing cropping * Fix typename * Working good now * Update PlaneDomainObject.ts * Fix updating * Fix updating * Make a general bounding box * Fixes * make flipping consistent * Change name * Reorganization * Fixing * more fixes * Fix icon * Fixing bounding box * Smaller fixing * Fix flaes * Update RevealButtons.tsx * Fix updating of pending * Update CropBoxDomainObject.ts * Update comments * Example uses ClipPlane, differentiate Left/Right button, contrained resizing * Update BoxDragger.ts * Fixes according to review * Fix unit in box dragging --- .../architecture/base/commands/BaseCommand.ts | 20 +- .../base/commands/BaseEditTool.ts | 81 +++-- .../architecture/base/commands/BaseTool.ts | 20 +- .../base/commands/DomainObjectCommand.ts | 31 ++ .../base/commands/InstanceCommand.ts | 42 +++ .../commands/ShowAllDomainObjectsCommand.ts | 45 +++ .../commands/ShowDomainObjectsOnTopCommand.ts | 50 +++ .../CopyToClipboardCommand.ts | 12 +- .../DeleteDomainObjectCommand.ts | 18 +- .../NavigationTool.ts | 26 +- .../base/domainObjects/DomainObject.ts | 49 ++- .../base/domainObjects/FolderDomainObject.ts | 5 +- .../base/domainObjects/RootDomainObject.ts | 5 +- .../base/domainObjects/VisualDomainObject.ts | 15 +- .../base/domainObjectsHelpers/BaseCreator.ts | 6 +- .../base/domainObjectsHelpers/Changes.ts | 1 + .../base/domainObjectsHelpers/PanelInfo.ts | 22 +- .../base/domainObjectsHelpers/PopupStyle.ts | 4 +- .../base/domainObjectsHelpers/Quantity.ts | 2 +- .../reactUpdaters/DomainObjectPanelUpdater.ts | 27 +- .../base/renderStyles/CommonRenderStyle.ts | 13 + .../RenderStyle.ts | 0 .../base/renderTarget/BaseRevealConfig.ts | 2 +- .../base/renderTarget/CommandsController.ts | 22 +- .../base/renderTarget/RevealRenderTarget.ts | 85 ++++-- .../base/renderTarget/UnitSystem.ts | 18 +- .../base/utilities/TranslateKey.ts | 4 +- .../base/utilities/colors/colorExtensions.ts | 11 + .../utilities/extensions/mathExtensions.ts | 4 +- .../utilities/extensions/rayExtensions.ts | 12 +- .../utilities/extensions/vectorExtensions.ts | 26 ++ .../base/utilities/geometry/Range1.ts | 4 +- .../base/utilities/geometry/Range3.ts | 77 ++++- .../utilities/geometry/TrianglesBuffers.ts | 25 +- .../geometry/getBoundingBoxFromPlanes.ts | 93 ++++++ .../architecture/base/views/GroupThreeView.ts | 15 +- .../src/architecture/base/views/ThreeView.ts | 2 +- .../concrete/axis/AxisDomainObject.ts | 7 +- .../concrete/axis/AxisRenderStyle.ts | 2 +- .../concrete/axis/AxisThreeView.ts | 2 +- .../boxDomainObject/MeasureDomainObject.ts | 54 ---- .../concrete/boxDomainObject/MeasureType.ts | 92 ------ .../boxDomainObject/SetCropBoxCommand.ts | 66 ---- .../SetMeasurementTypeCommand.ts | 80 ----- .../ShowMeasurementsOnTopCommand.ts | 57 ---- .../concrete/clipping/ClipTool.ts | 95 ++++++ .../concrete/clipping/CropBoxDomainObject.ts | 116 +++++++ .../concrete/clipping/SliceDomainObject.ts | 85 ++++++ .../clipping/commands/ApplyClipCommand.ts | 83 +++++ .../clipping/commands/FlipSliceCommand.ts | 32 ++ .../clipping/commands/SetClipTypeCommand.ts | 143 +++++++++ .../commands/ShowAllClippingCommand.ts | 61 ++++ .../commands/ShowClippingOnTopCommand.ts | 23 ++ .../concrete/config/StoryBookConfig.ts | 6 +- .../architecture/concrete/course/README.md | 2 +- .../ExampleDomainObject.ts | 23 +- .../exampleDomainObject/ExampleRenderStyle.ts | 6 +- .../exampleDomainObject/ExampleTool.ts | 6 +- .../exampleDomainObject/ExampleView.ts | 7 +- .../commands/DeleteAllExamplesCommand.ts | 19 +- .../commands/ResetAllExamplesCommand.ts | 24 +- .../commands/ShowAllExamplesCommand.ts | 44 +-- .../commands/ShowExamplesOnTopCommand.ts | 51 +--- .../concrete/gizmoBox/GizmoBoxDomainObject.ts | 41 +++ .../measurements/MeasureBoxDomainObject.ts | 18 ++ .../measurements/MeasureLineDomainObject.ts | 18 ++ .../concrete/measurements/MeasurementTool.ts | 98 ++++++ .../commands/SetMeasurementTypeCommand.ts | 124 ++++++++ .../commands/ShowMeasurementsOnTopCommand.ts | 30 ++ .../measurements/getIconByPrimitiveType.ts | 32 ++ .../PrimitiveEditTool.ts} | 263 +++++++--------- .../PrimitiveRenderStyle.ts} | 17 +- .../concrete/primitives/PrimitiveType.ts | 17 ++ .../box/BoxCreator.ts} | 65 ++-- .../box/BoxDomainObject.ts} | 209 +++++++------ .../box/BoxDragger.ts} | 104 +++++-- .../box/BoxRenderStyle.ts} | 11 +- .../box/BoxView.ts} | 149 ++++----- .../line/LineCreator.ts} | 35 ++- .../line/LineDomainObject.ts} | 92 ++++-- .../line/LineRenderStyle.ts} | 8 +- .../line/LineView.ts} | 89 +++--- .../concrete/primitives/plane/PlaneCreator.ts | 118 ++++++++ .../primitives/plane/PlaneDomainObject.ts | 228 ++++++++++++++ .../concrete/primitives/plane/PlaneDragger.ts | 76 +++++ .../primitives/plane/PlaneRenderStyle.ts | 30 ++ .../concrete/primitives/plane/PlaneView.ts | 284 ++++++++++++++++++ .../SetTerrainVisibleCommand.ts | 10 +- .../TerrainDomainObject.ts | 9 +- .../terrainDomainObject/TerrainRenderStyle.ts | 2 +- .../UpdateTerrainCommand.ts | 12 +- .../components/Architecture/CommandButton.tsx | 15 +- .../Architecture/DomainObjectPanel.tsx | 43 +-- .../components/Architecture/RevealButtons.tsx | 6 +- .../src/components/Architecture/Toolbar.tsx | 4 +- 95 files changed, 3106 insertions(+), 1231 deletions(-) create mode 100644 react-components/src/architecture/base/commands/DomainObjectCommand.ts create mode 100644 react-components/src/architecture/base/commands/InstanceCommand.ts create mode 100644 react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts create mode 100644 react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts rename react-components/src/architecture/base/{commands => concreteCommands}/NavigationTool.ts (65%) create mode 100644 react-components/src/architecture/base/renderStyles/CommonRenderStyle.ts rename react-components/src/architecture/base/{domainObjectsHelpers => renderStyles}/RenderStyle.ts (100%) create mode 100644 react-components/src/architecture/base/utilities/geometry/getBoundingBoxFromPlanes.ts delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/MeasureDomainObject.ts delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/SetMeasurementTypeCommand.ts delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/ShowMeasurementsOnTopCommand.ts create mode 100644 react-components/src/architecture/concrete/clipping/ClipTool.ts create mode 100644 react-components/src/architecture/concrete/clipping/CropBoxDomainObject.ts create mode 100644 react-components/src/architecture/concrete/clipping/SliceDomainObject.ts create mode 100644 react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts create mode 100644 react-components/src/architecture/concrete/clipping/commands/FlipSliceCommand.ts create mode 100644 react-components/src/architecture/concrete/clipping/commands/SetClipTypeCommand.ts create mode 100644 react-components/src/architecture/concrete/clipping/commands/ShowAllClippingCommand.ts create mode 100644 react-components/src/architecture/concrete/clipping/commands/ShowClippingOnTopCommand.ts create mode 100644 react-components/src/architecture/concrete/gizmoBox/GizmoBoxDomainObject.ts create mode 100644 react-components/src/architecture/concrete/measurements/MeasureBoxDomainObject.ts create mode 100644 react-components/src/architecture/concrete/measurements/MeasureLineDomainObject.ts create mode 100644 react-components/src/architecture/concrete/measurements/MeasurementTool.ts create mode 100644 react-components/src/architecture/concrete/measurements/commands/SetMeasurementTypeCommand.ts create mode 100644 react-components/src/architecture/concrete/measurements/commands/ShowMeasurementsOnTopCommand.ts create mode 100644 react-components/src/architecture/concrete/measurements/getIconByPrimitiveType.ts rename react-components/src/architecture/concrete/{boxDomainObject/MeasurementTool.ts => primitives/PrimitiveEditTool.ts} (54%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureRenderStyle.ts => primitives/PrimitiveRenderStyle.ts} (52%) create mode 100644 react-components/src/architecture/concrete/primitives/PrimitiveType.ts rename react-components/src/architecture/concrete/{boxDomainObject/MeasureBoxCreator.ts => primitives/box/BoxCreator.ts} (69%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureBoxDomainObject.ts => primitives/box/BoxDomainObject.ts} (51%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureBoxDragger.ts => primitives/box/BoxDragger.ts} (67%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureBoxRenderStyle.ts => primitives/box/BoxRenderStyle.ts} (60%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureBoxView.ts => primitives/box/BoxView.ts} (83%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureLineCreator.ts => primitives/line/LineCreator.ts} (68%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureLineDomainObject.ts => primitives/line/LineDomainObject.ts} (62%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureLineRenderStyle.ts => primitives/line/LineRenderStyle.ts} (68%) rename react-components/src/architecture/concrete/{boxDomainObject/MeasureLineView.ts => primitives/line/LineView.ts} (78%) create mode 100644 react-components/src/architecture/concrete/primitives/plane/PlaneCreator.ts create mode 100644 react-components/src/architecture/concrete/primitives/plane/PlaneDomainObject.ts create mode 100644 react-components/src/architecture/concrete/primitives/plane/PlaneDragger.ts create mode 100644 react-components/src/architecture/concrete/primitives/plane/PlaneRenderStyle.ts create mode 100644 react-components/src/architecture/concrete/primitives/plane/PlaneView.ts diff --git a/react-components/src/architecture/base/commands/BaseCommand.ts b/react-components/src/architecture/base/commands/BaseCommand.ts index d3fe91c0ff7..f78a685e8f6 100644 --- a/react-components/src/architecture/base/commands/BaseCommand.ts +++ b/react-components/src/architecture/base/commands/BaseCommand.ts @@ -22,23 +22,27 @@ export abstract class BaseCommand { private readonly _listeners: UpdateDelegate[] = []; - // Unique index for the command, used by in React to force rerender + // Unique id for the command, used by in React to force rerender // when the command changes for a button. - public readonly _uniqueIndex: number; + private readonly _uniqueId: number; - public get uniqueIndex(): number { - return this._uniqueIndex; + public get uniqueId(): number { + return this._uniqueId; } // ================================================== - // VIRTUAL METHODS (To be override) - // ================================================= + // CONSTRUCTOR + // ================================================== constructor() { BaseCommand._counter++; - this._uniqueIndex = BaseCommand._counter; + this._uniqueId = BaseCommand._counter; } + // ================================================== + // VIRTUAL METHODS (To be override) + // ================================================= + public get name(): string { return this.tooltip.fallback ?? this.tooltip.key; } @@ -48,7 +52,7 @@ export abstract class BaseCommand { } public get tooltip(): TranslateKey { - return { key: '' }; + return { fallback: '' }; } public get icon(): string { diff --git a/react-components/src/architecture/base/commands/BaseEditTool.ts b/react-components/src/architecture/base/commands/BaseEditTool.ts index 6094b0d11dd..b7f517bb5d8 100644 --- a/react-components/src/architecture/base/commands/BaseEditTool.ts +++ b/react-components/src/architecture/base/commands/BaseEditTool.ts @@ -2,11 +2,12 @@ * Copyright 2024 Cognite AS */ -import { NavigationTool } from './NavigationTool'; +import { NavigationTool } from '../concreteCommands/NavigationTool'; import { isDomainObjectIntersection } from '../domainObjectsHelpers/DomainObjectIntersection'; import { type BaseDragger } from '../domainObjectsHelpers/BaseDragger'; import { VisualDomainObject } from '../domainObjects/VisualDomainObject'; import { type AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; +import { DomainObjectPanelUpdater } from '../reactUpdaters/DomainObjectPanelUpdater'; /** * The `BaseEditTool` class is an abstract class that extends the `NavigationTool` class. @@ -35,10 +36,10 @@ export abstract class BaseEditTool extends NavigationTool { this._dragger = undefined; } - public override async onPointerDown(event: PointerEvent, leftButton: boolean): Promise { + public override async onLeftPointerDown(event: PointerEvent): Promise { this._dragger = await this.createDragger(event); if (this._dragger === undefined) { - await super.onPointerDown(event, leftButton); + await super.onLeftPointerDown(event); return; } this._dragger.onPointerDown(event); @@ -46,27 +47,33 @@ export abstract class BaseEditTool extends NavigationTool { this._dragger.domainObject.setSelectedInteractive(true); } - public override async onPointerDrag(event: PointerEvent, leftButton: boolean): Promise { + public override async onLeftPointerDrag(event: PointerEvent): Promise { if (this._dragger === undefined) { - await super.onPointerDrag(event, leftButton); + await super.onLeftPointerDrag(event); return; } const ray = this.getRay(event, true); this._dragger.onPointerDrag(event, ray); } - public override async onPointerUp(event: PointerEvent, leftButton: boolean): Promise { + public override async onLeftPointerUp(event: PointerEvent): Promise { if (this._dragger === undefined) { - await super.onPointerUp(event, leftButton); + await super.onLeftPointerUp(event); } else { this._dragger.onPointerUp(event); this._dragger = undefined; } } + public override onActivate(): void { + super.onActivate(); + const selected = this.getSelected(); + DomainObjectPanelUpdater.show(selected); + } + public override onDeactivate(): void { super.onDeactivate(); - this.deselectAll(); + DomainObjectPanelUpdater.hide(); } // ================================================== @@ -111,22 +118,29 @@ export abstract class BaseEditTool extends NavigationTool { // INSTANCE METHODS // ================================================== - /** - * Deselects all visual domain objects except for the specified object. - * If no object is specified, all visual domain objects will be deselected. - * @param except - The visual domain object to exclude from deselection. - */ - protected deselectAll(except?: VisualDomainObject | undefined): void { + protected *getSelectable(): Generator { const { rootDomainObject } = this; for (const domainObject of rootDomainObject.getDescendantsByType(VisualDomainObject)) { if (!this.canBeSelected(domainObject)) { continue; } - if (except !== undefined && domainObject === except) { + yield domainObject; + } + } + + /** + * Retrieves the selected VisualDomainObject. + * Use only if single selection is expected. + * @returns The selected DomainObject, or undefined if no object is selected. + */ + protected getSelected(): VisualDomainObject | undefined { + for (const domainObject of this.getSelectable()) { + if (!domainObject.isSelected) { continue; } - domainObject.setSelectedInteractive(false); + return domainObject; } + return undefined; } /** @@ -135,35 +149,36 @@ export abstract class BaseEditTool extends NavigationTool { * @returns A generator that yields each selected domain object. */ protected *getAllSelected(): Generator { - const { rootDomainObject } = this; - for (const domainObject of rootDomainObject.getDescendantsByType(VisualDomainObject)) { + for (const domainObject of this.getSelectable()) { if (!domainObject.isSelected) { continue; } - if (!this.canBeSelected(domainObject)) { - continue; - } yield domainObject; } } /** - * Retrieves the selected VisualDomainObject. - * Use only if single selection is expected. - * @returns The selected DomainObject, or undefined if no object is selected. + * Deselects all selectable objects except for the specified object. + * If no object is specified, all visual domain objects will be deselected. + * @param except - The visual domain object to exclude from deselection. */ - protected getSelected(): VisualDomainObject | undefined { - const { rootDomainObject } = this; - for (const domainObject of rootDomainObject.getDescendantsByType(VisualDomainObject)) { - if (!domainObject.isSelected) { - continue; - } - if (!this.canBeSelected(domainObject)) { + protected deselectAll(except?: VisualDomainObject | undefined): void { + for (const domainObject of this.getSelectable()) { + if (except !== undefined && domainObject === except) { continue; } - return domainObject; + domainObject.setSelectedInteractive(false); + } + } + + /** + * Sets the visibility of all selectable objects. + * @param visible - A boolean indicating whether the objects should be visible or not. + */ + protected setAllVisible(visible: boolean): void { + for (const domainObject of this.getSelectable()) { + domainObject.setVisibleInteractive(visible, this.renderTarget); } - return undefined; } /** diff --git a/react-components/src/architecture/base/commands/BaseTool.ts b/react-components/src/architecture/base/commands/BaseTool.ts index 5fcfc9bdd19..5f7b5f3099f 100644 --- a/react-components/src/architecture/base/commands/BaseTool.ts +++ b/react-components/src/architecture/base/commands/BaseTool.ts @@ -57,7 +57,7 @@ export abstract class BaseTool extends RenderTargetCommand { } public getToolbarStyle(): PopupStyle { - // Override this to place the the toolbar + // Override this to place the toolbar // Default lower left corner return new PopupStyle({ bottom: 0, left: 0 }); } @@ -89,15 +89,27 @@ export abstract class BaseTool extends RenderTargetCommand { await Promise.resolve(); } - public async onPointerDown(_event: PointerEvent, _leftButton: boolean): Promise { + public async onLeftPointerDown(_event: PointerEvent): Promise { await Promise.resolve(); } - public async onPointerDrag(_event: PointerEvent, _leftButton: boolean): Promise { + public async onLeftPointerDrag(_event: PointerEvent): Promise { await Promise.resolve(); } - public async onPointerUp(_event: PointerEvent, _leftButton: boolean): Promise { + public async onLeftPointerUp(_event: PointerEvent): Promise { + await Promise.resolve(); + } + + public async onRightPointerDown(_event: PointerEvent): Promise { + await Promise.resolve(); + } + + public async onRightPointerDrag(_event: PointerEvent): Promise { + await Promise.resolve(); + } + + public async onRightPointerUp(_event: PointerEvent): Promise { await Promise.resolve(); } diff --git a/react-components/src/architecture/base/commands/DomainObjectCommand.ts b/react-components/src/architecture/base/commands/DomainObjectCommand.ts new file mode 100644 index 00000000000..ce684c5d9ff --- /dev/null +++ b/react-components/src/architecture/base/commands/DomainObjectCommand.ts @@ -0,0 +1,31 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type DomainObject } from '../domainObjects/DomainObject'; +import { BaseCommand } from './BaseCommand'; + +export abstract class DomainObjectCommand extends BaseCommand { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + protected readonly _domainObject: Type; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(domainObject: Type) { + super(); + this._domainObject = domainObject; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get hasData(): boolean { + return true; + } +} diff --git a/react-components/src/architecture/base/commands/InstanceCommand.ts b/react-components/src/architecture/base/commands/InstanceCommand.ts new file mode 100644 index 00000000000..8fcb73c98fd --- /dev/null +++ b/react-components/src/architecture/base/commands/InstanceCommand.ts @@ -0,0 +1,42 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { RenderTargetCommand } from './RenderTargetCommand'; +import { type DomainObject } from '../domainObjects/DomainObject'; + +export abstract class InstanceCommand extends RenderTargetCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get isEnabled(): boolean { + return this.anyInstances; + } + + // ================================================== + // VIRTUAL METHODS + // ================================================== + + protected abstract isInstance(domainObject: DomainObject): boolean; + + // ================================================== + // INSTANCE METHODS + // ================================================== + + protected get anyInstances(): boolean { + return this.getFirstInstance() !== undefined; + } + + protected getFirstInstance(): DomainObject | undefined { + return this.getInstances().next().value; + } + + protected *getInstances(): Generator { + for (const domainObject of this.rootDomainObject.getDescendants()) { + if (this.isInstance(domainObject)) { + yield domainObject; + } + } + } +} diff --git a/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts b/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts new file mode 100644 index 00000000000..7bf9f076068 --- /dev/null +++ b/react-components/src/architecture/base/commands/ShowAllDomainObjectsCommand.ts @@ -0,0 +1,45 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type TranslateKey } from '../utilities/TranslateKey'; +import { InstanceCommand } from './InstanceCommand'; + +export abstract class ShowAllDomainObjectsCommand extends InstanceCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { fallback: 'Show or hide' }; + } + + public override get icon(): string { + return 'EyeShow'; + } + + public override get isChecked(): boolean { + return this.isAnyVisible(); + } + + protected override invokeCore(): boolean { + const isVisible = this.isAnyVisible(); + for (const domainObject of this.getInstances()) { + domainObject.setVisibleInteractive(!isVisible, this.renderTarget); + } + return true; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private isAnyVisible(): boolean { + for (const domainObject of this.getInstances()) { + if (domainObject.isVisible(this.renderTarget)) { + return true; + } + } + return false; + } +} diff --git a/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts b/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts new file mode 100644 index 00000000000..9b9c6968003 --- /dev/null +++ b/react-components/src/architecture/base/commands/ShowDomainObjectsOnTopCommand.ts @@ -0,0 +1,50 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Changes } from '../domainObjectsHelpers/Changes'; +import { CommonRenderStyle } from '../renderStyles/CommonRenderStyle'; +import { InstanceCommand } from './InstanceCommand'; + +export abstract class ShowDomainObjectsOnTopCommand extends InstanceCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get icon(): string { + return 'Flag'; + } + + public override get isChecked(): boolean { + return !this.getDepthTest(); + } + + protected override invokeCore(): boolean { + const depthTest = this.getDepthTest(); + for (const domainObject of this.getInstances()) { + const renderStyle = domainObject.getRenderStyle(); + if (!(renderStyle instanceof CommonRenderStyle)) { + continue; + } + renderStyle.depthTest = !depthTest; + domainObject.notify(Changes.renderStyle); + } + return true; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private getDepthTest(): boolean { + const domainObject = this.getFirstInstance(); + if (domainObject === undefined) { + return false; + } + const renderStyle = domainObject.getRenderStyle(); + if (renderStyle instanceof CommonRenderStyle) { + return renderStyle.depthTest; + } + return false; + } +} diff --git a/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts b/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts index 8ba7a9610e2..331e118d84d 100644 --- a/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/CopyToClipboardCommand.ts @@ -8,15 +8,15 @@ import { type TranslateKey } from '../utilities/TranslateKey'; type GetStringDelegate = () => string; export class CopyToClipboardCommand extends BaseCommand { - private readonly _getString: GetStringDelegate; + public getString?: GetStringDelegate; // ================================================== // CONSTRUCTOR // ================================================== - public constructor(getString: GetStringDelegate) { + public constructor(getString?: GetStringDelegate) { super(); - this._getString = getString; + this.getString = getString; } // ================================================== @@ -32,7 +32,7 @@ export class CopyToClipboardCommand extends BaseCommand { } public override get isEnabled(): boolean { - return this._getString !== undefined; + return this.getString !== undefined; } public override get hasData(): boolean { @@ -40,11 +40,11 @@ export class CopyToClipboardCommand extends BaseCommand { } protected override invokeCore(): boolean { - if (this._getString === undefined) { + if (this.getString === undefined) { return false; } navigator.clipboard - .writeText(this._getString()) + .writeText(this.getString()) .then((_result) => { return true; }) diff --git a/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts b/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts index 730c5e194f2..826fb8aaed5 100644 --- a/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/DeleteDomainObjectCommand.ts @@ -2,16 +2,11 @@ * Copyright 2024 Cognite AS */ -import { BaseCommand } from '../commands/BaseCommand'; import { type TranslateKey } from '../utilities/TranslateKey'; import { type DomainObject } from '../domainObjects/DomainObject'; +import { DomainObjectCommand } from '../commands/DomainObjectCommand'; -export class DeleteDomainObjectCommand extends BaseCommand { - private readonly _domainObject: DomainObject | undefined = undefined; - public constructor(domainObject: DomainObject) { - super(); - this._domainObject = domainObject; - } +export class DeleteDomainObjectCommand extends DomainObjectCommand { // ================================================== // OVERRIDES // ================================================== @@ -29,17 +24,10 @@ export class DeleteDomainObjectCommand extends BaseCommand { } public override get isEnabled(): boolean { - return this._domainObject !== undefined && this._domainObject.canBeRemoved; - } - - public override get hasData(): boolean { - return true; + return this._domainObject.canBeRemoved; } protected override invokeCore(): boolean { - if (this._domainObject === undefined) { - return false; - } return this._domainObject.removeInteractive(); } } diff --git a/react-components/src/architecture/base/commands/NavigationTool.ts b/react-components/src/architecture/base/concreteCommands/NavigationTool.ts similarity index 65% rename from react-components/src/architecture/base/commands/NavigationTool.ts rename to react-components/src/architecture/base/concreteCommands/NavigationTool.ts index 8f11495686f..0400bcdb817 100644 --- a/react-components/src/architecture/base/commands/NavigationTool.ts +++ b/react-components/src/architecture/base/concreteCommands/NavigationTool.ts @@ -2,7 +2,7 @@ * Copyright 2024 Cognite AS */ -import { BaseTool } from './BaseTool'; +import { BaseTool } from '../commands/BaseTool'; import { type IFlexibleCameraManager } from '@cognite/reveal'; import { type TranslateKey } from '../utilities/TranslateKey'; @@ -43,16 +43,28 @@ export class NavigationTool extends BaseTool { await this.cameraManager.onDoubleClick(event); } - public override async onPointerDown(event: PointerEvent, leftButton: boolean): Promise { - await this.cameraManager.onPointerDown(event, leftButton); + public override async onLeftPointerDown(event: PointerEvent): Promise { + await this.cameraManager.onPointerDown(event, true); } - public override async onPointerDrag(event: PointerEvent, leftButton: boolean): Promise { - await this.cameraManager.onPointerDrag(event, leftButton); + public override async onLeftPointerDrag(event: PointerEvent): Promise { + await this.cameraManager.onPointerDrag(event, true); } - public override async onPointerUp(event: PointerEvent, leftButton: boolean): Promise { - await this.cameraManager.onPointerUp(event, leftButton); + public override async onLeftPointerUp(event: PointerEvent): Promise { + await this.cameraManager.onPointerUp(event, true); + } + + public override async onRightPointerDown(event: PointerEvent): Promise { + await this.cameraManager.onPointerDown(event, false); + } + + public override async onRightPointerDrag(event: PointerEvent): Promise { + await this.cameraManager.onPointerDrag(event, false); + } + + public override async onRightPointerUp(event: PointerEvent): Promise { + await this.cameraManager.onPointerUp(event, false); } public override async onWheel(event: WheelEvent, delta: number): Promise { diff --git a/react-components/src/architecture/base/domainObjects/DomainObject.ts b/react-components/src/architecture/base/domainObjects/DomainObject.ts index 65749b0ca7e..4b93b116463 100644 --- a/react-components/src/architecture/base/domainObjects/DomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/DomainObject.ts @@ -4,7 +4,7 @@ import { type Color } from 'three'; import { BLACK_COLOR, WHITE_COLOR } from '../utilities/colors/colorExtensions'; -import { type RenderStyle } from '../domainObjectsHelpers/RenderStyle'; +import { type RenderStyle } from '../renderStyles/RenderStyle'; import { DomainObjectChange } from '../domainObjectsHelpers/DomainObjectChange'; import { Changes } from '../domainObjectsHelpers/Changes'; import { isInstanceOf, type Class } from '../domainObjectsHelpers/Class'; @@ -20,6 +20,10 @@ import { PopupStyle } from '../domainObjectsHelpers/PopupStyle'; import { RootDomainObject } from './RootDomainObject'; import { CommandsUpdater } from '../reactUpdaters/CommandsUpdater'; import { DomainObjectPanelUpdater } from '../reactUpdaters/DomainObjectPanelUpdater'; +import { type TranslateKey } from '../utilities/TranslateKey'; +import { DeleteDomainObjectCommand } from '../concreteCommands/DeleteDomainObjectCommand'; +import { CopyToClipboardCommand } from '../concreteCommands/CopyToClipboardCommand'; +import { type BaseCommand } from '../commands/BaseCommand'; /** * Represents an abstract base class for domain objects. @@ -55,11 +59,28 @@ export abstract class DomainObject { // Views and listeners public readonly views: Views = new Views(); + // Unique index for the domain object, used as soft reference + private readonly _uniqueId: number; + private static _counter: number = 0; // Counter for the unique index + + public get uniqueId(): number { + return this._uniqueId; + } + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor() { + DomainObject._counter++; + this._uniqueId = DomainObject._counter; + } + // ================================================== // INSTANCE/VIRTUAL PROPERTIES // ================================================== - public abstract get typeName(): string; // to be overridden + public abstract get typeName(): TranslateKey; // to be overridden public get path(): string { return `${this.parent !== undefined ? this.parent.path : ''}\\${this.name}`; @@ -291,7 +312,11 @@ export abstract class DomainObject { public getPanelInfoStyle(): PopupStyle { // to be overridden // Default lower left corner - return new PopupStyle({ bottom: 0, left: 0 }); + return new PopupStyle({ bottom: 50, left: 0 }); + } + + public getPanelToolbar(): BaseCommand[] { + return [new DeleteDomainObjectCommand(this), new CopyToClipboardCommand()]; } // ================================================== @@ -462,6 +487,16 @@ export abstract class DomainObject { } } + /** + * Checks if the domain object is visible in the specified render target. + * @param renderTarget - The render target to check visibility in. + * @returns `true` if the domain object is visible in the target, `false` otherwise. + */ + public isVisible(renderTarget: RevealRenderTarget): boolean { + const visibleState = this.getVisibleState(renderTarget); + return visibleState === VisibleState.Some || visibleState === VisibleState.All; + } + // ================================================== // INSTANCE PROPERTIES: Child-Parent relationship // ================================================== @@ -687,10 +722,10 @@ export abstract class DomainObject { public addChild(child: DomainObject, insertFirst = false): void { if (child.hasParent) { - throw Error(`The child ${child.typeName} already has a parent`); + throw Error(`The child ${child.typeName.fallback} already has a parent`); } if (child === this) { - throw Error(`Trying to add illegal child ${child.typeName}`); + throw Error(`Trying to add illegal child ${child.typeName.fallback}`); } if (insertFirst) { this._children.unshift(child); @@ -709,7 +744,7 @@ export abstract class DomainObject { private remove(): boolean { const { childIndex } = this; if (childIndex === undefined) { - throw Error(`The child ${this.typeName} is not child of it's parent`); + throw Error(`The child ${this.typeName.fallback} is not child of it's parent`); } clear(this._children); this.removeCore(); @@ -773,7 +808,7 @@ export abstract class DomainObject { } private generateNewName(): string { - let result = this.typeName; + let result = this.typeName.fallback; if (!this.canChangeName) { return result; } diff --git a/react-components/src/architecture/base/domainObjects/FolderDomainObject.ts b/react-components/src/architecture/base/domainObjects/FolderDomainObject.ts index 94ebce8c2f3..02dc5b121e4 100644 --- a/react-components/src/architecture/base/domainObjects/FolderDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/FolderDomainObject.ts @@ -2,6 +2,7 @@ * Copyright 2024 Cognite AS */ +import { type TranslateKey } from '../utilities/TranslateKey'; import { DomainObject } from './DomainObject'; export class FolderDomainObject extends DomainObject { @@ -9,7 +10,7 @@ export class FolderDomainObject extends DomainObject { // OVERRIDES of DomainObject // ================================================== - public override get typeName(): string { - return 'Folder'; + public override get typeName(): TranslateKey { + return { fallback: 'Folder' }; } } diff --git a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts index 70e336bd2af..9154c827354 100644 --- a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts @@ -5,6 +5,7 @@ import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { UnitSystem } from '../renderTarget/UnitSystem'; import { DomainObject } from './DomainObject'; +import { type TranslateKey } from '../utilities/TranslateKey'; export class RootDomainObject extends DomainObject { // ================================================== @@ -36,7 +37,7 @@ export class RootDomainObject extends DomainObject { // OVERRIDES of DomainObject // ================================================== - public override get typeName(): string { - return 'Root'; + public override get typeName(): TranslateKey { + return { fallback: 'Root' }; } } diff --git a/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts b/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts index 545a6f627eb..5cd1ee27d6c 100644 --- a/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts @@ -21,7 +21,7 @@ export abstract class VisualDomainObject extends DomainObject { // ================================================== public override getVisibleState(renderTarget: RevealRenderTarget): VisibleState { - if (this.isVisible(renderTarget)) { + if (this.getViewByTarget(renderTarget) !== undefined) { return VisibleState.All; } if (this.canCreateThreeView()) { @@ -79,6 +79,10 @@ export abstract class VisualDomainObject extends DomainObject { return undefined; } + public get useClippingInIntersection(): boolean { + return true; + } + // ================================================== // INSTANCE METHODS // ================================================== @@ -91,15 +95,6 @@ export abstract class VisualDomainObject extends DomainObject { } } - /** - * Checks if the visual domain object is visible in the specified render target. - * @param renderTarget - The render target to check visibility in. - * @returns `true` if the visual domain object is visible in the target, `false` otherwise. - */ - public isVisible(renderTarget: RevealRenderTarget): boolean { - return this.getViewByTarget(renderTarget) !== undefined; - } - /** * Sets the visibility of the visual domain object for a specific target. * @param visible - A boolean indicating whether the visual domain object should be visible or not. diff --git a/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts b/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts index fb0d5e66c4a..af70b8cd8fd 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts @@ -97,7 +97,11 @@ export abstract class BaseCreator { * @returns {boolean} Returns true if the pending object is created successfully, false if it is removed */ public handleEscape(): boolean { - return false; + if (this.notPendingPointCount >= this.minimumPointCount) { + return true; // Successfully + } + this.domainObject.removeInteractive(); + return false; // Removed } // ================================================== diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts index bf42d4d713b..5347f8a2256 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts @@ -9,6 +9,7 @@ export class Changes { public static readonly expanded: symbol = Symbol('expanded'); public static readonly selected: symbol = Symbol('selected'); public static readonly focus: symbol = Symbol('focus'); + public static readonly clipping: symbol = Symbol('visibleState'); // Domain object Fields changed public static readonly naming: symbol = Symbol('naming'); diff --git a/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts b/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts index 11e3c7b4dfe..175c0333beb 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/PanelInfo.ts @@ -2,12 +2,12 @@ * Copyright 2024 Cognite AS */ -import { type TranslateKey } from '../utilities/TranslateKey'; +import { type TranslateDelegate, type TranslateKey } from '../utilities/TranslateKey'; import { Quantity } from './Quantity'; type PanelItemProps = { - key: string; - fallback?: string; + key?: string; + fallback: string; icon?: string; value?: number; quantity?: Quantity; @@ -17,7 +17,8 @@ export class PanelInfo { public header?: PanelItem; public readonly items: NumberPanelItem[] = []; - public setHeader(key: string, fallback: string): void { + public setHeader(translateKey: TranslateKey): void { + const { key, fallback } = translateKey; this.header = new PanelItem({ key, fallback }); } @@ -29,12 +30,23 @@ export class PanelInfo { export class PanelItem { public key?: string; - public fallback?: string; + public fallback: string; constructor(props: TranslateKey) { this.key = props.key; this.fallback = props.fallback; } + + public getText(translate: TranslateDelegate): string | undefined { + const { key, fallback } = this; + if (key !== undefined) { + return translate(key, fallback); + } + if (fallback.length === 0) { + return undefined; + } + return fallback; + } } export class NumberPanelItem extends PanelItem { diff --git a/react-components/src/architecture/base/domainObjectsHelpers/PopupStyle.ts b/react-components/src/architecture/base/domainObjectsHelpers/PopupStyle.ts index 369eb5303cb..e14fe7c41cc 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/PopupStyle.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/PopupStyle.ts @@ -17,8 +17,8 @@ export class PopupStyle { private readonly _right?: number = undefined; private readonly _top?: number = undefined; private readonly _bottom?: number = undefined; - private readonly _margin: number = 16; // margin outside the popup - private readonly _padding: number = 8; // margin inside the popup + private readonly _margin: number = 8; // margin outside the popup + private readonly _padding: number = 6; // margin inside the popup private readonly _horizontal: boolean = true; // Used for toolbars only public constructor(props: PopupProps) { diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts b/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts index f0a90367961..b4d6b3e241b 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/Quantity.ts @@ -7,5 +7,5 @@ export enum Quantity { Length, Area, Volume, - Degrees + Angle } diff --git a/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts b/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts index 46138a22ec7..972d67104b5 100644 --- a/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts +++ b/react-components/src/architecture/base/reactUpdaters/DomainObjectPanelUpdater.ts @@ -20,14 +20,21 @@ export class DomainObjectPanelUpdater { // STATIC METHODS // ================================================== - public static get isActive(): boolean { - return this._setDomainObject !== undefined; - } - public static setDomainObjectDelegate(value: SetDomainObjectInfoDelegate | undefined): void { this._setDomainObject = value; } + public static show(domainObject: DomainObject | undefined): void { + if (this._setDomainObject === undefined) { + return; + } + if (domainObject !== undefined) { + this._setDomainObject({ domainObject }); + } else { + this.hide(); + } + } + public static hide(): void { if (this._setDomainObject === undefined) { return; @@ -36,7 +43,7 @@ export class DomainObjectPanelUpdater { } public static notify(domainObject: DomainObject, change: DomainObjectChange): void { - if (!this.isActive) { + if (this._setDomainObject === undefined) { return; } if (domainObject.isSelected) { @@ -44,7 +51,7 @@ export class DomainObjectPanelUpdater { this.hide(); } if (change.isChanged(Changes.selected, Changes.geometry, Changes.naming, Changes.unit)) { - this.update(domainObject); + this.show(domainObject); } } else { if (change.isChanged(Changes.selected)) { @@ -52,12 +59,4 @@ export class DomainObjectPanelUpdater { } } } - - private static update(domainObject: DomainObject): void { - if (this._setDomainObject === undefined) { - return; - } - const info = { domainObject }; - this._setDomainObject(info); - } } diff --git a/react-components/src/architecture/base/renderStyles/CommonRenderStyle.ts b/react-components/src/architecture/base/renderStyles/CommonRenderStyle.ts new file mode 100644 index 00000000000..176c67adea8 --- /dev/null +++ b/react-components/src/architecture/base/renderStyles/CommonRenderStyle.ts @@ -0,0 +1,13 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { RenderStyle } from './RenderStyle'; + +export abstract class CommonRenderStyle extends RenderStyle { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + public depthTest = true; +} diff --git a/react-components/src/architecture/base/domainObjectsHelpers/RenderStyle.ts b/react-components/src/architecture/base/renderStyles/RenderStyle.ts similarity index 100% rename from react-components/src/architecture/base/domainObjectsHelpers/RenderStyle.ts rename to react-components/src/architecture/base/renderStyles/RenderStyle.ts diff --git a/react-components/src/architecture/base/renderTarget/BaseRevealConfig.ts b/react-components/src/architecture/base/renderTarget/BaseRevealConfig.ts index 18c9a7c1ec3..4868f3b9937 100644 --- a/react-components/src/architecture/base/renderTarget/BaseRevealConfig.ts +++ b/react-components/src/architecture/base/renderTarget/BaseRevealConfig.ts @@ -5,7 +5,7 @@ import { type AxisGizmoTool } from '@cognite/reveal/tools'; import { type BaseCommand } from '../commands/BaseCommand'; import { PopupStyle } from '../domainObjectsHelpers/PopupStyle'; -import { NavigationTool } from '../commands/NavigationTool'; +import { NavigationTool } from '../concreteCommands/NavigationTool'; import { type BaseTool } from '../commands/BaseTool'; import { type RevealRenderTarget } from './RevealRenderTarget'; diff --git a/react-components/src/architecture/base/renderTarget/CommandsController.ts b/react-components/src/architecture/base/renderTarget/CommandsController.ts index 18fee984d95..a180e666514 100644 --- a/react-components/src/architecture/base/renderTarget/CommandsController.ts +++ b/react-components/src/architecture/base/renderTarget/CommandsController.ts @@ -69,15 +69,27 @@ export class CommandsController extends PointerEvents { public override async onPointerDown(event: PointerEvent, leftButton: boolean): Promise { this._domElement.focus(); - await this.activeTool?.onPointerDown(event, leftButton); + if (leftButton) { + await this.activeTool?.onLeftPointerDown(event); + } else { + await this.defaultTool?.onRightPointerDown(event); + } } - public override async onPointerUp(event: PointerEvent, leftButton: boolean): Promise { - await this.activeTool?.onPointerUp(event, leftButton); + public override async onPointerDrag(event: PointerEvent, leftButton: boolean): Promise { + if (leftButton) { + await this.activeTool?.onLeftPointerDrag(event); + } else { + await this.defaultTool?.onRightPointerDrag(event); + } } - public override async onPointerDrag(event: PointerEvent, leftButton: boolean): Promise { - await this.activeTool?.onPointerDrag(event, leftButton); + public override async onPointerUp(event: PointerEvent, leftButton: boolean): Promise { + if (leftButton) { + await this.activeTool?.onLeftPointerUp(event); + } else { + await this.defaultTool?.onRightPointerUp(event); + } } // ================================================== diff --git a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts index f1ad5caf8fe..eafa130b3f6 100644 --- a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts +++ b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts @@ -7,7 +7,8 @@ import { CustomObject, isFlexibleCameraManager, type Cognite3DViewer, - type IFlexibleCameraManager + type IFlexibleCameraManager, + CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; import { Vector3, @@ -26,6 +27,9 @@ import { type AxisGizmoTool } from '@cognite/reveal/tools'; import { type BaseRevealConfig } from './BaseRevealConfig'; import { DefaultRevealConfig } from './DefaultRevealConfig'; import { CommandsUpdater } from '../reactUpdaters/CommandsUpdater'; +import { Range3 } from '../utilities/geometry/Range3'; +import { getBoundingBoxFromPlanes } from '../utilities/geometry/getBoundingBoxFromPlanes'; +import { Changes } from '../domainObjectsHelpers/Changes'; const DIRECTIONAL_LIGHT_NAME = 'DirectionalLight'; @@ -39,8 +43,8 @@ export class RevealRenderTarget { private readonly _rootDomainObject: RootDomainObject; private _ambientLight: AmbientLight | undefined; private _directionalLight: DirectionalLight | undefined; - private _cropBoxBoundingBox: Box3 | undefined; - private _cropBoxName: string | undefined = undefined; + private _clippedBoundingBox: Box3 | undefined; + private _cropBoxUniqueId: number | undefined = undefined; private _axisGizmoTool: AxisGizmoTool | undefined; private _config: BaseRevealConfig | undefined = undefined; @@ -117,14 +121,19 @@ export class RevealRenderTarget { return this.cameraManager.getCamera(); } - public get sceneBoundingBox(): Box3 { - const boundingBox = this.viewer.getSceneBoundingBox(); - if (this._cropBoxBoundingBox !== undefined) { - boundingBox.intersect(this._cropBoxBoundingBox); + public get clippedSceneBoundingBox(): Box3 { + if (this._clippedBoundingBox === undefined) { + return this.sceneBoundingBox; } + const boundingBox = this.sceneBoundingBox.clone(); + boundingBox.intersect(this._clippedBoundingBox); return boundingBox; } + public get sceneBoundingBox(): Box3 { + return this.viewer.getSceneBoundingBox(); + } + // ================================================== // INSTANCE METHODS // ================================================== @@ -205,43 +214,63 @@ export class RevealRenderTarget { // ================================================== public fitView(): boolean { - const boundingBox = this.sceneBoundingBox; + const boundingBox = this.clippedSceneBoundingBox; if (boundingBox.isEmpty()) { return false; } - this.viewer.fitCameraToBoundingBox(this.sceneBoundingBox); + this.viewer.fitCameraToBoundingBox(this.clippedSceneBoundingBox); return true; } // ================================================== - // INSTANCE METHODS: Crop box operations (Experimental code) + // INSTANCE METHODS: Clipping operations (Experimental code) // ================================================== - public setGlobalCropBox( - clippingPlanes: Plane[], - boundingBox: Box3, - domainObject: DomainObject - ): void { - // Input in Viewer coordinates - this.viewer.setGlobalClippingPlanes(clippingPlanes); - this._cropBoxBoundingBox = boundingBox; - this._cropBoxName = domainObject.name; + public getGlobalClippingPlanes(): Plane[] { + return this.viewer.getGlobalClippingPlanes(); } - public clearGlobalCropBox(): void { - this.viewer.setGlobalClippingPlanes([]); - this._cropBoxBoundingBox = undefined; - this._cropBoxName = undefined; + public get isGlobalClippingActive(): boolean { + return this.getGlobalClippingPlanes().length > 0; + } + + public get isGlobalCropBoxActive(): boolean { + return this.isGlobalClippingActive && this._cropBoxUniqueId !== undefined; } public isGlobalCropBox(domainObject: DomainObject): boolean { - return this._cropBoxName !== undefined && domainObject.hasEqualName(this._cropBoxName); + return this._cropBoxUniqueId !== undefined && domainObject.uniqueId === this._cropBoxUniqueId; } - public get isGlobalCropBoxActive(): boolean { - return ( - this.viewer.getGlobalClippingPlanes().length > 0 && this._cropBoxBoundingBox !== undefined - ); + public setGlobalClipping(clippingPlanes: Plane[], domainObject?: DomainObject): void { + if (clippingPlanes.length === 0) { + this.clearGlobalClipping(); + return; + } + const sceneBoundingBox = this.sceneBoundingBox.clone(); + sceneBoundingBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + const sceneRange = new Range3(); + sceneRange.copy(sceneBoundingBox); + const clippedRange = getBoundingBoxFromPlanes(clippingPlanes, sceneRange); + const clippedBoundingBox = clippedRange.getBox(); + + for (const plane of clippingPlanes) { + plane.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + } + clippedBoundingBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + + // Set the values + this.viewer.setGlobalClippingPlanes(clippingPlanes); + this._clippedBoundingBox = clippedBoundingBox; + this._cropBoxUniqueId = domainObject?.uniqueId; + this.rootDomainObject.notifyDescendants(Changes.clipping); + } + + public clearGlobalClipping(): void { + this.viewer.setGlobalClippingPlanes([]); + this._clippedBoundingBox = undefined; + this._cropBoxUniqueId = undefined; + this.rootDomainObject.notifyDescendants(Changes.clipping); } // ================================================== diff --git a/react-components/src/architecture/base/renderTarget/UnitSystem.ts b/react-components/src/architecture/base/renderTarget/UnitSystem.ts index b1be8ad2ca2..28250c2cd47 100644 --- a/react-components/src/architecture/base/renderTarget/UnitSystem.ts +++ b/react-components/src/architecture/base/renderTarget/UnitSystem.ts @@ -36,6 +36,20 @@ export class UnitSystem { return value; } + public convertFromUnit(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 // ================================================== @@ -64,14 +78,14 @@ export class UnitSystem { return this.isMetric ? 'm²' : 'ft²'; case Quantity.Volume: return this.isMetric ? 'm³' : 'ft³'; - case Quantity.Degrees: + case Quantity.Angle: return '°'; } } private getFractionDigits(quantity: Quantity): number { switch (quantity) { - case Quantity.Degrees: + case Quantity.Angle: 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 index 54e05e2dc3e..321bf4c4828 100644 --- a/react-components/src/architecture/base/utilities/TranslateKey.ts +++ b/react-components/src/architecture/base/utilities/TranslateKey.ts @@ -4,6 +4,6 @@ export type TranslateDelegate = (key: string, fallback?: string) => string; export type TranslateKey = { - key: string; - fallback?: string; + key?: string; + fallback: string; }; diff --git a/react-components/src/architecture/base/utilities/colors/colorExtensions.ts b/react-components/src/architecture/base/utilities/colors/colorExtensions.ts index 12b2f7fae40..b05437810ee 100644 --- a/react-components/src/architecture/base/utilities/colors/colorExtensions.ts +++ b/react-components/src/architecture/base/utilities/colors/colorExtensions.ts @@ -6,6 +6,7 @@ import { Color, type HSL } from 'three'; export const WHITE_COLOR = new Color(1, 1, 1); export const BLACK_COLOR = new Color(0, 0, 0); +export const GREY_COLOR = new Color(0.67, 0.67, 0.67); export const MAX_BYTE = 255; export function getMixedColor(color: Color, other: Color, fraction = 0.5): Color { @@ -49,6 +50,16 @@ export function getGammaCorrectedColor(color: Color, gamma = 2.2): Color { return new Color(r, g, b); } +export function convertToComplementary(color: Color): void { + color.r = 1 - color.r; + color.g = 1 - color.g; + color.b = 1 - color.b; +} + +export function getComplementary(color: Color): Color { + return new Color(1 - color.r, 1 - color.g, 1 - color.b); +} + export function fractionToByte(fraction: number): number { return fraction * MAX_BYTE; } diff --git a/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts b/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts index 92be9b308cb..7aa6f862155 100644 --- a/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts @@ -177,7 +177,7 @@ export function getRandomGaussian(mean = 0, stdDev = 1): number { if (b <= Number.EPSILON) { continue; } - const gausian = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); - return gausian * stdDev + mean; + const gaussian = Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b); + return gaussian * stdDev + mean; } } diff --git a/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts b/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts index 44314e4575a..7247266a7b1 100644 --- a/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts @@ -8,22 +8,22 @@ import { Vector3, type Ray } from 'three'; * @param ray - The ray to calculate the closest point from. * @param lineDirection - The direction of the line. * @param pointOnLine - A point on the line. - * @param optionalClosestPointOnLine - An optional Vector3 to store the closest point on the line. + * @param target - An optional Vector3 to store the closest point on the line. * @returns The closest point on the line to the ray. */ export function getClosestPointOnLine( ray: Ray, lineDirection: Vector3, pointOnLine: Vector3, - optionalClosestPointOnLine?: Vector3 + target?: Vector3 ): Vector3 { - if (optionalClosestPointOnLine === undefined) { - optionalClosestPointOnLine = new Vector3(); + if (target === undefined) { + target = new Vector3(); } // Three.js lack a distance to line function, so use the line segment function const lineLength = ray.distanceToPoint(pointOnLine) * 100; const v0 = pointOnLine.clone().addScaledVector(lineDirection, -lineLength); const v1 = pointOnLine.clone().addScaledVector(lineDirection, +lineLength); - ray.distanceSqToSegment(v0, v1, undefined, optionalClosestPointOnLine); - return optionalClosestPointOnLine; + ray.distanceSqToSegment(v0, v1, undefined, target); + return target; } diff --git a/react-components/src/architecture/base/utilities/extensions/vectorExtensions.ts b/react-components/src/architecture/base/utilities/extensions/vectorExtensions.ts index e9b65b96107..534bb3a9bdf 100644 --- a/react-components/src/architecture/base/utilities/extensions/vectorExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/vectorExtensions.ts @@ -19,10 +19,36 @@ export function verticalDistanceTo(from: Vector3, to: Vector3): number { return Math.abs(from.z - to.z); } +export function rotateHorizontal(vector: Vector3, angle: number): void { + const dx = vector.x; + vector.x = dx * Math.cos(angle) - vector.y * Math.sin(angle); + vector.y = dx * Math.sin(angle) + vector.y * Math.cos(angle); +} + +export function getAbsMaxComponentIndex(vector: Vector3): number { + let max = 0; + let maxIndex = 0; + for (let index = 0; index < 3; index++) { + const value = Math.abs(vector.getComponent(index)); + if (value < max) { + continue; + } + maxIndex = index; + max = value; + } + return maxIndex; +} + export function getHorizontalCrossProduct(self: Vector3, other: Vector3): number { return self.x * other.y - self.y * other.x; } +export function rotatePiHalf(vector: Vector3): void { + const x = vector.x; + vector.x = -vector.y; + vector.y = x; +} + export function getOctDir(vector: Vector2): number { // The octdirs are: // North (Positive Y-axis) diff --git a/react-components/src/architecture/base/utilities/geometry/Range1.ts b/react-components/src/architecture/base/utilities/geometry/Range1.ts index c27d93b9d80..7a1cc452fa8 100644 --- a/react-components/src/architecture/base/utilities/geometry/Range1.ts +++ b/react-components/src/architecture/base/utilities/geometry/Range1.ts @@ -82,7 +82,7 @@ export class Range1 { // INSTANCE METHODS: Requests // ================================================== - equals(other: Range1): boolean { + public equals(other: Range1): boolean { if (other === undefined) { return false; } @@ -95,7 +95,7 @@ export class Range1 { return this.min === other.min && this.max === other.max; } - isInside(value: number): boolean { + public isInside(value: number): boolean { return this.min <= value && value <= this.max; } diff --git a/react-components/src/architecture/base/utilities/geometry/Range3.ts b/react-components/src/architecture/base/utilities/geometry/Range3.ts index c31b53674cb..30676d4d84c 100644 --- a/react-components/src/architecture/base/utilities/geometry/Range3.ts +++ b/react-components/src/architecture/base/utilities/geometry/Range3.ts @@ -2,9 +2,10 @@ * Copyright 2024 Cognite AS */ -import { type Vector2, Vector3, type Box3 } from 'three'; +import { type Vector2, Vector3, Box3, type Plane, Line3 } from 'three'; import { Range1 } from './Range1'; import { square } from '../extensions/mathExtensions'; +import { Vector3Pool } from '@cognite/reveal'; export class Range3 { // ================================================== @@ -90,6 +91,10 @@ export class Range3 { return this.x.equals(other.x) && this.y.equals(other.y) && this.z.equals(other.z); } + public isInside(point: Vector3): boolean { + return this.x.isInside(point.x) && this.y.isInside(point.y) && this.z.isInside(point.z); + } + // ================================================== // INSTANCE METHODS: Getters // ================================================== @@ -114,7 +119,10 @@ export class Range3 { return target.set(this.x.center, this.y.center, this.z.center); } - public getBox(target: Box3): Box3 { + public getBox(target?: Box3): Box3 { + if (target === undefined) { + target = new Box3(); + } target.min.set(this.x.min, this.y.min, this.z.min); target.max.set(this.x.max, this.y.max, this.z.max); return target; @@ -158,6 +166,56 @@ export class Range3 { } } + // ================================================== + // INSTANCE METHODS: Plane intersection + // ================================================== + + public getHorizontalIntersection(plane: Plane, cornerIndex: number): Vector3 { + const corner = this.getCornerPoint(cornerIndex, newVector3()); + return plane.projectPoint(corner, corner); + } + + public getIntersectionOfEdge( + plane: Plane, + cornerIndex1: number, + cornerIndex2: number + ): Vector3 | undefined { + // Finds 2 corners and make a line between them, then intersect the line + const corner1 = this.getCornerPoint(cornerIndex1, newVector3()); + const corner2 = this.getCornerPoint(cornerIndex2, newVector3()); + TEMPORARY_LINE.set(corner1, corner2); + const point = plane.intersectLine(TEMPORARY_LINE, newVector3()); + return point ?? undefined; + } + + public getVerticalPlaneIntersection( + plane: Plane, + isTop: boolean, // Give top or bottom of the range here + start: Vector3, + end: Vector3 + ): boolean { + const startIndex = isTop ? 4 : 0; + let count = 0; + for (let corner = 0; corner < 4; corner++) { + const corner1 = startIndex + corner; + const corner2 = startIndex + ((corner + 1) % 4); + const intersection = this.getIntersectionOfEdge(plane, corner1, corner2); + if (intersection === undefined) { + continue; + } + if (count === 0) { + start.copy(intersection); + count++; + continue; + } + if (count === 1) { + end.copy(intersection); + count++; + break; + } + } + return count >= 2; + } // ================================================== // INSTANCE METHODS: Operations // ================================================== @@ -195,6 +253,11 @@ export class Range3 { this.y.add(value.y); } + public addHorizontal(value: Vector3): void { + this.x.add(value.x); + this.y.add(value.y); + } + public addRange(value: Range3 | undefined): void { if (value === undefined) return; this.x.addRange(value.x); @@ -232,3 +295,13 @@ export class Range3 { return range; } } + +// ================================================== +// PRIVATE FUNCTIONS: Vector pool +// ================================================== + +const TEMPORARY_LINE = new Line3(); // Temporary, used in getIntersection() only +const VECTOR_POOL = new Vector3Pool(); +function newVector3(copyFrom?: Vector3): Vector3 { + return VECTOR_POOL.getNext(copyFrom); +} diff --git a/react-components/src/architecture/base/utilities/geometry/TrianglesBuffers.ts b/react-components/src/architecture/base/utilities/geometry/TrianglesBuffers.ts index fff83523479..eaefef9c4d9 100644 --- a/react-components/src/architecture/base/utilities/geometry/TrianglesBuffers.ts +++ b/react-components/src/architecture/base/utilities/geometry/TrianglesBuffers.ts @@ -14,6 +14,10 @@ export class TrianglesBuffers { protected triangleIndexes: number[] = []; protected uniqueIndex = 0; + public get isFilled(): boolean { + return this.uniqueIndex === this.positions.length / 3; + } + // ================================================== // CONSTRUCTOR // ================================================== @@ -46,10 +50,10 @@ export class TrianglesBuffers { } // ================================================== - // INSTANCE METHODS: Add operation + // INSTANCE METHODS: Add to triangle strip // ================================================== - public addPair(p1: Vector3, p2: Vector3, n1: Vector3, n2: Vector3, u = 0): void { + public addPairWithNormals(p1: Vector3, p2: Vector3, n1: Vector3, n2: Vector3, u = 0): void { if (this.uniqueIndex >= 2) { // 2------3 // | | @@ -59,15 +63,14 @@ export class TrianglesBuffers { const unique2 = this.uniqueIndex; const unique3 = this.uniqueIndex + 1; - this.addTriangle(unique0, unique2, unique3); - this.addTriangle(unique0, unique3, unique1); + this.addTriangle(unique0, unique3, unique2); + this.addTriangle(unique0, unique1, unique3); } - this.add(p1, n1, u); this.add(p2, n2, u); } - public addPair2(p1: Vector3, p2: Vector3, normal: Vector3, u: number): void { + public addPairWithNormal(p1: Vector3, p2: Vector3, normal: Vector3, u: number = 0): void { if (this.uniqueIndex >= 2) { // 2------3 // | | @@ -77,13 +80,11 @@ export class TrianglesBuffers { const unique2 = this.uniqueIndex; const unique3 = this.uniqueIndex + 1; - this.addTriangle(unique0, unique2, unique3); - this.addTriangle(unique0, unique3, unique1); - } - if (this.uvs !== undefined) { - this.add(p1, normal, u); - this.add(p2, normal, u); + this.addTriangle(unique0, unique3, unique2); + this.addTriangle(unique0, unique1, unique3); } + this.add(p1, normal, u); + this.add(p2, normal, u); } public addTriangle(index0: number, index1: number, index2: number): void { diff --git a/react-components/src/architecture/base/utilities/geometry/getBoundingBoxFromPlanes.ts b/react-components/src/architecture/base/utilities/geometry/getBoundingBoxFromPlanes.ts new file mode 100644 index 00000000000..adac89954a3 --- /dev/null +++ b/react-components/src/architecture/base/utilities/geometry/getBoundingBoxFromPlanes.ts @@ -0,0 +1,93 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Vector3, type Plane, Line3 } from 'three'; +import { Range3 } from './Range3'; + +export function getBoundingBoxFromPlanes(planes: Plane[], originalBoundingBox: Range3): Range3 { + const result = new Range3(); + const tempVector = new Vector3(); + { + const horizontalPlanes = planes.filter((plane) => isHorizontalPlane(plane)); + + // Calculate the z range based on visible corners + for (let corner = 0; corner < 8; corner += 4) { + const cornerPoint = originalBoundingBox.getCornerPoint(corner, tempVector); + if (isVisible(horizontalPlanes, cornerPoint)) { + result.z.add(cornerPoint.z); + } + } + // Calculate the z range based on the horizontal planes + const origin = new Vector3(0, 0, 0); + for (const plane of horizontalPlanes) { + const pointOnPlane = plane.projectPoint(origin, tempVector); + if (isVisible(horizontalPlanes, pointOnPlane, plane)) { + result.z.add(pointOnPlane.z); + } + } + } + { + const verticalPlanes = planes.filter((plane) => !isHorizontalPlane(plane)); + // Calculate the x and y range based on visible corners + for (let corner = 0; corner < 8; corner++) { + const cornerPoint = originalBoundingBox.getCornerPoint(corner, tempVector); + if (isVisible(verticalPlanes, cornerPoint)) { + result.addHorizontal(cornerPoint); + } + } + // Calculate the x and y range based on the vertical planes + for (const plane of verticalPlanes) { + const start = new Vector3(); + const end = new Vector3(); + if (!originalBoundingBox.getVerticalPlaneIntersection(plane, false, start, end)) { + continue; + } + // Cut the line into smaller lines by the other planes + const lines = new Array(); + lines.push(new Line3(start, end)); + + for (const otherPlane of verticalPlanes) { + if (otherPlane === plane) { + continue; + } + const length = lines.length; // Note the lines array is growing + for (let i = 0; i < length; i++) { + const line = lines[i]; + const intersection = otherPlane.intersectLine(line, new Vector3()); + if (intersection === null) { + continue; + } + lines.push(new Line3(intersection, line.end.clone())); + line.end.copy(intersection); + } + } + // Check if the each line segments is visible, if so add to result range + for (const line of lines) { + const center = line.getCenter(tempVector); + if (!isVisible(verticalPlanes, center, plane)) { + continue; + } + result.addHorizontal(line.start); + result.addHorizontal(line.end); + } + } + } + return result; + + function isHorizontalPlane(plane: Plane): boolean { + return plane.normal.x === 0 && plane.normal.y === 0; + } + + function isVisible(planes: Plane[], point: Vector3, except?: Plane): boolean { + for (const plane of planes) { + if (except !== undefined && plane === except) { + continue; + } + if (plane.distanceToPoint(point) < 0) { + return false; + } + } + return true; + } +} diff --git a/react-components/src/architecture/base/views/GroupThreeView.ts b/react-components/src/architecture/base/views/GroupThreeView.ts index 87345ad232e..afcde5e5f04 100644 --- a/react-components/src/architecture/base/views/GroupThreeView.ts +++ b/react-components/src/architecture/base/views/GroupThreeView.ts @@ -12,6 +12,7 @@ import { type ICustomObject } from '@cognite/reveal'; import { type DomainObjectIntersection } from '../domainObjectsHelpers/DomainObjectIntersection'; +import { VisualDomainObject } from '../domainObjects/VisualDomainObject'; /** * Represents an abstract class for a Three.js view that renders an Object3D. @@ -83,8 +84,16 @@ export abstract class GroupThreeView extends ThreeView implements ICustomObject if (closestDistance !== undefined && closestDistance < distance) { return undefined; } - if (!intersectInput.isVisible(point)) { - return undefined; + const { domainObject } = this; + + if (domainObject instanceof VisualDomainObject) { + if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { + return undefined; + } + } else { + if (!intersectInput.isVisible(point)) { + return undefined; + } } const customObjectIntersection: DomainObjectIntersection = { type: 'customObject', @@ -92,7 +101,7 @@ export abstract class GroupThreeView extends ThreeView implements ICustomObject distanceToCamera: distance, userData: intersection[0], customObject: this, - domainObject: this.domainObject + domainObject }; if (this.shouldPickBoundingBox) { const boundingBox = this.boundingBox; diff --git a/react-components/src/architecture/base/views/ThreeView.ts b/react-components/src/architecture/base/views/ThreeView.ts index 5078d7b02e4..2cb0d6d26fe 100644 --- a/react-components/src/architecture/base/views/ThreeView.ts +++ b/react-components/src/architecture/base/views/ThreeView.ts @@ -5,7 +5,7 @@ import { type DomainObjectChange } from '../domainObjectsHelpers/DomainObjectChange'; import { BaseView } from './BaseView'; import { Changes } from '../domainObjectsHelpers/Changes'; -import { type RenderStyle } from '../domainObjectsHelpers/RenderStyle'; +import { type RenderStyle } from '../renderStyles/RenderStyle'; import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { type DomainObject } from '../domainObjects/DomainObject'; import { type PerspectiveCamera, type Box3 } from 'three'; diff --git a/react-components/src/architecture/concrete/axis/AxisDomainObject.ts b/react-components/src/architecture/concrete/axis/AxisDomainObject.ts index 421b4f31167..019b1951f58 100644 --- a/react-components/src/architecture/concrete/axis/AxisDomainObject.ts +++ b/react-components/src/architecture/concrete/axis/AxisDomainObject.ts @@ -3,7 +3,8 @@ */ import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { type ThreeView } from '../../base/views/ThreeView'; import { AxisRenderStyle } from './AxisRenderStyle'; import { AxisThreeView } from './AxisThreeView'; @@ -13,8 +14,8 @@ export class AxisDomainObject extends VisualDomainObject { // OVERRIDES of DomainObject // ================================================== - public override get typeName(): string { - return 'Axis3D'; + public override get typeName(): TranslateKey { + return { fallback: 'Axis' }; } public override createRenderStyle(): RenderStyle | undefined { diff --git a/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts b/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts index b67f91f4765..0d2a8953d6b 100644 --- a/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts +++ b/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts @@ -3,7 +3,7 @@ */ import { cloneDeep } from 'lodash'; -import { RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { RenderStyle } from '../../base/renderStyles/RenderStyle'; import { Color } from 'three'; import { getMixedColor } from '../../base/utilities/colors/colorExtensions'; diff --git a/react-components/src/architecture/concrete/axis/AxisThreeView.ts b/react-components/src/architecture/concrete/axis/AxisThreeView.ts index 6399c88028e..b56b674718a 100644 --- a/react-components/src/architecture/concrete/axis/AxisThreeView.ts +++ b/react-components/src/architecture/concrete/axis/AxisThreeView.ts @@ -113,7 +113,7 @@ export class AxisThreeView extends GroupThreeView { const target = this.renderTarget; // Check if bounding box is different - const sceneBoundingBox = target.sceneBoundingBox; + const sceneBoundingBox = target.clippedSceneBoundingBox; if (sceneBoundingBox.equals(this._sceneBoundingBox)) { return false; } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureDomainObject.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureDomainObject.ts deleted file mode 100644 index ab40a47703f..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureDomainObject.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ - -import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { getIconByMeasureType, getNameByMeasureType, type MeasureType } from './MeasureType'; -import { type MeasureRenderStyle } from './MeasureRenderStyle'; -import { PopupStyle } from '../../base/domainObjectsHelpers/PopupStyle'; - -export abstract class MeasureDomainObject extends VisualDomainObject { - private readonly _measureType: MeasureType; - - // ================================================== - // INSTANCE PROPERTIES - // ================================================== - - public get renderStyle(): MeasureRenderStyle { - return this.getRenderStyle() as MeasureRenderStyle; - } - - public get measureType(): MeasureType { - return this._measureType; - } - - // ================================================== - // CONSTRUCTOR - // ================================================== - - public constructor(measureType: MeasureType) { - super(); - this._measureType = measureType; - } - - // ================================================== - // OVERRIDES - // ================================================== - - public override get icon(): string { - return getIconByMeasureType(this.measureType); - } - - public override get typeName(): string { - return getNameByMeasureType(this.measureType); - } - - public override get hasPanelInfo(): boolean { - return true; - } - - public override getPanelInfoStyle(): PopupStyle { - // bottom = 66 because the toolbar is below - return new PopupStyle({ bottom: 66, left: 0 }); - } -} diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts deleted file mode 100644 index ef8b55de138..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureType.ts +++ /dev/null @@ -1,92 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ - -import { type TranslateKey } from '../../base/utilities/TranslateKey'; - -export enum MeasureType { - None, - Line, - Polyline, - Polygon, - HorizontalArea, - VerticalArea, - Volume -} - -export function getIconByMeasureType(measureType: MeasureType): string { - switch (measureType) { - case MeasureType.Line: - return 'VectorLine'; - case MeasureType.Polyline: - return 'VectorZigzag'; - case MeasureType.Polygon: - return 'Polygon'; - case MeasureType.HorizontalArea: - return 'FrameTool'; - case MeasureType.VerticalArea: - return 'Perspective'; - case MeasureType.Volume: - return 'Cube'; - default: - throw new Error('Unknown MeasureType type'); - } -} - -export function getNameByMeasureType(measureType: MeasureType): string { - switch (measureType) { - case MeasureType.Line: - return 'Line'; - case MeasureType.Polyline: - return 'Polyline'; - case MeasureType.Polygon: - return 'Polygon'; - case MeasureType.HorizontalArea: - return 'Horizontal area'; - case MeasureType.VerticalArea: - return 'Vertical area'; - case MeasureType.Volume: - return 'Volume'; - default: - throw new Error('Unknown MeasureType type'); - } -} - -export function getTooltipByMeasureType(measureType: MeasureType): TranslateKey { - switch (measureType) { - case MeasureType.Line: - return { - key: 'MEASUREMENTS_ADD_LINE', - fallback: 'Measure distance between two points. Click at the start point and the end point.' - }; - case MeasureType.Polyline: - return { - key: 'MEASUREMENTS_ADD_POLYLINE', - fallback: - 'Measure the length of a continuous polyline. Click at any number of points and end with Esc.' - }; - case MeasureType.Polygon: - return { - key: 'MEASUREMENTS_ADD_POLYGON', - fallback: 'Measure an area of a polygon. Click at least 3 points and end with Esc.' - }; - case MeasureType.VerticalArea: - return { - key: 'MEASUREMENTS_ADD_VERTICAL_AREA', - fallback: 'Measure rectangular vertical Area. Click at two points in a vertical plan.' - }; - case MeasureType.HorizontalArea: - return { - key: 'MEASUREMENTS_ADD_HORIZONTAL_AREA', - fallback: 'Measure rectangular horizontal Area. Click at three points in a horizontal plan.' - }; - case MeasureType.Volume: - return { - key: 'MEASUREMENTS_ADD_VOLUME', - fallback: - 'Measure volume of a box. Click at three points in a horizontal plan and the fourth to give it height.' - }; - default: - throw new Error('Unknown MeasureType type'); - } -} diff --git a/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts deleted file mode 100644 index 3cb948b77ac..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/SetCropBoxCommand.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ - -import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; -import { MeasureType } from './MeasureType'; - -// Experimental code for crop box - -export class SetCropBoxCommand extends RenderTargetCommand { - // ================================================== - // OVERRIDES - // ================================================== - - public override get tooltip(): TranslateKey { - return { key: 'CROP_BOX', fallback: 'Set as crop box' }; - } - - public override get icon(): string { - return 'Crop'; - } - - public override get isEnabled(): boolean { - if (this.renderTarget.isGlobalCropBoxActive) { - return true; - } - return this.getMeasureBoxDomainObject() !== undefined; - } - - public override get isChecked(): boolean { - return this.renderTarget.isGlobalCropBoxActive; - } - - protected override invokeCore(): boolean { - const { renderTarget } = this; - const domainObject = this.getMeasureBoxDomainObject(); - if (domainObject === undefined || this.renderTarget.isGlobalCropBoxActive) { - renderTarget.clearGlobalCropBox(); - return false; - } - if (domainObject.isUseAsCropBox) { - renderTarget.clearGlobalCropBox(); - return true; - } - domainObject.setUseAsCropBox(true); - renderTarget.fitView(); - return true; - } - - // ================================================== - // INSTANCE METHODS - // ================================================== - - private getMeasureBoxDomainObject(): MeasureBoxDomainObject | undefined { - const domainObject = this.rootDomainObject.getSelectedDescendantByType(MeasureBoxDomainObject); - if (domainObject === undefined) { - return undefined; - } - if (domainObject.measureType !== MeasureType.Volume) { - return undefined; - } - return domainObject; - } -} diff --git a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurementTypeCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/SetMeasurementTypeCommand.ts deleted file mode 100644 index be21b504d02..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurementTypeCommand.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ - -import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -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 SetMeasurementTypeCommand extends RenderTargetCommand { - private readonly _measureType: MeasureType; - - // ================================================== - // CONSTRUCTOR - // ================================================== - - public constructor(measureType: MeasureType) { - super(); - this._measureType = measureType; - } - - // ================================================== - // OVERRIDES of BaseCommand - // ================================================== - - public override get icon(): string { - return getIconByMeasureType(this._measureType); - } - - public override get tooltip(): TranslateKey { - return getTooltipByMeasureType(this._measureType); - } - - public override get isEnabled(): boolean { - return this.measurementTool !== undefined; - } - - public override get isChecked(): boolean { - const { measurementTool } = this; - if (measurementTool === undefined) { - return false; - } - return measurementTool.measureType === this._measureType; - } - - protected override invokeCore(): boolean { - const { measurementTool } = this; - if (measurementTool === undefined) { - return false; - } - measurementTool.handleEscape(); - measurementTool.clearDragging(); - if (measurementTool.measureType === this._measureType) { - measurementTool.measureType = MeasureType.None; - } else { - measurementTool.measureType = this._measureType; - } - return true; - } - - public override equals(other: BaseCommand): boolean { - if (!(other instanceof SetMeasurementTypeCommand)) { - return false; - } - return this._measureType === other._measureType; - } - - // ================================================== - // INSTANCE METHODS - // ================================================== - - private get measurementTool(): MeasurementTool | undefined { - const activeTool = this.renderTarget.commandsController.activeTool; - if (!(activeTool instanceof MeasurementTool)) { - return undefined; - } - return activeTool; - } -} diff --git a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurementsOnTopCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurementsOnTopCommand.ts deleted file mode 100644 index 741b6d17a92..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurementsOnTopCommand.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ - -import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { MeasureDomainObject } from './MeasureDomainObject'; - -export class ShowMeasurementsOnTopCommand extends RenderTargetCommand { - // ================================================== - // OVERRIDES - // ================================================== - - public override get tooltip(): TranslateKey { - return { key: 'MEASUREMENTS_SHOW_ON_TOP', fallback: 'Show all measurements on top' }; - } - - public override get icon(): string { - return 'EyeShow'; - } - - public override get isEnabled(): boolean { - return this.getFirst() !== undefined; - } - - public override get isChecked(): boolean { - return !this.getDepthTest(); - } - - protected override invokeCore(): boolean { - const depthTest = this.getDepthTest(); - for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { - const style = domainObject.renderStyle; - style.depthTest = !depthTest; - domainObject.notify(Changes.renderStyle); - } - return true; - } - - // ================================================== - // INSTANCE METHODS - // ================================================== - - private getDepthTest(): boolean { - const domainObject = this.getFirst(); - if (domainObject === undefined) { - return false; - } - const style = domainObject.renderStyle; - return style.depthTest; - } - - private getFirst(): MeasureDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(MeasureDomainObject); - } -} diff --git a/react-components/src/architecture/concrete/clipping/ClipTool.ts b/react-components/src/architecture/concrete/clipping/ClipTool.ts new file mode 100644 index 00000000000..36eaa7d630b --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/ClipTool.ts @@ -0,0 +1,95 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type BaseCommand } from '../../base/commands/BaseCommand'; +import { type BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { PrimitiveEditTool } from '../primitives/PrimitiveEditTool'; +import { PrimitiveType } from '../primitives/PrimitiveType'; +import { BoxCreator } from '../primitives/box/BoxCreator'; +import { CropBoxDomainObject } from './CropBoxDomainObject'; +import { ApplyClipCommand } from './commands/ApplyClipCommand'; +import { ShowClippingOnTopCommand } from './commands/ShowClippingOnTopCommand'; +import { ShowAllClippingCommand } from './commands/ShowAllClippingCommand'; +import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { SetClipTypeCommand } from './commands/SetClipTypeCommand'; +import { PlaneCreator } from '../primitives/plane/PlaneCreator'; +import { SliceDomainObject } from './SliceDomainObject'; + +export class ClipTool extends PrimitiveEditTool { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor() { + super(PrimitiveType.None); + } + + // ================================================== + // OVERRIDES of BaseCommand + // ================================================== + + public override get icon(): string { + return 'Crop'; + } + + public override get tooltip(): TranslateKey { + return { key: 'CLIP_TOOL', fallback: 'Create or edit crop box and slice planes' }; + } + + public override getToolbar(): Array { + return [ + new SetClipTypeCommand(PrimitiveType.PlaneX), + new SetClipTypeCommand(PrimitiveType.PlaneY), + new SetClipTypeCommand(PrimitiveType.PlaneZ), + new SetClipTypeCommand(PrimitiveType.PlaneXY), + new SetClipTypeCommand(PrimitiveType.Box), + undefined, // Separator + new ApplyClipCommand(), + new ShowClippingOnTopCommand(), + new ShowAllClippingCommand() + ]; + } + + // ================================================== + // OVERRIDES of BaseTool + // ================================================== + + public override onActivate(): void { + super.onActivate(); + this.setAllVisible(true); + } + + public override onDeactivate(): void { + super.onDeactivate(); + this.setAllVisible(false); + } + + // ================================================== + // OVERRIDES of BaseEditTool + // ================================================== + + protected override canBeSelected(domainObject: VisualDomainObject): boolean { + return domainObject instanceof CropBoxDomainObject || domainObject instanceof SliceDomainObject; + } + + // ================================================== + // OVERRIDES of BoxOrLineEditTool + // ================================================== + + protected override createCreator(): BaseCreator | undefined { + switch (this.primitiveType) { + case PrimitiveType.PlaneX: + case PrimitiveType.PlaneY: + case PrimitiveType.PlaneZ: + case PrimitiveType.PlaneXY: + return new PlaneCreator(new SliceDomainObject(this.primitiveType)); + + case PrimitiveType.Box: + return new BoxCreator(new CropBoxDomainObject()); + default: + return undefined; + } + } +} diff --git a/react-components/src/architecture/concrete/clipping/CropBoxDomainObject.ts b/react-components/src/architecture/concrete/clipping/CropBoxDomainObject.ts new file mode 100644 index 00000000000..35df2fff171 --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/CropBoxDomainObject.ts @@ -0,0 +1,116 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { BoxFace } from '../../base/utilities/box/BoxFace'; +import { BoxDomainObject } from '../primitives/box/BoxDomainObject'; +import { Color, type Plane } from 'three'; +import { BoxRenderStyle } from '../primitives/box/BoxRenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { ApplyClipCommand } from './commands/ApplyClipCommand'; +import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; + +export class CropBoxDomainObject extends BoxDomainObject { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor() { + super(); + this.color = new Color(Color.NAMES.orange); + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get icon(): string { + return 'Crop'; + } + + public override get typeName(): TranslateKey { + return { key: 'CROP_BOX', fallback: 'Crop box' }; + } + + public override createRenderStyle(): RenderStyle | undefined { + const style = new BoxRenderStyle(); + style.showLabel = false; + return style; + } + + protected override notifyCore(change: DomainObjectChange): void { + super.notifyCore(change); + + if (change.isChanged(Changes.deleted, Changes.added, Changes.geometry, Changes.selected)) { + if (change.isChanged(Changes.deleted)) { + this.focusType = FocusType.Pending; // Make sure that the crop box is not used in clipping anymore + } + this.updateClippingPlanes(change); + } + } + + public override get useClippingInIntersection(): boolean { + return false; + } + + // ================================================== + // INSTANCE METHODS: Others + // ================================================== + + public setThisAsGlobalCropBox(): void { + const root = this.rootDomainObject; + if (root === undefined) { + return; + } + if (this.focusType === FocusType.Pending) { + // Fallback to default. Do not use any pending objects in clipping + ApplyClipCommand.setClippingPlanes(root); + return; + } + const planes = this.createClippingPlanes(); + root.renderTarget.setGlobalClipping(planes, this); + } + + private createClippingPlanes(): Plane[] { + const root = this.rootDomainObject; + if (root === undefined) { + return []; + } + const matrix = this.getMatrix(); + return BoxFace.createClippingPlanes(matrix); + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private updateClippingPlanes(change: DomainObjectChange): void { + // Update the clipping planes if necessary + const root = this.rootDomainObject; + if (root === undefined) { + return; + } + const renderTarget = root.renderTarget; + if (!renderTarget.isGlobalClippingActive) { + return; + } + const isGlobalCropBox = renderTarget.isGlobalCropBox(this); + if (isGlobalCropBox) { + if ( + change.isChanged(Changes.deleted) || + (change.isChanged(Changes.selected) && !this.isSelected) + ) { + ApplyClipCommand.setClippingPlanes(root); + return; + } + } + if ((isGlobalCropBox || this.isSelected) && change.isChanged(Changes.geometry)) { + this.setThisAsGlobalCropBox(); + } else if (change.isChanged(Changes.selected) && this.isSelected) { + this.setThisAsGlobalCropBox(); + } + } +} diff --git a/react-components/src/architecture/concrete/clipping/SliceDomainObject.ts b/react-components/src/architecture/concrete/clipping/SliceDomainObject.ts new file mode 100644 index 00000000000..a95bfa8d97b --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/SliceDomainObject.ts @@ -0,0 +1,85 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Color } from 'three'; +import { PlaneDomainObject } from '../primitives/plane/PlaneDomainObject'; +import { PrimitiveType } from '../primitives/PrimitiveType'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; +import { FlipSliceCommand } from './commands/FlipSliceCommand'; +import { ApplyClipCommand } from './commands/ApplyClipCommand'; +import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; +export class SliceDomainObject extends PlaneDomainObject { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(primitiveType); + this.color = new Color(Color.NAMES.greenyellow); + this.backSideColor = new Color(Color.NAMES.red); + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get typeName(): TranslateKey { + switch (this.primitiveType) { + case PrimitiveType.PlaneX: + return { key: 'SLICE_X', fallback: 'X slice' }; + case PrimitiveType.PlaneY: + return { key: 'SLICE_Y', fallback: 'Y slice' }; + case PrimitiveType.PlaneZ: + return { key: 'SLICE_Z', fallback: 'Z slice' }; + case PrimitiveType.PlaneXY: + return { key: 'SLICE_XY', fallback: 'XY slice' }; + default: + throw new Error('Unknown PrimitiveType'); + } + } + + public override getPanelToolbar(): BaseCommand[] { + const commands = super.getPanelToolbar(); + commands.push(new FlipSliceCommand(this)); + return commands; + } + + protected override notifyCore(change: DomainObjectChange): void { + super.notifyCore(change); + + if (change.isChanged(Changes.added, Changes.deleted, Changes.geometry)) { + if (change.isChanged(Changes.deleted)) { + this.focusType = FocusType.Pending; // Make sure that the slice is not used in clipping anymore + } + this.updateClippingPlanes(); + } + } + + public override get useClippingInIntersection(): boolean { + return false; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private updateClippingPlanes(): void { + // Update the clipping planes if necessary + const root = this.rootDomainObject; + if (root === undefined) { + return; + } + const renderTarget = root.renderTarget; + if (!renderTarget.isGlobalClippingActive) { + return; + } + if (renderTarget.isGlobalCropBoxActive) { + return; + } + ApplyClipCommand.setClippingPlanes(root); + } +} diff --git a/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts b/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts new file mode 100644 index 00000000000..a166170d1e8 --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/ApplyClipCommand.ts @@ -0,0 +1,83 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type Plane } from 'three'; +import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { CropBoxDomainObject } from '../CropBoxDomainObject'; +import { SliceDomainObject } from '../SliceDomainObject'; +import { type RootDomainObject } from '../../../base/domainObjects/RootDomainObject'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; + +export class ApplyClipCommand extends RenderTargetCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { + key: 'CLIP_APPLY', + fallback: 'Apply selected crop box to the model if any, otherwise apply all slice planes' + }; + } + + public override get icon(): string { + return 'Crop'; + } + + public override get isEnabled(): boolean { + if (this.getSelectedCropBoxDomainObject() !== undefined) { + return true; + } + if (this.rootDomainObject.getDescendantByType(SliceDomainObject) !== undefined) { + return true; + } + return false; + } + + public override get isChecked(): boolean { + return this.renderTarget.isGlobalClippingActive; + } + + protected override invokeCore(): boolean { + const { renderTarget } = this; + if (this.renderTarget.isGlobalClippingActive) { + renderTarget.clearGlobalClipping(); + return true; + } + const cropBox = this.getSelectedCropBoxDomainObject(); + if (cropBox !== undefined) { + cropBox.setThisAsGlobalCropBox(); + } else { + ApplyClipCommand.setClippingPlanes(this.rootDomainObject); + } + renderTarget.fitView(); + return true; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private getSelectedCropBoxDomainObject(): CropBoxDomainObject | undefined { + return this.rootDomainObject.getSelectedDescendantByType(CropBoxDomainObject); + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + public static setClippingPlanes(root: RootDomainObject): boolean { + const planes: Plane[] = []; + for (const sliceDomainObject of root.getDescendantsByType(SliceDomainObject)) { + const plane = sliceDomainObject.plane.clone(); + if (sliceDomainObject.focusType === FocusType.Pending) { + continue; // Do not use any pending objects in clipping + } + planes.push(plane); + } + root.renderTarget.setGlobalClipping(planes); + return true; + } +} diff --git a/react-components/src/architecture/concrete/clipping/commands/FlipSliceCommand.ts b/react-components/src/architecture/concrete/clipping/commands/FlipSliceCommand.ts new file mode 100644 index 00000000000..50c883da0ea --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/FlipSliceCommand.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { PrimitiveType } from '../../primitives/PrimitiveType'; +import { type SliceDomainObject } from '../SliceDomainObject'; +import { DomainObjectCommand } from '../../../base/commands/DomainObjectCommand'; + +export class FlipSliceCommand extends DomainObjectCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'SLICE_FLIP', fallback: 'Flip side on this slice' }; + } + + public override get icon(): string { + if (this._domainObject.primitiveType === PrimitiveType.PlaneZ) { + return 'FlipHorizontal'; + } + return 'FlipVertical'; // Maybe 'Exchange'; instead + } + + protected override invokeCore(): boolean { + this._domainObject.flip(); + this._domainObject.notify(Changes.geometry); + return true; + } +} diff --git a/react-components/src/architecture/concrete/clipping/commands/SetClipTypeCommand.ts b/react-components/src/architecture/concrete/clipping/commands/SetClipTypeCommand.ts new file mode 100644 index 00000000000..447c84fffe5 --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/SetClipTypeCommand.ts @@ -0,0 +1,143 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { type BaseCommand } from '../../../base/commands/BaseCommand'; +import { PrimitiveType } from '../../primitives/PrimitiveType'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { ClipTool } from '../ClipTool'; +import { getIconByPrimitiveType } from '../../measurements/getIconByPrimitiveType'; +import { SliceDomainObject } from '../SliceDomainObject'; + +export class SetClipTypeCommand extends RenderTargetCommand { + private readonly _primitiveType: PrimitiveType; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(); + this._primitiveType = primitiveType; + } + + // ================================================== + // OVERRIDES of BaseCommand + // ================================================== + + public override get icon(): string { + return getIconByPrimitiveType(this._primitiveType); + } + + public override get tooltip(): TranslateKey { + return getTooltipByPrimitiveType(this._primitiveType); + } + + public override get isVisible(): boolean { + return true; + } + + public override get isEnabled(): boolean { + if (this.tool === undefined) { + return false; + } + if ( + this._primitiveType === PrimitiveType.Box || + this._primitiveType === PrimitiveType.PlaneXY + ) { + return true; + } + // Allow maximum 2 slices of each type + let count = 0; + for (const domainObject of this.rootDomainObject.getDescendantsByType(SliceDomainObject)) { + if (domainObject.primitiveType !== this._primitiveType) { + continue; + } + count++; + if (count >= 2) { + return false; + } + } + return true; + } + + public override get isChecked(): boolean { + const { tool } = this; + if (tool === undefined) { + return false; + } + return tool.primitiveType === this._primitiveType; + } + + protected override invokeCore(): boolean { + const { tool } = this; + if (tool === undefined) { + return false; + } + tool.handleEscape(); + tool.clearDragging(); + if (tool.primitiveType === this._primitiveType) { + tool.primitiveType = PrimitiveType.None; + } else { + tool.primitiveType = this._primitiveType; + } + return true; + } + + public override equals(other: BaseCommand): boolean { + if (!(other instanceof SetClipTypeCommand)) { + return false; + } + return this._primitiveType === other._primitiveType; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private get tool(): ClipTool | undefined { + const activeTool = this.renderTarget.commandsController.activeTool; + if (!(activeTool instanceof ClipTool)) { + return undefined; + } + return activeTool; + } +} + +// ================================================== +// PRIMATE FUNCTIONS +// ================================================== + +function getTooltipByPrimitiveType(primitiveType: PrimitiveType): TranslateKey { + switch (primitiveType) { + case PrimitiveType.PlaneX: + return { + key: 'SLICE_X_ADD', + fallback: 'Add X slice. Click at one point.' + }; + case PrimitiveType.PlaneY: + return { + key: 'SLICE_Y_ADD', + fallback: 'Add Y slice. Click at one point.' + }; + case PrimitiveType.PlaneZ: + return { + key: 'SLICE_Z_ADD', + fallback: 'Add Z slice. Click at one point.' + }; + case PrimitiveType.PlaneXY: + return { + key: 'SLICE_XY_ADD', + fallback: 'Add XY slice. Click at two points.' + }; + case PrimitiveType.Box: + return { + key: 'CROP_BOX_ADD', + fallback: + 'Create crop box. Click at three points in a horizontal plan and the fourth to give it height.' + }; + default: + throw new Error('Unknown PrimitiveType'); + } +} diff --git a/react-components/src/architecture/concrete/clipping/commands/ShowAllClippingCommand.ts b/react-components/src/architecture/concrete/clipping/commands/ShowAllClippingCommand.ts new file mode 100644 index 00000000000..277371532cd --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/ShowAllClippingCommand.ts @@ -0,0 +1,61 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { InstanceCommand } from '../../../base/commands/InstanceCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { CropBoxDomainObject } from '../CropBoxDomainObject'; +import { SliceDomainObject } from '../SliceDomainObject'; + +export class ShowAllClippingCommand extends InstanceCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { + key: 'CLIP_SHOW_SELECTED_ONLY', + fallback: 'Show or hide all other slicing planes and crop boxes than selected' + }; + } + + public override get icon(): string { + return 'EyeShow'; + } + + public override get isChecked(): boolean { + return this.isAnyVisible(); + } + + protected override invokeCore(): boolean { + const isVisible = this.isAnyVisible(); + for (const domainObject of this.getInstances()) { + if (domainObject.isSelected) { + continue; + } + domainObject.setVisibleInteractive(!isVisible, this.renderTarget); + } + return true; + } + + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof CropBoxDomainObject || domainObject instanceof SliceDomainObject; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private isAnyVisible(): boolean { + for (const domainObject of this.getInstances()) { + if (domainObject.isSelected) { + continue; + } + if (domainObject.isVisible(this.renderTarget)) { + return true; + } + } + return false; + } +} diff --git a/react-components/src/architecture/concrete/clipping/commands/ShowClippingOnTopCommand.ts b/react-components/src/architecture/concrete/clipping/commands/ShowClippingOnTopCommand.ts new file mode 100644 index 00000000000..3faf9672216 --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/ShowClippingOnTopCommand.ts @@ -0,0 +1,23 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { ShowDomainObjectsOnTopCommand } from '../../../base/commands/ShowDomainObjectsOnTopCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { CropBoxDomainObject } from '../CropBoxDomainObject'; +import { SliceDomainObject } from '../SliceDomainObject'; + +export class ShowClippingOnTopCommand extends ShowDomainObjectsOnTopCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + return { key: 'CLIP_SHOW_ON_TOP', fallback: 'Show all crop boxes and slices on top' }; + } + + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof CropBoxDomainObject || domainObject instanceof SliceDomainObject; + } +} diff --git a/react-components/src/architecture/concrete/config/StoryBookConfig.ts b/react-components/src/architecture/concrete/config/StoryBookConfig.ts index 3bc83fb6b30..adee0bac141 100644 --- a/react-components/src/architecture/concrete/config/StoryBookConfig.ts +++ b/react-components/src/architecture/concrete/config/StoryBookConfig.ts @@ -11,13 +11,14 @@ import { UpdateTerrainCommand } from '../terrainDomainObject/UpdateTerrainComman import { FitViewCommand } from '../../base/concreteCommands/FitViewCommand'; import { SetAxisVisibleCommand } from '../axis/SetAxisVisibleCommand'; import { ExampleTool } from '../exampleDomainObject/ExampleTool'; -import { MeasurementTool } from '../boxDomainObject/MeasurementTool'; import { AxisGizmoTool } from '@cognite/reveal/tools'; import { BaseRevealConfig } from '../../base/renderTarget/BaseRevealConfig'; import { type RevealRenderTarget } from '../../base/renderTarget/RevealRenderTarget'; -import { NavigationTool } from '../../base/commands/NavigationTool'; +import { NavigationTool } from '../../base/concreteCommands/NavigationTool'; import { type BaseTool } from '../../base/commands/BaseTool'; import { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetricUnitsCommand'; +import { MeasurementTool } from '../measurements/MeasurementTool'; +import { ClipTool } from '../clipping/ClipTool'; export class StoryBookConfig extends BaseRevealConfig { // ================================================== @@ -39,6 +40,7 @@ export class StoryBookConfig extends BaseRevealConfig { undefined, new ExampleTool(), new MeasurementTool(), + new ClipTool(), undefined, new SetTerrainVisibleCommand(), new UpdateTerrainCommand() diff --git a/react-components/src/architecture/concrete/course/README.md b/react-components/src/architecture/concrete/course/README.md index 3d7902c1a87..4cc7071f443 100644 --- a/react-components/src/architecture/concrete/course/README.md +++ b/react-components/src/architecture/concrete/course/README.md @@ -206,7 +206,7 @@ The second thing you have to implement is in the view. You need to override the When we already do this, also add `Changes.renderStyle`, `Changes.color` and `Changes.geometry` since we need this changes later. The method `isChanged` takes multiple arguments. -Note that we clear the memory when it change. This is a convenient method to remove all redundant data. The next time some of the data is needed, it will be generated automatically (lazy creation). The method addChildren will automatically be run when needed. +Note that we clear the memory when it change. This is a convenient method to remove all redundant data. The next time some of the data is needed, it will be generated automatically (lazy creation). The method `addChildren` will automatically be run when needed. For large objects, where `addChildren` takes more time, you cannot do this. For instance, when the color change, you only need to update the material. If the geometry change, you need to update the geometry only. If the geometry is large, you can specify which part of geometry you need to update. This is not implemented yet, since we haven't seen any use cases for this, but is a part of this architecture. diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts index 8842ab26e70..c749e10cde3 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleDomainObject.ts @@ -3,7 +3,7 @@ */ import { ExampleRenderStyle } from './ExampleRenderStyle'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; import { type ThreeView } from '../../base/views/ThreeView'; import { ExampleView } from './ExampleView'; import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; @@ -16,6 +16,7 @@ import { PopupStyle } from '../../base/domainObjectsHelpers/PopupStyle'; import { type BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; import { ExampleDragger } from './ExampleDragger'; import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class ExampleDomainObject extends VisualDomainObject { // ================================================== @@ -40,8 +41,8 @@ export class ExampleDomainObject extends VisualDomainObject { return 'Circle'; } - public override get typeName(): string { - return 'Example'; + public override get typeName(): TranslateKey { + return { fallback: 'Example' }; } public override get canBeRemoved(): boolean { @@ -62,20 +63,20 @@ export class ExampleDomainObject extends VisualDomainObject { public override getPanelInfo(): PanelInfo | undefined { const info = new PanelInfo(); - info.setHeader('NAME', this.name); - add('XCOORDINATE', 'X coordinate', this.center.x, Quantity.Length); - add('YCOORDINATE', 'Y coordinate', this.center.y, Quantity.Length); - add('ZCOORDINATE', 'Z coordinate', this.center.z, Quantity.Length); + info.setHeader(this.typeName); + // In production code, you should add a Key also! + add('X coordinate', this.center.x, Quantity.Length); + add('Y coordinate', this.center.y, Quantity.Length); + add('Z coordinate', this.center.z, Quantity.Length); return info; - function add(key: string, fallback: string, value: number, quantity: Quantity): void { - info.add({ key, fallback, value, quantity }); + function add(fallback: string, value: number, quantity: Quantity): void { + info.add({ fallback, value, quantity }); } } public override getPanelInfoStyle(): PopupStyle { - // bottom = 66 because the toolbar is below - return new PopupStyle({ bottom: 66, left: 0 }); + return new PopupStyle({ bottom: 50, left: 0 }); } // ================================================== diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleRenderStyle.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleRenderStyle.ts index 652055e7b27..b261d245f9a 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleRenderStyle.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleRenderStyle.ts @@ -3,16 +3,16 @@ */ import { cloneDeep } from 'lodash'; -import { RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { CommonRenderStyle } from '../../base/renderStyles/CommonRenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; -export class ExampleRenderStyle extends RenderStyle { +export class ExampleRenderStyle extends CommonRenderStyle { // ================================================== // INSTANCE FIELDS // ================================================== public radius = 1; public opacity = 0.75; - public depthTest = true; // ================================================== // OVERRIDES of BaseStyle diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts index f75134629ff..57a4ceee67b 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts @@ -11,11 +11,11 @@ import { ResetAllExamplesCommand } from './commands/ResetAllExamplesCommand'; import { DeleteAllExamplesCommand } from './commands/DeleteAllExamplesCommand'; 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'; import { ShowExamplesOnTopCommand } from './commands/ShowExamplesOnTopCommand'; import { DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; +import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; export class ExampleTool extends BaseEditTool { // ================================================== @@ -27,7 +27,7 @@ export class ExampleTool extends BaseEditTool { } public override get tooltip(): TranslateKey { - return { key: 'EXAMPLE_EDIT', fallback: 'Create or edit a single point' }; + return { fallback: 'Create or edit a single point' }; } // ================================================== @@ -127,7 +127,7 @@ export class ExampleTool extends BaseEditTool { // OVERRIDES of BaseEditTool // ================================================== - protected override canBeSelected(domainObject: DomainObject): boolean { + protected override canBeSelected(domainObject: VisualDomainObject): boolean { return domainObject instanceof ExampleDomainObject; } } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleView.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleView.ts index de0780adda1..8df8fd2e8f1 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleView.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleView.ts @@ -35,7 +35,7 @@ export class ExampleView extends GroupThreeView { public override update(change: DomainObjectChange): void { super.update(change); - if (change.isChanged(Changes.selected, Changes.renderStyle, Changes.color)) { + if (change.isChanged(Changes.selected, Changes.renderStyle, Changes.color, Changes.clipping)) { this.clearMemory(); this.invalidateRenderTarget(); } @@ -50,7 +50,7 @@ export class ExampleView extends GroupThreeView { } protected override addChildren(): void { - const { domainObject, style } = this; + const { domainObject, style, renderTarget } = this; const geometry = new SphereGeometry(style.radius, 32, 16); const material = new MeshPhongMaterial({ @@ -62,6 +62,7 @@ export class ExampleView extends GroupThreeView { transparent: true, depthTest: style.depthTest }); + material.clippingPlanes = renderTarget.getGlobalClippingPlanes(); const sphere = new Mesh(geometry, material); const center = domainObject.center.clone(); center.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); @@ -88,7 +89,7 @@ export class ExampleView extends GroupThreeView { if (closestDistance !== undefined && closestDistance < distanceToCamera) { return undefined; } - if (!intersectInput.isVisible(point)) { + if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { return undefined; } const customObjectIntersection: DomainObjectIntersection = { diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts index f4857350952..03cc50e648f 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/DeleteAllExamplesCommand.ts @@ -2,17 +2,18 @@ * Copyright 2024 Cognite AS */ -import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { InstanceCommand } from '../../../base/commands/InstanceCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; import { type TranslateKey } from '../../../base/utilities/TranslateKey'; import { ExampleDomainObject } from '../ExampleDomainObject'; -export class DeleteAllExamplesCommand extends RenderTargetCommand { +export class DeleteAllExamplesCommand extends InstanceCommand { // ================================================== // OVERRIDES // ================================================== public override get tooltip(): TranslateKey { - return { key: 'EXAMPLES_DELETE', fallback: 'Remove all examples' }; + return { fallback: 'Remove all examples' }; } public override get icon(): string { @@ -24,12 +25,12 @@ export class DeleteAllExamplesCommand extends RenderTargetCommand { } public override get isEnabled(): boolean { - const first = this.getFirst(); + const first = this.getFirstInstance(); return first !== undefined && first.canBeRemoved; } protected override invokeCore(): boolean { - const array = Array.from(this.rootDomainObject.getDescendantsByType(ExampleDomainObject)); + const array = Array.from(this.getInstances()); array.reverse(); for (const domainObject of array) { domainObject.removeInteractive(); @@ -37,11 +38,7 @@ export class DeleteAllExamplesCommand extends RenderTargetCommand { return true; } - // ================================================== - // INSTANCE METHODS - // ================================================== - - private getFirst(): ExampleDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ExampleDomainObject); + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof ExampleDomainObject; } } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts index 99677cb9c05..d3aec323a80 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/ResetAllExamplesCommand.ts @@ -2,44 +2,34 @@ * Copyright 2024 Cognite AS */ -import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; import { ExampleDomainObject } from '../ExampleDomainObject'; import { Changes } from '../../../base/domainObjectsHelpers/Changes'; import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { InstanceCommand } from '../../../base/commands/InstanceCommand'; -export class ResetAllExamplesCommand extends RenderTargetCommand { +export class ResetAllExamplesCommand extends InstanceCommand { // ================================================== // OVERRIDES // ================================================== public override get tooltip(): TranslateKey { - return { - key: 'EXAMPLES_RESET', - fallback: 'Reset the visual style for all examples to default' - }; + return { fallback: 'Reset the visual style for all examples to default' }; } public override get icon(): string { return 'ClearAll'; } - public override get isEnabled(): boolean { - return this.getFirst() !== undefined; - } - protected override invokeCore(): boolean { - for (const domainObject of this.rootDomainObject.getDescendantsByType(ExampleDomainObject)) { + for (const domainObject of this.getInstances()) { domainObject.setRenderStyle(undefined); domainObject.notify(Changes.renderStyle); } return true; } - // ================================================== - // INSTANCE METHODS - // ================================================== - - private getFirst(): ExampleDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ExampleDomainObject); + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof ExampleDomainObject; } } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts index ebb0e65dde3..ea834e2bea4 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowAllExamplesCommand.ts @@ -2,53 +2,21 @@ * Copyright 2024 Cognite AS */ -import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { ShowAllDomainObjectsCommand } from '../../../base/commands/ShowAllDomainObjectsCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; import { type TranslateKey } from '../../../base/utilities/TranslateKey'; import { ExampleDomainObject } from '../ExampleDomainObject'; -export class ShowAllExamplesCommand extends RenderTargetCommand { +export class ShowAllExamplesCommand extends ShowAllDomainObjectsCommand { // ================================================== // OVERRIDES // ================================================== public override get tooltip(): TranslateKey { - return { key: 'EXAMPLES_SHOW', fallback: 'Show or hide all examples' }; + return { fallback: 'Show or hide all examples' }; } - public override get icon(): string { - return 'EyeShow'; - } - - public override get isEnabled(): boolean { - return this.getFirst() !== undefined; - } - - public override get isChecked(): boolean { - return this.isAnyVisible(); - } - - protected override invokeCore(): boolean { - const isVisible = this.isAnyVisible(); - for (const domainObject of this.rootDomainObject.getDescendantsByType(ExampleDomainObject)) { - domainObject.setVisibleInteractive(!isVisible, this.renderTarget); - } - return true; - } - - // ================================================== - // INSTANCE METHODS - // ================================================== - - private isAnyVisible(): boolean { - for (const descendant of this.rootDomainObject.getDescendantsByType(ExampleDomainObject)) { - if (descendant.isVisible(this.renderTarget)) { - return true; - } - } - return false; - } - - private getFirst(): ExampleDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ExampleDomainObject); + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof ExampleDomainObject; } } diff --git a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowExamplesOnTopCommand.ts b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowExamplesOnTopCommand.ts index b82521760fe..e478e056130 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowExamplesOnTopCommand.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/commands/ShowExamplesOnTopCommand.ts @@ -2,60 +2,21 @@ * Copyright 2024 Cognite AS */ -import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; -import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { ShowDomainObjectsOnTopCommand } from '../../../base/commands/ShowDomainObjectsOnTopCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; import { type TranslateKey } from '../../../base/utilities/TranslateKey'; import { ExampleDomainObject } from '../ExampleDomainObject'; -export class ShowExamplesOnTopCommand extends RenderTargetCommand { +export class ShowExamplesOnTopCommand extends ShowDomainObjectsOnTopCommand { // ================================================== // OVERRIDES // ================================================== public override get tooltip(): TranslateKey { - return { key: 'EXAMPLES_SHOW_ON_TOP', fallback: 'Show all examples on top' }; + return { fallback: 'Show all examples on top' }; } - public override get icon(): string { - return 'Flag'; - } - - public override get isEnabled(): boolean { - return this.getFirstVisible() !== undefined; - } - - public override get isChecked(): boolean { - return !this.getDepthTest(); - } - - protected override invokeCore(): boolean { - const depthTest = this.getDepthTest(); - for (const domainObject of this.rootDomainObject.getDescendantsByType(ExampleDomainObject)) { - const style = domainObject.renderStyle; - style.depthTest = !depthTest; - domainObject.notify(Changes.renderStyle); - } - return true; - } - - // ================================================== - // INSTANCE METHODS - // ================================================== - - private getDepthTest(): boolean { - const domainObject = this.getFirstVisible(); - if (domainObject === undefined) { - return false; - } - return domainObject.renderStyle.depthTest; - } - - private getFirstVisible(): ExampleDomainObject | undefined { - for (const descendant of this.rootDomainObject.getDescendantsByType(ExampleDomainObject)) { - if (descendant.isVisible(this.renderTarget)) { - return descendant; - } - } - return undefined; + protected override isInstance(domainObject: DomainObject): boolean { + return domainObject instanceof ExampleDomainObject; } } diff --git a/react-components/src/architecture/concrete/gizmoBox/GizmoBoxDomainObject.ts b/react-components/src/architecture/concrete/gizmoBox/GizmoBoxDomainObject.ts new file mode 100644 index 00000000000..32d1663094f --- /dev/null +++ b/react-components/src/architecture/concrete/gizmoBox/GizmoBoxDomainObject.ts @@ -0,0 +1,41 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { BoxDomainObject } from '../primitives/box/BoxDomainObject'; +import { Color, type Vector3 } from 'three'; +import { BoxRenderStyle } from '../primitives/box/BoxRenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; + +export class GizmoBoxDomainObject extends BoxDomainObject { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(center: Vector3, size: Vector3, zRotation: number) { + super(); + this.center.copy(center); + this.size.copy(size); + this.zRotation = zRotation; + this.color = new Color(Color.NAMES.white); + } + + // ================================================== + // OVERRIDES of DomainObject + // ================================================== + + public override get typeName(): TranslateKey { + return { fallback: 'Gizmo box' }; + } + + public override createRenderStyle(): RenderStyle | undefined { + const style = new BoxRenderStyle(); + style.showLabel = false; + return style; + } + + public override get hasPanelInfo(): boolean { + return false; + } +} diff --git a/react-components/src/architecture/concrete/measurements/MeasureBoxDomainObject.ts b/react-components/src/architecture/concrete/measurements/MeasureBoxDomainObject.ts new file mode 100644 index 00000000000..4048cd3aeca --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/MeasureBoxDomainObject.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type PrimitiveType } from '../primitives/PrimitiveType'; +import { BoxDomainObject } from '../primitives/box/BoxDomainObject'; +import { Color } from 'three'; + +export class MeasureBoxDomainObject extends BoxDomainObject { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(primitiveType); + this.color = new Color(Color.NAMES.magenta); + } +} diff --git a/react-components/src/architecture/concrete/measurements/MeasureLineDomainObject.ts b/react-components/src/architecture/concrete/measurements/MeasureLineDomainObject.ts new file mode 100644 index 00000000000..06a9a1d8e53 --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/MeasureLineDomainObject.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type PrimitiveType } from '../primitives/PrimitiveType'; +import { LineDomainObject } from '../primitives/line/LineDomainObject'; +import { Color } from 'three'; + +export class MeasureLineDomainObject extends LineDomainObject { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(primitiveType); + this.color = new Color(Color.NAMES.red); + } +} diff --git a/react-components/src/architecture/concrete/measurements/MeasurementTool.ts b/react-components/src/architecture/concrete/measurements/MeasurementTool.ts new file mode 100644 index 00000000000..9d0171dab50 --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/MeasurementTool.ts @@ -0,0 +1,98 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type BaseCommand } from '../../base/commands/BaseCommand'; +import { type BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; +import { ShowMeasurementsOnTopCommand } from './commands/ShowMeasurementsOnTopCommand'; +import { SetMeasurementTypeCommand } from './commands/SetMeasurementTypeCommand'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetricUnitsCommand'; +import { PrimitiveEditTool } from '../primitives/PrimitiveEditTool'; +import { MeasureLineDomainObject } from './MeasureLineDomainObject'; +import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; +import { PrimitiveType } from '../primitives/PrimitiveType'; +import { BoxCreator } from '../primitives/box/BoxCreator'; +import { LineCreator } from '../primitives/line/LineCreator'; +import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; + +export class MeasurementTool extends PrimitiveEditTool { + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor() { + super(PrimitiveType.None); + } + + // ================================================== + // OVERRIDES of BaseCommand + // ================================================== + + public override get icon(): string { + return 'Ruler'; + } + + public override get tooltip(): TranslateKey { + return { key: 'MEASUREMENTS', fallback: 'Measurements' }; + } + + public override getToolbar(): Array { + return [ + new SetMeasurementTypeCommand(PrimitiveType.Line), + new SetMeasurementTypeCommand(PrimitiveType.Polyline), + new SetMeasurementTypeCommand(PrimitiveType.Polygon), + new SetMeasurementTypeCommand(PrimitiveType.HorizontalArea), + new SetMeasurementTypeCommand(PrimitiveType.VerticalArea), + new SetMeasurementTypeCommand(PrimitiveType.Box), + undefined, // Separator + new ToggleMetricUnitsCommand(), + new ShowMeasurementsOnTopCommand() + ]; + } + + // ================================================== + // OVERRIDES of BaseTool + // ================================================== + + public override onActivate(): void { + super.onActivate(); + this.setAllVisible(true); + } + + public override onDeactivate(): void { + super.onDeactivate(); + this.setAllVisible(false); + } + + // ================================================== + // OVERRIDES of BaseEditTool + // ================================================== + + protected override canBeSelected(domainObject: VisualDomainObject): boolean { + return ( + domainObject instanceof MeasureBoxDomainObject || + domainObject instanceof MeasureLineDomainObject + ); + } + + // ================================================== + // OVERRIDES of BoxOrLineEditTool + // ================================================== + + protected override createCreator(): BaseCreator | undefined { + switch (this.primitiveType) { + case PrimitiveType.Line: + case PrimitiveType.Polyline: + case PrimitiveType.Polygon: + return new LineCreator(new MeasureLineDomainObject(this.primitiveType)); + + case PrimitiveType.HorizontalArea: + case PrimitiveType.VerticalArea: + case PrimitiveType.Box: + return new BoxCreator(new MeasureBoxDomainObject(this.primitiveType)); + default: + return undefined; + } + } +} diff --git a/react-components/src/architecture/concrete/measurements/commands/SetMeasurementTypeCommand.ts b/react-components/src/architecture/concrete/measurements/commands/SetMeasurementTypeCommand.ts new file mode 100644 index 00000000000..2895dfc552d --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/commands/SetMeasurementTypeCommand.ts @@ -0,0 +1,124 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { type BaseCommand } from '../../../base/commands/BaseCommand'; +import { PrimitiveType } from '../../primitives/PrimitiveType'; +import { getIconByPrimitiveType } from '../getIconByPrimitiveType'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { MeasurementTool } from '../MeasurementTool'; + +export class SetMeasurementTypeCommand extends RenderTargetCommand { + private readonly _primitiveType: PrimitiveType; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(); + this._primitiveType = primitiveType; + } + + // ================================================== + // OVERRIDES of BaseCommand + // ================================================== + + public override get icon(): string { + return getIconByPrimitiveType(this._primitiveType); + } + + public override get tooltip(): TranslateKey { + return getTooltipByPrimitiveType(this._primitiveType); + } + + public override get isEnabled(): boolean { + return this.tool !== undefined; + } + + public override get isChecked(): boolean { + const { tool } = this; + if (tool === undefined) { + return false; + } + return tool.primitiveType === this._primitiveType; + } + + protected override invokeCore(): boolean { + const { tool } = this; + if (tool === undefined) { + return false; + } + tool.handleEscape(); + tool.clearDragging(); + if (tool.primitiveType === this._primitiveType) { + tool.primitiveType = PrimitiveType.None; + } else { + tool.primitiveType = this._primitiveType; + } + return true; + } + + public override equals(other: BaseCommand): boolean { + if (!(other instanceof SetMeasurementTypeCommand)) { + return false; + } + return this._primitiveType === other._primitiveType; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private get tool(): MeasurementTool | undefined { + const activeTool = this.renderTarget.commandsController.activeTool; + if (!(activeTool instanceof MeasurementTool)) { + return undefined; + } + return activeTool; + } +} + +// ================================================== +// PRIMATE FUNCTIONS +// ================================================== + +function getTooltipByPrimitiveType(primitiveType: PrimitiveType): TranslateKey { + switch (primitiveType) { + case PrimitiveType.Line: + return { + key: 'MEASUREMENTS_ADD_LINE', + fallback: 'Measure distance between two points. Click at the start point and the end point.' + }; + case PrimitiveType.Polyline: + return { + key: 'MEASUREMENTS_ADD_POLYLINE', + fallback: + 'Measure the length of a continuous polyline. Click at any number of points and end with Esc.' + }; + case PrimitiveType.Polygon: + return { + key: 'MEASUREMENTS_ADD_POLYGON', + fallback: 'Measure an area of a polygon. Click at least 3 points and end with Esc.' + }; + case PrimitiveType.VerticalArea: + return { + key: 'MEASUREMENTS_ADD_VERTICAL_AREA', + fallback: 'Measure rectangular vertical Area. Click at two points in a vertical plan.' + }; + case PrimitiveType.HorizontalArea: + return { + key: 'MEASUREMENTS_ADD_HORIZONTAL_AREA', + fallback: 'Measure rectangular horizontal Area. Click at three points in a horizontal plan.' + }; + case PrimitiveType.Box: + return { + key: 'MEASUREMENTS_ADD_VOLUME', + fallback: + 'Measure volume of a box. Click at three points in a horizontal plan and the fourth to give it height.' + }; + default: + throw new Error('Unknown PrimitiveType'); + } +} diff --git a/react-components/src/architecture/concrete/measurements/commands/ShowMeasurementsOnTopCommand.ts b/react-components/src/architecture/concrete/measurements/commands/ShowMeasurementsOnTopCommand.ts new file mode 100644 index 00000000000..716198e4b88 --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/commands/ShowMeasurementsOnTopCommand.ts @@ -0,0 +1,30 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { ShowDomainObjectsOnTopCommand } from '../../../base/commands/ShowDomainObjectsOnTopCommand'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { MeasureBoxDomainObject } from '../MeasureBoxDomainObject'; +import { MeasureLineDomainObject } from '../MeasureLineDomainObject'; + +export class ShowMeasurementsOnTopCommand extends ShowDomainObjectsOnTopCommand { + // ================================================== + // OVERRIDES + // ================================================== + + public override get icon(): string { + return 'EyeShow'; + } + + public override get tooltip(): TranslateKey { + return { key: 'MEASUREMENTS_SHOW_ON_TOP', fallback: 'Show all measurements on top' }; + } + + protected override isInstance(domainObject: DomainObject): boolean { + return ( + domainObject instanceof MeasureBoxDomainObject || + domainObject instanceof MeasureLineDomainObject + ); + } +} diff --git a/react-components/src/architecture/concrete/measurements/getIconByPrimitiveType.ts b/react-components/src/architecture/concrete/measurements/getIconByPrimitiveType.ts new file mode 100644 index 00000000000..192254f7030 --- /dev/null +++ b/react-components/src/architecture/concrete/measurements/getIconByPrimitiveType.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { PrimitiveType } from '../primitives/PrimitiveType'; + +export function getIconByPrimitiveType(primitiveType: PrimitiveType): string { + switch (primitiveType) { + case PrimitiveType.Line: + return 'VectorLine'; + case PrimitiveType.Polyline: + return 'VectorZigzag'; + case PrimitiveType.Polygon: + return 'Polygon'; + case PrimitiveType.PlaneX: + return 'CubeFrontLeft'; + case PrimitiveType.PlaneY: + return 'CubeFrontRight'; + case PrimitiveType.PlaneZ: + return 'CubeTop'; + case PrimitiveType.PlaneXY: + return 'Perspective'; + case PrimitiveType.HorizontalArea: + return 'FrameTool'; + case PrimitiveType.VerticalArea: + return 'Perspective'; + case PrimitiveType.Box: + return 'Cube'; + default: + throw new Error('Unknown PrimitiveType'); + } +} diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts b/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts similarity index 54% rename from react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts rename to react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts index 4ebeee6c2ff..95f716539c8 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts +++ b/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts @@ -2,80 +2,47 @@ * Copyright 2024 Cognite AS */ -import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; -import { type AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; -import { type BaseCommand } from '../../base/commands/BaseCommand'; +import { CDF_TO_VIEWER_TRANSFORMATION, type CustomObjectIntersection } from '@cognite/reveal'; import { isDomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; import { type Vector3 } from 'three'; -import { MeasureBoxCreator } from './MeasureBoxCreator'; -import { MeasureType } from './MeasureType'; +import { PrimitiveType } from './PrimitiveType'; import { type BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; -import { MeasureLineCreator } from './MeasureLineCreator'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; -import { MeasureLineDomainObject } from './MeasureLineDomainObject'; -import { MeasureRenderStyle } from './MeasureRenderStyle'; import { type DomainObject } from '../../base/domainObjects/DomainObject'; -import { MeasureDomainObject } from './MeasureDomainObject'; -import { ShowMeasurementsOnTopCommand } from './ShowMeasurementsOnTopCommand'; -import { SetMeasurementTypeCommand } from './SetMeasurementTypeCommand'; -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 { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetricUnitsCommand'; +import { BoxDomainObject } from './box/BoxDomainObject'; +import { LineDomainObject } from './line/LineDomainObject'; +import { CommonRenderStyle } from '../../base/renderStyles/CommonRenderStyle'; +import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { PlaneDomainObject } from './plane/PlaneDomainObject'; -export class MeasurementTool extends BaseEditTool { +export abstract class PrimitiveEditTool extends BaseEditTool { // ================================================== // INSTANCE FIELDS // ================================================== private _creator: BaseCreator | undefined = undefined; - public measureType: MeasureType = MeasureType.None; // Default none, let the user decide + public primitiveType: PrimitiveType; + public defaultPrimitiveType: PrimitiveType; // ================================================== - // OVERRIDES of BaseCommand + // CONSTRUCTOR // ================================================== - public override get icon(): string { - return 'Ruler'; - } - - public override get tooltip(): TranslateKey { - return { key: 'MEASUREMENTS', fallback: 'Measurements' }; - } - - public override getToolbar(): Array { - return [ - new SetMeasurementTypeCommand(MeasureType.Line), - new SetMeasurementTypeCommand(MeasureType.Polyline), - new SetMeasurementTypeCommand(MeasureType.Polygon), - new SetMeasurementTypeCommand(MeasureType.HorizontalArea), - new SetMeasurementTypeCommand(MeasureType.VerticalArea), - new SetMeasurementTypeCommand(MeasureType.Volume), - undefined, // Separator - new ToggleMetricUnitsCommand(), - new ShowMeasurementsOnTopCommand() - ]; - } - - public override getToolbarStyle(): PopupStyle { - return new PopupStyle({ bottom: 0, left: 0 }); + protected constructor(primitiveType: PrimitiveType = PrimitiveType.None) { + super(); + this.defaultPrimitiveType = primitiveType; + this.primitiveType = this.defaultPrimitiveType; } // ================================================== // OVERRIDES of BaseTool // ================================================== - public override onActivate(): void { - super.onActivate(); - this.setAllMeasurementsVisible(true); - } - public override onDeactivate(): void { this.handleEscape(); - this.setAllMeasurementsVisible(false); super.onDeactivate(); } @@ -101,7 +68,7 @@ export class MeasurementTool extends BaseEditTool { public override async onHover(event: PointerEvent): Promise { // Handle when creator is set first - if (this.measureType !== MeasureType.None && this._creator !== undefined) { + if (this.primitiveType !== PrimitiveType.None && this._creator !== undefined) { const { _creator: creator } = this; if (!creator.preferIntersection) { // Hover in the "air" @@ -124,7 +91,7 @@ export class MeasurementTool extends BaseEditTool { this.renderTarget.setNavigateCursor(); return; } - if (this.getMeasurement(intersection) !== undefined) { + if (this.getIntersectedSelectableDomainObject(intersection) !== undefined) { this.renderTarget.setNavigateCursor(); return; } @@ -137,46 +104,30 @@ export class MeasurementTool extends BaseEditTool { return; } const intersection = await this.getIntersection(event); - const domainObject = this.getMeasurement(intersection); + const domainObject = this.getIntersectedSelectableDomainObject(intersection); if (!isDomainObjectIntersection(intersection) || domainObject === undefined) { this.defocusAll(); - if (this.measureType === MeasureType.None || intersection === undefined) { + if (this.primitiveType === PrimitiveType.None || intersection === undefined) { this.renderTarget.setNavigateCursor(); } else { this.setDefaultCursor(); } return; } - // Set focus on the hovered object - if (domainObject instanceof MeasureLineDomainObject) { - this.defocusAll(domainObject); - domainObject.setFocusInteractive(FocusType.Focus); - this.renderTarget.setDefaultCursor(); - } else if (domainObject instanceof MeasureBoxDomainObject) { - const pickInfo = intersection.userData as BoxPickInfo; - if (pickInfo === undefined) { - this.defocusAll(); - this.renderTarget.setDefaultCursor(); - return; - } - this.setCursor(domainObject, intersection.point, pickInfo); - this.defocusAll(domainObject); - domainObject.setFocusInteractive(pickInfo.focusType, pickInfo.face); - } + this.setFocus(domainObject, intersection); } public override async onClick(event: PointerEvent): Promise { - const { renderTarget, rootDomainObject } = this; + const { renderTarget } = this; + let creator = this._creator; - const { _creator: creator } = this; // Click in the "air" if (creator !== undefined && !creator.preferIntersection) { const ray = this.getRay(event); if (creator.addPoint(ray, undefined)) { if (creator.isFinished) { this._creator = undefined; - this.measureType = MeasureType.None; - CommandsUpdater.update(renderTarget); + this.setDefaultPrimitiveType(); } return; } @@ -186,50 +137,62 @@ export class MeasurementTool extends BaseEditTool { // Click in the "air" return; } - const measurement = this.getMeasurement(intersection); - if (measurement !== undefined) { - this.deselectAll(measurement); - measurement.setSelectedInteractive(true); + if (creator !== undefined) { + const ray = this.getRay(event); + if (creator.addPoint(ray, intersection)) { + if (creator.isFinished) { + this._creator = undefined; + this.setDefaultPrimitiveType(); + } + } + return; + } + const domainObject = this.getIntersectedSelectableDomainObject(intersection); + if (domainObject !== undefined) { + this.deselectAll(domainObject); + domainObject.setSelectedInteractive(true); return; } const ray = this.getRay(event); if (creator === undefined) { - const creator = (this._creator = createCreator(this.measureType)); + creator = this._creator = this.createCreator(); if (creator === undefined) { return; } if (creator.addPoint(ray, intersection)) { const { domainObject } = creator; - initializeStyle(domainObject, this.rootDomainObject); + this.initializeStyle(domainObject); this.deselectAll(); - rootDomainObject.addChildInteractive(domainObject); + + const parent = this.getOrCreateParent(); + parent.addChildInteractive(domainObject); domainObject.setSelectedInteractive(true); domainObject.setVisibleInteractive(true, renderTarget); } - } else { - if (creator.addPoint(ray, intersection)) { - if (creator.isFinished) { - this.measureType = MeasureType.None; - CommandsUpdater.update(renderTarget); - this._creator = undefined; - } - } + } + if (creator !== undefined && creator.isFinished) { + this.setDefaultPrimitiveType(); + this._creator = undefined; } } - public override async onPointerDown(event: PointerEvent, leftButton: boolean): Promise { + public override async onLeftPointerDown(event: PointerEvent): Promise { if (this._creator !== undefined) { return; // Prevent dragging while creating the new } - await super.onPointerDown(event, leftButton); + await super.onLeftPointerDown(event); } // ================================================== - // OVERRIDES of BaseEditTool + // VIRTUAL METHODS // ================================================== - protected override canBeSelected(domainObject: DomainObject): boolean { - return domainObject instanceof MeasureDomainObject; + protected createCreator(): BaseCreator | undefined { + return undefined; + } + + protected getOrCreateParent(): DomainObject { + return this.rootDomainObject; } // ================================================== @@ -241,37 +204,13 @@ export class MeasurementTool extends BaseEditTool { return; } if (this._creator.handleEscape()) { - // Successfully created, set it back to none - this.measureType = MeasureType.None; - CommandsUpdater.update(this.renderTarget); + // Successfully created, set it back to default + this.setDefaultPrimitiveType(); } this._creator = undefined; } - private setAllMeasurementsVisible(visible: boolean): void { - for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { - domainObject.setVisibleInteractive(visible, this.renderTarget); - } - } - - private getMeasurement( - intersection: AnyIntersection | undefined - ): MeasureDomainObject | undefined { - if (!isDomainObjectIntersection(intersection)) { - return undefined; - } - if (intersection.domainObject instanceof MeasureDomainObject) { - return intersection.domainObject; - } else { - return undefined; - } - } - - private setCursor( - boxDomainObject: MeasureBoxDomainObject, - point: Vector3, - pickInfo: BoxPickInfo - ): void { + private setCursor(boxDomainObject: BoxDomainObject, point: Vector3, pickInfo: BoxPickInfo): void { if (pickInfo.focusType === FocusType.Body) { this.renderTarget.setMoveCursor(); } else if (pickInfo.focusType === FocusType.Face) { @@ -305,51 +244,71 @@ export class MeasurementTool extends BaseEditTool { } } + private setFocus(domainObject: VisualDomainObject, intersection: CustomObjectIntersection): void { + if (domainObject instanceof LineDomainObject) { + this.defocusAll(domainObject); + domainObject.setFocusInteractive(FocusType.Focus); + this.renderTarget.setDefaultCursor(); + } else if (domainObject instanceof PlaneDomainObject) { + this.defocusAll(domainObject); + domainObject.setFocusInteractive(FocusType.Focus); + this.renderTarget.setMoveCursor(); + } else if (domainObject instanceof BoxDomainObject) { + const pickInfo = intersection.userData as BoxPickInfo; + if (pickInfo === undefined) { + this.defocusAll(); + this.renderTarget.setDefaultCursor(); + return; + } + this.setCursor(domainObject, intersection.point, pickInfo); + this.defocusAll(domainObject); + domainObject.setFocusInteractive(pickInfo.focusType, pickInfo.face); + } + } + protected defocusAll(except?: DomainObject | undefined): void { - for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { + for (const domainObject of this.getSelectable()) { if (except !== undefined && domainObject === except) { continue; } - if (domainObject instanceof MeasureLineDomainObject) { + if (domainObject instanceof LineDomainObject) { domainObject.setFocusInteractive(FocusType.None); - } - if (domainObject instanceof MeasureBoxDomainObject) { + } else if (domainObject instanceof PlaneDomainObject) { + domainObject.setFocusInteractive(FocusType.None); + } else if (domainObject instanceof BoxDomainObject) { domainObject.setFocusInteractive(FocusType.None); } } } -} -// ================================================== -// PRIVATE FUNCTIONS -// ================================================== - -function initializeStyle(domainObject: DomainObject, rootDomainObject: RootDomainObject): void { - // Just copy the style the depthTest field from any other MeasureDomainObject - const otherDomainObject = rootDomainObject.getDescendantByType(MeasureDomainObject); - if (otherDomainObject === undefined) { - return; - } - const otherStyle = otherDomainObject.renderStyle; - const style = domainObject.getRenderStyle(); - if (!(style instanceof MeasureRenderStyle)) { - return; + private initializeStyle(domainObject: DomainObject): void { + // Just copy the style the depthTest field from any other selectable + const depthTest = this.getDepthTestOnFirstSelectable(); + if (depthTest === undefined) { + return; + } + const style = domainObject.getRenderStyle(); + if (!(style instanceof CommonRenderStyle)) { + return; + } + style.depthTest = depthTest; } - style.depthTest = otherStyle.depthTest; -} -function createCreator(measureType: MeasureType): BaseCreator | undefined { - switch (measureType) { - case MeasureType.Line: - case MeasureType.Polyline: - case MeasureType.Polygon: - return new MeasureLineCreator(measureType); + private getDepthTestOnFirstSelectable(): boolean | undefined { + for (const otherDomainObject of this.getSelectable()) { + const otherStyle = otherDomainObject.getRenderStyle(); + if (otherStyle instanceof CommonRenderStyle) { + return otherStyle.depthTest; + } + } + return undefined; + } - case MeasureType.HorizontalArea: - case MeasureType.VerticalArea: - case MeasureType.Volume: - return new MeasureBoxCreator(measureType); - default: - return undefined; + private setDefaultPrimitiveType(): void { + if (this.primitiveType === this.defaultPrimitiveType) { + return; + } + this.primitiveType = this.defaultPrimitiveType; + CommandsUpdater.update(this.renderTarget); } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts b/react-components/src/architecture/concrete/primitives/PrimitiveRenderStyle.ts similarity index 52% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts rename to react-components/src/architecture/concrete/primitives/PrimitiveRenderStyle.ts index 5fd0bab3f0e..0b3959ddc6e 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts +++ b/react-components/src/architecture/concrete/primitives/PrimitiveRenderStyle.ts @@ -3,19 +3,22 @@ */ import { ColorType } from '../../base/domainObjectsHelpers/ColorType'; -import { RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; import { Color } from 'three'; import { WHITE_COLOR } from '../../base/utilities/colors/colorExtensions'; +import { CommonRenderStyle } from '../../base/renderStyles/CommonRenderStyle'; -export abstract class MeasureRenderStyle extends RenderStyle { +export abstract class PrimitiveRenderStyle extends CommonRenderStyle { // ================================================== // INSTANCE FIELDS // ================================================== - public depthTest = true; + // For the object itself public colorType = ColorType.Specified; - public textColor = WHITE_COLOR.clone(); - public textBgColor = new Color('#232323'); - public textOpacity = 0.9; - public relativeTextSize = 0.05; // Relative to diagonal of the measurement object for box and average of lenght of line segments for line + + // For labels only + public showLabel = true; + public labelColor = WHITE_COLOR.clone(); + public labelBgColor = new Color('#232323'); + public labelOpacity = 0.9; + public relativeTextSize = 0.05; // Relative to diagonal of the object for box and average of length of line segments for line } diff --git a/react-components/src/architecture/concrete/primitives/PrimitiveType.ts b/react-components/src/architecture/concrete/primitives/PrimitiveType.ts new file mode 100644 index 00000000000..fa5069820ed --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/PrimitiveType.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2024 Cognite AS + */ + +export enum PrimitiveType { + None, + Line, + Polyline, + Polygon, + HorizontalArea, + VerticalArea, + Box, + PlaneX, + PlaneY, + PlaneZ, + PlaneXY +} diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts b/react-components/src/architecture/concrete/primitives/box/BoxCreator.ts similarity index 69% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts rename to react-components/src/architecture/concrete/primitives/box/BoxCreator.ts index 699126cc9c1..6cf6b01af04 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts +++ b/react-components/src/architecture/concrete/primitives/box/BoxCreator.ts @@ -6,35 +6,35 @@ import { Matrix4, Plane, type Ray, Vector3 } from 'three'; import { horizontalAngle, verticalDistanceTo -} from '../../base/utilities/extensions/vectorExtensions'; -import { Range3 } from '../../base/utilities/geometry/Range3'; -import { forceBetween0AndPi } from '../../base/utilities/extensions/mathExtensions'; -import { MeasureBoxDomainObject } from './MeasureBoxDomainObject'; -import { MeasureType } from './MeasureType'; -import { getClosestPointOnLine } from '../../base/utilities/extensions/rayExtensions'; -import { BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; -import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type DomainObject } from '../../base/domainObjects/DomainObject'; +} from '../../../base/utilities/extensions/vectorExtensions'; +import { Range3 } from '../../../base/utilities/geometry/Range3'; +import { forceBetween0AndPi } from '../../../base/utilities/extensions/mathExtensions'; +import { PrimitiveType } from '../PrimitiveType'; +import { getClosestPointOnLine } from '../../../base/utilities/extensions/rayExtensions'; +import { BaseCreator } from '../../../base/domainObjectsHelpers/BaseCreator'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { type BoxDomainObject } from './BoxDomainObject'; const UP_VECTOR = new Vector3(0, 0, 1); /** - * Helper class for generate a MeasureBoxDomainObject by clicking around + * Helper class for generate a BoxDomainObject by clicking around */ -export class MeasureBoxCreator extends BaseCreator { +export class BoxCreator extends BaseCreator { // ================================================== // INSTANCE FIELDS // ================================================== - private readonly _domainObject: MeasureBoxDomainObject; + private readonly _domainObject: BoxDomainObject; // ================================================== // CONSTRUCTOR // ================================================== - constructor(measureType: MeasureType) { + constructor(domainObject: BoxDomainObject) { super(); - this._domainObject = new MeasureBoxDomainObject(measureType); + this._domainObject = domainObject; this._domainObject.focusType = FocusType.Pending; } @@ -51,15 +51,15 @@ export class MeasureBoxCreator extends BaseCreator { } public override get maximumPointCount(): number { - switch (this._domainObject.measureType) { - case MeasureType.VerticalArea: + switch (this._domainObject.primitiveType) { + case PrimitiveType.VerticalArea: return 2; - case MeasureType.HorizontalArea: + case PrimitiveType.HorizontalArea: return 3; - case MeasureType.Volume: + case PrimitiveType.Box: return 4; default: - throw new Error('Unknown measurement type'); + throw new Error('Unknown primitiveType'); } } @@ -69,7 +69,7 @@ export class MeasureBoxCreator extends BaseCreator { isPending: boolean ): boolean { const domainObject = this._domainObject; - point = this.recalculatePoint(point, ray, domainObject.measureType); + point = this.recalculatePoint(point, ray, domainObject.primitiveType); if (point === undefined) { return false; } @@ -79,20 +79,11 @@ export class MeasureBoxCreator extends BaseCreator { } domainObject.notify(Changes.geometry); if (this.isFinished) { - domainObject.setSelectedInteractive(true); domainObject.setFocusInteractive(FocusType.Focus); } return true; } - public override handleEscape(): boolean { - if (this.notPendingPointCount >= this.minimumPointCount) { - return true; // Successfully - } - this._domainObject.removeInteractive(); - return false; // Removed - } - // ================================================== // INSTANCE METHODS // ================================================== @@ -100,18 +91,18 @@ export class MeasureBoxCreator extends BaseCreator { private recalculatePoint( point: Vector3 | undefined, ray: Ray, - measureType: MeasureType + primitiveType: PrimitiveType ): Vector3 | undefined { - if (measureType === MeasureType.VerticalArea) { + if (primitiveType === PrimitiveType.VerticalArea) { return point; } - // Recalculate the point anywhy for >= 1 points + // Recalculate the point anyway for >= 1 points // This makes it more natural and you can pick in empty space if (this.notPendingPointCount === 1 || this.notPendingPointCount === 2) { const plane = new Plane().setFromNormalAndCoplanarPoint(UP_VECTOR, this.firstPoint); const newPoint = ray.intersectPlane(plane, new Vector3()); return newPoint ?? undefined; - } else if (this.notPendingPointCount === 3 && measureType === MeasureType.Volume) { + } else if (this.notPendingPointCount === 3 && primitiveType === PrimitiveType.Box) { return getClosestPointOnLine(ray, UP_VECTOR, this.points[2], point); } return point; @@ -132,7 +123,7 @@ export class MeasureBoxCreator extends BaseCreator { if (this.pointCount === 1) { domainObject.forceMinSize(); domainObject.center.copy(this.firstPoint); - if (domainObject.measureType !== MeasureType.VerticalArea) { + if (domainObject.primitiveType !== PrimitiveType.VerticalArea) { domainObject.center.z += domainObject.size.z / 2; } return true; @@ -142,7 +133,7 @@ export class MeasureBoxCreator extends BaseCreator { const vector = new Vector3().subVectors(this.firstPoint, this.lastPoint); domainObject.zRotation = forceBetween0AndPi(horizontalAngle(vector)); } - const measureType = domainObject.measureType; + const primitiveType = domainObject.primitiveType; if (this.pointCount <= 3) { // Set the center and the size only in 2D space const newCenter = new Vector3(); @@ -152,7 +143,7 @@ export class MeasureBoxCreator extends BaseCreator { domainObject.center.x = newCenter.x; domainObject.size.x = newSize.x; - if (measureType === MeasureType.VerticalArea) { + if (primitiveType === PrimitiveType.VerticalArea) { domainObject.center.z = newCenter.z; domainObject.center.y = newCenter.y; domainObject.size.z = newSize.z; diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts b/react-components/src/architecture/concrete/primitives/box/BoxDomainObject.ts similarity index 51% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts rename to react-components/src/architecture/concrete/primitives/box/BoxDomainObject.ts index 2817436b7a9..b288207cf05 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts +++ b/react-components/src/architecture/concrete/primitives/box/BoxDomainObject.ts @@ -2,30 +2,32 @@ * Copyright 2024 Cognite AS */ -import { MeasureBoxRenderStyle } from './MeasureBoxRenderStyle'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; -import { type ThreeView } from '../../base/views/ThreeView'; -import { MeasureBoxView } from './MeasureBoxView'; -import { Box3, Color, Matrix4, Vector3 } from 'three'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { BoxFace } from '../../base/utilities/box/BoxFace'; -import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; -import { MeasureType } from './MeasureType'; -import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; -import { type BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; -import { MeasureBoxDragger } from './MeasureBoxDragger'; -import { MeasureDomainObject } from './MeasureDomainObject'; -import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { BoxRenderStyle } from './BoxRenderStyle'; +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; +import { type ThreeView } from '../../../base/views/ThreeView'; +import { BoxView } from './BoxView'; +import { Box3, Matrix4, Vector3 } from 'three'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { BoxFace } from '../../../base/utilities/box/BoxFace'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { PrimitiveType } from '../PrimitiveType'; +import { type BoxPickInfo } from '../../../base/utilities/box/BoxPickInfo'; +import { type BaseDragger } from '../../../base/domainObjectsHelpers/BaseDragger'; +import { BoxDragger } from './BoxDragger'; +import { + VisualDomainObject, + type CreateDraggerProps +} from '../../../base/domainObjects/VisualDomainObject'; +import { Range3 } from '../../../base/utilities/geometry/Range3'; +import { getIconByPrimitiveType } from '../../measurements/getIconByPrimitiveType'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { Quantity } from '../../../base/domainObjectsHelpers/Quantity'; +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 { 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; -export class MeasureBoxDomainObject extends MeasureDomainObject { +export abstract class BoxDomainObject extends VisualDomainObject { // ================================================== // INSTANCE FIELDS // ================================================== @@ -37,30 +39,52 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { // For focus when edit in 3D (Used when isSelected is true only) public focusType: FocusType = FocusType.None; public focusFace: BoxFace | undefined = undefined; + private readonly _primitiveType: PrimitiveType; // ================================================== // INSTANCE PROPERTIES // ================================================== - public override get renderStyle(): MeasureBoxRenderStyle { - return this.getRenderStyle() as MeasureBoxRenderStyle; + public get renderStyle(): BoxRenderStyle { + return this.getRenderStyle() as BoxRenderStyle; + } + + public get primitiveType(): PrimitiveType { + return this._primitiveType; } // ================================================== // CONSTRUCTOR // ================================================== - public constructor(measureType: MeasureType) { - super(measureType); - this.color = new Color(Color.NAMES.magenta); + protected constructor(primitiveType: PrimitiveType = PrimitiveType.Box) { + super(); + this._primitiveType = primitiveType; } // ================================================== // OVERRIDES of DomainObject // ================================================== + public override get icon(): string { + return getIconByPrimitiveType(this.primitiveType); + } + + public override get typeName(): TranslateKey { + switch (this.primitiveType) { + case PrimitiveType.HorizontalArea: + return { key: 'MEASUREMENTS_HORIZONTAL_AREA', fallback: 'Horizontal area' }; + case PrimitiveType.VerticalArea: + return { key: 'MEASUREMENTS_VERTICAL_AREA', fallback: 'Vertical area' }; + case PrimitiveType.Box: + return { key: 'MEASUREMENTS_VOLUME', fallback: 'Volume' }; + default: + throw new Error('Unknown PrimitiveType'); + } + } + public override createRenderStyle(): RenderStyle | undefined { - return new MeasureBoxRenderStyle(); + return new BoxRenderStyle(); } public override createDragger(props: CreateDraggerProps): BaseDragger | undefined { @@ -68,88 +92,73 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { if (pickInfo === undefined) { return undefined; // If the BoxPickInfo isn't specified, no dragger is created } - return new MeasureBoxDragger(props, this); + return new BoxDragger(props, this); } - protected override notifyCore(change: DomainObjectChange): void { - super.notifyCore(change); - - // Experimental code for crop box (let it stay here) - // if (this.isUseAsCropBox) { - // if (change.isChanged(Changes.deleted)) { - // this.setUseAsCropBox(false); - // } - // if (change.isChanged(Changes.geometry)) { - // this.setUseAsCropBox(true); - // } - // } else if (change.isChanged(Changes.selected) && this.isSelected) { - // const root = this.root as RootDomainObject; - // if (root instanceof RootDomainObject && root.renderTarget.isGlobalCropBoxActive) { - // this.setUseAsCropBox(true); - // } - // } + public override get hasPanelInfo(): boolean { + return true; } public override getPanelInfo(): PanelInfo | undefined { const info = new PanelInfo(); - const { measureType } = this; + info.setHeader(this.typeName); + + const { primitiveType } = this; const isFinished = this.focusType !== FocusType.Pending; - switch (measureType) { - case MeasureType.HorizontalArea: - info.setHeader('MEASUREMENTS_HORIZONTAL_AREA', 'Horizontal area'); - break; - case MeasureType.VerticalArea: - info.setHeader('MEASUREMENTS_VERTICAL_AREA', 'Vertical area'); - break; - case MeasureType.Volume: - info.setHeader('MEASUREMENTS_VOLUME', 'Volume'); - break; - } - if (isFinished || isValidSize(this.size.x)) { + const hasX = BoxDomainObject.isValidSize(this.size.x); + const hasY = BoxDomainObject.isValidSize(this.size.y); + const hasZ = BoxDomainObject.isValidSize(this.size.z); + + if (isFinished || hasX) { add('MEASUREMENTS_LENGTH', 'Length', this.size.x, Quantity.Length); } - if (measureType !== MeasureType.VerticalArea && (isFinished || isValidSize(this.size.y))) { + if (primitiveType !== PrimitiveType.VerticalArea && (isFinished || hasY)) { add('MEASUREMENTS_DEPTH', 'Depth', this.size.y, Quantity.Length); } - if (measureType !== MeasureType.HorizontalArea && (isFinished || isValidSize(this.size.z))) { + if (primitiveType !== PrimitiveType.HorizontalArea && (isFinished || hasZ)) { add('MEASUREMENTS_HEIGHT', 'Height', this.size.z, Quantity.Length); } - if (measureType !== MeasureType.Volume && (isFinished || this.hasArea)) { + if (primitiveType !== PrimitiveType.Box && (isFinished || this.hasArea)) { add('MEASUREMENTS_AREA', 'Area', this.area, Quantity.Area); } - if (measureType === MeasureType.Volume && (isFinished || this.hasHorizontalArea)) { + if (primitiveType === PrimitiveType.Box && (isFinished || this.hasHorizontalArea)) { add('MEASUREMENTS_HORIZONTAL_AREA', 'Horizontal area', this.horizontalArea, Quantity.Area); } - if (measureType === MeasureType.Volume && (isFinished || this.hasVolume)) { + if (primitiveType === PrimitiveType.Box && (isFinished || this.hasVolume)) { 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({ - key: '', + key: undefined, + fallback: '', icon: 'Angle', value: radToDeg(this.zRotation), - quantity: Quantity.Degrees + quantity: Quantity.Angle }); } return info; - function add(key: string, fallback: string, value: number, quantity: Quantity): void { + function add( + key: string | undefined, + fallback: string, + value: number, + quantity: Quantity + ): void { info.add({ key, fallback, value, quantity }); } } - // ================================================== // OVERRIDES of VisualDomainObject // ================================================== protected override createThreeView(): ThreeView | undefined { - return new MeasureBoxView(); + return new BoxView(); } // ================================================== - // INSTANCE METHODS: Getters/Properties + // INSTANCE METHODS / PROPERTIES: Geometrical getters // ================================================== public get diagonal(): number { @@ -158,19 +167,19 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { public get hasArea(): boolean { let count = 0; - if (isValidSize(this.size.x)) count++; - if (isValidSize(this.size.y)) count++; - if (isValidSize(this.size.z)) count++; + if (BoxDomainObject.isValidSize(this.size.x)) count++; + if (BoxDomainObject.isValidSize(this.size.y)) count++; + if (BoxDomainObject.isValidSize(this.size.z)) count++; return count >= 2; } public get area(): number { - switch (this.measureType) { - case MeasureType.HorizontalArea: + switch (this.primitiveType) { + case PrimitiveType.HorizontalArea: return this.size.x * this.size.y; - case MeasureType.VerticalArea: + case PrimitiveType.VerticalArea: return this.size.x * this.size.z; - case MeasureType.Volume: { + case PrimitiveType.Box: { const a = this.size.x * this.size.y + this.size.y * this.size.z + this.size.z * this.size.x; return a * 2; } @@ -180,7 +189,7 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } public get hasHorizontalArea(): boolean { - return isValidSize(this.size.x) && isValidSize(this.size.y); + return BoxDomainObject.isValidSize(this.size.x) && BoxDomainObject.isValidSize(this.size.y); } public get horizontalArea(): number { @@ -188,7 +197,11 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } public get hasVolume(): boolean { - return isValidSize(this.size.x) && isValidSize(this.size.y) && isValidSize(this.size.z); + return ( + BoxDomainObject.isValidSize(this.size.x) && + BoxDomainObject.isValidSize(this.size.y) && + BoxDomainObject.isValidSize(this.size.z) + ); } public get volume(): number { @@ -208,6 +221,10 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { return boundingBox; } + // ================================================== + // INSTANCE METHODS: Matrix getters + // ================================================== + public getRotationMatrix(matrix: Matrix4 = new Matrix4()): Matrix4 { matrix.makeRotationZ(this.zRotation); return matrix; @@ -236,6 +253,8 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } public setFocusInteractive(focusType: FocusType, focusFace?: BoxFace): boolean { + const changeFromPending = + this.focusType === FocusType.Pending && focusType !== FocusType.Pending; if (focusType === FocusType.None) { if (this.focusType === FocusType.None) { return false; // No change @@ -250,39 +269,13 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { this.focusFace = focusFace; } this.notify(Changes.focus); + if (changeFromPending) { + this.notify(Changes.geometry); + } return true; } - public get isUseAsCropBox(): boolean { - const root = this.rootDomainObject; - if (root === undefined) { - return false; - } - return root.renderTarget.isGlobalCropBox(this); + public static isValidSize(value: number): boolean { + return value > MIN_BOX_SIZE; } - - public setUseAsCropBox(use: boolean): void { - const root = this.rootDomainObject; - if (root === undefined) { - return; - } - if (!use) { - root.renderTarget.clearGlobalCropBox(); - } else { - const boundingBox = this.getBoundingBox(); - boundingBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); - const matrix = this.getMatrix(); - matrix.premultiply(CDF_TO_VIEWER_TRANSFORMATION); - const planes = BoxFace.createClippingPlanes(matrix); - root.renderTarget.setGlobalCropBox(planes, boundingBox, this); - } - } -} - -// ================================================== -// PUBLIC FUNCTIONS -// ================================================== - -export function isValidSize(value: number): boolean { - return value > MIN_BOX_SIZE; } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDragger.ts b/react-components/src/architecture/concrete/primitives/box/BoxDragger.ts similarity index 67% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDragger.ts rename to react-components/src/architecture/concrete/primitives/box/BoxDragger.ts index 4fc1a7ece99..5e886740301 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDragger.ts +++ b/react-components/src/architecture/concrete/primitives/box/BoxDragger.ts @@ -3,33 +3,46 @@ */ import { type Ray, Vector3, Plane, Matrix4 } from 'three'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type BoxFace } from '../../base/utilities/box/BoxFace'; -import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; -import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; -import { forceBetween0AndPi } from '../../base/utilities/extensions/mathExtensions'; -import { horizontalAngle } from '../../base/utilities/extensions/vectorExtensions'; -import { MeasureType } from './MeasureType'; -import { getClosestPointOnLine } from '../../base/utilities/extensions/rayExtensions'; -import { type MeasureBoxDomainObject } from './MeasureBoxDomainObject'; -import { BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type BoxFace } from '../../../base/utilities/box/BoxFace'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { type BoxPickInfo } from '../../../base/utilities/box/BoxPickInfo'; +import { + forceBetween0AndPi, + round, + roundIncrement +} from '../../../base/utilities/extensions/mathExtensions'; +import { + getAbsMaxComponentIndex, + horizontalAngle, + rotateHorizontal +} from '../../../base/utilities/extensions/vectorExtensions'; +import { PrimitiveType } from '../PrimitiveType'; +import { getClosestPointOnLine } from '../../../base/utilities/extensions/rayExtensions'; +import { BoxDomainObject } from './BoxDomainObject'; +import { BaseDragger } from '../../../base/domainObjectsHelpers/BaseDragger'; import { type VisualDomainObject, type CreateDraggerProps -} from '../../base/domainObjects/VisualDomainObject'; +} from '../../../base/domainObjects/VisualDomainObject'; import { Vector3Pool } from '@cognite/reveal'; +import { degToRad, radToDeg } from 'three/src/math/MathUtils.js'; +import { Quantity } from '../../../base/domainObjectsHelpers/Quantity'; +import { type UnitSystem } from '../../../base/renderTarget/UnitSystem'; +const CONSTRAINED_ANGLE_INCREMENT = 15; /** * The `BoxDragger` class represents a utility for dragging and manipulating a box in a 3D space. * It provides methods for scaling, translating, and rotating the box based on user interactions. * All geometry in this class assume Z-axis is up */ -export class MeasureBoxDragger extends BaseDragger { + +export class BoxDragger extends BaseDragger { // ================================================== // INSTANCE FIELDS // ================================================== - private readonly _domainObject: MeasureBoxDomainObject; + private readonly _domainObject: BoxDomainObject; private readonly _face; private readonly _focusType: FocusType; @@ -42,6 +55,7 @@ export class MeasureBoxDragger extends BaseDragger { private readonly _zRotationOfBox: number = 0; private readonly _cornerSign = new Vector3(); // Indicate the corner of the face + private readonly _unitSystem: UnitSystem | undefined = undefined; // ================================================== // INSTANCE PROPERTIES @@ -59,7 +73,7 @@ export class MeasureBoxDragger extends BaseDragger { // CONSTRUCTOR // ================================================== - public constructor(props: CreateDraggerProps, domainObject: MeasureBoxDomainObject) { + public constructor(props: CreateDraggerProps, domainObject: BoxDomainObject) { super(props); const pickInfo = props.intersection.userData as BoxPickInfo; @@ -79,6 +93,11 @@ export class MeasureBoxDragger extends BaseDragger { this._sizeOfBox.copy(this._domainObject.size); this._centerOfBox.copy(this._domainObject.center); this._zRotationOfBox = this._domainObject.zRotation; + + const root = this._domainObject.rootDomainObject; + if (root !== undefined) { + this._unitSystem = root.unitSystem; + } } // ================================================== @@ -93,8 +112,8 @@ export class MeasureBoxDragger extends BaseDragger { this._domainObject.setFocusInteractive(this.focusType, this.face); } - public override onPointerDrag(_event: PointerEvent, ray: Ray): boolean { - if (!this.applyByFocusType(this.focusType, ray)) { + public override onPointerDrag(event: PointerEvent, ray: Ray): boolean { + if (!this.applyByFocusType(this.focusType, ray, event.shiftKey)) { return false; } this.domainObject.notify(Changes.geometry); @@ -105,22 +124,22 @@ export class MeasureBoxDragger extends BaseDragger { // INSTANCE METHODS // ================================================== - private applyByFocusType(focusType: FocusType, ray: Ray): boolean { + private applyByFocusType(focusType: FocusType, ray: Ray, shift: boolean): boolean { switch (focusType) { case FocusType.Face: - return this.moveFace(ray); + return this.moveFace(ray, shift); case FocusType.Corner: return this.resize(ray); case FocusType.Body: - return this.translate(ray); + return this.translate(ray, shift); case FocusType.Rotation: - return this.rotate(ray); + return this.rotate(ray, shift); default: return false; } } - private translate(ray: Ray): boolean { + private translate(ray: Ray, shift: boolean): boolean { // This translation can only be done in one plane, so we need to find the intersection point const planeIntersect = ray.intersectPlane(this._planeOfBox, newVector3()); if (planeIntersect === null) { @@ -130,6 +149,17 @@ export class MeasureBoxDragger extends BaseDragger { if (deltaCenter.lengthSq() === 0) { return false; } + if (shift) { + rotateHorizontal(deltaCenter, -this._domainObject.zRotation); + const maxIndex = getAbsMaxComponentIndex(deltaCenter); + for (let index = 0; index < 3; index++) { + if (index === maxIndex) { + continue; + } + deltaCenter.setComponent(index, 0); + } + rotateHorizontal(deltaCenter, this._domainObject.zRotation); + } // First copy the original values const { center } = this._domainObject; center.copy(this._centerOfBox); @@ -139,8 +169,8 @@ export class MeasureBoxDragger extends BaseDragger { return true; } - private moveFace(ray: Ray): boolean { - // Take find closest point between the ray and the line perpenducular to the face of in picked box. + private moveFace(ray: Ray, shift: boolean): boolean { + // Take find closest point between the ray and the line perpendicular to the face of in picked box. // The distance from this point to the face of in picked box is the change. const pointOnSegment = newVector3(); @@ -156,17 +186,29 @@ export class MeasureBoxDragger extends BaseDragger { const index = this._face.index; let deltaCenter: number; - if (this._domainObject.measureType !== MeasureType.Volume) { + if (this._domainObject.primitiveType !== PrimitiveType.Box) { deltaCenter = this._face.sign * deltaSize; } else { // Set new size size.setComponent(index, deltaSize + size.getComponent(index)); this._domainObject.forceMinSize(); + if ( + shift && + this._unitSystem !== undefined && + BoxDomainObject.isValidSize(size.getComponent(index)) + ) { + const newSize = this._unitSystem.convertToUnit(size.getComponent(index), Quantity.Length); + // Divide the box into abound some parts and use that as the increment + const increment = roundIncrement(newSize / 25); + let roundedNewSize = round(newSize, increment); + roundedNewSize = this._unitSystem.convertFromUnit(roundedNewSize, Quantity.Length); + size.setComponent(index, roundedNewSize); + } if (size.getComponent(index) === this._sizeOfBox.getComponent(index)) { return false; // Nothing has changed } - // The center of the box should be moved by half of the delta size and take the rotation into accont. + // The center of the box should be moved by half of the delta size and take the rotation into account. const newDeltaSize = size.getComponent(index) - this._sizeOfBox.getComponent(index); deltaCenter = (this._face.sign * newDeltaSize) / 2; } @@ -207,7 +249,7 @@ export class MeasureBoxDragger extends BaseDragger { if (size.lengthSq() === this._sizeOfBox.lengthSq()) { return false; // Nothing has changed } - // The center of the box should be moved by half of the delta size and take the rotation into accont. + // The center of the box should be moved by half of the delta size and take the rotation into account. const newDeltaSize = newVector3().subVectors(size, this._sizeOfBox); const deltaCenter = newDeltaSize.divideScalar(2); deltaCenter.multiply(this._cornerSign); @@ -216,7 +258,7 @@ export class MeasureBoxDragger extends BaseDragger { return true; } - private rotate(ray: Ray): boolean { + private rotate(ray: Ray, shift: boolean): boolean { const endPoint = ray.intersectPlane(this._planeOfBox, newVector3()); if (endPoint === null) { return false; @@ -229,7 +271,13 @@ export class MeasureBoxDragger extends BaseDragger { const deltaAngle = horizontalAngle(centerToEndPoint) - horizontalAngle(centerToStartPoint); // Rotate - this._domainObject.zRotation = forceBetween0AndPi(deltaAngle + this._zRotationOfBox); + let zRotation = forceBetween0AndPi(deltaAngle + this._zRotationOfBox); + if (shift) { + let degrees = radToDeg(zRotation); + degrees = round(degrees, CONSTRAINED_ANGLE_INCREMENT); + zRotation = degToRad(degrees); + } + this._domainObject.zRotation = zRotation; return true; } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxRenderStyle.ts b/react-components/src/architecture/concrete/primitives/box/BoxRenderStyle.ts similarity index 60% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureBoxRenderStyle.ts rename to react-components/src/architecture/concrete/primitives/box/BoxRenderStyle.ts index d74a4af16eb..dbbabdcdb2f 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxRenderStyle.ts +++ b/react-components/src/architecture/concrete/primitives/box/BoxRenderStyle.ts @@ -3,14 +3,17 @@ */ import { cloneDeep } from 'lodash'; -import { MeasureRenderStyle } from './MeasureRenderStyle'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { PrimitiveRenderStyle } from '../PrimitiveRenderStyle'; +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; -export class MeasureBoxRenderStyle extends MeasureRenderStyle { +export class BoxRenderStyle extends PrimitiveRenderStyle { // ================================================== // INSTANCE FIELDS // ================================================== + public showLines = true; + public showSolid = true; + public opacity = 0.5; public opacityUse = true; @@ -19,6 +22,6 @@ export class MeasureBoxRenderStyle extends MeasureRenderStyle { // ================================================== public override clone(): RenderStyle { - return cloneDeep(this); + return cloneDeep(this); } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts b/react-components/src/architecture/concrete/primitives/box/BoxView.ts similarity index 83% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts rename to react-components/src/architecture/concrete/primitives/box/BoxView.ts index 914b29e04ea..ca4dc586a8d 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts +++ b/react-components/src/architecture/concrete/primitives/box/BoxView.ts @@ -21,32 +21,32 @@ import { FrontSide, type PerspectiveCamera } from 'three'; -import { type MeasureBoxDomainObject, isValidSize } from './MeasureBoxDomainObject'; -import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type MeasureBoxRenderStyle } from './MeasureBoxRenderStyle'; -import { GroupThreeView } from '../../base/views/GroupThreeView'; +import { BoxDomainObject } from './BoxDomainObject'; +import { type DomainObjectChange } from '../../../base/domainObjectsHelpers/DomainObjectChange'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type BoxRenderStyle } from './BoxRenderStyle'; +import { GroupThreeView } from '../../../base/views/GroupThreeView'; import { CDF_TO_VIEWER_TRANSFORMATION, type CustomObjectIntersectInput, type CustomObjectIntersection, Vector3Pool } from '@cognite/reveal'; -import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; -import { BoxFace } from '../../base/utilities/box/BoxFace'; -import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; -import { clear } from '../../base/utilities/extensions/arrayExtensions'; -import { createSpriteWithText } from '../../base/utilities/sprites/createSprite'; +import { type DomainObjectIntersection } from '../../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { BoxFace } from '../../../base/utilities/box/BoxFace'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { clear } from '../../../base/utilities/extensions/arrayExtensions'; +import { createSpriteWithText } from '../../../base/utilities/sprites/createSprite'; import { createLineSegmentsBufferGeometryForBox, createOrientedBox -} from '../../base/utilities/box/createLineSegmentsBufferGeometryForBox'; -import { BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; +} from '../../../base/utilities/box/createLineSegmentsBufferGeometryForBox'; +import { BoxPickInfo } from '../../../base/utilities/box/BoxPickInfo'; 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'; +import { Range1 } from '../../../base/utilities/geometry/Range1'; +import { PrimitiveType } from '../PrimitiveType'; +import { Quantity } from '../../../base/domainObjectsHelpers/Quantity'; +import { type PrimitiveRenderStyle } from '../PrimitiveRenderStyle'; const RELATIVE_RESIZE_RADIUS = 0.15; const RELATIVE_ROTATION_RADIUS = new Range1(0.6, 0.75); @@ -55,7 +55,7 @@ const TOP_FACE = new BoxFace(2); const CIRCULAR_SEGMENTS = 32; const RENDER_ORDER = 100; -export class MeasureBoxView extends GroupThreeView { +export class BoxView extends GroupThreeView { // ================================================== // INSTANCE FIELDS // ================================================== @@ -67,12 +67,12 @@ export class MeasureBoxView extends GroupThreeView { // INSTANCE PROPERTIES // ================================================== - public override get domainObject(): MeasureBoxDomainObject { - return super.domainObject as MeasureBoxDomainObject; + public override get domainObject(): BoxDomainObject { + return super.domainObject as BoxDomainObject; } - protected override get style(): MeasureBoxRenderStyle { - return super.style as MeasureBoxRenderStyle; + protected override get style(): BoxRenderStyle { + return super.style as BoxRenderStyle; } // ================================================== @@ -112,22 +112,31 @@ export class MeasureBoxView extends GroupThreeView { // OVERRIDES of GroupThreeView // ================================================== + public override get useDepthTest(): boolean { + return this.style.depthTest; + } + protected override addChildren(): void { - const { domainObject } = this; + const { domainObject, style } = this; const matrix = this.getMatrix(); const { focusType } = domainObject; - this.addChild(this.createSolid(matrix)); - this.addChild(this.createLines(matrix)); + if (style.showSolid) { + this.addChild(this.createSolid(matrix)); + } + if (style.showLines) { + this.addChild(this.createLines(matrix)); + } if (showMarkers(focusType)) { this.addChild(this.createRotationRing(matrix)); this.addEdgeCircles(matrix); } - this.addLabels(matrix); - } - - public override get useDepthTest(): boolean { - return this.style.depthTest; + if (style.showLabel) { + this.addLabels(matrix); + } else if (focusType === FocusType.Rotation) { + const spriteHeight = this.getTextHeight(this.style.relativeTextSize); + this.addChild(this.createRotationLabel(matrix, spriteHeight)); + } } public override intersectIfCloser( @@ -151,7 +160,7 @@ export class MeasureBoxView extends GroupThreeView { if (closestDistance !== undefined && closestDistance < distanceToCamera) { return undefined; } - if (!intersectInput.isVisible(point)) { + if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { return undefined; } const positionAtFace = newVector3(point).applyMatrix4(matrix.invert()); @@ -251,8 +260,8 @@ export class MeasureBoxView extends GroupThreeView { if (degrees === 0) { return undefined; // Not show when about 0 } - const text = rootDomainObject.unitSystem.toStringWithUnit(degrees, Quantity.Degrees); - const sprite = createSprite(text, this.style, spriteHeight); + const text = rootDomainObject.unitSystem.toStringWithUnit(degrees, Quantity.Angle); + const sprite = BoxView.createSprite(text, this.style, spriteHeight); if (sprite === undefined) { return undefined; } @@ -267,7 +276,7 @@ export class MeasureBoxView extends GroupThreeView { if (!this.isFaceVisible(TOP_FACE)) { return undefined; } - const sprite = createSprite('Pending', this.style, spriteHeight); + const sprite = BoxView.createSprite('Pending', this.style, spriteHeight); if (sprite === undefined) { return undefined; } @@ -305,12 +314,12 @@ export class MeasureBoxView extends GroupThreeView { private createEdgeCircle(matrix: Matrix4, material: Material, face: BoxFace): Mesh | undefined { const { domainObject } = this; - const adjecentSize1 = domainObject.size.getComponent(face.tangentIndex1); - if (!isValidSize(adjecentSize1)) { + const adjacentSize1 = domainObject.size.getComponent(face.tangentIndex1); + if (!BoxDomainObject.isValidSize(adjacentSize1)) { return undefined; } - const adjecentSize2 = domainObject.size.getComponent(face.tangentIndex2); - if (!isValidSize(adjecentSize2)) { + const adjacentSize2 = domainObject.size.getComponent(face.tangentIndex2); + if (!BoxDomainObject.isValidSize(adjacentSize2)) { return undefined; } const radius = RELATIVE_RESIZE_RADIUS * this.getFaceRadius(face); @@ -324,7 +333,7 @@ export class MeasureBoxView extends GroupThreeView { center.applyMatrix4(matrix); result.position.copy(center); - // Must be roteted correctly because of sideness + // Must be rotated correctly because of sideness if (face.face === 2) { result.rotateX(-Math.PI / 2); } else if (face.face === 5) { @@ -355,12 +364,12 @@ export class MeasureBoxView extends GroupThreeView { clear(this._sprites); for (let index = 0; index < 3; index++) { const size = domainObject.size.getComponent(index); - if (!isValidSize(size)) { + if (!BoxDomainObject.isValidSize(size)) { this._sprites.push(undefined); continue; } const text = rootDomainObject.unitSystem.toStringWithUnit(size, Quantity.Length); - const sprite = createSprite(text, style, spriteHeight); + const sprite = BoxView.createSprite(text, style, spriteHeight); if (sprite === undefined) { this._sprites.push(undefined); continue; @@ -397,7 +406,7 @@ export class MeasureBoxView extends GroupThreeView { } const spriteHeight = this.getTextHeight(style.relativeTextSize); - // If the 2 adjecent faces are visible, show the sprite along the edge + // If the 2 adjacent faces are visible, show the sprite along the edge for (let index = 0; index < this._sprites.length; index++) { const sprite = this._sprites[index]; if (sprite === undefined) { @@ -526,15 +535,35 @@ export class MeasureBoxView extends GroupThreeView { private isFaceVisible(boxFace: BoxFace): boolean { const { domainObject } = this; - switch (domainObject.measureType) { - case MeasureType.VerticalArea: + switch (domainObject.primitiveType) { + case PrimitiveType.VerticalArea: return boxFace.index === 1; // Y Face visible - case MeasureType.HorizontalArea: + case PrimitiveType.HorizontalArea: return boxFace.index === 2; // Z face visible } return true; } + + // ================================================== + // STATIC METHODS + // ================================================== + + public static createSprite( + text: string, + style: PrimitiveRenderStyle, + height: number + ): Sprite | undefined { + const result = createSpriteWithText(text, height, style.labelColor, style.labelBgColor); + if (result === undefined) { + return undefined; + } + result.material.transparent = true; + result.material.opacity = style.labelOpacity; + result.material.depthTest = style.depthTest; + result.renderOrder = RENDER_ORDER; + return result; + } } // ================================================== @@ -568,8 +597,8 @@ function showMarkers(focusType: FocusType): boolean { function updateSolidMaterial( material: MeshPhongMaterial, - domainObject: MeasureBoxDomainObject, - style: MeasureBoxRenderStyle + domainObject: BoxDomainObject, + style: BoxRenderStyle ): void { const color = domainObject.getColorByColorType(style.colorType); const isSelected = domainObject.isSelected; @@ -590,8 +619,8 @@ function updateSolidMaterial( function updateLineSegmentsMaterial( material: LineBasicMaterial, - domainObject: MeasureBoxDomainObject, - style: MeasureBoxRenderStyle + domainObject: BoxDomainObject, + style: BoxRenderStyle ): void { const color = domainObject.getColorByColorType(style.colorType); material.color = color; @@ -602,8 +631,8 @@ function updateLineSegmentsMaterial( function updateMarkerMaterial( material: MeshPhongMaterial, - domainObject: MeasureBoxDomainObject, - style: MeasureBoxRenderStyle, + domainObject: BoxDomainObject, + style: BoxRenderStyle, hasFocus: boolean ): void { material.color = ARROW_AND_RING_COLOR; @@ -623,24 +652,8 @@ function updateMarkerMaterial( // PRIVATE FUNCTIONS: Create object3D's // ================================================== -function createSprite(text: string, style: MeasureRenderStyle, height: number): Sprite | undefined { - const result = createSpriteWithText(text, height, style.textColor, style.textBgColor); - if (result === undefined) { - return undefined; - } - result.material.transparent = true; - result.material.opacity = style.textOpacity; - result.material.depthTest = style.depthTest; - result.renderOrder = RENDER_ORDER; - return result; -} - -function adjustLabel( - point: Vector3, - domainObject: MeasureBoxDomainObject, - spriteHeight: number -): void { - if (domainObject.measureType !== MeasureType.VerticalArea) { +function adjustLabel(point: Vector3, domainObject: BoxDomainObject, spriteHeight: number): void { + if (domainObject.primitiveType !== PrimitiveType.VerticalArea) { point.y += (1.1 * spriteHeight) / 2; } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts b/react-components/src/architecture/concrete/primitives/line/LineCreator.ts similarity index 68% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts rename to react-components/src/architecture/concrete/primitives/line/LineCreator.ts index 5c00b7669bc..823b0abda69 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts +++ b/react-components/src/architecture/concrete/primitives/line/LineCreator.ts @@ -3,31 +3,31 @@ */ import { Plane, type Ray, Vector3 } from 'three'; -import { MeasureType } from './MeasureType'; -import { BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; -import { MeasureLineDomainObject } from './MeasureLineDomainObject'; -import { copy } from '../../base/utilities/extensions/arrayExtensions'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { type DomainObject } from '../../base/domainObjects/DomainObject'; -import { FocusType } from '../../base/domainObjectsHelpers/FocusType'; +import { PrimitiveType } from '../PrimitiveType'; +import { BaseCreator } from '../../../base/domainObjectsHelpers/BaseCreator'; +import { copy } from '../../../base/utilities/extensions/arrayExtensions'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { type LineDomainObject } from './LineDomainObject'; /** - * Helper class for generate a MeasureLineDomainObject by clicking around + * Helper class for generate a LineDomainObject by clicking around */ -export class MeasureLineCreator extends BaseCreator { +export class LineCreator extends BaseCreator { // ================================================== // INSTANCE FIELDS // ================================================== - private readonly _domainObject: MeasureLineDomainObject; + private readonly _domainObject: LineDomainObject; // ================================================== // CONSTRUCTOR // ================================================== - constructor(measureType: MeasureType) { + constructor(domainObject: LineDomainObject) { super(); - this._domainObject = new MeasureLineDomainObject(measureType); + this._domainObject = domainObject; this._domainObject.focusType = FocusType.Pending; } @@ -48,14 +48,14 @@ export class MeasureLineCreator extends BaseCreator { } public override get maximumPointCount(): number { - switch (this._domainObject.measureType) { - case MeasureType.Line: + switch (this._domainObject.primitiveType) { + case PrimitiveType.Line: return 2; - case MeasureType.Polyline: - case MeasureType.Polygon: + case PrimitiveType.Polyline: + case PrimitiveType.Polygon: return Number.MAX_SAFE_INTEGER; default: - throw new Error('Unknown measurement type'); + throw new Error('Unknown primitiveType'); } } @@ -83,7 +83,6 @@ export class MeasureLineCreator extends BaseCreator { domainObject.notify(Changes.geometry); if (this.isFinished) { - domainObject.setSelectedInteractive(true); domainObject.setFocusInteractive(FocusType.Focus); } return true; diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts b/react-components/src/architecture/concrete/primitives/line/LineDomainObject.ts similarity index 62% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts rename to react-components/src/architecture/concrete/primitives/line/LineDomainObject.ts index 746e6ab3fd8..b7b7dff90a9 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineDomainObject.ts +++ b/react-components/src/architecture/concrete/primitives/line/LineDomainObject.ts @@ -2,54 +2,82 @@ * Copyright 2024 Cognite AS */ -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; -import { type ThreeView } from '../../base/views/ThreeView'; -import { MeasureLineView } from './MeasureLineView'; -import { Color, Vector3 } from 'three'; -import { MeasureType } from './MeasureType'; -import { MeasureLineRenderStyle } from './MeasureLineRenderStyle'; -import { MeasureDomainObject } from './MeasureDomainObject'; +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; +import { type ThreeView } from '../../../base/views/ThreeView'; +import { LineView } from './LineView'; +import { Vector3 } from 'three'; +import { PrimitiveType } from '../PrimitiveType'; +import { LineRenderStyle } from './LineRenderStyle'; import { getHorizontalCrossProduct, horizontalDistanceTo, verticalDistanceTo -} from '../../base/utilities/extensions/vectorExtensions'; -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 { +} from '../../../base/utilities/extensions/vectorExtensions'; +import { PanelInfo } from '../../../base/domainObjectsHelpers/PanelInfo'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { Quantity } from '../../../base/domainObjectsHelpers/Quantity'; +import { VisualDomainObject } from '../../../base/domainObjects/VisualDomainObject'; +import { getIconByPrimitiveType } from '../../measurements/getIconByPrimitiveType'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; + +export abstract class LineDomainObject extends VisualDomainObject { // ================================================== // INSTANCE FIELDS // ================================================== public readonly points: Vector3[] = []; + private readonly _primitiveType: PrimitiveType; public focusType = FocusType.None; // ================================================== // INSTANCE PROPERTIES // ================================================== - public override get renderStyle(): MeasureLineRenderStyle { - return this.getRenderStyle() as MeasureLineRenderStyle; + public get renderStyle(): LineRenderStyle { + return this.getRenderStyle() as LineRenderStyle; + } + + public get primitiveType(): PrimitiveType { + return this._primitiveType; } // ================================================== // CONSTRUCTOR // ================================================== - public constructor(measureType: MeasureType) { - super(measureType); - this.color = new Color(Color.NAMES.red); + protected constructor(primitiveType: PrimitiveType) { + super(); + this._primitiveType = primitiveType; } // ================================================== // OVERRIDES of DomainObject // ================================================== + public override get icon(): string { + return getIconByPrimitiveType(this.primitiveType); + } + + public override get typeName(): TranslateKey { + switch (this.primitiveType) { + case PrimitiveType.Line: + return { key: 'MEASUREMENTS_LINE', fallback: 'Line' }; + case PrimitiveType.Polyline: + return { key: 'MEASUREMENTS_POLYLINE', fallback: 'Polyline' }; + case PrimitiveType.Polygon: + return { key: 'MEASUREMENTS_POLYGON', fallback: 'Polygon' }; + default: + throw new Error('Unknown PrimitiveType'); + } + } + public override createRenderStyle(): RenderStyle | undefined { - return new MeasureLineRenderStyle(); + return new LineRenderStyle(); + } + + public override get hasPanelInfo(): boolean { + return true; } public override getPanelInfo(): PanelInfo | undefined { @@ -57,20 +85,19 @@ export class MeasureLineDomainObject extends MeasureDomainObject { return undefined; } const info = new PanelInfo(); - switch (this.measureType) { - case MeasureType.Line: - info.setHeader('MEASUREMENTS_LINE', 'Line'); + info.setHeader(this.typeName); + + switch (this.primitiveType) { + case PrimitiveType.Line: add('MEASUREMENTS_LENGTH', 'Length', this.getTotalLength()); add('MEASUREMENTS_HORIZONTAL_LENGTH', 'Horizontal length', this.getHorizontalLength()); add('MEASUREMENTS_VERTICAL_LENGTH', 'Vertical length', this.getVerticalLength()); break; - case MeasureType.Polyline: - info.setHeader('MEASUREMENTS_POLYLINE', 'Polyline'); + case PrimitiveType.Polyline: add('MEASUREMENTS_TOTAL_LENGTH', 'Total length', this.getTotalLength()); break; - case MeasureType.Polygon: - info.setHeader('MEASUREMENTS_POLYGON', 'Polygon'); + case PrimitiveType.Polygon: add('MEASUREMENTS_TOTAL_LENGTH', 'Total length', this.getTotalLength()); if (this.points.length > 2) { add( @@ -83,7 +110,7 @@ export class MeasureLineDomainObject extends MeasureDomainObject { break; default: - throw new Error('Unknown MeasureType type'); + throw new Error('Unknown PrimitiveType type'); } return info; @@ -97,7 +124,7 @@ export class MeasureLineDomainObject extends MeasureDomainObject { // ================================================== protected override createThreeView(): ThreeView | undefined { - return new MeasureLineView(); + return new LineView(); } // ================================================== @@ -163,7 +190,7 @@ export class MeasureLineDomainObject extends MeasureDomainObject { for (let index = 1; index <= count; index++) { p1.copy(points[index % count]); - p1.sub(first); // Translate down to first point, to increase acceracy + p1.sub(first); // Translate down to first point, to increase accuracy sum += getHorizontalCrossProduct(p0, p1); p0.copy(p1); } @@ -174,8 +201,13 @@ export class MeasureLineDomainObject extends MeasureDomainObject { if (this.focusType === focusType) { return false; } + const changeFromPending = + this.focusType === FocusType.Pending && focusType !== FocusType.Pending; this.focusType = focusType; this.notify(Changes.focus); + if (changeFromPending) { + this.notify(Changes.geometry); + } return true; } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineRenderStyle.ts b/react-components/src/architecture/concrete/primitives/line/LineRenderStyle.ts similarity index 68% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureLineRenderStyle.ts rename to react-components/src/architecture/concrete/primitives/line/LineRenderStyle.ts index 5a6765943a7..cab78e99312 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineRenderStyle.ts +++ b/react-components/src/architecture/concrete/primitives/line/LineRenderStyle.ts @@ -3,10 +3,10 @@ */ import { cloneDeep } from 'lodash'; -import { MeasureRenderStyle } from './MeasureRenderStyle'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { PrimitiveRenderStyle } from '../PrimitiveRenderStyle'; +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; -export class MeasureLineRenderStyle extends MeasureRenderStyle { +export class LineRenderStyle extends PrimitiveRenderStyle { // ================================================== // INSTANCE FIELDS // ================================================== @@ -21,6 +21,6 @@ export class MeasureLineRenderStyle extends MeasureRenderStyle { // ================================================== public override clone(): RenderStyle { - return cloneDeep(this); + return cloneDeep(this); } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts b/react-components/src/architecture/concrete/primitives/line/LineView.ts similarity index 78% rename from react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts rename to react-components/src/architecture/concrete/primitives/line/LineView.ts index eba2613e3f5..6b198f5b571 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts +++ b/react-components/src/architecture/concrete/primitives/line/LineView.ts @@ -13,16 +13,15 @@ import { Mesh, MeshPhongMaterial, Quaternion, - Sprite, Vector2, Vector3 } from 'three'; import { Wireframe } from 'three/examples/jsm/lines/Wireframe.js'; -import { MeasureLineDomainObject } from './MeasureLineDomainObject'; -import { DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { MeasureLineRenderStyle } from './MeasureLineRenderStyle'; -import { GroupThreeView } from '../../base/views/GroupThreeView'; +import { LineDomainObject } from './LineDomainObject'; +import { DomainObjectChange } from '../../../base/domainObjectsHelpers/DomainObjectChange'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { LineRenderStyle } from './LineRenderStyle'; +import { GroupThreeView } from '../../../base/views/GroupThreeView'; import { CDF_TO_VIEWER_TRANSFORMATION, CustomObjectIntersectInput, @@ -30,30 +29,29 @@ import { } from '@cognite/reveal'; import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; -import { MeasureType } from './MeasureType'; -import { createSpriteWithText } from '../../base/utilities/sprites/createSprite'; +import { PrimitiveType } from '../PrimitiveType'; 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'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +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'; +import { BoxView } from '../box/BoxView'; const CYLINDER_DEFAULT_AXIS = new Vector3(0, 1, 0); const RENDER_ORDER = 100; -export class MeasureLineView extends GroupThreeView { +export class LineView extends GroupThreeView { // ================================================== // INSTANCE PROPERTIES // ================================================== - public override get domainObject(): MeasureLineDomainObject { - return super.domainObject as MeasureLineDomainObject; + public override get domainObject(): LineDomainObject { + return super.domainObject as LineDomainObject; } - protected override get style(): MeasureLineRenderStyle { - return super.style as MeasureLineRenderStyle; + protected override get style(): LineRenderStyle { + return super.style as LineRenderStyle; } // ================================================== @@ -81,25 +79,25 @@ export class MeasureLineView extends GroupThreeView { // OVERRIDES of GroupThreeView // ================================================== + public override get useDepthTest(): boolean { + return this.style.depthTest; + } + protected override addChildren(): void { this.addChild(this.createPipe()); this.addChild(this.createLines()); // Create a line so it can be seen from long distance this.addLabels(); } - public override get useDepthTest(): boolean { - return this.style.depthTest; - } - public override intersectIfCloser( intersectInput: CustomObjectIntersectInput, closestDistance: number | undefined ): undefined | CustomObjectIntersection { - if (this.domainObject.focusType === FocusType.Pending) { + const { domainObject, style } = this; + if (domainObject.focusType === FocusType.Pending) { return undefined; // Should never be picked } // Implement the intersection logic here, because of bug in tree.js - const { domainObject, style } = this; const radius = getRadius(domainObject, style); if (radius <= 0) { return; @@ -121,7 +119,7 @@ export class MeasureLineView extends GroupThreeView { if (closestDistance !== undefined) { closestFinder.minDistance = closestDistance; } - const loopLength = domainObject.measureType === MeasureType.Polygon ? length + 1 : length; + const loopLength = domainObject.primitiveType === PrimitiveType.Polygon ? length + 1 : length; for (let i = 0; i < loopLength; i++) { thisPoint.copy(points[i % length]); thisPoint.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); @@ -164,7 +162,7 @@ export class MeasureLineView extends GroupThreeView { return undefined; } const geometries: CylinderGeometry[] = []; - const loopLength = domainObject.measureType === MeasureType.Polygon ? length + 1 : length; + const loopLength = domainObject.primitiveType === PrimitiveType.Polygon ? length + 1 : length; // Just allocate all needed objects once const prevPoint = new Vector3(); @@ -182,7 +180,7 @@ export class MeasureLineView extends GroupThreeView { const distance = prevPoint.distanceTo(thisPoint); const cylinder = new CylinderGeometry(radius, radius, distance, 6, 1); - // use quaterion to orient cylinder to align along the vector formed between + // use quaternion to orient cylinder to align along the vector formed between // the pair of vertices direction.copy(thisPoint).sub(prevPoint).normalize(); quaternion.setFromUnitVectors(CYLINDER_DEFAULT_AXIS, direction); @@ -251,6 +249,9 @@ export class MeasureLineView extends GroupThreeView { private addLabels(): void { const { domainObject, style } = this; + if (!style.showLabel) { + return; + } const { points, rootDomainObject } = domainObject; if (rootDomainObject === undefined) { return; @@ -263,7 +264,7 @@ export class MeasureLineView extends GroupThreeView { if (spriteHeight <= 0) { return; } - const loopLength = domainObject.measureType === MeasureType.Polygon ? length : length - 1; + const loopLength = domainObject.primitiveType === PrimitiveType.Polygon ? length : length - 1; const center = new Vector3(); for (let i = 0; i < loopLength; i++) { const point1 = points[i % length]; @@ -274,7 +275,7 @@ export class MeasureLineView extends GroupThreeView { center.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); const text = rootDomainObject.unitSystem.toStringWithUnit(distance, Quantity.Length); - const sprite = createSprite(text, style, spriteHeight); + const sprite = BoxView.createSprite(text, style, spriteHeight); if (sprite === undefined) { continue; } @@ -293,14 +294,14 @@ export class MeasureLineView extends GroupThreeView { // PRIVATE FUNCTIONS: Create object3D's // ================================================== -function createVertices(domainObject: MeasureLineDomainObject): number[] | undefined { +function createVertices(domainObject: LineDomainObject): number[] | undefined { const { points } = domainObject; const { length } = points; if (length < 2) { return undefined; } const vertices: number[] = []; - const loopLength = domainObject.measureType === MeasureType.Polygon ? length + 1 : length; + const loopLength = domainObject.primitiveType === PrimitiveType.Polygon ? length + 1 : length; for (let i = 0; i < loopLength; i++) { const point = points[i % length].clone(); @@ -313,22 +314,10 @@ function createVertices(domainObject: MeasureLineDomainObject): number[] | undef return vertices; } -function createSprite(text: string, style: MeasureRenderStyle, height: number): Sprite | undefined { - const result = createSpriteWithText(text, height, style.textColor, style.textBgColor); - if (result === undefined) { - return undefined; - } - result.material.transparent = true; - result.material.opacity = style.textOpacity; - result.material.depthTest = style.depthTest; - result.renderOrder = RENDER_ORDER; - return result; -} - function updateSolidMaterial( material: MeshPhongMaterial, - boxDomainObject: MeasureLineDomainObject, - style: MeasureLineRenderStyle + boxDomainObject: LineDomainObject, + style: LineRenderStyle ): void { const color = boxDomainObject.getColorByColorType(style.colorType); material.color = color; @@ -344,15 +333,15 @@ function updateSolidMaterial( function adjustLabel( point: Vector3, - domainObject: MeasureLineDomainObject, - style: MeasureLineRenderStyle, + domainObject: LineDomainObject, + style: LineRenderStyle, spriteHeight: number ): void { - if (domainObject.measureType !== MeasureType.VerticalArea) { + if (domainObject.primitiveType !== PrimitiveType.VerticalArea) { point.y += (1.1 * spriteHeight) / 2 + style.pipeRadius; } } -function getRadius(domainObject: MeasureLineDomainObject, style: MeasureLineRenderStyle): number { +function getRadius(domainObject: LineDomainObject, style: LineRenderStyle): number { return domainObject.isSelected ? style.selectedPipeRadius : style.pipeRadius; } diff --git a/react-components/src/architecture/concrete/primitives/plane/PlaneCreator.ts b/react-components/src/architecture/concrete/primitives/plane/PlaneCreator.ts new file mode 100644 index 00000000000..e78ee6fb375 --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/plane/PlaneCreator.ts @@ -0,0 +1,118 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type Ray, Vector3 } from 'three'; +import { PrimitiveType } from '../PrimitiveType'; +import { BaseCreator } from '../../../base/domainObjectsHelpers/BaseCreator'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type DomainObject } from '../../../base/domainObjects/DomainObject'; +import { type PlaneDomainObject } from './PlaneDomainObject'; +import { rotatePiHalf } from '../../../base/utilities/extensions/vectorExtensions'; + +/** + * Helper class for generate a PlaneDomainObject by clicking around + */ +export class PlaneCreator extends BaseCreator { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + private readonly _domainObject: PlaneDomainObject; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + constructor(domainObject: PlaneDomainObject) { + super(); + this._domainObject = domainObject; + this._domainObject.focusType = FocusType.Pending; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get domainObject(): DomainObject { + return this._domainObject; + } + + public override get preferIntersection(): boolean { + return true; + } + + public override get minimumPointCount(): number { + return this.maximumPointCount; + } + + public override get maximumPointCount(): number { + switch (this._domainObject.primitiveType) { + case PrimitiveType.PlaneX: + case PrimitiveType.PlaneY: + case PrimitiveType.PlaneZ: + return 1; + case PrimitiveType.PlaneXY: + return 2; + default: + throw new Error('Unknown primitiveType'); + } + } + + protected override addPointCore( + ray: Ray, + point: Vector3 | undefined, + isPending: boolean + ): boolean { + if (point === undefined) { + return false; + } + const domainObject = this._domainObject; + this.addRawPoint(point, isPending); + if (!this.rebuild(ray)) { + return false; + } + domainObject.notify(Changes.geometry); + if (this.isFinished) { + domainObject.setFocusInteractive(FocusType.Focus); + } + return true; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private rebuild(ray: Ray): boolean { + if (this.pointCount === 0) { + throw new Error('Cannot create a plane without points'); + } + const domainObject = this._domainObject; + switch (domainObject.primitiveType) { + case PrimitiveType.PlaneX: + domainObject.plane.setFromNormalAndCoplanarPoint(new Vector3(1, 0, 0), this.firstPoint); + break; + + case PrimitiveType.PlaneY: + domainObject.plane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), this.firstPoint); + break; + + case PrimitiveType.PlaneZ: + domainObject.plane.setFromNormalAndCoplanarPoint(new Vector3(0, 0, 1), this.firstPoint); + break; + + case PrimitiveType.PlaneXY: + if (this.pointCount === 1) { + const normal = ray.direction.clone().normalize(); + domainObject.plane.setFromNormalAndCoplanarPoint(normal, this.firstPoint); + } else if (this.pointCount === 2) { + const normal = new Vector3().subVectors(this.lastPoint, this.firstPoint).normalize(); + rotatePiHalf(normal); + domainObject.plane.setFromNormalAndCoplanarPoint(normal, this.firstPoint); + } + break; + } + return true; + } +} diff --git a/react-components/src/architecture/concrete/primitives/plane/PlaneDomainObject.ts b/react-components/src/architecture/concrete/primitives/plane/PlaneDomainObject.ts new file mode 100644 index 00000000000..8f9dfe871b3 --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/plane/PlaneDomainObject.ts @@ -0,0 +1,228 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; +import { type ThreeView } from '../../../base/views/ThreeView'; +import { PlaneView } from './PlaneView'; +import { type Color, Plane, Vector3 } from 'three'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { PrimitiveType } from '../PrimitiveType'; +import { type BaseDragger } from '../../../base/domainObjectsHelpers/BaseDragger'; +import { + VisualDomainObject, + type CreateDraggerProps +} from '../../../base/domainObjects/VisualDomainObject'; +import { PlaneRenderStyle } from './PlaneRenderStyle'; +import { PlaneDragger } from './PlaneDragger'; +import { getIconByPrimitiveType } from '../../measurements/getIconByPrimitiveType'; +import { getComplementary } from '../../../base/utilities/colors/colorExtensions'; +import { + horizontalAngle, + rotateHorizontal +} from '../../../base/utilities/extensions/vectorExtensions'; +import { forceBetween0AndPi } from '../../../base/utilities/extensions/mathExtensions'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { PanelInfo } from '../../../base/domainObjectsHelpers/PanelInfo'; +import { Quantity } from '../../../base/domainObjectsHelpers/Quantity'; +import { radToDeg } from 'three/src/math/MathUtils.js'; +import { type DomainObjectChange } from '../../../base/domainObjectsHelpers/DomainObjectChange'; + +const ORIGIN = new Vector3(0, 0, 0); + +export abstract class PlaneDomainObject extends VisualDomainObject { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + public readonly plane = new Plane(); + private readonly _primitiveType: PrimitiveType; + private _backSideColor: Color | undefined = undefined; + + // For focus when edit in 3D (Used when isSelected is true only) + public focusType: FocusType = FocusType.None; + + // ================================================== + // INSTANCE PROPERTIES + // ================================================== + + public get renderStyle(): PlaneRenderStyle { + return this.getRenderStyle() as PlaneRenderStyle; + } + + public get primitiveType(): PrimitiveType { + return this._primitiveType; + } + + public get backSideColor(): Color { + if (this._backSideColor === undefined) { + this._backSideColor = getComplementary(this.color); + } + return this._backSideColor; + } + + public set backSideColor(color: Color) { + this._backSideColor = color; + } + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(primitiveType: PrimitiveType) { + super(); + this._primitiveType = primitiveType; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get icon(): string { + return getIconByPrimitiveType(this.primitiveType); + } + + public override get typeName(): TranslateKey { + switch (this.primitiveType) { + case PrimitiveType.PlaneX: + return { key: 'PLANE_X', fallback: 'Vertical plane along Y-axis' }; + case PrimitiveType.PlaneY: + return { key: 'PLANE_Y', fallback: 'Vertical plane along X-axis' }; + case PrimitiveType.PlaneZ: + return { key: 'PLANE_Z', fallback: 'Horizontal plane' }; + case PrimitiveType.PlaneXY: + return { key: 'PLANE_XY', fallback: 'Vertical plane' }; + default: + throw new Error('Unknown PrimitiveType'); + } + } + + public override createRenderStyle(): RenderStyle | undefined { + return new PlaneRenderStyle(); + } + + public override createDragger(props: CreateDraggerProps): BaseDragger | undefined { + return new PlaneDragger(props, this); + } + + public override get hasPanelInfo(): boolean { + return true; + } + + public override getPanelInfo(): PanelInfo | undefined { + const info = new PanelInfo(); + info.setHeader(this.typeName); + + switch (this.primitiveType) { + case PrimitiveType.PlaneX: + add('XCOORDINATE', 'X coordinate', this.coordinate, Quantity.Length); + break; + case PrimitiveType.PlaneY: + add('YCOORDINATE', 'Y coordinate', this.coordinate, Quantity.Length); + break; + case PrimitiveType.PlaneZ: + add('ZCOORDINATE', 'Z coordinate', this.coordinate, Quantity.Length); + break; + case PrimitiveType.PlaneXY: + add('DISTANCE_TO_ORIGIN', 'Distance to origin', this.coordinate, Quantity.Length); + add('ANGLE', 'Angle', radToDeg(this.angle), Quantity.Angle); + break; + } + return info; + + function add(key: string, fallback: string, value: number, quantity: Quantity): void { + info.add({ key, fallback, value, quantity }); + } + } + + protected override notifyCore(change: DomainObjectChange): void { + super.notifyCore(change); + + if (change.isChanged(Changes.added)) { + this.makeFlippingConsistent(); + } + } + + protected override createThreeView(): ThreeView | undefined { + return new PlaneView(); + } + + // ================================================== + // INSTANCE METHODS / PROPERTIES: Geometrical getters + // ================================================== + + public get coordinate(): number { + const pointOnPlane = this.plane.projectPoint(ORIGIN, new Vector3()); + switch (this.primitiveType) { + case PrimitiveType.PlaneX: + return pointOnPlane.x; + case PrimitiveType.PlaneY: + return pointOnPlane.y; + case PrimitiveType.PlaneZ: + return pointOnPlane.z; + case PrimitiveType.PlaneXY: + return pointOnPlane.distanceTo(ORIGIN); + default: + throw new Error('Unknown PrimitiveType'); + } + } + + public get angle(): number { + const vector = this.plane.normal.clone(); + rotateHorizontal(vector, Math.PI / 2); + const angle = horizontalAngle(vector); + return forceBetween0AndPi(angle); + } + + // ================================================== + // INSTANCE METHODS: Others + // ================================================== + + public setFocusInteractive(focusType: FocusType): boolean { + if (this.focusType === focusType) { + return false; + } + const changeFromPending = + this.focusType === FocusType.Pending && focusType !== FocusType.Pending; + this.focusType = focusType; + this.notify(Changes.focus); + if (changeFromPending) { + this.notify(Changes.geometry); + } + return true; + } + + public flip(): void { + const { plane } = this; + plane.normal.negate(); + plane.constant = -plane.constant; + } + + public makeFlippingConsistent(): void { + const root = this.rootDomainObject; + if (root === undefined) { + return; + } + for (const other of root.getDescendantsByType(PlaneDomainObject)) { + if (this.primitiveType !== other.primitiveType) { + continue; + } + if (this === other) { + continue; + } + const origin = new Vector3(0, 0, 0); + const pointOnThisPlane = this.plane.projectPoint(origin, new Vector3()); + if (other.plane.distanceToPoint(pointOnThisPlane) < 0) { + other.flip(); + other.notify(Changes.geometry); + } + const pointOnOtherPlane = other.plane.projectPoint(origin, new Vector3()); + if (this.plane.distanceToPoint(pointOnOtherPlane) < 0) { + this.flip(); + this.notify(Changes.geometry); + } + break; + } + } +} diff --git a/react-components/src/architecture/concrete/primitives/plane/PlaneDragger.ts b/react-components/src/architecture/concrete/primitives/plane/PlaneDragger.ts new file mode 100644 index 00000000000..7f351bde969 --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/plane/PlaneDragger.ts @@ -0,0 +1,76 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type Ray, Plane, type Box3 } from 'three'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type PlaneDomainObject } from './PlaneDomainObject'; +import { BaseDragger } from '../../../base/domainObjectsHelpers/BaseDragger'; +import { + type VisualDomainObject, + type CreateDraggerProps +} from '../../../base/domainObjects/VisualDomainObject'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { getClosestPointOnLine } from '../../../base/utilities/extensions/rayExtensions'; +import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; + +/** + * The `PlaneDragger` class represents a utility for dragging and manipulating a plane in a 3D space. + * All geometry in this class assume Z-axis is up + */ +export class PlaneDragger extends BaseDragger { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + private readonly _domainObject: PlaneDomainObject; + private readonly _plane: Plane; + + // Constrain the plane inside this box + private readonly _boundingBox: Box3 | undefined = undefined; + + // ================================================== + // CONSTRUCTOR + // ================================================== + + public constructor(props: CreateDraggerProps, domainObject: PlaneDomainObject) { + super(props); + + this._domainObject = domainObject; + this._plane = this._domainObject.plane.clone(); + + const root = this._domainObject.rootDomainObject; + if (root === undefined) { + return; + } + const boundingBox = root.renderTarget.sceneBoundingBox; + this._boundingBox = boundingBox.clone(); + this._boundingBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get domainObject(): VisualDomainObject { + return this._domainObject; + } + + public override onPointerDown(_event: PointerEvent): void { + this._domainObject.setFocusInteractive(FocusType.Focus); + } + + public override onPointerDrag(_event: PointerEvent, ray: Ray): boolean { + const newPoint = getClosestPointOnLine(ray, this._plane.normal, this.point); + + // Constrain the plane to inside the sceneBoundingBox + if (this._boundingBox !== undefined && !this._boundingBox.containsPoint(newPoint)) { + return false; + } + const newPlane = new Plane().setFromNormalAndCoplanarPoint(this._plane.normal, newPoint); + this._domainObject.plane.copy(newPlane); + this._domainObject.makeFlippingConsistent(); + this.domainObject.notify(Changes.geometry); + return true; + } +} diff --git a/react-components/src/architecture/concrete/primitives/plane/PlaneRenderStyle.ts b/react-components/src/architecture/concrete/primitives/plane/PlaneRenderStyle.ts new file mode 100644 index 00000000000..0be0ff11cdc --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/plane/PlaneRenderStyle.ts @@ -0,0 +1,30 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { cloneDeep } from 'lodash'; +import { type RenderStyle } from '../../../base/renderStyles/RenderStyle'; +import { CommonRenderStyle } from '../../../base/renderStyles/CommonRenderStyle'; +import { BLACK_COLOR, WHITE_COLOR } from '../../../base/utilities/colors/colorExtensions'; + +export class PlaneRenderStyle extends CommonRenderStyle { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + public showSolid = true; + public showLines = true; + public linesColor = BLACK_COLOR.clone(); + public selectedLinesColor = WHITE_COLOR.clone(); + public opacityUse = true; + public opacity = 0.5; + public selectedOpacity = 0.8; + + // ================================================== + // OVERRIDES of BaseStyle + // ================================================== + + public override clone(): RenderStyle { + return cloneDeep(this); + } +} diff --git a/react-components/src/architecture/concrete/primitives/plane/PlaneView.ts b/react-components/src/architecture/concrete/primitives/plane/PlaneView.ts new file mode 100644 index 00000000000..020a95ab9c6 --- /dev/null +++ b/react-components/src/architecture/concrete/primitives/plane/PlaneView.ts @@ -0,0 +1,284 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { + Mesh, + MeshPhongMaterial, + Vector3, + type PerspectiveCamera, + Box3, + FrontSide, + BackSide, + type Side, + Triangle, + LineBasicMaterial, + BufferGeometry, + Line +} from 'three'; +import { type PlaneDomainObject } from './PlaneDomainObject'; +import { type DomainObjectChange } from '../../../base/domainObjectsHelpers/DomainObjectChange'; +import { Changes } from '../../../base/domainObjectsHelpers/Changes'; +import { type PlaneRenderStyle } from './PlaneRenderStyle'; +import { GroupThreeView } from '../../../base/views/GroupThreeView'; +import { Range3 } from '../../../base/utilities/geometry/Range3'; +import { TrianglesBuffers } from '../../../base/utilities/geometry/TrianglesBuffers'; +import { + CDF_TO_VIEWER_TRANSFORMATION, + type CustomObjectIntersectInput, + type CustomObjectIntersection +} from '@cognite/reveal'; +import { FocusType } from '../../../base/domainObjectsHelpers/FocusType'; +import { type DomainObjectIntersection } from '../../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { PrimitiveType } from '../PrimitiveType'; + +export class PlaneView extends GroupThreeView { + // ================================================== + // INSTANCE FIELDS + // ================================================== + + private readonly _sceneBoundingBox: Box3 = new Box3().makeEmpty(); // Cache the bounding box of the scene + private readonly _sceneRange: Range3 = new Range3(); + + // ================================================== + // INSTANCE PROPERTIES + // ================================================== + + public override get domainObject(): PlaneDomainObject { + return super.domainObject as PlaneDomainObject; + } + + protected override get style(): PlaneRenderStyle { + return super.style as PlaneRenderStyle; + } + + // ================================================== + // OVERRIDES of BaseView + // ================================================== + + public override update(change: DomainObjectChange): void { + super.update(change); + if ( + change.isChanged( + Changes.selected, + Changes.focus, + Changes.renderStyle, + Changes.color, + Changes.unit + ) + ) { + this.removeChildren(); + this.invalidateBoundingBox(); + this.invalidateRenderTarget(); + } + } + + // ================================================== + // OVERRIDES of ThreeView + // ================================================== + + public override beforeRender(camera: PerspectiveCamera): void { + super.beforeRender(camera); + } + + // ================================================== + // OVERRIDES of GroupThreeView + // ================================================== + + public override get isPartOfBoundingBox(): boolean { + return false; + } + + public override get useDepthTest(): boolean { + return this.style.depthTest; + } + + protected override get needsUpdate(): boolean { + const target = this.renderTarget; + + // Check if bounding box is different + const sceneBoundingBox = target.sceneBoundingBox; + if (sceneBoundingBox.equals(this._sceneBoundingBox)) { + return false; + } + this._sceneBoundingBox.copy(sceneBoundingBox); + this._sceneRange.copy(this._sceneBoundingBox); + return true; + } + + protected override addChildren(): void { + const { domainObject, style } = this; + const plane = domainObject.plane; + + const sceneBoundingBox = this._sceneBoundingBox.clone(); + sceneBoundingBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + const range = new Range3(); + range.copy(sceneBoundingBox); + + let p0: Vector3 | undefined; + let p1: Vector3 | undefined; + let p2: Vector3 | undefined; + let p3: Vector3 | undefined; + + if (domainObject.primitiveType === PrimitiveType.PlaneZ) { + p0 = range.getHorizontalIntersection(plane, 0); + p1 = range.getHorizontalIntersection(plane, 1); + p2 = range.getHorizontalIntersection(plane, 3); + p3 = range.getHorizontalIntersection(plane, 2); + } else { + for (let startIndex = 0; startIndex < 2; startIndex++) { + const start = new Vector3(); + const end = new Vector3(); + if (!range.getVerticalPlaneIntersection(plane, startIndex > 0, start, end)) { + continue; + } + if (startIndex === 0) { + p0 = start; + p1 = end; + } else { + p2 = start; + p3 = end; + } + } + } + if (p0 === undefined || p1 === undefined || p2 === undefined || p3 === undefined) { + return; + } + // Layout of the points + // 2------3 + // | | + // 0------1 + // Make sure it is right handed + const normal = Triangle.getNormal(p0, p1, p2, new Vector3()).normalize(); + const angle = normal.angleTo(plane.normal); + if (Math.abs(angle) > Math.PI / 2) { + [p0, p1] = [p1, p0]; + [p2, p3] = [p3, p2]; + } + p0.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + p1.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + p2.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + p3.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + + if (style.showSolid) { + const buffer = new TrianglesBuffers(4); + buffer.addPairWithNormal(p0, p1, plane.normal); + buffer.addPairWithNormal(p2, p3, plane.normal); + this.addSolid(buffer); + } + if (style.showLines) { + const material = new LineBasicMaterial({}); + + const hasFocus = domainObject.focusType !== FocusType.None; + material.color = + domainObject.isSelected || hasFocus ? style.selectedLinesColor : style.linesColor; + + const points = [p0, p1, p3, p2, p0]; + const geometry = new BufferGeometry().setFromPoints(points); + + const line = new Line(geometry, material); + this.addChild(line); + } + } + + public override intersectIfCloser( + intersectInput: CustomObjectIntersectInput, + closestDistance: number | undefined + ): undefined | CustomObjectIntersection { + const { domainObject } = this; + + if (domainObject.focusType === FocusType.Pending) { + return undefined; // Should never be picked + } + const plane = domainObject.plane.clone(); + plane.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); + + const ray = intersectInput.raycaster.ray; + const point = ray.intersectPlane(plane, new Vector3()); + if (point === null) { + return undefined; + } + if (domainObject.primitiveType === PrimitiveType.PlaneZ) { + if (!this._sceneRange.x.isInside(point.x)) { + return undefined; + } + if (!this._sceneRange.z.isInside(point.z)) { + return undefined; + } + } else { + if (!this._sceneRange.isInside(point)) { + return undefined; + } + } + const distanceToCamera = point.distanceTo(ray.origin); + if (closestDistance !== undefined && closestDistance < distanceToCamera) { + return undefined; + } + if (domainObject.useClippingInIntersection && !intersectInput.isVisible(point)) { + return undefined; + } + const customObjectIntersection: DomainObjectIntersection = { + type: 'customObject', + point, + distanceToCamera, + customObject: this, + domainObject + }; + if (this.shouldPickBoundingBox) { + customObjectIntersection.boundingBox = this.boundingBox; + } + return customObjectIntersection; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private addSolid(buffer: TrianglesBuffers): void { + if (!buffer.isFilled) { + return; + } + const { domainObject, style } = this; + const geometry = buffer.createBufferGeometry(); + { + const material = new MeshPhongMaterial(); + updateSolidMaterial(material, domainObject, style, FrontSide); + + const mesh = new Mesh(geometry, material); + this.addChild(mesh); + } + { + const material = new MeshPhongMaterial(); + updateSolidMaterial(material, domainObject, style, BackSide); + + const mesh = new Mesh(geometry, material); + this.addChild(mesh); + } + } +} + +// ================================================== +// PRIVATE FUNCTIONS: +// ================================================== + +function updateSolidMaterial( + material: MeshPhongMaterial, + domainObject: PlaneDomainObject, + style: PlaneRenderStyle, + side: Side +): void { + const color = side === FrontSide ? domainObject.color : domainObject.backSideColor; + const opacity = domainObject.isSelected ? style.selectedOpacity : style.opacity; + material.polygonOffset = true; + material.polygonOffsetFactor = 1; + material.polygonOffsetUnits = 4.0; + material.color = color; + material.opacity = style.opacityUse ? opacity : 1; + material.transparent = true; + material.emissive = color; + material.emissiveIntensity = 0.2; + material.side = side; + material.flatShading = true; + material.depthWrite = false; + material.depthTest = style.depthTest; +} diff --git a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts index f4063797870..9277917bc50 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts @@ -6,7 +6,7 @@ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; import { Vector3 } from 'three'; import { Range3 } from '../../base/utilities/geometry/Range3'; import { createFractalRegularGrid2 } from './geometry/createFractalRegularGrid2'; -import { DEFAULT_TERRAIN_NAME, TerrainDomainObject } from './TerrainDomainObject'; +import { TerrainDomainObject } from './TerrainDomainObject'; import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class SetTerrainVisibleCommand extends RenderTargetCommand { @@ -19,20 +19,16 @@ export class SetTerrainVisibleCommand extends RenderTargetCommand { } public override get tooltip(): TranslateKey { - return { key: 'UNKNOWN', fallback: 'Set terrain visible. Create it if not done' }; + return { fallback: 'Set terrain visible. Create it if not done' }; } protected override invokeCore(): boolean { const { renderTarget, rootDomainObject } = this; - let terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( - TerrainDomainObject, - DEFAULT_TERRAIN_NAME - ); + let terrainDomainObject = rootDomainObject.getDescendantByType(TerrainDomainObject); if (terrainDomainObject === undefined) { terrainDomainObject = new TerrainDomainObject(); const range = new Range3(new Vector3(0, 0, 0), new Vector3(1000, 1000, 200)); terrainDomainObject.grid = createFractalRegularGrid2(range); - terrainDomainObject.name = DEFAULT_TERRAIN_NAME; rootDomainObject.addChildInteractive(terrainDomainObject); terrainDomainObject.setVisibleInteractive(true, renderTarget); diff --git a/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts b/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts index 16f75151fea..e56c4114445 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts @@ -5,11 +5,10 @@ import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; import { TerrainRenderStyle } from './TerrainRenderStyle'; import { type RegularGrid2 } from './geometry/RegularGrid2'; -import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { type RenderStyle } from '../../base/renderStyles/RenderStyle'; import { type ThreeView } from '../../base/views/ThreeView'; import { TerrainThreeView } from './TerrainThreeView'; - -export const DEFAULT_TERRAIN_NAME = 'Terrain'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; export class TerrainDomainObject extends VisualDomainObject { // ================================================== @@ -38,8 +37,8 @@ export class TerrainDomainObject extends VisualDomainObject { // OVERRIDES of DomainObject // ================================================== - public override get typeName(): string { - return DEFAULT_TERRAIN_NAME; + public override get typeName(): TranslateKey { + return { fallback: 'Terrain' }; } public override createRenderStyle(): RenderStyle | undefined { diff --git a/react-components/src/architecture/concrete/terrainDomainObject/TerrainRenderStyle.ts b/react-components/src/architecture/concrete/terrainDomainObject/TerrainRenderStyle.ts index 81012efb3ea..379c2600518 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/TerrainRenderStyle.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/TerrainRenderStyle.ts @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash'; import { ColorType } from '../../base/domainObjectsHelpers/ColorType'; -import { RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; +import { RenderStyle } from '../../base/renderStyles/RenderStyle'; import { ColorMapType } from '../../base/utilities/colors/ColorMapType'; export class TerrainRenderStyle extends RenderStyle { diff --git a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts index a08401a44fb..8389deff498 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts @@ -6,7 +6,7 @@ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; import { Vector3 } from 'three'; import { Range3 } from '../../base/utilities/geometry/Range3'; import { createFractalRegularGrid2 } from './geometry/createFractalRegularGrid2'; -import { DEFAULT_TERRAIN_NAME, TerrainDomainObject } from './TerrainDomainObject'; +import { TerrainDomainObject } from './TerrainDomainObject'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type TranslateKey } from '../../base/utilities/TranslateKey'; @@ -25,10 +25,7 @@ export class UpdateTerrainCommand extends RenderTargetCommand { public override get isEnabled(): boolean { const { renderTarget, rootDomainObject } = this; - const terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( - TerrainDomainObject, - DEFAULT_TERRAIN_NAME - ); + const terrainDomainObject = rootDomainObject.getDescendantByType(TerrainDomainObject); if (terrainDomainObject === undefined) { return false; } @@ -37,10 +34,7 @@ export class UpdateTerrainCommand extends RenderTargetCommand { protected override invokeCore(): boolean { const { renderTarget, rootDomainObject } = this; - const terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( - TerrainDomainObject, - DEFAULT_TERRAIN_NAME - ); + const terrainDomainObject = rootDomainObject.getDescendantByType(TerrainDomainObject); if (terrainDomainObject === undefined) { return false; } diff --git a/react-components/src/components/Architecture/CommandButton.tsx b/react-components/src/components/Architecture/CommandButton.tsx index 3e884ff5df2..9ff00e31213 100644 --- a/react-components/src/components/Architecture/CommandButton.tsx +++ b/react-components/src/components/Architecture/CommandButton.tsx @@ -44,7 +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 [uniqueId, setUniqueId] = useState(0); const [icon, setIcon] = useState('Copy'); useEffect(() => { @@ -52,7 +52,7 @@ export const CommandButton = ({ setChecked(command.isChecked); setEnabled(command.isEnabled); setVisible(command.isVisible); - setUniqueIndex(command._uniqueIndex); + setUniqueId(command.uniqueId); setIcon(command.icon as IconType); } update(newCommand); @@ -67,20 +67,21 @@ export const CommandButton = ({ } const placement = isHorizontal ? 'top' : 'right'; const { key, fallback } = newCommand.tooltip; - // This was the only way it went through compiler: (more bytton types will be added in the future) + // This was the only way it went through compiler: (more button types will be added in the future) const type = newCommand.buttonType; if (type !== 'ghost' && type !== 'ghost-destructive') { return <>; } + const text = key === undefined ? fallback : t(key, fallback); return ( - +