From bbde4df32958e5d0a3045c43d76adc780e9e82da Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 13:37:37 +0800 Subject: [PATCH] fix(bug): fix --- app/project/detail/TaskTableContainer.tsx | 37 +- .../detail/components/WorkflowModal.tsx | 1 + components/label/components/BottomTools.tsx | 229 ++++++--- .../label/components/PaperContainer.tsx | 53 +- components/label/usePaperStore.ts | 467 ++++++++++++++---- 5 files changed, 610 insertions(+), 177 deletions(-) diff --git a/app/project/detail/TaskTableContainer.tsx b/app/project/detail/TaskTableContainer.tsx index bed3b6a..c9c4120 100644 --- a/app/project/detail/TaskTableContainer.tsx +++ b/app/project/detail/TaskTableContainer.tsx @@ -33,6 +33,8 @@ import { IconRefresh, IconSearch, } from "@tabler/icons-react" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" import { DataTable, DataTableColumn } from "mantine-datatable" import { useRouter, useSearchParams } from "next/navigation" import { useCallback, useEffect, useMemo, useState } from "react" @@ -41,6 +43,7 @@ import DispatchModal from "./components/DispatchModal" import ReleaseModal from "./components/ReleaseModal" import TaskStatusTag from "./components/TaskStatusTag" import WorkflowModal from "./components/WorkflowModal" +dayjs.extend(duration) interface FilterFormRecord { search_id: string @@ -441,6 +444,10 @@ export default function TaskTableContainer(props: { } catch {} } + const getTime = (time: number) => { + return dayjs.duration(time, "seconds").format("HH:mm:ss") + } + const columns = useMemo(() => { const cols: DataTableColumn[] = [ { @@ -673,37 +680,55 @@ export default function TaskTableContainer(props: { accessor: "work_time.label_work_time", title: "标注总耗时", width: 120, - render: (record) => record.work_time?.label_work_time ?? "-", + render: (record) => + record.work_time?.label_work_time + ? getTime(record.work_time?.label_work_time) + : "-", }, { accessor: "work_time.first_review_work_time", title: "审核总耗时", width: 120, - render: (record) => record.work_time?.first_review_work_time ?? "-", + render: (record) => + record.work_time?.first_review_work_time + ? getTime(record.work_time?.first_review_work_time) + : "-", }, { accessor: "work_time.second_review_work_time", title: "复审总耗时", width: 120, - render: (record) => record.work_time?.second_review_work_time ?? "-", + render: (record) => + record.work_time?.second_review_work_time + ? getTime(record.work_time?.second_review_work_time) + : "-", }, { accessor: "label_first_work_time", title: "标注首次耗时", width: 120, - render: (record) => record.label_first_work_time ?? "-", + render: (record) => + record.label_first_work_time + ? getTime(record.label_first_work_time) + : "-", }, { accessor: "review1_first_work_time", title: "审核首次耗时", width: 120, - render: (record) => record.review1_first_work_time ?? "-", + render: (record) => + record.review1_first_work_time + ? getTime(record.review1_first_work_time) + : "-", }, { accessor: "review2_first_work_time", title: "复审首次耗时", width: 120, - render: (record) => record.review2_first_work_time ?? "-", + render: (record) => + record.review2_first_work_time + ? getTime(record.review2_first_work_time) + : "-", }, { accessor: "first_reject_label_time", diff --git a/app/project/detail/components/WorkflowModal.tsx b/app/project/detail/components/WorkflowModal.tsx index d71cafd..58bb986 100644 --- a/app/project/detail/components/WorkflowModal.tsx +++ b/app/project/detail/components/WorkflowModal.tsx @@ -19,6 +19,7 @@ const DISPLAY_COLUMNS: Array<{ accessor: string; title: string }> = [ { accessor: "task_status_dst", title: "目标状态" }, { accessor: "old_username", title: "原处理人" }, { accessor: "new_username", title: "目标处理人" }, + { accessor: "create_at", title: "创建时间" }, { accessor: "action", title: "操作" }, ] diff --git a/components/label/components/BottomTools.tsx b/components/label/components/BottomTools.tsx index 35b382d..5d34d46 100644 --- a/components/label/components/BottomTools.tsx +++ b/components/label/components/BottomTools.tsx @@ -17,6 +17,10 @@ import { IconChevronsLeft, IconChevronsRight, IconFlag, + IconPlayerTrackNext, + IconPlayerTrackNextFilled, + IconPlayerTrackPrev, + IconPlayerTrackPrevFilled, } from "@tabler/icons-react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Project } from "../api/project/typing" @@ -46,6 +50,8 @@ interface BottomToolsComponentProps { ) => void | null } +type PlaybackDirection = "backward" | "forward" + const BottomTools: React.FC = (props) => { const { labelState, projectDetail, taskDetail, renderPolygons } = props @@ -72,35 +78,34 @@ const BottomTools: React.FC = (props) => { const [open, setOpen] = useState(false) const [conflictOpen, setConflictOpen] = useState(false) + const [playDirection, setPlayDirection] = useState( + null + ) + const allKeys = useMemo(() => Array.from(labelState.keys()), [labelState]) const isShowKeyFrame = projectDetail && [5, 6].includes(projectDetail.label_type) && - Array.from(labelState.keys()).some( + allKeys.some( (k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame ) && onlyKeyFrame - const currentKeys = Array.from(labelState.keys()).filter((k) => - isShowKeyFrame - ? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame - : true + const currentKeys = useMemo( + () => + allKeys.filter((k) => + isShowKeyFrame + ? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame + : true + ), + [allKeys, isShowKeyFrame, keyFrameData] ) // 上一帧图片id const previousFrameKey = useMemo(() => { - let key = "" - const keys = Array.from(labelState.keys()).filter((k) => - isShowKeyFrame - ? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame - : true - ) - keys.forEach((k, index) => { - if (k === activeImage && index > 0) key = keys[index - 1] - }) - return key - }, [activeImage, isShowKeyFrame, keyFrameData, labelState]) + const currentIndex = currentKeys.findIndex((k) => k === activeImage) + return currentIndex > 0 ? currentKeys[currentIndex - 1] : "" + }, [activeImage, currentKeys]) useEffect(() => { - const keys = Array.from(labelState.keys()) - if (!keys.length) { + if (!currentKeys.length) { setActiveImage("") setInputNumber("") setActiveImageId(0) @@ -108,36 +113,42 @@ const BottomTools: React.FC = (props) => { } const currentIndex = activeImage - ? keys.findIndex((k) => k === activeImage) + ? currentKeys.findIndex((k) => k === activeImage) : -1 if (currentIndex >= 0) { setInputNumber(String(currentIndex + 1)) - setActiveImageId(currentIndex + 1) + const absoluteIndex = allKeys.findIndex((k) => k === activeImage) + setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0) return } - const firstKey = keys[0] + const firstKey = currentKeys[0] + const absoluteIndex = allKeys.findIndex((k) => k === firstKey) setActiveImage(firstKey) setInputNumber("1") - setActiveImageId(1) + setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 1) }, [ activeImage, - labelState, + allKeys, + currentKeys, setActiveImage, setActiveImageId, setInputNumber, ]) - const scrollToIndex = (index: number) => { - const key = currentKeys[index] - if (key && itemRefs.current[key]) { - itemRefs.current[key]?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - inline: "center", - }) - } - } + const scrollToIndex = useCallback( + (index: number) => { + const key = currentKeys[index] + if (key && itemRefs.current[key]) { + itemRefs.current[key]?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }) + } + }, + [currentKeys] + ) const closeRightContext = useCallback(() => { useOtherToolsStore.getState().setShowContextMenu({ @@ -152,6 +163,72 @@ const BottomTools: React.FC = (props) => { }) }, []) + const goToVisibleIndex = useCallback( + (index: number) => { + if (index < 0 || index >= currentKeys.length) return false + + const nextImage = currentKeys[index] + const absoluteIndex = allKeys.findIndex((k) => k === nextImage) + + scrollToIndex(index) + setInputNumber(String(index + 1)) + setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0) + setActiveImage(nextImage) + closeRightContext() + + return true + }, + [ + allKeys, + closeRightContext, + currentKeys, + scrollToIndex, + setActiveImage, + setActiveImageId, + setInputNumber, + ] + ) + + const stepVisibleImage = useCallback( + (direction: PlaybackDirection) => { + const currentIndex = currentKeys.findIndex((k) => k === activeImage) + if (currentIndex < 0) return false + + const nextIndex = + direction === "backward" ? currentIndex - 1 : currentIndex + 1 + + return goToVisibleIndex(nextIndex) + }, + [activeImage, currentKeys, goToVisibleIndex] + ) + + const togglePlayback = useCallback( + (direction: PlaybackDirection) => { + if (currentKeys.length < 2) return + setPlayDirection((current) => (current === direction ? null : direction)) + }, + [currentKeys.length] + ) + + useEffect(() => { + if (!playDirection) return + if (currentKeys.length < 2) { + setPlayDirection(null) + return + } + + const timer = window.setInterval(() => { + const moved = stepVisibleImage(playDirection) + if (!moved) { + setPlayDirection(null) + } + }, 1000) + + return () => { + window.clearInterval(timer) + } + }, [currentKeys.length, playDirection, stepVisibleImage]) + const getOperationSchema = useCallback( (operationId: string) => { return ( @@ -368,28 +445,32 @@ const BottomTools: React.FC = (props) => { color="gray" size="sm" onClick={() => { - scrollToIndex(0) - currentKeys.length && setActiveImage(currentKeys[0]) - setInputNumber("1") - setActiveImageId(1) - closeRightContext() + goToVisibleIndex(0) }}> + { + togglePlayback("backward") + }}> + {playDirection === "backward" ? ( + + ) : ( + + )} + { - if (inputNumber !== "1") { - let newActiveIndex = +inputNumber - 1 - setInputNumber(newActiveIndex.toString()) - setActiveImageId(newActiveIndex) - scrollToIndex(newActiveIndex - 1) - - setActiveImage(currentKeys?.[newActiveIndex - 1]) - closeRightContext() - } + stepVisibleImage("backward") }}> @@ -410,10 +491,21 @@ const BottomTools: React.FC = (props) => { }} onKeyDown={(e) => { if (e.key === "Enter") { - scrollToIndex(+inputNumber - 1) - setActiveImage(currentKeys?.[+inputNumber - 1]) - setActiveImageId(+inputNumber) - closeRightContext() + const targetIndex = Number(inputNumber) + if ( + Number.isInteger(targetIndex) && + targetIndex >= 1 && + targetIndex <= currentKeys.length + ) { + goToVisibleIndex(targetIndex - 1) + } else { + const currentIndex = currentKeys.findIndex( + (key) => key === activeImage + ) + setInputNumber( + currentIndex >= 0 ? String(currentIndex + 1) : "" + ) + } } }} /> @@ -423,27 +515,32 @@ const BottomTools: React.FC = (props) => { color="gray" size="sm" onClick={() => { - if (+inputNumber < currentKeys.length) { - let newActiveIndex = +inputNumber + 1 - setInputNumber(newActiveIndex.toString()) - setActiveImageId(newActiveIndex) - scrollToIndex(newActiveIndex - 1) - setActiveImage(currentKeys?.[newActiveIndex - 1]) - closeRightContext() - } + stepVisibleImage("forward") }}> + { + togglePlayback("forward") + }}> + {playDirection === "forward" ? ( + + ) : ( + + )} + { - scrollToIndex(currentKeys.length - 1) - setInputNumber(currentKeys.length.toString()) - setActiveImageId(currentKeys.length) - setActiveImage(currentKeys?.[currentKeys.length - 1]) - closeRightContext() + goToVisibleIndex(currentKeys.length - 1) }}> @@ -657,9 +754,7 @@ const BottomTools: React.FC = (props) => { if (ctrlKey || shiftKey) { setSelectedItems(key, ctrlKey, shiftKey) } else { - setInputNumber(index + 1 + "") - setActiveImage(key) - setActiveImageId(index + 1) + goToVisibleIndex(index) setSelectedItems("", false, false) } closeRightContext() diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx index 99d7aac..003b8f8 100644 --- a/components/label/components/PaperContainer.tsx +++ b/components/label/components/PaperContainer.tsx @@ -95,6 +95,7 @@ const renderRaster = (paperGroup: paper.Group, option: any) => { } type NormalizedPoint = [number, number] +type SupportBox = [number, number, number, number] type ContextMenuLayout = { top: number left: number @@ -447,7 +448,8 @@ const PaperContainer = ( async ( points: Array<[number, number]>, tags: number[], - clear_previous_state = false + clear_previous_state = false, + boxes: SupportBox[] = [] ) => { void clear_previous_state try { @@ -475,6 +477,9 @@ const PaperContainer = ( usePaperStore.getState().rasterScale[activeImage] ?? 1 const draw = () => { + const paperGroup = usePaperStore.getState().group! + const raster = paperGroup.getItems({ data: { name: "pic" } })[0] + points.forEach((point, index) => { const drawPoint = new paper.Path.Circle({ center: point, @@ -486,16 +491,30 @@ const PaperContainer = ( type: "point", }, }) - drawPoint.parent = usePaperStore.getState().group! - const raster = usePaperStore - .getState() - .group!.getItems({ data: { name: "pic" } })[0] + drawPoint.parent = paperGroup if (!raster.contains(drawPoint.position)) { throw new Error("点位超出图片范围") } }) + + boxes.forEach(([left, top, right, bottom]) => { + new paper.Path.Rectangle({ + parent: paperGroup, + from: [left, top], + to: [right, bottom], + strokeColor: "#FFF", + strokeWidth: 2, + strokeScaling: false, + dashArray: [6, 4], + fillColor: new paper.Color(1, 1, 1, 0.05), + data: { + id: "support", + type: "box", + }, + }) + }) } - console.log(points, tags) + console.log(points, tags, boxes) clearSupportAnnotationItem() draw() @@ -512,8 +531,12 @@ const PaperContainer = ( const promptPoints = foregroundPoints.length ? foregroundPoints : normalizedPoints - // const promptBox = - // promptPoints.length > 1 ? getContourBox(promptPoints) : null + const normalizedBoxes = boxes.map(([left, top, right, bottom]) => [ + Math.round(Math.min(left, right) / currentScale), + Math.round(Math.min(top, bottom) / currentScale), + Math.round(Math.max(left, right) / currentScale), + Math.round(Math.max(top, bottom) / currentScale), + ]) as SupportBox[] const projectId = projectDetail?.name || projectDetail?.rpath || @@ -522,8 +545,8 @@ const PaperContainer = ( if (!projectId) { throw new Error("缺少 project_id,无法调用模型接口") } - if (!promptPoints.length) { - throw new Error("缺少有效点位,无法调用模型接口") + if (!promptPoints.length && !normalizedBoxes.length) { + throw new Error("缺少有效提示,无法调用模型接口") } // const picSize = usePaperStore.getState().rasterSize[activeImage] @@ -533,9 +556,13 @@ const PaperContainer = ( image: activeImage, // project_id: "image_test_bk", prompt: { - foreground_points: promptPoints, - background_points: background_points, - // ...(promptBox ? { boxes: [promptBox] } : {}), + ...(promptPoints.length + ? { foreground_points: promptPoints } + : {}), + ...(background_points.length + ? { background_points: background_points } + : {}), + ...(normalizedBoxes.length ? { boxes: normalizedBoxes } : {}), }, }) const { contours = [], hollows = [] } = data || {} diff --git a/components/label/usePaperStore.ts b/components/label/usePaperStore.ts index 1800651..e946436 100644 --- a/components/label/usePaperStore.ts +++ b/components/label/usePaperStore.ts @@ -139,7 +139,8 @@ interface PaperState { renderPath: ( points: Array<[number, number]>, tags: number[], - flag?: boolean + flag?: boolean, + boxes?: Array<[number, number, number, number]> ) => void ) => void getClickedCircle: ( @@ -2574,6 +2575,12 @@ export const usePaperStore = create((set) => ({ } polygon.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return if (polygon.data.id) { let category = useTopToolsStore .getState() @@ -2620,6 +2627,12 @@ export const usePaperStore = create((set) => ({ point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return if (compoundPath.data.id) { let category = useTopToolsStore .getState() @@ -2659,11 +2672,12 @@ export const usePaperStore = create((set) => ({ let polygon: paper.Path | paper.CompoundPath | null let compoundPolygon: paper.Path | paper.CompoundPath | null let points: paper.Point[] = [] - let lastPoint: paper.Point - let lastTime: number + let lastPoint: paper.Point | null = null + let lastTime = 0 let isMouseDown: boolean = false let polygonTool = paperPolygonTool let currentMagnetPoint: paper.Point | null = null + let draftId: number | null = null const handleCircles = (path: paper.Path | paper.CompoundPath | null) => { // 更新选中的点 @@ -2688,6 +2702,71 @@ export const usePaperStore = create((set) => ({ }) } + const getDraftPolygonData = () => ({ + imageId: useBottomToolsStore.getState().activeImage, + operationId: useTopToolsStore.getState().activeOperation, + id: + draftId ?? + (draftId = + useRightToolsStore + .getState() + .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1), + }) + + const clearPolygonMagnet = () => { + const magnetPaths = usePaperStore + .getState() + .group!.getItems({ data: { type: "magnet" } }) + magnetPaths.forEach((item) => { + item.remove() + }) + currentMagnetPoint = null + } + + const rebuildPolygonDraft = () => { + polygon?.remove() + polygon = null + + if (points.length) { + polygon = usePaperStore.getState().addPolygon( + renderPath, + points, + { + ...usePaperStore.getState().toolOption, + }, + getDraftPolygonData() + ) + } else { + draftId = null + } + + handleCircles(polygon) + lastPoint = points.length + ? usePaperStore + .getState() + .group!.localToGlobal(points[points.length - 1]) + : null + clearPolygonMagnet() + } + + const resetPolygonDraft = () => { + polygon?.remove() + polygon = null + points = [] + handleCircles(null) + lastPoint = null + lastTime = 0 + draftId = null + clearPolygonMagnet() + } + + const undoPolygonPoint = () => { + if (!points.length) return false + points.pop() + rebuildPolygonDraft() + return true + } + const handlePolygonIntersectRaster = (savePath: paper.Path) => { let oldPolygon: any = savePath polygon = oldPolygon.intersect( @@ -2781,6 +2860,12 @@ export const usePaperStore = create((set) => ({ point: paper.Point }) => { if (startMiddleMousePan(e.event)) return + if (e.event.button === 2) { + e.event.preventDefault() + hidePaperContextMenu() + undoPolygonPoint() + return + } if (e.event.button !== 0) return isMouseDown = true @@ -2803,6 +2888,7 @@ export const usePaperStore = create((set) => ({ selectedCircles.forEach((item) => { item.remove() }) + selectedCircles = [] } // 调整polygon点位初始顺序 @@ -3000,15 +3086,12 @@ export const usePaperStore = create((set) => ({ selectedCircles.forEach((item) => { item.remove() }) - const magnetPaths = usePaperStore - .getState() - .group!.getItems({ data: { type: "magnet" } }) - magnetPaths.forEach((item) => { - item.remove() - }) - currentMagnetPoint = null - + selectedCircles = [] + clearPolygonMagnet() useTopToolsStore.getState().setPressA(false) + draftId = null + lastPoint = null + lastTime = 0 return } } else { @@ -3036,14 +3119,15 @@ export const usePaperStore = create((set) => ({ let point = currentMagnetPoint ? currentMagnetPoint : temp.globalToLocal(e.point) + const draftData = getDraftPolygonData() let circle = usePaperStore .getState() .getClickedCircle( point, selectedCircles.length, - polygon?.data.id, - polygon?.data.blankColor + draftData.id, + usePaperStore.getState().toolOption.blankColor ) // add circle selectedCircles.push(circle) @@ -3060,14 +3144,7 @@ export const usePaperStore = create((set) => ({ // option ...usePaperStore.getState().toolOption, }, - { - imageId: useBottomToolsStore.getState().activeImage, - operationId: useTopToolsStore.getState().activeOperation, - id: - useRightToolsStore - .getState() - .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1, - } + draftData ) lastPoint = checkPoint @@ -3139,7 +3216,7 @@ export const usePaperStore = create((set) => ({ } } }) - if (isMouseDown) { + if (isMouseDown && lastPoint) { const delta = currentMagnetPoint ? (currentMagnetPoint as paper.Point).subtract(lastPoint) : e.point.subtract(lastPoint) @@ -3187,27 +3264,17 @@ export const usePaperStore = create((set) => ({ // option ...usePaperStore.getState().toolOption, }, - {} + getDraftPolygonData() ) } } } polygonTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { - if (polygon) { - polygon.remove() - polygon = null - points = [] - usePaperSupportStore.getState().handleBufferPaths(polygon) - handleCircles(polygon) + if (polygon || points.length) { + resetPolygonDraft() + usePaperSupportStore.getState().handleBufferPaths(null) } - const magnetPaths = usePaperStore - .getState() - .group!.getItems({ data: { type: "magnet" } }) - magnetPaths.forEach((item) => { - item.remove() - }) - currentMagnetPoint = null } else if (e.key === "delete") { handleDelete() forceUpdate() @@ -3255,6 +3322,12 @@ export const usePaperStore = create((set) => ({ } rect.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return if (rect.data.id) { let category = useTopToolsStore .getState() @@ -3469,6 +3542,12 @@ export const usePaperStore = create((set) => ({ // }); brush.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return // console.log(e.event.clientX, e.event.clientY); if (brush.data.id) { let category = useTopToolsStore @@ -3506,10 +3585,66 @@ export const usePaperStore = create((set) => ({ let brushCircles: paper.Path.Circle[] = [] let myPath: paper.Path | null let compoundPolygon: paper.Path | paper.CompoundPath | null - let lastPoint: paper.Point + let lastPoint: paper.Point | null = null let points: paper.Point[] = [] let brushTool = paperBrushTool + let draftId: number | null = null + + const getDraftBrushData = () => ({ + imageId: useBottomToolsStore.getState().activeImage, + operationId: useTopToolsStore.getState().activeOperation, + id: + draftId ?? + (draftId = + useRightToolsStore + .getState() + .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1), + }) + + const rebuildBrushDraft = () => { + myPath?.remove() + myPath = null + + if (points.length) { + myPath = usePaperStore.getState().addBrush( + renderPath, + points, + { + ...usePaperStore.getState().toolOption, + }, + getDraftBrushData() + ) + } else { + draftId = null + } + + lastPoint = points.length + ? usePaperStore + .getState() + .group!.localToGlobal(points[points.length - 1]) + : null + } + + const resetBrushDraft = () => { + brushCircles.forEach((item) => { + item.remove() + }) + brushCircles = [] + myPath?.remove() + myPath = null + points = [] + lastPoint = null + draftId = null + } + + const undoBrushPoint = () => { + if (!points.length) return false + points.pop() + brushCircles.pop()?.remove() + rebuildBrushDraft() + return true + } const handleBrushIntersectRaster = (savePath: paper.Path) => { let intersections = savePath.getIntersections( @@ -3576,6 +3711,12 @@ export const usePaperStore = create((set) => ({ brushTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (startMiddleMousePan(e.event)) return + if (e.event.button === 2) { + e.event.preventDefault() + hidePaperContextMenu() + undoBrushPoint() + return + } if (e.event.button !== 0) return // 绘制时去除选中辅助 @@ -3771,8 +3912,11 @@ export const usePaperStore = create((set) => ({ brushCircles.forEach((item) => { item.remove() }) + brushCircles = [] useTopToolsStore.getState().setPressA(false) + draftId = null + lastPoint = null return } } else { @@ -3790,6 +3934,7 @@ export const usePaperStore = create((set) => ({ let temp = renderPath(usePaperStore.getState().group!, {}) let point = temp.globalToLocal(e.point) + const draftData = getDraftBrushData() // add point points.push(point) @@ -3803,22 +3948,15 @@ export const usePaperStore = create((set) => ({ // option ...usePaperStore.getState().toolOption, }, - { - imageId: useBottomToolsStore.getState().activeImage, - operationId: useTopToolsStore.getState().activeOperation, - id: - useRightToolsStore - .getState() - .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1, - } + draftData ) let circle = usePaperStore .getState() .getClickedCircle( point, brushCircles.length, - myPath?.data.id, - myPath?.data.blankColor + draftData.id, + usePaperStore.getState().toolOption.blankColor ) brushCircles.push(circle) @@ -3846,7 +3984,7 @@ export const usePaperStore = create((set) => ({ fillColor: undefined, closed: false, }, - {} + getDraftBrushData() ) } } @@ -3857,13 +3995,8 @@ export const usePaperStore = create((set) => ({ brushTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { - if (myPath) { - brushCircles.forEach((item) => { - item.remove() - }) - myPath.remove() - myPath = null - points = [] + if (myPath || points.length) { + resetBrushDraft() } } else if (e.key === "delete") { handleDelete() @@ -3900,6 +4033,12 @@ export const usePaperStore = create((set) => ({ myCircle.data = Object.assign({ type: "circle", ...option }, data) myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return // console.log(e.event.clientX, e.event.clientY); if (myCircle.data.id) { let category = useTopToolsStore @@ -3973,13 +4112,82 @@ export const usePaperStore = create((set) => ({ let myCircleText: paper.PointText[] = [] let textNumbers: number[] = [] let myPath: paper.Path | null - let lastPoint: paper.Point + let lastPoint: paper.Point | null = null let points: paper.Point[] = [] let pointTool = paperPointTool + let draftId: number | null = null + + const getDraftPointData = () => ({ + imageId: useBottomToolsStore.getState().activeImage, + operationId: useTopToolsStore.getState().activeOperation, + id: + draftId ?? + (draftId = + useRightToolsStore + .getState() + .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1), + }) + + const rebuildPointDraft = () => { + myPath?.remove() + myPath = null + + if (points.length) { + myPath = usePaperStore.getState().addPointLine( + renderPath, + points, + { + ...usePaperStore.getState().toolOption, + }, + getDraftPointData() + ) + } else { + draftId = null + } + + lastPoint = points.length + ? usePaperStore + .getState() + .group!.localToGlobal(points[points.length - 1]) + : null + } + + const resetPointDraft = () => { + myPath?.remove() + myPath = null + points = [] + textNumbers = [] + myCircleText.forEach((item) => { + item.remove() + }) + myCircleText = [] + myCircle.forEach((item) => { + item.remove() + }) + myCircle = [] + lastPoint = null + draftId = null + } + + const undoPoint = () => { + if (!points.length) return false + points.pop() + textNumbers.pop() + myCircle.pop()?.remove() + myCircleText.pop()?.remove() + rebuildPointDraft() + return true + } pointTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (startMiddleMousePan(e.event)) return + if (e.event.button === 2) { + e.event.preventDefault() + hidePaperContextMenu() + undoPoint() + return + } if (e.event.button !== 0) return // 绘制时去除选中辅助 @@ -4106,6 +4314,8 @@ export const usePaperStore = create((set) => ({ myCircleText = [] // 避免后续按键删除已绘制的circle myCircle = [] + draftId = null + lastPoint = null return } @@ -4125,6 +4335,7 @@ export const usePaperStore = create((set) => ({ let temp = renderCircle(usePaperStore.getState().group!, {}) let point = temp.globalToLocal(e.point) let text = textNumbers.reduce((a, b) => Math.max(a, b), 0) + 1 + const draftData = getDraftPointData() let circle = usePaperStore.getState().addPoint( renderCircle, @@ -4137,13 +4348,8 @@ export const usePaperStore = create((set) => ({ strokeScaling: false, }, { - imageId: useBottomToolsStore.getState().activeImage, - operationId: useTopToolsStore.getState().activeOperation, + ...draftData, text: text, - id: - useRightToolsStore - .getState() - .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1, } ) // add circle @@ -4174,14 +4380,7 @@ export const usePaperStore = create((set) => ({ // option ...usePaperStore.getState().toolOption, }, - { - imageId: useBottomToolsStore.getState().activeImage, - operationId: useTopToolsStore.getState().activeOperation, - id: - useRightToolsStore - .getState() - .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1, - } + draftData ) lastPoint = e.point @@ -4208,7 +4407,7 @@ export const usePaperStore = create((set) => ({ fillColor: undefined, closed: false, }, - {} + getDraftPointData() ) } } @@ -4217,19 +4416,8 @@ export const usePaperStore = create((set) => ({ } pointTool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape" || e.key === "alt") { - if (myPath) { - myPath.remove() - myPath = null - points = [] - textNumbers = [] - myCircleText.forEach((item) => { - item.remove() - }) - myCircleText = [] - myCircle.forEach((item) => { - item.remove() - }) - myCircle = [] + if (myPath || points.length) { + resetPointDraft() } } else if (e.key === "delete") { handleDelete() @@ -4283,6 +4471,12 @@ export const usePaperStore = create((set) => ({ circle.scale(newScale / currentScaling) circle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (e.event && e.event.button === 2) { + if ( + ["polygon", "brush", "point"].includes( + usePaperStore.getState().mode || "" + ) + ) + return // console.log(e.event.clientX, e.event.clientY); if (circle.data.id) { let category = useTopToolsStore @@ -4325,34 +4519,125 @@ export const usePaperStore = create((set) => ({ initSupportTool: (tool, renderPath) => { let points: Array<[number, number]> = [] let tags: number[] = [] + let boxes: Array<[number, number, number, number]> = [] + let dragStartPoint: paper.Point | null = null + let dragButton: number | null = null + let supportBox: paper.Path.Rectangle | null = null + let isBoxDragging = false + + const BOX_DRAG_THRESHOLD = 6 + + const clearSupportBoxPreview = () => { + supportBox?.remove() + supportBox = null + isBoxDragging = false + } + + const getSupportBox = ( + from: paper.Point, + to: paper.Point + ): [number, number, number, number] => [ + Math.min(from.x, to.x), + Math.min(from.y, to.y), + Math.max(from.x, to.x), + Math.max(from.y, to.y), + ] + + const renderSupportBoxPreview = (from: paper.Point, to: paper.Point) => { + const [left, top, right, bottom] = getSupportBox(from, to) + supportBox?.remove() + supportBox = new paper.Path.Rectangle({ + parent: usePaperStore.getState().group!, + from: [left, top], + to: [right, bottom], + strokeColor: "#FFF", + strokeWidth: 2, + strokeScaling: false, + dashArray: [6, 4], + fillColor: new paper.Color(1, 1, 1, 0.05), + data: { + id: "support", + type: "box", + }, + }) + } + + const renderSupportPrompt = () => { + void renderPath(points, tags, points.length + boxes.length === 1, boxes) + } tool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { if (startMiddleMousePan(e.event)) return - let point = usePaperStore.getState().group!.globalToLocal(e.point) - points.push([point.x, point.y]) - tags.push(e.event.button === 0 ? 1 : 0) - renderPath(points, tags, points.length === 1) + dragStartPoint = usePaperStore.getState().group!.globalToLocal(e.point) + dragButton = e.event.button + clearSupportBoxPreview() } - tool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => { + + tool.onMouseDrag = (e: { + event: MouseEvent + point: paper.Point + delta: paper.Point + }) => { if (dragMiddleMousePan(e)) return + + if (!dragStartPoint || dragButton !== 0) return + + const currentPoint = usePaperStore + .getState() + .group!.globalToLocal(e.point) + if (currentPoint.subtract(dragStartPoint).length <= BOX_DRAG_THRESHOLD) + return + + isBoxDragging = true + renderSupportBoxPreview(dragStartPoint, currentPoint) } - tool.onMouseUp = (_e: paper.ToolEvent) => { - stopMiddleMousePan() + + tool.onMouseUp = (e: paper.ToolEvent) => { + if (stopMiddleMousePan()) return + if (!dragStartPoint || dragButton === null) return + + const currentPoint = usePaperStore + .getState() + .group!.globalToLocal(e.point) + + if (dragButton === 0 && isBoxDragging) { + boxes.push(getSupportBox(dragStartPoint, currentPoint)) + } else { + points.push([dragStartPoint.x, dragStartPoint.y]) + tags.push(dragButton === 0 ? 1 : 0) + } + + renderSupportPrompt() + clearSupportBoxPreview() + dragStartPoint = null + dragButton = null } tool.onKeyDown = (e: paper.KeyEvent) => { if (e.key === "escape") { points = [] tags = [] + boxes = [] + dragStartPoint = null + dragButton = null + clearSupportBoxPreview() clearSupportAnnotationItem() usePaperStore.getState().setPaperMode("pan") } if (e.key === "f1") { points = [] tags = [] + boxes = [] + dragStartPoint = null + dragButton = null + clearSupportBoxPreview() } } + tool.onMouseMove = (e: paper.MouseEvent) => { + handleMouseMove(e) + } + set((state: PaperState) => ({ ...state, supportTool: tool,