// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, {
ReactElement, useEffect, useRef,
} from 'react';
import { connect, useSelector } from 'react-redux';
import {
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons';
import Spin from 'antd/lib/spin';
import {
activateObject,
confirmCanvasReady,
createAnnotationsAsync,
dragCanvas,
editShape,
groupAnnotationsAsync,
groupObjects,
resetCanvas,
shapeDrawn,
updateAnnotationsAsync,
updateCanvasContextMenu,
} from 'actions/annotation-actions';
import {
ColorBy, CombinedState, ContextMenuType, ObjectType, Workspace,
} from 'reducers';
import { CameraAction, Canvas3d, ViewsDOM } from 'cvat-canvas3d-wrapper';
import CVATTooltip from 'components/common/cvat-tooltip';
import { LogType } from 'cvat-logger';
import { getCore } from 'cvat-core-wrapper';
const cvat = getCore();
interface StateToProps {
opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d;
jobInstance: any;
frameData: any;
annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number | null;
activatedStateID: number | null;
activeObjectType: ObjectType;
workspace: Workspace;
frame: number;
resetZoom: boolean;
}
interface DispatchToProps {
onDragCanvas: (enabled: boolean) => void;
onSetupCanvas(): void;
onGroupObjects: (enabled: boolean) => void;
onResetCanvas(): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onActivateObject: (activatedStateID: number | null) => void;
onShapeDrawn: () => void;
onEditShape: (enabled: boolean) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
instance: canvasInstance,
contextMenu: { visible: contextMenuVisibility },
},
drawing: { activeLabelID, activeObjectType },
job: { instance: jobInstance },
player: {
frame: { data: frameData, number: frame, fetching: frameFetching },
},
annotations: {
states: annotations,
activatedStateID,
},
workspace,
},
settings: {
player: {
resetZoom,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor,
},
},
} = state;
return {
canvasInstance: canvasInstance as Canvas3d,
jobInstance,
frameData,
contextMenuVisibility,
annotations,
frameFetching,
frame,
opacity,
colorBy,
selectedOpacity,
outlined,
outlineColor,
activeLabelID,
activatedStateID,
activeObjectType,
resetZoom,
workspace,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onActivateObject(activatedStateID: number | null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID, null, null));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onUpdateContextMenu(
visible: boolean,
left: number,
top: number,
type: ContextMenuType,
pointID?: number,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
};
}
type Props = StateToProps & DispatchToProps;
const Spinner = React.memo(() => (
));
export const PerspectiveViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
canvas.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
};
const ArrowGroup = (): ReactElement => (
);
const ControlGroup = (): ReactElement => (
);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().perspective);
}
}, []);
return (
);
},
);
export const TopViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().top);
}
}, []);
return (
{ !canvasIsReady &&
}
Top
);
},
);
export const SideViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().side);
}
}, []);
return (
{ !canvasIsReady &&
}
Side
);
},
);
export const FrontViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().front);
}
}, []);
return (
{ !canvasIsReady &&
}
Front
);
},
);
const Canvas3DWrapperComponent = React.memo((props: Props): ReactElement => {
const animateId = useRef(0);
const {
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
contextMenuVisibility,
frameData,
onResetCanvas,
onSetupCanvas,
annotations,
frame,
jobInstance,
activeLabelID,
activatedStateID,
resetZoom,
activeObjectType,
onShapeDrawn,
onCreateAnnotations,
} = props;
const { canvasInstance } = props as { canvasInstance: Canvas3d };
const onCanvasSetup = (): void => {
onSetupCanvas();
};
const onCanvasDragStart = (): void => {
const { onDragCanvas } = props;
onDragCanvas(true);
};
const onCanvasDragDone = (): void => {
const { onDragCanvas } = props;
onDragCanvas(false);
};
const animateCanvas = (): void => {
canvasInstance.render();
animateId.current = requestAnimationFrame(animateCanvas);
};
const updateCanvas = (): void => {
if (frameData !== null) {
canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
);
}
};
const onCanvasCancel = (): void => {
onResetCanvas();
};
const onCanvasShapeDrawn = (event: any): void => {
if (!event.detail.continue) {
onShapeDrawn();
}
const { state, duration } = event.detail;
const isDrawnFromScratch = !state.label;
if (isDrawnFromScratch) {
jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame;
state.zOrder = 0;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
};
const onCanvasClick = (e: MouseEvent): void => {
const { onUpdateContextMenu } = props;
if (contextMenuVisibility) {
onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
};
const initialSetup = (): void => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
canvasInstance.configure({ resetZoom });
};
const keyControlsKeyDown = (key: KeyboardEvent): void => {
canvasInstance.keyControls(key);
};
const keyControlsKeyUp = (key: KeyboardEvent): void => {
if (key.code === 'ControlLeft') {
canvasInstance.keyControls(key);
}
};
const onCanvasShapeSelected = (event: any): void => {
const { onActivateObject } = props;
const { clientID } = event.detail;
onActivateObject(clientID);
canvasInstance.activate(clientID);
};
const onCanvasEditDone = (event: any): void => {
const { onEditShape, onUpdateAnnotations } = props;
onEditShape(false);
const { state, points } = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html();
document.addEventListener('keydown', keyControlsKeyDown);
document.addEventListener('keyup', keyControlsKeyUp);
initialSetup();
updateCanvas();
animateCanvas();
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
document.removeEventListener('keydown', keyControlsKeyDown);
document.removeEventListener('keyup', keyControlsKeyUp);
cancelAnimationFrame(animateId.current);
};
}, []);
useEffect(() => {
canvasInstance.activate(activatedStateID);
}, [activatedStateID]);
useEffect(() => {
canvasInstance.configure({ resetZoom });
}, [resetZoom]);
const updateShapesView = (): void => {
(canvasInstance as Canvas3d).configureShapes({
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
});
};
const onContextMenu = (event: any): void => {
const { onUpdateContextMenu, onActivateObject } = props;
onActivateObject(event.detail.clientID);
onUpdateContextMenu(
event.detail.clientID !== null,
event.detail.clientX,
event.detail.clientY,
ContextMenuType.CANVAS_SHAPE,
);
};
const onCanvasObjectsGroupped = (event: any): void => {
const { onGroupAnnotations, onGroupObjects } = props;
onGroupObjects(false);
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
};
useEffect(() => {
updateShapesView();
}, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
updateCanvas();
canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
};
}, [frameData, annotations, activeLabelID, contextMenuVisibility]);
return <>>;
});
export default connect(mapStateToProps, mapDispatchToProps)(Canvas3DWrapperComponent);