From 37dbd67fd24417dab96bac91651ce220b872e3a0 Mon Sep 17 00:00:00 2001 From: Nils Petter Fremming <35219649+nilscognite@users.noreply.github.com> Date: Mon, 27 May 2024 16:36:49 +0200 Subject: [PATCH] (fix) Continue the architecture work (#4531) * Initial commit * Some movements * Fix linter problems * Make the axis a tiny better * Comments only * Some rafactoring * Make it better * Update MeasurementTool.ts * Fix lint * Make better axis tick labels, but still not perfect * handleEscape better * Changes in RevealRenderTarget * Update RootDomainObject.ts * Fix smal bug * Moving code * Fix typo * Set new default value * Fix typo --- .../architecture/base/commands/BaseCommand.ts | 5 + .../base/commands/BaseEditTool.ts | 23 +- .../architecture/base/commands/BaseTool.ts | 11 +- .../base/commands/NavigationTool.ts | 4 + .../base/commands/RenderTargetCommand.ts | 9 + .../base/concreteCommands/FitViewCommand.ts | 9 +- .../base/domainObjects/DomainObject.ts | 285 ++++++++++-------- .../base/domainObjects/RootDomainObject.ts | 10 +- .../base/domainObjects/VisualDomainObject.ts | 65 +++- .../base/domainObjectsHelpers/BaseCreator.ts | 68 +++-- .../base/domainObjectsHelpers/BaseDragger.ts | 12 +- .../base/domainObjectsHelpers/Changes.ts | 18 +- .../base/domainObjectsHelpers/ColorType.ts | 8 +- .../DomainObjectChange.ts | 71 ++--- .../base/domainObjectsHelpers/VisibleState.ts | 2 +- .../base/renderTarget/RevealRenderTarget.ts | 58 +++- .../base/utilities/box/BoxFace.ts | 40 ++- .../utilities/extensions/mathExtensions.ts | 21 +- .../utilities/extensions/rayExtensions.ts | 8 + .../utilities/extensions/stringExtensions.ts | 31 +- .../base/utilities/geometry/Range1.ts | 2 +- .../base/utilities/geometry/Range3.ts | 16 +- .../src/architecture/base/views/BaseView.ts | 14 +- .../architecture/base/views/GroupThreeView.ts | 15 +- .../src/architecture/base/views/ThreeView.ts | 11 +- .../concrete/axis/AxisRenderStyle.ts | 2 +- .../concrete/axis/AxisThreeView.ts | 94 ++++-- .../concrete/axis/SetAxisVisibleCommand.ts | 6 +- .../boxDomainObject/MeasureBoxCreator.ts | 12 +- .../boxDomainObject/MeasureBoxDomainObject.ts | 140 +++++---- .../boxDomainObject/MeasureBoxDragger.ts | 11 +- .../boxDomainObject/MeasureBoxView.ts | 142 ++++----- .../boxDomainObject/MeasureLineCreator.ts | 10 +- .../boxDomainObject/MeasureLineView.ts | 30 +- .../boxDomainObject/MeasureRenderStyle.ts | 2 +- .../boxDomainObject/MeasurementFunctions.ts | 30 -- .../boxDomainObject/MeasurementTool.ts | 87 +++--- .../SetMeasurmentTypeCommand.ts | 18 +- .../ShowMeasurmentsOnTopCommand.ts | 8 +- .../SetTerrainVisibleCommand.ts | 4 +- .../TerrainDomainObject.ts | 4 +- .../terrainDomainObject/TerrainThreeView.ts | 28 +- .../UpdateTerrainCommand.ts | 7 +- .../geometry/ContouringService.ts | 4 +- .../Architecture/DomainObjectPanel.tsx | 4 +- 45 files changed, 864 insertions(+), 595 deletions(-) delete mode 100644 react-components/src/architecture/concrete/boxDomainObject/MeasurementFunctions.ts diff --git a/react-components/src/architecture/base/commands/BaseCommand.ts b/react-components/src/architecture/base/commands/BaseCommand.ts index 3f8ad3f7e05..04ee863126d 100644 --- a/react-components/src/architecture/base/commands/BaseCommand.ts +++ b/react-components/src/architecture/base/commands/BaseCommand.ts @@ -11,6 +11,11 @@ export type Tooltip = { fallback?: string; }; +/** + * Base class for all command and tools. Thses are object that can do a + * user interaction with the system. It also have enough information to + * generate the UI for the command. + */ export abstract class BaseCommand { // ================================================== // INSTANCE FIELDS diff --git a/react-components/src/architecture/base/commands/BaseEditTool.ts b/react-components/src/architecture/base/commands/BaseEditTool.ts index ebd552b168f..afb6f425d98 100644 --- a/react-components/src/architecture/base/commands/BaseEditTool.ts +++ b/react-components/src/architecture/base/commands/BaseEditTool.ts @@ -6,7 +6,16 @@ import { NavigationTool } from './NavigationTool'; import { type DomainObject } from '../domainObjects/DomainObject'; import { isDomainObjectIntersection } from '../domainObjectsHelpers/DomainObjectIntersection'; import { type BaseDragger } from '../domainObjectsHelpers/BaseDragger'; +import { type VisualDomainObject } from '../domainObjects/VisualDomainObject'; +import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; +/** + * The `BaseEditTool` class is an abstract class that extends the `NavigationTool` class. + * It provides a base implementation for editing tools in a specific architecture. + * Custom editing tools can be created by extending this class and overriding its methods. + * This class will also proivide the dragging functionality if the picked domain object has + * createDragger() overridden. + */ export abstract class BaseEditTool extends NavigationTool { // ================================================== // INSTANCE FIELDS @@ -57,7 +66,7 @@ export abstract class BaseEditTool extends NavigationTool { } // ================================================== - // VIRTUALS METHODS + // VIRTUAL METHODS // ================================================== /** @@ -73,11 +82,16 @@ export abstract class BaseEditTool extends NavigationTool { if (!isDomainObjectIntersection(intersection)) { return undefined; } - const domainObject = intersection.domainObject; + const domainObject = intersection.domainObject as VisualDomainObject; if (domainObject === undefined) { return undefined; } - return domainObject.createDragger(intersection); + const ray = this.getRay(event); + const matrix = CDF_TO_VIEWER_TRANSFORMATION.clone().invert(); + const point = intersection.point.clone(); + point.applyMatrix4(matrix); + ray.applyMatrix4(matrix); + return domainObject.createDragger({ intersection, point, ray }); } // ================================================== @@ -85,8 +99,7 @@ export abstract class BaseEditTool extends NavigationTool { // ================================================== protected deselectAll(except?: DomainObject | undefined): void { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { rootDomainObject } = this; for (const domainObject of rootDomainObject.getDescendants()) { if (except !== undefined && domainObject === except) { continue; diff --git a/react-components/src/architecture/base/commands/BaseTool.ts b/react-components/src/architecture/base/commands/BaseTool.ts index 03c814a2342..af3ca3a4b20 100644 --- a/react-components/src/architecture/base/commands/BaseTool.ts +++ b/react-components/src/architecture/base/commands/BaseTool.ts @@ -21,6 +21,10 @@ import { type BaseCommand } from './BaseCommand'; import { ActiveToolUpdater } from '../reactUpdaters/ActiveToolUpdater'; import { PopupStyle } from '../domainObjectsHelpers/PopupStyle'; +/** + * Base class for intraction in the 3D viewer + * Provides common functionality and virtual methods to be overridden by derived classes. + */ export abstract class BaseTool extends RenderTargetCommand { // ================================================== // OVERRIDES @@ -116,7 +120,9 @@ export abstract class BaseTool extends RenderTargetCommand { this.renderTarget.cursor = this.defaultCursor; } - protected async getIntersection(event: PointerEvent): Promise { + protected async getIntersection( + event: PointerEvent | WheelEvent + ): Promise { const { renderTarget } = this; const { viewer } = renderTarget; const point = viewer.getPixelCoordinatesFromEvent(event); @@ -132,8 +138,7 @@ export abstract class BaseTool extends RenderTargetCommand { classType: Class ): DomainObjectIntersection | undefined { // This function is similar to getIntersection, but it only considers a specific DomainObject - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { renderTarget, rootDomainObject } = this; const { viewer } = renderTarget; const point = viewer.getPixelCoordinatesFromEvent(event); diff --git a/react-components/src/architecture/base/commands/NavigationTool.ts b/react-components/src/architecture/base/commands/NavigationTool.ts index 44826126fc1..fcd2b0266c3 100644 --- a/react-components/src/architecture/base/commands/NavigationTool.ts +++ b/react-components/src/architecture/base/commands/NavigationTool.ts @@ -7,6 +7,10 @@ import { BaseTool } from './BaseTool'; import { type Tooltip } from './BaseCommand'; import { type IFlexibleCameraManager } from '@cognite/reveal'; +/** + * Represents a tool navigation tool used for camera manipulation. + * Inherit from this class if you like to have some camera manipulation in your tool. + */ export class NavigationTool extends BaseTool { // ================================================== // INSTANVE PROPERTIES diff --git a/react-components/src/architecture/base/commands/RenderTargetCommand.ts b/react-components/src/architecture/base/commands/RenderTargetCommand.ts index 47bf3fc9b48..d297277be1a 100644 --- a/react-components/src/architecture/base/commands/RenderTargetCommand.ts +++ b/react-components/src/architecture/base/commands/RenderTargetCommand.ts @@ -4,7 +4,12 @@ import { BaseCommand } from './BaseCommand'; import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; +import { type RootDomainObject } from '../domainObjects/RootDomainObject'; +/** + * Represents a base class where the render target is known. + * Subclasses of this class is used to interact with the render target + */ export abstract class RenderTargetCommand extends BaseCommand { public _renderTarget: RevealRenderTarget | undefined = undefined; @@ -15,6 +20,10 @@ export abstract class RenderTargetCommand extends BaseCommand { return this._renderTarget; } + public get rootDomainObject(): RootDomainObject { + return this.renderTarget.rootDomainObject; + } + public override invoke(): boolean { const success = this.invokeCore(); if (success) { diff --git a/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts b/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts index 9d830a965e2..a3b1cfa64cb 100644 --- a/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts +++ b/react-components/src/architecture/base/concreteCommands/FitViewCommand.ts @@ -16,13 +16,6 @@ export class FitViewCommand extends RenderTargetCommand { protected override invokeCore(): boolean { const { renderTarget } = this; - const { viewer } = renderTarget; - - const boundingBox = viewer.getSceneBoundingBox(); - if (boundingBox.isEmpty()) { - return false; - } - viewer.fitCameraToBoundingBox(boundingBox); - return true; + return renderTarget.fitView(); } } diff --git a/react-components/src/architecture/base/domainObjects/DomainObject.ts b/react-components/src/architecture/base/domainObjects/DomainObject.ts index 54dacfd389b..ec7db29f776 100644 --- a/react-components/src/architecture/base/domainObjects/DomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/DomainObject.ts @@ -14,8 +14,6 @@ import { getNextColor } from '../utilities/colors/getNextColor'; import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { ColorType } from '../domainObjectsHelpers/ColorType'; import { BLACK_COLOR, WHITE_COLOR } from '../utilities/colors/colorExtensions'; -import { type DomainObjectIntersection } from '../domainObjectsHelpers/DomainObjectIntersection'; -import { type BaseDragger } from '../domainObjectsHelpers/BaseDragger'; import { Views } from '../domainObjectsHelpers/Views'; import { type PanelInfo } from '../domainObjectsHelpers/PanelInfo'; import { PopupStyle } from '../domainObjectsHelpers/PopupStyle'; @@ -23,7 +21,6 @@ import { PopupStyle } from '../domainObjectsHelpers/PopupStyle'; /** * Represents an abstract base class for domain objects. * @abstract - * @extends BaseSubject */ export abstract class DomainObject { // ================================================== @@ -33,8 +30,16 @@ export abstract class DomainObject { // Some basic states private _name: string | undefined = undefined; private _color: Color | undefined = undefined; + + // Selection. This is used for selection in 3D viewer. Selection in a tree view is + // not implemented yet, but should be added in the future. private _isSelected: boolean = false; + + // Maximum one active object for each type of domain object. Works as a long term selection + // For instance you can have many crop boxes, but only one can be used at the time. private _isActive: boolean = false; + + // Expaned when it is shown in a tree view private _isExpanded = false; // Parent-Child relationship @@ -65,33 +70,6 @@ export abstract class DomainObject { return `${this.name} [${nameExtension}]`; } - // ================================================== - // VIRTUAL METHODS: Others - // ================================================== - - /** - * Initializes the core functionality of the domain object. - * This method should be overridden in derived classes to provide custom implementation. - * @param change - The change object representing the update. - * @remarks - * Always call `super.initializeCore()` in the overrides. - */ - protected initializeCore(): void {} - - /** - * Removes the core functionality of the domain object. - * This method should be overridden in derived classes to provide custom implementation. - * @remarks - * Always call `super.removeCore()` in the overrides. - */ - protected removeCore(): void { - this.views.clear(); - } - - public get icon(): string { - return 'Unknown'; - } - // ================================================== // INSTANCE/VIRTUAL METHODS: Nameing // ================================================== @@ -119,29 +97,6 @@ export abstract class DomainObject { return equalsIgnoreCase(this.name, name); } - // ================================================== - // INSTANCE/VIRTUAL METHODS: Notification - // ================================================== - - /** - * Notifies the registered views and listeners about a change in the domain object. - * This method should be overridden in derived classes to provide custom implementation. - * @param change - The change object representing the update. - * @remarks - * Always call `super.notifyCore()` in the overrides. - */ - protected notifyCore(change: DomainObjectChange): void { - this.views.notify(this, change); - } - - public notify(change: DomainObjectChange | symbol): void { - if (change instanceof DomainObjectChange) { - this.notifyCore(change); - } else { - this.notify(new DomainObjectChange(change)); - } - } - // ================================================== // INSTANCE/VIRTUAL METHODS: Color // ================================================== @@ -238,7 +193,7 @@ export abstract class DomainObject { } // ================================================== - // INSTANCE/INSTANCE METHODS: Expanded + // INSTANCE/INSTANCE METHODS: Expanded in tree view // ================================================== public get canBeExpanded(): boolean { @@ -262,6 +217,33 @@ export abstract class DomainObject { return true; } + // ================================================== + // VIRTUAL METHODS: Appearance in the tree view + // ================================================== + + public get canBeRemoved(): boolean { + return true; // to be overridden + } + + public canBeChecked(_target: RevealRenderTarget): boolean { + return true; // to be overridden + } + + // ================================================== + // VIRTUAL METHODS: Notification + // ================================================== + + /** + * Notifies the registered views and listeners about a change in the domain object. + * This method should be overridden in derived classes to provide custom implementation. + * @param change - The change object representing the update. + * @remarks + * Always call `super.notifyCore()` in the overrides. + */ + protected notifyCore(change: DomainObjectChange): void { + this.views.notify(this, change); + } + // ================================================== // VIRTUAL METHODS: For updating the panel // ================================================== @@ -277,28 +259,65 @@ export abstract class DomainObject { } // ================================================== - // VIRTUAL METHODS: Appearance in the explorer + // VIRTUAL METHODS: Others // ================================================== - public get canBeDeleted(): boolean { - return true; // to be overridden + public get icon(): string { + return 'Unknown'; } - public canBeChecked(_target: RevealRenderTarget): boolean { - return true; // to be overridden + /** + * Removes the core functionality of the domain object. + * This method should be overridden in derived classes to provide custom implementation. + * @remarks + * Always call `super.removeCore()` in the overrides. + */ + protected removeCore(): void { + this.views.clear(); + } + + // ================================================== + // VIRTUAL METHODS: Render styles + // ================================================== + + /** + * Override if the render style is taken from another domain object, for instance the parent + * or somewhere else in the hieracy + * @returns The render style root + */ + public get renderStyleRoot(): DomainObject | undefined { + return undefined; + } + + /** + * Factory method to create the render style for the domain object. + * Override this method to create a custom render style. + * @returns The render style + */ + public createRenderStyle(): RenderStyle | undefined { + return undefined; } + /** + * Verifies the render style for the domain object, because the render style may + * be not valid in some cases. In this method you can change the render style. + * You can also change som fields in the rebnderstyle to get default values + * dependent of the domain object itself. + * Override this method when needed + */ + public verifyRenderStyle(_style: RenderStyle): void {} + // ================================================== // VIRTUAL METHODS: Visibility // ================================================== - public getVisibleState(target: RevealRenderTarget): VisibleState { + public getVisibleState(renderTarget: RevealRenderTarget): VisibleState { let numCandidates = 0; let numAll = 0; let numNone = 0; for (const child of this.children) { - const childState = child.getVisibleState(target); + const childState = child.getVisibleState(renderTarget); if (childState === VisibleState.Disabled) { continue; } @@ -319,26 +338,26 @@ export abstract class DomainObject { return VisibleState.All; } if (numCandidates === numNone) { - return this.canBeChecked(target) ? VisibleState.None : VisibleState.CanNotBeChecked; + return this.canBeChecked(renderTarget) ? VisibleState.None : VisibleState.CanNotBeChecked; } return VisibleState.Some; } public setVisibleInteractive( visible: boolean, - target: RevealRenderTarget, + renderTarget: RevealRenderTarget, topLevel = true // When calling this from outside, this value should alwaus be true ): boolean { - const visibleState = this.getVisibleState(target); + const visibleState = this.getVisibleState(renderTarget); if (visibleState === VisibleState.Disabled) { return false; } - if (visibleState === VisibleState.None && !this.canBeChecked(target)) { + if (visibleState === VisibleState.None && !this.canBeChecked(renderTarget)) { return false; } let hasChanged = false; for (const child of this.children) { - if (child.setVisibleInteractive(visible, target, false)) { + if (child.setVisibleInteractive(visible, renderTarget, false)) { hasChanged = true; } } @@ -351,6 +370,26 @@ export abstract class DomainObject { return true; } + // ================================================== + // INSTANCE METHODS: Notification + // ================================================== + + public notify(change: DomainObjectChange | symbol): void { + if (!(change instanceof DomainObjectChange)) { + change = new DomainObjectChange(change); + } + this.notifyCore(change); + } + + public notifyRecursive(change: DomainObjectChange | symbol): void { + if (!(change instanceof DomainObjectChange)) { + change = new DomainObjectChange(change); + } + for (const descendant of this.getDescendants()) { + descendant.notify(change); + } + } + protected notifyVisibleStateChange(): void { const change = new DomainObjectChange(Changes.visibleState); this.notify(change); @@ -361,37 +400,15 @@ export abstract class DomainObject { descendant.notify(change); } } - - public toggleVisibleInteractive(target: RevealRenderTarget): void { - const visibleState = this.getVisibleState(target); - if (visibleState === VisibleState.None) this.setVisibleInteractive(true, target); - else if (visibleState === VisibleState.Some || visibleState === VisibleState.All) - this.setVisibleInteractive(false, target); - } - - // ================================================== - // VIRTUAL METHODS: Render styles - // ================================================== - - public get renderStyleRoot(): DomainObject | undefined { - return undefined; // Override if the render style is taken from another domain object - } - - public createRenderStyle(): RenderStyle | undefined { - return undefined; // Override when creating a render style - } - - public verifyRenderStyle(_style: RenderStyle): void { - // override when validating the render style - } - // ================================================== - // VIRTUAL METHODS: Create dragger + // INSTANCE METHODS: Visibility // ================================================== - // override when creating a dragger operation in the BaseEditTool - public createDragger(_intersection: DomainObjectIntersection): BaseDragger | undefined { - return undefined; + public toggleVisibleInteractive(renderTarget: RevealRenderTarget): void { + const visibleState = this.getVisibleState(renderTarget); + if (visibleState === VisibleState.None) this.setVisibleInteractive(true, renderTarget); + else if (visibleState === VisibleState.Some || visibleState === VisibleState.All) + this.setVisibleInteractive(false, renderTarget); } // ================================================== @@ -470,18 +487,9 @@ export abstract class DomainObject { } // ================================================== - // INSTANCE METHODS: Get descendants + // INSTANCE METHODS: Get descendants (returning a generator) // ================================================== - public getSelected(): DomainObject | undefined { - for (const descendant of this.getThisAndDescendants()) { - if (descendant.isSelected) { - return descendant; - } - } - return undefined; - } - public *getDescendants(): Generator { for (const child of this.children) { yield child; @@ -498,6 +506,36 @@ export abstract class DomainObject { } } + public *getDescendantsByType(classType: Class): Generator { + for (const descendant of this.getDescendants()) { + if (isInstanceOf(descendant, classType)) { + yield descendant; + } + } + } + + public *getThisAndDescendantsByType(classType: Class): Generator { + for (const descendant of this.getThisAndDescendants()) { + if (isInstanceOf(descendant, classType)) { + yield descendant; + } + } + } + + // ================================================== + // INSTANCE METHODS: Get single descendant + // (returning a DomainObject | undefined) + // ================================================== + + public getDescendantByType(classType: Class): T | undefined { + for (const descendant of this.getDescendants()) { + if (isInstanceOf(descendant, classType)) { + return descendant; + } + } + return undefined; + } + public getDescendantByName(name: string): DomainObject | undefined { for (const descendant of this.getDescendants()) { if (descendant.hasEqualName(name)) { @@ -519,33 +557,27 @@ export abstract class DomainObject { return undefined; } - public getDescendantByType(classType: Class): T | undefined { - for (const descendant of this.getDescendants()) { - if (isInstanceOf(descendant, classType)) { + public getSelectedDescendant(): DomainObject | undefined { + for (const descendant of this.getThisAndDescendants()) { + if (descendant.isSelected) { return descendant; } } return undefined; } - public *getDescendantsByType(classType: Class): Generator { - for (const child of this.children) { - if (isInstanceOf(child, classType)) { - yield child; - } - for (const descendant of child.getDescendantsByType(classType)) { - yield descendant; + public getSelectedDescendantByType(classType: Class): T | undefined { + for (const descendant of this.getDescendantsByType(classType)) { + if (descendant.isSelected) { + return descendant; } } + return undefined; } public getActiveDescendantByType(classType: Class): T | undefined { - for (const child of this.children) { - if (child.isActive && isInstanceOf(child, classType)) { - return child; - } - const descendant = child.getActiveDescendantByType(classType); - if (descendant !== undefined) { + for (const descendant of this.getDescendantsByType(classType)) { + if (descendant.isActive) { return descendant; } } @@ -632,6 +664,7 @@ export abstract class DomainObject { } public removeInteractive(): void { + // You may call canBeDeleted() before calling this for (const child of this.children) { child.removeInteractive(); } @@ -650,13 +683,16 @@ export abstract class DomainObject { // ================================================== public getRenderStyle(): RenderStyle | undefined { + // Find the root of the render style const root = this.renderStyleRoot; if (root !== undefined && root !== this) { return root.getRenderStyle(); } + // Create it if not created if (this._renderStyle === undefined) { this._renderStyle = this.createRenderStyle(); } + // Verify it if (this._renderStyle !== undefined) { this.verifyRenderStyle(this._renderStyle); } @@ -671,11 +707,11 @@ export abstract class DomainObject { // INSTANCE METHODS: Get auto name and color // ================================================== - protected generateNewColor(): Color { + private generateNewColor(): Color { return this.canChangeColor ? getNextColor().clone() : WHITE_COLOR.clone(); } - protected generateNewName(): string { + private generateNewName(): string { let result = this.typeName; if (!this.canChangeName) { return result; @@ -696,6 +732,11 @@ export abstract class DomainObject { return result; } + // ================================================== + // INSTANCE METHODS: Color type + // Used in the renderstyle to determin which of the color a doamin object should have. + // ================================================== + public supportsColorType(colorType: ColorType, solid: boolean): boolean { switch (colorType) { case ColorType.Specified: diff --git a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts index 2ca67d20e3f..14972f804da 100644 --- a/react-components/src/architecture/base/domainObjects/RootDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/RootDomainObject.ts @@ -2,16 +2,24 @@ * Copyright 2024 Cognite AS */ +import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { DomainObject } from './DomainObject'; export class RootDomainObject extends DomainObject { + private readonly _renderTarget: RevealRenderTarget; + + public get renderTarget(): RevealRenderTarget { + return this._renderTarget; + } + // ================================================== // CONSTRUCTOR // ================================================== - public constructor() { + public constructor(renderTarget: RevealRenderTarget) { super(); this.name = 'Root'; + this._renderTarget = renderTarget; } // ================================================== diff --git a/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts b/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts index dc9756c4b54..29ba7a64eea 100644 --- a/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts +++ b/react-components/src/architecture/base/domainObjects/VisualDomainObject.ts @@ -6,18 +6,26 @@ import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget'; import { ThreeView } from '../views/ThreeView'; import { VisibleState } from '../domainObjectsHelpers/VisibleState'; import { DomainObject } from './DomainObject'; +import { type DomainObjectIntersection } from '../domainObjectsHelpers/DomainObjectIntersection'; +import { type BaseDragger } from '../domainObjectsHelpers/BaseDragger'; +import { type Vector3, type Ray } from 'three'; + +/** + * Represents a visual domain object that can be rendered and manipulated in a three-dimensional space. + * This class extends the `DomainObject` class and provides additional functionality for visualization. + */ export abstract class VisualDomainObject extends DomainObject { // ================================================== // OVERRIDES of DomainObject // ================================================== - public override getVisibleState(target: RevealRenderTarget): VisibleState { - if (this.isVisible(target)) { + public override getVisibleState(renderTarget: RevealRenderTarget): VisibleState { + if (this.isVisible(renderTarget)) { return VisibleState.All; } if (this.canCreateThreeView()) { - if (this.canBeChecked(target)) { + if (this.canBeChecked(renderTarget)) { return VisibleState.None; } return VisibleState.CanNotBeChecked; @@ -27,13 +35,13 @@ export abstract class VisualDomainObject extends DomainObject { public override setVisibleInteractive( visible: boolean, - target: RevealRenderTarget, + renderTarget: RevealRenderTarget, topLevel = true ): boolean { - if (visible && !this.canBeChecked(target)) { + if (visible && !this.canBeChecked(renderTarget)) { return false; } - if (!this.setVisible(visible, target)) { + if (!this.setVisible(visible, renderTarget)) { return false; } if (topLevel) { @@ -46,37 +54,60 @@ export abstract class VisualDomainObject extends DomainObject { // VIRTUAL METHODS // ================================================== + /** + * Factory methods to create its own three view for visualization in three.js + */ protected abstract createThreeView(): ThreeView | undefined; + /** + * Determines whether the visual domain object can create a three view. + * It may have a state when it can not create a view bacause of other dependencies + * + * @returns A boolean value indicating whether the visual domain object can create a three view. + */ protected canCreateThreeView(): boolean { return true; } + /** + * Factory method to create a dragger to interpret the mouse dragging operation + * This function is used in BaseEditTool + * + * @returns The dragger + */ + public createDragger(_props: CreateDraggerProps): BaseDragger | undefined { + return undefined; + } + // ================================================== // INSTANCE METHODS // ================================================== - public getViewByTarget(target: RevealRenderTarget): ThreeView | undefined { + public getViewByTarget(renderTarget: RevealRenderTarget): ThreeView | undefined { for (const view of this.views.getByType(ThreeView)) { - if (view.renderTarget === target) { + if (view.renderTarget === renderTarget) { return view; } } } - public isVisible(target: RevealRenderTarget): boolean { - return this.getViewByTarget(target) !== undefined; + /** + * 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. * @param target - The target RevealRenderTarget where the visual domain object will be attached. * @returns A boolean indicating whether the state has changed. */ - public setVisible(visible: boolean, target: RevealRenderTarget): boolean { - let view = this.getViewByTarget(target); + public setVisible(visible: boolean, renderTarget: RevealRenderTarget): boolean { + let view = this.getViewByTarget(renderTarget); if (visible) { if (view !== undefined) { return false; @@ -89,7 +120,7 @@ export abstract class VisualDomainObject extends DomainObject { return false; } this.views.addView(view); - view.attach(this, target); + view.attach(this, renderTarget); view.initialize(); view.onShow(); } else { @@ -101,3 +132,9 @@ export abstract class VisualDomainObject extends DomainObject { return true; // State has changed } } + +export type CreateDraggerProps = { + intersection: DomainObjectIntersection; + point: Vector3; + ray: Ray; +}; diff --git a/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts b/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts index c0398338d15..0735a332859 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/BaseCreator.ts @@ -2,9 +2,9 @@ * Copyright 2024 Cognite AS */ -import { type Ray, type Vector3 } from 'three'; +import { type Vector3, type Ray } from 'three'; import { replaceLast } from '../utilities/extensions/arrayExtensions'; -import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; +import { type AnyIntersection, CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; import { type DomainObject } from '../domainObjects/DomainObject'; /** @@ -22,26 +22,30 @@ export abstract class BaseCreator { // INSTANCE PROPERTIES // ================================================== - public get points(): Vector3[] { + protected get points(): Vector3[] { return this._points; } - public get pointCount(): number { + protected get pointCount(): number { return this.points.length; } - public get realPointCount(): number { + protected get notPendingPointCount(): number { return this.lastIsPending ? this.pointCount - 1 : this.pointCount; } - public get firstPoint(): Vector3 { + protected get firstPoint(): Vector3 { return this.points[0]; } - public get lastPoint(): Vector3 { + protected get lastPoint(): Vector3 { return this.points[this.pointCount - 1]; } + protected get lastNotPendingPoint(): Vector3 { + return this.points[this.notPendingPointCount - 1]; + } + protected get lastIsPending(): boolean { return this._lastIsPending; } @@ -50,36 +54,66 @@ export abstract class BaseCreator { // VIRTUAL METHODS // ================================================== + /** + * Gets the value indicating whether to prefer intersection with somthing. + * If this is true, it will first try to intersect an object. If false the point + * will normally be calculatd based on the previous point and the ray in addPointCore + * + * @returns {boolean} The value indicating whether to prefer intersection. + */ public get preferIntersection(): boolean { return false; } public abstract get domainObject(): DomainObject; - public abstract get maximumPointCount(): number; - - public abstract get minimumPointCount(): number; - + /** + * @returns The minimum required points to create the domain object. + */ + protected abstract get minimumPointCount(): number; + + /** + * @returns The maximim required points to create the domain object. + */ + + protected abstract get maximumPointCount(): number; + /** + * Adds a new point + * @param ray - The ray the camera has (in Cdf coordinates) + * @param point - The point to add.(in Cdf coordinates). If undefined, it indicates that + * it wasn't intersection anything. Then then ray and the previous point can be used to calculate the point. + * @param isPending - Indicates whether the point is pending (hover over instead of clicking). + * @returns A boolean value indicating whether the point was successfully added. + */ protected abstract addPointCore( ray: Ray, point: Vector3 | undefined, isPending: boolean ): boolean; - public handleEscape(): void {} + /** + * Handles the escape key press event. + * + * @returns {boolean} Returns true if the pending object is created successfully, false if it is removed + */ + public handleEscape(): boolean { + return false; + } // ================================================== // INSTANCE METHODS // ================================================== public get isFinished(): boolean { - return this.realPointCount === this.maximumPointCount; + return this.notPendingPointCount >= this.maximumPointCount; } - public addPoint(ray: Ray, point: Vector3 | undefined, isPending: boolean): boolean { - if (point !== undefined) { - point = point.clone(); - } + public addPoint( + ray: Ray, + intersection: AnyIntersection | undefined, + isPending: boolean = false + ): boolean { + const point = intersection?.point.clone(); this.convertToCdfCoords(ray, point); return this.addPointCore(ray, point, isPending); } diff --git a/react-components/src/architecture/base/domainObjectsHelpers/BaseDragger.ts b/react-components/src/architecture/base/domainObjectsHelpers/BaseDragger.ts index c2c918c8721..aee6d3481c9 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/BaseDragger.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/BaseDragger.ts @@ -2,8 +2,9 @@ * Copyright 2024 Cognite AS */ -import { type Ray, Vector3 } from 'three'; +import { Ray, type Vector3 } from 'three'; import { type DomainObject } from '../domainObjects/DomainObject'; +import { type CreateDraggerProps } from '../domainObjects/VisualDomainObject'; /** * The `BaseDragger` class represents a utility for dragging and manipulating any object in 3D space. @@ -14,14 +15,16 @@ export abstract class BaseDragger { // INSTANCE FIELDS // ================================================== - public readonly point: Vector3 = new Vector3(); // Intersection point at pointer down + protected readonly point: Vector3; // Intersection point at pointer down in CDF coordinates + protected readonly ray: Ray = new Ray(); // Intersection point at pointer down in CDF coordinates // ================================================== // CONTRUCTOR // ================================================== - protected constructor(startDragPoint: Vector3) { - this.point.copy(startDragPoint); + protected constructor(props: CreateDraggerProps) { + this.point = props.point; + this.ray = props.ray; } // ================================================== @@ -35,6 +38,7 @@ export abstract class BaseDragger { } // This must be overriden + // Notte that the ray comes in CDF coordinates public abstract onPointerDrag(_event: PointerEvent, ray: Ray): boolean; public onPointerUp(_event: PointerEvent): void { diff --git a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts index 513f25812d8..883275159eb 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/Changes.ts @@ -3,26 +3,26 @@ */ export class Changes { - // States changed + // Domain object boolean states changed public static readonly visibleState: symbol = Symbol('visibleState'); public static readonly active: symbol = Symbol('active'); public static readonly expanded: symbol = Symbol('expanded'); public static readonly selected: symbol = Symbol('selected'); public static readonly focus: symbol = Symbol('focus'); - public static readonly loaded: symbol = Symbol('loaded'); - // Fields changed + // Domain object Fields changed public static readonly naming: symbol = Symbol('naming'); public static readonly color: symbol = Symbol('color'); public static readonly icon: symbol = Symbol('icon'); public static readonly colorMap: symbol = Symbol('colorMap'); - public static readonly geometry: symbol = Symbol('geometry'); public static readonly renderStyle: symbol = Symbol('renderStyle'); - // Parent-child relationship changed - public static readonly childDeleted: symbol = Symbol('childDeleted'); - public static readonly childAdded: symbol = Symbol('childAdded'); + // Something in the geometry changed + public static readonly geometry: symbol = Symbol('geometry'); - public static readonly added: symbol = Symbol('added'); - public static readonly deleted: symbol = Symbol('deleted'); + // Parent-child relationship changed + public static readonly added: symbol = Symbol('added'); // When added to the system + public static readonly deleted: symbol = Symbol('deleted'); // When deleted from the system + public static readonly childDeleted: symbol = Symbol('childDeleted'); // When a child is deleted + public static readonly childAdded: symbol = Symbol('childAdded'); // When a child is added } diff --git a/react-components/src/architecture/base/domainObjectsHelpers/ColorType.ts b/react-components/src/architecture/base/domainObjectsHelpers/ColorType.ts index 3ee6febda21..e4b391b1945 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/ColorType.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/ColorType.ts @@ -3,10 +3,10 @@ */ export enum ColorType { - ColorMap, // Color by the given color map - Specified, // Use the color of the node - Parent, // Use the color of the parent node + Specified, // Use the color of the domain object itself + Parent, // Use the color of the parent domain object Black, White, - Different // Use different colors (normally use for debugging) + Different, // Use different colors (normally use for debugging) + ColorMap // Color by the given color map } diff --git a/react-components/src/architecture/base/domainObjectsHelpers/DomainObjectChange.ts b/react-components/src/architecture/base/domainObjectsHelpers/DomainObjectChange.ts index 3a874328405..73c5e1f69e6 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/DomainObjectChange.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/DomainObjectChange.ts @@ -2,6 +2,8 @@ * Copyright 2024 Cognite AS */ +import { equalsIgnoreCaseAndSpace } from '../utilities/extensions/stringExtensions'; + export class DomainObjectChange { // ================================================== // INSTANCE FIELDS @@ -13,9 +15,9 @@ export class DomainObjectChange { // CONSTRUCTOR // ================================================== - public constructor(change?: symbol, name?: string) { + public constructor(change?: symbol, fieldName?: string) { if (change !== undefined) { - this.add(change, name); + this.add(change, fieldName); } } @@ -27,6 +29,11 @@ export class DomainObjectChange { return this._changes === undefined || this._changes.length === 0; } + /** + * Checks if the domain object has been changed based on the specified changes. + * @param changes - The changes to check. + * @returns `true` if any of the specified changes are present in the domain object, `false` otherwise. + */ public isChanged(...changes: symbol[]): boolean { if (this._changes === undefined) { return false; @@ -39,40 +46,24 @@ export class DomainObjectChange { return false; } - public isNameChanged(change: symbol, ...names: string[]): boolean { + /** + * Checks if the domain object has been changed based on the specified change and some specific fieldnames. + * For instance + * if (isFieldNameChanged(Changes.renderStyle, 'lineWidth', 'lineColor')) { + * // Now you have to update the line matrial only + * } + * @param change - The symbol representing the change. + * @param fieldNames - The field names to compare against the change symbol. + * @returns A boolean indicating whether the name has changed or not. + */ + public isFieldNameChanged(change: symbol, ...fieldNames: string[]): boolean { // This igonores space and case. - const name = this.getName(change); - if (name === undefined) { + const fieldName = this.getFieldNameBySymbol(change); + if (fieldName === undefined) { return false; } - - const isSpace = (s: string): boolean => s === ' '; - - const { length } = name; - for (const otherName of names) { - let found = true; - const otherLength = otherName.length; - - for (let i = 0, j = 0; i < length && found; i++) { - const a = name.charAt(i); - if (isSpace(a)) { - continue; - } - const lowerA = a.toLowerCase(); - for (; j < otherLength; j++) { - const b = otherName.charAt(j); - if (isSpace(b)) { - continue; - } - const lowerB = b.toLowerCase(); - if (lowerB === lowerA) { - continue; - } - found = false; - break; - } - } - if (found) { + for (const otherFieldName of fieldNames) { + if (equalsIgnoreCaseAndSpace(fieldName, otherFieldName)) { return true; } } @@ -90,23 +81,23 @@ export class DomainObjectChange { return this._changes.find((desc: ChangedDescription) => desc.change === change); } - private getName(change: symbol): string | undefined { + private getFieldNameBySymbol(change: symbol): string | undefined { const changedDescription = this.getChangedDescription(change); - return changedDescription === undefined ? undefined : changedDescription.name; + return changedDescription === undefined ? undefined : changedDescription.fieldName; } // ================================================== // INSTANCE METHODS: Operations // ================================================== - public add(change: symbol, name?: string): void { + public add(change: symbol, fieldName?: string): void { if (change === undefined) { return; } if (this._changes === undefined) { this._changes = []; } - this._changes.push(new ChangedDescription(change, name)); + this._changes.push(new ChangedDescription(change, fieldName)); } } @@ -116,10 +107,10 @@ export class DomainObjectChange { class ChangedDescription { public change: symbol; - public name: string | undefined; + public fieldName: string | undefined; - public constructor(change: symbol, name?: string) { + public constructor(change: symbol, fieldName?: string) { this.change = change; - this.name = name; + this.fieldName = fieldName; } } diff --git a/react-components/src/architecture/base/domainObjectsHelpers/VisibleState.ts b/react-components/src/architecture/base/domainObjectsHelpers/VisibleState.ts index 0d5b02109c5..140d105f979 100644 --- a/react-components/src/architecture/base/domainObjectsHelpers/VisibleState.ts +++ b/react-components/src/architecture/base/domainObjectsHelpers/VisibleState.ts @@ -6,6 +6,6 @@ export enum VisibleState { All, // Visible Some, // Partly visible None, // None visible - CanNotBeChecked, // Cab not be checked + CanNotBeChecked, // Can not be checked on, but it can be visible Disabled // Visiable disabled } diff --git a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts index dc1678b1104..58f0cf9aea7 100644 --- a/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts +++ b/react-components/src/architecture/base/renderTarget/RevealRenderTarget.ts @@ -17,7 +17,8 @@ import { DirectionalLight, type PerspectiveCamera, type Box3, - type WebGLRenderer + type WebGLRenderer, + type Plane } from 'three'; import { ToolControllers } from './ToolController'; import { RootDomainObject } from '../domainObjects/RootDomainObject'; @@ -25,6 +26,7 @@ import { getOctDir } from '../utilities/extensions/vectorExtensions'; import { getResizeCursor } from '../utilities/geometry/getResizeCursor'; import { VisualDomainObject } from '../domainObjects/VisualDomainObject'; import { ThreeView } from '../views/ThreeView'; +import { type DomainObject } from '../domainObjects/DomainObject'; const DIRECTIONAL_LIGHT_NAME = 'DirectionalLight'; @@ -39,6 +41,8 @@ export class RevealRenderTarget { private _axisGizmoTool: AxisGizmoTool | undefined; private _ambientLight: AmbientLight | undefined; private _directionalLight: DirectionalLight | undefined; + private _cropBoxBoundingBox: Box3 | undefined; + private _cropBoxName: string | undefined = undefined; // ================================================== // CONTRUCTORS @@ -53,7 +57,7 @@ export class RevealRenderTarget { } this._toolController = new ToolControllers(this.domElement); this._toolController.addEventListeners(); - this._rootDomainObject = new RootDomainObject(); + this._rootDomainObject = new RootDomainObject(this); this.initializeLights(); this._viewer.on('cameraChange', this.cameraChangeHandler); @@ -109,7 +113,11 @@ export class RevealRenderTarget { } public get sceneBoundingBox(): Box3 { - return this.viewer.getSceneBoundingBox(); + const boundingBox = this.viewer.getSceneBoundingBox(); + if (this._cropBoxBoundingBox !== undefined) { + boundingBox.intersect(this._cropBoxBoundingBox); + } + return boundingBox; } // ================================================== @@ -192,6 +200,50 @@ export class RevealRenderTarget { } }; + // ================================================== + // INSTANCE METHODS: Fit operations + // ================================================== + + public fitView(): boolean { + const boundingBox = this.sceneBoundingBox; + if (boundingBox.isEmpty()) { + return false; + } + this.viewer.fitCameraToBoundingBox(this.sceneBoundingBox); + return true; + } + + // ================================================== + // INSTANCE METHODS: Crop box 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 clearGlobalCropBox(): void { + this.viewer.setGlobalClippingPlanes([]); + this._cropBoxBoundingBox = undefined; + this._cropBoxName = undefined; + } + + public isGlobalCropBox(domainObject: DomainObject): boolean { + return this._cropBoxName !== undefined && domainObject.hasEqualName(this._cropBoxName); + } + + public get isGlobalCropBoxActive(): boolean { + return ( + this.viewer.getGlobalClippingPlanes().length > 0 && this._cropBoxBoundingBox !== undefined + ); + } + // ================================================== // INSTANCE METHODS: Cursor // See: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor diff --git a/react-components/src/architecture/base/utilities/box/BoxFace.ts b/react-components/src/architecture/base/utilities/box/BoxFace.ts index a4a84d7b212..44f2139947a 100644 --- a/react-components/src/architecture/base/utilities/box/BoxFace.ts +++ b/react-components/src/architecture/base/utilities/box/BoxFace.ts @@ -2,7 +2,7 @@ * Copyright 2024 Cognite AS */ -import { Vector2, Vector3 } from 'three'; +import { type Matrix4, Plane, Vector2, Vector3 } from 'three'; /** * Represents a face of a box. @@ -11,10 +11,18 @@ export class BoxFace { // Face is 0-5, where 0-2 are positive faces and 3-5 are negative faces private _face: number = 0; + // ================================================== + // CONTRUCTOR + // ================================================== + public constructor(face: number = 0) { this._face = face; } + // ================================================== + // INSTANCE PROPERTIES + // ================================================== + public get face(): number { return this._face; } @@ -39,12 +47,16 @@ export class BoxFace { return this._face < 3 ? 1 : -1; } - copy(other: BoxFace): this { + // ================================================== + // INSTANCE METHODS + // ================================================== + + public copy(other: BoxFace): this { this._face = other._face; return this; } - equals(other: BoxFace): boolean { + public equals(other: BoxFace): boolean { return this.face === other.face; } @@ -114,6 +126,10 @@ export class BoxFace { return target.setComponent(this.index, this.sign * 0.5); } + // ================================================== + // STATIC METHODS + // ================================================== + public static *getAllFaces(target?: BoxFace): Generator { if (target === undefined) { target = new BoxFace(); @@ -123,6 +139,24 @@ export class BoxFace { } } + public static createClippingPlanes(matrix: Matrix4, exceptFaceIndex?: number): Plane[] { + const planes: Plane[] = []; + + const center = new Vector3(); + const normal = new Vector3(); + for (const boxFace of BoxFace.getAllFaces()) { + if (exceptFaceIndex !== undefined && boxFace.index === exceptFaceIndex) { + continue; + } + boxFace.getCenter(center); + boxFace.getNormal(normal).negate(); + const plane = new Plane().setFromNormalAndCoplanarPoint(normal, center); + plane.applyMatrix4(matrix); + planes.push(plane); + } + return planes; + } + public static equals(face: BoxFace | undefined, other: BoxFace | undefined): boolean { if (face === undefined || other === undefined) { return true; diff --git a/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts b/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts index bf84cac6c48..70625ce2e96 100644 --- a/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/mathExtensions.ts @@ -59,20 +59,19 @@ export function isBetween(min: number, value: number, max: number): boolean { // FUNCTIONS: Returning a number // ================================================== -export function max(a: number, b: number, c: number): number { - return Math.max(a, Math.max(b, c)); -} - -export function min(a: number, b: number, c: number): number { - return Math.min(a, Math.min(b, c)); -} - export function square(value: number): number { return value * value; } -export function roundInc(increment: number): number { - // Get the exponent for the number [1-10] and scale the inc so the number is between 1 and 10. +/** + * Round a number closest to one of these values: 2*10^N, 2.5*10^N, 5*10^N or 10*10^N. + * This is used to give axies a natural increment between the ticks or + * contour intervals on a terrain surface + * @param increment - The value to be rounded + * @returns The rounded value + */ +export function roundIncrement(increment: number): number { + // First get the exponent for the number [1-10] and scale the inc so the number is between 1 and 10. let exp = 0; let inc = increment; let found = false; @@ -101,7 +100,7 @@ export function roundInc(increment: number): number { } else { inc = 10; } - // Upscale the inc to the real number + // Upscale the increment to the real number if (exp < 0) { for (; exp !== 0; exp++) inc /= 10; } else { diff --git a/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts b/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts index 3c7f647b392..44314e4575a 100644 --- a/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/rayExtensions.ts @@ -3,6 +3,14 @@ */ import { Vector3, type Ray } from 'three'; +/** + * Calculates the closest point on a line to a given ray. + * @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. + * @returns The closest point on the line to the ray. + */ export function getClosestPointOnLine( ray: Ray, lineDirection: Vector3, diff --git a/react-components/src/architecture/base/utilities/extensions/stringExtensions.ts b/react-components/src/architecture/base/utilities/extensions/stringExtensions.ts index 6b8f78c0350..9d5e398ed33 100644 --- a/react-components/src/architecture/base/utilities/extensions/stringExtensions.ts +++ b/react-components/src/architecture/base/utilities/extensions/stringExtensions.ts @@ -10,15 +10,28 @@ export function equalsIgnoreCase(value1: string, value2: string): boolean { return value1.toLowerCase() === value2.toLowerCase(); } -export function isNumber(text: string): boolean { - const value = Number(text); - return !Number.isNaN(value); -} +export function equalsIgnoreCaseAndSpace(value1: string, value2: string): boolean { + const isSpace = (s: string): boolean => s === ' '; + const { length: length1 } = value1; + const { length: length2 } = value2; -export function getNumber(text: string): number { - const value = Number(text); - if (Number.isNaN(value)) { - return Number.NaN; + for (let i = 0, j = 0; i < length1; i++) { + const char1 = value1.charAt(i); + if (isSpace(char1)) { + continue; + } + const lowerChar1 = char1.toLowerCase(); + for (; j < length2; j++) { + const char2 = value2.charAt(j); + if (isSpace(char2)) { + continue; + } + const lowerChar2 = char2.toLowerCase(); + if (lowerChar2 === lowerChar1) { + break; + } + return false; + } } - return value; + return true; } diff --git a/react-components/src/architecture/base/utilities/geometry/Range1.ts b/react-components/src/architecture/base/utilities/geometry/Range1.ts index 39ce6e636c6..4144fb8d33c 100644 --- a/react-components/src/architecture/base/utilities/geometry/Range1.ts +++ b/react-components/src/architecture/base/utilities/geometry/Range1.ts @@ -2,7 +2,7 @@ * Copyright 2024 Cognite AS */ -import { ceil, floor, isIncrement, roundInc as roundIncrement } from '../extensions/mathExtensions'; +import { ceil, floor, isIncrement, roundIncrement } from '../extensions/mathExtensions'; const MAX_NUMBER_OF_TICKS = 1000; export class Range1 { diff --git a/react-components/src/architecture/base/utilities/geometry/Range3.ts b/react-components/src/architecture/base/utilities/geometry/Range3.ts index b5d5dbadb43..c31b53674cb 100644 --- a/react-components/src/architecture/base/utilities/geometry/Range3.ts +++ b/react-components/src/architecture/base/utilities/geometry/Range3.ts @@ -224,19 +224,11 @@ export class Range3 { // STATIC METHODS // ================================================== - public static createByMinAndMax(xmin: number, ymin: number, xmax: number, ymax: number): Range3 { + public static createCube(halfSize: number): Range3 { const range = new Range3(); - range.x.set(xmin, xmax); - range.y.set(ymin, ymax); - range.z.set(0, 0); - return range; - } - - public static createByMinAndDelta(xmin: number, ymin: number, dx: number, dy: number): Range3 { - const range = new Range3(); - range.x.set(xmin, xmin + dx); - range.y.set(ymin, ymin + dy); - range.z.set(0, 0); + range.x.set(-halfSize, halfSize); + range.y.set(-halfSize, halfSize); + range.z.set(-halfSize, halfSize); return range; } } diff --git a/react-components/src/architecture/base/views/BaseView.ts b/react-components/src/architecture/base/views/BaseView.ts index 4f0a41c3d0d..4f35878ea0b 100644 --- a/react-components/src/architecture/base/views/BaseView.ts +++ b/react-components/src/architecture/base/views/BaseView.ts @@ -24,17 +24,13 @@ export abstract class BaseView { return this._domainObject !== undefined; } - public get domainObject(): DomainObject { + protected get domainObject(): DomainObject { if (this._domainObject === undefined) { throw Error('The DomainObject is missing in the view'); } return this._domainObject; } - protected set domainObject(value: DomainObject) { - this._domainObject = value; - } - // ================================================== // VIRTUAL METHODS // ================================================== @@ -91,4 +87,12 @@ export abstract class BaseView { public dispose(): void { this._domainObject = undefined; } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + public setDomainObject(domainObject: DomainObject): void { + this._domainObject = domainObject; + } } diff --git a/react-components/src/architecture/base/views/GroupThreeView.ts b/react-components/src/architecture/base/views/GroupThreeView.ts index e79fc8fbedb..b0b62a2fac4 100644 --- a/react-components/src/architecture/base/views/GroupThreeView.ts +++ b/react-components/src/architecture/base/views/GroupThreeView.ts @@ -15,9 +15,12 @@ import { type DomainObjectIntersection } from '../domainObjectsHelpers/DomainObj /** * Represents an abstract class for a Three.js view that renders an Object3D. - * This class extends the ThreeView class. + * This class extends the ThreeView class. It created a group object3D and adds children to it. * @remarks * You only have to override addChildren() to create the object3D to be added to the group. + * Other methods you may override is: + * - intersectIfCloser() to determine the exact intersection point. + * - calculateBoundingBox() to calculate the bounding box if you don not relay on three.js. */ export abstract class GroupThreeView extends ThreeView implements ICustomObject { @@ -33,6 +36,8 @@ export abstract class GroupThreeView extends ThreeView implements ICustomObject // ================================================== // IMPLEMENTATION of ICustomObject + // some of the methods are virtual and can be overridden, + // see each method. // ================================================== public get object(): Object3D { @@ -131,6 +136,14 @@ export abstract class GroupThreeView extends ThreeView implements ICustomObject // OVERRIDES of ThreeView // ================================================== + /** + * Calculates the bounding box of the object. + * Overrides this if you want to calculate the bounding box in a + * different way that three.js does it. For instance if you don't + * want to include text labels in the bounding box. + * + * @returns The calculated bounding box. + */ protected override calculateBoundingBox(): Box3 { if (this.object === undefined) { return new Box3().makeEmpty(); diff --git a/react-components/src/architecture/base/views/ThreeView.ts b/react-components/src/architecture/base/views/ThreeView.ts index 143e6045e0d..600e7a61e1b 100644 --- a/react-components/src/architecture/base/views/ThreeView.ts +++ b/react-components/src/architecture/base/views/ThreeView.ts @@ -12,14 +12,17 @@ import { type PerspectiveCamera, type Box3 } from 'three'; /** * Represents an abstract base class for a Three.js view in the application. - * Extends the `BaseView` class. + * Extends the `BaseView` class. It only has the poiinter to the renderTarget and a bounding box. + * Inirit from this class if you want some visualization that do dot require a group object3D as the root object. + * It can for instance be a view that chages somting on another view, dor instance texture on a surface or whatever. + * I just wanted to make it ready for some corner cases I have seen during a long time as 3D develper. */ export abstract class ThreeView extends BaseView { // ================================================== // INSTANCE FIELDS // ================================================== - private _boundingBox: Box3 | undefined = undefined; // Cashe of the bounding box of the view + private _boundingBox: Box3 | undefined = undefined; // Cache of the bounding box of the view private _renderTarget: RevealRenderTarget | undefined = undefined; // ================================================== @@ -87,7 +90,7 @@ export abstract class ThreeView extends BaseView { /** * This method is called before rendering the view. * Override this function to perform any necessary operations - * just before rendering.Have in mind that the Object3D are build at the time this is + * just before rendering. Have in mind that the Object3D are build at the time this is * called, so you can only do adjustment on existing object3D's. * @remarks * Always call `super.beforeRender(camera)` in the overrides. @@ -110,7 +113,7 @@ export abstract class ThreeView extends BaseView { } public attach(domainObject: DomainObject, renderTarget: RevealRenderTarget): void { - this.domainObject = domainObject; + super.setDomainObject(domainObject); this._renderTarget = renderTarget; } diff --git a/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts b/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts index 5992ff0fcbf..ba594132ba0 100644 --- a/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts +++ b/react-components/src/architecture/concrete/axis/AxisRenderStyle.ts @@ -25,7 +25,7 @@ export class AxisRenderStyle extends RenderStyle { public showAxisTicks = true; public showGrid = true; - public numberOfTicks = 30; // Appoximately number of ticks for the largest axis + public numberOfTicks = 40; // Appoximately number of ticks for the largest axis public tickLength = 0.005; // In fraction of the bounding box diagonal public tickFontSize = 2; // In fraction of the real tickLength public axisLabelFontSize = 4; // In fraction of the real tickLength diff --git a/react-components/src/architecture/concrete/axis/AxisThreeView.ts b/react-components/src/architecture/concrete/axis/AxisThreeView.ts index 3e0d916008b..9f4c1cba658 100644 --- a/react-components/src/architecture/concrete/axis/AxisThreeView.ts +++ b/react-components/src/architecture/concrete/axis/AxisThreeView.ts @@ -42,7 +42,7 @@ export class AxisThreeView extends GroupThreeView { private readonly _corners: Vector3[]; private readonly _faceCenters: Vector3[]; - private readonly _sceneBoundingBox: Box3 = new Box3().makeEmpty(); // Caching the bounding box of the scene + private readonly _sceneBoundingBox: Box3 = new Box3().makeEmpty(); // Cache the bounding box of the scene private readonly _expandedSceneBoundingBox: Range3 = new Range3(); // ================================================== @@ -245,6 +245,7 @@ export class AxisThreeView extends GroupThreeView { // Draw ticks const labelInc = range.getBoldIncrement(increment); const tickDirection = getTickDirection(faceIndex1, faceIndex2, new Vector3()); + tickDirection.normalize(); // Add tick marks and labels if (style.showAxisTicks || style.showAxisNumbers) { @@ -253,14 +254,11 @@ export class AxisThreeView extends GroupThreeView { for (const tick of range.getTicks(increment)) { const start = newVector3(this._corners[i0]); start.setComponent(dimension, tick); - const end = newVector3(start); - const vector = newVector3(tickDirection); - vector.multiplyScalar(tickLength); // Add tick mark if (style.showAxisTicks) { - end.add(vector); + end.addScaledVector(tickDirection, tickLength); vertices.push(...start); vertices.push(...end); } @@ -272,15 +270,15 @@ export class AxisThreeView extends GroupThreeView { minLabelTick = tick; } labelCount += 1; - end.add(vector); - - // Add sprite - const sprite = createSpriteWithText(`${tick}`, tickFontSize, style.textColor); - if (sprite !== undefined) { - moveSpriteByPositionAndDirection(sprite, end, tickDirection); - this.addChild(sprite); - this.setUserDataOnAxis(sprite, faceIndex1, faceIndex2, true); + end.addScaledVector(tickDirection, 2 * tickLength); + const text = incrementToString(tick); + const sprite = createSpriteWithText(text, tickFontSize, style.textColor); + if (sprite === undefined) { + continue; } + sprite.position.copy(end); + this.addChild(sprite); + this.setUserDataOnAxis(sprite, faceIndex1, faceIndex2, true); } } if (style.showAxisTicks) { @@ -295,19 +293,14 @@ export class AxisThreeView extends GroupThreeView { // Find the best position by collision detect const position = newVector3(); - if (labelCount >= 2) { - let tick = minLabelTick + Math.round(0.5 * labelCount - 0.5) * labelInc; - if (labelInc === increment) { - tick -= increment / 2; - } else { - tick -= increment; - } - position.copy(this._corners[i0]); - position.setComponent(dimension, tick); + let tick = minLabelTick + Math.round(0.5 * labelCount - 0.5) * labelInc; + if (labelInc === increment) { + tick -= increment / 2; } else { - position.copy(this._corners[i0]); - position.add(this._corners[i1]); + tick -= increment; } + position.copy(this._corners[i0]); + position.setComponent(dimension, tick); position.addScaledVector(tickDirection, tickLength * 5); const sprite = createSpriteWithText( @@ -315,11 +308,12 @@ export class AxisThreeView extends GroupThreeView { labelFontSize, style.textColor ); - if (sprite !== undefined) { - moveSpriteByPositionAndDirection(sprite, position, tickDirection); - this.addChild(sprite); - this.setUserDataOnAxis(sprite, faceIndex1, faceIndex2, true); + if (sprite === undefined) { + return; } + moveSpriteByPositionAndDirection(sprite, position, tickDirection); + this.addChild(sprite); + this.setUserDataOnAxis(sprite, faceIndex1, faceIndex2, true); } } @@ -536,7 +530,7 @@ function createLineSegments(vertices: number[], color: Color, linewidth: number) } // ================================================== -// PRIVATE METHODS: Some math for Range3 +// PRIVATE METHODS: Some math // ================================================== // Corner and faces is pr. definition: @@ -607,6 +601,48 @@ function getTickDirection(faceIndex1: number, faceIndex2: number, target: Vector return target; } +function incrementToString(value: number): string { + // Sometimes the number comes out like this: 1.20000005 or 1.19999992 due to numeric precision limitations. + // To get better rounded values, I wrote this myself: Multiply by some high integer and round it, then + // convert to text, and insert the comma manually afterwards. + + // Small number get less accurate result in tjhis algorithm,, so use the default string conversion. + if (Math.abs(value) < 0.001) { + return `${value}`; + } + const sign = Math.sign(value); + const rounded = Math.abs(Math.round(value * 1e5)); + let text = `${rounded}`; + if (text.length === 1) { + text = `${'0.0000'}${text}`; + } else if (text.length === 2) { + text = `${'0.000'}${text}`; + } else if (text.length === 3) { + text = `${'0.00'}${text}`; + } else if (text.length === 4) { + text = `${'0.0'}${text}`; + } else if (text.length === 5) { + text = `${'0.'}${text}`; + } else if (text.length >= 6) { + const i = text.length - 5; + text = `${text.slice(0, i)}${'.'}${text.slice(i)}`; + } + // Since we know that the comma are there, + // we can safely remove trailing zeros + while (text[text.length - 1] === '0') { + text = text.slice(0, -1); + } + // Remove if last is a comma + if (text[text.length - 1] === '.') { + text = text.slice(0, -1); + } + // Put the negative sign in the front + if (sign < 0) { + text = `${'-'}${text}`; + } + return text; +} + // ================================================== // PRIVATE FUNCTIONS: Vector pool // ================================================== diff --git a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts index 81191d5f650..80882314e15 100644 --- a/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts +++ b/react-components/src/architecture/concrete/axis/SetAxisVisibleCommand.ts @@ -29,8 +29,7 @@ export class SetAxisVisibleCommand extends RenderTargetCommand { } public override get isChecked(): boolean { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { renderTarget, rootDomainObject } = this; const axis = rootDomainObject.getDescendantByType(AxisDomainObject); if (axis === undefined) { @@ -40,8 +39,7 @@ export class SetAxisVisibleCommand extends RenderTargetCommand { } protected override invokeCore(): boolean { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { renderTarget, rootDomainObject } = this; let axis = rootDomainObject.getDescendantByType(AxisDomainObject); if (axis === undefined) { diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts index 5af7e11c7cc..001ec405c29 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxCreator.ts @@ -85,10 +85,12 @@ export class MeasureBoxCreator extends BaseCreator { return true; } - public override handleEscape(): void { - if (this.realPointCount < this.minimumPointCount) { - this._domainObject.removeInteractive(); + public override handleEscape(): boolean { + if (this.notPendingPointCount >= this.minimumPointCount) { + return true; // Successfully } + this._domainObject.removeInteractive(); + return false; // Removed } // ================================================== @@ -105,11 +107,11 @@ export class MeasureBoxCreator extends BaseCreator { } // Recalculate the point anywhy for >= 1 points // This makes it more natural and you can pick in empty space - if (this.realPointCount === 1 || this.realPointCount === 2) { + 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.realPointCount === 3 && measureType === MeasureType.Volume) { + } else if (this.notPendingPointCount === 3 && measureType === MeasureType.Volume) { return getClosestPointOnLine(ray, UP_VECTOR, this.points[2], point); } return point; diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts index 2c5db32b464..49a70857e83 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDomainObject.ts @@ -6,18 +6,19 @@ import { MeasureBoxRenderStyle } from './MeasureBoxRenderStyle'; import { type RenderStyle } from '../../base/domainObjectsHelpers/RenderStyle'; import { type ThreeView } from '../../base/views/ThreeView'; import { MeasureBoxView } from './MeasureBoxView'; -import { Matrix4, Vector3 } from 'three'; +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 { MeasureType } from './MeasureType'; -import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { type BoxPickInfo } from '../../base/utilities/box/BoxPickInfo'; import { type BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; import { MeasureBoxDragger } from './MeasureBoxDragger'; import { MeasureDomainObject } from './MeasureDomainObject'; import { NumberType, PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; import { radToDeg } from 'three/src/math/MathUtils.js'; +import { type CreateDraggerProps } from '../../base/domainObjects/VisualDomainObject'; +import { Range3 } from '../../base/utilities/geometry/Range3'; export const MIN_BOX_SIZE = 0.01; @@ -38,49 +39,6 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { // INSTANCE PROPERTIES // ================================================== - public get diagonal(): number { - return this.size.length(); - } - - public get hasArea(): boolean { - let count = 0; - if (isValid(this.size.x)) count++; - if (isValid(this.size.y)) count++; - if (isValid(this.size.z)) count++; - return count >= 2; - } - - public get area(): number { - switch (this.measureType) { - case MeasureType.HorizontalArea: - return this.size.x * this.size.y; - case MeasureType.VerticalArea: - return this.size.x * this.size.z; - case MeasureType.Volume: { - const a = this.size.x * this.size.y + this.size.y * this.size.z + this.size.z * this.size.x; - return a * 2; - } - default: - throw new Error('Unknown MeasureType type'); - } - } - - public get hasHorizontalArea(): boolean { - return isValid(this.size.x) && isValid(this.size.y); - } - - public get horizontalArea(): number { - return this.size.x * this.size.y; - } - - public get hasVolume(): boolean { - return isValid(this.size.x) && isValid(this.size.y) && isValid(this.size.z); - } - - public get volume(): number { - return this.size.x * this.size.y * this.size.z; - } - public override get renderStyle(): MeasureBoxRenderStyle { return this.getRenderStyle() as MeasureBoxRenderStyle; } @@ -101,12 +59,12 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { return new MeasureBoxRenderStyle(); } - public override createDragger(intersection: DomainObjectIntersection): BaseDragger | undefined { - const pickInfo = intersection.userData as BoxPickInfo; + public override createDragger(props: CreateDraggerProps): BaseDragger | undefined { + const pickInfo = props.intersection.userData as BoxPickInfo; if (pickInfo === undefined) { - return undefined; + return undefined; // If the BoxPickInfo isn't specified, no dragger iscreated } - return new MeasureBoxDragger(this, intersection.point, pickInfo); + return new MeasureBoxDragger(props, this); } public override getPanelInfo(): PanelInfo | undefined { @@ -125,13 +83,13 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { info.setHeader('MEASUREMENTS_VOLUME', 'Volume'); break; } - if (isFinished || isValid(this.size.x)) { + if (isFinished || isValidSize(this.size.x)) { add('MEASUREMENTS_LENGTH', 'Length', this.size.x, NumberType.Length); } - if (measureType !== MeasureType.VerticalArea && (isFinished || isValid(this.size.y))) { + if (measureType !== MeasureType.VerticalArea && (isFinished || isValidSize(this.size.y))) { add('MEASUREMENTS_DEPTH', 'Depth', this.size.y, NumberType.Length); } - if (measureType !== MeasureType.HorizontalArea && (isFinished || isValid(this.size.z))) { + if (measureType !== MeasureType.HorizontalArea && (isFinished || isValidSize(this.size.z))) { add('MEASUREMENTS_HEIGHT', 'Height', this.size.z, NumberType.Length); } if (measureType !== MeasureType.Volume && (isFinished || this.hasArea)) { @@ -163,14 +121,63 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } // ================================================== - // INSTANCE METHODS + // INSTANCE METHODS: Getters/Properties // ================================================== - public forceMinSize(): void { - const { size } = this; - size.x = Math.max(MIN_BOX_SIZE, size.x); - size.y = Math.max(MIN_BOX_SIZE, size.y); - size.z = Math.max(MIN_BOX_SIZE, size.z); + public get diagonal(): number { + return this.size.length(); + } + + 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++; + return count >= 2; + } + + public get area(): number { + switch (this.measureType) { + case MeasureType.HorizontalArea: + return this.size.x * this.size.y; + case MeasureType.VerticalArea: + return this.size.x * this.size.z; + case MeasureType.Volume: { + const a = this.size.x * this.size.y + this.size.y * this.size.z + this.size.z * this.size.x; + return a * 2; + } + default: + throw new Error('Unknown MeasureType type'); + } + } + + public get hasHorizontalArea(): boolean { + return isValidSize(this.size.x) && isValidSize(this.size.y); + } + + public get horizontalArea(): number { + return this.size.x * this.size.y; + } + + public get hasVolume(): boolean { + return isValidSize(this.size.x) && isValidSize(this.size.y) && isValidSize(this.size.z); + } + + public get volume(): number { + return this.size.x * this.size.y * this.size.z; + } + + public getBoundingBox(): Box3 { + const matrix = this.getMatrix(); + const boundingBox = new Box3().makeEmpty(); + const unitCube = Range3.createCube(0.5); + const corner = new Vector3(); + for (let i = 0; i < 8; i++) { + unitCube.getCornerPoint(i, corner); + corner.applyMatrix4(matrix); + boundingBox.expandByPoint(corner); + } + return boundingBox; } public getRotationMatrix(matrix: Matrix4 = new Matrix4()): Matrix4 { @@ -189,6 +196,17 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { return matrix; } + // ================================================== + // INSTANCE METHODS: Others + // ================================================== + + public forceMinSize(): void { + const { size } = this; + size.x = Math.max(MIN_BOX_SIZE, size.x); + size.y = Math.max(MIN_BOX_SIZE, size.y); + size.z = Math.max(MIN_BOX_SIZE, size.z); + } + public setFocusInteractive(focusType: FocusType, focusFace?: BoxFace): boolean { if (focusType === FocusType.None) { if (this.focusType === FocusType.None) { @@ -208,6 +226,10 @@ export class MeasureBoxDomainObject extends MeasureDomainObject { } } -function isValid(value: number): boolean { +// ================================================== +// 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/boxDomainObject/MeasureBoxDragger.ts index 46711e76ce9..69723791092 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDragger.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxDragger.ts @@ -15,7 +15,7 @@ import { MeasureType } from './MeasureType'; import { getClosestPointOnLine } from '../../base/utilities/extensions/rayExtensions'; import { type MeasureBoxDomainObject } from './MeasureBoxDomainObject'; import { BaseDragger } from '../../base/domainObjectsHelpers/BaseDragger'; -import { CDF_TO_VIEWER_TRANSFORMATION } from '@cognite/reveal'; +import { type CreateDraggerProps } from '../../base/domainObjects/VisualDomainObject'; /** * The `BoxDragger` class represents a utility for dragging and manipulating a box in a 3D space. @@ -57,11 +57,10 @@ export class MeasureBoxDragger extends BaseDragger { // CONTRUCTOR // ================================================== - public constructor(domainObject: MeasureBoxDomainObject, point: Vector3, pickInfo: BoxPickInfo) { - point = point.clone(); - point.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); - super(point); + public constructor(props: CreateDraggerProps, domainObject: MeasureBoxDomainObject) { + super(props); + const pickInfo = props.intersection.userData as BoxPickInfo; this._domainObject = domainObject; this._face = pickInfo.face; this._focusType = pickInfo.focusType; @@ -72,7 +71,7 @@ export class MeasureBoxDragger extends BaseDragger { this._normal.applyMatrix4(rotationMatrix); this._normal.normalize(); - this._planeOfBox.setFromNormalAndCoplanarPoint(this._normal, point); + this._planeOfBox.setFromNormalAndCoplanarPoint(this._normal, this.point); // Back up the original values this._sizeOfBox.copy(this._domainObject.size); diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts index 734b0ccf63f..8ff105bf68f 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureBoxView.ts @@ -18,11 +18,10 @@ import { type Camera, CircleGeometry, type Material, - Plane, FrontSide, type PerspectiveCamera } from 'three'; -import { type MeasureBoxDomainObject, MIN_BOX_SIZE } from './MeasureBoxDomainObject'; +import { type MeasureBoxDomainObject, isValidSize } from './MeasureBoxDomainObject'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type MeasureBoxRenderStyle } from './MeasureBoxRenderStyle'; @@ -66,7 +65,7 @@ export class MeasureBoxView extends GroupThreeView { // INSTANCE PROPERTIES // ================================================== - protected get boxDomainObject(): MeasureBoxDomainObject { + protected override get domainObject(): MeasureBoxDomainObject { return super.domainObject as MeasureBoxDomainObject; } @@ -112,10 +111,10 @@ export class MeasureBoxView extends GroupThreeView { // ================================================== protected override addChildren(): void { - const { boxDomainObject } = this; + const { domainObject } = this; const matrix = this.getMatrix(); - const { focusType } = boxDomainObject; + const { focusType } = domainObject; this.addChild(this.createSolid(matrix)); this.addChild(this.createLines(matrix)); if (showMarkers(focusType)) { @@ -129,8 +128,8 @@ export class MeasureBoxView extends GroupThreeView { intersectInput: CustomObjectIntersectInput, closestDistance: number | undefined ): undefined | CustomObjectIntersection { - const { boxDomainObject } = this; - if (boxDomainObject.focusType === FocusType.Pending) { + const { domainObject } = this; + if (domainObject.focusType === FocusType.Pending) { return undefined; // Should never be picked } const orientedBox = createOrientedBox(); @@ -165,7 +164,7 @@ export class MeasureBoxView extends GroupThreeView { distanceToCamera, userData: new BoxPickInfo(boxFace, focusType, cornerSign), customObject: this, - domainObject: boxDomainObject + domainObject }; if (this.shouldPickBoundingBox) { customObjectIntersection.boundingBox = this.boundingBox; @@ -178,26 +177,26 @@ export class MeasureBoxView extends GroupThreeView { // ================================================== private getTextHeight(relativeTextSize: number): number { - return relativeTextSize * this.boxDomainObject.diagonal; + return relativeTextSize * this.domainObject.diagonal; } private getFaceRadius(boxFace: BoxFace): number { - const { size } = this.boxDomainObject; + const { size } = this.domainObject; const size1 = size.getComponent(boxFace.tangentIndex1); const size2 = size.getComponent(boxFace.tangentIndex2); return (size1 + size2) / 4; } private getMatrix(): Matrix4 { - const { boxDomainObject } = this; - const matrix = boxDomainObject.getMatrix(); + const { domainObject } = this; + const matrix = domainObject.getMatrix(); matrix.premultiply(CDF_TO_VIEWER_TRANSFORMATION); return matrix; } private getRotationMatrix(): Matrix4 { - const { boxDomainObject } = this; - const matrix = boxDomainObject.getRotationMatrix(); + const { domainObject } = this; + const matrix = domainObject.getRotationMatrix(); matrix.premultiply(CDF_TO_VIEWER_TRANSFORMATION); return matrix; } @@ -207,11 +206,11 @@ export class MeasureBoxView extends GroupThreeView { // ================================================== private createSolid(matrix: Matrix4): Object3D | undefined { - const { boxDomainObject } = this; + const { domainObject } = this; const { style } = this; const material = new MeshPhongMaterial(); - updateSolidMaterial(material, boxDomainObject, style); + updateSolidMaterial(material, domainObject, style); const geometry = new BoxGeometry(1, 1, 1); const result = new Mesh(geometry, material); result.applyMatrix4(matrix); @@ -219,11 +218,11 @@ export class MeasureBoxView extends GroupThreeView { } private createLines(matrix: Matrix4): Object3D | undefined { - const { boxDomainObject } = this; + const { domainObject } = this; const { style } = this; const material = new LineBasicMaterial(); - updateLineSegmentsMaterial(material, boxDomainObject, style); + updateLineSegmentsMaterial(material, domainObject, style); const geometry = createLineSegmentsBufferGeometryForBox(); const result = new LineSegments(geometry, material); @@ -235,8 +234,8 @@ export class MeasureBoxView extends GroupThreeView { if (!this.isFaceVisible(TOP_FACE)) { return undefined; } - const { boxDomainObject } = this; - const degrees = radToDeg(boxDomainObject.zRotation); + const { domainObject } = this; + const degrees = radToDeg(domainObject.zRotation); const text = degrees.toFixed(1); if (text === '0.0') { return undefined; // Not show when about 0 @@ -247,7 +246,7 @@ export class MeasureBoxView extends GroupThreeView { } const faceCenter = TOP_FACE.getCenter(newVector3()); faceCenter.applyMatrix4(matrix); - adjustLabel(faceCenter, boxDomainObject, spriteHeight); + adjustLabel(faceCenter, domainObject, spriteHeight); sprite.position.copy(faceCenter); return sprite; } @@ -262,7 +261,7 @@ export class MeasureBoxView extends GroupThreeView { } const faceCenter = TOP_FACE.getCenter(newVector3()); faceCenter.applyMatrix4(matrix); - adjustLabel(faceCenter, this.boxDomainObject, spriteHeight); + adjustLabel(faceCenter, this.domainObject, spriteHeight); sprite.position.copy(faceCenter); return sprite; } @@ -271,8 +270,8 @@ export class MeasureBoxView extends GroupThreeView { if (!this.isFaceVisible(TOP_FACE)) { return undefined; } - const { boxDomainObject, style } = this; - const { focusType } = boxDomainObject; + const { domainObject, style } = this; + const { focusType } = domainObject; const radius = this.getFaceRadius(TOP_FACE); const outerRadius = RELATIVE_ROTATION_RADIUS.max * radius; @@ -280,8 +279,8 @@ export class MeasureBoxView extends GroupThreeView { const geometry = new RingGeometry(innerRadius, outerRadius, CIRCULAR_SEGMENTS); const material = new MeshPhongMaterial(); - updateMarkerMaterial(material, boxDomainObject, style, focusType === FocusType.Rotation); - material.clippingPlanes = this.createClippingPlanes(matrix, TOP_FACE.index); + updateMarkerMaterial(material, domainObject, style, focusType === FocusType.Rotation); + material.clippingPlanes = BoxFace.createClippingPlanes(matrix, TOP_FACE.index); const mesh = new Mesh(geometry, material); const center = TOP_FACE.getCenter(newVector3()); @@ -292,13 +291,13 @@ export class MeasureBoxView extends GroupThreeView { } private createEdgeCircle(matrix: Matrix4, material: Material, face: BoxFace): Mesh | undefined { - const { boxDomainObject } = this; - const adjecentSize1 = boxDomainObject.size.getComponent(face.tangentIndex1); - if (!isValid(adjecentSize1)) { + const { domainObject } = this; + const adjecentSize1 = domainObject.size.getComponent(face.tangentIndex1); + if (!isValidSize(adjecentSize1)) { return undefined; } - const adjecentSize2 = boxDomainObject.size.getComponent(face.tangentIndex2); - if (!isValid(adjecentSize2)) { + const adjecentSize2 = domainObject.size.getComponent(face.tangentIndex2); + if (!isValidSize(adjecentSize2)) { return undefined; } const radius = RELATIVE_RESIZE_RADIUS * this.getFaceRadius(face); @@ -317,44 +316,28 @@ export class MeasureBoxView extends GroupThreeView { } else if (face.face === 5) { mesh.rotateX(Math.PI / 2); } else if (face.face === 0) { - mesh.rotateY(Math.PI / 2 + boxDomainObject.zRotation); + mesh.rotateY(Math.PI / 2 + domainObject.zRotation); } else if (face.face === 3) { - mesh.rotateY(-Math.PI / 2 + boxDomainObject.zRotation); + mesh.rotateY(-Math.PI / 2 + domainObject.zRotation); } else if (face.face === 1) { - mesh.rotateY(Math.PI + boxDomainObject.zRotation); + mesh.rotateY(Math.PI + domainObject.zRotation); } else if (face.face === 4) { - mesh.rotateY(boxDomainObject.zRotation); + mesh.rotateY(domainObject.zRotation); } return mesh; } - private createClippingPlanes(matrix: Matrix4, faceIndex: number): Plane[] { - const planes: Plane[] = []; - - for (const boxFace of BoxFace.getAllFaces()) { - if (boxFace.index === faceIndex) { - continue; - } - const center = boxFace.getCenter(newVector3()); - const normal = boxFace.getNormal(newVector3()).negate(); - const plane = new Plane().setFromNormalAndCoplanarPoint(normal, center); - plane.applyMatrix4(matrix); - planes.push(plane); - } - return planes; - } - // ================================================== // INSTANCE METHODS: Add Object3D's // ================================================== private addLabels(matrix: Matrix4): void { - const { boxDomainObject, style } = this; + const { domainObject, style } = this; const spriteHeight = this.getTextHeight(style.relativeTextSize); clear(this._sprites); for (let index = 0; index < 3; index++) { - const size = boxDomainObject.size.getComponent(index); - if (!isValid(size)) { + const size = domainObject.size.getComponent(index); + if (!isValidSize(size)) { this._sprites.push(undefined); continue; } @@ -367,8 +350,8 @@ export class MeasureBoxView extends GroupThreeView { this.addChild(sprite); } this.updateLabels(this.renderTarget.camera); - const { focusType } = boxDomainObject; - if (focusType === FocusType.Pending && boxDomainObject.hasArea) { + const { focusType } = domainObject; + if (focusType === FocusType.Pending && domainObject.hasArea) { this.addChild(this.createPendingLabel(matrix, spriteHeight)); } else if (showRotationLabel(focusType)) { this.addChild(this.createRotationLabel(matrix, spriteHeight)); @@ -376,11 +359,11 @@ export class MeasureBoxView extends GroupThreeView { } private updateLabels(camera: Camera): void { - const { boxDomainObject, style } = this; + const { domainObject, style } = this; const matrix = this.getMatrix(); const rotationMatrix = this.getRotationMatrix(); - const centerOfBox = newVector3(boxDomainObject.center); + const centerOfBox = newVector3(domainObject.center); centerOfBox.applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); const cameraPosition = camera.getWorldPosition(newVector3()); const cameraDirection = centerOfBox.sub(cameraPosition).normalize(); @@ -423,7 +406,7 @@ export class MeasureBoxView extends GroupThreeView { const edgeCenter = faceCenter2.add(faceCenter1); edgeCenter.applyMatrix4(matrix); - adjustLabel(edgeCenter, boxDomainObject, spriteHeight); + adjustLabel(edgeCenter, domainObject, spriteHeight); // Move the sprite slightly away from the box to avoid z-fighting edgeCenter.addScaledVector(cameraDirection, -spriteHeight / 2); @@ -439,13 +422,13 @@ export class MeasureBoxView extends GroupThreeView { } private addEdgeCircles(matrix: Matrix4): void { - const { boxDomainObject, style } = this; - let selectedFace = boxDomainObject.focusFace; - if (this.boxDomainObject.focusType !== FocusType.Face) { + const { domainObject, style } = this; + let selectedFace = domainObject.focusFace; + if (this.domainObject.focusType !== FocusType.Face) { selectedFace = undefined; } const material = new MeshPhongMaterial(); - updateMarkerMaterial(material, boxDomainObject, style, false); + updateMarkerMaterial(material, domainObject, style, false); for (const boxFace of BoxFace.getAllFaces()) { if (!this.isFaceVisible(boxFace)) { continue; @@ -456,7 +439,7 @@ export class MeasureBoxView extends GroupThreeView { } if (selectedFace !== undefined && this.isFaceVisible(selectedFace)) { const material = new MeshPhongMaterial(); - updateMarkerMaterial(material, boxDomainObject, style, true); + updateMarkerMaterial(material, domainObject, style, true); this.addChild(this.createEdgeCircle(matrix, material, selectedFace)); } } @@ -470,9 +453,9 @@ export class MeasureBoxView extends GroupThreeView { boxFace: BoxFace, outputCornerSign: Vector3 ): FocusType { - const { boxDomainObject } = this; + const { domainObject } = this; const scale = newVector3().setScalar(this.getFaceRadius(boxFace)); - const scaledMatrix = boxDomainObject.getScaledMatrix(scale); + const scaledMatrix = domainObject.getScaledMatrix(scale); scaledMatrix.invert(); const scaledPositionAtFace = newVector3(realPosition).applyMatrix4(scaledMatrix); const planePoint = boxFace.getPlanePoint(scaledPositionAtFace); @@ -496,9 +479,9 @@ export class MeasureBoxView extends GroupThreeView { } private getCornerSign(realPosition: Vector3, boxFace: BoxFace): Vector3 { - const { boxDomainObject } = this; + const { domainObject } = this; const scale = newVector3().setScalar(this.getFaceRadius(boxFace)); - const scaledMatrix = boxDomainObject.getScaledMatrix(scale); + const scaledMatrix = domainObject.getScaledMatrix(scale); scaledMatrix.invert(); const scaledPositionAtFace = realPosition.clone().applyMatrix4(scaledMatrix); scaledPositionAtFace.setComponent(boxFace.index, 0); @@ -514,17 +497,17 @@ export class MeasureBoxView extends GroupThreeView { } private getCorner(cornerSign: Vector3, boxFace: BoxFace): Vector3 { - const { boxDomainObject } = this; + const { domainObject } = this; const center = boxFace.getCenter(new Vector3()); // In range (-0.5, 0.5) const corner = center.addScaledVector(cornerSign, 0.5); - const matrix = boxDomainObject.getMatrix(); + const matrix = domainObject.getMatrix(); corner.applyMatrix4(matrix); return corner; } private isFaceVisible(boxFace: BoxFace): boolean { - const { boxDomainObject } = this; - switch (boxDomainObject.measureType) { + const { domainObject } = this; + switch (domainObject.measureType) { case MeasureType.VerticalArea: return boxFace.index === 1; // Y Face visible @@ -566,11 +549,11 @@ function showMarkers(focusType: FocusType): boolean { function updateSolidMaterial( material: MeshPhongMaterial, - boxDomainObject: MeasureBoxDomainObject, + domainObject: MeasureBoxDomainObject, style: MeasureBoxRenderStyle ): void { - const color = boxDomainObject.getColorByColorType(style.colorType); - const isSelected = boxDomainObject.isSelected; + const color = domainObject.getColorByColorType(style.colorType); + const isSelected = domainObject.isSelected; const opacity = isSelected ? style.opacity : style.opacity / 4; material.polygonOffset = true; material.polygonOffsetFactor = 1; @@ -588,10 +571,10 @@ function updateSolidMaterial( function updateLineSegmentsMaterial( material: LineBasicMaterial, - boxDomainObject: MeasureBoxDomainObject, + domainObject: MeasureBoxDomainObject, style: MeasureBoxRenderStyle ): void { - const color = boxDomainObject.getColorByColorType(style.colorType); + const color = domainObject.getColorByColorType(style.colorType); material.color = color; material.transparent = true; material.depthWrite = false; @@ -600,7 +583,7 @@ function updateLineSegmentsMaterial( function updateMarkerMaterial( material: MeshPhongMaterial, - boxDomainObject: MeasureBoxDomainObject, + domainObject: MeasureBoxDomainObject, style: MeasureBoxRenderStyle, hasFocus: boolean ): void { @@ -642,9 +625,6 @@ function adjustLabel( } } -function isValid(value: number): boolean { - return value > MIN_BOX_SIZE; -} // ================================================== // PRIVATE FUNCTIONS: Vector pool // ================================================== diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts index fdf106735ff..2179eec0fab 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineCreator.ts @@ -65,8 +65,8 @@ export class MeasureLineCreator extends BaseCreator { isPending: boolean ): boolean { // Figure out where the point should be if no intersection - if (isPending && this.realPointCount >= 1 && point === undefined) { - const lastPoint = this.points[this.realPointCount - 1]; + if (isPending && this.notPendingPointCount >= 1 && point === undefined) { + const lastPoint = this.lastNotPendingPoint; const plane = new Plane().setFromNormalAndCoplanarPoint(ray.direction, lastPoint); const newPoint = ray.intersectPlane(plane, new Vector3()); if (newPoint === null) { @@ -89,14 +89,16 @@ export class MeasureLineCreator extends BaseCreator { return true; } - public override handleEscape(): void { + public override handleEscape(): boolean { const domainObject = this._domainObject; - if (this.realPointCount < this.minimumPointCount) { + if (this.notPendingPointCount < this.minimumPointCount) { domainObject.removeInteractive(); + return false; // Removed } else if (this.lastIsPending) { domainObject.points.pop(); this.removePendingPoint(); domainObject.notify(Changes.geometry); } + return true; // Successfully } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts index a0f74b9186a..c4017d62a3d 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureLineView.ts @@ -39,7 +39,7 @@ export class MeasureLineView extends GroupThreeView { // INSTANCE PROPERTIES // ================================================== - protected get lineDomainObject(): MeasureLineDomainObject { + protected override get domainObject(): MeasureLineDomainObject { return super.domainObject as MeasureLineDomainObject; } @@ -81,7 +81,7 @@ export class MeasureLineView extends GroupThreeView { intersectInput: CustomObjectIntersectInput, closestDistance: number | undefined ): undefined | CustomObjectIntersection { - if (this.lineDomainObject.focusType === FocusType.Pending) { + if (this.domainObject.focusType === FocusType.Pending) { return undefined; // Should never be picked } return super.intersectIfCloser(intersectInput, closestDistance); @@ -92,8 +92,8 @@ export class MeasureLineView extends GroupThreeView { // ================================================== private createCylinders(): Mesh | undefined { - const { lineDomainObject, style } = this; - const { points } = lineDomainObject; + const { domainObject, style } = this; + const { points } = domainObject; const { length } = points; if (length < 2) { return undefined; @@ -103,7 +103,7 @@ export class MeasureLineView extends GroupThreeView { return; } const geometries: CylinderGeometry[] = []; - const loopLength = lineDomainObject.measureType === MeasureType.Polygon ? length + 1 : length; + const loopLength = domainObject.measureType === MeasureType.Polygon ? length + 1 : length; // Just allocate all needed objects once const prevPoint = new Vector3(); @@ -134,18 +134,18 @@ export class MeasureLineView extends GroupThreeView { prevPoint.copy(thisPoint); } const material = new MeshPhongMaterial(); - updateSolidMaterial(material, lineDomainObject, style); + updateSolidMaterial(material, domainObject, style); return new Mesh(mergeGeometries(geometries, false), material); } private createLines(): Wireframe | undefined { - const { lineDomainObject, style } = this; - const vertices = createVertices(lineDomainObject); + const { domainObject, style } = this; + const vertices = createVertices(domainObject); if (vertices === undefined) { return undefined; } - const color = lineDomainObject.getColorByColorType(style.colorType); - const linewidth = lineDomainObject.isSelected ? style.selectedLineWidth : style.lineWidth; + const color = domainObject.getColorByColorType(style.colorType); + const linewidth = domainObject.isSelected ? style.selectedLineWidth : style.lineWidth; const geometry = new LineSegmentsGeometry().setPositions(vertices); const material = new LineMaterial({ linewidth: linewidth / 50, @@ -158,8 +158,8 @@ export class MeasureLineView extends GroupThreeView { } private addLabels(): void { - const { lineDomainObject, style } = this; - const { points } = lineDomainObject; + const { domainObject, style } = this; + const { points } = domainObject; const { length } = points; if (length < 2) { return; @@ -168,7 +168,7 @@ export class MeasureLineView extends GroupThreeView { if (spriteHeight <= 0) { return; } - const loopLength = lineDomainObject.measureType === MeasureType.Polygon ? length : length - 1; + const loopLength = domainObject.measureType === MeasureType.Polygon ? length : length - 1; const center = new Vector3(); for (let i = 0; i < loopLength; i++) { const point1 = points[i % length]; @@ -181,14 +181,14 @@ export class MeasureLineView extends GroupThreeView { if (sprite === undefined) { continue; } - adjustLabel(center, lineDomainObject, style, spriteHeight); + adjustLabel(center, domainObject, style, spriteHeight); sprite.position.copy(center); this.addChild(sprite); } } private getTextHeight(relativeTextSize: number): number { - return relativeTextSize * this.lineDomainObject.getAverageLength(); + return relativeTextSize * this.domainObject.getAverageLength(); } } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts index bc0ccc71bdd..0bd5541251f 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasureRenderStyle.ts @@ -16,6 +16,6 @@ export abstract class MeasureRenderStyle extends RenderStyle { public colorType = ColorType.Specified; public textColor = WHITE_COLOR.clone(); public textBgColor = new Color().setScalar(0.05); // Dark gray - public textOpacity = 0.75; // Dark gray + public textOpacity = 0.75; public relativeTextSize = 0.05; // Relative to diagonal of the measurment object for box and average of lenght of line segments for line } diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasurementFunctions.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasurementFunctions.ts deleted file mode 100644 index e524204d598..00000000000 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasurementFunctions.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - * BaseTool: Base class for the tool are used to interact with the render target. - */ - -import { type RevealRenderTarget } from '../../base/renderTarget/RevealRenderTarget'; -import { MeasureDomainObject } from './MeasureDomainObject'; - -// ================================================== -// PUBLIC FUNCTIONS -// ================================================== - -export function getAnyMeasureDomainObject( - renderTarget: RevealRenderTarget -): MeasureDomainObject | undefined { - // eslint-disable-next-line no-unreachable-loop - for (const domainObject of getMeasureDomainObjects(renderTarget)) { - return domainObject; - } - return undefined; -} - -export function* getMeasureDomainObjects( - renderTarget: RevealRenderTarget -): Generator { - const { rootDomainObject } = renderTarget; - for (const descendant of rootDomainObject.getDescendantsByType(MeasureDomainObject)) { - yield descendant; - } -} diff --git a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts b/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts index 9796f3c3e91..31b7455036a 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/MeasurementTool.ts @@ -15,14 +15,13 @@ import { type BaseCreator } from '../../base/domainObjectsHelpers/BaseCreator'; import { MeasureLineCreator } from './MeasureLineCreator'; import { BaseEditTool } from '../../base/commands/BaseEditTool'; import { MeasureLineDomainObject } from './MeasureLineDomainObject'; -import { getAnyMeasureDomainObject, getMeasureDomainObjects } from './MeasurementFunctions'; import { MeasureRenderStyle } from './MeasureRenderStyle'; import { type DomainObject } from '../../base/domainObjects/DomainObject'; -import { type RevealRenderTarget } from '../../base/renderTarget/RevealRenderTarget'; import { MeasureDomainObject } from './MeasureDomainObject'; import { ShowMeasurmentsOnTopCommand } from './ShowMeasurmentsOnTopCommand'; import { SetMeasurmentTypeCommand } from './SetMeasurmentTypeCommand'; import { PopupStyle } from '../../base/domainObjectsHelpers/PopupStyle'; +import { type RootDomainObject } from '../../base/domainObjects/RootDomainObject'; export class MeasurementTool extends BaseEditTool { // ================================================== @@ -30,7 +29,7 @@ export class MeasurementTool extends BaseEditTool { // ================================================== private _creator: BaseCreator | undefined = undefined; - public measureType: MeasureType = MeasureType.Line; + public measureType: MeasureType = MeasureType.None; // Default none, let the user decide // ================================================== // OVERRIDES of BaseCommand @@ -84,10 +83,8 @@ export class MeasurementTool extends BaseEditTool { public override onKey(event: KeyboardEvent, down: boolean): void { if (down && event.key === 'Delete') { - for (const domainObject of getMeasureDomainObjects(this.renderTarget)) { - if (!domainObject.isSelected) { - continue; - } + const domainObject = this.rootDomainObject.getSelectedDescendantByType(MeasureDomainObject); + if (domainObject !== undefined) { domainObject.removeInteractive(); } this._creator = undefined; @@ -129,7 +126,7 @@ export class MeasurementTool extends BaseEditTool { return; } const ray = this.getRay(event); - if (creator.addPoint(ray, intersection.point, true)) { + if (creator.addPoint(ray, intersection, true)) { this.setDefaultCursor(); return; } @@ -166,16 +163,17 @@ export class MeasurementTool extends BaseEditTool { } public override async onClick(event: PointerEvent): Promise { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { renderTarget, rootDomainObject } = this; const { _creator: creator } = this; // Click in the "air" if (creator !== undefined && !creator.preferIntersection) { const ray = this.getRay(event); - if (creator.addPoint(ray, undefined, false)) { + if (creator.addPoint(ray, undefined)) { if (creator.isFinished) { this._creator = undefined; + this.measureType = MeasureType.None; + this.renderTarget.toolController.update(); } return; } @@ -188,22 +186,21 @@ export class MeasurementTool extends BaseEditTool { } const measurment = this.getMeasurement(intersection); if (measurment !== undefined) { - // Click at "a measurement" - // Do not want to click on other measurments this.deselectAll(measurment); measurment.setSelectedInteractive(true); + this.renderTarget.toolController.update(); return; } const ray = this.getRay(event); if (creator === undefined) { - const creator = (this._creator = this.createCreator()); + const creator = (this._creator = createCreator(this.measureType)); if (creator === undefined) { await super.onClick(event); return; } - if (creator.addPoint(ray, intersection.point, false)) { + if (creator.addPoint(ray, intersection)) { const { domainObject } = creator; - initializeStyle(domainObject, renderTarget); + initializeStyle(domainObject, this.rootDomainObject); this.deselectAll(); rootDomainObject.addChildInteractive(domainObject); domainObject.setSelectedInteractive(true); @@ -211,8 +208,10 @@ export class MeasurementTool extends BaseEditTool { this.renderTarget.toolController.update(); } } else { - if (creator.addPoint(ray, intersection.point, false)) { + if (creator.addPoint(ray, intersection)) { if (creator.isFinished) { + this.measureType = MeasureType.None; + this.renderTarget.toolController.update(); this._creator = undefined; } } @@ -234,12 +233,16 @@ export class MeasurementTool extends BaseEditTool { if (this._creator === undefined) { return; } - this._creator.handleEscape(); + if (this._creator.handleEscape()) { + // Sucessfully created, set it back to none + this.measureType = MeasureType.None; + this.renderTarget.toolController.update(); + } this._creator = undefined; } private setAllMeasurementsVisible(visible: boolean): void { - for (const domainObject of getMeasureDomainObjects(this.renderTarget)) { + for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { domainObject.setVisibleInteractive(visible, this.renderTarget); } } @@ -247,10 +250,6 @@ export class MeasurementTool extends BaseEditTool { private getMeasurement( intersection: AnyIntersection | undefined ): MeasureDomainObject | undefined { - if (intersection === undefined) { - return undefined; - } - // Do not want to click on other boxes if (!isDomainObjectIntersection(intersection)) { return undefined; } @@ -299,23 +298,8 @@ export class MeasurementTool extends BaseEditTool { } } - private createCreator(): BaseCreator | undefined { - switch (this.measureType) { - case MeasureType.Line: - case MeasureType.Polyline: - case MeasureType.Polygon: - return new MeasureLineCreator(this.measureType); - case MeasureType.HorizontalArea: - case MeasureType.VerticalArea: - case MeasureType.Volume: - return new MeasureBoxCreator(this.measureType); - default: - return undefined; - } - } - protected defocusAll(except?: DomainObject | undefined): void { - for (const domainObject of getMeasureDomainObjects(this.renderTarget)) { + for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { if (except !== undefined && domainObject === except) { continue; } @@ -329,8 +313,13 @@ export class MeasurementTool extends BaseEditTool { } } -function initializeStyle(domainObject: DomainObject, renderTarget: RevealRenderTarget): void { - const otherDomainObject = getAnyMeasureDomainObject(renderTarget); +// ================================================== +// 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; } @@ -341,3 +330,19 @@ function initializeStyle(domainObject: DomainObject, renderTarget: RevealRenderT } 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); + + case MeasureType.HorizontalArea: + case MeasureType.VerticalArea: + case MeasureType.Volume: + return new MeasureBoxCreator(measureType); + default: + return undefined; + } +} diff --git a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts index be6e75bbd44..51b3017b80f 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/SetMeasurmentTypeCommand.ts @@ -24,13 +24,6 @@ export class SetMeasurmentTypeCommand extends RenderTargetCommand { // OVERRIDES of BaseCommand // ================================================== - public override equals(other: BaseCommand): boolean { - if (!(other instanceof SetMeasurmentTypeCommand)) { - return false; - } - return this._measureType === other._measureType; - } - public override get icon(): string { return getIconByMeasureType(this._measureType); } @@ -60,16 +53,23 @@ export class SetMeasurmentTypeCommand extends RenderTargetCommand { if (measurementTool === undefined) { return false; } + measurementTool.handleEscape(); + measurementTool.clearDragging(); if (measurementTool.measureType === this._measureType) { measurementTool.measureType = MeasureType.None; } else { measurementTool.measureType = this._measureType; } - measurementTool.handleEscape(); - measurementTool.clearDragging(); return true; } + public override equals(other: BaseCommand): boolean { + if (!(other instanceof SetMeasurmentTypeCommand)) { + return false; + } + return this._measureType === other._measureType; + } + // ================================================== // INSTANCE METHODS // ================================================== diff --git a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts b/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts index 319fcc7a964..9d66341e6c5 100644 --- a/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts +++ b/react-components/src/architecture/concrete/boxDomainObject/ShowMeasurmentsOnTopCommand.ts @@ -6,7 +6,7 @@ import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; import { type Tooltip } from '../../base/commands/BaseCommand'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { getAnyMeasureDomainObject, getMeasureDomainObjects } from './MeasurementFunctions'; +import { MeasureDomainObject } from './MeasureDomainObject'; export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { // ================================================== @@ -22,7 +22,7 @@ export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { } public override get isEnabled(): boolean { - const domainObject = getAnyMeasureDomainObject(this.renderTarget); + const domainObject = this.rootDomainObject.getDescendantByType(MeasureDomainObject); return domainObject !== undefined; } @@ -36,7 +36,7 @@ export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { protected override invokeCore(): boolean { const depthTest = this.getDepthTest(); - for (const domainObject of getMeasureDomainObjects(this.renderTarget)) { + for (const domainObject of this.rootDomainObject.getDescendantsByType(MeasureDomainObject)) { const style = domainObject.renderStyle; style.depthTest = !depthTest; domainObject.notify(Changes.renderStyle); @@ -49,7 +49,7 @@ export class ShowMeasurmentsOnTopCommand extends RenderTargetCommand { // ================================================== public getDepthTest(): boolean { - const domainObject = getAnyMeasureDomainObject(this.renderTarget); + const domainObject = this.rootDomainObject.getDescendantByType(MeasureDomainObject); if (domainObject === undefined) { return false; } diff --git a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts index 60e3a491190..0e164f4e97f 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/SetTerrainVisibleCommand.ts @@ -24,9 +24,7 @@ export class SetTerrainVisibleCommand extends RenderTargetCommand { } protected override invokeCore(): boolean { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; - + const { renderTarget, rootDomainObject } = this; let terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( TerrainDomainObject, DEFAULT_TERRAIN_NAME diff --git a/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts b/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts index dcaad6dd14d..16f75151fea 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/TerrainDomainObject.ts @@ -50,7 +50,9 @@ export class TerrainDomainObject extends VisualDomainObject { if (!(style instanceof TerrainRenderStyle)) { return; } - // The rest checks if the increment is valid. To many contour lines with hang/crash the app. + // The rest checks if the increment is valid. + // Too many contour lines with hang/crash the app, so it recalculate + // its value if it is not set or too large/small. const { grid } = this; if (grid === undefined) { return; diff --git a/react-components/src/architecture/concrete/terrainDomainObject/TerrainThreeView.ts b/react-components/src/architecture/concrete/terrainDomainObject/TerrainThreeView.ts index f51d16972c9..7901c9a3753 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/TerrainThreeView.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/TerrainThreeView.ts @@ -40,7 +40,7 @@ export class TerrainThreeView extends GroupThreeView { // INSTANCE PROPERTIES // ================================================== - private get terrainDomainObject(): TerrainDomainObject { + protected override get domainObject(): TerrainDomainObject { return super.domainObject as TerrainDomainObject; } @@ -73,7 +73,7 @@ export class TerrainThreeView extends GroupThreeView { this.addChild(this.createSolid()); this.invalidateRenderTarget(); } else if (solid !== undefined) { - updateSolidMaterial(solid.material as MeshPhongMaterial, this.terrainDomainObject, style); + updateSolidMaterial(solid.material as MeshPhongMaterial, this.domainObject, style); this.invalidateRenderTarget(); } } @@ -86,11 +86,7 @@ export class TerrainThreeView extends GroupThreeView { this.addChild(this.createContours()); this.invalidateRenderTarget(); } else if (contours !== undefined) { - updateContoursMaterial( - contours.material as LineBasicMaterial, - this.terrainDomainObject, - style - ); + updateContoursMaterial(contours.material as LineBasicMaterial, this.domainObject, style); this.invalidateRenderTarget(); } } @@ -102,8 +98,7 @@ export class TerrainThreeView extends GroupThreeView { // ================================================== protected override calculateBoundingBox(): Box3 { - const { terrainDomainObject } = this; - const { grid } = terrainDomainObject; + const { grid } = this.domainObject; if (grid === undefined) { return new Box3().makeEmpty(); } @@ -125,8 +120,7 @@ export class TerrainThreeView extends GroupThreeView { // ================================================== protected override addChildren(): void { - const { terrainDomainObject } = this; - const { grid } = terrainDomainObject; + const { grid } = this.domainObject; if (grid === undefined) { return undefined; } @@ -143,8 +137,8 @@ export class TerrainThreeView extends GroupThreeView { if (!style.showSolid) { return undefined; } - const { terrainDomainObject } = this; - const { grid } = terrainDomainObject; + const { domainObject } = this; + const { grid } = domainObject; if (grid === undefined) { return undefined; } @@ -152,7 +146,7 @@ export class TerrainThreeView extends GroupThreeView { const geometry = buffers.createBufferGeometry(); const material = new MeshPhongMaterial(); - updateSolidMaterial(material, terrainDomainObject, style); + updateSolidMaterial(material, domainObject, style); const result = new Mesh(geometry, material); result.name = SOLID_NAME; @@ -165,8 +159,8 @@ export class TerrainThreeView extends GroupThreeView { if (!style.showContours) { return undefined; } - const { terrainDomainObject } = this; - const { grid } = terrainDomainObject; + const { domainObject } = this; + const { grid } = domainObject; if (grid === undefined) { return undefined; } @@ -179,7 +173,7 @@ export class TerrainThreeView extends GroupThreeView { geometry.setAttribute('position', new Float32BufferAttribute(contoursBuffer, 3)); const material = new LineBasicMaterial(); - updateContoursMaterial(material, terrainDomainObject, style); + updateContoursMaterial(material, domainObject, style); const result = new LineSegments(geometry, material); result.name = CONTOURS_NAME; diff --git a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts index d23d5eb55a3..1358fb736ef 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/UpdateTerrainCommand.ts @@ -25,8 +25,7 @@ export class UpdateTerrainCommand extends RenderTargetCommand { } public override get isEnabled(): boolean { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; + const { renderTarget, rootDomainObject } = this; const terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( TerrainDomainObject, DEFAULT_TERRAIN_NAME @@ -38,9 +37,7 @@ export class UpdateTerrainCommand extends RenderTargetCommand { } protected override invokeCore(): boolean { - const { renderTarget } = this; - const { rootDomainObject } = renderTarget; - + const { renderTarget, rootDomainObject } = this; const terrainDomainObject = rootDomainObject.getDescendantByTypeAndName( TerrainDomainObject, DEFAULT_TERRAIN_NAME diff --git a/react-components/src/architecture/concrete/terrainDomainObject/geometry/ContouringService.ts b/react-components/src/architecture/concrete/terrainDomainObject/geometry/ContouringService.ts index fe7c7ece6b9..8beb4a574d6 100644 --- a/react-components/src/architecture/concrete/terrainDomainObject/geometry/ContouringService.ts +++ b/react-components/src/architecture/concrete/terrainDomainObject/geometry/ContouringService.ts @@ -5,7 +5,7 @@ import { Vector3 } from 'three'; import { Range1 } from '../../../base/utilities/geometry/Range1'; import { type RegularGrid2 } from './RegularGrid2'; -import { isAbsEqual, isBetween, max, min } from '../../../base/utilities/extensions/mathExtensions'; +import { isAbsEqual, isBetween } from '../../../base/utilities/extensions/mathExtensions'; export class ContouringService { // ================================================== @@ -80,7 +80,7 @@ export class ContouringService { // ================================================== private addTriangle(a: Vector3, b: Vector3, c: Vector3): void { - this._tempRange.set(min(a.z, b.z, c.z), max(a.z, b.z, c.z)); + this._tempRange.set(Math.min(a.z, b.z, c.z), Math.max(a.z, b.z, c.z)); for (const anyTick of this._tempRange.getFastTicks(this._increment, this._tolerance)) { const z = Number(anyTick); diff --git a/react-components/src/components/Architecture/DomainObjectPanel.tsx b/react-components/src/components/Architecture/DomainObjectPanel.tsx index c00bb7125cb..e4a0758d9b8 100644 --- a/react-components/src/components/Architecture/DomainObjectPanel.tsx +++ b/react-components/src/components/Architecture/DomainObjectPanel.tsx @@ -64,7 +64,9 @@ export const DomainObjectPanel = (): ReactElement => { appendTo={document.body}>