diff --git a/react-components/src/architecture/concrete/clipping/ClipTool.ts b/react-components/src/architecture/concrete/clipping/ClipTool.ts index 36eaa7d630b..082c73a4119 100644 --- a/react-components/src/architecture/concrete/clipping/ClipTool.ts +++ b/react-components/src/architecture/concrete/clipping/ClipTool.ts @@ -16,6 +16,7 @@ import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainOb import { SetClipTypeCommand } from './commands/SetClipTypeCommand'; import { PlaneCreator } from '../primitives/plane/PlaneCreator'; import { SliceDomainObject } from './SliceDomainObject'; +import { NextOrPrevClippingCommand } from './commands/NextClippingCommand'; export class ClipTool extends PrimitiveEditTool { // ================================================== @@ -47,8 +48,10 @@ export class ClipTool extends PrimitiveEditTool { new SetClipTypeCommand(PrimitiveType.Box), undefined, // Separator new ApplyClipCommand(), - new ShowClippingOnTopCommand(), - new ShowAllClippingCommand() + new NextOrPrevClippingCommand(false), + new NextOrPrevClippingCommand(true), + new ShowAllClippingCommand(), + new ShowClippingOnTopCommand() ]; } diff --git a/react-components/src/architecture/concrete/clipping/commands/NextClippingCommand.ts b/react-components/src/architecture/concrete/clipping/commands/NextClippingCommand.ts new file mode 100644 index 00000000000..13b35b8b0ac --- /dev/null +++ b/react-components/src/architecture/concrete/clipping/commands/NextClippingCommand.ts @@ -0,0 +1,180 @@ +/*! + * Copyright 2024 Cognite AS + */ + +import { type BaseCommand } from '../../../base/commands/BaseCommand'; +import { RenderTargetCommand } from '../../../base/commands/RenderTargetCommand'; +import { type TranslateKey } from '../../../base/utilities/TranslateKey'; +import { CropBoxDomainObject } from '../CropBoxDomainObject'; +import { SliceDomainObject } from '../SliceDomainObject'; +import { ApplyClipCommand } from './ApplyClipCommand'; + +export class NextOrPrevClippingCommand extends RenderTargetCommand { + private readonly _next: boolean; + + // ================================================== + // CONSTRUCTORS + // ================================================== + + public constructor(next: boolean) { + super(); + this._next = next; + } + + // ================================================== + // OVERRIDES + // ================================================== + + public override get tooltip(): TranslateKey { + if (this._next) { + return { + key: 'CLIP_NEXT', + fallback: 'Set the next crop box or slicing plane as global clipping' + }; + } else { + return { + key: 'CLIP_PREV', + fallback: 'Set the previous crop box or slicing plane as global clipping' + }; + } + } + + public override get icon(): string { + return this._next ? 'ArrowRight' : 'ArrowLeft'; + } + + public override get isEnabled(): boolean { + if (!this.renderTarget.isGlobalClippingActive) { + return false; + } + const minimumCount = this._next ? 2 : 3; // Don't need both buttons if it is less than 3 + const { rootDomainObject } = this; + // Require at least two crop boxes or one crop box and one slice + let count = 0; + for (const domainObject of rootDomainObject.getDescendants()) { + if (domainObject instanceof CropBoxDomainObject) { + count++; + if (count >= minimumCount) { + return true; // Optimization + } + } + } + if (rootDomainObject.getDescendantByType(SliceDomainObject) !== undefined) { + count++; + } + return count >= minimumCount; + } + + public override equals(other: BaseCommand): boolean { + if (!(other instanceof NextOrPrevClippingCommand)) { + return false; + } + return this._next === other._next; + } + + protected override invokeCore(): boolean { + // This code treat the slicing planes as one single group, along with all the crop boxes. + // The next selected crop box or slicing planes will be used as clipping. + const array = this.createCropBoxesAndSliceArray(); + if (array.length <= 1) { + return false; + } + const selectedIndex = array.findIndex((domainObject) => domainObject.isSelected); + if (selectedIndex === undefined) { + return false; + } + const nextIndex = this.getNextIndex(selectedIndex, array.length); + this.setAllInvisibleAndDeselected(array, nextIndex); + + // Take the next crop box or slicing planes and use it as clipping + const nextCropBoxOrSlice = array[nextIndex]; + this.setVisibleAndSelected(nextCropBoxOrSlice, true); + if (nextCropBoxOrSlice instanceof CropBoxDomainObject) { + nextCropBoxOrSlice.setThisAsGlobalCropBox(); + } else { + ApplyClipCommand.setClippingPlanes(this.rootDomainObject); + } + this.renderTarget.fitView(); + return true; + } + + // ================================================== + // INSTANCE METHODS + // ================================================== + + private createCropBoxesAndSliceArray(): Array { + const { rootDomainObject } = this; + // Build the array of crop boxes and at least one slice + const array = new Array(); + for (const cropBox of rootDomainObject.getDescendantsByType(CropBoxDomainObject)) { + array.push(cropBox); + } + // Take the selected slice, otherwise take the first one + const selectedSlice = rootDomainObject.getSelectedDescendantByType(SliceDomainObject); + if (selectedSlice !== undefined) { + array.push(selectedSlice); + } else { + const sliceDomainObject = rootDomainObject.getDescendantByType(SliceDomainObject); + if (sliceDomainObject !== undefined) { + array.push(sliceDomainObject); + } + } + return array; + } + + private getNextIndex(selectedIndex: number, arrayLength: number): number { + const increment = this._next ? 1 : -1; + const nextIndex = selectedIndex + increment; + if (nextIndex < 0) { + return arrayLength - 1; + } else if (nextIndex >= arrayLength) { + return 0; + } + return nextIndex; + } + + private setAllInvisibleAndDeselected( + array: Array, + exceptIndex: number + ): void { + for (let i = 0; i < array.length; i++) { + if (i !== exceptIndex) { + this.setVisibleAndSelected(array[i], false); + } + } + } + + private setVisibleAndSelected( + domainObject: CropBoxDomainObject | SliceDomainObject, + value: boolean + ): void { + domainObject.setSelectedInteractive(value); + if (domainObject instanceof SliceDomainObject) { + this.setAllSliceDomainObjectsVisible(value); + } else { + domainObject.setVisibleInteractive(value, this.renderTarget); + } + } + + private setAllSliceDomainObjectsVisible(visible: boolean): void { + const { rootDomainObject } = this; + for (const sliceDomainObject of rootDomainObject.getDescendantsByType(SliceDomainObject)) { + sliceDomainObject.setVisibleInteractive(visible, this.renderTarget); + } + } + + public getCropBoxesAndSliceCount(): number { + const { rootDomainObject } = this; + // Require at least two crop boxes or one crop box and one slice + let count = 0; + for (const domainObject of rootDomainObject.getDescendants()) { + if (domainObject instanceof CropBoxDomainObject) { + count++; + } + } + if (rootDomainObject.getDescendantByType(SliceDomainObject) !== undefined) { + count++; + } + return count; + } +} diff --git a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts index 57a4ceee67b..4b7ae41b383 100644 --- a/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts +++ b/react-components/src/architecture/concrete/exampleDomainObject/ExampleTool.ts @@ -35,7 +35,7 @@ export class ExampleTool extends BaseEditTool { // ================================================== public override onKey(event: KeyboardEvent, down: boolean): void { - if (down && event.key === 'Delete') { + if (down && (event.key === 'Delete' || event.key === 'Backspace')) { const domainObject = this.getSelected(); if (domainObject !== undefined) { domainObject.removeInteractive(); @@ -61,7 +61,7 @@ export class ExampleTool extends BaseEditTool { hsl.h = (hsl.h + Math.sign(delta) * 0.02) % 1; domainObject.color.setHSL(hsl.h, hsl.s, hsl.l); domainObject.notify(Changes.color); - } else if (event.ctrlKey) { + } else if (event.ctrlKey || event.metaKey) { // Change opacity const opacity = domainObject.renderStyle.opacity + Math.sign(delta) * 0.05; domainObject.renderStyle.opacity = clamp(opacity, 0.2, 1); diff --git a/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts b/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts index 95f716539c8..8e0c6ae2762 100644 --- a/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts +++ b/react-components/src/architecture/concrete/primitives/PrimitiveEditTool.ts @@ -52,7 +52,7 @@ export abstract class PrimitiveEditTool extends BaseEditTool { } public override onKey(event: KeyboardEvent, down: boolean): void { - if (down && event.key === 'Delete') { + if (down && (event.key === 'Delete' || event.key === 'Backspace')) { const domainObject = this.getSelected(); if (domainObject !== undefined) { domainObject.removeInteractive();