You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

276 lines
12 KiB
TypeScript

// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './brush-toolbox-styles.scss';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'antd/lib/button';
import Icon, { VerticalAlignBottomOutlined } from '@ant-design/icons';
import InputNumber from 'antd/lib/input-number';
import Select from 'antd/lib/select';
import { getCore } from 'cvat-core-wrapper';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import {
BrushIcon, EraserIcon, PolygonMinusIcon, PolygonPlusIcon,
PlusIcon, CheckIcon, MoveIcon,
} from 'icons';
import CVATTooltip from 'components/common/cvat-tooltip';
import { CombinedState, ObjectType, ShapeType } from 'reducers';
import LabelSelector from 'components/label-selector/label-selector';
import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions';
import useDraggable from './draggable-hoc';
const DraggableArea = (
<div className='cvat-brush-tools-draggable-area'>
<Icon component={MoveIcon} />
</div>
);
const MIN_BRUSH_SIZE = 1;
function BrushTools(): React.ReactPortal | null {
const dispatch = useDispatch();
const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID);
const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools);
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const labels = useSelector((state: CombinedState) => state.annotation.job.labels);
const { visible } = config;
const [editableState, setEditableState] = useState<any | null>(null);
const [currentTool, setCurrentTool] = useState<'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'>('brush');
const [brushForm, setBrushForm] = useState<'circle' | 'square'>('circle');
const [[top, left], setTopLeft] = useState([0, 0]);
const [brushSize, setBrushSize] = useState(10);
const [removeUnderlyingPixels, setRemoveUnderlyingPixels] = useState(false);
const dragBar = useDraggable(
(): number[] => {
const [element] = window.document.getElementsByClassName('cvat-brush-tools-toolbox');
if (element) {
const { offsetTop, offsetLeft } = element as HTMLDivElement;
return [offsetTop, offsetLeft];
}
return [0, 0];
},
(newTop, newLeft) => setTopLeft([newTop, newLeft]),
DraggableArea,
);
useEffect(() => {
const label = labels.find((_label: any) => _label.id === defaultLabelID);
getCore().config.removeUnderlyingMaskPixels = removeUnderlyingPixels;
if (visible && label && canvasInstance instanceof Canvas) {
const onUpdateConfiguration = ({ brushTool }: any): void => {
if (brushTool?.size) {
setBrushSize(Math.max(MIN_BRUSH_SIZE, brushTool.size));
}
};
if (canvasInstance.mode() === CanvasMode.DRAW) {
canvasInstance.draw({
enabled: true,
shapeType: ShapeType.MASK,
crosshair: false,
brushTool: {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
},
onUpdateConfiguration,
});
} else if (canvasInstance.mode() === CanvasMode.EDIT && editableState) {
canvasInstance.edit({
enabled: true,
state: editableState,
brushTool: {
type: currentTool,
size: brushSize,
form: brushForm,
color: label.color,
},
onUpdateConfiguration,
});
}
}
}, [currentTool, brushSize, brushForm, visible, defaultLabelID, editableState]);
useEffect(() => {
const canvasContainer = window.document.getElementsByClassName('cvat-canvas-container')[0];
if (canvasContainer) {
const { offsetTop, offsetLeft } = canvasContainer.parentElement as HTMLElement;
setTopLeft([offsetTop, offsetLeft]);
}
}, []);
useEffect(() => {
const hideToolset = (): void => {
if (visible) {
dispatch(updateCanvasBrushTools({ visible: false }));
}
};
const showToolset = (e: Event): void => {
const evt = e as CustomEvent;
if (evt.detail?.state?.shapeType === ShapeType.MASK ||
(evt.detail?.drawData?.shapeType === ShapeType.MASK && !evt.detail?.drawData?.initialState)) {
dispatch(updateCanvasBrushTools({ visible: true }));
}
};
const updateEditableState = (e: Event): void => {
const evt = e as CustomEvent;
if (evt.type === 'canvas.editstart' && evt.detail.state) {
setEditableState(evt.detail.state);
} else if (editableState) {
setEditableState(null);
}
};
if (canvasInstance instanceof Canvas) {
canvasInstance.html().addEventListener('canvas.drawn', hideToolset);
canvasInstance.html().addEventListener('canvas.canceled', hideToolset);
canvasInstance.html().addEventListener('canvas.canceled', updateEditableState);
canvasInstance.html().addEventListener('canvas.drawstart', showToolset);
canvasInstance.html().addEventListener('canvas.editstart', showToolset);
canvasInstance.html().addEventListener('canvas.editstart', updateEditableState);
canvasInstance.html().addEventListener('canvas.editdone', updateEditableState);
}
return () => {
if (canvasInstance instanceof Canvas) {
canvasInstance.html().removeEventListener('canvas.drawn', hideToolset);
canvasInstance.html().removeEventListener('canvas.canceled', hideToolset);
canvasInstance.html().removeEventListener('canvas.canceled', updateEditableState);
canvasInstance.html().removeEventListener('canvas.drawstart', showToolset);
canvasInstance.html().removeEventListener('canvas.editstart', showToolset);
canvasInstance.html().removeEventListener('canvas.editstart', updateEditableState);
canvasInstance.html().removeEventListener('canvas.editdone', updateEditableState);
}
};
}, [visible, editableState]);
if (!labels.length) {
return null;
}
return ReactDOM.createPortal((
<div className='cvat-brush-tools-toolbox' style={{ top, left, display: visible ? '' : 'none' }}>
<Button
type='text'
className='cvat-brush-tools-finish'
icon={<Icon component={CheckIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
if (editableState) {
canvasInstance.edit({ enabled: false });
} else {
canvasInstance.draw({ enabled: false });
}
}
}}
/>
{!editableState && (
<Button
type='text'
disabled={!!editableState}
className='cvat-brush-tools-continue'
icon={<Icon component={PlusIcon} />}
onClick={() => {
if (canvasInstance instanceof Canvas) {
canvasInstance.draw({ enabled: false, continue: true });
dispatch(
rememberObject({
activeObjectType: ObjectType.SHAPE,
activeShapeType: ShapeType.MASK,
activeLabelID: defaultLabelID,
}),
);
}
}}
/>
)}
<hr />
<Button
type='text'
className={['cvat-brush-tools-brush', ...(currentTool === 'brush' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={BrushIcon} />}
onClick={() => setCurrentTool('brush')}
/>
<Button
type='text'
className={['cvat-brush-tools-eraser', ...(currentTool === 'eraser' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={EraserIcon} />}
onClick={() => setCurrentTool('eraser')}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-plus', ...(currentTool === 'polygon-plus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonPlusIcon} />}
onClick={() => setCurrentTool('polygon-plus')}
/>
<Button
type='text'
className={['cvat-brush-tools-polygon-minus', ...(currentTool === 'polygon-minus' ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<Icon component={PolygonMinusIcon} />}
onClick={() => setCurrentTool('polygon-minus')}
/>
{ ['brush', 'eraser'].includes(currentTool) ? (
<CVATTooltip title='Brush size [Hold Alt + Right Mouse Click + Drag Left/Right]'>
<InputNumber
className='cvat-brush-tools-brush-size'
value={brushSize}
min={MIN_BRUSH_SIZE}
formatter={(val: number | undefined) => {
if (val) return `${val}px`;
return '';
}}
parser={(val: string | undefined): number => {
if (val) return +val.replace('px', '');
return 0;
}}
onChange={(value: number) => {
if (Number.isInteger(value) && value >= MIN_BRUSH_SIZE) {
setBrushSize(value);
}
}}
/>
</CVATTooltip>
) : null}
{ ['brush', 'eraser'].includes(currentTool) ? (
<Select value={brushForm} onChange={(value: 'circle' | 'square') => setBrushForm(value)}>
<Select.Option value='circle'>Circle</Select.Option>
<Select.Option value='square'>Square</Select.Option>
</Select>
) : null}
<Button
type='text'
className={['cvat-brush-tools-underlying-pixels', ...(removeUnderlyingPixels ? ['cvat-brush-tools-active-tool'] : [])].join(' ')}
icon={<VerticalAlignBottomOutlined />}
onClick={() => setRemoveUnderlyingPixels(!removeUnderlyingPixels)}
/>
{ !editableState && (
<LabelSelector
labels={labels}
value={defaultLabelID}
onChange={({ id: labelID }: { id: number }) => {
if (Number.isInteger(labelID)) {
dispatch(
rememberObject({ activeLabelID: labelID }),
);
}
}}
/>
)}
{ dragBar }
</div>
), window.document.body);
}
export default React.memo(BrushTools);