Merge branch 'zh' into 'main'

Zh

See merge request prism/lable/labelimage!5
This commit is contained in:
刘耀勇
2026-04-03 14:00:11 +08:00
7 changed files with 968 additions and 304 deletions

View File

@@ -33,6 +33,8 @@ import {
IconRefresh, IconRefresh,
IconSearch, IconSearch,
} from "@tabler/icons-react" } from "@tabler/icons-react"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import { DataTable, DataTableColumn } from "mantine-datatable" import { DataTable, DataTableColumn } from "mantine-datatable"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
@@ -41,6 +43,7 @@ import DispatchModal from "./components/DispatchModal"
import ReleaseModal from "./components/ReleaseModal" import ReleaseModal from "./components/ReleaseModal"
import TaskStatusTag from "./components/TaskStatusTag" import TaskStatusTag from "./components/TaskStatusTag"
import WorkflowModal from "./components/WorkflowModal" import WorkflowModal from "./components/WorkflowModal"
dayjs.extend(duration)
interface FilterFormRecord { interface FilterFormRecord {
search_id: string search_id: string
@@ -441,6 +444,10 @@ export default function TaskTableContainer(props: {
} catch {} } catch {}
} }
const getTime = (time: number) => {
return dayjs.duration(time, "seconds").format("HH:mm:ss")
}
const columns = useMemo(() => { const columns = useMemo(() => {
const cols: DataTableColumn<Task.DataProps>[] = [ const cols: DataTableColumn<Task.DataProps>[] = [
{ {
@@ -673,37 +680,55 @@ export default function TaskTableContainer(props: {
accessor: "work_time.label_work_time", accessor: "work_time.label_work_time",
title: "标注总耗时", title: "标注总耗时",
width: 120, 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", accessor: "work_time.first_review_work_time",
title: "审核总耗时", title: "审核总耗时",
width: 120, 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", accessor: "work_time.second_review_work_time",
title: "复审总耗时", title: "复审总耗时",
width: 120, 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", accessor: "label_first_work_time",
title: "标注首次耗时", title: "标注首次耗时",
width: 120, 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", accessor: "review1_first_work_time",
title: "审核首次耗时", title: "审核首次耗时",
width: 120, 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", accessor: "review2_first_work_time",
title: "复审首次耗时", title: "复审首次耗时",
width: 120, 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", accessor: "first_reject_label_time",

View File

@@ -19,6 +19,7 @@ const DISPLAY_COLUMNS: Array<{ accessor: string; title: string }> = [
{ accessor: "task_status_dst", title: "目标状态" }, { accessor: "task_status_dst", title: "目标状态" },
{ accessor: "old_username", title: "原处理人" }, { accessor: "old_username", title: "原处理人" },
{ accessor: "new_username", title: "目标处理人" }, { accessor: "new_username", title: "目标处理人" },
{ accessor: "create_at", title: "创建时间" },
{ accessor: "action", title: "操作" }, { accessor: "action", title: "操作" },
] ]

View File

@@ -1642,7 +1642,6 @@ const LabelPage = ({
showGroupList, showGroupList,
showDescList && projectDetail && projectDetail.label_type === 5, showDescList && projectDetail && projectDetail.label_type === 5,
showDescList && projectDetail && projectDetail.label_type === 6, showDescList && projectDetail && projectDetail.label_type === 6,
showDescList && projectDetail && projectDetail.label_type === 7,
showObjectList, showObjectList,
] ]
let count = 0 let count = 0

View File

@@ -17,6 +17,10 @@ import {
IconChevronsLeft, IconChevronsLeft,
IconChevronsRight, IconChevronsRight,
IconFlag, IconFlag,
IconPlayerTrackNext,
IconPlayerTrackNextFilled,
IconPlayerTrackPrev,
IconPlayerTrackPrevFilled,
} from "@tabler/icons-react" } from "@tabler/icons-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Project } from "../api/project/typing" import { Project } from "../api/project/typing"
@@ -46,6 +50,8 @@ interface BottomToolsComponentProps {
) => void | null ) => void | null
} }
type PlaybackDirection = "backward" | "forward"
const BottomTools: React.FC<BottomToolsComponentProps> = (props) => { const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
const { labelState, projectDetail, taskDetail, renderPolygons } = props const { labelState, projectDetail, taskDetail, renderPolygons } = props
@@ -72,35 +78,34 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [conflictOpen, setConflictOpen] = useState(false) const [conflictOpen, setConflictOpen] = useState(false)
const [playDirection, setPlayDirection] = useState<PlaybackDirection | null>(
null
)
const allKeys = useMemo(() => Array.from(labelState.keys()), [labelState])
const isShowKeyFrame = const isShowKeyFrame =
projectDetail && projectDetail &&
[5, 6].includes(projectDetail.label_type) && [5, 6].includes(projectDetail.label_type) &&
Array.from(labelState.keys()).some( allKeys.some(
(k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame (k) => keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
) && ) &&
onlyKeyFrame onlyKeyFrame
const currentKeys = Array.from(labelState.keys()).filter((k) => const currentKeys = useMemo(
isShowKeyFrame () =>
? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame allKeys.filter((k) =>
: true isShowKeyFrame
? keyFrameData.has(k) && keyFrameData.get(k)?.key_frame
: true
),
[allKeys, isShowKeyFrame, keyFrameData]
) )
// 上一帧图片id // 上一帧图片id
const previousFrameKey = useMemo(() => { const previousFrameKey = useMemo(() => {
let key = "" const currentIndex = currentKeys.findIndex((k) => k === activeImage)
const keys = Array.from(labelState.keys()).filter((k) => return currentIndex > 0 ? currentKeys[currentIndex - 1] : ""
isShowKeyFrame }, [activeImage, currentKeys])
? 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])
useEffect(() => { useEffect(() => {
const keys = Array.from(labelState.keys()) if (!currentKeys.length) {
if (!keys.length) {
setActiveImage("") setActiveImage("")
setInputNumber("") setInputNumber("")
setActiveImageId(0) setActiveImageId(0)
@@ -108,36 +113,42 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
} }
const currentIndex = activeImage const currentIndex = activeImage
? keys.findIndex((k) => k === activeImage) ? currentKeys.findIndex((k) => k === activeImage)
: -1 : -1
if (currentIndex >= 0) { if (currentIndex >= 0) {
setInputNumber(String(currentIndex + 1)) setInputNumber(String(currentIndex + 1))
setActiveImageId(currentIndex + 1) const absoluteIndex = allKeys.findIndex((k) => k === activeImage)
setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 0)
return return
} }
const firstKey = keys[0] const firstKey = currentKeys[0]
const absoluteIndex = allKeys.findIndex((k) => k === firstKey)
setActiveImage(firstKey) setActiveImage(firstKey)
setInputNumber("1") setInputNumber("1")
setActiveImageId(1) setActiveImageId(absoluteIndex >= 0 ? absoluteIndex + 1 : 1)
}, [ }, [
activeImage, activeImage,
labelState, allKeys,
currentKeys,
setActiveImage, setActiveImage,
setActiveImageId, setActiveImageId,
setInputNumber, setInputNumber,
]) ])
const scrollToIndex = (index: number) => { const scrollToIndex = useCallback(
const key = currentKeys[index] (index: number) => {
if (key && itemRefs.current[key]) { const key = currentKeys[index]
itemRefs.current[key]?.scrollIntoView({ if (key && itemRefs.current[key]) {
behavior: "smooth", itemRefs.current[key]?.scrollIntoView({
block: "nearest", behavior: "smooth",
inline: "center", block: "nearest",
}) inline: "center",
} })
} }
},
[currentKeys]
)
const closeRightContext = useCallback(() => { const closeRightContext = useCallback(() => {
useOtherToolsStore.getState().setShowContextMenu({ useOtherToolsStore.getState().setShowContextMenu({
@@ -152,6 +163,72 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (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( const getOperationSchema = useCallback(
(operationId: string) => { (operationId: string) => {
return ( return (
@@ -368,28 +445,32 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
color="gray" color="gray"
size="sm" size="sm"
onClick={() => { onClick={() => {
scrollToIndex(0) goToVisibleIndex(0)
currentKeys.length && setActiveImage(currentKeys[0])
setInputNumber("1")
setActiveImageId(1)
closeRightContext()
}}> }}>
<IconChevronsLeft size={16} /> <IconChevronsLeft size={16} />
</ActionIcon> </ActionIcon>
<ActionIcon
variant={playDirection === "backward" ? "filled" : "subtle"}
color={playDirection === "backward" ? "blue" : "gray"}
size="sm"
disabled={currentKeys.length < 2}
aria-label="向前播放"
title="向前播放"
onClick={() => {
togglePlayback("backward")
}}>
{playDirection === "backward" ? (
<IconPlayerTrackPrevFilled size={16} />
) : (
<IconPlayerTrackPrev size={16} />
)}
</ActionIcon>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
color="gray" color="gray"
size="sm" size="sm"
onClick={() => { onClick={() => {
if (inputNumber !== "1") { stepVisibleImage("backward")
let newActiveIndex = +inputNumber - 1
setInputNumber(newActiveIndex.toString())
setActiveImageId(newActiveIndex)
scrollToIndex(newActiveIndex - 1)
setActiveImage(currentKeys?.[newActiveIndex - 1])
closeRightContext()
}
}}> }}>
<IconChevronLeft size={16} /> <IconChevronLeft size={16} />
</ActionIcon> </ActionIcon>
@@ -410,10 +491,21 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
scrollToIndex(+inputNumber - 1) const targetIndex = Number(inputNumber)
setActiveImage(currentKeys?.[+inputNumber - 1]) if (
setActiveImageId(+inputNumber) Number.isInteger(targetIndex) &&
closeRightContext() 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<BottomToolsComponentProps> = (props) => {
color="gray" color="gray"
size="sm" size="sm"
onClick={() => { onClick={() => {
if (+inputNumber < currentKeys.length) { stepVisibleImage("forward")
let newActiveIndex = +inputNumber + 1
setInputNumber(newActiveIndex.toString())
setActiveImageId(newActiveIndex)
scrollToIndex(newActiveIndex - 1)
setActiveImage(currentKeys?.[newActiveIndex - 1])
closeRightContext()
}
}}> }}>
<IconChevronRight size={16} /> <IconChevronRight size={16} />
</ActionIcon> </ActionIcon>
<ActionIcon
variant={playDirection === "forward" ? "filled" : "subtle"}
color={playDirection === "forward" ? "blue" : "gray"}
size="sm"
disabled={currentKeys.length < 2}
aria-label="向后播放"
title="向后播放"
onClick={() => {
togglePlayback("forward")
}}>
{playDirection === "forward" ? (
<IconPlayerTrackNextFilled size={16} />
) : (
<IconPlayerTrackNext size={16} />
)}
</ActionIcon>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
color="gray" color="gray"
size="sm" size="sm"
onClick={() => { onClick={() => {
scrollToIndex(currentKeys.length - 1) goToVisibleIndex(currentKeys.length - 1)
setInputNumber(currentKeys.length.toString())
setActiveImageId(currentKeys.length)
setActiveImage(currentKeys?.[currentKeys.length - 1])
closeRightContext()
}}> }}>
<IconChevronsRight size={16} /> <IconChevronsRight size={16} />
</ActionIcon> </ActionIcon>
@@ -657,9 +754,7 @@ const BottomTools: React.FC<BottomToolsComponentProps> = (props) => {
if (ctrlKey || shiftKey) { if (ctrlKey || shiftKey) {
setSelectedItems(key, ctrlKey, shiftKey) setSelectedItems(key, ctrlKey, shiftKey)
} else { } else {
setInputNumber(index + 1 + "") goToVisibleIndex(index)
setActiveImage(key)
setActiveImageId(index + 1)
setSelectedItems("", false, false) setSelectedItems("", false, false)
} }
closeRightContext() closeRightContext()

View File

@@ -26,6 +26,7 @@ import React, {
useCallback, useCallback,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -94,6 +95,13 @@ const renderRaster = (paperGroup: paper.Group, option: any) => {
} }
type NormalizedPoint = [number, number] type NormalizedPoint = [number, number]
type SupportBox = [number, number, number, number]
type ContextMenuLayout = {
top: number
left: number
maxHeight: number
maxWidth: number
}
const normalizePoint = (value: any): NormalizedPoint | null => { const normalizePoint = (value: any): NormalizedPoint | null => {
if (value instanceof paper.Point) return [value.x, value.y] if (value instanceof paper.Point) return [value.x, value.y]
@@ -133,10 +141,19 @@ const PaperContainer = (
) => { ) => {
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
const containerRef = useRef<any>(null) const containerRef = useRef<any>(null)
const contextMenuRef = useRef<HTMLDivElement>(null)
const [imgSize, setImageSize] = useState<{ const [imgSize, setImageSize] = useState<{
clientWidth: number clientWidth: number
clientHeight: number clientHeight: number
}>() }>()
const [contextMenuLayout, setContextMenuLayout] = useState<ContextMenuLayout>(
{
top: 0,
left: 0,
maxHeight: 0,
maxWidth: 0,
}
)
const [firstRender, setFirstRender] = useState(false) const [firstRender, setFirstRender] = useState(false)
const [confirmModalOpened, setConfirmModalOpened] = useState(false) const [confirmModalOpened, setConfirmModalOpened] = useState(false)
@@ -431,7 +448,8 @@ const PaperContainer = (
async ( async (
points: Array<[number, number]>, points: Array<[number, number]>,
tags: number[], tags: number[],
clear_previous_state = false clear_previous_state = false,
boxes: SupportBox[] = []
) => { ) => {
void clear_previous_state void clear_previous_state
try { try {
@@ -459,6 +477,9 @@ const PaperContainer = (
usePaperStore.getState().rasterScale[activeImage] ?? 1 usePaperStore.getState().rasterScale[activeImage] ?? 1
const draw = () => { const draw = () => {
const paperGroup = usePaperStore.getState().group!
const raster = paperGroup.getItems({ data: { name: "pic" } })[0]
points.forEach((point, index) => { points.forEach((point, index) => {
const drawPoint = new paper.Path.Circle({ const drawPoint = new paper.Path.Circle({
center: point, center: point,
@@ -470,16 +491,30 @@ const PaperContainer = (
type: "point", type: "point",
}, },
}) })
drawPoint.parent = usePaperStore.getState().group! drawPoint.parent = paperGroup
const raster = usePaperStore
.getState()
.group!.getItems({ data: { name: "pic" } })[0]
if (!raster.contains(drawPoint.position)) { if (!raster.contains(drawPoint.position)) {
throw new Error("点位超出图片范围") 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() clearSupportAnnotationItem()
draw() draw()
@@ -496,8 +531,12 @@ const PaperContainer = (
const promptPoints = foregroundPoints.length const promptPoints = foregroundPoints.length
? foregroundPoints ? foregroundPoints
: normalizedPoints : normalizedPoints
// const promptBox = const normalizedBoxes = boxes.map(([left, top, right, bottom]) => [
// promptPoints.length > 1 ? getContourBox(promptPoints) : null 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 = const projectId =
projectDetail?.name || projectDetail?.name ||
projectDetail?.rpath || projectDetail?.rpath ||
@@ -506,8 +545,8 @@ const PaperContainer = (
if (!projectId) { if (!projectId) {
throw new Error("缺少 project_id无法调用模型接口") throw new Error("缺少 project_id无法调用模型接口")
} }
if (!promptPoints.length) { if (!promptPoints.length && !normalizedBoxes.length) {
throw new Error("缺少有效点位,无法调用模型接口") throw new Error("缺少有效提示,无法调用模型接口")
} }
// const picSize = usePaperStore.getState().rasterSize[activeImage] // const picSize = usePaperStore.getState().rasterSize[activeImage]
@@ -517,9 +556,13 @@ const PaperContainer = (
image: activeImage, image: activeImage,
// project_id: "image_test_bk", // project_id: "image_test_bk",
prompt: { prompt: {
foreground_points: promptPoints, ...(promptPoints.length
background_points: background_points, ? { foreground_points: promptPoints }
// ...(promptBox ? { boxes: [promptBox] } : {}), : {}),
...(background_points.length
? { background_points: background_points }
: {}),
...(normalizedBoxes.length ? { boxes: normalizedBoxes } : {}),
}, },
}) })
const { contours = [], hollows = [] } = data || {} const { contours = [], hollows = [] } = data || {}
@@ -1918,6 +1961,61 @@ const PaperContainer = (
?.find((item) => item[0] === id) ?.find((item) => item[0] === id)
}, [activeImage, id, operationId, storeLabel]) }, [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数据 // 打开弹窗时初始化form数据
useEffect(() => { useEffect(() => {
if (showContextMenu) { if (showContextMenu) {
@@ -2264,6 +2362,7 @@ const PaperContainer = (
return ( return (
<Card <Card
ref={contextMenuRef}
shadow="sm" shadow="sm"
padding="xs" padding="xs"
radius="md" radius="md"
@@ -2272,10 +2371,20 @@ const PaperContainer = (
position: "absolute", position: "absolute",
zIndex: 1000, zIndex: 1000,
width: 224, width: 224,
maxWidth: contextMenuLayout.maxWidth
? `${contextMenuLayout.maxWidth}px`
: undefined,
maxHeight: contextMenuLayout.maxHeight
? `${contextMenuLayout.maxHeight}px`
: undefined,
height: "fit-content", height: "fit-content",
display: !showContextMenu ? "none" : "block", display: !showContextMenu ? "none" : "block",
top: `${position.top}px`, top: `${contextMenuLayout.top}px`,
left: `${position.left}px`, left: `${contextMenuLayout.left}px`,
overflowY: "auto",
overflowX: "hidden",
overscrollBehavior: "contain",
wordBreak: "break-word",
}}> }}>
<Box p="xs"> <Box p="xs">
<Stack gap="xs"> <Stack gap="xs">

View File

@@ -628,16 +628,48 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
<Stack gap="xs" pb="md"> <Stack gap="xs" pb="md">
{labelState.get(activeImage) && {labelState.get(activeImage) &&
labelState.get(activeImage)?.map((operationId) => { labelState.get(activeImage)?.map((operationId) => {
const operationSchema = getOperationSchema(operationId)
const isCollapsed = const isCollapsed =
operationStatus[activeImage + operationId]?.["COLLAPSE"] operationStatus[activeImage + operationId]?.["COLLAPSE"]
const isHide = const isHide =
operationStatus[activeImage + operationId]?.HIDE operationStatus[activeImage + operationId]?.HIDE
const hasSubAttrError = getOperationSubAttrStatus(operationId) 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 ( return (
<Box key={operationId} w="100%"> <Box key={operationId} w="100%">
<Paper withBorder p="xs" radius="md" shadow="sm"> <Paper withBorder p="xs" radius="md" shadow="sm">
<Flex justify="space-between" align="center"> <Flex align="center" gap="xs" style={{ minWidth: 0 }}>
<Group gap="xs"> <Flex
align="center"
gap="xs"
style={{ flex: 1, minWidth: 0 }}>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
size="sm" size="sm"
@@ -650,23 +682,62 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
<EyeOff size={16} /> <EyeOff size={16} />
)} )}
</ActionIcon> </ActionIcon>
{renderOperationIcon( <Group
getOperationSchema(operationId) gap={8}
)} wrap="nowrap"
<Text style={{ flex: 1, minWidth: 0 }}>
size="sm" <Group
c={hasSubAttrError ? "red" : undefined}> gap={6}
{renderShortcutKey(operationId)} wrap="nowrap"
</Text> px="xs"
</Group> py={4}
<Text style={{
size="sm" flexShrink: 0,
c={hasSubAttrError ? "red" : undefined}> borderRadius: 999,
{getOperationSchema(operationId)?.label_class} border: `1px solid ${
</Text> hasSubAttrError
<Group gap="xs"> ? "var(--mantine-color-red-2)"
: "var(--mantine-color-gray-2)"
}`,
background: hasSubAttrError
? "var(--mantine-color-red-0)"
: "var(--mantine-color-gray-0)",
}}>
{renderOperationIcon(operationSchema)}
<Text
size="xs"
fw={700}
c={hasSubAttrError ? "red" : "dimmed"}
style={{
whiteSpace: "nowrap",
lineHeight: 1,
}}>
{renderShortcutKey(operationId)}
</Text>
</Group>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text
size="sm"
fw={500}
c={hasSubAttrError ? "red" : undefined}
title={operationLabel || undefined}
style={{
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{operationLabel}
</Text>
</Box>
</Group>
</Flex>
<Group
gap={6}
wrap="nowrap"
style={{ flexShrink: 0, marginLeft: "auto" }}>
<TextInput <TextInput
w={96} w={88}
size="xs" size="xs"
placeholder="搜索子属性" placeholder="搜索子属性"
value={searchSubAttrs[operationId] || ""} value={searchSubAttrs[operationId] || ""}
@@ -680,10 +751,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
handleSelectedBySearchSubAttr(e, operationId) handleSelectedBySearchSubAttr(e, operationId)
} }
/> />
<Text size="sm"> <Badge
{storeLabel.get(activeImage)?.get(operationId) size="sm"
?.length || 0} variant="light"
</Text> radius="xl"
color={hasSubAttrError ? "red" : "gray"}>
{objectCount}
</Badge>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
size="sm" size="sm"
@@ -704,58 +778,93 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
</Flex> </Flex>
</Paper> </Paper>
<Collapse in={!isCollapsed}> <Collapse in={!isCollapsed}>
<Stack gap="xs" mt="xs" pl="xs"> <Stack gap="xs" mt="xs" ml="xs">
{storeLabel {visibleChildren.map((child, childIndex) => {
.get(activeImage) const { comment } = child[2]
?.get(operationId) const isSelected = selectedPath[
?.map((child) => { activeImage
const { comment } = child[2] ]?.includes(child[0])
let visibleFlag = true const isChildHide =
if (comment && comment.length) { pathStatus[activeImage + child[0]]?.["HIDE"]
const latest_comment = const hasChildError = getSubAttrStatus(
(taskDetail && getOperationSchema(operationId),
taskDetail.label_status === 2) || child[2]?.sub_attributes
comment.length === 1 )
? comment[comment.length - 1] const isLastVisibleChild =
: comment[comment.length - 2] childIndex === visibleChildren.length - 1
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
const isSelected = selectedPath[ let textColor = undefined
activeImage let connectorColor = "var(--mantine-color-gray-4)"
]?.includes(child[0])
const isChildHide =
pathStatus[activeImage + child[0]]?.["HIDE"]
const hasChildError = getSubAttrStatus(
getOperationSchema(operationId),
child[2]?.sub_attributes
)
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) { return (
textColor = "red" <Box
} else if (isSelected) { key={child[0]}
textColor = "white" pl="lg"
} style={{ position: "relative" }}>
<Box
if (!visibleFlag) return null style={{
position: "absolute",
return ( left: 7,
top: 0,
bottom: isLastVisibleChild ? "75%" : 0,
width: 1.5,
borderRadius: 999,
background: connectorColor,
opacity: isSelected ? 0.9 : 0.35,
}}
/>
{isLastVisibleChild ? (
<Box
style={{
position: "absolute",
left: 7,
top: "calc(50% - 10px)",
width: 14,
height: 10,
borderLeft: `1.5px solid ${connectorColor}`,
borderBottom: `1.5px solid ${connectorColor}`,
borderBottomLeftRadius: 10,
opacity: isSelected ? 0.9 : 0.55,
}}
/>
) : (
<>
<Box
style={{
position: "absolute",
left: 7,
top: "50%",
width: 14,
borderTop: `1.5px solid ${connectorColor}`,
opacity: isSelected ? 0.9 : 0.45,
}}
/>
<Box
style={{
position: "absolute",
left: 4,
top: "calc(50% - 3px)",
width: 6,
height: 6,
borderRadius: 999,
background: connectorColor,
boxShadow:
"0 0 0 2px var(--mantine-color-body)",
opacity: isSelected ? 1 : 0.85,
}}
/>
</>
)}
<Paper <Paper
key={child[0]}
withBorder withBorder
p="xs" p="xs"
radius="md" radius="md"
@@ -763,7 +872,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
bg={ bg={
isSelected isSelected
? "var(--mantine-primary-color-filled)" ? "var(--mantine-primary-color-filled)"
: undefined : "var(--mantine-color-gray-0)"
} }
c={isSelected ? "white" : undefined} c={isSelected ? "white" : undefined}
onClick={() => onClick={() =>
@@ -944,8 +1053,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
) : null} ) : null}
</Flex> </Flex>
</Paper> </Paper>
) </Box>
})} )
})}
</Stack> </Stack>
</Collapse> </Collapse>
</Box> </Box>

View File

@@ -139,7 +139,8 @@ interface PaperState {
renderPath: ( renderPath: (
points: Array<[number, number]>, points: Array<[number, number]>,
tags: number[], tags: number[],
flag?: boolean flag?: boolean,
boxes?: Array<[number, number, number, number]>
) => void ) => void
) => void ) => void
getClickedCircle: ( getClickedCircle: (
@@ -290,6 +291,49 @@ const handleMouseMove = (e: paper.MouseEvent) => {
usePositionStore.getState().setCurrentMousePosition([e.point.x, e.point.y]) 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() const { user_id } = usePermissionStore.getState()
type OrderPoint = [number, number] type OrderPoint = [number, number]
@@ -427,45 +471,23 @@ export const clearSupportAnnotationItem = () => {
} }
// 根据点击位置判断弹窗弹出的top、left // 根据点击位置判断弹窗弹出的top、left
const containerWidth = 244
const containerHeight = 240
export const getShowContextMenuPosition = ( export const getShowContextMenuPosition = (
clickPoint: paper.Point, clickPoint: paper.Point,
category: Project.LabelSchemaList, _category: Project.LabelSchemaList,
offsetWidth = 0, offsetWidth = 0,
offsetHeight = 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 element = document.getElementById("paper-container")
const rasterPos = [element!.clientWidth, element!.clientHeight] const containerWidth = element?.clientWidth ?? 0
console.log(rasterPos, finalHeight) const containerHeight = element?.clientHeight ?? 0
let left = const left = Math.min(
clickPoint.x + containerWidth > rasterPos[0] Math.max(clickPoint.x + offsetWidth, 0),
? clickPoint.x + offsetWidth - containerWidth containerWidth || clickPoint.x + offsetWidth
: clickPoint.x + offsetWidth )
let top = const top = Math.min(
clickPoint.y + finalHeight > rasterPos[1] Math.max(clickPoint.y + offsetHeight, 0),
? clickPoint.y + offsetHeight - finalHeight < 0 containerHeight || clickPoint.y + offsetHeight
? 0 )
: clickPoint.y + offsetHeight - finalHeight
: clickPoint.y + offsetHeight
console.log("转换之后的x、y", left, top)
return { return {
top, top,
@@ -722,25 +744,13 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (startMiddleMousePan(e.event)) return
if (pressCtrl) return if (pressCtrl) return
const currentTime = Date.now() const currentTime = Date.now()
const timeDelta = currentTime - lastTime const timeDelta = currentTime - lastTime
if (e.event && e.event.button === 0) { if (e.event && e.event.button === 0) {
useOtherToolsStore.getState().setShowContextMenu({ hidePaperContextMenu()
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
} }
if (useTopToolsStore.getState().isView) { if (useTopToolsStore.getState().isView) {
panMode = "" panMode = ""
@@ -1433,12 +1443,7 @@ export const usePaperStore = create<PaperState>((set) => ({
downPoint: paper.Point downPoint: paper.Point
delta: paper.Point delta: paper.Point
}) => { }) => {
// 优先判断移动图片 if (dragMiddleMousePan(e)) return
if (panMode === "item") {
usePaperStore.getState().group!.position = usePaperStore
.getState()
.group?.position.add(e.delta)!
}
if (useTopToolsStore.getState().isView) return if (useTopToolsStore.getState().isView) return
@@ -1677,6 +1682,7 @@ export const usePaperStore = create<PaperState>((set) => ({
// 改变对象不能用 activeImage activeOperation (因为图形只在对应图片渲染所以activeImage是一样的) // 改变对象不能用 activeImage activeOperation (因为图形只在对应图片渲染所以activeImage是一样的)
panTool.onMouseUp = (_e: paper.ToolEvent) => { panTool.onMouseUp = (_e: paper.ToolEvent) => {
if (stopMiddleMousePan()) return
if (useTopToolsStore.getState().isView) return if (useTopToolsStore.getState().isView) return
let newDrawPath: paper.Path | paper.CompoundPath | null = null let newDrawPath: paper.Path | paper.CompoundPath | null = null
@@ -2569,6 +2575,12 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
polygon.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { polygon.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
if (polygon.data.id) { if (polygon.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
@@ -2615,6 +2627,12 @@ export const usePaperStore = create<PaperState>((set) => ({
point: paper.Point point: paper.Point
}) => { }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
if (compoundPath.data.id) { if (compoundPath.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
@@ -2654,11 +2672,12 @@ export const usePaperStore = create<PaperState>((set) => ({
let polygon: paper.Path | paper.CompoundPath | null let polygon: paper.Path | paper.CompoundPath | null
let compoundPolygon: paper.Path | paper.CompoundPath | null let compoundPolygon: paper.Path | paper.CompoundPath | null
let points: paper.Point[] = [] let points: paper.Point[] = []
let lastPoint: paper.Point let lastPoint: paper.Point | null = null
let lastTime: number let lastTime = 0
let isMouseDown: boolean = false let isMouseDown: boolean = false
let polygonTool = paperPolygonTool let polygonTool = paperPolygonTool
let currentMagnetPoint: paper.Point | null = null let currentMagnetPoint: paper.Point | null = null
let draftId: number | null = null
const handleCircles = (path: paper.Path | paper.CompoundPath | null) => { const handleCircles = (path: paper.Path | paper.CompoundPath | null) => {
// 更新选中的点 // 更新选中的点
@@ -2683,6 +2702,71 @@ export const usePaperStore = create<PaperState>((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) => { const handlePolygonIntersectRaster = (savePath: paper.Path) => {
let oldPolygon: any = savePath let oldPolygon: any = savePath
polygon = oldPolygon.intersect( polygon = oldPolygon.intersect(
@@ -2775,6 +2859,13 @@ export const usePaperStore = create<PaperState>((set) => ({
event: MouseEvent event: MouseEvent
point: paper.Point 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 if (e.event.button !== 0) return
isMouseDown = true isMouseDown = true
@@ -2797,6 +2888,7 @@ export const usePaperStore = create<PaperState>((set) => ({
selectedCircles.forEach((item) => { selectedCircles.forEach((item) => {
item.remove() item.remove()
}) })
selectedCircles = []
} }
// 调整polygon点位初始顺序 // 调整polygon点位初始顺序
@@ -2994,15 +3086,12 @@ export const usePaperStore = create<PaperState>((set) => ({
selectedCircles.forEach((item) => { selectedCircles.forEach((item) => {
item.remove() item.remove()
}) })
const magnetPaths = usePaperStore selectedCircles = []
.getState() clearPolygonMagnet()
.group!.getItems({ data: { type: "magnet" } })
magnetPaths.forEach((item) => {
item.remove()
})
currentMagnetPoint = null
useTopToolsStore.getState().setPressA(false) useTopToolsStore.getState().setPressA(false)
draftId = null
lastPoint = null
lastTime = 0
return return
} }
} else { } else {
@@ -3030,14 +3119,15 @@ export const usePaperStore = create<PaperState>((set) => ({
let point = currentMagnetPoint let point = currentMagnetPoint
? currentMagnetPoint ? currentMagnetPoint
: temp.globalToLocal(e.point) : temp.globalToLocal(e.point)
const draftData = getDraftPolygonData()
let circle = usePaperStore let circle = usePaperStore
.getState() .getState()
.getClickedCircle( .getClickedCircle(
point, point,
selectedCircles.length, selectedCircles.length,
polygon?.data.id, draftData.id,
polygon?.data.blankColor usePaperStore.getState().toolOption.blankColor
) )
// add circle // add circle
selectedCircles.push(circle) selectedCircles.push(circle)
@@ -3054,20 +3144,20 @@ export const usePaperStore = create<PaperState>((set) => ({
// option // option
...usePaperStore.getState().toolOption, ...usePaperStore.getState().toolOption,
}, },
{ draftData
imageId: useBottomToolsStore.getState().activeImage,
operationId: useTopToolsStore.getState().activeOperation,
id:
useRightToolsStore
.getState()
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
}
) )
lastPoint = checkPoint lastPoint = checkPoint
lastTime = Date.now() lastTime = Date.now()
} }
polygonTool.onMouseDrag = (e: {
event: MouseEvent
delta: paper.Point
}) => {
if (dragMiddleMousePan(e)) return
}
polygonTool.onMouseUp = (_e: paper.ToolEvent) => { polygonTool.onMouseUp = (_e: paper.ToolEvent) => {
if (stopMiddleMousePan()) return
isMouseDown = false isMouseDown = false
} }
polygonTool.onMouseMove = (e: paper.MouseEvent) => { polygonTool.onMouseMove = (e: paper.MouseEvent) => {
@@ -3126,7 +3216,7 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
} }
}) })
if (isMouseDown) { if (isMouseDown && lastPoint) {
const delta = currentMagnetPoint const delta = currentMagnetPoint
? (currentMagnetPoint as paper.Point).subtract(lastPoint) ? (currentMagnetPoint as paper.Point).subtract(lastPoint)
: e.point.subtract(lastPoint) : e.point.subtract(lastPoint)
@@ -3174,27 +3264,17 @@ export const usePaperStore = create<PaperState>((set) => ({
// option // option
...usePaperStore.getState().toolOption, ...usePaperStore.getState().toolOption,
}, },
{} getDraftPolygonData()
) )
} }
} }
} }
polygonTool.onKeyDown = (e: paper.KeyEvent) => { polygonTool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape" || e.key === "alt") { if (e.key === "escape" || e.key === "alt") {
if (polygon) { if (polygon || points.length) {
polygon.remove() resetPolygonDraft()
polygon = null usePaperSupportStore.getState().handleBufferPaths(null)
points = []
usePaperSupportStore.getState().handleBufferPaths(polygon)
handleCircles(polygon)
} }
const magnetPaths = usePaperStore
.getState()
.group!.getItems({ data: { type: "magnet" } })
magnetPaths.forEach((item) => {
item.remove()
})
currentMagnetPoint = null
} else if (e.key === "delete") { } else if (e.key === "delete") {
handleDelete() handleDelete()
forceUpdate() forceUpdate()
@@ -3242,6 +3322,12 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
rect.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { rect.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
if (rect.data.id) { if (rect.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
@@ -3278,6 +3364,7 @@ export const usePaperStore = create<PaperState>((set) => ({
let rectangleTool = paperRectangleTool let rectangleTool = paperRectangleTool
rectangleTool.onMouseDown = (e: { event: MouseEvent }) => { rectangleTool.onMouseDown = (e: { event: MouseEvent }) => {
if (startMiddleMousePan(e.event)) return
if (e.event && e.event.button !== 2) if (e.event && e.event.button !== 2)
useOtherToolsStore.getState().setShowContextMenu({ useOtherToolsStore.getState().setShowContextMenu({
showContextMenu: false, showContextMenu: false,
@@ -3300,7 +3387,9 @@ export const usePaperStore = create<PaperState>((set) => ({
event: MouseEvent event: MouseEvent
point: paper.Point point: paper.Point
downPoint: paper.Point downPoint: paper.Point
delta: paper.Point
}) => { }) => {
if (dragMiddleMousePan(e)) return
const delta = e.point.subtract(e.downPoint) const delta = e.point.subtract(e.downPoint)
// 阻止右键移动矩形(e.event.button === 0) // 阻止右键移动矩形(e.event.button === 0)
if (delta.length > 10 && e.event.button === 0) { if (delta.length > 10 && e.event.button === 0) {
@@ -3337,6 +3426,7 @@ export const usePaperStore = create<PaperState>((set) => ({
event: MouseEvent event: MouseEvent
point: paper.Point point: paper.Point
}) => { }) => {
if (stopMiddleMousePan()) return
// add segment judgement // add segment judgement
if (rect && rect.segments?.length && rect?.data?.id) { if (rect && rect.segments?.length && rect?.data?.id) {
// 去除超出图片边界的部分 // 去除超出图片边界的部分
@@ -3452,6 +3542,12 @@ export const usePaperStore = create<PaperState>((set) => ({
// }); // });
brush.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { brush.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
// console.log(e.event.clientX, e.event.clientY); // console.log(e.event.clientX, e.event.clientY);
if (brush.data.id) { if (brush.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
@@ -3489,10 +3585,66 @@ export const usePaperStore = create<PaperState>((set) => ({
let brushCircles: paper.Path.Circle[] = [] let brushCircles: paper.Path.Circle[] = []
let myPath: paper.Path | null let myPath: paper.Path | null
let compoundPolygon: paper.Path | paper.CompoundPath | null let compoundPolygon: paper.Path | paper.CompoundPath | null
let lastPoint: paper.Point let lastPoint: paper.Point | null = null
let points: paper.Point[] = [] let points: paper.Point[] = []
let brushTool = paperBrushTool 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) => { const handleBrushIntersectRaster = (savePath: paper.Path) => {
let intersections = savePath.getIntersections( let intersections = savePath.getIntersections(
@@ -3558,6 +3710,13 @@ export const usePaperStore = create<PaperState>((set) => ({
} }
brushTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { 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 if (e.event.button !== 0) return
// 绘制时去除选中辅助 // 绘制时去除选中辅助
@@ -3753,8 +3912,11 @@ export const usePaperStore = create<PaperState>((set) => ({
brushCircles.forEach((item) => { brushCircles.forEach((item) => {
item.remove() item.remove()
}) })
brushCircles = []
useTopToolsStore.getState().setPressA(false) useTopToolsStore.getState().setPressA(false)
draftId = null
lastPoint = null
return return
} }
} else { } else {
@@ -3772,6 +3934,7 @@ export const usePaperStore = create<PaperState>((set) => ({
let temp = renderPath(usePaperStore.getState().group!, {}) let temp = renderPath(usePaperStore.getState().group!, {})
let point = temp.globalToLocal(e.point) let point = temp.globalToLocal(e.point)
const draftData = getDraftBrushData()
// add point // add point
points.push(point) points.push(point)
@@ -3785,22 +3948,15 @@ export const usePaperStore = create<PaperState>((set) => ({
// option // option
...usePaperStore.getState().toolOption, ...usePaperStore.getState().toolOption,
}, },
{ draftData
imageId: useBottomToolsStore.getState().activeImage,
operationId: useTopToolsStore.getState().activeOperation,
id:
useRightToolsStore
.getState()
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
}
) )
let circle = usePaperStore let circle = usePaperStore
.getState() .getState()
.getClickedCircle( .getClickedCircle(
point, point,
brushCircles.length, brushCircles.length,
myPath?.data.id, draftData.id,
myPath?.data.blankColor usePaperStore.getState().toolOption.blankColor
) )
brushCircles.push(circle) brushCircles.push(circle)
@@ -3808,6 +3964,10 @@ export const usePaperStore = create<PaperState>((set) => ({
temp.remove() temp.remove()
} }
brushTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => {
if (dragMiddleMousePan(e)) return
}
brushTool.onMouseMove = (e: paper.MouseEvent) => { brushTool.onMouseMove = (e: paper.MouseEvent) => {
handleMouseMove(e) handleMouseMove(e)
@@ -3824,20 +3984,19 @@ export const usePaperStore = create<PaperState>((set) => ({
fillColor: undefined, fillColor: undefined,
closed: false, closed: false,
}, },
{} getDraftBrushData()
) )
} }
} }
brushTool.onMouseUp = (_e: paper.ToolEvent) => {
stopMiddleMousePan()
}
brushTool.onKeyDown = (e: paper.KeyEvent) => { brushTool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape" || e.key === "alt") { if (e.key === "escape" || e.key === "alt") {
if (myPath) { if (myPath || points.length) {
brushCircles.forEach((item) => { resetBrushDraft()
item.remove()
})
myPath.remove()
myPath = null
points = []
} }
} else if (e.key === "delete") { } else if (e.key === "delete") {
handleDelete() handleDelete()
@@ -3874,6 +4033,12 @@ export const usePaperStore = create<PaperState>((set) => ({
myCircle.data = Object.assign({ type: "circle", ...option }, data) myCircle.data = Object.assign({ type: "circle", ...option }, data)
myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
// console.log(e.event.clientX, e.event.clientY); // console.log(e.event.clientX, e.event.clientY);
if (myCircle.data.id) { if (myCircle.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
@@ -3947,12 +4112,82 @@ export const usePaperStore = create<PaperState>((set) => ({
let myCircleText: paper.PointText[] = [] let myCircleText: paper.PointText[] = []
let textNumbers: number[] = [] let textNumbers: number[] = []
let myPath: paper.Path | null let myPath: paper.Path | null
let lastPoint: paper.Point let lastPoint: paper.Point | null = null
let points: paper.Point[] = [] let points: paper.Point[] = []
let pointTool = paperPointTool 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 }) => { 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 if (e.event.button !== 0) return
// 绘制时去除选中辅助 // 绘制时去除选中辅助
@@ -4079,6 +4314,8 @@ export const usePaperStore = create<PaperState>((set) => ({
myCircleText = [] myCircleText = []
// 避免后续按键删除已绘制的circle // 避免后续按键删除已绘制的circle
myCircle = [] myCircle = []
draftId = null
lastPoint = null
return return
} }
@@ -4098,6 +4335,7 @@ export const usePaperStore = create<PaperState>((set) => ({
let temp = renderCircle(usePaperStore.getState().group!, {}) let temp = renderCircle(usePaperStore.getState().group!, {})
let point = temp.globalToLocal(e.point) let point = temp.globalToLocal(e.point)
let text = textNumbers.reduce((a, b) => Math.max(a, b), 0) + 1 let text = textNumbers.reduce((a, b) => Math.max(a, b), 0) + 1
const draftData = getDraftPointData()
let circle = usePaperStore.getState().addPoint( let circle = usePaperStore.getState().addPoint(
renderCircle, renderCircle,
@@ -4110,13 +4348,8 @@ export const usePaperStore = create<PaperState>((set) => ({
strokeScaling: false, strokeScaling: false,
}, },
{ {
imageId: useBottomToolsStore.getState().activeImage, ...draftData,
operationId: useTopToolsStore.getState().activeOperation,
text: text, text: text,
id:
useRightToolsStore
.getState()
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
} }
) )
// add circle // add circle
@@ -4147,14 +4380,7 @@ export const usePaperStore = create<PaperState>((set) => ({
// option // option
...usePaperStore.getState().toolOption, ...usePaperStore.getState().toolOption,
}, },
{ draftData
imageId: useBottomToolsStore.getState().activeImage,
operationId: useTopToolsStore.getState().activeOperation,
id:
useRightToolsStore
.getState()
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1,
}
) )
lastPoint = e.point lastPoint = e.point
@@ -4162,6 +4388,9 @@ export const usePaperStore = create<PaperState>((set) => ({
// group.current?.addChild(myCircle); // group.current?.addChild(myCircle);
temp.remove() temp.remove()
} }
pointTool.onMouseDrag = (e: { event: MouseEvent; delta: paper.Point }) => {
if (dragMiddleMousePan(e)) return
}
pointTool.onMouseMove = (e: paper.MouseEvent) => { pointTool.onMouseMove = (e: paper.MouseEvent) => {
handleMouseMove(e) handleMouseMove(e)
@@ -4178,25 +4407,17 @@ export const usePaperStore = create<PaperState>((set) => ({
fillColor: undefined, fillColor: undefined,
closed: false, closed: false,
}, },
{} getDraftPointData()
) )
} }
} }
pointTool.onMouseUp = (_e: paper.ToolEvent) => {
stopMiddleMousePan()
}
pointTool.onKeyDown = (e: paper.KeyEvent) => { pointTool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape" || e.key === "alt") { if (e.key === "escape" || e.key === "alt") {
if (myPath) { if (myPath || points.length) {
myPath.remove() resetPointDraft()
myPath = null
points = []
textNumbers = []
myCircleText.forEach((item) => {
item.remove()
})
myCircleText = []
myCircle.forEach((item) => {
item.remove()
})
myCircle = []
} }
} else if (e.key === "delete") { } else if (e.key === "delete") {
handleDelete() handleDelete()
@@ -4250,6 +4471,12 @@ export const usePaperStore = create<PaperState>((set) => ({
circle.scale(newScale / currentScaling) circle.scale(newScale / currentScaling)
circle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { circle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
if (
["polygon", "brush", "point"].includes(
usePaperStore.getState().mode || ""
)
)
return
// console.log(e.event.clientX, e.event.clientY); // console.log(e.event.clientX, e.event.clientY);
if (circle.data.id) { if (circle.data.id) {
let category = useTopToolsStore let category = useTopToolsStore
@@ -4292,28 +4519,125 @@ export const usePaperStore = create<PaperState>((set) => ({
initSupportTool: (tool, renderPath) => { initSupportTool: (tool, renderPath) => {
let points: Array<[number, number]> = [] let points: Array<[number, number]> = []
let tags: 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 }) => { tool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
let point = usePaperStore.getState().group!.globalToLocal(e.point) if (startMiddleMousePan(e.event)) return
if (e.event.button === 1) return dragStartPoint = usePaperStore.getState().group!.globalToLocal(e.point)
points.push([point.x, point.y]) dragButton = e.event.button
tags.push(e.event.button === 0 ? 1 : 0) clearSupportBoxPreview()
renderPath(points, tags, points.length === 1) }
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) => {
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) => { tool.onKeyDown = (e: paper.KeyEvent) => {
if (e.key === "escape") { if (e.key === "escape") {
points = [] points = []
tags = [] tags = []
boxes = []
dragStartPoint = null
dragButton = null
clearSupportBoxPreview()
clearSupportAnnotationItem() clearSupportAnnotationItem()
usePaperStore.getState().setPaperMode("pan") usePaperStore.getState().setPaperMode("pan")
} }
if (e.key === "f1") { if (e.key === "f1") {
points = [] points = []
tags = [] tags = []
boxes = []
dragStartPoint = null
dragButton = null
clearSupportBoxPreview()
} }
} }
tool.onMouseMove = (e: paper.MouseEvent) => {
handleMouseMove(e)
}
set((state: PaperState) => ({ set((state: PaperState) => ({
...state, ...state,
supportTool: tool, supportTool: tool,
@@ -4416,6 +4740,7 @@ export const usePaperStore = create<PaperState>((set) => ({
return bufferPath return bufferPath
}, },
setPaperMode: (modeParam) => { setPaperMode: (modeParam) => {
stopMiddleMousePan()
set((state: PaperState) => ({ set((state: PaperState) => ({
...state, ...state,
mode: modeParam, mode: modeParam,