"use client" import { ActionIcon, Badge, Box, Checkbox, Collapse, Flex, Group, Paper, Popover, ScrollArea, Stack, Text, TextInput, Title, Tooltip, } from "@mantine/core" import dayjs from "dayjs" import { ChevronDown, ChevronUp, CircleDot, Eye, EyeOff, Info, RectangleHorizontal, Settings, Spline, } from "lucide-react" import paper from "paper" import { useCallback, useState } from "react" import { Project } from "../api/project/typing" import { Task } from "../api/task/typing" import { LabelState } from "../LabelNossr" import { useKeyEventStore, useLabelStore, useObjectStore } from "../store" import { useAllUserStore } from "../store/auth" import { useBottomToolsStore } from "../useBottomToolsStore" import { useOtherToolsStore } from "../useOtherToolsStore" import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore" import { usePaperSupportStore } from "../usePaperSupportStore" import { useTopToolsStore } from "../useTopToolsStore" import { getSubAttrStatus } from "../util" import { labelTypeMap } from "../utils/constants" interface RightObjectToolsComponentProps { labelState: LabelState projectDetail?: Project.DetailResponse taskDetail?: Task.DataProps } export const renderOperationIcon = ( operationSchema: Project.LabelSchemaList | null ) => { if (operationSchema) { let color = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})` const style = { color } switch (operationSchema && labelTypeMap.get(operationSchema.label_type)) { case "多边形": { return } case "关键点": { return } case "多线段": { return } case "2D框": { return } default: return } } else return } const RightObjectTools: React.FC = (props) => { const { labelState, projectDetail, taskDetail } = props const { label: storeLabel } = useLabelStore() const { activeImage } = useBottomToolsStore() const { operationStatus, pathStatus, selectedPath, imageStatus, updateImageStatus, updateOperationStatus, updatePathStatus, updateSelectedPath, } = useObjectStore() const { userOpts } = useAllUserStore() const [searchId, setSearchId] = useState("") const [searchSubAttrs, setSearchSubAttrs] = useState<{ [x: string]: string }>({}) const [checkBoxsChecked, setCheckBoxsChecked] = useState([ true, true, true, true, ]) const getOperationSchema = useCallback( (operationId: string) => { return ( projectDetail?.label_schema_list?.find( (item) => item.category_id === +operationId ) || null ) }, [projectDetail?.label_schema_list] ) const renderShortcutKey = (operationId: string) => { if (+operationId > 10) { return "ctrl+" + (+operationId % 10).toString() } else { return (+operationId % 10).toString() } } const getCheckBoxColor = useCallback((type: number) => { switch (type) { case 3: return "yellow" case 2: return "red" case 1: return "green" case 0: return "gray" default: return "blue" } }, []) const { shift: pressShift } = useKeyEventStore() const { getItemById, getItemsById } = usePaperStore() // 切换选中对象时,清除所有其他对象的当前选中状态 const clearSelectedItems = () => { usePaperStore .getState() .paperScope?.project.selectedItems.forEach((item) => { item.data.selected = false }) // const group = usePaperStore.getState().group!; // const needClear = [ // ...group.getItems({ data: { type: "mask" } }), // ...group.getItems({ data: { type: "text" } }), // ]; // needClear.forEach((item) => { // item.remove(); // }); usePaperSupportStore.getState().clearAll() } const handleSelectedPath = (path: paper.Path) => { console.log("handleSelectedPath", path) if (path.data.type === "rectangle") { path.data.selected = true usePaperSupportStore.getState().handleBufferPaths(path) usePaperSupportStore.getState().handleCircles(path) path.fillColor = path.data.fillColor } else if (path.data.type === "brush") { path.data.selected = true usePaperSupportStore.getState().handleMask(path) usePaperSupportStore.getState().handleBufferPaths(path) usePaperSupportStore.getState().handleCircles(path) } else if (path.data.type === "point") { path.data.selected = true usePaperSupportStore.getState().handleMask(path) usePaperSupportStore.getState().handleText(path) usePaperSupportStore.getState().handleBufferPaths(path) usePaperSupportStore.getState().handleCircles(path) } else if (path.data.type === "polygon") { path.fillColor = path.data.fillColor } path.bringToFront() } const cancelSelectedPathColor = (path: paper.Path | paper.CompoundPath) => { if (path.data.type === "rectangle" || path.data.type === "polygon") path.fillColor = path.data.blankColor } const handleSelect = useCallback( (id: number, operationId: any) => { if (selectedPath[activeImage]?.length) { if (!selectedPath[activeImage]?.includes(id)) { if (pressShift) { if (getItemsById(id)?.length) { getItemsById(id).forEach((item) => { if (item instanceof paper.Path) { handleSelectedPath(item) } else if (item instanceof paper.CompoundPath) { if (item.data.type === "polygon") item.fillColor = item.data.fillColor else item.data.selected = true } }) let category = useTopToolsStore .getState() .objectOperations.find( (item) => item.category_id.toString() === operationId ) const globalPosition = getItemById(id)!.globalMatrix.transform( getItemById(id)!.position ) const { top, left } = getShowContextMenuPosition( globalPosition, category! ) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: true, position: { top, left }, imageId: activeImage, operationId: operationId, id, }) } } else { clearSelectedItems() selectedPath[activeImage].forEach((selectedId) => { if (getItemsById(selectedId)?.length) { getItemsById(selectedId).forEach((item) => { cancelSelectedPathColor(item) }) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: false, position: { top: 0, left: 0, }, imageId: activeImage, operationId: operationId, id, }) } }) if (getItemsById(id)?.length) { getItemsById(id).forEach((item) => { if (item instanceof paper.Path) { handleSelectedPath(item) } else if (item instanceof paper.CompoundPath) { if (item.data.type === "polygon") item.fillColor = item.data.fillColor else item.data.selected = true } }) let category = useTopToolsStore .getState() .objectOperations.find( (item) => item.category_id.toString() === operationId ) const globalPosition = getItemById(id)!.globalMatrix.transform( getItemById(id)!.position ) const { top, left } = getShowContextMenuPosition( globalPosition, category! ) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: true, position: { top, left }, imageId: activeImage, operationId: operationId, id, }) } } updateSelectedPath(activeImage, "ADD", id) } else { clearSelectedItems() updateSelectedPath(activeImage, "DELETE", id) if (getItemsById(id)?.length) { getItemsById(id).forEach((item) => { cancelSelectedPathColor(item) }) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: false, position: { top: 0, left: 0, }, imageId: activeImage, operationId: operationId, id, }) } } } else { clearSelectedItems() updateSelectedPath(activeImage, "ADD", id) if (getItemsById(id)?.length) { getItemsById(id).forEach((item) => { if (item instanceof paper.Path) { handleSelectedPath(item) } else if (item instanceof paper.CompoundPath) { if (item.data.type === "polygon") item.fillColor = item.data.fillColor else item.data.selected = true } // if (item.data.type === "rectangle") { // item.data.selected = true; // usePaperSupportStore.getState().handleCircles(item); // } // item.fillColor = item.data?.fillColor; // item.bringToFront(); }) let category = useTopToolsStore .getState() .objectOperations.find( (item) => item.category_id.toString() === operationId ) const globalPosition = getItemById(id)!.globalMatrix.transform( getItemById(id)!.position ) const { top, left } = getShowContextMenuPosition( globalPosition, category! ) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: true, position: { top, left }, imageId: activeImage, operationId: operationId, id, }) } } }, [ activeImage, getItemById, getItemsById, pressShift, selectedPath, updateSelectedPath, ] ) // 根据img operationId 更新操作栏operation状态 const checkIfOperationNeedsUpdate = (imgId: string, operationId: any) => { const operationMap = storeLabel.get(imgId) if (operationMap) { const objList = operationMap.get(operationId) if (objList && objList.length) { let hideFlag = true objList.forEach(([id]) => { const objStatus = useObjectStore.getState().pathStatus[`${imgId}${id}`] if (objStatus && !objStatus["HIDE"]) { hideFlag = false } }) updateOperationStatus(imgId + operationId, "HIDE", hideFlag) } } } // 隐藏时清空当前选中的item选中状态 const setSelectedItemFalse = (id: number) => { if (!id) return const sameIdItems = usePaperStore.getState().getItemsById(id) sameIdItems.forEach((i) => { if (i.data.type === "mask" || i.data.type === "text") { i.remove() } if (i.data.selected) i.data.selected = false }) } const operationHide = useCallback( (operationId: string, hide: boolean) => { updateOperationStatus(activeImage + operationId, "HIDE", hide) storeLabel .get(activeImage) ?.get(operationId) ?.map((child) => { updatePathStatus(activeImage + child[0], "HIDE", hide) if (getItemsById(child[0]) && getItemsById(child[0]).length) { getItemsById(child[0]).forEach((item) => { item.visible = !hide }) // 隐藏时清空选中状态 if (hide) setSelectedItemFalse(child[0]) } }) }, [ activeImage, getItemsById, storeLabel, updateOperationStatus, updatePathStatus, ] ) const objectHide = useCallback( (operationId: string, objectId: number, hide: boolean) => { if (hide) { updatePathStatus(activeImage + objectId, "HIDE", hide) if (getItemsById(objectId) && getItemsById(objectId).length) { getItemsById(objectId).forEach((item) => (item.visible = !hide)) // 隐藏时清空选中状态 setSelectedItemFalse(objectId) } let childrenHideStatus = storeLabel .get(activeImage) ?.get(operationId) ?.map((child) => { return getItemById(child[0])!.visible }) if (childrenHideStatus?.findIndex((bool) => bool === true) === -1) { updateOperationStatus(activeImage + operationId, "HIDE", hide) } } else { updateOperationStatus(activeImage + operationId, "HIDE", hide) updatePathStatus(activeImage + objectId, "HIDE", hide) const configs = useTopToolsStore.getState().showTagsConfigs if (getItemsById(objectId) && getItemsById(objectId).length) getItemsById(objectId).forEach((item) => { let visible = !hide if (item.data.type === "tag") { if (item.data.tag_category === "mask") { visible = configs.includes("外框") } else if (item.data.tag_category === "point_text") { visible = configs.includes("点ID") } else { visible = true } } item.visible = visible }) } }, [ activeImage, getItemById, getItemsById, storeLabel, updateOperationStatus, updatePathStatus, ] ) // const renderObjectName = useCallback((name: string, input: string) => { // const index = name?.indexOf(input); // const beforeStr = name?.substring(0, index); // const afterStr = name?.slice(index + input.length); // return ( // // {beforeStr} // {input} // {afterStr} // // ); // }, []); // child 是否存在不符合要求的子属性 const getOperationSubAttrStatus = useCallback( (operationId: string) => { let bool = storeLabel .get(activeImage) ?.get(operationId) ?.some((child) => { return getSubAttrStatus( getOperationSchema(operationId), child[2]?.sub_attributes ) }) return bool }, [activeImage, getOperationSchema, storeLabel] ) // 搜索标注对象id const handleSelectedBySearchId = (e: any) => { if (e.key === "Enter") { if (searchId && !isNaN(Number(searchId))) { const id = Number(searchId) const labelObjMap = storeLabel.get(activeImage) Array.from(labelObjMap!.entries()).forEach(([operationId, objList]) => { objList.forEach((obj) => { if (id === obj[0]) { handleSelect(id, operationId) } }) }) } } } // 搜索标注对象子属性 const handleSelectedBySearchSubAttr = (e: any, currentOperationId: any) => { if (e.key === "Enter") { Array.from(storeLabel.entries()).forEach(([imgId, objectMap]) => { if (objectMap.get(currentOperationId)) { const list = objectMap.get(currentOperationId) list!.forEach((item) => { const { sub_attributes } = item[2] // 如果没有子属性搜索值,则把对象HIDE设为false if (!searchSubAttrs[currentOperationId]) { updatePathStatus(imgId + item[0], "HIDE", false) updateOperationStatus(imgId + currentOperationId, "HIDE", false) if (getItemsById(item[0])) getItemsById(item[0]).forEach((i) => { i.visible = true }) return } if (sub_attributes && Object.entries(sub_attributes).length) { let flag = true Object.entries(sub_attributes).forEach(([_k, value]) => { if (searchSubAttrs[currentOperationId] === value) { flag = false } }) if (flag) { updatePathStatus(imgId + item[0], "HIDE", true) if (getItemsById(item[0])) { getItemsById(item[0]).forEach((i) => { i.visible = false }) setSelectedItemFalse(item[0]) } } else { updatePathStatus(imgId + item[0], "HIDE", false) if (getItemsById(item[0])) getItemsById(item[0]).forEach((i) => { i.visible = true }) } } else { // 如果有子属性搜索值,但对象无子属性,则把对象HIDE设为true updatePathStatus(imgId + item[0], "HIDE", true) if (getItemsById(item[0])) { getItemsById(item[0]).forEach((i) => { i.visible = false }) setSelectedItemFalse(item[0]) } } }) checkIfOperationNeedsUpdate(imgId, currentOperationId) } }) } } return ( 对象列表 setSearchId(e.target.value)} onKeyDown={handleSelectedBySearchId} /> {[3, 2, 1, 0].map((type, index) => ( { setCheckBoxsChecked((pre) => { const arr = [...pre] arr[index] = e.target.checked return arr }) }} size="xs" /> ))} { updateImageStatus( activeImage, "COLLAPSE", !imageStatus[activeImage]?.["COLLAPSE"] ) }}> {!imageStatus[activeImage]?.["COLLAPSE"] ? ( ) : ( )} {labelState.get(activeImage) && labelState.get(activeImage)?.map((operationId) => { const isCollapsed = operationStatus[activeImage + operationId]?.["COLLAPSE"] const isHide = operationStatus[activeImage + operationId]?.HIDE const hasSubAttrError = getOperationSubAttrStatus(operationId) return ( operationHide(operationId, !isHide) }> {!isHide ? ( ) : ( )} {renderOperationIcon( getOperationSchema(operationId) )} {renderShortcutKey(operationId)} {getOperationSchema(operationId)?.label_class} { setSearchSubAttrs((searchValues) => ({ ...searchValues, [operationId]: e.target.value, })) }} onKeyDown={(e) => handleSelectedBySearchSubAttr(e, operationId) } /> {storeLabel.get(activeImage)?.get(operationId) ?.length || 0} { updateOperationStatus( activeImage + operationId, "COLLAPSE", !isCollapsed ) }}> {!isCollapsed ? ( ) : ( )} {storeLabel .get(activeImage) ?.get(operationId) ?.map((child) => { const { comment } = child[2] let visibleFlag = true if (comment && comment.length) { const latest_comment = (taskDetail && taskDetail.label_status === 2) || comment.length === 1 ? comment[comment.length - 1] : comment[comment.length - 2] let type = latest_comment.comment_type if (type === 2) { const { last_modified_timestamp } = child[2] if ( last_modified_timestamp && last_modified_timestamp > latest_comment.date ) type = 3 } visibleFlag = checkBoxsChecked[3 - type] } if (checkBoxsChecked.every((value) => !value)) visibleFlag = true const isSelected = selectedPath[ activeImage ]?.includes(child[0]) const isChildHide = pathStatus[activeImage + child[0]]?.["HIDE"] const hasChildError = getSubAttrStatus( getOperationSchema(operationId), child[2]?.sub_attributes ) let textColor = undefined if (hasChildError) { textColor = "red" } else if (isSelected) { textColor = "white" } if (!visibleFlag) return null return ( handleSelect(child[0], operationId) } style={{ cursor: "pointer" }}> { e.stopPropagation() objectHide( operationId, child[0], !isChildHide ) }}> {!isChildHide ? ( ) : ( )} {child[0]?.toString()} 创建: { userOpts.find( (item) => item.value === child[2]?.uid )?.label } {dayjs( (child[2]?.create_timestamp || 0) * 1000 ).format("YYYY-MM-DD HH:mm:ss")} {child[2]?.first_modified_uid && ( 首次修改: { userOpts.find( (item) => item.value === child[2] ?.first_modified_uid )?.label } {dayjs( (child[2] ?.first_modified_timestamp || 0) * 1000 ).format( "YYYY-MM-DD HH:mm:ss" )} )} {child[2]?.last_modified_uid && ( 最新修改: { userOpts.find( (item) => item.value === child[2] ?.last_modified_uid )?.label } {dayjs( (child[2] ?.last_modified_timestamp || 0) * 1000 ).format( "YYYY-MM-DD HH:mm:ss" )} )} } multiline bg="white" c="black" withArrow> {comment ? ( {comment.map((c: any) => { const count = c.check_box_comments.length + c.text_comments.length const { last_modified_timestamp } = child[2] let flag = false if ( count > 0 && last_modified_timestamp && last_modified_timestamp > c.date ) flag = true let badgeColor = "gray" if (c.comment_type === 1) badgeColor = "green" if (c.comment_type === 2) badgeColor = "red" if (flag) badgeColor = "yellow" return ( {count} {`P${child[2].image_id}:${ child[2].uid }:${c.check_box_comments.join( "," )},${c.text_comments.join( "," )} ${ c.date ? dayjs( c.date * 1000 ).format( "YYYY-MM-DD HH:mm:ss" ) : "" }`} ) })} ) : null} ) })} ) })} ) } export default RightObjectTools