fix(label): fix save bug
This commit is contained in:
@@ -2434,10 +2434,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状态
|
||||||
|
|||||||
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
|
||||||
@@ -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,6 +183,7 @@ 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)
|
||||||
@@ -191,6 +196,12 @@ const TopTools = (
|
|||||||
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,
|
||||||
@@ -249,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)
|
||||||
@@ -953,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()
|
||||||
@@ -1143,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")
|
||||||
@@ -1190,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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1851,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)
|
||||||
}
|
}
|
||||||
@@ -2541,6 +2681,16 @@ 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
|
||||||
|
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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -2568,6 +2718,14 @@ 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
|
||||||
|
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>
|
||||||
@@ -2658,21 +2816,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)
|
||||||
}
|
}
|
||||||
@@ -2775,6 +2927,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}
|
||||||
|
|||||||
Reference in New Issue
Block a user