feat(shortcut): b/m

This commit is contained in:
zhangheng
2026-04-03 18:51:59 +08:00
parent e4aae0a7af
commit 7c700bf952
2 changed files with 305 additions and 3 deletions

View File

@@ -1467,6 +1467,7 @@ const LabelPage = ({
}
setActiveOperation(selectedContext.operationId)
usePaperStore.getState().setContinuePolygonTarget(null)
setEditMode(true)
setDrawOption("default")
useKeyboardStore.getState().setDrawAction(mode)
@@ -1566,6 +1567,47 @@ const LabelPage = ({
[]
)
const handleContinueSelectedPolygon = useCallback(() => {
const selectedContext = resolveSelectedObjectContext()
if (!selectedContext) {
notifications.show({
color: "yellow",
message: "请先选中单个对象",
})
return
}
if (selectedContext.objectType !== "polygon") {
notifications.show({
color: "yellow",
message: "继续标注快捷键仅支持 polygon 类型对象",
})
return
}
setEditMode(true)
setDrawOption("default")
setPressA(false)
setActiveOperation(selectedContext.operationId)
useKeyboardStore.getState().setDrawAction("none")
usePaperStore.getState().setContinuePolygonTarget({
imageId: selectedContext.imageId,
operationId: selectedContext.operationId,
objectId: selectedContext.objectId,
})
usePaperStore.getState().setPaperMode("polygon")
notifications.show({
color: "green",
message: "已进入继续标注模式",
})
}, [
resolveSelectedObjectContext,
setActiveOperation,
setDrawOption,
setEditMode,
setPressA,
])
const hasShapeSegments = useCallback((shape: any) => {
if (!shape) return false
if (shape instanceof paper.CompoundPath) {
@@ -2039,13 +2081,23 @@ const LabelPage = ({
useEffect(() => {
const down = (e: KeyboardEvent) => {
// console.log(e, e.key);
if (focusInput || isView) return
if (focusInput) return
console.log(e.key)
const mode = usePaperStore.getState().mode
const lowerKey = e.key.toLowerCase()
if (e.repeat && ["a", "d", "h", ",", "."].includes(lowerKey)) return
if (e.repeat && ["a", "b", "d", "h", "m", ",", "."].includes(lowerKey))
return
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") {
e.preventDefault()
const topState = useTopToolsStore.getState()
topState.setMagnetFlag(!topState.magnetFlag)
return
}
if (isView) return
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
e.preventDefault()
@@ -2065,6 +2117,12 @@ const LabelPage = ({
return
}
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "b") {
e.preventDefault()
handleContinueSelectedPolygon()
return
}
if (
!e.ctrlKey &&
!e.metaKey &&
@@ -2335,6 +2393,7 @@ const LabelPage = ({
getOperationSchema,
handleToggleHideSelectedObject,
handleMergeSelectedObjects,
handleContinueSelectedPolygon,
handleShortcut,
isView,
projectDetail?.label_type,
@@ -2360,6 +2419,7 @@ const LabelPage = ({
useEffect(() => {
useKeyboardStore.getState().setCopyDataIds([])
useKeyboardStore.getState().setDrawAction("none")
usePaperStore.getState().setContinuePolygonTarget(null)
drawShortcutSnapshotRef.current = null
}, [activeImage])

View File

@@ -1,5 +1,6 @@
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"
@@ -19,6 +20,12 @@ import { useRightToolsStore } from "./useRightToolsStore"
import { 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
@@ -36,6 +43,7 @@ interface PaperState {
rasterScale: { [x: string]: number }
rasterSize: { [x: string]: [number, number] }
reciprocalRasterScale: { [x: string]: number }
continuePolygonTarget: ContinuePolygonTarget | null
loadingData: boolean
init: (
initEl: HTMLCanvasElement,
@@ -170,6 +178,7 @@ interface PaperState {
getAll: () => paper.Path[]
getItemById: (id: number) => paper.Path | null
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
setLoadingData: (flag: boolean) => void
}
@@ -520,6 +529,7 @@ export const usePaperStore = create<PaperState>((set) => ({
rasterScale: initialPaperState.rasterScale,
rasterSize: initialPaperState.rasterSize,
reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
continuePolygonTarget: null,
currentMousePosition: [0, 0],
loadingData: false,
init: (
@@ -567,6 +577,11 @@ export const usePaperStore = create<PaperState>((set) => ({
...state,
loadingData: flag,
})),
setContinuePolygonTarget: (target) =>
set((state: PaperState) => ({
...state,
continuePolygonTarget: target,
})),
setCurrentMousePosition: (point: [number, number]) =>
set((state: PaperState) => ({
...state,
@@ -2679,6 +2694,9 @@ export const usePaperStore = create<PaperState>((set) => ({
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
const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
// 更新选中的点
@@ -2724,6 +2742,193 @@ export const usePaperStore = create<PaperState>((set) => ({
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 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
usePaperStore.getState().setContinuePolygonTarget(null)
}
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
)
if (!targetTuple) {
notifications.show({
color: "yellow",
message: "未找到可继续标注的对象",
})
clearContinuePolygonContext(true)
return false
}
const nextPoints = safeClone(targetTuple[1] || [])
let contourIndex = -1
for (let i = nextPoints.length - 1; i >= 0; i -= 1) {
if (nextPoints[i]?.length > 3) {
contourIndex = i
break
}
}
if (contourIndex === -1) {
notifications.show({
color: "yellow",
message: "当前对象没有可继续标注的区域",
})
clearContinuePolygonContext(true)
return false
}
const contourSegments = safeClone(nextPoints[contourIndex] || [])
const continueSegments = contourSegments.slice().reverse()
continueSegments.pop()
const contourPoints = continueSegments
.map((segment: any) => getPointFromSegment(segment))
.filter((point: paper.Point | null): point is paper.Point => !!point)
if (contourPoints.length < 3) {
notifications.show({
color: "yellow",
message: "当前区域至少保留3个节点无法继续撤回",
})
clearContinuePolygonContext(true)
return false
}
clearContinueSourceItems(true)
continueSourceItems = usePaperStore
.getState()
.getItemsById(target.objectId)
.filter((item) => item.data?.type === "polygon")
continueSourceItems.forEach((item) => {
item.visible = false
})
continueTargetContourIndex = contourIndex
continueTargetContext = target
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
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
)
if (!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] || []),
])
useObjectStore
.getState()
.updateSelectedPath(target.imageId, "ADD", target.objectId)
clearContinuePolygonContext(false)
forceUpdate()
return true
}
const rebuildPolygonDraft = () => {
polygon?.remove()
polygon = null
@@ -3099,6 +3304,14 @@ export const usePaperStore = create<PaperState>((set) => ({
usePaperSupportStore.getState().clearAll()
}
if (
!polygon &&
!points.length &&
usePaperStore.getState().continuePolygonTarget
) {
if (!startContinuePolygonDraft()) return
}
// 先判断是否存在吸附点
let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point
// doubleclick
@@ -3122,6 +3335,30 @@ export const usePaperStore = create<PaperState>((set) => ({
// 初始化 compoundPolygon
compoundPolygon = new paper.Path()
if (usePaperStore.getState().continuePolygonTarget) {
;(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)
@@ -3544,8 +3781,13 @@ export const usePaperStore = create<PaperState>((set) => ({
}
polygonTool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape" || e.key === "alt") {
if (polygon || points.length) {
if (
polygon ||
points.length ||
usePaperStore.getState().continuePolygonTarget
) {
resetPolygonDraft()
clearContinuePolygonContext(true)
usePaperSupportStore.getState().handleBufferPaths(null)
}
} else if (e.key === "delete") {