From f0dabe849ef00ab1b61cbfe7be78308942433f99 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 9 Apr 2026 15:46:42 +0800 Subject: [PATCH 1/9] fix(paper): fix doubleclick --- components/label/usePaperStore.ts | 110 +++++++++++++++++++----------- 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 9c742f7..e56bcc5 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -805,6 +805,68 @@ export const usePaperStore = create((set) => ({ .updateSelectedPath(useBottomToolsStore.getState().activeImage, "ADD") } + const getPanHitTolerance = (baseTolerance = 8) => + usePaperStore.getState().paperScope + ? baseTolerance / useTopToolsStore.getState().scale + : 0 + + const createPanHitOptions = ( + tolerance: number, + match: (hit: paper.HitResult) => boolean + ) => ({ + segments: true, + stroke: true, + curves: true, + fill: true, + guides: false, + tolerance, + match, + }) + + const resolvePanHitResult = (projectPoint: paper.Point) => { + const group = usePaperStore.getState().group + if (!group) return null + + const supportHit = group.hitTest( + projectPoint, + createPanHitOptions(getPanHitTolerance(), (hit) => + ["pathCircle", "pathBuff"].includes(hit.item.data?.type || "") + ) + ) + if (supportHit) return supportHit + + return group.hitTest( + projectPoint, + createPanHitOptions(0, (hit) => + ["rectangle", "polygon", "brush", "point"].includes( + hit.item.data?.type || "" + ) + ) + ) + } + + const clearCurrentSelection = () => { + let shouldClearSupport = false + + usePaperStore + .getState() + .group!.getItems({ + data: { selected: true }, + }) + .forEach((item) => { + if (item.data.selected) { + item.data.selected = false + shouldClearSupport = true + } + }) + + if (shouldClearSupport) { + usePaperSupportStore.getState().clearAll() + } + + handleCancelSelect() + } + panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (startMiddleMousePan(e.event)) return if (pressCtrl) return @@ -825,16 +887,7 @@ export const usePaperStore = create((set) => ({ "local" ) - let hitResult = usePaperStore.getState().group?.hitTest(e.point, { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false, - tolerance: usePaperStore.getState().paperScope - ? 8 / useTopToolsStore.getState().scale - : 0, - }) + let hitResult = resolvePanHitResult(e.point) console.log("hitResult", hitResult) let lastTagDataId = -1 if (activePath?.data.id) { @@ -941,35 +994,16 @@ export const usePaperStore = create((set) => ({ if (pressShift) { } else { - // not press shift - let selectedItems = usePaperStore.getState().group!.getItems({ - data: { selected: true }, - }) - if (selectedItems && selectedItems.length) { - selectedItems.forEach((item) => { - if (item.data.selected) { - let itemHitResult = item.hitTest(point, { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false, - tolerance: usePaperStore.getState().paperScope - ? 8 / usePaperStore.getState().paperScope!.view.zoom - : 0, - }) + const selectedIds = + useObjectStore.getState().selectedPath[ + useBottomToolsStore.getState().activeImage + ] || [] + const hitId = hitResult?.item?.data?.id + const keepCurrentSelection = + hitId && selectedIds.length === 1 && selectedIds[0] === hitId - if (!itemHitResult) { - item.data.selected = false - usePaperSupportStore.getState().clearAll() - - handleCancelSelect() - } - } - // item.data.selected = false; - }) - } else { - handleCancelSelect() + if (!keepCurrentSelection) { + clearCurrentSelection() } } From b10fd464749a1dc83e41ac84204b0e81e2981f43 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 9 Apr 2026 15:49:31 +0800 Subject: [PATCH 2/9] fix(paper): click area --- components/label/usePaperStore.ts | 340 ++++++++++++++++++++++++++---- 1 file changed, 295 insertions(+), 45 deletions(-) diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index e56bcc5..36d3f6d 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -1,17 +1,17 @@ -import { Comment } from "./api/label/typing" -import { Project } from "./api/project/typing" import { notifications } from "@mantine/notifications" import dayjs from "dayjs" import paper from "paper" import { DispatchWithoutAction } from "react" import { create } from "zustand" -import { usePermissionStore } from "./store/auth" +import { Comment } from "./api/label/typing" +import { Project } from "./api/project/typing" import { initialDetail, useKeyEventStore, useLabelStore, useObjectStore, } from "./store" +import { usePermissionStore } from "./store/auth" import { useBottomToolsStore } from "./useBottomToolsStore" import { useKeyboardStore } from "./useKeyBoardStore" import { useOtherToolsStore } from "./useOtherToolsStore" @@ -319,6 +319,26 @@ const hidePaperContextMenu = () => { } let isMiddleMousePanning = false +let hoveredBufferPathInfo: { + id: number + index: number + path: paper.Path + clearHighlight: () => void + scheduleClear: () => void + cancelScheduledClear: () => void +} | null = null +let hoveredPathCircleInfo: { + id: number + index: number + item: paper.Path.Circle +} | null = null + +const clearHoveredBufferPathHighlight = () => { + if (hoveredBufferPathInfo) { + hoveredBufferPathInfo.clearHighlight() + } + hoveredBufferPathInfo = null +} const startMiddleMousePan = (event?: MouseEvent) => { if (!event || event.button !== 1) return false @@ -510,6 +530,8 @@ export const getShowContextMenuPosition = ( } const DOUBLE_CLICK_TOLERANCE_PX = 8 +const PAN_DRAG_THRESHOLD_PX = 4 +const BUFFER_HOVER_STICKY_MS = 180 const getPaperDoubleClickTolerance = ( coordinateSpace: "project" | "local" = "project" @@ -525,6 +547,37 @@ const getPaperDoubleClickTolerance = ( return projectTolerance } +const getPaperDragThreshold = ( + coordinateSpace: "project" | "local" = "project" +) => { + const viewZoom = usePaperStore.getState().paperScope?.view.zoom || 1 + const projectThreshold = PAN_DRAG_THRESHOLD_PX / viewZoom + + if (coordinateSpace === "local") { + const groupScale = usePaperStore.getState().group?.scaling.x || 1 + return projectThreshold / groupScale + } + + return projectThreshold +} + +const showDeletePointLimitNotification = ( + type: "polygon" | "brush" | "point" +) => { + const messageMap = { + polygon: "当前对象至少保留3个点,无法继续删除", + brush: "当前对象至少保留2个点,无法继续删除", + point: "当前对象至少保留1个点,无法继续删除", + } + + notifications.show({ + id: "label-delete-point-limit", + color: "yellow", + autoClose: 1500, + message: messageMap[type], + }) +} + const isPaperDoubleClick = ( event: MouseEvent, lastPoint: paper.Point | null | undefined, @@ -668,6 +721,7 @@ export const usePaperStore = create((set) => ({ let panTool = paperPanTool let lastPoint: paper.Point let isChanged: boolean = false + let hasExceededPanDragThreshold = false // const handleSelected = (path: paper.Path) => { // // path.blendMode = "subtract"; @@ -810,6 +864,8 @@ export const usePaperStore = create((set) => ({ ? baseTolerance / useTopToolsStore.getState().scale : 0 + false && getPanHitTolerance + const createPanHitOptions = ( tolerance: number, match: (hit: paper.HitResult) => boolean @@ -827,13 +883,39 @@ export const usePaperStore = create((set) => ({ const group = usePaperStore.getState().group if (!group) return null - const supportHit = group.hitTest( - projectPoint, - createPanHitOptions(getPanHitTolerance(), (hit) => - ["pathCircle", "pathBuff"].includes(hit.item.data?.type || "") + const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent + ? hoveredPathCircleInfo + : null + if (hoveredCircleInfo) { + const pointHit = group.hitTest( + projectPoint, + createPanHitOptions(0, (hit) => { + return ( + hit.item.data?.type === "pathCircle" && + hit.item.data?.id === hoveredCircleInfo.id && + hit.item.data?.index === hoveredCircleInfo.index + ) + }) ) - ) - if (supportHit) return supportHit + if (pointHit) return pointHit + } + + const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent + ? hoveredBufferPathInfo + : null + if (hoveredBufferInfo) { + const edgeHit = group.hitTest( + projectPoint, + createPanHitOptions(0, (hit) => { + return ( + hit.item.data?.type === "pathBuff" && + hit.item.data?.id === hoveredBufferInfo.id && + hit.item.data?.index === hoveredBufferInfo.index + ) + }) + ) + if (edgeHit) return edgeHit + } return group.hitTest( projectPoint, @@ -886,9 +968,16 @@ export const usePaperStore = create((set) => ({ point, "local" ) + hasExceededPanDragThreshold = false let hitResult = resolvePanHitResult(e.point) console.log("hitResult", hitResult) + const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent + ? hoveredBufferPathInfo + : null + const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent + ? hoveredPathCircleInfo + : null let lastTagDataId = -1 if (activePath?.data.id) { lastTagDataId = activePath.data.id @@ -999,8 +1088,18 @@ export const usePaperStore = create((set) => ({ useBottomToolsStore.getState().activeImage ] || [] const hitId = hitResult?.item?.data?.id + const keepHoveredPointSelection = + !!hoveredCircleInfo && + selectedIds.length === 1 && + selectedIds[0] === hoveredCircleInfo.id + const keepHoveredEdgeSelection = + !!hoveredBufferInfo && + selectedIds.length === 1 && + selectedIds[0] === hoveredBufferInfo.id const keepCurrentSelection = - hitId && selectedIds.length === 1 && selectedIds[0] === hitId + keepHoveredPointSelection || + keepHoveredEdgeSelection || + (hitId && selectedIds.length === 1 && selectedIds[0] === hitId) if (!keepCurrentSelection) { clearCurrentSelection() @@ -1242,6 +1341,56 @@ export const usePaperStore = create((set) => ({ id: data.id, }) } + } else if ( + isDoubleClick && + hoveredBufferInfo && + hoveredBufferInfo.id === activePath?.data?.id && + ["polygon", "brush", "point"].includes(activePath?.data?.type || "") + ) { + let currentPath: paper.Path | null = null + if (activePath instanceof paper.CompoundPath) { + currentPath = + (activePath.children as paper.Path[]).find( + (item) => item.data.selected + ) || null + } else if (activePath instanceof paper.Path) { + currentPath = activePath + } + + if (currentPath) { + hitIndex = hoveredBufferInfo.index + const insertPoint = currentPath.getNearestPoint(point) + currentPath.insert(hitIndex + 1, insertPoint) + usePaperSupportStore.getState().handleBufferPaths(currentPath) + usePaperSupportStore.getState().handleCircles(currentPath) + isChanged = true + panMode = "stroke" + hitIndex = hitIndex + 1 + + if (currentPath.data.type === "brush") { + usePaperSupportStore.getState().handleMask(currentPath) + } + if (currentPath.data.type === "point") { + usePaperSupportStore.getState().handleText(currentPath) + let circles = usePaperStore.getState().group!.getItems({ + data: { + id: currentPath.data.id, + type: "circle", + }, + }) as paper.Path[] + + currentPath.segments.forEach((segment, index) => { + let findIndex = circles.findIndex((circle) => { + return circle.contains(segment.point) + }) + circles[index].data = Object.assign({}, circles[index]?.data, { + text: findIndex + 1, + }) + }) + usePaperSupportStore.getState().handleBufferPaths(currentPath) + usePaperSupportStore.getState().handleCircles(currentPath) + } + } } else if (hitResult && hitResult.type === "fill") { if ( hitResult && @@ -1276,28 +1425,30 @@ export const usePaperStore = create((set) => ({ // doubleclick if (isDoubleClick) { if (activePath instanceof paper.CompoundPath) { - ;(activePath.children as paper.Path[]).forEach((item) => { - const childHitResult = item.hitTest(point, { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false, - tolerance: usePaperStore.getState().paperScope - ? 8 / usePaperStore.getState().paperScope!.view.zoom - : 0, - }) - if (childHitResult) { - if (childHitResult.location.point) { - item.insert(hitIndex + 1, childHitResult.location.point) - usePaperSupportStore.getState().handleBufferPaths(item) - usePaperSupportStore.getState().handleCircles(item) - isChanged = true - panMode = "stroke" - hitIndex = hitIndex + 1 - } - } - }) + const selectedChild = + (activePath.children as paper.Path[]).find( + (item) => item.data.selected + ) || + (activePath.children as paper.Path[]).reduce( + (nearest, item) => { + if (!nearest) return item + return item.getNearestPoint(point).getDistance(point) < + nearest.getNearestPoint(point).getDistance(point) + ? item + : nearest + }, + null as paper.Path | null + ) + + if (selectedChild) { + const insertPoint = selectedChild.getNearestPoint(point) + selectedChild.insert(hitIndex + 1, insertPoint) + usePaperSupportStore.getState().handleBufferPaths(selectedChild) + usePaperSupportStore.getState().handleCircles(selectedChild) + isChanged = true + panMode = "stroke" + hitIndex = hitIndex + 1 + } } else { activePath.insert(hitIndex + 1, hitResult.location.point) activePath?.data?.type === "point" && @@ -1380,8 +1531,16 @@ export const usePaperStore = create((set) => ({ let checkLength = 3 if (item?.data?.type === "brush") checkLength = 2 if (item?.data?.type === "point") checkLength = 1 - if (item.segments && item.segments.length <= checkLength) + if (item.segments && item.segments.length <= checkLength) { + if ( + item?.data?.type === "polygon" || + item?.data?.type === "brush" || + item?.data?.type === "point" + ) { + showDeletePointLimitNotification(item.data.type) + } return + } ;(childHitResult.item as paper.Path).removeSegment(hitIndex) usePaperSupportStore .getState() @@ -1401,8 +1560,16 @@ export const usePaperStore = create((set) => ({ if ( activePath.segments && activePath.segments.length <= checkLength - ) + ) { + if ( + activePath?.data?.type === "polygon" || + activePath?.data?.type === "brush" || + activePath?.data?.type === "point" + ) { + showDeletePointLimitNotification(activePath.data.type) + } return + } activePath.removeSegment(hitIndex) usePaperSupportStore.getState().handleBufferPaths(activePath) @@ -1551,6 +1718,20 @@ export const usePaperStore = create((set) => ({ point = usePaperStore.getState().group!.globalToLocal(e.point) } + if ( + activePath && + ["fill", "stroke", "segment"].includes(panMode) && + !hasExceededPanDragThreshold + ) { + const dragDistance = e.point.getDistance(e.downPoint) + if (dragDistance < getPaperDragThreshold()) return + + hasExceededPanDragThreshold = true + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "move" + } + } + let movePath = (path: paper.Path) => { const currentGroupScaling = usePaperStore.getState().group!.scaling const delta = { @@ -1753,6 +1934,10 @@ export const usePaperStore = create((set) => ({ panTool.onMouseUp = (_e: paper.ToolEvent) => { if (stopMiddleMousePan()) return if (useTopToolsStore.getState().isView) return + hasExceededPanDragThreshold = false + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "default" + } let newDrawPath: paper.Path | paper.CompoundPath | null = null @@ -5233,12 +5418,27 @@ export const usePaperStore = create((set) => ({ ) circle.scale(newScale / currentScaling) circle.onMouseEnter = (_e: any) => { + hoveredPathCircleInfo = { + id, + index, + item: circle, + } + clearHoveredBufferPathHighlight() circle.strokeWidth = 4 circle.fillColor = new paper.Color("#FFF") + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "move" + } } circle.onMouseLeave = (_e: any) => { + if (hoveredPathCircleInfo?.item === circle) { + hoveredPathCircleInfo = null + } circle.strokeWidth = 2 circle.fillColor = color + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "default" + } } return circle }, @@ -5251,8 +5451,8 @@ export const usePaperStore = create((set) => ({ segments: [item.point, item.next], strokeColor: color, // 设置为透明,以便不可见 strokeScaling: false, - strokeWidth: 4, // 设置一个较大的strokeWidth作为缓冲区 - strokeCap: "butt", + strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击 + strokeCap: "round", onSelect: function (event: { stopPropagation: () => void }) { event.stopPropagation() // 阻止事件冒泡 }, @@ -5266,10 +5466,54 @@ export const usePaperStore = create((set) => ({ // bufferPath.segments[0].handleIn = item.handleIn; // bufferPath.segments[1].handleOut = item.handleOut; let edge: paper.Path | null + let clearTimer: ReturnType | null = null + const cancelScheduledClear = () => { + if (clearTimer) { + clearTimeout(clearTimer) + clearTimer = null + } + } + const clearHighlight = () => { + cancelScheduledClear() + edge?.remove() + edge = null + if (hoveredBufferPathInfo?.path === bufferPath) { + hoveredBufferPathInfo = null + } + if ( + usePaperStore.getState().el && + !hoveredPathCircleInfo?.item.parent && + !hoveredBufferPathInfo + ) { + usePaperStore.getState().el!.style.cursor = "default" + } + } + const scheduleClear = () => { + cancelScheduledClear() + clearTimer = setTimeout(() => { + clearHighlight() + }, BUFFER_HOVER_STICKY_MS) + } // 鼠标移入时高亮显示 bufferPath.onMouseEnter = function (event: { - stopPropagation: () => void + stopPropagation?: () => void }) { + if (hoveredPathCircleInfo?.item.parent) { + event.stopPropagation?.() + return + } + cancelScheduledClear() + hoveredBufferPathInfo = { + id, + index, + path: bufferPath, + clearHighlight, + scheduleClear, + cancelScheduledClear, + } + if (edge && !edge.parent) { + edge = null + } if (!edge) { edge = new paper.Path( Object.assign( @@ -5280,27 +5524,33 @@ export const usePaperStore = create((set) => ({ bufferPath.segments[1].point, ], strokeColor: "white", // 初始颜色与原始路径相同 - strokeWidth: 2, + strokeWidth: 3, strokeScaling: false, - strokeCap: "butt", + strokeCap: "round", }, { - data: { index, type: "pathBuff" }, + data: { index, type: "pathBuffHover" }, } ) ) // edge.bringToFront(); - event.stopPropagation() // 阻止事件冒泡 + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "crosshair" + } } + event.stopPropagation?.() // 阻止事件冒泡 } // 鼠标移出时取消高亮 bufferPath.onMouseLeave = function (event: { - stopPropagation: () => void + stopPropagation?: () => void }) { - edge?.remove() - edge = null - event.stopPropagation() // 阻止事件冒泡 + if (hoveredBufferPathInfo?.path === bufferPath) { + hoveredBufferPathInfo.scheduleClear() + } else { + scheduleClear() + } + event.stopPropagation?.() // 阻止事件冒泡 } return bufferPath }, From d1fac5d158e69c021f057f47b5d3aa9d85338f70 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 9 Apr 2026 18:20:12 +0800 Subject: [PATCH 3/9] feat(task): add filters --- app/layout.tsx | 1 + app/project/detail/TaskTableContainer.tsx | 276 +++++++++++----------- components/label/api/task/typing.ts | 2 + package.json | 1 + 4 files changed, 137 insertions(+), 143 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 230ae6c..fb37f14 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import "@mantine/core/styles.css" import "@mantine/core/styles.layer.css" +import "@mantine/dates/styles.css" import "@mantine/notifications/styles.css" import "mantine-datatable/styles.css" import Script from "next/script" diff --git a/app/project/detail/TaskTableContainer.tsx b/app/project/detail/TaskTableContainer.tsx index 096744d..7999573 100644 --- a/app/project/detail/TaskTableContainer.tsx +++ b/app/project/detail/TaskTableContainer.tsx @@ -32,6 +32,7 @@ import { Text, TextInput, } from "@mantine/core" +import { DatePickerInput, type DatesRangeValue } from "@mantine/dates" import { modals } from "@mantine/modals" import { notifications } from "@mantine/notifications" import { @@ -41,8 +42,9 @@ import { IconSearch, } from "@tabler/icons-react" import dayjs from "dayjs" +import "dayjs/locale/zh-cn" import duration from "dayjs/plugin/duration" -import { DataTableColumn } from "mantine-datatable" +import { DataTableColumn, type DataTableSortStatus } from "mantine-datatable" import { useRouter, useSearchParams } from "next/navigation" import { useCallback, useEffect, useMemo, useState } from "react" import * as XLSX from "xlsx-js-style" @@ -60,6 +62,8 @@ interface FilterFormRecord { label_user: string[] review_user1: string[] review_user2: string[] + first_commit_label_time: DatesRangeValue + first_commit_review1_time: DatesRangeValue } function createInitialFilters(): FilterFormRecord { @@ -71,10 +75,52 @@ function createInitialFilters(): FilterFormRecord { label_user: [], review_user1: [], review_user2: [], + first_commit_label_time: [null, null], + first_commit_review1_time: [null, null], } } const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20] +const INITIAL_SORT_ACCESSOR = "__initial__" + +type SortableTaskSizeAccessor = keyof Task.SizeProps + +const SORTABLE_TASK_SIZE_COLUMNS: Array<{ + accessor: SortableTaskSizeAccessor + title: string + width: number +}> = [ + { accessor: "object_size", title: "对象总数", width: 110 }, + { accessor: "valid_size", title: "有效对象数", width: 120 }, + { accessor: "new_size", title: "新增对象数", width: 120 }, + { accessor: "key_frame_size", title: "关键帧数", width: 120 }, + { accessor: "dynamic_size", title: "动态对象数", width: 120 }, + { accessor: "static_size", title: "静态对象数", width: 120 }, + { accessor: "prelabel_size", title: "预标注对象数", width: 120 }, + { accessor: "prelabel_modify_size", title: "预标注对象修改数", width: 160 }, + { accessor: "question_size", title: "问题数", width: 120 }, + { accessor: "total_qa_group_size", title: "总问答组数", width: 120 }, + { accessor: "single_qa_group_size", title: "单轮问答组数", width: 120 }, + { accessor: "multiple_qa_group_size", title: "多轮问答组数", width: 120 }, + { accessor: "new_qa_group_size", title: "新增问答组数", width: 120 }, + { + accessor: "new_single_qa_group_size", + title: "新增单轮问答组数", + width: 160, + }, + { + accessor: "new_multiple_qa_group_size", + title: "新增多轮问答组数", + width: 160, + }, + { accessor: "review_question_size", title: "审核问题数", width: 120 }, + { accessor: "comment_question_size", title: "首次批注问题数", width: 140 }, + { accessor: "review_size", title: "审核对象数", width: 120 }, + { accessor: "review_dynamic_size", title: "审核动态对象数", width: 140 }, + { accessor: "review_static_size", title: "审核静态对象数", width: 140 }, + { accessor: "first_comment_size", title: "批注数(1)", width: 120 }, + { accessor: "second_comment_size", title: "批注数(2)", width: 120 }, +] export interface selectedDataProps { status: number @@ -167,6 +213,12 @@ export default function TaskTableContainer(props: { const [selectedRecords, setSelectedRecords] = useState([]) const [loading, setLoading] = useState(false) const [queryTrigger, setQueryTrigger] = useState(0) + const [sortStatus, setSortStatus] = useState< + DataTableSortStatus + >({ + columnAccessor: INITIAL_SORT_ACCESSOR, + direction: "asc", + }) const [workflowTaskId, setWorkflowTaskId] = useState(null) const [dispatchRecordModal, setDispatchModalConfig] = useState<{ @@ -223,9 +275,33 @@ export default function TaskTableContainer(props: { if (form.review_user2?.length) { obj.review_user2 = form.review_user2?.map((item) => Number(item)) } + if (form.first_commit_label_time[0] && form.first_commit_label_time[1]) { + obj.first_commit_label_time_start = dayjs( + form.first_commit_label_time[0] + ).format("YYYY-MM-DD") + obj.first_commit_label_time_end = dayjs( + form.first_commit_label_time[1] + ).format("YYYY-MM-DD") + } + if ( + form.first_commit_review1_time[0] && + form.first_commit_review1_time[1] + ) { + obj.first_commit_review1_time_start = dayjs( + form.first_commit_review1_time[0] + ).format("YYYY-MM-DD") + obj.first_commit_review1_time_end = dayjs( + form.first_commit_review1_time[1] + ).format("YYYY-MM-DD") + } + if (sortStatus.columnAccessor !== INITIAL_SORT_ACCESSOR) { + obj.order_by = `${sortStatus.columnAccessor} ${sortStatus.direction}` + } return obj }, [ form.current_uid, + form.first_commit_label_time, + form.first_commit_review1_time, form.label_user, form.rejected, form.review_user1, @@ -235,6 +311,8 @@ export default function TaskTableContainer(props: { pageNumber, pageSize, projectId, + sortStatus.columnAccessor, + sortStatus.direction, ]) const load = useCallback(async () => { @@ -528,6 +606,22 @@ export default function TaskTableContainer(props: { } const columns = useMemo(() => { + const createSortableSizeColumn = ({ + accessor, + title, + width, + }: { + accessor: SortableTaskSizeAccessor + title: string + width: number + }): DataTableColumn => ({ + accessor, + title, + width, + sortable: true, + render: (record) => record.label_object_size?.[accessor] ?? "-", + }) + const cols: DataTableColumn[] = [ { accessor: "id", @@ -595,148 +689,7 @@ export default function TaskTableContainer(props: { width: 120, render: (record) => record.review2_username ?? "-", }, - { - accessor: "label_object_size.object_size", - title: "对象总数", - width: 110, - render: (record) => record.label_object_size?.object_size ?? "-", - }, - { - accessor: "label_object_size.valid_size", - title: "有效对象数", - width: 120, - render: (record) => record.label_object_size?.valid_size ?? "-", - }, - { - accessor: "label_object_size.new_size", - title: "新增对象数", - width: 120, - render: (record) => record.label_object_size?.new_size ?? "-", - }, - { - accessor: "label_object_size.key_frame_size", - title: "关键帧数", - width: 120, - render: (record) => record.label_object_size?.key_frame_size ?? "-", - }, - { - accessor: "label_object_size.dynamic_size", - title: "动态对象数", - width: 120, - render: (record) => record.label_object_size?.dynamic_size ?? "-", - }, - { - accessor: "label_object_size.static_size", - title: "静态对象数", - width: 120, - render: (record) => record.label_object_size?.static_size ?? "-", - }, - { - accessor: "label_object_size.prelabel_size", - title: "预标注对象数", - width: 120, - render: (record) => record.label_object_size?.prelabel_size ?? "-", - }, - { - accessor: "label_object_size.prelabel_modify_size", - title: "预标注对象修改数", - width: 160, - render: (record) => - record.label_object_size?.prelabel_modify_size ?? "-", - }, - { - accessor: "label_object_size.question_size", - title: "问题数", - width: 120, - render: (record) => record.label_object_size?.question_size ?? "-", - }, - { - accessor: "label_object_size.total_qa_group_size", - title: "总问答组数", - width: 120, - render: (record) => - record.label_object_size?.total_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.single_qa_group_size", - title: "单轮问答组数", - width: 120, - render: (record) => - record.label_object_size?.single_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.multiple_qa_group_size", - title: "多轮问答组数", - width: 120, - render: (record) => - record.label_object_size?.multiple_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.new_qa_group_size", - title: "新增问答组数", - width: 120, - render: (record) => record.label_object_size?.new_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.new_single_qa_group_size", - title: "新增单轮问答组数", - width: 160, - render: (record) => - record.label_object_size?.new_single_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.new_multiple_qa_group_size", - title: "新增多轮问答组数", - width: 160, - render: (record) => - record.label_object_size?.new_multiple_qa_group_size ?? "-", - }, - { - accessor: "label_object_size.review_question_size", - title: "审核问题数", - width: 120, - render: (record) => - record.label_object_size?.review_question_size ?? "-", - }, - { - accessor: "label_object_size.comment_question_size", - title: "首次批注问题数", - width: 140, - render: (record) => - record.label_object_size?.comment_question_size ?? "-", - }, - { - accessor: "label_object_size.review_size", - title: "审核对象数", - width: 120, - render: (record) => record.label_object_size?.review_size ?? "-", - }, - { - accessor: "label_object_size.review_dynamic_size", - title: "审核动态对象数", - width: 140, - render: (record) => - record.label_object_size?.review_dynamic_size ?? "-", - }, - { - accessor: "label_object_size.review_static_size", - title: "审核静态对象数", - width: 140, - render: (record) => record.label_object_size?.review_static_size ?? "-", - }, - { - accessor: "label_object_size.first_comment_size", - title: "批注数(1)", - width: 120, - render: (record) => record.label_object_size?.first_comment_size ?? "-", - }, - { - accessor: "label_object_size.second_comment_size", - title: "批注数(2)", - width: 120, - render: (record) => - record.label_object_size?.second_comment_size ?? "-", - }, + ...SORTABLE_TASK_SIZE_COLUMNS.map(createSortableSizeColumn), { accessor: "first_commit_label_time", title: "标注首次提交时间", @@ -966,6 +919,36 @@ export default function TaskTableContainer(props: { setFilters((s) => ({ ...s, review_user2: v })) } /> + + setFilters((s) => ({ + ...s, + first_commit_label_time: value, + })) + } + /> + + setFilters((s) => ({ + ...s, + first_commit_review1_time: value, + })) + } + /> @@ -1038,6 +1021,13 @@ export default function TaskTableContainer(props: { fetching={loading} records={records} columns={columns} + sortStatus={sortStatus} + onSortStatusChange={( + nextSortStatus: DataTableSortStatus + ) => { + setPageNumber(1) + setSortStatus(nextSortStatus) + }} totalRecords={total} recordsPerPage={pageSize} page={pageNumber} diff --git a/components/label/api/task/typing.ts b/components/label/api/task/typing.ts index c1a1ec3..b1f77ce 100644 --- a/components/label/api/task/typing.ts +++ b/components/label/api/task/typing.ts @@ -1,5 +1,6 @@ export namespace Task { export interface DataProps { + [key: string]: unknown id: number create_date: string data_type?: number @@ -74,6 +75,7 @@ export namespace Task { id?: number[] label_status?: number[] label_user?: number[] + // sample: object_size asc/object_size desc order_by?: string page_number: number page_size: number diff --git a/package.json b/package.json index 35c1ea4..cc440cf 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@ffmpeg/util": "^0.12.2", "@fingerprintjs/fingerprintjs": "^5.0.1", "@mantine/core": "^8.3.1", + "@mantine/dates": "^8.3.18", "@mantine/form": "^8.3.1", "@mantine/hooks": "^8.3.1", "@mantine/modals": "^8.3.10", From d6183a00365c9c7c6594cd7cecb07fc0c596625b Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 9 Apr 2026 18:21:12 +0800 Subject: [PATCH 4/9] feat(tool): eclipse --- components/label/LabelNossr.tsx | 9 +- .../label/components/PaperContainer.tsx | 181 +++++++++++++++++- components/label/components/TopTools.tsx | 16 +- components/label/usePaperStore.ts | 3 + components/label/useTopToolsStore.ts | 9 + 5 files changed, 210 insertions(+), 8 deletions(-) diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index e1375f3..e3330a8 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -267,6 +267,7 @@ const LabelPage = ({ setShowMultiFrame, isView, setIsView, + renderMode, scale, setScale, objectOperations, @@ -1307,8 +1308,8 @@ const LabelPage = ({ ) const setDrawType = useCallback(() => { - if (isView) { - setPaperMode("pan") + if (isView || renderMode) { + setPaperMode(null) } else { let operationSchema = getOperationSchema(activeOperation) if (operationSchema) { @@ -1371,6 +1372,7 @@ const LabelPage = ({ editMode, getOperationSchema, isView, + renderMode, setPaperMode, setToolOption, ]) @@ -2117,7 +2119,7 @@ const LabelPage = ({ return } - if (isView) return + if (isView || renderMode) return if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") { e.preventDefault() @@ -2426,6 +2428,7 @@ const LabelPage = ({ handleContinueSelectedPolygon, handleShortcut, isView, + renderMode, projectDetail?.label_type, setEditMode, setGroupScale, diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 4143de7..85e75fc 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -51,6 +51,7 @@ import { useBottomToolsStore } from "../useBottomToolsStore" import { useKeyboardStore } from "../useKeyBoardStore" import { useOtherToolsStore } from "../useOtherToolsStore" import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore" +import { usePaperSupportStore } from "../usePaperSupportStore" import { renderGroupPath } from "../useRenderGroupPath" import useRenderTag from "../useRenderTag" import { useRightToolsStore } from "../useRightToolsStore" @@ -443,6 +444,7 @@ const PaperContainer = ( crosshairStatus, assistToolEnabled, assistToolSize, + renderMode, showTags, showTagsConfigs, } = useTopToolsStore() @@ -452,6 +454,8 @@ const PaperContainer = ( selectedPath, updateSelectedPath, } = useObjectStore() + const currentSelectedPath = selectedPath[activeImage] || [] + const currentSelectedPathKey = currentSelectedPath.join(",") const { showContextMenu, @@ -511,6 +515,11 @@ const PaperContainer = ( token: number imageName: string } | null>(null) + const renderModePanRef = useRef({ + active: false, + lastX: 0, + lastY: 0, + }) const paperScope = useRef(null) useEffect(() => { @@ -1812,6 +1821,52 @@ const PaperContainer = ( ] ) + const syncRenderModeVisuals = useCallback( + (targetGroup?: paper.Group | null) => { + const group = targetGroup ?? usePaperStore.getState().group + if (!group) return + + group.children.forEach((item) => { + const itemData = item.data || {} + const itemType = itemData.type + + if ( + itemData.name === "pic" || + itemData.id === "support" || + !itemData.fillColor || + !itemData.blankColor || + [ + "mask", + "text", + "tag", + "groupPath", + "pathCircle", + "pathBuff", + "circle", + ].includes(itemType) + ) { + return + } + + const nextFillState = renderMode || itemData.selected ? "fill" : "blank" + if (itemData.renderModeFillState === nextFillState) { + return + } + itemData.renderModeFillState = nextFillState + + const nextFillColor = + renderMode || itemData.selected + ? itemData.fillColor + : itemData.blankColor + const pathItem = item as paper.PathItem + pathItem.fillColor = nextFillColor + ? new paper.Color(nextFillColor) + : null + }) + }, + [renderMode] + ) + // 生成辅助标注结果数据并绘制 const saveSupportAnnotationData = useCallback(() => { const group = usePaperStore.getState().group @@ -2163,6 +2218,7 @@ const PaperContainer = ( if (useBottomToolsStore.getState().activeImage !== expectedImage) return renderGroupPath() renderTag() + syncRenderModeVisuals() return } @@ -2194,6 +2250,7 @@ const PaperContainer = ( if (useBottomToolsStore.getState().activeImage !== expectedImage) return renderGroupPath() renderTag() + syncRenderModeVisuals() return } @@ -2238,6 +2295,7 @@ const PaperContainer = ( if (useBottomToolsStore.getState().activeImage !== expectedImage) return renderGroupPath() renderTag() + syncRenderModeVisuals() } if (typeof globalThis.requestAnimationFrame === "function") { @@ -2246,7 +2304,13 @@ const PaperContainer = ( setTimeout(runBatch, 0) } }, - [clearRenderedAnnotationItems, objectOperations, renderPolygons, renderTag] + [ + clearRenderedAnnotationItems, + objectOperations, + renderPolygons, + renderTag, + syncRenderModeVisuals, + ] ) const withPaperGroup = useCallback( @@ -2365,6 +2429,7 @@ const PaperContainer = ( }) if (!tasks.length) { + syncRenderModeVisuals(targetGroup) resolve(true) return } @@ -2403,6 +2468,7 @@ const PaperContainer = ( return } + syncRenderModeVisuals(targetGroup) resolve(true) } @@ -2416,10 +2482,107 @@ const PaperContainer = ( isPendingFrameSwitchStale, objectOperations, renderPolygons, + syncRenderModeVisuals, withPaperGroup, ] ) + useEffect(() => { + if (!setup || loadingData) return + + const group = usePaperStore.getState().group + if (!group) return + + syncRenderModeVisuals(group) + + if (renderMode) { + usePaperSupportStore.getState().clearAll() + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: false, + position: { top: 0, left: 0 }, + imageId: "", + operationId: "", + id: 0, + }) + } + }, [ + activeImage, + activeImageLabel, + currentSelectedPathKey, + loadingData, + renderMode, + setup, + syncRenderModeVisuals, + ]) + + const stopRenderModePan = useCallback(() => { + renderModePanRef.current.active = false + }, []) + + const handleRenderModeOverlayMouseDown = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: false, + position: { top: 0, left: 0 }, + imageId: "", + operationId: "", + id: 0, + }) + + if (event.button !== 1) return + + renderModePanRef.current = { + active: true, + lastX: event.clientX, + lastY: event.clientY, + } + }, + [] + ) + + useEffect(() => { + if (!renderMode) { + stopRenderModePan() + return + } + + const handleMouseMove = (event: MouseEvent) => { + const currentPan = renderModePanRef.current + if (!currentPan.active) return + + event.preventDefault() + const group = usePaperStore.getState().group + if (!group) return + + const deltaX = event.clientX - currentPan.lastX + const deltaY = event.clientY - currentPan.lastY + if (!deltaX && !deltaY) return + + group.position = group.position.add(new paper.Point(deltaX, deltaY)) + renderModePanRef.current.lastX = event.clientX + renderModePanRef.current.lastY = event.clientY + } + + const handleMouseUp = (event: MouseEvent) => { + if (event.button === 1) { + event.preventDefault() + } + stopRenderModePan() + } + + window.addEventListener("mousemove", handleMouseMove, { passive: false }) + window.addEventListener("mouseup", handleMouseUp, { passive: false }) + + return () => { + window.removeEventListener("mousemove", handleMouseMove) + window.removeEventListener("mouseup", handleMouseUp) + stopRenderModePan() + } + }, [renderMode, stopRenderModePan]) + const preparePendingFrameSwitch = useCallback( async (imageName: string, token: number) => { if ( @@ -4024,6 +4187,22 @@ const PaperContainer = ( }} /> )} + {renderMode && ( + { + event.preventDefault() + }} + onMouseDown={handleRenderModeOverlayMouseDown} + style={{ + position: "absolute", + inset: 0, + zIndex: 47, + background: "rgba(128, 128, 128, 0.24)", + pointerEvents: "auto", + }} + /> + )} {contextMenu} )} - + + { + setRenderMode(!renderMode) + }}> + + + ((set) => ({ break default: usePaperStore.getState().viewTool?.activate() + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = "default" + } break } }, diff --git a/components/label/useTopToolsStore.ts b/components/label/useTopToolsStore.ts index cc06de4..7a189b4 100644 --- a/components/label/useTopToolsStore.ts +++ b/components/label/useTopToolsStore.ts @@ -10,6 +10,8 @@ interface TopToolsState { // 标注页面是否仅查看 isView: boolean setIsView: (val: boolean) => void + renderMode: boolean + setRenderMode: (val: boolean) => void // 绘制模式 editMode: boolean setEditMode: (val: boolean) => void @@ -81,6 +83,7 @@ interface TopToolsState { const initialTopToolsState = { isView: false, + renderMode: false, editMode: false, pressA: false, imageFilter: { @@ -114,6 +117,12 @@ export const useTopToolsStore = create()( ...state, isView: val, })), + renderMode: initialTopToolsState.renderMode, + setRenderMode: (val: boolean) => + set((state: TopToolsState) => ({ + ...state, + renderMode: val, + })), editMode: initialTopToolsState.editMode, setEditMode: (val: boolean) => set((state: TopToolsState) => ({ From f2cd168b1cbbde9fb49e148b8a367804ca7e4338 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 09:54:40 +0800 Subject: [PATCH 5/9] fix(task): fix height --- app/project/detail/OwnTaskTableContainer.tsx | 1 + app/project/detail/TaskTableContainer.tsx | 1 + components/setting/PageSurface.tsx | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/app/project/detail/OwnTaskTableContainer.tsx b/app/project/detail/OwnTaskTableContainer.tsx index c53c081..ccf6066 100644 --- a/app/project/detail/OwnTaskTableContainer.tsx +++ b/app/project/detail/OwnTaskTableContainer.tsx @@ -405,6 +405,7 @@ export default function OwnTaskTableContainer(props: { /> width="100%" + minHeight={0} style={{ width: "100%", flex: 1 }} pinFirstColumn pinLastColumn diff --git a/app/project/detail/TaskTableContainer.tsx b/app/project/detail/TaskTableContainer.tsx index 7999573..2a63715 100644 --- a/app/project/detail/TaskTableContainer.tsx +++ b/app/project/detail/TaskTableContainer.tsx @@ -1014,6 +1014,7 @@ export default function TaskTableContainer(props: { /> width="100%" + minHeight={0} style={{ width: "100%", flex: 1 }} pinFirstColumn pinLastColumn diff --git a/components/setting/PageSurface.tsx b/components/setting/PageSurface.tsx index 8d2ff0b..65a06df 100644 --- a/components/setting/PageSurface.tsx +++ b/components/setting/PageSurface.tsx @@ -195,11 +195,14 @@ const settingDataTableStyles = { }, root: { height: "100%", + maxWidth: "100%", + minWidth: 0, fontSize: "14px", color: "#31373D", borderBottom: "1px solid #E8EDF3", borderRadius: 10, overflow: "hidden", + boxSizing: "border-box", backgroundColor: "#FFFFFF", }, pagination: { @@ -307,6 +310,7 @@ export const settingTabsStyles = { panel: { flex: 1, minHeight: 0, + minWidth: 0, paddingTop: 16, }, } as const From 6aff25a38f9f10c051d38a7136972379f43016a2 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 11:14:06 +0800 Subject: [PATCH 6/9] feat(tool): hide all object --- .../label/components/RightObjectTools.tsx | 840 +++++++++--------- 1 file changed, 433 insertions(+), 407 deletions(-) diff --git a/components/label/components/RightObjectTools.tsx b/components/label/components/RightObjectTools.tsx index 0d8934b..91ebed6 100644 --- a/components/label/components/RightObjectTools.tsx +++ b/components/label/components/RightObjectTools.tsx @@ -355,13 +355,10 @@ const RightObjectTools: React.FC = (props) => { 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 - } + const hideFlag = objList.every(([id]) => { + return useObjectStore.getState().pathStatus[`${imgId}${id}`]?.["HIDE"] + ? true + : false }) updateOperationStatus(imgId + operationId, "HIDE", hideFlag) } @@ -401,6 +398,39 @@ const RightObjectTools: React.FC = (props) => { [activeImage] ) + const activeImageOperations = labelState.get(activeImage) || [] + + const isObjectHidden = (imageId: string, objectId: number) => { + return pathStatus[imageId + objectId]?.["HIDE"] ?? false + } + + const isOperationHidden = (imageId: string, operationId: string) => { + const operationChildren = storeLabel.get(imageId)?.get(operationId) || [] + return ( + operationChildren.length > 0 && + operationChildren.every(([objectId]) => isObjectHidden(imageId, objectId)) + ) + } + + const totalObjectCount = activeImageOperations.reduce( + (count, operationId) => { + return ( + count + (storeLabel.get(activeImage)?.get(operationId)?.length || 0) + ) + }, + 0 + ) + + const areAllObjectsHidden = + totalObjectCount > 0 && + activeImageOperations.every((operationId) => { + const operationChildren = + storeLabel.get(activeImage)?.get(operationId) || [] + return operationChildren.every(([objectId]) => + isObjectHidden(activeImage, objectId) + ) + }) + // const renderObjectName = useCallback((name: string, input: string) => { // const index = name?.indexOf(input); // const beforeStr = name?.substring(0, index); @@ -509,9 +539,22 @@ const RightObjectTools: React.FC = (props) => { - - 对象列表 - + + { + if (!totalObjectCount) return + activeImageOperations.forEach((operationId) => { + operationHide(operationId, !areAllObjectsHidden) + }) + }}> + {areAllObjectsHidden ? : } + + + 对象列表 + + = (props) => { }}> - {labelState.get(activeImage) && - labelState.get(activeImage)?.map((operationId) => { - const operationSchema = getOperationSchema(operationId) - const isCollapsed = - operationStatus[activeImage + operationId]?.["COLLAPSE"] - const isHide = - operationStatus[activeImage + operationId]?.HIDE - const hasSubAttrError = getOperationSubAttrStatus(operationId) - const operationLabel = operationSchema?.label_class - const operationChildren = - storeLabel.get(activeImage)?.get(operationId) || [] - const objectCount = operationChildren.length - const visibleChildren = operationChildren.filter((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] + {activeImageOperations.map((operationId) => { + const operationSchema = getOperationSchema(operationId) + const isCollapsed = + operationStatus[activeImage + operationId]?.["COLLAPSE"] + const isHide = isOperationHidden(activeImage, operationId) + const hasSubAttrError = getOperationSubAttrStatus(operationId) + const operationLabel = operationSchema?.label_class + const operationChildren = + storeLabel.get(activeImage)?.get(operationId) || [] + const objectCount = operationChildren.length + const visibleChildren = operationChildren.filter((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 } - if (checkBoxsChecked.every((value) => !value)) - visibleFlag = true - return visibleFlag - }) - return ( - - - - - - operationHide(operationId, !isHide) - }> - {!isHide ? ( - - ) : ( - - )} - - - - {renderOperationIcon(operationSchema)} - - {renderShortcutKey(operationId)} - - - - - {operationLabel} - - - - + visibleFlag = checkBoxsChecked[3 - type] + } + if (checkBoxsChecked.every((value) => !value)) + visibleFlag = true + return visibleFlag + }) + return ( + + + + + operationHide(operationId, !isHide)}> + {!isHide ? : } + - { - setSearchSubAttrs((searchValues) => ({ - ...searchValues, - [operationId]: e.target.value, - })) - }} - onKeyDown={(e) => - handleSelectedBySearchSubAttr(e, operationId) - } - /> - - {objectCount} - - { - updateOperationStatus( - activeImage + operationId, - "COLLAPSE", - !isCollapsed - ) + style={{ flex: 1, minWidth: 0 }}> + - {!isCollapsed ? ( - - ) : ( - - )} - + {renderOperationIcon(operationSchema)} + + {renderShortcutKey(operationId)} + + + + + {operationLabel} + + - - - - {visibleChildren.map((child, childIndex) => { - const { comment } = child[2] - const isSelected = selectedPath[ - activeImage - ]?.includes(child[0]) - const isChildHide = - pathStatus[activeImage + child[0]]?.["HIDE"] - const hasChildError = getSubAttrStatus( - getOperationSchema(operationId), - child[2]?.sub_attributes - ) - const isLastVisibleChild = - childIndex === visibleChildren.length - 1 - - let textColor = undefined - let connectorColor = "var(--mantine-color-gray-4)" - - if (hasChildError) { - textColor = "red" - connectorColor = "var(--mantine-color-red-3)" - } else if (isSelected) { - textColor = "white" - connectorColor = - "var(--mantine-primary-color-filled)" + + { + setSearchSubAttrs((searchValues) => ({ + ...searchValues, + [operationId]: e.target.value, + })) + }} + onKeyDown={(e) => + handleSelectedBySearchSubAttr(e, operationId) } + /> + + {objectCount} + + { + updateOperationStatus( + activeImage + operationId, + "COLLAPSE", + !isCollapsed + ) + }}> + {!isCollapsed ? ( + + ) : ( + + )} + + + + + + + {visibleChildren.map((child, childIndex) => { + const { comment } = child[2] + const isSelected = selectedPath[ + activeImage + ]?.includes(child[0]) + const isChildHide = + pathStatus[activeImage + child[0]]?.["HIDE"] + const hasChildError = getSubAttrStatus( + getOperationSchema(operationId), + child[2]?.sub_attributes + ) + const isLastVisibleChild = + childIndex === visibleChildren.length - 1 - return ( + let textColor = undefined + let connectorColor = "var(--mantine-color-gray-4)" + + if (hasChildError) { + textColor = "red" + connectorColor = "var(--mantine-color-red-3)" + } else if (isSelected) { + textColor = "white" + connectorColor = + "var(--mantine-primary-color-filled)" + } + + return ( + + style={{ + position: "absolute", + left: 7, + top: 0, + bottom: isLastVisibleChild ? "75%" : 0, + width: 1.5, + borderRadius: 999, + background: connectorColor, + opacity: isSelected ? 0.9 : 0.35, + }} + /> + {isLastVisibleChild ? ( - {isLastVisibleChild ? ( + ) : ( + <> - ) : ( - <> - - - - )} - - handleSelect(child[0], operationId) - } - style={{ cursor: "pointer" }}> - - - { - e.stopPropagation() - objectHide( - operationId, - child[0], - !isChildHide - ) - }}> - {!isChildHide ? ( - - ) : ( - - )} - - - {child[0]?.toString()} - - + + + )} + + 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]?.uid + child[2] + ?.first_modified_uid )?.label } {dayjs( - (child[2]?.create_timestamp || + (child[2] + ?.first_modified_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> - - + )} + {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" + ) + : "" + }`} + + + + ) + })} - {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} - - - - ) - })} - - - - ) - })} + ) : null} + + + + ) + })} + + + + ) + })} From 98d697a542a9f60f53df0ce237be538a62ff5a81 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 11:21:30 +0800 Subject: [PATCH 7/9] feat(label): add right select --- components/label/usePaperStore.ts | 304 ++++++++++++++++++++++++++---- 1 file changed, 271 insertions(+), 33 deletions(-) diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 21c538b..3354bf2 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -318,6 +318,229 @@ const hidePaperContextMenu = () => { }) } +const PAPER_OBJECT_TYPES = ["rectangle", "polygon", "brush", "point"] as const +const PAPER_CONTEXT_HIT_TYPES = [ + ...PAPER_OBJECT_TYPES, + "circle", + "mask", + "pathBuff", + "pathCircle", +] as const + +const isPaperObjectType = (type: unknown) => + typeof type === "string" && + PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number]) + +const isPaperContextHitType = (type: unknown) => + typeof type === "string" && + PAPER_CONTEXT_HIT_TYPES.includes( + type as (typeof PAPER_CONTEXT_HIT_TYPES)[number] + ) + +const getPaperCursorByMode = ( + mode: PaperState["mode"] = usePaperStore.getState().mode +) => { + if ( + ["polygon", "rectangle", "brush", "point", "support"].includes(mode || "") + ) { + return "crosshair" + } + return "default" +} + +const syncPaperCursorByMode = ( + mode: PaperState["mode"] = usePaperStore.getState().mode +) => { + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = getPaperCursorByMode(mode) + } +} + +const clearPaperSelectedObjects = () => { + const group = usePaperStore.getState().group + if (!group) return + + group + .getItems({ + match: (item: paper.Item) => !!item.data?.selected, + }) + .forEach((item) => { + item.data.selected = false + }) + + usePaperSupportStore.getState().clearAll() + + const activeImage = useBottomToolsStore.getState().activeImage + const selectedIds = [ + ...(useObjectStore.getState().selectedPath[activeImage] || []), + ] + + selectedIds.forEach((id) => { + usePaperStore + .getState() + .getItemsById(id) + .forEach((item) => { + if ( + (item instanceof paper.Path || item instanceof paper.CompoundPath) && + ["rectangle", "polygon"].includes(item.data?.type || "") + ) { + item.fillColor = item.data?.blankColor || item.fillColor || null + } + }) + }) + + selectedIds.forEach((id) => { + useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", id) + }) +} + +const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => { + if (item.data.type === "rectangle" && item instanceof paper.Path) { + item.data.selected = true + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + item.fillColor = item.data.fillColor || item.fillColor || null + } else if (item.data.type === "brush") { + item.data.selected = true + if (item instanceof paper.Path) { + usePaperSupportStore.getState().handleMask(item) + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + } + } else if (item.data.type === "point" && item instanceof paper.Path) { + item.data.selected = true + usePaperSupportStore.getState().handleMask(item) + usePaperSupportStore.getState().handleText(item) + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + } else if (item.data.type === "polygon") { + item.data.selected = true + if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) { + item.fillColor = item.data.fillColor || item.fillColor || null + } + } + + item.bringToFront() +} + +const getPaperSelectionPath = ( + id: number, + localPoint: paper.Point, + preferredItem?: paper.Item | null +) => { + if ( + preferredItem instanceof paper.Path && + isPaperObjectType(preferredItem.data?.type) + ) { + return preferredItem + } + + const objectPaths = usePaperStore + .getState() + .getItemsById(id) + .filter( + (item): item is paper.Path => + item instanceof paper.Path && isPaperObjectType(item.data?.type) + ) + + if (!objectPaths.length) return null + if (objectPaths.length === 1) return objectPaths[0] + + let nearestPath = objectPaths[0] + for (let index = 1; index < objectPaths.length; index += 1) { + const item = objectPaths[index] + if ( + item.getNearestPoint(localPoint).getDistance(localPoint) < + nearestPath.getNearestPoint(localPoint).getDistance(localPoint) + ) { + nearestPath = item + } + } + + return nearestPath +} + +const trySelectPaperObjectByPoint = (clickPoint: paper.Point) => { + const group = usePaperStore.getState().group + if (!group) return false + + const hitResult = group.hitTest(clickPoint, { + segments: true, + stroke: true, + curves: true, + fill: true, + guides: false, + tolerance: usePaperStore.getState().paperScope + ? 8 / useTopToolsStore.getState().scale + : 0, + match: (hit: paper.HitResult) => + !!hit.item.data?.id && isPaperContextHitType(hit.item.data?.type), + }) + + const id = hitResult?.item?.data?.id + if (!id) return false + + clearPaperSelectedObjects() + + const localPoint = group.globalToLocal(clickPoint) + const selectedPath = getPaperSelectionPath(id, localPoint, hitResult?.item) + const operationId = ( + selectedPath?.data?.operationId || + usePaperStore.getState().getItemById(id)?.data?.operationId || + hitResult?.item?.data?.operationId || + "" + ).toString() + + usePaperStore + .getState() + .getItemsById(id) + .forEach((item) => { + if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) + return + if (!isPaperObjectType(item.data?.type)) return + + if (item.data.type === "polygon") { + applyPaperSelectedPath(item) + return + } + + if ( + item instanceof paper.CompoundPath || + (selectedPath && item === selectedPath) + ) { + applyPaperSelectedPath(item) + } + }) + + const activeImage = useBottomToolsStore.getState().activeImage + useObjectStore.getState().updateSelectedPath(activeImage, "ADD", id) + + const category = useTopToolsStore + .getState() + .objectOperations.find( + (item) => item.category_id.toString() === operationId + ) + const { top, left } = getShowContextMenuPosition( + clickPoint, + (category || {}) as Project.LabelSchemaList, + 5, + 5 + ) + + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: true, + position: { top, left }, + imageId: + selectedPath?.data?.imageId || + hitResult?.item?.data?.imageId || + activeImage, + operationId, + id, + }) + + return true +} + let isMiddleMousePanning = false let hoveredBufferPathInfo: { id: number @@ -3530,8 +3753,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoPolygonPoint() + if (polygon || points.length) { + hidePaperContextMenu() + undoPolygonPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -4113,8 +4342,18 @@ export const usePaperStore = create((set) => ({ let rect: paper.Path.Rectangle | null let rectangleTool = paperRectangleTool - rectangleTool.onMouseDown = (e: { event: MouseEvent }) => { + rectangleTool.onMouseDown = (e: { + event: MouseEvent + point: paper.Point + }) => { if (startMiddleMousePan(e.event)) return + if (e.event.button === 2) { + e.event.preventDefault() + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } + return + } if (e.event && e.event.button !== 2) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: false, @@ -4463,8 +4702,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoBrushPoint() + if (myPath || points.length) { + hidePaperContextMenu() + undoBrushPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -4934,8 +5179,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoPoint() + if (myPath || points.length) { + hidePaperContextMenu() + undoPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -5426,7 +5677,10 @@ export const usePaperStore = create((set) => ({ clearHoveredBufferPathHighlight() circle.strokeWidth = 4 circle.fillColor = new paper.Color("#FFF") - if (usePaperStore.getState().el) { + if ( + usePaperStore.getState().mode === "pan" && + usePaperStore.getState().el + ) { usePaperStore.getState().el!.style.cursor = "move" } } @@ -5436,9 +5690,7 @@ export const usePaperStore = create((set) => ({ } circle.strokeWidth = 2 circle.fillColor = color - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode() } return circle }, @@ -5485,7 +5737,7 @@ export const usePaperStore = create((set) => ({ !hoveredPathCircleInfo?.item.parent && !hoveredBufferPathInfo ) { - usePaperStore.getState().el!.style.cursor = "default" + syncPaperCursorByMode() } } const scheduleClear = () => { @@ -5564,45 +5816,31 @@ export const usePaperStore = create((set) => ({ switch (modeParam) { case "pan": usePaperStore.getState().panTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode(modeParam) break case "polygon": usePaperStore.getState().polygonTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "rectangle": usePaperStore.getState().rectangleTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "brush": usePaperStore.getState().brushTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "point": usePaperStore.getState().pointTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "support": usePaperStore.getState().supportTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break default: usePaperStore.getState().viewTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode(modeParam) break } }, From 140df9202d977760e4637072aa123b4aba2c6386 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 13:18:33 +0800 Subject: [PATCH 8/9] feat(tool): size/pos store --- .../label/components/PaperContainer.tsx | 73 +++++++++++++++++-- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 85e75fc..fd5c1fc 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -38,6 +38,7 @@ import { getTrackingAuxiliaryAnnotation, } from "../api/label" import { Project } from "../api/project/typing" +import { labelimagePerformanceConfig } from "../config/performance" import { LabelState } from "../LabelNossr" import { initialDetail, @@ -57,7 +58,6 @@ import useRenderTag from "../useRenderTag" import { useRightToolsStore } from "../useRightToolsStore" import { useTopToolsStore } from "../useTopToolsStore" import { checkCommentsIsSame } from "../util" -import { labelimagePerformanceConfig } from "../config/performance" import { safeClone } from "../utils/clone" import { labelTypeMap } from "../utils/constants" import { adjustPoints } from "../utils/paperjs" @@ -126,6 +126,11 @@ type FrameSwitchGhostState = { animating: boolean } +const clonePaperPoint = (point: paper.Point | null | undefined) => { + if (!point) return null + return new paper.Point(point.x, point.y) +} + const normalizePoint = (value: any): NormalizedPoint | null => { if (value instanceof paper.Point) return [value.x, value.y] if (value instanceof paper.Segment) { @@ -445,6 +450,7 @@ const PaperContainer = ( assistToolEnabled, assistToolSize, renderMode, + saveCurrentScale, showTags, showTagsConfigs, } = useTopToolsStore() @@ -2583,6 +2589,24 @@ const PaperContainer = ( } }, [renderMode, stopRenderModePan]) + const getTargetGroupPosition = useCallback( + ({ + defaultPosition, + preservedPosition, + canPreserve, + }: { + defaultPosition: paper.Point + preservedPosition?: paper.Point | null + canPreserve?: boolean + }) => { + if (saveCurrentScale && canPreserve && preservedPosition) { + return clonePaperPoint(preservedPosition) ?? defaultPosition.clone() + } + return defaultPosition.clone() + }, + [saveCurrentScale] + ) + const preparePendingFrameSwitch = useCallback( async (imageName: string, token: number) => { if ( @@ -2600,6 +2624,12 @@ const PaperContainer = ( clearPendingFrameSwitch(token) return } + const preserveGroupPosition = + saveCurrentScale && + currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0 + const preservedGroupPosition = preserveGroupPosition + ? clonePaperPoint(currentVisibleGroup.position) + : null const stagingGroup = new paper.Group({ applyMatrix: false, @@ -2684,10 +2714,15 @@ const PaperContainer = ( raster.bounds.width / 2, raster.bounds.height / 2 ) - stagingGroup.position = new paper.Point( + const centeredGroupPosition = new paper.Point( raster.bounds.width / 2, raster.bounds.height / 2 ) + stagingGroup.position = getTargetGroupPosition({ + defaultPosition: centeredGroupPosition, + preservedPosition: preservedGroupPosition, + canPreserve: preserveGroupPosition, + }) raster.sendToBack() const imageData = useLabelStore.getState().label.get(imageName) @@ -2732,6 +2767,7 @@ const PaperContainer = ( [ clearPendingFrameSwitch, commitFrameSwitch, + getTargetGroupPosition, imgSize?.clientHeight, imgSize?.clientWidth, isPendingFrameSwitchStale, @@ -2739,6 +2775,7 @@ const PaperContainer = ( reconcileRasterScaleForImage, renderImagePolygonsBatchedToGroup, renderTag, + saveCurrentScale, setGroup, setRasterPath, setRasterSize, @@ -2823,6 +2860,11 @@ const PaperContainer = ( if (activeImage && projectDetail?.id) { const requestId = ++rasterLoadRequestIdRef.current const requestImage = activeImage + const preserveGroupPosition = + saveCurrentScale && getPicRasters(paperGroup).length > 0 + const preservedGroupPosition = preserveGroupPosition + ? clonePaperPoint(paperGroup.position) + : null rasterLoadingImageRef.current = requestImage cancelRasterFadeTransition() getPicRasters(paperGroup).forEach((item) => { @@ -2981,6 +3023,10 @@ const PaperContainer = ( raster.bounds.width / 2, raster.bounds.height / 2 ) + const centeredGroupPosition = new paper.Point( + raster.bounds.width / 2, + raster.bounds.height / 2 + ) raster.visible = true const previousRasters = getPicRasters(paperGroup).filter( (item) => item !== raster @@ -3034,10 +3080,11 @@ const PaperContainer = ( }) } - usePaperStore.getState().group!.position = new paper.Point( - raster.bounds.width / 2, - raster.bounds.height / 2 - ) + paperGroup.position = getTargetGroupPosition({ + defaultPosition: centeredGroupPosition, + preservedPosition: preservedGroupPosition, + canPreserve: preserveGroupPosition, + }) setRasterPath(new paper.Path.Rectangle(raster.bounds)) raster.sendToBack() renderedImageRef.current = requestImage @@ -3059,9 +3106,11 @@ const PaperContainer = ( cancelRasterFadeTransition, decodeFrameImage, getPicRasters, + getTargetGroupPosition, imgSize?.clientHeight, imgSize?.clientWidth, projectDetail?.id, + saveCurrentScale, setRasterPath, setRasterScale, setRasterSize, @@ -3171,6 +3220,9 @@ const PaperContainer = ( : undefined) || (rasterItems.length ? rasterItems[rasterItems.length - 1] : undefined) if (!currentRaster) return false + const preservedGroupPosition = saveCurrentScale + ? clonePaperPoint(group.position) + : null const rasterSizeStore = usePaperStore.getState().rasterSize const rawRasterSize = rasterSizeStore[activeImage] ?? [ @@ -3224,10 +3276,15 @@ const PaperContainer = ( currentRaster.bounds.width / 2, currentRaster.bounds.height / 2 ) - group.position = new paper.Point( + const centeredGroupPosition = new paper.Point( currentRaster.bounds.width / 2, currentRaster.bounds.height / 2 ) + group.position = getTargetGroupPosition({ + defaultPosition: centeredGroupPosition, + preservedPosition: preservedGroupPosition, + canPreserve: saveCurrentScale, + }) setRasterPath(new paper.Path.Rectangle(currentRaster.bounds)) setRasterScale(activeImage, nextScale) @@ -3241,9 +3298,11 @@ const PaperContainer = ( }, [ activeImage, getPicRasters, + getTargetGroupPosition, imgSize?.clientHeight, imgSize?.clientWidth, renderTag, + saveCurrentScale, scaleRenderedAnnotationItemsInPlace, setRasterPath, setRasterScale, From 033f11fb9bd8bc4d396a3bba871099b01a46d7d2 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 13:51:52 +0800 Subject: [PATCH 9/9] perf(sam2): code --- components/label/LabelNossr.tsx | 1 + components/label/api/label/index.ts | 26 +- ...PaperContainer.renderTrackingAnnotation.md | 96 ---- .../label/components/PaperContainer.tsx | 516 ++++++------------ 4 files changed, 176 insertions(+), 463 deletions(-) delete mode 100644 components/label/components/PaperContainer.renderTrackingAnnotation.md diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index e3330a8..1875527 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -2267,6 +2267,7 @@ const LabelPage = ({ // 追踪 if (e.key === "i") { e.preventDefault() + // sam2 暂未有图片接口 paperContainerRef.current.renderTrackingAnnotation() } diff --git a/components/label/api/label/index.ts b/components/label/api/label/index.ts index c32f8e8..47f4f58 100644 --- a/components/label/api/label/index.ts +++ b/components/label/api/label/index.ts @@ -108,18 +108,6 @@ export const getLabelImage = (activeImage: string, path: string) => { }) } -// 辅助标注 -export const getAuxiliaryAnnotation = (data: any) => { - return httpFetch({ - url: BASE_LABEL_API + "/api/model_server/sam2/sa", - method: "POST", - body: JSON.stringify(data), - headerAttr: { - responseType: "arraybuffer", - }, - }) -} - const BASE_SAM_API = process.env.NEXT_PUBLIC_ENV === "production" ? "http://172.16.103.224:8000" @@ -127,7 +115,7 @@ const BASE_SAM_API = ? "http://172.30.21.211:8000" : "http://172.30.21.211:8000" -export const getAuxiliaryAnnotation2 = (data: { +export const getAuxiliaryAnnotation = (data: { project_id: number image_name: string prompt: { @@ -146,18 +134,6 @@ export const getAuxiliaryAnnotation2 = (data: { }) } -// 模型标注 追踪 -export const getTrackingAuxiliaryAnnotation = (data: any) => { - return httpFetch({ - url: BASE_LABEL_API + "/api/model_server/sam2/vos", - method: "POST", - body: JSON.stringify(data), - headerAttr: { - responseType: "arraybuffer", - }, - }) -} - // 用户登录 export const userLogin = (data: { name: string; password: string }) => { return httpFetch({ diff --git a/components/label/components/PaperContainer.renderTrackingAnnotation.md b/components/label/components/PaperContainer.renderTrackingAnnotation.md deleted file mode 100644 index ef53d7b..0000000 --- a/components/label/components/PaperContainer.renderTrackingAnnotation.md +++ /dev/null @@ -1,96 +0,0 @@ -# `renderTrackingAnnotation` 流程说明 - -来源文件: - -- `labelimage/components/label/components/PaperContainer.tsx` -- `labelimage/components/label/LabelNossr.tsx` -- `labelimage/components/label/api/label/index.ts` - -## 概述 - -`renderTrackingAnnotation` 是单对象跨帧追踪的入口方法。 - -它会把当前激活图片上选中的标注对象作为 seed,将其轮廓数据发送给追踪模型接口,再把模型返回的后续帧轮廓结果写回 `label store`。 - -## Mermaid - -```mermaid -flowchart TD - A["在 `LabelNossr.tsx` 中按下 `i`"] --> B["调用 `paperContainerRef.current.renderTrackingAnnotation()`"] - B --> C["执行 `updateLoadingFlag(true)`
并显示加载通知"] - C --> D["从 store 读取运行时状态:
`selectedPath[activeImage]`
`selectedItems`
`rasterSize[activeImage]`
`rasterScale[activeImage]`"] - - D --> E{"是否恰好选中了一个标注对象?"} - E -->|未选中| F["提示错误:
`请先选中标注对象后再进行模型调用`"] - E -->|选中了多个| G["提示错误:
`请勿选择多个标注对象进行模型调用`"] - E -->|是| H["在 `label.get(activeImage)` 中查找
满足 `item[0] === id` 的标注对象"] - - H --> I{"是否找到 `categoryId` 和 `data`?"} - I -->|否| Z["跳过请求参数构造"] - I -->|是| J["提取源标注几何数据:
`data[1]` => 外轮廓 `segmentation`
`data[3]` => 镂空轮廓 `hollow_segmentation`"] - - J --> K["将画布坐标转换为原图坐标:
`Math.round(x / currentScale)`
`Math.round(y / currentScale)`"] - K --> L["构造请求参数:
`project_id`
`file_names = [activeImage, ...selectedItems]`
`image_hw = [height, width]`
`contours`
`hollows`"] - L --> M["使用 `msgpack.encode(params)` 序列化"] - M --> N["通过 `getTrackingAuxiliaryAnnotation()`
POST 到 `/api/model_server/sam2/vos`"] - N --> O["以 `arraybuffer` 形式接收响应体"] - O --> P["使用
`msgpack.decode(new Uint8Array(res))`
解码响应数据"] - - P --> Q{"resData.msg === 'success'?"} - Q -->|否| R["内层流程仅执行 `console.log(error/resData)`"] - Q -->|是| S["读取响应中的 `future_contours`"] - - S --> T["克隆当前 `label store`:
`safeClone(useLabelStore.getState().label)`"] - T --> U["遍历每个未来帧结果:
目标文件 = `file_names[index + 1]`"] - U --> V["读取目标帧缩放比例:
`rasterScale[fileName]`"] - V --> W["基于 `pathIds` 生成 `newId` 并注册"] - W --> X["将原图坐标还原为画布坐标:
`x * imgScale`
`y * imgScale`"] - X --> Y["将返回轮廓拆分为:
`segmentation`
`hollow_segmentation`"] - Y --> AA["构造标注元组:
`[newId, segmentation, detail, hollow_segmentation]`"] - AA --> AB["将结果合并到
`Map>`"] - AB --> AC["持久化结果:
`setLabel(nowTaskData)`
`pushStateStack(nowTaskData)`"] - AC --> AD["显示成功通知"] - - F --> AE["finally:执行 `updateLoadingFlag(false)`"] - G --> AE - Z --> AE - R --> AE - AD --> AE - C --> AF["外层 `try/catch`"] - AF -->|异常| AG["显示通用失败通知:
`模型生成失败`"] - AG --> AE -``` - -## 数据结构 - -被追踪的源标注对象,使用和普通多边形标注一致的元组结构: - -```ts -;[id, segmentation, detail, hollow_segmentation] -``` - -各字段含义: - -- `item[0]`:标注 id -- `item[1]`:外轮廓多边形 -- `item[2]`:详情元数据 -- `item[3]`:镂空轮廓多边形 - -## 为什么使用 `arraybuffer` - -这条追踪请求并不是从头到尾都按普通 JSON 方式传输。 - -- 请求参数先通过 `msgpack.encode(params)` 打包。 -- API 封装层把这份打包后的数据发给后端。 -- 响应体以 `arraybuffer` 的形式返回。 -- `renderTrackingAnnotation` 再通过 `msgpack.decode(new Uint8Array(res))` 还原出 JavaScript 对象。 - -因此这里必须配置 `responseType: "arraybuffer"`,因为前端预期拿到的是二进制响应体,再自行解码。 - -## 备注 - -- 当前帧是 seed 帧,模型返回的结果会写入 `file_names[index + 1]` 对应的后续帧。 -- 这个方法更新的是 `label store`,不会直接在当前页面把其他帧的结果立刻画出来。 -- 这个方法只支持一次追踪一个被选中的对象。 -- 内层请求失败时目前主要是 `console.log`,不一定总会显示专门的失败提示。 -- 这个方法里生成的 `detail.image_id` 使用的是 `activeImage`,建议再结合整体数据模型确认一下是否符合预期。 diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index fd5c1fc..e8c197a 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -18,7 +18,7 @@ import { import { useForm } from "@mantine/form" import { notifications } from "@mantine/notifications" import dayjs from "dayjs" -import msgpack from "msgpack-lite" +// import msgpack from "msgpack-lite" import paper from "paper" import React, { DispatchWithoutAction, @@ -31,12 +31,7 @@ import React, { useRef, useState, } from "react" -import { - getAuxiliaryAnnotation, - getAuxiliaryAnnotation2, - getServerImage, - getTrackingAuxiliaryAnnotation, -} from "../api/label" +import { getAuxiliaryAnnotation, getServerImage } from "../api/label" import { Project } from "../api/project/typing" import { labelimagePerformanceConfig } from "../config/performance" import { LabelState } from "../LabelNossr" @@ -185,6 +180,7 @@ const PaperContainer = ( ref: React.Ref | undefined ) => { const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props + false && updateLoadingFlag const containerRef = useRef(null) const contextMenuRef = useRef(null) const [imgSize, setImageSize] = useState<{ @@ -884,164 +880,6 @@ const PaperContainer = ( }, [clearPendingFrameSwitch, loadingData]) const renderSupportAnnotation = useCallback( - async ( - points: Array<[number, number]>, - tags: number[], - clear_previous_state = false - ) => { - try { - notifications.show({ - id: "sam2", - message: "模型生成中", - loading: true, - autoClose: false, - }) - const operationSchema = - projectDetail?.label_schema_list?.find( - (item) => - item.category_id === +useTopToolsStore.getState().activeOperation - ) || null - const strokeColor = operationSchema - ? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})` - : "rgb(0,0,0)" - const fillColor = operationSchema - ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)` - : "rgba(0,0,0,0.3)" - const blankColor = operationSchema - ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)` - : "rgba(0,0,0,0.01)" - const currentScale = - usePaperStore.getState().rasterScale[activeImage] ?? 1 - - const draw = () => { - points.forEach((point, index) => { - const drawPoint = new paper.Path.Circle({ - center: point, - radius: 2, - fillColor: tags[index] === 1 ? "red" : "blue", - strokeColor: tags[index] === 1 ? "red" : "blue", - data: { - id: "support", - type: "point", - }, - }) - drawPoint.parent = usePaperStore.getState().group! - const raster = usePaperStore - .getState() - .group!.getItems({ data: { name: "pic" } })[0] - if (!raster.contains(drawPoint.position)) { - throw new Error("点位超出图片范围") - } - }) - } - console.log(points, tags) - // 绘制点位 - clearSupportAnnotationItem() - draw() - - const picSize = usePaperStore.getState().rasterSize[activeImage] - - const params = { - project_id: projectDetail?.id || 0, - file_name: activeImage, - image_hw: - picSize && picSize.length ? [picSize[1], picSize[0]] : [0, 0], - points_and_box: points.map(([x, y]) => [ - Math.round(x / currentScale), - Math.round(y / currentScale), - ]), - tags, - clear_previous_state, - } - try { - const res = await getAuxiliaryAnnotation( - msgpack.encode(params) as any - ) - const data = msgpack.decode(new Uint8Array(res)) - console.log(data) - if (data.msg === "success") { - const { contours, hollows } = data - const outerPaths: any[] = [] - const innerPaths: any[] = [] - contours.forEach((contour: any, index: number) => { - if (contour.length > 3) { - const path = new paper.Path({ - segments: contour.map(([x, y]: [number, number]) => [ - x * currentScale, - y * currentScale, - ]), - fillColor: blankColor, - strokeColor: strokeColor, - closed: true, - data: { - id: "support", - type: "polygon", - isHollowPolygon: false, - fillColor, - strokeColor, - blankColor, - }, - parent: usePaperStore.getState().group, - strokeScaling: false, - }) - if ( - Math.abs(path.area) < useTopToolsStore.getState().checkSize - ) { - path.remove() - } else { - // path.scaling = usePaperStore.getState().group!.scaling; - if (hollows[index]) { - // 更新标志位 - path.data.isHollowPolygon = true - innerPaths.push(path) - } else { - // 上色 - path.fillColor = new paper.Color(fillColor) - outerPaths.push(path) - } - } - } - }) - const compoundPath = new paper.CompoundPath({ - children: [...outerPaths, ...innerPaths], - data: { - id: "support", - type: "parent", - }, - strokeColor: strokeColor, - strokeWidth: 2, - fillColor: fillColor, - }) - compoundPath.parent = usePaperStore.getState().group! - notifications.show({ - id: "sam2", - message: "模型生成成功", - color: "green", - }) - } - } catch (error) { - console.log(error) - } - } catch (error: any) { - console.log(error) - notifications.show({ - id: "sam2", - message: error.message ?? "模型生成失败", - color: "red", - }) - if (error.message === "点位超出图片范围") { - usePaperStore - .getState() - .supportTool?.emit("keydown", { key: "escape" }) - } - } - }, - [activeImage, projectDetail] - ) - - false && renderSupportAnnotation - - const renderSupportAnnotation2 = useCallback( async ( points: Array<[number, number]>, tags: number[], @@ -1145,7 +983,7 @@ const PaperContainer = ( // const picSize = usePaperStore.getState().rasterSize[activeImage] try { - const data = await getAuxiliaryAnnotation2({ + const data = await getAuxiliaryAnnotation({ project_id: projectId, image_name: activeImage, // project_id: "image_test_bk", @@ -1233,180 +1071,174 @@ const PaperContainer = ( [activeImage, projectDetail] ) - const renderTrackingAnnotation = useCallback(async () => { - updateLoadingFlag(true) - try { - notifications.show({ - id: "sam2", - message: "模型生成中", - loading: true, - autoClose: false, - }) - const ids = - useObjectStore.getState().selectedPath[ - useBottomToolsStore.getState().activeImage - ] - const picArr = useBottomToolsStore.getState().selectedItems - const [width, height] = usePaperStore.getState().rasterSize[ - activeImage - ] ?? [0, 0] - const currentScale = - usePaperStore.getState().rasterScale[activeImage] ?? 1 - if (ids.length === 1) { - const id = ids[0] - let categoryId: any = null - let data: any = null - const activeImageData = useLabelStore.getState().label.get(activeImage)! - for (let [category, objArr] of activeImageData.entries()) { - objArr.forEach((item) => { - if (item[0] === id) { - categoryId = category - data = item - } - }) - } - if (categoryId && data) { - const contours: [number, number][] = [] - const tags: boolean[] = [] - data[1].forEach((item: any) => { - contours.push( - item.map(([x, y]: any) => [ - Math.round(x / currentScale), - Math.round(y / currentScale), - ]) - ) - tags.push(false) - }) - data[3].forEach((item: any) => { - contours.push( - item.map(([x, y]: any) => [ - Math.round(x / currentScale), - Math.round(y / currentScale), - ]) - ) - tags.push(true) - }) - const file_names = [...new Set([activeImage, ...picArr])] - const params = { - project_id: projectDetail?.id || 0, - file_names, - image_hw: [height, width], - contours, - hollows: tags, - } - console.log("传参", params) - try { - const res = await getTrackingAuxiliaryAnnotation( - msgpack.encode(params) - ) - const resData = msgpack.decode(new Uint8Array(res)) - if (resData.msg === "success") { - const { - future_contours, - }: { - future_contours: { - contours: [number, number][][] - hollows: boolean[] - }[] - } = resData - const nowTaskData = safeClone(useLabelStore.getState().label) - future_contours.forEach(({ contours, hollows }, index) => { - const fileName = file_names[index + 1] - const imgScale = - usePaperStore.getState().rasterScale[fileName] ?? 1 - let newId = - useRightToolsStore - .getState() - .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1 - useRightToolsStore.getState().pushPathId(newId) - // 处理点位数据 - let segmentation: Array<[number, number][]> = [] - let hollow_segmentation: Array<[number, number][]> = [] - contours.forEach((contour, index) => { - if (!hollows[index]) - segmentation.push( - contour.map(([x, y]: any) => [x * imgScale, y * imgScale]) - ) - else - hollow_segmentation.push( - contour.map(([x, y]: any) => [x * imgScale, y * imgScale]) - ) - }) - // 详情数据 - let detail = { - ...initialDetail, - uid: usePermissionStore.getState().user_id, - comment: [...useLabelStore.getState().labelDefaultComments], - create_timestamp: dayjs().unix(), - sub_attributes: {}, - image_id: activeImage, - category_id: categoryId, - } - const updateData: any = [ - newId, - segmentation, - detail, - hollow_segmentation, - ] - if (nowTaskData.has(fileName)) { - const categoryMap = nowTaskData.get(fileName)! - if (categoryMap.has(categoryId)) { - const existArr = categoryMap.get(categoryId) - existArr?.push(updateData) - } else { - categoryMap.set(categoryId, [updateData]) - } - } else { - let m = new Map() - m.set(categoryId, [updateData]) - nowTaskData.set(fileName, m) - } - }) - console.log(nowTaskData) - useLabelStore.getState().setLabel(nowTaskData) - useLabelStore.getState().pushStateStack(nowTaskData) - notifications.show({ - id: "sam2", - message: "模型生成成功", - color: "green", - }) - } - } catch (error) { - console.log(error) - } - } - } else if (!ids.length) { - notifications.show({ - id: "sam2", - message: "请先选中标注对象后再进行模型调用", - color: "red", - }) - } else { - notifications.show({ - id: "sam2", - message: "请勿选择多个标注对象进行模型调用", - color: "red", - }) - } - } catch (error) { - console.log(error) - notifications.show({ - id: "sam2", - message: "模型生成失败", - color: "red", - }) - } finally { - updateLoadingFlag(false) - } - }, [activeImage, projectDetail?.id, updateLoadingFlag]) + // const renderTrackingAnnotation = useCallback(async () => { + // updateLoadingFlag(true) + // try { + // notifications.show({ + // id: "sam2", + // message: "模型生成中", + // loading: true, + // autoClose: false, + // }) + // const ids = + // useObjectStore.getState().selectedPath[ + // useBottomToolsStore.getState().activeImage + // ] + // const picArr = useBottomToolsStore.getState().selectedItems + // const [width, height] = usePaperStore.getState().rasterSize[ + // activeImage + // ] ?? [0, 0] + // const currentScale = + // usePaperStore.getState().rasterScale[activeImage] ?? 1 + // if (ids.length === 1) { + // const id = ids[0] + // let categoryId: any = null + // let data: any = null + // const activeImageData = useLabelStore.getState().label.get(activeImage)! + // for (let [category, objArr] of activeImageData.entries()) { + // objArr.forEach((item) => { + // if (item[0] === id) { + // categoryId = category + // data = item + // } + // }) + // } + // if (categoryId && data) { + // const contours: [number, number][] = [] + // const tags: boolean[] = [] + // data[1].forEach((item: any) => { + // contours.push( + // item.map(([x, y]: any) => [ + // Math.round(x / currentScale), + // Math.round(y / currentScale), + // ]) + // ) + // tags.push(false) + // }) + // data[3].forEach((item: any) => { + // contours.push( + // item.map(([x, y]: any) => [ + // Math.round(x / currentScale), + // Math.round(y / currentScale), + // ]) + // ) + // tags.push(true) + // }) + // const file_names = [...new Set([activeImage, ...picArr])] + // const params = { + // project_id: projectDetail?.id || 0, + // file_names, + // image_hw: [height, width], + // contours, + // hollows: tags, + // } + // console.log("传参", params) + // try { + // const res = await getTrackingAuxiliaryAnnotation( + // msgpack.encode(params) + // ) + // const resData = msgpack.decode(new Uint8Array(res)) + // if (resData.msg === "success") { + // const { + // future_contours, + // }: { + // future_contours: { + // contours: [number, number][][] + // hollows: boolean[] + // }[] + // } = resData + // const nowTaskData = safeClone(useLabelStore.getState().label) + // future_contours.forEach(({ contours, hollows }, index) => { + // const fileName = file_names[index + 1] + // const imgScale = + // usePaperStore.getState().rasterScale[fileName] ?? 1 + // let newId = + // useRightToolsStore + // .getState() + // .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1 + // useRightToolsStore.getState().pushPathId(newId) + // // 处理点位数据 + // let segmentation: Array<[number, number][]> = [] + // let hollow_segmentation: Array<[number, number][]> = [] + // contours.forEach((contour, index) => { + // if (!hollows[index]) + // segmentation.push( + // contour.map(([x, y]: any) => [x * imgScale, y * imgScale]) + // ) + // else + // hollow_segmentation.push( + // contour.map(([x, y]: any) => [x * imgScale, y * imgScale]) + // ) + // }) + // // 详情数据 + // let detail = { + // ...initialDetail, + // uid: usePermissionStore.getState().user_id, + // comment: [...useLabelStore.getState().labelDefaultComments], + // create_timestamp: dayjs().unix(), + // sub_attributes: {}, + // image_id: activeImage, + // category_id: categoryId, + // } + // const updateData: any = [ + // newId, + // segmentation, + // detail, + // hollow_segmentation, + // ] + // if (nowTaskData.has(fileName)) { + // const categoryMap = nowTaskData.get(fileName)! + // if (categoryMap.has(categoryId)) { + // const existArr = categoryMap.get(categoryId) + // existArr?.push(updateData) + // } else { + // categoryMap.set(categoryId, [updateData]) + // } + // } else { + // let m = new Map() + // m.set(categoryId, [updateData]) + // nowTaskData.set(fileName, m) + // } + // }) + // console.log(nowTaskData) + // useLabelStore.getState().setLabel(nowTaskData) + // useLabelStore.getState().pushStateStack(nowTaskData) + // notifications.show({ + // id: "sam2", + // message: "模型生成成功", + // color: "green", + // }) + // } + // } catch (error) { + // console.log(error) + // } + // } + // } else if (!ids.length) { + // notifications.show({ + // id: "sam2", + // message: "请先选中标注对象后再进行模型调用", + // color: "red", + // }) + // } else { + // notifications.show({ + // id: "sam2", + // message: "请勿选择多个标注对象进行模型调用", + // color: "red", + // }) + // } + // } catch (error) { + // console.log(error) + // notifications.show({ + // id: "sam2", + // message: "模型生成失败", + // color: "red", + // }) + // } finally { + // updateLoadingFlag(false) + // } + // }, [activeImage, projectDetail?.id, updateLoadingFlag]) - // const getBase64 = (file: any): Promise => - // new Promise((resolve, reject) => { - // const reader = new FileReader(); - // reader.readAsDataURL(file); - // reader.onload = () => resolve(reader.result as string); - // reader.onerror = (error) => reject(error); - // }); + const renderTrackingAnnotation = useCallback(async () => {}, []) useEffect(() => { if (setup) { @@ -1439,7 +1271,7 @@ const PaperContainer = ( let pointTool = new paper.Tool() initPointTool(pointTool, renderCircle, renderPath, forceUpdate) let supportTool = new paper.Tool() - initSupportTool(supportTool, renderSupportAnnotation2) + initSupportTool(supportTool, renderSupportAnnotation) } return () => { newGroup.remove() @@ -1455,7 +1287,7 @@ const PaperContainer = ( initRectangleTool, initSupportTool, initViewTool, - renderSupportAnnotation2, + renderSupportAnnotation, setup, ]) @@ -4148,7 +3980,7 @@ const PaperContainer = ( renderPolygons, handleCrosshairMove, renderTrackingAnnotation, - renderSupportAnnotation2, + renderSupportAnnotation, saveSupportAnnotationData, copyAnnotationDataToMultiFrame, }))