fix(scale): fix

This commit is contained in:
zhangheng
2026-04-21 15:49:41 +08:00
parent 853801b947
commit d7b2195078
6 changed files with 276 additions and 24 deletions

View File

@@ -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" }}>
<ScaleComponent
options={{
offsetX: 0,
offsetY: 0,
scale: scale,
offsetX: scaleOffsets.offsetX,
offsetY: scaleOffsets.offsetY,
scale: displayedImageScale,
}}
mode="horizontal"
/>
<Flex h="calc(100% - 32px)">
<ScaleComponent
options={{
offsetX: 0,
offsetY: 0,
scale: scale,
offsetX: scaleOffsets.offsetX,
offsetY: scaleOffsets.offsetY,
scale: displayedImageScale,
}}
mode="vertical"
/>

View File

@@ -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<HTMLCanvasElement>(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(() => {

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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<TopToolsState>()(
...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) => ({

View File

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