Grid view and multiple context images supported (#5542)
### Motivation and context <img width="1918" alt="image" src="https://user-images.githubusercontent.com/40690378/210207552-7a7dcb0b-4f0c-4cb6-a030-9522ff68a710.png"> <img width="1920" alt="image" src="https://user-images.githubusercontent.com/40690378/210207577-d05503e8-71d5-4e5c-aecd-03e5a762d7b1.png">main
parent
bc33ba430c
commit
fb0b8675e1
@ -1,586 +0,0 @@
|
|||||||
// 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);
|
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
export interface ItemLayout {
|
||||||
|
viewType: ViewType;
|
||||||
|
offset: number[];
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
viewIndex?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ViewType {
|
||||||
|
CANVAS = 'canvas',
|
||||||
|
CANVAS_3D = 'canvas3D',
|
||||||
|
CANVAS_3D_TOP = 'canvas3DTop',
|
||||||
|
CANVAS_3D_SIDE = 'canvas3DSide',
|
||||||
|
CANVAS_3D_FRONT = 'canvas3DFront',
|
||||||
|
RELATED_IMAGE = 'relatedImage',
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLayout: {
|
||||||
|
'2D': {
|
||||||
|
[index: string]: ItemLayout[];
|
||||||
|
};
|
||||||
|
'3D': {
|
||||||
|
[index: string]: ItemLayout[];
|
||||||
|
};
|
||||||
|
} = { '2D': {}, '3D': {} };
|
||||||
|
|
||||||
|
defaultLayout['2D']['0'] = [{
|
||||||
|
viewType: ViewType.CANVAS,
|
||||||
|
offset: [0],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 12,
|
||||||
|
h: 12,
|
||||||
|
}];
|
||||||
|
|
||||||
|
defaultLayout['2D']['1'] = [
|
||||||
|
{ ...defaultLayout['2D']['0'][0], w: 9 }, {
|
||||||
|
viewType: ViewType.RELATED_IMAGE,
|
||||||
|
offset: [0, 0],
|
||||||
|
x: 9,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 4,
|
||||||
|
viewIndex: '0',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultLayout['2D']['2'] = [
|
||||||
|
...defaultLayout['2D']['1'], {
|
||||||
|
...defaultLayout['2D']['1'][1],
|
||||||
|
viewType: ViewType.RELATED_IMAGE,
|
||||||
|
viewIndex: '1',
|
||||||
|
offset: [0, 1],
|
||||||
|
y: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultLayout['2D']['3'] = [
|
||||||
|
...defaultLayout['2D']['2'], {
|
||||||
|
...defaultLayout['2D']['2'][2],
|
||||||
|
viewIndex: '2',
|
||||||
|
offset: [0, 2],
|
||||||
|
y: 8,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultLayout['3D']['0'] = [{
|
||||||
|
viewType: ViewType.CANVAS_3D,
|
||||||
|
offset: [0],
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 12,
|
||||||
|
h: 9,
|
||||||
|
}, {
|
||||||
|
viewType: ViewType.CANVAS_3D_TOP,
|
||||||
|
offset: [0],
|
||||||
|
x: 0,
|
||||||
|
y: 9,
|
||||||
|
w: 4,
|
||||||
|
h: 3,
|
||||||
|
}, {
|
||||||
|
viewType: ViewType.CANVAS_3D_SIDE,
|
||||||
|
offset: [0],
|
||||||
|
x: 4,
|
||||||
|
y: 9,
|
||||||
|
w: 4,
|
||||||
|
h: 3,
|
||||||
|
}, {
|
||||||
|
viewType: ViewType.CANVAS_3D_FRONT,
|
||||||
|
offset: [0],
|
||||||
|
x: 8,
|
||||||
|
y: 9,
|
||||||
|
w: 4,
|
||||||
|
h: 3,
|
||||||
|
}];
|
||||||
|
|
||||||
|
defaultLayout['3D']['1'] = [
|
||||||
|
{ ...defaultLayout['3D']['0'][0], w: 9 },
|
||||||
|
{ ...defaultLayout['3D']['0'][1], w: 3 },
|
||||||
|
{ ...defaultLayout['3D']['0'][2], x: 3, w: 3 },
|
||||||
|
{ ...defaultLayout['3D']['0'][3], x: 6, w: 3 },
|
||||||
|
{
|
||||||
|
viewType: ViewType.RELATED_IMAGE,
|
||||||
|
offset: [0, 0],
|
||||||
|
x: 9,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 4,
|
||||||
|
viewIndex: '0',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultLayout['3D']['2'] = [
|
||||||
|
...defaultLayout['3D']['1'],
|
||||||
|
{
|
||||||
|
...defaultLayout['3D']['1'][4],
|
||||||
|
viewIndex: '1',
|
||||||
|
offset: [0, 1],
|
||||||
|
y: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultLayout['3D']['3'] = [
|
||||||
|
...defaultLayout['3D']['2'],
|
||||||
|
{
|
||||||
|
...defaultLayout['3D']['2'][5],
|
||||||
|
viewIndex: '2',
|
||||||
|
offset: [0, 2],
|
||||||
|
y: 8,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defaultLayout;
|
||||||
@ -0,0 +1,365 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
import 'react-grid-layout/css/styles.css';
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import RGL, { WidthProvider } from 'react-grid-layout';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import Layout from 'antd/lib/layout';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
DragOutlined,
|
||||||
|
FullscreenExitOutlined,
|
||||||
|
FullscreenOutlined,
|
||||||
|
PicCenterOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
import consts from 'consts';
|
||||||
|
import { DimensionType, CombinedState } from 'reducers';
|
||||||
|
import CanvasWrapperComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-wrapper';
|
||||||
|
import CanvasWrapper3DComponent, {
|
||||||
|
PerspectiveViewComponent,
|
||||||
|
TopViewComponent,
|
||||||
|
SideViewComponent,
|
||||||
|
FrontViewComponent,
|
||||||
|
} from 'components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D';
|
||||||
|
import ContextImage from 'components/annotation-page/canvas/views/context-image/context-image';
|
||||||
|
import CVATTooltip from 'components/common/cvat-tooltip';
|
||||||
|
import defaultLayout, { ItemLayout, ViewType } from './canvas-layout.conf';
|
||||||
|
|
||||||
|
const ReactGridLayout = WidthProvider(RGL);
|
||||||
|
|
||||||
|
const ViewFabric = (itemLayout: ItemLayout): JSX.Element => {
|
||||||
|
const { viewType: type, offset } = itemLayout;
|
||||||
|
|
||||||
|
let component = null;
|
||||||
|
switch (type) {
|
||||||
|
case ViewType.CANVAS:
|
||||||
|
component = <CanvasWrapperComponent />;
|
||||||
|
break;
|
||||||
|
case ViewType.CANVAS_3D:
|
||||||
|
component = <PerspectiveViewComponent />;
|
||||||
|
break;
|
||||||
|
case ViewType.RELATED_IMAGE:
|
||||||
|
component = <ContextImage offset={offset} />;
|
||||||
|
break;
|
||||||
|
case ViewType.CANVAS_3D_FRONT:
|
||||||
|
component = <FrontViewComponent />;
|
||||||
|
break;
|
||||||
|
case ViewType.CANVAS_3D_SIDE:
|
||||||
|
component = <SideViewComponent />;
|
||||||
|
break;
|
||||||
|
case ViewType.CANVAS_3D_TOP:
|
||||||
|
component = <TopViewComponent />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
component = <div> Undefined view </div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitLayout = (type: DimensionType, layoutConfig: ItemLayout[]): ItemLayout[] => {
|
||||||
|
const updatedLayout: ItemLayout[] = [];
|
||||||
|
|
||||||
|
const relatedViews = layoutConfig
|
||||||
|
.filter((item: ItemLayout) => item.viewType === ViewType.RELATED_IMAGE);
|
||||||
|
const relatedViewsCols = relatedViews.length > 6 ? 2 : 1;
|
||||||
|
const height = Math.floor(consts.CANVAS_WORKSPACE_ROWS / (relatedViews.length / relatedViewsCols));
|
||||||
|
relatedViews.forEach((view: ItemLayout, i: number) => {
|
||||||
|
updatedLayout.push({
|
||||||
|
...view,
|
||||||
|
h: height,
|
||||||
|
w: relatedViews.length > 6 ? 2 : 3,
|
||||||
|
x: relatedViewsCols === 1 ? 9 : 8 + (i % 2) * 2,
|
||||||
|
y: height * i,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let widthAvail = consts.CANVAS_WORKSPACE_COLS;
|
||||||
|
if (updatedLayout.length > 0) {
|
||||||
|
widthAvail -= updatedLayout[0].w * relatedViewsCols;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === DimensionType.DIM_2D) {
|
||||||
|
const canvas = layoutConfig
|
||||||
|
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS) as ItemLayout;
|
||||||
|
updatedLayout.push({
|
||||||
|
...canvas,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: widthAvail,
|
||||||
|
h: consts.CANVAS_WORKSPACE_ROWS,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const canvas = layoutConfig
|
||||||
|
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D) as ItemLayout;
|
||||||
|
const top = layoutConfig
|
||||||
|
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_TOP) as ItemLayout;
|
||||||
|
const side = layoutConfig
|
||||||
|
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_SIDE) as ItemLayout;
|
||||||
|
const front = layoutConfig
|
||||||
|
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_FRONT) as ItemLayout;
|
||||||
|
const helpfulCanvasViewHeight = 3;
|
||||||
|
updatedLayout.push({
|
||||||
|
...canvas,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: widthAvail,
|
||||||
|
h: consts.CANVAS_WORKSPACE_ROWS - helpfulCanvasViewHeight,
|
||||||
|
}, {
|
||||||
|
...top,
|
||||||
|
x: 0,
|
||||||
|
y: consts.CANVAS_WORKSPACE_ROWS,
|
||||||
|
w: Math.ceil(widthAvail / 3),
|
||||||
|
h: helpfulCanvasViewHeight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...side,
|
||||||
|
x: Math.ceil(widthAvail / 3),
|
||||||
|
y: consts.CANVAS_WORKSPACE_ROWS,
|
||||||
|
w: Math.ceil(widthAvail / 3),
|
||||||
|
h: helpfulCanvasViewHeight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...front,
|
||||||
|
x: Math.ceil(widthAvail / 3) * 2,
|
||||||
|
y: consts.CANVAS_WORKSPACE_ROWS,
|
||||||
|
w: Math.floor(widthAvail / 3),
|
||||||
|
h: helpfulCanvasViewHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedLayout;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CanvasLayout({ type }: { type?: DimensionType }): JSX.Element {
|
||||||
|
const relatedFiles = useSelector((state: CombinedState) => state.annotation.player.frame.relatedFiles);
|
||||||
|
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
|
||||||
|
const canvasBackgroundColor = useSelector((state: CombinedState) => state.settings.player.canvasBackgroundColor);
|
||||||
|
|
||||||
|
const computeRowHeight = (): number => {
|
||||||
|
const container = window.document.getElementsByClassName('cvat-annotation-header')[0];
|
||||||
|
let containerHeight = window.innerHeight;
|
||||||
|
if (container) {
|
||||||
|
containerHeight = window.innerHeight - container.getBoundingClientRect().bottom;
|
||||||
|
// https://github.com/react-grid-layout/react-grid-layout/issues/628#issuecomment-1228453084
|
||||||
|
return Math.floor(
|
||||||
|
(containerHeight - consts.CANVAS_WORKSPACE_MARGIN * (consts.CANVAS_WORKSPACE_ROWS)) /
|
||||||
|
consts.CANVAS_WORKSPACE_ROWS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLayout = useCallback(() => (
|
||||||
|
defaultLayout[(type as DimensionType).toUpperCase() as '2D' | '3D'][Math.min(relatedFiles, 3)]
|
||||||
|
), [type, relatedFiles]);
|
||||||
|
|
||||||
|
const [layoutConfig, setLayoutConfig] = useState<ItemLayout[]>(getLayout());
|
||||||
|
const [rowHeight, setRowHeight] = useState<number>(Math.floor(computeRowHeight()));
|
||||||
|
const [fullscreenKey, setFullscreenKey] = useState<string>('');
|
||||||
|
|
||||||
|
const fitCanvas = useCallback(() => {
|
||||||
|
if (canvasInstance) {
|
||||||
|
canvasInstance.fitCanvas();
|
||||||
|
canvasInstance.fit();
|
||||||
|
}
|
||||||
|
}, [canvasInstance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onResize = (): void => {
|
||||||
|
setRowHeight(computeRowHeight());
|
||||||
|
fitCanvas();
|
||||||
|
const [el] = window.document.getElementsByClassName('cvat-canvas-grid-root');
|
||||||
|
if (el) {
|
||||||
|
el.addEventListener('transitionend', () => {
|
||||||
|
fitCanvas();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, [fitCanvas]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRowHeight(computeRowHeight());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}, [layoutConfig]);
|
||||||
|
|
||||||
|
const children = layoutConfig.map((value: ItemLayout) => ViewFabric(value));
|
||||||
|
const layout = layoutConfig.map((value: ItemLayout) => ({
|
||||||
|
x: value.x,
|
||||||
|
y: value.y,
|
||||||
|
w: value.w,
|
||||||
|
h: value.h,
|
||||||
|
i: typeof (value.viewIndex) !== 'undefined' ? `${value.viewType}_${value.viewIndex}` : `${value.viewType}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Content>
|
||||||
|
{ !!rowHeight && (
|
||||||
|
<ReactGridLayout
|
||||||
|
cols={consts.CANVAS_WORKSPACE_COLS}
|
||||||
|
maxRows={consts.CANVAS_WORKSPACE_ROWS}
|
||||||
|
style={{ background: canvasBackgroundColor }}
|
||||||
|
containerPadding={[consts.CANVAS_WORKSPACE_PADDING, consts.CANVAS_WORKSPACE_PADDING]}
|
||||||
|
margin={[consts.CANVAS_WORKSPACE_MARGIN, consts.CANVAS_WORKSPACE_MARGIN]}
|
||||||
|
className='cvat-canvas-grid-root'
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
layout={layout}
|
||||||
|
onLayoutChange={(updatedLayout: RGL.Layout[]) => {
|
||||||
|
const transformedLayout = layoutConfig.map((itemLayout: ItemLayout, i: number): ItemLayout => ({
|
||||||
|
...itemLayout,
|
||||||
|
x: updatedLayout[i].x,
|
||||||
|
y: updatedLayout[i].y,
|
||||||
|
w: updatedLayout[i].w,
|
||||||
|
h: updatedLayout[i].h,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!isEqual(layoutConfig, transformedLayout)) {
|
||||||
|
setLayoutConfig(transformedLayout);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
resizeHandle={(_: any, ref: React.MutableRefObject<HTMLDivElement>) => (
|
||||||
|
<div ref={ref} className='cvat-grid-item-resize-handler react-resizable-handle' />
|
||||||
|
)}
|
||||||
|
draggableHandle='.cvat-grid-item-drag-handler'
|
||||||
|
>
|
||||||
|
{ children.map((child: JSX.Element, idx: number): JSX.Element => {
|
||||||
|
const { viewType, viewIndex } = layoutConfig[idx];
|
||||||
|
const key = typeof viewIndex !== 'undefined' ? `${viewType}_${viewIndex}` : `${viewType}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={fullscreenKey === key ? { backgroundColor: canvasBackgroundColor } : {}}
|
||||||
|
className={fullscreenKey === key ?
|
||||||
|
'cvat-canvas-grid-item cvat-canvas-grid-fullscreen-item' :
|
||||||
|
'cvat-canvas-grid-item'}
|
||||||
|
key={key}
|
||||||
|
>
|
||||||
|
<DragOutlined className='cvat-grid-item-drag-handler' />
|
||||||
|
<CloseOutlined
|
||||||
|
className='cvat-grid-item-close-button'
|
||||||
|
style={{
|
||||||
|
pointerEvents: viewType !== ViewType.RELATED_IMAGE ? 'none' : undefined,
|
||||||
|
opacity: viewType !== ViewType.RELATED_IMAGE ? 0.2 : undefined,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (viewType === ViewType.RELATED_IMAGE) {
|
||||||
|
setLayoutConfig(
|
||||||
|
layoutConfig
|
||||||
|
.filter((item: ItemLayout) => !(
|
||||||
|
item.viewType === viewType && item.viewIndex === viewIndex
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{fullscreenKey === key ? (
|
||||||
|
<FullscreenExitOutlined
|
||||||
|
className='cvat-grid-item-fullscreen-handler'
|
||||||
|
onClick={() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
setFullscreenKey('');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FullscreenOutlined
|
||||||
|
className='cvat-grid-item-fullscreen-handler'
|
||||||
|
onClick={() => {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
setFullscreenKey(key);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ child }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}) }
|
||||||
|
</ReactGridLayout>
|
||||||
|
)}
|
||||||
|
{ type === DimensionType.DIM_3D && <CanvasWrapper3DComponent /> }
|
||||||
|
<div className='cvat-grid-layout-common-setups'>
|
||||||
|
<CVATTooltip title='Fit views'>
|
||||||
|
<PicCenterOutlined
|
||||||
|
onClick={() => {
|
||||||
|
setLayoutConfig(fitLayout(type as DimensionType, layoutConfig));
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CVATTooltip>
|
||||||
|
<CVATTooltip title='Add context image'>
|
||||||
|
<PlusOutlined
|
||||||
|
style={{
|
||||||
|
pointerEvents: !relatedFiles ? 'none' : undefined,
|
||||||
|
opacity: !relatedFiles ? 0.2 : undefined,
|
||||||
|
}}
|
||||||
|
disabled={!!relatedFiles}
|
||||||
|
onClick={() => {
|
||||||
|
const MAXIMUM_RELATED = 12;
|
||||||
|
const existingRelated = layoutConfig
|
||||||
|
.filter((configItem: ItemLayout) => configItem.viewType === ViewType.RELATED_IMAGE);
|
||||||
|
|
||||||
|
if (existingRelated.length >= MAXIMUM_RELATED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingRelated.length === 0) {
|
||||||
|
setLayoutConfig(defaultLayout[type?.toUpperCase() as '2D' | '3D']['1']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewIndexes = existingRelated
|
||||||
|
.map((item: ItemLayout) => +(item.viewIndex as string)).sort();
|
||||||
|
const max = Math.max(...viewIndexes);
|
||||||
|
let viewIndex = max + 1;
|
||||||
|
for (let i = 0; i < max + 1; i++) {
|
||||||
|
if (!viewIndexes.includes(i)) {
|
||||||
|
viewIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = existingRelated[existingRelated.length - 1];
|
||||||
|
const copy = { ...latest, offset: [0, viewIndex], viewIndex: `${viewIndex}` };
|
||||||
|
setLayoutConfig(fitLayout(type as DimensionType, [...layoutConfig, copy]));
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CVATTooltip>
|
||||||
|
<CVATTooltip title='Reload layout'>
|
||||||
|
<ReloadOutlined onClick={() => {
|
||||||
|
setLayoutConfig([...getLayout()]);
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CVATTooltip>
|
||||||
|
</div>
|
||||||
|
</Layout.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CanvasLayout.defaultProps = {
|
||||||
|
type: DimensionType.DIM_2D,
|
||||||
|
};
|
||||||
|
|
||||||
|
CanvasLayout.PropType = {
|
||||||
|
type: PropTypes.oneOf(Object.values(DimensionType)),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(CanvasLayout);
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@import 'base.scss';
|
||||||
|
|
||||||
|
.cvat-canvas-grid-root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-grid-layout-common-setups {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 50%;
|
||||||
|
transform: translate(0, calc($grid-unit-size * 12 - 1px));
|
||||||
|
z-index: 1000;
|
||||||
|
background: $background-color-2;
|
||||||
|
line-height: $grid-unit-size * 3;
|
||||||
|
height: calc($grid-unit-size * 3 + 1px);
|
||||||
|
padding-bottom: $grid-unit-size;
|
||||||
|
padding-right: $grid-unit-size;
|
||||||
|
padding-left: $grid-unit-size;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border-bottom: 1px solid $border-color-1;
|
||||||
|
border-right: 1px solid $border-color-1;
|
||||||
|
border-left: 1px solid $border-color-1;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-right: $grid-unit-size * 2;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-canvas-grid-item {
|
||||||
|
background-color: rgba(241, 241, 241, 0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.react-grid-item.cssTransforms {
|
||||||
|
transition-property: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cvat-canvas-grid-fullscreen-item {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
padding-right: $grid-unit-size;
|
||||||
|
transform: translate(4px, 4px) !important;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.cvat-grid-item-resize-handler.react-resizable-handle,
|
||||||
|
.cvat-grid-item-drag-handler {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-grid-item-drag-handler,
|
||||||
|
.cvat-grid-item-fullscreen-handler,
|
||||||
|
.cvat-grid-item-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: $grid-unit-size;
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 16px;
|
||||||
|
background: $header-color;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 200ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cvat-grid-item-drag-handler {
|
||||||
|
left: $grid-unit-size * 4;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cvat-grid-item-fullscreen-handler {
|
||||||
|
left: $grid-unit-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cvat-grid-item-close-button {
|
||||||
|
right: $grid-unit-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-grid-item-resize-handler.react-resizable-handle {
|
||||||
|
bottom: -3px;
|
||||||
|
right: -3px;
|
||||||
|
cursor: se-resize;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 9px;
|
||||||
|
height: 10px;
|
||||||
|
border-right: 2px solid rgba(0, 0, 0, 1);
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
// Copyright (C) 2022 CVAT.ai Corporation
|
// Copyright (C) 2022-2023 CVAT.ai Corporation
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
@import '../../../base.scss';
|
@import 'base.scss';
|
||||||
|
|
||||||
.cvat-brush-tools-toolbox {
|
.cvat-brush-tools-toolbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -0,0 +1,593 @@
|
|||||||
|
// 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(() => (
|
||||||
|
<div className='cvat-spinner-container'>
|
||||||
|
<Spin className='cvat-spinner' />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const PerspectiveViewComponent = React.memo(
|
||||||
|
(): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement>(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 => (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.appendChild(canvas.html().perspective);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cvat-canvas3d-perspective'>
|
||||||
|
{ !canvasIsReady && <Spinner /> }
|
||||||
|
<div
|
||||||
|
className='cvat-canvas-container cvat-canvas-container-overflow'
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
<ArrowGroup />
|
||||||
|
<ControlGroup />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TopViewComponent = React.memo(
|
||||||
|
(): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement>(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 (
|
||||||
|
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-topview'>
|
||||||
|
{ !canvasIsReady && <Spinner /> }
|
||||||
|
<div className='cvat-canvas3d-header'>Top</div>
|
||||||
|
<div
|
||||||
|
className='cvat-canvas3d-fullsize'
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SideViewComponent = React.memo(
|
||||||
|
(): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement>(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 (
|
||||||
|
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-sideview'>
|
||||||
|
{ !canvasIsReady && <Spinner /> }
|
||||||
|
<div className='cvat-canvas3d-header'>Side</div>
|
||||||
|
<div
|
||||||
|
className='cvat-canvas3d-fullsize'
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FrontViewComponent = React.memo(
|
||||||
|
(): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement>(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 (
|
||||||
|
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-frontview'>
|
||||||
|
{ !canvasIsReady && <Spinner /> }
|
||||||
|
<div className='cvat-canvas3d-header'>Front</div>
|
||||||
|
<div
|
||||||
|
className='cvat-canvas3d-fullsize'
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
images: Record<string, ImageBitmap>;
|
||||||
|
offset: number;
|
||||||
|
onChangeOffset: (offset: number) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CanvasWithRef({
|
||||||
|
image, isActive, onClick, name,
|
||||||
|
}: { image: ImageBitmap, name: string, isActive: boolean, onClick: () => void }): JSX.Element {
|
||||||
|
const ref = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
if (ref.current) {
|
||||||
|
const context = ref.current.getContext('2d');
|
||||||
|
if (context) {
|
||||||
|
ref.current.width = image.width;
|
||||||
|
ref.current.height = image.height;
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [image, ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={(isActive ? ['cvat-context-image-gallery-item cvat-context-image-gallery-item-current'] : ['cvat-context-image-gallery-item']).join(' ')}>
|
||||||
|
<Text strong className='cvat-context-image-gallery-item-name'>{name}</Text>
|
||||||
|
<canvas
|
||||||
|
ref={ref}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextImageSelector(props: Props): React.ReactPortal {
|
||||||
|
const {
|
||||||
|
images, offset, onChangeOffset, onClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const keys = Object.keys(images).sort();
|
||||||
|
|
||||||
|
return ReactDOM.createPortal((
|
||||||
|
<div className='cvat-context-image-overlay'>
|
||||||
|
<div className='cvat-context-image-gallery'>
|
||||||
|
<div className='cvat-context-image-gallery-header'>
|
||||||
|
<Text>
|
||||||
|
Click the image to display it as a context image
|
||||||
|
</Text>
|
||||||
|
<CloseOutlined className='cvat-context-image-close-button' onClick={onClose} />
|
||||||
|
</div>
|
||||||
|
<div className='cvat-context-image-gallery-items'>
|
||||||
|
{ keys.map((key, i: number) => (
|
||||||
|
<CanvasWithRef
|
||||||
|
name={key}
|
||||||
|
image={images[key]}
|
||||||
|
isActive={offset === i}
|
||||||
|
onClick={() => {
|
||||||
|
onChangeOffset(i);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
key={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
), window.document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextImageSelector.PropType = {
|
||||||
|
images: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
offset: PropTypes.number,
|
||||||
|
onChangeOffset: PropTypes.func,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ContextImageSelector);
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import notification from 'antd/lib/notification';
|
||||||
|
import Spin from 'antd/lib/spin';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
import CVATTooltop from 'components/common/cvat-tooltip';
|
||||||
|
import { CombinedState } from 'reducers';
|
||||||
|
import ContextImageSelector from './context-image-selector';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
offset: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextImage(props: Props): JSX.Element {
|
||||||
|
const { offset } = props;
|
||||||
|
const defaultFrameOffset = (offset[0] || 0);
|
||||||
|
const defaultContextImageOffset = (offset[1] || 0);
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const job = useSelector((state: CombinedState) => state.annotation.job.instance);
|
||||||
|
const { number: frame, relatedFiles } = useSelector((state: CombinedState) => state.annotation.player.frame);
|
||||||
|
const frameIndex = frame + defaultFrameOffset;
|
||||||
|
|
||||||
|
const [contextImageData, setContextImageData] = useState<Record<string, ImageBitmap>>({});
|
||||||
|
const [fetching, setFetching] = useState<boolean>(false);
|
||||||
|
const [contextImageOffset, setContextImageOffset] = useState<number>(
|
||||||
|
Math.min(defaultContextImageOffset, relatedFiles),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [hasError, setHasError] = useState<boolean>(false);
|
||||||
|
const [showSelector, setShowSelector] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unmounted = false;
|
||||||
|
const promise = job.frames.contextImage(frameIndex);
|
||||||
|
setFetching(true);
|
||||||
|
promise.then((imageBitmaps: Record<string, ImageBitmap>) => {
|
||||||
|
if (!unmounted) {
|
||||||
|
setContextImageData(imageBitmaps);
|
||||||
|
}
|
||||||
|
}).catch((error: any) => {
|
||||||
|
if (!unmounted) {
|
||||||
|
setHasError(true);
|
||||||
|
notification.error({
|
||||||
|
message: `Could not fetch context images. Frame: ${frameIndex}`,
|
||||||
|
description: error.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
if (!unmounted) {
|
||||||
|
setFetching(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setContextImageData({});
|
||||||
|
unmounted = true;
|
||||||
|
};
|
||||||
|
}, [frameIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const sortedKeys = Object.keys(contextImageData).sort();
|
||||||
|
const key = sortedKeys[contextImageOffset];
|
||||||
|
const image = contextImageData[key];
|
||||||
|
const context = canvasRef.current.getContext('2d');
|
||||||
|
if (context && image) {
|
||||||
|
canvasRef.current.width = image.width;
|
||||||
|
canvasRef.current.height = image.height;
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [contextImageData, contextImageOffset, canvasRef]);
|
||||||
|
|
||||||
|
const contextImageName = Object.keys(contextImageData).sort()[contextImageOffset];
|
||||||
|
return (
|
||||||
|
<div className='cvat-context-image-wrapper'>
|
||||||
|
<div className='cvat-context-image-header'>
|
||||||
|
{ relatedFiles > 1 && (
|
||||||
|
<SettingOutlined
|
||||||
|
className='cvat-context-image-setup-button'
|
||||||
|
onClick={() => {
|
||||||
|
setShowSelector(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='cvat-context-image-title'>
|
||||||
|
<CVATTooltop title={contextImageName}>
|
||||||
|
<Text>{contextImageName}</Text>
|
||||||
|
</CVATTooltop>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ (hasError ||
|
||||||
|
(!fetching && contextImageOffset >= Object.keys(contextImageData).length)) && <Text> No data </Text>}
|
||||||
|
{ fetching && <Spin size='small' /> }
|
||||||
|
{
|
||||||
|
contextImageOffset < Object.keys(contextImageData).length &&
|
||||||
|
<canvas ref={canvasRef} />
|
||||||
|
}
|
||||||
|
{ showSelector && (
|
||||||
|
<ContextImageSelector
|
||||||
|
images={contextImageData}
|
||||||
|
offset={contextImageOffset}
|
||||||
|
onChangeOffset={(newContextImageOffset: number) => {
|
||||||
|
setContextImageOffset(newContextImageOffset);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowSelector(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextImage.PropType = {
|
||||||
|
offset: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ContextImage);
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (C) 2023 CVAT.ai Corporation
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@import 'base.scss';
|
||||||
|
|
||||||
|
.cvat-context-image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> .ant-spin {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .ant-typography {
|
||||||
|
top: 50%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-context-image-header {
|
||||||
|
position: absolute;
|
||||||
|
height: $grid-unit-size * 4;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 1;
|
||||||
|
background: $header-color;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .cvat-context-image-title {
|
||||||
|
width: calc(100% - $grid-unit-size * 13);
|
||||||
|
margin-right: $grid-unit-size * 7;
|
||||||
|
margin-left: $grid-unit-size * 7;
|
||||||
|
|
||||||
|
> span.ant-typography {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: $grid-unit-size * 4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .cvat-context-image-setup-button {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 200ms;
|
||||||
|
position: absolute;
|
||||||
|
top: $grid-unit-size;
|
||||||
|
right: $grid-unit-size * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .cvat-context-image-close-button {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 200ms;
|
||||||
|
position: absolute;
|
||||||
|
top: $grid-unit-size;
|
||||||
|
right: $grid-unit-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
object-fit: contain;
|
||||||
|
position: relative;
|
||||||
|
top: calc(50% + $grid-unit-size * 2);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - $grid-unit-size * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> .cvat-context-image-header > .cvat-context-image-setup-button {
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-context-image-overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
position: absolute;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.cvat-context-image-gallery {
|
||||||
|
width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
padding: $grid-unit-size;
|
||||||
|
display: block;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.cvat-context-image-gallery-items {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.cvat-context-image-gallery-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: $grid-unit-size;
|
||||||
|
opacity: 0.6;
|
||||||
|
width: 25%;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
&.cvat-context-image-gallery-item-current {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvat-context-image-gallery-header {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.cvat-context-image-close-button {
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: all 200ms;
|
||||||
|
opacity: 0.6;
|
||||||
|
position: absolute;
|
||||||
|
top: $grid-unit-size;
|
||||||
|
right: $grid-unit-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,89 +0,0 @@
|
|||||||
// Copyright (C) 2021-2022 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import notification from 'antd/lib/notification';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { QuestionCircleOutlined, ShrinkOutlined } from '@ant-design/icons';
|
|
||||||
import Spin from 'antd/lib/spin';
|
|
||||||
import Image from 'antd/lib/image';
|
|
||||||
|
|
||||||
import { CombinedState } from 'reducers';
|
|
||||||
import { hideShowContextImage, getContextImageAsync } from 'actions/annotation-actions';
|
|
||||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
|
||||||
|
|
||||||
export function adjustContextImagePosition(sidebarCollapsed: boolean): void {
|
|
||||||
const element = window.document.getElementsByClassName('cvat-context-image-wrapper')[0] as
|
|
||||||
| HTMLDivElement
|
|
||||||
| undefined;
|
|
||||||
if (element) {
|
|
||||||
if (sidebarCollapsed) {
|
|
||||||
element.style.right = '40px';
|
|
||||||
} else {
|
|
||||||
element.style.right = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextImage(): JSX.Element | null {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { number: frame, hasRelatedContext } = useSelector((state: CombinedState) => state.annotation.player.frame);
|
|
||||||
const { data: contextImageData, hidden: contextImageHidden, fetching: contextImageFetching } = useSelector(
|
|
||||||
(state: CombinedState) => state.annotation.player.contextImage,
|
|
||||||
);
|
|
||||||
const [requested, setRequested] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (requested) {
|
|
||||||
setRequested(false);
|
|
||||||
}
|
|
||||||
}, [frame, contextImageData]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasRelatedContext && !contextImageHidden && !requested) {
|
|
||||||
dispatch(getContextImageAsync());
|
|
||||||
setRequested(true);
|
|
||||||
}
|
|
||||||
}, [contextImageHidden, requested, hasRelatedContext]);
|
|
||||||
|
|
||||||
if (!hasRelatedContext) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='cvat-context-image-wrapper' {...(contextImageHidden ? { style: { width: '32px' } } : {})}>
|
|
||||||
<div className='cvat-context-image-wrapper-header' />
|
|
||||||
{contextImageFetching ? <Spin size='small' /> : null}
|
|
||||||
{contextImageHidden ? (
|
|
||||||
<CVATTooltip title='A context image is available'>
|
|
||||||
<QuestionCircleOutlined
|
|
||||||
className='cvat-context-image-switcher'
|
|
||||||
onClick={() => dispatch(hideShowContextImage(false))}
|
|
||||||
/>
|
|
||||||
</CVATTooltip>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ShrinkOutlined
|
|
||||||
className='cvat-context-image-switcher'
|
|
||||||
onClick={() => dispatch(hideShowContextImage(true))}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
{...(contextImageData ? { src: contextImageData } : {})}
|
|
||||||
onError={() => {
|
|
||||||
notification.error({
|
|
||||||
message: 'Could not display context image',
|
|
||||||
description: `Source is ${
|
|
||||||
contextImageData === null ? 'empty' : contextImageData.slice(0, 100)
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className='cvat-context-image'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(ContextImage);
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
// Copyright (C) 2021-2022 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import CameraIcon from '@ant-design/icons/CameraOutlined';
|
|
||||||
|
|
||||||
import CVATTooltip from 'components/common/cvat-tooltip';
|
|
||||||
import { Canvas3d } from 'cvat-canvas3d-wrapper';
|
|
||||||
import { Canvas } from 'cvat-canvas-wrapper';
|
|
||||||
import { ActiveControl } from 'reducers';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
canvasInstance: Canvas3d | Canvas;
|
|
||||||
activeControl: ActiveControl;
|
|
||||||
hideShowContextImage: (hidden: boolean) => void;
|
|
||||||
contextImageHide: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PhotoContextControl(props: Props): JSX.Element {
|
|
||||||
const { activeControl, contextImageHide, hideShowContextImage } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CVATTooltip title='Photo context show/hide' placement='right'>
|
|
||||||
<CameraIcon
|
|
||||||
className={`cvat-context-image-control
|
|
||||||
cvat-control-side-bar-icon-size ${
|
|
||||||
activeControl === ActiveControl.PHOTO_CONTEXT ? 'cvat-active-canvas-control' : ''
|
|
||||||
}`}
|
|
||||||
onClick={(): void => {
|
|
||||||
hideShowContextImage(!contextImageHide);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CVATTooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(PhotoContextControl);
|
|
||||||
@ -1,339 +0,0 @@
|
|||||||
// Copyright (C) 2020-2022 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { KeyMap } from 'utils/mousetrap-react';
|
|
||||||
|
|
||||||
import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper';
|
|
||||||
import {
|
|
||||||
confirmCanvasReady,
|
|
||||||
dragCanvas,
|
|
||||||
zoomCanvas,
|
|
||||||
resetCanvas,
|
|
||||||
shapeDrawn,
|
|
||||||
mergeObjects,
|
|
||||||
groupObjects,
|
|
||||||
splitTrack,
|
|
||||||
editShape,
|
|
||||||
updateAnnotationsAsync,
|
|
||||||
createAnnotationsAsync,
|
|
||||||
mergeAnnotationsAsync,
|
|
||||||
groupAnnotationsAsync,
|
|
||||||
splitAnnotationsAsync,
|
|
||||||
activateObject,
|
|
||||||
updateCanvasContextMenu,
|
|
||||||
addZLayer,
|
|
||||||
switchZLayer,
|
|
||||||
fetchAnnotationsAsync,
|
|
||||||
getDataFailed,
|
|
||||||
} from 'actions/annotation-actions';
|
|
||||||
import {
|
|
||||||
switchGrid,
|
|
||||||
changeGridColor,
|
|
||||||
changeGridOpacity,
|
|
||||||
changeBrightnessLevel,
|
|
||||||
changeContrastLevel,
|
|
||||||
changeSaturationLevel,
|
|
||||||
switchAutomaticBordering,
|
|
||||||
} from 'actions/settings-actions';
|
|
||||||
import { reviewActions } from 'actions/review-actions';
|
|
||||||
import {
|
|
||||||
ColorBy,
|
|
||||||
GridColor,
|
|
||||||
ObjectType,
|
|
||||||
CombinedState,
|
|
||||||
ContextMenuType,
|
|
||||||
Workspace,
|
|
||||||
ActiveControl,
|
|
||||||
} from 'reducers';
|
|
||||||
|
|
||||||
import { Canvas } from 'cvat-canvas-wrapper';
|
|
||||||
import { Canvas3d } from 'cvat-canvas3d-wrapper';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
sidebarCollapsed: boolean;
|
|
||||||
canvasInstance: Canvas | Canvas3d | null;
|
|
||||||
jobInstance: any;
|
|
||||||
activatedStateID: number | null;
|
|
||||||
activatedElementID: number | null;
|
|
||||||
activatedAttributeID: number | null;
|
|
||||||
annotations: any[];
|
|
||||||
frameData: any;
|
|
||||||
frameAngle: number;
|
|
||||||
frameFetching: boolean;
|
|
||||||
frame: number;
|
|
||||||
opacity: number;
|
|
||||||
colorBy: ColorBy;
|
|
||||||
selectedOpacity: number;
|
|
||||||
outlined: boolean;
|
|
||||||
outlineColor: string;
|
|
||||||
showBitmap: boolean;
|
|
||||||
showProjections: boolean;
|
|
||||||
grid: boolean;
|
|
||||||
gridSize: number;
|
|
||||||
gridColor: GridColor;
|
|
||||||
gridOpacity: number;
|
|
||||||
activeLabelID: number;
|
|
||||||
activeObjectType: ObjectType;
|
|
||||||
brightnessLevel: number;
|
|
||||||
contrastLevel: number;
|
|
||||||
saturationLevel: number;
|
|
||||||
resetZoom: boolean;
|
|
||||||
smoothImage: boolean;
|
|
||||||
aamZoomMargin: number;
|
|
||||||
showObjectsTextAlways: boolean;
|
|
||||||
textFontSize: number;
|
|
||||||
controlPointsSize: number;
|
|
||||||
textPosition: 'auto' | 'center';
|
|
||||||
textContent: string;
|
|
||||||
showAllInterpolationTracks: boolean;
|
|
||||||
workspace: Workspace;
|
|
||||||
minZLayer: number;
|
|
||||||
maxZLayer: number;
|
|
||||||
curZLayer: number;
|
|
||||||
automaticBordering: boolean;
|
|
||||||
intelligentPolygonCrop: boolean;
|
|
||||||
switchableAutomaticBordering: boolean;
|
|
||||||
keyMap: KeyMap;
|
|
||||||
canvasBackgroundColor: string;
|
|
||||||
showTagsOnFrame: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchToProps {
|
|
||||||
onSetupCanvas(): void;
|
|
||||||
onDragCanvas: (enabled: boolean) => void;
|
|
||||||
onZoomCanvas: (enabled: boolean) => void;
|
|
||||||
onResetCanvas: () => void;
|
|
||||||
onShapeDrawn: () => void;
|
|
||||||
onMergeObjects: (enabled: boolean) => void;
|
|
||||||
onGroupObjects: (enabled: boolean) => void;
|
|
||||||
onSplitTrack: (enabled: boolean) => void;
|
|
||||||
onEditShape: (enabled: boolean) => void;
|
|
||||||
onUpdateAnnotations(states: any[]): void;
|
|
||||||
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
|
|
||||||
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
|
|
||||||
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
|
|
||||||
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
|
|
||||||
onActivateObject: (activatedStateID: number | null, activatedElementID: number | null) => void;
|
|
||||||
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
|
|
||||||
onAddZLayer(): void;
|
|
||||||
onSwitchZLayer(cur: number): void;
|
|
||||||
onChangeBrightnessLevel(level: number): void;
|
|
||||||
onChangeContrastLevel(level: number): void;
|
|
||||||
onChangeSaturationLevel(level: number): void;
|
|
||||||
onChangeGridOpacity(opacity: number): void;
|
|
||||||
onChangeGridColor(color: GridColor): void;
|
|
||||||
onSwitchGrid(enabled: boolean): void;
|
|
||||||
onSwitchAutomaticBordering(enabled: boolean): void;
|
|
||||||
onFetchAnnotation(): void;
|
|
||||||
onGetDataFailed(error: any): void;
|
|
||||||
onStartIssue(position: number[]): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: CombinedState): StateToProps {
|
|
||||||
const {
|
|
||||||
annotation: {
|
|
||||||
canvas: { activeControl, instance: canvasInstance },
|
|
||||||
drawing: { activeLabelID, activeObjectType },
|
|
||||||
job: { instance: jobInstance },
|
|
||||||
player: {
|
|
||||||
frame: { data: frameData, number: frame, fetching: frameFetching },
|
|
||||||
frameAngles,
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
states: annotations,
|
|
||||||
activatedStateID,
|
|
||||||
activatedElementID,
|
|
||||||
activatedAttributeID,
|
|
||||||
zLayer: { cur: curZLayer, min: minZLayer, max: maxZLayer },
|
|
||||||
},
|
|
||||||
sidebarCollapsed,
|
|
||||||
workspace,
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
player: {
|
|
||||||
canvasBackgroundColor,
|
|
||||||
grid,
|
|
||||||
gridSize,
|
|
||||||
gridColor,
|
|
||||||
gridOpacity,
|
|
||||||
brightnessLevel,
|
|
||||||
contrastLevel,
|
|
||||||
saturationLevel,
|
|
||||||
resetZoom,
|
|
||||||
smoothImage,
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
aamZoomMargin,
|
|
||||||
showObjectsTextAlways,
|
|
||||||
showAllInterpolationTracks,
|
|
||||||
showTagsOnFrame,
|
|
||||||
automaticBordering,
|
|
||||||
intelligentPolygonCrop,
|
|
||||||
textFontSize,
|
|
||||||
controlPointsSize,
|
|
||||||
textPosition,
|
|
||||||
textContent,
|
|
||||||
},
|
|
||||||
shapes: {
|
|
||||||
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
shortcuts: { keyMap },
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sidebarCollapsed,
|
|
||||||
canvasInstance,
|
|
||||||
jobInstance,
|
|
||||||
frameData,
|
|
||||||
frameAngle: frameAngles[frame - jobInstance.startFrame],
|
|
||||||
frameFetching,
|
|
||||||
frame,
|
|
||||||
activatedStateID,
|
|
||||||
activatedElementID,
|
|
||||||
activatedAttributeID,
|
|
||||||
annotations,
|
|
||||||
opacity: opacity / 100,
|
|
||||||
colorBy,
|
|
||||||
selectedOpacity: selectedOpacity / 100,
|
|
||||||
outlined,
|
|
||||||
outlineColor,
|
|
||||||
showBitmap,
|
|
||||||
showProjections,
|
|
||||||
grid,
|
|
||||||
gridSize,
|
|
||||||
gridColor,
|
|
||||||
gridOpacity: gridOpacity / 100,
|
|
||||||
activeLabelID,
|
|
||||||
activeObjectType,
|
|
||||||
brightnessLevel: brightnessLevel / 100,
|
|
||||||
contrastLevel: contrastLevel / 100,
|
|
||||||
saturationLevel: saturationLevel / 100,
|
|
||||||
resetZoom,
|
|
||||||
smoothImage,
|
|
||||||
aamZoomMargin,
|
|
||||||
showObjectsTextAlways,
|
|
||||||
textFontSize,
|
|
||||||
controlPointsSize,
|
|
||||||
textPosition,
|
|
||||||
textContent,
|
|
||||||
showAllInterpolationTracks,
|
|
||||||
showTagsOnFrame,
|
|
||||||
curZLayer,
|
|
||||||
minZLayer,
|
|
||||||
maxZLayer,
|
|
||||||
automaticBordering,
|
|
||||||
intelligentPolygonCrop,
|
|
||||||
workspace,
|
|
||||||
keyMap,
|
|
||||||
canvasBackgroundColor,
|
|
||||||
switchableAutomaticBordering:
|
|
||||||
activeControl === ActiveControl.DRAW_POLYGON ||
|
|
||||||
activeControl === ActiveControl.DRAW_POLYLINE ||
|
|
||||||
activeControl === ActiveControl.DRAW_MASK ||
|
|
||||||
activeControl === ActiveControl.EDIT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
|
||||||
return {
|
|
||||||
onSetupCanvas(): void {
|
|
||||||
dispatch(confirmCanvasReady());
|
|
||||||
},
|
|
||||||
onDragCanvas(enabled: boolean): void {
|
|
||||||
dispatch(dragCanvas(enabled));
|
|
||||||
},
|
|
||||||
onZoomCanvas(enabled: boolean): void {
|
|
||||||
dispatch(zoomCanvas(enabled));
|
|
||||||
},
|
|
||||||
onResetCanvas(): void {
|
|
||||||
dispatch(resetCanvas());
|
|
||||||
},
|
|
||||||
onShapeDrawn(): void {
|
|
||||||
dispatch(shapeDrawn());
|
|
||||||
},
|
|
||||||
onMergeObjects(enabled: boolean): void {
|
|
||||||
dispatch(mergeObjects(enabled));
|
|
||||||
},
|
|
||||||
onGroupObjects(enabled: boolean): void {
|
|
||||||
dispatch(groupObjects(enabled));
|
|
||||||
},
|
|
||||||
onSplitTrack(enabled: boolean): void {
|
|
||||||
dispatch(splitTrack(enabled));
|
|
||||||
},
|
|
||||||
onEditShape(enabled: boolean): void {
|
|
||||||
dispatch(editShape(enabled));
|
|
||||||
},
|
|
||||||
onUpdateAnnotations(states: any[]): void {
|
|
||||||
dispatch(updateAnnotationsAsync(states));
|
|
||||||
},
|
|
||||||
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
|
|
||||||
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
|
|
||||||
},
|
|
||||||
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void {
|
|
||||||
dispatch(mergeAnnotationsAsync(sessionInstance, frame, states));
|
|
||||||
},
|
|
||||||
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
|
|
||||||
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
|
|
||||||
},
|
|
||||||
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
|
|
||||||
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
|
|
||||||
},
|
|
||||||
onActivateObject(activatedStateID: number | null, activatedElementID: number | null = null): void {
|
|
||||||
if (activatedStateID === null) {
|
|
||||||
dispatch(updateCanvasContextMenu(false, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(activateObject(activatedStateID, activatedElementID, null));
|
|
||||||
},
|
|
||||||
onUpdateContextMenu(
|
|
||||||
visible: boolean,
|
|
||||||
left: number,
|
|
||||||
top: number,
|
|
||||||
type: ContextMenuType,
|
|
||||||
pointID?: number,
|
|
||||||
): void {
|
|
||||||
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
|
|
||||||
},
|
|
||||||
onAddZLayer(): void {
|
|
||||||
dispatch(addZLayer());
|
|
||||||
},
|
|
||||||
onSwitchZLayer(cur: number): void {
|
|
||||||
dispatch(switchZLayer(cur));
|
|
||||||
},
|
|
||||||
onChangeBrightnessLevel(level: number): void {
|
|
||||||
dispatch(changeBrightnessLevel(level));
|
|
||||||
},
|
|
||||||
onChangeContrastLevel(level: number): void {
|
|
||||||
dispatch(changeContrastLevel(level));
|
|
||||||
},
|
|
||||||
onChangeSaturationLevel(level: number): void {
|
|
||||||
dispatch(changeSaturationLevel(level));
|
|
||||||
},
|
|
||||||
onChangeGridOpacity(opacity: number): void {
|
|
||||||
dispatch(changeGridOpacity(opacity));
|
|
||||||
},
|
|
||||||
onChangeGridColor(color: GridColor): void {
|
|
||||||
dispatch(changeGridColor(color));
|
|
||||||
},
|
|
||||||
onSwitchGrid(enabled: boolean): void {
|
|
||||||
dispatch(switchGrid(enabled));
|
|
||||||
},
|
|
||||||
onSwitchAutomaticBordering(enabled: boolean): void {
|
|
||||||
dispatch(switchAutomaticBordering(enabled));
|
|
||||||
},
|
|
||||||
onFetchAnnotation(): void {
|
|
||||||
dispatch(fetchAnnotationsAsync());
|
|
||||||
},
|
|
||||||
onGetDataFailed(error: any): void {
|
|
||||||
dispatch(getDataFailed(error));
|
|
||||||
},
|
|
||||||
onStartIssue(position: number[]): void {
|
|
||||||
dispatch(reviewActions.startIssue(position));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent);
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
// Copyright (C) 2021-2022 Intel Corporation
|
|
||||||
// Copyright (C) 2022 CVAT.ai Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D';
|
|
||||||
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 { Canvas3d } from 'cvat-canvas3d-wrapper';
|
|
||||||
import { Canvas } from 'cvat-canvas-wrapper';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
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;
|
|
||||||
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,
|
|
||||||
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));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent);
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
// Copyright (C) 2021-2022 Intel Corporation
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { taskName } from '../../support/const_canvas3d';
|
|
||||||
|
|
||||||
context('Canvas 3D functionality. Resize views.', () => {
|
|
||||||
const caseId = '62';
|
|
||||||
let widthHeightArrBeforeResize = [];
|
|
||||||
let widthHeightArrAfterResize = [];
|
|
||||||
|
|
||||||
function getViewWidthHeight(element, arrToPush) {
|
|
||||||
cy.get(element)
|
|
||||||
.find('canvas')
|
|
||||||
.invoke('attr', 'width')
|
|
||||||
.then(($topviewWidth) => {
|
|
||||||
cy.get(element)
|
|
||||||
.find('canvas')
|
|
||||||
.invoke('attr', 'height')
|
|
||||||
.then(($topviewHeight) => {
|
|
||||||
arrToPush.push([$topviewWidth, $topviewHeight]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
cy.openTaskJob(taskName);
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrBeforeResize);
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrBeforeResize);
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrBeforeResize);
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrBeforeResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`Testing case "${caseId}"`, () => {
|
|
||||||
it('Resizing perspective.', () => {
|
|
||||||
cy.get('.cvat-resizable-handle-horizontal').trigger('mousedown', { button: 0, scrollBehavior: false });
|
|
||||||
cy.get('.cvat-canvas3d-perspective')
|
|
||||||
.trigger('mousemove', 600, 300, { scrollBehavior: false })
|
|
||||||
.trigger('mouseup');
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrAfterResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Resizing topview.', () => {
|
|
||||||
cy.get('.cvat-resizable-handle-vertical-top').trigger('mousedown', { button: 0, scrollBehavior: false });
|
|
||||||
cy.get('.cvat-canvas3d-topview')
|
|
||||||
.trigger('mousemove', 200, 200, { scrollBehavior: false })
|
|
||||||
.trigger('mouseup');
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrAfterResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Resizing sideview.', () => {
|
|
||||||
cy.get('.cvat-resizable-handle-vertical-side').trigger('mousedown', { button: 0, scrollBehavior: false });
|
|
||||||
cy.get('.cvat-canvas3d-frontview')
|
|
||||||
.trigger('mousemove', 200, 200, { scrollBehavior: false })
|
|
||||||
.trigger('mouseup');
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrAfterResize);
|
|
||||||
getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrAfterResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Checking for elements resizing.', () => {
|
|
||||||
expect(widthHeightArrBeforeResize[0][0]).to.be.equal(widthHeightArrAfterResize[0][0]); // Width of cvat-canvas3d-perspective before and after didn't change
|
|
||||||
expect(widthHeightArrBeforeResize[0][1]).not.be.equal(widthHeightArrAfterResize[0][1]); // Height of cvat-canvas3d-perspective changed
|
|
||||||
expect(widthHeightArrAfterResize[1][1])
|
|
||||||
.to.be.equal(widthHeightArrAfterResize[2][1])
|
|
||||||
.to.be.equal(widthHeightArrAfterResize[3][1]); // Top/side/front has equal height after changes
|
|
||||||
[
|
|
||||||
[widthHeightArrBeforeResize[1][0], widthHeightArrAfterResize[1][0]],
|
|
||||||
[widthHeightArrBeforeResize[2][0], widthHeightArrAfterResize[2][0]],
|
|
||||||
[widthHeightArrBeforeResize[3][0], widthHeightArrAfterResize[3][0]],
|
|
||||||
].forEach(([widthBefore, widthAfter]) => {
|
|
||||||
expect(widthBefore).not.be.equal(widthAfter); // Width of top/side/front changed
|
|
||||||
});
|
|
||||||
[
|
|
||||||
[widthHeightArrBeforeResize[1][1], widthHeightArrAfterResize[1][1]],
|
|
||||||
[widthHeightArrBeforeResize[2][1], widthHeightArrAfterResize[2][1]],
|
|
||||||
[widthHeightArrBeforeResize[3][1], widthHeightArrAfterResize[3][1]],
|
|
||||||
].forEach(([heightBefore, heightAfter]) => {
|
|
||||||
expect(heightBefore).not.be.equal(heightAfter); // Height of top/side/front changed
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue