Files
labelmain-demo/components/label/components/RightGroupTools.tsx
2026-02-03 18:05:47 +08:00

470 lines
17 KiB
TypeScript

"use client"
import {
ActionIcon,
Box,
Collapse,
Flex,
Group,
Paper,
ScrollArea,
Stack,
Switch,
Text,
Title,
Tooltip,
} from "@mantine/core"
import { ChevronDown, ChevronUp, Link2Off, Pencil, Trash2 } from "lucide-react"
import { useCallback, useState } from "react"
import { Project } from "../api/project/typing"
import { LabelState } from "../LabelNossr"
import { useKeyEventStore, useLabelStore, useObjectStore } from "../store"
import { useBottomToolsStore } from "../useBottomToolsStore"
import { useOtherToolsStore } from "../useOtherToolsStore"
import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore"
import { renderGroupPath } from "../useRenderGroupPath"
import { useRightToolsStore } from "../useRightToolsStore"
import { useTopToolsStore } from "../useTopToolsStore"
import { labelTypeMap } from "../utils/constants"
import RightGroupEditModal from "./RightGroupEditModal"
interface RightGroupToolsComponentProps {
labelState: LabelState
projectDetail?: Project.DetailResponse
}
const RightGroupTools: React.FC<RightGroupToolsComponentProps> = (props) => {
const { projectDetail } = props
const [open, setOpen] = useState(false)
const [editId, setEditId] = useState<number | null>(null)
const getOperationSchema = useCallback(
(operationId: string) => {
return (
projectDetail?.label_schema_list?.find(
(item) => item.category_id === +operationId
) || null
)
},
[projectDetail?.label_schema_list]
)
const { setLabel, pushStateStack } = useLabelStore()
const { pathGroupMap, groupPathVisible, setGroupPathVisible } =
useRightToolsStore()
const { activeImage } = useBottomToolsStore()
const { groupStatus, updateGroupStatus, selectedPath, updateSelectedPath } =
useObjectStore()
const { getItemById, getItemsById } = usePaperStore()
const { shift: pressShift } = useKeyEventStore()
const [isGroupCollapsed, setIsGroupCollapsed] = useState<boolean>(true)
const handleSelect = useCallback(
(id: number) => {
let operationId = getItemById(id)?.data.operationId
if (selectedPath[activeImage]?.length) {
if (!selectedPath[activeImage]?.includes(id)) {
if (pressShift) {
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
item.fillColor = item.data?.fillColor
})
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 {
selectedPath[activeImage].forEach((selectedId) => {
if (getItemsById(selectedId)?.length) {
getItemsById(selectedId).forEach((item) => {
item.fillColor = item.data?.blankColor
})
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: activeImage,
operationId: operationId,
id,
})
}
})
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
item.fillColor = item.data?.fillColor
})
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 {
updateSelectedPath(activeImage, "DELETE", id)
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
item.fillColor = item.data?.blankColor
})
useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: activeImage,
operationId: operationId,
id,
})
}
}
} else {
updateSelectedPath(activeImage, "ADD", id)
if (getItemsById(id)?.length) {
getItemsById(id).forEach((item) => {
item.fillColor = item.data?.fillColor
})
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,
]
)
const getOperationIdByPathId = (pathId: number) => {
let pathItem = getItemById(pathId)
return pathItem?.data?.operationId || ""
}
const unlinkGroup = (id: number) => {
const group = usePaperStore.getState().group
const imageId = useBottomToolsStore.getState().activeImage
const groupMap =
useRightToolsStore.getState().pathGroupMap.get(imageId) ??
new Map<number, number[]>()
if (!group) return
if (!Array.from(groupMap.values()).length) return
// update children path data
const currentGroupChildrenIds = groupMap.get(id) ?? []
const needUpdateOperationIds: any[] = []
currentGroupChildrenIds?.forEach((childId, index) => {
const childPath = usePaperStore.getState().getItemById(childId)!
childPath.data.parentGroupId = null
if (childPath.data.operationId && !needUpdateOperationIds[index])
needUpdateOperationIds[index] = childPath.data.operationId
})
// handle labeldata update
const currentData = structuredClone(useLabelStore.getState().label)
needUpdateOperationIds.forEach((operationId, index) => {
if (currentData.get(imageId)?.get(operationId)?.length) {
let result =
currentData
.get(imageId)
?.get(operationId)
?.map((item) =>
item[0] !== currentGroupChildrenIds[index]
? item
: [
item[0],
item[1],
{ ...item[2], parentGroupId: null },
item[3],
]
) || []
currentData.get(imageId)?.set(operationId, result as any)
}
})
// update group tool
useRightToolsStore.getState().removePathGroupInMap(imageId, id)
// setlabeldata
setLabel(currentData)
pushStateStack(currentData)
}
const deleteGroup = (id: number) => {
const group = usePaperStore.getState().group
const imageId = useBottomToolsStore.getState().activeImage
const groupMap =
useRightToolsStore.getState().pathGroupMap.get(imageId) ??
new Map<number, number[]>()
if (!group) return
if (!Array.from(groupMap.values()).length) return
// remove group children
const currentGroupChildrenIds = groupMap.get(id) ?? []
const needRemoveOperationIds: any[] = []
currentGroupChildrenIds?.forEach((childId, index) => {
const childPaths = group.getItems({ data: { id: childId } })
childPaths.forEach((p) => {
p.remove()
if (p.data.operationId && !needRemoveOperationIds[index])
needRemoveOperationIds[index] = p.data.operationId
})
})
const currentData = structuredClone(useLabelStore.getState().label)
needRemoveOperationIds.forEach((operationId, index) => {
if (currentData.get(imageId)?.get(operationId)?.length) {
let result =
currentData
.get(imageId)
?.get(operationId)
?.filter((item) => item[0] !== currentGroupChildrenIds[index]) || []
currentData.get(imageId)?.set(operationId, result)
}
})
// update group tool
useRightToolsStore.getState().removePathGroupInMap(imageId, id)
setLabel(currentData)
pushStateStack(currentData)
}
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>
<ActionIcon
variant="subtle"
onClick={() => setIsGroupCollapsed(!isGroupCollapsed)}>
{isGroupCollapsed ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</ActionIcon>
</Flex>
<Collapse
in={isGroupCollapsed}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<Flex direction="column" h="100%">
<Box px="md" py="xs">
<Group gap="xs">
<Text size="sm"></Text>
<Switch
onLabel="是"
offLabel="否"
checked={groupPathVisible}
onChange={(event) => {
setGroupPathVisible(event.currentTarget.checked)
renderGroupPath()
}}
/>
</Group>
</Box>
<ScrollArea style={{ flex: 1 }} px="md">
<Stack gap="xs" pb="md">
{pathGroupMap.get(activeImage) &&
Array.from(pathGroupMap.get(activeImage)!.keys())?.map(
(groupId) => {
const isOpen =
!groupStatus[activeImage + groupId]?.["COLLAPSE"]
return (
<Box key={groupId} w="100%">
<Paper withBorder p="xs" radius="md" shadow="sm">
<Flex justify="space-between" align="center">
<Group>
<Text size="sm" ff="mono">
@{groupId}
</Text>
<Group
gap={4}
display={
useTopToolsStore.getState().isView
? "none"
: "flex"
}>
<Tooltip label="编辑群组">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => {
setOpen(true)
setEditId(groupId)
}}>
<Pencil size={14} />
</ActionIcon>
</Tooltip>
<Tooltip label="解除群组">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => {
unlinkGroup(groupId)
}}>
<Link2Off size={14} />
</ActionIcon>
</Tooltip>
<Tooltip label="删除群组">
<ActionIcon
variant="subtle"
color="red"
size="sm"
onClick={() => {
deleteGroup(groupId)
}}>
<Trash2 size={14} />
</ActionIcon>
</Tooltip>
</Group>
</Group>
<Group gap="xs">
<Text size="sm">
{pathGroupMap.get(activeImage)?.get(groupId)
?.length || 0}
</Text>
<ActionIcon
variant="subtle"
size="sm"
onClick={() => {
updateGroupStatus(
activeImage + groupId,
"COLLAPSE",
isOpen
)
}}>
{isOpen ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)}
</ActionIcon>
</Group>
</Flex>
</Paper>
<Collapse in={isOpen}>
<Stack gap="xs" mt="xs" pl="xs">
{pathGroupMap
.get(activeImage)
?.get(groupId)
?.map((child) => {
const isSelected =
selectedPath[activeImage]?.includes(child)
return (
<Paper
withBorder
p="xs"
radius="md"
shadow="sm"
key={child}
bg={
isSelected
? "var(--mantine-primary-color-filled)"
: undefined
}
c={isSelected ? "white" : undefined}
onClick={() => {
handleSelect(child)
}}
style={{ cursor: "pointer" }}>
<Flex
justify="flex-end"
align="center"
gap="md">
<Text size="sm">
{child}-
{labelTypeMap.get(
getOperationSchema(
getOperationIdByPathId(child)
)?.label_type || 0
)}
</Text>
</Flex>
</Paper>
)
})}
</Stack>
</Collapse>
</Box>
)
}
)}
</Stack>
</ScrollArea>
</Flex>
</Collapse>
</Flex>
{open && (
<RightGroupEditModal
open={open}
editId={editId}
afterChange={(val: number) => {
setEditId(val)
}}
closeModal={() => {
setOpen(false)
setEditId(null)
}}
/>
)}
</Box>
)
}
export default RightGroupTools