feat(devtool): 图像标注器支持标注 HintBox
This commit is contained in:
parent
7365153e69
commit
c4be314241
|
@ -4,7 +4,7 @@ import { SideToolBar, Tool } from '../../components/SideToolBar';
|
||||||
import PropertyGrid, { Property, PropertyCategory } from '../../components/PropertyGrid';
|
import PropertyGrid, { Property, PropertyCategory } from '../../components/PropertyGrid';
|
||||||
import ImageEditor, { AnnotationChangedEvent } from '../../components/ImageEditor/ImageEditor';
|
import ImageEditor, { AnnotationChangedEvent } from '../../components/ImageEditor/ImageEditor';
|
||||||
import { Annotation, Tool as EditorTool } from '../../components/ImageEditor/types';
|
import { Annotation, Tool as EditorTool } from '../../components/ImageEditor/types';
|
||||||
import { BsCursor, BsSquare, BsFolder2Open, BsFloppy } from 'react-icons/bs';
|
import { BsCursor, BsSquare, BsFolder2Open, BsFloppy, BsCardImage, BsQuestionSquare } from 'react-icons/bs';
|
||||||
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition, Definitions } from '../../hooks/useImageMetaData';
|
import useImageMetaData, { Definition, DefinitionType, ImageMetaData, TemplateDefinition, Definitions } from '../../hooks/useImageMetaData';
|
||||||
import { useImageViewerModal } from '../../components/ImageViewerModal';
|
import { useImageViewerModal } from '../../components/ImageViewerModal';
|
||||||
import { useMessageBox } from '../../hooks/useMessageBox';
|
import { useMessageBox } from '../../hooks/useMessageBox';
|
||||||
|
@ -12,6 +12,7 @@ import { useToast } from '../../components/ToastMessage';
|
||||||
import DragArea from './DragArea';
|
import DragArea from './DragArea';
|
||||||
import { cropImage, openFileWFS, openFileInput, downloadJSONToFile, readFileAsJSON, readFileAsDataURL, FileResult, saveFileWFS, saveFileAsWFS } from '../../utils/fileUtils';
|
import { cropImage, openFileWFS, openFileInput, downloadJSONToFile, readFileAsJSON, readFileAsDataURL, FileResult, saveFileWFS, saveFileAsWFS } from '../../utils/fileUtils';
|
||||||
import NativeDiv from '../../components/NativeDiv';
|
import NativeDiv from '../../components/NativeDiv';
|
||||||
|
import useHotkey from '../../hooks/useHotkey';
|
||||||
|
|
||||||
const PageContainer = styled.div`
|
const PageContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,6 +50,12 @@ const Tip = styled.span`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 工具栏配置
|
// 工具栏配置
|
||||||
|
export enum Tools {
|
||||||
|
Drag = EditorTool.Drag,
|
||||||
|
Template = 'template',
|
||||||
|
HintBox = 'hintbox',
|
||||||
|
}
|
||||||
|
|
||||||
const tools: Array<Tool | 'separator'> = [
|
const tools: Array<Tool | 'separator'> = [
|
||||||
{
|
{
|
||||||
id: 'open',
|
id: 'open',
|
||||||
|
@ -64,25 +71,35 @@ const tools: Array<Tool | 'separator'> = [
|
||||||
},
|
},
|
||||||
'separator',
|
'separator',
|
||||||
{
|
{
|
||||||
id: 'drag',
|
id: Tools.Drag,
|
||||||
icon: <BsCursor size={24} />,
|
icon: <BsCursor size={24} />,
|
||||||
title: '拖动工具 (V)',
|
title: '拖动工具 (V)',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rect',
|
id: Tools.Template,
|
||||||
icon: <BsSquare size={24} />,
|
icon: <BsCardImage size={24} />,
|
||||||
title: '矩形工具 (R)',
|
title: '模板工具 (T)',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: Tools.HintBox,
|
||||||
|
icon: <BsQuestionSquare size={24} />,
|
||||||
|
title: 'HintBox 工具 (B)',
|
||||||
|
selectable: true,
|
||||||
|
}
|
||||||
];
|
];
|
||||||
const toolsMap: Record<string, EditorTool> = {
|
const STR_TO_TOOL: Record<string, Tools> = {
|
||||||
drag: EditorTool.Drag,
|
drag: Tools.Drag,
|
||||||
rect: EditorTool.Rect,
|
template: Tools.Template,
|
||||||
|
hintbox: Tools.HintBox,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 示例图片URL
|
const TOOL_TO_EDITOR_TOOL: Record<Tools, EditorTool> = {
|
||||||
const SAMPLE_IMAGE_URL = 'https://picsum.photos/seed/123/800/600';
|
[Tools.Drag]: EditorTool.Drag,
|
||||||
|
[Tools.Template]: EditorTool.Rect,
|
||||||
|
[Tools.HintBox]: EditorTool.Rect,
|
||||||
|
};
|
||||||
|
|
||||||
// 计算最大公约数
|
// 计算最大公约数
|
||||||
const gcd = (a: number, b: number): number => {
|
const gcd = (a: number, b: number): number => {
|
||||||
|
@ -268,14 +285,15 @@ const usePropertyGridData = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageAnnotation: React.FC = () => {
|
const ImageAnnotation: React.FC = () => {
|
||||||
const [currentTool, setCurrentTool] = useState<EditorTool>(EditorTool.Drag);
|
const [currentTool, setCurrentTool] = useState<Tools>(Tools.Drag);
|
||||||
|
|
||||||
const { imageMetaData, Definitions, Annotations, clear, load, toString, fromString } = useImageMetaData();
|
const { imageMetaData, Definitions, Annotations, clear, load, toString, fromString } = useImageMetaData();
|
||||||
const [selectedAnnotation, setSelectedAnnotation] = useState<Annotation | null>(null);
|
const [selectedAnnotation, setSelectedAnnotation] = useState<Annotation | null>(null);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [image, setImage] = useState<HTMLImageElement | null>(null);
|
const [image, setImage] = useState<HTMLImageElement | null>(null);
|
||||||
const imageFileNameRef = useRef<string>('');
|
const imageFileNameRef = useRef<string>('');
|
||||||
const { modal, openModal } = useImageViewerModal('裁剪预览');
|
const { modal, openModal } = useImageViewerModal('裁剪预览');
|
||||||
const [imageUrl, setImageUrl] = useState<string>(SAMPLE_IMAGE_URL);
|
const [imageUrl, setImageUrl] = useState<string>('');
|
||||||
const { yesNo, MessageBoxComponent } = useMessageBox();
|
const { yesNo, MessageBoxComponent } = useMessageBox();
|
||||||
const { showToast, ToastComponent } = useToast();
|
const { showToast, ToastComponent } = useToast();
|
||||||
const currentFileResult = useRef<FileResult | null>(null);
|
const currentFileResult = useRef<FileResult | null>(null);
|
||||||
|
@ -301,6 +319,7 @@ const ImageAnnotation: React.FC = () => {
|
||||||
}, [clear]);
|
}, [clear]);
|
||||||
|
|
||||||
const handleImageLoad = useCallback(async (result: FileResult, shouldClearMetaData: boolean = true) => {
|
const handleImageLoad = useCallback(async (result: FileResult, shouldClearMetaData: boolean = true) => {
|
||||||
|
currentFileResult.current = result;
|
||||||
imageFileNameRef.current = result.name;
|
imageFileNameRef.current = result.name;
|
||||||
const dataUrl = await readFileAsDataURL(result.file);
|
const dataUrl = await readFileAsDataURL(result.file);
|
||||||
loadImage(dataUrl, shouldClearMetaData);
|
loadImage(dataUrl, shouldClearMetaData);
|
||||||
|
@ -310,9 +329,12 @@ const ImageAnnotation: React.FC = () => {
|
||||||
const handleAnnotationChange = (e: AnnotationChangedEvent) => {
|
const handleAnnotationChange = (e: AnnotationChangedEvent) => {
|
||||||
if (e.type === 'add') {
|
if (e.type === 'add') {
|
||||||
let type: DefinitionType | undefined = undefined;
|
let type: DefinitionType | undefined = undefined;
|
||||||
if (currentTool === EditorTool.Rect) {
|
if (currentTool === Tools.Template) {
|
||||||
type = 'template';
|
type = 'template';
|
||||||
}
|
}
|
||||||
|
else if (currentTool === Tools.HintBox) {
|
||||||
|
type = 'hint-box';
|
||||||
|
}
|
||||||
if (!type) {
|
if (!type) {
|
||||||
showToast('danger', '错误', '无法识别的标注类型');
|
showToast('danger', '错误', '无法识别的标注类型');
|
||||||
return;
|
return;
|
||||||
|
@ -449,8 +471,8 @@ const ImageAnnotation: React.FC = () => {
|
||||||
}, [currentFileResult, imageMetaData, showToast]);
|
}, [currentFileResult, imageMetaData, showToast]);
|
||||||
|
|
||||||
const handleToolSelect = useCallback((id: string) => {
|
const handleToolSelect = useCallback((id: string) => {
|
||||||
setCurrentTool(toolsMap[id]);
|
setCurrentTool(STR_TO_TOOL[id]);
|
||||||
}, [toolsMap]);
|
}, [STR_TO_TOOL]);
|
||||||
const handleToolClick = useCallback((id: string) => {
|
const handleToolClick = useCallback((id: string) => {
|
||||||
if (id === 'open') {
|
if (id === 'open') {
|
||||||
handleOpen();
|
handleOpen();
|
||||||
|
@ -481,29 +503,31 @@ const ImageAnnotation: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
useHotkey([
|
||||||
// 如果正在输入文本,不处理快捷键
|
{
|
||||||
console.log(e.target);
|
key: 's',
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
ctrl: true,
|
||||||
return;
|
callback: handleSave
|
||||||
}
|
},
|
||||||
|
{
|
||||||
// 处理 Ctrl + S 保存快捷键
|
key: 'v',
|
||||||
if (e.ctrlKey && e.key.toLowerCase() === 's') {
|
single: true,
|
||||||
e.preventDefault(); // 阻止浏览器默认的保存行为
|
callback: () => setCurrentTool(Tools.Drag)
|
||||||
handleSave();
|
},
|
||||||
return;
|
{
|
||||||
}
|
key: 't',
|
||||||
|
single: true,
|
||||||
const key = e.key.toLowerCase();
|
callback: () => setCurrentTool(Tools.Template)
|
||||||
switch (key) {
|
},
|
||||||
case 'v':
|
{
|
||||||
setCurrentTool(EditorTool.Drag);
|
key: 'b',
|
||||||
break;
|
single: true,
|
||||||
case 'r':
|
callback: () => setCurrentTool(Tools.HintBox)
|
||||||
setCurrentTool(EditorTool.Rect);
|
},
|
||||||
break;
|
{
|
||||||
case 'delete':
|
key: 'delete',
|
||||||
|
single: true,
|
||||||
|
callback: () => {
|
||||||
if (selectedAnnotation) {
|
if (selectedAnnotation) {
|
||||||
handleAnnotationChange({
|
handleAnnotationChange({
|
||||||
currentTool: EditorTool.Drag,
|
currentTool: EditorTool.Drag,
|
||||||
|
@ -513,16 +537,9 @@ const ImageAnnotation: React.FC = () => {
|
||||||
});
|
});
|
||||||
setSelectedAnnotation(null);
|
setSelectedAnnotation(null);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}, [selectedAnnotation, handleAnnotationChange, handleSave]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [handleKeyDown]);
|
|
||||||
|
|
||||||
const properties = usePropertyGridData(
|
const properties = usePropertyGridData(
|
||||||
selectedAnnotation,
|
selectedAnnotation,
|
||||||
|
@ -547,7 +564,7 @@ const ImageAnnotation: React.FC = () => {
|
||||||
<DragArea onImageLoad={handleImageLoad}>
|
<DragArea onImageLoad={handleImageLoad}>
|
||||||
<ImageEditor
|
<ImageEditor
|
||||||
image={imageUrl}
|
image={imageUrl}
|
||||||
tool={currentTool}
|
tool={TOOL_TO_EDITOR_TOOL[currentTool]}
|
||||||
annotations={imageMetaData.annotations}
|
annotations={imageMetaData.annotations}
|
||||||
onAnnotationChanged={handleAnnotationChange}
|
onAnnotationChanged={handleAnnotationChange}
|
||||||
onAnnotationSelected={handleAnnotationSelect}
|
onAnnotationSelected={handleAnnotationSelect}
|
||||||
|
|
Loading…
Reference in New Issue