Files
labelimage/components/label/components/PaperContainer.tsx
2026-04-03 20:40:04 +08:00

2770 lines
91 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,
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 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 SupportBox = [number, number, number, number]
type ContextMenuLayout = {
top: number
left: number
maxHeight: number
maxWidth: 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 contextMenuRef = useRef<HTMLDivElement>(null)
const [imgSize, setImageSize] = useState<{
clientWidth: number
clientHeight: number
}>()
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 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,
assistToolEnabled,
assistToolSize,
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,
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?.name ||
projectDetail?.rpath ||
`${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 getAuxiliaryAnnotation2({
project_id: projectId,
image: 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 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 (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 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以00为中心点进行缩放
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的左上角到00
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])
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,
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}
/>
)}
{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,
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)