4134 lines
131 KiB
TypeScript
4134 lines
131 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Checkbox,
|
||
Group,
|
||
Modal,
|
||
Radio,
|
||
ScrollArea,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
Textarea,
|
||
TextInput,
|
||
} from "@mantine/core"
|
||
import { useForm } from "@mantine/form"
|
||
import { notifications } from "@mantine/notifications"
|
||
import dayjs from "dayjs"
|
||
// import msgpack from "msgpack-lite"
|
||
import paper from "paper"
|
||
import React, {
|
||
DispatchWithoutAction,
|
||
forwardRef,
|
||
useCallback,
|
||
useEffect,
|
||
useImperativeHandle,
|
||
useLayoutEffect,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from "react"
|
||
import { getAuxiliaryAnnotation, getServerImage } from "../api/label"
|
||
import { Project } from "../api/project/typing"
|
||
import { labelimagePerformanceConfig } from "../config/performance"
|
||
import { LabelState } from "../LabelNossr"
|
||
import {
|
||
initialDetail,
|
||
useImagesStore,
|
||
useKeyEventStore,
|
||
useLabelStore,
|
||
useObjectStore,
|
||
} from "../store"
|
||
import { usePermissionStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useKeyboardStore } from "../useKeyBoardStore"
|
||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||
import { renderGroupPath } from "../useRenderGroupPath"
|
||
import useRenderTag from "../useRenderTag"
|
||
import { useRightToolsStore } from "../useRightToolsStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import { checkCommentsIsSame } from "../util"
|
||
import { safeClone } from "../utils/clone"
|
||
import { labelTypeMap } from "../utils/constants"
|
||
import { adjustPoints } from "../utils/paperjs"
|
||
import AssistShapeComponent from "./AssistShapeComponent"
|
||
import CrosshairComponent from "./CrosshairComponent"
|
||
import { renderOperationIcon } from "./RightObjectTools"
|
||
|
||
interface PaperComponentProps {
|
||
imgSrc: string
|
||
forceUpdate: DispatchWithoutAction
|
||
projectDetail?: Project.DetailResponse
|
||
labelState: LabelState
|
||
updateLoadingFlag: (flag: boolean) => void
|
||
}
|
||
|
||
const renderPath = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path(
|
||
Object.assign({ parent: paperGroup, closed: true }, option)
|
||
)
|
||
}
|
||
|
||
const renderCompoundPath = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.CompoundPath(
|
||
Object.assign({ parent: paperGroup, closed: true }, option)
|
||
)
|
||
}
|
||
|
||
const renderRectangle = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path.Rectangle(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
const renderCircle = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path.Circle(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
const renderRaster = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Raster(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
type NormalizedPoint = [number, number]
|
||
type LabelPathItem = [number, any[], any, any[]]
|
||
type ImageLabelData = Map<string, LabelPathItem[]>
|
||
type SupportBox = [number, number, number, number]
|
||
type ContextMenuLayout = {
|
||
top: number
|
||
left: number
|
||
maxHeight: number
|
||
maxWidth: number
|
||
}
|
||
|
||
type ResizeGhostState = {
|
||
id: number
|
||
src: string
|
||
fromWidth: number
|
||
fromHeight: number
|
||
scale: number
|
||
fadeOnly: boolean
|
||
animating: boolean
|
||
}
|
||
|
||
type FrameSwitchGhostState = {
|
||
id: number
|
||
src: string
|
||
width: number
|
||
height: number
|
||
animating: boolean
|
||
}
|
||
|
||
const clonePaperPoint = (point: paper.Point | null | undefined) => {
|
||
if (!point) return null
|
||
return new paper.Point(point.x, point.y)
|
||
}
|
||
|
||
const normalizePoint = (value: any): NormalizedPoint | null => {
|
||
if (value instanceof paper.Point) return [value.x, value.y]
|
||
if (value instanceof paper.Segment) {
|
||
return [value.point.x, value.point.y]
|
||
}
|
||
if (Array.isArray(value)) {
|
||
if (
|
||
value.length >= 2 &&
|
||
typeof value[0] === "number" &&
|
||
typeof value[1] === "number"
|
||
) {
|
||
return [value[0], value[1]]
|
||
}
|
||
if (value.length && Array.isArray(value[0])) {
|
||
return normalizePoint(value[0])
|
||
}
|
||
}
|
||
if (value && typeof value.x === "number" && typeof value.y === "number") {
|
||
return [value.x, value.y]
|
||
}
|
||
if (value?.point) {
|
||
return normalizePoint(value.point)
|
||
}
|
||
return null
|
||
}
|
||
|
||
const normalizeContour = (contour: any[] = []): NormalizedPoint[] => {
|
||
return contour
|
||
.map((point) => normalizePoint(point))
|
||
.filter((point): point is NormalizedPoint => point !== null)
|
||
}
|
||
|
||
const FRAME_PREFETCH_RADIUS = 2
|
||
const FRAME_CACHE_KEEP_RADIUS = 6
|
||
const POLYGON_RENDER_BATCH_SIZE =
|
||
labelimagePerformanceConfig.polygonRenderBatchSize
|
||
const POLYGON_RENDER_BATCH_BUDGET_MS =
|
||
labelimagePerformanceConfig.polygonRenderBatchBudgetMs
|
||
const FRAME_SWITCH_LOADING_DELAY_MS =
|
||
labelimagePerformanceConfig.frameSwitchLoadingDelayMs
|
||
const RASTER_CROSSFADE_DURATION_MS =
|
||
labelimagePerformanceConfig.rasterCrossfadeDurationMs
|
||
const RESIZE_COMMIT_DEBOUNCE_MS =
|
||
labelimagePerformanceConfig.resizeCommitDebounceMs
|
||
const RESIZE_INTERPOLATION_DURATION_MS =
|
||
labelimagePerformanceConfig.resizeInterpolationDurationMs
|
||
const RESIZE_GHOST_MIN_SCALE_DELTA = 0.001
|
||
const RESIZE_GHOST_FADE_ONLY_SCALE_DELTA = 0.03
|
||
|
||
const PaperContainer = (
|
||
props: PaperComponentProps,
|
||
ref: React.Ref<unknown> | undefined
|
||
) => {
|
||
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
|
||
false && updateLoadingFlag
|
||
const containerRef = useRef<any>(null)
|
||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||
const [imgSize, setImageSize] = useState<{
|
||
clientWidth: number
|
||
clientHeight: number
|
||
}>()
|
||
const [resizeGhost, setResizeGhost] = useState<ResizeGhostState | null>(null)
|
||
const [frameSwitchGhost, setFrameSwitchGhost] =
|
||
useState<FrameSwitchGhostState | null>(null)
|
||
const [contextMenuLayout, setContextMenuLayout] = useState<ContextMenuLayout>(
|
||
{
|
||
top: 0,
|
||
left: 0,
|
||
maxHeight: 0,
|
||
maxWidth: 0,
|
||
}
|
||
)
|
||
|
||
const [firstRender, setFirstRender] = useState(false)
|
||
const [confirmModalOpened, setConfirmModalOpened] = useState(false)
|
||
const [confirmModalCallback, setConfirmModalCallback] = useState<
|
||
((flag?: boolean) => void) | null
|
||
>(null)
|
||
|
||
// 监听画布容器宽高变化
|
||
useEffect(() => {
|
||
const container = containerRef.current
|
||
const scheduleCommitImageSize = (
|
||
size: { clientWidth: number; clientHeight: number },
|
||
immediate = false
|
||
) => {
|
||
const commit = () => {
|
||
const lastSize = lastContainerSizeRef.current
|
||
if (
|
||
lastSize?.clientWidth === size.clientWidth &&
|
||
lastSize?.clientHeight === size.clientHeight
|
||
) {
|
||
return
|
||
}
|
||
|
||
if (
|
||
RESIZE_INTERPOLATION_DURATION_MS > 0 &&
|
||
firstRender &&
|
||
lastSize?.clientWidth &&
|
||
lastSize?.clientHeight
|
||
) {
|
||
const forcedGhost = forcedResizeGhostSnapshotRef.current
|
||
let snapshotSrc = ""
|
||
if (forcedGhost?.src) {
|
||
snapshotSrc = forcedGhost.src
|
||
} else {
|
||
try {
|
||
snapshotSrc = canvasElem.current?.toDataURL("image/png") || ""
|
||
} catch (error) {
|
||
void error
|
||
}
|
||
}
|
||
|
||
const resizeImageId = useBottomToolsStore.getState().activeImage
|
||
const rawRasterSize =
|
||
usePaperStore.getState().rasterSize[resizeImageId] || null
|
||
const currentScale =
|
||
labelRenderScaleRef.current[resizeImageId] ??
|
||
usePaperStore.getState().rasterScale[resizeImageId]
|
||
const nextScale =
|
||
rawRasterSize?.[0] && rawRasterSize?.[1]
|
||
? Math.min(
|
||
size.clientWidth / rawRasterSize[0],
|
||
size.clientHeight / rawRasterSize[1]
|
||
)
|
||
: null
|
||
const scale =
|
||
currentScale &&
|
||
Number.isFinite(currentScale) &&
|
||
nextScale &&
|
||
Number.isFinite(nextScale) &&
|
||
currentScale > 0
|
||
? nextScale / currentScale
|
||
: null
|
||
const scaleDelta =
|
||
typeof scale === "number" && Number.isFinite(scale)
|
||
? Math.abs(scale - 1)
|
||
: null
|
||
const fallbackScale =
|
||
forcedGhost?.fromWidth && size.clientWidth
|
||
? size.clientWidth / forcedGhost.fromWidth
|
||
: 1
|
||
|
||
if (snapshotSrc && forcedGhost) {
|
||
pendingResizeGhostRef.current = {
|
||
id: resizeGhostIdRef.current + 1,
|
||
src: snapshotSrc,
|
||
fromWidth: forcedGhost.fromWidth,
|
||
fromHeight: forcedGhost.fromHeight,
|
||
scale:
|
||
typeof scale === "number" && Number.isFinite(scale)
|
||
? scale
|
||
: fallbackScale,
|
||
fadeOnly: false,
|
||
}
|
||
resizeGhostIdRef.current += 1
|
||
forcedResizeGhostSnapshotRef.current = null
|
||
} else if (
|
||
snapshotSrc &&
|
||
typeof scale === "number" &&
|
||
Number.isFinite(scale) &&
|
||
typeof scaleDelta === "number" &&
|
||
scaleDelta > RESIZE_GHOST_MIN_SCALE_DELTA
|
||
) {
|
||
pendingResizeGhostRef.current = {
|
||
id: resizeGhostIdRef.current + 1,
|
||
src: snapshotSrc,
|
||
fromWidth: lastSize.clientWidth,
|
||
fromHeight: lastSize.clientHeight,
|
||
scale,
|
||
fadeOnly: scaleDelta < RESIZE_GHOST_FADE_ONLY_SCALE_DELTA,
|
||
}
|
||
resizeGhostIdRef.current += 1
|
||
} else {
|
||
pendingResizeGhostRef.current = null
|
||
}
|
||
} else {
|
||
pendingResizeGhostRef.current = null
|
||
}
|
||
|
||
lastContainerSizeRef.current = size
|
||
setImageSize(size)
|
||
}
|
||
|
||
if (immediate || RESIZE_COMMIT_DEBOUNCE_MS <= 0) {
|
||
if (containerResizeCommitTimerRef.current !== null) {
|
||
clearTimeout(containerResizeCommitTimerRef.current)
|
||
containerResizeCommitTimerRef.current = null
|
||
}
|
||
commit()
|
||
return
|
||
}
|
||
|
||
if (containerResizeCommitTimerRef.current !== null) {
|
||
clearTimeout(containerResizeCommitTimerRef.current)
|
||
}
|
||
containerResizeCommitTimerRef.current = window.setTimeout(() => {
|
||
containerResizeCommitTimerRef.current = null
|
||
commit()
|
||
}, RESIZE_COMMIT_DEBOUNCE_MS)
|
||
}
|
||
|
||
const resizeObserver = new ResizeObserver((entries) => {
|
||
for (let entry of entries) {
|
||
if (entry.target === container) {
|
||
const nextSize = {
|
||
clientWidth: Math.max(0, Math.round(entry.contentRect.width)),
|
||
clientHeight: Math.max(0, Math.round(entry.contentRect.height)),
|
||
}
|
||
if (!nextSize.clientWidth || !nextSize.clientHeight) continue
|
||
if (!firstRender) {
|
||
scheduleCommitImageSize(nextSize, true)
|
||
setFirstRender(true)
|
||
continue
|
||
}
|
||
pendingContainerSizeRef.current = nextSize
|
||
if (typeof globalThis.requestAnimationFrame !== "function") {
|
||
scheduleCommitImageSize(nextSize)
|
||
} else if (containerResizeRafIdRef.current === null) {
|
||
containerResizeRafIdRef.current = globalThis.requestAnimationFrame(
|
||
() => {
|
||
containerResizeRafIdRef.current = null
|
||
const nextPendingSize = pendingContainerSizeRef.current
|
||
if (!nextPendingSize) return
|
||
pendingContainerSizeRef.current = null
|
||
scheduleCommitImageSize(nextPendingSize)
|
||
}
|
||
)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
if (container) {
|
||
resizeObserver.observe(container)
|
||
}
|
||
|
||
return () => {
|
||
if (
|
||
containerResizeRafIdRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(containerResizeRafIdRef.current)
|
||
}
|
||
if (containerResizeCommitTimerRef.current !== null) {
|
||
clearTimeout(containerResizeCommitTimerRef.current)
|
||
}
|
||
if (
|
||
resizeGhostRafRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(resizeGhostRafRef.current)
|
||
}
|
||
if (resizeGhostTimerRef.current !== null) {
|
||
clearTimeout(resizeGhostTimerRef.current)
|
||
}
|
||
containerResizeRafIdRef.current = null
|
||
containerResizeCommitTimerRef.current = null
|
||
resizeGhostRafRef.current = null
|
||
resizeGhostTimerRef.current = null
|
||
pendingContainerSizeRef.current = null
|
||
pendingResizeGhostRef.current = null
|
||
if (container) {
|
||
resizeObserver.unobserve(container)
|
||
}
|
||
}
|
||
}, [firstRender])
|
||
|
||
const canvasElem = useRef<HTMLCanvasElement>(null)
|
||
|
||
const {
|
||
init,
|
||
initViewTool,
|
||
initPanTool,
|
||
initPolygonTool,
|
||
initRectangleTool,
|
||
initBrushTool,
|
||
initPointTool,
|
||
initSupportTool,
|
||
addPolygon,
|
||
addBrush,
|
||
addPointLine,
|
||
addPoint,
|
||
addCompoundPath,
|
||
addRectangle,
|
||
setGroup,
|
||
setRasterPath,
|
||
setRasterSize,
|
||
setRasterScale,
|
||
loadingData,
|
||
resizeGhostRequestToken,
|
||
} = usePaperStore()
|
||
|
||
const {
|
||
activeImage,
|
||
pendingImage,
|
||
frameSwitchStatus,
|
||
frameSwitchToken,
|
||
allItems,
|
||
commitFrameSwitch,
|
||
clearPendingFrameSwitch,
|
||
} = useBottomToolsStore()
|
||
|
||
const storeLabel = useLabelStore((state) => state.label)
|
||
const activeImageLabel = useMemo(
|
||
() => storeLabel.get(activeImage),
|
||
[activeImage, storeLabel]
|
||
)
|
||
const deleteOperation = useLabelStore((state) => state.deleteOperation)
|
||
const setOperation = useLabelStore((state) => state.setOperation)
|
||
const getLabelDetailDataByKeys = useLabelStore(
|
||
(state) => state.getLabelDetailDataByKeys
|
||
)
|
||
|
||
const {
|
||
objectOperations,
|
||
imageFilter,
|
||
crosshairStatus,
|
||
assistToolEnabled,
|
||
assistToolSize,
|
||
renderMode,
|
||
saveCurrentScale,
|
||
showTags,
|
||
showTagsConfigs,
|
||
} = useTopToolsStore()
|
||
|
||
const {
|
||
pathStatus: paperPathStatus,
|
||
selectedPath,
|
||
updateSelectedPath,
|
||
} = useObjectStore()
|
||
const currentSelectedPath = selectedPath[activeImage] || []
|
||
const currentSelectedPathKey = currentSelectedPath.join(",")
|
||
|
||
const {
|
||
showContextMenu,
|
||
position,
|
||
operationId,
|
||
id,
|
||
setContextMenuId,
|
||
setContextMenuOperationId,
|
||
} = useOtherToolsStore()
|
||
const { renderTag } = useRenderTag()
|
||
|
||
const [setup, setSetup] = useState(false)
|
||
const labelRenderScaleRef = useRef<Record<string, number>>({})
|
||
const renderedImageRef = useRef("")
|
||
const frameImageCacheRef = useRef<Map<string, HTMLImageElement>>(new Map())
|
||
const frameDecodeTaskRef = useRef<
|
||
Map<string, Promise<HTMLImageElement | null>>
|
||
>(new Map())
|
||
const decodeFrameImageRef = useRef<
|
||
(imageName: string) => Promise<HTMLImageElement | null>
|
||
>(async () => null)
|
||
const polygonRenderJobIdRef = useRef(0)
|
||
const polygonRenderRunningRef = useRef(false)
|
||
const containerResizeRafIdRef = useRef<number | null>(null)
|
||
const containerResizeCommitTimerRef = useRef<number | null>(null)
|
||
const pendingContainerSizeRef = useRef<{
|
||
clientWidth: number
|
||
clientHeight: number
|
||
} | null>(null)
|
||
const lastContainerSizeRef = useRef<{
|
||
clientWidth: number
|
||
clientHeight: number
|
||
} | null>(null)
|
||
const pendingResizeGhostRef = useRef<Omit<
|
||
ResizeGhostState,
|
||
"animating"
|
||
> | null>(null)
|
||
const resizeGhostRafRef = useRef<number | null>(null)
|
||
const resizeGhostTimerRef = useRef<number | null>(null)
|
||
const resizeGhostIdRef = useRef(0)
|
||
const forcedResizeGhostSnapshotRef = useRef<{
|
||
token: number
|
||
src: string
|
||
fromWidth: number
|
||
fromHeight: number
|
||
} | null>(null)
|
||
const rasterLoadingImageRef = useRef<string | null>(null)
|
||
const prevActiveImageRef = useRef("")
|
||
const rasterFadeTransitionIdRef = useRef(0)
|
||
const rasterFadeRafIdRef = useRef<number | null>(null)
|
||
const frameSwitchNoticeTimerRef = useRef<number | null>(null)
|
||
const frameSwitchGhostIdRef = useRef(0)
|
||
const frameSwitchGhostRafRef = useRef<number | null>(null)
|
||
const frameSwitchGhostTimerRef = useRef<number | null>(null)
|
||
const frameSwitchGhostCapturedTokenRef = useRef<number | null>(null)
|
||
const frameSwitchGhostTargetRef = useRef<{
|
||
token: number
|
||
imageName: string
|
||
} | null>(null)
|
||
const renderModePanRef = useRef({
|
||
active: false,
|
||
lastX: 0,
|
||
lastY: 0,
|
||
})
|
||
|
||
const paperScope = useRef<paper.PaperScope>(null)
|
||
useEffect(() => {
|
||
if (firstRender) {
|
||
paperScope.current = new paper.PaperScope()
|
||
if (canvasElem.current) {
|
||
paperScope.current.setup(canvasElem.current)
|
||
setSetup(true)
|
||
}
|
||
}
|
||
}, [firstRender])
|
||
|
||
useEffect(() => {
|
||
if (!imgSize) return
|
||
const pScope = paperScope.current
|
||
if (pScope) {
|
||
const view = pScope.view
|
||
const nextViewSize = new paper.Size(
|
||
imgSize.clientWidth,
|
||
imgSize.clientHeight
|
||
)
|
||
view.viewSize = nextViewSize
|
||
view.center = new paper.Point(
|
||
nextViewSize.width / 2,
|
||
nextViewSize.height / 2
|
||
)
|
||
}
|
||
}, [imgSize])
|
||
|
||
useLayoutEffect(() => {
|
||
const pendingGhost = pendingResizeGhostRef.current
|
||
if (!pendingGhost || RESIZE_INTERPOLATION_DURATION_MS <= 0) return
|
||
pendingResizeGhostRef.current = null
|
||
|
||
if (
|
||
resizeGhostRafRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(resizeGhostRafRef.current)
|
||
}
|
||
if (resizeGhostTimerRef.current !== null) {
|
||
clearTimeout(resizeGhostTimerRef.current)
|
||
}
|
||
resizeGhostRafRef.current = null
|
||
resizeGhostTimerRef.current = null
|
||
|
||
setResizeGhost({
|
||
...pendingGhost,
|
||
animating: false,
|
||
})
|
||
|
||
const startGhostAnimation = () => {
|
||
setResizeGhost((current) => {
|
||
if (!current || current.id !== pendingGhost.id) return current
|
||
return {
|
||
...current,
|
||
animating: true,
|
||
}
|
||
})
|
||
|
||
resizeGhostTimerRef.current = window.setTimeout(() => {
|
||
setResizeGhost((current) => {
|
||
if (!current || current.id !== pendingGhost.id) return current
|
||
return null
|
||
})
|
||
resizeGhostTimerRef.current = null
|
||
}, RESIZE_INTERPOLATION_DURATION_MS + 40)
|
||
}
|
||
|
||
if (typeof globalThis.requestAnimationFrame === "function") {
|
||
resizeGhostRafRef.current = globalThis.requestAnimationFrame(() => {
|
||
resizeGhostRafRef.current = null
|
||
startGhostAnimation()
|
||
})
|
||
} else {
|
||
startGhostAnimation()
|
||
}
|
||
}, [imgSize?.clientHeight, imgSize?.clientWidth])
|
||
|
||
useEffect(() => {
|
||
if (!resizeGhostRequestToken) return
|
||
if (
|
||
!canvasElem.current ||
|
||
!imgSize?.clientWidth ||
|
||
!imgSize?.clientHeight
|
||
) {
|
||
return
|
||
}
|
||
|
||
let snapshotSrc = ""
|
||
try {
|
||
snapshotSrc = canvasElem.current.toDataURL("image/png")
|
||
} catch (error) {
|
||
void error
|
||
}
|
||
|
||
if (!snapshotSrc) return
|
||
|
||
forcedResizeGhostSnapshotRef.current = {
|
||
token: resizeGhostRequestToken,
|
||
src: snapshotSrc,
|
||
fromWidth: imgSize.clientWidth,
|
||
fromHeight: imgSize.clientHeight,
|
||
}
|
||
}, [imgSize?.clientHeight, imgSize?.clientWidth, resizeGhostRequestToken])
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (
|
||
resizeGhostRafRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(resizeGhostRafRef.current)
|
||
}
|
||
if (resizeGhostTimerRef.current !== null) {
|
||
clearTimeout(resizeGhostTimerRef.current)
|
||
}
|
||
resizeGhostRafRef.current = null
|
||
resizeGhostTimerRef.current = null
|
||
pendingResizeGhostRef.current = null
|
||
forcedResizeGhostSnapshotRef.current = null
|
||
}
|
||
}, [])
|
||
|
||
const cancelFrameSwitchGhostTransition = useCallback(() => {
|
||
if (
|
||
frameSwitchGhostRafRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(frameSwitchGhostRafRef.current)
|
||
}
|
||
if (frameSwitchGhostTimerRef.current !== null) {
|
||
clearTimeout(frameSwitchGhostTimerRef.current)
|
||
}
|
||
frameSwitchGhostRafRef.current = null
|
||
frameSwitchGhostTimerRef.current = null
|
||
}, [])
|
||
|
||
const captureFrameSwitchGhost = useCallback(
|
||
(token: number, imageName: string) => {
|
||
if (
|
||
!canvasElem.current ||
|
||
!imgSize?.clientWidth ||
|
||
!imgSize?.clientHeight
|
||
) {
|
||
frameSwitchGhostTargetRef.current = {
|
||
token,
|
||
imageName,
|
||
}
|
||
frameSwitchGhostCapturedTokenRef.current = token
|
||
return
|
||
}
|
||
|
||
let snapshotSrc = ""
|
||
try {
|
||
snapshotSrc = canvasElem.current.toDataURL("image/png")
|
||
} catch (error) {
|
||
void error
|
||
}
|
||
|
||
frameSwitchGhostTargetRef.current = {
|
||
token,
|
||
imageName,
|
||
}
|
||
frameSwitchGhostCapturedTokenRef.current = token
|
||
cancelFrameSwitchGhostTransition()
|
||
|
||
if (!snapshotSrc) {
|
||
setFrameSwitchGhost(null)
|
||
return
|
||
}
|
||
|
||
const ghostId = frameSwitchGhostIdRef.current + 1
|
||
frameSwitchGhostIdRef.current = ghostId
|
||
setFrameSwitchGhost({
|
||
id: ghostId,
|
||
src: snapshotSrc,
|
||
width: imgSize.clientWidth,
|
||
height: imgSize.clientHeight,
|
||
animating: false,
|
||
})
|
||
},
|
||
[
|
||
cancelFrameSwitchGhostTransition,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
]
|
||
)
|
||
|
||
const startFrameSwitchGhostFade = useCallback(
|
||
(token: number, imageName: string) => {
|
||
const target = frameSwitchGhostTargetRef.current
|
||
if (!target || target.token !== token || target.imageName !== imageName) {
|
||
return
|
||
}
|
||
|
||
cancelFrameSwitchGhostTransition()
|
||
|
||
const currentGhostId = frameSwitchGhostIdRef.current
|
||
const beginFade = () => {
|
||
setFrameSwitchGhost((current) => {
|
||
if (!current || current.id !== currentGhostId) return current
|
||
if (current.animating) return current
|
||
return {
|
||
...current,
|
||
animating: true,
|
||
}
|
||
})
|
||
|
||
frameSwitchGhostTimerRef.current = window.setTimeout(() => {
|
||
setFrameSwitchGhost((current) => {
|
||
if (!current || current.id !== currentGhostId) return current
|
||
return null
|
||
})
|
||
if (
|
||
frameSwitchGhostTargetRef.current?.token === token &&
|
||
frameSwitchGhostTargetRef.current?.imageName === imageName
|
||
) {
|
||
frameSwitchGhostTargetRef.current = null
|
||
}
|
||
frameSwitchGhostTimerRef.current = null
|
||
}, RASTER_CROSSFADE_DURATION_MS + 40)
|
||
}
|
||
|
||
const runAfterPaint = () => {
|
||
if (typeof globalThis.requestAnimationFrame !== "function") {
|
||
beginFade()
|
||
return
|
||
}
|
||
frameSwitchGhostRafRef.current = globalThis.requestAnimationFrame(
|
||
() => {
|
||
frameSwitchGhostRafRef.current = globalThis.requestAnimationFrame(
|
||
() => {
|
||
frameSwitchGhostRafRef.current = null
|
||
beginFade()
|
||
}
|
||
)
|
||
}
|
||
)
|
||
}
|
||
|
||
runAfterPaint()
|
||
},
|
||
[cancelFrameSwitchGhostTransition]
|
||
)
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
cancelFrameSwitchGhostTransition()
|
||
frameSwitchGhostCapturedTokenRef.current = null
|
||
frameSwitchGhostTargetRef.current = null
|
||
}
|
||
}, [cancelFrameSwitchGhostTransition])
|
||
|
||
useEffect(() => {
|
||
if (
|
||
frameSwitchStatus === "preparing" &&
|
||
pendingImage &&
|
||
pendingImage !== activeImage
|
||
) {
|
||
if (frameSwitchGhostCapturedTokenRef.current !== frameSwitchToken) {
|
||
captureFrameSwitchGhost(frameSwitchToken, pendingImage)
|
||
}
|
||
return
|
||
}
|
||
|
||
const target = frameSwitchGhostTargetRef.current
|
||
if (!target) {
|
||
frameSwitchGhostCapturedTokenRef.current = null
|
||
setFrameSwitchGhost(null)
|
||
return
|
||
}
|
||
|
||
if (activeImage === target.imageName) {
|
||
startFrameSwitchGhostFade(target.token, target.imageName)
|
||
frameSwitchGhostCapturedTokenRef.current = null
|
||
return
|
||
}
|
||
|
||
cancelFrameSwitchGhostTransition()
|
||
frameSwitchGhostTargetRef.current = null
|
||
frameSwitchGhostCapturedTokenRef.current = null
|
||
setFrameSwitchGhost(null)
|
||
}, [
|
||
activeImage,
|
||
cancelFrameSwitchGhostTransition,
|
||
captureFrameSwitchGhost,
|
||
frameSwitchStatus,
|
||
frameSwitchToken,
|
||
pendingImage,
|
||
startFrameSwitchGhostFade,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (frameSwitchNoticeTimerRef.current !== null) {
|
||
clearTimeout(frameSwitchNoticeTimerRef.current)
|
||
frameSwitchNoticeTimerRef.current = null
|
||
}
|
||
|
||
if (
|
||
frameSwitchStatus !== "preparing" ||
|
||
!pendingImage ||
|
||
pendingImage === activeImage
|
||
) {
|
||
notifications.hide("frameSwitching")
|
||
return
|
||
}
|
||
|
||
if (FRAME_SWITCH_LOADING_DELAY_MS <= 0) {
|
||
notifications.show({
|
||
id: "frameSwitching",
|
||
message: "切换中...",
|
||
autoClose: false,
|
||
withCloseButton: false,
|
||
})
|
||
return
|
||
}
|
||
|
||
frameSwitchNoticeTimerRef.current = window.setTimeout(() => {
|
||
notifications.show({
|
||
id: "frameSwitching",
|
||
message: "切换中...",
|
||
autoClose: false,
|
||
withCloseButton: false,
|
||
})
|
||
frameSwitchNoticeTimerRef.current = null
|
||
}, FRAME_SWITCH_LOADING_DELAY_MS)
|
||
|
||
return () => {
|
||
if (frameSwitchNoticeTimerRef.current !== null) {
|
||
clearTimeout(frameSwitchNoticeTimerRef.current)
|
||
frameSwitchNoticeTimerRef.current = null
|
||
}
|
||
notifications.hide("frameSwitching")
|
||
}
|
||
}, [activeImage, frameSwitchStatus, pendingImage])
|
||
|
||
useEffect(() => {
|
||
if (loadingData) {
|
||
polygonRenderJobIdRef.current += 1
|
||
polygonRenderRunningRef.current = false
|
||
labelRenderScaleRef.current = {}
|
||
renderedImageRef.current = ""
|
||
rasterLoadingImageRef.current = null
|
||
prevActiveImageRef.current = ""
|
||
frameImageCacheRef.current.clear()
|
||
frameDecodeTaskRef.current.clear()
|
||
clearPendingFrameSwitch()
|
||
notifications.hide("frameSwitching")
|
||
const group = usePaperStore.getState().group
|
||
if (group) {
|
||
group.removeChildren()
|
||
}
|
||
setRasterInited(false)
|
||
}
|
||
}, [clearPendingFrameSwitch, loadingData])
|
||
|
||
const renderSupportAnnotation = useCallback(
|
||
async (
|
||
points: Array<[number, number]>,
|
||
tags: number[],
|
||
clear_previous_state = false,
|
||
boxes: SupportBox[] = []
|
||
) => {
|
||
void clear_previous_state
|
||
try {
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成中",
|
||
loading: true,
|
||
autoClose: false,
|
||
})
|
||
const operationSchema =
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) =>
|
||
item.category_id === +useTopToolsStore.getState().activeOperation
|
||
) || null
|
||
const strokeColor = operationSchema
|
||
? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||
: "rgb(0,0,0)"
|
||
const fillColor = operationSchema
|
||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||
: "rgba(0,0,0,0.3)"
|
||
const blankColor = operationSchema
|
||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||
: "rgba(0,0,0,0.01)"
|
||
const currentScale =
|
||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
|
||
const draw = () => {
|
||
const paperGroup = usePaperStore.getState().group!
|
||
const raster = paperGroup.getItems({ data: { name: "pic" } })[0]
|
||
|
||
points.forEach((point, index) => {
|
||
const drawPoint = new paper.Path.Circle({
|
||
center: point,
|
||
radius: 2,
|
||
fillColor: tags[index] === 1 ? "red" : "blue",
|
||
strokeColor: tags[index] === 1 ? "red" : "blue",
|
||
data: {
|
||
id: "support",
|
||
type: "point",
|
||
},
|
||
})
|
||
drawPoint.parent = paperGroup
|
||
if (!raster.contains(drawPoint.position)) {
|
||
throw new Error("点位超出图片范围")
|
||
}
|
||
})
|
||
|
||
boxes.forEach(([left, top, right, bottom]) => {
|
||
new paper.Path.Rectangle({
|
||
parent: paperGroup,
|
||
from: [left, top],
|
||
to: [right, bottom],
|
||
strokeColor: "#FFF",
|
||
strokeWidth: 2,
|
||
strokeScaling: false,
|
||
dashArray: [6, 4],
|
||
fillColor: new paper.Color(1, 1, 1, 0.05),
|
||
data: {
|
||
id: "support",
|
||
type: "box",
|
||
},
|
||
})
|
||
})
|
||
}
|
||
console.log(points, tags, boxes)
|
||
clearSupportAnnotationItem()
|
||
draw()
|
||
|
||
const normalizedPoints = points.map(([x, y]) => [
|
||
Math.round(x / currentScale),
|
||
Math.round(y / currentScale),
|
||
]) as Array<[number, number]>
|
||
const foregroundPoints = normalizedPoints.filter(
|
||
(_point, index) => tags[index] === 1
|
||
)
|
||
const background_points = normalizedPoints.filter(
|
||
(_point, index) => tags[index] !== 1
|
||
)
|
||
const promptPoints = foregroundPoints.length
|
||
? foregroundPoints
|
||
: normalizedPoints
|
||
const normalizedBoxes = boxes.map(([left, top, right, bottom]) => [
|
||
Math.round(Math.min(left, right) / currentScale),
|
||
Math.round(Math.min(top, bottom) / currentScale),
|
||
Math.round(Math.max(left, right) / currentScale),
|
||
Math.round(Math.max(top, bottom) / currentScale),
|
||
]) as SupportBox[]
|
||
const projectId = projectDetail?.id || ""
|
||
|
||
if (!projectId) {
|
||
throw new Error("缺少 project_id,无法调用模型接口")
|
||
}
|
||
if (!promptPoints.length && !normalizedBoxes.length) {
|
||
throw new Error("缺少有效提示,无法调用模型接口")
|
||
}
|
||
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||
|
||
try {
|
||
const data = await getAuxiliaryAnnotation({
|
||
project_id: projectId,
|
||
image_name: activeImage,
|
||
// project_id: "image_test_bk",
|
||
prompt: {
|
||
...(promptPoints.length
|
||
? { foreground_points: promptPoints }
|
||
: {}),
|
||
...(background_points.length
|
||
? { background_points: background_points }
|
||
: {}),
|
||
...(normalizedBoxes.length ? { boxes: normalizedBoxes } : {}),
|
||
},
|
||
})
|
||
const { contours = [], hollows = [] } = data || {}
|
||
const outerPaths: any[] = []
|
||
const innerPaths: any[] = []
|
||
contours.forEach((contour: any, index: number) => {
|
||
const normalizedContour = normalizeContour(contour)
|
||
if (normalizedContour.length > 3) {
|
||
const path = new paper.Path({
|
||
segments: normalizedContour.map(([x, y]) => [
|
||
x * currentScale,
|
||
y * currentScale,
|
||
]),
|
||
fillColor: blankColor,
|
||
strokeColor: strokeColor,
|
||
closed: true,
|
||
data: {
|
||
id: "support",
|
||
type: "polygon",
|
||
isHollowPolygon: false,
|
||
fillColor,
|
||
strokeColor,
|
||
blankColor,
|
||
},
|
||
parent: usePaperStore.getState().group,
|
||
strokeScaling: false,
|
||
})
|
||
if (Math.abs(path.area) < useTopToolsStore.getState().checkSize) {
|
||
path.remove()
|
||
} else {
|
||
if (hollows[index]) {
|
||
path.data.isHollowPolygon = true
|
||
innerPaths.push(path)
|
||
} else {
|
||
path.fillColor = new paper.Color(fillColor)
|
||
outerPaths.push(path)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
const compoundPath = new paper.CompoundPath({
|
||
children: [...outerPaths, ...innerPaths],
|
||
data: {
|
||
id: "support",
|
||
type: "parent",
|
||
},
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
fillColor: fillColor,
|
||
})
|
||
compoundPath.parent = usePaperStore.getState().group!
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成成功",
|
||
color: "green",
|
||
})
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
} catch (error: any) {
|
||
console.log(error)
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: error.message ?? "模型生成失败",
|
||
color: "red",
|
||
})
|
||
if (error.message === "点位超出图片范围") {
|
||
usePaperStore
|
||
.getState()
|
||
.supportTool?.emit("keydown", { key: "escape" })
|
||
}
|
||
}
|
||
},
|
||
[activeImage, projectDetail]
|
||
)
|
||
|
||
// const renderTrackingAnnotation = useCallback(async () => {
|
||
// updateLoadingFlag(true)
|
||
// try {
|
||
// notifications.show({
|
||
// id: "sam2",
|
||
// message: "模型生成中",
|
||
// loading: true,
|
||
// autoClose: false,
|
||
// })
|
||
// const ids =
|
||
// useObjectStore.getState().selectedPath[
|
||
// useBottomToolsStore.getState().activeImage
|
||
// ]
|
||
// const picArr = useBottomToolsStore.getState().selectedItems
|
||
// const [width, height] = usePaperStore.getState().rasterSize[
|
||
// activeImage
|
||
// ] ?? [0, 0]
|
||
// const currentScale =
|
||
// usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
// if (ids.length === 1) {
|
||
// const id = ids[0]
|
||
// let categoryId: any = null
|
||
// let data: any = null
|
||
// const activeImageData = useLabelStore.getState().label.get(activeImage)!
|
||
// for (let [category, objArr] of activeImageData.entries()) {
|
||
// objArr.forEach((item) => {
|
||
// if (item[0] === id) {
|
||
// categoryId = category
|
||
// data = item
|
||
// }
|
||
// })
|
||
// }
|
||
// if (categoryId && data) {
|
||
// const contours: [number, number][] = []
|
||
// const tags: boolean[] = []
|
||
// data[1].forEach((item: any) => {
|
||
// contours.push(
|
||
// item.map(([x, y]: any) => [
|
||
// Math.round(x / currentScale),
|
||
// Math.round(y / currentScale),
|
||
// ])
|
||
// )
|
||
// tags.push(false)
|
||
// })
|
||
// data[3].forEach((item: any) => {
|
||
// contours.push(
|
||
// item.map(([x, y]: any) => [
|
||
// Math.round(x / currentScale),
|
||
// Math.round(y / currentScale),
|
||
// ])
|
||
// )
|
||
// tags.push(true)
|
||
// })
|
||
// const file_names = [...new Set([activeImage, ...picArr])]
|
||
// const params = {
|
||
// project_id: projectDetail?.id || 0,
|
||
// file_names,
|
||
// image_hw: [height, width],
|
||
// contours,
|
||
// hollows: tags,
|
||
// }
|
||
// console.log("传参", params)
|
||
// try {
|
||
// const res = await getTrackingAuxiliaryAnnotation(
|
||
// msgpack.encode(params)
|
||
// )
|
||
// const resData = msgpack.decode(new Uint8Array(res))
|
||
// if (resData.msg === "success") {
|
||
// const {
|
||
// future_contours,
|
||
// }: {
|
||
// future_contours: {
|
||
// contours: [number, number][][]
|
||
// hollows: boolean[]
|
||
// }[]
|
||
// } = resData
|
||
// const nowTaskData = safeClone(useLabelStore.getState().label)
|
||
// future_contours.forEach(({ contours, hollows }, index) => {
|
||
// const fileName = file_names[index + 1]
|
||
// const imgScale =
|
||
// usePaperStore.getState().rasterScale[fileName] ?? 1
|
||
// let newId =
|
||
// useRightToolsStore
|
||
// .getState()
|
||
// .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
// useRightToolsStore.getState().pushPathId(newId)
|
||
// // 处理点位数据
|
||
// let segmentation: Array<[number, number][]> = []
|
||
// let hollow_segmentation: Array<[number, number][]> = []
|
||
// contours.forEach((contour, index) => {
|
||
// if (!hollows[index])
|
||
// segmentation.push(
|
||
// contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||
// )
|
||
// else
|
||
// hollow_segmentation.push(
|
||
// contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||
// )
|
||
// })
|
||
// // 详情数据
|
||
// let detail = {
|
||
// ...initialDetail,
|
||
// uid: usePermissionStore.getState().user_id,
|
||
// comment: [...useLabelStore.getState().labelDefaultComments],
|
||
// create_timestamp: dayjs().unix(),
|
||
// sub_attributes: {},
|
||
// image_id: activeImage,
|
||
// category_id: categoryId,
|
||
// }
|
||
// const updateData: any = [
|
||
// newId,
|
||
// segmentation,
|
||
// detail,
|
||
// hollow_segmentation,
|
||
// ]
|
||
// if (nowTaskData.has(fileName)) {
|
||
// const categoryMap = nowTaskData.get(fileName)!
|
||
// if (categoryMap.has(categoryId)) {
|
||
// const existArr = categoryMap.get(categoryId)
|
||
// existArr?.push(updateData)
|
||
// } else {
|
||
// categoryMap.set(categoryId, [updateData])
|
||
// }
|
||
// } else {
|
||
// let m = new Map()
|
||
// m.set(categoryId, [updateData])
|
||
// nowTaskData.set(fileName, m)
|
||
// }
|
||
// })
|
||
// console.log(nowTaskData)
|
||
// useLabelStore.getState().setLabel(nowTaskData)
|
||
// useLabelStore.getState().pushStateStack(nowTaskData)
|
||
// notifications.show({
|
||
// id: "sam2",
|
||
// message: "模型生成成功",
|
||
// color: "green",
|
||
// })
|
||
// }
|
||
// } catch (error) {
|
||
// console.log(error)
|
||
// }
|
||
// }
|
||
// } else if (!ids.length) {
|
||
// notifications.show({
|
||
// id: "sam2",
|
||
// message: "请先选中标注对象后再进行模型调用",
|
||
// color: "red",
|
||
// })
|
||
// } else {
|
||
// notifications.show({
|
||
// id: "sam2",
|
||
// message: "请勿选择多个标注对象进行模型调用",
|
||
// color: "red",
|
||
// })
|
||
// }
|
||
// } catch (error) {
|
||
// console.log(error)
|
||
// notifications.show({
|
||
// id: "sam2",
|
||
// message: "模型生成失败",
|
||
// color: "red",
|
||
// })
|
||
// } finally {
|
||
// updateLoadingFlag(false)
|
||
// }
|
||
// }, [activeImage, projectDetail?.id, updateLoadingFlag])
|
||
|
||
const renderTrackingAnnotation = useCallback(async () => {}, [])
|
||
|
||
useEffect(() => {
|
||
if (setup) {
|
||
const newGroup = new paper.Group({
|
||
applyMatrix: false,
|
||
selected: true,
|
||
strokeScaling: false,
|
||
})
|
||
|
||
if (canvasElem.current && paper) {
|
||
init(canvasElem.current, paperScope.current!, newGroup)
|
||
canvasElem.current.oncontextmenu = (e) => {
|
||
e.preventDefault()
|
||
}
|
||
let viewTool = new paper.Tool()
|
||
initViewTool(viewTool)
|
||
let panTool = new paper.Tool()
|
||
initPanTool(panTool, renderRectangle, forceUpdate)
|
||
let polygonTool = new paper.Tool()
|
||
initPolygonTool(
|
||
polygonTool,
|
||
renderPath,
|
||
renderCompoundPath,
|
||
forceUpdate
|
||
)
|
||
let rectangleTool = new paper.Tool()
|
||
initRectangleTool(rectangleTool, renderRectangle, forceUpdate)
|
||
let brushTool = new paper.Tool()
|
||
initBrushTool(brushTool, renderPath, renderCompoundPath, forceUpdate)
|
||
let pointTool = new paper.Tool()
|
||
initPointTool(pointTool, renderCircle, renderPath, forceUpdate)
|
||
let supportTool = new paper.Tool()
|
||
initSupportTool(supportTool, renderSupportAnnotation)
|
||
}
|
||
return () => {
|
||
newGroup.remove()
|
||
}
|
||
}
|
||
}, [
|
||
forceUpdate,
|
||
init,
|
||
initBrushTool,
|
||
initPanTool,
|
||
initPointTool,
|
||
initPolygonTool,
|
||
initRectangleTool,
|
||
initSupportTool,
|
||
initViewTool,
|
||
renderSupportAnnotation,
|
||
setup,
|
||
])
|
||
|
||
const [paperSelectedPath] = useState<{
|
||
// key = imageId
|
||
[key: string]: number[]
|
||
}>(selectedPath)
|
||
|
||
const getOperationSchema = useCallback(
|
||
(operationId: string) => {
|
||
return (
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) => item.category_id === +operationId
|
||
) || null
|
||
)
|
||
},
|
||
[projectDetail?.label_schema_list]
|
||
)
|
||
|
||
const { getItemById, getItemsById } = usePaperStore()
|
||
|
||
const renderPolygons = useCallback(
|
||
(
|
||
operationId: string,
|
||
imageData: ImageLabelData | undefined,
|
||
pathsOverride?: LabelPathItem[],
|
||
imageIdOverride?: string
|
||
) => {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
const renderImageId = imageIdOverride ?? activeImage
|
||
|
||
if (operationSchema && imageData) {
|
||
const toPaperPoint = (segment: any): paper.Point | null => {
|
||
if (segment instanceof paper.Point) return segment
|
||
if (segment instanceof paper.Segment) {
|
||
return new paper.Point(segment.point)
|
||
}
|
||
if (
|
||
Array.isArray(segment) &&
|
||
segment.length >= 2 &&
|
||
typeof segment[0] === "number" &&
|
||
typeof segment[1] === "number"
|
||
) {
|
||
return new paper.Point(segment[0], segment[1])
|
||
}
|
||
if (
|
||
Array.isArray(segment) &&
|
||
Array.isArray(segment[0]) &&
|
||
segment[0].length >= 2 &&
|
||
typeof segment[0][0] === "number" &&
|
||
typeof segment[0][1] === "number"
|
||
) {
|
||
return new paper.Point(segment[0][0], segment[0][1])
|
||
}
|
||
if (
|
||
segment?.point &&
|
||
typeof segment.point.x === "number" &&
|
||
typeof segment.point.y === "number"
|
||
) {
|
||
return new paper.Point(segment.point.x, segment.point.y)
|
||
}
|
||
if (
|
||
segment?.point &&
|
||
Array.isArray(segment.point) &&
|
||
segment.point.length >= 2 &&
|
||
typeof segment.point[0] === "number" &&
|
||
typeof segment.point[1] === "number"
|
||
) {
|
||
return new paper.Point(segment.point[0], segment.point[1])
|
||
}
|
||
return null
|
||
}
|
||
const toPaperPoints = (segments: any[]) => {
|
||
return segments
|
||
.map((segment) => toPaperPoint(segment))
|
||
.filter((point): point is paper.Point => point !== null)
|
||
}
|
||
|
||
let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||
let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||
let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||
|
||
let paths = pathsOverride ?? imageData.get(operationId)
|
||
|
||
switch (
|
||
operationSchema &&
|
||
labelTypeMap.get(operationSchema.label_type)
|
||
) {
|
||
case "多边形":
|
||
paths?.forEach((item) => {
|
||
let outerPaths =
|
||
item[1]
|
||
?.map((child) => {
|
||
const points = toPaperPoints(child)
|
||
if (!points.length) return null
|
||
return addPolygon(
|
||
renderPath,
|
||
points,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
})
|
||
.filter((path): path is paper.Path => !!path) || []
|
||
let innerPaths =
|
||
item[3]
|
||
?.map((child) => {
|
||
const points = toPaperPoints(child)
|
||
if (!points.length) return null
|
||
return addPolygon(
|
||
renderPath,
|
||
points,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
fillColor: fillColor,
|
||
visible:
|
||
!paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(
|
||
item[0]
|
||
),
|
||
isHollowPolygon: true,
|
||
}
|
||
)
|
||
})
|
||
.filter((path): path is paper.Path => !!path) || []
|
||
|
||
addCompoundPath(
|
||
renderCompoundPath,
|
||
[...outerPaths, ...innerPaths],
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(item[0]),
|
||
}
|
||
)
|
||
|
||
// renderCompoundPath(usePaperStore.getState().group!, { children });
|
||
// item[1].forEach((child) => {
|
||
// addPolygon(
|
||
// renderPath,
|
||
// child.map((segment: any) => {
|
||
// if (segment instanceof paper.Point) {
|
||
// return segment;
|
||
// } else {
|
||
// return new paper.Point(segment[0], segment[1]);
|
||
// }
|
||
// }),
|
||
// {
|
||
// strokeColor: strokeColor,
|
||
// strokeWidth: 2,
|
||
// // fillColor: fillColor,
|
||
// fillColor: blankColor,
|
||
// visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
// selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
// },
|
||
// {
|
||
// type: "polygon",
|
||
// id: item[0],
|
||
// imageId: activeImage,
|
||
// operationId: operationId,
|
||
// fillColor: fillColor,
|
||
// blankColor: blankColor,
|
||
// }
|
||
// );
|
||
// });
|
||
})
|
||
return
|
||
case "关键点":
|
||
paths?.forEach((item) => {
|
||
item[1]?.forEach((child) => {
|
||
// 保存前排好序
|
||
child.forEach((segment: any, segmentIndex: number) => {
|
||
let point = toPaperPoint(segment)
|
||
if (!point) return
|
||
addPoint(
|
||
renderCircle,
|
||
point,
|
||
{
|
||
fillColor: null,
|
||
strokeColor: strokeColor,
|
||
radius: 3,
|
||
},
|
||
{
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
text: segmentIndex + 1,
|
||
id: item[0],
|
||
attributes: item[2]?.circles?.[segmentIndex]?.attributes,
|
||
}
|
||
)
|
||
})
|
||
const points = toPaperPoints(child)
|
||
if (!points.length) return
|
||
addPointLine(
|
||
renderPath,
|
||
points,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
})
|
||
})
|
||
return
|
||
case "多线段":
|
||
paths?.forEach((item) => {
|
||
let outerPaths =
|
||
item[1]
|
||
?.map((child) => {
|
||
const points = toPaperPoints(child)
|
||
if (!points.length) return null
|
||
return addBrush(
|
||
renderPath,
|
||
points,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
})
|
||
.filter((path): path is paper.Path => !!path) || []
|
||
|
||
addCompoundPath(
|
||
renderCompoundPath,
|
||
outerPaths,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(item[0]),
|
||
}
|
||
)
|
||
})
|
||
return
|
||
case "2D框":
|
||
paths?.forEach((item) => {
|
||
item[1].forEach((child) => {
|
||
let p = toPaperPoint(child[1])
|
||
let dp = toPaperPoint(child[3])
|
||
if (!p || !dp) {
|
||
console.warn(
|
||
"[PaperContainer] skip invalid rectangle segment",
|
||
{
|
||
id: item[0],
|
||
child,
|
||
}
|
||
)
|
||
return
|
||
}
|
||
|
||
addRectangle(
|
||
renderRectangle,
|
||
{
|
||
from: p,
|
||
to: dp,
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[renderImageId + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "rectangle",
|
||
id: item[0],
|
||
imageId: renderImageId,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[renderImageId]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
})
|
||
})
|
||
return
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
addBrush,
|
||
addCompoundPath,
|
||
addPoint,
|
||
addPointLine,
|
||
addPolygon,
|
||
addRectangle,
|
||
getOperationSchema,
|
||
paperPathStatus,
|
||
paperSelectedPath,
|
||
]
|
||
)
|
||
|
||
const syncRenderModeVisuals = useCallback(
|
||
(targetGroup?: paper.Group | null) => {
|
||
const group = targetGroup ?? usePaperStore.getState().group
|
||
if (!group) return
|
||
|
||
group.children.forEach((item) => {
|
||
const itemData = item.data || {}
|
||
const itemType = itemData.type
|
||
|
||
if (
|
||
itemData.name === "pic" ||
|
||
itemData.id === "support" ||
|
||
!itemData.fillColor ||
|
||
!itemData.blankColor ||
|
||
[
|
||
"mask",
|
||
"text",
|
||
"tag",
|
||
"groupPath",
|
||
"pathCircle",
|
||
"pathBuff",
|
||
"circle",
|
||
].includes(itemType)
|
||
) {
|
||
return
|
||
}
|
||
|
||
const nextFillState = renderMode || itemData.selected ? "fill" : "blank"
|
||
if (itemData.renderModeFillState === nextFillState) {
|
||
return
|
||
}
|
||
itemData.renderModeFillState = nextFillState
|
||
|
||
const nextFillColor =
|
||
renderMode || itemData.selected
|
||
? itemData.fillColor
|
||
: itemData.blankColor
|
||
const pathItem = item as paper.PathItem
|
||
pathItem.fillColor = nextFillColor
|
||
? new paper.Color(nextFillColor)
|
||
: null
|
||
})
|
||
},
|
||
[renderMode]
|
||
)
|
||
|
||
// 生成辅助标注结果数据并绘制
|
||
const saveSupportAnnotationData = useCallback(() => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
const compoundPath = group.getItems({
|
||
data: { id: "support", type: "parent" },
|
||
})?.[0]
|
||
if (compoundPath) {
|
||
console.log(compoundPath)
|
||
const children = compoundPath.children as paper.Path[]
|
||
const id =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
useRightToolsStore.getState().pushPathId(id)
|
||
// 处理点位数据
|
||
let segmentation: Array<[number, number][]> = []
|
||
let hollow_segmentation: Array<[number, number][]> = []
|
||
children.forEach((child) => {
|
||
if (child.data.isHollowPolygon) {
|
||
hollow_segmentation.push(
|
||
child.segments.map((seg) => [seg.point.x, seg.point.y])
|
||
)
|
||
} else {
|
||
segmentation.push(
|
||
child.segments.map((seg) => [seg.point.x, seg.point.y])
|
||
)
|
||
}
|
||
})
|
||
console.log(segmentation, hollow_segmentation)
|
||
// 处理详情数据
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = Object.assign(
|
||
{},
|
||
useTopToolsStore.getState().subAttributes[label_class]
|
||
)
|
||
let detail = {
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: activeImage,
|
||
category_id: Number(useTopToolsStore.getState().activeOperation),
|
||
}
|
||
// 清空support对象
|
||
clearSupportAnnotationItem()
|
||
setOperation(activeImage, useTopToolsStore.getState().activeOperation, [
|
||
id,
|
||
segmentation,
|
||
detail,
|
||
hollow_segmentation,
|
||
])
|
||
console.log(usePaperStore.getState().group)
|
||
// console.log(first)
|
||
// 更新当前页绘制数据
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.filter((item) => item.data.id)
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
const imgMap = useLabelStore.getState().label.get(activeImage)!
|
||
for (const key of imgMap.keys()) {
|
||
renderPolygons(key, imgMap)
|
||
}
|
||
// 模型数据生成完毕 调整绘制模式
|
||
usePaperStore.getState().setPaperMode("pan")
|
||
}
|
||
}, [activeImage, renderPolygons, setOperation])
|
||
|
||
const copyAnnotationDataToMultiFrame = useCallback(() => {
|
||
const picArr = useBottomToolsStore
|
||
.getState()
|
||
.selectedItems.filter((k) => k !== activeImage)
|
||
const copyArr = useKeyboardStore.getState().copyDataIds
|
||
if (picArr.length && copyArr.length) {
|
||
const data = safeClone(useLabelStore.getState().label)
|
||
// 先判断是否有重复的id
|
||
let existedIds: number[] = []
|
||
picArr.forEach((imageId) => {
|
||
if (data.has(imageId) && data.get(imageId)?.size) {
|
||
for (let arr of data.get(imageId)!.values()) {
|
||
arr.forEach((i) => existedIds.push(i[0]))
|
||
}
|
||
}
|
||
})
|
||
|
||
const handleData = (flag = false) => {
|
||
try {
|
||
const activeData = data.get(activeImage) ?? new Map()
|
||
const activeScale =
|
||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
const needCopyMap = new Map()
|
||
for (let [key, value] of activeData.entries()) {
|
||
let arr = value.filter(([id]: any) => copyArr.includes(id))
|
||
console.log(arr)
|
||
if (arr.length)
|
||
needCopyMap.set(
|
||
key,
|
||
arr.map(([id, seg, detail, hollow]: any) => [
|
||
id,
|
||
seg.map((item: any) =>
|
||
item.map((p: any) => {
|
||
if (p instanceof paper.Point) {
|
||
return [p.x / activeScale, p.y / activeScale]
|
||
} else {
|
||
return [p[0] / activeScale, p[1] / activeScale]
|
||
}
|
||
})
|
||
),
|
||
{
|
||
...detail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
create_timestamp: dayjs().unix(),
|
||
first_modified_timestamp: 0,
|
||
first_modified_uid: 0,
|
||
last_modified_timestamp: 0,
|
||
last_modified_uid: 0,
|
||
},
|
||
hollow.map((item: any) =>
|
||
item.map((p: any) => {
|
||
if (p instanceof paper.Segment) {
|
||
return [
|
||
p.point.x / activeScale,
|
||
p.point.y / activeScale,
|
||
]
|
||
} else {
|
||
return [p[0] / activeScale, p[1] / activeScale]
|
||
}
|
||
})
|
||
),
|
||
])
|
||
)
|
||
}
|
||
|
||
picArr.forEach((imageId) => {
|
||
const picScale = usePaperStore.getState().rasterScale[imageId] ?? 1
|
||
|
||
if (data.has(imageId)) {
|
||
const categoryMap = data.get(imageId)
|
||
if (categoryMap?.size) {
|
||
let needCopyIds: number[] = []
|
||
let needCopyCategoryIds: string[] = []
|
||
for (let [key, needCopyItemsArr] of needCopyMap.entries()) {
|
||
// console.log(key);
|
||
needCopyItemsArr.forEach((item: any) => {
|
||
needCopyIds.push(item[0])
|
||
needCopyCategoryIds.push(key)
|
||
})
|
||
// let arr = needCopyItemsArr.map((item: any) => [
|
||
// item[0],
|
||
// item[1].map((path: any) =>
|
||
// path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
// ),
|
||
// item[2],
|
||
// item[3].map((path: any) =>
|
||
// path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
// ),
|
||
// ]);
|
||
// needCopyMap.set(key, arr);
|
||
}
|
||
let currentImageItemIds: number[] = []
|
||
let currentImageCategoryIds: string[] = []
|
||
for (let [key, items] of categoryMap.entries()) {
|
||
items.forEach((item) => {
|
||
currentImageItemIds.push(item[0])
|
||
currentImageCategoryIds.push(key)
|
||
})
|
||
}
|
||
let finalArr: number[] = []
|
||
let finalCategory: string[] = []
|
||
needCopyIds.forEach((id, index) => {
|
||
const findIndex = currentImageItemIds.findIndex(
|
||
(i) => i === id
|
||
)
|
||
if (findIndex !== -1) {
|
||
if (flag) {
|
||
console.log(findIndex)
|
||
// 确认覆盖
|
||
finalArr.push(id)
|
||
finalCategory.push(needCopyCategoryIds[index])
|
||
let categoryId = currentImageCategoryIds[findIndex]
|
||
let arr = categoryMap.get(categoryId)!
|
||
let findIndexInArr = arr.findIndex((v) => v[0] === id)
|
||
let oldData = arr.find((a) => a[0] === id)!
|
||
let arr2 = needCopyMap.get(needCopyCategoryIds[index])!
|
||
let copiedItem = arr2.find((b: any) => b[0] === id)
|
||
if (!copiedItem) return
|
||
let newData = {
|
||
...copiedItem[2],
|
||
uid: oldData[2].uid,
|
||
create_timestamp: oldData[2].create_timestamp,
|
||
first_modified_timestamp:
|
||
oldData[2].first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid:
|
||
oldData[2].first_modified_uid ||
|
||
usePermissionStore.getState().user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid:
|
||
usePermissionStore.getState().user_id,
|
||
}
|
||
let updateArr = arr2.map((b: any) => {
|
||
return b[0] === id ? [b[0], b[1], newData, b[3]] : b
|
||
})
|
||
needCopyMap.set(needCopyCategoryIds[index], updateArr)
|
||
categoryMap.set(
|
||
categoryId,
|
||
arr.filter((_v, i) => i !== findIndexInArr)
|
||
)
|
||
}
|
||
} else {
|
||
finalArr.push(id)
|
||
finalCategory.push(needCopyCategoryIds[index])
|
||
}
|
||
})
|
||
|
||
finalCategory.forEach((category, index) => {
|
||
const categoryArr = categoryMap.get(category) ?? []
|
||
const copyCategoryArr = needCopyMap.get(category)!
|
||
let copyId = finalArr[index]
|
||
|
||
let newData = copyCategoryArr.find(
|
||
(copyItem: any) => copyItem[0] === copyId
|
||
)
|
||
newData = [
|
||
newData[0],
|
||
newData[1].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
newData[2],
|
||
newData[3].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
]
|
||
categoryArr.push(newData)
|
||
categoryMap.set(category, categoryArr)
|
||
})
|
||
} else {
|
||
// categoryMap实际没有值 需要重新生成
|
||
let newCategoryMap = new Map()
|
||
for (let [key, needCopyItemsArr] of needCopyMap) {
|
||
let arr = needCopyItemsArr.map((item: any) => [
|
||
item[0],
|
||
item[1].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
item[2],
|
||
item[3].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
])
|
||
newCategoryMap.set(key, arr)
|
||
}
|
||
data.set(imageId, newCategoryMap)
|
||
}
|
||
} else {
|
||
let newCategoryMap = new Map()
|
||
for (let [key, needCopyItemsArr] of needCopyMap) {
|
||
let arr = needCopyItemsArr.map((item: any) => [
|
||
item[0],
|
||
item[1].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
item[2],
|
||
item[3].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
])
|
||
newCategoryMap.set(key, arr)
|
||
}
|
||
data.set(imageId, newCategoryMap)
|
||
}
|
||
})
|
||
notifications.show({ message: "复制成功", color: "green" })
|
||
useLabelStore.getState().setLabel(data)
|
||
useLabelStore.getState().pushStateStack(data)
|
||
useBottomToolsStore.getState().setSelectedItems("", false, false)
|
||
setConfirmModalOpened(false)
|
||
} catch (error) {
|
||
console.log(error)
|
||
notifications.show({ message: "复制失败", color: "red" })
|
||
}
|
||
}
|
||
|
||
let flag = existedIds.find((item) => copyArr.includes(item))
|
||
if (flag) {
|
||
setConfirmModalCallback(() => handleData)
|
||
setConfirmModalOpened(true)
|
||
return
|
||
} else {
|
||
handleData()
|
||
}
|
||
}
|
||
}, [activeImage])
|
||
|
||
const [rasterInited, setRasterInited] = useState<boolean>(false)
|
||
const rasterLoadRequestIdRef = useRef(0)
|
||
|
||
const clearRenderedAnnotationItems = useCallback(() => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
group
|
||
.getItems({})
|
||
.filter((item) => item.data?.name !== "pic")
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
}, [])
|
||
|
||
const scaleRenderedAnnotationItemsInPlace = useCallback(
|
||
(scaleRatio: number, currentRaster?: paper.Raster) => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
if (!Number.isFinite(scaleRatio) || scaleRatio <= 0) return
|
||
|
||
group.children.forEach((item) => {
|
||
if (item === currentRaster) return
|
||
if (item.data?.name === "pic") return
|
||
if (item.data?.type === "tag") return
|
||
if (item.data?.type === "groupPath") return
|
||
item.scale(scaleRatio, new paper.Point(0, 0))
|
||
})
|
||
},
|
||
[]
|
||
)
|
||
|
||
const renderActiveImagePolygonsBatched = useCallback(
|
||
(imageId: string, imageData?: ImageLabelData) => {
|
||
const expectedImage =
|
||
imageId || useBottomToolsStore.getState().activeImage
|
||
const taskImageData = imageData
|
||
const jobId = ++polygonRenderJobIdRef.current
|
||
polygonRenderRunningRef.current = true
|
||
|
||
clearRenderedAnnotationItems()
|
||
|
||
if (!expectedImage || !taskImageData?.size) {
|
||
polygonRenderRunningRef.current = false
|
||
if (jobId !== polygonRenderJobIdRef.current) return
|
||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||
renderGroupPath()
|
||
renderTag()
|
||
syncRenderModeVisuals()
|
||
return
|
||
}
|
||
|
||
const tasks: Array<{
|
||
operationId: string
|
||
paths: LabelPathItem[]
|
||
}> = []
|
||
|
||
objectOperations?.forEach((operation) => {
|
||
const operationId = operation.category_id.toString()
|
||
const paths = taskImageData.get(operationId)
|
||
if (!paths?.length) return
|
||
|
||
for (
|
||
let start = 0;
|
||
start < paths.length;
|
||
start += POLYGON_RENDER_BATCH_SIZE
|
||
) {
|
||
tasks.push({
|
||
operationId,
|
||
paths: paths.slice(start, start + POLYGON_RENDER_BATCH_SIZE),
|
||
})
|
||
}
|
||
})
|
||
|
||
if (!tasks.length) {
|
||
polygonRenderRunningRef.current = false
|
||
if (jobId !== polygonRenderJobIdRef.current) return
|
||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||
renderGroupPath()
|
||
renderTag()
|
||
syncRenderModeVisuals()
|
||
return
|
||
}
|
||
|
||
let cursor = 0
|
||
const runBatch = () => {
|
||
if (jobId !== polygonRenderJobIdRef.current) {
|
||
polygonRenderRunningRef.current = false
|
||
return
|
||
}
|
||
if (useBottomToolsStore.getState().activeImage !== expectedImage) {
|
||
polygonRenderRunningRef.current = false
|
||
return
|
||
}
|
||
|
||
const now =
|
||
typeof globalThis.performance?.now === "function"
|
||
? () => globalThis.performance.now()
|
||
: () => Date.now()
|
||
const startedAt = now()
|
||
|
||
while (cursor < tasks.length) {
|
||
const task = tasks[cursor]
|
||
renderPolygons(task.operationId, taskImageData, task.paths)
|
||
cursor += 1
|
||
|
||
if (now() - startedAt >= POLYGON_RENDER_BATCH_BUDGET_MS) {
|
||
break
|
||
}
|
||
}
|
||
|
||
if (cursor < tasks.length) {
|
||
if (typeof globalThis.requestAnimationFrame === "function") {
|
||
globalThis.requestAnimationFrame(() => runBatch())
|
||
} else {
|
||
setTimeout(runBatch, 0)
|
||
}
|
||
return
|
||
}
|
||
|
||
polygonRenderRunningRef.current = false
|
||
if (jobId !== polygonRenderJobIdRef.current) return
|
||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||
renderGroupPath()
|
||
renderTag()
|
||
syncRenderModeVisuals()
|
||
}
|
||
|
||
if (typeof globalThis.requestAnimationFrame === "function") {
|
||
globalThis.requestAnimationFrame(() => runBatch())
|
||
} else {
|
||
setTimeout(runBatch, 0)
|
||
}
|
||
},
|
||
[
|
||
clearRenderedAnnotationItems,
|
||
objectOperations,
|
||
renderPolygons,
|
||
renderTag,
|
||
syncRenderModeVisuals,
|
||
]
|
||
)
|
||
|
||
const withPaperGroup = useCallback(
|
||
<T,>(targetGroup: paper.Group, runner: () => T) => {
|
||
const previousGroup = usePaperStore.getState().group
|
||
setGroup(targetGroup)
|
||
try {
|
||
return runner()
|
||
} finally {
|
||
setGroup(previousGroup)
|
||
}
|
||
},
|
||
[setGroup]
|
||
)
|
||
|
||
const isPendingFrameSwitchStale = useCallback(
|
||
(imageName: string, token: number) => {
|
||
const currentState = useBottomToolsStore.getState()
|
||
return (
|
||
currentState.frameSwitchToken !== token ||
|
||
currentState.frameSwitchStatus !== "preparing" ||
|
||
currentState.pendingImage !== imageName
|
||
)
|
||
},
|
||
[]
|
||
)
|
||
|
||
const reconcileRasterScaleForImage = useCallback(
|
||
(imageName: string, scale: number) => {
|
||
const currentImgScale = usePaperStore.getState().rasterScale[imageName]
|
||
const currentLabelRenderScale = labelRenderScaleRef.current[imageName]
|
||
const scaleEpsilon = 0.000001
|
||
|
||
if (!currentLabelRenderScale) {
|
||
setRasterScale(imageName, scale)
|
||
labelRenderScaleRef.current[imageName] = scale
|
||
if (useLabelStore.getState().label.get(imageName)) {
|
||
const newData = adjustPoints(
|
||
imageName,
|
||
useLabelStore.getState().label,
|
||
scale
|
||
)
|
||
useLabelStore.getState().setLabel(newData)
|
||
}
|
||
const stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(imageName, item, scale))
|
||
)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (Math.abs(currentLabelRenderScale - scale) > scaleEpsilon) {
|
||
const scaleRatio = scale / currentLabelRenderScale
|
||
setRasterScale(imageName, scale)
|
||
labelRenderScaleRef.current[imageName] = scale
|
||
const newData = adjustPoints(
|
||
imageName,
|
||
useLabelStore.getState().label,
|
||
scaleRatio
|
||
)
|
||
useLabelStore.getState().setLabel(newData)
|
||
const stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(imageName, item, scaleRatio))
|
||
)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (currentImgScale !== scale) {
|
||
setRasterScale(imageName, scale)
|
||
}
|
||
},
|
||
[setRasterScale]
|
||
)
|
||
|
||
const renderImagePolygonsBatchedToGroup = useCallback(
|
||
(
|
||
imageId: string,
|
||
imageData: ImageLabelData | undefined,
|
||
targetGroup: paper.Group,
|
||
token: number
|
||
) =>
|
||
new Promise<boolean>((resolve) => {
|
||
if (!imageData?.size) {
|
||
resolve(true)
|
||
return
|
||
}
|
||
|
||
const tasks: Array<{
|
||
operationId: string
|
||
paths: LabelPathItem[]
|
||
}> = []
|
||
|
||
objectOperations?.forEach((operation) => {
|
||
const operationId = operation.category_id.toString()
|
||
const paths = imageData.get(operationId)
|
||
if (!paths?.length) return
|
||
for (
|
||
let start = 0;
|
||
start < paths.length;
|
||
start += POLYGON_RENDER_BATCH_SIZE
|
||
) {
|
||
tasks.push({
|
||
operationId,
|
||
paths: paths.slice(start, start + POLYGON_RENDER_BATCH_SIZE),
|
||
})
|
||
}
|
||
})
|
||
|
||
if (!tasks.length) {
|
||
syncRenderModeVisuals(targetGroup)
|
||
resolve(true)
|
||
return
|
||
}
|
||
|
||
let cursor = 0
|
||
const runBatch = () => {
|
||
if (isPendingFrameSwitchStale(imageId, token)) {
|
||
resolve(false)
|
||
return
|
||
}
|
||
|
||
const now =
|
||
typeof globalThis.performance?.now === "function"
|
||
? () => globalThis.performance.now()
|
||
: () => Date.now()
|
||
const startedAt = now()
|
||
|
||
while (cursor < tasks.length) {
|
||
const task = tasks[cursor]
|
||
withPaperGroup(targetGroup, () => {
|
||
renderPolygons(task.operationId, imageData, task.paths, imageId)
|
||
})
|
||
cursor += 1
|
||
|
||
if (now() - startedAt >= POLYGON_RENDER_BATCH_BUDGET_MS) {
|
||
break
|
||
}
|
||
}
|
||
|
||
if (cursor < tasks.length) {
|
||
if (typeof globalThis.requestAnimationFrame === "function") {
|
||
globalThis.requestAnimationFrame(() => runBatch())
|
||
} else {
|
||
setTimeout(runBatch, 0)
|
||
}
|
||
return
|
||
}
|
||
|
||
syncRenderModeVisuals(targetGroup)
|
||
resolve(true)
|
||
}
|
||
|
||
if (typeof globalThis.requestAnimationFrame === "function") {
|
||
globalThis.requestAnimationFrame(() => runBatch())
|
||
} else {
|
||
setTimeout(runBatch, 0)
|
||
}
|
||
}),
|
||
[
|
||
isPendingFrameSwitchStale,
|
||
objectOperations,
|
||
renderPolygons,
|
||
syncRenderModeVisuals,
|
||
withPaperGroup,
|
||
]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (!setup || loadingData) return
|
||
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
|
||
syncRenderModeVisuals(group)
|
||
|
||
if (renderMode) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: { top: 0, left: 0 },
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
}
|
||
}, [
|
||
activeImage,
|
||
activeImageLabel,
|
||
currentSelectedPathKey,
|
||
loadingData,
|
||
renderMode,
|
||
setup,
|
||
syncRenderModeVisuals,
|
||
])
|
||
|
||
const stopRenderModePan = useCallback(() => {
|
||
renderModePanRef.current.active = false
|
||
}, [])
|
||
|
||
const handleRenderModeOverlayMouseDown = useCallback(
|
||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||
event.preventDefault()
|
||
event.stopPropagation()
|
||
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: { top: 0, left: 0 },
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
|
||
if (event.button !== 1) return
|
||
|
||
renderModePanRef.current = {
|
||
active: true,
|
||
lastX: event.clientX,
|
||
lastY: event.clientY,
|
||
}
|
||
},
|
||
[]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (!renderMode) {
|
||
stopRenderModePan()
|
||
return
|
||
}
|
||
|
||
const handleMouseMove = (event: MouseEvent) => {
|
||
const currentPan = renderModePanRef.current
|
||
if (!currentPan.active) return
|
||
|
||
event.preventDefault()
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
|
||
const deltaX = event.clientX - currentPan.lastX
|
||
const deltaY = event.clientY - currentPan.lastY
|
||
if (!deltaX && !deltaY) return
|
||
|
||
group.position = group.position.add(new paper.Point(deltaX, deltaY))
|
||
renderModePanRef.current.lastX = event.clientX
|
||
renderModePanRef.current.lastY = event.clientY
|
||
}
|
||
|
||
const handleMouseUp = (event: MouseEvent) => {
|
||
if (event.button === 1) {
|
||
event.preventDefault()
|
||
}
|
||
stopRenderModePan()
|
||
}
|
||
|
||
window.addEventListener("mousemove", handleMouseMove, { passive: false })
|
||
window.addEventListener("mouseup", handleMouseUp, { passive: false })
|
||
|
||
return () => {
|
||
window.removeEventListener("mousemove", handleMouseMove)
|
||
window.removeEventListener("mouseup", handleMouseUp)
|
||
stopRenderModePan()
|
||
}
|
||
}, [renderMode, stopRenderModePan])
|
||
|
||
const getTargetGroupPosition = useCallback(
|
||
({
|
||
defaultPosition,
|
||
preservedPosition,
|
||
canPreserve,
|
||
}: {
|
||
defaultPosition: paper.Point
|
||
preservedPosition?: paper.Point | null
|
||
canPreserve?: boolean
|
||
}) => {
|
||
if (saveCurrentScale && canPreserve && preservedPosition) {
|
||
return clonePaperPoint(preservedPosition) ?? defaultPosition.clone()
|
||
}
|
||
return defaultPosition.clone()
|
||
},
|
||
[saveCurrentScale]
|
||
)
|
||
|
||
const preparePendingFrameSwitch = useCallback(
|
||
async (imageName: string, token: number) => {
|
||
if (
|
||
!imageName ||
|
||
!projectDetail?.id ||
|
||
!imgSize?.clientWidth ||
|
||
!imgSize?.clientHeight
|
||
) {
|
||
clearPendingFrameSwitch(token)
|
||
return
|
||
}
|
||
|
||
const currentVisibleGroup = usePaperStore.getState().group
|
||
if (!currentVisibleGroup) {
|
||
clearPendingFrameSwitch(token)
|
||
return
|
||
}
|
||
const preserveGroupPosition =
|
||
saveCurrentScale &&
|
||
currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0
|
||
const preservedGroupPosition = preserveGroupPosition
|
||
? clonePaperPoint(currentVisibleGroup.position)
|
||
: null
|
||
|
||
const stagingGroup = new paper.Group({
|
||
applyMatrix: false,
|
||
selected: true,
|
||
strokeScaling: false,
|
||
visible: false,
|
||
})
|
||
stagingGroup.scaling = new paper.Point(
|
||
currentVisibleGroup.scaling.x,
|
||
currentVisibleGroup.scaling.y
|
||
)
|
||
stagingGroup.position = new paper.Point(
|
||
currentVisibleGroup.position.x,
|
||
currentVisibleGroup.position.y
|
||
)
|
||
|
||
const cleanupStagingGroup = () => {
|
||
try {
|
||
stagingGroup.removeChildren()
|
||
} catch (error) {
|
||
void error
|
||
}
|
||
try {
|
||
stagingGroup.remove()
|
||
} catch (error) {
|
||
void error
|
||
}
|
||
}
|
||
|
||
try {
|
||
const decodedImage = await decodeFrameImageRef.current(imageName)
|
||
if (!decodedImage?.src || isPendingFrameSwitchStale(imageName, token)) {
|
||
cleanupStagingGroup()
|
||
return
|
||
}
|
||
|
||
const raster = renderRaster(stagingGroup, {
|
||
source: decodedImage.src,
|
||
visible: false,
|
||
data: {
|
||
name: "pic",
|
||
imageId: imageName,
|
||
requestId: token,
|
||
},
|
||
})
|
||
|
||
await new Promise<void>((resolve, reject) => {
|
||
raster.onLoad = () => resolve()
|
||
raster.onError = () => reject(new Error("pending raster load failed"))
|
||
})
|
||
|
||
if (isPendingFrameSwitchStale(imageName, token)) {
|
||
cleanupStagingGroup()
|
||
return
|
||
}
|
||
|
||
const canvasWidth =
|
||
imgSize?.clientWidth ?? paperScope.current?.view.bounds.width
|
||
const canvasHeight =
|
||
imgSize?.clientHeight ?? paperScope.current?.view.bounds.height
|
||
|
||
if (!canvasWidth || !canvasHeight) {
|
||
cleanupStagingGroup()
|
||
clearPendingFrameSwitch(token)
|
||
return
|
||
}
|
||
|
||
const scaleX = canvasWidth / raster.bounds.width
|
||
const scaleY = canvasHeight / raster.bounds.height
|
||
const scale = Math.min(scaleX, scaleY)
|
||
|
||
if (!Number.isFinite(scale) || scale <= 0) {
|
||
cleanupStagingGroup()
|
||
clearPendingFrameSwitch(token)
|
||
return
|
||
}
|
||
|
||
setRasterSize(imageName, [raster.bounds.width, raster.bounds.height])
|
||
raster.scale(scale, new paper.Point(0, 0))
|
||
reconcileRasterScaleForImage(imageName, scale)
|
||
raster.position = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
const centeredGroupPosition = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
stagingGroup.position = getTargetGroupPosition({
|
||
defaultPosition: centeredGroupPosition,
|
||
preservedPosition: preservedGroupPosition,
|
||
canPreserve: preserveGroupPosition,
|
||
})
|
||
raster.sendToBack()
|
||
|
||
const imageData = useLabelStore.getState().label.get(imageName)
|
||
const renderOk = await renderImagePolygonsBatchedToGroup(
|
||
imageName,
|
||
imageData,
|
||
stagingGroup,
|
||
token
|
||
)
|
||
|
||
if (!renderOk || isPendingFrameSwitchStale(imageName, token)) {
|
||
cleanupStagingGroup()
|
||
return
|
||
}
|
||
|
||
const previousGroup = usePaperStore.getState().group
|
||
setGroup(stagingGroup)
|
||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||
renderedImageRef.current = imageName
|
||
commitFrameSwitch(imageName, token)
|
||
renderGroupPath()
|
||
renderTag()
|
||
stagingGroup.visible = true
|
||
stagingGroup.bringToFront()
|
||
|
||
if (previousGroup && previousGroup !== stagingGroup) {
|
||
previousGroup.remove()
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
cleanupStagingGroup()
|
||
if (!isPendingFrameSwitchStale(imageName, token)) {
|
||
clearPendingFrameSwitch(token)
|
||
notifications.show({
|
||
color: "red",
|
||
message: "切帧失败,请重试",
|
||
autoClose: 2500,
|
||
})
|
||
}
|
||
}
|
||
},
|
||
[
|
||
clearPendingFrameSwitch,
|
||
commitFrameSwitch,
|
||
getTargetGroupPosition,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
isPendingFrameSwitchStale,
|
||
projectDetail?.id,
|
||
reconcileRasterScaleForImage,
|
||
renderImagePolygonsBatchedToGroup,
|
||
renderTag,
|
||
saveCurrentScale,
|
||
setGroup,
|
||
setRasterPath,
|
||
setRasterSize,
|
||
]
|
||
)
|
||
|
||
const decodeFrameImage = useCallback(
|
||
async (imageName: string) => {
|
||
if (!imageName || !projectDetail?.id) return null
|
||
|
||
const cached = frameImageCacheRef.current.get(imageName)
|
||
if (cached) return cached
|
||
|
||
const inFlight = frameDecodeTaskRef.current.get(imageName)
|
||
if (inFlight) return inFlight
|
||
|
||
const task = (async () => {
|
||
let text = useImagesStore.getState().images.get(imageName)
|
||
if (!text) {
|
||
const response = await getServerImage({
|
||
data_names: [imageName],
|
||
data_type: 0,
|
||
project_id: projectDetail.id,
|
||
})
|
||
text = `data:image/jpeg;base64,${response}`
|
||
const images = useImagesStore.getState().images
|
||
images.set(imageName, text)
|
||
useImagesStore.getState().setImages(images)
|
||
}
|
||
|
||
if (!text) return null
|
||
|
||
const image = await new Promise<HTMLImageElement | null>((resolve) => {
|
||
const img = new Image()
|
||
img.onload = () => resolve(img)
|
||
img.onerror = () => resolve(null)
|
||
img.src = text!
|
||
})
|
||
|
||
if (image) {
|
||
frameImageCacheRef.current.set(imageName, image)
|
||
}
|
||
frameDecodeTaskRef.current.delete(imageName)
|
||
return image
|
||
})().catch((error) => {
|
||
frameDecodeTaskRef.current.delete(imageName)
|
||
throw error
|
||
})
|
||
|
||
frameDecodeTaskRef.current.set(imageName, task)
|
||
return task
|
||
},
|
||
[projectDetail?.id]
|
||
)
|
||
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 (
|
||
rasterFadeRafIdRef.current !== null &&
|
||
typeof globalThis.cancelAnimationFrame === "function"
|
||
) {
|
||
globalThis.cancelAnimationFrame(rasterFadeRafIdRef.current)
|
||
}
|
||
rasterFadeRafIdRef.current = null
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
cancelRasterFadeTransition()
|
||
}
|
||
}, [cancelRasterFadeTransition])
|
||
|
||
const renderAndScaleRaster = useCallback(
|
||
async (paperGroup: paper.Group) => {
|
||
if (activeImage && projectDetail?.id) {
|
||
const requestId = ++rasterLoadRequestIdRef.current
|
||
const requestImage = activeImage
|
||
const preserveGroupPosition =
|
||
saveCurrentScale && getPicRasters(paperGroup).length > 0
|
||
const preservedGroupPosition = preserveGroupPosition
|
||
? clonePaperPoint(paperGroup.position)
|
||
: null
|
||
rasterLoadingImageRef.current = requestImage
|
||
cancelRasterFadeTransition()
|
||
getPicRasters(paperGroup).forEach((item) => {
|
||
item.opacity = 1
|
||
})
|
||
const isStaleRasterRequest = () => {
|
||
const currentImage = useBottomToolsStore.getState().activeImage
|
||
const stale =
|
||
requestId !== rasterLoadRequestIdRef.current ||
|
||
currentImage !== requestImage
|
||
if (stale) {
|
||
console.warn("[PaperContainer] ignore stale raster callback", {
|
||
requestId,
|
||
latestRequestId: rasterLoadRequestIdRef.current,
|
||
requestImage,
|
||
currentImage,
|
||
})
|
||
}
|
||
return stale
|
||
}
|
||
|
||
notifications.show({
|
||
message: "图片加载中...",
|
||
loading: true,
|
||
autoClose: false,
|
||
id: "loadingRaster",
|
||
})
|
||
try {
|
||
let raster
|
||
const decodedImage = await decodeFrameImage(requestImage)
|
||
if (isStaleRasterRequest()) return
|
||
if (!decodedImage?.src) return
|
||
raster = renderRaster(paperGroup, {
|
||
source: decodedImage.src,
|
||
visible: false,
|
||
data: {
|
||
name: "pic",
|
||
imageId: requestImage,
|
||
requestId,
|
||
},
|
||
})
|
||
if (!raster) return
|
||
|
||
raster.onLoad = function () {
|
||
if (isStaleRasterRequest()) {
|
||
raster.remove()
|
||
return
|
||
}
|
||
// Scale the raster to fit the canvas while maintaining aspect ratio
|
||
let canvasWidth =
|
||
imgSize?.clientWidth ?? paperScope.current?.view.bounds.width
|
||
let canvasHeight =
|
||
imgSize?.clientHeight ?? paperScope.current?.view.bounds.height
|
||
|
||
if (!canvasWidth || !canvasHeight) {
|
||
console.warn(
|
||
"[PaperContainer] skip raster scale: invalid canvas",
|
||
{
|
||
requestId,
|
||
requestImage,
|
||
canvasWidth,
|
||
canvasHeight,
|
||
}
|
||
)
|
||
raster.remove()
|
||
return
|
||
}
|
||
|
||
let scaleX = canvasWidth! / raster.bounds.width
|
||
let scaleY = canvasHeight! / raster.bounds.height
|
||
setRasterSize(requestImage, [
|
||
raster.bounds.width,
|
||
raster.bounds.height,
|
||
])
|
||
|
||
let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio
|
||
if (!Number.isFinite(scale) || scale <= 0) {
|
||
console.warn(
|
||
"[PaperContainer] skip raster scale: invalid scale",
|
||
{
|
||
requestId,
|
||
requestImage,
|
||
scale,
|
||
scaleX,
|
||
scaleY,
|
||
}
|
||
)
|
||
raster.remove()
|
||
return
|
||
}
|
||
|
||
// 将raster以(0,0)为中心点进行缩放
|
||
raster.scale(scale, new paper.Point(0, 0))
|
||
const currentImgScale =
|
||
usePaperStore.getState().rasterScale[requestImage]
|
||
const currentLabelRenderScale =
|
||
labelRenderScaleRef.current[requestImage]
|
||
const scaleEpsilon = 0.000001
|
||
console.debug("[PaperContainer] raster scale reconcile", {
|
||
image: requestImage,
|
||
currentImgScale,
|
||
currentLabelRenderScale,
|
||
nextScale: scale,
|
||
branch: !currentLabelRenderScale
|
||
? "init-scale"
|
||
: Math.abs(currentLabelRenderScale - scale) > scaleEpsilon
|
||
? "rescale"
|
||
: "keep",
|
||
})
|
||
// 当前图片无已应用的渲染比例时,按原始坐标转换到当前显示比例
|
||
if (!currentLabelRenderScale) {
|
||
setRasterScale(requestImage, scale)
|
||
labelRenderScaleRef.current[requestImage] = scale
|
||
if (useLabelStore.getState().label.get(requestImage)) {
|
||
const newData = adjustPoints(
|
||
requestImage,
|
||
useLabelStore.getState().label,
|
||
scale
|
||
)
|
||
useLabelStore.getState().setLabel(newData)
|
||
}
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(requestImage, item, scale))
|
||
)
|
||
}
|
||
} else if (
|
||
Math.abs(currentLabelRenderScale - scale) > scaleEpsilon
|
||
) {
|
||
const scaleRatio = scale / currentLabelRenderScale
|
||
setRasterScale(requestImage, scale)
|
||
labelRenderScaleRef.current[requestImage] = scale
|
||
const newData = adjustPoints(
|
||
requestImage,
|
||
useLabelStore.getState().label,
|
||
scaleRatio
|
||
)
|
||
useLabelStore.getState().setLabel(newData)
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) =>
|
||
adjustPoints(requestImage, item, scaleRatio)
|
||
)
|
||
)
|
||
}
|
||
} else if (currentImgScale !== scale) {
|
||
setRasterScale(requestImage, scale)
|
||
}
|
||
raster.position = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
const centeredGroupPosition = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
raster.visible = true
|
||
const previousRasters = getPicRasters(paperGroup).filter(
|
||
(item) => item !== raster
|
||
)
|
||
|
||
if (
|
||
RASTER_CROSSFADE_DURATION_MS > 0 &&
|
||
previousRasters.length > 0 &&
|
||
typeof globalThis.requestAnimationFrame === "function"
|
||
) {
|
||
const transitionId = ++rasterFadeTransitionIdRef.current
|
||
const startTime =
|
||
typeof performance !== "undefined"
|
||
? performance.now()
|
||
: Date.now()
|
||
raster.opacity = 0
|
||
const step = (timestamp: number) => {
|
||
if (transitionId !== rasterFadeTransitionIdRef.current) return
|
||
if (isStaleRasterRequest()) {
|
||
raster.remove()
|
||
rasterFadeRafIdRef.current = null
|
||
return
|
||
}
|
||
const now =
|
||
typeof timestamp === "number" && Number.isFinite(timestamp)
|
||
? timestamp
|
||
: typeof performance !== "undefined"
|
||
? performance.now()
|
||
: Date.now()
|
||
const progress = Math.min(
|
||
1,
|
||
(now - startTime) / RASTER_CROSSFADE_DURATION_MS
|
||
)
|
||
raster.opacity = progress
|
||
if (progress >= 1) {
|
||
previousRasters.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
rasterFadeRafIdRef.current = null
|
||
return
|
||
}
|
||
rasterFadeRafIdRef.current =
|
||
globalThis.requestAnimationFrame(step)
|
||
}
|
||
rasterFadeRafIdRef.current =
|
||
globalThis.requestAnimationFrame(step)
|
||
} else {
|
||
raster.opacity = 1
|
||
previousRasters.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
}
|
||
|
||
paperGroup.position = getTargetGroupPosition({
|
||
defaultPosition: centeredGroupPosition,
|
||
preservedPosition: preservedGroupPosition,
|
||
canPreserve: preserveGroupPosition,
|
||
})
|
||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||
raster.sendToBack()
|
||
renderedImageRef.current = requestImage
|
||
setRasterInited(true)
|
||
usePaperStore.getState().group?.bringToFront()
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
} finally {
|
||
if (requestId === rasterLoadRequestIdRef.current) {
|
||
rasterLoadingImageRef.current = null
|
||
}
|
||
notifications.hide("loadingRaster")
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
cancelRasterFadeTransition,
|
||
decodeFrameImage,
|
||
getPicRasters,
|
||
getTargetGroupPosition,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
projectDetail?.id,
|
||
saveCurrentScale,
|
||
setRasterPath,
|
||
setRasterScale,
|
||
setRasterSize,
|
||
]
|
||
)
|
||
|
||
const loadImageAndPolygons = useCallback(
|
||
async (group: paper.Group) => {
|
||
clearRenderedAnnotationItems()
|
||
setRasterInited(false)
|
||
renderAndScaleRaster(group)
|
||
},
|
||
[clearRenderedAnnotationItems, renderAndScaleRaster]
|
||
)
|
||
|
||
useEffect(() => {
|
||
const focusImage = pendingImage || activeImage
|
||
if (!focusImage) return
|
||
|
||
void decodeFrameImage(focusImage).catch((error) => {
|
||
void error
|
||
})
|
||
|
||
const currentIndex = allItems.findIndex((item) => item === focusImage)
|
||
if (currentIndex < 0) {
|
||
const keepSet = new Set<string>([focusImage, activeImage].filter(Boolean))
|
||
for (const key of Array.from(frameImageCacheRef.current.keys())) {
|
||
if (!keepSet.has(key)) frameImageCacheRef.current.delete(key)
|
||
}
|
||
for (const key of Array.from(frameDecodeTaskRef.current.keys())) {
|
||
if (!keepSet.has(key)) frameDecodeTaskRef.current.delete(key)
|
||
}
|
||
return
|
||
}
|
||
|
||
const prefetchStart = Math.max(0, currentIndex - FRAME_PREFETCH_RADIUS)
|
||
const prefetchEnd = Math.min(
|
||
allItems.length - 1,
|
||
currentIndex + FRAME_PREFETCH_RADIUS
|
||
)
|
||
const prefetchSet = new Set<string>()
|
||
for (let index = prefetchStart; index <= prefetchEnd; index += 1) {
|
||
const imageName = allItems[index]
|
||
if (!imageName) continue
|
||
prefetchSet.add(imageName)
|
||
void decodeFrameImage(imageName).catch((error) => {
|
||
void error
|
||
})
|
||
}
|
||
|
||
const keepStart = Math.max(0, currentIndex - FRAME_CACHE_KEEP_RADIUS)
|
||
const keepEnd = Math.min(
|
||
allItems.length - 1,
|
||
currentIndex + FRAME_CACHE_KEEP_RADIUS
|
||
)
|
||
const keepSet = new Set<string>()
|
||
for (let index = keepStart; index <= keepEnd; index += 1) {
|
||
const imageName = allItems[index]
|
||
if (imageName) keepSet.add(imageName)
|
||
}
|
||
keepSet.add(focusImage)
|
||
if (activeImage) keepSet.add(activeImage)
|
||
|
||
for (const key of Array.from(frameImageCacheRef.current.keys())) {
|
||
if (!keepSet.has(key)) frameImageCacheRef.current.delete(key)
|
||
}
|
||
for (const key of Array.from(frameDecodeTaskRef.current.keys())) {
|
||
if (!keepSet.has(key) && !prefetchSet.has(key)) {
|
||
frameDecodeTaskRef.current.delete(key)
|
||
}
|
||
}
|
||
}, [activeImage, allItems, decodeFrameImage, pendingImage])
|
||
|
||
useEffect(() => {
|
||
if (
|
||
frameSwitchStatus !== "preparing" ||
|
||
!pendingImage ||
|
||
pendingImage === activeImage
|
||
) {
|
||
return
|
||
}
|
||
|
||
polygonRenderJobIdRef.current += 1
|
||
polygonRenderRunningRef.current = false
|
||
void preparePendingFrameSwitch(pendingImage, frameSwitchToken)
|
||
}, [
|
||
activeImage,
|
||
frameSwitchStatus,
|
||
frameSwitchToken,
|
||
pendingImage,
|
||
preparePendingFrameSwitch,
|
||
])
|
||
|
||
const resizeCurrentImage = useCallback(() => {
|
||
if (!imgSize?.clientWidth || !imgSize?.clientHeight) return false
|
||
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return false
|
||
|
||
const rasterItems = getPicRasters(group)
|
||
const activeRasters = rasterItems.filter(
|
||
(item) => item.data?.imageId === activeImage
|
||
)
|
||
const currentRaster =
|
||
(activeRasters.length
|
||
? activeRasters[activeRasters.length - 1]
|
||
: undefined) ||
|
||
(rasterItems.length ? rasterItems[rasterItems.length - 1] : undefined)
|
||
if (!currentRaster) return false
|
||
const preservedGroupPosition = saveCurrentScale
|
||
? clonePaperPoint(group.position)
|
||
: null
|
||
|
||
const rasterSizeStore = usePaperStore.getState().rasterSize
|
||
const rawRasterSize = rasterSizeStore[activeImage] ?? [
|
||
currentRaster.width,
|
||
currentRaster.height,
|
||
]
|
||
if (!rawRasterSize?.[0] || !rawRasterSize?.[1]) return false
|
||
if (!rasterSizeStore[activeImage]) {
|
||
setRasterSize(activeImage, [rawRasterSize[0], rawRasterSize[1]])
|
||
}
|
||
|
||
const nextScale = Math.min(
|
||
imgSize.clientWidth / rawRasterSize[0],
|
||
imgSize.clientHeight / rawRasterSize[1]
|
||
)
|
||
if (!Number.isFinite(nextScale) || nextScale <= 0) return false
|
||
|
||
const currentScale =
|
||
labelRenderScaleRef.current[activeImage] ??
|
||
usePaperStore.getState().rasterScale[activeImage]
|
||
if (!currentScale || !Number.isFinite(currentScale) || currentScale <= 0)
|
||
return false
|
||
|
||
const scaleEpsilon = 0.000001
|
||
const scaleRatio = nextScale / currentScale
|
||
|
||
if (Math.abs(scaleRatio - 1) <= scaleEpsilon) return true
|
||
|
||
if (Math.abs(scaleRatio - 1) > scaleEpsilon) {
|
||
currentRaster.scale(scaleRatio, new paper.Point(0, 0))
|
||
scaleRenderedAnnotationItemsInPlace(scaleRatio, currentRaster)
|
||
|
||
const currentLabel = useLabelStore.getState().label
|
||
if (currentLabel.get(activeImage)) {
|
||
useLabelStore
|
||
.getState()
|
||
.setLabel(adjustPoints(activeImage, currentLabel, scaleRatio))
|
||
}
|
||
|
||
const stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(activeImage, item, scaleRatio))
|
||
)
|
||
}
|
||
}
|
||
|
||
currentRaster.position = new paper.Point(
|
||
currentRaster.bounds.width / 2,
|
||
currentRaster.bounds.height / 2
|
||
)
|
||
const centeredGroupPosition = new paper.Point(
|
||
currentRaster.bounds.width / 2,
|
||
currentRaster.bounds.height / 2
|
||
)
|
||
group.position = getTargetGroupPosition({
|
||
defaultPosition: centeredGroupPosition,
|
||
preservedPosition: preservedGroupPosition,
|
||
canPreserve: saveCurrentScale,
|
||
})
|
||
|
||
setRasterPath(new paper.Path.Rectangle(currentRaster.bounds))
|
||
setRasterScale(activeImage, nextScale)
|
||
labelRenderScaleRef.current[activeImage] = nextScale
|
||
|
||
renderGroupPath()
|
||
renderTag()
|
||
|
||
group.bringToFront()
|
||
return true
|
||
}, [
|
||
activeImage,
|
||
getPicRasters,
|
||
getTargetGroupPosition,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
renderTag,
|
||
saveCurrentScale,
|
||
scaleRenderedAnnotationItemsInPlace,
|
||
setRasterPath,
|
||
setRasterScale,
|
||
setRasterSize,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (loadingData) return
|
||
if (!imgSize?.clientWidth || !imgSize?.clientHeight) return
|
||
const currentGroup = usePaperStore.getState().group
|
||
if (currentGroup && projectDetail) {
|
||
const activeImageChanged = prevActiveImageRef.current !== activeImage
|
||
prevActiveImageRef.current = activeImage
|
||
const hasPicRaster = getPicRasters(currentGroup).length > 0
|
||
|
||
if (!useTopToolsStore.getState().saveCurrentScale) {
|
||
currentGroup.scaling = new paper.Point(1, 1)
|
||
useTopToolsStore.getState().setScale(1)
|
||
} else {
|
||
usePaperStore
|
||
.getState()
|
||
.setGroupScale(useTopToolsStore.getState().scale)
|
||
}
|
||
|
||
if (!activeImageChanged) {
|
||
if (resizeCurrentImage()) {
|
||
return
|
||
}
|
||
if (hasPicRaster || rasterLoadingImageRef.current === activeImage) {
|
||
return
|
||
}
|
||
}
|
||
|
||
if (renderedImageRef.current === activeImage && hasPicRaster) {
|
||
resizeCurrentImage()
|
||
return
|
||
}
|
||
|
||
if (rasterLoadingImageRef.current === activeImage) {
|
||
return
|
||
}
|
||
|
||
loadImageAndPolygons(currentGroup)
|
||
}
|
||
}, [
|
||
activeImage,
|
||
getPicRasters,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
loadImageAndPolygons,
|
||
loadingData,
|
||
projectDetail,
|
||
resizeCurrentImage,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (rasterInited) {
|
||
renderActiveImagePolygonsBatched(activeImage, activeImageLabel)
|
||
setRasterInited(false)
|
||
}
|
||
}, [
|
||
activeImage,
|
||
activeImageLabel,
|
||
rasterInited,
|
||
renderActiveImagePolygonsBatched,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (polygonRenderRunningRef.current) return
|
||
renderGroupPath()
|
||
}, [activeImage, activeImageLabel])
|
||
|
||
useEffect(() => {
|
||
if (polygonRenderRunningRef.current) return
|
||
renderTag()
|
||
}, [activeImage, showTags, showTagsConfigs, activeImageLabel, renderTag])
|
||
|
||
const [ids, setIds] = useState<number[]>([])
|
||
const [existIds, setExistIds] = useState<number[]>([])
|
||
// select use existIds todo
|
||
false && console.log(existIds)
|
||
|
||
const [comments, setComments] = useState<string[]>([])
|
||
|
||
const form = useForm<any>({
|
||
initialValues: {
|
||
id: null,
|
||
operation: null,
|
||
sub_attributes: {},
|
||
check_box_comments: [],
|
||
text_comments: "",
|
||
},
|
||
})
|
||
const checkboxComments = form.values.check_box_comments
|
||
const textComment = form.values.text_comments
|
||
|
||
const filterOperations = useMemo(() => {
|
||
let currentLabelType = objectOperations.find(
|
||
(operation) => operation.category_id.toString() === operationId
|
||
)?.label_type
|
||
|
||
return objectOperations.filter(
|
||
(operation) => operation.label_type === currentLabelType
|
||
)
|
||
}, [objectOperations, operationId])
|
||
|
||
useEffect(() => {
|
||
if (projectDetail?.comment_rule) {
|
||
if (projectDetail?.comment_rule?.check_box_rules?.length) {
|
||
setComments(
|
||
projectDetail?.comment_rule?.check_box_rules.map((item) => item.text)
|
||
)
|
||
}
|
||
}
|
||
}, [projectDetail?.comment_rule])
|
||
|
||
const pathItem = useMemo(() => {
|
||
return storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.find((item) => item[0] === id)
|
||
}, [activeImage, id, operationId, storeLabel])
|
||
|
||
useLayoutEffect(() => {
|
||
if (!showContextMenu) return
|
||
const container = containerRef.current
|
||
const menu = contextMenuRef.current
|
||
if (!container || !menu) return
|
||
|
||
const safeMargin = 8
|
||
const updateMenuLayout = () => {
|
||
const availableWidth = Math.max(container.clientWidth - safeMargin * 2, 0)
|
||
const availableHeight = Math.max(
|
||
container.clientHeight - safeMargin * 2,
|
||
0
|
||
)
|
||
const menuWidth = Math.min(menu.offsetWidth, availableWidth)
|
||
const menuHeight = Math.min(menu.offsetHeight, availableHeight)
|
||
const nextLeft = Math.min(
|
||
Math.max(position.left, safeMargin),
|
||
Math.max(container.clientWidth - menuWidth - safeMargin, safeMargin)
|
||
)
|
||
const nextTop = Math.min(
|
||
Math.max(position.top, safeMargin),
|
||
Math.max(container.clientHeight - menuHeight - safeMargin, safeMargin)
|
||
)
|
||
|
||
setContextMenuLayout((prev) => {
|
||
if (
|
||
prev.left === nextLeft &&
|
||
prev.top === nextTop &&
|
||
prev.maxHeight === availableHeight &&
|
||
prev.maxWidth === availableWidth
|
||
) {
|
||
return prev
|
||
}
|
||
return {
|
||
left: nextLeft,
|
||
top: nextTop,
|
||
maxHeight: availableHeight,
|
||
maxWidth: availableWidth,
|
||
}
|
||
})
|
||
}
|
||
|
||
updateMenuLayout()
|
||
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
updateMenuLayout()
|
||
})
|
||
resizeObserver.observe(container)
|
||
resizeObserver.observe(menu)
|
||
|
||
return () => {
|
||
resizeObserver.disconnect()
|
||
}
|
||
}, [position.left, position.top, showContextMenu])
|
||
|
||
// 打开弹窗时,初始化form数据
|
||
useEffect(() => {
|
||
if (showContextMenu) {
|
||
form.reset()
|
||
|
||
const labelData = useLabelStore.getState().label
|
||
const currentPath = labelData
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.find((item) => item[0] === id)
|
||
|
||
form.setFieldValue("id", id.toString())
|
||
form.setFieldValue("operation", operationId)
|
||
if (currentPath?.[2].sub_attributes) {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
let obj: {
|
||
[key: string]: string | string[]
|
||
} = {}
|
||
Object.entries(currentPath?.[2].sub_attributes).map(([key, value]) => {
|
||
let select_mode = operationSchema?.sub_attributes_describe?.find(
|
||
(item) => item.chinese_name === key
|
||
)?.select_mode
|
||
switch (select_mode) {
|
||
case 2:
|
||
obj[key] = Array.isArray(value)
|
||
? value
|
||
: (value as string)
|
||
.split(",")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
return
|
||
default:
|
||
obj[key] = value as string
|
||
return
|
||
}
|
||
})
|
||
form.setFieldValue("sub_attributes", obj)
|
||
}
|
||
if (currentPath?.[2].comment) {
|
||
const nowComment =
|
||
currentPath?.[2].comment[currentPath?.[2].comment.length - 1]
|
||
form.setFieldValue(
|
||
"check_box_comments",
|
||
nowComment && nowComment.check_box_comments.length
|
||
? nowComment.check_box_comments
|
||
: []
|
||
)
|
||
form.setFieldValue(
|
||
"text_comments",
|
||
nowComment && nowComment.text_comments.length
|
||
? nowComment.text_comments[0]
|
||
: ""
|
||
)
|
||
}
|
||
// 可选的ids
|
||
let images = Array.from(labelState.keys()).filter(
|
||
(key) => key !== activeImage
|
||
)
|
||
let ids: number[] = [id]
|
||
images.map((imageId) => {
|
||
// 可切换成其他图片同一类型的id
|
||
filterOperations.map((operation) => {
|
||
labelData
|
||
.get(imageId)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])?.length &&
|
||
ids.push(
|
||
...labelData
|
||
.get(imageId)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])!
|
||
)
|
||
})
|
||
})
|
||
let existIds: number[] = []
|
||
objectOperations.forEach((operation) => {
|
||
labelData
|
||
.get(activeImage)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])?.length &&
|
||
existIds.push(
|
||
...labelData
|
||
.get(activeImage)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])!
|
||
)
|
||
})
|
||
setExistIds(Array.from(new Set(existIds)))
|
||
|
||
ids.push(
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
)
|
||
let selectIds = ids.filter(
|
||
(candidateId) => candidateId === id || !existIds.includes(candidateId)
|
||
)
|
||
|
||
setIds(Array.from(new Set(selectIds)))
|
||
} else {
|
||
// console.log(form.getFieldValue("id"), id);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
activeImage,
|
||
filterOperations,
|
||
getOperationSchema,
|
||
id,
|
||
labelState,
|
||
objectOperations,
|
||
operationId,
|
||
showContextMenu,
|
||
])
|
||
|
||
// 监听当前标注对象批注值变化
|
||
useEffect(() => {
|
||
if (!showContextMenu) return
|
||
if (!id || !operationId || !activeImage) return
|
||
if (useTopToolsStore.getState().currentTimeKey === "label") return
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(activeImage, operationId, id)
|
||
if (!currentDetail) return
|
||
const { comment } = currentDetail
|
||
let updatedComment: any[] = []
|
||
// 当表单当前值跟标注对象detail中comment值相同时不更新detail中comment状态
|
||
if (
|
||
checkCommentsIsSame(
|
||
comment[comment.length - 1].check_box_comments,
|
||
comment[comment.length - 1].text_comments,
|
||
checkboxComments,
|
||
textComment ? [textComment] : []
|
||
)
|
||
)
|
||
return
|
||
// 无批注
|
||
if ((!checkboxComments || !checkboxComments.length) && !textComment) {
|
||
updatedComment = comment.map((item: Comment, index: number) =>
|
||
index < comment.length - 1
|
||
? item
|
||
: {
|
||
...item,
|
||
comment_type: 1,
|
||
date: dayjs().unix(),
|
||
check_box_comments: [],
|
||
text_comments: [],
|
||
}
|
||
)
|
||
} else {
|
||
let latestItem = comment[comment.length - 1]
|
||
latestItem = {
|
||
...latestItem,
|
||
comment_type: 2,
|
||
date: dayjs().unix(),
|
||
check_box_comments:
|
||
checkboxComments && checkboxComments.length ? checkboxComments : [],
|
||
text_comments: textComment ? [textComment] : [],
|
||
}
|
||
updatedComment = comment.map((item: Comment, index: number) =>
|
||
index < comment.length - 1 ? item : latestItem
|
||
)
|
||
}
|
||
useLabelStore
|
||
.getState()
|
||
.updateLabelDetailDataByKeys(activeImage, operationId, id, {
|
||
...currentDetail,
|
||
comment: updatedComment,
|
||
})
|
||
}, [
|
||
activeImage,
|
||
checkboxComments,
|
||
id,
|
||
operationId,
|
||
showContextMenu,
|
||
textComment,
|
||
])
|
||
|
||
const newDetail = useMemo(() => {
|
||
const currentDetail = getLabelDetailDataByKeys(activeImage, operationId, id)
|
||
const detail = currentDetail
|
||
? {
|
||
...currentDetail,
|
||
first_modified_timestamp:
|
||
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid:
|
||
currentDetail.first_modified_uid ||
|
||
usePermissionStore.getState().user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid: usePermissionStore.getState().user_id,
|
||
}
|
||
: {
|
||
...initialDetail,
|
||
create_timestamp: dayjs().unix(),
|
||
}
|
||
return detail
|
||
}, [activeImage, getLabelDetailDataByKeys, id, operationId])
|
||
|
||
const handleChangeContextMenuId = useCallback(
|
||
(newId: number) => {
|
||
// 设置选中id
|
||
updateSelectedPath(activeImage, "DELETE", id)
|
||
updateSelectedPath(activeImage, "ADD", newId)
|
||
// 修改多边形data.id storelabel中 id detail
|
||
if (getItemById(id)) getItemById(id)!.data.id = newId
|
||
setOperation(activeImage, operationId, [
|
||
newId,
|
||
pathItem?.[1] || [],
|
||
newDetail,
|
||
pathItem?.[3] || [],
|
||
])
|
||
deleteOperation(activeImage, operationId, id)
|
||
// 设置菜单id
|
||
setContextMenuId(newId)
|
||
useRightToolsStore.getState().pushPathId(newId)
|
||
},
|
||
[
|
||
activeImage,
|
||
deleteOperation,
|
||
getItemById,
|
||
id,
|
||
newDetail,
|
||
operationId,
|
||
pathItem,
|
||
setContextMenuId,
|
||
setOperation,
|
||
updateSelectedPath,
|
||
]
|
||
)
|
||
|
||
const handleChangeContextMenuOperation = useCallback(
|
||
(newOperation: string) => {
|
||
const operationSchema = getOperationSchema(newOperation)
|
||
const strokeColor = operationSchema
|
||
? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||
: "rgb(0,0,0)"
|
||
const fillColor = operationSchema
|
||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||
: "rgba(0,0,0,0.3)"
|
||
const blankColor = operationSchema
|
||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||
: "rgba(0,0,0,0.01)"
|
||
if (getItemsById(id)) {
|
||
getItemsById(id).forEach((item) => {
|
||
const data = item.data
|
||
if (data.operationId) {
|
||
data.operationId = newOperation
|
||
if (data.type !== "text")
|
||
item.strokeColor = new paper.Color(strokeColor)
|
||
if (data.type !== "brush" && data.type !== "point")
|
||
item.fillColor = new paper.Color(fillColor)
|
||
item.data =
|
||
data.type !== "mask" && data.type !== "text"
|
||
? Object.assign(data || {}, {
|
||
fillColor,
|
||
blankColor,
|
||
strokeColor,
|
||
})
|
||
: data
|
||
// // 如果是组合类型 则遍历将children的data变更
|
||
// if (item instanceof paper.CompoundPath) {
|
||
// item.children.forEach((path) => {
|
||
// path.strokeColor = new paper.Color(strokeColor);
|
||
// path.fillColor = new paper.Color(fillColor);
|
||
// path.data = Object.assign(path.data || {}, {
|
||
// operationId: newOperation,
|
||
// fillColor,
|
||
// blankColor,
|
||
// strokeColor,
|
||
// });
|
||
// });
|
||
// }
|
||
}
|
||
})
|
||
}
|
||
// newOperation的subAttributes
|
||
let subAttr = Object.assign(
|
||
{},
|
||
useTopToolsStore.getState().subAttributes[
|
||
operationSchema?.label_class ?? ""
|
||
]
|
||
)
|
||
// 修改labeldata
|
||
setOperation(activeImage, newOperation, [
|
||
id,
|
||
pathItem?.[1] || [],
|
||
{ ...newDetail, sub_attributes: subAttr },
|
||
pathItem?.[3] || [],
|
||
])
|
||
deleteOperation(activeImage, operationId, id)
|
||
// 修改context的operationId
|
||
setContextMenuOperationId(newOperation)
|
||
},
|
||
[
|
||
activeImage,
|
||
deleteOperation,
|
||
getItemsById,
|
||
getOperationSchema,
|
||
id,
|
||
newDetail,
|
||
operationId,
|
||
pathItem,
|
||
setContextMenuOperationId,
|
||
setOperation,
|
||
]
|
||
)
|
||
|
||
const handleChangeContextMenuSubAttr = useCallback(
|
||
(key: string, value: string) => {
|
||
setOperation(activeImage, operationId, [
|
||
id,
|
||
pathItem?.[1] || [],
|
||
{
|
||
...newDetail,
|
||
...(pathItem?.[2] || {}),
|
||
sub_attributes: Object.assign(newDetail.sub_attributes || {}, {
|
||
[key]: value,
|
||
}),
|
||
},
|
||
pathItem?.[3] || [],
|
||
])
|
||
},
|
||
[activeImage, id, newDetail, operationId, pathItem, setOperation]
|
||
)
|
||
|
||
const renderSubAttrLabel = useCallback((name: string, required?: boolean) => {
|
||
return (
|
||
<Text component="span" size="xs" fw={500}>
|
||
{name}
|
||
<Text
|
||
component="span"
|
||
size="10px"
|
||
fw={700}
|
||
c={required ? "red" : "dimmed"}>
|
||
{required ? "(必填)" : "(选填)"}
|
||
</Text>
|
||
</Text>
|
||
)
|
||
}, [])
|
||
|
||
const contextMenu = useMemo(() => {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
const currentType = useTopToolsStore.getState().currentTimeKey
|
||
const textCommentsInputProps = form.getInputProps("text_comments")
|
||
|
||
return (
|
||
<Card
|
||
ref={contextMenuRef}
|
||
data-label-context-menu="true"
|
||
shadow="sm"
|
||
padding="xs"
|
||
radius="md"
|
||
withBorder
|
||
onWheel={(event) => {
|
||
event.stopPropagation()
|
||
}}
|
||
style={{
|
||
position: "absolute",
|
||
zIndex: 1000,
|
||
width: 224,
|
||
maxWidth: contextMenuLayout.maxWidth
|
||
? `${contextMenuLayout.maxWidth}px`
|
||
: undefined,
|
||
maxHeight: contextMenuLayout.maxHeight
|
||
? `${contextMenuLayout.maxHeight}px`
|
||
: undefined,
|
||
height: "fit-content",
|
||
display: !showContextMenu ? "none" : "block",
|
||
top: `${contextMenuLayout.top}px`,
|
||
left: `${contextMenuLayout.left}px`,
|
||
overflowY: "auto",
|
||
overflowX: "hidden",
|
||
overscrollBehavior: "contain",
|
||
wordBreak: "break-word",
|
||
}}>
|
||
<Box p="xs">
|
||
<Stack gap="xs">
|
||
<Select
|
||
label="ID"
|
||
size="xs"
|
||
placeholder="请选择ID!"
|
||
data={ids.map((item) => item.toString())}
|
||
searchable
|
||
// creatable
|
||
// getCreateLabel={(query) => `+ Add ${query}`}
|
||
// onCreate={(query) => {
|
||
// const id = +query
|
||
// if (isNaN(id) || ids.includes(id) || existIds.includes(id))
|
||
// return null
|
||
// setIds([...ids, id])
|
||
// return query
|
||
// }}
|
||
required
|
||
{...form.getInputProps("id")}
|
||
onChange={(value) => {
|
||
form.setFieldValue("id", value)
|
||
if (value && +value !== id) {
|
||
handleChangeContextMenuId(+value)
|
||
}
|
||
}}
|
||
/>
|
||
<Select
|
||
label="类型"
|
||
size="xs"
|
||
placeholder="请选择类型!"
|
||
data={filterOperations.map((operation) => ({
|
||
label: operation.label_class,
|
||
value: operation.category_id.toString(),
|
||
}))}
|
||
leftSection={
|
||
form.values.operation &&
|
||
renderOperationIcon(
|
||
getOperationSchema(form.values.operation.toString())
|
||
)
|
||
}
|
||
required
|
||
{...form.getInputProps("operation")}
|
||
onChange={(value) => {
|
||
form.setFieldValue("operation", value)
|
||
if (value && value !== operationId) {
|
||
handleChangeContextMenuOperation(value)
|
||
}
|
||
}}
|
||
/>
|
||
<Box
|
||
p="xs"
|
||
style={{
|
||
borderRadius: "2px",
|
||
border: "1px solid var(--mantine-color-blue-light)",
|
||
}}>
|
||
<Text c="blue" fw={600} mb="xs">
|
||
批注
|
||
</Text>
|
||
<ScrollArea.Autosize mah={48} mb="xs">
|
||
<Checkbox.Group {...form.getInputProps("check_box_comments")}>
|
||
<Stack gap="xs">
|
||
{comments.map((item) => (
|
||
<Checkbox
|
||
key={item}
|
||
value={item}
|
||
label={item}
|
||
disabled={currentType === "label"}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Checkbox.Group>
|
||
</ScrollArea.Autosize>
|
||
<Textarea
|
||
autosize
|
||
minRows={2}
|
||
maxRows={2}
|
||
disabled={currentType === "label"}
|
||
{...textCommentsInputProps}
|
||
onFocus={(event) => {
|
||
textCommentsInputProps.onFocus?.(event)
|
||
useKeyEventStore.getState().setFocusInput(true)
|
||
}}
|
||
onBlur={(event) => {
|
||
textCommentsInputProps.onBlur?.(event)
|
||
useKeyEventStore.getState().setFocusInput(false)
|
||
}}
|
||
/>
|
||
</Box>
|
||
<Box
|
||
px="xs"
|
||
pt="xs"
|
||
style={{
|
||
borderRadius: "2px",
|
||
border: "1px solid var(--mantine-color-blue-light)",
|
||
display: !operationSchema?.sub_attributes?.length
|
||
? "none"
|
||
: "block",
|
||
}}>
|
||
{operationSchema?.sub_attributes?.length ? (
|
||
<Group justify="space-between" align="center" mx="xs" mb="xs">
|
||
<Text c="blue" fw={600}>
|
||
子属性
|
||
</Text>
|
||
<Text size="10px" c="dimmed">
|
||
<Text component="span" c="red" fw={700}>
|
||
必填
|
||
</Text>
|
||
项需填写
|
||
</Text>
|
||
</Group>
|
||
) : null}
|
||
<ScrollArea.Autosize mah={280}>
|
||
{operationSchema?.sub_attributes_describe.map((item) => {
|
||
const label = renderSubAttrLabel(
|
||
item.chinese_name,
|
||
!!item.isrequired
|
||
)
|
||
const fieldWrapperStyle = {
|
||
borderRadius: "6px",
|
||
border: item.isrequired
|
||
? "1px solid rgba(250, 82, 82, 0.24)"
|
||
: "1px solid rgba(34, 139, 230, 0.14)",
|
||
background: item.isrequired
|
||
? "rgba(250, 82, 82, 0.04)"
|
||
: "rgba(34, 139, 230, 0.03)",
|
||
}
|
||
|
||
if (item.select_mode === 0) {
|
||
return (
|
||
<Box
|
||
key={item.chinese_name}
|
||
p="xs"
|
||
mb="xs"
|
||
style={fieldWrapperStyle}>
|
||
<TextInput
|
||
label={label}
|
||
size="xs"
|
||
placeholder={`请输入${item.chinese_name}`}
|
||
required={!!item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(e) => {
|
||
form
|
||
.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)
|
||
.onChange(e)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
e.target.value
|
||
)
|
||
}}
|
||
/>
|
||
</Box>
|
||
)
|
||
} else if (item.select_mode === 1) {
|
||
return (
|
||
<Box
|
||
key={item.chinese_name}
|
||
p="xs"
|
||
mb="xs"
|
||
style={fieldWrapperStyle}>
|
||
<Radio.Group
|
||
label={label}
|
||
size="xs"
|
||
required={!!item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(value) => {
|
||
form.setFieldValue(
|
||
`sub_attributes.${item.chinese_name}`,
|
||
value
|
||
)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
value
|
||
)
|
||
}}>
|
||
<Stack gap={6} mt={6}>
|
||
{item.optional_item.map((option) => (
|
||
<Radio
|
||
key={option}
|
||
value={option}
|
||
label={option}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Radio.Group>
|
||
</Box>
|
||
)
|
||
} else if (item.select_mode === 2) {
|
||
return (
|
||
<Box
|
||
key={item.chinese_name}
|
||
p="xs"
|
||
mb="xs"
|
||
style={fieldWrapperStyle}>
|
||
<Checkbox.Group
|
||
label={label}
|
||
size="xs"
|
||
required={!!item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(value) => {
|
||
form.setFieldValue(
|
||
`sub_attributes.${item.chinese_name}`,
|
||
value
|
||
)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
value.toString()
|
||
)
|
||
}}>
|
||
<Stack gap={6} mt={6}>
|
||
{item.optional_item.map((option) => (
|
||
<Checkbox
|
||
key={option}
|
||
value={option}
|
||
label={option}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Checkbox.Group>
|
||
</Box>
|
||
)
|
||
}
|
||
return null
|
||
})}
|
||
</ScrollArea.Autosize>
|
||
</Box>
|
||
</Stack>
|
||
</Box>
|
||
</Card>
|
||
)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
comments,
|
||
filterOperations,
|
||
getOperationSchema,
|
||
handleChangeContextMenuId,
|
||
handleChangeContextMenuOperation,
|
||
handleChangeContextMenuSubAttr,
|
||
id,
|
||
ids,
|
||
operationId,
|
||
form.values,
|
||
position.left,
|
||
position.top,
|
||
renderSubAttrLabel,
|
||
showContextMenu,
|
||
])
|
||
|
||
const imageFilterCss = useMemo(() => {
|
||
return {
|
||
brightness: +(1 + imageFilter.brightness / 100).toFixed(2),
|
||
saturate: +(1 + imageFilter.saturate / 100).toFixed(2),
|
||
contrast: +(1 + imageFilter.contrast / 100).toFixed(2),
|
||
}
|
||
}, [imageFilter.brightness, imageFilter.contrast, imageFilter.saturate])
|
||
|
||
const handleCreatePathGroup = useCallback(() => {
|
||
let selectedPaths =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
let groupId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathGroupIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
if (selectedPaths && selectedPaths.length > 1) {
|
||
// 修改group Map
|
||
useRightToolsStore
|
||
.getState()
|
||
.addPathGroupInMap(activeImage, groupId, selectedPaths)
|
||
// push groupId
|
||
useRightToolsStore.getState().pushPathGroupId(groupId)
|
||
}
|
||
}, [activeImage])
|
||
|
||
const crosshairComponentRef = useRef<any>(null)
|
||
const assistShapeComponentRef = useRef<any>(null)
|
||
|
||
const handleCrosshairMove = (event: { clientX: number; clientY: number }) => {
|
||
crosshairComponentRef.current?.updateCrosshair(event)
|
||
assistShapeComponentRef.current?.updateAssistShape(event)
|
||
}
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
handleCreatePathGroup,
|
||
renderPolygons,
|
||
handleCrosshairMove,
|
||
renderTrackingAnnotation,
|
||
renderSupportAnnotation,
|
||
saveSupportAnnotationData,
|
||
copyAnnotationDataToMultiFrame,
|
||
}))
|
||
|
||
return (
|
||
<Box
|
||
w="100%"
|
||
h="100%"
|
||
pos="relative"
|
||
style={{
|
||
flex: 1,
|
||
filter: `brightness(${imageFilterCss.brightness}) saturate(${imageFilterCss.saturate}) contrast(${imageFilterCss.contrast})`,
|
||
}}
|
||
id="paper-container"
|
||
ref={containerRef}>
|
||
{crosshairStatus !== "hidden" && (
|
||
<CrosshairComponent
|
||
isCarved={crosshairStatus === "carve"}
|
||
ref={crosshairComponentRef}
|
||
/>
|
||
)}
|
||
{assistToolEnabled && (
|
||
<AssistShapeComponent
|
||
size={assistToolSize}
|
||
ref={assistShapeComponentRef}
|
||
/>
|
||
)}
|
||
{imgSize?.clientWidth && imgSize?.clientHeight && (
|
||
<canvas
|
||
id="paper-canvas"
|
||
width={imgSize?.clientWidth}
|
||
height={imgSize?.clientHeight}
|
||
ref={canvasElem}
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
zIndex: 40,
|
||
width: imgSize?.clientWidth,
|
||
height: imgSize?.clientHeight,
|
||
display: "block",
|
||
}}
|
||
/>
|
||
)}
|
||
{resizeGhost && (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
alt=""
|
||
aria-hidden
|
||
draggable={false}
|
||
src={resizeGhost.src}
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
zIndex: 45,
|
||
width: resizeGhost.fromWidth,
|
||
height: resizeGhost.fromHeight,
|
||
pointerEvents: "none",
|
||
userSelect: "none",
|
||
transformOrigin: "top left",
|
||
transform: resizeGhost.animating
|
||
? resizeGhost.fadeOnly
|
||
? "scale(1, 1)"
|
||
: `scale(${resizeGhost.scale})`
|
||
: "scale(1, 1)",
|
||
opacity: resizeGhost.animating ? 0 : 1,
|
||
transition: resizeGhost.animating
|
||
? `transform ${RESIZE_INTERPOLATION_DURATION_MS}ms ease-out, opacity ${RESIZE_INTERPOLATION_DURATION_MS}ms ease-out`
|
||
: "none",
|
||
}}
|
||
/>
|
||
)}
|
||
{frameSwitchGhost && (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
alt=""
|
||
aria-hidden
|
||
draggable={false}
|
||
src={frameSwitchGhost.src}
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
zIndex: 46,
|
||
width: frameSwitchGhost.width,
|
||
height: frameSwitchGhost.height,
|
||
pointerEvents: "none",
|
||
userSelect: "none",
|
||
transformOrigin: "top left",
|
||
opacity: frameSwitchGhost.animating ? 0 : 1,
|
||
transition: frameSwitchGhost.animating
|
||
? `opacity ${RASTER_CROSSFADE_DURATION_MS}ms ease-out`
|
||
: "none",
|
||
}}
|
||
/>
|
||
)}
|
||
{renderMode && (
|
||
<Box
|
||
aria-hidden
|
||
onContextMenu={(event) => {
|
||
event.preventDefault()
|
||
}}
|
||
onMouseDown={handleRenderModeOverlayMouseDown}
|
||
style={{
|
||
position: "absolute",
|
||
inset: 0,
|
||
zIndex: 47,
|
||
background: "rgba(128, 128, 128, 0.24)",
|
||
pointerEvents: "auto",
|
||
}}
|
||
/>
|
||
)}
|
||
{contextMenu}
|
||
<Modal
|
||
opened={confirmModalOpened}
|
||
onClose={() => setConfirmModalOpened(false)}
|
||
title="提示"
|
||
centered>
|
||
<Text>其他帧已存在相同id的对象,是否覆盖?</Text>
|
||
<Group justify="flex-end" mt="md">
|
||
<Button
|
||
variant="default"
|
||
onClick={() => {
|
||
useBottomToolsStore.getState().setSelectedItems("", false, false)
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
if (confirmModalCallback) confirmModalCallback()
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
否
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
if (confirmModalCallback) confirmModalCallback(true)
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
是
|
||
</Button>
|
||
</Group>
|
||
</Modal>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default forwardRef(PaperContainer)
|