6452 lines
204 KiB
TypeScript
6452 lines
204 KiB
TypeScript
import { notifications } from "@mantine/notifications"
|
||
import dayjs from "dayjs"
|
||
import paper from "paper"
|
||
import { DispatchWithoutAction } from "react"
|
||
import { create } from "zustand"
|
||
import { Comment } from "./api/label/typing"
|
||
import { Project } from "./api/project/typing"
|
||
import {
|
||
initialDetail,
|
||
type 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"
|
||
|
||
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
|
||
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 handleDelete = () => {
|
||
const group = usePaperStore.getState().group!
|
||
const imageId = useBottomToolsStore.getState().activeImage
|
||
let ids = useObjectStore.getState().selectedPath[imageId]
|
||
let operationIds: any[] = []
|
||
let parentGroupIds: any[] = []
|
||
|
||
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
|
||
)
|
||
})
|
||
|
||
// handle group path update
|
||
if (parentGroupIds.length) {
|
||
console.log(parentGroupIds)
|
||
const groupMap = useRightToolsStore.getState().pathGroupMap.get(imageId)
|
||
parentGroupIds.forEach((parentGroupId, index) => {
|
||
if (parentGroupId) {
|
||
const currentGroupIds = groupMap
|
||
?.get(parentGroupId)
|
||
?.filter((v) => v !== ids[index])
|
||
if (!currentGroupIds || currentGroupIds?.length <= 1) {
|
||
groupMap?.delete(parentGroupId)
|
||
} else {
|
||
groupMap?.set(parentGroupId, currentGroupIds ?? [])
|
||
}
|
||
}
|
||
})
|
||
}
|
||
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)
|
||
})
|
||
useLabelStore.getState().setLabel(currentLabelData)
|
||
useLabelStore.getState().pushStateStack(currentLabelData)
|
||
// 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
|
||
) => {
|
||
if (!pathId) return
|
||
|
||
usePaperStore
|
||
.getState()
|
||
.group?.getItems({
|
||
data: { id: pathId },
|
||
})
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
|
||
useObjectStore.getState().updateSelectedPath(imageId, "DELETE", pathId)
|
||
|
||
if (!parentGroupId) return
|
||
|
||
const groupMap = useRightToolsStore.getState().pathGroupMap.get(imageId)
|
||
const currentGroupIds = groupMap
|
||
?.get(parentGroupId)
|
||
?.filter((id) => id !== pathId)
|
||
|
||
if (!currentGroupIds || currentGroupIds.length <= 1) {
|
||
groupMap?.delete(parentGroupId)
|
||
return
|
||
}
|
||
|
||
groupMap?.set(parentGroupId, currentGroupIds)
|
||
}
|
||
|
||
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])
|
||
|
||
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) => {
|
||
if (
|
||
(item instanceof paper.Path || item instanceof paper.CompoundPath) &&
|
||
["rectangle", "polygon"].includes(item.data?.type || "")
|
||
) {
|
||
item.fillColor = item.data?.blankColor || item.fillColor || null
|
||
}
|
||
})
|
||
})
|
||
|
||
selectedIds.forEach((id) => {
|
||
useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", id)
|
||
})
|
||
}
|
||
|
||
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
||
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 (item instanceof paper.Path) {
|
||
usePaperSupportStore.getState().handleMask(item)
|
||
usePaperSupportStore.getState().handleBufferPaths(item)
|
||
usePaperSupportStore.getState().handleCircles(item)
|
||
}
|
||
} else if (item.data.type === "point" && item instanceof paper.Path) {
|
||
item.data.selected = true
|
||
usePaperSupportStore.getState().handleMask(item)
|
||
usePaperSupportStore.getState().handleText(item)
|
||
usePaperSupportStore.getState().handleBufferPaths(item)
|
||
usePaperSupportStore.getState().handleCircles(item)
|
||
} else if (item.data.type === "polygon") {
|
||
item.data.selected = true
|
||
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,
|
||
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",
|
||
},
|
||
})
|
||
// 标识位为true时:选中路径隐藏tags; 没选中显示tags
|
||
const configs = useTopToolsStore.getState().showTagsConfigs
|
||
if (useTopToolsStore.getState().showTags) {
|
||
const setTagVisibleByConfig = (tag: any) => {
|
||
if (tag.data.tag_category === "mask")
|
||
tag.visible = configs.includes("外框")
|
||
else if (tag.data.tag_category === "point_text")
|
||
tag.visible = configs.includes("点ID")
|
||
else tag.visible = true
|
||
}
|
||
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 handlePolygonIntersectRaster = (path: paper.Path) => {
|
||
let intersections = path.getIntersections(
|
||
usePaperStore.getState().rasterPath!
|
||
)
|
||
let validIntersections = intersections.filter((intersection) => {
|
||
// 检查交点是否在两个路径的可见部分
|
||
return (
|
||
path.contains(intersection.point) &&
|
||
usePaperStore.getState().rasterPath!.contains(intersection.point)
|
||
)
|
||
})
|
||
|
||
if (validIntersections.length) {
|
||
let oldPolygon: any = path
|
||
console.log("处理前", path)
|
||
newDrawPath = oldPolygon.intersect(
|
||
usePaperStore.getState().rasterPath!
|
||
) as any
|
||
console.log("after", newDrawPath)
|
||
// if (!newDrawPath?.clockwise) newDrawPath?.reverse();
|
||
if (newDrawPath instanceof paper.CompoundPath)
|
||
(newDrawPath.children as paper.Path[]).forEach((child) => {
|
||
if (
|
||
!child.data ||
|
||
(child.data && !Object.keys(child.data).length)
|
||
)
|
||
child.data = safeClone(newDrawPath?.data || {})
|
||
})
|
||
oldPolygon.remove()
|
||
oldPolygon = null
|
||
} else {
|
||
// path整体移动到图片外 清空path的点
|
||
if (
|
||
!usePaperStore
|
||
.getState()
|
||
.rasterPath!.bounds.contains(path.bounds) &&
|
||
newDrawPath instanceof paper.Path
|
||
) {
|
||
newDrawPath.segments = []
|
||
}
|
||
}
|
||
usePaperSupportStore.getState().handleBufferPaths(newDrawPath)
|
||
usePaperSupportStore.getState().handleCircles(newDrawPath)
|
||
}
|
||
|
||
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 handlePolygonIntersect = () => {
|
||
if (panMode !== "stroke" && newDrawPath) {
|
||
const handleCurrentPathSub = (eachPath: paper.Path) => {
|
||
if (newDrawPath) {
|
||
let oldPolygon: any = newDrawPath
|
||
let oldParent: any = newDrawPath.parent
|
||
|
||
newDrawPath = oldPolygon.subtract(eachPath)
|
||
|
||
// if (!newDrawPath?.clockwise) newDrawPath?.reverse();
|
||
if (newDrawPath instanceof paper.CompoundPath) {
|
||
;(newDrawPath.children as paper.Path[]).forEach((child) => {
|
||
child.data = safeClone(
|
||
Object.assign({}, newDrawPath?.data || {}, {
|
||
isHollowPolygon: !child.clockwise,
|
||
})
|
||
)
|
||
})
|
||
if (oldParent instanceof paper.CompoundPath) {
|
||
;(oldParent.children as paper.Path[]).forEach((child) => {
|
||
child.data = safeClone(
|
||
Object.assign({}, newDrawPath?.data || {}, {
|
||
isHollowPolygon: !child.clockwise,
|
||
})
|
||
)
|
||
})
|
||
}
|
||
}
|
||
|
||
oldPolygon.remove()
|
||
oldPolygon = null
|
||
}
|
||
}
|
||
|
||
usePaperStore.getState().group?.children!.forEach((item: any) => {
|
||
if (
|
||
!item.data.id ||
|
||
item.data.type !== "polygon" ||
|
||
!item.visible ||
|
||
item.data.id === newDrawPath?.data.id
|
||
)
|
||
return
|
||
if (item instanceof paper.Path) {
|
||
if (item.segments.length) {
|
||
handleCurrentPathSub(item)
|
||
}
|
||
} else if (item instanceof paper.CompoundPath) {
|
||
;(item.children as paper.Path[]).forEach((child) => {
|
||
if (
|
||
child?.segments?.length &&
|
||
child.data.id !== newDrawPath?.data.id
|
||
) {
|
||
handleCurrentPathSub(child)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
const handlePolygonUnite = () => {
|
||
const handleCurrentPathUnite = (eachPath: paper.Path) => {
|
||
if (!newDrawPath) return
|
||
let intersectionPath = newDrawPath.intersect(eachPath, {
|
||
insert: false,
|
||
}) as paper.Path
|
||
if (intersectionPath.segments?.length) {
|
||
let oldPolygon: any = newDrawPath
|
||
newDrawPath = oldPolygon.unite(eachPath) as any
|
||
// if (!newDrawPath?.clockwise) newDrawPath?.reverse();
|
||
if (newDrawPath instanceof paper.CompoundPath)
|
||
(newDrawPath.children as paper.Path[]).forEach((child) => {
|
||
child.data = safeClone(newDrawPath?.data || {})
|
||
})
|
||
oldPolygon.remove()
|
||
oldPolygon = null
|
||
}
|
||
}
|
||
usePaperStore.getState().group?.children!.forEach((item: any) => {
|
||
if (!item.data.id || item.data.type !== "polygon" || !item.visible)
|
||
return
|
||
if (item instanceof paper.CompoundPath) {
|
||
;(item.children as paper.Path[]).forEach((child) => {
|
||
handleCurrentPathUnite(child)
|
||
})
|
||
} else if (item instanceof paper.Path) {
|
||
handleCurrentPathUnite(item)
|
||
}
|
||
})
|
||
}
|
||
|
||
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)
|
||
|
||
// 边界裁剪
|
||
handlePolygonIntersectRaster(newDrawPath as any)
|
||
|
||
usePaperSupportStore.getState().handleCircles(newDrawPath)
|
||
console.log("边界裁剪之后", newDrawPath)
|
||
|
||
if (useTopToolsStore.getState().drawOption === "intersect") {
|
||
// 裁剪
|
||
handlePolygonIntersect()
|
||
usePaperSupportStore.getState().handleCircles(newDrawPath)
|
||
} else if (useTopToolsStore.getState().drawOption === "unite") {
|
||
// 融合
|
||
handlePolygonUnite()
|
||
usePaperSupportStore.getState().handleCircles(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 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
|
||
}
|
||
})
|
||
}
|
||
|
||
// 边界裁剪
|
||
// handlePolygonIntersectRaster(newDrawPath as any);
|
||
|
||
// 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[]) => {
|
||
let polygonPoints: any[] = []
|
||
let polygonHollowPoints: any[] = []
|
||
let bool = paths.some((child) => {
|
||
return child.segments.length > 0
|
||
})
|
||
|
||
if (bool) {
|
||
;(paths as paper.Path[]).forEach((path) => {
|
||
if (path.data.type !== "polygon") return
|
||
let pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||
// 在镂空区域中 镂空区域路径方向为false
|
||
if (!path.data.isHollowPolygon) {
|
||
polygonPoints.push(pathData[1]?.segments)
|
||
} else {
|
||
if (path.data.id && path.segments?.length) {
|
||
polygonHollowPoints.push(pathData[1]?.segments)
|
||
}
|
||
}
|
||
})
|
||
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)
|
||
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)
|
||
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 handlePolygonIntersectRaster = (savePath: paper.Path) => {
|
||
let oldPolygon: any = savePath
|
||
polygon = oldPolygon.intersect(
|
||
usePaperStore.getState().rasterPath!
|
||
) as any
|
||
if (polygon instanceof paper.CompoundPath)
|
||
(polygon.children as paper.Path[]).forEach((child) => {
|
||
if (!child.data || (child.data && !Object.keys(child.data).length))
|
||
child.data = safeClone(
|
||
Object.assign(
|
||
{ isHollowPolygon: !child.clockwise },
|
||
polygon?.data || {}
|
||
)
|
||
)
|
||
})
|
||
oldPolygon.remove()
|
||
oldPolygon = null
|
||
}
|
||
|
||
const handlePolygonIntersect = (path: paper.Path) => {
|
||
const handleCurrentPathSub = (eachPath: paper.Path) => {
|
||
if (!polygon) return
|
||
let oldPolygon: any = polygon
|
||
polygon = oldPolygon?.subtract(eachPath) as any
|
||
if (polygon instanceof paper.CompoundPath)
|
||
(polygon.children as paper.Path[]).forEach((child) => {
|
||
child.data = safeClone(
|
||
Object.assign(
|
||
{ isHollowPolygon: !child.clockwise },
|
||
polygon?.data || {}
|
||
)
|
||
)
|
||
})
|
||
oldPolygon?.remove()
|
||
oldPolygon = null
|
||
}
|
||
|
||
usePaperStore.getState().group?.children!.forEach((item: any) => {
|
||
if (!item.data.id || item.data.type !== "polygon" || !item.visible)
|
||
return
|
||
if (item instanceof paper.Path) {
|
||
if (item?.segments?.length && item.data.id !== path?.data.id) {
|
||
handleCurrentPathSub(item)
|
||
}
|
||
} else if (
|
||
item instanceof paper.CompoundPath &&
|
||
item.data.id !== path.data.id
|
||
) {
|
||
;(item.children as paper.Path[]).forEach((child) => {
|
||
if (child?.segments?.length && child.data.id !== path?.data.id) {
|
||
handleCurrentPathSub(child)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const handlePolygonUnite = () => {
|
||
const handleCurrentPathUnite = (eachPath: paper.Path) => {
|
||
if (!polygon) return
|
||
let intersectionPath = polygon.intersect(eachPath, {
|
||
insert: false,
|
||
}) as paper.Path
|
||
if (intersectionPath.segments?.length) {
|
||
let oldPolygon: any = polygon
|
||
polygon = oldPolygon.unite(eachPath) as any
|
||
if (polygon instanceof paper.CompoundPath)
|
||
(polygon.children as paper.Path[]).forEach((child) => {
|
||
child.data = safeClone(polygon?.data || {})
|
||
})
|
||
oldPolygon?.remove()
|
||
oldPolygon = null
|
||
}
|
||
}
|
||
|
||
usePaperStore.getState().group?.children!.forEach((item: any) => {
|
||
if (!item.data.id || item.data.type !== "polygon" || !item.visible)
|
||
return
|
||
if (item instanceof paper.CompoundPath) {
|
||
;(item.children as paper.Path[]).forEach((child) => {
|
||
handleCurrentPathUnite(child)
|
||
})
|
||
} else if (item instanceof paper.Path) {
|
||
handleCurrentPathUnite(item)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 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 getSegmentPoint = (segment: any): [number, number] | null => {
|
||
if (
|
||
Array.isArray(segment) &&
|
||
segment.length >= 2 &&
|
||
typeof segment[0] === "number" &&
|
||
typeof segment[1] === "number"
|
||
) {
|
||
return [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 [segment[0][0], segment[0][1]]
|
||
}
|
||
if (
|
||
segment?.point &&
|
||
typeof segment.point.x === "number" &&
|
||
typeof segment.point.y === "number"
|
||
) {
|
||
return [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 [segment.point[0], segment.point[1]]
|
||
}
|
||
return null
|
||
}
|
||
|
||
const normalizePolygonFromSegments = (segments: any[]) => {
|
||
const polygon = (segments || [])
|
||
.map((segment) => getSegmentPoint(segment))
|
||
.filter((point): point is [number, number] => !!point)
|
||
|
||
if (polygon.length > 1) {
|
||
const [firstX, firstY] = polygon[0]
|
||
const [lastX, lastY] = polygon[polygon.length - 1]
|
||
if (
|
||
Math.abs(firstX - lastX) < 0.000001 &&
|
||
Math.abs(firstY - lastY) < 0.000001
|
||
) {
|
||
polygon.pop()
|
||
}
|
||
}
|
||
return polygon
|
||
}
|
||
|
||
const isPointInPolygon = (
|
||
point: [number, number],
|
||
polygon: [number, number][]
|
||
) => {
|
||
if (polygon.length < 3) return false
|
||
const [x, y] = point
|
||
let inside = false
|
||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||
const [xi, yi] = polygon[i]
|
||
const [xj, yj] = polygon[j]
|
||
const dy = yj - yi
|
||
if (yi > y !== yj > y) {
|
||
const crossX = ((xj - xi) * (y - yi)) / (dy || 0.000001) + xi
|
||
if (x < crossX) inside = !inside
|
||
}
|
||
}
|
||
return inside
|
||
}
|
||
|
||
const getHollowFlagsByContainment = (allSegments: any[][]) => {
|
||
const polygons = allSegments.map((segments) =>
|
||
normalizePolygonFromSegments(segments)
|
||
)
|
||
return polygons.map((polygon, index) => {
|
||
if (polygon.length < 3) return false
|
||
const samplePoint = polygon[0]
|
||
let containDepth = 0
|
||
polygons.forEach((otherPolygon, otherIndex) => {
|
||
if (otherIndex === index || otherPolygon.length < 3) return
|
||
if (isPointInPolygon(samplePoint, otherPolygon)) containDepth += 1
|
||
})
|
||
return containDepth % 2 === 1
|
||
})
|
||
}
|
||
|
||
const handleSelectedPolygonSubtract = (draftPath: paper.Path) => {
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
const selectedIds =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
if (selectedIds.length !== 1) return false
|
||
|
||
const selectedId = selectedIds[0]
|
||
const targetShapes = usePaperStore
|
||
.getState()
|
||
.getItemsById(selectedId)
|
||
.filter((item) => item.data?.type === "polygon")
|
||
|
||
const targetShape =
|
||
targetShapes.find((item) => item instanceof paper.CompoundPath) ||
|
||
targetShapes.find(
|
||
(item) => item instanceof paper.Path && !item.data?.isHollowPolygon
|
||
) ||
|
||
targetShapes.find((item) => item instanceof paper.Path)
|
||
|
||
if (!targetShape) return false
|
||
|
||
let clippedDraft: paper.Path | paper.CompoundPath | null =
|
||
draftPath.intersect(usePaperStore.getState().rasterPath!, {
|
||
insert: false,
|
||
trace: false,
|
||
}) as paper.Path | paper.CompoundPath
|
||
|
||
draftPath.remove()
|
||
|
||
const draftHasSegments =
|
||
clippedDraft instanceof paper.CompoundPath
|
||
? (clippedDraft.children as paper.Path[]).some(
|
||
(child) => child.segments?.length
|
||
)
|
||
: clippedDraft.segments?.length
|
||
if (!draftHasSegments) {
|
||
clippedDraft?.remove()
|
||
clippedDraft = null
|
||
return false
|
||
}
|
||
|
||
const targetData = getSafePathData(targetShape.data || {})
|
||
let nextPolygon = (
|
||
targetShape as paper.Path | paper.CompoundPath
|
||
).subtract(clippedDraft) as paper.Path | paper.CompoundPath
|
||
clippedDraft?.remove()
|
||
clippedDraft = null
|
||
targetShape.remove()
|
||
|
||
const points: any[] = []
|
||
const hollowPoints: any[] = []
|
||
let lastOuterContourPoints: paper.Point[] = []
|
||
const nextPaths =
|
||
nextPolygon instanceof paper.CompoundPath
|
||
? (nextPolygon.children as paper.Path[])
|
||
: ([nextPolygon] as paper.Path[])
|
||
const nextPathSegments = nextPaths.map((path) => {
|
||
if (!path.segments?.length) return null
|
||
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||
return pathData?.[1]?.segments || null
|
||
})
|
||
const hollowFlags = getHollowFlagsByContainment(
|
||
nextPathSegments.map((segments) => segments || [])
|
||
)
|
||
|
||
if (nextPolygon instanceof paper.CompoundPath) {
|
||
nextPolygon.data = Object.assign({}, targetData)
|
||
}
|
||
nextPaths.forEach((path, index) => {
|
||
const segments = nextPathSegments[index]
|
||
if (!segments?.length) return
|
||
const isHollowPolygon = !!hollowFlags[index]
|
||
path.data = Object.assign({}, targetData, {
|
||
isHollowPolygon,
|
||
})
|
||
if (isHollowPolygon) {
|
||
hollowPoints.push(segments)
|
||
} else {
|
||
points.push(segments)
|
||
lastOuterContourPoints = getContinueContourPoints(segments)
|
||
}
|
||
})
|
||
|
||
const operationId = (
|
||
targetData.operationId ||
|
||
useTopToolsStore.getState().activeOperation ||
|
||
""
|
||
).toString()
|
||
if (!operationId) {
|
||
nextPolygon.remove()
|
||
return false
|
||
}
|
||
const prevDetail = safeClone(
|
||
useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(activeImage, operationId, selectedId) ||
|
||
initialDetail
|
||
)
|
||
|
||
if (points.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(activeImage, operationId, [
|
||
selectedId,
|
||
points,
|
||
prevDetail,
|
||
hollowPoints,
|
||
])
|
||
setContinueDraftHint(
|
||
{
|
||
imageId: activeImage,
|
||
operationId,
|
||
objectId: selectedId,
|
||
},
|
||
points.length - 1,
|
||
lastOuterContourPoints
|
||
)
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(activeImage, "ADD", selectedId)
|
||
} else {
|
||
nextPolygon.remove()
|
||
useLabelStore
|
||
.getState()
|
||
.deleteOperation(activeImage, operationId, selectedId)
|
||
useObjectStore.getState().updateSelectedPath(activeImage, "ADD")
|
||
const nextLabel = safeClone(useLabelStore.getState().label)
|
||
useLabelStore.getState().setLabel(nextLabel)
|
||
useLabelStore.getState().pushStateStack(nextLabel)
|
||
}
|
||
|
||
forceUpdate()
|
||
return true
|
||
}
|
||
|
||
const handleSelectedPolygonAdd = (draftPath: paper.Path) => {
|
||
const activeImage = useBottomToolsStore.getState().activeImage
|
||
const selectedIds =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
if (selectedIds.length !== 1) return false
|
||
|
||
const selectedId = selectedIds[0]
|
||
const targetShapes = usePaperStore
|
||
.getState()
|
||
.getItemsById(selectedId)
|
||
.filter((item) => item.data?.type === "polygon")
|
||
|
||
const targetShape =
|
||
targetShapes.find((item) => item instanceof paper.CompoundPath) ||
|
||
targetShapes.find(
|
||
(item) => item instanceof paper.Path && !item.data?.isHollowPolygon
|
||
) ||
|
||
targetShapes.find((item) => item instanceof paper.Path)
|
||
|
||
if (!targetShape) return false
|
||
|
||
let clippedDraft: paper.Path | paper.CompoundPath | null =
|
||
draftPath.intersect(usePaperStore.getState().rasterPath!, {
|
||
insert: false,
|
||
trace: false,
|
||
}) as paper.Path | paper.CompoundPath
|
||
|
||
draftPath.remove()
|
||
|
||
const draftHasSegments =
|
||
clippedDraft instanceof paper.CompoundPath
|
||
? (clippedDraft.children as paper.Path[]).some(
|
||
(child) => child.segments?.length
|
||
)
|
||
: clippedDraft.segments?.length
|
||
if (!draftHasSegments) {
|
||
clippedDraft?.remove()
|
||
clippedDraft = null
|
||
return false
|
||
}
|
||
|
||
const targetData = getSafePathData(targetShape.data || {})
|
||
let nextPolygon = (targetShape as paper.Path | paper.CompoundPath).unite(
|
||
clippedDraft
|
||
) as paper.Path | paper.CompoundPath
|
||
clippedDraft?.remove()
|
||
clippedDraft = null
|
||
targetShape.remove()
|
||
|
||
const points: any[] = []
|
||
const hollowPoints: any[] = []
|
||
let lastOuterContourPoints: paper.Point[] = []
|
||
const nextPaths =
|
||
nextPolygon instanceof paper.CompoundPath
|
||
? (nextPolygon.children as paper.Path[])
|
||
: ([nextPolygon] as paper.Path[])
|
||
const nextPathSegments = nextPaths.map((path) => {
|
||
if (!path.segments?.length) return null
|
||
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||
return pathData?.[1]?.segments || null
|
||
})
|
||
const hollowFlags = getHollowFlagsByContainment(
|
||
nextPathSegments.map((segments) => segments || [])
|
||
)
|
||
|
||
if (nextPolygon instanceof paper.CompoundPath) {
|
||
nextPolygon.data = Object.assign({}, targetData)
|
||
}
|
||
nextPaths.forEach((path, index) => {
|
||
const segments = nextPathSegments[index]
|
||
if (!segments?.length) return
|
||
const isHollowPolygon = !!hollowFlags[index]
|
||
path.data = Object.assign({}, targetData, {
|
||
isHollowPolygon,
|
||
})
|
||
if (isHollowPolygon) {
|
||
hollowPoints.push(segments)
|
||
} else {
|
||
points.push(segments)
|
||
lastOuterContourPoints = getContinueContourPoints(segments)
|
||
}
|
||
})
|
||
|
||
const operationId = (
|
||
targetData.operationId ||
|
||
useTopToolsStore.getState().activeOperation ||
|
||
""
|
||
).toString()
|
||
if (!operationId) {
|
||
nextPolygon.remove()
|
||
return false
|
||
}
|
||
const prevDetail = safeClone(
|
||
useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(activeImage, operationId, selectedId) ||
|
||
initialDetail
|
||
)
|
||
|
||
if (points.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setOperation(activeImage, operationId, [
|
||
selectedId,
|
||
points,
|
||
prevDetail,
|
||
hollowPoints,
|
||
])
|
||
setContinueDraftHint(
|
||
{
|
||
imageId: activeImage,
|
||
operationId,
|
||
objectId: selectedId,
|
||
},
|
||
points.length - 1,
|
||
lastOuterContourPoints
|
||
)
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(activeImage, "ADD", selectedId)
|
||
} else {
|
||
nextPolygon.remove()
|
||
return false
|
||
}
|
||
|
||
forceUpdate()
|
||
return true
|
||
}
|
||
|
||
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()
|
||
handleSelectedPolygonSubtract(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()
|
||
handleSelectedPolygonAdd(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 }
|
||
)
|
||
}
|
||
}
|
||
|
||
if (useTopToolsStore.getState().drawOption === "intersect") {
|
||
// 裁剪
|
||
handlePolygonIntersect(polygon as paper.Path)
|
||
handleCircles(polygon)
|
||
} else if (useTopToolsStore.getState().drawOption === "unite") {
|
||
// 融合
|
||
handlePolygonUnite()
|
||
handleCircles(polygon)
|
||
}
|
||
console.log("裁剪之后", polygon)
|
||
|
||
// if (useTopToolsStore.getState().drawOption === "default") {
|
||
// if (useTopToolsStore.getState().pressA)
|
||
// compoundPolygon = polygon.clone();
|
||
// }
|
||
|
||
// 去除超出图片边界的部分
|
||
handlePolygonIntersectRaster(polygon as any)
|
||
// 去除超出图片边界的部分
|
||
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)
|
||
}
|
||
|
||
// 如果该类别存在子属性,则弹出子属性对话框
|
||
if (category && category.sub_attributes.length) {
|
||
const { top, left } = getShowContextMenuPosition(
|
||
e.point,
|
||
category!,
|
||
-30,
|
||
-30
|
||
)
|
||
useOtherToolsStore.getState().setShowContextMenu({
|
||
showContextMenu: true,
|
||
position: { top, left },
|
||
imageId: polygon.data.imageId,
|
||
operationId: polygon.data.operationId,
|
||
id: polygon.data.id,
|
||
})
|
||
}
|
||
|
||
// 设置其为selectedPath
|
||
useObjectStore
|
||
.getState()
|
||
.updateSelectedPath(
|
||
useBottomToolsStore.getState().activeImage,
|
||
"ADD",
|
||
polygon.data.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) => {
|
||
// clear color
|
||
item.fillColor = item?.data?.blankColor
|
||
})
|
||
}
|
||
|
||
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(),
|
||
}))
|
||
},
|
||
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)
|
||
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)
|
||
// 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)
|
||
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)
|
||
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
|
||
let bufferPath: paper.Path = new paper.Path(
|
||
Object.assign(
|
||
{
|
||
parent: usePaperStore.getState().group!,
|
||
segments: [item.point, item.next],
|
||
strokeColor: color, // 设置为透明,以便不可见
|
||
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 []
|
||
}
|
||
},
|
||
}))
|