From b6ae2a6a2907b1393851bb79a72bd6a44bdc6f0e Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 16:59:44 +0800 Subject: [PATCH] feat(shortcut): ctrl+f --- components/label/LabelNossr.tsx | 148 +++++++++++++++++++++++ components/label/components/TopTools.tsx | 1 + 2 files changed, 149 insertions(+) diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index a357099..f13b8d5 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -1376,6 +1376,141 @@ const LabelPage = ({ [getOperationSchema, setActiveOperation] ) + const handleMergeSelectedObjects = useCallback(() => { + const activeImage = useBottomToolsStore.getState().activeImage + const selectedIds = + useObjectStore.getState().selectedPath[activeImage] || [] + if (selectedIds.length <= 1) { + notifications.show({ + color: "yellow", + message: "请先选中同类别且数量大于1的对象", + }) + return + } + + const nextLabel = safeClone(useLabelStore.getState().label) + const imageLabel = nextLabel.get(activeImage) + if (!imageLabel) return + + const selectedIdSet = new Set(selectedIds) + const selectedByOperation = Array.from(imageLabel.entries()) + .map(([operationId, paths]) => { + const selectedPaths = paths.filter((item) => selectedIdSet.has(item[0])) + return { operationId, selectedPaths } + }) + .filter((item) => item.selectedPaths.length > 0) + + if ( + selectedByOperation.length !== 1 || + selectedByOperation[0].selectedPaths.length <= 1 || + selectedByOperation[0].selectedPaths.length !== selectedIds.length + ) { + notifications.show({ + color: "yellow", + message: "仅支持合并同类别且数量大于1的选中对象", + }) + return + } + + const { operationId, selectedPaths } = selectedByOperation[0] + const selectedTypeList = selectedIds + .map((id) => usePaperStore.getState().getItemById(id)?.data?.type) + .filter((type): type is string => typeof type === "string") + const selectedTypeSet = new Set(selectedTypeList) + const selectedType = Array.from(selectedTypeSet)[0] + const operationSchema = getOperationSchema(operationId) + const operationType = operationSchema + ? labelTypeMap.get(operationSchema.label_type) + : "" + + if ( + selectedTypeList.length !== selectedIds.length || + selectedTypeSet.size !== 1 || + !["polygon", "brush"].includes(selectedType) || + !["多边形", "多线段"].includes(operationType || "") + ) { + notifications.show({ + color: "yellow", + message: "仅支持合并 polygon/brush 类型对象", + }) + return + } + + const sortedSelectedPaths = [...selectedPaths].sort((a, b) => a[0] - b[0]) + const targetId = sortedSelectedPaths[0][0] + const mergedPaths = sortedSelectedPaths.flatMap((item) => + safeClone(item[1] || []) + ) + const mergedCompoundPaths = sortedSelectedPaths.flatMap((item) => + safeClone(item[3] || []) + ) + const mergedDetail = { + ...(safeClone(sortedSelectedPaths[0][2]) || {}), + parentGroupId: null, + } + + const mergedData: [number, any[], any, any[]] = [ + targetId, + mergedPaths, + mergedDetail, + mergedCompoundPaths, + ] + + const restPaths = (imageLabel.get(operationId) || []).filter( + (item) => !selectedIdSet.has(item[0]) + ) + imageLabel.set( + operationId, + [...restPaths, mergedData].sort((a, b) => a[0] - b[0]) + ) + + const nextPathGroupMap = safeClone( + useRightToolsStore.getState().pathGroupMap + ) + const imageGroupMap = nextPathGroupMap.get(activeImage) + if (imageGroupMap) { + for (let [groupId, pathIds] of imageGroupMap.entries()) { + const nextPathIds = pathIds.filter((id) => !selectedIdSet.has(id)) + if (nextPathIds.length > 1) { + imageGroupMap.set(groupId, nextPathIds) + } else { + imageGroupMap.delete(groupId) + } + } + if (!imageGroupMap.size) nextPathGroupMap.delete(activeImage) + useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap) + } + + useLabelStore.getState().setLabel(nextLabel) + useLabelStore.getState().pushStateStack(nextLabel) + useObjectStore.setState((state) => ({ + ...state, + selectedPath: { + ...state.selectedPath, + [activeImage]: [targetId], + }, + })) + + const paperGroup = usePaperStore.getState().group + if (paperGroup) { + paperGroup + .getItems({}) + .filter((item) => item.data.id) + .forEach((item) => { + item.remove() + }) + + for (const [categoryId] of imageLabel.entries()) { + paperContainerRef.current?.renderPolygons?.(categoryId, imageLabel) + } + } + + notifications.show({ + color: "green", + message: `已合并${selectedIds.length}个对象`, + }) + }, [getOperationSchema]) + useEffect(() => { const down = (e: KeyboardEvent) => { // console.log(e, e.key); @@ -1384,6 +1519,18 @@ const LabelPage = ({ console.log(e.key) const mode = usePaperStore.getState().mode + if (e.altKey && e.key.toLowerCase() === "x") { + e.preventDefault() + if (!isView) topRef.current?.commitTaskByActionKey?.() + return + } + + if (e.ctrlKey && e.key.toLowerCase() === "f") { + e.preventDefault() + handleMergeSelectedObjects() + return + } + if (e.key === "Alt" && mode !== "support") { e.preventDefault() // console.log(e.key); @@ -1601,6 +1748,7 @@ const LabelPage = ({ editMode, focusInput, getOperationSchema, + handleMergeSelectedObjects, handleShortcut, isView, projectDetail?.label_type, diff --git a/components/label/components/TopTools.tsx b/components/label/components/TopTools.tsx index ae55a0d..8ef91a4 100644 --- a/components/label/components/TopTools.tsx +++ b/components/label/components/TopTools.tsx @@ -1638,6 +1638,7 @@ const TopTools = ( useImperativeHandle(ref, () => ({ handleSave, handleBackup, + commitTaskByActionKey, })) const renderEditIcon = useMemo(() => {