Compare commits
11 Commits
e30eb3b589
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a91fe7702d | ||
|
|
bf65d7a123 | ||
|
|
7478788efd | ||
|
|
8776ce4181 | ||
|
|
ea4daa4631 | ||
|
|
d7b2195078 | ||
|
|
07ebedb183 | ||
|
|
853801b947 | ||
|
|
dbdf834caa | ||
|
|
d8fae8ab04 | ||
|
|
602dcc5f5e |
@@ -49,16 +49,13 @@ import { useDescToolsStore } from "./useDescToolsStore"
|
|||||||
import { useKeyboardStore } from "./useKeyBoardStore"
|
import { useKeyboardStore } from "./useKeyBoardStore"
|
||||||
import { usePaperStore } from "./usePaperStore"
|
import { usePaperStore } from "./usePaperStore"
|
||||||
import { useRightToolsStore } from "./useRightToolsStore"
|
import { useRightToolsStore } from "./useRightToolsStore"
|
||||||
import {
|
import { useIntervalStore, useLabelTimeStore } from "./useTimerStore"
|
||||||
useIntervalStore,
|
|
||||||
useLabelTimeStore,
|
|
||||||
useTimerStore,
|
|
||||||
} from "./useTimerStore"
|
|
||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { findGroupKey, isEditableKeyboardTarget } from "./util"
|
import { findGroupKey, isEditableKeyboardTarget } from "./util"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
import { labelTypeMap } from "./utils/constants"
|
import { labelTypeMap } from "./utils/constants"
|
||||||
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
||||||
|
import { getDisplayedImageScale } from "./utils/scale"
|
||||||
|
|
||||||
// Splitter component - will need custom implementation or alternative
|
// Splitter component - will need custom implementation or alternative
|
||||||
|
|
||||||
@@ -88,6 +85,8 @@ const RIGHT_PANEL_SEPARATOR =
|
|||||||
const RIGHT_PANEL_SOFT_MAIN_MIN_WIDTH = 420
|
const RIGHT_PANEL_SOFT_MAIN_MIN_WIDTH = 420
|
||||||
const RIGHT_PANEL_HARD_MAIN_MIN_WIDTH = 260
|
const RIGHT_PANEL_HARD_MAIN_MIN_WIDTH = 260
|
||||||
const RIGHT_PANEL_TARGET_WIDTH = 350
|
const RIGHT_PANEL_TARGET_WIDTH = 350
|
||||||
|
const INACTIVITY_VIEW_DELAY_MS = 5 * 60 * 1000
|
||||||
|
const INACTIVITY_ACTIVITY_THROTTLE_MS = 1000
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
const getRectPoints = ([sx, sy, w, h]: number[]) => {
|
const getRectPoints = ([sx, sy, w, h]: number[]) => {
|
||||||
@@ -279,6 +278,8 @@ const LabelPage = ({
|
|||||||
const topRef = useRef<any>(null)
|
const topRef = useRef<any>(null)
|
||||||
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const autoSaveInProgressRef = useRef(false)
|
const autoSaveInProgressRef = useRef(false)
|
||||||
|
const inactivityTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const lastInactivityResetAtRef = useRef(0)
|
||||||
const paperContainerRef = useRef<any>(null)
|
const paperContainerRef = useRef<any>(null)
|
||||||
const qaToolContainerRef = useRef<any>(null)
|
const qaToolContainerRef = useRef<any>(null)
|
||||||
const [isConfirmSave, setIsConfirmSave] = useState(false)
|
const [isConfirmSave, setIsConfirmSave] = useState(false)
|
||||||
@@ -325,6 +326,9 @@ const LabelPage = ({
|
|||||||
setObjectOperations,
|
setObjectOperations,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
const { activeImage } = useBottomToolsStore()
|
const { activeImage } = useBottomToolsStore()
|
||||||
|
const activeRasterScale = usePaperStore(
|
||||||
|
(state) => state.rasterScale[activeImage] ?? 1
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
setDescOperations,
|
setDescOperations,
|
||||||
metaOperation,
|
metaOperation,
|
||||||
@@ -333,6 +337,12 @@ const LabelPage = ({
|
|||||||
setQaData,
|
setQaData,
|
||||||
resetData,
|
resetData,
|
||||||
} = useDescToolsStore()
|
} = useDescToolsStore()
|
||||||
|
const displayedImageScale = useMemo(() => {
|
||||||
|
return getDisplayedImageScale({
|
||||||
|
rasterScale: activeRasterScale,
|
||||||
|
viewScale: scale,
|
||||||
|
})
|
||||||
|
}, [activeRasterScale, scale])
|
||||||
|
|
||||||
const asyncGetProjectDetail = useCallback(async () => {
|
const asyncGetProjectDetail = useCallback(async () => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
@@ -2156,7 +2166,8 @@ const LabelPage = ({
|
|||||||
|
|
||||||
if (lowerKey === "s" && commandKey) {
|
if (lowerKey === "s" && commandKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsConfirmSave(true)
|
topRef.current?.handleSave()
|
||||||
|
// setIsConfirmSave(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowerKey === "g" && commandKey) {
|
if (lowerKey === "g" && commandKey) {
|
||||||
@@ -2403,10 +2414,9 @@ const LabelPage = ({
|
|||||||
|
|
||||||
const handleBeforeUnload = (e: any) => {
|
const handleBeforeUnload = (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
e.returnValue = ""
|
||||||
// 备份
|
// 备份
|
||||||
topRef.current.handleBackup("auto_backup")
|
void topRef.current?.handleBackup?.("auto_backup")
|
||||||
// 清空操作栏状态
|
|
||||||
useObjectStore.getState().resetPathAndOperationStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新Splitter状态
|
// 更新Splitter状态
|
||||||
@@ -2442,22 +2452,86 @@ const LabelPage = ({
|
|||||||
}
|
}
|
||||||
}, [updateSplitterSizes])
|
}, [updateSplitterSizes])
|
||||||
|
|
||||||
|
const clearInactivityTimer = useCallback(() => {
|
||||||
|
if (inactivityTimerRef.current !== null) {
|
||||||
|
clearTimeout(inactivityTimerRef.current)
|
||||||
|
inactivityTimerRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const scheduleInactivityViewMode = useCallback(() => {
|
||||||
|
clearInactivityTimer()
|
||||||
|
inactivityTimerRef.current = setTimeout(() => {
|
||||||
|
const topToolsState = useTopToolsStore.getState()
|
||||||
|
if (!topToolsState.isView) {
|
||||||
|
topToolsState.setIsView(true)
|
||||||
|
}
|
||||||
|
}, INACTIVITY_VIEW_DELAY_MS)
|
||||||
|
}, [clearInactivityTimer])
|
||||||
|
|
||||||
|
const reportUserActivity = useCallback(
|
||||||
|
(force = false) => {
|
||||||
|
if (useTopToolsStore.getState().isView) return
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (
|
||||||
|
!force &&
|
||||||
|
now - lastInactivityResetAtRef.current < INACTIVITY_ACTIVITY_THROTTLE_MS
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastInactivityResetAtRef.current = now
|
||||||
|
scheduleInactivityViewMode()
|
||||||
|
},
|
||||||
|
[scheduleInactivityViewMode]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isView) {
|
if (isView) {
|
||||||
const delay = 5 * 60000
|
lastInactivityResetAtRef.current = 0
|
||||||
// 编辑模式下,修改labelData时启动或更新定时器
|
clearInactivityTimer()
|
||||||
useTimerStore.getState().startTimer(() => {
|
return
|
||||||
useTopToolsStore.getState().setIsView(true)
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
useTimerStore.getState().clearTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时清理定时器
|
// 使用真实输入事件追踪活跃度,避免进行中的标注因 labelData 未变化被误判为空闲。
|
||||||
return () => {
|
reportUserActivity(true)
|
||||||
useTimerStore.getState().clearTimer()
|
|
||||||
|
const handlePointerDown = () => {
|
||||||
|
reportUserActivity(true)
|
||||||
}
|
}
|
||||||
}, [isView, labelData])
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
if (event.buttons !== 0) {
|
||||||
|
reportUserActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const handleWheel = () => {
|
||||||
|
// reportUserActivity()
|
||||||
|
// }
|
||||||
|
const handleKeyDown = () => {
|
||||||
|
reportUserActivity(true)
|
||||||
|
}
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
reportUserActivity(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("pointerdown", handlePointerDown, true)
|
||||||
|
window.addEventListener("pointermove", handlePointerMove, true)
|
||||||
|
// window.addEventListener("wheel", handleWheel, true)
|
||||||
|
window.addEventListener("keydown", handleKeyDown, true)
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointerdown", handlePointerDown, true)
|
||||||
|
window.removeEventListener("pointermove", handlePointerMove, true)
|
||||||
|
// window.removeEventListener("wheel", handleWheel, true)
|
||||||
|
window.removeEventListener("keydown", handleKeyDown, true)
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange)
|
||||||
|
clearInactivityTimer()
|
||||||
|
}
|
||||||
|
}, [clearInactivityTimer, isView, reportUserActivity])
|
||||||
|
|
||||||
// auto save
|
// auto save
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2633,18 +2707,14 @@ const LabelPage = ({
|
|||||||
style={{ overflow: "hidden" }}>
|
style={{ overflow: "hidden" }}>
|
||||||
<ScaleComponent
|
<ScaleComponent
|
||||||
options={{
|
options={{
|
||||||
offsetX: 0,
|
scale: displayedImageScale,
|
||||||
offsetY: 0,
|
|
||||||
scale: scale,
|
|
||||||
}}
|
}}
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
/>
|
/>
|
||||||
<Flex h="calc(100% - 32px)">
|
<Flex h="calc(100% - 32px)">
|
||||||
<ScaleComponent
|
<ScaleComponent
|
||||||
options={{
|
options={{
|
||||||
offsetX: 0,
|
scale: displayedImageScale,
|
||||||
offsetY: 0,
|
|
||||||
scale: scale,
|
|
||||||
}}
|
}}
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react"
|
} from "react"
|
||||||
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
|
||||||
interface AssistShapeComponentProps {
|
interface AssistShapeComponentProps {
|
||||||
@@ -18,6 +20,10 @@ const AssistShapeComponent = (
|
|||||||
) => {
|
) => {
|
||||||
const { size } = props
|
const { size } = props
|
||||||
const scale = useTopToolsStore((state) => state.scale)
|
const scale = useTopToolsStore((state) => state.scale)
|
||||||
|
const activeImage = useBottomToolsStore((state) => state.activeImage)
|
||||||
|
const rasterScale = usePaperStore(
|
||||||
|
(state) => state.rasterScale[activeImage] ?? 1
|
||||||
|
)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
@@ -27,7 +33,7 @@ const AssistShapeComponent = (
|
|||||||
const ctx = canvas?.getContext("2d")
|
const ctx = canvas?.getContext("2d")
|
||||||
if (!canvas || !ctx) return
|
if (!canvas || !ctx) return
|
||||||
|
|
||||||
const radius = Math.max(1, size * scale)
|
const radius = Math.max(1, size * scale * rasterScale)
|
||||||
const side = radius * 2
|
const side = radius * 2
|
||||||
const left = centerX - radius
|
const left = centerX - radius
|
||||||
const top = centerY - radius
|
const top = centerY - radius
|
||||||
@@ -41,7 +47,7 @@ const AssistShapeComponent = (
|
|||||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
},
|
},
|
||||||
[scale, size]
|
[rasterScale, scale, size]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react"
|
} from "react"
|
||||||
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
import { getDisplayedImageScale } from "../utils/scale"
|
||||||
|
|
||||||
interface CrosshairComponentProps {
|
interface CrosshairComponentProps {
|
||||||
isCarved: boolean
|
isCarved: boolean
|
||||||
@@ -18,7 +22,17 @@ const CrosshairComponent = (
|
|||||||
) => {
|
) => {
|
||||||
const { isCarved } = props
|
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 canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
@@ -82,9 +96,9 @@ const CrosshairComponent = (
|
|||||||
ctx.lineWidth = carveLineWidth
|
ctx.lineWidth = carveLineWidth
|
||||||
|
|
||||||
// 间隔
|
// 间隔
|
||||||
const sparsity = getSparsity(scale)
|
const sparsity = getSparsity(displayedScale)
|
||||||
const part = 10
|
const part = 10
|
||||||
const pixelPerUnit = scale * sparsity
|
const pixelPerUnit = displayedScale * sparsity
|
||||||
const gap = pixelPerUnit / part
|
const gap = pixelPerUnit / part
|
||||||
|
|
||||||
const fixed = getFixed(sparsity)
|
const fixed = getFixed(sparsity)
|
||||||
@@ -136,7 +150,7 @@ const CrosshairComponent = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isCarved, scale]
|
[displayedScale, isCarved]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ import {
|
|||||||
paperShapeToMultiPolygon,
|
paperShapeToMultiPolygon,
|
||||||
} from "../utils/geometry/booleanAdapter"
|
} from "../utils/geometry/booleanAdapter"
|
||||||
import { adjustPoints } from "../utils/paperjs"
|
import { adjustPoints } from "../utils/paperjs"
|
||||||
|
import {
|
||||||
|
getDisplayedImageScale,
|
||||||
|
getViewScaleFromDisplayedScale,
|
||||||
|
} from "../utils/scale"
|
||||||
import AssistShapeComponent from "./AssistShapeComponent"
|
import AssistShapeComponent from "./AssistShapeComponent"
|
||||||
import CrosshairComponent from "./CrosshairComponent"
|
import CrosshairComponent from "./CrosshairComponent"
|
||||||
import { renderOperationIcon } from "./RightObjectTools"
|
import { renderOperationIcon } from "./RightObjectTools"
|
||||||
@@ -2923,6 +2927,80 @@ const PaperContainer = (
|
|||||||
}
|
}
|
||||||
}, [stopMiddleMousePan])
|
}, [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(
|
const getTargetGroupPosition = useCallback(
|
||||||
({
|
({
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
@@ -2958,6 +3036,7 @@ const PaperContainer = (
|
|||||||
clearPendingFrameSwitch(token)
|
clearPendingFrameSwitch(token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const cachedDisplayedScale = getCachedDisplayedScale(currentVisibleGroup)
|
||||||
const preserveGroupPosition =
|
const preserveGroupPosition =
|
||||||
saveCurrentScale &&
|
saveCurrentScale &&
|
||||||
currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0
|
currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0
|
||||||
@@ -3034,6 +3113,11 @@ const PaperContainer = (
|
|||||||
const scaleX = canvasWidth / raster.bounds.width
|
const scaleX = canvasWidth / raster.bounds.width
|
||||||
const scaleY = canvasHeight / raster.bounds.height
|
const scaleY = canvasHeight / raster.bounds.height
|
||||||
const scale = Math.min(scaleX, scaleY)
|
const scale = Math.min(scaleX, scaleY)
|
||||||
|
const targetGroupScale = getTargetGroupScale({
|
||||||
|
rasterScale: scale,
|
||||||
|
cachedDisplayedScale,
|
||||||
|
fallbackScale: currentVisibleGroup.scaling.x,
|
||||||
|
})
|
||||||
|
|
||||||
if (!Number.isFinite(scale) || scale <= 0) {
|
if (!Number.isFinite(scale) || scale <= 0) {
|
||||||
cleanupStagingGroup()
|
cleanupStagingGroup()
|
||||||
@@ -3048,6 +3132,10 @@ const PaperContainer = (
|
|||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
)
|
)
|
||||||
|
stagingGroup.scaling = new paper.Point(
|
||||||
|
targetGroupScale,
|
||||||
|
targetGroupScale
|
||||||
|
)
|
||||||
const centeredGroupPosition = new paper.Point(
|
const centeredGroupPosition = new paper.Point(
|
||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
@@ -3074,6 +3162,7 @@ const PaperContainer = (
|
|||||||
|
|
||||||
const previousGroup = usePaperStore.getState().group
|
const previousGroup = usePaperStore.getState().group
|
||||||
setGroup(stagingGroup)
|
setGroup(stagingGroup)
|
||||||
|
useTopToolsStore.getState().setScale(targetGroupScale)
|
||||||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||||||
renderedImageRef.current = imageName
|
renderedImageRef.current = imageName
|
||||||
commitFrameSwitch(imageName, token)
|
commitFrameSwitch(imageName, token)
|
||||||
@@ -3101,6 +3190,8 @@ const PaperContainer = (
|
|||||||
[
|
[
|
||||||
clearPendingFrameSwitch,
|
clearPendingFrameSwitch,
|
||||||
commitFrameSwitch,
|
commitFrameSwitch,
|
||||||
|
getCachedDisplayedScale,
|
||||||
|
getTargetGroupScale,
|
||||||
getTargetGroupPosition,
|
getTargetGroupPosition,
|
||||||
imgSize?.clientHeight,
|
imgSize?.clientHeight,
|
||||||
imgSize?.clientWidth,
|
imgSize?.clientWidth,
|
||||||
@@ -3166,12 +3257,6 @@ const PaperContainer = (
|
|||||||
)
|
)
|
||||||
decodeFrameImageRef.current = decodeFrameImage
|
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(() => {
|
const cancelRasterFadeTransition = useCallback(() => {
|
||||||
rasterFadeTransitionIdRef.current += 1
|
rasterFadeTransitionIdRef.current += 1
|
||||||
if (
|
if (
|
||||||
@@ -3194,6 +3279,7 @@ const PaperContainer = (
|
|||||||
if (activeImage && projectDetail?.id) {
|
if (activeImage && projectDetail?.id) {
|
||||||
const requestId = ++rasterLoadRequestIdRef.current
|
const requestId = ++rasterLoadRequestIdRef.current
|
||||||
const requestImage = activeImage
|
const requestImage = activeImage
|
||||||
|
const cachedDisplayedScale = getCachedDisplayedScale(paperGroup)
|
||||||
const preserveGroupPosition =
|
const preserveGroupPosition =
|
||||||
saveCurrentScale && getPicRasters(paperGroup).length > 0
|
saveCurrentScale && getPicRasters(paperGroup).length > 0
|
||||||
const preservedGroupPosition = preserveGroupPosition
|
const preservedGroupPosition = preserveGroupPosition
|
||||||
@@ -3275,6 +3361,11 @@ const PaperContainer = (
|
|||||||
])
|
])
|
||||||
|
|
||||||
let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio
|
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) {
|
if (!Number.isFinite(scale) || scale <= 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[PaperContainer] skip raster scale: invalid scale",
|
"[PaperContainer] skip raster scale: invalid scale",
|
||||||
@@ -3357,6 +3448,10 @@ const PaperContainer = (
|
|||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
)
|
)
|
||||||
|
paperGroup.scaling = new paper.Point(
|
||||||
|
targetGroupScale,
|
||||||
|
targetGroupScale
|
||||||
|
)
|
||||||
const centeredGroupPosition = new paper.Point(
|
const centeredGroupPosition = new paper.Point(
|
||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
@@ -3422,6 +3517,7 @@ const PaperContainer = (
|
|||||||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||||||
raster.sendToBack()
|
raster.sendToBack()
|
||||||
renderedImageRef.current = requestImage
|
renderedImageRef.current = requestImage
|
||||||
|
useTopToolsStore.getState().setScale(targetGroupScale)
|
||||||
setRasterInited(true)
|
setRasterInited(true)
|
||||||
usePaperStore.getState().group?.bringToFront()
|
usePaperStore.getState().group?.bringToFront()
|
||||||
}
|
}
|
||||||
@@ -3439,7 +3535,9 @@ const PaperContainer = (
|
|||||||
activeImage,
|
activeImage,
|
||||||
cancelRasterFadeTransition,
|
cancelRasterFadeTransition,
|
||||||
decodeFrameImage,
|
decodeFrameImage,
|
||||||
|
getCachedDisplayedScale,
|
||||||
getPicRasters,
|
getPicRasters,
|
||||||
|
getTargetGroupScale,
|
||||||
getTargetGroupPosition,
|
getTargetGroupPosition,
|
||||||
imgSize?.clientHeight,
|
imgSize?.clientHeight,
|
||||||
imgSize?.clientWidth,
|
imgSize?.clientWidth,
|
||||||
|
|||||||
99
components/label/components/RepairScaleModal.tsx
Normal file
99
components/label/components/RepairScaleModal.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Button, Group, NumberInput, Stack, Text } from "@mantine/core"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import CustomModal from "./CustomModal"
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
open: boolean
|
||||||
|
defaultFactor: number
|
||||||
|
selectedObjectCount: number
|
||||||
|
canRepairImage: boolean
|
||||||
|
handleCancel: () => void
|
||||||
|
handleRepairObject: (factor: number) => void
|
||||||
|
handleRepairImage: (factor: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RepairScaleModal = ({
|
||||||
|
open,
|
||||||
|
defaultFactor,
|
||||||
|
selectedObjectCount,
|
||||||
|
canRepairImage,
|
||||||
|
handleCancel,
|
||||||
|
handleRepairObject,
|
||||||
|
handleRepairImage,
|
||||||
|
}: ComponentProps) => {
|
||||||
|
const [factorInput, setFactorInput] = useState<number | string>(1)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return
|
||||||
|
const nextFactor =
|
||||||
|
Number.isFinite(defaultFactor) && defaultFactor > 0
|
||||||
|
? Number(defaultFactor.toFixed(6))
|
||||||
|
: 1
|
||||||
|
setFactorInput(nextFactor)
|
||||||
|
}, [defaultFactor, open])
|
||||||
|
|
||||||
|
const normalizedFactor = useMemo(() => {
|
||||||
|
if (typeof factorInput === "number") return factorInput
|
||||||
|
return Number(factorInput)
|
||||||
|
}, [factorInput])
|
||||||
|
|
||||||
|
const factorError =
|
||||||
|
Number.isFinite(normalizedFactor) && normalizedFactor > 0
|
||||||
|
? null
|
||||||
|
: "请输入大于 0 的修复因子"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomModal
|
||||||
|
title="修复标注对象"
|
||||||
|
open={open}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
centered
|
||||||
|
closeOnClickOutside={false}
|
||||||
|
footer={
|
||||||
|
<Group justify="flex-end" gap="xs" mt="md">
|
||||||
|
<Button variant="default" onClick={handleCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => handleRepairObject(normalizedFactor)}
|
||||||
|
disabled={!!factorError || selectedObjectCount !== 1}>
|
||||||
|
修复当前对象
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRepairImage(normalizedFactor)}
|
||||||
|
disabled={!!factorError || !canRepairImage}>
|
||||||
|
修复当前图片
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
}>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
该工具会将当前页面里的标注几何点统一除以修复因子,默认值取当前图片比例
|
||||||
|
`rasterScale`。修复后请先检查显示效果,再决定是否保存。
|
||||||
|
</Text>
|
||||||
|
<NumberInput
|
||||||
|
label="修复因子"
|
||||||
|
placeholder="请输入大于 0 的修复因子"
|
||||||
|
value={factorInput}
|
||||||
|
onChange={setFactorInput}
|
||||||
|
allowNegative={false}
|
||||||
|
decimalScale={6}
|
||||||
|
min={0.000001}
|
||||||
|
error={factorError || undefined}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
当前已选对象数:{selectedObjectCount}。修复当前对象需要恰好选中 1
|
||||||
|
个对象。
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
当前图片{canRepairImage ? "已有" : "暂无"}可修复标注数据。
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</CustomModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RepairScaleModal
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
|
HoverCard,
|
||||||
Paper,
|
Paper,
|
||||||
Popover,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { useCallback, useMemo, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
|
import { Comment } from "../api/label/typing"
|
||||||
import { Project } from "../api/project/typing"
|
import { Project } from "../api/project/typing"
|
||||||
import { Task } from "../api/task/typing"
|
import { Task } from "../api/task/typing"
|
||||||
import { LabelState } from "../LabelNossr"
|
import { LabelState } from "../LabelNossr"
|
||||||
@@ -141,6 +142,65 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const getCommentTextColor = useCallback((type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 3:
|
||||||
|
return "yellow.9"
|
||||||
|
case 2:
|
||||||
|
return "red.7"
|
||||||
|
case 1:
|
||||||
|
return "green.7"
|
||||||
|
case 0:
|
||||||
|
return "gray.7"
|
||||||
|
default:
|
||||||
|
return "blue.7"
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getUserLabel = useCallback(
|
||||||
|
(uid?: number) => {
|
||||||
|
if (uid === undefined || uid === null) return "-"
|
||||||
|
return (
|
||||||
|
userOpts.find((item) => item.value === uid)?.label || uid.toString()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[userOpts]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getCommentCount = useCallback((commentItem: Comment) => {
|
||||||
|
const checkBoxComments = (commentItem.check_box_comments || []).filter(
|
||||||
|
(item) => item?.trim()
|
||||||
|
)
|
||||||
|
const textComments = (commentItem.text_comments || []).filter((item) =>
|
||||||
|
item?.trim()
|
||||||
|
)
|
||||||
|
return checkBoxComments.length + textComments.length
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCommentContent = useCallback((commentItem: Comment) => {
|
||||||
|
const mergedComments = [
|
||||||
|
...(commentItem.check_box_comments || []),
|
||||||
|
...(commentItem.text_comments || []),
|
||||||
|
].filter((item) => item?.trim())
|
||||||
|
return mergedComments.length ? mergedComments.join(",") : "无批注"
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCommentDisplayType = useCallback(
|
||||||
|
(commentItem: Comment, lastModifiedTimestamp?: number) => {
|
||||||
|
const type = Math.max(0, Math.min(3, commentItem.comment_type ?? 0))
|
||||||
|
if (
|
||||||
|
type === 2 &&
|
||||||
|
lastModifiedTimestamp &&
|
||||||
|
lastModifiedTimestamp > commentItem.date &&
|
||||||
|
getCommentCount(commentItem) > 0
|
||||||
|
) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
},
|
||||||
|
[getCommentCount]
|
||||||
|
)
|
||||||
|
|
||||||
const { shift: pressShift } = useKeyEventStore()
|
const { shift: pressShift } = useKeyEventStore()
|
||||||
|
|
||||||
const continuePolygonTarget = usePaperStore(
|
const continuePolygonTarget = usePaperStore(
|
||||||
@@ -696,15 +756,10 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
comment.length === 1
|
comment.length === 1
|
||||||
? comment[comment.length - 1]
|
? comment[comment.length - 1]
|
||||||
: comment[comment.length - 2]
|
: comment[comment.length - 2]
|
||||||
let type = latest_comment.comment_type
|
const type = getCommentDisplayType(
|
||||||
if (type === 2) {
|
latest_comment,
|
||||||
const { last_modified_timestamp } = child[2]
|
child[2].last_modified_timestamp
|
||||||
if (
|
)
|
||||||
last_modified_timestamp &&
|
|
||||||
last_modified_timestamp > latest_comment.date
|
|
||||||
)
|
|
||||||
type = 3
|
|
||||||
}
|
|
||||||
visibleFlag = checkBoxsChecked[3 - type]
|
visibleFlag = checkBoxsChecked[3 - type]
|
||||||
}
|
}
|
||||||
if (checkBoxsChecked.every((value) => !value))
|
if (checkBoxsChecked.every((value) => !value))
|
||||||
@@ -954,12 +1009,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">创建:</Text>
|
<Text size="xs">创建:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(child[2]?.uid)}
|
||||||
userOpts.find(
|
|
||||||
(item) =>
|
|
||||||
item.value === child[2]?.uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -972,14 +1022,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">首次修改:</Text>
|
<Text size="xs">首次修改:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(
|
||||||
userOpts.find(
|
child[2]?.first_modified_uid
|
||||||
(item) =>
|
)}
|
||||||
item.value ===
|
|
||||||
child[2]
|
|
||||||
?.first_modified_uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -994,14 +1039,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">最新修改:</Text>
|
<Text size="xs">最新修改:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(
|
||||||
userOpts.find(
|
child[2]?.last_modified_uid
|
||||||
(item) =>
|
)}
|
||||||
item.value ===
|
|
||||||
child[2]
|
|
||||||
?.last_modified_uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -1027,33 +1067,28 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
</Group>
|
</Group>
|
||||||
{comment ? (
|
{comment ? (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{comment.map((c: any) => {
|
{comment.map((c: Comment) => {
|
||||||
const count =
|
const count = getCommentCount(c)
|
||||||
c.check_box_comments.length +
|
const commentType =
|
||||||
c.text_comments.length
|
getCommentDisplayType(
|
||||||
const { last_modified_timestamp } =
|
c,
|
||||||
child[2]
|
child[2]?.last_modified_timestamp
|
||||||
let flag = false
|
)
|
||||||
if (
|
const badgeColor =
|
||||||
count > 0 &&
|
getCheckBoxColor(commentType)
|
||||||
last_modified_timestamp &&
|
const commentTextColor =
|
||||||
last_modified_timestamp > c.date
|
getCommentTextColor(commentType)
|
||||||
)
|
|
||||||
flag = true
|
|
||||||
|
|
||||||
let badgeColor = "gray"
|
|
||||||
if (c.comment_type === 1)
|
|
||||||
badgeColor = "green"
|
|
||||||
if (c.comment_type === 2)
|
|
||||||
badgeColor = "red"
|
|
||||||
if (flag) badgeColor = "yellow"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<HoverCard
|
||||||
key={`${child[0]}${c.turn}${c.times}`}
|
key={`${child[0]}${c.turn}${c.times}`}
|
||||||
position="left"
|
position="left"
|
||||||
withArrow>
|
withArrow
|
||||||
<Popover.Target>
|
shadow="md"
|
||||||
|
openDelay={100}
|
||||||
|
closeDelay={100}
|
||||||
|
withinPortal>
|
||||||
|
<HoverCard.Target>
|
||||||
<Badge
|
<Badge
|
||||||
size="xs"
|
size="xs"
|
||||||
circle
|
circle
|
||||||
@@ -1063,16 +1098,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}}>
|
}}>
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Popover.Target>
|
</HoverCard.Target>
|
||||||
<Popover.Dropdown>
|
<HoverCard.Dropdown>
|
||||||
<Text size="xs" c={badgeColor}>
|
<Text
|
||||||
{`P${child[2].image_id}:${
|
size="xs"
|
||||||
child[2].uid
|
c={commentTextColor}>
|
||||||
}:${c.check_box_comments.join(
|
{`P${child[2].image_id}:${getUserLabel(
|
||||||
","
|
c.uid
|
||||||
)},${c.text_comments.join(
|
)}:${getCommentContent(c)} ${
|
||||||
","
|
|
||||||
)} ${
|
|
||||||
c.date
|
c.date
|
||||||
? dayjs(
|
? dayjs(
|
||||||
c.date * 1000
|
c.date * 1000
|
||||||
@@ -1082,8 +1115,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Popover.Dropdown>
|
</HoverCard.Dropdown>
|
||||||
</Popover>
|
</HoverCard>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import React, { useCallback, useEffect, useRef } from "react"
|
|||||||
|
|
||||||
interface ScaleComponentProps {
|
interface ScaleComponentProps {
|
||||||
options: {
|
options: {
|
||||||
offsetX: number
|
|
||||||
offsetY: number
|
|
||||||
scale: number
|
scale: number
|
||||||
}
|
}
|
||||||
mode: "horizontal" | "vertical"
|
mode: "horizontal" | "vertical"
|
||||||
@@ -14,6 +12,7 @@ interface ScaleComponentProps {
|
|||||||
const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
||||||
const { options, mode } = props
|
const { options, mode } = props
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const FLOAT_EPSILON = 0.0000001
|
||||||
|
|
||||||
const getFixed = (sparsity: number) => {
|
const getFixed = (sparsity: number) => {
|
||||||
const pointIdx = String(sparsity).indexOf(".")
|
const pointIdx = String(sparsity).indexOf(".")
|
||||||
@@ -22,8 +21,13 @@ const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isCloseToInteger = (num: number) => {
|
const isCloseToInteger = (num: number) => {
|
||||||
return Math.abs(num - Math.round(num)) < 0.0000001
|
return Math.abs(num - Math.round(num)) < FLOAT_EPSILON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeScaleValue = (num: number) => {
|
||||||
|
return Math.abs(num) < FLOAT_EPSILON ? 0 : num
|
||||||
|
}
|
||||||
|
|
||||||
const getSparsity = (scale: number) => {
|
const getSparsity = (scale: number) => {
|
||||||
if (scale <= 1) {
|
if (scale <= 1) {
|
||||||
return Math.round(100 / scale)
|
return Math.round(100 / scale)
|
||||||
@@ -63,20 +67,30 @@ const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
|||||||
|
|
||||||
ctx.font = "12px serif"
|
ctx.font = "12px serif"
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
const { offsetX, offsetY, scale } = options
|
const { scale } = options
|
||||||
const offset = mode === "horizontal" ? offsetX : offsetY
|
if (!Number.isFinite(scale) || scale <= 0) {
|
||||||
|
ctx.restore()
|
||||||
|
return
|
||||||
|
}
|
||||||
// 间隔
|
// 间隔
|
||||||
const sparsity = getSparsity(scale)
|
const sparsity = getSparsity(scale)
|
||||||
// 间隔内有多少条线
|
// 间隔内有多少条线
|
||||||
const part = 10
|
const part = 10
|
||||||
const pixelPerUnit = scale * sparsity
|
const pixelPerUnit = scale * sparsity
|
||||||
const gap = pixelPerUnit / part
|
const gap = pixelPerUnit / part
|
||||||
|
const unitGap = sparsity / part
|
||||||
const fixed = getFixed(sparsity)
|
const fixed = getFixed(sparsity)
|
||||||
let index = offset % gap > 0 ? gap - (offset % gap) : -offset % gap
|
|
||||||
if (mode === "horizontal") {
|
if (mode === "horizontal") {
|
||||||
ctx.translate(31.5, 0)
|
ctx.translate(31.5, 0)
|
||||||
do {
|
const maxWidth = Math.max(0, w - 32)
|
||||||
const num = ((offset + index) / pixelPerUnit) * sparsity
|
for (
|
||||||
|
let tickIndex = 0;
|
||||||
|
tickIndex * gap <= maxWidth + FLOAT_EPSILON;
|
||||||
|
tickIndex += 1
|
||||||
|
) {
|
||||||
|
const index = tickIndex * gap
|
||||||
|
const num = normalizeScaleValue(tickIndex * unitGap)
|
||||||
if (isCloseToInteger(num / sparsity)) {
|
if (isCloseToInteger(num / sparsity)) {
|
||||||
ctx.moveTo(index, h * 0.5)
|
ctx.moveTo(index, h * 0.5)
|
||||||
ctx.lineTo(index, h)
|
ctx.lineTo(index, h)
|
||||||
@@ -87,12 +101,16 @@ const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
|||||||
ctx.moveTo(index, h * 0.7)
|
ctx.moveTo(index, h * 0.7)
|
||||||
ctx.lineTo(index, h)
|
ctx.lineTo(index, h)
|
||||||
}
|
}
|
||||||
index += gap
|
}
|
||||||
} while (index < w)
|
|
||||||
} else {
|
} else {
|
||||||
ctx.translate(0, 0.5)
|
ctx.translate(0, 0.5)
|
||||||
do {
|
for (
|
||||||
const num = ((offset + index) / pixelPerUnit) * sparsity
|
let tickIndex = 0;
|
||||||
|
tickIndex * gap <= h + FLOAT_EPSILON;
|
||||||
|
tickIndex += 1
|
||||||
|
) {
|
||||||
|
const index = tickIndex * gap
|
||||||
|
const num = normalizeScaleValue(tickIndex * unitGap)
|
||||||
if (isCloseToInteger(num / sparsity)) {
|
if (isCloseToInteger(num / sparsity)) {
|
||||||
ctx.moveTo(w * 0.5, index)
|
ctx.moveTo(w * 0.5, index)
|
||||||
ctx.lineTo(w, index)
|
ctx.lineTo(w, index)
|
||||||
@@ -110,8 +128,7 @@ const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
|||||||
ctx.moveTo(w * 0.7, index)
|
ctx.moveTo(w * 0.7, index)
|
||||||
ctx.lineTo(w, index)
|
ctx.lineTo(w, index)
|
||||||
}
|
}
|
||||||
index += gap
|
}
|
||||||
} while (index < h)
|
|
||||||
}
|
}
|
||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
@@ -129,7 +146,7 @@ const ScaleComponent: React.FC<ScaleComponentProps> = (props) => {
|
|||||||
if (mode === "horizontal") {
|
if (mode === "horizontal") {
|
||||||
return {
|
return {
|
||||||
...baseStyles,
|
...baseStyles,
|
||||||
width: "100vw",
|
width: "100%",
|
||||||
height: "32px",
|
height: "32px",
|
||||||
cursor: "col-resize",
|
cursor: "col-resize",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,75 @@
|
|||||||
import { ActionIcon, Flex, NumberInput, Stack, Text } from "@mantine/core"
|
import { ActionIcon, Flex, NumberInput, Stack, Text } from "@mantine/core"
|
||||||
import { IconMinus, IconPlus, IconRefresh } from "@tabler/icons-react"
|
import { IconMinus, IconPlus, IconRefresh } from "@tabler/icons-react"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { useMemo } from "react"
|
import { useEffect, useMemo, useRef } from "react"
|
||||||
import { useKeyEventStore } from "../store"
|
import { useKeyEventStore } from "../store"
|
||||||
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
import { usePaperStore } from "../usePaperStore"
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
import {
|
||||||
|
getDisplayedImageScale,
|
||||||
|
getViewScaleFromDisplayedScale,
|
||||||
|
} from "../utils/scale"
|
||||||
|
|
||||||
const ScaleToolContainer = () => {
|
const ScaleToolContainer = () => {
|
||||||
const { scale, setScale } = useTopToolsStore()
|
const {
|
||||||
|
scale,
|
||||||
|
setScale,
|
||||||
|
displayedScale: savedDisplayedScale,
|
||||||
|
setDisplayedScale,
|
||||||
|
} = useTopToolsStore()
|
||||||
const { setFocusInput } = useKeyEventStore()
|
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(() => {
|
const displayNumber = useMemo(() => {
|
||||||
return Number((scale * 100).toFixed(0))
|
return Number((displayedScale * 100).toFixed(0))
|
||||||
}, [scale])
|
}, [displayedScale])
|
||||||
|
|
||||||
const setCurrentScale = (n: number) => {
|
const setCurrentScale = (n: number) => {
|
||||||
const currentScale = n / 100
|
const currentScale = getViewScaleFromDisplayedScale({
|
||||||
|
displayedScale: n / 100,
|
||||||
|
rasterScale,
|
||||||
|
})
|
||||||
const group = usePaperStore.getState().group
|
const group = usePaperStore.getState().group
|
||||||
if (!group) return
|
if (!group) return
|
||||||
group.scaling = new paper.Point(currentScale, currentScale)
|
group.scaling = new paper.Point(currentScale, currentScale)
|
||||||
setScale(currentScale)
|
setScale(currentScale)
|
||||||
|
setDisplayedScale(n / 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -31,9 +81,37 @@ const ScaleToolContainer = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!rasterList || !rasterList.length) return
|
if (!rasterList || !rasterList.length) return
|
||||||
const currentRaster = rasterList[0]
|
const currentRaster = rasterList.find(
|
||||||
group.position = currentRaster.position
|
(item): item is paper.Raster => item instanceof paper.Raster
|
||||||
setCurrentScale(100)
|
)
|
||||||
|
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) => {
|
const handleEnter = (e: any) => {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
SquarePen,
|
SquarePen,
|
||||||
Tag,
|
Tag,
|
||||||
Timer,
|
Timer,
|
||||||
|
Wrench,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import {
|
import {
|
||||||
@@ -102,12 +103,13 @@ import {
|
|||||||
import { buildSubAttributeFormValues, getSubAttrStatus } from "../util"
|
import { buildSubAttributeFormValues, getSubAttrStatus } from "../util"
|
||||||
import { safeClone } from "../utils/clone"
|
import { safeClone } from "../utils/clone"
|
||||||
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
||||||
import { adjustAllPoints } from "../utils/paperjs"
|
import { adjustAllPoints, adjustPoints } from "../utils/paperjs"
|
||||||
import BackConfirmModal from "./BackConfirmModal"
|
import BackConfirmModal from "./BackConfirmModal"
|
||||||
import BackupModal from "./BackupModal"
|
import BackupModal from "./BackupModal"
|
||||||
import ConfirmModal from "./ConfirmModal"
|
import ConfirmModal from "./ConfirmModal"
|
||||||
import { splitWord } from "./EditorContainer"
|
import { splitWord } from "./EditorContainer"
|
||||||
import RecoverModal from "./RecoverModal"
|
import RecoverModal from "./RecoverModal"
|
||||||
|
import RepairScaleModal from "./RepairScaleModal"
|
||||||
import { renderOperationIcon } from "./RightObjectTools"
|
import { renderOperationIcon } from "./RightObjectTools"
|
||||||
import TaskCacheModal from "./TaskCacheModal"
|
import TaskCacheModal from "./TaskCacheModal"
|
||||||
import TaskStatusTag from "./TaskStatusTag"
|
import TaskStatusTag from "./TaskStatusTag"
|
||||||
@@ -143,6 +145,8 @@ const initialWorkLoad: WorkLoad = {
|
|||||||
question_size: {},
|
question_size: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_SELECTED_OBJECT_IDS: number[] = []
|
||||||
|
|
||||||
const TaskTimerDisplay = ({
|
const TaskTimerDisplay = ({
|
||||||
currentTimeKey,
|
currentTimeKey,
|
||||||
}: {
|
}: {
|
||||||
@@ -179,15 +183,25 @@ const TopTools = (
|
|||||||
const [duplicateConfirmOpen, setDuplicateConfirmOpen] = useState(false)
|
const [duplicateConfirmOpen, setDuplicateConfirmOpen] = useState(false)
|
||||||
const [duplicateName, setDuplicateName] = useState("")
|
const [duplicateName, setDuplicateName] = useState("")
|
||||||
const [recoverOpen, setRecoverOpen] = useState(false)
|
const [recoverOpen, setRecoverOpen] = useState(false)
|
||||||
|
const [repairOpen, setRepairOpen] = useState(false)
|
||||||
const [taskOpen, setTaskOpen] = useState(false)
|
const [taskOpen, setTaskOpen] = useState(false)
|
||||||
const [submitConfirmOpen, setSubmitConfirmOpen] = useState(false)
|
const [submitConfirmOpen, setSubmitConfirmOpen] = useState(false)
|
||||||
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
|
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
|
||||||
const [operationKeyword, setOperationKeyword] = useState("")
|
const [operationKeyword, setOperationKeyword] = useState("")
|
||||||
const labelData = useLabelStore((state) => state.label)
|
const labelData = useLabelStore((state) => state.label)
|
||||||
|
const labelDefaultComments = useLabelStore(
|
||||||
|
(state) => state.labelDefaultComments
|
||||||
|
)
|
||||||
const setLabel = useLabelStore((state) => state.setLabel)
|
const setLabel = useLabelStore((state) => state.setLabel)
|
||||||
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
||||||
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
|
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
|
||||||
const { activeImage } = useBottomToolsStore()
|
const { activeImage } = useBottomToolsStore()
|
||||||
|
const selectedPathMap = useObjectStore((state) => state.selectedPath)
|
||||||
|
const currentRasterScale = usePaperStore(
|
||||||
|
(state) => state.rasterScale[activeImage] ?? 1
|
||||||
|
)
|
||||||
|
const currentSelectedObjectIds =
|
||||||
|
selectedPathMap[activeImage] ?? EMPTY_SELECTED_OBJECT_IDS
|
||||||
const {
|
const {
|
||||||
isView,
|
isView,
|
||||||
setIsView,
|
setIsView,
|
||||||
@@ -246,6 +260,15 @@ const TopTools = (
|
|||||||
useState<number | string>(auxiliarySizeThreshold)
|
useState<number | string>(auxiliarySizeThreshold)
|
||||||
|
|
||||||
const { pathGroupMap } = useRightToolsStore()
|
const { pathGroupMap } = useRightToolsStore()
|
||||||
|
const activeImageLabelData = useMemo(
|
||||||
|
() => labelData.get(activeImage),
|
||||||
|
[activeImage, labelData]
|
||||||
|
)
|
||||||
|
const repairFactorDefault = useMemo(() => {
|
||||||
|
return Number.isFinite(currentRasterScale) && currentRasterScale > 0
|
||||||
|
? Number(currentRasterScale.toFixed(6))
|
||||||
|
: 1
|
||||||
|
}, [currentRasterScale])
|
||||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||||
const setIsAutoSave = useIntervalStore((state) => state.setIsAutoSave)
|
const setIsAutoSave = useIntervalStore((state) => state.setIsAutoSave)
|
||||||
const autoSaveGap = useIntervalStore((state) => state.autoSaveGap)
|
const autoSaveGap = useIntervalStore((state) => state.autoSaveGap)
|
||||||
@@ -950,6 +973,36 @@ const TopTools = (
|
|||||||
loadingData,
|
loadingData,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const leaveCurrentPage = useCallback((url: string) => {
|
||||||
|
// `beforeunload` may still be cancelled by the browser/user, so do not
|
||||||
|
// mutate persisted label state after requesting a full-page navigation.
|
||||||
|
window.location.href = url
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const rerenderActiveImageAnnotations = useCallback(
|
||||||
|
(
|
||||||
|
nextLabelData: Map<string, Map<string, [number, any[], any, any[]][]>>
|
||||||
|
) => {
|
||||||
|
if (!renderPolygons || !activeImage) return
|
||||||
|
|
||||||
|
usePaperStore
|
||||||
|
.getState()
|
||||||
|
.group?.getItems({})
|
||||||
|
.filter((item: any) => item.data?.id)
|
||||||
|
.forEach((item: any) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentImageMap = nextLabelData.get(activeImage)
|
||||||
|
if (!currentImageMap) return
|
||||||
|
|
||||||
|
for (const key of currentImageMap.keys()) {
|
||||||
|
renderPolygons(key, currentImageMap)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeImage, renderPolygons]
|
||||||
|
)
|
||||||
|
|
||||||
const handleBackup = useCallback(
|
const handleBackup = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
let newTaskLabelData = new Map()
|
let newTaskLabelData = new Map()
|
||||||
@@ -1035,10 +1088,41 @@ const TopTools = (
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const finalData = adjustAllPoints(
|
const recoveredData = adjustAllPoints(
|
||||||
backupData,
|
backupData,
|
||||||
usePaperStore.getState().rasterScale
|
usePaperStore.getState().rasterScale
|
||||||
)
|
)
|
||||||
|
const finalData = safeClone(recoveredData)
|
||||||
|
for (const [imageId, categoryMap] of finalData.entries()) {
|
||||||
|
const currentDetailById = new Map<number, any>()
|
||||||
|
labelData.get(imageId)?.forEach((objects) => {
|
||||||
|
objects.forEach(([id, _points, detail]) => {
|
||||||
|
currentDetailById.set(id, detail)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
for (const [operationId, objects] of categoryMap.entries()) {
|
||||||
|
categoryMap.set(
|
||||||
|
operationId,
|
||||||
|
objects.map(([id, points, detail, hollowPoints]) => {
|
||||||
|
const currentDetail = currentDetailById.get(id)
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
...detail,
|
||||||
|
comment: currentDetail?.comment
|
||||||
|
? safeClone(currentDetail.comment)
|
||||||
|
: safeClone(labelDefaultComments),
|
||||||
|
last_modified_timestamp:
|
||||||
|
currentDetail?.last_modified_timestamp ??
|
||||||
|
detail.last_modified_timestamp,
|
||||||
|
},
|
||||||
|
hollowPoints,
|
||||||
|
]
|
||||||
|
}) as any
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const entry of finalData.entries()) {
|
for (const entry of finalData.entries()) {
|
||||||
// 赋值用过的id
|
// 赋值用过的id
|
||||||
for (const category of entry[1].values()) {
|
for (const category of entry[1].values()) {
|
||||||
@@ -1099,6 +1183,8 @@ const TopTools = (
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeImage,
|
activeImage,
|
||||||
|
labelData,
|
||||||
|
labelDefaultComments,
|
||||||
projectDetail?.label_type,
|
projectDetail?.label_type,
|
||||||
pushStateStack,
|
pushStateStack,
|
||||||
renderPolygons,
|
renderPolygons,
|
||||||
@@ -1107,6 +1193,102 @@ const TopTools = (
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleRepairByFactor = useCallback(
|
||||||
|
(scope: "object" | "image", factor: number) => {
|
||||||
|
if (!activeImage) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前图片未准备完成,暂时无法修复",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFactor = Number(factor)
|
||||||
|
if (!Number.isFinite(normalizedFactor) || normalizedFactor <= 0) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请输入大于 0 的修复因子",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentImageData = labelData.get(activeImage)
|
||||||
|
if (!currentImageData?.size) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前图片暂无可修复标注数据",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleRatio = 1 / normalizedFactor
|
||||||
|
let nextLabelData = adjustPoints(activeImage, labelData, scaleRatio)
|
||||||
|
|
||||||
|
if (scope === "object") {
|
||||||
|
const selectedIds =
|
||||||
|
useObjectStore.getState().selectedPath[activeImage] || []
|
||||||
|
if (selectedIds.length !== 1) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中 1 个标注对象后再修复",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaledImageData = nextLabelData.get(activeImage)
|
||||||
|
if (!scaledImageData) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前对象修复失败,请重试",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSet = new Set(selectedIds)
|
||||||
|
const mergedImageData = new Map<string, [number, any[], any, any[]][]>()
|
||||||
|
|
||||||
|
currentImageData.forEach((objects, operationId) => {
|
||||||
|
const scaledObjects = scaledImageData.get(operationId) || objects
|
||||||
|
const scaledObjectById = new Map(
|
||||||
|
scaledObjects.map((item) => [item[0], item] as const)
|
||||||
|
)
|
||||||
|
|
||||||
|
mergedImageData.set(
|
||||||
|
operationId,
|
||||||
|
objects.map((item) => {
|
||||||
|
if (!selectedSet.has(item[0])) return item
|
||||||
|
return scaledObjectById.get(item[0]) || item
|
||||||
|
}) as [number, any[], any, any[]][]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
nextLabelData = safeClone(labelData)
|
||||||
|
nextLabelData.set(activeImage, mergedImageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalData = safeClone(nextLabelData)
|
||||||
|
setLabel(finalData)
|
||||||
|
pushStateStack(finalData)
|
||||||
|
rerenderActiveImageAnnotations(finalData)
|
||||||
|
setRepairOpen(false)
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
color: "green",
|
||||||
|
message:
|
||||||
|
scope === "object"
|
||||||
|
? "已按修复因子更新当前对象,请检查后再保存"
|
||||||
|
: "已按修复因子更新当前图片,请检查后再保存",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
activeImage,
|
||||||
|
labelData,
|
||||||
|
pushStateStack,
|
||||||
|
rerenderActiveImageAnnotations,
|
||||||
|
setLabel,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (needBackup) {
|
if (needBackup) {
|
||||||
handleBackup("auto_backup")
|
handleBackup("auto_backup")
|
||||||
@@ -1154,10 +1336,7 @@ const TopTools = (
|
|||||||
// 无id返回时跳转回任务列表页
|
// 无id返回时跳转回任务列表页
|
||||||
const url = backUrl ? basePath + backUrl : "/"
|
const url = backUrl ? basePath + backUrl : "/"
|
||||||
// router.push(url)
|
// router.push(url)
|
||||||
window.location.href = url
|
leaveCurrentPage(url)
|
||||||
// 重置图片比例及选中标注对象
|
|
||||||
usePaperStore.getState().resetRasterScale()
|
|
||||||
useObjectStore.getState().resetPathAndOperationStatus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1815,11 +1994,8 @@ const TopTools = (
|
|||||||
if (isView) {
|
if (isView) {
|
||||||
const url = backUrl ? basePath + backUrl : "/"
|
const url = backUrl ? basePath + backUrl : "/"
|
||||||
// router.push(url)
|
// router.push(url)
|
||||||
window.location.href = url
|
leaveCurrentPage(url)
|
||||||
// handleBackup("auto_backup");
|
// handleBackup("auto_backup");
|
||||||
// 重置图片比例及选中标注对象
|
|
||||||
usePaperStore.getState().resetRasterScale()
|
|
||||||
useObjectStore.getState().resetPathAndOperationStatus()
|
|
||||||
} else {
|
} else {
|
||||||
setConfirmOpen(true)
|
setConfirmOpen(true)
|
||||||
}
|
}
|
||||||
@@ -2505,6 +2681,21 @@ const TopTools = (
|
|||||||
<CloudCog style={{ width: 20, height: 20 }} />
|
<CloudCog style={{ width: 20, height: 20 }} />
|
||||||
<Text size="xs">恢复</Text>
|
<Text size="xs">恢复</Text>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
display={
|
||||||
|
process.env.NEXT_PUBLIC_ENV === "development"
|
||||||
|
? "block"
|
||||||
|
: "none"
|
||||||
|
}
|
||||||
|
variant="transparent"
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
style={{ width: "auto", display: "flex", gap: 4 }}
|
||||||
|
onClick={() => {
|
||||||
|
setRepairOpen(true)
|
||||||
|
}}>
|
||||||
|
<Wrench style={{ width: 20, height: 20 }} />
|
||||||
|
<Text size="xs">修复</Text>
|
||||||
|
</ActionIcon>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -2532,6 +2723,19 @@ const TopTools = (
|
|||||||
<CloudCog style={{ width: 20, height: 20 }} />
|
<CloudCog style={{ width: 20, height: 20 }} />
|
||||||
<Text size="xs">恢复</Text>
|
<Text size="xs">恢复</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
display={
|
||||||
|
process.env.NEXT_PUBLIC_ENV === "development"
|
||||||
|
? "block"
|
||||||
|
: "none"
|
||||||
|
}
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
gap={4}
|
||||||
|
style={{ opacity: 0.5, cursor: "not-allowed" }}>
|
||||||
|
<Wrench style={{ width: 20, height: 20 }} />
|
||||||
|
<Text size="xs">修复</Text>
|
||||||
|
</Flex>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -2622,21 +2826,15 @@ const TopTools = (
|
|||||||
setConfirmOpen(false)
|
setConfirmOpen(false)
|
||||||
const url = backUrl ? basePath + backUrl : "/"
|
const url = backUrl ? basePath + backUrl : "/"
|
||||||
// router.push(url)
|
// router.push(url)
|
||||||
window.location.href = url
|
leaveCurrentPage(url)
|
||||||
// 重置图片比例及选中标注对象
|
|
||||||
usePaperStore.getState().resetRasterScale()
|
|
||||||
useObjectStore.getState().resetPathAndOperationStatus()
|
|
||||||
}}
|
}}
|
||||||
handleCancel={async () => {
|
handleCancel={async () => {
|
||||||
try {
|
try {
|
||||||
setConfirmOpen(false)
|
setConfirmOpen(false)
|
||||||
handleBackup("auto_backup")
|
await handleBackup("auto_backup")
|
||||||
const url = backUrl ? basePath + backUrl : "/"
|
const url = backUrl ? basePath + backUrl : "/"
|
||||||
// router.push(url)
|
// router.push(url)
|
||||||
window.location.href = url
|
leaveCurrentPage(url)
|
||||||
// 重置图片比例及选中标注对象
|
|
||||||
usePaperStore.getState().resetRasterScale()
|
|
||||||
useObjectStore.getState().resetPathAndOperationStatus()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
@@ -2739,6 +2937,23 @@ const TopTools = (
|
|||||||
handleOk={handleRecover}
|
handleOk={handleRecover}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{repairOpen && (
|
||||||
|
<RepairScaleModal
|
||||||
|
open={repairOpen}
|
||||||
|
defaultFactor={repairFactorDefault}
|
||||||
|
selectedObjectCount={currentSelectedObjectIds.length}
|
||||||
|
canRepairImage={!!activeImageLabelData?.size}
|
||||||
|
handleCancel={() => {
|
||||||
|
setRepairOpen(false)
|
||||||
|
}}
|
||||||
|
handleRepairObject={(factor) => {
|
||||||
|
handleRepairByFactor("object", factor)
|
||||||
|
}}
|
||||||
|
handleRepairImage={(factor) => {
|
||||||
|
handleRepairByFactor("image", factor)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{taskOpen && (
|
{taskOpen && (
|
||||||
<TaskCacheModal
|
<TaskCacheModal
|
||||||
open={taskOpen}
|
open={taskOpen}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ interface TopToolsState {
|
|||||||
setSaveCurrentScale: (val: boolean) => void
|
setSaveCurrentScale: (val: boolean) => void
|
||||||
scale: number
|
scale: number
|
||||||
setScale: (val: number) => void
|
setScale: (val: number) => void
|
||||||
|
displayedScale: number
|
||||||
|
setDisplayedScale: (val: number) => void
|
||||||
activeOperation: string
|
activeOperation: string
|
||||||
setActiveOperation: (val: string) => void
|
setActiveOperation: (val: string) => void
|
||||||
showObjectList: boolean
|
showObjectList: boolean
|
||||||
@@ -114,6 +116,7 @@ const initialTopToolsState = {
|
|||||||
drawOption: "default" as "default" | "intersect" | "unite",
|
drawOption: "default" as "default" | "intersect" | "unite",
|
||||||
saveCurrentScale: false,
|
saveCurrentScale: false,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
displayedScale: 1,
|
||||||
activeOperation: "",
|
activeOperation: "",
|
||||||
showObjectList: false,
|
showObjectList: false,
|
||||||
showGroupList: false,
|
showGroupList: false,
|
||||||
@@ -248,6 +251,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
...state,
|
...state,
|
||||||
scale: val,
|
scale: val,
|
||||||
})),
|
})),
|
||||||
|
displayedScale: initialTopToolsState.displayedScale,
|
||||||
|
setDisplayedScale: (val: number) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
displayedScale: Number.isFinite(val) && val > 0 ? val : 1,
|
||||||
|
})),
|
||||||
activeOperation: initialTopToolsState.activeOperation,
|
activeOperation: initialTopToolsState.activeOperation,
|
||||||
setActiveOperation: (val: string) =>
|
setActiveOperation: (val: string) =>
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
|
|||||||
23
components/label/utils/scale.ts
Normal file
23
components/label/utils/scale.ts
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user