From 98d697a542a9f60f53df0ce237be538a62ff5a81 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 10 Apr 2026 11:21:30 +0800 Subject: [PATCH] feat(label): add right select --- components/label/usePaperStore.ts | 304 ++++++++++++++++++++++++++---- 1 file changed, 271 insertions(+), 33 deletions(-) diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 21c538b..3354bf2 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -318,6 +318,229 @@ const hidePaperContextMenu = () => { }) } +const PAPER_OBJECT_TYPES = ["rectangle", "polygon", "brush", "point"] as const +const PAPER_CONTEXT_HIT_TYPES = [ + ...PAPER_OBJECT_TYPES, + "circle", + "mask", + "pathBuff", + "pathCircle", +] as const + +const isPaperObjectType = (type: unknown) => + typeof type === "string" && + PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number]) + +const isPaperContextHitType = (type: unknown) => + typeof type === "string" && + PAPER_CONTEXT_HIT_TYPES.includes( + type as (typeof PAPER_CONTEXT_HIT_TYPES)[number] + ) + +const getPaperCursorByMode = ( + mode: PaperState["mode"] = usePaperStore.getState().mode +) => { + if ( + ["polygon", "rectangle", "brush", "point", "support"].includes(mode || "") + ) { + return "crosshair" + } + return "default" +} + +const syncPaperCursorByMode = ( + mode: PaperState["mode"] = usePaperStore.getState().mode +) => { + if (usePaperStore.getState().el) { + usePaperStore.getState().el!.style.cursor = getPaperCursorByMode(mode) + } +} + +const clearPaperSelectedObjects = () => { + const group = usePaperStore.getState().group + if (!group) return + + group + .getItems({ + match: (item: paper.Item) => !!item.data?.selected, + }) + .forEach((item) => { + item.data.selected = false + }) + + usePaperSupportStore.getState().clearAll() + + const activeImage = useBottomToolsStore.getState().activeImage + const selectedIds = [ + ...(useObjectStore.getState().selectedPath[activeImage] || []), + ] + + selectedIds.forEach((id) => { + usePaperStore + .getState() + .getItemsById(id) + .forEach((item) => { + if ( + (item instanceof paper.Path || item instanceof paper.CompoundPath) && + ["rectangle", "polygon"].includes(item.data?.type || "") + ) { + item.fillColor = item.data?.blankColor || item.fillColor || null + } + }) + }) + + selectedIds.forEach((id) => { + useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", id) + }) +} + +const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => { + if (item.data.type === "rectangle" && item instanceof paper.Path) { + item.data.selected = true + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + item.fillColor = item.data.fillColor || item.fillColor || null + } else if (item.data.type === "brush") { + item.data.selected = true + if (item instanceof paper.Path) { + usePaperSupportStore.getState().handleMask(item) + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + } + } else if (item.data.type === "point" && item instanceof paper.Path) { + item.data.selected = true + usePaperSupportStore.getState().handleMask(item) + usePaperSupportStore.getState().handleText(item) + usePaperSupportStore.getState().handleBufferPaths(item) + usePaperSupportStore.getState().handleCircles(item) + } else if (item.data.type === "polygon") { + item.data.selected = true + if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) { + item.fillColor = item.data.fillColor || item.fillColor || null + } + } + + item.bringToFront() +} + +const getPaperSelectionPath = ( + id: number, + localPoint: paper.Point, + preferredItem?: paper.Item | null +) => { + if ( + preferredItem instanceof paper.Path && + isPaperObjectType(preferredItem.data?.type) + ) { + return preferredItem + } + + const objectPaths = usePaperStore + .getState() + .getItemsById(id) + .filter( + (item): item is paper.Path => + item instanceof paper.Path && isPaperObjectType(item.data?.type) + ) + + if (!objectPaths.length) return null + if (objectPaths.length === 1) return objectPaths[0] + + let nearestPath = objectPaths[0] + for (let index = 1; index < objectPaths.length; index += 1) { + const item = objectPaths[index] + if ( + item.getNearestPoint(localPoint).getDistance(localPoint) < + nearestPath.getNearestPoint(localPoint).getDistance(localPoint) + ) { + nearestPath = item + } + } + + return nearestPath +} + +const trySelectPaperObjectByPoint = (clickPoint: paper.Point) => { + const group = usePaperStore.getState().group + if (!group) return false + + const hitResult = group.hitTest(clickPoint, { + segments: true, + stroke: true, + curves: true, + fill: true, + guides: false, + tolerance: usePaperStore.getState().paperScope + ? 8 / useTopToolsStore.getState().scale + : 0, + match: (hit: paper.HitResult) => + !!hit.item.data?.id && isPaperContextHitType(hit.item.data?.type), + }) + + const id = hitResult?.item?.data?.id + if (!id) return false + + clearPaperSelectedObjects() + + const localPoint = group.globalToLocal(clickPoint) + const selectedPath = getPaperSelectionPath(id, localPoint, hitResult?.item) + const operationId = ( + selectedPath?.data?.operationId || + usePaperStore.getState().getItemById(id)?.data?.operationId || + hitResult?.item?.data?.operationId || + "" + ).toString() + + usePaperStore + .getState() + .getItemsById(id) + .forEach((item) => { + if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) + return + if (!isPaperObjectType(item.data?.type)) return + + if (item.data.type === "polygon") { + applyPaperSelectedPath(item) + return + } + + if ( + item instanceof paper.CompoundPath || + (selectedPath && item === selectedPath) + ) { + applyPaperSelectedPath(item) + } + }) + + const activeImage = useBottomToolsStore.getState().activeImage + useObjectStore.getState().updateSelectedPath(activeImage, "ADD", id) + + const category = useTopToolsStore + .getState() + .objectOperations.find( + (item) => item.category_id.toString() === operationId + ) + const { top, left } = getShowContextMenuPosition( + clickPoint, + (category || {}) as Project.LabelSchemaList, + 5, + 5 + ) + + useOtherToolsStore.getState().setShowContextMenu({ + showContextMenu: true, + position: { top, left }, + imageId: + selectedPath?.data?.imageId || + hitResult?.item?.data?.imageId || + activeImage, + operationId, + id, + }) + + return true +} + let isMiddleMousePanning = false let hoveredBufferPathInfo: { id: number @@ -3530,8 +3753,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoPolygonPoint() + if (polygon || points.length) { + hidePaperContextMenu() + undoPolygonPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -4113,8 +4342,18 @@ export const usePaperStore = create((set) => ({ let rect: paper.Path.Rectangle | null let rectangleTool = paperRectangleTool - rectangleTool.onMouseDown = (e: { event: MouseEvent }) => { + rectangleTool.onMouseDown = (e: { + event: MouseEvent + point: paper.Point + }) => { if (startMiddleMousePan(e.event)) return + if (e.event.button === 2) { + e.event.preventDefault() + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } + return + } if (e.event && e.event.button !== 2) useOtherToolsStore.getState().setShowContextMenu({ showContextMenu: false, @@ -4463,8 +4702,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoBrushPoint() + if (myPath || points.length) { + hidePaperContextMenu() + undoBrushPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -4934,8 +5179,14 @@ export const usePaperStore = create((set) => ({ if (startMiddleMousePan(e.event)) return if (e.event.button === 2) { e.event.preventDefault() - hidePaperContextMenu() - undoPoint() + if (myPath || points.length) { + hidePaperContextMenu() + undoPoint() + return + } + if (!trySelectPaperObjectByPoint(e.point)) { + hidePaperContextMenu() + } return } if (e.event.button !== 0) return @@ -5426,7 +5677,10 @@ export const usePaperStore = create((set) => ({ clearHoveredBufferPathHighlight() circle.strokeWidth = 4 circle.fillColor = new paper.Color("#FFF") - if (usePaperStore.getState().el) { + if ( + usePaperStore.getState().mode === "pan" && + usePaperStore.getState().el + ) { usePaperStore.getState().el!.style.cursor = "move" } } @@ -5436,9 +5690,7 @@ export const usePaperStore = create((set) => ({ } circle.strokeWidth = 2 circle.fillColor = color - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode() } return circle }, @@ -5485,7 +5737,7 @@ export const usePaperStore = create((set) => ({ !hoveredPathCircleInfo?.item.parent && !hoveredBufferPathInfo ) { - usePaperStore.getState().el!.style.cursor = "default" + syncPaperCursorByMode() } } const scheduleClear = () => { @@ -5564,45 +5816,31 @@ export const usePaperStore = create((set) => ({ switch (modeParam) { case "pan": usePaperStore.getState().panTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode(modeParam) break case "polygon": usePaperStore.getState().polygonTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "rectangle": usePaperStore.getState().rectangleTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "brush": usePaperStore.getState().brushTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "point": usePaperStore.getState().pointTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break case "support": usePaperStore.getState().supportTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "crosshair" - } + syncPaperCursorByMode(modeParam) break default: usePaperStore.getState().viewTool?.activate() - if (usePaperStore.getState().el) { - usePaperStore.getState().el!.style.cursor = "default" - } + syncPaperCursorByMode(modeParam) break } },