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) setActiveOperation(selectedContext.operationId)
usePaperStore.getState().setContinuePolygonTarget(null)
setEditMode(true) setEditMode(true)
setDrawOption("default") setDrawOption("default")
useKeyboardStore.getState().setDrawAction(mode) 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) => { const hasShapeSegments = useCallback((shape: any) => {
if (!shape) return false if (!shape) return false
if (shape instanceof paper.CompoundPath) { if (shape instanceof paper.CompoundPath) {
@@ -2039,13 +2081,23 @@ const LabelPage = ({
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
// console.log(e, e.key); // console.log(e, e.key);
if (focusInput || isView) return if (focusInput) return
console.log(e.key) console.log(e.key)
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", "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") { if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
e.preventDefault() e.preventDefault()
@@ -2065,6 +2117,12 @@ const LabelPage = ({
return return
} }
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "b") {
e.preventDefault()
handleContinueSelectedPolygon()
return
}
if ( if (
!e.ctrlKey && !e.ctrlKey &&
!e.metaKey && !e.metaKey &&
@@ -2335,6 +2393,7 @@ const LabelPage = ({
getOperationSchema, getOperationSchema,
handleToggleHideSelectedObject, handleToggleHideSelectedObject,
handleMergeSelectedObjects, handleMergeSelectedObjects,
handleContinueSelectedPolygon,
handleShortcut, handleShortcut,
isView, isView,
projectDetail?.label_type, projectDetail?.label_type,
@@ -2360,6 +2419,7 @@ const LabelPage = ({
useEffect(() => { useEffect(() => {
useKeyboardStore.getState().setCopyDataIds([]) useKeyboardStore.getState().setCopyDataIds([])
useKeyboardStore.getState().setDrawAction("none") useKeyboardStore.getState().setDrawAction("none")
usePaperStore.getState().setContinuePolygonTarget(null)
drawShortcutSnapshotRef.current = null drawShortcutSnapshotRef.current = null
}, [activeImage]) }, [activeImage])

View File

@@ -1,5 +1,6 @@
import { Comment } from "./api/label/typing" import { Comment } from "./api/label/typing"
import { Project } from "./api/project/typing" import { Project } from "./api/project/typing"
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"
@@ -19,6 +20,12 @@ import { useRightToolsStore } from "./useRightToolsStore"
import { useTopToolsStore } from "./useTopToolsStore" import { useTopToolsStore } from "./useTopToolsStore"
import { safeClone } from "./utils/clone" import { safeClone } from "./utils/clone"
interface ContinuePolygonTarget {
imageId: string
operationId: string
objectId: number
}
interface PaperState { interface PaperState {
paperScope: paper.PaperScope | null paperScope: paper.PaperScope | null
group: paper.Group | null group: paper.Group | null
@@ -36,6 +43,7 @@ interface PaperState {
rasterScale: { [x: string]: number } rasterScale: { [x: string]: number }
rasterSize: { [x: string]: [number, number] } rasterSize: { [x: string]: [number, number] }
reciprocalRasterScale: { [x: string]: number } reciprocalRasterScale: { [x: string]: number }
continuePolygonTarget: ContinuePolygonTarget | null
loadingData: boolean loadingData: boolean
init: ( init: (
initEl: HTMLCanvasElement, initEl: HTMLCanvasElement,
@@ -170,6 +178,7 @@ interface PaperState {
getAll: () => paper.Path[] getAll: () => paper.Path[]
getItemById: (id: number) => paper.Path | null getItemById: (id: number) => paper.Path | null
getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath> getItemsById: (id: number) => Array<paper.Path | paper.CompoundPath>
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
setLoadingData: (flag: boolean) => void setLoadingData: (flag: boolean) => void
} }
@@ -520,6 +529,7 @@ export const usePaperStore = create<PaperState>((set) => ({
rasterScale: initialPaperState.rasterScale, rasterScale: initialPaperState.rasterScale,
rasterSize: initialPaperState.rasterSize, rasterSize: initialPaperState.rasterSize,
reciprocalRasterScale: initialPaperState.reciprocalRasterScale, reciprocalRasterScale: initialPaperState.reciprocalRasterScale,
continuePolygonTarget: null,
currentMousePosition: [0, 0], currentMousePosition: [0, 0],
loadingData: false, loadingData: false,
init: ( init: (
@@ -567,6 +577,11 @@ export const usePaperStore = create<PaperState>((set) => ({
...state, ...state,
loadingData: flag, loadingData: flag,
})), })),
setContinuePolygonTarget: (target) =>
set((state: PaperState) => ({
...state,
continuePolygonTarget: target,
})),
setCurrentMousePosition: (point: [number, number]) => setCurrentMousePosition: (point: [number, number]) =>
set((state: PaperState) => ({ set((state: PaperState) => ({
...state, ...state,
@@ -2679,6 +2694,9 @@ export const usePaperStore = create<PaperState>((set) => ({
let polygonTool = paperPolygonTool let polygonTool = paperPolygonTool
let currentMagnetPoint: paper.Point | null = null let currentMagnetPoint: paper.Point | null = null
let draftId: number | 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) => { const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
// 更新选中的点 // 更新选中的点
@@ -2724,6 +2742,193 @@ export const usePaperStore = create<PaperState>((set) => ({
currentMagnetPoint = null 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 = () => { const rebuildPolygonDraft = () => {
polygon?.remove() polygon?.remove()
polygon = null polygon = null
@@ -3099,6 +3304,14 @@ export const usePaperStore = create<PaperState>((set) => ({
usePaperSupportStore.getState().clearAll() usePaperSupportStore.getState().clearAll()
} }
if (
!polygon &&
!points.length &&
usePaperStore.getState().continuePolygonTarget
) {
if (!startContinuePolygonDraft()) return
}
// 先判断是否存在吸附点 // 先判断是否存在吸附点
let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point let checkPoint = currentMagnetPoint ? currentMagnetPoint : e.point
// doubleclick // doubleclick
@@ -3122,6 +3335,30 @@ export const usePaperStore = create<PaperState>((set) => ({
// 初始化 compoundPolygon // 初始化 compoundPolygon
compoundPolygon = new paper.Path() 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") { if (useKeyboardStore.getState().drawAction === "subtract") {
;(polygon as paper.Path).closePath() ;(polygon as paper.Path).closePath()
handleSelectedPolygonSubtract(polygon as paper.Path) handleSelectedPolygonSubtract(polygon as paper.Path)
@@ -3544,8 +3781,13 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
polygonTool.onKeyDown = (e: paper.KeyEvent) => { polygonTool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape" || e.key === "alt") { if (e.key === "escape" || e.key === "alt") {
if (polygon || points.length) { if (
polygon ||
points.length ||
usePaperStore.getState().continuePolygonTarget
) {
resetPolygonDraft() resetPolygonDraft()
clearContinuePolygonContext(true)
usePaperSupportStore.getState().handleBufferPaths(null) usePaperSupportStore.getState().handleBufferPaths(null)
} }
} else if (e.key === "delete") { } else if (e.key === "delete") {