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)
+}