feat(shortcut): h/a/d
This commit is contained in:
@@ -56,6 +56,7 @@ import {
|
|||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { findGroupKey } from "./util"
|
import { findGroupKey } from "./util"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
|
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
||||||
import { labelTypeMap } from "./utils/constants"
|
import { labelTypeMap } from "./utils/constants"
|
||||||
|
|
||||||
// Splitter component - will need custom implementation or alternative
|
// Splitter component - will need custom implementation or alternative
|
||||||
@@ -233,9 +234,11 @@ const LabelPage = ({
|
|||||||
const {
|
const {
|
||||||
editMode,
|
editMode,
|
||||||
setEditMode,
|
setEditMode,
|
||||||
|
pressA,
|
||||||
setPressA,
|
setPressA,
|
||||||
activeOperation,
|
activeOperation,
|
||||||
setActiveOperation,
|
setActiveOperation,
|
||||||
|
setDrawOption,
|
||||||
showObjectList,
|
showObjectList,
|
||||||
showGroupList,
|
showGroupList,
|
||||||
showTaskList,
|
showTaskList,
|
||||||
@@ -1357,6 +1360,12 @@ const LabelPage = ({
|
|||||||
}, [editMode, setDrawType])
|
}, [editMode, setDrawType])
|
||||||
|
|
||||||
const { setShift, focusInput } = useKeyEventStore()
|
const { setShift, focusInput } = useKeyEventStore()
|
||||||
|
const drawAction = useKeyboardStore((state) => state.drawAction)
|
||||||
|
const drawShortcutSnapshotRef = useRef<{
|
||||||
|
activeOperation: string
|
||||||
|
drawOption: "default" | "intersect" | "unite"
|
||||||
|
editMode: boolean
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const handleShortcut = useCallback(
|
const handleShortcut = useCallback(
|
||||||
(key: string, ctrl: boolean) => {
|
(key: string, ctrl: boolean) => {
|
||||||
@@ -1376,6 +1385,132 @@ const LabelPage = ({
|
|||||||
[getOperationSchema, setActiveOperation]
|
[getOperationSchema, setActiveOperation]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resolveSelectedObjectContext = useCallback(() => {
|
||||||
|
const imageId = useBottomToolsStore.getState().activeImage
|
||||||
|
const selectedIds = useObjectStore.getState().selectedPath[imageId] || []
|
||||||
|
if (selectedIds.length !== 1) return null
|
||||||
|
|
||||||
|
const objectId = selectedIds[0]
|
||||||
|
const imageLabel = useLabelStore.getState().label.get(imageId)
|
||||||
|
if (!imageLabel) return null
|
||||||
|
|
||||||
|
let operationId = ""
|
||||||
|
for (const [categoryId, paths] of imageLabel.entries()) {
|
||||||
|
if (paths.some((path) => path[0] === objectId)) {
|
||||||
|
operationId = categoryId
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!operationId) return null
|
||||||
|
|
||||||
|
const selectedItem = usePaperStore.getState().getItemById(objectId)
|
||||||
|
if (!selectedItem?.data?.type) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageId,
|
||||||
|
objectId,
|
||||||
|
operationId,
|
||||||
|
objectType: selectedItem.data.type as string,
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const restoreDrawShortcutSnapshot = useCallback(() => {
|
||||||
|
useKeyboardStore.getState().setDrawAction("none")
|
||||||
|
|
||||||
|
const snapshot = drawShortcutSnapshotRef.current
|
||||||
|
if (!snapshot) return
|
||||||
|
|
||||||
|
setActiveOperation(snapshot.activeOperation)
|
||||||
|
setDrawOption(snapshot.drawOption)
|
||||||
|
setEditMode(snapshot.editMode)
|
||||||
|
drawShortcutSnapshotRef.current = null
|
||||||
|
}, [setActiveOperation, setDrawOption, setEditMode])
|
||||||
|
|
||||||
|
const enterSelectedObjectDrawMode = useCallback(
|
||||||
|
(mode: "add" | "subtract") => {
|
||||||
|
const selectedContext = resolveSelectedObjectContext()
|
||||||
|
if (!selectedContext) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中单个对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
mode === "add" &&
|
||||||
|
!["polygon", "brush"].includes(selectedContext.objectType)
|
||||||
|
) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "新增区域仅支持 polygon/brush 类型对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "subtract" && selectedContext.objectType !== "polygon") {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "删除区域仅支持 polygon 类型对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!drawShortcutSnapshotRef.current) {
|
||||||
|
const topState = useTopToolsStore.getState()
|
||||||
|
drawShortcutSnapshotRef.current = {
|
||||||
|
activeOperation: topState.activeOperation,
|
||||||
|
drawOption: topState.drawOption,
|
||||||
|
editMode: topState.editMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveOperation(selectedContext.operationId)
|
||||||
|
setEditMode(true)
|
||||||
|
setDrawOption("default")
|
||||||
|
useKeyboardStore.getState().setDrawAction(mode)
|
||||||
|
setPressA(true)
|
||||||
|
|
||||||
|
if (!useTopToolsStore.getState().pressA) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "当前对象无法进入区域编辑模式",
|
||||||
|
})
|
||||||
|
restoreDrawShortcutSnapshot()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
resolveSelectedObjectContext,
|
||||||
|
restoreDrawShortcutSnapshot,
|
||||||
|
setActiveOperation,
|
||||||
|
setDrawOption,
|
||||||
|
setEditMode,
|
||||||
|
setPressA,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleToggleHideSelectedObject = useCallback(() => {
|
||||||
|
const selectedContext = resolveSelectedObjectContext()
|
||||||
|
if (!selectedContext) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中单个对象",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentHideStatus =
|
||||||
|
useObjectStore.getState().pathStatus[
|
||||||
|
selectedContext.imageId + selectedContext.objectId
|
||||||
|
]?.HIDE || false
|
||||||
|
toggleObjectHideByShortcut(
|
||||||
|
selectedContext.imageId,
|
||||||
|
selectedContext.operationId,
|
||||||
|
selectedContext.objectId,
|
||||||
|
!currentHideStatus
|
||||||
|
)
|
||||||
|
}, [resolveSelectedObjectContext])
|
||||||
|
|
||||||
const handleMergeSelectedObjects = useCallback(() => {
|
const handleMergeSelectedObjects = useCallback(() => {
|
||||||
const activeImage = useBottomToolsStore.getState().activeImage
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
const selectedIds =
|
const selectedIds =
|
||||||
@@ -1518,6 +1653,27 @@ const LabelPage = ({
|
|||||||
|
|
||||||
console.log(e.key)
|
console.log(e.key)
|
||||||
const mode = usePaperStore.getState().mode
|
const mode = usePaperStore.getState().mode
|
||||||
|
const lowerKey = e.key.toLowerCase()
|
||||||
|
|
||||||
|
if (e.repeat && ["a", "d", "h"].includes(lowerKey)) return
|
||||||
|
|
||||||
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
||||||
|
e.preventDefault()
|
||||||
|
handleToggleHideSelectedObject()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "a") {
|
||||||
|
e.preventDefault()
|
||||||
|
enterSelectedObjectDrawMode("add")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "d") {
|
||||||
|
e.preventDefault()
|
||||||
|
enterSelectedObjectDrawMode("subtract")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (e.altKey && e.key.toLowerCase() === "x") {
|
if (e.altKey && e.key.toLowerCase() === "x") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -1561,11 +1717,6 @@ const LabelPage = ({
|
|||||||
paperContainerRef.current.handleCreatePathGroup()
|
paperContainerRef.current.handleCreatePathGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "a") {
|
|
||||||
e.preventDefault()
|
|
||||||
setPressA(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === "Control") {
|
if (e.key === "Control") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useKeyboardStore.getState().setCtrl(true)
|
useKeyboardStore.getState().setCtrl(true)
|
||||||
@@ -1746,19 +1897,27 @@ const LabelPage = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
editMode,
|
editMode,
|
||||||
|
enterSelectedObjectDrawMode,
|
||||||
focusInput,
|
focusInput,
|
||||||
getOperationSchema,
|
getOperationSchema,
|
||||||
|
handleToggleHideSelectedObject,
|
||||||
handleMergeSelectedObjects,
|
handleMergeSelectedObjects,
|
||||||
handleShortcut,
|
handleShortcut,
|
||||||
isView,
|
isView,
|
||||||
projectDetail?.label_type,
|
projectDetail?.label_type,
|
||||||
setEditMode,
|
setEditMode,
|
||||||
setGroupScale,
|
setGroupScale,
|
||||||
|
setDrawOption,
|
||||||
setPressA,
|
setPressA,
|
||||||
setScale,
|
setScale,
|
||||||
setShift,
|
setShift,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (drawAction === "none" || pressA) return
|
||||||
|
restoreDrawShortcutSnapshot()
|
||||||
|
}, [drawAction, pressA, restoreDrawShortcutSnapshot])
|
||||||
|
|
||||||
const setGlobalLoading = useCallback((flag: boolean) => {
|
const setGlobalLoading = useCallback((flag: boolean) => {
|
||||||
setLoading(flag)
|
setLoading(flag)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -1766,6 +1925,8 @@ const LabelPage = ({
|
|||||||
// 切换图片时清空复制保存的数组
|
// 切换图片时清空复制保存的数组
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useKeyboardStore.getState().setCopyDataIds([])
|
useKeyboardStore.getState().setCopyDataIds([])
|
||||||
|
useKeyboardStore.getState().setDrawAction("none")
|
||||||
|
drawShortcutSnapshotRef.current = null
|
||||||
}, [activeImage])
|
}, [activeImage])
|
||||||
|
|
||||||
const handleBeforeUnload = (e: any) => {
|
const handleBeforeUnload = (e: any) => {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore"
|
|||||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
import { getSubAttrStatus } from "../util"
|
import { getSubAttrStatus } from "../util"
|
||||||
|
import { toggleObjectHideByShortcut } from "../utils/objectVisibility"
|
||||||
import { labelTypeMap } from "../utils/constants"
|
import { labelTypeMap } from "../utils/constants"
|
||||||
|
|
||||||
interface RightObjectToolsComponentProps {
|
interface RightObjectToolsComponentProps {
|
||||||
@@ -379,33 +380,6 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTagVisibility = (tagCategory?: string) => {
|
|
||||||
const { showTags, showTagsConfigs } = useTopToolsStore.getState()
|
|
||||||
if (!showTags) return false
|
|
||||||
if (tagCategory === "mask") return showTagsConfigs.includes("外框")
|
|
||||||
if (tagCategory === "point_text") return showTagsConfigs.includes("点ID")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncObjectItemsVisibility = useCallback(
|
|
||||||
(objectId: number, hide: boolean) => {
|
|
||||||
const sameIdItems = getItemsById(objectId)
|
|
||||||
if (!sameIdItems?.length) return
|
|
||||||
sameIdItems.forEach((item) => {
|
|
||||||
if (hide) {
|
|
||||||
item.visible = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (item.data?.type === "tag") {
|
|
||||||
item.visible = getTagVisibility(item.data?.tag_category)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
item.visible = true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[getItemsById]
|
|
||||||
)
|
|
||||||
|
|
||||||
const operationHide = useCallback(
|
const operationHide = useCallback(
|
||||||
(operationId: string, hide: boolean) => {
|
(operationId: string, hide: boolean) => {
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
||||||
@@ -414,51 +388,17 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
.get(activeImage)
|
.get(activeImage)
|
||||||
?.get(operationId)
|
?.get(operationId)
|
||||||
?.map((child) => {
|
?.map((child) => {
|
||||||
updatePathStatus(activeImage + child[0], "HIDE", hide)
|
toggleObjectHideByShortcut(activeImage, operationId, child[0], hide)
|
||||||
syncObjectItemsVisibility(child[0], hide)
|
|
||||||
// 隐藏时清空选中状态
|
|
||||||
if (hide) setSelectedItemFalse(child[0])
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[
|
[activeImage, storeLabel, updateOperationStatus]
|
||||||
activeImage,
|
|
||||||
storeLabel,
|
|
||||||
syncObjectItemsVisibility,
|
|
||||||
updateOperationStatus,
|
|
||||||
updatePathStatus,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const objectHide = useCallback(
|
const objectHide = useCallback(
|
||||||
(operationId: string, objectId: number, hide: boolean) => {
|
(operationId: string, objectId: number, hide: boolean) => {
|
||||||
if (hide) {
|
toggleObjectHideByShortcut(activeImage, operationId, objectId, hide)
|
||||||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
|
||||||
syncObjectItemsVisibility(objectId, hide)
|
|
||||||
// 隐藏时清空选中状态
|
|
||||||
setSelectedItemFalse(objectId)
|
|
||||||
let childrenHideStatus = storeLabel
|
|
||||||
.get(activeImage)
|
|
||||||
?.get(operationId)
|
|
||||||
?.map((child) => {
|
|
||||||
return getItemById(child[0])!.visible
|
|
||||||
})
|
|
||||||
if (childrenHideStatus?.findIndex((bool) => bool === true) === -1) {
|
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateOperationStatus(activeImage + operationId, "HIDE", hide)
|
|
||||||
updatePathStatus(activeImage + objectId, "HIDE", hide)
|
|
||||||
syncObjectItemsVisibility(objectId, hide)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[activeImage]
|
||||||
activeImage,
|
|
||||||
getItemById,
|
|
||||||
storeLabel,
|
|
||||||
syncObjectItemsVisibility,
|
|
||||||
updateOperationStatus,
|
|
||||||
updatePathStatus,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// const renderObjectName = useCallback((name: string, input: string) => {
|
// const renderObjectName = useCallback((name: string, input: string) => {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
import { useBackUrlStore, usePermissionStore } from "../store/auth"
|
import { useBackUrlStore, usePermissionStore } from "../store/auth"
|
||||||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
import { useDescToolsStore } from "../useDescToolsStore"
|
import { useDescToolsStore } from "../useDescToolsStore"
|
||||||
|
import { useKeyboardStore } from "../useKeyBoardStore"
|
||||||
import { usePaperStore } from "../usePaperStore"
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useRightToolsStore } from "../useRightToolsStore"
|
import { useRightToolsStore } from "../useRightToolsStore"
|
||||||
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
||||||
@@ -1531,6 +1532,7 @@ const TopTools = (
|
|||||||
}, [activeOperation, mode])
|
}, [activeOperation, mode])
|
||||||
|
|
||||||
const operationSchema = getOperationSchema(activeOperation)
|
const operationSchema = getOperationSchema(activeOperation)
|
||||||
|
const drawAction = useKeyboardStore((state) => state.drawAction)
|
||||||
|
|
||||||
const subAttrForm = (
|
const subAttrForm = (
|
||||||
<Box w={256}>
|
<Box w={256}>
|
||||||
@@ -1645,6 +1647,12 @@ const TopTools = (
|
|||||||
const iconStyle = { width: 20, height: 20 }
|
const iconStyle = { width: 20, height: 20 }
|
||||||
if (mode === "support") return <Paintbrush style={iconStyle} />
|
if (mode === "support") return <Paintbrush style={iconStyle} />
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
|
if (pressA && drawAction === "subtract") {
|
||||||
|
return <CopyMinus style={iconStyle} />
|
||||||
|
}
|
||||||
|
if (pressA && drawAction === "add") {
|
||||||
|
return <CopyPlus style={iconStyle} />
|
||||||
|
}
|
||||||
return pressA ? (
|
return pressA ? (
|
||||||
<CirclePlus style={iconStyle} />
|
<CirclePlus style={iconStyle} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1653,7 +1661,7 @@ const TopTools = (
|
|||||||
} else {
|
} else {
|
||||||
return <MousePointer style={iconStyle} />
|
return <MousePointer style={iconStyle} />
|
||||||
}
|
}
|
||||||
}, [mode, editMode, pressA])
|
}, [mode, editMode, pressA, drawAction])
|
||||||
|
|
||||||
const showTagConfigContainer = useMemo(() => {
|
const showTagConfigContainer = useMemo(() => {
|
||||||
const configs = ["ID", "类别", "外框", "点ID", "子属性", "属性值"]
|
const configs = ["ID", "类别", "外框", "点ID", "子属性", "属性值"]
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ interface KeyboardStore {
|
|||||||
setCtrl: (v: boolean) => void
|
setCtrl: (v: boolean) => void
|
||||||
shift: boolean
|
shift: boolean
|
||||||
setShift: (v: boolean) => void
|
setShift: (v: boolean) => void
|
||||||
|
drawAction: "none" | "add" | "subtract"
|
||||||
|
setDrawAction: (mode: "none" | "add" | "subtract") => void
|
||||||
copyDataIds: number[] // ctrl + c 复制数据
|
copyDataIds: number[] // ctrl + c 复制数据
|
||||||
setCopyDataIds: (ids: number[]) => void
|
setCopyDataIds: (ids: number[]) => void
|
||||||
}
|
}
|
||||||
@@ -12,8 +14,10 @@ interface KeyboardStore {
|
|||||||
export const useKeyboardStore = create<KeyboardStore>((set) => ({
|
export const useKeyboardStore = create<KeyboardStore>((set) => ({
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
|
drawAction: "none",
|
||||||
copyDataIds: [],
|
copyDataIds: [],
|
||||||
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
setCtrl: (v) => set((state) => ({ ...state, ctrl: v })),
|
||||||
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
setShift: (v) => set((state) => ({ ...state, shift: v })),
|
||||||
|
setDrawAction: (mode) => set((state) => ({ ...state, drawAction: mode })),
|
||||||
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useObjectStore,
|
useObjectStore,
|
||||||
} from "./store"
|
} from "./store"
|
||||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||||
|
import { useKeyboardStore } from "./useKeyBoardStore"
|
||||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||||
import { usePaperSupportStore } from "./usePaperSupportStore"
|
import { usePaperSupportStore } from "./usePaperSupportStore"
|
||||||
import { useRightToolsStore } from "./useRightToolsStore"
|
import { useRightToolsStore } from "./useRightToolsStore"
|
||||||
@@ -2855,6 +2856,139 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paper path data里可能带函数(例如事件回调), 不能直接structuredClone
|
||||||
|
const getSafePathData = (data: any) => {
|
||||||
|
const nextData: Record<string, any> = {}
|
||||||
|
Object.entries(data || {}).forEach(([key, value]) => {
|
||||||
|
if (typeof value === "function") return
|
||||||
|
nextData[key] = value
|
||||||
|
})
|
||||||
|
return nextData
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectedPolygonSubtract = (draftPath: paper.Path) => {
|
||||||
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
|
const selectedIds =
|
||||||
|
useObjectStore.getState().selectedPath[activeImage] || []
|
||||||
|
if (selectedIds.length !== 1) return false
|
||||||
|
|
||||||
|
const selectedId = selectedIds[0]
|
||||||
|
const targetShapes = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(selectedId)
|
||||||
|
.filter((item) => item.data?.type === "polygon")
|
||||||
|
|
||||||
|
const targetShape =
|
||||||
|
targetShapes.find((item) => item instanceof paper.CompoundPath) ||
|
||||||
|
targetShapes.find(
|
||||||
|
(item) => item instanceof paper.Path && !item.data?.isHollowPolygon
|
||||||
|
) ||
|
||||||
|
targetShapes.find((item) => item instanceof paper.Path)
|
||||||
|
|
||||||
|
if (!targetShape) return false
|
||||||
|
|
||||||
|
let clippedDraft: paper.Path | paper.CompoundPath | null =
|
||||||
|
draftPath.intersect(usePaperStore.getState().rasterPath!, {
|
||||||
|
insert: false,
|
||||||
|
trace: false,
|
||||||
|
}) as paper.Path | paper.CompoundPath
|
||||||
|
|
||||||
|
draftPath.remove()
|
||||||
|
|
||||||
|
const draftHasSegments =
|
||||||
|
clippedDraft instanceof paper.CompoundPath
|
||||||
|
? (clippedDraft.children as paper.Path[]).some(
|
||||||
|
(child) => child.segments?.length
|
||||||
|
)
|
||||||
|
: clippedDraft.segments?.length
|
||||||
|
if (!draftHasSegments) {
|
||||||
|
clippedDraft?.remove()
|
||||||
|
clippedDraft = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetData = getSafePathData(targetShape.data || {})
|
||||||
|
let nextPolygon = (
|
||||||
|
targetShape as paper.Path | paper.CompoundPath
|
||||||
|
).subtract(clippedDraft) as paper.Path | paper.CompoundPath
|
||||||
|
clippedDraft?.remove()
|
||||||
|
clippedDraft = null
|
||||||
|
targetShape.remove()
|
||||||
|
|
||||||
|
if (nextPolygon instanceof paper.CompoundPath) {
|
||||||
|
nextPolygon.data = Object.assign({}, targetData)
|
||||||
|
;(nextPolygon.children as paper.Path[]).forEach((child) => {
|
||||||
|
child.data = Object.assign({}, targetData, {
|
||||||
|
isHollowPolygon: !child.clockwise,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
nextPolygon.data = Object.assign({}, targetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const points: any[] = []
|
||||||
|
const hollowPoints: any[] = []
|
||||||
|
const collectPathPoints = (path: paper.Path) => {
|
||||||
|
if (!path.segments?.length) return
|
||||||
|
const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
|
||||||
|
if (path.clockwise) {
|
||||||
|
points.push(pathData[1]?.segments)
|
||||||
|
} else {
|
||||||
|
hollowPoints.push(pathData[1]?.segments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPolygon instanceof paper.CompoundPath) {
|
||||||
|
;(nextPolygon.children as paper.Path[]).forEach((child) => {
|
||||||
|
collectPathPoints(child)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
collectPathPoints(nextPolygon)
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationId = (
|
||||||
|
targetData.operationId ||
|
||||||
|
useTopToolsStore.getState().activeOperation ||
|
||||||
|
""
|
||||||
|
).toString()
|
||||||
|
if (!operationId) {
|
||||||
|
nextPolygon.remove()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const prevDetail = safeClone(
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.getLabelDetailDataByKeys(activeImage, operationId, selectedId) ||
|
||||||
|
initialDetail
|
||||||
|
)
|
||||||
|
|
||||||
|
if (points.length) {
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.setOperation(activeImage, operationId, [
|
||||||
|
selectedId,
|
||||||
|
points,
|
||||||
|
prevDetail,
|
||||||
|
hollowPoints,
|
||||||
|
])
|
||||||
|
useObjectStore
|
||||||
|
.getState()
|
||||||
|
.updateSelectedPath(activeImage, "ADD", selectedId)
|
||||||
|
} else {
|
||||||
|
nextPolygon.remove()
|
||||||
|
useLabelStore
|
||||||
|
.getState()
|
||||||
|
.deleteOperation(activeImage, operationId, selectedId)
|
||||||
|
useObjectStore.getState().updateSelectedPath(activeImage, "ADD")
|
||||||
|
const nextLabel = safeClone(useLabelStore.getState().label)
|
||||||
|
useLabelStore.getState().setLabel(nextLabel)
|
||||||
|
useLabelStore.getState().pushStateStack(nextLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
forceUpdate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
polygonTool.onMouseDown = (e: {
|
polygonTool.onMouseDown = (e: {
|
||||||
event: MouseEvent
|
event: MouseEvent
|
||||||
point: paper.Point
|
point: paper.Point
|
||||||
@@ -2897,35 +3031,83 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
// 初始化 compoundPolygon
|
// 初始化 compoundPolygon
|
||||||
compoundPolygon = new paper.Path()
|
compoundPolygon = new paper.Path()
|
||||||
|
|
||||||
|
if (useKeyboardStore.getState().drawAction === "subtract") {
|
||||||
|
;(polygon as paper.Path).closePath()
|
||||||
|
handleSelectedPolygonSubtract(polygon as paper.Path)
|
||||||
|
|
||||||
|
selectedCircles.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
selectedCircles = []
|
||||||
|
clearPolygonMagnet()
|
||||||
|
useTopToolsStore.getState().setPressA(false)
|
||||||
|
draftId = null
|
||||||
|
lastPoint = null
|
||||||
|
lastTime = 0
|
||||||
|
points = []
|
||||||
|
polygon = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// pressA 模式下添加多边形
|
// pressA 模式下添加多边形
|
||||||
if (useTopToolsStore.getState().pressA) {
|
if (useTopToolsStore.getState().pressA) {
|
||||||
let ids =
|
let ids =
|
||||||
useObjectStore.getState().selectedPath[
|
useObjectStore.getState().selectedPath[
|
||||||
useBottomToolsStore.getState().activeImage
|
useBottomToolsStore.getState().activeImage
|
||||||
]
|
]
|
||||||
|
const selectedId = ids?.[0]
|
||||||
|
if (!selectedId) {
|
||||||
|
resetPolygonDraft()
|
||||||
|
return
|
||||||
|
}
|
||||||
let paths = usePaperStore.getState().getItemsById(ids[0])
|
let paths = usePaperStore.getState().getItemsById(ids[0])
|
||||||
let oldPolygon = polygon
|
let oldPolygon = polygon
|
||||||
// 闭合绘制路径
|
// 闭合绘制路径
|
||||||
oldPolygon.closePath()
|
oldPolygon.closePath()
|
||||||
|
const polygonPaths = paths.filter(
|
||||||
|
(path) => path.data?.type === "polygon"
|
||||||
|
)
|
||||||
|
const prevCompoundPath =
|
||||||
|
(polygonPaths.find(
|
||||||
|
(path) => path instanceof paper.CompoundPath
|
||||||
|
) as paper.CompoundPath | undefined) ||
|
||||||
|
((polygonPaths.find(
|
||||||
|
(path) => path.parent instanceof paper.CompoundPath
|
||||||
|
)?.parent as paper.CompoundPath | undefined) ??
|
||||||
|
undefined)
|
||||||
|
|
||||||
if (paths.length === 1) {
|
const basePathData = getSafePathData(
|
||||||
const prevPath = paths[0]
|
polygonPaths.find((path) => !!path.data)?.data || {}
|
||||||
|
)
|
||||||
|
oldPolygon.data = Object.assign({}, basePathData, oldPolygon.data, {
|
||||||
|
id: selectedId,
|
||||||
|
type: "polygon",
|
||||||
|
isHollowPolygon: !oldPolygon.clockwise,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (prevCompoundPath) {
|
||||||
|
// 已是多区域对象, 直接追加 child
|
||||||
|
prevCompoundPath.addChild(oldPolygon)
|
||||||
|
prevCompoundPath.data = Object.assign(
|
||||||
|
{},
|
||||||
|
getSafePathData(prevCompoundPath.data || {}),
|
||||||
|
basePathData,
|
||||||
|
{ id: selectedId, type: "polygon" }
|
||||||
|
)
|
||||||
|
polygon = prevCompoundPath
|
||||||
|
} else {
|
||||||
|
const prevPaths = polygonPaths.filter(
|
||||||
|
(path): path is paper.Path => path instanceof paper.Path
|
||||||
|
)
|
||||||
polygon = usePaperStore.getState().addCompoundPath(
|
polygon = usePaperStore.getState().addCompoundPath(
|
||||||
renderCompoundPath,
|
renderCompoundPath,
|
||||||
[prevPath, oldPolygon] as paper.Path[],
|
[...prevPaths, oldPolygon] as paper.Path[],
|
||||||
{
|
{
|
||||||
// option
|
// option
|
||||||
...usePaperStore.getState().toolOption,
|
...usePaperStore.getState().toolOption,
|
||||||
},
|
},
|
||||||
{ ...oldPolygon.data, id: prevPath?.data.id }
|
{ ...basePathData, ...oldPolygon.data, id: selectedId }
|
||||||
)
|
)
|
||||||
} else if (paths.length > 1) {
|
|
||||||
const prevPath = paths.filter(
|
|
||||||
(path) => path instanceof paper.CompoundPath
|
|
||||||
)[0]
|
|
||||||
// 直接将当前路径添加到原有父路径
|
|
||||||
prevPath.addChild(oldPolygon)
|
|
||||||
polygon = prevPath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
}
|
}
|
||||||
let path = usePaperStore.getState().getItemById(ids[0])
|
let path = usePaperStore.getState().getItemById(ids[0])
|
||||||
if (path && path.data.type !== "rectangle") {
|
if (path && path.data.type !== "rectangle") {
|
||||||
let flag = val && usePaperStore.getState().mode !== "rectangle"
|
let flag = val
|
||||||
|
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
71
components/label/utils/objectVisibility.ts
Normal file
71
components/label/utils/objectVisibility.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import paper from "paper"
|
||||||
|
import { useLabelStore, useObjectStore } from "../store"
|
||||||
|
import { usePaperStore } from "../usePaperStore"
|
||||||
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
|
||||||
|
const getTagVisibility = (tagCategory?: string) => {
|
||||||
|
const { showTags, showTagsConfigs } = useTopToolsStore.getState()
|
||||||
|
if (!showTags) return false
|
||||||
|
if (tagCategory === "mask") return showTagsConfigs.includes("外框")
|
||||||
|
if (tagCategory === "point_text") return showTagsConfigs.includes("点ID")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelectedItemFalse = (id: number) => {
|
||||||
|
if (!id) return
|
||||||
|
const sameIdItems = usePaperStore.getState().getItemsById(id)
|
||||||
|
sameIdItems.forEach((item) => {
|
||||||
|
if (item.data.type === "mask" || item.data.type === "text") {
|
||||||
|
item.remove()
|
||||||
|
}
|
||||||
|
if (item.data.selected) item.data.selected = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncObjectItemsVisibility = (objectId: number, hide: boolean) => {
|
||||||
|
const sameIdItems = usePaperStore.getState().getItemsById(objectId)
|
||||||
|
if (!sameIdItems?.length) return
|
||||||
|
sameIdItems.forEach((item) => {
|
||||||
|
if (hide) {
|
||||||
|
item.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.data?.type === "tag") {
|
||||||
|
item.visible = getTagVisibility(item.data?.tag_category)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item.visible = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleObjectHideByShortcut = (
|
||||||
|
imageId: string,
|
||||||
|
operationId: string,
|
||||||
|
objectId: number,
|
||||||
|
hide: boolean
|
||||||
|
) => {
|
||||||
|
const { updatePathStatus, updateOperationStatus } = useObjectStore.getState()
|
||||||
|
const storeLabel = useLabelStore.getState().label
|
||||||
|
|
||||||
|
if (hide) {
|
||||||
|
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||||
|
syncObjectItemsVisibility(objectId, hide)
|
||||||
|
setSelectedItemFalse(objectId)
|
||||||
|
|
||||||
|
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
|
||||||
|
const hasVisibleChild = operationChildren.some((child) => {
|
||||||
|
const item = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemById(child[0]) as paper.Item | null
|
||||||
|
return !!item?.visible
|
||||||
|
})
|
||||||
|
if (!hasVisibleChild) {
|
||||||
|
updateOperationStatus(imageId + operationId, "HIDE", hide)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOperationStatus(imageId + operationId, "HIDE", hide)
|
||||||
|
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||||
|
syncObjectItemsVisibility(objectId, hide)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user