You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
587 lines
23 KiB
TypeScript
587 lines
23 KiB
TypeScript
// Copyright (C) 2021-2022 Intel Corporation
|
|
// Copyright (C) 2022 CVAT.ai Corporation
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import React, {
|
|
ReactElement, SyntheticEvent, useEffect, useReducer, useRef,
|
|
} from 'react';
|
|
import Layout from 'antd/lib/layout/layout';
|
|
import {
|
|
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
|
|
} from '@ant-design/icons';
|
|
import { ResizableBox } from 'react-resizable';
|
|
import {
|
|
ColorBy, ContextMenuType, ObjectType, Workspace,
|
|
} from 'reducers';
|
|
import {
|
|
CameraAction, Canvas3d, ViewType, ViewsDOM,
|
|
} from 'cvat-canvas3d-wrapper';
|
|
import { Canvas } from 'cvat-canvas-wrapper';
|
|
import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image';
|
|
import CVATTooltip from 'components/common/cvat-tooltip';
|
|
import { LogType } from 'cvat-logger';
|
|
import { getCore } from 'cvat-core-wrapper';
|
|
|
|
const cvat = getCore();
|
|
|
|
interface Props {
|
|
opacity: number;
|
|
selectedOpacity: number;
|
|
outlined: boolean;
|
|
outlineColor: string;
|
|
colorBy: ColorBy;
|
|
frameFetching: boolean;
|
|
canvasInstance: Canvas3d | Canvas;
|
|
jobInstance: any;
|
|
frameData: any;
|
|
annotations: any[];
|
|
contextMenuVisibility: boolean;
|
|
activeLabelID: number;
|
|
activeObjectType: ObjectType;
|
|
activatedStateID: number | null;
|
|
onSetupCanvas: () => void;
|
|
onGroupObjects: (enabled: boolean) => void;
|
|
onResetCanvas(): void;
|
|
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
|
|
onActivateObject(activatedStateID: number | null): void;
|
|
onUpdateAnnotations(states: any[]): void;
|
|
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
|
|
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
|
|
onEditShape: (enabled: boolean) => void;
|
|
onDragCanvas: (enabled: boolean) => void;
|
|
onShapeDrawn: () => void;
|
|
workspace: Workspace;
|
|
frame: number;
|
|
resetZoom: boolean;
|
|
}
|
|
|
|
interface ViewSize {
|
|
fullHeight: number;
|
|
fullWidth: number;
|
|
vertical: number;
|
|
top: number;
|
|
side: number;
|
|
front: number;
|
|
}
|
|
|
|
function viewSizeReducer(
|
|
state: ViewSize,
|
|
action: { type: ViewType | 'set' | 'resize'; e?: SyntheticEvent; data?: ViewSize },
|
|
): ViewSize {
|
|
const event = (action.e as unknown) as MouseEvent;
|
|
const canvas3dContainer = document.getElementById('canvas3d-container');
|
|
if (canvas3dContainer) {
|
|
switch (action.type) {
|
|
case ViewType.TOP: {
|
|
const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
|
|
const topWidth = state.top;
|
|
if (topWidth < width) {
|
|
const top = state.top + (width - topWidth);
|
|
const side = state.side - (width - topWidth);
|
|
return {
|
|
...state,
|
|
top,
|
|
side,
|
|
};
|
|
}
|
|
const top = state.top - (topWidth - width);
|
|
const side = state.side + (topWidth - width);
|
|
return {
|
|
...state,
|
|
top,
|
|
side,
|
|
};
|
|
}
|
|
case ViewType.SIDE: {
|
|
const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
|
|
const topSideWidth = state.top + state.side;
|
|
if (topSideWidth < width) {
|
|
const side = state.side + (width - topSideWidth);
|
|
const front = state.front - (width - topSideWidth);
|
|
return {
|
|
...state,
|
|
side,
|
|
front,
|
|
};
|
|
}
|
|
const side = state.side - (topSideWidth - width);
|
|
const front = state.front + (topSideWidth - width);
|
|
return {
|
|
...state,
|
|
side,
|
|
front,
|
|
};
|
|
}
|
|
case ViewType.PERSPECTIVE:
|
|
return {
|
|
...state,
|
|
vertical: event.clientY - canvas3dContainer.getBoundingClientRect().top,
|
|
};
|
|
case 'set':
|
|
return action.data as ViewSize;
|
|
case 'resize': {
|
|
const canvasPerspectiveContainer = document.getElementById('cvat-canvas3d-perspective');
|
|
let midState = { ...state };
|
|
if (canvasPerspectiveContainer) {
|
|
if (state.fullHeight !== canvas3dContainer.clientHeight) {
|
|
const diff = canvas3dContainer.clientHeight - state.fullHeight;
|
|
midState = {
|
|
...midState,
|
|
fullHeight: canvas3dContainer.clientHeight,
|
|
vertical: state.vertical + diff,
|
|
};
|
|
}
|
|
if (state.fullWidth !== canvasPerspectiveContainer.clientWidth) {
|
|
const oldWidth = state.fullWidth;
|
|
const width = canvasPerspectiveContainer.clientWidth;
|
|
midState = {
|
|
...midState,
|
|
fullWidth: width,
|
|
top: (state.top / oldWidth) * width,
|
|
side: (state.side / oldWidth) * width,
|
|
front: (state.front / oldWidth) * width,
|
|
};
|
|
}
|
|
return midState;
|
|
}
|
|
return state;
|
|
}
|
|
default:
|
|
throw new Error();
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
const CanvasWrapperComponent = (props: Props): ReactElement => {
|
|
const animateId = useRef(0);
|
|
const [viewSize, setViewSize] = useReducer(viewSizeReducer, {
|
|
fullHeight: 0,
|
|
fullWidth: 0,
|
|
vertical: 0,
|
|
top: 0,
|
|
side: 0,
|
|
front: 0,
|
|
});
|
|
const perspectiveView = useRef<HTMLDivElement | null>(null);
|
|
const topView = useRef<HTMLDivElement | null>(null);
|
|
const sideView = useRef<HTMLDivElement | null>(null);
|
|
const frontView = useRef<HTMLDivElement | null>(null);
|
|
|
|
const {
|
|
opacity,
|
|
outlined,
|
|
outlineColor,
|
|
selectedOpacity,
|
|
colorBy,
|
|
contextMenuVisibility,
|
|
frameData,
|
|
onResetCanvas,
|
|
onSetupCanvas,
|
|
annotations,
|
|
frame,
|
|
jobInstance,
|
|
activeLabelID,
|
|
activatedStateID,
|
|
resetZoom,
|
|
activeObjectType,
|
|
onShapeDrawn,
|
|
onCreateAnnotations,
|
|
frameFetching,
|
|
} = 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();
|
|
if (
|
|
perspectiveView &&
|
|
perspectiveView.current &&
|
|
topView &&
|
|
topView.current &&
|
|
sideView &&
|
|
sideView.current &&
|
|
frontView &&
|
|
frontView.current
|
|
) {
|
|
perspectiveView.current.appendChild(canvasInstanceDOM.perspective);
|
|
topView.current.appendChild(canvasInstanceDOM.top);
|
|
sideView.current.appendChild(canvasInstanceDOM.side);
|
|
frontView.current.appendChild(canvasInstanceDOM.front);
|
|
const canvas3dContainer = document.getElementById('canvas3d-container');
|
|
if (canvas3dContainer) {
|
|
const width = canvas3dContainer.clientWidth / 3;
|
|
setViewSize({
|
|
type: 'set',
|
|
data: {
|
|
fullHeight: canvas3dContainer.clientHeight,
|
|
fullWidth: canvas3dContainer.clientWidth,
|
|
vertical: canvas3dContainer.clientHeight / 2,
|
|
top: width,
|
|
side: width,
|
|
front: width,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
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 onResize = (): void => {
|
|
setViewSize({
|
|
type: 'resize',
|
|
});
|
|
};
|
|
|
|
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.fit', onResize);
|
|
canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
|
|
window.addEventListener('resize', onResize);
|
|
|
|
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.fit', onResize);
|
|
canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
|
|
window.removeEventListener('resize', onResize);
|
|
};
|
|
}, [frameData, annotations, activeLabelID, contextMenuVisibility]);
|
|
|
|
const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
|
|
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
|
|
};
|
|
|
|
const ArrowGroup = (): ReactElement => (
|
|
<span className='cvat-canvas3d-perspective-arrow-directions'>
|
|
<CVATTooltip title='Shift+Arrow Up' placement='topRight'>
|
|
<button
|
|
data-cy='arrow-up'
|
|
onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-arrow-directions-icons-up'
|
|
>
|
|
<ArrowUpOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
|
|
</button>
|
|
</CVATTooltip>
|
|
<br />
|
|
<CVATTooltip title='Shift+Arrow Left' placement='topRight'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
|
|
>
|
|
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
|
|
>
|
|
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Shift+Arrow Right' placement='topRight'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
|
|
>
|
|
<ArrowRightOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
|
|
</button>
|
|
</CVATTooltip>
|
|
</span>
|
|
);
|
|
|
|
const ControlGroup = (): ReactElement => (
|
|
<span className='cvat-canvas3d-perspective-directions'>
|
|
<CVATTooltip title='Alt+U' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
U
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Alt+I' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
I
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Alt+O' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
O
|
|
</button>
|
|
</CVATTooltip>
|
|
<br />
|
|
<CVATTooltip title='Alt+J' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
J
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Alt+K' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
K
|
|
</button>
|
|
</CVATTooltip>
|
|
<CVATTooltip title='Alt+L' placement='topLeft'>
|
|
<button
|
|
onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
|
|
type='button'
|
|
className='cvat-canvas3d-perspective-directions-icon'
|
|
>
|
|
L
|
|
</button>
|
|
</CVATTooltip>
|
|
</span>
|
|
);
|
|
|
|
return (
|
|
<Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
|
|
<ContextImage />
|
|
<ResizableBox
|
|
className='cvat-resizable'
|
|
width={Infinity}
|
|
height={viewSize.vertical}
|
|
axis='y'
|
|
handle={<span className='cvat-resizable-handle-horizontal' />}
|
|
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })}
|
|
>
|
|
<>
|
|
{frameFetching ? (
|
|
<svg id='cvat_canvas_loading_animation'>
|
|
<circle id='cvat_canvas_loading_circle' r='30' cx='50%' cy='50%' />
|
|
</svg>
|
|
) : null}
|
|
<div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'>
|
|
<div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} />
|
|
<ArrowGroup />
|
|
<ControlGroup />
|
|
</div>
|
|
</>
|
|
</ResizableBox>
|
|
<div
|
|
className='cvat-canvas3d-orthographic-views'
|
|
style={{ height: viewSize.fullHeight - viewSize.vertical }}
|
|
>
|
|
<ResizableBox
|
|
className='cvat-resizable'
|
|
width={viewSize.top}
|
|
height={viewSize.fullHeight - viewSize.vertical}
|
|
axis='x'
|
|
handle={<span className='cvat-resizable-handle-vertical-top' />}
|
|
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })}
|
|
>
|
|
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-topview'>
|
|
<div className='cvat-canvas3d-header'>TOP</div>
|
|
<div className='cvat-canvas3d-fullsize' ref={topView} />
|
|
</div>
|
|
</ResizableBox>
|
|
<ResizableBox
|
|
className='cvat-resizable'
|
|
width={viewSize.side}
|
|
height={viewSize.fullHeight - viewSize.vertical}
|
|
axis='x'
|
|
handle={<span className='cvat-resizable-handle-vertical-side' />}
|
|
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })}
|
|
>
|
|
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-sideview'>
|
|
<div className='cvat-canvas3d-header'>SIDE</div>
|
|
<div className='cvat-canvas3d-fullsize' ref={sideView} />
|
|
</div>
|
|
</ResizableBox>
|
|
<div
|
|
className='cvat-canvas3d-orthographic-view cvat-canvas3d-frontview'
|
|
style={{ width: viewSize.front, height: viewSize.fullHeight - viewSize.vertical }}
|
|
>
|
|
<div className='cvat-canvas3d-header'>FRONT</div>
|
|
<div className='cvat-canvas3d-fullsize' ref={frontView} />
|
|
</div>
|
|
</div>
|
|
</Layout.Content>
|
|
);
|
|
};
|
|
|
|
export default React.memo(CanvasWrapperComponent);
|