From d7d2bbf43fc28ca05abd8554f8bdcd16172e0760 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 11:00:08 +0800 Subject: [PATCH] fix(label): fix bug --- components/label/LabelNossr.tsx | 1 - .../label/components/PaperContainer.tsx | 86 +++++- .../label/components/RightObjectTools.tsx | 256 +++++++++++++----- components/label/usePaperStore.ts | 148 ++++++---- 4 files changed, 361 insertions(+), 130 deletions(-) diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 7af68f8..a357099 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -1642,7 +1642,6 @@ const LabelPage = ({ showGroupList, showDescList && projectDetail && projectDetail.label_type === 5, showDescList && projectDetail && projectDetail.label_type === 6, - showDescList && projectDetail && projectDetail.label_type === 7, showObjectList, ] let count = 0 diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 5ce17ae..99d7aac 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -26,6 +26,7 @@ import React, { useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, useState, @@ -94,6 +95,12 @@ const renderRaster = (paperGroup: paper.Group, option: any) => { } type NormalizedPoint = [number, number] +type ContextMenuLayout = { + top: number + left: number + maxHeight: number + maxWidth: number +} const normalizePoint = (value: any): NormalizedPoint | null => { if (value instanceof paper.Point) return [value.x, value.y] @@ -133,10 +140,19 @@ const PaperContainer = ( ) => { const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props const containerRef = useRef(null) + const contextMenuRef = useRef(null) const [imgSize, setImageSize] = useState<{ clientWidth: number clientHeight: number }>() + const [contextMenuLayout, setContextMenuLayout] = useState( + { + top: 0, + left: 0, + maxHeight: 0, + maxWidth: 0, + } + ) const [firstRender, setFirstRender] = useState(false) const [confirmModalOpened, setConfirmModalOpened] = useState(false) @@ -1918,6 +1934,61 @@ const PaperContainer = ( ?.find((item) => item[0] === id) }, [activeImage, id, operationId, storeLabel]) + useLayoutEffect(() => { + if (!showContextMenu) return + const container = containerRef.current + const menu = contextMenuRef.current + if (!container || !menu) return + + const safeMargin = 8 + const updateMenuLayout = () => { + const availableWidth = Math.max(container.clientWidth - safeMargin * 2, 0) + const availableHeight = Math.max( + container.clientHeight - safeMargin * 2, + 0 + ) + const menuWidth = Math.min(menu.offsetWidth, availableWidth) + const menuHeight = Math.min(menu.offsetHeight, availableHeight) + const nextLeft = Math.min( + Math.max(position.left, safeMargin), + Math.max(container.clientWidth - menuWidth - safeMargin, safeMargin) + ) + const nextTop = Math.min( + Math.max(position.top, safeMargin), + Math.max(container.clientHeight - menuHeight - safeMargin, safeMargin) + ) + + setContextMenuLayout((prev) => { + if ( + prev.left === nextLeft && + prev.top === nextTop && + prev.maxHeight === availableHeight && + prev.maxWidth === availableWidth + ) { + return prev + } + return { + left: nextLeft, + top: nextTop, + maxHeight: availableHeight, + maxWidth: availableWidth, + } + }) + } + + updateMenuLayout() + + const resizeObserver = new ResizeObserver(() => { + updateMenuLayout() + }) + resizeObserver.observe(container) + resizeObserver.observe(menu) + + return () => { + resizeObserver.disconnect() + } + }, [position.left, position.top, showContextMenu]) + // 打开弹窗时,初始化form数据 useEffect(() => { if (showContextMenu) { @@ -2264,6 +2335,7 @@ const PaperContainer = ( return ( diff --git a/components/label/components/RightObjectTools.tsx b/components/label/components/RightObjectTools.tsx index b51b728..f1e9aa9 100644 --- a/components/label/components/RightObjectTools.tsx +++ b/components/label/components/RightObjectTools.tsx @@ -628,16 +628,48 @@ const RightObjectTools: React.FC = (props) => { {labelState.get(activeImage) && labelState.get(activeImage)?.map((operationId) => { + const operationSchema = getOperationSchema(operationId) const isCollapsed = operationStatus[activeImage + operationId]?.["COLLAPSE"] const isHide = operationStatus[activeImage + operationId]?.HIDE const hasSubAttrError = getOperationSubAttrStatus(operationId) + const operationLabel = operationSchema?.label_class + const operationChildren = + storeLabel.get(activeImage)?.get(operationId) || [] + const objectCount = operationChildren.length + const visibleChildren = operationChildren.filter((child) => { + const { comment } = child[2] + let visibleFlag = true + if (comment && comment.length) { + const latest_comment = + (taskDetail && taskDetail.label_status === 2) || + comment.length === 1 + ? comment[comment.length - 1] + : comment[comment.length - 2] + let type = latest_comment.comment_type + if (type === 2) { + const { last_modified_timestamp } = child[2] + if ( + last_modified_timestamp && + last_modified_timestamp > latest_comment.date + ) + type = 3 + } + visibleFlag = checkBoxsChecked[3 - type] + } + if (checkBoxsChecked.every((value) => !value)) + visibleFlag = true + return visibleFlag + }) return ( - - + + = (props) => { )} - {renderOperationIcon( - getOperationSchema(operationId) - )} - - {renderShortcutKey(operationId)} - - - - {getOperationSchema(operationId)?.label_class} - - + + + {renderOperationIcon(operationSchema)} + + {renderShortcutKey(operationId)} + + + + + {operationLabel} + + + + + = (props) => { handleSelectedBySearchSubAttr(e, operationId) } /> - - {storeLabel.get(activeImage)?.get(operationId) - ?.length || 0} - + + {objectCount} + = (props) => { - - {storeLabel - .get(activeImage) - ?.get(operationId) - ?.map((child) => { - const { comment } = child[2] - let visibleFlag = true - if (comment && comment.length) { - const latest_comment = - (taskDetail && - taskDetail.label_status === 2) || - comment.length === 1 - ? comment[comment.length - 1] - : comment[comment.length - 2] - let type = latest_comment.comment_type - if (type === 2) { - const { last_modified_timestamp } = child[2] - if ( - last_modified_timestamp && - last_modified_timestamp > - latest_comment.date - ) - type = 3 - } - visibleFlag = checkBoxsChecked[3 - type] - } - if (checkBoxsChecked.every((value) => !value)) - visibleFlag = true + + {visibleChildren.map((child, childIndex) => { + const { comment } = child[2] + const isSelected = selectedPath[ + activeImage + ]?.includes(child[0]) + const isChildHide = + pathStatus[activeImage + child[0]]?.["HIDE"] + const hasChildError = getSubAttrStatus( + getOperationSchema(operationId), + child[2]?.sub_attributes + ) + const isLastVisibleChild = + childIndex === visibleChildren.length - 1 - const isSelected = selectedPath[ - activeImage - ]?.includes(child[0]) - const isChildHide = - pathStatus[activeImage + child[0]]?.["HIDE"] - const hasChildError = getSubAttrStatus( - getOperationSchema(operationId), - child[2]?.sub_attributes - ) + let textColor = undefined + let connectorColor = "var(--mantine-color-gray-4)" - let textColor = undefined + if (hasChildError) { + textColor = "red" + connectorColor = "var(--mantine-color-red-3)" + } else if (isSelected) { + textColor = "white" + connectorColor = + "var(--mantine-primary-color-filled)" + } - if (hasChildError) { - textColor = "red" - } else if (isSelected) { - textColor = "white" - } - - if (!visibleFlag) return null - - return ( + return ( + + + {isLastVisibleChild ? ( + + ) : ( + <> + + + + )} = (props) => { bg={ isSelected ? "var(--mantine-primary-color-filled)" - : undefined + : "var(--mantine-color-gray-0)" } c={isSelected ? "white" : undefined} onClick={() => @@ -944,8 +1053,9 @@ const RightObjectTools: React.FC = (props) => { ) : null} - ) - })} + + ) + })} diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index c9b84fb..1800651 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -290,6 +290,49 @@ const handleMouseMove = (e: paper.MouseEvent) => { usePositionStore.getState().setCurrentMousePosition([e.point.x, e.point.y]) } +const hidePaperContextMenu = () => { + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: false, + position: { + top: 0, + left: 0, + }, + imageId: useOtherToolsStore.getState().imageId, + operationId: useOtherToolsStore.getState().operationId, + id: useOtherToolsStore.getState().id, + }) +} + +let isMiddleMousePanning = false + +const startMiddleMousePan = (event?: MouseEvent) => { + if (!event || event.button !== 1) return false + + event.preventDefault() + isMiddleMousePanning = true + hidePaperContextMenu() + + return true +} + +const dragMiddleMousePan = (e: { event: MouseEvent; delta: paper.Point }) => { + if (!isMiddleMousePanning) return false + + e.event.preventDefault() + usePaperStore.getState().group!.position = usePaperStore + .getState() + .group!.position.add(e.delta) + + return true +} + +const stopMiddleMousePan = () => { + if (!isMiddleMousePanning) return false + + isMiddleMousePanning = false + return true +} + const { user_id } = usePermissionStore.getState() type OrderPoint = [number, number] @@ -427,45 +470,23 @@ export const clearSupportAnnotationItem = () => { } // 根据点击位置判断弹窗弹出的top、left -const containerWidth = 244 -const containerHeight = 240 export const getShowContextMenuPosition = ( clickPoint: paper.Point, - category: Project.LabelSchemaList, + _category: Project.LabelSchemaList, offsetWidth = 0, offsetHeight = 0 ) => { - let subAttrHeight = 0 - if (category) { - const { sub_attributes_describe } = category - console.log(sub_attributes_describe) - - if (sub_attributes_describe.length) subAttrHeight += 30 - sub_attributes_describe.forEach((item) => { - if (item.chinese_name) subAttrHeight += 32 - if (item.optional_item && item.optional_item.length) - item.optional_item.forEach(() => { - subAttrHeight += 22 - }) - if (item.select_mode === 0) subAttrHeight += 32 - }) - } - - const finalHeight = containerHeight + subAttrHeight const element = document.getElementById("paper-container") - const rasterPos = [element!.clientWidth, element!.clientHeight] - console.log(rasterPos, finalHeight) - let left = - clickPoint.x + containerWidth > rasterPos[0] - ? clickPoint.x + offsetWidth - containerWidth - : clickPoint.x + offsetWidth - let top = - clickPoint.y + finalHeight > rasterPos[1] - ? clickPoint.y + offsetHeight - finalHeight < 0 - ? 0 - : clickPoint.y + offsetHeight - finalHeight - : clickPoint.y + offsetHeight - console.log("转换之后的x、y", left, top) + const containerWidth = element?.clientWidth ?? 0 + const containerHeight = element?.clientHeight ?? 0 + const left = Math.min( + Math.max(clickPoint.x + offsetWidth, 0), + containerWidth || clickPoint.x + offsetWidth + ) + const top = Math.min( + Math.max(clickPoint.y + offsetHeight, 0), + containerHeight || clickPoint.y + offsetHeight + ) return { top, @@ -722,25 +743,13 @@ export const usePaperStore = create((set) => ({ } panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { + if (startMiddleMousePan(e.event)) return if (pressCtrl) return const currentTime = Date.now() const timeDelta = currentTime - lastTime if (e.event && e.event.button === 0) { - useOtherToolsStore.getState().setShowContextMenu({ - showContextMenu: false, - position: { - top: 0, - left: 0, - }, - imageId: useOtherToolsStore.getState().imageId, - operationId: useOtherToolsStore.getState().operationId, - id: useOtherToolsStore.getState().id, - }) - } else if (e.event && e.event.button === 1) { - // 鼠标中键操作图片移动 - panMode = "item" - return + hidePaperContextMenu() } if (useTopToolsStore.getState().isView) { panMode = "" @@ -1433,12 +1442,7 @@ export const usePaperStore = create((set) => ({ downPoint: paper.Point delta: paper.Point }) => { - // 优先判断移动图片 - if (panMode === "item") { - usePaperStore.getState().group!.position = usePaperStore - .getState() - .group?.position.add(e.delta)! - } + if (dragMiddleMousePan(e)) return if (useTopToolsStore.getState().isView) return @@ -1677,6 +1681,7 @@ export const usePaperStore = create((set) => ({ // 改变对象不能用 activeImage activeOperation (因为图形只在对应图片渲染,所以activeImage是一样的) panTool.onMouseUp = (_e: paper.ToolEvent) => { + if (stopMiddleMousePan()) return if (useTopToolsStore.getState().isView) return let newDrawPath: paper.Path | paper.CompoundPath | null = null @@ -2775,6 +2780,7 @@ export const usePaperStore = create((set) => ({ event: MouseEvent point: paper.Point }) => { + if (startMiddleMousePan(e.event)) return if (e.event.button !== 0) return isMouseDown = true @@ -3067,7 +3073,14 @@ export const usePaperStore = create((set) => ({ lastPoint = checkPoint lastTime = Date.now() } + polygonTool.onMouseDrag = (e: { + event: MouseEvent + delta: paper.Point + }) => { + if (dragMiddleMousePan(e)) return + } polygonTool.onMouseUp = (_e: paper.ToolEvent) => { + if (stopMiddleMousePan()) return isMouseDown = false } polygonTool.onMouseMove = (e: paper.MouseEvent) => { @@ -3278,6 +3291,7 @@ export const usePaperStore = create((set) => ({ let rectangleTool = paperRectangleTool rectangleTool.onMouseDown = (e: { event: MouseEvent }) => { + if (startMiddleMousePan(e.event)) return if (e.event && e.event.button !== 2) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: false, @@ -3300,7 +3314,9 @@ export const usePaperStore = create((set) => ({ event: MouseEvent point: paper.Point downPoint: paper.Point + delta: paper.Point }) => { + if (dragMiddleMousePan(e)) return const delta = e.point.subtract(e.downPoint) // 阻止右键移动矩形(e.event.button === 0) if (delta.length > 10 && e.event.button === 0) { @@ -3337,6 +3353,7 @@ export const usePaperStore = create((set) => ({ event: MouseEvent point: paper.Point }) => { + if (stopMiddleMousePan()) return // add segment judgement if (rect && rect.segments?.length && rect?.data?.id) { // 去除超出图片边界的部分 @@ -3558,6 +3575,7 @@ export const usePaperStore = create((set) => ({ } brushTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { + if (startMiddleMousePan(e.event)) return if (e.event.button !== 0) return // 绘制时去除选中辅助 @@ -3808,6 +3826,10 @@ export const usePaperStore = create((set) => ({ temp.remove() } + brushTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => { + if (dragMiddleMousePan(e)) return + } + brushTool.onMouseMove = (e: paper.MouseEvent) => { handleMouseMove(e) @@ -3829,6 +3851,10 @@ export const usePaperStore = create((set) => ({ } } + brushTool.onMouseUp = (_e: paper.ToolEvent) => { + stopMiddleMousePan() + } + brushTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { if (myPath) { @@ -3953,6 +3979,7 @@ export const usePaperStore = create((set) => ({ let pointTool = paperPointTool pointTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { + if (startMiddleMousePan(e.event)) return if (e.event.button !== 0) return // 绘制时去除选中辅助 @@ -4162,6 +4189,9 @@ export const usePaperStore = create((set) => ({ // group.current?.addChild(myCircle); temp.remove() } + pointTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => { + if (dragMiddleMousePan(e)) return + } pointTool.onMouseMove = (e: paper.MouseEvent) => { handleMouseMove(e) @@ -4182,6 +4212,9 @@ export const usePaperStore = create((set) => ({ ) } } + pointTool.onMouseUp = (_e: paper.ToolEvent) => { + stopMiddleMousePan() + } pointTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { if (myPath) { @@ -4294,12 +4327,18 @@ export const usePaperStore = create((set) => ({ let tags: number[] = [] tool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { + if (startMiddleMousePan(e.event)) return let point = usePaperStore.getState().group!.globalToLocal(e.point) - if (e.event.button === 1) return points.push([point.x, point.y]) tags.push(e.event.button === 0 ? 1 : 0) renderPath(points, tags, points.length === 1) } + tool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => { + if (dragMiddleMousePan(e)) return + } + tool.onMouseUp = (_e: paper.ToolEvent) => { + stopMiddleMousePan() + } tool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape") { @@ -4416,6 +4455,7 @@ export const usePaperStore = create((set) => ({ return bufferPath }, setPaperMode: (modeParam) => { + stopMiddleMousePan() set((state: PaperState) => ({ ...state, mode: modeParam,