From d7d2bbf43fc28ca05abd8554f8bdcd16172e0760 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 11:00:08 +0800 Subject: [PATCH 1/2] 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, From bbde4df32958e5d0a3045c43d76adc780e9e82da Mon Sep 17 00:00:00 2001 From: zhangheng Date: Fri, 3 Apr 2026 13:37:37 +0800 Subject: [PATCH 2/2] 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,