Files
labelimage/components/label/usePaperStore.ts

6929 lines
214 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

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 []
}
},
}))