From cc9bea54e5ed3448fec1c6637be3088506d8b481 Mon Sep 17 00:00:00 2001 From: shay7sev Date: Sat, 28 Feb 2026 16:54:50 +0800 Subject: [PATCH] fix(structuredclone): change --- components/label/LabelNossr.tsx | 9 ++-- components/label/components/BottomTools.tsx | 21 +++----- .../label/components/PaperContainer.tsx | 7 ++- .../label/components/RightGroupEditModal.tsx | 3 +- .../label/components/RightGroupTools.tsx | 5 +- components/label/components/RightQATools.tsx | 23 ++++----- .../label/components/TaskCacheModal.tsx | 3 +- components/label/components/TopTools.tsx | 7 +-- components/label/store.ts | 51 +------------------ components/label/usePaperStore.ts | 23 +++++---- components/label/useRightToolsStore.ts | 3 +- components/label/utils/clone.ts | 49 ++++++++++++++++++ 12 files changed, 101 insertions(+), 103 deletions(-) create mode 100644 components/label/utils/clone.ts diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index abba0e9..abb2a9e 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -51,6 +51,7 @@ import { useRightToolsStore } from "./useRightToolsStore" import { useIntervalStore, useTimerStore } from "./useTimerStore" import { useTopToolsStore } from "./useTopToolsStore" import { findGroupKey } from "./util" +import { safeClone } from "./utils/clone" import { labelTypeMap } from "./utils/constants" // Splitter component - will need custom implementation or alternative @@ -662,19 +663,19 @@ const LabelPage = ({ if (JSON.stringify(current_data) === "{}") { current_data = qaData.get("init") } - const qaMap = structuredClone(qaData) + const qaMap = safeClone(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( + const currentMetaMap = safeClone( useDescToolsStore.getState().metaData ) currentMetaMap.set(key, meta_operation || []) useDescToolsStore.getState().setMetaData(currentMetaMap) - const currentDescMap = structuredClone( + const currentDescMap = safeClone( useDescToolsStore.getState().descData ) let descMap = new Map() @@ -1084,7 +1085,7 @@ const LabelPage = ({ // } // } // 更新当前数据 - useLabelStore.getState().setLabel(structuredClone(data)) + useLabelStore.getState().setLabel(safeClone(data)) } } diff --git a/components/label/components/BottomTools.tsx b/components/label/components/BottomTools.tsx index 81571c7..265b36a 100644 --- a/components/label/components/BottomTools.tsx +++ b/components/label/components/BottomTools.tsx @@ -31,6 +31,7 @@ import { useOtherToolsStore } from "../useOtherToolsStore" import { usePaperStore } from "../usePaperStore" import { useTopToolsStore } from "../useTopToolsStore" import { getSubAttrStatus } from "../util" +import { safeClone } from "../utils/clone" import CustomModal from "./CustomModal" import MetaOperationTool from "./MetaOperationTool" @@ -175,20 +176,16 @@ const BottomTools: React.FC = (props) => { const prevFramehasTextDesc = () => { if (projectDetail?.label_type === 5) { let flag = false - const currentDescData = structuredClone( - useDescToolsStore.getState().descData - ) + const currentDescData = safeClone(useDescToolsStore.getState().descData) const prevData1 = currentDescData.get(previousFrameKey) ?? new Map() flag = prevData1.size ? true : false if (flag) return true - const currentMetaData = structuredClone( - useDescToolsStore.getState().metaData - ) + const currentMetaData = safeClone(useDescToolsStore.getState().metaData) const prevData2 = currentMetaData.get(previousFrameKey) ?? [] flag = prevData2.length ? true : false return flag } else if (projectDetail?.label_type === 6) { - const currentQaData = structuredClone(useDescToolsStore.getState().qaData) + const currentQaData = safeClone(useDescToolsStore.getState().qaData) const prevData = currentQaData.get(previousFrameKey) ?? {} return JSON.stringify(prevData) === "{}" ? false : true } @@ -217,24 +214,20 @@ const BottomTools: React.FC = (props) => { // LLM if (projectDetail?.label_type === 5) { // 文本描述 - const currentDescData = structuredClone( - useDescToolsStore.getState().descData - ) + const currentDescData = safeClone(useDescToolsStore.getState().descData) const prevData1 = currentDescData.get(previousFrameKey) ?? new Map() currentDescData.set(activeImage, prevData1) useDescToolsStore.getState().setDescData(currentDescData) useDescToolsStore.getState().updateFlag(true) // 元操作序列 - const currentMetaData = structuredClone( - useDescToolsStore.getState().metaData - ) + const currentMetaData = safeClone(useDescToolsStore.getState().metaData) const prevData2 = currentMetaData.get(previousFrameKey) ?? [] currentMetaData.set(activeImage, prevData2) useDescToolsStore.getState().setMetaData(currentMetaData) } // QA if (projectDetail?.label_type === 6) { - const currentQaData = structuredClone(useDescToolsStore.getState().qaData) + const currentQaData = safeClone(useDescToolsStore.getState().qaData) const prevData = currentQaData.get(previousFrameKey) ?? {} currentQaData.set(activeImage, prevData) useDescToolsStore.getState().setQaData(currentQaData) diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index a04d296..72e0a0c 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -55,6 +55,7 @@ import { useRightToolsStore } from "../useRightToolsStore" import { useTopToolsStore } from "../useTopToolsStore" import { checkCommentsIsSame } from "../util" import { labelTypeMap } from "../utils/constants" +import { safeClone } from "../utils/clone" import { adjustPoints } from "../utils/paperjs" import CrosshairComponent from "./CrosshairComponent" import { renderOperationIcon } from "./RightObjectTools" @@ -456,9 +457,7 @@ const PaperContainer = ( hollows: boolean[] }[] } = resData - const nowTaskData = structuredClone( - useLabelStore.getState().label - ) + const nowTaskData = safeClone(useLabelStore.getState().label) future_contours.forEach(({ contours, hollows }, index) => { const fileName = file_names[index + 1] const imgScale = @@ -1013,7 +1012,7 @@ const PaperContainer = ( .selectedItems.filter((k) => k !== activeImage) const copyArr = useKeyboardStore.getState().copyDataIds if (picArr.length && copyArr.length) { - const data = structuredClone(useLabelStore.getState().label) + const data = safeClone(useLabelStore.getState().label) // 先判断是否有重复的id let existedIds: number[] = [] picArr.forEach((imageId) => { diff --git a/components/label/components/RightGroupEditModal.tsx b/components/label/components/RightGroupEditModal.tsx index c6218c6..b308164 100644 --- a/components/label/components/RightGroupEditModal.tsx +++ b/components/label/components/RightGroupEditModal.tsx @@ -7,6 +7,7 @@ import { useLabelStore } from "../store" import { useBottomToolsStore } from "../useBottomToolsStore" import { usePaperStore } from "../usePaperStore" import { useRightToolsStore } from "../useRightToolsStore" +import { safeClone } from "../utils/clone" interface ComponentProps { open: boolean @@ -59,7 +60,7 @@ const RightGroupEditModal = ({ if (groupMap?.has(id)) return const oldIds = groupMap?.get(editId) ?? [] - const labelData = structuredClone(useLabelStore.getState().label) + const labelData = safeClone(useLabelStore.getState().label) // update path let operationIds: any[] = [] oldIds.forEach((childId) => { diff --git a/components/label/components/RightGroupTools.tsx b/components/label/components/RightGroupTools.tsx index 58be830..3265bad 100644 --- a/components/label/components/RightGroupTools.tsx +++ b/components/label/components/RightGroupTools.tsx @@ -26,6 +26,7 @@ import { renderGroupPath } from "../useRenderGroupPath" import { useRightToolsStore } from "../useRightToolsStore" import { useTopToolsStore } from "../useTopToolsStore" import { labelTypeMap } from "../utils/constants" +import { safeClone } from "../utils/clone" import RightGroupEditModal from "./RightGroupEditModal" interface RightGroupToolsComponentProps { @@ -212,7 +213,7 @@ const RightGroupTools: React.FC = (props) => { needUpdateOperationIds[index] = childPath.data.operationId }) // handle labeldata update - const currentData = structuredClone(useLabelStore.getState().label) + const currentData = safeClone(useLabelStore.getState().label) needUpdateOperationIds.forEach((operationId, index) => { if (currentData.get(imageId)?.get(operationId)?.length) { let result = @@ -260,7 +261,7 @@ const RightGroupTools: React.FC = (props) => { needRemoveOperationIds[index] = p.data.operationId }) }) - const currentData = structuredClone(useLabelStore.getState().label) + const currentData = safeClone(useLabelStore.getState().label) needRemoveOperationIds.forEach((operationId, index) => { if (currentData.get(imageId)?.get(operationId)?.length) { let result = diff --git a/components/label/components/RightQATools.tsx b/components/label/components/RightQATools.tsx index 3ccd2f0..68f1de5 100644 --- a/components/label/components/RightQATools.tsx +++ b/components/label/components/RightQATools.tsx @@ -34,6 +34,7 @@ import { usePermissionStore } from "../store/auth" import { useBottomToolsStore } from "../useBottomToolsStore" import { useDescToolsStore } from "../useDescToolsStore" import { useTopToolsStore } from "../useTopToolsStore" +import { safeClone } from "../utils/clone" import CustomModal from "./CustomModal" import EditorContainer, { splitWord } from "./EditorContainer" @@ -147,7 +148,7 @@ const RightQAContent = forwardRef( } }) try { - const finalData = structuredClone(currentQaData) + const finalData = safeClone(currentQaData) finalData[category] = newArr qaData.set(activeImage, finalData) setQaData(new Map(qaData)) // Force update @@ -192,7 +193,7 @@ const RightQAContent = forwardRef( // 处理问题上移 const handleQuestionUp = (category: string, index: number) => { - const finalData = structuredClone(currentQaData) + const finalData = safeClone(currentQaData) let arr = finalData[category] let temp = arr[index] arr[index] = arr[index - 1] @@ -205,7 +206,7 @@ const RightQAContent = forwardRef( // 处理问题置顶 const handleQuestionUpToTop = (category: string, index: number) => { - const finalData = structuredClone(currentQaData) + const finalData = safeClone(currentQaData) let arr = finalData[category] const element = arr.splice(index, 1)[0] arr.unshift(element) @@ -226,7 +227,7 @@ const RightQAContent = forwardRef( const currentImg = useBottomToolsStore.getState().activeImage const qaData = useDescToolsStore.getState().qaData const currentQaData = qaData.get(currentImg) ?? qaData.get("init") ?? {} - const newQaData = structuredClone(currentQaData) + const newQaData = safeClone(currentQaData) const handleText = (v: string) => { // 提取文本内容 @@ -370,9 +371,8 @@ const RightQAContent = forwardRef( size="compact-xs" onClick={() => { const finalData = - structuredClone(currentQaData) - const updateData = - structuredClone(qaDataArr) + safeClone(currentQaData) + const updateData = safeClone(qaDataArr) updateData.splice(index, 1) finalData[qaCategory] = updateData qaData.set(activeImage, finalData) @@ -396,9 +396,8 @@ const RightQAContent = forwardRef( size="compact-xs" onClick={() => { const finalData = - structuredClone(currentQaData) - const updateData = - structuredClone(qaDataArr) + safeClone(currentQaData) + const updateData = safeClone(qaDataArr) updateData[index] = { [question_name]: { ...detail, @@ -599,8 +598,8 @@ const RightQAContent = forwardRef( } } - const finalData = structuredClone(currentQaData) - const updateData = structuredClone(currentOperation) + const finalData = safeClone(currentQaData) + const updateData = safeClone(currentOperation) let obj = { value: formData.value ?? "

", id: formData.round_id, diff --git a/components/label/components/TaskCacheModal.tsx b/components/label/components/TaskCacheModal.tsx index 9aa1a1f..453d07e 100644 --- a/components/label/components/TaskCacheModal.tsx +++ b/components/label/components/TaskCacheModal.tsx @@ -6,6 +6,7 @@ import { Skeleton, Stack, Flex, Box, Text } from "@mantine/core" import { FolderPen, ImageDown, Trash2 } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { storage, useImagesStore } from "../store" +import { safeClone } from "../utils/clone" interface ComponentProps { open: boolean @@ -65,7 +66,7 @@ const TaskCacheModal = ({ open, handleClose, taskDetail }: ComponentProps) => { // 删除图片缓存 if (data1 === "0.00 B") return const imgNames = taskDetail?.data_name.map((item) => item.name[0]) ?? [] - const imageData = structuredClone(useImagesStore.getState().images) + const imageData = safeClone(useImagesStore.getState().images) imgNames.forEach((name) => { imageData.delete(name) }) diff --git a/components/label/components/TopTools.tsx b/components/label/components/TopTools.tsx index 90c7af3..f60774c 100644 --- a/components/label/components/TopTools.tsx +++ b/components/label/components/TopTools.tsx @@ -93,6 +93,7 @@ import { useIntervalStore } from "../useTimerStore" import { useTopToolsStore } from "../useTopToolsStore" import { getSubAttrStatus } from "../util" import { labelTypeMap, TaskStatusEnum } from "../utils/constants" +import { safeClone } from "../utils/clone" import { adjustAllPoints } from "../utils/paperjs" import BackConfirmModal from "./BackConfirmModal" import BackupModal from "./BackupModal" @@ -562,7 +563,7 @@ const TopTools = ( } // 处理QA数据 if (projectDetail?.label_type === 6) { - const currentQaData = structuredClone( + const currentQaData = safeClone( useDescToolsStore.getState().qaData.get(name) || {} ) let other_desc: any = {} @@ -977,7 +978,7 @@ const TopTools = ( useDescToolsStore.getState().setMetaData(meta) } else if (projectDetail?.label_type === 6) { const { text } = data - const updateQaData = structuredClone( + const updateQaData = safeClone( useDescToolsStore.getState().qaData ) text.entries().forEach(([key, value]: any) => { @@ -1542,7 +1543,7 @@ const TopTools = ( }} c={showTagsConfigs.includes(config) ? "blue" : undefined} onClick={() => { - let arr = structuredClone(showTagsConfigs) + let arr = safeClone(showTagsConfigs) const index = arr.findIndex((item) => item === config) if (index !== -1) { arr.splice(index, 1) diff --git a/components/label/store.ts b/components/label/store.ts index c4aefe0..a7283bd 100644 --- a/components/label/store.ts +++ b/components/label/store.ts @@ -3,6 +3,7 @@ import superjson from "superjson" import { create } from "zustand" import { persist, PersistStorage, StorageValue } from "zustand/middleware" import { Comment } from "./api/label/typing" +import { safeClone } from "./utils/clone" type LabelData = Map> interface LabelState { @@ -145,56 +146,6 @@ const initialLabelState = { }, } -const deepCloneFallback = (value: T, cache = new WeakMap()): T => { - if (value === null || typeof value !== "object") return value - if (cache.has(value as object)) return cache.get(value as object) - - if (value instanceof Date) return new Date(value.getTime()) as T - if (value instanceof Map) { - const clonedMap = new Map() - cache.set(value as object, clonedMap) - value.forEach((mapValue, mapKey) => { - clonedMap.set( - deepCloneFallback(mapKey, cache), - deepCloneFallback(mapValue, cache) - ) - }) - return clonedMap as T - } - if (value instanceof Set) { - const clonedSet = new Set() - cache.set(value as object, clonedSet) - value.forEach((setValue) => { - clonedSet.add(deepCloneFallback(setValue, cache)) - }) - return clonedSet as T - } - if (Array.isArray(value)) { - const clonedArr: any[] = [] - cache.set(value as object, clonedArr) - value.forEach((item, index) => { - clonedArr[index] = deepCloneFallback(item, cache) - }) - return clonedArr as T - } - - const clonedObj: Record = {} - cache.set(value as object, clonedObj) - Object.keys(value as object).forEach((key) => { - clonedObj[key] = deepCloneFallback((value as any)[key], cache) - }) - return clonedObj as T -} - -const safeClone = (value: T): T => { - try { - return structuredClone(value) - } catch (error) { - console.warn("structuredClone failed, fallback to deep clone", error) - return deepCloneFallback(value) - } -} - export const initialDetail = { comment: [], create_timestamp: 0, diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 905a73b..b0d86d3 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -16,6 +16,7 @@ import { useOtherToolsStore } from "./useOtherToolsStore" import { usePaperSupportStore } from "./usePaperSupportStore" import { useRightToolsStore } from "./useRightToolsStore" import { useTopToolsStore } from "./useTopToolsStore" +import { safeClone } from "./utils/clone" interface PaperState { paperScope: paper.PaperScope | null @@ -239,7 +240,7 @@ const handleDelete = () => { } }) } - const currentLabelData = structuredClone(useLabelStore.getState().label) + const currentLabelData = safeClone(useLabelStore.getState().label) operationIds.forEach((operationId, index) => { let result = currentLabelData @@ -1808,7 +1809,7 @@ export const usePaperStore = create((set) => ({ !child.data || (child.data && !Object.keys(child.data).length) ) - child.data = structuredClone(newDrawPath?.data || {}) + child.data = safeClone(newDrawPath?.data || {}) }) oldPolygon.remove() oldPolygon = null @@ -1886,7 +1887,7 @@ export const usePaperStore = create((set) => ({ // if (!newDrawPath?.clockwise) newDrawPath?.reverse(); if (newDrawPath instanceof paper.CompoundPath) { ;(newDrawPath.children as paper.Path[]).forEach((child) => { - child.data = structuredClone( + child.data = safeClone( Object.assign({}, newDrawPath?.data || {}, { isHollowPolygon: !child.clockwise, }) @@ -1894,7 +1895,7 @@ export const usePaperStore = create((set) => ({ }) if (oldParent instanceof paper.CompoundPath) { ;(oldParent.children as paper.Path[]).forEach((child) => { - child.data = structuredClone( + child.data = safeClone( Object.assign({}, newDrawPath?.data || {}, { isHollowPolygon: !child.clockwise, }) @@ -1946,7 +1947,7 @@ export const usePaperStore = create((set) => ({ // if (!newDrawPath?.clockwise) newDrawPath?.reverse(); if (newDrawPath instanceof paper.CompoundPath) (newDrawPath.children as paper.Path[]).forEach((child) => { - child.data = structuredClone(newDrawPath?.data || {}) + child.data = safeClone(newDrawPath?.data || {}) }) oldPolygon.remove() oldPolygon = null @@ -2153,7 +2154,7 @@ export const usePaperStore = create((set) => ({ return } // hollowPath.path.clockwise = false; - hollowPath.path.data = structuredClone( + hollowPath.path.data = safeClone( Object.assign({}, hollowPath.path?.data || {}, { isHollowPolygon: true, }) @@ -2168,7 +2169,7 @@ export const usePaperStore = create((set) => ({ // if (finnalPath instanceof paper.CompoundPath) { // (finnalPath.children as paper.Path[]).forEach((child) => { // // child.clockwise = false; - // child.data = structuredClone( + // child.data = safeClone( // Object.assign({}, oldPolygon?.data || {}, { // isHollowPolygon: true, // }) @@ -2176,7 +2177,7 @@ export const usePaperStore = create((set) => ({ // }); // } else if (finnalPath instanceof paper.Path) { // // finnalPath.clockwise = false; - // finnalPath.data = structuredClone( + // finnalPath.data = safeClone( // Object.assign({}, oldPolygon?.data || {}, { // isHollowPolygon: true, // }) @@ -2656,7 +2657,7 @@ export const usePaperStore = create((set) => ({ if (polygon instanceof paper.CompoundPath) (polygon.children as paper.Path[]).forEach((child) => { if (!child.data || (child.data && !Object.keys(child.data).length)) - child.data = structuredClone( + child.data = safeClone( Object.assign( { isHollowPolygon: !child.clockwise }, polygon?.data || {} @@ -2674,7 +2675,7 @@ export const usePaperStore = create((set) => ({ polygon = oldPolygon?.subtract(eachPath) as any if (polygon instanceof paper.CompoundPath) (polygon.children as paper.Path[]).forEach((child) => { - child.data = structuredClone( + child.data = safeClone( Object.assign( { isHollowPolygon: !child.clockwise }, polygon?.data || {} @@ -2716,7 +2717,7 @@ export const usePaperStore = create((set) => ({ polygon = oldPolygon.unite(eachPath) as any if (polygon instanceof paper.CompoundPath) (polygon.children as paper.Path[]).forEach((child) => { - child.data = structuredClone(polygon?.data || {}) + child.data = safeClone(polygon?.data || {}) }) oldPolygon?.remove() oldPolygon = null diff --git a/components/label/useRightToolsStore.ts b/components/label/useRightToolsStore.ts index 2ea4b30..6c30ebe 100644 --- a/components/label/useRightToolsStore.ts +++ b/components/label/useRightToolsStore.ts @@ -2,6 +2,7 @@ import { create } from "zustand" import { persist } from "zustand/middleware" import { storage, useLabelStore } from "./store" import { usePaperStore } from "./usePaperStore" +import { safeClone } from "./utils/clone" interface RightToolsState { pathIds: number[] @@ -94,7 +95,7 @@ export const useRightToolsStore = create( } state.pathGroupMap.get(imageId)?.set(groupId, pathIds) // update LabelData - const data = structuredClone(useLabelStore.getState().label) + const data = safeClone(useLabelStore.getState().label) // handle delete if (deleteIds.length) { let operationIds: any[] = [] diff --git a/components/label/utils/clone.ts b/components/label/utils/clone.ts new file mode 100644 index 0000000..ca8d4c9 --- /dev/null +++ b/components/label/utils/clone.ts @@ -0,0 +1,49 @@ +const deepCloneFallback = (value: T, cache = new WeakMap()): T => { + if (value === null || typeof value !== "object") return value + if (cache.has(value as object)) return cache.get(value as object) + + if (value instanceof Date) return new Date(value.getTime()) as T + if (value instanceof Map) { + const clonedMap = new Map() + cache.set(value as object, clonedMap) + value.forEach((mapValue, mapKey) => { + clonedMap.set( + deepCloneFallback(mapKey, cache), + deepCloneFallback(mapValue, cache) + ) + }) + return clonedMap as T + } + if (value instanceof Set) { + const clonedSet = new Set() + cache.set(value as object, clonedSet) + value.forEach((setValue) => { + clonedSet.add(deepCloneFallback(setValue, cache)) + }) + return clonedSet as T + } + if (Array.isArray(value)) { + const clonedArr: any[] = [] + cache.set(value as object, clonedArr) + value.forEach((item, index) => { + clonedArr[index] = deepCloneFallback(item, cache) + }) + return clonedArr as T + } + + const clonedObj: Record = {} + cache.set(value as object, clonedObj) + Object.keys(value as object).forEach((key) => { + clonedObj[key] = deepCloneFallback((value as any)[key], cache) + }) + return clonedObj as T +} + +export const safeClone = (value: T): T => { + try { + return structuredClone(value) + } catch (error) { + console.warn("structuredClone failed, fallback to deep clone", error) + return deepCloneFallback(value) + } +}