From d7b219507833cc1132d6ab882aafb331891ada40 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Tue, 21 Apr 2026 15:49:41 +0800 Subject: [PATCH] fix(scale): fix --- components/label/LabelNossr.tsx | 42 ++++++- .../label/components/CrosshairComponent.tsx | 22 +++- .../label/components/PaperContainer.tsx | 110 +++++++++++++++++- .../label/components/ScaleToolContainer.tsx | 94 +++++++++++++-- components/label/useTopToolsStore.ts | 9 ++ components/label/utils/scale.ts | 23 ++++ 6 files changed, 276 insertions(+), 24 deletions(-) create mode 100644 components/label/utils/scale.ts diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 93cb40d..580a8d6 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -59,6 +59,7 @@ import { findGroupKey, isEditableKeyboardTarget } from "./util" import { safeClone } from "./utils/clone" import { labelTypeMap } from "./utils/constants" import { toggleObjectHideByShortcut } from "./utils/objectVisibility" +import { getDisplayedImageScale } from "./utils/scale" // Splitter component - will need custom implementation or alternative @@ -325,6 +326,13 @@ const LabelPage = ({ setObjectOperations, } = useTopToolsStore() const { activeImage } = useBottomToolsStore() + const activeRasterScale = usePaperStore( + (state) => state.rasterScale[activeImage] ?? 1 + ) + const activeRasterSize = usePaperStore( + (state) => state.rasterSize[activeImage] ?? null + ) + const currentGroup = usePaperStore((state) => state.group) const { setDescOperations, metaOperation, @@ -333,6 +341,28 @@ const LabelPage = ({ setQaData, resetData, } = useDescToolsStore() + const displayedImageScale = useMemo(() => { + return getDisplayedImageScale({ + rasterScale: activeRasterScale, + viewScale: scale, + }) + }, [activeRasterScale, scale]) + const scaleOffsets = useMemo(() => { + if (!currentGroup || !activeRasterSize?.[0] || !activeRasterSize?.[1]) { + return { + offsetX: 0, + offsetY: 0, + } + } + + const displayedWidth = activeRasterSize[0] * displayedImageScale + const displayedHeight = activeRasterSize[1] * displayedImageScale + + return { + offsetX: displayedWidth / 2 - currentGroup.position.x, + offsetY: displayedHeight / 2 - currentGroup.position.y, + } + }, [activeRasterSize, currentGroup, displayedImageScale]) const asyncGetProjectDetail = useCallback(async () => { if (projectId) { @@ -2634,18 +2664,18 @@ const LabelPage = ({ style={{ overflow: "hidden" }}> diff --git a/components/label/components/CrosshairComponent.tsx b/components/label/components/CrosshairComponent.tsx index e569746..d859bfb 100644 --- a/components/label/components/CrosshairComponent.tsx +++ b/components/label/components/CrosshairComponent.tsx @@ -4,9 +4,13 @@ import React, { useCallback, useEffect, useImperativeHandle, + useMemo, useRef, } from "react" +import { useBottomToolsStore } from "../useBottomToolsStore" +import { usePaperStore } from "../usePaperStore" import { useTopToolsStore } from "../useTopToolsStore" +import { getDisplayedImageScale } from "../utils/scale" interface CrosshairComponentProps { isCarved: boolean @@ -18,7 +22,17 @@ const CrosshairComponent = ( ) => { const { isCarved } = props - const { scale } = useTopToolsStore() + const scale = useTopToolsStore((state) => state.scale) + const activeImage = useBottomToolsStore((state) => state.activeImage) + const rasterScale = usePaperStore( + (state) => state.rasterScale[activeImage] ?? 1 + ) + const displayedScale = useMemo(() => { + return getDisplayedImageScale({ + rasterScale, + viewScale: scale, + }) + }, [rasterScale, scale]) const canvasRef = useRef(null) const lastPointerRef = useRef<{ x: number; y: number } | null>(null) @@ -82,9 +96,9 @@ const CrosshairComponent = ( ctx.lineWidth = carveLineWidth // 间隔 - const sparsity = getSparsity(scale) + const sparsity = getSparsity(displayedScale) const part = 10 - const pixelPerUnit = scale * sparsity + const pixelPerUnit = displayedScale * sparsity const gap = pixelPerUnit / part const fixed = getFixed(sparsity) @@ -136,7 +150,7 @@ const CrosshairComponent = ( } } }, - [isCarved, scale] + [displayedScale, isCarved] ) useEffect(() => { diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index a3f4a9f..452740f 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -66,6 +66,10 @@ import { paperShapeToMultiPolygon, } from "../utils/geometry/booleanAdapter" import { adjustPoints } from "../utils/paperjs" +import { + getDisplayedImageScale, + getViewScaleFromDisplayedScale, +} from "../utils/scale" import AssistShapeComponent from "./AssistShapeComponent" import CrosshairComponent from "./CrosshairComponent" import { renderOperationIcon } from "./RightObjectTools" @@ -2923,6 +2927,80 @@ const PaperContainer = ( } }, [stopMiddleMousePan]) + const getPicRasters = useCallback((group: paper.Group) => { + return group + .getItems({ data: { name: "pic" } }) + .filter((item): item is paper.Raster => item instanceof paper.Raster) + }, []) + + const getCachedDisplayedScale = useCallback( + (group: paper.Group | null | undefined) => { + if (!group || !saveCurrentScale) return null + const persistedDisplayedScale = useTopToolsStore.getState().displayedScale + + const rasterItems = getPicRasters(group) + if (!rasterItems.length) { + return Number.isFinite(persistedDisplayedScale) && + persistedDisplayedScale > 0 + ? persistedDisplayedScale + : null + } + const currentRaster = rasterItems.length + ? rasterItems[rasterItems.length - 1] + : null + const imageId = + typeof currentRaster?.data?.imageId === "string" + ? currentRaster.data.imageId + : useBottomToolsStore.getState().activeImage + + if (!imageId) { + return Number.isFinite(persistedDisplayedScale) && + persistedDisplayedScale > 0 + ? persistedDisplayedScale + : null + } + + const viewScale = + Number.isFinite(group.scaling?.x) && group.scaling.x > 0 + ? group.scaling.x + : useTopToolsStore.getState().scale + + return getDisplayedImageScale({ + rasterScale: usePaperStore.getState().rasterScale[imageId], + viewScale, + }) + }, + [getPicRasters, saveCurrentScale] + ) + + const getTargetGroupScale = useCallback( + ({ + rasterScale, + cachedDisplayedScale, + fallbackScale, + }: { + rasterScale: number + cachedDisplayedScale?: number | null + fallbackScale?: number | null + }) => { + if ( + saveCurrentScale && + Number.isFinite(cachedDisplayedScale) && + Number(cachedDisplayedScale) > 0 + ) { + return getViewScaleFromDisplayedScale({ + displayedScale: cachedDisplayedScale, + rasterScale, + }) + } + + return Number.isFinite(fallbackScale) && Number(fallbackScale) > 0 + ? Number(fallbackScale) + : 1 + }, + [saveCurrentScale] + ) + const getTargetGroupPosition = useCallback( ({ defaultPosition, @@ -2958,6 +3036,7 @@ const PaperContainer = ( clearPendingFrameSwitch(token) return } + const cachedDisplayedScale = getCachedDisplayedScale(currentVisibleGroup) const preserveGroupPosition = saveCurrentScale && currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0 @@ -3034,6 +3113,11 @@ const PaperContainer = ( const scaleX = canvasWidth / raster.bounds.width const scaleY = canvasHeight / raster.bounds.height const scale = Math.min(scaleX, scaleY) + const targetGroupScale = getTargetGroupScale({ + rasterScale: scale, + cachedDisplayedScale, + fallbackScale: currentVisibleGroup.scaling.x, + }) if (!Number.isFinite(scale) || scale <= 0) { cleanupStagingGroup() @@ -3048,6 +3132,10 @@ const PaperContainer = ( raster.bounds.width / 2, raster.bounds.height / 2 ) + stagingGroup.scaling = new paper.Point( + targetGroupScale, + targetGroupScale + ) const centeredGroupPosition = new paper.Point( raster.bounds.width / 2, raster.bounds.height / 2 @@ -3074,6 +3162,7 @@ const PaperContainer = ( const previousGroup = usePaperStore.getState().group setGroup(stagingGroup) + useTopToolsStore.getState().setScale(targetGroupScale) setRasterPath(new paper.Path.Rectangle(raster.bounds)) renderedImageRef.current = imageName commitFrameSwitch(imageName, token) @@ -3101,6 +3190,8 @@ const PaperContainer = ( [ clearPendingFrameSwitch, commitFrameSwitch, + getCachedDisplayedScale, + getTargetGroupScale, getTargetGroupPosition, imgSize?.clientHeight, imgSize?.clientWidth, @@ -3166,12 +3257,6 @@ const PaperContainer = ( ) decodeFrameImageRef.current = decodeFrameImage - const getPicRasters = useCallback((group: paper.Group) => { - return group - .getItems({ data: { name: "pic" } }) - .filter((item): item is paper.Raster => item instanceof paper.Raster) - }, []) - const cancelRasterFadeTransition = useCallback(() => { rasterFadeTransitionIdRef.current += 1 if ( @@ -3194,6 +3279,7 @@ const PaperContainer = ( if (activeImage && projectDetail?.id) { const requestId = ++rasterLoadRequestIdRef.current const requestImage = activeImage + const cachedDisplayedScale = getCachedDisplayedScale(paperGroup) const preserveGroupPosition = saveCurrentScale && getPicRasters(paperGroup).length > 0 const preservedGroupPosition = preserveGroupPosition @@ -3275,6 +3361,11 @@ const PaperContainer = ( ]) let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio + const targetGroupScale = getTargetGroupScale({ + rasterScale: scale, + cachedDisplayedScale, + fallbackScale: paperGroup.scaling.x, + }) if (!Number.isFinite(scale) || scale <= 0) { console.warn( "[PaperContainer] skip raster scale: invalid scale", @@ -3357,6 +3448,10 @@ const PaperContainer = ( raster.bounds.width / 2, raster.bounds.height / 2 ) + paperGroup.scaling = new paper.Point( + targetGroupScale, + targetGroupScale + ) const centeredGroupPosition = new paper.Point( raster.bounds.width / 2, raster.bounds.height / 2 @@ -3422,6 +3517,7 @@ const PaperContainer = ( setRasterPath(new paper.Path.Rectangle(raster.bounds)) raster.sendToBack() renderedImageRef.current = requestImage + useTopToolsStore.getState().setScale(targetGroupScale) setRasterInited(true) usePaperStore.getState().group?.bringToFront() } @@ -3439,7 +3535,9 @@ const PaperContainer = ( activeImage, cancelRasterFadeTransition, decodeFrameImage, + getCachedDisplayedScale, getPicRasters, + getTargetGroupScale, getTargetGroupPosition, imgSize?.clientHeight, imgSize?.clientWidth, diff --git a/components/label/components/ScaleToolContainer.tsx b/components/label/components/ScaleToolContainer.tsx index 780d3d0..d6873b6 100644 --- a/components/label/components/ScaleToolContainer.tsx +++ b/components/label/components/ScaleToolContainer.tsx @@ -1,25 +1,75 @@ import { ActionIcon, Flex, NumberInput, Stack, Text } from "@mantine/core" import { IconMinus, IconPlus, IconRefresh } from "@tabler/icons-react" import paper from "paper" -import { useMemo } from "react" +import { useEffect, useMemo, useRef } from "react" import { useKeyEventStore } from "../store" +import { useBottomToolsStore } from "../useBottomToolsStore" import { usePaperStore } from "../usePaperStore" import { useTopToolsStore } from "../useTopToolsStore" +import { + getDisplayedImageScale, + getViewScaleFromDisplayedScale, +} from "../utils/scale" const ScaleToolContainer = () => { - const { scale, setScale } = useTopToolsStore() + const { + scale, + setScale, + displayedScale: savedDisplayedScale, + setDisplayedScale, + } = useTopToolsStore() const { setFocusInput } = useKeyEventStore() + const activeImage = useBottomToolsStore((state) => state.activeImage) + const rasterScale = usePaperStore((state) => state.rasterScale[activeImage]) + const rasterSize = usePaperStore((state) => state.rasterSize[activeImage]) + const paperScope = usePaperStore((state) => state.paperScope) + const group = usePaperStore((state) => state.group) + const lastDisplayedScaleRef = useRef( + Number.isFinite(savedDisplayedScale) && savedDisplayedScale > 0 + ? savedDisplayedScale + : 1 + ) + + const displayedScale = useMemo(() => { + const hasValidRasterScale = + Number.isFinite(rasterScale) && Number(rasterScale) > 0 + + if (!activeImage || !hasValidRasterScale) { + if (group) return lastDisplayedScaleRef.current + return Number.isFinite(savedDisplayedScale) && savedDisplayedScale > 0 + ? savedDisplayedScale + : 1 + } + + return getDisplayedImageScale({ + rasterScale, + viewScale: scale, + }) + }, [activeImage, group, rasterScale, savedDisplayedScale, scale]) + + useEffect(() => { + if (!activeImage) return + if (!Number.isFinite(displayedScale) || displayedScale <= 0) return + lastDisplayedScaleRef.current = displayedScale + if (Math.abs(displayedScale - savedDisplayedScale) > 0.000001) { + setDisplayedScale(displayedScale) + } + }, [activeImage, displayedScale, savedDisplayedScale, setDisplayedScale]) const displayNumber = useMemo(() => { - return Number((scale * 100).toFixed(0)) - }, [scale]) + return Number((displayedScale * 100).toFixed(0)) + }, [displayedScale]) const setCurrentScale = (n: number) => { - const currentScale = n / 100 + const currentScale = getViewScaleFromDisplayedScale({ + displayedScale: n / 100, + rasterScale, + }) const group = usePaperStore.getState().group if (!group) return group.scaling = new paper.Point(currentScale, currentScale) setScale(currentScale) + setDisplayedScale(n / 100) } const handleReset = () => { @@ -31,9 +81,37 @@ const ScaleToolContainer = () => { }, }) if (!rasterList || !rasterList.length) return - const currentRaster = rasterList[0] - group.position = currentRaster.position - setCurrentScale(100) + const currentRaster = rasterList.find( + (item): item is paper.Raster => item instanceof paper.Raster + ) + if (!currentRaster) return + const rawWidth = rasterSize?.[0] ?? currentRaster.width + const rawHeight = rasterSize?.[1] ?? currentRaster.height + const canvasWidth = paperScope?.view.bounds.width + const canvasHeight = paperScope?.view.bounds.height + const fitDisplayedScale = + rawWidth && + rawHeight && + canvasWidth && + canvasHeight && + Number.isFinite(canvasWidth) && + Number.isFinite(canvasHeight) + ? Math.min(canvasWidth / rawWidth, canvasHeight / rawHeight) + : getDisplayedImageScale({ + rasterScale, + viewScale: 1, + }) + const targetViewScale = getViewScaleFromDisplayedScale({ + displayedScale: fitDisplayedScale, + rasterScale, + }) + group.scaling = new paper.Point(targetViewScale, targetViewScale) + group.position = new paper.Point( + (rawWidth * fitDisplayedScale) / 2, + (rawHeight * fitDisplayedScale) / 2 + ) + setScale(targetViewScale) + setDisplayedScale(fitDisplayedScale) } const handleEnter = (e: any) => { diff --git a/components/label/useTopToolsStore.ts b/components/label/useTopToolsStore.ts index c2351c2..d5f0500 100644 --- a/components/label/useTopToolsStore.ts +++ b/components/label/useTopToolsStore.ts @@ -62,6 +62,8 @@ interface TopToolsState { setSaveCurrentScale: (val: boolean) => void scale: number setScale: (val: number) => void + displayedScale: number + setDisplayedScale: (val: number) => void activeOperation: string setActiveOperation: (val: string) => void showObjectList: boolean @@ -114,6 +116,7 @@ const initialTopToolsState = { drawOption: "default" as "default" | "intersect" | "unite", saveCurrentScale: false, scale: 1, + displayedScale: 1, activeOperation: "", showObjectList: false, showGroupList: false, @@ -248,6 +251,12 @@ export const useTopToolsStore = create()( ...state, scale: val, })), + displayedScale: initialTopToolsState.displayedScale, + setDisplayedScale: (val: number) => + set((state: TopToolsState) => ({ + ...state, + displayedScale: Number.isFinite(val) && val > 0 ? val : 1, + })), activeOperation: initialTopToolsState.activeOperation, setActiveOperation: (val: string) => set((state: TopToolsState) => ({ diff --git a/components/label/utils/scale.ts b/components/label/utils/scale.ts new file mode 100644 index 0000000..28dc11d --- /dev/null +++ b/components/label/utils/scale.ts @@ -0,0 +1,23 @@ +export const normalizeScale = (value: number | null | undefined) => { + return Number.isFinite(value) && Number(value) > 0 ? Number(value) : 1 +} + +export const getDisplayedImageScale = ({ + rasterScale, + viewScale, +}: { + rasterScale?: number | null + viewScale?: number | null +}) => { + return normalizeScale(rasterScale) * normalizeScale(viewScale) +} + +export const getViewScaleFromDisplayedScale = ({ + displayedScale, + rasterScale, +}: { + displayedScale?: number | null + rasterScale?: number | null +}) => { + return normalizeScale(displayedScale) / normalizeScale(rasterScale) +}