2630 lines
86 KiB
TypeScript
2630 lines
86 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Checkbox,
|
||
Group,
|
||
Modal,
|
||
Radio,
|
||
ScrollArea,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
Textarea,
|
||
TextInput,
|
||
} from "@mantine/core"
|
||
import { useForm } from "@mantine/form"
|
||
import { notifications } from "@mantine/notifications"
|
||
import dayjs from "dayjs"
|
||
import msgpack from "msgpack-lite"
|
||
import paper from "paper"
|
||
import React, {
|
||
DispatchWithoutAction,
|
||
forwardRef,
|
||
useCallback,
|
||
useEffect,
|
||
useImperativeHandle,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from "react"
|
||
import {
|
||
getAuxiliaryAnnotation,
|
||
getAuxiliaryAnnotation2,
|
||
getServerImage,
|
||
getTrackingAuxiliaryAnnotation,
|
||
} from "../api/label"
|
||
import { Project } from "../api/project/typing"
|
||
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 { 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 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]
|
||
|
||
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 PaperContainer = (
|
||
props: PaperComponentProps,
|
||
ref: React.Ref<unknown> | undefined
|
||
) => {
|
||
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
|
||
const containerRef = useRef<any>(null)
|
||
const [imgSize, setImageSize] = useState<{
|
||
clientWidth: number
|
||
clientHeight: number
|
||
}>()
|
||
|
||
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 resizeObserver = new ResizeObserver((entries) => {
|
||
for (let entry of entries) {
|
||
if (entry.target === container) {
|
||
setImageSize({
|
||
clientWidth: entry.contentRect.width,
|
||
clientHeight: entry.contentRect.height,
|
||
})
|
||
if (!firstRender) setFirstRender(true)
|
||
}
|
||
}
|
||
})
|
||
if (container) {
|
||
resizeObserver.observe(container)
|
||
}
|
||
|
||
return () => {
|
||
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,
|
||
setRasterPath,
|
||
setRasterSize,
|
||
setRasterScale,
|
||
loadingData,
|
||
} = usePaperStore()
|
||
|
||
const { activeImage } = useBottomToolsStore()
|
||
|
||
const storeLabel = useLabelStore((state) => state.label)
|
||
const deleteOperation = useLabelStore((state) => state.deleteOperation)
|
||
const setOperation = useLabelStore((state) => state.setOperation)
|
||
const getLabelDetailDataByKeys = useLabelStore(
|
||
(state) => state.getLabelDetailDataByKeys
|
||
)
|
||
|
||
const {
|
||
objectOperations,
|
||
imageFilter,
|
||
crosshairStatus,
|
||
showTags,
|
||
showTagsConfigs,
|
||
} = useTopToolsStore()
|
||
|
||
const {
|
||
pathStatus: paperPathStatus,
|
||
selectedPath,
|
||
updateSelectedPath,
|
||
} = useObjectStore()
|
||
|
||
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 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])
|
||
|
||
useEffect(() => {
|
||
if (loadingData) {
|
||
labelRenderScaleRef.current = {}
|
||
renderedImageRef.current = ""
|
||
const group = usePaperStore.getState().group
|
||
if (group) {
|
||
group.removeChildren()
|
||
}
|
||
setRasterInited(false)
|
||
}
|
||
}, [loadingData])
|
||
|
||
const renderSupportAnnotation = useCallback(
|
||
async (
|
||
points: Array<[number, number]>,
|
||
tags: number[],
|
||
clear_previous_state = false
|
||
) => {
|
||
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 = () => {
|
||
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 = usePaperStore.getState().group!
|
||
const raster = usePaperStore
|
||
.getState()
|
||
.group!.getItems({ data: { name: "pic" } })[0]
|
||
if (!raster.contains(drawPoint.position)) {
|
||
throw new Error("点位超出图片范围")
|
||
}
|
||
})
|
||
}
|
||
console.log(points, tags)
|
||
// 绘制点位
|
||
clearSupportAnnotationItem()
|
||
draw()
|
||
|
||
const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||
|
||
const params = {
|
||
project_id: projectDetail?.id || 0,
|
||
file_name: activeImage,
|
||
image_hw:
|
||
picSize && picSize.length ? [picSize[1], picSize[0]] : [0, 0],
|
||
points_and_box: points.map(([x, y]) => [
|
||
Math.round(x / currentScale),
|
||
Math.round(y / currentScale),
|
||
]),
|
||
tags,
|
||
clear_previous_state,
|
||
}
|
||
try {
|
||
const res = await getAuxiliaryAnnotation(
|
||
msgpack.encode(params) as any
|
||
)
|
||
const data = msgpack.decode(new Uint8Array(res))
|
||
console.log(data)
|
||
if (data.msg === "success") {
|
||
const { contours, hollows } = data
|
||
const outerPaths: any[] = []
|
||
const innerPaths: any[] = []
|
||
contours.forEach((contour: any, index: number) => {
|
||
if (contour.length > 3) {
|
||
const path = new paper.Path({
|
||
segments: contour.map(([x, y]: [number, number]) => [
|
||
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 {
|
||
// path.scaling = usePaperStore.getState().group!.scaling;
|
||
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]
|
||
)
|
||
|
||
false && renderSupportAnnotation
|
||
|
||
const renderSupportAnnotation2 = useCallback(
|
||
async (
|
||
points: Array<[number, number]>,
|
||
tags: number[],
|
||
clear_previous_state = false
|
||
) => {
|
||
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 = () => {
|
||
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 = usePaperStore.getState().group!
|
||
const raster = usePaperStore
|
||
.getState()
|
||
.group!.getItems({ data: { name: "pic" } })[0]
|
||
if (!raster.contains(drawPoint.position)) {
|
||
throw new Error("点位超出图片范围")
|
||
}
|
||
})
|
||
}
|
||
console.log(points, tags)
|
||
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 promptBox =
|
||
// promptPoints.length > 1 ? getContourBox(promptPoints) : null
|
||
const projectId =
|
||
projectDetail?.name ||
|
||
projectDetail?.rpath ||
|
||
`${projectDetail?.id || ""}`
|
||
|
||
if (!projectId) {
|
||
throw new Error("缺少 project_id,无法调用模型接口")
|
||
}
|
||
if (!promptPoints.length) {
|
||
throw new Error("缺少有效点位,无法调用模型接口")
|
||
}
|
||
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||
|
||
try {
|
||
const data = await getAuxiliaryAnnotation2({
|
||
project_id: projectId,
|
||
image_name: activeImage,
|
||
// project_id: "image_test_bk",
|
||
prompt: {
|
||
foreground_points: promptPoints,
|
||
background_points: background_points,
|
||
// ...(promptBox ? { boxes: [promptBox] } : {}),
|
||
},
|
||
})
|
||
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 getBase64 = (file: any): Promise<string> =>
|
||
// new Promise((resolve, reject) => {
|
||
// const reader = new FileReader();
|
||
// reader.readAsDataURL(file);
|
||
// reader.onload = () => resolve(reader.result as string);
|
||
// reader.onerror = (error) => reject(error);
|
||
// });
|
||
|
||
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, renderSupportAnnotation2)
|
||
}
|
||
return () => {
|
||
newGroup.remove()
|
||
}
|
||
}
|
||
}, [
|
||
forceUpdate,
|
||
init,
|
||
initBrushTool,
|
||
initPanTool,
|
||
initPointTool,
|
||
initPolygonTool,
|
||
initRectangleTool,
|
||
initSupportTool,
|
||
initViewTool,
|
||
renderSupportAnnotation2,
|
||
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: Map<string, [number, any[], any, any[]][]> | undefined
|
||
) => {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
|
||
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 = 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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.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: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
})
|
||
.filter((path): path is paper.Path => !!path) || []
|
||
|
||
addCompoundPath(
|
||
renderCompoundPath,
|
||
outerPaths,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.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[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "rectangle",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
}
|
||
)
|
||
})
|
||
})
|
||
return
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
addBrush,
|
||
addCompoundPath,
|
||
addPoint,
|
||
addPointLine,
|
||
addPolygon,
|
||
addRectangle,
|
||
getOperationSchema,
|
||
paperPathStatus,
|
||
paperSelectedPath,
|
||
]
|
||
)
|
||
|
||
// 生成辅助标注结果数据并绘制
|
||
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 (paper 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 newData = {
|
||
...arr2.find((b: any) => b[0] === id),
|
||
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 {
|
||
data.set(imageId, needCopyMap)
|
||
}
|
||
})
|
||
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 renderAndScaleRaster = useCallback(
|
||
async (paperGroup: paper.Group) => {
|
||
// const raster = renderRaster(paperGroup, {
|
||
// source: `${process.env.NEXT_PUBLIC_URL}/api/v1/label_server/data/image?data_name=${activeImage}&project_path=${projectDetail?.rpath}`,
|
||
// });
|
||
|
||
if (activeImage && projectDetail?.id) {
|
||
const requestId = ++rasterLoadRequestIdRef.current
|
||
const requestImage = activeImage
|
||
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
|
||
}
|
||
|
||
console.log(usePaperStore.getState().paperScope)
|
||
// console.log(activeImage, projectDetail?.rpath);
|
||
notifications.show({
|
||
message: "图片加载中...",
|
||
loading: true,
|
||
autoClose: false,
|
||
id: "loadingRaster",
|
||
})
|
||
try {
|
||
let raster
|
||
let text
|
||
const images = useImagesStore.getState().images
|
||
const currentImageBase64Text = images.get(activeImage)
|
||
if (currentImageBase64Text) {
|
||
text = currentImageBase64Text
|
||
} else {
|
||
const response = await getServerImage({
|
||
data_names: [activeImage],
|
||
data_type: 0,
|
||
project_id: projectDetail?.id,
|
||
})
|
||
if (isStaleRasterRequest()) return
|
||
text = `data:image/jpeg;base64,${response}`
|
||
images.set(activeImage, text)
|
||
useImagesStore.getState().setImages(images)
|
||
}
|
||
if (isStaleRasterRequest()) return
|
||
if (!text) return
|
||
raster = renderRaster(paperGroup, {
|
||
source: text,
|
||
data: {
|
||
name: "pic",
|
||
},
|
||
})
|
||
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(activeImage, [
|
||
raster.bounds.width,
|
||
raster.bounds.height,
|
||
])
|
||
|
||
let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio
|
||
if (!Number.isFinite(scale) || scale <= 0) {
|
||
console.warn(
|
||
"[PaperContainer] skip raster scale: invalid scale",
|
||
{
|
||
requestId,
|
||
requestImage,
|
||
scale,
|
||
scaleX,
|
||
scaleY,
|
||
}
|
||
)
|
||
raster.remove()
|
||
return
|
||
}
|
||
|
||
// 将raster以(0,0)为中心点进行缩放
|
||
raster.scale(scale, new paper.Point(0, 0))
|
||
const currentImgScale =
|
||
usePaperStore.getState().rasterScale[activeImage]
|
||
const currentLabelRenderScale =
|
||
labelRenderScaleRef.current[activeImage]
|
||
const scaleEpsilon = 0.000001
|
||
console.debug("[PaperContainer] raster scale reconcile", {
|
||
image: activeImage,
|
||
currentImgScale,
|
||
currentLabelRenderScale,
|
||
nextScale: scale,
|
||
branch: !currentLabelRenderScale
|
||
? "init-scale"
|
||
: Math.abs(currentLabelRenderScale - scale) > scaleEpsilon
|
||
? "rescale"
|
||
: "keep",
|
||
})
|
||
// 当前图片无已应用的渲染比例时,按原始坐标转换到当前显示比例
|
||
if (!currentLabelRenderScale) {
|
||
setRasterScale(activeImage, scale)
|
||
labelRenderScaleRef.current[activeImage] = scale
|
||
if (useLabelStore.getState().label.get(activeImage)) {
|
||
const newData = adjustPoints(
|
||
activeImage,
|
||
useLabelStore.getState().label,
|
||
scale
|
||
)
|
||
// 处理hollowPoints
|
||
useLabelStore.getState().setLabel(newData)
|
||
}
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(activeImage, item, scale))
|
||
)
|
||
}
|
||
} else if (
|
||
Math.abs(currentLabelRenderScale - scale) > scaleEpsilon
|
||
) {
|
||
const scaleRatio = scale / currentLabelRenderScale
|
||
setRasterScale(activeImage, scale)
|
||
labelRenderScaleRef.current[activeImage] = scale
|
||
const newData = adjustPoints(
|
||
activeImage,
|
||
useLabelStore.getState().label,
|
||
scaleRatio
|
||
)
|
||
useLabelStore.getState().setLabel(newData)
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) =>
|
||
adjustPoints(activeImage, item, scaleRatio)
|
||
)
|
||
)
|
||
}
|
||
} else if (currentImgScale !== scale) {
|
||
setRasterScale(activeImage, scale)
|
||
}
|
||
// 调整raster的左上角到(0,0)
|
||
raster.position = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
// if (!useTopToolsStore.getState().saveCurrentScale)
|
||
usePaperStore.getState().group!.position = raster.position
|
||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||
// 将raster置底
|
||
raster.sendToBack()
|
||
renderedImageRef.current = activeImage
|
||
setRasterInited(true)
|
||
console.log(usePaperStore.getState().group)
|
||
usePaperStore.getState().group?.bringToFront()
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
} finally {
|
||
notifications.hide("loadingRaster")
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
|
||
projectDetail?.id,
|
||
setRasterPath,
|
||
setRasterScale,
|
||
setRasterSize,
|
||
]
|
||
)
|
||
|
||
const loadImageAndPolygons = useCallback(
|
||
async (
|
||
// imageData: Map<string, [number, any[], any][]> | undefined,
|
||
group: paper.Group
|
||
) => {
|
||
// 清除当前的 raster 和多边形
|
||
group.removeChildren()
|
||
// 添加新的 raster
|
||
setRasterInited(false)
|
||
renderAndScaleRaster(group)
|
||
// if (imageData) {
|
||
// objectOperations?.forEach((item) => {
|
||
// renderPolygons(item.category_id.toString(), imageData);
|
||
// });
|
||
// }
|
||
},
|
||
[renderAndScaleRaster]
|
||
)
|
||
|
||
const resizeCurrentImage = useCallback(() => {
|
||
if (!imgSize?.clientWidth || !imgSize?.clientHeight) return false
|
||
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return false
|
||
|
||
const currentRaster = group.getItems({ data: { name: "pic" } })?.[0] as
|
||
| paper.Raster
|
||
| undefined
|
||
if (!currentRaster) return false
|
||
|
||
const rawRasterSize = usePaperStore.getState().rasterSize[activeImage]
|
||
if (!rawRasterSize?.[0] || !rawRasterSize?.[1]) return false
|
||
|
||
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))
|
||
|
||
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
|
||
)
|
||
group.position = currentRaster.position
|
||
|
||
group
|
||
.getItems({})
|
||
.filter((item) => item !== currentRaster && item.data?.id)
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
|
||
setRasterPath(new paper.Path.Rectangle(currentRaster.bounds))
|
||
setRasterScale(activeImage, nextScale)
|
||
labelRenderScaleRef.current[activeImage] = nextScale
|
||
|
||
const imageData = useLabelStore.getState().label.get(activeImage)
|
||
if (imageData) {
|
||
objectOperations?.forEach((item) => {
|
||
renderPolygons(item.category_id.toString(), imageData)
|
||
})
|
||
renderGroupPath()
|
||
renderTag()
|
||
}
|
||
|
||
group.bringToFront()
|
||
return true
|
||
}, [
|
||
activeImage,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
objectOperations,
|
||
renderPolygons,
|
||
renderTag,
|
||
setRasterPath,
|
||
setRasterScale,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (loadingData) return
|
||
if (!imgSize?.clientWidth || !imgSize?.clientHeight) return
|
||
const currentGroup = usePaperStore.getState().group
|
||
if (currentGroup && projectDetail) {
|
||
if (!useTopToolsStore.getState().saveCurrentScale) {
|
||
currentGroup.scaling = new paper.Point(1, 1)
|
||
useTopToolsStore.getState().setScale(1)
|
||
} else {
|
||
usePaperStore
|
||
.getState()
|
||
.setGroupScale(useTopToolsStore.getState().scale)
|
||
}
|
||
|
||
if (
|
||
renderedImageRef.current === activeImage &&
|
||
currentGroup.getItems({ data: { name: "pic" } })?.length
|
||
) {
|
||
resizeCurrentImage()
|
||
return
|
||
}
|
||
|
||
loadImageAndPolygons(
|
||
// storeLabel.get(activeImage),
|
||
currentGroup
|
||
)
|
||
}
|
||
}, [
|
||
activeImage,
|
||
imgSize?.clientHeight,
|
||
imgSize?.clientWidth,
|
||
loadImageAndPolygons,
|
||
loadingData,
|
||
projectDetail,
|
||
resizeCurrentImage,
|
||
])
|
||
|
||
useEffect(() => {
|
||
if (rasterInited) {
|
||
if (storeLabel.get(activeImage)) {
|
||
objectOperations?.forEach((item) => {
|
||
renderPolygons(
|
||
item.category_id.toString(),
|
||
storeLabel.get(activeImage)
|
||
)
|
||
})
|
||
renderGroupPath()
|
||
renderTag()
|
||
}
|
||
setRasterInited(false)
|
||
}
|
||
}, [
|
||
activeImage,
|
||
objectOperations,
|
||
rasterInited,
|
||
renderPolygons,
|
||
renderTag,
|
||
storeLabel,
|
||
])
|
||
|
||
useEffect(() => {
|
||
renderGroupPath()
|
||
}, [storeLabel])
|
||
|
||
useEffect(() => {
|
||
renderTag()
|
||
}, [showTags, showTagsConfigs, storeLabel, 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])
|
||
|
||
// 打开弹窗时,初始化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
|
||
shadow="sm"
|
||
padding="xs"
|
||
radius="md"
|
||
withBorder
|
||
style={{
|
||
position: "absolute",
|
||
zIndex: 1000,
|
||
width: 224,
|
||
height: "fit-content",
|
||
display: !showContextMenu ? "none" : "block",
|
||
top: `${position.top}px`,
|
||
left: `${position.left}px`,
|
||
}}>
|
||
<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 handleCrosshairMove = (event: { clientX: number; clientY: number }) => {
|
||
crosshairComponentRef.current?.updateCrosshair(event)
|
||
}
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
handleCreatePathGroup,
|
||
renderPolygons,
|
||
handleCrosshairMove,
|
||
renderTrackingAnnotation,
|
||
renderSupportAnnotation2,
|
||
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}
|
||
/>
|
||
)}
|
||
{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,
|
||
minWidth: "fit-content",
|
||
maxWidth: "fit-content",
|
||
}}
|
||
/>
|
||
)}
|
||
{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)
|