feat(shortcut): b/m
This commit is contained in:
@@ -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])
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user