From 19010c4b417e436b9864dd283220cfc7646f1888 Mon Sep 17 00:00:00 2001 From: shay7sev Date: Wed, 4 Mar 2026 10:36:24 +0800 Subject: [PATCH] fix(paperjs): fix scale bug --- components/label/LabelNossr.tsx | 49 ++- .../label/components/PaperContainer.tsx | 336 ++++++++++++------ components/label/utils/paperjs.ts | 80 +++-- 3 files changed, 325 insertions(+), 140 deletions(-) diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index abb2a9e..72bd448 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -72,6 +72,8 @@ type Action = const initialState: LabelState = new Map() const operationTypeList = [101, 102, 103, 104] +let latestProjectDetailRequestSeq = 0 +let latestTaskDetailRequestSeq = 0 // utils const getRectPoints = ([sx, sy, w, h]: number[]) => { @@ -200,8 +202,17 @@ const LabelPage = ({ const asyncGetProjectDetail = useCallback(async () => { if (projectId) { + const requestSeq = ++latestProjectDetailRequestSeq try { const res = await getProjectDetail(projectId) + if (requestSeq !== latestProjectDetailRequestSeq) { + console.warn("[LabelNossr] ignore stale project detail response", { + projectId, + requestSeq, + latestRequestSeq: latestProjectDetailRequestSeq, + }) + return + } setProjectDetail(res) let operations: Project.LabelSchemaList[] = [] let textOperations: Project.LabelSchemaList[] = [] @@ -362,6 +373,7 @@ const LabelPage = ({ const asyncGetTaskDetail = useCallback(async () => { if (taskId) { + const requestSeq = ++latestTaskDetailRequestSeq try { usePaperStore.getState().setLoadingData(true) // 获取当前任务全量数据 @@ -371,8 +383,24 @@ const LabelPage = ({ page_number: 1, page_size: 1, }) + if (requestSeq !== latestTaskDetailRequestSeq) { + console.warn("[LabelNossr] ignore stale task list response", { + taskId, + requestSeq, + latestRequestSeq: latestTaskDetailRequestSeq, + }) + return + } let detail = res.task_list[0] detail = await normalizeVideoTaskData(detail) + if (requestSeq !== latestTaskDetailRequestSeq) { + console.warn("[LabelNossr] ignore stale normalized task response", { + taskId, + requestSeq, + latestRequestSeq: latestTaskDetailRequestSeq, + }) + return + } setTaskDetail(detail) // 帧列表 useBottomToolsStore @@ -383,6 +411,14 @@ const LabelPage = ({ let turn1time = 1 let turn2time = 1 const flowRes = await getTaskWorkFlow(taskId) + if (requestSeq !== latestTaskDetailRequestSeq) { + console.warn("[LabelNossr] ignore stale workflow response", { + taskId, + requestSeq, + latestRequestSeq: latestTaskDetailRequestSeq, + }) + return + } flowRes.forEach((item) => { if (item.task_status_src !== 4 && item.task_status_dst === 4) { flowCommentArr.push({ @@ -424,6 +460,14 @@ const LabelPage = ({ }) // 获取并处理当前任务标注结果数据 const dataRes = await getLabelResult(taskId) + if (requestSeq !== latestTaskDetailRequestSeq) { + console.warn("[LabelNossr] ignore stale label result response", { + taskId, + requestSeq, + latestRequestSeq: latestTaskDetailRequestSeq, + }) + return + } const { results } = dataRes const resultImageEntries = results.flatMap((item) => expandLabelResultImages(item) @@ -714,6 +758,7 @@ const LabelPage = ({ useDescToolsStore.getState().updateFlag(true) useRightToolsStore.getState().setPathGroupMap(pathGroupData) } catch (err) { + if (requestSeq !== latestTaskDetailRequestSeq) return console.log(err) setOldWorkLoad(null) setTaskDetail(undefined) @@ -729,7 +774,9 @@ const LabelPage = ({ }) } } finally { - usePaperStore.getState().setLoadingData(false) + if (requestSeq === latestTaskDetailRequestSeq) { + usePaperStore.getState().setLoadingData(false) + } } } }, [ diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 72e0a0c..6c36864 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -633,6 +633,52 @@ const PaperContainer = ( let operationSchema = getOperationSchema(operationId) if (operationSchema && imageData) { + const toPaperPoint = (segment: any): paper.Point | null => { + if (segment instanceof paper.Point) return segment + if (segment instanceof paper.Segment) { + return new paper.Point(segment.point) + } + if ( + Array.isArray(segment) && + segment.length >= 2 && + typeof segment[0] === "number" && + typeof segment[1] === "number" + ) { + return new paper.Point(segment[0], segment[1]) + } + if ( + Array.isArray(segment) && + Array.isArray(segment[0]) && + segment[0].length >= 2 && + typeof segment[0][0] === "number" && + typeof segment[0][1] === "number" + ) { + return new paper.Point(segment[0][0], segment[0][1]) + } + if ( + segment?.point && + typeof segment.point.x === "number" && + typeof segment.point.y === "number" + ) { + return new paper.Point(segment.point.x, segment.point.y) + } + if ( + segment?.point && + Array.isArray(segment.point) && + segment.point.length >= 2 && + typeof segment.point[0] === "number" && + typeof segment.point[1] === "number" + ) { + return new paper.Point(segment.point[0], segment.point[1]) + } + return null + } + const toPaperPoints = (segments: any[]) => { + return segments + .map((segment) => toPaperPoint(segment)) + .filter((point): point is paper.Point => point !== null) + } + let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})` let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)` let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)` @@ -646,69 +692,65 @@ const PaperContainer = ( case "多边形": paths?.forEach((item) => { let outerPaths = - item[1]?.map((child) => { - return addPolygon( - renderPath, - child.map((segment: any) => { - if (segment instanceof paper.Point) { - return segment - } else { - return new paper.Point(segment[0], segment[1]) + item[1] + ?.map((child) => { + const points = toPaperPoints(child) + if (!points.length) return null + return addPolygon( + renderPath, + points, + { + strokeColor: strokeColor, + strokeWidth: 2, + // fillColor: fillColor, + fillColor: blankColor, + visible: + !paperPathStatus[activeImage + item[0]]?.["HIDE"], + }, + { + type: "polygon", + id: item[0], + imageId: activeImage, + operationId: operationId, + fillColor: fillColor, + blankColor: blankColor, + selected: paperSelectedPath[activeImage]?.includes( + item[0] + ), } - }), - { - strokeColor: strokeColor, - strokeWidth: 2, - // fillColor: fillColor, - fillColor: blankColor, - visible: - !paperPathStatus[activeImage + item[0]]?.["HIDE"], - }, - { - type: "polygon", - id: item[0], - imageId: activeImage, - operationId: operationId, - fillColor: fillColor, - blankColor: blankColor, - selected: paperSelectedPath[activeImage]?.includes( - item[0] - ), - } - ) - }) || [] + ) + }) + .filter((path): path is paper.Path => !!path) || [] let innerPaths = - item[3]?.map((child) => { - return addPolygon( - renderPath, - child.map((segment: any) => { - if (segment instanceof paper.Point) { - return segment - } else { - return new paper.Point(segment[0], segment[1]) + item[3] + ?.map((child) => { + const points = toPaperPoints(child) + if (!points.length) return null + return addPolygon( + renderPath, + points, + { + strokeColor: strokeColor, + strokeWidth: 2, + fillColor: fillColor, + visible: + !paperPathStatus[activeImage + item[0]]?.["HIDE"], + }, + { + type: "polygon", + id: item[0], + imageId: activeImage, + operationId: operationId, + fillColor: fillColor, + blankColor: blankColor, + selected: paperSelectedPath[activeImage]?.includes( + item[0] + ), + isHollowPolygon: true, } - }), - { - strokeColor: strokeColor, - strokeWidth: 2, - fillColor: fillColor, - visible: - !paperPathStatus[activeImage + item[0]]?.["HIDE"], - }, - { - type: "polygon", - id: item[0], - imageId: activeImage, - operationId: operationId, - fillColor: fillColor, - blankColor: blankColor, - selected: paperSelectedPath[activeImage]?.includes( - item[0] - ), - isHollowPolygon: true, - } - ) - }) || [] + ) + }) + .filter((path): path is paper.Path => !!path) || [] addCompoundPath( renderCompoundPath, @@ -767,10 +809,8 @@ const PaperContainer = ( item[1]?.forEach((child) => { // 保存前排好序 child.forEach((segment: any, segmentIndex: number) => { - let point = - segment instanceof paper.Segment - ? new paper.Point(segment.point) - : new paper.Point(segment[0], segment[1]) + let point = toPaperPoint(segment) + if (!point) return addPoint( renderCircle, point, @@ -788,15 +828,11 @@ const PaperContainer = ( } ) }) + const points = toPaperPoints(child) + if (!points.length) return addPointLine( renderPath, - child.map((segment: any) => { - if (segment instanceof paper.Segment) { - return new paper.Point(segment.point) - } else { - return new paper.Point(segment[0], segment[1]) - } - }), + points, { strokeColor: strokeColor, strokeWidth: 2, @@ -819,37 +855,35 @@ const PaperContainer = ( case "多线段": paths?.forEach((item) => { let outerPaths = - item[1]?.map((child) => { - return addBrush( - renderPath, - child.map((segment: any) => { - if (segment instanceof paper.Point) { - return segment - } else { - return new paper.Point(segment[0], segment[1]) + item[1] + ?.map((child) => { + const points = toPaperPoints(child) + if (!points.length) return null + return addBrush( + renderPath, + points, + { + strokeColor: strokeColor, + strokeWidth: 2, + // fillColor: fillColor, + fillColor: blankColor, + visible: + !paperPathStatus[activeImage + item[0]]?.["HIDE"], + }, + { + type: "brush", + id: item[0], + imageId: activeImage, + operationId: operationId, + fillColor: fillColor, + blankColor: blankColor, + selected: paperSelectedPath[activeImage]?.includes( + item[0] + ), } - }), - { - strokeColor: strokeColor, - strokeWidth: 2, - // fillColor: fillColor, - fillColor: blankColor, - visible: - !paperPathStatus[activeImage + item[0]]?.["HIDE"], - }, - { - type: "brush", - id: item[0], - imageId: activeImage, - operationId: operationId, - fillColor: fillColor, - blankColor: blankColor, - selected: paperSelectedPath[activeImage]?.includes( - item[0] - ), - } - ) - }) || [] + ) + }) + .filter((path): path is paper.Path => !!path) || [] addCompoundPath( renderCompoundPath, @@ -876,13 +910,18 @@ const PaperContainer = ( case "2D框": paths?.forEach((item) => { item[1].forEach((child) => { - let flag = child[0] instanceof paper.Point - let p = flag - ? child[1] - : new paper.Point(child[1][0], child[1][1]) - let dp = flag - ? child[3] - : new paper.Point(child[3][0], child[3][1]) + let p = toPaperPoint(child[1]) + let dp = toPaperPoint(child[3]) + if (!p || !dp) { + console.warn( + "[PaperContainer] skip invalid rectangle segment", + { + id: item[0], + child, + } + ) + return + } addRectangle( renderRectangle, @@ -1216,6 +1255,7 @@ const PaperContainer = ( }, [activeImage]) const [rasterInited, setRasterInited] = useState(false) + const rasterLoadRequestIdRef = useRef(0) const renderAndScaleRaster = useCallback( async (paperGroup: paper.Group) => { @@ -1224,6 +1264,24 @@ const PaperContainer = ( // }); if (activeImage && projectDetail?.id) { + const requestId = ++rasterLoadRequestIdRef.current + const requestImage = activeImage + const isStaleRasterRequest = () => { + const currentImage = useBottomToolsStore.getState().activeImage + const stale = + requestId !== rasterLoadRequestIdRef.current || + currentImage !== requestImage + if (stale) { + console.warn("[PaperContainer] ignore stale raster callback", { + requestId, + latestRequestId: rasterLoadRequestIdRef.current, + requestImage, + currentImage, + }) + } + return stale + } + console.log(usePaperStore.getState().paperScope) // console.log(activeImage, projectDetail?.rpath); notifications.show({ @@ -1245,10 +1303,12 @@ const PaperContainer = ( data_type: 0, project_id: projectDetail?.id, }) + if (isStaleRasterRequest()) return text = `data:image/jpeg;base64,${response}` images.set(activeImage, text) useImagesStore.getState().setImages(images) } + if (isStaleRasterRequest()) return if (!text) return raster = renderRaster(paperGroup, { source: text, @@ -1259,9 +1319,29 @@ const PaperContainer = ( if (!raster) return raster.onLoad = function () { + if (isStaleRasterRequest()) { + raster.remove() + return + } // Scale the raster to fit the canvas while maintaining aspect ratio - let canvasWidth = paperScope.current?.view.bounds.width - let canvasHeight = paperScope.current?.view.bounds.height + let canvasWidth = + imgSize?.clientWidth ?? paperScope.current?.view.bounds.width + let canvasHeight = + imgSize?.clientHeight ?? paperScope.current?.view.bounds.height + + if (!canvasWidth || !canvasHeight) { + console.warn( + "[PaperContainer] skip raster scale: invalid canvas", + { + requestId, + requestImage, + canvasWidth, + canvasHeight, + } + ) + raster.remove() + return + } let scaleX = canvasWidth! / raster.bounds.width let scaleY = canvasHeight! / raster.bounds.height @@ -1271,11 +1351,35 @@ const PaperContainer = ( ]) let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio + if (!Number.isFinite(scale) || scale <= 0) { + console.warn( + "[PaperContainer] skip raster scale: invalid scale", + { + requestId, + requestImage, + scale, + scaleX, + scaleY, + } + ) + raster.remove() + return + } // 将raster以(0,0)为中心点进行缩放 raster.scale(scale, new paper.Point(0, 0)) const currentImgScale = usePaperStore.getState().rasterScale[activeImage] + console.debug("[PaperContainer] raster scale reconcile", { + image: activeImage, + currentImgScale, + nextScale: scale, + branch: !currentImgScale + ? "init-scale" + : currentImgScale !== scale + ? "rescale" + : "keep", + }) // 当前图片无缩放比例或缩放比例不等于上次记录比例时 if (!currentImgScale) { setRasterScale(activeImage, scale) @@ -1338,6 +1442,8 @@ const PaperContainer = ( }, [ activeImage, + imgSize?.clientHeight, + imgSize?.clientWidth, projectDetail?.id, setRasterPath, @@ -1367,6 +1473,7 @@ const PaperContainer = ( useEffect(() => { if (loadingData) return + if (!imgSize?.clientWidth || !imgSize?.clientHeight) return const currentGroup = usePaperStore.getState().group if (currentGroup && projectDetail) { if (!useTopToolsStore.getState().saveCurrentScale) { @@ -1382,7 +1489,14 @@ const PaperContainer = ( currentGroup ) } - }, [activeImage, loadImageAndPolygons, loadingData, projectDetail]) + }, [ + activeImage, + imgSize?.clientHeight, + imgSize?.clientWidth, + loadImageAndPolygons, + loadingData, + projectDetail, + ]) useEffect(() => { if (rasterInited) { diff --git a/components/label/utils/paperjs.ts b/components/label/utils/paperjs.ts index cf2298f..7172799 100644 --- a/components/label/utils/paperjs.ts +++ b/components/label/utils/paperjs.ts @@ -1,5 +1,53 @@ import paper from "paper" +const scaleFlatPoint = (point: any, scale: number): [number, number] => { + return [point[0] * scale, point[1] * scale] +} + +const scaleSegmentToPoint = (seg: any, scale: number): any => { + if (seg instanceof paper.Point) { + return [seg.x * scale, seg.y * scale] + } + if (seg instanceof paper.Segment) { + return [seg.point.x * scale, seg.point.y * scale] + } + if ( + Array.isArray(seg) && + seg.length >= 2 && + typeof seg[0] === "number" && + typeof seg[1] === "number" + ) { + return scaleFlatPoint(seg, scale) + } + if ( + Array.isArray(seg) && + Array.isArray(seg[0]) && + seg[0].length >= 2 && + typeof seg[0][0] === "number" && + typeof seg[0][1] === "number" + ) { + // Support paper JSON segment format: [[x,y], [handleInX, handleInY], [handleOutX, handleOutY]] + return scaleFlatPoint(seg[0], scale) + } + if ( + seg?.point && + typeof seg.point.x === "number" && + typeof seg.point.y === "number" + ) { + return [seg.point.x * scale, seg.point.y * scale] + } + if ( + seg?.point && + Array.isArray(seg.point) && + seg.point.length >= 2 && + typeof seg.point[0] === "number" && + typeof seg.point[1] === "number" + ) { + return [seg.point[0] * scale, seg.point[1] * scale] + } + return seg +} + // 调整原始比例点位到当前图片的缩放比例 export const adjustPoints: ( id: string, @@ -34,25 +82,13 @@ export const adjustPoints: ( let adjustedPoints = points.map((child) => { return child.map((seg: any) => { - if (seg instanceof paper.Point) { - return [seg.x * scale, seg.y * scale] - } else if (seg instanceof paper.Segment) { - return [seg.point.x * scale, seg.point.y * scale] - } else { - return seg.map((item: any) => item * scale) - } + return scaleSegmentToPoint(seg, scale) }) }) let adjustedHollowPoints = hollowPoints.map((child) => { return child.map((seg: any) => { - if (seg instanceof paper.Point) { - return [seg.x * scale, seg.y * scale] - } else if (seg instanceof paper.Segment) { - return [seg.point.x * scale, seg.point.y * scale] - } else { - return seg.map((item: any) => item * scale) - } + return scaleSegmentToPoint(seg, scale) }) }) @@ -103,25 +139,13 @@ export const adjustAllPoints: ( let adjustedPoints = points.map((child) => { return child.map((seg: any) => { - if (seg instanceof paper.Point) { - return [seg.x * scale, seg.y * scale] - } else if (seg instanceof paper.Segment) { - return [seg.point.x * scale, seg.point.y * scale] - } else { - return seg.map((item: any) => item * scale) - } + return scaleSegmentToPoint(seg, scale) }) }) let adjustedHollowPoints = hollowPoints.map((child) => { return child.map((seg: any) => { - if (seg instanceof paper.Point) { - return [seg.x * scale, seg.y * scale] - } else if (seg instanceof paper.Segment) { - return [seg.point.x * scale, seg.point.y * scale] - } else { - return seg.map((item: any) => item * scale) - } + return scaleSegmentToPoint(seg, scale) }) })