diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 21d989f..28721b9 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -2024,10 +2024,22 @@ const LabelPage = ({ const mode = usePaperStore.getState().mode const lowerKey = e.key.toLowerCase() 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 + 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") { e.preventDefault() const topState = useTopToolsStore.getState() diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index d354a2b..c6856b3 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -121,6 +121,49 @@ type FrameSwitchGhostState = { 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) => { if (!point) return null return new paper.Point(point.x, point.y) @@ -183,6 +226,11 @@ const PaperContainer = ( false && updateLoadingFlag const containerRef = useRef(null) const contextMenuRef = useRef(null) + const shortcutGuideCardRef = useRef(null) + const shortcutGuideDragRef = useRef<{ + offsetX: number + offsetY: number + } | null>(null) const [imgSize, setImageSize] = useState<{ clientWidth: number clientHeight: number @@ -451,6 +499,21 @@ const PaperContainer = ( showTags, showTagsConfigs, } = 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 { pathStatus: paperPathStatus, @@ -469,8 +532,107 @@ const PaperContainer = ( setContextMenuOperationId, } = useOtherToolsStore() const { renderTag } = useRenderTag() - 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) => { + 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>({}) const renderedImageRef = useRef("") const frameImageCacheRef = useRef>(new Map()) @@ -4105,6 +4267,150 @@ const PaperContainer = ( }} /> )} + {showShortcutGuide && ( + + { + 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)", + }}> + + + + + + 快捷键说明 + + + 默认靠左显示,可拖动;按 ? 快速开关 + + + + 拖动 + + + + {SHORTCUT_GUIDE_SECTIONS.map((section) => { + const collapsed = !!shortcutGuideCollapsedSections[section.id] + + return ( + + { + toggleShortcutGuideSection(section.id) + }} + style={{ + cursor: "pointer", + userSelect: "none", + }}> + + {section.title} + + + {collapsed ? "展开" : "收起"} + + + {!collapsed && ( + + {section.items.map(([key, description]) => ( + + + {key} + + + {description} + + + ))} + + )} + + ) + })} + + + + )} {contextMenu} state.drawAction) + const showShortcutGuide = useKeyboardStore((state) => state.showShortcutGuide) + const setShowShortcutGuide = useKeyboardStore( + (state) => state.setShowShortcutGuide + ) const subAttrForm = ( @@ -2529,6 +2534,23 @@ const TopTools = ( 对象 + + { + setShowShortcutGuide(!showShortcutGuide) + }}> + + 快捷键 + + diff --git a/components/label/useKeyBoardStore.tsx b/components/label/useKeyBoardStore.tsx index 0dd39f6..868baf5 100644 --- a/components/label/useKeyBoardStore.tsx +++ b/components/label/useKeyBoardStore.tsx @@ -1,4 +1,5 @@ import { create } from "zustand" +import { persist } from "zustand/middleware" export interface SelectedObjectEditTarget { imageId: string @@ -12,6 +13,15 @@ interface KeyboardStore { setCtrl: (v: boolean) => void shift: boolean setShift: (v: boolean) => void + showShortcutGuide: boolean + setShowShortcutGuide: (v: boolean) => void + shortcutGuidePosition: { + x: number + y: number + } + setShortcutGuidePosition: (position: { x: number; y: number }) => void + shortcutGuideCollapsedSections: Record + toggleShortcutGuideSection: (sectionId: string) => void drawAction: "none" | "add" | "subtract" setDrawAction: (mode: "none" | "add" | "subtract") => void selectedObjectEditTarget: SelectedObjectEditTarget | null @@ -20,16 +30,53 @@ interface KeyboardStore { setCopyDataIds: (ids: number[]) => void } -export const useKeyboardStore = create((set) => ({ - ctrl: false, - shift: false, - drawAction: "none", - selectedObjectEditTarget: null, - copyDataIds: [], - setCtrl: (v) => set((state) => ({ ...state, ctrl: v })), - setShift: (v) => set((state) => ({ ...state, shift: v })), - setDrawAction: (mode) => set((state) => ({ ...state, drawAction: mode })), - setSelectedObjectEditTarget: (target) => - set((state) => ({ ...state, selectedObjectEditTarget: target })), - setCopyDataIds: (ids) => set((state) => ({ ...state, copyDataIds: ids })), -})) +export const useKeyboardStore = create()( + persist( + (set) => ({ + ctrl: false, + shift: false, + showShortcutGuide: false, + shortcutGuidePosition: { + x: 16, + y: 16, + }, + shortcutGuideCollapsedSections: {}, + drawAction: "none", + selectedObjectEditTarget: null, + copyDataIds: [], + setCtrl: (v) => set((state) => ({ ...state, ctrl: v })), + setShift: (v) => set((state) => ({ ...state, shift: v })), + 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) => + set((state) => ({ ...state, selectedObjectEditTarget: target })), + setCopyDataIds: (ids) => + set((state) => ({ ...state, copyDataIds: ids })), + }), + { + name: "keyboard-store", + partialize: (state) => ({ + shortcutGuidePosition: state.shortcutGuidePosition, + shortcutGuideCollapsedSections: state.shortcutGuideCollapsedSections, + }), + } + ) +)