fix(paper): click area

This commit is contained in:
zhangheng
2026-04-09 15:49:31 +08:00
parent f0dabe849e
commit b10fd46474

View File

@@ -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
},