feat(shortcut): ,/
This commit is contained in:
@@ -12,6 +12,7 @@ import ScaleComponent from "./components/ScaleComponent"
|
|||||||
|
|
||||||
import { Box, Button, Flex, LoadingOverlay, Stack } from "@mantine/core"
|
import { Box, Button, Flex, LoadingOverlay, Stack } from "@mantine/core"
|
||||||
import { notifications, showNotification } from "@mantine/notifications"
|
import { notifications, showNotification } from "@mantine/notifications"
|
||||||
|
import paper from "paper"
|
||||||
import { getLabelResult, getServerImage } from "./api/label"
|
import { getLabelResult, getServerImage } from "./api/label"
|
||||||
import {
|
import {
|
||||||
Comment,
|
Comment,
|
||||||
@@ -56,8 +57,8 @@ import {
|
|||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { findGroupKey } from "./util"
|
import { findGroupKey } from "./util"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
|
||||||
import { labelTypeMap } from "./utils/constants"
|
import { labelTypeMap } from "./utils/constants"
|
||||||
|
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
||||||
|
|
||||||
// Splitter component - will need custom implementation or alternative
|
// Splitter component - will need custom implementation or alternative
|
||||||
|
|
||||||
@@ -1511,6 +1512,395 @@ const LabelPage = ({
|
|||||||
)
|
)
|
||||||
}, [resolveSelectedObjectContext])
|
}, [resolveSelectedObjectContext])
|
||||||
|
|
||||||
|
const rerenderActiveImageByLabel = useCallback(
|
||||||
|
(
|
||||||
|
imageId: string,
|
||||||
|
imageLabel: Map<string, [number, any[], any, any[]][]>,
|
||||||
|
selectedIds: number[]
|
||||||
|
) => {
|
||||||
|
const paperGroup = usePaperStore.getState().group
|
||||||
|
if (paperGroup) {
|
||||||
|
paperGroup
|
||||||
|
.getItems({})
|
||||||
|
.filter((item) => item.data.id)
|
||||||
|
.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [categoryId] of imageLabel.entries()) {
|
||||||
|
paperContainerRef.current?.renderPolygons?.(categoryId, imageLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useObjectStore.setState((state) => ({
|
||||||
|
...state,
|
||||||
|
selectedPath: {
|
||||||
|
...state.selectedPath,
|
||||||
|
[imageId]: selectedIds,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeObjectFromGroupMap = useCallback(
|
||||||
|
(imageId: string, id: number) => {
|
||||||
|
const nextPathGroupMap = safeClone(
|
||||||
|
useRightToolsStore.getState().pathGroupMap
|
||||||
|
)
|
||||||
|
const imageGroupMap = nextPathGroupMap.get(imageId)
|
||||||
|
if (!imageGroupMap) return
|
||||||
|
|
||||||
|
for (let [groupId, pathIds] of imageGroupMap.entries()) {
|
||||||
|
const nextPathIds = pathIds.filter((pathId) => pathId !== id)
|
||||||
|
if (nextPathIds.length > 1) {
|
||||||
|
imageGroupMap.set(groupId, nextPathIds)
|
||||||
|
} else {
|
||||||
|
imageGroupMap.delete(groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageGroupMap.size) nextPathGroupMap.delete(imageId)
|
||||||
|
useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasShapeSegments = useCallback((shape: any) => {
|
||||||
|
if (!shape) return false
|
||||||
|
if (shape instanceof paper.CompoundPath) {
|
||||||
|
return (shape.children as paper.Path[]).some(
|
||||||
|
(child) => child.segments?.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (shape instanceof paper.Path) {
|
||||||
|
return !!shape.segments?.length
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createWorkingPolygonShape = useCallback((items: any[]) => {
|
||||||
|
const compound = items.find((item) => item instanceof paper.CompoundPath)
|
||||||
|
if (compound) {
|
||||||
|
return (compound as paper.CompoundPath).clone({ insert: false }) as
|
||||||
|
| paper.Path
|
||||||
|
| paper.CompoundPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = items.filter(
|
||||||
|
(item) => item instanceof paper.Path
|
||||||
|
) as paper.Path[]
|
||||||
|
if (!paths.length) return null
|
||||||
|
if (paths.length === 1) {
|
||||||
|
return paths[0].clone({ insert: false }) as
|
||||||
|
| paper.Path
|
||||||
|
| paper.CompoundPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = new paper.CompoundPath({ insert: false })
|
||||||
|
paths.forEach((path) => {
|
||||||
|
merged.addChild(path.clone({ insert: false }))
|
||||||
|
})
|
||||||
|
return merged as paper.Path | paper.CompoundPath
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const collectPolygonPoints = useCallback(
|
||||||
|
(shape: paper.Path | paper.CompoundPath) => {
|
||||||
|
const polygonPoints: any[] = []
|
||||||
|
const polygonHollowPoints: any[] = []
|
||||||
|
|
||||||
|
const collectPath = (path: paper.Path) => {
|
||||||
|
if (!path.segments?.length) return
|
||||||
|
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||||||
|
if (path.clockwise) {
|
||||||
|
polygonPoints.push(pathData[1]?.segments)
|
||||||
|
} else {
|
||||||
|
polygonHollowPoints.push(pathData[1]?.segments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape instanceof paper.CompoundPath) {
|
||||||
|
;(shape.children as paper.Path[]).forEach((path) => collectPath(path))
|
||||||
|
} else {
|
||||||
|
collectPath(shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { polygonPoints, polygonHollowPoints }
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const applySelectedPolygonOverlap = useCallback(
|
||||||
|
(mode: "subtract" | "unite") => {
|
||||||
|
const selectedContext = resolveSelectedObjectContext()
|
||||||
|
if (!selectedContext) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中单个对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedContext.objectType !== "polygon") {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message:
|
||||||
|
mode === "subtract"
|
||||||
|
? "镂空快捷键仅支持 polygon 类型对象"
|
||||||
|
: "融合快捷键仅支持 polygon/brush 类型对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
const targetItems = group
|
||||||
|
.getItems({ data: { id: selectedContext.objectId } })
|
||||||
|
.filter((item) => item.data?.type === "polygon")
|
||||||
|
if (!targetItems.length) return
|
||||||
|
|
||||||
|
let workingShape = createWorkingPolygonShape(targetItems)
|
||||||
|
if (!workingShape || !hasShapeSegments(workingShape)) return
|
||||||
|
|
||||||
|
const otherPolygonPaths: paper.Path[] = []
|
||||||
|
group.children.forEach((item: any) => {
|
||||||
|
if (
|
||||||
|
!item.visible ||
|
||||||
|
!item.data?.id ||
|
||||||
|
item.data.id === selectedContext.objectId ||
|
||||||
|
item.data.type !== "polygon"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (item instanceof paper.Path) {
|
||||||
|
if (item.segments?.length) otherPolygonPaths.push(item)
|
||||||
|
} else if (item instanceof paper.CompoundPath) {
|
||||||
|
;(item.children as paper.Path[]).forEach((child) => {
|
||||||
|
if (child.segments?.length) otherPolygonPaths.push(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let changed = false
|
||||||
|
|
||||||
|
for (const eachPath of otherPolygonPaths) {
|
||||||
|
if (!workingShape || !hasShapeSegments(workingShape)) break
|
||||||
|
|
||||||
|
const eachPathClone = eachPath.clone({ insert: false }) as paper.Path
|
||||||
|
const intersectionPath = (workingShape as any).intersect(
|
||||||
|
eachPathClone,
|
||||||
|
{
|
||||||
|
insert: false,
|
||||||
|
}
|
||||||
|
) as paper.Path | paper.CompoundPath
|
||||||
|
const hasIntersection = hasShapeSegments(intersectionPath)
|
||||||
|
intersectionPath?.remove()
|
||||||
|
if (!hasIntersection) {
|
||||||
|
eachPathClone.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextShape: paper.Path | paper.CompoundPath =
|
||||||
|
mode === "subtract"
|
||||||
|
? ((workingShape as any).subtract(eachPathClone, {
|
||||||
|
insert: false,
|
||||||
|
}) as paper.Path | paper.CompoundPath)
|
||||||
|
: ((workingShape as any).unite(eachPathClone, {
|
||||||
|
insert: false,
|
||||||
|
}) as paper.Path | paper.CompoundPath)
|
||||||
|
eachPathClone.remove()
|
||||||
|
workingShape?.remove()
|
||||||
|
workingShape = nextShape
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
workingShape?.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!workingShape) return
|
||||||
|
|
||||||
|
const raster = usePaperStore.getState().rasterPath
|
||||||
|
if (raster && hasShapeSegments(workingShape)) {
|
||||||
|
const clippedShape = (workingShape as any).intersect(raster, {
|
||||||
|
insert: false,
|
||||||
|
trace: false,
|
||||||
|
}) as paper.Path | paper.CompoundPath
|
||||||
|
workingShape?.remove()
|
||||||
|
workingShape = clippedShape
|
||||||
|
}
|
||||||
|
if (!workingShape) return
|
||||||
|
|
||||||
|
const { polygonPoints, polygonHollowPoints } =
|
||||||
|
collectPolygonPoints(workingShape)
|
||||||
|
workingShape?.remove()
|
||||||
|
|
||||||
|
const nextLabel = safeClone(useLabelStore.getState().label)
|
||||||
|
const imageLabel = nextLabel.get(selectedContext.imageId)
|
||||||
|
if (!imageLabel) return
|
||||||
|
const operationPaths = safeClone(
|
||||||
|
imageLabel.get(selectedContext.operationId) || []
|
||||||
|
)
|
||||||
|
const selectedIndex = operationPaths.findIndex(
|
||||||
|
(item) => item[0] === selectedContext.objectId
|
||||||
|
)
|
||||||
|
if (selectedIndex === -1) return
|
||||||
|
|
||||||
|
const currentTuple = operationPaths[selectedIndex]
|
||||||
|
if (polygonPoints.length) {
|
||||||
|
operationPaths[selectedIndex] = [
|
||||||
|
selectedContext.objectId,
|
||||||
|
polygonPoints,
|
||||||
|
safeClone(currentTuple?.[2] || {}),
|
||||||
|
polygonHollowPoints,
|
||||||
|
]
|
||||||
|
imageLabel.set(selectedContext.operationId, operationPaths)
|
||||||
|
} else {
|
||||||
|
imageLabel.set(
|
||||||
|
selectedContext.operationId,
|
||||||
|
operationPaths.filter((item) => item[0] !== selectedContext.objectId)
|
||||||
|
)
|
||||||
|
removeObjectFromGroupMap(
|
||||||
|
selectedContext.imageId,
|
||||||
|
selectedContext.objectId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLabelStore.getState().setLabel(nextLabel)
|
||||||
|
useLabelStore.getState().pushStateStack(nextLabel)
|
||||||
|
|
||||||
|
rerenderActiveImageByLabel(
|
||||||
|
selectedContext.imageId,
|
||||||
|
imageLabel,
|
||||||
|
polygonPoints.length ? [selectedContext.objectId] : []
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
collectPolygonPoints,
|
||||||
|
createWorkingPolygonShape,
|
||||||
|
hasShapeSegments,
|
||||||
|
removeObjectFromGroupMap,
|
||||||
|
rerenderActiveImageByLabel,
|
||||||
|
resolveSelectedObjectContext,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const applySelectedBrushIntersectUnite = useCallback(() => {
|
||||||
|
const selectedContext = resolveSelectedObjectContext()
|
||||||
|
if (!selectedContext) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中单个对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedContext.objectType !== "brush") {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "融合快捷键仅支持 polygon/brush 类型对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
const selectedBrushPaths = group
|
||||||
|
.getItems({ data: { id: selectedContext.objectId } })
|
||||||
|
.filter((item) => item.data?.type === "brush")
|
||||||
|
.flatMap((item) => {
|
||||||
|
if (item instanceof paper.CompoundPath)
|
||||||
|
return item.children as paper.Path[]
|
||||||
|
if (item instanceof paper.Path) return [item]
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
.filter((path) => path.segments?.length)
|
||||||
|
|
||||||
|
if (!selectedBrushPaths.length) return
|
||||||
|
|
||||||
|
const otherBrushPathMap = new Map<number, paper.Path[]>()
|
||||||
|
group.children.forEach((item: any) => {
|
||||||
|
if (
|
||||||
|
!item.visible ||
|
||||||
|
!item.data?.id ||
|
||||||
|
item.data.id === selectedContext.objectId ||
|
||||||
|
item.data.type !== "brush"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
const targetId = Number(item.data.id)
|
||||||
|
const currentPaths = otherBrushPathMap.get(targetId) || []
|
||||||
|
if (item instanceof paper.CompoundPath) {
|
||||||
|
otherBrushPathMap.set(targetId, [
|
||||||
|
...currentPaths,
|
||||||
|
...(item.children as paper.Path[]),
|
||||||
|
])
|
||||||
|
} else if (item instanceof paper.Path) {
|
||||||
|
otherBrushPathMap.set(targetId, [...currentPaths, item])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const intersectingIds = new Set<number>()
|
||||||
|
for (const [otherId, paths] of otherBrushPathMap.entries()) {
|
||||||
|
const hasIntersection = paths.some((otherPath) => {
|
||||||
|
return selectedBrushPaths.some((selectedPath) => {
|
||||||
|
if (!selectedPath.bounds.intersects(otherPath.bounds)) return false
|
||||||
|
return selectedPath.getIntersections(otherPath).length > 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (hasIntersection) intersectingIds.add(otherId)
|
||||||
|
}
|
||||||
|
if (!intersectingIds.size) return
|
||||||
|
|
||||||
|
const nextLabel = safeClone(useLabelStore.getState().label)
|
||||||
|
const imageLabel = nextLabel.get(selectedContext.imageId)
|
||||||
|
if (!imageLabel) return
|
||||||
|
|
||||||
|
const selectedOperationPaths = safeClone(
|
||||||
|
imageLabel.get(selectedContext.operationId) || []
|
||||||
|
)
|
||||||
|
const selectedIndex = selectedOperationPaths.findIndex(
|
||||||
|
(item) => item[0] === selectedContext.objectId
|
||||||
|
)
|
||||||
|
if (selectedIndex === -1) return
|
||||||
|
|
||||||
|
const selectedTuple = selectedOperationPaths[selectedIndex]
|
||||||
|
const mergedPoints = safeClone(selectedTuple?.[1] || [])
|
||||||
|
const pointSet = new Set(
|
||||||
|
mergedPoints.map((pointItem) => JSON.stringify(pointItem))
|
||||||
|
)
|
||||||
|
|
||||||
|
let appendedCount = 0
|
||||||
|
imageLabel.forEach((paths) => {
|
||||||
|
paths.forEach((item) => {
|
||||||
|
if (!intersectingIds.has(item[0])) return
|
||||||
|
;(item[1] || []).forEach((pathPoints: any) => {
|
||||||
|
const key = JSON.stringify(pathPoints)
|
||||||
|
if (pointSet.has(key)) return
|
||||||
|
pointSet.add(key)
|
||||||
|
mergedPoints.push(safeClone(pathPoints))
|
||||||
|
appendedCount++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!appendedCount) return
|
||||||
|
|
||||||
|
selectedOperationPaths[selectedIndex] = [
|
||||||
|
selectedContext.objectId,
|
||||||
|
mergedPoints,
|
||||||
|
safeClone(selectedTuple?.[2] || {}),
|
||||||
|
safeClone(selectedTuple?.[3] || []),
|
||||||
|
]
|
||||||
|
imageLabel.set(selectedContext.operationId, selectedOperationPaths)
|
||||||
|
|
||||||
|
useLabelStore.getState().setLabel(nextLabel)
|
||||||
|
useLabelStore.getState().pushStateStack(nextLabel)
|
||||||
|
|
||||||
|
rerenderActiveImageByLabel(selectedContext.imageId, imageLabel, [
|
||||||
|
selectedContext.objectId,
|
||||||
|
])
|
||||||
|
}, [rerenderActiveImageByLabel, resolveSelectedObjectContext])
|
||||||
|
|
||||||
const handleMergeSelectedObjects = useCallback(() => {
|
const handleMergeSelectedObjects = useCallback(() => {
|
||||||
const activeImage = useBottomToolsStore.getState().activeImage
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
const selectedIds =
|
const selectedIds =
|
||||||
@@ -1655,7 +2045,7 @@ const LabelPage = ({
|
|||||||
const mode = usePaperStore.getState().mode
|
const mode = usePaperStore.getState().mode
|
||||||
const lowerKey = e.key.toLowerCase()
|
const lowerKey = e.key.toLowerCase()
|
||||||
|
|
||||||
if (e.repeat && ["a", "d", "h"].includes(lowerKey)) return
|
if (e.repeat && ["a", "d", "h", ",", "."].includes(lowerKey)) return
|
||||||
|
|
||||||
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -1675,6 +2065,47 @@ const LabelPage = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.altKey &&
|
||||||
|
[",", ","].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
applySelectedPolygonOverlap("subtract")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.metaKey &&
|
||||||
|
!e.altKey &&
|
||||||
|
[".", "。"].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
const selectedContext = resolveSelectedObjectContext()
|
||||||
|
if (!selectedContext) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中单个对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedContext.objectType === "polygon") {
|
||||||
|
applySelectedPolygonOverlap("unite")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (selectedContext.objectType === "brush") {
|
||||||
|
applySelectedBrushIntersectUnite()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "融合快捷键仅支持 polygon/brush 类型对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.altKey && e.key.toLowerCase() === "x") {
|
if (e.altKey && e.key.toLowerCase() === "x") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!isView) topRef.current?.commitTaskByActionKey?.()
|
if (!isView) topRef.current?.commitTaskByActionKey?.()
|
||||||
@@ -1896,6 +2327,8 @@ const LabelPage = ({
|
|||||||
element?.removeEventListener("mousemove", mousemove)
|
element?.removeEventListener("mousemove", mousemove)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
applySelectedBrushIntersectUnite,
|
||||||
|
applySelectedPolygonOverlap,
|
||||||
editMode,
|
editMode,
|
||||||
enterSelectedObjectDrawMode,
|
enterSelectedObjectDrawMode,
|
||||||
focusInput,
|
focusInput,
|
||||||
@@ -1911,6 +2344,7 @@ const LabelPage = ({
|
|||||||
setPressA,
|
setPressA,
|
||||||
setScale,
|
setScale,
|
||||||
setShift,
|
setShift,
|
||||||
|
resolveSelectedObjectContext,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2866,6 +2866,96 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
return nextData
|
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 handleSelectedPolygonSubtract = (draftPath: paper.Path) => {
|
||||||
const activeImage = useBottomToolsStore.getState().activeImage
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
const selectedIds =
|
const selectedIds =
|
||||||
@@ -2915,36 +3005,37 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
clippedDraft = null
|
clippedDraft = null
|
||||||
targetShape.remove()
|
targetShape.remove()
|
||||||
|
|
||||||
if (nextPolygon instanceof paper.CompoundPath) {
|
|
||||||
nextPolygon.data = Object.assign({}, targetData)
|
|
||||||
;(nextPolygon.children as paper.Path[]).forEach((child) => {
|
|
||||||
child.data = Object.assign({}, targetData, {
|
|
||||||
isHollowPolygon: !child.clockwise,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
nextPolygon.data = Object.assign({}, targetData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const points: any[] = []
|
const points: any[] = []
|
||||||
const hollowPoints: any[] = []
|
const hollowPoints: any[] = []
|
||||||
const collectPathPoints = (path: paper.Path) => {
|
const nextPaths =
|
||||||
if (!path.segments?.length) return
|
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 }))
|
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||||||
if (path.clockwise) {
|
return pathData?.[1]?.segments || null
|
||||||
points.push(pathData[1]?.segments)
|
})
|
||||||
} else {
|
const hollowFlags = getHollowFlagsByContainment(
|
||||||
hollowPoints.push(pathData[1]?.segments)
|
nextPathSegments.map((segments) => segments || [])
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if (nextPolygon instanceof paper.CompoundPath) {
|
if (nextPolygon instanceof paper.CompoundPath) {
|
||||||
;(nextPolygon.children as paper.Path[]).forEach((child) => {
|
nextPolygon.data = Object.assign({}, targetData)
|
||||||
collectPathPoints(child)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
collectPathPoints(nextPolygon)
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const operationId = (
|
const operationId = (
|
||||||
targetData.operationId ||
|
targetData.operationId ||
|
||||||
|
|||||||
Reference in New Issue
Block a user