Files
labelimage/components/label/components/PaperContainer.tsx
2026-04-10 13:51:52 +08:00

4134 lines
131 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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以00为中心点进行缩放
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)