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
|
||||
|
||||
@import '../../../base.scss';
|
||||
@import 'base.scss';
|
||||
|
||||
.cvat-brush-tools-toolbox {
|
||||
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