// 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(null); const topView = useRef(null); const sideView = useRef(null); const frontView = useRef(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 => (
); const ControlGroup = (): ReactElement => (
); return ( } onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })} > <> {frameFetching ? ( ) : null}
} onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })} >
TOP
} onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })} >
SIDE
FRONT
); }; export default React.memo(CanvasWrapperComponent);