feat(tool): help

This commit is contained in:
2026-04-18 21:55:01 +08:00
parent a57b9c4427
commit cad763d2a3
4 changed files with 402 additions and 15 deletions

View File

@@ -2024,10 +2024,22 @@ const LabelPage = ({
const mode = usePaperStore.getState().mode const mode = usePaperStore.getState().mode
const lowerKey = e.key.toLowerCase() const lowerKey = e.key.toLowerCase()
const commandKey = e.ctrlKey || e.metaKey const commandKey = e.ctrlKey || e.metaKey
const isShortcutGuideKey =
e.code === "Slash" || e.key === "?" || e.key === "/"
if (e.repeat && ["a", "b", "d", "h", "m", ",", "."].includes(lowerKey)) if (
e.repeat &&
["a", "b", "d", "h", "m", ",", ".", "/", "?"].includes(lowerKey)
)
return return
if (!e.ctrlKey && !e.metaKey && !e.altKey && isShortcutGuideKey) {
e.preventDefault()
const keyboardState = useKeyboardStore.getState()
keyboardState.setShowShortcutGuide(!keyboardState.showShortcutGuide)
return
}
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") { if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") {
e.preventDefault() e.preventDefault()
const topState = useTopToolsStore.getState() const topState = useTopToolsStore.getState()

View File

@@ -121,6 +121,49 @@ type FrameSwitchGhostState = {
animating: boolean animating: boolean
} }
const SHORTCUT_GUIDE_SECTIONS = [
{
id: "global",
title: "全局",
items: [
["? / /Shift+/", "显示或隐藏快捷键说明"],
["0-9", "切换当前标注类别"],
["滚轮", "缩放画布"],
["Ctrl / ⌘ + S", "保存当前标注"],
["Ctrl / ⌘ + Z", "撤销上一步"],
["Ctrl / ⌘ + C", "复制当前选中对象"],
["Ctrl / ⌘ + V", "粘贴到多帧"],
["Ctrl / ⌘ + F", "合并当前选中对象"],
["Ctrl / ⌘ + G", "创建对象组"],
["Alt + X", "提交任务"],
],
},
{
id: "edit",
title: "对象编辑",
items: [
["Delete", "删除当前选中对象或点"],
["H", "隐藏当前选中对象 / 全部对象"],
["M", "切换磁吸模式"],
["A", "进入选中对象补加绘制"],
["D", "进入选中对象裁剪绘制"],
["B", "续画当前选中的 polygon"],
[",", "对选中 polygon 执行相减"],
[".", "对选中 polygon / brush 执行融合"],
["Esc", "取消当前绘制或退出辅助态"],
],
},
{
id: "tool",
title: "专项工具",
items: [
["F1", "进入辅助标注 / 在辅助模式下确认"],
["I", "执行追踪"],
["P", "QA 文本场景检查错词"],
],
},
] as const
const clonePaperPoint = (point: paper.Point | null | undefined) => { const clonePaperPoint = (point: paper.Point | null | undefined) => {
if (!point) return null if (!point) return null
return new paper.Point(point.x, point.y) return new paper.Point(point.x, point.y)
@@ -183,6 +226,11 @@ const PaperContainer = (
false && updateLoadingFlag false && updateLoadingFlag
const containerRef = useRef<any>(null) const containerRef = useRef<any>(null)
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
const shortcutGuideCardRef = useRef<HTMLDivElement>(null)
const shortcutGuideDragRef = useRef<{
offsetX: number
offsetY: number
} | null>(null)
const [imgSize, setImageSize] = useState<{ const [imgSize, setImageSize] = useState<{
clientWidth: number clientWidth: number
clientHeight: number clientHeight: number
@@ -451,6 +499,21 @@ const PaperContainer = (
showTags, showTags,
showTagsConfigs, showTagsConfigs,
} = useTopToolsStore() } = useTopToolsStore()
const showShortcutGuide = useKeyboardStore(
(state) => state.showShortcutGuide
)
const shortcutGuidePosition = useKeyboardStore(
(state) => state.shortcutGuidePosition
)
const setShortcutGuidePosition = useKeyboardStore(
(state) => state.setShortcutGuidePosition
)
const shortcutGuideCollapsedSections = useKeyboardStore(
(state) => state.shortcutGuideCollapsedSections
)
const toggleShortcutGuideSection = useKeyboardStore(
(state) => state.toggleShortcutGuideSection
)
const { const {
pathStatus: paperPathStatus, pathStatus: paperPathStatus,
@@ -469,8 +532,107 @@ const PaperContainer = (
setContextMenuOperationId, setContextMenuOperationId,
} = useOtherToolsStore() } = useOtherToolsStore()
const { renderTag } = useRenderTag() const { renderTag } = useRenderTag()
const [setup, setSetup] = useState(false) const [setup, setSetup] = useState(false)
const [isShortcutGuideDragging, setIsShortcutGuideDragging] = useState(false)
const clampShortcutGuidePosition = useCallback(
(position: { x: number; y: number }) => {
const container = containerRef.current as HTMLDivElement | null
const card = shortcutGuideCardRef.current
if (!container || !card) return position
const padding = 12
const maxX = Math.max(
padding,
container.clientWidth - card.offsetWidth - padding
)
const maxY = Math.max(
padding,
container.clientHeight - card.offsetHeight - padding
)
return {
x: Math.min(Math.max(position.x, padding), maxX),
y: Math.min(Math.max(position.y, padding), maxY),
}
},
[]
)
const handleShortcutGuideDragStart = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (event.button !== 0) return
const container = containerRef.current as HTMLDivElement | null
if (!container) return
const rect = container.getBoundingClientRect()
shortcutGuideDragRef.current = {
offsetX: event.clientX - rect.left - shortcutGuidePosition.x,
offsetY: event.clientY - rect.top - shortcutGuidePosition.y,
}
setIsShortcutGuideDragging(true)
event.preventDefault()
event.stopPropagation()
},
[shortcutGuidePosition.x, shortcutGuidePosition.y]
)
useEffect(() => {
if (!isShortcutGuideDragging) return
const handleMouseMove = (event: MouseEvent) => {
const container = containerRef.current as HTMLDivElement | null
const dragState = shortcutGuideDragRef.current
if (!container || !dragState) return
const rect = container.getBoundingClientRect()
setShortcutGuidePosition(
clampShortcutGuidePosition({
x: event.clientX - rect.left - dragState.offsetX,
y: event.clientY - rect.top - dragState.offsetY,
})
)
}
const stopDragging = () => {
shortcutGuideDragRef.current = null
setIsShortcutGuideDragging(false)
}
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", stopDragging)
return () => {
window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", stopDragging)
}
}, [
clampShortcutGuidePosition,
isShortcutGuideDragging,
setShortcutGuidePosition,
])
useLayoutEffect(() => {
if (!showShortcutGuide) return
const nextPosition = clampShortcutGuidePosition(shortcutGuidePosition)
if (
nextPosition.x !== shortcutGuidePosition.x ||
nextPosition.y !== shortcutGuidePosition.y
) {
setShortcutGuidePosition(nextPosition)
}
}, [
clampShortcutGuidePosition,
imgSize?.clientHeight,
imgSize?.clientWidth,
setShortcutGuidePosition,
shortcutGuideCollapsedSections,
shortcutGuidePosition,
showShortcutGuide,
])
const labelRenderScaleRef = useRef<Record<string, number>>({}) const labelRenderScaleRef = useRef<Record<string, number>>({})
const renderedImageRef = useRef("") const renderedImageRef = useRef("")
const frameImageCacheRef = useRef<Map<string, HTMLImageElement>>(new Map()) const frameImageCacheRef = useRef<Map<string, HTMLImageElement>>(new Map())
@@ -4105,6 +4267,150 @@ const PaperContainer = (
}} }}
/> />
)} )}
{showShortcutGuide && (
<Box
style={{
position: "absolute",
top: shortcutGuidePosition.y,
left: shortcutGuidePosition.x,
zIndex: 48,
width: 264,
maxWidth: "min(264px, calc(100% - 24px))",
pointerEvents: "none",
background: "transparent",
}}>
<Card
ref={shortcutGuideCardRef}
shadow="sm"
padding="xs"
radius="md"
withBorder
onWheel={(event) => {
event.stopPropagation()
}}
style={{
pointerEvents: "auto",
maxHeight: "calc(100% - 24px)",
overflowY: "auto",
background:
"light-dark(rgba(255,255,255,0.78), rgba(24,28,36,0.74))",
borderColor:
"light-dark(rgba(34,139,230,0.18), rgba(114,192,252,0.18))",
backdropFilter: "blur(6px)",
}}>
<Stack gap={8}>
<Box
onMouseDown={handleShortcutGuideDragStart}
style={{
cursor: isShortcutGuideDragging ? "grabbing" : "grab",
userSelect: "none",
borderRadius: 10,
padding: "8px 10px",
background:
"light-dark(rgba(34,139,230,0.08), rgba(76,171,247,0.12))",
border:
"1px solid light-dark(rgba(34,139,230,0.12), rgba(114,192,252,0.14))",
}}>
<Group justify="space-between" align="flex-start" gap="xs" wrap="nowrap">
<Box>
<Text fw={700} size="sm">
</Text>
<Text size="10px" c="dimmed" style={{ lineHeight: 1.35 }}>
<Text component="span" fw={700}>?</Text>
</Text>
</Box>
<Text
size="10px"
fw={700}
px={6}
py={3}
style={{
flexShrink: 0,
borderRadius: 999,
background:
"light-dark(rgba(255,255,255,0.82), rgba(255,255,255,0.08))",
}}>
</Text>
</Group>
</Box>
{SHORTCUT_GUIDE_SECTIONS.map((section) => {
const collapsed = !!shortcutGuideCollapsedSections[section.id]
return (
<Box
key={section.id}
px="xs"
py={8}
style={{
borderRadius: 10,
background:
"light-dark(rgba(248,250,252,0.72), rgba(15,18,24,0.32))",
border:
"1px solid light-dark(rgba(34,139,230,0.08), rgba(114,192,252,0.08))",
}}>
<Group
justify="space-between"
align="center"
gap="xs"
wrap="nowrap"
onClick={() => {
toggleShortcutGuideSection(section.id)
}}
style={{
cursor: "pointer",
userSelect: "none",
}}>
<Text size="xs" fw={700} c="blue">
{section.title}
</Text>
<Text size="10px" c="dimmed" fw={700}>
{collapsed ? "展开" : "收起"}
</Text>
</Group>
{!collapsed && (
<Stack gap={6} mt={8}>
{section.items.map(([key, description]) => (
<Group
key={`${section.id}-${key}`}
justify="space-between"
align="flex-start"
gap={6}
wrap="nowrap">
<Text
size="10px"
fw={700}
px={5}
py={2}
style={{
flexShrink: 0,
borderRadius: 6,
border:
"1px solid light-dark(rgba(34,139,230,0.16), rgba(114,192,252,0.16))",
background:
"light-dark(rgba(255,255,255,0.88), rgba(255,255,255,0.06))",
}}>
{key}
</Text>
<Text
size="10px"
c="dimmed"
ta="right"
style={{ lineHeight: 1.35 }}>
{description}
</Text>
</Group>
))}
</Stack>
)}
</Box>
)
})}
</Stack>
</Card>
</Box>
)}
{contextMenu} {contextMenu}
<Modal <Modal
opened={confirmModalOpened} opened={confirmModalOpened}

View File

@@ -46,6 +46,7 @@ import {
Film, Film,
FolderDownIcon, FolderDownIcon,
Group as GroupIcon, Group as GroupIcon,
Keyboard,
Locate, Locate,
LocateFixed, LocateFixed,
LocateOff, LocateOff,
@@ -1560,6 +1561,10 @@ const TopTools = (
const operationSchema = getOperationSchema(activeOperation) const operationSchema = getOperationSchema(activeOperation)
const drawAction = useKeyboardStore((state) => state.drawAction) const drawAction = useKeyboardStore((state) => state.drawAction)
const showShortcutGuide = useKeyboardStore((state) => state.showShortcutGuide)
const setShowShortcutGuide = useKeyboardStore(
(state) => state.setShowShortcutGuide
)
const subAttrForm = ( const subAttrForm = (
<Box w={256}> <Box w={256}>
@@ -2529,6 +2534,23 @@ const TopTools = (
<Component style={{ width: 20, height: 20 }} /> <Component style={{ width: 20, height: 20 }} />
<Text size="xs"></Text> <Text size="xs"></Text>
</ActionIcon> </ActionIcon>
<Tooltip
label={
showShortcutGuide
? "关闭快捷键说明(?"
: "打开快捷键说明(?"
}>
<ActionIcon
variant="transparent"
c={showShortcutGuide ? "var(--mantine-color-text)" : "gray"}
style={{ width: "auto", display: "flex", gap: 4 }}
onClick={() => {
setShowShortcutGuide(!showShortcutGuide)
}}>
<Keyboard style={{ width: 20, height: 20 }} />
<Text size="xs"></Text>
</ActionIcon>
</Tooltip>
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -1,4 +1,5 @@
import { create } from "zustand" import { create } from "zustand"
import { persist } from "zustand/middleware"
export interface SelectedObjectEditTarget { export interface SelectedObjectEditTarget {
imageId: string imageId: string
@@ -12,6 +13,15 @@ interface KeyboardStore {
setCtrl: (v: boolean) => void setCtrl: (v: boolean) => void
shift: boolean shift: boolean
setShift: (v: boolean) => void setShift: (v: boolean) => void
showShortcutGuide: boolean
setShowShortcutGuide: (v: boolean) => void
shortcutGuidePosition: {
x: number
y: number
}
setShortcutGuidePosition: (position: { x: number; y: number }) => void
shortcutGuideCollapsedSections: Record<string, boolean>
toggleShortcutGuideSection: (sectionId: string) => void
drawAction: "none" | "add" | "subtract" drawAction: "none" | "add" | "subtract"
setDrawAction: (mode: "none" | "add" | "subtract") => void setDrawAction: (mode: "none" | "add" | "subtract") => void
selectedObjectEditTarget: SelectedObjectEditTarget | null selectedObjectEditTarget: SelectedObjectEditTarget | null
@@ -20,16 +30,53 @@ interface KeyboardStore {
setCopyDataIds: (ids: number[]) => void setCopyDataIds: (ids: number[]) => void
} }
export const useKeyboardStore = create<KeyboardStore>((set) => ({ export const useKeyboardStore = create<KeyboardStore>()(
persist(
(set) => ({
ctrl: false, ctrl: false,
shift: false, shift: false,
showShortcutGuide: false,
shortcutGuidePosition: {
x: 16,
y: 16,
},
shortcutGuideCollapsedSections: {},
drawAction: "none", drawAction: "none",
selectedObjectEditTarget: null, selectedObjectEditTarget: null,
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 })), setShowShortcutGuide: (v) =>
set((state) => ({ ...state, showShortcutGuide: v })),
setShortcutGuidePosition: (position) =>
set((state) => ({
...state,
shortcutGuidePosition: {
x: Math.round(position.x),
y: Math.round(position.y),
},
})),
toggleShortcutGuideSection: (sectionId) =>
set((state) => ({
...state,
shortcutGuideCollapsedSections: {
...state.shortcutGuideCollapsedSections,
[sectionId]: !state.shortcutGuideCollapsedSections[sectionId],
},
})),
setDrawAction: (mode) =>
set((state) => ({ ...state, drawAction: mode })),
setSelectedObjectEditTarget: (target) => setSelectedObjectEditTarget: (target) =>
set((state) => ({ ...state, selectedObjectEditTarget: target })), set((state) => ({ ...state, selectedObjectEditTarget: target })),
setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })), setCopyDataIds: (ids) =>
})) set((state) => ({ ...state, copyDataIds: ids })),
}),
{
name: "keyboard-store",
partialize: (state) => ({
shortcutGuidePosition: state.shortcutGuidePosition,
shortcutGuideCollapsedSections: state.shortcutGuideCollapsedSections,
}),
}
)
)