feat(devtool): 图像标注器支持标注 HintBox

This commit is contained in:
XcantloadX 2025-02-10 19:11:37 +08:00
parent 7365153e69
commit c4be314241
1 changed files with 65 additions and 48 deletions

View File

@ -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}