diff --git a/api/fetch.ts b/api/fetch.ts index 67c5467..2a4f405 100644 --- a/api/fetch.ts +++ b/api/fetch.ts @@ -17,6 +17,7 @@ fetch 参数: "use client" import { useUserStore } from "@/app/store/user" +import { usePermissionStore } from "@/components/label/store/auth" import { deleteCookie, refreshKey } from "@/components/login/api" import { isNumber } from "@/libs/util" @@ -40,6 +41,7 @@ interface OptionsProps { async function httpfetch(options: OptionsProps): Promise { const { removeUserInfo, refreshToken } = useUserStore.getState() + const { logout } = usePermissionStore.getState() const access_token = useUserStore.getState().access_token const refresh_token = useUserStore.getState().refresh_token @@ -197,6 +199,7 @@ async function httpfetch(options: OptionsProps): Promise { } finally { // 清空身份信息并跳转至登录页面 removeUserInfo() + logout() window.location.href = "/login" } } @@ -241,6 +244,7 @@ async function httpfetch(options: OptionsProps): Promise { } finally { // 清空身份信息并跳转至登录页面 removeUserInfo() + logout() window.location.href = "/login" } } else if (status >= 510) { diff --git a/app/project/detail/TaskTableContainer.tsx b/app/project/detail/TaskTableContainer.tsx index 2a63715..4f9e404 100644 --- a/app/project/detail/TaskTableContainer.tsx +++ b/app/project/detail/TaskTableContainer.tsx @@ -181,7 +181,8 @@ export default function TaskTableContainer(props: { }) { const { info, userEnums } = props - const { user_id } = usePermissionStore() + const { user_id, detailInfo } = usePermissionStore() + const hasRolePermissions = Boolean(detailInfo?.roles?.length) const urlParams = useSearchParams() const id = urlParams.get("id") @@ -775,22 +776,24 @@ export default function TaskTableContainer(props: { textAlign: "center", render: (record) => ( - + {hasRolePermissions ? ( + + ) : null} + {hasRolePermissions ? ( + + ) : null} - - + {hasRolePermissions ? ( + + ) : null} + {hasRolePermissions ? ( + + ) : null} } /> diff --git a/components/label/LabelNossr.tsx b/components/label/LabelNossr.tsx index 1875527..e4241e9 100644 --- a/components/label/LabelNossr.tsx +++ b/components/label/LabelNossr.tsx @@ -1512,27 +1512,37 @@ const LabelPage = ({ ] ) - const handleToggleHideSelectedObject = useCallback(() => { - const selectedContext = resolveSelectedObjectContext() - if (!selectedContext) { - notifications.show({ - color: "yellow", - message: "请先选中单个对象", - }) - return - } + const handleToggleHideAllObjects = useCallback(() => { + const activeImageLabel = labelData.get(activeImage) + if (!activeImageLabel?.size) return - const currentHideStatus = - useObjectStore.getState().pathStatus[ - selectedContext.imageId + selectedContext.objectId - ]?.HIDE || false - toggleObjectHideByShortcut( - selectedContext.imageId, - selectedContext.operationId, - selectedContext.objectId, - !currentHideStatus + const activeImageOperations = Array.from(activeImageLabel.keys()) + const totalObjectCount = activeImageOperations.reduce( + (count, operationId) => { + return count + (activeImageLabel.get(operationId)?.length || 0) + }, + 0 ) - }, [resolveSelectedObjectContext]) + if (!totalObjectCount) return + + const pathStatus = useObjectStore.getState().pathStatus + const areAllObjectsHidden = activeImageOperations.every((operationId) => { + const operationChildren = activeImageLabel.get(operationId) || [] + return operationChildren.every( + ([objectId]) => pathStatus[activeImage + objectId]?.HIDE ?? false + ) + }) + + const nextHide = !areAllObjectsHidden + const { updateOperationStatus } = useObjectStore.getState() + + activeImageOperations.forEach((operationId) => { + updateOperationStatus(activeImage + operationId, "HIDE", nextHide) + activeImageLabel.get(operationId)?.forEach(([objectId]) => { + toggleObjectHideByShortcut(activeImage, operationId, objectId, nextHide) + }) + }) + }, [activeImage, labelData]) const rerenderActiveImageByLabel = useCallback( ( @@ -1617,6 +1627,8 @@ const LabelPage = ({ objectId: selectedContext.objectId, }) usePaperStore.getState().setPaperMode("polygon") + const started = usePaperStore.getState().beginContinuePolygonDraft() + if (!started) return notifications.show({ color: "green", message: "已进入继续标注模式", @@ -2123,7 +2135,7 @@ const LabelPage = ({ if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") { e.preventDefault() - handleToggleHideSelectedObject() + handleToggleHideAllObjects() return } @@ -2424,7 +2436,7 @@ const LabelPage = ({ enterSelectedObjectDrawMode, focusInput, getOperationSchema, - handleToggleHideSelectedObject, + handleToggleHideAllObjects, handleMergeSelectedObjects, handleContinueSelectedPolygon, handleShortcut, diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index e8c197a..4566e76 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -1291,11 +1291,6 @@ const PaperContainer = ( setup, ]) - const [paperSelectedPath] = useState<{ - // key = imageId - [key: string]: number[] - }>(selectedPath) - const getOperationSchema = useCallback( (operationId: string) => { return ( @@ -1320,6 +1315,8 @@ const PaperContainer = ( const renderImageId = imageIdOverride ?? activeImage if (operationSchema && imageData) { + const selectedIds = + useObjectStore.getState().selectedPath[renderImageId] || [] const toPaperPoint = (segment: any): paper.Point | null => { if (segment instanceof paper.Point) return segment if (segment instanceof paper.Segment) { @@ -1401,9 +1398,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes( - item[0] - ), + selected: selectedIds.includes(item[0]), } ) }) @@ -1430,9 +1425,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes( - item[0] - ), + selected: selectedIds.includes(item[0]), isHollowPolygon: true, } ) @@ -1456,7 +1449,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes(item[0]), + selected: selectedIds.includes(item[0]), } ) @@ -1534,9 +1527,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes( - item[0] - ), + selected: selectedIds.includes(item[0]), } ) }) @@ -1567,9 +1558,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes( - item[0] - ), + selected: selectedIds.includes(item[0]), } ) }) @@ -1592,7 +1581,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes(item[0]), + selected: selectedIds.includes(item[0]), } ) }) @@ -1632,9 +1621,7 @@ const PaperContainer = ( operationId: operationId, fillColor: fillColor, blankColor: blankColor, - selected: paperSelectedPath[renderImageId]?.includes( - item[0] - ), + selected: selectedIds.includes(item[0]), } ) }) @@ -1655,7 +1642,6 @@ const PaperContainer = ( addRectangle, getOperationSchema, paperPathStatus, - paperSelectedPath, ] ) @@ -1765,6 +1751,7 @@ const PaperContainer = ( detail, hollow_segmentation, ]) + useObjectStore.getState().updateSelectedPath(activeImage, "ADD", id) console.log(usePaperStore.getState().group) // console.log(first) // 更新当前页绘制数据 diff --git a/components/label/components/RightGroupTools.tsx b/components/label/components/RightGroupTools.tsx index e602285..1b941df 100644 --- a/components/label/components/RightGroupTools.tsx +++ b/components/label/components/RightGroupTools.tsx @@ -25,8 +25,8 @@ import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore" import { renderGroupPath } from "../useRenderGroupPath" import { useRightToolsStore } from "../useRightToolsStore" import { useTopToolsStore } from "../useTopToolsStore" -import { labelTypeMap } from "../utils/constants" import { safeClone } from "../utils/clone" +import { labelTypeMap } from "../utils/constants" import RightGroupEditModal from "./RightGroupEditModal" interface RightGroupToolsComponentProps { @@ -56,9 +56,25 @@ const RightGroupTools: React.FC = (props) => { const { activeImage } = useBottomToolsStore() const { groupStatus, updateGroupStatus, selectedPath, updateSelectedPath } = useObjectStore() + const continuePolygonTarget = usePaperStore( + (state) => state.continuePolygonTarget + ) const { getItemById, getItemsById } = usePaperStore() const { shift: pressShift } = useKeyEventStore() const [isGroupCollapsed, setIsGroupCollapsed] = useState(true) + const continueObjectId = + continuePolygonTarget?.imageId === activeImage + ? continuePolygonTarget.objectId + : null + + const getGroupChildren = useCallback( + (groupId: number) => { + const groupChildren = pathGroupMap.get(activeImage)?.get(groupId) || [] + if (continueObjectId === null) return groupChildren + return groupChildren.filter((childId) => childId !== continueObjectId) + }, + [activeImage, continueObjectId, pathGroupMap] + ) const handleSelect = useCallback( (id: number) => { @@ -322,8 +338,10 @@ const RightGroupTools: React.FC = (props) => { {pathGroupMap.get(activeImage) && - Array.from(pathGroupMap.get(activeImage)!.keys())?.map( - (groupId) => { + Array.from(pathGroupMap.get(activeImage)!.keys()) + .filter((groupId) => getGroupChildren(groupId).length) + .map((groupId) => { + const groupChildren = getGroupChildren(groupId) const isOpen = !groupStatus[activeImage + groupId]?.["COLLAPSE"] return ( @@ -376,10 +394,7 @@ const RightGroupTools: React.FC = (props) => { - - {pathGroupMap.get(activeImage)?.get(groupId) - ?.length || 0} - + {groupChildren.length} = (props) => { - {pathGroupMap - .get(activeImage) - ?.get(groupId) - ?.map((child) => { - const isSelected = - selectedPath[activeImage]?.includes(child) - return ( - { - handleSelect(child) - }} - style={{ cursor: "pointer" }}> - - - {child}- - {labelTypeMap.get( - getOperationSchema( - getOperationIdByPathId(child) - )?.label_type || 0 - )} - - - - ) - })} + {groupChildren.map((child) => { + const isSelected = + selectedPath[activeImage]?.includes(child) + return ( + { + handleSelect(child) + }} + style={{ cursor: "pointer" }}> + + + {child}- + {labelTypeMap.get( + getOperationSchema( + getOperationIdByPathId(child) + )?.label_type || 0 + )} + + + + ) + })} ) - } - )} + })} diff --git a/components/label/components/RightObjectTools.tsx b/components/label/components/RightObjectTools.tsx index 91ebed6..1a4e4ea 100644 --- a/components/label/components/RightObjectTools.tsx +++ b/components/label/components/RightObjectTools.tsx @@ -42,8 +42,8 @@ import { getShowContextMenuPosition, usePaperStore } from "../usePaperStore" import { usePaperSupportStore } from "../usePaperSupportStore" import { useTopToolsStore } from "../useTopToolsStore" import { getSubAttrStatus } from "../util" -import { toggleObjectHideByShortcut } from "../utils/objectVisibility" import { labelTypeMap } from "../utils/constants" +import { toggleObjectHideByShortcut } from "../utils/objectVisibility" interface RightObjectToolsComponentProps { labelState: LabelState @@ -143,7 +143,27 @@ const RightObjectTools: React.FC = (props) => { const { shift: pressShift } = useKeyEventStore() + const continuePolygonTarget = usePaperStore( + (state) => state.continuePolygonTarget + ) const { getItemById, getItemsById } = usePaperStore() + const continueObjectId = + continuePolygonTarget?.imageId === activeImage + ? continuePolygonTarget.objectId + : null + + const getOperationChildren = useCallback( + (imageId: string, operationId: string) => { + const operationChildren = storeLabel.get(imageId)?.get(operationId) || [] + if (continueObjectId === null || imageId !== activeImage) { + return operationChildren + } + return operationChildren.filter( + ([objectId]) => objectId !== continueObjectId + ) + }, + [activeImage, continueObjectId, storeLabel] + ) // 切换选中对象时,清除所有其他对象的当前选中状态 const clearSelectedItems = () => { @@ -381,14 +401,11 @@ const RightObjectTools: React.FC = (props) => { (operationId: string, hide: boolean) => { updateOperationStatus(activeImage + operationId, "HIDE", hide) - storeLabel - .get(activeImage) - ?.get(operationId) - ?.map((child) => { - toggleObjectHideByShortcut(activeImage, operationId, child[0], hide) - }) + getOperationChildren(activeImage, operationId).map((child) => { + toggleObjectHideByShortcut(activeImage, operationId, child[0], hide) + }) }, - [activeImage, storeLabel, updateOperationStatus] + [activeImage, getOperationChildren, updateOperationStatus] ) const objectHide = useCallback( @@ -398,14 +415,16 @@ const RightObjectTools: React.FC = (props) => { [activeImage] ) - const activeImageOperations = labelState.get(activeImage) || [] + const activeImageOperations = (labelState.get(activeImage) || []).filter( + (operationId) => getOperationChildren(activeImage, operationId).length + ) const isObjectHidden = (imageId: string, objectId: number) => { return pathStatus[imageId + objectId]?.["HIDE"] ?? false } const isOperationHidden = (imageId: string, operationId: string) => { - const operationChildren = storeLabel.get(imageId)?.get(operationId) || [] + const operationChildren = getOperationChildren(imageId, operationId) return ( operationChildren.length > 0 && operationChildren.every(([objectId]) => isObjectHidden(imageId, objectId)) @@ -414,9 +433,7 @@ const RightObjectTools: React.FC = (props) => { const totalObjectCount = activeImageOperations.reduce( (count, operationId) => { - return ( - count + (storeLabel.get(activeImage)?.get(operationId)?.length || 0) - ) + return count + getOperationChildren(activeImage, operationId).length }, 0 ) @@ -424,8 +441,7 @@ const RightObjectTools: React.FC = (props) => { const areAllObjectsHidden = totalObjectCount > 0 && activeImageOperations.every((operationId) => { - const operationChildren = - storeLabel.get(activeImage)?.get(operationId) || [] + const operationChildren = getOperationChildren(activeImage, operationId) return operationChildren.every(([objectId]) => isObjectHidden(activeImage, objectId) ) @@ -451,6 +467,9 @@ const RightObjectTools: React.FC = (props) => { .get(activeImage) ?.get(operationId) ?.some((child) => { + if (continueObjectId !== null && child[0] === continueObjectId) { + return false + } return getSubAttrStatus( getOperationSchema(operationId), child[2]?.sub_attributes @@ -458,7 +477,7 @@ const RightObjectTools: React.FC = (props) => { }) return bool }, - [activeImage, getOperationSchema, storeLabel] + [activeImage, continueObjectId, getOperationSchema, storeLabel] ) // 搜索标注对象id @@ -468,7 +487,8 @@ const RightObjectTools: React.FC = (props) => { const id = Number(searchId) const labelObjMap = storeLabel.get(activeImage) Array.from(labelObjMap!.entries()).forEach(([operationId, objList]) => { - objList.forEach((obj) => { + false && objList + getOperationChildren(activeImage, operationId).forEach((obj) => { if (id === obj[0]) { handleSelect(id, operationId) } @@ -483,7 +503,7 @@ const RightObjectTools: React.FC = (props) => { if (e.key === "Enter") { Array.from(storeLabel.entries()).forEach(([imgId, objectMap]) => { if (objectMap.get(currentOperationId)) { - const list = objectMap.get(currentOperationId) + const list = getOperationChildren(imgId, currentOperationId) list!.forEach((item) => { const { sub_attributes } = item[2] // 如果没有子属性搜索值,则把对象HIDE设为false @@ -616,8 +636,10 @@ const RightObjectTools: React.FC = (props) => { const isHide = isOperationHidden(activeImage, operationId) const hasSubAttrError = getOperationSubAttrStatus(operationId) const operationLabel = operationSchema?.label_class - const operationChildren = - storeLabel.get(activeImage)?.get(operationId) || [] + const operationChildren = getOperationChildren( + activeImage, + operationId + ) const objectCount = operationChildren.length const visibleChildren = operationChildren.filter((child) => { const { comment } = child[2] diff --git a/components/label/store.ts b/components/label/store.ts index a479563..794a335 100644 --- a/components/label/store.ts +++ b/components/label/store.ts @@ -5,8 +5,8 @@ import { persist, PersistStorage, StorageValue } from "zustand/middleware" import { Comment } from "./api/label/typing" import { safeClone } from "./utils/clone" -type LabelData = Map> -type LabelPathTuple = [number, any[], any, any[]] +export type LabelData = Map> +export type LabelPathTuple = [number, any[], any, any[]] type LabelTime = { label: number review1: number diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 3354bf2..73fa3dd 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -7,6 +7,7 @@ import { Comment } from "./api/label/typing" import { Project } from "./api/project/typing" import { initialDetail, + type LabelPathTuple, useKeyEventStore, useLabelStore, useObjectStore, @@ -180,6 +181,7 @@ interface PaperState { getItemById: (id: number) => paper.Path | null getItemsById: (id: number) => Array setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void + beginContinuePolygonDraft: () => boolean setLoadingData: (flag: boolean) => void resizeGhostRequestToken: number requestResizeGhost: () => void @@ -844,6 +846,7 @@ export const usePaperStore = create((set) => ({ continuePolygonTarget: null, currentMousePosition: [0, 0], loadingData: false, + beginContinuePolygonDraft: () => false, init: ( initEl: HTMLCanvasElement, initScope: paper.PaperScope, @@ -3158,6 +3161,11 @@ export const usePaperStore = create((set) => ({ let continueSourceItems: Array = [] let continueTargetContourIndex: number | null = null let continueTargetContext: ContinuePolygonTarget | null = null + let continueTargetSnapshot: LabelPathTuple | null = null + let continueDraftHints = new Map< + string, + { contourIndex: number; points: paper.Point[] } + >() const handleCircles = (path: paper.Path | paper.CompoundPath | null) => { // 更新选中的点 @@ -3243,6 +3251,146 @@ export const usePaperStore = create((set) => ({ return null } + const getContinueCandidatePaths = (objectId: number) => { + const flattenedPaths: paper.Path[] = [] + const visitedPaths = new Set() + + usePaperStore + .getState() + .getItemsById(objectId) + .filter((item) => item.data?.type === "polygon") + .forEach((item) => { + if (item instanceof paper.CompoundPath) { + ;(item.children as paper.Path[]).forEach((child) => { + if (visitedPaths.has(child)) return + visitedPaths.add(child) + flattenedPaths.push(child) + }) + return + } + if (item instanceof paper.Path && !visitedPaths.has(item)) { + visitedPaths.add(item) + flattenedPaths.push(item) + } + }) + + const candidatePaths = flattenedPaths.filter( + (path) => path.segments?.length > 3 + ) + if (!candidatePaths.length) return [] as paper.Path[] + + const preferredPaths = candidatePaths.filter( + (path) => !path.data?.isHollowPolygon + ) + return preferredPaths.length ? preferredPaths : candidatePaths + } + + const clonePaperPoints = (pathPoints: paper.Point[]) => { + return pathPoints.map((point) => new paper.Point(point)) + } + + const removeDuplicatedClosingPoint = (pathPoints: paper.Point[]) => { + if (pathPoints.length <= 1) return pathPoints + const firstPoint = pathPoints[0] + const lastPoint = pathPoints[pathPoints.length - 1] + if ( + Math.abs(firstPoint.x - lastPoint.x) < 0.000001 && + Math.abs(firstPoint.y - lastPoint.y) < 0.000001 + ) { + pathPoints.pop() + } + return pathPoints + } + + const getContinueContourPoints = (segments: any[] | undefined) => { + return removeDuplicatedClosingPoint( + (segments || []) + .map((segment) => getPointFromSegment(segment)) + .filter((point): point is paper.Point => point !== null) + ) + } + + const getContinueDraftHintKey = (target: ContinuePolygonTarget) => { + return `${target.imageId}::${target.operationId}::${target.objectId}` + } + + const normalizeComparableContour = (pathPoints: paper.Point[]) => { + return removeDuplicatedClosingPoint(clonePaperPoints(pathPoints)).map( + (point) => [point.x, point.y] as [number, number] + ) + } + + const isSameComparablePoint = ( + pointA: [number, number], + pointB: [number, number] + ) => { + return ( + Math.abs(pointA[0] - pointB[0]) < 0.000001 && + Math.abs(pointA[1] - pointB[1]) < 0.000001 + ) + } + + const isSameContourOrder = ( + contourA: [number, number][], + contourB: [number, number][] + ) => { + if (contourA.length !== contourB.length) return false + if (!contourA.length) return false + for (let offset = 0; offset < contourB.length; offset += 1) { + let matched = true + for (let index = 0; index < contourA.length; index += 1) { + if ( + !isSameComparablePoint( + contourA[index], + contourB[(index + offset) % contourB.length] + ) + ) { + matched = false + break + } + } + if (matched) return true + } + return false + } + + const isSameContour = (pathA: paper.Point[], pathB: paper.Point[]) => { + const contourA = normalizeComparableContour(pathA) + const contourB = normalizeComparableContour(pathB) + if (contourA.length !== contourB.length || !contourA.length) { + return false + } + return ( + isSameContourOrder(contourA, contourB) || + isSameContourOrder(contourA, [...contourB].reverse()) + ) + } + + const findMatchingContinueContourIndex = ( + candidatePoints: paper.Point[], + contours: any[] + ) => { + for (let index = contours.length - 1; index >= 0; index -= 1) { + const contourPoints = getContinueContourPoints(contours[index]) + if (isSameContour(candidatePoints, contourPoints)) { + return index + } + } + return -1 + } + + const setContinueDraftHint = ( + target: ContinuePolygonTarget, + contourIndex: number, + contourPoints: paper.Point[] + ) => { + if (contourIndex < 0 || !contourPoints.length) return + continueDraftHints.set(getContinueDraftHintKey(target), { + contourIndex, + points: removeDuplicatedClosingPoint(clonePaperPoints(contourPoints)), + }) + } + const clearContinueSourceItems = (restoreVisible: boolean) => { continueSourceItems.forEach((item) => { if (restoreVisible) { @@ -3258,18 +3406,26 @@ export const usePaperStore = create((set) => ({ clearContinueSourceItems(restoreVisible) continueTargetContourIndex = null continueTargetContext = null + continueTargetSnapshot = null usePaperStore.getState().setContinuePolygonTarget(null) } + const hasContinuePolygonDraft = () => { + return !!( + continueTargetContext || + continueTargetSnapshot || + usePaperStore.getState().continuePolygonTarget + ) + } + const startContinuePolygonDraft = () => { const target = usePaperStore.getState().continuePolygonTarget if (!target) return false const imageLabel = useLabelStore.getState().label.get(target.imageId) const operationPaths = imageLabel?.get(target.operationId) || [] - const targetTuple = operationPaths.find( - (path) => path[0] === target.objectId - ) + const targetTuple = + operationPaths.find((path) => path[0] === target.objectId) || null if (!targetTuple) { notifications.show({ color: "yellow", @@ -3280,14 +3436,71 @@ export const usePaperStore = create((set) => ({ } const nextPoints = safeClone(targetTuple[1] || []) + const continuePaths = getContinueCandidatePaths(target.objectId) + const continuePath = continuePaths[continuePaths.length - 1] + const continuePathPoints = continuePath?.segments?.length + ? removeDuplicatedClosingPoint( + continuePath.segments.map( + (segment) => new paper.Point(segment.point) + ) + ) + : [] + const continueHint = continueDraftHints.get( + getContinueDraftHintKey(target) + ) + let contourIndex = -1 - for (let i = nextPoints.length - 1; i >= 0; i -= 1) { - if (nextPoints[i]?.length > 3) { + if ( + continueHint && + continueHint.contourIndex >= 0 && + continueHint.contourIndex < nextPoints.length + ) { + contourIndex = continueHint.contourIndex + } else if (continuePathPoints.length) { + contourIndex = findMatchingContinueContourIndex( + continuePathPoints, + nextPoints + ) + } + for ( + let i = nextPoints.length - 1; + contourIndex === -1 && i >= 0; + i -= 1 + ) { + if (nextPoints[i]?.length > 2) { contourIndex = i break } } - if (contourIndex === -1) { + if (contourIndex === -1 && nextPoints.length) { + contourIndex = nextPoints.length - 1 + } + + let sourceContourPoints = continueHint?.points?.length + ? clonePaperPoints(continueHint.points) + : [] + + if ( + sourceContourPoints.length && + continuePathPoints.length && + !isSameContour(sourceContourPoints, continuePathPoints) + ) { + sourceContourPoints = [] + } + + if (!sourceContourPoints.length && continuePathPoints.length) { + sourceContourPoints = clonePaperPoints(continuePathPoints) + } + + if (!sourceContourPoints.length && contourIndex >= 0) { + sourceContourPoints = getContinueContourPoints(nextPoints[contourIndex]) + } + + const contourPoints = sourceContourPoints + .slice(0, -1) + .map((point) => new paper.Point(point)) + + if (!sourceContourPoints.length) { notifications.show({ color: "yellow", message: "当前对象没有可继续标注的区域", @@ -3296,17 +3509,10 @@ export const usePaperStore = create((set) => ({ return false } - const contourSegments = safeClone(nextPoints[contourIndex] || []) - const continueSegments = contourSegments.slice().reverse() - continueSegments.pop() - const contourPoints = continueSegments - .map((segment: any) => getPointFromSegment(segment)) - .filter((point: paper.Point | null): point is paper.Point => !!point) - - if (contourPoints.length < 3) { + if (contourPoints.length < 2) { notifications.show({ color: "yellow", - message: "当前区域至少保留3个节点,无法继续撤回", + message: "当前区域至少保留2个节点,无法继续撤回", }) clearContinuePolygonContext(true) return false @@ -3321,8 +3527,19 @@ export const usePaperStore = create((set) => ({ item.visible = false }) + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: false, + position: { top: 0, left: 0 }, + imageId: target.imageId, + operationId: target.operationId, + id: target.objectId, + }) + usePaperSupportStore.getState().clearAll() + useObjectStore.getState().updateSelectedPath(target.imageId, "ADD") + continueTargetContourIndex = contourIndex continueTargetContext = target + continueTargetSnapshot = safeClone(targetTuple) as LabelPathTuple points = contourPoints draftId = target.objectId polygon?.remove() @@ -3351,14 +3568,8 @@ export const usePaperStore = create((set) => ({ const saveContinuePolygonDraft = (draftPath: paper.Path) => { const target = continueTargetContext || usePaperStore.getState().continuePolygonTarget - if (!target) return false - - const imageLabel = useLabelStore.getState().label.get(target.imageId) - const operationPaths = imageLabel?.get(target.operationId) || [] - const targetTuple = operationPaths.find( - (path) => path[0] === target.objectId - ) - if (!targetTuple) return false + const targetTuple = continueTargetSnapshot + if (!target || !targetTuple) return false const pathData = JSON.parse(draftPath.exportJSON({ precision: 20 })) const nextContour = pathData?.[1]?.segments @@ -3382,6 +3593,7 @@ export const usePaperStore = create((set) => ({ safeClone(targetTuple[2] || initialDetail), safeClone(targetTuple[3] || []), ]) + setContinueDraftHint(target, contourIndex, clonePaperPoints(points)) useObjectStore .getState() .updateSelectedPath(target.imageId, "ADD", target.objectId) @@ -3673,6 +3885,7 @@ export const usePaperStore = create((set) => ({ const points: any[] = [] const hollowPoints: any[] = [] + let lastOuterContourPoints: paper.Point[] = [] const nextPaths = nextPolygon instanceof paper.CompoundPath ? (nextPolygon.children as paper.Path[]) @@ -3700,6 +3913,7 @@ export const usePaperStore = create((set) => ({ hollowPoints.push(segments) } else { points.push(segments) + lastOuterContourPoints = getContinueContourPoints(segments) } }) @@ -3728,6 +3942,15 @@ export const usePaperStore = create((set) => ({ prevDetail, hollowPoints, ]) + setContinueDraftHint( + { + imageId: activeImage, + operationId, + objectId: selectedId, + }, + points.length - 1, + lastOuterContourPoints + ) useObjectStore .getState() .updateSelectedPath(activeImage, "ADD", selectedId) @@ -3746,6 +3969,135 @@ export const usePaperStore = create((set) => ({ return true } + const handleSelectedPolygonAdd = (draftPath: paper.Path) => { + const activeImage = useBottomToolsStore.getState().activeImage + const selectedIds = + useObjectStore.getState().selectedPath[activeImage] || [] + if (selectedIds.length !== 1) return false + + const selectedId = selectedIds[0] + const targetShapes = usePaperStore + .getState() + .getItemsById(selectedId) + .filter((item) => item.data?.type === "polygon") + + const targetShape = + targetShapes.find((item) => item instanceof paper.CompoundPath) || + targetShapes.find( + (item) => item instanceof paper.Path && !item.data?.isHollowPolygon + ) || + targetShapes.find((item) => item instanceof paper.Path) + + if (!targetShape) return false + + let clippedDraft: paper.Path | paper.CompoundPath | null = + draftPath.intersect(usePaperStore.getState().rasterPath!, { + insert: false, + trace: false, + }) as paper.Path | paper.CompoundPath + + draftPath.remove() + + const draftHasSegments = + clippedDraft instanceof paper.CompoundPath + ? (clippedDraft.children as paper.Path[]).some( + (child) => child.segments?.length + ) + : clippedDraft.segments?.length + if (!draftHasSegments) { + clippedDraft?.remove() + clippedDraft = null + return false + } + + const targetData = getSafePathData(targetShape.data || {}) + let nextPolygon = (targetShape as paper.Path | paper.CompoundPath).unite( + clippedDraft + ) as paper.Path | paper.CompoundPath + clippedDraft?.remove() + clippedDraft = null + targetShape.remove() + + const points: any[] = [] + const hollowPoints: any[] = [] + let lastOuterContourPoints: paper.Point[] = [] + const nextPaths = + nextPolygon instanceof paper.CompoundPath + ? (nextPolygon.children as paper.Path[]) + : ([nextPolygon] as paper.Path[]) + const nextPathSegments = nextPaths.map((path) => { + if (!path.segments?.length) return null + const pathData = JSON.parse(path.exportJSON({ precision: 20 })) + return pathData?.[1]?.segments || null + }) + const hollowFlags = getHollowFlagsByContainment( + nextPathSegments.map((segments) => segments || []) + ) + + if (nextPolygon instanceof paper.CompoundPath) { + nextPolygon.data = Object.assign({}, targetData) + } + nextPaths.forEach((path, index) => { + const segments = nextPathSegments[index] + if (!segments?.length) return + const isHollowPolygon = !!hollowFlags[index] + path.data = Object.assign({}, targetData, { + isHollowPolygon, + }) + if (isHollowPolygon) { + hollowPoints.push(segments) + } else { + points.push(segments) + lastOuterContourPoints = getContinueContourPoints(segments) + } + }) + + const operationId = ( + targetData.operationId || + useTopToolsStore.getState().activeOperation || + "" + ).toString() + if (!operationId) { + nextPolygon.remove() + return false + } + const prevDetail = safeClone( + useLabelStore + .getState() + .getLabelDetailDataByKeys(activeImage, operationId, selectedId) || + initialDetail + ) + + if (points.length) { + useLabelStore + .getState() + .setOperation(activeImage, operationId, [ + selectedId, + points, + prevDetail, + hollowPoints, + ]) + setContinueDraftHint( + { + imageId: activeImage, + operationId, + objectId: selectedId, + }, + points.length - 1, + lastOuterContourPoints + ) + useObjectStore + .getState() + .updateSelectedPath(activeImage, "ADD", selectedId) + } else { + nextPolygon.remove() + return false + } + + forceUpdate() + return true + } + polygonTool.onMouseDown = (e: { event: MouseEvent point: paper.Point @@ -3756,6 +4108,9 @@ export const usePaperStore = create((set) => ({ if (polygon || points.length) { hidePaperContextMenu() undoPolygonPoint() + if (!points.length && hasContinuePolygonDraft()) { + clearContinuePolygonContext(true) + } return } if (!trySelectPaperObjectByPoint(e.point)) { @@ -3771,11 +4126,7 @@ export const usePaperStore = create((set) => ({ usePaperSupportStore.getState().clearAll() } - if ( - !polygon && - !points.length && - usePaperStore.getState().continuePolygonTarget - ) { + if (!polygon && !points.length && hasContinuePolygonDraft()) { if (!startContinuePolygonDraft()) return } @@ -3799,7 +4150,7 @@ export const usePaperStore = create((set) => ({ // 初始化 compoundPolygon compoundPolygon = new paper.Path() - if (usePaperStore.getState().continuePolygonTarget) { + if (hasContinuePolygonDraft()) { ;(polygon as paper.Path).closePath() const continueSaved = saveContinuePolygonDraft( polygon as paper.Path @@ -3841,6 +4192,36 @@ export const usePaperStore = create((set) => ({ return } + if ( + useKeyboardStore.getState().drawAction === "add" && + useTopToolsStore.getState().pressA + ) { + const selectedId = + useObjectStore.getState().selectedPath[ + useBottomToolsStore.getState().activeImage + ]?.[0] + const selectedItem = selectedId + ? usePaperStore.getState().getItemById(selectedId) + : null + if (selectedItem?.data?.type === "polygon") { + ;(polygon as paper.Path).closePath() + handleSelectedPolygonAdd(polygon as paper.Path) + + selectedCircles.forEach((item) => { + item.remove() + }) + selectedCircles = [] + clearPolygonMagnet() + useTopToolsStore.getState().setPressA(false) + draftId = null + lastPoint = null + lastTime = 0 + points = [] + polygon = null + return + } + } + // pressA 模式下添加多边形 if (useTopToolsStore.getState().pressA) { let ids = @@ -3941,11 +4322,15 @@ export const usePaperStore = create((set) => ({ if (polygon.children && polygon.children.length) { let points: any[] = [] let hollowPoints: any[] = [] + let lastOuterContourPoints: paper.Point[] = [] ;(polygon.children as paper.Path[]).forEach((path) => { let pathData = JSON.parse(path.exportJSON({ precision: 20 })) // 在镂空或分割的区域中 路径方向是false if (path.clockwise) { points.push(pathData[1]?.segments) + lastOuterContourPoints = getContinueContourPoints( + pathData[1]?.segments + ) } else { hollowPoints.push(pathData[1]?.segments) } @@ -3981,6 +4366,17 @@ export const usePaperStore = create((set) => ({ hollowPoints, ] ) + if (points.length) { + setContinueDraftHint( + { + imageId: useBottomToolsStore.getState().activeImage, + operationId: useTopToolsStore.getState().activeOperation, + objectId: polygon.data.id, + }, + points.length - 1, + lastOuterContourPoints + ) + } if (!useTopToolsStore.getState().pressA) useRightToolsStore.getState().pushPathId(polygon.data.id) } @@ -4015,6 +4411,15 @@ export const usePaperStore = create((set) => ({ [], ] ) + setContinueDraftHint( + { + imageId: useBottomToolsStore.getState().activeImage, + operationId: useTopToolsStore.getState().activeOperation, + objectId: polygon.data.id, + }, + 0, + clonePaperPoints(points) + ) useRightToolsStore.getState().pushPathId(polygon.data.id) } @@ -4245,11 +4650,7 @@ export const usePaperStore = create((set) => ({ } polygonTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { - if ( - polygon || - points.length || - usePaperStore.getState().continuePolygonTarget - ) { + if (polygon || points.length || hasContinuePolygonDraft()) { resetPolygonDraft() clearContinuePolygonContext(true) usePaperSupportStore.getState().handleBufferPaths(null) @@ -4269,6 +4670,7 @@ export const usePaperStore = create((set) => ({ set((state: PaperState) => ({ ...state, polygonTool: polygonTool, + beginContinuePolygonDraft: () => startContinuePolygonDraft(), })) }, addRectangle: ( diff --git a/components/label/utils/constants.ts b/components/label/utils/constants.ts index 4a8e7e7..45f3b6d 100644 --- a/components/label/utils/constants.ts +++ b/components/label/utils/constants.ts @@ -32,9 +32,9 @@ export const processOpts = [ // 状态码 export const STATUS_CODE = new Map([ [1, "已创建"], - [101, "已创建"], // 审核 - [102, "已创建"], // 复审 - [103, "已创建"], // 审核3 + [101, "已创建待审核"], // 审核 + [102, "已创建待复审"], // 复审 + [103, "已创建待终审"], // 审核3 [180, "数据同步中"], [190, "Embedding生成中"], [191, "Embedding生成失败"], diff --git a/components/layout/AppLayout.tsx b/components/layout/AppLayout.tsx index 0a21bfe..a2b80f4 100644 --- a/components/layout/AppLayout.tsx +++ b/components/layout/AppLayout.tsx @@ -136,6 +136,7 @@ export default function AppLayout({ const handleLogout = async () => { useUserStore.getState().removeUserInfo() + usePermissionStore.getState().logout() process.env.NEXT_PUBLIC_OUTPUT_TYPE === "standalone" && (await deleteCookie()) router.push("/login") diff --git a/components/layout/common.ts b/components/layout/common.ts index d6f3276..c19782b 100644 --- a/components/layout/common.ts +++ b/components/layout/common.ts @@ -12,12 +12,12 @@ const currentComponentList: MenuItem[] = [ { title: "图片标注", url: "project", - icon: "SystemIcon", + icon: "ImageAnnotationIcon", items: [ { title: "我的项目", url: "own", - icon: "PersonalMenuIcon", + icon: "MyProjectsIcon", }, ], }, @@ -27,23 +27,23 @@ export const componentList: MenuItem[] = [ { title: "个人中心", url: "person", - icon: "PersonalMenuIcon", + icon: "PersonalCenterIcon", }, ...currentComponentList, { title: "视频标注", url: "video", - icon: "SystemIcon", + icon: "VideoAnnotationIcon", + }, + { + title: "点云标注", + url: "lidar", + icon: "LidarAnnotationIcon", }, - // { - // title: "点云标注", - // url: "lidar", - // icon: "SystemIcon", - // }, { title: "管理中心", url: "mgt", - icon: "SystemIcon", + icon: "ManagementCenterIcon", }, ] diff --git a/components/layout/components/ClientIcon.tsx b/components/layout/components/ClientIcon.tsx index 273efcc..480455a 100644 --- a/components/layout/components/ClientIcon.tsx +++ b/components/layout/components/ClientIcon.tsx @@ -1,14 +1,39 @@ -// import { type LucideIcon } from "lucide-react"; - import { - AuthMenuIcon, - PersonalMenuIcon, - SystemIcon, - TaskMenuIcon, -} from "./icons/custom" + IconCategory2, + IconCube3dSphere, + IconFileText, + IconFolder, + IconFolders, + IconHierarchy3, + IconLayoutDashboard, + IconPhoto, + IconSettings, + IconUserCircle, + IconUsersGroup, + IconVideo, +} from "@tabler/icons-react" import React from "react" +const iconMap = { + DashboardIcon: IconLayoutDashboard, + EmployeeManagementIcon: IconUsersGroup, + ImageAnnotationIcon: IconPhoto, + LidarAnnotationIcon: IconCube3dSphere, + ManagementCenterIcon: IconSettings, + MyProjectsIcon: IconFolder, + PersonalCenterIcon: IconUserCircle, + ProjectCategoryIcon: IconCategory2, + ProjectManagementIcon: IconFolders, + ReportIcon: IconFileText, + TeamManagementIcon: IconHierarchy3, + VideoAnnotationIcon: IconVideo, + SystemIcon: IconSettings, + PersonalMenuIcon: IconUserCircle, + TaskMenuIcon: IconLayoutDashboard, + AuthMenuIcon: IconFileText, +} as const + export const ClientIcon = React.forwardRef< HTMLDivElement, React.ComponentProps<"button"> & { @@ -16,12 +41,7 @@ export const ClientIcon = React.forwardRef< } >(({ icon, ...props }, _ref) => { if (icon) { - const IconComponent = { - SystemIcon, - PersonalMenuIcon, - TaskMenuIcon, - AuthMenuIcon, - }[icon] + const IconComponent = iconMap[icon as keyof typeof iconMap] return IconComponent ? : null } else { return null diff --git a/components/project/index.tsx b/components/project/index.tsx index e5b652d..c175355 100644 --- a/components/project/index.tsx +++ b/components/project/index.tsx @@ -19,6 +19,7 @@ import { useProjectStore, } from "@/components/label/store/auth" import { + ALL_CODE, projectLabelTypeOpts, projectStatusOpts, STATUS_CODE, @@ -36,7 +37,6 @@ import { Modal, MultiSelect, Pagination, - Paper, Select, SimpleGrid, Stack, @@ -46,6 +46,15 @@ import { Tooltip, UnstyledButton, } from "@mantine/core" +import { + SettingContentPanel, + SettingFilterActions, + SettingFilterPanel, + SettingHeaderActions, + SettingListHeader, + SettingPage, + SettingPanel, +} from "@/components/setting/PageSurface" import { useForm } from "@mantine/form" import { notifications } from "@mantine/notifications" import { @@ -134,6 +143,7 @@ export default function ProjectInfoPage({ type: "dynamic" | "collect" | "" }) { const router = useRouter() + const storeType = type || "all" const { userOpts } = useAllUserStore() const userName = usePermissionStore((s) => s.user_name) @@ -150,20 +160,20 @@ export default function ProjectInfoPage({ }: any = useProjectStore() const storeParams = useMemo(() => { - return type === "collect" + return storeType === "collect" ? collectParams - : type === "dynamic" + : storeType === "dynamic" ? dynamicParams : allParams - }, [allParams, collectParams, dynamicParams, type]) + }, [allParams, collectParams, dynamicParams, storeType]) const total = useMemo(() => { - return type === "collect" + return storeType === "collect" ? collectTotal - : type === "dynamic" + : storeType === "dynamic" ? dynamicTotal : allTotal - }, [allTotal, collectTotal, dynamicTotal, type]) + }, [allTotal, collectTotal, dynamicTotal, storeType]) const [typeOpts, setTypeOpts] = useState< Array<{ label: string; value: string }> @@ -179,7 +189,7 @@ export default function ProjectInfoPage({ status: "", owner: "", label_type: "", - admin_user: "", + admin_user: [] as string[], start_date: dayjs().subtract(6, "day").format("YYYY-MM-DD"), end_date: dayjs().format("YYYY-MM-DD"), } @@ -200,9 +210,9 @@ export default function ProjectInfoPage({ const queryParams = useMemo(() => { const base = - type === "collect" + storeType === "collect" ? { ...initCollectParams } - : type === "dynamic" + : storeType === "dynamic" ? { ...initDynamicParams } : { ...initAllParams } @@ -214,7 +224,9 @@ export default function ProjectInfoPage({ project_name: appliedFilters.project_name || undefined, project_type: appliedFilters.project_type || undefined, owner: appliedFilters.owner || undefined, - admin_user: appliedFilters.admin_user || undefined, + admin_user: appliedFilters.admin_user?.length + ? appliedFilters.admin_user.map((n) => Number(n)) + : undefined, start_date: appliedFilters.start_date || undefined, end_date: appliedFilters.end_date || undefined, label_type: appliedFilters.label_type @@ -222,11 +234,11 @@ export default function ProjectInfoPage({ : undefined, // 0: 图片标注 1: 点云标注 2: 视频标注 project_data_type: 0, - status: parseStatusValue(appliedFilters.status), + status: parseStatusValue(appliedFilters.status) ?? [...ALL_CODE], } return params - }, [appliedFilters, storeParams, type]) + }, [appliedFilters, storeParams, storeType]) const load = useCallback(async () => { if (!queryTrigger) return @@ -234,10 +246,10 @@ export default function ProjectInfoPage({ try { const res = await getProjectList(queryParams) setRecords(res?.project_list ?? []) - setTotal(res?.total_items ?? 0, type) + setTotal(res?.total_items ?? 0, storeType) } catch (e) { setRecords([]) - setTotal(0, type) + setTotal(0, storeType) notifications.show({ color: "red", title: "加载失败", @@ -246,7 +258,7 @@ export default function ProjectInfoPage({ } finally { setLoading(false) } - }, [queryParams, queryTrigger, setTotal, type]) + }, [queryParams, queryTrigger, setTotal, storeType]) useEffect(() => { load() @@ -345,11 +357,10 @@ export default function ProjectInfoPage({ const canDoAction = !!actionKey return ( - @@ -551,7 +565,7 @@ export default function ProjectInfoPage({ - + ) } @@ -604,19 +618,12 @@ export default function ProjectInfoPage({ } return ( - - + + - - {type === "collect" - ? "我的收藏" - : type === "dynamic" - ? "动态项目" - : "我的项目"} - + 筛选条件 - + setFilters((s) => ({ @@ -688,8 +689,6 @@ export default function ProjectInfoPage({ /> ({ label: o.label, - value: o.label, + value: o.value.toString(), }))} value={filters.status} onChange={(v) => setFilters((s) => ({ ...s, status: v ?? "" }))} /> setFilters((s) => ({ ...s, owner: e.currentTarget.value })) @@ -721,8 +716,6 @@ export default function ProjectInfoPage({ /> ({ label: o.label, value: String(o.value), }))} - value={filters.admin_user || null} - onChange={(v) => - setFilters((s) => ({ ...s, admin_user: v ?? "" })) - } + value={filters.admin_user} + onChange={(v) => setFilters((s) => ({ ...s, admin_user: v }))} + searchable + clearable /> @@ -764,8 +751,6 @@ export default function ProjectInfoPage({ /> @@ -775,18 +760,36 @@ export default function ProjectInfoPage({ - + - + + + + } + />
{ - setSearchParams({ ...storeParams, page_number: p }, type) + setSearchParams({ ...storeParams, page_number: p }, storeType) setQueryTrigger((v) => v + 1) }} onChangePageSize={(s) => { setSearchParams( { ...storeParams, page_number: 1, page_size: s }, - type + storeType ) setQueryTrigger((v) => v + 1) }} />
-
+ - + ) }