Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (react-components): Add general option and use it to make camera speed settings #4619

Merged
merged 20 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"peerDependencies": {
"@cognite/cogs.js": ">=9.84.3",
"@cognite/reveal": "4.14.7",
"@cognite/reveal": "4.15.0",
"react": ">=18",
"react-dom": ">=18",
"styled-components": ">=5"
Expand All @@ -44,7 +44,7 @@
"@cognite/cdf-i18n-utils": "^0.7.5",
"@cognite/cdf-utilities": "^3.6.0",
"@cognite/cogs.js": "^9.84.3",
"@cognite/reveal": "^4.14.7",
"@cognite/reveal": "^4.15.0",
"@cognite/sdk": "^9.13.0",
"@playwright/test": "^1.43.1",
"@storybook/addon-essentials": "^8.0.9",
Expand Down
16 changes: 12 additions & 4 deletions react-components/src/architecture/base/commands/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright 2024 Cognite AS
*/

import { type TranslateKey } from '../utilities/TranslateKey';
import { type TranslateDelegate, type TranslateKey } from '../utilities/TranslateKey';
import { clear, remove } from '../utilities/extensions/arrayExtensions';

type UpdateDelegate = (command: BaseCommand) => void;
Expand Down Expand Up @@ -44,7 +44,7 @@ export abstract class BaseCommand {
// =================================================

public get name(): string {
return this.tooltip.fallback ?? this.tooltip.key;
return this.tooltip.fallback;
}

public get shortCutKey(): string | undefined {
Expand All @@ -55,8 +55,8 @@ export abstract class BaseCommand {
return { fallback: '' };
}

public get icon(): string {
return 'Unknown';
public get icon(): string | undefined {
return undefined; // Means no icon
}

public get buttonType(): string {
Expand Down Expand Up @@ -128,4 +128,12 @@ export abstract class BaseCommand {
listener(this);
}
}

public getLabel(translate: TranslateDelegate): string {
const { key, fallback } = this.tooltip;
if (key === undefined) {
return fallback;
}
return translate(key, fallback);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*!
* Copyright 2024 Cognite AS
*/

import { type RevealRenderTarget } from '../renderTarget/RevealRenderTarget';
import { BaseCommand } from './BaseCommand';
import { RenderTargetCommand } from './RenderTargetCommand';

/**
* Base class for all command and tools. These are object that can do a
* user interaction with the system. It also have enough information to
* generate the UI for the command.
*/

export abstract class BaseOptionCommand extends BaseCommand {
private _options: BaseCommand[] | undefined = undefined;

// ==================================================
// VIRTUAL METHODS
// ==================================================

public createOptions(): BaseCommand[] {
return []; // Override this to add options
}

// ==================================================
// INSTANCE METHODS
// ==================================================

public getOrCreateOptions(renderTarget: RevealRenderTarget): BaseCommand[] {
if (this._options === undefined) {
this._options = this.createOptions();
for (const option of this._options) {
if (option instanceof RenderTargetCommand) {
option.attach(renderTarget);
}
}
}
return this._options;
}

public get selectedOption(): BaseCommand | undefined {
if (this._options === undefined) {
return undefined;
}
return this._options.find((option) => option.isChecked);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*!
* Copyright 2024 Cognite AS
*/

import { type BaseCommand } from '../commands/BaseCommand';
import { BaseOptionCommand } from '../commands/BaseOptionCommand';
import { RenderTargetCommand } from '../commands/RenderTargetCommand';
import { type TranslateKey } from '../utilities/TranslateKey';

const KEYBOARD_SPEED_VALUES = [0.5, 1, 2, 5, 10, 20];

export class KeyboardSpeedCommand extends BaseOptionCommand {
// ==================================================
// OVERRIDES
// ==================================================

public override get tooltip(): TranslateKey {
return { key: 'FLY_SPEED', fallback: 'Set fly speed on the camera' };
}

public override createOptions(): BaseCommand[] {
const options: BaseCommand[] = [];
for (const value of KEYBOARD_SPEED_VALUES) {
options.push(new OptionCommand(value));
}
return options;
}
}

// Note: This is not exported, as it is only used internally
class OptionCommand extends RenderTargetCommand {
private readonly _value;

public constructor(value: number) {
super();
this._value = value;
}

public override get tooltip(): TranslateKey {
return { fallback: `${this._value.toString()}x` };
}

public override get isChecked(): boolean {
return this._value === this.renderTarget.flexibleCameraManager.options.keyboardSpeed;
}

public override invokeCore(): boolean {
this.renderTarget.flexibleCameraManager.options.keyboardSpeed = this._value;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,8 @@ export abstract class DomainObject {
// VIRTUAL METHODS: Others
// ==================================================

public get icon(): string {
return 'Unknown';
public get icon(): string | undefined {
return undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { type BaseTool } from '../../base/commands/BaseTool';
import { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetricUnitsCommand';
import { MeasurementTool } from '../measurements/MeasurementTool';
import { ClipTool } from '../clipping/ClipTool';
import { KeyboardSpeedCommand } from '../../base/concreteCommands/KeyboardSpeedCommand';

export class StoryBookConfig extends BaseRevealConfig {
// ==================================================
Expand All @@ -37,6 +38,7 @@ export class StoryBookConfig extends BaseRevealConfig {
new FitViewCommand(),
new SetAxisVisibleCommand(),
new ToggleMetricUnitsCommand(),
new KeyboardSpeedCommand(),
undefined,
new ExampleTool(),
new MeasurementTool(),
Expand Down
123 changes: 27 additions & 96 deletions react-components/src/components/Architecture/CommandButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,134 +2,65 @@
* Copyright 2023 Cognite AS
*/

import { type ReactElement, useState, useEffect, useMemo } from 'react';
import { type ReactElement, useState, useEffect, useMemo, useCallback } from 'react';
import { useRenderTarget } from '../RevealCanvas/ViewerContext';
import { Button, Tooltip as CogsTooltip, Divider, type IconType } from '@cognite/cogs.js';
import { Button, Tooltip as CogsTooltip, type IconType } from '@cognite/cogs.js';
import { useTranslation } from '../i18n/I18n';
import { type BaseCommand } from '../../architecture/base/commands/BaseCommand';
import { type RevealRenderTarget } from '../../architecture/base/renderTarget/RevealRenderTarget';
import { RenderTargetCommand } from '../../architecture/base/commands/RenderTargetCommand';

export const CommandButtons = ({
commands,
isHorizontal = false
}: {
commands: Array<BaseCommand | undefined>;
isHorizontal: boolean;
}): ReactElement => {
return (
<>
{commands.map(
(command, index): ReactElement => (
<CommandButtonWrapper
command={command}
isHorizontal={isHorizontal}
key={getKey(command, index)}
/>
)
)}
</>
);
};

export const CreateCommandButton = (command: BaseCommand, isHorizontal = false): ReactElement => {
return <CommandButton command={command} isHorizontal={isHorizontal} />;
};
import { getButtonType, getDefaultCommand, getIcon, getTooltipPlacement } from './utilities';

export const CommandButton = ({
command,
inputCommand,
isHorizontal = false
}: {
command: BaseCommand;
inputCommand: BaseCommand;
isHorizontal: boolean;
}): ReactElement => {
const renderTarget = useRenderTarget();
const { t } = useTranslation();
const newCommand = useMemo<BaseCommand>(() => getDefaultCommand(command, renderTarget), []);
const command = useMemo<BaseCommand>(() => getDefaultCommand(inputCommand, renderTarget), []);

const [isChecked, setChecked] = useState<boolean>(false);
const [isEnabled, setEnabled] = useState<boolean>(true);
const [isVisible, setVisible] = useState<boolean>(true);
const [uniqueId, setUniqueId] = useState<number>(0);
const [icon, setIcon] = useState<IconType>('Copy');
const [icon, setIcon] = useState<IconType | undefined>(undefined);

const update = useCallback((command: BaseCommand) => {
setChecked(command.isChecked);
setEnabled(command.isEnabled);
setVisible(command.isVisible);
setUniqueId(command.uniqueId);
setIcon(getIcon(command));
}, []);

useEffect(() => {
function update(command: BaseCommand): void {
setChecked(command.isChecked);
setEnabled(command.isEnabled);
setVisible(command.isVisible);
setUniqueId(command.uniqueId);
setIcon(command.icon as IconType);
}
update(newCommand);
newCommand.addEventListener(update);
update(command);
command.addEventListener(update);
return () => {
newCommand.removeEventListener(update);
command.removeEventListener(update);
};
}, [newCommand.isEnabled, newCommand.isChecked, newCommand.isVisible]);
}, [command.isEnabled, command.isChecked, command.isVisible]);

if (!isVisible) {
return <></>;
}
const placement = isHorizontal ? 'top' : 'right';
const { key, fallback } = newCommand.tooltip;
// This was the only way it went through compiler: (more button types will be added in the future)
const type = newCommand.buttonType;
if (type !== 'ghost' && type !== 'ghost-destructive' && type !== 'primary') {
return <></>;
}
const text = key === undefined ? fallback : t(key, fallback);
const placement = getTooltipPlacement(isHorizontal);
const tooltip = command.getLabel(t);
return (
<CogsTooltip content={text} placement={placement} appendTo={document.body}>
<CogsTooltip content={tooltip} placement={placement} appendTo={document.body}>
<Button
type={type}
type={getButtonType(command)}
icon={icon}
key={uniqueId}
toggled={isChecked}
disabled={!isEnabled}
aria-label={text}
toggled={isChecked}
aria-label={tooltip}
iconPlacement="right"
onClick={() => {
newCommand.invoke();
command.invoke();
}}
/>
</CogsTooltip>
);
};

function getDefaultCommand(newCommand: BaseCommand, renderTarget: RevealRenderTarget): BaseCommand {
// If it exists from before, return the existing command
// Otherwise, add the new command to the controller and attach the renderTarget.
if (!newCommand.hasData) {
const oldCommand = renderTarget.commandsController.getEqual(newCommand);
if (oldCommand !== undefined) {
return oldCommand;
}
renderTarget.commandsController.add(newCommand);
}
if (newCommand instanceof RenderTargetCommand) {
newCommand.attach(renderTarget);
}
return newCommand;
}

function getKey(command: BaseCommand | undefined, index: number): number {
if (command === undefined) {
return -index;
}

return command.uniqueId;
}

function CommandButtonWrapper({
command,
isHorizontal
}: {
command: BaseCommand | undefined;
isHorizontal: boolean;
}): ReactElement {
if (command === undefined) {
const direction = !isHorizontal ? 'horizontal' : 'vertical';
return <Divider weight="2px" length="24px" direction={direction} />;
}
return <CommandButton command={command} isHorizontal={isHorizontal} />;
}
Loading
Loading