Files
labelmain/components/label/LabelNossr.tsx
2026-02-27 16:08:42 +08:00

1438 lines
44 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 {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from "react"
import ScaleComponent from "./components/ScaleComponent"
import { processVideo } from "@/app/component/label/video2image/processVideo"
import { Box, Flex, LoadingOverlay, Stack } from "@mantine/core"
import { showNotification } from "@mantine/notifications"
import { getLabelResult, getServerImage } from "./api/label"
import {
Comment,
ImageObjects,
LabelResult,
WorkLoad,
} from "./api/label/typing"
import { getProjectDetail } from "./api/project"
import { Project } from "./api/project/typing"
import { getTaskList, getTaskWorkFlow } from "./api/task"
import { Task } from "./api/task/typing"
import BottomTools from "./components/BottomTools"
import ConfirmModal from "./components/ConfirmModal"
import { splitWord } from "./components/EditorContainer"
import PaperContainer from "./components/PaperContainer"
import RightDescTools from "./components/RightDescTools"
import RightGroupTools from "./components/RightGroupTools"
import RightObjectTools from "./components/RightObjectTools"
import RightQATools from "./components/RightQATools"
import RightTaskTools from "./components/RightTaskTools"
import ScaleToolContainer from "./components/ScaleToolContainer"
import TopTools from "./components/TopTools"
import {
useImagesStore,
useKeyEventStore,
useLabelStore,
useObjectStore,
useVideoFrameStore,
} from "./store"
import { usePermissionStore } from "./store/auth"
import { useBottomToolsStore } from "./useBottomToolsStore"
import { useDescToolsStore } from "./useDescToolsStore"
import { useKeyboardStore } from "./useKeyBoardStore"
import { usePaperStore } from "./usePaperStore"
import { useRightToolsStore } from "./useRightToolsStore"
import { useIntervalStore, useTimerStore } from "./useTimerStore"
import { useTopToolsStore } from "./useTopToolsStore"
import { findGroupKey } from "./util"
import { labelTypeMap } from "./utils/constants"
// Splitter component - will need custom implementation or alternative
// 定义状态的类型
export type LabelState = Map<string, string[]>
// 定义操作的类型
type Action =
| {
type: "INIT_IMAGES"
imageIds: string[]
objectOperations: Project.LabelSchemaList[]
}
| { type: "REMOVE_IMAGE"; imageId: string }
// 定义初始状态
const initialState: LabelState = new Map()
const operationTypeList = [101, 102, 103, 104]
// utils
const getRectPoints = ([sx, sy, w, h]: number[]) => {
return [
[sx, sy + h],
[sx, sy],
[sx + w, sy],
[sx + w, sy + h],
]
}
const getSegmentPoints = (arr: Array<number[]>) => {
return arr.map((item) => [item[0], item[1]])
}
const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i
const normalizeNamePrefix = (name: string) => {
const value = name
.replace(/[^a-zA-Z0-9_-]+/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "")
return value || "video"
}
interface ResultImageEntry {
frameName: string
imageObjects: ImageObjects
}
const expandLabelResultImages = (item: LabelResult): ResultImageEntry[] => {
if (item.image_objects) {
return [
{
frameName: item.data_name,
imageObjects: item.image_objects,
},
]
}
if (!item.video_objects) return []
return Object.entries(item.video_objects).map(([frameName, imageObjects]) => {
return {
frameName,
imageObjects,
}
})
}
interface LabelProps {
headerHeight: number
leftWidth: number
project_id: number
task_id: number
}
const LabelPage = ({
headerHeight = 0,
leftWidth = 0,
project_id = -1,
task_id = -1,
}: LabelProps) => {
const projectId = useMemo(() => {
return project_id
}, [project_id])
const taskId = useMemo(() => {
return task_id
}, [task_id])
// const searchParams = useSearchParams()
// const id1 = searchParams.get("project_id")
// const projectId =
// typeof id1 === "string" && !isNaN(Number(id1)) ? Number(id1) : 1
// const id2 = searchParams.get("task_id")
// const taskId =
// typeof id2 === "string" && !isNaN(Number(id2)) ? Number(id2) : 2
const user_id = usePermissionStore.getState().user_id
const [projectDetail, setProjectDetail] = useState<Project.DetailResponse>()
const [taskDetail, setTaskDetail] = useState<Task.DataProps>()
const [oldWorkLoad, setOldWorkLoad] = useState<WorkLoad | null>(null)
// const [objectOperations, setObjectOperations] = useState<
// Project.LabelSchemaList[]
// >([]);
const topRef = useRef<any>(null)
const paperContainerRef = useRef<any>(null)
const qaToolContainerRef = useRef<any>(null)
const [isConfirmSave, setIsConfirmSave] = useState(false)
const [loading, setLoading] = useState(false)
const [sizes, setSizes] = useState<number[]>([])
const {
label: labelData,
setLabel,
setStateStack,
setLabelTime,
} = useLabelStore()
const { isAutoSave, autoSaveGap } = useIntervalStore()
const {
editMode,
setEditMode,
setPressA,
activeOperation,
setActiveOperation,
showObjectList,
showGroupList,
showTaskList,
showDescList,
showMultiFrame,
setShowMultiFrame,
isView,
setIsView,
scale,
setScale,
objectOperations,
setObjectOperations,
} = useTopToolsStore()
const { activeImage } = useBottomToolsStore()
const {
setDescOperations,
metaOperation,
setMetaOperation,
setQaOperations,
setQaData,
resetData,
} = useDescToolsStore()
const asyncGetProjectDetail = useCallback(async () => {
if (projectId) {
try {
const res = await getProjectDetail(projectId)
setProjectDetail(res)
let operations: Project.LabelSchemaList[] = []
let textOperations: Project.LabelSchemaList[] = []
let metaOperation: Project.LabelSchemaList | null = null
res.label_schema_list?.forEach((schema) => {
if (operationTypeList.includes(schema.label_type)) {
operations.push(schema)
} else if ([201].includes(schema.label_type)) {
if (schema.label_class === "元操作序列") metaOperation = schema
else textOperations.push(schema)
}
})
setObjectOperations(operations)
if (res.label_type === 5) {
setDescOperations(textOperations)
if (metaOperation) setMetaOperation(metaOperation)
} else {
setDescOperations([])
setMetaOperation(null)
}
if (res.label_type === 6) {
let obj: any = {}
textOperations.forEach((item) => {
let arr = item.sub_attributes_describe.map((sub) => {
return {
[sub.chinese_name]: {
value: "",
id: sub.round_id,
is_pre: true,
tag: 0,
uid: 0,
create_timestamp: 0,
modify_uid: 0,
modify_timestamp: 0,
comment: "",
flag: 0,
},
}
})
obj[item.label_class] = arr
})
let map = new Map()
map.set("init", obj)
console.log(map)
setQaData(map)
setQaOperations(textOperations)
} else {
setQaOperations([])
}
resetData()
} catch {}
}
}, [
projectId,
resetData,
setDescOperations,
setMetaOperation,
setObjectOperations,
setQaData,
setQaOperations,
])
const normalizeVideoTaskData = useCallback(
async (detail: Task.DataProps) => {
const sourceNames = detail.data_name
.map((item) => item?.name?.[0] || "")
.filter((name) => !!name)
if (!sourceNames.length || !projectId || !taskId) return detail
const taskDataType = Number(
(detail as { [key: string]: any })?.data_type ??
(detail as { [key: string]: any })?.task_data_type ??
-1
)
const isVideoTask =
taskDataType === 2 ||
taskDataType === 7 ||
sourceNames.some((name) => videoExtPattern.test(name))
if (!isVideoTask) return detail
const imagesMap = new Map(useImagesStore.getState().images)
const frameMapUpdates = new Map<string, string[]>()
let imageMapUpdated = false
const normalizedDataName: Task.DataProps["data_name"] = []
for (const videoName of sourceNames) {
const cacheKey = `${projectId}:${taskId}:${videoName}`
let frameNames =
useVideoFrameStore.getState().videoFrames.get(cacheKey) ?? []
const hasAllCachedFrames =
frameNames.length > 0 &&
frameNames.every((frameName) => {
const cache = imagesMap.get(frameName)
return typeof cache === "string" && cache.length > 0
})
if (!hasAllCachedFrames) {
let frameData: Awaited<ReturnType<typeof processVideo>> = []
try {
const base64Video = await getServerImage({
data_names: [videoName],
data_type: 2,
project_id: projectId,
})
frameData = await processVideo({
base64Data: String(base64Video || ""),
fileName: videoName,
namePrefix: normalizeNamePrefix(
`${projectId}_${taskId}_${videoName.replace(/\.[^.]+$/, "")}`
),
})
} catch (error) {
const message = error instanceof Error ? error.message : "未知错误"
throw new Error(`视频帧生成失败:${videoName}${message}`)
}
frameNames = frameData.map((frame) => frame.name)
if (!frameNames.length) {
throw new Error(`视频帧生成失败:${videoName}`)
}
frameData.forEach((frame) => {
imagesMap.set(frame.name, frame.dataUrl)
})
imageMapUpdated = true
frameMapUpdates.set(cacheKey, frameNames)
}
if (!frameNames.length) {
throw new Error(`视频帧映射为空:${videoName}`)
}
frameNames.forEach((frameName) => {
normalizedDataName.push({
name: [frameName] as [string],
related_images: [],
})
})
}
if (!normalizedDataName.length) {
throw new Error("未生成可用视频帧")
}
if (imageMapUpdated) {
useImagesStore.getState().setImages(imagesMap)
}
frameMapUpdates.forEach((frameNames, cacheKey) => {
useVideoFrameStore.getState().setVideoFrames(cacheKey, frameNames)
})
return {
...detail,
data_name: normalizedDataName,
}
},
[projectId, taskId]
)
const asyncGetTaskDetail = useCallback(async () => {
if (taskId) {
try {
usePaperStore.getState().setLoadingData(true)
// 获取当前任务全量数据
const res = await getTaskList({
id: [taskId],
get_data: true,
page_number: 1,
page_size: 1,
})
let detail = res.task_list[0]
detail = await normalizeVideoTaskData(detail)
setTaskDetail(detail)
// 帧列表
useBottomToolsStore
.getState()
.setAllItems(detail.data_name.map((item) => item.name[0] || ""))
// 获取当前任务流转记录
let flowCommentArr: Comment[] = []
let turn1time = 1
let turn2time = 1
const flowRes = await getTaskWorkFlow(taskId)
flowRes.forEach((item) => {
if (item.task_status_src !== 4 && item.task_status_dst === 4) {
flowCommentArr.push({
uid: item.new_uid,
turn: 1,
times: turn1time,
check_box_comments: [],
text_comments: [],
comment_type: 0,
date: 0,
scene_info: {
type: 0,
},
})
turn1time++
} else if (item.task_status_src !== 6 && item.task_status_dst === 6) {
flowCommentArr.push({
uid: item.new_uid,
turn: 2,
times: turn2time,
check_box_comments: [],
text_comments: [],
comment_type: 0,
date: 0,
scene_info: {
type: 0,
},
})
turn2time++
}
})
console.log(flowCommentArr)
useLabelStore.getState().setLabelDefaultComments(flowCommentArr)
const { work_time, label_status } = detail
setLabelTime({
label: work_time.label_work_time,
review1: work_time.first_review_work_time,
review2: work_time.second_review_work_time,
})
// 获取并处理当前任务标注结果数据
const dataRes = await getLabelResult(taskId)
const { results } = dataRes
const resultImageEntries = results.flatMap((item) =>
expandLabelResultImages(item)
)
let currentWorkTime = 0
if (label_status === 2) {
currentWorkTime = work_time.label_work_time
} else if (label_status === 4) {
currentWorkTime = work_time.first_review_work_time
} else if (label_status === 6) {
currentWorkTime = work_time.second_review_work_time
}
// 生成当前workload
const workLoad: WorkLoad = {
object_size: {},
prelabel_modify_size: {},
review_size: 0,
comment_size: 0,
work_time: currentWorkTime,
dynamic_size: {},
static_size: {},
review_dynamic_size: {},
review_static_size: {},
review_question_size: {},
key_frame_size: {},
review_key_frame_size: {},
question_size: {},
}
resultImageEntries.forEach(({ frameName, imageObjects }) => {
const { annotations, images, key_frame, desc } = imageObjects
const rasterName = frameName || images.file_name
if (rasterName) {
usePaperStore
.getState()
.setRasterSize(rasterName, [images.width, images.height])
}
annotations.forEach((annotation) => {
let reviewed = false
const { comment, prelabel, uid } = annotation
if (prelabel)
workLoad.prelabel_modify_size[uid]
? workLoad.prelabel_modify_size[uid]++
: (workLoad.prelabel_modify_size[uid] = 1)
else
workLoad.object_size[uid]
? workLoad.object_size[uid]++
: (workLoad.object_size[uid] = 1)
if (comment) {
comment.forEach((c) => {
if (c.comment_type > 0 && c.uid === user_id) reviewed = true
if (c.comment_type === 2 && c.uid === user_id)
workLoad.comment_size++
})
}
if (reviewed) workLoad.review_size++
})
if (key_frame) {
if (label_status === 2) {
let id = imageObjects.label1_uid!
if (id)
workLoad.key_frame_size[id]
? workLoad.key_frame_size[id]++
: (workLoad.key_frame_size[id] = 1)
} else if (label_status === 4) {
let id = imageObjects.review1_uid!
if (id)
workLoad.review_key_frame_size[id]
? workLoad.review_key_frame_size[id]++
: (workLoad.review_key_frame_size[id] = 1)
} else if (label_status === 6) {
let id = imageObjects.review2_uid!
if (id)
workLoad.review_key_frame_size[id]
? workLoad.review_key_frame_size[id]++
: (workLoad.review_key_frame_size[id] = 1)
}
}
if (useDescToolsStore.getState().qaOperations.length) {
if (desc) {
const { other_desc } = desc
const descData = other_desc ? JSON.parse(other_desc) : {}
Object.values(descData).forEach((questionArr: any) => {
questionArr.forEach((q: any) => {
const v: any = Object.values(q)[0]
if (v.uid) {
workLoad.question_size[v.uid]
? workLoad.question_size[v.uid]++
: (workLoad.question_size[v.uid] = 1)
}
if (v.tag) {
const uid = usePermissionStore.getState().user_id
workLoad.review_question_size[uid]
? workLoad.review_question_size[uid]++
: (workLoad.review_question_size[uid] = 1)
}
})
})
}
}
})
console.log("获取时计算的工作量统计", workLoad)
setOldWorkLoad(workLoad)
const taskLabelData = new Map()
const pathGroupData = new Map()
// 重置 PathIds
useRightToolsStore.getState().setPathIds([])
// 重置 PathGroupIds
useRightToolsStore.getState().setPathGroupIds([])
// 生成标注结果
resultImageEntries.forEach(({ frameName, imageObjects }) => {
const key = frameName
if (!key) return
const categoryMap = new Map()
const { annotations, image_groups, desc } = imageObjects
// 赋值用过的 id
annotations.map((annotation) => {
useRightToolsStore.getState().pushPathId(annotation.id)
})
// 赋值用过的 groupId
image_groups &&
Object.keys(image_groups)?.map((groupId) => {
useRightToolsStore.getState().pushPathGroupId(+groupId)
})
annotations.forEach((annotation) => {
const { id, category_id, comment } = annotation
let current_comment: Comment[] = []
if (comment) {
current_comment = flowCommentArr.map((c) => {
const { turn, times } = c
let modified_comment = null
comment.forEach((item) => {
if (item.turn === turn && item.times === times)
modified_comment = item
})
return modified_comment ? modified_comment : c
})
} else {
current_comment = [...flowCommentArr]
}
let detail: any = {
...annotation,
comment: current_comment,
}
// group info
if (image_groups) {
const key = findGroupKey(image_groups as any, id)
if (key !== -1) detail.parentGroupId = key
}
const categoryKey = `${category_id}`
let arr = categoryMap.has(categoryKey)
? categoryMap.get(categoryKey)
: []
if (annotation.rect) {
// arr.push([id, getRectPoints(annotation.rect), detail, []]);
arr.push([id, [getRectPoints(annotation.rect)], detail, []])
}
if (annotation.line_segment) {
arr.push([
id,
annotation.line_segment.map((item) => getSegmentPoints(item)),
detail,
[],
])
}
if (annotation.segmentation) {
arr.push([
id,
// getSegmentPoints(annotation.segmentation[0]),
annotation.segmentation.map((item) => getSegmentPoints(item)),
detail,
annotation.hollow_segmentation?.map((item) =>
getSegmentPoints(item)
) || [],
])
}
if (annotation.key_points) {
arr.push([
id,
// getSegmentPoints(annotation.segmentation[0]),
[
getSegmentPoints(
annotation.key_points.map((item) => item.point)
),
],
Object.assign(detail, {
circles: annotation.key_points,
}),
[],
])
}
categoryMap.set(categoryKey, arr)
})
taskLabelData.set(key, categoryMap)
// 关键帧信息
const keyFrame = {
key_frame: imageObjects.key_frame || false,
label1_ts: imageObjects.label1_ts || 0,
label1_uid: imageObjects.label1_uid || 0,
review1_ts: imageObjects.review1_ts || 0,
review1_uid: imageObjects.review1_uid || 0,
review2_ts: imageObjects.review2_ts || 0,
review2_uid: imageObjects.review2_uid || 0,
}
useBottomToolsStore.getState().setKeyFrameData(key, keyFrame)
// 大语言模型 两种数据情况
if (useDescToolsStore.getState().qaOperations.length) {
if (desc) {
const { other_desc } = desc
let current_data: any = {}
const descData = other_desc ? JSON.parse(other_desc) : {}
Object.entries(descData).forEach(
([label_class, questionArr]: any) => {
let arr = questionArr.map((question: any) => {
let [name, val]: any = Object.entries(question)[0]
return {
[name]: {
...val,
value: val.value
? `${val.value}${splitWord}${val.value}`
: undefined,
comment: val.comment
? `${val.comment}${splitWord}${val.comment}`
: undefined,
},
}
})
current_data[label_class] = arr
}
)
const qaData = useDescToolsStore.getState().qaData
// 无文本描述时 赋予预设值
if (JSON.stringify(current_data) === "{}") {
current_data = qaData.get("init")
}
const qaMap = structuredClone(qaData)
qaMap.set(key, current_data)
useDescToolsStore.getState().setQaData(qaMap)
}
} else if (useDescToolsStore.getState().descOperations.length) {
if (desc) {
const { meta_operation, other_desc } = desc
const currentMetaMap = structuredClone(
useDescToolsStore.getState().metaData
)
currentMetaMap.set(key, meta_operation || [])
useDescToolsStore.getState().setMetaData(currentMetaMap)
const currentDescMap = structuredClone(
useDescToolsStore.getState().descData
)
let descMap = new Map()
const descData = other_desc ? JSON.parse(other_desc) : {}
console.log(descData)
Object.entries(descData).forEach(
([label_class, { value, is_correct, comment }]: any) => {
descMap.set(label_class, {
value:
(typeof value === "string"
? `${value}${splitWord}${value}`
: value) || undefined,
is_correct: is_correct || undefined,
comment: comment || undefined,
})
}
)
currentDescMap.set(key, descMap)
useDescToolsStore.getState().setDescData(currentDescMap)
}
}
image_groups &&
pathGroupData.set(
key,
new Map(
Object.entries(image_groups).map(([key, value]: any) => [
key * 1,
value,
])
)
)
})
setLabel(taskLabelData)
// 获取任务详情时设置状态栈初始值
setStateStack([taskLabelData])
useDescToolsStore.getState().updateFlag(true)
useRightToolsStore.getState().setPathGroupMap(pathGroupData)
} catch (err) {
console.log(err)
setOldWorkLoad(null)
setTaskDetail(undefined)
useBottomToolsStore.getState().setAllItems([])
setLabel(new Map())
setStateStack([])
useRightToolsStore.getState().setPathGroupMap(new Map())
if (err instanceof Error && err.message.includes("视频")) {
showNotification({
color: "red",
title: "视频解码失败",
message: err.message,
})
}
} finally {
usePaperStore.getState().setLoadingData(false)
}
}
}, [
normalizeVideoTaskData,
setLabel,
setLabelTime,
setStateStack,
taskId,
user_id,
])
// 保存时更新当前workload
const updateOldWorkLoad = (data: WorkLoad) => {
setOldWorkLoad(data)
}
useEffect(() => {
asyncGetProjectDetail()
asyncGetTaskDetail()
}, [asyncGetProjectDetail, asyncGetTaskDetail])
// 定义 reducer 函数
const reducer = useCallback(
(state: LabelState, action: Action): LabelState => {
switch (action.type) {
case "INIT_IMAGES":
const newState = new Map() // 创建状态的副本
action.imageIds.forEach((imageId) => {
newState.set(
imageId,
action.objectOperations.map((item) => item.category_id.toString())
) // 将操作列表添加到新状态中
})
return newState
case "REMOVE_IMAGE":
const stateAfterRemoval = new Map(state)
if (stateAfterRemoval.has(action.imageId)) {
stateAfterRemoval.delete(action.imageId) // 删除该图片的条目
}
return stateAfterRemoval
default:
return state // 返回当前状态
}
},
[]
)
const [labelState, dispatch] = useReducer(reducer, initialState)
const initImages = (
imageIds: string[],
objectOperations: Project.LabelSchemaList[]
) => {
dispatch({
type: "INIT_IMAGES",
imageIds,
objectOperations,
})
}
const [isLabelTask, setIsLabelTask] = useState<boolean>(false)
useEffect(() => {
if (!taskDetail || !projectDetail) {
setIsLabelTask(false)
setIsView(true)
setEditMode(false)
return
}
if (taskDetail) {
if (
![201, 202, 203, 500].includes(projectDetail!.status) ||
![2, 4, 6, 7].includes(taskDetail.label_status) ||
taskDetail.current_uid !== user_id
) {
// 如果不是能标注的任务
setIsLabelTask(false)
setIsView(true)
setEditMode(false)
} else {
setIsLabelTask(true)
setIsView(false)
}
initImages(
taskDetail.data_name.map((item) => item.name?.[0]),
objectOperations
)
if (taskDetail.data_name?.length) setShowMultiFrame(true)
}
}, [
objectOperations,
setIsView,
setShowMultiFrame,
user_id,
taskDetail,
projectDetail,
setEditMode,
])
const { setPaperMode, setToolOption, setGroupScale } = usePaperStore()
// @ts-expect-error to_do
const forceUpdate = useReducer((bool) => !bool)[1]
const getOperationSchema = useCallback(
(operationId: string) => {
return (
projectDetail?.label_schema_list?.find(
(item) => item.category_id === +operationId && item.label_type !== 201
) || null
)
},
[projectDetail?.label_schema_list]
)
const setDrawType = useCallback(() => {
if (isView) {
setPaperMode("pan")
} else {
let operationSchema = getOperationSchema(activeOperation)
if (operationSchema) {
let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
if (editMode) {
switch (
operationSchema &&
labelTypeMap.get(operationSchema.label_type)
) {
case "多边形":
setToolOption({
strokeColor: strokeColor,
strokeWidth: 2,
fillColor: fillColor,
blankColor: blankColor,
})
setPaperMode("polygon")
return
case "关键点":
setToolOption({
strokeColor: strokeColor,
strokeWidth: 2,
fillColor: fillColor,
blankColor: blankColor,
})
setPaperMode("point")
return
case "多线段":
setToolOption({
strokeColor: strokeColor,
strokeWidth: 2,
fillColor: fillColor,
blankColor: blankColor,
})
setPaperMode("brush")
return
case "2D框":
setToolOption({
strokeColor: strokeColor,
strokeWidth: 2,
fillColor: fillColor,
blankColor: blankColor,
})
setPaperMode("rectangle")
return
default:
setPaperMode("polygon")
return
}
} else {
setPaperMode("pan")
}
}
}
}, [
activeOperation,
editMode,
getOperationSchema,
isView,
setPaperMode,
setToolOption,
])
useEffect(() => {
setDrawType()
}, [editMode, setDrawType])
const { setShift, focusInput } = useKeyEventStore()
const handleShortcut = useCallback(
(key: string, ctrl: boolean) => {
let checkOperation = getOperationSchema(key)
const mode = usePaperStore.getState().mode
// 标注方案里面存在的非文本类型标注才能切换
if (checkOperation) {
// 辅助标注模式下非多边形类型不能切换
if (mode === "support" && checkOperation.label_type !== 102) return
if (ctrl) {
setActiveOperation((+key + 10).toString())
} else {
setActiveOperation(key)
}
}
},
[getOperationSchema, setActiveOperation]
)
useEffect(() => {
const down = (e: KeyboardEvent) => {
// console.log(e, e.key);
if (focusInput || isView) return
console.log(e.key)
const mode = usePaperStore.getState().mode
if (e.key === "Alt" && mode !== "support") {
e.preventDefault()
// console.log(e.key);
setEditMode(!editMode)
if (!editMode) setPressA(false)
}
if (e.key === "Shift") {
e.preventDefault()
setShift(true)
useKeyboardStore.getState().setShift(true)
}
if (mode !== "support")
if (Number(e.key)) {
handleShortcut(e.key, e.ctrlKey)
} else if (Number(e.key) === 0) {
handleShortcut("10", e.ctrlKey)
}
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setIsConfirmSave(true)
}
if (e.key === "g" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
paperContainerRef.current.handleCreatePathGroup()
}
if (e.key === "a") {
e.preventDefault()
setPressA(true)
}
if (e.key === "Control") {
e.preventDefault()
useKeyboardStore.getState().setCtrl(true)
}
// 辅助标注 分割
if (e.key === "F1") {
// 判断类型
e.preventDefault()
const schema = getOperationSchema(
useTopToolsStore.getState().activeOperation
)
console.log(schema)
if (schema?.label_type !== 102) {
showNotification({
title: "错误",
message: "当前类型不支持辅助标注!",
color: "red",
})
} else {
if (mode === "support") {
// 辅助标注生成对应标注结果
paperContainerRef.current.saveSupportAnnotationData()
} else {
usePaperStore.getState().setPaperMode("support")
}
}
}
// 追踪
if (e.key === "i") {
e.preventDefault()
paperContainerRef.current.renderTrackingAnnotation()
}
// 复制
if (e.key === "c" && e.ctrlKey) {
e.preventDefault()
useKeyboardStore
.getState()
.setCopyDataIds(
useObjectStore.getState().selectedPath[
useBottomToolsStore.getState().activeImage
]
)
}
// 粘贴
if (e.key === "v" && e.ctrlKey) {
e.preventDefault()
paperContainerRef.current.copyAnnotationDataToMultiFrame()
}
// 撤回
if (e.key === "z" && e.ctrlKey) {
e.preventDefault()
// 清除当前页数据
const clear = () => {
usePaperStore
.getState()
.group!.getItems({})
.filter((item) => item.data.id)
.forEach((item) => {
item.remove()
})
}
const data = useLabelStore.getState().popStateStack()
console.log("弹出栈的数据", data)
if (data) {
clear()
const imageId = useBottomToolsStore.getState().activeImage
const pathGroupMap = new Map()
for (const [imageKey, categories] of data) {
const groupMap = new Map()
for (const [key, value] of categories) {
value.forEach((item) => {
const vId = item[2].parentGroupId
if (vId)
if (groupMap.has(vId)) groupMap.get(vId).push(item[0])
else groupMap.set(vId, [item[0]])
})
if (imageId === imageKey)
paperContainerRef.current.renderPolygons(key, categories)
}
pathGroupMap.set(imageKey, groupMap)
}
useRightToolsStore.getState().setPathGroupMap(pathGroupMap)
console.log(pathGroupMap)
// if (data.size) {
// if (data.has(imageId)) {
// const map = data.get(imageId);
// if (map) {
// for (const [key, value] of map) {
// console.log(key, value);
// paperContainerRef.current.renderPolygons(key, map);
// }
// }
// }
// }
// 更新当前数据
useLabelStore.getState().setLabel(structuredClone(data))
}
}
if (e.key === "p") {
e.preventDefault()
if (projectDetail?.label_type === 6 && qaToolContainerRef.current)
qaToolContainerRef.current.checkWrongWords()
}
// if (e.key === "=" && (e.metaKey || e.ctrlKey)) {
// e.preventDefault();
// setScale((num) => {
// let newScale = num + 0.1;
// return newScale;
// });
// } else if (e.key === "-" && (e.metaKey || e.ctrlKey)) {
// e.preventDefault();
// setScale((num) => {
// if (num <= 1) {
// let newScale = num * 0.9;
// return newScale;
// } else {
// let newScale = num - 0.1;
// return newScale;
// }
// });
// } else if (e.key === "3") {
// e.preventDefault();
// } else if (e.key === "4") {
// e.preventDefault();
// } else if (e.key === "5") {
// e.preventDefault();
// } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
// e.preventDefault();
// }
}
const up = (e: KeyboardEvent) => {
// console.log("键盘抬起", e);
if (e.key === "Shift") {
setShift(false)
useKeyboardStore.getState().setShift(false)
}
if (e.key === "Control") {
useKeyboardStore.getState().setCtrl(false)
}
}
const wheel = (e: WheelEvent) => {
e.preventDefault()
const currentScale = useTopToolsStore.getState().scale
let newScale = e.deltaY < 0 ? currentScale * 1.1 : currentScale * 0.9
if (newScale >= 100) return
setScale(newScale)
setGroupScale(newScale)
}
const mouseclick = (_e: MouseEvent) => {
useTopToolsStore.getState().setSubAttrPresetShow(false)
}
const mousemove = (event: { clientX: number; clientY: number }) => {
paperContainerRef.current.handleCrosshairMove(event)
}
let element = document.getElementById("resize-container")
document.addEventListener("keydown", down)
document.addEventListener("keyup", up)
element?.addEventListener("wheel", wheel)
element?.addEventListener("mousedown", mouseclick)
element?.addEventListener("mousemove", mousemove)
return () => {
document.removeEventListener("keydown", down)
document.removeEventListener("keyup", up)
element?.removeEventListener("wheel", wheel)
element?.removeEventListener("mousedown", mouseclick)
element?.removeEventListener("mousemove", mousemove)
}
}, [
editMode,
focusInput,
getOperationSchema,
handleShortcut,
isView,
projectDetail?.label_type,
setEditMode,
setGroupScale,
setPressA,
setScale,
setShift,
])
const setGlobalLoading = useCallback((flag: boolean) => {
setLoading(flag)
}, [])
// 切换图片时清空复制保存的数组
useEffect(() => {
useKeyboardStore.getState().setCopyDataIds([])
}, [activeImage])
const handleBeforeUnload = (e: any) => {
e.preventDefault()
// 备份
topRef.current.handleBackup("auto_backup")
// 清空操作栏状态
useObjectStore.getState().resetPathAndOperationStatus()
}
// 更新Splitter状态
const updateSplitterSizes = useCallback(() => {
console.log(
"document.documentElement.clientWidth",
document.documentElement.clientWidth
)
const len = document.documentElement.clientWidth - 4
let arr = [len, 0, 0, 0, 0, 0]
const flags = [
showTaskList,
showGroupList,
showDescList && projectDetail && projectDetail.label_type === 5,
showDescList && projectDetail && projectDetail.label_type === 6,
showDescList && projectDetail && projectDetail.label_type === 7,
showObjectList,
]
let count = 0
flags.forEach((flag) => {
if (flag) count++
})
if (count === 1 || count === 2) {
arr[0] = count === 1 ? len * 0.8 : len * 0.6
flags.forEach((flag, index) => {
if (flag) {
arr[index + 1] = len * 0.2
}
})
} else if (count === 3) {
arr[0] = len * 0.55
flags.forEach((flag, index) => {
if (flag) {
arr[index + 1] = len * 0.15
}
})
} else if (count === 4) {
arr[0] = len * 0.6
flags.forEach((flag, index) => {
if (flag) {
arr[index + 1] = len * 0.1
}
})
}
setSizes(arr)
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
useEffect(() => {
updateSplitterSizes()
}, [updateSplitterSizes])
useEffect(() => {
if (!isView) {
const delay = 10 * 60000
// 编辑模式下修改labelData时启动或更新定时器
useTimerStore.getState().startTimer(() => {
useTopToolsStore.getState().setIsView(true)
}, delay)
} else {
useTimerStore.getState().clearTimer()
}
// 组件卸载时清理定时器
return () => {
useTimerStore.getState().clearTimer()
}
}, [isView, labelData])
// auto save
useEffect(() => {
if (isAutoSave && !isView) {
const minute = 60 * 1000
useIntervalStore.getState().startTimer(() => {
topRef.current.handleSave()
}, autoSaveGap * minute)
} else {
useIntervalStore.getState().clearTimer()
}
return () => {
useIntervalStore.getState().clearTimer()
}
}, [autoSaveGap, isAutoSave, isView])
useEffect(() => {
window.addEventListener("beforeunload", handleBeforeUnload)
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [])
const mainBoxHeight = useMemo(() => {
if (projectDetail?.label_type === 5 && metaOperation) {
let h = `calc(100% - ${headerHeight}px - 198px)`
return h
} else if (showMultiFrame) {
let h = `calc(100% - ${headerHeight}px - 150px)`
return h
}
let h = `calc(100% - ${headerHeight}px - 70px)`
return h
}, [projectDetail?.label_type, metaOperation, showMultiFrame, headerHeight])
const fullscreenRef = useRef<HTMLDivElement>(null)
return (
<Stack w="100%" h="100vh" pos="relative" ref={fullscreenRef} gap={0}>
<LoadingOverlay
visible={loading}
zIndex={2000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Box w="100%" h={80}>
<TopTools
ref={topRef}
taskDetail={taskDetail}
projectDetail={projectDetail}
oldWorkLoad={oldWorkLoad}
updateOldWorkLoad={updateOldWorkLoad}
isLabelTask={isLabelTask}
renderPolygons={
paperContainerRef.current
? paperContainerRef.current.renderPolygons
: null
}
/>
</Box>
<Flex w="calc(100% - 4px)" h={mainBoxHeight}>
{/* <Flex h="100%" w={"100%"}> */}
<Box w={sizes[0] - leftWidth} style={{ flexShrink: 0 }}>
<Box h="100%" id="resize-container">
<Flex h="100%" w="100%">
<Flex
direction="column"
h="100%"
w={`calc(100% - 64px)`}
pos="relative"
style={{ overflow: "hidden" }}>
<ScaleComponent
options={{
offsetX: 0,
offsetY: 0,
scale: scale,
}}
mode="horizontal"
/>
<Flex h="calc(100% - 32px)">
<ScaleComponent
options={{
offsetX: 0,
offsetY: 0,
scale: scale,
}}
mode="vertical"
/>
<PaperContainer
imgSrc={"/test.jpeg"}
ref={paperContainerRef}
forceUpdate={forceUpdate}
labelState={labelState}
projectDetail={projectDetail}
updateLoadingFlag={setGlobalLoading}
/>
</Flex>
</Flex>
<ScaleToolContainer />
</Flex>
</Box>
</Box>
{showTaskList && (
<Box w={sizes[1]} miw={300} style={{ flexShrink: 0 }}>
<RightTaskTools
projectDetail={projectDetail}
project_id={projectId}
task_id={taskId}
/>
</Box>
)}
{showGroupList && (
<Box w={sizes[2]} miw={200} style={{ flexShrink: 0 }}>
<RightGroupTools
labelState={labelState}
projectDetail={projectDetail}
/>
</Box>
)}
{showDescList && projectDetail && projectDetail.label_type === 5 && (
<Box w={sizes[3]} miw={350} style={{ flexShrink: 0 }}>
<RightDescTools taskDetail={taskDetail} />
</Box>
)}
{showDescList && projectDetail && projectDetail.label_type === 6 && (
<Box w={sizes[4]} miw={350} style={{ flexShrink: 0 }}>
<RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
</Box>
)}
{showObjectList && (
<Box w={sizes[5]} miw={350} style={{ flexShrink: 0 }}>
<RightObjectTools
taskDetail={taskDetail}
labelState={labelState}
projectDetail={projectDetail}
/>
</Box>
)}
{/* </Flex> */}
</Flex>
<Box
w="100%"
h={projectDetail?.label_type === 5 && metaOperation ? 128 : 80}
display={showMultiFrame ? "block" : "none"}>
<BottomTools
labelState={labelState}
projectDetail={projectDetail}
taskDetail={taskDetail}
renderPolygons={
paperContainerRef.current
? paperContainerRef.current.renderPolygons
: null
}
/>
</Box>
{isConfirmSave && (
<ConfirmModal
open={isConfirmSave}
title="保存标注结果"
content="是否确定保存当前标注结果?"
onOk={async () => {
try {
await topRef.current?.handleSave()
setIsConfirmSave(false)
} catch {}
}}
onCancel={() => {
setIsConfirmSave(false)
}}
/>
)}
</Stack>
)
}
export default LabelPage