fix(paper): click area
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { Comment } from "./api/label/typing"
|
||||
import { Project } from "./api/project/typing"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
import dayjs from "dayjs"
|
||||
import paper from "paper"
|
||||
import { DispatchWithoutAction } from "react"
|
||||
import { create } from "zustand"
|
||||
import { usePermissionStore } from "./store/auth"
|
||||
import { Comment } from "./api/label/typing"
|
||||
import { Project } from "./api/project/typing"
|
||||
import {
|
||||
initialDetail,
|
||||
useKeyEventStore,
|
||||
useLabelStore,
|
||||
useObjectStore,
|
||||
} from "./store"
|
||||
import { usePermissionStore } from "./store/auth"
|
||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||
import { useKeyboardStore } from "./useKeyBoardStore"
|
||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||
@@ -319,6 +319,26 @@ const hidePaperContextMenu = () => {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -510,6 +530,8 @@ export const getShowContextMenuPosition = (
|
||||
}
|
||||
|
||||
const DOUBLE_CLICK_TOLERANCE_PX = 8
|
||||
const PAN_DRAG_THRESHOLD_PX = 4
|
||||
const BUFFER_HOVER_STICKY_MS = 180
|
||||
|
||||
const getPaperDoubleClickTolerance = (
|
||||
coordinateSpace: "project" | "local" = "project"
|
||||
@@ -525,6 +547,37 @@ const getPaperDoubleClickTolerance = (
|
||||
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,
|
||||
@@ -668,6 +721,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
let panTool = paperPanTool
|
||||
let lastPoint: paper.Point
|
||||
let isChanged: boolean = false
|
||||
let hasExceededPanDragThreshold = false
|
||||
|
||||
// const handleSelected = (path: paper.Path) => {
|
||||
// // path.blendMode = "subtract";
|
||||
@@ -810,6 +864,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
? baseTolerance / useTopToolsStore.getState().scale
|
||||
: 0
|
||||
|
||||
false && getPanHitTolerance
|
||||
|
||||
const createPanHitOptions = (
|
||||
tolerance: number,
|
||||
match: (hit: paper.HitResult) => boolean
|
||||
@@ -827,13 +883,39 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return null
|
||||
|
||||
const supportHit = group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(getPanHitTolerance(), (hit) =>
|
||||
["pathCircle", "pathBuff"].includes(hit.item.data?.type || "")
|
||||
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
|
||||
? hoveredPathCircleInfo
|
||||
: null
|
||||
if (hoveredCircleInfo) {
|
||||
const pointHit = group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(0, (hit) => {
|
||||
return (
|
||||
hit.item.data?.type === "pathCircle" &&
|
||||
hit.item.data?.id === hoveredCircleInfo.id &&
|
||||
hit.item.data?.index === hoveredCircleInfo.index
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
if (supportHit) return supportHit
|
||||
if (pointHit) return pointHit
|
||||
}
|
||||
|
||||
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||||
? hoveredBufferPathInfo
|
||||
: null
|
||||
if (hoveredBufferInfo) {
|
||||
const edgeHit = group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(0, (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,
|
||||
@@ -886,9 +968,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
point,
|
||||
"local"
|
||||
)
|
||||
hasExceededPanDragThreshold = false
|
||||
|
||||
let hitResult = resolvePanHitResult(e.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
|
||||
@@ -999,8 +1088,18 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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 =
|
||||
hitId && selectedIds.length === 1 && selectedIds[0] === hitId
|
||||
keepHoveredPointSelection ||
|
||||
keepHoveredEdgeSelection ||
|
||||
(hitId && selectedIds.length === 1 && selectedIds[0] === hitId)
|
||||
|
||||
if (!keepCurrentSelection) {
|
||||
clearCurrentSelection()
|
||||
@@ -1242,6 +1341,56 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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)
|
||||
let circles = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: currentPath.data.id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
|
||||
currentPath.segments.forEach((segment, index) => {
|
||||
let findIndex = circles.findIndex((circle) => {
|
||||
return circle.contains(segment.point)
|
||||
})
|
||||
circles[index].data = Object.assign({}, circles[index]?.data, {
|
||||
text: findIndex + 1,
|
||||
})
|
||||
})
|
||||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||||
usePaperSupportStore.getState().handleCircles(currentPath)
|
||||
}
|
||||
}
|
||||
} else if (hitResult && hitResult.type === "fill") {
|
||||
if (
|
||||
hitResult &&
|
||||
@@ -1276,28 +1425,30 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
// doubleclick
|
||||
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) {
|
||||
if (childHitResult.location.point) {
|
||||
item.insert(hitIndex + 1, childHitResult.location.point)
|
||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||
usePaperSupportStore.getState().handleCircles(item)
|
||||
isChanged = true
|
||||
panMode = "stroke"
|
||||
hitIndex = hitIndex + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
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)
|
||||
activePath?.data?.type === "point" &&
|
||||
@@ -1380,8 +1531,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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.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()
|
||||
@@ -1401,8 +1560,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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)
|
||||
@@ -1551,6 +1718,20 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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 = {
|
||||
@@ -1753,6 +1934,10 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
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
|
||||
|
||||
@@ -5233,12 +5418,27 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
)
|
||||
circle.scale(newScale / currentScaling)
|
||||
circle.onMouseEnter = (_e: any) => {
|
||||
hoveredPathCircleInfo = {
|
||||
id,
|
||||
index,
|
||||
item: circle,
|
||||
}
|
||||
clearHoveredBufferPathHighlight()
|
||||
circle.strokeWidth = 4
|
||||
circle.fillColor = new paper.Color("#FFF")
|
||||
if (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
|
||||
if (usePaperStore.getState().el) {
|
||||
usePaperStore.getState().el!.style.cursor = "default"
|
||||
}
|
||||
}
|
||||
return circle
|
||||
},
|
||||
@@ -5251,8 +5451,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
segments: [item.point, item.next],
|
||||
strokeColor: color, // 设置为透明,以便不可见
|
||||
strokeScaling: false,
|
||||
strokeWidth: 4, // 设置一个较大的strokeWidth作为缓冲区
|
||||
strokeCap: "butt",
|
||||
strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
|
||||
strokeCap: "round",
|
||||
onSelect: function (event: { stopPropagation: () => void }) {
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
},
|
||||
@@ -5266,10 +5466,54 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
// 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
|
||||
) {
|
||||
usePaperStore.getState().el!.style.cursor = "default"
|
||||
}
|
||||
}
|
||||
const scheduleClear = () => {
|
||||
cancelScheduledClear()
|
||||
clearTimer = setTimeout(() => {
|
||||
clearHighlight()
|
||||
}, BUFFER_HOVER_STICKY_MS)
|
||||
}
|
||||
// 鼠标移入时高亮显示
|
||||
bufferPath.onMouseEnter = function (event: {
|
||||
stopPropagation: () => void
|
||||
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(
|
||||
@@ -5280,27 +5524,33 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
bufferPath.segments[1].point,
|
||||
],
|
||||
strokeColor: "white", // 初始颜色与原始路径相同
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 3,
|
||||
strokeScaling: false,
|
||||
strokeCap: "butt",
|
||||
strokeCap: "round",
|
||||
},
|
||||
{
|
||||
data: { index, type: "pathBuff" },
|
||||
data: { index, type: "pathBuffHover" },
|
||||
}
|
||||
)
|
||||
)
|
||||
// edge.bringToFront();
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
if (usePaperStore.getState().el) {
|
||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
||||
}
|
||||
}
|
||||
event.stopPropagation?.() // 阻止事件冒泡
|
||||
}
|
||||
|
||||
// 鼠标移出时取消高亮
|
||||
bufferPath.onMouseLeave = function (event: {
|
||||
stopPropagation: () => void
|
||||
stopPropagation?: () => void
|
||||
}) {
|
||||
edge?.remove()
|
||||
edge = null
|
||||
event.stopPropagation() // 阻止事件冒泡
|
||||
if (hoveredBufferPathInfo?.path === bufferPath) {
|
||||
hoveredBufferPathInfo.scheduleClear()
|
||||
} else {
|
||||
scheduleClear()
|
||||
}
|
||||
event.stopPropagation?.() // 阻止事件冒泡
|
||||
}
|
||||
return bufferPath
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user