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