From f9ce43a6e549c60b9c10d644ba03d1480940d965 Mon Sep 17 00:00:00 2001 From: zhangyang8 Date: Wed, 15 Jul 2026 10:33:38 +0800 Subject: [PATCH 1/3] feat: Keep pre-annotation option when clearing results --- .../pointCloudView/PointCloudListener.tsx | 15 ++- .../hooks/usePointCloudViews.ts | 92 ++++++++++++++++++- .../lb-components/src/hooks/annotation.ts | 2 +- .../GeneralOperation/ActionsConfirm.tsx | 29 ++++-- .../sidebar/GeneralOperation/index.tsx | 47 +++++++++- packages/lb-utils/src/i18n/resources.json | 4 + 6 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/lb-components/src/components/pointCloudView/PointCloudListener.tsx b/packages/lb-components/src/components/pointCloudView/PointCloudListener.tsx index 932795f8f..2345b8dee 100644 --- a/packages/lb-components/src/components/pointCloudView/PointCloudListener.tsx +++ b/packages/lb-components/src/components/pointCloudView/PointCloudListener.tsx @@ -60,7 +60,7 @@ const PointCloudListener: React.FC = ({ const { updateRotate } = useRotate({ currentData }); const { updateRotateEdge } = useRotateEdge({ currentData }); - const { updatePointCloudData, topViewSelectedChanged } = usePointCloudViews({ + const { updatePointCloudData, restorePreResult, topViewSelectedChanged } = usePointCloudViews({ setResourceLoading, }); const { @@ -327,7 +327,18 @@ const PointCloudListener: React.FC = ({ } } }; - toolInstanceRef.current.clearResult = () => { + toolInstanceRef.current.clearResult = async (options?: { keepPreResult?: boolean }) => { + if (options?.keepPreResult) { + try { + const restored = await restorePreResult?.(); + if (!restored) { + message.error(t('ClearKeepPreAnnotationFailed')); + } + } catch { + message.error(t('ClearKeepPreAnnotationFailed')); + } + return; + } clearAllResult?.(); }; diff --git a/packages/lb-components/src/components/pointCloudView/hooks/usePointCloudViews.ts b/packages/lb-components/src/components/pointCloudView/hooks/usePointCloudViews.ts index cc4d96156..2b51e85cb 100644 --- a/packages/lb-components/src/components/pointCloudView/hooks/usePointCloudViews.ts +++ b/packages/lb-components/src/components/pointCloudView/hooks/usePointCloudViews.ts @@ -650,7 +650,8 @@ export const usePointCloudViews = (params?: IUsePointCloudViewsParams) => { history, linkageImageNameRectMap, } = ptCtx; - const { addHistory, initHistory, pushHistoryUnderUpdatePolygon } = useHistory(); + const { addHistory, initHistory, pushHistoryUnderUpdatePolygon, pushHistoryWithList } = + useHistory(); const { selectedPolygon } = usePolygon(); const { getPointCloudSphereByID, updatePointCloudSphere, selectedSphere } = useSphere(); @@ -721,6 +722,7 @@ export const usePointCloudViews = (params?: IUsePointCloudViewsParams) => { topViewSelectedChanged: () => {}, sideViewUpdateBox: () => {}, backViewUpdateBox: () => {}, + restorePreResult: async () => false, }; } @@ -1427,6 +1429,93 @@ export const usePointCloudViews = (params?: IUsePointCloudViewsParams) => { params?.setResourceLoading?.(false); }; + /** + * Restore current frame to pre-annotation only (no PCD reload). + * @returns whether restore succeeded + */ + const restorePreResult = async () => { + if (!currentData?.url || !mainViewInstance) { + return false; + } + + const preStep = jsonParser(currentData.preResult)?.[POINT_CLOUD_DEFAULT_STEP]; + if (!preStep) { + return false; + } + + const resultObj = jsonParser(currentData.result || '{}'); + resultObj[POINT_CLOUD_DEFAULT_STEP] = _.cloneDeep(preStep); + let resultString = JSON.stringify(resultObj); + const url = currentData.url; + + SetAnnotationLoading(dispatch, true); + params?.setResourceLoading?.(true); + + try { + setHighlight2DDataList([]); + setSelectedIDs(undefined); + + mainViewInstance.clearAllBox(); + mainViewInstance.clearAllSphere(); + + let boxParamsList: any[] = []; + let lineList: any[] = []; + let polygonList: any[] = []; + let sphereParamsList: IPointCloudSphere[] = []; + + ptCtx.setPointCloudValid(resolveFileItemValid(resultString, currentData.preResult)); + ptCtx.sideViewInstance?.clearAllData(); + ptCtx.backViewInstance?.clearAllData(); + + boxParamsList = PointCloudUtils.getBoxParamsFromResultList(resultString); + + if (boxParamsList?.length > 0 && config?.lowerLimitPointsNumInBox > 0) { + // @ts-ignore + boxParamsList = await mainViewInstance?.filterPreResult(url, config, boxParamsList); + const newDataResultObj = jsonParser(resultString); + newDataResultObj[POINT_CLOUD_DEFAULT_STEP].result = boxParamsList; + resultString = JSON.stringify(newDataResultObj); + ptCtx.setPointCloudResult(boxParamsList); + } + + polygonList = PointCloudUtils.getPolygonListFromResultList(resultString); + lineList = PointCloudUtils.getLineListFromResultList(resultString); + sphereParamsList = PointCloudUtils.getSphereParamsFromResultList(resultString); + const { rectList } = PointCloudUtils.parsePointCloudCurrentResult(resultString); + + // Context must be set here: unlike page-enter, imgIndex does not change to trigger PointCloudView sync. + ptCtx.setPointCloudResult(boxParamsList); + ptCtx.setPolygonList(polygonList); + ptCtx.setLineList(lineList); + ptCtx.setPointCloudSphereList(sphereParamsList); + ptCtx.setRectList(rectList); + + topViewInstance.updateData(url, resultString, { + radius: config?.radius ?? DEFAULT_RADIUS, + }); + mainViewInstance.generateBoxes(boxParamsList); + mainViewInstance.generateSpheres(sphereParamsList); + + await ptCtx.syncAllViewPointCloudColor( + EPointCloudBoxRenderTrigger.Default, + boxParamsList, + [], + ); + + pushHistoryWithList({ + pointCloudBoxList: boxParamsList, + polygonList, + lineList, + pointCloudSphereList: sphereParamsList, + }); + + return true; + } finally { + SetAnnotationLoading(dispatch, false); + params?.setResourceLoading?.(false); + } + }; + return { topViewAddSphere, topViewAddBox, @@ -1442,6 +1531,7 @@ export const usePointCloudViews = (params?: IUsePointCloudViewsParams) => { pointCloudBoxListUpdated, initPointCloud3d, updatePointCloudData, + restorePreResult, updateViewsByDefaultSize, generateRects, update2DViewRect, diff --git a/packages/lb-components/src/hooks/annotation.ts b/packages/lb-components/src/hooks/annotation.ts index 277edceeb..2e23e767d 100644 --- a/packages/lb-components/src/hooks/annotation.ts +++ b/packages/lb-components/src/hooks/annotation.ts @@ -13,7 +13,7 @@ export interface ICustomToolInstance { exportData: () => [any, {}]; exportCustomData: () => {}; singleOn: (eventName: string, callback: (...args: any[]) => void) => void; - clearResult: () => void; + clearResult: (options?: { keepPreResult?: boolean }) => void | Promise; on: (eventName: string, callback: (...args: any[]) => void) => void; unbind: () => void; setResult: (result: any) => void; diff --git a/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/ActionsConfirm.tsx b/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/ActionsConfirm.tsx index 5ee5549e4..e4e96bf2b 100644 --- a/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/ActionsConfirm.tsx +++ b/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/ActionsConfirm.tsx @@ -13,6 +13,9 @@ export interface IOperationConfig { forbidConfirm?: boolean; // 是否禁止二次确认 forbidOperation?: boolean; // 禁止操作,会有置灰操作(该部分由用户自己更改 ImgSvg 进行展示) content?: React.ReactElement; + /** Explicit extra UI under Popconfirm title; only rendered when set (opt-in). */ + confirmExtra?: React.ReactElement; + onConfirmVisibleChange?: (visible: boolean) => void; } // 禁止的样式 @@ -22,16 +25,25 @@ const forbidStyle = { const PopconfirmTitle = ({ info }: { info: IOperationConfig }) => { const { t } = useTranslation(); - if (info.key.startsWith('sure')) { - return ( -
- {`${t('ConfirmTo')}${info.name.slice(0)}?`} - {info?.content ? info.content : ''} -
- ); + const title = info.key.startsWith('sure') ? ( +
+ {`${t('ConfirmTo')}${info.name.slice(0)}?`} + {info?.content ? info.content : ''} +
+ ) : ( + {info.name} + ); + + if (!info.confirmExtra) { + return title; } - return {info.name}; + return ( +
+ {title} + {info.confirmExtra} +
+ ); }; const ActionIcon = ({ icon }: { icon: React.ReactElement | string }) => { @@ -94,6 +106,7 @@ const ActionsConfirm: React.FC<{ cancelText={t('Cancel')} getPopupContainer={() => ref.current ?? document.body} onConfirm={info.onClick} + onVisibleChange={info.onConfirmVisibleChange} overlayClassName={`${prefix}-pop-confirm`} >
diff --git a/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/index.tsx b/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/index.tsx index ec8137dd7..15b567f34 100644 --- a/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/index.tsx +++ b/packages/lb-components/src/views/MainView/sidebar/GeneralOperation/index.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { Checkbox } from 'antd'; import { AppState } from '@/store'; import { connect, ConnectedComponent } from 'react-redux'; import { ToolInstance } from '@/store/annotation/types'; @@ -17,6 +18,8 @@ import UnifyParamsModal from '../../../../components/pointCloudView/components/U import { useSingleBox } from '@/components/pointCloudView/hooks/useSingleBox'; import { composeResultByToolInstance } from '@/store/annotation/reducer'; import { useStatus } from '@/components/pointCloudView/hooks/useStatus'; +import { ICustomToolInstance } from '@/hooks/annotation'; +import { POINT_CLOUD_DEFAULT_STEP } from '@labelbee/lb-utils'; const mapStateToProps = (state: AppState) => { const stepInfo = StepUtils.getCurrentStepInfo(state.annotation?.step, state.annotation?.stepList); @@ -79,12 +82,52 @@ export const PointCloudOperation: ConnectedComponent< const operationList = useOperationList(toolInstance); const [isShowModal, setShowModal] = useState(false); const [composeImgList, setComposeImgList] = useState([]); + const [keepPreResult, setKeepPreResult] = useState(false); const { isPointCloudDetectionPattern, isPointCloudSegmentationPattern } = useStatus(); const config = jsonParser(stepInfo.config); + const currentData = imgList?.[imgIndex]; + const hasPreResult = useMemo(() => { + const preStep = jsonParser(currentData?.preResult)?.[POINT_CLOUD_DEFAULT_STEP]; + return Boolean(preStep); + }, [currentData?.preResult]); + + // Detection-only: keep-pre-annotation restore is not wired for segmentation clearResult. + const showKeepPreResultOption = hasPreResult && isPointCloudDetectionPattern; + + const emptyOperation: IOperationConfig = { + ...operationList.empty, + confirmExtra: showKeepPreResultOption ? ( +
e.stopPropagation()} + style={{ marginTop: 8, textAlign: 'left', width: '100%' }} + > + setKeepPreResult(e.target.checked)} + style={{ marginLeft: 0 }} + > + {t('KeepPreAnnotation')} + +
+ ) : undefined, + onConfirmVisibleChange: (visible) => { + if (visible) { + setKeepPreResult(false); + } + }, + onClick: () => { + const pointCloudToolInstance = toolInstance as unknown as ICustomToolInstance; + pointCloudToolInstance.clearResult({ + keepPreResult: showKeepPreResultOption && keepPreResult, + }); + setKeepPreResult(false); + }, + }; + let allOperation: IOperationConfig[] = [ operationList.copyPrevious, - operationList.empty, + emptyOperation, operationList.setValidity, ]; diff --git a/packages/lb-utils/src/i18n/resources.json b/packages/lb-utils/src/i18n/resources.json index 5bd5cd4d6..c7dd2afa8 100644 --- a/packages/lb-utils/src/i18n/resources.json +++ b/packages/lb-utils/src/i18n/resources.json @@ -21,6 +21,8 @@ "OriginalScale": "Original Scale", "CopyThePrevious": "Copy Prev", "ClearLabel": "Clear", + "KeepPreAnnotation": "Keep pre-annotation", + "ClearKeepPreAnnotationFailed": "Failed to clear and restore pre-annotation", "SetAsInvalid": "Set As Invalid", "SetAsValid": "Set As Valid", "Confirm": "Confirm", @@ -408,6 +410,8 @@ "OriginalScale": "按原图比例显示", "CopyThePrevious": "复制上张", "ClearLabel": "清空标注", + "KeepPreAnnotation": "保留预标注数据", + "ClearKeepPreAnnotationFailed": "清空并保留预标注失败", "SetAsInvalid": "标为无效", "SetAsValid": "取消无效", "Confirm": "确认", From 0d21dfaa59552ad0282704cd355397d7ee99b400 Mon Sep 17 00:00:00 2001 From: zhangyang8 Date: Wed, 15 Jul 2026 11:01:00 +0800 Subject: [PATCH 2/3] fix: Add terser-webpack-plugin --- packages/wavesurfer/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wavesurfer/package.json b/packages/wavesurfer/package.json index 345072fdb..0fa5544e6 100644 --- a/packages/wavesurfer/package.json +++ b/packages/wavesurfer/package.json @@ -83,6 +83,7 @@ "lint-staged": "^13.0.3", "load-script": "^2.0.0", "pre-commit": "^1.2.2", + "terser-webpack-plugin": "^5.6.1", "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.9.3", From 677540bc99b47fd4b8183842e84ac804ad3a38c5 Mon Sep 17 00:00:00 2001 From: zhangyang8 Date: Wed, 15 Jul 2026 11:22:55 +0800 Subject: [PATCH 3/3] chore: Add @types/node dependenc --- packages/lb-annotation/package.json | 1 + packages/lb-components/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/lb-annotation/package.json b/packages/lb-annotation/package.json index e33c0b779..bbb7787f6 100644 --- a/packages/lb-annotation/package.json +++ b/packages/lb-annotation/package.json @@ -61,6 +61,7 @@ "@rollup/plugin-node-resolve": "^11.2.1", "@types/jest": "^24.0.19", "@types/lodash": "^4.14.165", + "@types/node": "^18.19.130", "@types/three": "^0.141.0", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", diff --git a/packages/lb-components/package.json b/packages/lb-components/package.json index b95e9a20c..3f851437f 100644 --- a/packages/lb-components/package.json +++ b/packages/lb-components/package.json @@ -89,6 +89,7 @@ "@svgr/rollup": "^8.1.0", "@types/diff-match-patch": "^1.0.36", "@types/lodash": "^4.14.168", + "@types/node": "^18.19.130", "@types/react": "16.14.62", "@types/react-redux": "^7.1.16", "@types/react-syntax-highlighter": "^15.5.0",