Files
labelmain/components/label/components/RightObjectTools.tsx
2026-03-04 16:00:20 +08:00

956 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 <Settings size={20} style={style} />
}
case "关键点": {
return <CircleDot size={20} style={style} />
}
case "多线段": {
return <Spline size={20} style={style} />
}
case "2D框": {
return <RectangleHorizontal size={20} style={style} />
}
default:
return <Settings size={20} />
}
} else return <Settings size={20} />
}
const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
const { labelState, projectDetail, taskDetail } = props
const storeLabel = useLabelStore((state) => state.label)
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 (
// <span>
// {beforeStr}
// <span className="text-primary">{input}</span>
// {afterStr}
// </span>
// );
// }, []);
// 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 (
<Box h="100%" w="100%">
<Flex direction="column" h="100%" gap="xs">
<Flex justify="space-between" align="center" px="md" py="xs">
<Title order={4} size="h4">
</Title>
<Group gap="xs">
<TextInput
w={96}
size="xs"
placeholder="输入ID"
value={searchId}
onChange={(e) => setSearchId(e.target.value)}
onKeyDown={handleSelectedBySearchId}
/>
<Group gap={4}>
{[3, 2, 1, 0].map((type, index) => (
<Checkbox
key={type}
id={type + ""}
color={getCheckBoxColor(type)}
checked={checkBoxsChecked[index]}
onChange={(e) => {
setCheckBoxsChecked((pre) => {
const arr = [...pre]
arr[index] = e.target.checked
return arr
})
}}
size="xs"
/>
))}
</Group>
<ActionIcon
variant="subtle"
onClick={() => {
updateImageStatus(
activeImage,
"COLLAPSE",
!imageStatus[activeImage]?.["COLLAPSE"]
)
}}>
{!imageStatus[activeImage]?.["COLLAPSE"] ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</ActionIcon>
</Group>
</Flex>
<Collapse
in={!imageStatus[activeImage]?.["COLLAPSE"]}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<ScrollArea style={{ flex: 1 }} px="md">
<Stack gap="xs" pb="md">
{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 (
<Box key={operationId} w="100%">
<Paper withBorder p="xs" radius="md" shadow="sm">
<Flex justify="space-between" align="center">
<Group gap="xs">
<ActionIcon
variant="subtle"
size="sm"
onClick={() =>
operationHide(operationId, !isHide)
}>
{!isHide ? (
<Eye size={16} />
) : (
<EyeOff size={16} />
)}
</ActionIcon>
{renderOperationIcon(
getOperationSchema(operationId)
)}
<Text
size="sm"
c={hasSubAttrError ? "red" : undefined}>
{renderShortcutKey(operationId)}
</Text>
</Group>
<Text
size="sm"
c={hasSubAttrError ? "red" : undefined}>
{getOperationSchema(operationId)?.label_class}
</Text>
<Group gap="xs">
<TextInput
w={96}
size="xs"
placeholder="搜索子属性"
value={searchSubAttrs[operationId] || ""}
onChange={(e) => {
setSearchSubAttrs((searchValues) => ({
...searchValues,
[operationId]: e.target.value,
}))
}}
onKeyDown={(e) =>
handleSelectedBySearchSubAttr(e, operationId)
}
/>
<Text size="sm">
{storeLabel.get(activeImage)?.get(operationId)
?.length || 0}
</Text>
<ActionIcon
variant="subtle"
size="sm"
onClick={() => {
updateOperationStatus(
activeImage + operationId,
"COLLAPSE",
!isCollapsed
)
}}>
{!isCollapsed ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</ActionIcon>
</Group>
</Flex>
</Paper>
<Collapse in={!isCollapsed}>
<Stack gap="xs" mt="xs" pl="xs">
{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 (
<Paper
key={child[0]}
withBorder
p="xs"
radius="md"
shadow="sm"
bg={
isSelected
? "var(--mantine-primary-color-filled)"
: undefined
}
c={isSelected ? "white" : undefined}
onClick={() =>
handleSelect(child[0], operationId)
}
style={{ cursor: "pointer" }}>
<Flex justify="space-between" align="center">
<Group gap="xs">
<ActionIcon
variant="subtle"
size="sm"
c={isSelected ? "white" : undefined}
onClick={(e) => {
e.stopPropagation()
objectHide(
operationId,
child[0],
!isChildHide
)
}}>
{!isChildHide ? (
<Eye size={16} />
) : (
<EyeOff size={16} />
)}
</ActionIcon>
<Text size="sm" c={textColor}>
{child[0]?.toString()}
</Text>
<Tooltip
label={
<Stack gap={0}>
<Group gap={4}>
<Text size="xs"></Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value ===
child[2]?.uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]?.create_timestamp ||
0) * 1000
).format("YYYY-MM-DD HH:mm:ss")}
</Text>
</Group>
{child[2]?.first_modified_uid && (
<Group gap={4}>
<Text size="xs">
</Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value ===
child[2]
?.first_modified_uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]
?.first_modified_timestamp ||
0) * 1000
).format(
"YYYY-MM-DD HH:mm:ss"
)}
</Text>
</Group>
)}
{child[2]?.last_modified_uid && (
<Group gap={4}>
<Text size="xs">
</Text>
<Text size="xs">
{
userOpts.find(
(item) =>
item.value ===
child[2]
?.last_modified_uid
)?.label
}
</Text>
<Text size="xs">
{dayjs(
(child[2]
?.last_modified_timestamp ||
0) * 1000
).format(
"YYYY-MM-DD HH:mm:ss"
)}
</Text>
</Group>
)}
</Stack>
}
multiline
bg="white"
c="black"
withArrow>
<Info
size={16}
style={{ cursor: "pointer" }}
color={isSelected ? "white" : "black"}
/>
</Tooltip>
</Group>
{comment ? (
<Group gap={4}>
{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 (
<Popover
key={`${child[0]}${c.turn}${c.times}`}
position="left"
withArrow>
<Popover.Target>
<Badge
size="xs"
circle
color={badgeColor}
style={{
cursor: "pointer",
}}>
{count}
</Badge>
</Popover.Target>
<Popover.Dropdown>
<Text size="xs" c={badgeColor}>
{`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"
)
: ""
}`}
</Text>
</Popover.Dropdown>
</Popover>
)
})}
</Group>
) : null}
</Flex>
</Paper>
)
})}
</Stack>
</Collapse>
</Box>
)
})}
</Stack>
</ScrollArea>
</Collapse>
</Flex>
</Box>
)
}
export default RightObjectTools