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 { notifications } from "@mantine/notifications"
import dayjs from "dayjs" import dayjs from "dayjs"
import paper from "paper" import paper from "paper"
import { DispatchWithoutAction } from "react" import { DispatchWithoutAction } from "react"
import { create } from "zustand" import { create } from "zustand"
import { usePermissionStore } from "./store/auth" import { Comment } from "./api/label/typing"
import { Project } from "./api/project/typing"
import { import {
initialDetail, initialDetail,
useKeyEventStore, useKeyEventStore,
useLabelStore, useLabelStore,
useObjectStore, useObjectStore,
} from "./store" } from "./store"
import { usePermissionStore } from "./store/auth"
import { useBottomToolsStore } from "./useBottomToolsStore" import { useBottomToolsStore } from "./useBottomToolsStore"
import { useKeyboardStore } from "./useKeyBoardStore" import { useKeyboardStore } from "./useKeyBoardStore"
import { useOtherToolsStore } from "./useOtherToolsStore" import { useOtherToolsStore } from "./useOtherToolsStore"
@@ -319,6 +319,26 @@ const hidePaperContextMenu = () => {
} }
let isMiddleMousePanning = false 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) => { const startMiddleMousePan = (event?: MouseEvent) => {
if (!event || event.button !== 1) return false if (!event || event.button !== 1) return false
@@ -510,6 +530,8 @@ export const getShowContextMenuPosition = (
} }
const DOUBLE_CLICK_TOLERANCE_PX = 8 const DOUBLE_CLICK_TOLERANCE_PX = 8
const PAN_DRAG_THRESHOLD_PX = 4
const BUFFER_HOVER_STICKY_MS = 180
const getPaperDoubleClickTolerance = ( const getPaperDoubleClickTolerance = (
coordinateSpace: "project" | "local" = "project" coordinateSpace: "project" | "local" = "project"
@@ -525,6 +547,37 @@ const getPaperDoubleClickTolerance = (
return projectTolerance 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 = ( const isPaperDoubleClick = (
event: MouseEvent, event: MouseEvent,
lastPoint: paper.Point | null | undefined, lastPoint: paper.Point | null | undefined,
@@ -668,6 +721,7 @@ export const usePaperStore = create<PaperState>((set) => ({
let panTool = paperPanTool let panTool = paperPanTool
let lastPoint: paper.Point let lastPoint: paper.Point
let isChanged: boolean = false let isChanged: boolean = false
let hasExceededPanDragThreshold = false
// const handleSelected = (path: paper.Path) => { // const handleSelected = (path: paper.Path) => {
// // path.blendMode = "subtract"; // // path.blendMode = "subtract";
@@ -810,6 +864,8 @@ export const usePaperStore = create<PaperState>((set) => ({
? baseTolerance / useTopToolsStore.getState().scale ? baseTolerance / useTopToolsStore.getState().scale
: 0 : 0
false && getPanHitTolerance
const createPanHitOptions = ( const createPanHitOptions = (
tolerance: number, tolerance: number,
match: (hit: paper.HitResult) => boolean match: (hit: paper.HitResult) => boolean
@@ -827,13 +883,39 @@ export const usePaperStore = create<PaperState>((set) => ({
const group = usePaperStore.getState().group const group = usePaperStore.getState().group
if (!group) return null if (!group) return null
const supportHit = group.hitTest( const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
? hoveredPathCircleInfo
: null
if (hoveredCircleInfo) {
const pointHit = group.hitTest(
projectPoint, projectPoint,
createPanHitOptions(getPanHitTolerance(), (hit) => createPanHitOptions(0, (hit) => {
["pathCircle", "pathBuff"].includes(hit.item.data?.type || "") 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( return group.hitTest(
projectPoint, projectPoint,
@@ -886,9 +968,16 @@ export const usePaperStore = create<PaperState>((set) => ({
point, point,
"local" "local"
) )
hasExceededPanDragThreshold = false
let hitResult = resolvePanHitResult(e.point) let hitResult = resolvePanHitResult(e.point)
console.log("hitResult", hitResult) console.log("hitResult", hitResult)
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
? hoveredBufferPathInfo
: null
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
? hoveredPathCircleInfo
: null
let lastTagDataId = -1 let lastTagDataId = -1
if (activePath?.data.id) { if (activePath?.data.id) {
lastTagDataId = activePath.data.id lastTagDataId = activePath.data.id
@@ -999,8 +1088,18 @@ export const usePaperStore = create<PaperState>((set) => ({
useBottomToolsStore.getState().activeImage useBottomToolsStore.getState().activeImage
] || [] ] || []
const hitId = hitResult?.item?.data?.id 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 = const keepCurrentSelection =
hitId && selectedIds.length === 1 && selectedIds[0] === hitId keepHoveredPointSelection ||
keepHoveredEdgeSelection ||
(hitId && selectedIds.length === 1 && selectedIds[0] === hitId)
if (!keepCurrentSelection) { if (!keepCurrentSelection) {
clearCurrentSelection() clearCurrentSelection()
@@ -1242,6 +1341,56 @@ export const usePaperStore = create<PaperState>((set) => ({
id: data.id, 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") { } else if (hitResult && hitResult.type === "fill") {
if ( if (
hitResult && hitResult &&
@@ -1276,28 +1425,30 @@ export const usePaperStore = create<PaperState>((set) => ({
// doubleclick // doubleclick
if (isDoubleClick) { if (isDoubleClick) {
if (activePath instanceof paper.CompoundPath) { if (activePath instanceof paper.CompoundPath) {
;(activePath.children as paper.Path[]).forEach((item) => { const selectedChild =
const childHitResult = item.hitTest(point, { (activePath.children as paper.Path[]).find(
segments: true, (item) => item.data.selected
stroke: true, ) ||
curves: true, (activePath.children as paper.Path[]).reduce(
fill: true, (nearest, item) => {
guide: false, if (!nearest) return item
tolerance: usePaperStore.getState().paperScope return item.getNearestPoint(point).getDistance(point) <
? 8 / usePaperStore.getState().paperScope!.view.zoom nearest.getNearestPoint(point).getDistance(point)
: 0, ? item
}) : nearest
if (childHitResult) { },
if (childHitResult.location.point) { null as paper.Path | null
item.insert(hitIndex + 1, childHitResult.location.point) )
usePaperSupportStore.getState().handleBufferPaths(item)
usePaperSupportStore.getState().handleCircles(item) if (selectedChild) {
const insertPoint = selectedChild.getNearestPoint(point)
selectedChild.insert(hitIndex + 1, insertPoint)
usePaperSupportStore.getState().handleBufferPaths(selectedChild)
usePaperSupportStore.getState().handleCircles(selectedChild)
isChanged = true isChanged = true
panMode = "stroke" panMode = "stroke"
hitIndex = hitIndex + 1 hitIndex = hitIndex + 1
} }
}
})
} else { } else {
activePath.insert(hitIndex + 1, hitResult.location.point) activePath.insert(hitIndex + 1, hitResult.location.point)
activePath?.data?.type === "point" && activePath?.data?.type === "point" &&
@@ -1380,8 +1531,16 @@ export const usePaperStore = create<PaperState>((set) => ({
let checkLength = 3 let checkLength = 3
if (item?.data?.type === "brush") checkLength = 2 if (item?.data?.type === "brush") checkLength = 2
if (item?.data?.type === "point") checkLength = 1 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 return
}
;(childHitResult.item as paper.Path).removeSegment(hitIndex) ;(childHitResult.item as paper.Path).removeSegment(hitIndex)
usePaperSupportStore usePaperSupportStore
.getState() .getState()
@@ -1401,8 +1560,16 @@ export const usePaperStore = create<PaperState>((set) => ({
if ( if (
activePath.segments && activePath.segments &&
activePath.segments.length <= checkLength activePath.segments.length <= checkLength
) ) {
if (
activePath?.data?.type === "polygon" ||
activePath?.data?.type === "brush" ||
activePath?.data?.type === "point"
) {
showDeletePointLimitNotification(activePath.data.type)
}
return return
}
activePath.removeSegment(hitIndex) activePath.removeSegment(hitIndex)
usePaperSupportStore.getState().handleBufferPaths(activePath) usePaperSupportStore.getState().handleBufferPaths(activePath)
@@ -1551,6 +1718,20 @@ export const usePaperStore = create<PaperState>((set) => ({
point = usePaperStore.getState().group!.globalToLocal(e.point) 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) => { let movePath = (path: paper.Path) => {
const currentGroupScaling = usePaperStore.getState().group!.scaling const currentGroupScaling = usePaperStore.getState().group!.scaling
const delta = { const delta = {
@@ -1753,6 +1934,10 @@ export const usePaperStore = create<PaperState>((set) => ({
panTool.onMouseUp = (_e: paper.ToolEvent) => { panTool.onMouseUp = (_e: paper.ToolEvent) => {
if (stopMiddleMousePan()) return if (stopMiddleMousePan()) return
if (useTopToolsStore.getState().isView) 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 let newDrawPath: paper.Path | paper.CompoundPath | null = null
@@ -5233,12 +5418,27 @@ export const usePaperStore = create<PaperState>((set) => ({
) )
circle.scale(newScale / currentScaling) circle.scale(newScale / currentScaling)
circle.onMouseEnter = (_e: any) => { circle.onMouseEnter = (_e: any) => {
hoveredPathCircleInfo = {
id,
index,
item: circle,
}
clearHoveredBufferPathHighlight()
circle.strokeWidth = 4 circle.strokeWidth = 4
circle.fillColor = new paper.Color("#FFF") circle.fillColor = new paper.Color("#FFF")
if (usePaperStore.getState().el) {
usePaperStore.getState().el!.style.cursor = "move"
}
} }
circle.onMouseLeave = (_e: any) => { circle.onMouseLeave = (_e: any) => {
if (hoveredPathCircleInfo?.item === circle) {
hoveredPathCircleInfo = null
}
circle.strokeWidth = 2 circle.strokeWidth = 2
circle.fillColor = color circle.fillColor = color
if (usePaperStore.getState().el) {
usePaperStore.getState().el!.style.cursor = "default"
}
} }
return circle return circle
}, },
@@ -5251,8 +5451,8 @@ export const usePaperStore = create<PaperState>((set) => ({
segments: [item.point, item.next], segments: [item.point, item.next],
strokeColor: color, // 设置为透明,以便不可见 strokeColor: color, // 设置为透明,以便不可见
strokeScaling: false, strokeScaling: false,
strokeWidth: 4, // 设置一个较大的strokeWidth作为缓冲区 strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
strokeCap: "butt", strokeCap: "round",
onSelect: function (event: { stopPropagation: () => void }) { onSelect: function (event: { stopPropagation: () => void }) {
event.stopPropagation() // 阻止事件冒泡 event.stopPropagation() // 阻止事件冒泡
}, },
@@ -5266,10 +5466,54 @@ export const usePaperStore = create<PaperState>((set) => ({
// bufferPath.segments[0].handleIn = item.handleIn; // bufferPath.segments[0].handleIn = item.handleIn;
// bufferPath.segments[1].handleOut = item.handleOut; // bufferPath.segments[1].handleOut = item.handleOut;
let edge: paper.Path | null 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: { 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) { if (!edge) {
edge = new paper.Path( edge = new paper.Path(
Object.assign( Object.assign(
@@ -5280,27 +5524,33 @@ export const usePaperStore = create<PaperState>((set) => ({
bufferPath.segments[1].point, bufferPath.segments[1].point,
], ],
strokeColor: "white", // 初始颜色与原始路径相同 strokeColor: "white", // 初始颜色与原始路径相同
strokeWidth: 2, strokeWidth: 3,
strokeScaling: false, strokeScaling: false,
strokeCap: "butt", strokeCap: "round",
}, },
{ {
data: { index, type: "pathBuff" }, data: { index, type: "pathBuffHover" },
} }
) )
) )
// edge.bringToFront(); // edge.bringToFront();
event.stopPropagation() // 阻止事件冒泡 if (usePaperStore.getState().el) {
usePaperStore.getState().el!.style.cursor = "crosshair"
} }
} }
event.stopPropagation?.() // 阻止事件冒泡
}
// 鼠标移出时取消高亮 // 鼠标移出时取消高亮
bufferPath.onMouseLeave = function (event: { bufferPath.onMouseLeave = function (event: {
stopPropagation: () => void stopPropagation?: () => void
}) { }) {
edge?.remove() if (hoveredBufferPathInfo?.path === bufferPath) {
edge = null hoveredBufferPathInfo.scheduleClear()
event.stopPropagation() // 阻止事件冒泡 } else {
scheduleClear()
}
event.stopPropagation?.() // 阻止事件冒泡
} }
return bufferPath return bufferPath
}, },