import styled, { css } from 'styled-components'; import { useCallback, useMemo } from 'react'; import { EllipsisText } from '@labelu/components-react'; import type { AnnotationData, ToolName } from '@labelu/image'; import { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'; import { ReactComponent as DeleteIcon } from '@/assets/icons/delete.svg'; import { ReactComponent as VisibilityIcon } from '@/assets/icons/visibility.svg'; import { ReactComponent as VisibilityOffIcon } from '@/assets/icons/visibility-off.svg'; // tool icons import { ReactComponent as PointToolIcon } from '@/assets/tools/point.svg'; import { ReactComponent as LineToolIcon } from '@/assets/tools/line.svg'; import { ReactComponent as RectToolIcon } from '@/assets/tools/rect.svg'; import { ReactComponent as PolygonToolIcon } from '@/assets/tools/polygon.svg'; import { ReactComponent as CuboidToolIcon } from '@/assets/tools/cuboid.svg'; import { ReactComponent as RelationToolIcon } from '@/assets/tools/relation.svg'; import { ReactComponent as UnknownIcon } from '@/assets/tools/unknown.svg'; import { openAttributeModal } from '@/LabelSection'; import type { AnnotationDataInUI } from '@/context/annotation.context'; import { useAnnotationCtx } from '@/context/annotation.context'; import { useTool } from '@/context/tool.context'; const ToolIconMapping: Record< ToolName, React.FunctionComponent< React.SVGProps & { title?: string | undefined; } > > = { point: PointToolIcon, line: LineToolIcon, rect: RectToolIcon, polygon: PolygonToolIcon, cuboid: CuboidToolIcon, relation: RelationToolIcon, }; interface AttributeItemProps { annotation: AnnotationDataInUI; order: number; labelText: string; color: string; active?: boolean; } export const Action = styled.div` display: flex; gap: 0.5rem; align-items: center; font-size: 1.125rem; color: #999; svg { opacity: 0; cursor: pointer; &:hover { color: var(--color-primary); } &:last-child { &:hover { color: red; } } } `; export const Header = styled.div` display: flex; align-items: center; justify-content: space-between; &:hover svg { opacity: 1; } `; const InnerHeader = styled.div` display: flex; align-items: center; gap: 0.5rem; svg { font-size: 1.25rem; flex-shrink: 0; } `; // @ts-ignore const StyledVisibilityOffIcon = styled(VisibilityOffIcon)` opacity: 1 !important; `; const AsideAttributeWrapper = styled.div<{ active?: boolean }>` display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem 1rem 0.5rem 2rem; cursor: pointer; &:hover svg { opacity: 1; } &:hover ${InnerHeader} { ${({ color }) => css` color: ${color}; `} } &:last-child { margin-bottom: 0; } ${({ active }) => active && css` background-color: #f5f5f5; `} `; export interface AttributeActionProps { annotation?: AnnotationDataInUI; annotations?: AnnotationDataInUI[]; showEdit?: boolean; } export function AttributeAction({ annotation, annotations, showEdit = true }: AttributeActionProps) { const { engine, requestEdit, labelMapping, currentTool } = useTool(); const { onAnnotationChange, onAnnotationsChange, onAnnotationRemove, onAnnotationsRemove, disabled } = useAnnotationCtx(); const requestEditable = useCallback(() => { if (annotation) { return typeof requestEdit === 'function' ? requestEdit?.('update', { label: annotation.label, toolName: annotation.tool }) : true; } if (annotations) { for (const item of annotations) { if (typeof requestEdit === 'function' && !requestEdit('update', { label: item.label, toolName: item.tool })) { return false; } } return true; } return false; }, [annotation, annotations, requestEdit]); const annotationsMapping = useMemo(() => { if (!annotations) { return {}; } return annotations.reduce((acc, item) => { acc[item.id] = item; return acc; }, {} as Record); }, [annotations]); const visible = useMemo(() => { if (annotation) { return typeof annotation.visible === 'undefined' ? true : Boolean(annotation.visible); } if (annotations) { return annotations.every((item) => (typeof item.visible === 'undefined' ? true : item.visible)); } return true; }, [annotation, annotations]); const handleEditClick = (e: React.MouseEvent) => { engine.selectAnnotation(annotation!.tool, annotation!.id); const secondaryEditable = typeof requestEdit === 'function' ? requestEdit('update', { label: annotation!.label, toolName: annotation!.tool }) : true; if (disabled || !annotation?.label || !secondaryEditable) { return; } const currentLabelConfig = labelMapping[annotation.tool][annotation.label]; const defaultLabelConfig = Object.values(labelMapping[annotation.tool])[0]; // 如果预标注的标签不在用户配置中,打开编辑框时默认选中用户配置的第一个标签 if (currentTool === annotation!.tool && !currentLabelConfig) { engine.setLabel(defaultLabelConfig.value); } // 等selectedAnnotation状态更新了再打开,因为Form里的值是从selectedAnnotation里取的 setTimeout(() => { openAttributeModal({ labelValue: annotation!.label, engine, e, openModalAnyway: true, labelConfig: currentLabelConfig || defaultLabelConfig, }); }); }; const toggleOneVisibility = (value: boolean) => (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!annotation) { return; } engine?.toggleAnnotationsVisibility(annotation.tool, [annotation.id], value); onAnnotationChange({ ...annotation, visible: value }); }; const toggleBatchVisibility = (value: boolean) => (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!annotations) { return; } const toolIds: Record = {} as Record; annotations.forEach((item) => { if (!toolIds[item.tool]) { toolIds[item.tool] = []; } toolIds[item.tool].push(item.id); }); Object.keys(toolIds).forEach((tool) => { engine?.toggleAnnotationsVisibility(tool as ToolName, toolIds[tool as ToolName], value); }); onAnnotationsChange( annotations.map((item) => { if (annotationsMapping[item.id]) { return { ...item, visible: value }; } return item; }), ); }; // 单个标注 if (annotation) { return ( {showEdit && } {visible && } {!visible && } { if (disabled || !requestEditable()) { return; } engine?.removeAnnotationById(annotation.tool, annotation.id); onAnnotationRemove(annotation); }} /> ); } // 一组标注 return ( {showEdit && } {visible && } {!visible && } { if (disabled || !requestEditable()) { return; } annotations?.forEach((item) => { engine?.removeAnnotationById(item.tool, item.id); }); onAnnotationsRemove(annotations!); }} /> ); } export default function AsideAttributeItem({ annotation, active, order, labelText, color }: AttributeItemProps) { const { engine } = useTool(); const ToolIcon = ToolIconMapping[annotation.tool as ToolName] ?? UnknownIcon; return ( { engine?.selectAnnotation(annotation.tool, annotation.id); }} >
{order}.
{labelText}
); }