6929 lines
214 KiB
TypeScript
6929 lines
214 KiB
TypeScript
import { notifications } from "@mantine/notifications"
|
||
import dayjs from "dayjs"
|
||
import paper from "paper"
|
||
import { DispatchWithoutAction } from "react"
|
||
import { create } from "zustand"
|
||
import { Comment } from "./api/label/typing"
|
||
import { Project } from "./api/project/typing"
|
||
import {
|
||
initialDetail,
|
||
type LabelData,
|
||
type LabelPathTuple,
|
||
useKeyEventStore,
|
||
useLabelStore,
|
||
useObjectStore,
|
||
} from "./store"
|
||
import { usePermissionStore } from "./store/auth"
|
||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||
import { useKeyboardStore } from "./useKeyBoardStore"
|
||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||
import { usePaperSupportStore } from "./usePaperSupportStore"
|
||
import { useRightToolsStore } from "./useRightToolsStore"
|
||
import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore"
|
||
import { safeClone } from "./utils/clone"
|
||
import {
|
||
applyBooleanAdapter,
|
||
findBestMatchingOuterContourIndex,
|
||
multiPolygonToStoredContours,
|
||
paperShapeToMultiPolygon,
|
||
} from "./utils/geometry/booleanAdapter"
|
||
import {
|
||
getObjectTagVisibility,
|
||
syncSelectedObjectsVisualState,
|
||
} from "./utils/objectVisibility"
|
||
|
||
type BooleanPipelineDrawOption = "default" | "intersect" | "unite"
|
||
type BooleanPipelineRasterClipOrder = "none" | "before" | "after" | "both"
|
||
|
||
const getSafePaperPathData = (data: any) => {
|
||
const nextData: Record<string, any> = {}
|
||
Object.entries(data || {}).forEach(([key, value]) => {
|
||
if (typeof value === "function") return
|
||
nextData[key] = value
|
||
})
|
||
return nextData
|
||
}
|
||
|
||
const syncPaperCompoundPolygonChildrenData = (
|
||
shape: paper.Path | paper.CompoundPath | null,
|
||
phase: "clip" | "intersect" | "unite"
|
||
) => {
|
||
if (!(shape instanceof paper.CompoundPath)) return
|
||
|
||
const shapeData = getSafePaperPathData(shape.data || {})
|
||
;(shape.children as paper.Path[]).forEach((child) => {
|
||
if (phase === "clip" && child.data && Object.keys(child.data).length) {
|
||
return
|
||
}
|
||
|
||
child.data = safeClone(
|
||
phase === "unite"
|
||
? Object.assign({}, shapeData)
|
||
: Object.assign({}, shapeData, {
|
||
isHollowPolygon: !child.clockwise,
|
||
})
|
||
)
|
||
})
|
||
}
|
||
|
||
const replacePaperWorkingPolygonShape = (
|
||
currentShape: paper.Path | paper.CompoundPath | null,
|
||
nextShape: paper.Path | paper.CompoundPath | null,
|
||
phase: "clip" | "intersect" | "unite"
|
||
) => {
|
||
const originalParent = currentShape?.parent
|
||
const originalIndex = currentShape?.index ?? -1
|
||
|
||
if (!nextShape) {
|
||
currentShape?.remove()
|
||
return null
|
||
}
|
||
|
||
syncPaperCompoundPolygonChildrenData(nextShape, phase)
|
||
currentShape?.remove()
|
||
|
||
if (!nextShape.parent && originalParent && "insertChild" in originalParent) {
|
||
originalParent.insertChild(
|
||
Math.min(originalIndex, originalParent.children.length),
|
||
nextShape
|
||
)
|
||
}
|
||
|
||
if (nextShape.parent === usePaperStore.getState().group) {
|
||
insertItemBelowTags(nextShape)
|
||
}
|
||
|
||
return nextShape
|
||
}
|
||
|
||
const clonePaperColor = (
|
||
color: paper.Color | string | null | undefined
|
||
): paper.Color | null => {
|
||
if (color instanceof paper.Color) {
|
||
return color.clone()
|
||
}
|
||
|
||
if (typeof color === "string") {
|
||
return new paper.Color(color)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const createPaperPathFromGeometryContour = ({
|
||
contour,
|
||
baseData,
|
||
strokeColor,
|
||
strokeWidth,
|
||
fillColor,
|
||
visible,
|
||
opacity,
|
||
isHollowPolygon,
|
||
selected,
|
||
}: {
|
||
contour: Array<[number, number]>
|
||
baseData: Record<string, any>
|
||
strokeColor: paper.Color | string | null | undefined
|
||
strokeWidth: number
|
||
fillColor: paper.Color | string | null | undefined
|
||
visible: boolean
|
||
opacity: number
|
||
isHollowPolygon: boolean
|
||
selected: boolean
|
||
}) => {
|
||
const path = new paper.Path({ insert: false })
|
||
|
||
contour.slice(0, -1).forEach(([x, y]) => {
|
||
path.add(new paper.Point(x, y))
|
||
})
|
||
|
||
path.closed = true
|
||
path.data = safeClone(
|
||
Object.assign({}, baseData, {
|
||
isHollowPolygon,
|
||
selected,
|
||
})
|
||
)
|
||
path.strokeColor = clonePaperColor(strokeColor)
|
||
path.strokeWidth = strokeWidth
|
||
path.fillColor = clonePaperColor(fillColor)
|
||
path.visible = visible
|
||
path.opacity = opacity
|
||
|
||
return path
|
||
}
|
||
|
||
const createPaperPolygonShapeFromGeometry = ({
|
||
geometry,
|
||
sourceShape,
|
||
}: {
|
||
geometry: ReturnType<typeof paperShapeToMultiPolygon>
|
||
sourceShape: paper.Path | paper.CompoundPath
|
||
}) => {
|
||
const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
|
||
if (!points.length) return null
|
||
|
||
const baseData = getSafePaperPathData(sourceShape.data || {})
|
||
const strokeColor = clonePaperColor(sourceShape.strokeColor)
|
||
const fillColor = clonePaperColor(sourceShape.fillColor)
|
||
const visible = sourceShape.visible
|
||
const opacity = sourceShape.opacity
|
||
const strokeWidth = sourceShape.strokeWidth
|
||
let selectedAssigned = false
|
||
|
||
const outerPaths = points.map((contour) => {
|
||
const selected = !!baseData.selected && !selectedAssigned
|
||
if (selected) {
|
||
selectedAssigned = true
|
||
}
|
||
|
||
return createPaperPathFromGeometryContour({
|
||
contour,
|
||
baseData,
|
||
strokeColor,
|
||
strokeWidth,
|
||
fillColor,
|
||
visible,
|
||
opacity,
|
||
isHollowPolygon: false,
|
||
selected,
|
||
})
|
||
})
|
||
|
||
const holePaths = hollowPoints.map((contour) =>
|
||
createPaperPathFromGeometryContour({
|
||
contour,
|
||
baseData,
|
||
strokeColor,
|
||
strokeWidth,
|
||
fillColor,
|
||
visible,
|
||
opacity,
|
||
isHollowPolygon: true,
|
||
selected: false,
|
||
})
|
||
)
|
||
|
||
if (outerPaths.length === 1 && !holePaths.length) {
|
||
return outerPaths[0]
|
||
}
|
||
|
||
const compoundPath = new paper.CompoundPath({ insert: false })
|
||
;[...outerPaths, ...holePaths].forEach((child) => {
|
||
compoundPath.addChild(child)
|
||
})
|
||
compoundPath.data = safeClone(baseData)
|
||
compoundPath.strokeColor = clonePaperColor(strokeColor)
|
||
compoundPath.strokeWidth = strokeWidth
|
||
compoundPath.fillColor = clonePaperColor(fillColor)
|
||
compoundPath.visible = visible
|
||
compoundPath.opacity = opacity
|
||
|
||
return compoundPath
|
||
}
|
||
|
||
const paperItemsToMultiPolygon = (
|
||
items: Array<paper.Path | paper.CompoundPath>
|
||
) => {
|
||
if (!items.length) return [] as ReturnType<typeof paperShapeToMultiPolygon>
|
||
|
||
if (items.length === 1) {
|
||
return paperShapeToMultiPolygon(items[0])
|
||
}
|
||
|
||
const paths = items.flatMap((item) => {
|
||
if (item instanceof paper.CompoundPath) {
|
||
return item.children as paper.Path[]
|
||
}
|
||
return [item]
|
||
})
|
||
|
||
return paperShapeToMultiPolygon({ children: paths })
|
||
}
|
||
|
||
const hasGeometryIntersection = (
|
||
subject: ReturnType<typeof paperShapeToMultiPolygon>,
|
||
operand: ReturnType<typeof paperShapeToMultiPolygon>
|
||
) => {
|
||
if (!subject.length || !operand.length) return false
|
||
|
||
return (
|
||
applyBooleanAdapter({
|
||
subject,
|
||
operands: [operand],
|
||
mode: "intersect",
|
||
}).geometry.length > 0
|
||
)
|
||
}
|
||
|
||
const collectVisiblePaperPolygonOperandGeometries = ({
|
||
group,
|
||
excludedObjectId = null,
|
||
subjectGeometry = null,
|
||
}: {
|
||
group: paper.Group | null
|
||
excludedObjectId?: number | null
|
||
subjectGeometry?: ReturnType<typeof paperShapeToMultiPolygon> | null
|
||
}) => {
|
||
const groupedItems = new Map<number, Array<paper.Path | paper.CompoundPath>>()
|
||
|
||
group?.children?.forEach((item) => {
|
||
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) {
|
||
return
|
||
}
|
||
|
||
if (!item.visible || !item.data?.id || item.data?.type !== "polygon") {
|
||
return
|
||
}
|
||
|
||
const objectId = Number(item.data.id)
|
||
if (excludedObjectId !== null && objectId === excludedObjectId) {
|
||
return
|
||
}
|
||
|
||
const currentItems = groupedItems.get(objectId) || []
|
||
currentItems.push(item)
|
||
groupedItems.set(objectId, currentItems)
|
||
})
|
||
|
||
return Array.from(groupedItems.values())
|
||
.map((items) => paperItemsToMultiPolygon(items))
|
||
.filter((geometry) => {
|
||
if (!geometry.length) return false
|
||
if (!subjectGeometry?.length) return true
|
||
return hasGeometryIntersection(subjectGeometry, geometry)
|
||
})
|
||
}
|
||
|
||
const clipPaperPolygonShapeToRaster = (
|
||
shape: paper.Path | paper.CompoundPath | null,
|
||
rasterPath: paper.Path | null
|
||
) => {
|
||
if (!shape || !rasterPath) return shape
|
||
|
||
const clippedShape = createPaperPolygonShapeFromGeometry({
|
||
geometry: applyBooleanAdapter({
|
||
subject: paperShapeToMultiPolygon(shape),
|
||
mode: "unite",
|
||
rasterClip: paperShapeToMultiPolygon(rasterPath),
|
||
}).geometry,
|
||
sourceShape: shape,
|
||
})
|
||
|
||
return replacePaperWorkingPolygonShape(shape, clippedShape, "clip")
|
||
}
|
||
|
||
const applyGeometryBooleanWithVisibleOperands = ({
|
||
group,
|
||
shape,
|
||
excludedObjectId = null,
|
||
mode,
|
||
}: {
|
||
group: paper.Group | null
|
||
shape: paper.Path | paper.CompoundPath | null
|
||
excludedObjectId?: number | null
|
||
mode: "subtract" | "unite"
|
||
}) => {
|
||
if (!shape) return null
|
||
|
||
const subjectGeometry = paperShapeToMultiPolygon(shape)
|
||
if (!subjectGeometry.length) {
|
||
return replacePaperWorkingPolygonShape(
|
||
shape,
|
||
null,
|
||
mode === "subtract" ? "intersect" : "unite"
|
||
)
|
||
}
|
||
|
||
const operands = collectVisiblePaperPolygonOperandGeometries({
|
||
group,
|
||
excludedObjectId,
|
||
subjectGeometry,
|
||
})
|
||
if (!operands.length) return shape
|
||
|
||
const nextShape = createPaperPolygonShapeFromGeometry({
|
||
geometry: applyBooleanAdapter({
|
||
subject: subjectGeometry,
|
||
operands,
|
||
mode,
|
||
}).geometry,
|
||
sourceShape: shape,
|
||
})
|
||
|
||
return replacePaperWorkingPolygonShape(
|
||
shape,
|
||
nextShape,
|
||
mode === "subtract" ? "intersect" : "unite"
|
||
)
|
||
}
|
||
|
||
const executePaperPolygonBooleanPipeline = ({
|
||
group,
|
||
rasterPath,
|
||
shape,
|
||
drawOption = "default",
|
||
excludedObjectId = null,
|
||
rasterClipOrder = "after",
|
||
}: {
|
||
group: paper.Group | null
|
||
rasterPath: paper.Path | null
|
||
shape: paper.Path | paper.CompoundPath | null
|
||
drawOption?: BooleanPipelineDrawOption
|
||
excludedObjectId?: number | null
|
||
rasterClipOrder?: BooleanPipelineRasterClipOrder
|
||
}) => {
|
||
let nextShape = shape
|
||
const nextShapeObjectId = nextShape?.data?.id
|
||
const resolvedExcludedObjectId =
|
||
excludedObjectId ??
|
||
(nextShapeObjectId === undefined || nextShapeObjectId === null
|
||
? null
|
||
: Number(nextShapeObjectId))
|
||
|
||
if (nextShape && ["before", "both"].includes(rasterClipOrder)) {
|
||
nextShape = clipPaperPolygonShapeToRaster(nextShape, rasterPath)
|
||
}
|
||
|
||
if (drawOption === "intersect") {
|
||
nextShape = applyGeometryBooleanWithVisibleOperands({
|
||
group,
|
||
shape: nextShape,
|
||
excludedObjectId: resolvedExcludedObjectId,
|
||
mode: "subtract",
|
||
})
|
||
} else if (drawOption === "unite") {
|
||
nextShape = applyGeometryBooleanWithVisibleOperands({
|
||
group,
|
||
shape: nextShape,
|
||
excludedObjectId: resolvedExcludedObjectId,
|
||
mode: "unite",
|
||
})
|
||
}
|
||
|
||
if (nextShape && ["after", "both"].includes(rasterClipOrder)) {
|
||
nextShape = clipPaperPolygonShapeToRaster(nextShape, rasterPath)
|
||
}
|
||
|
||
return nextShape
|
||
}
|
||
|
||
interface ContinuePolygonTarget {
|
||
imageId: string
|
||
operationId: string
|
||
objectId: number
|
||
}
|
||
|
||
interface PaperState {
|
||
paperScope: paper.PaperScope | null
|
||
group: paper.Group | null
|
||
rasterPath: paper.Path | null
|
||
el: HTMLCanvasElement | null
|
||
viewTool: paper.Tool | null
|
||
panTool: paper.Tool | null
|
||
toolOption: any
|
||
polygonTool: paper.Tool | null
|
||
rectangleTool: paper.Tool | null
|
||
brushTool: paper.Tool | null
|
||
pointTool: paper.Tool | null
|
||
supportTool: paper.Tool | null
|
||
mode: "pan" | "polygon" | "rectangle" | "brush" | "point" | "support" | null
|
||
rasterScale: { [x: string]: number }
|
||
rasterSize: { [x: string]: [number, number] }
|
||
reciprocalRasterScale: { [x: string]: number }
|
||
continuePolygonTarget: ContinuePolygonTarget | null
|
||
loadingData: boolean
|
||
init: (
|
||
initEl: HTMLCanvasElement,
|
||
initScope: paper.PaperScope,
|
||
initGroup: paper.Group
|
||
) => void
|
||
setGroup: (group: paper.Group | null) => void
|
||
setRasterPath: (paperRaster: paper.Path) => void
|
||
setRasterScale: (id: string, scale: number) => void
|
||
setRasterSize: (id: string, size: [number, number]) => void
|
||
resetRasterScale: () => void
|
||
initViewTool: (paperPanTool: paper.Tool) => void
|
||
initPanTool: (
|
||
paperPanTool: paper.Tool,
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => void
|
||
setToolOption: (toolOption: any) => void
|
||
addPolygon: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => paper.Path
|
||
addCompoundPath: (
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
children: paper.Path[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => paper.CompoundPath
|
||
initPolygonTool: (
|
||
paperPolygonTool: paper.Tool,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => void
|
||
addRectangle: (
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
option: any,
|
||
data: any
|
||
) => paper.Path.Rectangle
|
||
initRectangleTool: (
|
||
paperRectangleTool: paper.Tool,
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => void
|
||
addBrush: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => paper.Path
|
||
initBrushTool: (
|
||
paperBrushTool: paper.Tool,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => void
|
||
addPoint: (
|
||
renderCircle: (paperGroup: paper.Group, option: any) => paper.Path.Circle,
|
||
points: paper.Point | undefined,
|
||
option: any,
|
||
data: any
|
||
) => paper.Path.Circle
|
||
addPointLine: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => paper.Path
|
||
initPointTool: (
|
||
paperPointTool: paper.Tool,
|
||
renderCircle: (paperGroup: paper.Group, option: any) => paper.Path.Circle,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => void
|
||
getPointCircle: (
|
||
point: paper.Point,
|
||
index: number,
|
||
id: number,
|
||
color: any
|
||
) => paper.Path.Circle
|
||
initSupportTool: (
|
||
tool: paper.Tool,
|
||
renderPath: (
|
||
points: Array<[number, number]>,
|
||
tags: number[],
|
||
flag?: boolean,
|
||
boxes?: Array<[number, number, number, number]>
|
||
) => void
|
||
) => void
|
||
getClickedCircle: (
|
||
point: paper.Point,
|
||
index: number,
|
||
id: number,
|
||
color: any
|
||
) => paper.Path.Circle
|
||
getBuffPath: (
|
||
item: paper.Segment,
|
||
index: number,
|
||
id: number,
|
||
_color: any
|
||
) => paper.Path | null
|
||
setPaperMode: (
|
||
modeParam:
|
||
| "pan"
|
||
| "polygon"
|
||
| "rectangle"
|
||
| "brush"
|
||
| "point"
|
||
| "support"
|
||
| null
|
||
) => void
|
||
setGroupScale: (scale: number) => void
|
||
getAll: () => paper.Path[]
|
||
getItemById: (id: number) => paper.Path | null
|
||
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
|
||
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
|
||
beginContinuePolygonDraft: () => boolean
|
||
applySelectedPolygonBooleanOperation: (
|
||
mode: "subtract" | "unite",
|
||
options?: {
|
||
draftPath?: paper.Path | null
|
||
}
|
||
) => boolean
|
||
setLoadingData: (flag: boolean) => void
|
||
refreshNodeVisualSize: () => void
|
||
resizeGhostRequestToken: number
|
||
requestResizeGhost: () => void
|
||
}
|
||
|
||
interface PositionStore {
|
||
currentMousePosition: [number, number]
|
||
setCurrentMousePosition: (p: [number, number]) => void
|
||
}
|
||
|
||
const initialPaperState = {
|
||
paperScope: null,
|
||
group: null,
|
||
rasterPath: null,
|
||
el: null,
|
||
mode: null,
|
||
viewTool: null,
|
||
panTool: null,
|
||
toolOption: {
|
||
strokeColor: "red",
|
||
strokeWidth: 2,
|
||
fillColor: "#ff000033",
|
||
blankColor: "#ff000033",
|
||
},
|
||
polygonTool: null,
|
||
rectangleTool: null,
|
||
brushTool: null,
|
||
pointTool: null,
|
||
rasterScale: {},
|
||
rasterSize: {},
|
||
reciprocalRasterScale: {},
|
||
resizeGhostRequestToken: 0,
|
||
}
|
||
|
||
const insertItemBelowTags = (item: paper.Item | null | undefined) => {
|
||
if (!item) return
|
||
const group = usePaperStore.getState().group
|
||
if (!group || item.parent !== group) return
|
||
const firstTagItem = group.getItems({ data: { type: "tag" } })[0]
|
||
if (!firstTagItem || firstTagItem === item) return
|
||
item.insertBelow(firstTagItem)
|
||
}
|
||
|
||
const clonePathGroupMap = (
|
||
source: Map<string, Map<number, number[]>>
|
||
): Map<string, Map<number, number[]>> => {
|
||
const nextMap = new Map<string, Map<number, number[]>>()
|
||
source.forEach((groupMap, imageId) => {
|
||
nextMap.set(
|
||
imageId,
|
||
new Map(
|
||
Array.from(groupMap.entries()).map(([groupId, pathIds]) => [
|
||
groupId,
|
||
[...pathIds],
|
||
])
|
||
)
|
||
)
|
||
})
|
||
return nextMap
|
||
}
|
||
|
||
const clearPathParentGroupMetadata = (
|
||
labelData: LabelData,
|
||
imageId: string,
|
||
pathId: number
|
||
) => {
|
||
let changed = false
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(pathId)
|
||
.forEach((item) => {
|
||
if (!item.data?.parentGroupId) return
|
||
item.data = Object.assign({}, item.data, {
|
||
parentGroupId: null,
|
||
})
|
||
changed = true
|
||
})
|
||
|
||
const imageLabel = labelData.get(imageId)
|
||
if (!imageLabel) return changed
|
||
|
||
Array.from(imageLabel.entries()).forEach(([operationId, paths]) => {
|
||
let operationChanged = false
|
||
const nextPaths = paths.map((item) => {
|
||
if (item[0] !== pathId) return item
|
||
if (!item[2]?.parentGroupId) return item
|
||
|
||
operationChanged = true
|
||
changed = true
|
||
return [
|
||
item[0],
|
||
item[1],
|
||
{
|
||
...(item[2] && typeof item[2] === "object" ? item[2] : {}),
|
||
parentGroupId: null,
|
||
},
|
||
item[3],
|
||
]
|
||
})
|
||
|
||
if (operationChanged) {
|
||
imageLabel.set(operationId, nextPaths as typeof paths)
|
||
}
|
||
})
|
||
|
||
return changed
|
||
}
|
||
|
||
const syncDeletedPathGroupState = ({
|
||
imageId,
|
||
removedPathIds,
|
||
removedParentGroupIds,
|
||
labelData,
|
||
updateLabelStore = false,
|
||
}: {
|
||
imageId: string
|
||
removedPathIds: number[]
|
||
removedParentGroupIds: Array<number | undefined>
|
||
labelData: LabelData
|
||
updateLabelStore?: boolean
|
||
}) => {
|
||
const nextPathGroupMap = clonePathGroupMap(
|
||
useRightToolsStore.getState().pathGroupMap
|
||
)
|
||
const imageGroupMap = nextPathGroupMap.get(imageId)
|
||
const clearedPathIds = new Set<number>()
|
||
|
||
removedParentGroupIds.forEach((parentGroupId, index) => {
|
||
if (!parentGroupId || !imageGroupMap?.has(parentGroupId)) return
|
||
|
||
const nextGroupIds =
|
||
imageGroupMap
|
||
.get(parentGroupId)
|
||
?.filter((id) => id !== removedPathIds[index]) || []
|
||
|
||
if (nextGroupIds.length > 1) {
|
||
imageGroupMap.set(parentGroupId, nextGroupIds)
|
||
return
|
||
}
|
||
|
||
imageGroupMap.delete(parentGroupId)
|
||
nextGroupIds.forEach((pathId) => {
|
||
if (clearedPathIds.has(pathId)) return
|
||
clearedPathIds.add(pathId)
|
||
clearPathParentGroupMetadata(labelData, imageId, pathId)
|
||
})
|
||
})
|
||
|
||
if (imageGroupMap && !imageGroupMap.size) {
|
||
nextPathGroupMap.delete(imageId)
|
||
}
|
||
|
||
useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap)
|
||
|
||
if (updateLabelStore) {
|
||
useLabelStore.getState().setLabel(labelData)
|
||
}
|
||
|
||
return labelData
|
||
}
|
||
|
||
const handleDelete = () => {
|
||
const group = usePaperStore.getState().group!
|
||
const imageId = useBottomToolsStore.getState().activeImage
|
||
let ids = useObjectStore.getState().selectedPath[imageId] || []
|
||
let operationIds: any[] = []
|
||
let parentGroupIds: any[] = []
|
||
|
||
const otherToolsState = useOtherToolsStore.getState()
|
||
if (
|
||
otherToolsState.showContextMenu &&
|
||
otherToolsState.imageId === imageId &&
|
||
ids?.includes(otherToolsState.id)
|
||
) {
|
||
hidePaperContextMenu()
|
||
}
|
||
|
||
ids.forEach((id, index) => {
|
||
const items = group.getItems({ data: { id } })
|
||
items.forEach((item: any) => {
|
||
item.remove()
|
||
if (item.data.operationId && !operationIds[index])
|
||
operationIds[index] = item.data.operationId
|
||
if (item.data.parentGroupId && !parentGroupIds[index])
|
||
parentGroupIds[index] = item.data.parentGroupId
|
||
})
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"DELETE",
|
||
id
|
||
)
|
||
})
|
||
|
||
const currentLabelData = safeClone(useLabelStore.getState().label)
|
||
operationIds.forEach((operationId, index) => {
|
||
let result =
|
||
currentLabelData
|
||
.get(imageId)
|
||
?.get(operationId)
|
||
?.filter((item) => item[0] !== ids[index]) ?? []
|
||
currentLabelData.get(imageId)?.set(operationId, result)
|
||
})
|
||
syncDeletedPathGroupState({
|
||
imageId,
|
||
removedPathIds: ids,
|
||
removedParentGroupIds: parentGroupIds,
|
||
labelData: currentLabelData,
|
||
})
|
||
useLabelStore.getState().setLabel(currentLabelData)
|
||
useLabelStore.getState().pushStateStack(currentLabelData)
|
||
syncSelectedObjectsVisualState(imageId)
|
||
// ids.forEach((id) => {
|
||
// usePaperStore
|
||
// .getState()
|
||
// .group?.getItems({
|
||
// data: { id: id },
|
||
// })
|
||
// .forEach((item: any) => {
|
||
// item.remove();
|
||
// if (item.data.id) {
|
||
// if (item.data.imageId && item.data.operationId) {
|
||
// useLabelStore
|
||
// .getState()
|
||
// .deleteOperation(
|
||
// item.data.imageId,
|
||
// item.data.operationId,
|
||
// item.data.id
|
||
// );
|
||
// }
|
||
// // tocheck
|
||
// useObjectStore
|
||
// .getState()
|
||
// .updateSelectedPath(
|
||
// useBottomToolsStore.getState().activeImage,
|
||
// item.data.id
|
||
// );
|
||
// }
|
||
// });
|
||
// });
|
||
}
|
||
|
||
const cleanupDeletedObjectItems = (
|
||
imageId: string,
|
||
pathId?: number,
|
||
parentGroupId?: number,
|
||
options: {
|
||
pushState?: boolean
|
||
} = {}
|
||
) => {
|
||
if (!pathId) return
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.group?.getItems({
|
||
data: { id: pathId },
|
||
})
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
|
||
useObjectStore.getState().updateSelectedPath(imageId, "DELETE", pathId)
|
||
const otherToolsState = useOtherToolsStore.getState()
|
||
if (
|
||
otherToolsState.showContextMenu &&
|
||
otherToolsState.imageId === imageId &&
|
||
otherToolsState.id === pathId
|
||
) {
|
||
hidePaperContextMenu()
|
||
}
|
||
|
||
const nextLabelData = safeClone(useLabelStore.getState().label)
|
||
syncDeletedPathGroupState({
|
||
imageId,
|
||
removedPathIds: [pathId],
|
||
removedParentGroupIds: [parentGroupId],
|
||
labelData: nextLabelData,
|
||
updateLabelStore: true,
|
||
})
|
||
if (options.pushState !== false) {
|
||
useLabelStore.getState().pushStateStack(nextLabelData)
|
||
}
|
||
syncSelectedObjectsVisualState(imageId)
|
||
}
|
||
|
||
const handleShift = (flag: boolean) => {
|
||
useKeyEventStore.getState().setShift(flag)
|
||
}
|
||
|
||
// 通用 鼠标在paperScope上移动时更新当前鼠标位置
|
||
const handleMouseMove = (e: paper.MouseEvent) => {
|
||
usePositionStore.getState().setCurrentMousePosition([e.point.x, e.point.y])
|
||
}
|
||
|
||
const hidePaperContextMenu = () => {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: useOtherToolsStore.getState().imageId,
|
||
operationId: useOtherToolsStore.getState().operationId,
|
||
id: useOtherToolsStore.getState().id,
|
||
})
|
||
}
|
||
|
||
const PAPER_OBJECT_TYPES = ["rectangle", "polygon", "brush", "point"] as const
|
||
const PAPER_CONTEXT_HIT_TYPES = [
|
||
...PAPER_OBJECT_TYPES,
|
||
"circle",
|
||
"mask",
|
||
"pathBuff",
|
||
"pathCircle",
|
||
] as const
|
||
const LEGACY_PAPER_NODE_DIAMETER = 6
|
||
|
||
const isPaperObjectType = (type: unknown) =>
|
||
typeof type === "string" &&
|
||
PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number])
|
||
|
||
export const ensureEditablePaperPathSelected = (objectId: number) => {
|
||
const objectPaths = usePaperStore
|
||
.getState()
|
||
.getItemsById(objectId)
|
||
.filter(
|
||
(item): item is paper.Path =>
|
||
item instanceof paper.Path && isPaperObjectType(item.data?.type)
|
||
)
|
||
|
||
if (!objectPaths.length) return null
|
||
|
||
const editablePaths = objectPaths.filter((item) => !item.data?.isHollowPolygon)
|
||
const activePath =
|
||
editablePaths.find((item) => item.data?.selected) || editablePaths[0] || null
|
||
|
||
objectPaths.forEach((item) => {
|
||
item.data.selected = item === activePath
|
||
})
|
||
|
||
return activePath
|
||
}
|
||
|
||
const isPaperContextHitType = (type: unknown) =>
|
||
typeof type === "string" &&
|
||
PAPER_CONTEXT_HIT_TYPES.includes(
|
||
type as (typeof PAPER_CONTEXT_HIT_TYPES)[number]
|
||
)
|
||
|
||
const getPaperNodeDiameter = () => {
|
||
const configuredDiameter = useTopToolsStore.getState().nodeSize
|
||
return Number.isFinite(configuredDiameter)
|
||
? Math.round(configuredDiameter)
|
||
: DEFAULT_NODE_SIZE
|
||
}
|
||
|
||
const getPaperNodeRadius = () => getPaperNodeDiameter() / 2
|
||
|
||
const getPaperNodeScaleFactor = () => {
|
||
const currentScaling = usePaperStore.getState().group?.scaling.x || 1
|
||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||
return newScale / currentScaling
|
||
}
|
||
|
||
const syncPaperCircleDiameter = (circle: paper.Path) => {
|
||
const nextDiameter = getPaperNodeDiameter()
|
||
const currentDiameter =
|
||
Number(circle.data?.diameter) || LEGACY_PAPER_NODE_DIAMETER
|
||
|
||
if (
|
||
Number.isFinite(currentDiameter) &&
|
||
currentDiameter > 0 &&
|
||
Math.abs(currentDiameter - nextDiameter) > 0.001
|
||
) {
|
||
circle.scale(nextDiameter / currentDiameter)
|
||
}
|
||
|
||
circle.data = Object.assign({}, circle.data, {
|
||
diameter: nextDiameter,
|
||
})
|
||
}
|
||
|
||
const getPaperCursorByMode = (
|
||
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||
) => {
|
||
if (
|
||
["polygon", "rectangle", "brush", "point", "support"].includes(mode || "")
|
||
) {
|
||
return "crosshair"
|
||
}
|
||
return "default"
|
||
}
|
||
|
||
const syncPaperCursorByMode = (
|
||
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||
) => {
|
||
if (usePaperStore.getState().el) {
|
||
usePaperStore.getState().el!.style.cursor = getPaperCursorByMode(mode)
|
||
}
|
||
}
|
||
|
||
const clearPaperSelectedObjects = () => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
|
||
group
|
||
.getItems({
|
||
match: (item: paper.Item) => !!item.data?.selected,
|
||
})
|
||
.forEach((item) => {
|
||
item.data.selected = false
|
||
})
|
||
|
||
usePaperSupportStore.getState().clearAll()
|
||
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
const selectedIds = [
|
||
...(useObjectStore.getState().selectedPath[activeImage] || []),
|
||
]
|
||
|
||
selectedIds.forEach((id) => {
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(id)
|
||
.forEach((item) => {
|
||
resetPaperShapeFillColor(item)
|
||
})
|
||
})
|
||
|
||
selectedIds.forEach((id) => {
|
||
useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", id)
|
||
})
|
||
}
|
||
|
||
const resetPaperShapeFillColor = (item: paper.Item) => {
|
||
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) {
|
||
return
|
||
}
|
||
if (!["rectangle", "polygon"].includes(item.data?.type || "")) {
|
||
return
|
||
}
|
||
item.fillColor = item.data?.blankColor || item.fillColor || null
|
||
}
|
||
|
||
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
||
const objectId = Number(item.data?.id)
|
||
const editablePath = Number.isFinite(objectId)
|
||
? ensureEditablePaperPathSelected(objectId)
|
||
: item instanceof paper.Path && !item.data?.isHollowPolygon
|
||
? item
|
||
: null
|
||
|
||
if (item.data.type === "rectangle" && item instanceof paper.Path) {
|
||
item.data.selected = true
|
||
usePaperSupportStore.getState().handleBufferPaths(item)
|
||
usePaperSupportStore.getState().handleCircles(item)
|
||
item.fillColor = item.data.fillColor || item.fillColor || null
|
||
} else if (item.data.type === "brush") {
|
||
item.data.selected = true
|
||
if (editablePath) {
|
||
usePaperSupportStore.getState().handleMask(editablePath)
|
||
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||
usePaperSupportStore.getState().handleCircles(editablePath)
|
||
}
|
||
} else if (item.data.type === "point") {
|
||
item.data.selected = true
|
||
if (editablePath) {
|
||
usePaperSupportStore.getState().handleMask(editablePath)
|
||
usePaperSupportStore.getState().handleText(editablePath)
|
||
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||
usePaperSupportStore.getState().handleCircles(editablePath)
|
||
}
|
||
} else if (item.data.type === "polygon") {
|
||
item.data.selected = true
|
||
if (editablePath) {
|
||
usePaperSupportStore.getState().handleBufferPaths(editablePath)
|
||
usePaperSupportStore.getState().handleCircles(editablePath)
|
||
}
|
||
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
||
item.fillColor = item.data.fillColor || item.fillColor || null
|
||
}
|
||
}
|
||
|
||
item.bringToFront()
|
||
}
|
||
|
||
const getPaperSelectionPath = (
|
||
id: number,
|
||
localPoint: paper.Point,
|
||
preferredItem?: paper.Item | null
|
||
) => {
|
||
if (
|
||
preferredItem instanceof paper.Path &&
|
||
isPaperObjectType(preferredItem.data?.type)
|
||
) {
|
||
return preferredItem
|
||
}
|
||
|
||
const objectPaths = usePaperStore
|
||
.getState()
|
||
.getItemsById(id)
|
||
.filter(
|
||
(item): item is paper.Path =>
|
||
item instanceof paper.Path && isPaperObjectType(item.data?.type)
|
||
)
|
||
|
||
if (!objectPaths.length) return null
|
||
if (objectPaths.length === 1) return objectPaths[0]
|
||
|
||
let nearestPath = objectPaths[0]
|
||
for (let index = 1; index < objectPaths.length; index += 1) {
|
||
const item = objectPaths[index]
|
||
if (
|
||
item.getNearestPoint(localPoint).getDistance(localPoint) <
|
||
nearestPath.getNearestPoint(localPoint).getDistance(localPoint)
|
||
) {
|
||
nearestPath = item
|
||
}
|
||
}
|
||
|
||
return nearestPath
|
||
}
|
||
|
||
const getNearestPaperPathSegmentIndex = (
|
||
path: paper.Path,
|
||
localPoint: paper.Point
|
||
) => {
|
||
let nearestIndex = 0
|
||
let nearestDistance = Infinity
|
||
|
||
path.segments.forEach((segment, index) => {
|
||
const distance = segment.point.getDistance(localPoint)
|
||
if (distance < nearestDistance) {
|
||
nearestDistance = distance
|
||
nearestIndex = index
|
||
}
|
||
})
|
||
|
||
return nearestIndex
|
||
}
|
||
|
||
const normalizePointPanHitResult = (
|
||
hitResult: paper.HitResult | null,
|
||
localPoint: paper.Point
|
||
) => {
|
||
if (!hitResult?.item?.data?.id || hitResult.item.data?.type !== "circle") {
|
||
return hitResult
|
||
}
|
||
|
||
const selectedPath = getPaperSelectionPath(hitResult.item.data.id, localPoint)
|
||
if (
|
||
!(selectedPath instanceof paper.Path) ||
|
||
selectedPath.data?.type !== "point" ||
|
||
!selectedPath.segments.length
|
||
) {
|
||
return hitResult
|
||
}
|
||
|
||
const segmentIndex = getNearestPaperPathSegmentIndex(selectedPath, localPoint)
|
||
|
||
return {
|
||
...hitResult,
|
||
item: selectedPath,
|
||
type: "segment",
|
||
segment: selectedPath.segments[segmentIndex],
|
||
} as paper.HitResult
|
||
}
|
||
|
||
const getPointCirclesById = (id: number) =>
|
||
(usePaperStore.getState().group?.getItems({
|
||
data: {
|
||
id,
|
||
type: "circle",
|
||
},
|
||
}) as paper.Path.Circle[]) || []
|
||
|
||
const createPointCircleForPath = (
|
||
path: paper.Path,
|
||
point: paper.Point,
|
||
index: number
|
||
) => {
|
||
const circle = usePaperStore
|
||
.getState()
|
||
.getPointCircle(point, index, path.data.id, path.data.blankColor)
|
||
|
||
circle.data = Object.assign({}, circle.data, {
|
||
id: path.data.id,
|
||
imageId: path.data.imageId,
|
||
operationId: path.data.operationId,
|
||
text: index + 1,
|
||
diameter: getPaperNodeDiameter(),
|
||
fillColor: path.data.blankColor,
|
||
strokeColor: path.data.strokeColor,
|
||
})
|
||
circle.fillColor = path.data.blankColor || circle.fillColor || null
|
||
circle.strokeColor = path.data.strokeColor || circle.strokeColor || null
|
||
|
||
return circle
|
||
}
|
||
|
||
const syncPointCirclesWithPath = (path: paper.Path) => {
|
||
if (path.data?.type !== "point" || !path.data?.id) return []
|
||
|
||
const unusedCircles = getPointCirclesById(path.data.id).slice()
|
||
const orderedCircles = path.segments.map((segment, index) => {
|
||
const matchedCircleIndex = unusedCircles.findIndex((circle) => {
|
||
return (
|
||
circle.position.getDistance(segment.point) <= 0.01 ||
|
||
circle.contains(segment.point)
|
||
)
|
||
})
|
||
|
||
const circle =
|
||
matchedCircleIndex >= 0
|
||
? unusedCircles.splice(matchedCircleIndex, 1)[0]
|
||
: createPointCircleForPath(path, segment.point, index)
|
||
|
||
circle.position.x = segment.point.x
|
||
circle.position.y = segment.point.y
|
||
circle.data = Object.assign({}, circle.data, {
|
||
id: path.data.id,
|
||
imageId: path.data.imageId,
|
||
operationId: path.data.operationId,
|
||
text: index + 1,
|
||
diameter: getPaperNodeDiameter(),
|
||
fillColor: path.data.blankColor,
|
||
strokeColor: path.data.strokeColor,
|
||
})
|
||
circle.fillColor = path.data.blankColor || circle.fillColor || null
|
||
circle.strokeColor = path.data.strokeColor || circle.strokeColor || null
|
||
|
||
return circle
|
||
})
|
||
|
||
unusedCircles.forEach((circle) => {
|
||
circle.remove()
|
||
})
|
||
|
||
return orderedCircles
|
||
}
|
||
|
||
const trySelectPaperObjectByPoint = (clickPoint: paper.Point) => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return false
|
||
|
||
const hitResult = group.hitTest(clickPoint, {
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guides: false,
|
||
tolerance: usePaperStore.getState().paperScope
|
||
? 8 / useTopToolsStore.getState().scale
|
||
: 0,
|
||
match: (hit: paper.HitResult) =>
|
||
!!hit.item.data?.id && isPaperContextHitType(hit.item.data?.type),
|
||
})
|
||
|
||
const id = hitResult?.item?.data?.id
|
||
if (!id) return false
|
||
|
||
clearPaperSelectedObjects()
|
||
|
||
const localPoint = group.globalToLocal(clickPoint)
|
||
const selectedPath = getPaperSelectionPath(id, localPoint, hitResult?.item)
|
||
const operationId = (
|
||
selectedPath?.data?.operationId ||
|
||
usePaperStore.getState().getItemById(id)?.data?.operationId ||
|
||
hitResult?.item?.data?.operationId ||
|
||
""
|
||
).toString()
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(id)
|
||
.forEach((item) => {
|
||
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath))
|
||
return
|
||
if (!isPaperObjectType(item.data?.type)) return
|
||
|
||
if (item.data.type === "polygon") {
|
||
applyPaperSelectedPath(item)
|
||
return
|
||
}
|
||
|
||
if (
|
||
item instanceof paper.CompoundPath ||
|
||
(selectedPath && item === selectedPath)
|
||
) {
|
||
applyPaperSelectedPath(item)
|
||
}
|
||
})
|
||
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
useObjectStore.getState().updateSelectedPath(activeImage, "ADD", id)
|
||
|
||
const category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
clickPoint,
|
||
(category || {}) as Project.LabelSchemaList,
|
||
5,
|
||
5
|
||
)
|
||
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId:
|
||
selectedPath?.data?.imageId ||
|
||
hitResult?.item?.data?.imageId ||
|
||
activeImage,
|
||
operationId,
|
||
id,
|
||
})
|
||
|
||
return true
|
||
}
|
||
|
||
let isMiddleMousePanning = false
|
||
let hoveredBufferPathInfo: {
|
||
id: number
|
||
index: number
|
||
path: paper.Path
|
||
clearHighlight: () => void
|
||
scheduleClear: () => void
|
||
cancelScheduledClear: () => void
|
||
} | null = null
|
||
let hoveredPathCircleInfo: {
|
||
id: number
|
||
index: number
|
||
item: paper.Path.Circle
|
||
} | null = null
|
||
|
||
const clearHoveredBufferPathHighlight = () => {
|
||
if (hoveredBufferPathInfo) {
|
||
hoveredBufferPathInfo.clearHighlight()
|
||
}
|
||
hoveredBufferPathInfo = null
|
||
}
|
||
|
||
const startMiddleMousePan = (event?: MouseEvent) => {
|
||
if (!event || event.button !== 1) return false
|
||
|
||
event.preventDefault()
|
||
isMiddleMousePanning = true
|
||
hidePaperContextMenu()
|
||
|
||
return true
|
||
}
|
||
|
||
const dragMiddleMousePan = (e: { event: MouseEvent; delta: paper.Point }) => {
|
||
if (!isMiddleMousePanning) return false
|
||
|
||
e.event.preventDefault()
|
||
usePaperStore.getState().group!.position = usePaperStore
|
||
.getState()
|
||
.group!.position.add(e.delta)
|
||
|
||
return true
|
||
}
|
||
|
||
const stopMiddleMousePan = () => {
|
||
if (!isMiddleMousePanning) return false
|
||
|
||
isMiddleMousePanning = false
|
||
return true
|
||
}
|
||
|
||
const { user_id } = usePermissionStore.getState()
|
||
|
||
type OrderPoint = [number, number]
|
||
const getOrder = (b: OrderPoint[]): number[] => {
|
||
// 左下角:x最小,y最大
|
||
// 左上角:x最小,y最小
|
||
// 右上角:x最大,y最小
|
||
// 右下角:x最大,y最大
|
||
const bottomLeft = b.reduce(
|
||
(acc, point) =>
|
||
point[0] < acc[0] || (point[0] === acc[0] && point[1] > acc[1])
|
||
? point
|
||
: acc,
|
||
b[0]
|
||
)
|
||
const topLeft = b.reduce(
|
||
(acc, point) =>
|
||
point[0] < acc[0] || (point[0] === acc[0] && point[1] < acc[1])
|
||
? point
|
||
: acc,
|
||
b[0]
|
||
)
|
||
const topRight = b.reduce(
|
||
(acc, point) =>
|
||
point[0] > acc[0] || (point[0] === acc[0] && point[1] < acc[1])
|
||
? point
|
||
: acc,
|
||
b[0]
|
||
)
|
||
const bottomRight = b.reduce(
|
||
(acc, point) =>
|
||
point[0] > acc[0] || (point[0] === acc[0] && point[1] > acc[1])
|
||
? point
|
||
: acc,
|
||
b[0]
|
||
)
|
||
|
||
return [
|
||
b.indexOf(bottomLeft),
|
||
b.indexOf(topLeft),
|
||
b.indexOf(topRight),
|
||
b.indexOf(bottomRight),
|
||
]
|
||
}
|
||
|
||
const handleRectangleIntersectRaster = (rect: paper.Path) => {
|
||
let intersections = rect.getIntersections(
|
||
usePaperStore.getState().rasterPath!
|
||
)
|
||
let validIntersections = intersections.filter((intersection) => {
|
||
// 检查交点是否在两个路径的可见部分
|
||
return (
|
||
rect?.contains(intersection.point) &&
|
||
usePaperStore.getState().rasterPath!.contains(intersection.point)
|
||
)
|
||
})
|
||
|
||
// 没有 validIntersections 不进行操作
|
||
if (validIntersections.length) {
|
||
let intersectionPath: paper.Path | null = rect.intersect(
|
||
usePaperStore.getState().rasterPath!
|
||
) as paper.Path
|
||
|
||
let rectOrder = getOrder(
|
||
rect.segments.map((segment) => [
|
||
+segment.point.x.toFixed(2),
|
||
+segment.point.y.toFixed(2),
|
||
])
|
||
)
|
||
|
||
let intersectionOrder = getOrder(
|
||
intersectionPath.segments.map((segment) => [
|
||
+segment.point.x.toFixed(2),
|
||
+segment.point.y.toFixed(2),
|
||
])
|
||
)
|
||
|
||
let order = rectOrder.map((rorder) =>
|
||
intersectionOrder.findIndex((iorder) => iorder === rorder)
|
||
)
|
||
|
||
if (intersectionPath) {
|
||
rect.segments = order.map((child) => intersectionPath!.segments[child])
|
||
}
|
||
|
||
// rect.segments = [
|
||
// intersectionPath.segments[1],
|
||
// intersectionPath.segments[2],
|
||
// intersectionPath.segments[3],
|
||
// intersectionPath.segments[0],
|
||
// ];
|
||
// rect.segments = intersectionPath.segments;
|
||
intersectionPath.remove()
|
||
intersectionPath = null
|
||
} else {
|
||
let intersectionPath: paper.Path | null = rect.intersect(
|
||
usePaperStore.getState().rasterPath!
|
||
) as paper.Path
|
||
rect.segments = intersectionPath.segments
|
||
intersectionPath.remove()
|
||
intersectionPath = null
|
||
}
|
||
}
|
||
|
||
export const updateObjLatestComment = (
|
||
imageId: any,
|
||
operationId: any,
|
||
id: any
|
||
) => {
|
||
// 标注中的任务不对批注状态进行更新
|
||
if (useTopToolsStore.getState().currentTimeKey === "label") return
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(imageId, operationId, id)
|
||
const comment = currentDetail?.comment || []
|
||
const updatedComment = comment.map((item: Comment, index: number) =>
|
||
index < comment.length - 1
|
||
? item
|
||
: { ...item, comment_type: item.comment_type || 1 }
|
||
)
|
||
useLabelStore
|
||
.getState()
|
||
.updateLabelDetailDataByKeys(imageId, operationId, id, {
|
||
...currentDetail,
|
||
comment: updatedComment,
|
||
})
|
||
}
|
||
|
||
// 清空辅助标注形成的path
|
||
export const clearSupportAnnotationItem = () => {
|
||
const needClearItems = usePaperStore
|
||
.getState()
|
||
.group!.getItems({ data: { id: "support" } })
|
||
needClearItems.forEach((item: any) => item.remove())
|
||
}
|
||
|
||
// 根据点击位置判断弹窗弹出的top、left
|
||
export const getShowContextMenuPosition = (
|
||
clickPoint: paper.Point,
|
||
_category: Project.LabelSchemaList,
|
||
offsetWidth = 0,
|
||
offsetHeight = 0
|
||
) => {
|
||
const element = document.getElementById("paper-container")
|
||
const containerWidth = element?.clientWidth ?? 0
|
||
const containerHeight = element?.clientHeight ?? 0
|
||
const left = Math.min(
|
||
Math.max(clickPoint.x + offsetWidth, 0),
|
||
containerWidth || clickPoint.x + offsetWidth
|
||
)
|
||
const top = Math.min(
|
||
Math.max(clickPoint.y + offsetHeight, 0),
|
||
containerHeight || clickPoint.y + offsetHeight
|
||
)
|
||
|
||
return {
|
||
top,
|
||
left,
|
||
}
|
||
}
|
||
|
||
const DOUBLE_CLICK_TOLERANCE_PX = 8
|
||
const PAN_DRAG_THRESHOLD_PX = 4
|
||
const BUFFER_HOVER_STICKY_MS = 180
|
||
|
||
const getPaperDoubleClickTolerance = (
|
||
coordinateSpace: "project" | "local" = "project"
|
||
) => {
|
||
const viewZoom = usePaperStore.getState().paperScope?.view.zoom || 1
|
||
const projectTolerance = DOUBLE_CLICK_TOLERANCE_PX / viewZoom
|
||
|
||
if (coordinateSpace === "local") {
|
||
const groupScale = usePaperStore.getState().group?.scaling.x || 1
|
||
return projectTolerance / groupScale
|
||
}
|
||
|
||
return projectTolerance
|
||
}
|
||
|
||
const getPaperDragThreshold = (
|
||
coordinateSpace: "project" | "local" = "project"
|
||
) => {
|
||
const viewZoom = usePaperStore.getState().paperScope?.view.zoom || 1
|
||
const projectThreshold = PAN_DRAG_THRESHOLD_PX / viewZoom
|
||
|
||
if (coordinateSpace === "local") {
|
||
const groupScale = usePaperStore.getState().group?.scaling.x || 1
|
||
return projectThreshold / groupScale
|
||
}
|
||
|
||
return projectThreshold
|
||
}
|
||
|
||
const showDeletePointLimitNotification = (
|
||
type: "polygon" | "brush" | "point"
|
||
) => {
|
||
const messageMap = {
|
||
polygon: "当前对象至少保留3个点,无法继续删除",
|
||
brush: "当前对象至少保留2个点,无法继续删除",
|
||
point: "当前对象至少保留1个点,无法继续删除",
|
||
}
|
||
|
||
notifications.show({
|
||
id: "label-delete-point-limit",
|
||
color: "yellow",
|
||
autoClose: 1500,
|
||
message: messageMap[type],
|
||
})
|
||
}
|
||
|
||
const isPaperDoubleClick = (
|
||
event: MouseEvent,
|
||
lastPoint: paper.Point | null | undefined,
|
||
currentPoint: paper.Point,
|
||
coordinateSpace: "project" | "local" = "project"
|
||
) => {
|
||
if (event.button !== 0 || event.detail < 2 || !lastPoint) return false
|
||
|
||
// 交给浏览器按系统双击速度判定,再补一个小范围容差,避免画布点击抖动。
|
||
return (
|
||
lastPoint.getDistance(currentPoint) <=
|
||
getPaperDoubleClickTolerance(coordinateSpace)
|
||
)
|
||
}
|
||
|
||
export const usePositionStore = create<PositionStore>((set) => ({
|
||
currentMousePosition: [0, 0],
|
||
setCurrentMousePosition: (point) =>
|
||
set((state) => ({ ...state, currentMousePosition: point })),
|
||
}))
|
||
|
||
// store paper
|
||
export const usePaperStore = create<PaperState>((set) => ({
|
||
paperScope: initialPaperState.paperScope,
|
||
group: initialPaperState.group,
|
||
rasterPath: initialPaperState.rasterPath,
|
||
el: initialPaperState.el,
|
||
mode: initialPaperState.mode,
|
||
viewTool: initialPaperState.viewTool,
|
||
panTool: initialPaperState.panTool,
|
||
toolOption: initialPaperState.toolOption,
|
||
polygonTool: initialPaperState.polygonTool,
|
||
rectangleTool: initialPaperState.rectangleTool,
|
||
brushTool: initialPaperState.brushTool,
|
||
pointTool: initialPaperState.pointTool,
|
||
supportTool: null,
|
||
rasterScale: initialPaperState.rasterScale,
|
||
rasterSize: initialPaperState.rasterSize,
|
||
reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
|
||
resizeGhostRequestToken: initialPaperState.resizeGhostRequestToken,
|
||
continuePolygonTarget: null,
|
||
currentMousePosition: [0, 0],
|
||
loadingData: false,
|
||
beginContinuePolygonDraft: () => false,
|
||
applySelectedPolygonBooleanOperation: () => false,
|
||
init: (
|
||
initEl: HTMLCanvasElement,
|
||
initScope: paper.PaperScope,
|
||
initGroup: paper.Group
|
||
) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
paperScope: initScope,
|
||
group: initGroup,
|
||
el: initEl,
|
||
}))
|
||
},
|
||
setGroup: (group) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
group,
|
||
}))
|
||
},
|
||
setRasterPath: (rasterPath) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
rasterPath: rasterPath,
|
||
}))
|
||
},
|
||
setRasterScale: (id: string, scale: number) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
rasterScale: { ...state.rasterScale, [id]: scale },
|
||
reciprocalRasterScale: {
|
||
...state.reciprocalRasterScale,
|
||
[id]: 1 / scale,
|
||
},
|
||
}))
|
||
},
|
||
setRasterSize: (id: string, size: [number, number]) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
rasterSize: { ...state.rasterSize, [id]: size },
|
||
}))
|
||
},
|
||
resetRasterScale: () =>
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
rasterScale: {},
|
||
reciprocalRasterScale: {},
|
||
})),
|
||
setLoadingData: (flag) =>
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
loadingData: flag,
|
||
})),
|
||
refreshNodeVisualSize: () => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
;(
|
||
group.getItems({
|
||
match: (item: paper.Item) =>
|
||
item instanceof paper.Path &&
|
||
["circle", "pathCircle"].includes(item.data?.type || ""),
|
||
}) as paper.Path[]
|
||
).forEach((circle) => {
|
||
syncPaperCircleDiameter(circle)
|
||
})
|
||
},
|
||
requestResizeGhost: () =>
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
resizeGhostRequestToken: state.resizeGhostRequestToken + 1,
|
||
})),
|
||
setContinuePolygonTarget: (target) =>
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
continuePolygonTarget: target,
|
||
})),
|
||
setCurrentMousePosition: (point: [number, number]) =>
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
currentMousePosition: point,
|
||
})),
|
||
setToolOption: (toolOption: any) => {
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
toolOption: toolOption,
|
||
}))
|
||
},
|
||
initViewTool: (paperViewTool: paper.Tool) => {
|
||
let viewTool = paperViewTool
|
||
viewTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
}
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
viewTool: viewTool,
|
||
}))
|
||
},
|
||
initPanTool: (
|
||
paperPanTool: paper.Tool,
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => {
|
||
let panMode = ""
|
||
let activePath: paper.Path
|
||
// let compoundPolygon: paper.Path | paper.CompoundPath | null;
|
||
let hitIndex: number
|
||
let pressShift: boolean = false
|
||
let pressCtrl: boolean = false
|
||
let rect: paper.Path.Rectangle | null
|
||
let panTool = paperPanTool
|
||
let lastPoint: paper.Point
|
||
let isChanged: boolean = false
|
||
let hasExceededPanDragThreshold = false
|
||
|
||
// const handleSelected = (path: paper.Path) => {
|
||
// // path.blendMode = "subtract";
|
||
// path.segments.forEach((segment) => {
|
||
// // console.log(segment.index);
|
||
|
||
// // let point = usePaperStore
|
||
// // .getState()
|
||
// // .group!.globalToLocal(segment.point);
|
||
// let circle = usePaperStore
|
||
// .getState()
|
||
// .getClickedCircle(
|
||
// segment.point,
|
||
// segment.index,
|
||
// path.data.id,
|
||
// path.data.blankColor
|
||
// );
|
||
// // circle.insertBelow(path);
|
||
// // path.addChild(circle);
|
||
|
||
// selectedCircles.push(circle);
|
||
// // usePaperStore.getState().group?.addChild(circle);
|
||
// // circle.bringToFront();
|
||
// // circle.addTo(path);
|
||
// });
|
||
// };
|
||
|
||
const handleCtrl = (rect: paper.Path.Rectangle) => {
|
||
let items = usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.filter(
|
||
(item) =>
|
||
item.data?.id &&
|
||
["rectangle", "polygon", "brush", "point"].includes(item.data.type)
|
||
)
|
||
// 清空选中
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({
|
||
data: { selected: true },
|
||
})
|
||
.forEach((item) => {
|
||
if (item.data.selected) {
|
||
item.data.selected = false
|
||
usePaperSupportStore.getState().clearMasks()
|
||
usePaperSupportStore.getState().clearTexts()
|
||
}
|
||
// item.data.selected = false;
|
||
})
|
||
handleCancelSelect()
|
||
for (let i = 0; i < items.length; i++) {
|
||
let child = items[i]
|
||
if (child instanceof paper.Path) {
|
||
if (
|
||
rect.intersects(child.bounds as any) ||
|
||
rect.contains(child.bounds)
|
||
) {
|
||
if (rect.data.id !== child.data.id) {
|
||
if (child.data.type !== "brush" && child.data.type !== "point") {
|
||
child.fillColor = child.data.fillColor
|
||
}
|
||
if (child.data.type === "rectangle") {
|
||
if (child instanceof paper.Path) child.data.selected = true
|
||
} else if (child.data.type === "brush") {
|
||
child.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(child)
|
||
} else if (child.data.type === "point") {
|
||
child.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(child)
|
||
usePaperSupportStore.getState().handleText(child as paper.Path)
|
||
}
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADDSOME",
|
||
child.data?.id
|
||
)
|
||
}
|
||
}
|
||
} else if (child instanceof paper.CompoundPath) {
|
||
let bool = child.children.some((item) => {
|
||
return (
|
||
rect.intersects(item.bounds as any) || rect.contains(item.bounds)
|
||
)
|
||
})
|
||
if (bool) {
|
||
if (rect.data.id !== child.data.id) {
|
||
if (child.data.type !== "brush" && child.data.type !== "point") {
|
||
child.fillColor = child.data.fillColor
|
||
}
|
||
if (child.data.type === "rectangle") {
|
||
if (child instanceof paper.Path) child.data.selected = true
|
||
} else if (child.data.type === "brush") {
|
||
child.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(child)
|
||
} else if (child.data.type === "point") {
|
||
child.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(child)
|
||
usePaperSupportStore.getState().handleText(child as paper.Path)
|
||
}
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADDSOME",
|
||
child.data?.id
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleCancelSelect = () => {
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.forEach((item) => {
|
||
// clear color
|
||
if (
|
||
!(
|
||
item.data.type === "mask" ||
|
||
item.data.type === "text" ||
|
||
item.data.type === "tag"
|
||
)
|
||
)
|
||
item.fillColor = item?.data?.blankColor
|
||
})
|
||
|
||
// 如果没有按下shift 当前图片SelectedPath被清空 下面从空数组开始push
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(useBottomToolsStore.getState().activeImage, "ADD")
|
||
}
|
||
|
||
const getPanHitTolerance = (baseTolerance = 8) =>
|
||
usePaperStore.getState().paperScope
|
||
? baseTolerance / useTopToolsStore.getState().scale
|
||
: 0
|
||
|
||
const createPanHitOptions = (
|
||
tolerance: number,
|
||
match: (hit: paper.HitResult) => boolean
|
||
) => ({
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guides: false,
|
||
tolerance,
|
||
match,
|
||
})
|
||
|
||
const resolvePanHitResult = (projectPoint: paper.Point) => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return null
|
||
|
||
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
|
||
? hoveredPathCircleInfo
|
||
: null
|
||
if (hoveredCircleInfo) {
|
||
const pointHit = group.hitTest(
|
||
projectPoint,
|
||
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||
return (
|
||
hit.item.data?.type === "pathCircle" &&
|
||
hit.item.data?.id === hoveredCircleInfo.id &&
|
||
hit.item.data?.index === hoveredCircleInfo.index
|
||
)
|
||
})
|
||
)
|
||
if (pointHit) return pointHit
|
||
}
|
||
|
||
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||
? hoveredBufferPathInfo
|
||
: null
|
||
if (hoveredBufferInfo) {
|
||
const edgeHit = group.hitTest(
|
||
projectPoint,
|
||
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||
return (
|
||
hit.item.data?.type === "pathBuff" &&
|
||
hit.item.data?.id === hoveredBufferInfo.id &&
|
||
hit.item.data?.index === hoveredBufferInfo.index
|
||
)
|
||
})
|
||
)
|
||
if (edgeHit) return edgeHit
|
||
}
|
||
|
||
return group.hitTest(
|
||
projectPoint,
|
||
createPanHitOptions(getPanHitTolerance(), (hit) =>
|
||
["rectangle", "polygon", "brush", "point", "circle"].includes(
|
||
hit.item.data?.type || ""
|
||
)
|
||
)
|
||
)
|
||
}
|
||
|
||
const clearCurrentSelection = () => {
|
||
let shouldClearSupport = false
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({
|
||
data: { selected: true },
|
||
})
|
||
.forEach((item) => {
|
||
if (item.data.selected) {
|
||
item.data.selected = false
|
||
shouldClearSupport = true
|
||
}
|
||
})
|
||
|
||
if (shouldClearSupport) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
|
||
handleCancelSelect()
|
||
}
|
||
|
||
panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
if (pressCtrl) return
|
||
|
||
if (e.event && e.event.button === 0) {
|
||
hidePaperContextMenu()
|
||
}
|
||
if (useTopToolsStore.getState().isView) {
|
||
panMode = ""
|
||
return
|
||
}
|
||
|
||
let point = usePaperStore.getState().group!.globalToLocal(e.point)
|
||
const isDoubleClick = isPaperDoubleClick(
|
||
e.event,
|
||
lastPoint,
|
||
point,
|
||
"local"
|
||
)
|
||
hasExceededPanDragThreshold = false
|
||
|
||
let hitResult = resolvePanHitResult(e.point)
|
||
hitResult = normalizePointPanHitResult(hitResult, point)
|
||
console.log("hitResult", hitResult)
|
||
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||
? hoveredBufferPathInfo
|
||
: null
|
||
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
|
||
? hoveredPathCircleInfo
|
||
: null
|
||
let lastTagDataId = -1
|
||
if (activePath?.data.id) {
|
||
lastTagDataId = activePath.data.id
|
||
}
|
||
|
||
// 处理相同id标注对象的函数 生成对应activePath 更新rect和polygon的fillColor
|
||
const handleSameIdItems = (id: number) => {
|
||
const sameIdItems = usePaperStore
|
||
.getState()
|
||
.getItemsById(id)
|
||
.filter((item) => {
|
||
return item.data.type !== "tag"
|
||
})
|
||
const types = [
|
||
...new Set(sameIdItems.map((item) => item.data.type || "")),
|
||
]
|
||
if (types.includes("rectangle")) {
|
||
activePath = sameIdItems.filter((item) => {
|
||
if (item.data.type === "rectangle") {
|
||
item.fillColor = item.data.fillColor
|
||
return true
|
||
} else return false
|
||
})[0] as paper.Path
|
||
} else if (types.includes("polygon")) {
|
||
const items = sameIdItems.filter((item) => {
|
||
if (item.data.type === "polygon") {
|
||
if (!item.data.isHollowPolygon)
|
||
item.fillColor = item.data.fillColor
|
||
return true
|
||
} else return false
|
||
})
|
||
console.log(items)
|
||
} else if (types.includes("brush")) {
|
||
const items = sameIdItems.filter((item) => item.data.type === "brush")
|
||
if (items.length === 1) {
|
||
// 此时 items[0] 类型只可能为path
|
||
activePath = items[0] as paper.Path
|
||
} else if (items.length > 1) {
|
||
let parent: any = items.filter(
|
||
(item) => item instanceof paper.CompoundPath
|
||
)[0]
|
||
const children = parent.children
|
||
if (children.length === 1) {
|
||
activePath = children[0]
|
||
} else if (children.length > 1) {
|
||
let activeItem: any
|
||
let min = 1000000
|
||
children.forEach((child: any) => {
|
||
let nearestPoint = child.getNearestPoint(point)
|
||
let distance = point.getDistance(nearestPoint)
|
||
if (distance < min) {
|
||
min = distance
|
||
activeItem = child
|
||
}
|
||
})
|
||
activePath = activeItem
|
||
}
|
||
// 根据点击位置更新实际点击类型
|
||
let currentHitRes = activePath.hitTest(point, {
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guide: false,
|
||
tolerance: usePaperStore.getState().paperScope
|
||
? 8 / useTopToolsStore.getState().scale
|
||
: 0,
|
||
})
|
||
if (currentHitRes) {
|
||
if (currentHitRes.type === "segment") {
|
||
hitIndex = currentHitRes.segment.index
|
||
panMode = "segment"
|
||
} else if (currentHitRes.type === "stroke") {
|
||
hitIndex = currentHitRes.location.index
|
||
panMode = "stroke"
|
||
}
|
||
}
|
||
}
|
||
} else if (types.includes("point")) {
|
||
const items = sameIdItems.filter((item) => item.data.type === "point")
|
||
if (items.length === 1) {
|
||
// 点类型没有单对象多区域的情况
|
||
activePath = items[0] as paper.Path
|
||
}
|
||
}
|
||
console.log("处理相同id对象后", activePath)
|
||
if (activePath instanceof paper.CompoundPath) {
|
||
let selected = (activePath.children as paper.Path[]).find(
|
||
(child) => child.data.selected
|
||
)
|
||
selected && usePaperSupportStore.getState().handleCircles(selected)
|
||
}
|
||
}
|
||
|
||
// 当前点击结果为遮罩 处理线段和点的相关情况
|
||
if (hitResult && hitResult.item.data.type === "mask") {
|
||
panMode = "fill"
|
||
if (hitResult.item.data.id) {
|
||
handleSameIdItems(hitResult.item.data.id)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (pressShift) {
|
||
} else {
|
||
const selectedIds =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
] || []
|
||
const hitId = hitResult?.item?.data?.id
|
||
const keepHoveredPointSelection =
|
||
!!hoveredCircleInfo &&
|
||
selectedIds.length === 1 &&
|
||
selectedIds[0] === hoveredCircleInfo.id
|
||
const keepHoveredEdgeSelection =
|
||
!!hoveredBufferInfo &&
|
||
selectedIds.length === 1 &&
|
||
selectedIds[0] === hoveredBufferInfo.id
|
||
const keepCurrentSelection =
|
||
keepHoveredPointSelection ||
|
||
keepHoveredEdgeSelection ||
|
||
(hitId && selectedIds.length === 1 && selectedIds[0] === hitId)
|
||
|
||
if (!keepCurrentSelection) {
|
||
clearCurrentSelection()
|
||
}
|
||
}
|
||
|
||
if (hitResult && hitResult.item.className !== "Raster") {
|
||
if (hitResult.item.data?.id) {
|
||
// 处理多边形和矩形填充色
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({
|
||
data: { id: hitResult.item.data?.id },
|
||
})
|
||
.forEach((item) => {
|
||
// 点击图形本身 fill color
|
||
if (
|
||
hitResult.item.data.type !== "brush" &&
|
||
hitResult.item.data.type !== "point" &&
|
||
hitResult.item.data.type !== "pathCircle" &&
|
||
hitResult.item.data.type !== "pathBuff" &&
|
||
hitResult.item.data.type !== "text" &&
|
||
item.data.type !== "tag"
|
||
) {
|
||
item.fillColor = hitResult.item.data.fillColor
|
||
}
|
||
})
|
||
|
||
if (hitResult.item.data.type === "polygon") {
|
||
if (hitResult.item instanceof paper.CompoundPath) {
|
||
;(hitResult.item.children as paper.Path[]).forEach((item) => {
|
||
const childHitResult = item.hitTest(point, {
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guide: false,
|
||
tolerance: 0,
|
||
})
|
||
|
||
if (childHitResult) {
|
||
if (childHitResult.item.data.isHollowPolygon) return
|
||
// item.fillColor = new paper.Color("transparent");
|
||
// item.strokeColor = item.strokeColor || new paper.Color("red");
|
||
// item.strokeWidth = item.strokeWidth || 1;
|
||
if (isDoubleClick) {
|
||
console.log("点击位置 多边形子路径", childHitResult)
|
||
if (childHitResult.item instanceof paper.Path) {
|
||
// if (!childHitResult.item.data.selected) {
|
||
// childHitResult.item.data.selected = true;
|
||
// }
|
||
let selectedItems = usePaperStore
|
||
.getState()
|
||
.group!.getItems({
|
||
data: { selected: true },
|
||
})
|
||
selectedItems.forEach((item) => {
|
||
item.data.selected = false
|
||
})
|
||
childHitResult.item.data.selected = true
|
||
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(childHitResult.item)
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleCircles(childHitResult.item)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
} else if (hitResult.item instanceof paper.Path) {
|
||
// doubleclick;
|
||
if (isDoubleClick) {
|
||
if (hitResult.item.data.isHollowPolygon) return
|
||
let selectedItems = usePaperStore.getState().group!.getItems({
|
||
data: { selected: true },
|
||
})
|
||
selectedItems.forEach((item) => {
|
||
item.data.selected = false
|
||
})
|
||
hitResult.item.data.selected = true
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(hitResult.item)
|
||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||
// if (!hitResult.item.data.selected) {
|
||
// hitResult.item.data.selected = true;
|
||
// usePaperSupportStore
|
||
// .getState()
|
||
// .handleBufferPaths(hitResult.item);
|
||
// usePaperSupportStore.getState().handleCircles(hitResult.item);
|
||
// }
|
||
}
|
||
}
|
||
activePath = hitResult.item as paper.Path
|
||
} else if (hitResult.item.data.type === "rectangle") {
|
||
if (hitResult.item instanceof paper.Path) {
|
||
if (!hitResult.item.data.selected) {
|
||
hitResult.item.data.selected = true
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(hitResult.item)
|
||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||
}
|
||
activePath = hitResult.item as paper.Path
|
||
}
|
||
} else if (hitResult.item.data.type === "brush") {
|
||
// hitResult.item.data.selected = true;
|
||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||
// 绘制选中的circle
|
||
if (hitResult.item instanceof paper.CompoundPath) {
|
||
;(hitResult.item.children as paper.Path[]).forEach((item) => {
|
||
const childHitResult = item.hitTest(point, {
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guide: false,
|
||
tolerance: usePaperStore.getState().paperScope
|
||
? 8 / usePaperStore.getState().paperScope!.view.zoom
|
||
: 0,
|
||
})
|
||
if (childHitResult) {
|
||
if (childHitResult.item instanceof paper.Path) {
|
||
if (!childHitResult.item.data.selected) {
|
||
childHitResult.item.data.selected = true
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(childHitResult.item)
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleCircles(childHitResult.item)
|
||
}
|
||
activePath = childHitResult.item as paper.Path
|
||
}
|
||
}
|
||
})
|
||
} else if (hitResult.item instanceof paper.Path) {
|
||
if (!hitResult.item.data.selected) {
|
||
hitResult.item.data.selected = true
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(hitResult.item)
|
||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||
}
|
||
activePath = hitResult.item as paper.Path
|
||
}
|
||
} else if (hitResult.item.data.type === "point") {
|
||
if (
|
||
["segment", "stroke"].includes(hitResult.type) &&
|
||
hitResult.item instanceof paper.Path
|
||
) {
|
||
if (!hitResult.item.data.selected) {
|
||
hitResult.item.data.selected = true
|
||
// 绘制遮罩
|
||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||
// 绘制点对应的数字
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleText(hitResult.item as paper.Path)
|
||
// 绘制选中边
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(hitResult.item)
|
||
// 绘制选中圆
|
||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||
}
|
||
activePath = hitResult.item as paper.Path
|
||
}
|
||
} else if (
|
||
hitResult.item.data.type === "pathBuff" ||
|
||
hitResult.item.data.type === "pathCircle"
|
||
) {
|
||
handleSameIdItems(hitResult.item.data.id)
|
||
}
|
||
|
||
// // 更新被选中的标注对象detail
|
||
// updateObjLatestComment(
|
||
// hitResult.item.data?.imageId,
|
||
// hitResult.item.data?.operationId,
|
||
// hitResult.item.data?.id
|
||
// );
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
hitResult.item.data?.id
|
||
)
|
||
|
||
// usePaperStore.getState().group?.addChild(activePath); // 放到顶层
|
||
|
||
// activePath.bringToFront();
|
||
}
|
||
} else {
|
||
// if Raster
|
||
if (e.event && e.event.button === 2) {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: useOtherToolsStore.getState().imageId,
|
||
operationId: useOtherToolsStore.getState().operationId,
|
||
id: useOtherToolsStore.getState().id,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 禁止右键拖拽
|
||
if (e.event && e.event.button === 2) {
|
||
panMode = ""
|
||
if (
|
||
hitResult?.item?.data.id &&
|
||
["rectangle", "polygon", "brush", "point"].includes(
|
||
hitResult?.item?.data.type
|
||
)
|
||
) {
|
||
const data = hitResult?.item?.data
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: data.imageId,
|
||
operationId: data.operationId,
|
||
id: data.id,
|
||
})
|
||
}
|
||
} else if (
|
||
isDoubleClick &&
|
||
hoveredBufferInfo &&
|
||
hoveredBufferInfo.id === activePath?.data?.id &&
|
||
["polygon", "brush", "point"].includes(activePath?.data?.type || "")
|
||
) {
|
||
let currentPath: paper.Path | null = null
|
||
if (activePath instanceof paper.CompoundPath) {
|
||
currentPath =
|
||
(activePath.children as paper.Path[]).find(
|
||
(item) => item.data.selected
|
||
) || null
|
||
} else if (activePath instanceof paper.Path) {
|
||
currentPath = activePath
|
||
}
|
||
|
||
if (currentPath) {
|
||
hitIndex = hoveredBufferInfo.index
|
||
const insertPoint = currentPath.getNearestPoint(point)
|
||
currentPath.insert(hitIndex + 1, insertPoint)
|
||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||
usePaperSupportStore.getState().handleCircles(currentPath)
|
||
isChanged = true
|
||
panMode = "stroke"
|
||
hitIndex = hitIndex + 1
|
||
|
||
if (currentPath.data.type === "brush") {
|
||
usePaperSupportStore.getState().handleMask(currentPath)
|
||
}
|
||
if (currentPath.data.type === "point") {
|
||
usePaperSupportStore.getState().handleText(currentPath)
|
||
syncPointCirclesWithPath(currentPath)
|
||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||
usePaperSupportStore.getState().handleCircles(currentPath)
|
||
}
|
||
}
|
||
} else if (hitResult && hitResult.type === "fill") {
|
||
if (
|
||
hitResult &&
|
||
hitResult.item?.data?.type === "pathCircle" &&
|
||
activePath?.data?.type === "rectangle"
|
||
) {
|
||
// 放大后 点击(只考虑了rect) 1204todo
|
||
hitIndex = hitResult.item.data.index
|
||
panMode = "segment"
|
||
} else {
|
||
panMode = "fill"
|
||
}
|
||
} else if (hitResult && hitResult.type === "stroke") {
|
||
// 镂空区域不可添加点
|
||
if (activePath && activePath.data.isHollowPolygon) return
|
||
panMode = "stroke"
|
||
// hitIndex = hitResult.location.index;
|
||
|
||
if (hitResult.item?.data?.type === "pathBuff") {
|
||
hitIndex = hitResult.item.data.index
|
||
} else {
|
||
hitIndex = hitResult.location.index
|
||
return
|
||
}
|
||
|
||
//点击多边形边,添加一个 端点
|
||
if (
|
||
activePath?.data?.type === "polygon" ||
|
||
activePath?.data?.type === "brush" ||
|
||
activePath?.data?.type === "point"
|
||
) {
|
||
// doubleclick
|
||
if (isDoubleClick) {
|
||
if (activePath instanceof paper.CompoundPath) {
|
||
const selectedChild =
|
||
(activePath.children as paper.Path[]).find(
|
||
(item) => item.data.selected
|
||
) ||
|
||
(activePath.children as paper.Path[]).reduce(
|
||
(nearest, item) => {
|
||
if (!nearest) return item
|
||
return item.getNearestPoint(point).getDistance(point) <
|
||
nearest.getNearestPoint(point).getDistance(point)
|
||
? item
|
||
: nearest
|
||
},
|
||
null as paper.Path | null
|
||
)
|
||
|
||
if (selectedChild) {
|
||
const insertPoint = selectedChild.getNearestPoint(point)
|
||
selectedChild.insert(hitIndex + 1, insertPoint)
|
||
usePaperSupportStore.getState().handleBufferPaths(selectedChild)
|
||
usePaperSupportStore.getState().handleCircles(selectedChild)
|
||
isChanged = true
|
||
panMode = "stroke"
|
||
hitIndex = hitIndex + 1
|
||
}
|
||
} else {
|
||
activePath.insert(hitIndex + 1, hitResult.location.point)
|
||
if (activePath?.data?.type === "point") {
|
||
syncPointCirclesWithPath(activePath)
|
||
}
|
||
activePath.bringToFront()
|
||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||
usePaperSupportStore.getState().handleCircles(activePath)
|
||
isChanged = true
|
||
panMode = "stroke"
|
||
hitIndex = hitIndex + 1
|
||
}
|
||
if (activePath?.data?.type === "brush") {
|
||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||
}
|
||
if (activePath?.data?.type === "point") {
|
||
// 绘制点对应的数字
|
||
usePaperSupportStore.getState().handleText(activePath)
|
||
syncPointCirclesWithPath(activePath)
|
||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||
usePaperSupportStore.getState().handleCircles(activePath)
|
||
}
|
||
}
|
||
}
|
||
} else if (hitResult && hitResult.type === "segment") {
|
||
// 镂空区域不可删除点
|
||
if (activePath && activePath.data.isHollowPolygon) return
|
||
panMode = "segment"
|
||
|
||
if (hitResult.item?.data?.type === "pathCircle") {
|
||
hitIndex = hitResult.item.data.index
|
||
} else {
|
||
hitIndex = hitResult.segment.index
|
||
}
|
||
|
||
// shift点击 删除端点
|
||
// if (e.event.shiftKey) {
|
||
// activePath.removeSegment(hitIndex);
|
||
// panMode = "";
|
||
// }
|
||
if (
|
||
activePath?.data?.type === "polygon" ||
|
||
activePath?.data?.type === "brush" ||
|
||
activePath?.data?.type === "point"
|
||
) {
|
||
if (isDoubleClick) {
|
||
if (activePath instanceof paper.CompoundPath) {
|
||
;(activePath.children as paper.Path[]).forEach((item) => {
|
||
const childHitResult = item.hitTest(point, {
|
||
segments: true,
|
||
stroke: true,
|
||
curves: true,
|
||
fill: true,
|
||
guide: false,
|
||
tolerance: usePaperStore.getState().paperScope
|
||
? 8 / usePaperStore.getState().paperScope!.view.zoom
|
||
: 0,
|
||
})
|
||
if (childHitResult) {
|
||
let checkLength = 3
|
||
if (item?.data?.type === "brush") checkLength = 2
|
||
if (item?.data?.type === "point") checkLength = 1
|
||
if (item.segments && item.segments.length <= checkLength) {
|
||
if (
|
||
item?.data?.type === "polygon" ||
|
||
item?.data?.type === "brush" ||
|
||
item?.data?.type === "point"
|
||
) {
|
||
showDeletePointLimitNotification(item.data.type)
|
||
}
|
||
return
|
||
}
|
||
;(childHitResult.item as paper.Path).removeSegment(hitIndex)
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleBufferPaths(childHitResult.item as paper.Path)
|
||
usePaperSupportStore
|
||
.getState()
|
||
.handleCircles(childHitResult.item as paper.Path)
|
||
|
||
isChanged = true
|
||
panMode = "segment"
|
||
}
|
||
})
|
||
} else {
|
||
let checkLength = 3
|
||
if (activePath?.data?.type === "brush") checkLength = 2
|
||
if (activePath?.data?.type === "point") checkLength = 1
|
||
if (
|
||
activePath.segments &&
|
||
activePath.segments.length <= checkLength
|
||
) {
|
||
if (
|
||
activePath?.data?.type === "polygon" ||
|
||
activePath?.data?.type === "brush" ||
|
||
activePath?.data?.type === "point"
|
||
) {
|
||
showDeletePointLimitNotification(activePath.data.type)
|
||
}
|
||
return
|
||
}
|
||
|
||
activePath.removeSegment(hitIndex)
|
||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||
usePaperSupportStore.getState().handleCircles(activePath)
|
||
isChanged = true
|
||
panMode = "segment"
|
||
}
|
||
if (activePath?.data?.type === "brush") {
|
||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||
}
|
||
if (activePath?.data?.type === "point") {
|
||
syncPointCirclesWithPath(activePath)
|
||
|
||
// paths.forEach((child) => {
|
||
// if (
|
||
// child.data.text ===
|
||
// activePath.segments.length - hitIndex + 1
|
||
// ) {
|
||
// // 删除 circle
|
||
// child.remove();
|
||
// } else if (
|
||
// child.data.text >
|
||
// activePath.segments.length - hitIndex + 1
|
||
// ) {
|
||
// child.data.text = child.data.text - 1;
|
||
// }
|
||
// });
|
||
// usePaperSupportStore.getState().handleMask(hitResult.item);
|
||
// usePaperSupportStore
|
||
// .getState()
|
||
// .handleText(hitResult.item as paper.Path);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
panMode = ""
|
||
}
|
||
|
||
// 当前选中路径 tags
|
||
const tags = usePaperStore.getState().group!.getItems({
|
||
data: {
|
||
id: activePath?.data?.id,
|
||
type: "tag",
|
||
},
|
||
})
|
||
// 上一选中路径 tags
|
||
const lastTags = usePaperStore.getState().group!.getItems({
|
||
data: {
|
||
id: lastTagDataId,
|
||
type: "tag",
|
||
},
|
||
})
|
||
// 仅恢复当前应显示的标签,隐藏对象的标签保持不可见
|
||
if (useTopToolsStore.getState().showTags) {
|
||
const setTagVisibleByConfig = (tag: any) => {
|
||
tag.visible = getObjectTagVisibility(
|
||
useBottomToolsStore.getState().activeImage,
|
||
Number(tag.data?.id),
|
||
tag.data?.tag_category
|
||
)
|
||
}
|
||
lastTags.forEach(setTagVisibleByConfig)
|
||
tags.forEach(setTagVisibleByConfig)
|
||
}
|
||
|
||
console.log("鼠标按下 选中路径", activePath, "当前模式", panMode)
|
||
|
||
lastPoint = point
|
||
}
|
||
|
||
panTool.onMouseDrag = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
downPoint: paper.Point
|
||
delta: paper.Point
|
||
}) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
|
||
if (useTopToolsStore.getState().isView) return
|
||
|
||
if (pressCtrl) {
|
||
const delta = e.point.subtract(e.downPoint)
|
||
if (delta.length > 10 && e.event.button === 0) {
|
||
let temp = renderRectangle(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
let downPoint = temp.globalToLocal(e.downPoint)
|
||
|
||
temp.remove()
|
||
rect = usePaperStore.getState().addRectangle(
|
||
renderRectangle,
|
||
{
|
||
from: downPoint,
|
||
to: point,
|
||
// option
|
||
strokeColor: "#FFF",
|
||
strokeWidth: 2,
|
||
fillColor: undefined,
|
||
},
|
||
{
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
id:
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
|
||
}
|
||
)
|
||
rect.removeOnDrag()
|
||
}
|
||
} else {
|
||
let point: paper.Point | undefined = undefined
|
||
if (activePath) {
|
||
point = usePaperStore.getState().group!.globalToLocal(e.point)
|
||
}
|
||
|
||
if (
|
||
activePath &&
|
||
["fill", "stroke", "segment"].includes(panMode) &&
|
||
!hasExceededPanDragThreshold
|
||
) {
|
||
const dragDistance = e.point.getDistance(e.downPoint)
|
||
if (dragDistance < getPaperDragThreshold()) return
|
||
|
||
hasExceededPanDragThreshold = true
|
||
if (usePaperStore.getState().el) {
|
||
usePaperStore.getState().el!.style.cursor = "move"
|
||
}
|
||
}
|
||
|
||
let movePath = (path: paper.Path) => {
|
||
const currentGroupScaling = usePaperStore.getState().group!.scaling
|
||
const delta = {
|
||
x: e.delta.x / currentGroupScaling.x,
|
||
y: e.delta.y / currentGroupScaling.y,
|
||
}
|
||
|
||
if (panMode === "fill") {
|
||
if (path.data.type === "brush") {
|
||
path.position = path.position.add(delta)
|
||
usePaperSupportStore
|
||
.getState()
|
||
.selectedCircles.forEach((item) => {
|
||
item.position = item.position.add(delta)
|
||
})
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
} else if (path.data.type === "point") {
|
||
path.position = path.position.add(delta)
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
usePaperSupportStore.getState().handleText(path)
|
||
let circles = usePaperStore.getState().group!.getItems({
|
||
data: {
|
||
id: path.data.id,
|
||
type: "circle",
|
||
},
|
||
}) as paper.Path[]
|
||
if (circles && circles.length) {
|
||
circles.forEach((circle) => {
|
||
circle.position = circle.position.add(delta)
|
||
})
|
||
}
|
||
usePaperSupportStore
|
||
.getState()
|
||
.selectedCircles.forEach((circle) => {
|
||
circle.position = circle.position.add(delta)
|
||
})
|
||
} else {
|
||
path.position = path.position.add(delta)
|
||
// 镂空区域移动不影响选中点移动
|
||
if (!path.data.isHollowPolygon) {
|
||
usePaperSupportStore
|
||
.getState()
|
||
.selectedCircles.forEach((item) => {
|
||
item.position = item.position.add(delta)
|
||
})
|
||
}
|
||
usePaperSupportStore.getState().handleBufferPaths(null)
|
||
}
|
||
} else if (panMode === "stroke") {
|
||
if (path.data.type === "rectangle") {
|
||
usePaperSupportStore.getState().handleBufferPaths(null)
|
||
let axis: "x" | "y" = hitIndex % 2 ? "y" : "x"
|
||
path.segments[hitIndex].point[axis] = point![axis]
|
||
path.segments[hitIndex].next.point[axis] = point![axis]
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position[axis] = point![axis]
|
||
let next =
|
||
hitIndex + 1 ===
|
||
usePaperSupportStore.getState().selectedCircles.length
|
||
? 0
|
||
: hitIndex + 1
|
||
usePaperSupportStore.getState().selectedCircles[next].position[
|
||
axis
|
||
] = point![axis]
|
||
} else if (path.data.type === "polygon") {
|
||
} else if (path.data.type === "brush") {
|
||
// path.position = path.position.add(e.delta);
|
||
// handleMask(path);
|
||
}
|
||
} else if (panMode === "segment") {
|
||
if (path.data.type === "rectangle") {
|
||
usePaperSupportStore.getState().handleBufferPaths(null)
|
||
path.segments[hitIndex].point.x = point!.x
|
||
path.segments[hitIndex].point.y = point!.y
|
||
path.segments[hitIndex].next.point[hitIndex % 2 ? "y" : "x"] =
|
||
point![hitIndex % 2 ? "y" : "x"]
|
||
path.segments[hitIndex].previous.point[hitIndex % 2 ? "x" : "y"] =
|
||
point![hitIndex % 2 ? "x" : "y"]
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.x = point!.x
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.y = point!.y
|
||
let previous =
|
||
hitIndex + 1 ===
|
||
usePaperSupportStore.getState().selectedCircles.length
|
||
? 0
|
||
: hitIndex + 1
|
||
let next =
|
||
hitIndex - 1 < 0
|
||
? usePaperSupportStore.getState().selectedCircles.length - 1
|
||
: hitIndex - 1
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
previous
|
||
].position[hitIndex % 2 ? "y" : "x"] =
|
||
point![hitIndex % 2 ? "y" : "x"]
|
||
usePaperSupportStore.getState().selectedCircles[next].position[
|
||
hitIndex % 2 ? "x" : "y"
|
||
] = point![hitIndex % 2 ? "x" : "y"]
|
||
} else if (path.data.type === "polygon") {
|
||
if (path.data.isHollowPolygon) return
|
||
|
||
usePaperSupportStore.getState().clearBufferPaths()
|
||
path.segments[hitIndex].point.x = point!.x
|
||
path.segments[hitIndex].point.y = point!.y
|
||
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.x = point!.x
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.y = point!.y
|
||
} else if (path.data.type === "brush") {
|
||
path.segments[hitIndex].point.x = point!.x
|
||
path.segments[hitIndex].point.y = point!.y
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.x = point!.x
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.y = point!.y
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
} else if (path.data.type === "point") {
|
||
const circles = syncPointCirclesWithPath(path)
|
||
|
||
if (circles[hitIndex]) {
|
||
circles[hitIndex].position.x = point!.x
|
||
circles[hitIndex].position.y = point!.y
|
||
}
|
||
// 移动路径
|
||
path.segments[hitIndex].point.x = point!.x
|
||
path.segments[hitIndex].point.y = point!.y
|
||
// 移动选中点
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.x = point!.x
|
||
usePaperSupportStore.getState().selectedCircles[
|
||
hitIndex
|
||
]!.position.y = point!.y
|
||
|
||
// 移动蒙版
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
usePaperSupportStore.getState().handleText(path as paper.Path)
|
||
}
|
||
}
|
||
}
|
||
// 如果 activePath 被选中了 即可以被移动修改
|
||
if (activePath?.data.selected) {
|
||
movePath(activePath)
|
||
isChanged = true
|
||
} else {
|
||
// 没被选中可能是children被选中
|
||
if (activePath instanceof paper.CompoundPath) {
|
||
// let selectedItems = activePath.children.filter(
|
||
// (item) => item.data.selected
|
||
// );
|
||
// console.log("selectedItems", selectedItems);
|
||
|
||
const selectedItem: paper.Path = (
|
||
activePath.children as paper.Path[]
|
||
).filter((item) => item.data.selected)[0]
|
||
;(activePath.children as paper.Path[]).forEach((item) => {
|
||
if (item && item.data.selected) {
|
||
movePath(item as paper.Path)
|
||
isChanged = true
|
||
} else if (
|
||
item.data.isHollowPolygon &&
|
||
selectedItem &&
|
||
selectedItem.bounds.contains(item.bounds)
|
||
) {
|
||
// 当前被选中路径的镂空区域需要一起移动
|
||
movePath(item as paper.Path)
|
||
isChanged = true
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 改变对象不能用 activeImage activeOperation (因为图形只在对应图片渲染,所以activeImage是一样的)
|
||
panTool.onMouseUp = (_e: paper.ToolEvent) => {
|
||
if (stopMiddleMousePan()) return
|
||
if (useTopToolsStore.getState().isView) return
|
||
hasExceededPanDragThreshold = false
|
||
if (usePaperStore.getState().el) {
|
||
usePaperStore.getState().el!.style.cursor = "default"
|
||
}
|
||
|
||
let newDrawPath: paper.Path | paper.CompoundPath | null = null
|
||
|
||
if (pressCtrl) {
|
||
if (rect) handleCtrl(rect)
|
||
rect?.remove()
|
||
return
|
||
}
|
||
|
||
const handleBrushIntersectRaster = (currentPath: paper.Path) => {
|
||
const currentSegments = currentPath.segments
|
||
if (!currentSegments || (currentSegments && !currentSegments.length))
|
||
return
|
||
// 设置首尾连线 判断是否跟自身当前有交点
|
||
let flag: boolean
|
||
if (currentSegments.length === 2) flag = false
|
||
else if (currentSegments.length > 2) {
|
||
const line = new paper.Path([
|
||
currentSegments[0],
|
||
currentSegments[currentSegments.length - 1],
|
||
])
|
||
const lineIntersections = line.getIntersections(currentPath)
|
||
flag = lineIntersections.length > 1 ? false : true
|
||
} else {
|
||
flag = false
|
||
}
|
||
let intersections = currentPath.getIntersections(
|
||
usePaperStore.getState().rasterPath!
|
||
)
|
||
let validIntersections = intersections.filter((intersection) => {
|
||
// 检查交点是否在两个路径的可见部分
|
||
return (
|
||
currentPath.contains(intersection.point) &&
|
||
usePaperStore.getState().rasterPath!.contains(intersection.point)
|
||
)
|
||
})
|
||
|
||
// intersectionPath as paper.CompoundPath | paper.Path | null
|
||
let intersectionPath: any = currentPath.intersect(
|
||
usePaperStore.getState().rasterPath!,
|
||
{
|
||
insert: false,
|
||
trace: flag,
|
||
}
|
||
)
|
||
|
||
// 没有 validIntersections 不进行操作
|
||
if (validIntersections.length) {
|
||
// 处理相交部分
|
||
if (intersectionPath instanceof paper.CompoundPath) {
|
||
// intersectionPath是CompoundPath
|
||
let new_segments: any[] = []
|
||
if (
|
||
intersectionPath.children &&
|
||
!intersectionPath.children.length
|
||
) {
|
||
// unconfirm activePath被修改了
|
||
;(activePath.children as paper.Path[]).forEach(
|
||
(activeChild, index) => {
|
||
if (index !== 0) {
|
||
new_segments.push(...activeChild.segments)
|
||
}
|
||
}
|
||
)
|
||
activePath.removeChildren(1)
|
||
currentPath.segments = new_segments
|
||
} else {
|
||
;(intersectionPath.children as paper.Path[]).forEach((child) => {
|
||
if (child.segments && child.segments.length) {
|
||
new_segments.push(...child.segments)
|
||
}
|
||
})
|
||
currentPath.segments = new_segments
|
||
}
|
||
} else if (
|
||
intersectionPath.segments &&
|
||
intersectionPath.segments.length
|
||
) {
|
||
currentPath.segments = intersectionPath.segments
|
||
}
|
||
|
||
// let oldCompoundPolygon = compoundPolygon;
|
||
// compoundPolygon = oldCompoundPolygon?.intersect(
|
||
// usePaperStore.getState().rasterPath!
|
||
// ) as paper.Path;
|
||
// compoundPolygon.closed = false;
|
||
// oldCompoundPolygon?.remove();
|
||
// usePaperSupportStore.getState().handleMask(currentPath);
|
||
} else {
|
||
// path整体移动到图片外 清空path的点
|
||
if (
|
||
!usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(currentPath.bounds)
|
||
) {
|
||
currentPath.segments = []
|
||
}
|
||
}
|
||
intersectionPath.remove()
|
||
intersectionPath = null
|
||
usePaperSupportStore.getState().handleMask(currentPath)
|
||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||
usePaperSupportStore.getState().handleCircles(currentPath)
|
||
}
|
||
|
||
const handlePointIntersectRaster = (path: paper.Path) => {
|
||
path.segments = path.segments.filter((segment) => {
|
||
return usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(segment.point)
|
||
})
|
||
syncPointCirclesWithPath(path)
|
||
// 去除超出图片边界的部分 end
|
||
usePaperSupportStore.getState().handleMask(path)
|
||
usePaperSupportStore.getState().handleBufferPaths(path)
|
||
usePaperSupportStore.getState().handleCircles(path)
|
||
usePaperSupportStore.getState().handleText(path)
|
||
}
|
||
|
||
const getNewDetail = (detailPathData: {
|
||
imageId: string
|
||
operationId: any
|
||
id: number
|
||
}) => {
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(
|
||
detailPathData.imageId,
|
||
detailPathData.operationId,
|
||
detailPathData.id
|
||
)
|
||
const newDetail = currentDetail
|
||
? {
|
||
...currentDetail,
|
||
first_modified_timestamp:
|
||
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid: currentDetail.first_modified_uid || user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid: user_id,
|
||
}
|
||
: {
|
||
...initialDetail,
|
||
create_timestamp: dayjs().unix(),
|
||
category_id: Number(detailPathData.operationId),
|
||
}
|
||
return newDetail
|
||
}
|
||
|
||
// 保存矩形类型数据结果
|
||
const handleSaveRect = (rectPath: any) => {
|
||
// 去除超出图片边界的部分
|
||
handleRectangleIntersectRaster(rectPath)
|
||
usePaperSupportStore.getState().handleBufferPaths(rectPath)
|
||
usePaperSupportStore.getState().handleCircles(rectPath)
|
||
const newDetail = getNewDetail(rectPath.data)
|
||
let pathData = JSON.parse(rectPath.exportJSON({ precision: 20 }))
|
||
|
||
if (rectPath.data.id && rectPath.segments?.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(rectPath.data.imageId, rectPath.data.operationId, [
|
||
rectPath.data.id,
|
||
[pathData[1].segments],
|
||
newDetail,
|
||
[],
|
||
])
|
||
} else {
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
rectPath.data.imageId,
|
||
rectPath.data.operationId,
|
||
rectPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
rectPath.data.imageId,
|
||
rectPath.data.id,
|
||
rectPath.data.parentGroupId
|
||
)
|
||
}
|
||
|
||
forceUpdate()
|
||
}
|
||
|
||
// 保存多边形类型数据结果
|
||
const handleSavePolygon = (polygonPath: any) => {
|
||
newDrawPath = new paper.Path()
|
||
let parent: any = usePaperStore.getState().group
|
||
let hollowPaths: Array<{ path: paper.Path; index: number }> = []
|
||
let area = 0
|
||
if (polygonPath instanceof paper.CompoundPath) {
|
||
let oldSelected = (polygonPath.children as paper.Path[]).find(
|
||
(item) => item.data.selected
|
||
)!
|
||
parent = polygonPath
|
||
newDrawPath.set({
|
||
segments: oldSelected.segments,
|
||
data: oldSelected.data,
|
||
strokeColor: oldSelected.strokeColor,
|
||
strokeWidth: oldSelected.strokeWidth,
|
||
fillColor: oldSelected.fillColor,
|
||
closed: oldSelected.closed,
|
||
parent: usePaperStore.getState().group,
|
||
})
|
||
oldSelected.remove()
|
||
// newDrawPath = (polygonPath.children as paper.Path[]).find(
|
||
// (item) => item.data.selected
|
||
// )!;
|
||
area = (newDrawPath as paper.Path).area
|
||
// 设置镂空数组
|
||
polygonPath.children.forEach((child: any, index: number) => {
|
||
if (
|
||
child.data.isHollowPolygon &&
|
||
newDrawPath?.bounds.contains(child.bounds)
|
||
) {
|
||
hollowPaths.push({ path: child, index })
|
||
}
|
||
})
|
||
} else {
|
||
newDrawPath = polygonPath
|
||
area = (newDrawPath as paper.Path).area
|
||
}
|
||
console.log("保存当前被选中路径的结果", newDrawPath)
|
||
|
||
const nextDrawPath = executePaperPolygonBooleanPipeline({
|
||
group: usePaperStore.getState().group,
|
||
rasterPath: usePaperStore.getState().rasterPath,
|
||
shape: newDrawPath as paper.Path | paper.CompoundPath,
|
||
drawOption: useTopToolsStore.getState()
|
||
.drawOption as BooleanPipelineDrawOption,
|
||
rasterClipOrder: "before",
|
||
})
|
||
newDrawPath = nextDrawPath as any
|
||
usePaperSupportStore.getState().handleCircles(newDrawPath)
|
||
console.log("边界裁剪之后", newDrawPath)
|
||
|
||
// 路径已发生改变则清空之前存的镂空区域,后续无需处理
|
||
// if (
|
||
// newDrawPath instanceof paper.CompoundPath ||
|
||
// (newDrawPath instanceof paper.Path && newDrawPath.area !== area)
|
||
// ) {
|
||
// hollowPaths.forEach((item) => {
|
||
// item.path?.remove();
|
||
// });
|
||
// hollowPaths = [];
|
||
// }
|
||
|
||
console.log("裁剪之后", newDrawPath)
|
||
|
||
if (
|
||
!newDrawPath ||
|
||
newDrawPath instanceof paper.CompoundPath ||
|
||
(newDrawPath instanceof paper.Path && newDrawPath.area !== area)
|
||
) {
|
||
hollowPaths.forEach((item) => {
|
||
item.path?.remove()
|
||
})
|
||
hollowPaths = []
|
||
} else {
|
||
hollowPaths = hollowPaths.filter((item) => {
|
||
let subtractPath = newDrawPath!.subtract(item.path!, {
|
||
insert: false,
|
||
})
|
||
if (subtractPath instanceof paper.Path) {
|
||
item.path?.remove()
|
||
return false
|
||
} else {
|
||
return true
|
||
}
|
||
})
|
||
}
|
||
|
||
// if (
|
||
// newDrawPath instanceof paper.CompoundPath ||
|
||
// (newDrawPath instanceof paper.Path && newDrawPath.area !== area)
|
||
// ) {
|
||
// hollowPaths.forEach((item) => {
|
||
// item.path?.remove();
|
||
// });
|
||
// hollowPaths = [];
|
||
// }
|
||
// usePaperSupportStore.getState().handleCircles(newDrawPath);
|
||
// console.log("边界裁剪之后", newDrawPath);
|
||
|
||
if (newDrawPath instanceof paper.CompoundPath) {
|
||
let selectedItems = usePaperStore.getState().group!.getItems({
|
||
data: { selected: true },
|
||
})
|
||
selectedItems.forEach((item) => {
|
||
item.data.selected = false
|
||
})
|
||
handleCancelSelect()
|
||
}
|
||
|
||
// 存在镂空区域时,对镂空区域进行边界裁剪
|
||
if (hollowPaths.length) {
|
||
hollowPaths.forEach((hollowPath) => {
|
||
if (
|
||
!usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(hollowPath.path.bounds)
|
||
) {
|
||
polygonPath.removeChildren(hollowPath.index, hollowPath.index + 1)
|
||
return
|
||
}
|
||
// hollowPath.path.clockwise = false;
|
||
hollowPath.path.data = safeClone(
|
||
Object.assign({}, hollowPath.path?.data || {}, {
|
||
isHollowPolygon: true,
|
||
})
|
||
)
|
||
// let oldPolygon: any = hollowPath.path;
|
||
// let finnalPath = oldPolygon.intersect(
|
||
// usePaperStore.getState().rasterPath!
|
||
// ) as any;
|
||
// console.log("finnalPath", finnalPath);
|
||
|
||
// // if (hollowPath.clockwise) hollowPath.reverse();
|
||
// if (finnalPath instanceof paper.CompoundPath) {
|
||
// (finnalPath.children as paper.Path[]).forEach((child) => {
|
||
// // child.clockwise = false;
|
||
// child.data = safeClone(
|
||
// Object.assign({}, oldPolygon?.data || {}, {
|
||
// isHollowPolygon: true,
|
||
// })
|
||
// );
|
||
// });
|
||
// } else if (finnalPath instanceof paper.Path) {
|
||
// // finnalPath.clockwise = false;
|
||
// finnalPath.data = safeClone(
|
||
// Object.assign({}, oldPolygon?.data || {}, {
|
||
// isHollowPolygon: true,
|
||
// })
|
||
// );
|
||
// }
|
||
// oldPolygon.remove();
|
||
// oldPolygon = null;
|
||
|
||
// polygonPath.removeChildren(hollowPath.index, hollowPath.index + 1);
|
||
// polygonPath.addChild(finnalPath);
|
||
})
|
||
}
|
||
|
||
if (!(parent instanceof paper.Group)) {
|
||
if (newDrawPath instanceof paper.CompoundPath) {
|
||
let len = newDrawPath.children.length
|
||
for (let i = len - 1; i >= 0; i--) {
|
||
let child = newDrawPath.children[i]
|
||
child.set({
|
||
parent: parent,
|
||
})
|
||
}
|
||
} else {
|
||
newDrawPath?.set({
|
||
parent: parent,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 创建对象 重新渲染对象 都需要添加 imageId operationId
|
||
if (
|
||
polygonPath.data.imageId &&
|
||
polygonPath.data.operationId &&
|
||
polygonPath.data.id
|
||
) {
|
||
const newDetail = getNewDetail(polygonPath.data)
|
||
let savedItems = usePaperStore.getState().group!.getItems({
|
||
data: { id: polygonPath.data.id },
|
||
}) as paper.Path[]
|
||
|
||
console.log(savedItems)
|
||
|
||
let saveCompoundItems = savedItems.filter(
|
||
(item) =>
|
||
item instanceof paper.Path &&
|
||
item.data.type === "polygon" &&
|
||
item.segments.length
|
||
)
|
||
|
||
let handleSavePaths = (paths: paper.Path[]) => {
|
||
const { points: polygonPoints, hollowPoints: polygonHollowPoints } =
|
||
multiPolygonToStoredContours(
|
||
paperShapeToMultiPolygon(
|
||
paths.length === 1
|
||
? paths[0]
|
||
: ({ children: paths } as { children: paper.Path[] })
|
||
)
|
||
)
|
||
let bool = polygonPoints.length > 0
|
||
|
||
if (bool) {
|
||
console.log(
|
||
"编辑后最终保存结果",
|
||
polygonPoints,
|
||
polygonHollowPoints
|
||
)
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
polygonPath.data.imageId,
|
||
polygonPath.data.operationId,
|
||
[
|
||
polygonPath.data.id,
|
||
polygonPoints,
|
||
newDetail,
|
||
polygonHollowPoints,
|
||
]
|
||
)
|
||
} else {
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
polygonPath.data.imageId,
|
||
polygonPath.data.operationId,
|
||
polygonPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
polygonPath.data.imageId,
|
||
polygonPath.data.id,
|
||
polygonPath.data.parentGroupId
|
||
)
|
||
}
|
||
}
|
||
|
||
handleSavePaths(saveCompoundItems)
|
||
activePath =
|
||
newDrawPath?.parent instanceof paper.CompoundPath
|
||
? newDrawPath.parent
|
||
: (newDrawPath as any)
|
||
forceUpdate()
|
||
}
|
||
}
|
||
|
||
// 保存线段类型数据结果
|
||
const handleSaveBrush = (brushPath: any) => {
|
||
if (brushPath instanceof paper.CompoundPath) {
|
||
;(brushPath.children as paper.Path[]).forEach((path) => {
|
||
handleBrushIntersectRaster(path)
|
||
})
|
||
} else {
|
||
handleBrushIntersectRaster(brushPath)
|
||
}
|
||
|
||
if (
|
||
brushPath.data.imageId &&
|
||
brushPath.data.operationId &&
|
||
brushPath.data.id
|
||
) {
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
brushPath.data.id
|
||
)
|
||
const newDetail = currentDetail
|
||
? {
|
||
...currentDetail,
|
||
first_modified_timestamp:
|
||
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid: currentDetail.first_modified_uid || user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid: user_id,
|
||
}
|
||
: {
|
||
...initialDetail,
|
||
create_timestamp: dayjs().unix(),
|
||
category_id: Number(brushPath.data.operationId),
|
||
}
|
||
if (brushPath instanceof paper.CompoundPath) {
|
||
const brushChildren = brushPath.children
|
||
if (brushChildren && brushChildren.length) {
|
||
const points: any[] = []
|
||
;(brushChildren as paper.Path[]).forEach((childPath) => {
|
||
if (childPath.segments && childPath.segments.length) {
|
||
let pathData = JSON.parse(
|
||
childPath.exportJSON({ precision: 20 })
|
||
)
|
||
points.push(pathData[1]?.segments)
|
||
}
|
||
})
|
||
if (points.length)
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
[brushPath.data.id, points, newDetail, []]
|
||
)
|
||
else {
|
||
usePaperSupportStore
|
||
.getState()
|
||
.removeMaskById(brushPath.data.id)
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
brushPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
brushPath.data.imageId,
|
||
brushPath.data.id,
|
||
brushPath.data.parentGroupId
|
||
)
|
||
}
|
||
}
|
||
} else if (brushPath instanceof paper.Path) {
|
||
if (brushPath.segments && brushPath.segments.length) {
|
||
const pathData = JSON.parse(
|
||
brushPath.exportJSON({ precision: 20 })
|
||
)
|
||
const brushSegments = pathData?.[1]?.segments
|
||
if (brushSegments?.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
[brushPath.data.id, [brushSegments], newDetail, []]
|
||
)
|
||
} else {
|
||
usePaperSupportStore
|
||
.getState()
|
||
.removeMaskById(brushPath.data.id)
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
brushPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
brushPath.data.imageId,
|
||
brushPath.data.id,
|
||
brushPath.data.parentGroupId
|
||
)
|
||
}
|
||
} else {
|
||
usePaperSupportStore.getState().removeMaskById(brushPath.data.id)
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
brushPath.data.imageId,
|
||
brushPath.data.operationId,
|
||
brushPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
brushPath.data.imageId,
|
||
brushPath.data.id,
|
||
brushPath.data.parentGroupId
|
||
)
|
||
}
|
||
}
|
||
|
||
forceUpdate()
|
||
}
|
||
}
|
||
|
||
const handleSavePoint = (pointPath: any) => {
|
||
handlePointIntersectRaster(pointPath)
|
||
if (
|
||
pointPath.data.imageId &&
|
||
pointPath.data.operationId &&
|
||
pointPath.data.id
|
||
) {
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(
|
||
pointPath.data.imageId,
|
||
pointPath.data.operationId,
|
||
pointPath.data.id
|
||
)
|
||
const newDetail = currentDetail
|
||
? {
|
||
...currentDetail,
|
||
first_modified_timestamp:
|
||
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid: currentDetail.first_modified_uid || user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid: user_id,
|
||
}
|
||
: {
|
||
...initialDetail,
|
||
create_timestamp: dayjs().unix(),
|
||
category_id: Number(pointPath.data.operationId),
|
||
}
|
||
if (pointPath instanceof paper.Path) {
|
||
if (pointPath.segments && pointPath.segments.length) {
|
||
const pathData = JSON.parse(
|
||
pointPath.exportJSON({ precision: 20 })
|
||
)
|
||
const pointSegments = pathData?.[1]?.segments
|
||
if (pointSegments?.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
pointPath.data.imageId,
|
||
pointPath.data.operationId,
|
||
[pointPath.data.id, [pointSegments], newDetail, []]
|
||
)
|
||
} else {
|
||
usePaperSupportStore
|
||
.getState()
|
||
.removeMaskById(pointPath.data.id)
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
pointPath.data.imageId,
|
||
pointPath.data.operationId,
|
||
pointPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
pointPath.data.imageId,
|
||
pointPath.data.id,
|
||
pointPath.data.parentGroupId
|
||
)
|
||
}
|
||
} else {
|
||
usePaperSupportStore.getState().removeMaskById(pointPath.data.id)
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
pointPath.data.imageId,
|
||
pointPath.data.operationId,
|
||
pointPath.data.id
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
pointPath.data.imageId,
|
||
pointPath.data.id,
|
||
pointPath.data.parentGroupId
|
||
)
|
||
}
|
||
}
|
||
|
||
forceUpdate()
|
||
}
|
||
}
|
||
|
||
if (isChanged) {
|
||
if (
|
||
(activePath && activePath.data.selected) ||
|
||
(activePath?.children?.length &&
|
||
activePath?.children?.findIndex((item) => item.data.selected) !==
|
||
-1)
|
||
) {
|
||
if (
|
||
panMode === "segment" ||
|
||
panMode === "fill" ||
|
||
panMode === "stroke"
|
||
) {
|
||
if (activePath.data.type === "rectangle") {
|
||
handleSaveRect(activePath)
|
||
} else if (activePath.data.type === "brush") {
|
||
handleSaveBrush(activePath)
|
||
} else if (activePath.data.type === "polygon") {
|
||
handleSavePolygon(activePath)
|
||
} else if (activePath.data.type === "point") {
|
||
handleSavePoint(activePath)
|
||
}
|
||
}
|
||
} else {
|
||
// 没被选中
|
||
}
|
||
|
||
isChanged = false
|
||
}
|
||
}
|
||
panTool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "delete") {
|
||
handleDelete()
|
||
usePaperSupportStore.getState().clearTexts()
|
||
|
||
let ids =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
]
|
||
|
||
usePaperSupportStore.getState().removeMaskByIds(ids)
|
||
|
||
forceUpdate()
|
||
} else if (e.key === "shift") {
|
||
pressShift = true
|
||
useKeyEventStore.getState().setShift(true)
|
||
} else if (e.key === "control") {
|
||
pressCtrl = true
|
||
}
|
||
}
|
||
panTool.onKeyUp = (e: paper.KeyEvent) => {
|
||
if (e.key === "shift") {
|
||
pressShift = false
|
||
useKeyEventStore.getState().setShift(false)
|
||
} else if (e.key === "control") {
|
||
pressCtrl = false
|
||
}
|
||
}
|
||
panTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
}
|
||
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
panTool: panTool,
|
||
}))
|
||
},
|
||
addPolygon: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let polygon = renderPath(
|
||
usePaperStore.getState().group!,
|
||
Object.assign(option, {
|
||
strokeScaling: false,
|
||
})
|
||
)
|
||
polygon.data = Object.assign({ type: "polygon", ...option }, data)
|
||
insertItemBelowTags(polygon)
|
||
points?.forEach((point) => {
|
||
polygon.add(point)
|
||
})
|
||
|
||
polygon.onMouseEnter = (_e: paper.MouseEvent) => {
|
||
// if (usePaperStore.getState().mode === "pan") {
|
||
// e.target.strokeWidth += 2;
|
||
// if (usePaperStore.getState().el)
|
||
// usePaperStore.getState().el!.style.cursor = "move";
|
||
// }
|
||
}
|
||
polygon.onMouseLeave = (_e: paper.MouseEvent) => {
|
||
// if (usePaperStore.getState().mode === "pan") {
|
||
// // render 结束没有触发 onMouseEnter
|
||
// if (e.target.strokeWidth - 2 > 0) e.target.strokeWidth -= 2;
|
||
// if (usePaperStore.getState().el)
|
||
// usePaperStore.getState().el!.style.cursor = "default";
|
||
// }
|
||
}
|
||
|
||
polygon.onDoubleClick = (e: paper.MouseEvent) => {
|
||
console.log("polygon.onDoubleClick", e)
|
||
}
|
||
polygon.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
if (polygon.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === polygon.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: polygon.data.imageId,
|
||
operationId: polygon.data.operationId,
|
||
id: polygon.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return polygon
|
||
},
|
||
addCompoundPath: (
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
children: paper.Path[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let compoundPath = renderCompoundPath(
|
||
usePaperStore.getState().group!,
|
||
Object.assign(option, {
|
||
children,
|
||
strokeScaling: false,
|
||
})
|
||
)
|
||
compoundPath.data = Object.assign({ type: "polygon", ...option }, data)
|
||
insertItemBelowTags(compoundPath)
|
||
compoundPath.onMouseDown = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
}) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
if (compoundPath.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() === compoundPath.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: compoundPath.data.imageId,
|
||
operationId: compoundPath.data.operationId,
|
||
id: compoundPath.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return compoundPath
|
||
},
|
||
initPolygonTool: (
|
||
paperPolygonTool: paper.Tool,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => {
|
||
let selectedCircles: paper.Path.Circle[] = []
|
||
let polygon: paper.Path | paper.CompoundPath | null
|
||
let compoundPolygon: paper.Path | paper.CompoundPath | null
|
||
let points: paper.Point[] = []
|
||
let lastPoint: paper.Point | null = null
|
||
let lastTime = 0
|
||
let isMouseDown: boolean = false
|
||
let polygonTool = paperPolygonTool
|
||
let currentMagnetPoint: paper.Point | null = null
|
||
let draftId: number | null = null
|
||
let continueSourceItems: Array<paper.Path | paper.CompoundPath> = []
|
||
let continueTargetContourIndex: number | null = null
|
||
let continueTargetContext: ContinuePolygonTarget | null = null
|
||
let continueTargetSnapshot: LabelPathTuple | null = null
|
||
let continueDraftHints = new Map<
|
||
string,
|
||
{ contourIndex: number; points: paper.Point[] }
|
||
>()
|
||
|
||
const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
|
||
// 更新选中的点
|
||
if (selectedCircles && selectedCircles.length) {
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
}
|
||
path &&
|
||
path instanceof paper.Path &&
|
||
path.segments.forEach((segment) => {
|
||
let circle = usePaperStore
|
||
.getState()
|
||
.getClickedCircle(
|
||
segment.point,
|
||
segment.index,
|
||
path.data.id,
|
||
path.data.blankColor
|
||
)
|
||
selectedCircles.push(circle)
|
||
})
|
||
}
|
||
|
||
const getDraftPolygonData = () => ({
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
id:
|
||
draftId ??
|
||
(draftId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1),
|
||
})
|
||
|
||
const clearPolygonMagnet = () => {
|
||
const magnetPaths = usePaperStore
|
||
.getState()
|
||
.group!.getItems({ data: { type: "magnet" } })
|
||
magnetPaths.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
currentMagnetPoint = null
|
||
}
|
||
|
||
const getPointFromSegment = (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 getContinueCandidatePaths = (objectId: number) => {
|
||
const flattenedPaths: paper.Path[] = []
|
||
const visitedPaths = new Set<paper.Path>()
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(objectId)
|
||
.filter((item) => item.data?.type === "polygon")
|
||
.forEach((item) => {
|
||
if (item instanceof paper.CompoundPath) {
|
||
;(item.children as paper.Path[]).forEach((child) => {
|
||
if (visitedPaths.has(child)) return
|
||
visitedPaths.add(child)
|
||
flattenedPaths.push(child)
|
||
})
|
||
return
|
||
}
|
||
if (item instanceof paper.Path && !visitedPaths.has(item)) {
|
||
visitedPaths.add(item)
|
||
flattenedPaths.push(item)
|
||
}
|
||
})
|
||
|
||
const candidatePaths = flattenedPaths.filter(
|
||
(path) => path.segments?.length > 3
|
||
)
|
||
if (!candidatePaths.length) return [] as paper.Path[]
|
||
|
||
const preferredPaths = candidatePaths.filter(
|
||
(path) => !path.data?.isHollowPolygon
|
||
)
|
||
return preferredPaths.length ? preferredPaths : candidatePaths
|
||
}
|
||
|
||
const clonePaperPoints = (pathPoints: paper.Point[]) => {
|
||
return pathPoints.map((point) => new paper.Point(point))
|
||
}
|
||
|
||
const removeDuplicatedClosingPoint = (pathPoints: paper.Point[]) => {
|
||
if (pathPoints.length <= 1) return pathPoints
|
||
const firstPoint = pathPoints[0]
|
||
const lastPoint = pathPoints[pathPoints.length - 1]
|
||
if (
|
||
Math.abs(firstPoint.x - lastPoint.x) < 0.000001 &&
|
||
Math.abs(firstPoint.y - lastPoint.y) < 0.000001
|
||
) {
|
||
pathPoints.pop()
|
||
}
|
||
return pathPoints
|
||
}
|
||
|
||
const getContinueContourPoints = (segments: any[] | undefined) => {
|
||
return removeDuplicatedClosingPoint(
|
||
(segments || [])
|
||
.map((segment) => getPointFromSegment(segment))
|
||
.filter((point): point is paper.Point => point !== null)
|
||
)
|
||
}
|
||
|
||
const getContinueDraftHintKey = (target: ContinuePolygonTarget) => {
|
||
return `${target.imageId}::${target.operationId}::${target.objectId}`
|
||
}
|
||
|
||
const normalizeComparableContour = (pathPoints: paper.Point[]) => {
|
||
return removeDuplicatedClosingPoint(clonePaperPoints(pathPoints)).map(
|
||
(point) => [point.x, point.y] as [number, number]
|
||
)
|
||
}
|
||
|
||
const isSameComparablePoint = (
|
||
pointA: [number, number],
|
||
pointB: [number, number]
|
||
) => {
|
||
return (
|
||
Math.abs(pointA[0] - pointB[0]) < 0.000001 &&
|
||
Math.abs(pointA[1] - pointB[1]) < 0.000001
|
||
)
|
||
}
|
||
|
||
const isSameContourOrder = (
|
||
contourA: [number, number][],
|
||
contourB: [number, number][]
|
||
) => {
|
||
if (contourA.length !== contourB.length) return false
|
||
if (!contourA.length) return false
|
||
for (let offset = 0; offset < contourB.length; offset += 1) {
|
||
let matched = true
|
||
for (let index = 0; index < contourA.length; index += 1) {
|
||
if (
|
||
!isSameComparablePoint(
|
||
contourA[index],
|
||
contourB[(index + offset) % contourB.length]
|
||
)
|
||
) {
|
||
matched = false
|
||
break
|
||
}
|
||
}
|
||
if (matched) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
const isSameContour = (pathA: paper.Point[], pathB: paper.Point[]) => {
|
||
const contourA = normalizeComparableContour(pathA)
|
||
const contourB = normalizeComparableContour(pathB)
|
||
if (contourA.length !== contourB.length || !contourA.length) {
|
||
return false
|
||
}
|
||
return (
|
||
isSameContourOrder(contourA, contourB) ||
|
||
isSameContourOrder(contourA, [...contourB].reverse())
|
||
)
|
||
}
|
||
|
||
const findMatchingContinueContourIndex = (
|
||
candidatePoints: paper.Point[],
|
||
contours: any[]
|
||
) => {
|
||
for (let index = contours.length - 1; index >= 0; index -= 1) {
|
||
const contourPoints = getContinueContourPoints(contours[index])
|
||
if (isSameContour(candidatePoints, contourPoints)) {
|
||
return index
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
const setContinueDraftHint = (
|
||
target: ContinuePolygonTarget,
|
||
contourIndex: number,
|
||
contourPoints: paper.Point[]
|
||
) => {
|
||
if (contourIndex < 0 || !contourPoints.length) return
|
||
continueDraftHints.set(getContinueDraftHintKey(target), {
|
||
contourIndex,
|
||
points: removeDuplicatedClosingPoint(clonePaperPoints(contourPoints)),
|
||
})
|
||
}
|
||
|
||
const clearContinueSourceItems = (restoreVisible: boolean) => {
|
||
continueSourceItems.forEach((item) => {
|
||
if (restoreVisible) {
|
||
item.visible = true
|
||
} else {
|
||
item.remove()
|
||
}
|
||
})
|
||
continueSourceItems = []
|
||
}
|
||
|
||
const clearContinuePolygonContext = (restoreVisible: boolean) => {
|
||
clearContinueSourceItems(restoreVisible)
|
||
continueTargetContourIndex = null
|
||
continueTargetContext = null
|
||
continueTargetSnapshot = null
|
||
usePaperStore.getState().setContinuePolygonTarget(null)
|
||
}
|
||
|
||
const hasContinuePolygonDraft = () => {
|
||
return !!(
|
||
continueTargetContext ||
|
||
continueTargetSnapshot ||
|
||
usePaperStore.getState().continuePolygonTarget
|
||
)
|
||
}
|
||
|
||
const startContinuePolygonDraft = () => {
|
||
const target = usePaperStore.getState().continuePolygonTarget
|
||
if (!target) return false
|
||
|
||
const imageLabel = useLabelStore.getState().label.get(target.imageId)
|
||
const operationPaths = imageLabel?.get(target.operationId) || []
|
||
const targetTuple =
|
||
operationPaths.find((path) => path[0] === target.objectId) || null
|
||
if (!targetTuple) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "未找到可继续标注的对象",
|
||
})
|
||
clearContinuePolygonContext(true)
|
||
return false
|
||
}
|
||
|
||
const nextPoints = safeClone(targetTuple[1] || [])
|
||
const continuePaths = getContinueCandidatePaths(target.objectId)
|
||
const continuePath = continuePaths[continuePaths.length - 1]
|
||
const continuePathPoints = continuePath?.segments?.length
|
||
? removeDuplicatedClosingPoint(
|
||
continuePath.segments.map(
|
||
(segment) => new paper.Point(segment.point)
|
||
)
|
||
)
|
||
: []
|
||
const continueHint = continueDraftHints.get(
|
||
getContinueDraftHintKey(target)
|
||
)
|
||
|
||
let contourIndex = -1
|
||
if (
|
||
continueHint &&
|
||
continueHint.contourIndex >= 0 &&
|
||
continueHint.contourIndex < nextPoints.length
|
||
) {
|
||
contourIndex = continueHint.contourIndex
|
||
} else if (continuePathPoints.length) {
|
||
contourIndex = findMatchingContinueContourIndex(
|
||
continuePathPoints,
|
||
nextPoints
|
||
)
|
||
}
|
||
for (
|
||
let i = nextPoints.length - 1;
|
||
contourIndex === -1 && i >= 0;
|
||
i -= 1
|
||
) {
|
||
if (nextPoints[i]?.length > 2) {
|
||
contourIndex = i
|
||
break
|
||
}
|
||
}
|
||
if (contourIndex === -1 && nextPoints.length) {
|
||
contourIndex = nextPoints.length - 1
|
||
}
|
||
|
||
let sourceContourPoints = continueHint?.points?.length
|
||
? clonePaperPoints(continueHint.points)
|
||
: []
|
||
|
||
if (
|
||
sourceContourPoints.length &&
|
||
continuePathPoints.length &&
|
||
!isSameContour(sourceContourPoints, continuePathPoints)
|
||
) {
|
||
sourceContourPoints = []
|
||
}
|
||
|
||
if (!sourceContourPoints.length && continuePathPoints.length) {
|
||
sourceContourPoints = clonePaperPoints(continuePathPoints)
|
||
}
|
||
|
||
if (!sourceContourPoints.length && contourIndex >= 0) {
|
||
sourceContourPoints = getContinueContourPoints(nextPoints[contourIndex])
|
||
}
|
||
|
||
const contourPoints = sourceContourPoints
|
||
.slice(0, -1)
|
||
.map((point) => new paper.Point(point))
|
||
|
||
if (!sourceContourPoints.length) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前对象没有可继续标注的区域",
|
||
})
|
||
clearContinuePolygonContext(true)
|
||
return false
|
||
}
|
||
|
||
if (contourPoints.length < 2) {
|
||
notifications.show({
|
||
color: "yellow",
|
||
message: "当前区域至少保留2个节点,无法继续撤回",
|
||
})
|
||
clearContinuePolygonContext(true)
|
||
return false
|
||
}
|
||
|
||
clearContinueSourceItems(true)
|
||
continueSourceItems = usePaperStore
|
||
.getState()
|
||
.getItemsById(target.objectId)
|
||
.filter((item) => item.data?.type === "polygon")
|
||
continueSourceItems.forEach((item) => {
|
||
item.visible = false
|
||
})
|
||
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: { top: 0, left: 0 },
|
||
imageId: target.imageId,
|
||
operationId: target.operationId,
|
||
id: target.objectId,
|
||
})
|
||
usePaperSupportStore.getState().clearAll()
|
||
useObjectStore.getState().updateSelectedPath(target.imageId, "ADD")
|
||
|
||
continueTargetContourIndex = contourIndex
|
||
continueTargetContext = target
|
||
continueTargetSnapshot = safeClone(targetTuple) as LabelPathTuple
|
||
points = contourPoints
|
||
draftId = target.objectId
|
||
polygon?.remove()
|
||
polygon = usePaperStore.getState().addPolygon(
|
||
renderPath,
|
||
points,
|
||
{
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
{
|
||
imageId: target.imageId,
|
||
operationId: target.operationId,
|
||
id: target.objectId,
|
||
type: "polygon",
|
||
}
|
||
)
|
||
handleCircles(polygon)
|
||
lastPoint = usePaperStore
|
||
.getState()
|
||
.group!.localToGlobal(points[points.length - 1])
|
||
lastTime = Date.now()
|
||
clearPolygonMagnet()
|
||
return true
|
||
}
|
||
|
||
const saveContinuePolygonDraft = (draftPath: paper.Path) => {
|
||
const target =
|
||
continueTargetContext || usePaperStore.getState().continuePolygonTarget
|
||
const targetTuple = continueTargetSnapshot
|
||
if (!target || !targetTuple) return false
|
||
|
||
const pathData = JSON.parse(draftPath.exportJSON({ precision: 20 }))
|
||
const nextContour = pathData?.[1]?.segments
|
||
if (!nextContour?.length) return false
|
||
|
||
const nextPoints = safeClone(targetTuple[1] || [])
|
||
const contourIndex =
|
||
continueTargetContourIndex !== null &&
|
||
continueTargetContourIndex >= 0 &&
|
||
continueTargetContourIndex < nextPoints.length
|
||
? continueTargetContourIndex
|
||
: nextPoints.length - 1
|
||
if (contourIndex < 0) return false
|
||
|
||
nextPoints[contourIndex] = nextContour
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(target.imageId, target.operationId, [
|
||
target.objectId,
|
||
nextPoints,
|
||
safeClone(targetTuple[2] || initialDetail),
|
||
safeClone(targetTuple[3] || []),
|
||
])
|
||
setContinueDraftHint(target, contourIndex, clonePaperPoints(points))
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(target.imageId, "ADD", target.objectId)
|
||
clearContinuePolygonContext(false)
|
||
forceUpdate()
|
||
return true
|
||
}
|
||
|
||
const rebuildPolygonDraft = () => {
|
||
polygon?.remove()
|
||
polygon = null
|
||
|
||
if (points.length) {
|
||
polygon = usePaperStore.getState().addPolygon(
|
||
renderPath,
|
||
points,
|
||
{
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
getDraftPolygonData()
|
||
)
|
||
} else {
|
||
draftId = null
|
||
}
|
||
|
||
handleCircles(polygon)
|
||
lastPoint = points.length
|
||
? usePaperStore
|
||
.getState()
|
||
.group!.localToGlobal(points[points.length - 1])
|
||
: null
|
||
clearPolygonMagnet()
|
||
}
|
||
|
||
const resetPolygonDraft = () => {
|
||
polygon?.remove()
|
||
polygon = null
|
||
points = []
|
||
handleCircles(null)
|
||
lastPoint = null
|
||
lastTime = 0
|
||
draftId = null
|
||
clearPolygonMagnet()
|
||
}
|
||
|
||
const undoPolygonPoint = () => {
|
||
if (!points.length) return false
|
||
points.pop()
|
||
rebuildPolygonDraft()
|
||
return true
|
||
}
|
||
|
||
const runPaperPolygonBooleanPipeline = ({
|
||
shape,
|
||
drawOption = "default",
|
||
excludedObjectId = null,
|
||
rasterClipOrder = "after",
|
||
}: {
|
||
shape: paper.Path | paper.CompoundPath | null
|
||
drawOption?: BooleanPipelineDrawOption
|
||
excludedObjectId?: number | null
|
||
rasterClipOrder?: BooleanPipelineRasterClipOrder
|
||
}) => {
|
||
polygon = executePaperPolygonBooleanPipeline({
|
||
group: usePaperStore.getState().group,
|
||
rasterPath: usePaperStore.getState().rasterPath,
|
||
shape,
|
||
drawOption,
|
||
excludedObjectId,
|
||
rasterClipOrder,
|
||
})
|
||
|
||
return polygon
|
||
}
|
||
|
||
// paper path data里可能带函数(例如事件回调), 不能直接structuredClone
|
||
const getSafePathData = (data: any) => {
|
||
const nextData: Record<string, any> = {}
|
||
Object.entries(data || {}).forEach(([key, value]) => {
|
||
if (typeof value === "function") return
|
||
nextData[key] = value
|
||
})
|
||
return nextData
|
||
}
|
||
|
||
const restoreSelectedPolygonFillColor = () => {
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
const selectedIds =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
if (!selectedIds.length) return
|
||
|
||
selectedIds.forEach((id) => {
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(id)
|
||
.forEach((item) => {
|
||
if (item.data?.type !== "polygon") return
|
||
if (item instanceof paper.Path) {
|
||
if (!item.data?.isHollowPolygon) {
|
||
item.fillColor = item.data.fillColor || item.fillColor || null
|
||
}
|
||
return
|
||
}
|
||
if (item instanceof paper.CompoundPath) {
|
||
item.fillColor = item.data.fillColor || item.fillColor || null
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
const hasShapeSegments = (
|
||
shape: paper.Path | paper.CompoundPath | null
|
||
) => {
|
||
if (!shape) return false
|
||
if (shape instanceof paper.CompoundPath) {
|
||
return (shape.children as paper.Path[]).some(
|
||
(child) => child.segments?.length
|
||
)
|
||
}
|
||
return !!shape.segments?.length
|
||
}
|
||
|
||
const createWorkingPolygonShape = (items: Array<paper.Item>) => {
|
||
const compound = items.find((item) => item instanceof paper.CompoundPath)
|
||
if (compound instanceof paper.CompoundPath) {
|
||
return compound.clone({ insert: false }) as
|
||
| paper.Path
|
||
| paper.CompoundPath
|
||
}
|
||
|
||
const paths = items.filter(
|
||
(item): item is paper.Path => item instanceof paper.Path
|
||
)
|
||
if (!paths.length) return null
|
||
if (paths.length === 1) {
|
||
return paths[0].clone({ insert: false }) as paper.Path
|
||
}
|
||
|
||
const merged = new paper.CompoundPath({ insert: false })
|
||
paths.forEach((path) => {
|
||
merged.addChild(path.clone({ insert: false }))
|
||
})
|
||
return merged as paper.CompoundPath
|
||
}
|
||
|
||
const getSelectedPolygonContext = () => {
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
const lockedEditTarget =
|
||
useKeyboardStore.getState().selectedObjectEditTarget
|
||
const selectedIds =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
const selectedId =
|
||
lockedEditTarget &&
|
||
lockedEditTarget.imageId === activeImage &&
|
||
lockedEditTarget.objectType === "polygon"
|
||
? lockedEditTarget.objectId
|
||
: selectedIds.length === 1
|
||
? selectedIds[0]
|
||
: null
|
||
if (selectedId === null) return null
|
||
|
||
const targetShapes = usePaperStore
|
||
.getState()
|
||
.getItemsById(selectedId)
|
||
.filter((item) => item.data?.type === "polygon")
|
||
if (!targetShapes.length) return null
|
||
|
||
const operationId = (
|
||
lockedEditTarget &&
|
||
lockedEditTarget.imageId === activeImage &&
|
||
lockedEditTarget.objectId === selectedId
|
||
? lockedEditTarget.operationId
|
||
: targetShapes.find((item) => !!item.data)?.data?.operationId ||
|
||
useTopToolsStore.getState().activeOperation ||
|
||
""
|
||
).toString()
|
||
if (!operationId) return null
|
||
|
||
const targetData = getSafePathData(
|
||
targetShapes.find((item) => !!item.data)?.data || {}
|
||
)
|
||
const prevDetail = safeClone(
|
||
useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(activeImage, operationId, selectedId) ||
|
||
initialDetail
|
||
)
|
||
|
||
return {
|
||
activeImage,
|
||
selectedId,
|
||
targetData,
|
||
targetShapes,
|
||
operationId,
|
||
prevDetail,
|
||
}
|
||
}
|
||
|
||
const rebuildPersistedSelectedPolygon = (
|
||
context: NonNullable<ReturnType<typeof getSelectedPolygonContext>>
|
||
) => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return null
|
||
|
||
const persistedTuple =
|
||
useLabelStore
|
||
.getState()
|
||
.label.get(context.activeImage)
|
||
?.get(context.operationId)
|
||
?.find((item) => item[0] === context.selectedId) || null
|
||
if (!persistedTuple) return null
|
||
|
||
const visible =
|
||
!useObjectStore.getState().pathStatus[
|
||
context.activeImage + context.selectedId
|
||
]?.HIDE
|
||
const selected = (
|
||
useObjectStore.getState().selectedPath[context.activeImage] || []
|
||
).includes(context.selectedId)
|
||
const fillColor =
|
||
context.targetData.fillColor ||
|
||
usePaperStore.getState().toolOption.fillColor
|
||
const blankColor =
|
||
context.targetData.blankColor ||
|
||
fillColor ||
|
||
usePaperStore.getState().toolOption.blankColor
|
||
const strokeColor =
|
||
context.targetData.strokeColor ||
|
||
usePaperStore.getState().toolOption.strokeColor
|
||
const strokeWidth =
|
||
context.targetData.strokeWidth ||
|
||
usePaperStore.getState().toolOption.strokeWidth
|
||
const baseData = Object.assign({}, context.targetData, {
|
||
type: "polygon",
|
||
id: context.selectedId,
|
||
imageId: context.activeImage,
|
||
operationId: context.operationId,
|
||
fillColor,
|
||
blankColor,
|
||
strokeColor,
|
||
strokeWidth,
|
||
selected,
|
||
})
|
||
|
||
group
|
||
.getItems({ data: { id: context.selectedId } })
|
||
.filter(
|
||
(item) => item.data?.type === "polygon" && item.parent === group
|
||
)
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
|
||
const buildPolygonPath = (segments: any[], isHollowPolygon: boolean) => {
|
||
const contourPoints = (segments || [])
|
||
.map((segment) => getPointFromSegment(segment))
|
||
.filter((point): point is paper.Point => point !== null)
|
||
if (!contourPoints.length) return null
|
||
|
||
return usePaperStore.getState().addPolygon(
|
||
renderPath,
|
||
contourPoints,
|
||
{
|
||
strokeColor,
|
||
strokeWidth,
|
||
fillColor: isHollowPolygon ? fillColor : blankColor,
|
||
visible,
|
||
},
|
||
Object.assign({}, baseData, {
|
||
isHollowPolygon,
|
||
})
|
||
)
|
||
}
|
||
|
||
const outerPaths =
|
||
persistedTuple[1]
|
||
?.map((segments) => buildPolygonPath(segments, false))
|
||
.filter((item): item is paper.Path => !!item) || []
|
||
const innerPaths =
|
||
persistedTuple[3]
|
||
?.map((segments) => buildPolygonPath(segments, true))
|
||
.filter((item): item is paper.Path => !!item) || []
|
||
const children = [...outerPaths, ...innerPaths]
|
||
if (!children.length) return null
|
||
if (outerPaths.length === 1 && !innerPaths.length) {
|
||
return outerPaths[0]
|
||
}
|
||
|
||
return usePaperStore.getState().addCompoundPath(
|
||
renderCompoundPath,
|
||
children,
|
||
{
|
||
strokeColor,
|
||
strokeWidth,
|
||
fillColor: blankColor,
|
||
visible,
|
||
},
|
||
baseData
|
||
)
|
||
}
|
||
|
||
const clearPolygonBooleanHelperItems = () => {
|
||
if (selectedCircles.length) {
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
}
|
||
clearPolygonMagnet()
|
||
}
|
||
|
||
const getEditedPolygonDetail = (
|
||
context: NonNullable<ReturnType<typeof getSelectedPolygonContext>>
|
||
) => {
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(
|
||
context.activeImage,
|
||
context.operationId,
|
||
context.selectedId
|
||
)
|
||
return currentDetail
|
||
? {
|
||
...currentDetail,
|
||
first_modified_timestamp:
|
||
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid: currentDetail.first_modified_uid || user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid: user_id,
|
||
}
|
||
: {
|
||
...initialDetail,
|
||
create_timestamp: dayjs().unix(),
|
||
category_id: Number(context.operationId),
|
||
}
|
||
}
|
||
|
||
const resolvePreferredContinueOuterContourIndex = (
|
||
context: NonNullable<ReturnType<typeof getSelectedPolygonContext>>,
|
||
contours: any[]
|
||
) => {
|
||
if (!contours.length) return -1
|
||
|
||
const continueTarget = {
|
||
imageId: context.activeImage,
|
||
operationId: context.operationId,
|
||
objectId: context.selectedId,
|
||
}
|
||
const continueHint = continueDraftHints.get(
|
||
getContinueDraftHintKey(continueTarget)
|
||
)
|
||
|
||
if (
|
||
continueHint &&
|
||
continueHint.contourIndex >= 0 &&
|
||
continueHint.contourIndex < contours.length &&
|
||
contours[continueHint.contourIndex]?.length > 2
|
||
) {
|
||
return continueHint.contourIndex
|
||
}
|
||
|
||
const continuePaths = getContinueCandidatePaths(context.selectedId)
|
||
const continuePath = continuePaths[continuePaths.length - 1]
|
||
const continuePathPoints = continuePath?.segments?.length
|
||
? removeDuplicatedClosingPoint(
|
||
continuePath.segments.map(
|
||
(segment) => new paper.Point(segment.point)
|
||
)
|
||
)
|
||
: []
|
||
|
||
if (continuePathPoints.length) {
|
||
const matchedIndex = findMatchingContinueContourIndex(
|
||
continuePathPoints,
|
||
contours
|
||
)
|
||
if (matchedIndex >= 0) {
|
||
return matchedIndex
|
||
}
|
||
}
|
||
|
||
for (let index = contours.length - 1; index >= 0; index -= 1) {
|
||
if (contours[index]?.length > 2) {
|
||
return index
|
||
}
|
||
}
|
||
|
||
return contours.length ? contours.length - 1 : -1
|
||
}
|
||
|
||
const persistSelectedPolygonGeometryResult = (
|
||
context: NonNullable<ReturnType<typeof getSelectedPolygonContext>>,
|
||
geometry: ReturnType<typeof paperShapeToMultiPolygon>
|
||
) => {
|
||
const previousTuple =
|
||
useLabelStore
|
||
.getState()
|
||
.label.get(context.activeImage)
|
||
?.get(context.operationId)
|
||
?.find((item) => item[0] === context.selectedId) || null
|
||
const previousPoints = safeClone(previousTuple?.[1] || [])
|
||
const preferredContourIndex = resolvePreferredContinueOuterContourIndex(
|
||
context,
|
||
previousPoints
|
||
)
|
||
const newDetail = getEditedPolygonDetail(context)
|
||
const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
|
||
const nextContinueContourIndex = findBestMatchingOuterContourIndex({
|
||
previousContours: previousPoints,
|
||
nextContours: points,
|
||
preferredIndex: preferredContourIndex,
|
||
})
|
||
const nextContinueContourSegments =
|
||
points[
|
||
nextContinueContourIndex >= 0
|
||
? nextContinueContourIndex
|
||
: points.length - 1
|
||
] || []
|
||
|
||
if (points.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(context.activeImage, context.operationId, [
|
||
context.selectedId,
|
||
points,
|
||
newDetail,
|
||
hollowPoints,
|
||
])
|
||
setContinueDraftHint(
|
||
{
|
||
imageId: context.activeImage,
|
||
operationId: context.operationId,
|
||
objectId: context.selectedId,
|
||
},
|
||
nextContinueContourIndex >= 0
|
||
? nextContinueContourIndex
|
||
: points.length - 1,
|
||
getContinueContourPoints(nextContinueContourSegments)
|
||
)
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(context.activeImage, "ADD", context.selectedId)
|
||
} else {
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(
|
||
context.activeImage,
|
||
context.operationId,
|
||
context.selectedId
|
||
)
|
||
cleanupDeletedObjectItems(
|
||
context.activeImage,
|
||
context.selectedId,
|
||
context.targetData.parentGroupId
|
||
)
|
||
}
|
||
|
||
rebuildPersistedSelectedPolygon(context)
|
||
syncSelectedObjectsVisualState(context.activeImage)
|
||
forceUpdate()
|
||
return true
|
||
}
|
||
|
||
const applySelectedPolygonBooleanOperation = (
|
||
mode: "subtract" | "unite",
|
||
options: {
|
||
draftPath?: paper.Path | null
|
||
} = {}
|
||
) => {
|
||
const context = getSelectedPolygonContext()
|
||
if (!context) {
|
||
options.draftPath?.remove()
|
||
return false
|
||
}
|
||
const prevDrawOption = useTopToolsStore.getState().drawOption
|
||
const nextDrawOption = mode === "subtract" ? "intersect" : "unite"
|
||
const baseShape = createWorkingPolygonShape(context.targetShapes)
|
||
const rasterGeometry = paperShapeToMultiPolygon(
|
||
usePaperStore.getState().rasterPath
|
||
)
|
||
const overlapMode = nextDrawOption === "intersect" ? "subtract" : "unite"
|
||
|
||
if (!baseShape || !hasShapeSegments(baseShape)) {
|
||
options.draftPath?.remove()
|
||
baseShape?.remove()
|
||
return false
|
||
}
|
||
|
||
let workingGeometry = paperShapeToMultiPolygon(baseShape)
|
||
baseShape.remove()
|
||
if (!workingGeometry.length) {
|
||
options.draftPath?.remove()
|
||
return false
|
||
}
|
||
|
||
useTopToolsStore.getState().setDrawOption(nextDrawOption)
|
||
try {
|
||
if (options.draftPath) {
|
||
const clippedDraftGeometry = applyBooleanAdapter({
|
||
subject: paperShapeToMultiPolygon(options.draftPath),
|
||
mode: "unite",
|
||
rasterClip: rasterGeometry.length ? rasterGeometry : null,
|
||
}).geometry
|
||
options.draftPath.remove()
|
||
|
||
if (!clippedDraftGeometry.length) {
|
||
clearPolygonBooleanHelperItems()
|
||
polygon = null
|
||
return false
|
||
}
|
||
|
||
workingGeometry = applyBooleanAdapter({
|
||
subject: workingGeometry,
|
||
operands: [clippedDraftGeometry],
|
||
mode,
|
||
}).geometry
|
||
}
|
||
|
||
const overlapOperands = collectVisiblePaperPolygonOperandGeometries({
|
||
group: usePaperStore.getState().group,
|
||
excludedObjectId: context.selectedId,
|
||
subjectGeometry: workingGeometry,
|
||
})
|
||
|
||
workingGeometry = applyBooleanAdapter({
|
||
subject: workingGeometry,
|
||
operands: overlapOperands,
|
||
mode: overlapMode,
|
||
rasterClip: rasterGeometry.length ? rasterGeometry : null,
|
||
}).geometry
|
||
|
||
const persisted = persistSelectedPolygonGeometryResult(
|
||
context,
|
||
workingGeometry
|
||
)
|
||
clearPolygonBooleanHelperItems()
|
||
polygon = null
|
||
return persisted
|
||
} finally {
|
||
useTopToolsStore.getState().setDrawOption(prevDrawOption)
|
||
}
|
||
}
|
||
|
||
polygonTool.onMouseDown = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
}) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
if (e.event.button === 2) {
|
||
e.event.preventDefault()
|
||
if (polygon || points.length) {
|
||
hidePaperContextMenu()
|
||
undoPolygonPoint()
|
||
if (!points.length && hasContinuePolygonDraft()) {
|
||
clearContinuePolygonContext(true)
|
||
}
|
||
return
|
||
}
|
||
if (!trySelectPaperObjectByPoint(e.point)) {
|
||
hidePaperContextMenu()
|
||
}
|
||
return
|
||
}
|
||
if (e.event.button !== 0) return
|
||
isMouseDown = true
|
||
|
||
// 绘制时去除选中辅助
|
||
if (usePaperSupportStore.getState().selectedCircles.length) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
|
||
if (!polygon && !points.length && hasContinuePolygonDraft()) {
|
||
if (!startContinuePolygonDraft()) return
|
||
}
|
||
|
||
// 先判断是否存在吸附点
|
||
let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point
|
||
const isDoubleClick = isPaperDoubleClick(e.event, lastPoint, checkPoint)
|
||
// doubleclick
|
||
if (isDoubleClick) {
|
||
if (polygon) {
|
||
if ((polygon as paper.Path).segments.length < 3) {
|
||
polygon.remove()
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
}
|
||
|
||
// 调整polygon点位初始顺序
|
||
if (!polygon.clockwise) polygon.reverse()
|
||
|
||
// 初始化 compoundPolygon
|
||
compoundPolygon = new paper.Path()
|
||
|
||
if (hasContinuePolygonDraft()) {
|
||
;(polygon as paper.Path).closePath()
|
||
const continueSaved = saveContinuePolygonDraft(
|
||
polygon as paper.Path
|
||
)
|
||
if (!continueSaved) {
|
||
polygon.remove()
|
||
clearContinuePolygonContext(true)
|
||
}
|
||
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
clearPolygonMagnet()
|
||
useTopToolsStore.getState().setPressA(false)
|
||
draftId = null
|
||
lastPoint = null
|
||
lastTime = 0
|
||
points = []
|
||
polygon = null
|
||
return
|
||
}
|
||
|
||
if (useKeyboardStore.getState().drawAction === "subtract") {
|
||
;(polygon as paper.Path).closePath()
|
||
applySelectedPolygonBooleanOperation("subtract", {
|
||
draftPath: polygon as paper.Path,
|
||
})
|
||
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
clearPolygonMagnet()
|
||
useTopToolsStore.getState().setPressA(false)
|
||
draftId = null
|
||
lastPoint = null
|
||
lastTime = 0
|
||
points = []
|
||
polygon = null
|
||
return
|
||
}
|
||
|
||
if (
|
||
useKeyboardStore.getState().drawAction === "add" &&
|
||
useTopToolsStore.getState().pressA
|
||
) {
|
||
const selectedId =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
]?.[0]
|
||
const selectedItem = selectedId
|
||
? usePaperStore.getState().getItemById(selectedId)
|
||
: null
|
||
if (selectedItem?.data?.type === "polygon") {
|
||
;(polygon as paper.Path).closePath()
|
||
applySelectedPolygonBooleanOperation("unite", {
|
||
draftPath: polygon as paper.Path,
|
||
})
|
||
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
clearPolygonMagnet()
|
||
useTopToolsStore.getState().setPressA(false)
|
||
draftId = null
|
||
lastPoint = null
|
||
lastTime = 0
|
||
points = []
|
||
polygon = null
|
||
return
|
||
}
|
||
}
|
||
|
||
// pressA 模式下添加多边形
|
||
if (useTopToolsStore.getState().pressA) {
|
||
let ids =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
]
|
||
const selectedId = ids?.[0]
|
||
if (!selectedId) {
|
||
resetPolygonDraft()
|
||
return
|
||
}
|
||
let paths = usePaperStore.getState().getItemsById(ids[0])
|
||
let oldPolygon = polygon
|
||
// 闭合绘制路径
|
||
oldPolygon.closePath()
|
||
const polygonPaths = paths.filter(
|
||
(path) => path.data?.type === "polygon"
|
||
)
|
||
const prevCompoundPath =
|
||
(polygonPaths.find(
|
||
(path) => path instanceof paper.CompoundPath
|
||
) as paper.CompoundPath | undefined) ||
|
||
((polygonPaths.find(
|
||
(path) => path.parent instanceof paper.CompoundPath
|
||
)?.parent as paper.CompoundPath | undefined) ??
|
||
undefined)
|
||
|
||
const basePathData = getSafePathData(
|
||
polygonPaths.find((path) => !!path.data)?.data || {}
|
||
)
|
||
oldPolygon.data = Object.assign({}, basePathData, oldPolygon.data, {
|
||
id: selectedId,
|
||
type: "polygon",
|
||
isHollowPolygon: !oldPolygon.clockwise,
|
||
})
|
||
|
||
if (prevCompoundPath) {
|
||
// 已是多区域对象, 直接追加 child
|
||
prevCompoundPath.addChild(oldPolygon)
|
||
prevCompoundPath.data = Object.assign(
|
||
{},
|
||
getSafePathData(prevCompoundPath.data || {}),
|
||
basePathData,
|
||
{ id: selectedId, type: "polygon" }
|
||
)
|
||
polygon = prevCompoundPath
|
||
} else {
|
||
const prevPaths = polygonPaths.filter(
|
||
(path): path is paper.Path => path instanceof paper.Path
|
||
)
|
||
polygon = usePaperStore.getState().addCompoundPath(
|
||
renderCompoundPath,
|
||
[...prevPaths, oldPolygon] as paper.Path[],
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
{ ...basePathData, ...oldPolygon.data, id: selectedId }
|
||
)
|
||
}
|
||
}
|
||
|
||
polygon = runPaperPolygonBooleanPipeline({
|
||
shape: polygon,
|
||
drawOption: useTopToolsStore.getState()
|
||
.drawOption as BooleanPipelineDrawOption,
|
||
rasterClipOrder: "after",
|
||
})
|
||
handleCircles(polygon)
|
||
console.log("裁剪之后", polygon)
|
||
console.log("边界裁剪之后", polygon)
|
||
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = safeClone(
|
||
useTopToolsStore.getState().subAttributes[label_class] || {}
|
||
)
|
||
|
||
// 镂空或分割的多边形存值
|
||
if (polygon instanceof paper.CompoundPath) {
|
||
if (polygon.children && polygon.children.length) {
|
||
let points: any[] = []
|
||
let hollowPoints: any[] = []
|
||
let lastOuterContourPoints: paper.Point[] = []
|
||
;(polygon.children as paper.Path[]).forEach((path) => {
|
||
let pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||
// 在镂空或分割的区域中 路径方向是false
|
||
if (path.clockwise) {
|
||
points.push(pathData[1]?.segments)
|
||
lastOuterContourPoints = getContinueContourPoints(
|
||
pathData[1]?.segments
|
||
)
|
||
} else {
|
||
hollowPoints.push(pathData[1]?.segments)
|
||
}
|
||
})
|
||
console.log(
|
||
"处理完成的点位数据",
|
||
points,
|
||
hollowPoints,
|
||
polygon.data
|
||
)
|
||
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
polygon.data.id,
|
||
points,
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [
|
||
...useLabelStore.getState().labelDefaultComments,
|
||
],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
},
|
||
hollowPoints,
|
||
]
|
||
)
|
||
if (points.length) {
|
||
setContinueDraftHint(
|
||
{
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
objectId: polygon.data.id,
|
||
},
|
||
points.length - 1,
|
||
lastOuterContourPoints
|
||
)
|
||
}
|
||
if (!useTopToolsStore.getState().pressA)
|
||
useRightToolsStore.getState().pushPathId(polygon.data.id)
|
||
}
|
||
} else if (
|
||
// 未镂空或分割 存值
|
||
polygon instanceof paper.Path &&
|
||
polygon.data.id &&
|
||
polygon.segments?.length
|
||
) {
|
||
let pathData = JSON.parse(polygon.exportJSON({ precision: 20 }))
|
||
console.log(pathData[1]?.segments)
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
polygon.data.id,
|
||
// todo intersect+a | +a会变成CompoundPath 理论上已解决这个todo
|
||
[pathData[1]?.segments, ...[]],
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
},
|
||
[],
|
||
]
|
||
)
|
||
setContinueDraftHint(
|
||
{
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
objectId: polygon.data.id,
|
||
},
|
||
0,
|
||
clonePaperPoints(points)
|
||
)
|
||
useRightToolsStore.getState().pushPathId(polygon.data.id)
|
||
}
|
||
|
||
const polygonData = polygon?.data
|
||
|
||
// 如果该类别存在子属性,则弹出子属性对话框
|
||
if (polygonData?.id && category && category.sub_attributes.length) {
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
-30,
|
||
-30
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: polygonData.imageId,
|
||
operationId: polygonData.operationId,
|
||
id: polygonData.id,
|
||
})
|
||
}
|
||
|
||
// 设置其为selectedPath
|
||
if (polygonData?.id) {
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
polygonData.id
|
||
)
|
||
}
|
||
|
||
forceUpdate()
|
||
|
||
if (compoundPolygon instanceof paper.CompoundPath) {
|
||
polygon?.remove()
|
||
polygon = null
|
||
points = []
|
||
} else {
|
||
compoundPolygon?.remove()
|
||
compoundPolygon = null
|
||
polygon?.closePath()
|
||
polygon = null
|
||
points = []
|
||
}
|
||
selectedCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
selectedCircles = []
|
||
clearPolygonMagnet()
|
||
useTopToolsStore.getState().setPressA(false)
|
||
draftId = null
|
||
lastPoint = null
|
||
lastTime = 0
|
||
return
|
||
}
|
||
} else {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
// 清空多边形 fillColor
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.forEach((item) => {
|
||
resetPaperShapeFillColor(item)
|
||
})
|
||
if (
|
||
useTopToolsStore.getState().pressA &&
|
||
useKeyboardStore.getState().drawAction !== "none"
|
||
) {
|
||
restoreSelectedPolygonFillColor()
|
||
}
|
||
}
|
||
|
||
let temp = renderPath(usePaperStore.getState().group!, {})
|
||
let point = currentMagnetPoint
|
||
? currentMagnetPoint
|
||
: temp.globalToLocal(e.point)
|
||
const draftData = getDraftPolygonData()
|
||
|
||
let circle = usePaperStore
|
||
.getState()
|
||
.getClickedCircle(
|
||
point,
|
||
selectedCircles.length,
|
||
draftData.id,
|
||
usePaperStore.getState().toolOption.blankColor
|
||
)
|
||
// add circle
|
||
selectedCircles.push(circle)
|
||
|
||
// add point
|
||
points.push(point)
|
||
if (polygon) {
|
||
polygon.remove()
|
||
}
|
||
polygon = usePaperStore.getState().addPolygon(
|
||
renderPath,
|
||
points,
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
draftData
|
||
)
|
||
|
||
lastPoint = checkPoint
|
||
lastTime = Date.now()
|
||
}
|
||
polygonTool.onMouseDrag = (e: {
|
||
event: MouseEvent
|
||
delta: paper.Point
|
||
}) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
}
|
||
polygonTool.onMouseUp = (_e: paper.ToolEvent) => {
|
||
if (stopMiddleMousePan()) return
|
||
isMouseDown = false
|
||
}
|
||
polygonTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
|
||
if (polygon) {
|
||
let temp = renderPath(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
// let point = e.point;
|
||
// when mousedown then drag draw
|
||
const paperGroup = usePaperStore.getState().group!
|
||
const currentPloygons = paperGroup.getItems({
|
||
data: { type: "polygon" },
|
||
})
|
||
// clear prev magnet
|
||
const prevHitResult = paperGroup.getItems({
|
||
data: { type: "magnet" },
|
||
})
|
||
prevHitResult.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
currentMagnetPoint = null
|
||
if (useTopToolsStore.getState().magnetFlag)
|
||
currentPloygons.forEach((imgPolygon) => {
|
||
if (
|
||
imgPolygon instanceof paper.Path &&
|
||
imgPolygon.data.id !== polygon!.data.id
|
||
) {
|
||
const hitResult = imgPolygon.hitTest(point, {
|
||
stroke: true,
|
||
tolerance: usePaperStore.getState().paperScope
|
||
? 8 / usePaperStore.getState().paperScope!.view.zoom
|
||
: 0,
|
||
})
|
||
if (hitResult) {
|
||
// 绘制高亮边及其端点
|
||
const { point, location } = hitResult
|
||
const magnetPoint = new paper.Path.Circle(point, 4).set({
|
||
fillColor: "white",
|
||
strokeScaling: false,
|
||
data: { type: "magnet" },
|
||
})
|
||
magnetPoint.scale(1 / usePaperStore.getState().group!.scaling.x)
|
||
const magnetLine = new paper.Path.Line(
|
||
location.curve.segment1.point,
|
||
location.curve.segment2.point
|
||
).set({
|
||
strokeColor: "white",
|
||
strokeWidth: 2,
|
||
strokeScaling: false,
|
||
data: { type: "magnet" },
|
||
})
|
||
paperGroup.addChild(magnetPoint)
|
||
paperGroup.addChild(magnetLine)
|
||
currentMagnetPoint = point
|
||
}
|
||
}
|
||
})
|
||
if (isMouseDown && lastPoint) {
|
||
const delta = currentMagnetPoint
|
||
? (currentMagnetPoint as paper.Point).subtract(lastPoint)
|
||
: e.point.subtract(lastPoint)
|
||
const currentTime = Date.now()
|
||
const timeDelta = currentTime - lastTime
|
||
if (delta.length > 10 && timeDelta > 50) {
|
||
// Add a point to the current path if the distance exceeds the threshold
|
||
if (!currentMagnetPoint) {
|
||
points.push(point)
|
||
let circle = usePaperStore
|
||
.getState()
|
||
.getClickedCircle(
|
||
point,
|
||
selectedCircles.length,
|
||
polygon.data.id,
|
||
polygon.data.blankColor
|
||
)
|
||
// add circle
|
||
selectedCircles.push(circle)
|
||
;(polygon as paper.Path).add(point)
|
||
lastPoint = e.point
|
||
} else {
|
||
points.push(currentMagnetPoint)
|
||
let circle = usePaperStore
|
||
.getState()
|
||
.getClickedCircle(
|
||
currentMagnetPoint,
|
||
selectedCircles.length,
|
||
polygon.data.id,
|
||
polygon.data.blankColor
|
||
)
|
||
// add circle
|
||
selectedCircles.push(circle)
|
||
;(polygon as paper.Path).add(currentMagnetPoint)
|
||
lastPoint = currentMagnetPoint
|
||
}
|
||
lastTime = currentTime
|
||
}
|
||
} else {
|
||
polygon.remove()
|
||
polygon = usePaperStore.getState().addPolygon(
|
||
renderPath,
|
||
points.concat(point),
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
getDraftPolygonData()
|
||
)
|
||
}
|
||
}
|
||
}
|
||
polygonTool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "escape" || e.key === "alt") {
|
||
if (polygon || points.length || hasContinuePolygonDraft()) {
|
||
resetPolygonDraft()
|
||
clearContinuePolygonContext(true)
|
||
usePaperSupportStore.getState().handleBufferPaths(null)
|
||
}
|
||
} else if (e.key === "delete") {
|
||
handleDelete()
|
||
forceUpdate()
|
||
} else if (e.key === "shift") {
|
||
handleShift(true)
|
||
}
|
||
}
|
||
polygonTool.onKeyUp = (e: paper.KeyEvent) => {
|
||
if (e.key === "shift") {
|
||
handleShift(false)
|
||
}
|
||
}
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
polygonTool: polygonTool,
|
||
beginContinuePolygonDraft: () => startContinuePolygonDraft(),
|
||
applySelectedPolygonBooleanOperation: (mode, options) =>
|
||
applySelectedPolygonBooleanOperation(mode, options),
|
||
}))
|
||
},
|
||
addRectangle: (
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let rect = renderRectangle(
|
||
usePaperStore.getState().group!,
|
||
Object.assign(option, { strokeScaling: false })
|
||
)
|
||
rect.data = Object.assign({ type: "rectangle", ...option }, data)
|
||
insertItemBelowTags(rect)
|
||
rect.onMouseEnter = (_e: paper.MouseEvent) => {
|
||
// if (usePaperStore.getState().mode === "pan") {
|
||
// e.target.strokeWidth += 2;
|
||
// if (usePaperStore.getState().el)
|
||
// usePaperStore.getState().el!.style.cursor = "move";
|
||
// }
|
||
}
|
||
rect.onMouseLeave = (_e: paper.MouseEvent) => {
|
||
// if (usePaperStore.getState().mode === "pan") {
|
||
// // render 结束没有触发 onMouseEnter
|
||
// if (e.target.strokeWidth - 2 > 0) e.target.strokeWidth -= 2;
|
||
// if (usePaperStore.getState().el)
|
||
// usePaperStore.getState().el!.style.cursor = "default";
|
||
// }
|
||
}
|
||
rect.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
if (rect.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === rect.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: rect.data.imageId,
|
||
operationId: rect.data.operationId,
|
||
id: rect.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return rect
|
||
},
|
||
initRectangleTool: (
|
||
paperRectangleTool: paper.Tool,
|
||
renderRectangle: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.Path.Rectangle,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => {
|
||
let rect: paper.Path.Rectangle | null
|
||
let rectangleTool = paperRectangleTool
|
||
|
||
rectangleTool.onMouseDown = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
}) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
if (e.event.button === 2) {
|
||
e.event.preventDefault()
|
||
if (!trySelectPaperObjectByPoint(e.point)) {
|
||
hidePaperContextMenu()
|
||
}
|
||
return
|
||
}
|
||
if (e.event && e.event.button !== 2)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
|
||
// 绘制时去除选中辅助
|
||
if (usePaperSupportStore.getState().selectedCircles.length) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
}
|
||
|
||
rectangleTool.onMouseDrag = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
downPoint: paper.Point
|
||
delta: paper.Point
|
||
}) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
const delta = e.point.subtract(e.downPoint)
|
||
// 阻止右键移动矩形(e.event.button === 0)
|
||
if (delta.length > 10 && e.event.button === 0) {
|
||
let temp = renderRectangle(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
let downPoint = temp.globalToLocal(e.downPoint)
|
||
|
||
temp.remove()
|
||
rect = usePaperStore.getState().addRectangle(
|
||
renderRectangle,
|
||
{
|
||
from: downPoint,
|
||
to: point,
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
{
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
id:
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
|
||
}
|
||
)
|
||
rect.removeOnDrag()
|
||
|
||
// !!! 矩形不进行裁剪
|
||
// intersect drawOption 舍弃交集
|
||
// 舍弃交集
|
||
}
|
||
}
|
||
rectangleTool.onMouseUp = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
}) => {
|
||
if (stopMiddleMousePan()) return
|
||
// add segment judgement
|
||
if (rect && rect.segments?.length && rect?.data?.id) {
|
||
// 去除超出图片边界的部分
|
||
handleRectangleIntersectRaster(rect)
|
||
// 去除后无点位 则不作后续新增处理
|
||
if (!rect.segments.length) return
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = safeClone(
|
||
useTopToolsStore.getState().subAttributes[label_class] || {}
|
||
)
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
rect.data.id,
|
||
[JSON.parse(rect.exportJSON({ precision: 20 }))[1]?.segments],
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
},
|
||
[],
|
||
]
|
||
)
|
||
useRightToolsStore.getState().pushPathId(rect.data.id)
|
||
|
||
// 设置其为selectedPath
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
rect.data?.id
|
||
)
|
||
|
||
forceUpdate()
|
||
// 如果该类别存在子属性,则弹出子属性对话框
|
||
if (category && category.sub_attributes.length) {
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category,
|
||
-30,
|
||
-30
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: rect.data.imageId,
|
||
operationId: rect.data.operationId,
|
||
id: rect.data.id,
|
||
})
|
||
}
|
||
}
|
||
rect = null
|
||
}
|
||
rectangleTool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "escape" || e.key === "alt") {
|
||
if (rect) {
|
||
rect.remove()
|
||
rect = null
|
||
}
|
||
} else if (e.key === "delete") {
|
||
handleDelete()
|
||
forceUpdate()
|
||
} else if (e.key === "shift") {
|
||
handleShift(true)
|
||
}
|
||
}
|
||
rectangleTool.onKeyUp = (e: paper.KeyEvent) => {
|
||
if (e.key === "shift") {
|
||
handleShift(false)
|
||
}
|
||
}
|
||
rectangleTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
}
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
rectangleTool: rectangleTool,
|
||
}))
|
||
},
|
||
addBrush: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let brush = renderPath(
|
||
usePaperStore.getState().group!,
|
||
Object.assign(
|
||
{ ...option, fillColor: undefined, closed: false },
|
||
{ segments: points, strokeScaling: false }
|
||
)
|
||
)
|
||
brush.data = Object.assign({ type: "brush", ...option }, data)
|
||
insertItemBelowTags(brush)
|
||
// points?.forEach((point) => {
|
||
// brush.add(point);
|
||
// });
|
||
brush.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
// console.log(e.event.clientX, e.event.clientY);
|
||
if (brush.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === brush.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: brush.data.imageId,
|
||
operationId: brush.data.operationId,
|
||
id: brush.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return brush
|
||
},
|
||
initBrushTool: (
|
||
paperBrushTool: paper.Tool,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
renderCompoundPath: (
|
||
paperGroup: paper.Group,
|
||
option: any
|
||
) => paper.CompoundPath,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => {
|
||
let brushCircles: paper.Path.Circle[] = []
|
||
let myPath: paper.Path | null
|
||
let compoundPolygon: paper.Path | paper.CompoundPath | null
|
||
let lastPoint: paper.Point | null = null
|
||
let points: paper.Point[] = []
|
||
|
||
let brushTool = paperBrushTool
|
||
let draftId: number | null = null
|
||
|
||
const getDraftBrushData = () => ({
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
id:
|
||
draftId ??
|
||
(draftId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1),
|
||
})
|
||
|
||
const rebuildBrushDraft = () => {
|
||
myPath?.remove()
|
||
myPath = null
|
||
|
||
if (points.length) {
|
||
myPath = usePaperStore.getState().addBrush(
|
||
renderPath,
|
||
points,
|
||
{
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
getDraftBrushData()
|
||
)
|
||
} else {
|
||
draftId = null
|
||
}
|
||
|
||
lastPoint = points.length
|
||
? usePaperStore
|
||
.getState()
|
||
.group!.localToGlobal(points[points.length - 1])
|
||
: null
|
||
}
|
||
|
||
const resetBrushDraft = () => {
|
||
brushCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
brushCircles = []
|
||
myPath?.remove()
|
||
myPath = null
|
||
points = []
|
||
lastPoint = null
|
||
draftId = null
|
||
}
|
||
|
||
const undoBrushPoint = () => {
|
||
if (!points.length) return false
|
||
points.pop()
|
||
brushCircles.pop()?.remove()
|
||
rebuildBrushDraft()
|
||
return true
|
||
}
|
||
|
||
const handleBrushIntersectRaster = (savePath: paper.Path) => {
|
||
let intersections = savePath.getIntersections(
|
||
usePaperStore.getState().rasterPath!
|
||
)
|
||
let validIntersections = intersections.filter((intersection) => {
|
||
// 检查交点是否在两个路径的可见部分
|
||
return (
|
||
savePath.contains(intersection.point) &&
|
||
usePaperStore.getState().rasterPath!.contains(intersection.point)
|
||
)
|
||
})
|
||
|
||
// 没有 validIntersections 不进行操作
|
||
if (validIntersections.length) {
|
||
// intersectionPath as paper.CompoundPath | paper.Path | null
|
||
let intersectionPath: any = savePath.intersect(
|
||
usePaperStore.getState().rasterPath!,
|
||
{
|
||
insert: false,
|
||
trace: false,
|
||
}
|
||
)
|
||
|
||
// 检测交点并处理
|
||
if (intersectionPath instanceof paper.CompoundPath) {
|
||
let new_segments: any[] = []
|
||
;(intersectionPath.children as paper.Path[]).forEach((child) => {
|
||
if (child.segments && child.segments.length) {
|
||
new_segments.push(...child.segments)
|
||
}
|
||
})
|
||
savePath.segments = new_segments
|
||
} else if (
|
||
intersectionPath.segments &&
|
||
intersectionPath.segments.length
|
||
) {
|
||
// intersectionPath是CompoundPath
|
||
savePath.segments = intersectionPath.segments
|
||
} else {
|
||
}
|
||
intersectionPath.remove()
|
||
intersectionPath = null
|
||
|
||
let oldCompoundPolygon = compoundPolygon
|
||
compoundPolygon = oldCompoundPolygon?.intersect(
|
||
usePaperStore.getState().rasterPath!
|
||
) as paper.Path
|
||
compoundPolygon.closed = false
|
||
oldCompoundPolygon?.remove()
|
||
} else {
|
||
let intersectionPath: paper.Path | null = savePath.intersect(
|
||
usePaperStore.getState().rasterPath!,
|
||
{
|
||
insert: false,
|
||
trace: false,
|
||
}
|
||
) as paper.Path
|
||
savePath.segments = intersectionPath.segments
|
||
intersectionPath.remove()
|
||
intersectionPath = null
|
||
}
|
||
}
|
||
|
||
brushTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
if (e.event.button === 2) {
|
||
e.event.preventDefault()
|
||
if (myPath || points.length) {
|
||
hidePaperContextMenu()
|
||
undoBrushPoint()
|
||
return
|
||
}
|
||
if (!trySelectPaperObjectByPoint(e.point)) {
|
||
hidePaperContextMenu()
|
||
}
|
||
return
|
||
}
|
||
if (e.event.button !== 0) return
|
||
|
||
// 绘制时去除选中辅助
|
||
if (usePaperSupportStore.getState().selectedCircles.length) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
|
||
// doubleclick
|
||
if (isPaperDoubleClick(e.event, lastPoint, e.point)) {
|
||
if (myPath) {
|
||
// 初始化 compoundPolygon
|
||
compoundPolygon = new paper.Path()
|
||
if (useTopToolsStore.getState().pressA)
|
||
compoundPolygon = myPath.clone()
|
||
|
||
// 去除超出图片边界的部分
|
||
handleBrushIntersectRaster(myPath)
|
||
// 去除超出图片边界的部分
|
||
|
||
// pressA 模式下添加线段
|
||
if (useTopToolsStore.getState().pressA) {
|
||
let ids =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
]
|
||
let path = usePaperStore.getState().getItemById(ids[0])
|
||
let paths =
|
||
usePaperStore
|
||
.getState()
|
||
.getItemsById(ids[0])
|
||
.filter((item) => item instanceof paper.Path && item.area) || []
|
||
|
||
// compoundPolygon === Path
|
||
let oldCompoundPolygon: any = compoundPolygon
|
||
let children: paper.Path[] = []
|
||
if (oldCompoundPolygon instanceof paper.CompoundPath) {
|
||
children = oldCompoundPolygon.children as paper.Path[]
|
||
} else if (oldCompoundPolygon instanceof paper.Path) {
|
||
children = [oldCompoundPolygon]
|
||
}
|
||
compoundPolygon = usePaperStore.getState().addCompoundPath(
|
||
renderCompoundPath,
|
||
[...paths!, ...children] as paper.Path[],
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
fillColor: undefined,
|
||
closed: false,
|
||
},
|
||
{ ...oldCompoundPolygon.data, id: path?.data.id, type: "brush" }
|
||
)
|
||
// oldCompoundPolygon?.remove();
|
||
oldCompoundPolygon.data.id = path?.data.id
|
||
}
|
||
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = safeClone(
|
||
useTopToolsStore.getState().subAttributes[label_class] || {}
|
||
)
|
||
|
||
// 多线段存值
|
||
if (compoundPolygon instanceof paper.CompoundPath) {
|
||
if (compoundPolygon.children && compoundPolygon.children.length) {
|
||
let polygonPoints: any[] = []
|
||
let polygonHollowPoints: any[] = []
|
||
|
||
;(compoundPolygon.children as paper.Path[]).forEach((path) => {
|
||
let pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||
// 在镂空或分割的区域中 路径方向是false
|
||
if (path.clockwise) {
|
||
polygonPoints.push(pathData[1]?.segments)
|
||
} else {
|
||
if (path.data.id && path.segments?.length) {
|
||
polygonHollowPoints.push(pathData[1]?.segments)
|
||
}
|
||
}
|
||
})
|
||
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
useTopToolsStore.getState().pressA
|
||
? compoundPolygon.data.id
|
||
: myPath.data.id,
|
||
polygonPoints,
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [
|
||
...useLabelStore.getState().labelDefaultComments,
|
||
],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
},
|
||
polygonHollowPoints,
|
||
]
|
||
)
|
||
useRightToolsStore
|
||
.getState()
|
||
.pushPathId(
|
||
useTopToolsStore.getState().pressA
|
||
? compoundPolygon.data.id
|
||
: myPath.data.id
|
||
)
|
||
}
|
||
} else if (
|
||
compoundPolygon instanceof paper.Path &&
|
||
myPath.data.id &&
|
||
myPath.segments?.length
|
||
) {
|
||
let pathData = JSON.parse(myPath.exportJSON({ precision: 20 }))
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
myPath.data.id,
|
||
// todo intersect+a | +a会变成CompoundPath 理论上已解决这个todo
|
||
[pathData[1]?.segments, ...[]],
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
},
|
||
[],
|
||
]
|
||
)
|
||
useRightToolsStore.getState().pushPathId(myPath.data.id)
|
||
}
|
||
|
||
// 如果该类别存在子属性,则弹出子属性对话框
|
||
if (category && category.sub_attributes.length) {
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category,
|
||
-30,
|
||
-30
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: myPath.data.imageId,
|
||
operationId: myPath.data.operationId,
|
||
id: myPath.data.id,
|
||
})
|
||
}
|
||
|
||
// 设置其为selectedPath
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
useTopToolsStore.getState().pressA
|
||
? compoundPolygon.data.id
|
||
: myPath.data.id
|
||
)
|
||
|
||
forceUpdate()
|
||
|
||
if (compoundPolygon instanceof paper.CompoundPath) {
|
||
myPath.remove()
|
||
myPath = null
|
||
points = []
|
||
} else {
|
||
compoundPolygon?.remove()
|
||
compoundPolygon = null
|
||
myPath = null
|
||
points = []
|
||
}
|
||
|
||
brushCircles.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
brushCircles = []
|
||
|
||
useTopToolsStore.getState().setPressA(false)
|
||
draftId = null
|
||
lastPoint = null
|
||
return
|
||
}
|
||
} else {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
}
|
||
|
||
let temp = renderPath(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
const draftData = getDraftBrushData()
|
||
|
||
// add point
|
||
points.push(point)
|
||
if (myPath) {
|
||
myPath.remove()
|
||
}
|
||
myPath = usePaperStore.getState().addBrush(
|
||
renderPath,
|
||
points,
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
draftData
|
||
)
|
||
let circle = usePaperStore
|
||
.getState()
|
||
.getClickedCircle(
|
||
point,
|
||
brushCircles.length,
|
||
draftData.id,
|
||
usePaperStore.getState().toolOption.blankColor
|
||
)
|
||
brushCircles.push(circle)
|
||
|
||
lastPoint = e.point
|
||
temp.remove()
|
||
}
|
||
|
||
brushTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
}
|
||
|
||
brushTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
|
||
if (myPath) {
|
||
let temp = renderPath(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
myPath.remove()
|
||
myPath = usePaperStore.getState().addBrush(
|
||
renderPath,
|
||
points.concat(point),
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
fillColor: undefined,
|
||
closed: false,
|
||
},
|
||
getDraftBrushData()
|
||
)
|
||
}
|
||
}
|
||
|
||
brushTool.onMouseUp = (_e: paper.ToolEvent) => {
|
||
stopMiddleMousePan()
|
||
}
|
||
|
||
brushTool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "escape" || e.key === "alt") {
|
||
if (myPath || points.length) {
|
||
resetBrushDraft()
|
||
}
|
||
} else if (e.key === "delete") {
|
||
handleDelete()
|
||
forceUpdate()
|
||
} else if (e.key === "shift") {
|
||
handleShift(true)
|
||
}
|
||
}
|
||
brushTool.onKeyUp = (e: paper.KeyEvent) => {
|
||
if (e.key === "shift") {
|
||
handleShift(false)
|
||
}
|
||
}
|
||
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
brushTool: brushTool,
|
||
}))
|
||
},
|
||
addPoint: (
|
||
renderCircle: (paperGroup: paper.Group, option: any) => paper.Path.Circle,
|
||
point: paper.Point | undefined,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let myCircle = renderCircle(usePaperStore.getState().group!, {
|
||
center: point,
|
||
...option,
|
||
strokeScaling: false,
|
||
})
|
||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||
myCircle.scale(newScale / currentScaling)
|
||
myCircle.data = Object.assign({ type: "circle", ...option }, data)
|
||
insertItemBelowTags(myCircle)
|
||
syncPaperCircleDiameter(myCircle)
|
||
myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
// console.log(e.event.clientX, e.event.clientY);
|
||
if (myCircle.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() === myCircle.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: myCircle.data.imageId,
|
||
operationId: myCircle.data.operationId,
|
||
id: myCircle.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
myCircle.onMouseEnter = (e: paper.MouseEvent) => {
|
||
if (usePaperStore.getState().mode === "pan") {
|
||
e.target.strokeWidth += 2
|
||
if (usePaperStore.getState().el)
|
||
usePaperStore.getState().el!.style.cursor = "move"
|
||
}
|
||
}
|
||
myCircle.onMouseLeave = (e: paper.MouseEvent) => {
|
||
if (usePaperStore.getState().mode === "pan") {
|
||
e.target.strokeWidth -= 2
|
||
if (usePaperStore.getState().el)
|
||
usePaperStore.getState().el!.style.cursor = "default"
|
||
}
|
||
}
|
||
return myCircle
|
||
},
|
||
addPointLine: (
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
points: paper.Point[] | undefined,
|
||
option: any,
|
||
data: any
|
||
) => {
|
||
let myCircleLine = renderPath(
|
||
usePaperStore.getState().group!,
|
||
Object.assign(
|
||
{
|
||
...option,
|
||
fillColor: undefined,
|
||
closed: false,
|
||
dashArray: [4, 4],
|
||
strokeScaling: false,
|
||
},
|
||
{ segments: points }
|
||
)
|
||
)
|
||
myCircleLine.data = Object.assign({ type: "point", ...option }, data)
|
||
insertItemBelowTags(myCircleLine)
|
||
return myCircleLine
|
||
},
|
||
|
||
initPointTool: (
|
||
paperPointTool: paper.Tool,
|
||
renderCircle: (paperGroup: paper.Group, option: any) => paper.Path.Circle,
|
||
renderPath: (paperGroup: paper.Group, option: any) => paper.Path,
|
||
forceUpdate: DispatchWithoutAction
|
||
) => {
|
||
let myCircle: paper.Path.Circle[] = []
|
||
let myCircleText: paper.PointText[] = []
|
||
let textNumbers: number[] = []
|
||
let myPath: paper.Path | null
|
||
let lastPoint: paper.Point | null = null
|
||
let points: paper.Point[] = []
|
||
|
||
let pointTool = paperPointTool
|
||
let draftId: number | null = null
|
||
|
||
const getDraftPointData = () => ({
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
id:
|
||
draftId ??
|
||
(draftId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1),
|
||
})
|
||
|
||
const rebuildPointDraft = () => {
|
||
myPath?.remove()
|
||
myPath = null
|
||
|
||
if (points.length) {
|
||
myPath = usePaperStore.getState().addPointLine(
|
||
renderPath,
|
||
points,
|
||
{
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
getDraftPointData()
|
||
)
|
||
} else {
|
||
draftId = null
|
||
}
|
||
|
||
lastPoint = points.length
|
||
? usePaperStore
|
||
.getState()
|
||
.group!.localToGlobal(points[points.length - 1])
|
||
: null
|
||
}
|
||
|
||
const resetPointDraft = () => {
|
||
myPath?.remove()
|
||
myPath = null
|
||
points = []
|
||
textNumbers = []
|
||
myCircleText.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
myCircleText = []
|
||
myCircle.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
myCircle = []
|
||
lastPoint = null
|
||
draftId = null
|
||
}
|
||
|
||
const undoPoint = () => {
|
||
if (!points.length) return false
|
||
points.pop()
|
||
textNumbers.pop()
|
||
myCircle.pop()?.remove()
|
||
myCircleText.pop()?.remove()
|
||
rebuildPointDraft()
|
||
return true
|
||
}
|
||
|
||
pointTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
if (e.event.button === 2) {
|
||
e.event.preventDefault()
|
||
if (myPath || points.length) {
|
||
hidePaperContextMenu()
|
||
undoPoint()
|
||
return
|
||
}
|
||
if (!trySelectPaperObjectByPoint(e.point)) {
|
||
hidePaperContextMenu()
|
||
}
|
||
return
|
||
}
|
||
if (e.event.button !== 0) return
|
||
|
||
// 绘制时去除选中辅助
|
||
if (usePaperSupportStore.getState().selectedCircles.length) {
|
||
usePaperSupportStore.getState().clearAll()
|
||
}
|
||
|
||
// doubleclick
|
||
if (isPaperDoubleClick(e.event, lastPoint, e.point)) {
|
||
if (myPath) {
|
||
// 去除超出图片边界的部分
|
||
myCircle = myCircle.filter((circle) => {
|
||
if (circle.data.id === myPath!.data.id) {
|
||
if (
|
||
usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(circle.bounds.center)
|
||
) {
|
||
return true
|
||
} else {
|
||
circle.remove()
|
||
return false
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
})
|
||
myPath.segments = myPath.segments.filter((segment) => {
|
||
return usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(segment.point)
|
||
})
|
||
// 去除超出图片边界的部分 end
|
||
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = safeClone(
|
||
useTopToolsStore.getState().subAttributes[label_class] || {}
|
||
)
|
||
|
||
// 存值
|
||
// let circles = usePaperStore.getState().group!.getItems({
|
||
// data: { type: "circle", id: myPath.data.id },
|
||
// });
|
||
let circles = myCircle.filter((item) => {
|
||
return item.data.id === myPath!.data.id
|
||
})
|
||
if (circles.length) {
|
||
let segments = circles.map((circle, circleIndex) => {
|
||
// 重新设置显示数字
|
||
circle.data.text = circleIndex + 1
|
||
return {
|
||
point: [circle.position.x, circle.position.y],
|
||
attributes: circle.data.attributes,
|
||
}
|
||
})
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(
|
||
useBottomToolsStore.getState().activeImage,
|
||
useTopToolsStore.getState().activeOperation,
|
||
[
|
||
myPath.data.id,
|
||
// 关键点 没有多区域
|
||
[segments.map((item) => item.point)],
|
||
{
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: useBottomToolsStore.getState().activeImageId,
|
||
category_id: Number(
|
||
useTopToolsStore.getState().activeOperation
|
||
),
|
||
circles: segments,
|
||
},
|
||
[],
|
||
]
|
||
)
|
||
|
||
// 如果该类别存在子属性,则弹出子属性对话框
|
||
if (category && category.sub_attributes.length) {
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category,
|
||
-30,
|
||
-30
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: myPath.data.imageId,
|
||
operationId: myPath.data.operationId,
|
||
id: myPath.data.id,
|
||
})
|
||
}
|
||
|
||
// 设置其为selectedPath
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
myPath.data.id
|
||
)
|
||
useRightToolsStore.getState().pushPathId(myPath.data.id)
|
||
|
||
forceUpdate()
|
||
}
|
||
|
||
myPath = null
|
||
points = []
|
||
textNumbers = []
|
||
myCircleText.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
myCircleText = []
|
||
// 避免后续按键删除已绘制的circle
|
||
myCircle = []
|
||
draftId = null
|
||
lastPoint = null
|
||
|
||
return
|
||
}
|
||
} else {
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: false,
|
||
position: {
|
||
top: 0,
|
||
left: 0,
|
||
},
|
||
imageId: "",
|
||
operationId: "",
|
||
id: 0,
|
||
})
|
||
}
|
||
|
||
let temp = renderCircle(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
let text = textNumbers.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
const draftData = getDraftPointData()
|
||
|
||
let circle = usePaperStore.getState().addPoint(
|
||
renderCircle,
|
||
point,
|
||
{
|
||
fillColor: null,
|
||
// center: point,
|
||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||
radius: 3,
|
||
strokeScaling: false,
|
||
},
|
||
{
|
||
...draftData,
|
||
text: text,
|
||
}
|
||
)
|
||
// add circle
|
||
myCircle.push(circle)
|
||
let pointText = new paper.PointText({
|
||
parent: usePaperStore.getState().group!,
|
||
point: point,
|
||
content: text.toString(),
|
||
justification: "right",
|
||
fillColor: usePaperStore.getState().toolOption?.strokeColor,
|
||
fontSize: 12,
|
||
})
|
||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||
pointText.scale(newScale / currentScaling)
|
||
textNumbers.push(text)
|
||
myCircleText.push(pointText)
|
||
// add point
|
||
points.push(point)
|
||
|
||
if (myPath) {
|
||
myPath.remove()
|
||
}
|
||
myPath = usePaperStore.getState().addPointLine(
|
||
renderPath,
|
||
points,
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
},
|
||
draftData
|
||
)
|
||
|
||
lastPoint = e.point
|
||
|
||
// group.current?.addChild(myCircle);
|
||
temp.remove()
|
||
}
|
||
pointTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
}
|
||
pointTool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
|
||
if (myPath) {
|
||
let temp = renderPath(usePaperStore.getState().group!, {})
|
||
let point = temp.globalToLocal(e.point)
|
||
myPath.remove()
|
||
myPath = usePaperStore.getState().addPointLine(
|
||
renderPath,
|
||
points.concat(point),
|
||
{
|
||
// option
|
||
...usePaperStore.getState().toolOption,
|
||
fillColor: undefined,
|
||
closed: false,
|
||
},
|
||
getDraftPointData()
|
||
)
|
||
}
|
||
}
|
||
pointTool.onMouseUp = (_e: paper.ToolEvent) => {
|
||
stopMiddleMousePan()
|
||
}
|
||
pointTool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "escape" || e.key === "alt") {
|
||
if (myPath || points.length) {
|
||
resetPointDraft()
|
||
}
|
||
} else if (e.key === "delete") {
|
||
handleDelete()
|
||
forceUpdate()
|
||
} else if (e.key === "shift") {
|
||
handleShift(true)
|
||
}
|
||
}
|
||
pointTool.onKeyUp = (e: paper.KeyEvent) => {
|
||
if (e.key === "shift") {
|
||
handleShift(false)
|
||
}
|
||
}
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
pointTool: pointTool,
|
||
}))
|
||
},
|
||
getPointCircle: (
|
||
point: paper.Point,
|
||
index: number,
|
||
id: number,
|
||
color: any
|
||
) => {
|
||
const nodeDiameter = getPaperNodeDiameter()
|
||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||
Object.assign(
|
||
{ parent: usePaperStore.getState().group!, center: point },
|
||
{
|
||
fillColor: color,
|
||
// center: point,
|
||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||
radius: getPaperNodeRadius(),
|
||
strokeScaling: false,
|
||
},
|
||
{
|
||
data: {
|
||
type: "circle",
|
||
diameter: nodeDiameter,
|
||
fillColor: color,
|
||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
// bug
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
text: +index + 1,
|
||
id: id,
|
||
},
|
||
}
|
||
)
|
||
)
|
||
circle.scale(getPaperNodeScaleFactor())
|
||
circle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (e.event && e.event.button === 2) {
|
||
if (
|
||
["polygon", "brush", "point"].includes(
|
||
usePaperStore.getState().mode || ""
|
||
)
|
||
)
|
||
return
|
||
// console.log(e.event.clientX, e.event.clientY);
|
||
if (circle.data.id) {
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) => item.category_id.toString() === circle.data.operationId
|
||
)
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
5,
|
||
5
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: circle.data.imageId,
|
||
operationId: circle.data.operationId,
|
||
id: circle.data.id,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
circle.onMouseEnter = (e: paper.MouseEvent) => {
|
||
if (usePaperStore.getState().mode === "pan") {
|
||
e.target.strokeWidth += 2
|
||
if (usePaperStore.getState().el)
|
||
usePaperStore.getState().el!.style.cursor = "move"
|
||
}
|
||
}
|
||
circle.onMouseLeave = (e: paper.MouseEvent) => {
|
||
if (usePaperStore.getState().mode === "pan") {
|
||
e.target.strokeWidth -= 2
|
||
if (usePaperStore.getState().el)
|
||
usePaperStore.getState().el!.style.cursor = "default"
|
||
}
|
||
}
|
||
return circle
|
||
},
|
||
initSupportTool: (tool, renderPath) => {
|
||
let points: Array<[number, number]> = []
|
||
let tags: number[] = []
|
||
let boxes: Array<[number, number, number, number]> = []
|
||
let dragStartPoint: paper.Point | null = null
|
||
let dragButton: number | null = null
|
||
let supportBox: paper.Path.Rectangle | null = null
|
||
let isBoxDragging = false
|
||
|
||
const BOX_DRAG_THRESHOLD = 6
|
||
|
||
const clearSupportBoxPreview = () => {
|
||
supportBox?.remove()
|
||
supportBox = null
|
||
isBoxDragging = false
|
||
}
|
||
|
||
const getSupportBox = (
|
||
from: paper.Point,
|
||
to: paper.Point
|
||
): [number, number, number, number] => [
|
||
Math.min(from.x, to.x),
|
||
Math.min(from.y, to.y),
|
||
Math.max(from.x, to.x),
|
||
Math.max(from.y, to.y),
|
||
]
|
||
|
||
const renderSupportBoxPreview = (from: paper.Point, to: paper.Point) => {
|
||
const [left, top, right, bottom] = getSupportBox(from, to)
|
||
supportBox?.remove()
|
||
supportBox = new paper.Path.Rectangle({
|
||
parent: usePaperStore.getState().group!,
|
||
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",
|
||
},
|
||
})
|
||
}
|
||
|
||
const renderSupportPrompt = () => {
|
||
void renderPath(points, tags, points.length + boxes.length === 1, boxes)
|
||
}
|
||
|
||
tool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||
if (startMiddleMousePan(e.event)) return
|
||
dragStartPoint = usePaperStore.getState().group!.globalToLocal(e.point)
|
||
dragButton = e.event.button
|
||
clearSupportBoxPreview()
|
||
}
|
||
|
||
tool.onMouseDrag = (e: {
|
||
event: MouseEvent
|
||
point: paper.Point
|
||
delta: paper.Point
|
||
}) => {
|
||
if (dragMiddleMousePan(e)) return
|
||
|
||
if (!dragStartPoint || dragButton !== 0) return
|
||
|
||
const currentPoint = usePaperStore
|
||
.getState()
|
||
.group!.globalToLocal(e.point)
|
||
if (currentPoint.subtract(dragStartPoint).length <= BOX_DRAG_THRESHOLD)
|
||
return
|
||
|
||
isBoxDragging = true
|
||
renderSupportBoxPreview(dragStartPoint, currentPoint)
|
||
}
|
||
|
||
tool.onMouseUp = (e: paper.ToolEvent) => {
|
||
if (stopMiddleMousePan()) return
|
||
if (!dragStartPoint || dragButton === null) return
|
||
|
||
const currentPoint = usePaperStore
|
||
.getState()
|
||
.group!.globalToLocal(e.point)
|
||
|
||
if (dragButton === 0 && isBoxDragging) {
|
||
boxes.push(getSupportBox(dragStartPoint, currentPoint))
|
||
} else {
|
||
points.push([dragStartPoint.x, dragStartPoint.y])
|
||
tags.push(dragButton === 0 ? 1 : 0)
|
||
}
|
||
|
||
renderSupportPrompt()
|
||
clearSupportBoxPreview()
|
||
dragStartPoint = null
|
||
dragButton = null
|
||
}
|
||
|
||
tool.onKeyDown = (e: paper.KeyEvent) => {
|
||
if (e.key === "escape") {
|
||
points = []
|
||
tags = []
|
||
boxes = []
|
||
dragStartPoint = null
|
||
dragButton = null
|
||
clearSupportBoxPreview()
|
||
clearSupportAnnotationItem()
|
||
usePaperStore.getState().setPaperMode("pan")
|
||
}
|
||
if (e.key === "f1") {
|
||
points = []
|
||
tags = []
|
||
boxes = []
|
||
dragStartPoint = null
|
||
dragButton = null
|
||
clearSupportBoxPreview()
|
||
}
|
||
}
|
||
|
||
tool.onMouseMove = (e: paper.MouseEvent) => {
|
||
handleMouseMove(e)
|
||
}
|
||
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
supportTool: tool,
|
||
}))
|
||
},
|
||
getClickedCircle: (
|
||
point: paper.Point,
|
||
index: number,
|
||
id: number,
|
||
color: any
|
||
) => {
|
||
const nodeDiameter = getPaperNodeDiameter()
|
||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||
Object.assign(
|
||
{ parent: usePaperStore.getState().group!, center: point },
|
||
{
|
||
fillColor: color,
|
||
// center: point,
|
||
strokeColor: "#FFF",
|
||
strokeWidth: 2,
|
||
radius: getPaperNodeRadius(),
|
||
strokeScaling: false,
|
||
},
|
||
{
|
||
data: { index, type: "pathCircle", id, diameter: nodeDiameter },
|
||
}
|
||
)
|
||
)
|
||
circle.scale(getPaperNodeScaleFactor())
|
||
circle.onMouseEnter = (_e: any) => {
|
||
hoveredPathCircleInfo = {
|
||
id,
|
||
index,
|
||
item: circle,
|
||
}
|
||
clearHoveredBufferPathHighlight()
|
||
circle.strokeWidth = 4
|
||
circle.fillColor = new paper.Color("#FFF")
|
||
if (
|
||
usePaperStore.getState().mode === "pan" &&
|
||
usePaperStore.getState().el
|
||
) {
|
||
usePaperStore.getState().el!.style.cursor = "move"
|
||
}
|
||
}
|
||
circle.onMouseLeave = (_e: any) => {
|
||
if (hoveredPathCircleInfo?.item === circle) {
|
||
hoveredPathCircleInfo = null
|
||
}
|
||
circle.strokeWidth = 2
|
||
circle.fillColor = color
|
||
syncPaperCursorByMode()
|
||
}
|
||
return circle
|
||
},
|
||
getBuffPath: (
|
||
item: paper.Segment,
|
||
index: number,
|
||
id: number,
|
||
color: any
|
||
) => {
|
||
if (!item.next) return null
|
||
const hitStrokeColor = clonePaperColor(color) || new paper.Color("#fff")
|
||
hitStrokeColor.alpha = 0.001
|
||
let bufferPath: paper.Path = new paper.Path(
|
||
Object.assign(
|
||
{
|
||
parent: usePaperStore.getState().group!,
|
||
segments: [item.point, item.next],
|
||
strokeColor: hitStrokeColor, // 透明度不能为 0,否则部分命中/hover 事件会失效
|
||
strokeScaling: false,
|
||
strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
|
||
strokeCap: "round",
|
||
onSelect: function (event: { stopPropagation: () => void }) {
|
||
event.stopPropagation() // 阻止事件冒泡
|
||
},
|
||
},
|
||
{
|
||
data: { index, type: "pathBuff", id },
|
||
}
|
||
)
|
||
)
|
||
// 复制段的handleIn和handleOut到缓冲路径
|
||
// bufferPath.segments[0].handleIn = item.handleIn;
|
||
// bufferPath.segments[1].handleOut = item.handleOut;
|
||
let edge: paper.Path | null
|
||
let clearTimer: ReturnType<typeof setTimeout> | null = null
|
||
const cancelScheduledClear = () => {
|
||
if (clearTimer) {
|
||
clearTimeout(clearTimer)
|
||
clearTimer = null
|
||
}
|
||
}
|
||
const clearHighlight = () => {
|
||
cancelScheduledClear()
|
||
edge?.remove()
|
||
edge = null
|
||
if (hoveredBufferPathInfo?.path === bufferPath) {
|
||
hoveredBufferPathInfo = null
|
||
}
|
||
if (
|
||
usePaperStore.getState().el &&
|
||
!hoveredPathCircleInfo?.item.parent &&
|
||
!hoveredBufferPathInfo
|
||
) {
|
||
syncPaperCursorByMode()
|
||
}
|
||
}
|
||
const scheduleClear = () => {
|
||
cancelScheduledClear()
|
||
clearTimer = setTimeout(() => {
|
||
clearHighlight()
|
||
}, BUFFER_HOVER_STICKY_MS)
|
||
}
|
||
// 鼠标移入时高亮显示
|
||
bufferPath.onMouseEnter = function (event: {
|
||
stopPropagation?: () => void
|
||
}) {
|
||
if (hoveredPathCircleInfo?.item.parent) {
|
||
event.stopPropagation?.()
|
||
return
|
||
}
|
||
cancelScheduledClear()
|
||
hoveredBufferPathInfo = {
|
||
id,
|
||
index,
|
||
path: bufferPath,
|
||
clearHighlight,
|
||
scheduleClear,
|
||
cancelScheduledClear,
|
||
}
|
||
if (edge && !edge.parent) {
|
||
edge = null
|
||
}
|
||
if (!edge) {
|
||
edge = new paper.Path(
|
||
Object.assign(
|
||
{
|
||
parent: usePaperStore.getState().group!,
|
||
segments: [
|
||
bufferPath.segments[0].point,
|
||
bufferPath.segments[1].point,
|
||
],
|
||
strokeColor: "white", // 初始颜色与原始路径相同
|
||
strokeWidth: 3,
|
||
strokeScaling: false,
|
||
strokeCap: "round",
|
||
},
|
||
{
|
||
data: { index, type: "pathBuffHover" },
|
||
}
|
||
)
|
||
)
|
||
// edge.bringToFront();
|
||
if (usePaperStore.getState().el) {
|
||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
||
}
|
||
}
|
||
event.stopPropagation?.() // 阻止事件冒泡
|
||
}
|
||
|
||
// 鼠标移出时取消高亮
|
||
bufferPath.onMouseLeave = function (event: {
|
||
stopPropagation?: () => void
|
||
}) {
|
||
if (hoveredBufferPathInfo?.path === bufferPath) {
|
||
hoveredBufferPathInfo.scheduleClear()
|
||
} else {
|
||
scheduleClear()
|
||
}
|
||
event.stopPropagation?.() // 阻止事件冒泡
|
||
}
|
||
return bufferPath
|
||
},
|
||
setPaperMode: (modeParam) => {
|
||
stopMiddleMousePan()
|
||
set((state: PaperState) => ({
|
||
...state,
|
||
mode: modeParam,
|
||
}))
|
||
|
||
switch (modeParam) {
|
||
case "pan":
|
||
usePaperStore.getState().panTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
case "polygon":
|
||
usePaperStore.getState().polygonTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
case "rectangle":
|
||
usePaperStore.getState().rectangleTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
case "brush":
|
||
usePaperStore.getState().brushTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
case "point":
|
||
usePaperStore.getState().pointTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
case "support":
|
||
usePaperStore.getState().supportTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
default:
|
||
usePaperStore.getState().viewTool?.activate()
|
||
syncPaperCursorByMode(modeParam)
|
||
break
|
||
}
|
||
},
|
||
setGroupScale: (scale: number) => {
|
||
if (usePaperStore.getState().group) {
|
||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
||
const newScale = scale / currentScaling
|
||
const point = usePositionStore.getState().currentMousePosition
|
||
|
||
if (point[0] !== 0 || point[1] !== 0)
|
||
usePaperStore.getState().group!.scale(newScale, new paper.Point(point))
|
||
else usePaperStore.getState().group!.scale(newScale)
|
||
|
||
let texts = usePaperStore.getState().group!.getItems({
|
||
data: { type: "text" },
|
||
}) as paper.Path[]
|
||
texts.forEach((text) => {
|
||
text.scale(1 / newScale)
|
||
})
|
||
|
||
let pathCircles = usePaperStore.getState().group!.getItems({
|
||
data: { type: "pathCircle" },
|
||
}) as paper.Path[]
|
||
pathCircles.forEach((pathCircle) => {
|
||
pathCircle.scale(1 / newScale)
|
||
})
|
||
let circles = usePaperStore.getState().group!.getItems({
|
||
data: { type: "circle" },
|
||
}) as paper.Path[]
|
||
circles.forEach((circle) => {
|
||
circle.scale(1 / newScale)
|
||
})
|
||
|
||
const tagItems = usePaperStore.getState().group!.getItems({
|
||
data: { type: "tag" },
|
||
}) as paper.Item[]
|
||
tagItems.forEach((item) => {
|
||
if (item.data?.tag_category === "mask") return
|
||
const anchor = item.data?.tag_anchor
|
||
if (Array.isArray(anchor) && anchor.length >= 2) {
|
||
const anchorX = Number(anchor[0])
|
||
const anchorY = Number(anchor[1])
|
||
if (Number.isFinite(anchorX) && Number.isFinite(anchorY)) {
|
||
item.scale(1 / newScale, new paper.Point(anchorX, anchorY))
|
||
return
|
||
}
|
||
}
|
||
item.scale(1 / newScale)
|
||
})
|
||
}
|
||
},
|
||
getAll: () => {
|
||
let polygon = usePaperStore.getState().group!.getItems({
|
||
data: { type: "polygon" },
|
||
}) as paper.Path[]
|
||
return polygon
|
||
},
|
||
getItemById: (id: number) => {
|
||
// let item = usePaperStore.getState().group!.getItems({
|
||
// id: id,
|
||
// }) as paper.Path[];
|
||
let savedItem: paper.Path[] = (
|
||
usePaperStore.getState().group!.getItems({
|
||
data: { id: id },
|
||
}) as paper.Path[]
|
||
).filter((item) =>
|
||
["rectangle", "polygon", "brush", "point"].includes(item.data.type)
|
||
)
|
||
if (savedItem?.[0]) {
|
||
return savedItem[0]
|
||
} else {
|
||
return null
|
||
}
|
||
},
|
||
getItemsById: (id: number) => {
|
||
// let item = usePaperStore.getState().group!.getItems({
|
||
// id: id,
|
||
// }) as paper.Path[];
|
||
let savedItem = usePaperStore.getState().group!.getItems({
|
||
data: { id: id },
|
||
}) as paper.Path[]
|
||
if (savedItem?.length) {
|
||
return savedItem
|
||
} else {
|
||
return []
|
||
}
|
||
},
|
||
}))
|