Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!12
This commit is contained in:
@@ -3,15 +3,7 @@
|
||||
import { taskDispatch } from "@/components/label/api/task"
|
||||
import { Task } from "@/components/label/api/task/typing"
|
||||
import { usePermissionStore } from "@/components/label/store/auth"
|
||||
import {
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
} from "@mantine/core"
|
||||
import { Badge, Button, Group, Modal, Select, Stack, Text } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
import { useEffect } from "react"
|
||||
@@ -30,7 +22,6 @@ export default function DispatchModal(props: {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
reject: false,
|
||||
task_status_dst: "",
|
||||
uid_dst: "",
|
||||
},
|
||||
@@ -41,7 +32,6 @@ export default function DispatchModal(props: {
|
||||
|
||||
const resetFormState = () => {
|
||||
form.setValues({
|
||||
reject: false,
|
||||
task_status_dst: "",
|
||||
uid_dst: "",
|
||||
})
|
||||
@@ -61,6 +51,7 @@ export default function DispatchModal(props: {
|
||||
|
||||
const taskStatusSrc = dispatchData?.status ?? 0
|
||||
const taskStatusSrcLabel = dispatchOpts[taskStatusSrc]?.label ?? "-"
|
||||
const taskRejected = Boolean(dispatchData?.reject)
|
||||
const dstOptions =
|
||||
dispatchOpts[taskStatusSrc]?.opts?.map((o) => ({
|
||||
label: o.label,
|
||||
@@ -116,7 +107,7 @@ export default function DispatchModal(props: {
|
||||
try {
|
||||
await taskDispatch({
|
||||
operation_uid: opUid,
|
||||
reject: values.reject,
|
||||
reject: taskRejected,
|
||||
task_ids: task_ids.map((item) => item.id),
|
||||
task_status_src: taskStatusSrc,
|
||||
task_status_dst: taskStatusDst,
|
||||
@@ -143,9 +134,16 @@ export default function DispatchModal(props: {
|
||||
当前任务:
|
||||
{task_ids.map((item) => item.project_name).join("、") ?? "-"}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
(源状态:{taskStatusSrcLabel})
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
<Text size="sm" c="dimmed">
|
||||
(源状态:{taskStatusSrcLabel})
|
||||
</Text>
|
||||
{taskRejected ? (
|
||||
<Badge variant="outline" color="yellow" size="sm">
|
||||
返工
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
<Select
|
||||
label="目标状态"
|
||||
@@ -168,14 +166,6 @@ export default function DispatchModal(props: {
|
||||
onChange={(v) => form.setFieldValue("uid_dst", v ?? "")}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label="是否返工"
|
||||
checked={form.values.reject}
|
||||
onChange={(e) =>
|
||||
form.setFieldValue("reject", e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
useTimerStore,
|
||||
} from "./useTimerStore"
|
||||
import { useTopToolsStore } from "./useTopToolsStore"
|
||||
import { findGroupKey } from "./util"
|
||||
import { findGroupKey, isEditableKeyboardTarget } from "./util"
|
||||
import { safeClone } from "./utils/clone"
|
||||
import { labelTypeMap } from "./utils/constants"
|
||||
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
||||
@@ -83,6 +83,11 @@ let latestTaskDetailRequestSeq = 0
|
||||
const RIGHT_PANEL_TRANSITION =
|
||||
"width 160ms ease, min-width 160ms ease, opacity 160ms ease"
|
||||
const RIGHT_PANEL_WILL_CHANGE = "width, min-width, opacity"
|
||||
const RIGHT_PANEL_SEPARATOR =
|
||||
"1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))"
|
||||
const RIGHT_PANEL_SOFT_MAIN_MIN_WIDTH = 420
|
||||
const RIGHT_PANEL_HARD_MAIN_MIN_WIDTH = 260
|
||||
const RIGHT_PANEL_TARGET_WIDTH = 350
|
||||
|
||||
// utils
|
||||
const getRectPoints = ([sx, sy, w, h]: number[]) => {
|
||||
@@ -111,6 +116,51 @@ const getRightPanelViewportStyle = (width: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const allocateRightPanelSizes = (
|
||||
availableWidth: number,
|
||||
visibleFlags: boolean[]
|
||||
) => {
|
||||
const resolvedWidth = Math.max(0, Math.round(availableWidth))
|
||||
const nextSizes = [resolvedWidth, 0, 0, 0, 0, 0]
|
||||
const activeIndexes = visibleFlags.reduce<number[]>((result, flag, index) => {
|
||||
if (flag) result.push(index + 1)
|
||||
return result
|
||||
}, [])
|
||||
|
||||
if (!activeIndexes.length) return nextSizes
|
||||
|
||||
const activeCount = activeIndexes.length
|
||||
const softPanelBudget = Math.max(
|
||||
0,
|
||||
resolvedWidth - RIGHT_PANEL_SOFT_MAIN_MIN_WIDTH
|
||||
)
|
||||
const hardPanelBudget = Math.max(
|
||||
0,
|
||||
resolvedWidth - RIGHT_PANEL_HARD_MAIN_MIN_WIDTH
|
||||
)
|
||||
let panelWidth = Math.min(
|
||||
RIGHT_PANEL_TARGET_WIDTH,
|
||||
Math.floor(softPanelBudget / activeCount)
|
||||
)
|
||||
|
||||
if (panelWidth < RIGHT_PANEL_TARGET_WIDTH) {
|
||||
panelWidth = Math.min(
|
||||
RIGHT_PANEL_TARGET_WIDTH,
|
||||
Math.floor(hardPanelBudget / activeCount)
|
||||
)
|
||||
}
|
||||
|
||||
panelWidth = Math.max(0, panelWidth)
|
||||
activeIndexes.forEach((index) => {
|
||||
nextSizes[index] = panelWidth
|
||||
})
|
||||
|
||||
const rightPanelTotalWidth = panelWidth * activeCount
|
||||
nextSizes[0] = Math.max(0, resolvedWidth - rightPanelTotalWidth)
|
||||
|
||||
return nextSizes
|
||||
}
|
||||
|
||||
const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i
|
||||
const GLOBAL_LOADING_Z_INDEX = 900
|
||||
const videoMimeByExt: Record<string, string> = {
|
||||
@@ -233,7 +283,7 @@ const LabelPage = ({
|
||||
const qaToolContainerRef = useRef<any>(null)
|
||||
const [isConfirmSave, setIsConfirmSave] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [sizes, setSizes] = useState<number[]>([])
|
||||
const [sizes, setSizes] = useState<number[]>([0, 0, 0, 0, 0, 0])
|
||||
const [videoFetchPendingCount, setVideoFetchPendingCount] = useState(0)
|
||||
const [hasReadyVideoFrame, setHasReadyVideoFrame] = useState(false)
|
||||
const [sourceVideoNames, setSourceVideoNames] = useState<string[]>([])
|
||||
@@ -2114,7 +2164,7 @@ const LabelPage = ({
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
// console.log(e, e.key);
|
||||
if (focusInput) return
|
||||
if (focusInput || isEditableKeyboardTarget(e.target)) return
|
||||
|
||||
console.log(e.key)
|
||||
const mode = usePaperStore.getState().mode
|
||||
@@ -2479,51 +2529,35 @@ const LabelPage = ({
|
||||
|
||||
// 更新Splitter状态
|
||||
const updateSplitterSizes = useCallback(() => {
|
||||
console.log(
|
||||
"document.documentElement.clientWidth",
|
||||
document.documentElement.clientWidth
|
||||
const availableWidth = Math.max(
|
||||
document.documentElement.clientWidth - leftWidth - 4,
|
||||
0
|
||||
)
|
||||
|
||||
const len = document.documentElement.clientWidth - 4
|
||||
let arr = [len, 0, 0, 0, 0, 0]
|
||||
const flags = [
|
||||
const flags: boolean[] = [
|
||||
showTaskList,
|
||||
showGroupList,
|
||||
showDescList && projectDetail && projectDetail.label_type === 5,
|
||||
showDescList && projectDetail && projectDetail.label_type === 6,
|
||||
(showDescList && projectDetail && projectDetail.label_type === 5) ||
|
||||
false,
|
||||
(showDescList && projectDetail && projectDetail.label_type === 6) ||
|
||||
false,
|
||||
showObjectList,
|
||||
]
|
||||
let count = 0
|
||||
flags.forEach((flag) => {
|
||||
if (flag) count++
|
||||
})
|
||||
if (count === 1 || count === 2) {
|
||||
arr[0] = count === 1 ? len * 0.8 : len * 0.6
|
||||
flags.forEach((flag, index) => {
|
||||
if (flag) {
|
||||
arr[index + 1] = len * 0.2
|
||||
}
|
||||
})
|
||||
} else if (count === 3) {
|
||||
arr[0] = len * 0.55
|
||||
flags.forEach((flag, index) => {
|
||||
if (flag) {
|
||||
arr[index + 1] = len * 0.15
|
||||
}
|
||||
})
|
||||
} else if (count === 4) {
|
||||
arr[0] = len * 0.6
|
||||
flags.forEach((flag, index) => {
|
||||
if (flag) {
|
||||
arr[index + 1] = len * 0.1
|
||||
}
|
||||
})
|
||||
}
|
||||
setSizes(arr)
|
||||
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
|
||||
setSizes(allocateRightPanelSizes(availableWidth, flags))
|
||||
}, [
|
||||
leftWidth,
|
||||
projectDetail,
|
||||
showDescList,
|
||||
showGroupList,
|
||||
showObjectList,
|
||||
showTaskList,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
updateSplitterSizes()
|
||||
window.addEventListener("resize", updateSplitterSizes)
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateSplitterSizes)
|
||||
}
|
||||
}, [updateSplitterSizes])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2702,7 +2736,7 @@ const LabelPage = ({
|
||||
</Box>
|
||||
<Flex w="calc(100% - 4px)" h={mainBoxHeight}>
|
||||
{/* <Flex h="100%" w={"100%"}> */}
|
||||
<Box w={sizes[0] - leftWidth} style={{ flexShrink: 0 }}>
|
||||
<Box w={sizes[0]} style={{ flexShrink: 0, minWidth: 0 }}>
|
||||
<Box h="100%" id="resize-container">
|
||||
<Flex h="100%" w="100%">
|
||||
<Flex
|
||||
@@ -2780,7 +2814,7 @@ const LabelPage = ({
|
||||
</Box>
|
||||
<Box
|
||||
w={sizes[1]}
|
||||
miw={showTaskList ? 300 : 0}
|
||||
miw={showTaskList ? sizes[1] : 0}
|
||||
style={{
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
@@ -2788,10 +2822,11 @@ const LabelPage = ({
|
||||
opacity: showTaskList ? 1 : 0,
|
||||
visibility: showTaskList ? "visible" : "hidden",
|
||||
pointerEvents: showTaskList ? "auto" : "none",
|
||||
borderLeft: showTaskList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||
transition: RIGHT_PANEL_TRANSITION,
|
||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||
}}>
|
||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[1], 300))}>
|
||||
<Box style={getRightPanelViewportStyle(sizes[1])}>
|
||||
<RightTaskTools
|
||||
projectDetail={projectDetail}
|
||||
project_id={projectId}
|
||||
@@ -2801,7 +2836,7 @@ const LabelPage = ({
|
||||
</Box>
|
||||
<Box
|
||||
w={sizes[2]}
|
||||
miw={showGroupList ? 200 : 0}
|
||||
miw={showGroupList ? sizes[2] : 0}
|
||||
style={{
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
@@ -2809,10 +2844,11 @@ const LabelPage = ({
|
||||
opacity: showGroupList ? 1 : 0,
|
||||
visibility: showGroupList ? "visible" : "hidden",
|
||||
pointerEvents: showGroupList ? "auto" : "none",
|
||||
borderLeft: showGroupList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||
transition: RIGHT_PANEL_TRANSITION,
|
||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||
}}>
|
||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[2], 200))}>
|
||||
<Box style={getRightPanelViewportStyle(sizes[2])}>
|
||||
<RightGroupTools
|
||||
labelState={labelState}
|
||||
projectDetail={projectDetail}
|
||||
@@ -2823,7 +2859,7 @@ const LabelPage = ({
|
||||
w={sizes[3]}
|
||||
miw={
|
||||
showDescList && projectDetail && projectDetail.label_type === 5
|
||||
? 350
|
||||
? sizes[3]
|
||||
: 0
|
||||
}
|
||||
style={{
|
||||
@@ -2842,10 +2878,14 @@ const LabelPage = ({
|
||||
showDescList && projectDetail && projectDetail.label_type === 5
|
||||
? "auto"
|
||||
: "none",
|
||||
borderLeft:
|
||||
showDescList && projectDetail && projectDetail.label_type === 5
|
||||
? RIGHT_PANEL_SEPARATOR
|
||||
: "none",
|
||||
transition: RIGHT_PANEL_TRANSITION,
|
||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||
}}>
|
||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[3], 350))}>
|
||||
<Box style={getRightPanelViewportStyle(sizes[3])}>
|
||||
<RightDescTools taskDetail={taskDetail} />
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -2853,7 +2893,7 @@ const LabelPage = ({
|
||||
w={sizes[4]}
|
||||
miw={
|
||||
showDescList && projectDetail && projectDetail.label_type === 6
|
||||
? 350
|
||||
? sizes[4]
|
||||
: 0
|
||||
}
|
||||
style={{
|
||||
@@ -2872,16 +2912,20 @@ const LabelPage = ({
|
||||
showDescList && projectDetail && projectDetail.label_type === 6
|
||||
? "auto"
|
||||
: "none",
|
||||
borderLeft:
|
||||
showDescList && projectDetail && projectDetail.label_type === 6
|
||||
? RIGHT_PANEL_SEPARATOR
|
||||
: "none",
|
||||
transition: RIGHT_PANEL_TRANSITION,
|
||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||
}}>
|
||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[4], 350))}>
|
||||
<Box style={getRightPanelViewportStyle(sizes[4])}>
|
||||
<RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
w={sizes[5]}
|
||||
miw={showObjectList ? 350 : 0}
|
||||
miw={showObjectList ? sizes[5] : 0}
|
||||
style={{
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
@@ -2889,10 +2933,11 @@ const LabelPage = ({
|
||||
opacity: showObjectList ? 1 : 0,
|
||||
visibility: showObjectList ? "visible" : "hidden",
|
||||
pointerEvents: showObjectList ? "auto" : "none",
|
||||
borderLeft: showObjectList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||
transition: RIGHT_PANEL_TRANSITION,
|
||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||
}}>
|
||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[5], 350))}>
|
||||
<Box style={getRightPanelViewportStyle(sizes[5])}>
|
||||
<RightObjectTools
|
||||
taskDetail={taskDetail}
|
||||
labelState={labelState}
|
||||
|
||||
@@ -222,6 +222,7 @@ const PaperContainer = (
|
||||
}
|
||||
|
||||
if (
|
||||
!useTopToolsStore.getState().saveCurrentScale &&
|
||||
RESIZE_INTERPOLATION_DURATION_MS > 0 &&
|
||||
firstRender &&
|
||||
lastSize?.clientWidth &&
|
||||
@@ -1022,16 +1023,12 @@ const PaperContainer = (
|
||||
parent: usePaperStore.getState().group,
|
||||
strokeScaling: false,
|
||||
})
|
||||
if (Math.abs(path.area) < useTopToolsStore.getState().checkSize) {
|
||||
path.remove()
|
||||
if (hollows[index]) {
|
||||
path.data.isHollowPolygon = true
|
||||
innerPaths.push(path)
|
||||
} else {
|
||||
if (hollows[index]) {
|
||||
path.data.isHollowPolygon = true
|
||||
innerPaths.push(path)
|
||||
} else {
|
||||
path.fillColor = new paper.Color(fillColor)
|
||||
outerPaths.push(path)
|
||||
}
|
||||
path.fillColor = new paper.Color(fillColor)
|
||||
outerPaths.push(path)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3136,6 +3133,8 @@ const PaperContainer = (
|
||||
const activeImageChanged = prevActiveImageRef.current !== activeImage
|
||||
prevActiveImageRef.current = activeImage
|
||||
const hasPicRaster = getPicRasters(currentGroup).length > 0
|
||||
const preserveViewportOnLayoutResize =
|
||||
useTopToolsStore.getState().saveCurrentScale
|
||||
|
||||
if (!useTopToolsStore.getState().saveCurrentScale) {
|
||||
currentGroup.scaling = new paper.Point(1, 1)
|
||||
@@ -3147,6 +3146,12 @@ const PaperContainer = (
|
||||
}
|
||||
|
||||
if (!activeImageChanged) {
|
||||
if (
|
||||
preserveViewportOnLayoutResize &&
|
||||
(hasPicRaster || rasterLoadingImageRef.current === activeImage)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (resizeCurrentImage()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
Spline,
|
||||
} from "lucide-react"
|
||||
import paper from "paper"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { Project } from "../api/project/typing"
|
||||
import { Task } from "../api/task/typing"
|
||||
import { LabelState } from "../LabelNossr"
|
||||
@@ -151,6 +151,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
continuePolygonTarget?.imageId === activeImage
|
||||
? continuePolygonTarget.objectId
|
||||
: null
|
||||
const objectOperations = useTopToolsStore((state) => state.objectOperations)
|
||||
|
||||
const getOperationChildren = useCallback(
|
||||
(imageId: string, operationId: string) => {
|
||||
@@ -415,9 +416,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
[activeImage]
|
||||
)
|
||||
|
||||
const activeImageOperations = (labelState.get(activeImage) || []).filter(
|
||||
(operationId) => getOperationChildren(activeImage, operationId).length
|
||||
)
|
||||
const activeImageOperations = useMemo(() => {
|
||||
const schemaOperationIds = objectOperations.map((item) =>
|
||||
item.category_id.toString()
|
||||
)
|
||||
const imageOperationIds = labelState.get(activeImage) || []
|
||||
return Array.from(new Set([...schemaOperationIds, ...imageOperationIds]))
|
||||
}, [activeImage, labelState, objectOperations])
|
||||
|
||||
const isObjectHidden = (imageId: string, objectId: number) => {
|
||||
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
|
||||
@@ -558,9 +563,17 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
return (
|
||||
<Box h="100%" w="100%">
|
||||
<Flex direction="column" h="100%" gap="xs">
|
||||
<Flex justify="space-between" align="center" px="md" py="xs">
|
||||
<Group gap="xs">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
gap={6}
|
||||
px="md"
|
||||
py="xs"
|
||||
style={{ minWidth: 0 }}>
|
||||
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
|
||||
onClick={() => {
|
||||
@@ -571,20 +584,23 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
}}>
|
||||
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</ActionIcon>
|
||||
<Title order={4} size="h4">
|
||||
<Title
|
||||
order={5}
|
||||
size="h5"
|
||||
style={{ whiteSpace: "nowrap", lineHeight: 1.1 }}>
|
||||
对象列表
|
||||
</Title>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
|
||||
<TextInput
|
||||
w={96}
|
||||
w={72}
|
||||
size="xs"
|
||||
placeholder="输入ID"
|
||||
value={searchId}
|
||||
onChange={(e) => setSearchId(e.target.value)}
|
||||
onKeyDown={handleSelectedBySearchId}
|
||||
/>
|
||||
<Group gap={4}>
|
||||
<Group gap={2} wrap="nowrap">
|
||||
{[3, 2, 1, 0].map((type, index) => (
|
||||
<Checkbox
|
||||
key={type}
|
||||
@@ -603,6 +619,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
))}
|
||||
</Group>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
updateImageStatus(
|
||||
|
||||
@@ -54,8 +54,8 @@ import {
|
||||
MousePointer,
|
||||
Paintbrush,
|
||||
Pen,
|
||||
SquarePen,
|
||||
SquareDashedMousePointer,
|
||||
SquarePen,
|
||||
Tag,
|
||||
Timer,
|
||||
} from "lucide-react"
|
||||
@@ -92,7 +92,12 @@ import { useKeyboardStore } from "../useKeyBoardStore"
|
||||
import { usePaperStore } from "../usePaperStore"
|
||||
import { useRightToolsStore } from "../useRightToolsStore"
|
||||
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
||||
import { useTopToolsStore } from "../useTopToolsStore"
|
||||
import {
|
||||
DEFAULT_NODE_SIZE,
|
||||
NODE_SIZE_MAX,
|
||||
NODE_SIZE_MIN,
|
||||
useTopToolsStore,
|
||||
} from "../useTopToolsStore"
|
||||
import { getSubAttrStatus } from "../util"
|
||||
import { safeClone } from "../utils/clone"
|
||||
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
||||
@@ -222,13 +227,14 @@ const TopTools = (
|
||||
setAssistToolEnabled,
|
||||
assistToolSize,
|
||||
setAssistToolSize,
|
||||
nodeSize,
|
||||
setNodeSize,
|
||||
needBackup,
|
||||
setNeedBackup,
|
||||
checkSize,
|
||||
setCheckSize,
|
||||
magnetFlag,
|
||||
setMagnetFlag,
|
||||
} = useTopToolsStore()
|
||||
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
||||
|
||||
const { pathGroupMap } = useRightToolsStore()
|
||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||
@@ -247,19 +253,25 @@ const TopTools = (
|
||||
].filter(Boolean).length
|
||||
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
|
||||
|
||||
useEffect(() => {
|
||||
setNodeSizeInput(nodeSize)
|
||||
usePaperStore.getState().refreshNodeVisualSize()
|
||||
}, [nodeSize])
|
||||
|
||||
const toggleRightPanel = useCallback(
|
||||
(visible: boolean, setter: (val: boolean) => void) => {
|
||||
const nextVisible = !visible
|
||||
const nextCount = visibleRightPanelCount + (nextVisible ? 1 : -1)
|
||||
if (
|
||||
(visibleRightPanelCount === 0 && nextCount === 1) ||
|
||||
(visibleRightPanelCount === 1 && nextCount === 0)
|
||||
!saveCurrentScale &&
|
||||
((visibleRightPanelCount === 0 && nextCount === 1) ||
|
||||
(visibleRightPanelCount === 1 && nextCount === 0))
|
||||
) {
|
||||
requestResizeGhost()
|
||||
}
|
||||
setter(nextVisible)
|
||||
},
|
||||
[requestResizeGhost, visibleRightPanelCount]
|
||||
[requestResizeGhost, saveCurrentScale, visibleRightPanelCount]
|
||||
)
|
||||
|
||||
// 当前是否可以操作功能按键
|
||||
@@ -1107,6 +1119,7 @@ const TopTools = (
|
||||
})
|
||||
try {
|
||||
await handleSave()
|
||||
notifications.hide("save")
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return
|
||||
@@ -2188,23 +2201,39 @@ const TopTools = (
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
||||
<NumberInput
|
||||
value={checkSize}
|
||||
w={50}
|
||||
min={1}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
hideControls
|
||||
onChange={(v) => {
|
||||
if (v) setCheckSize(Number(v))
|
||||
}}
|
||||
onFocus={() => {
|
||||
useKeyEventStore.getState().setFocusInput(true)
|
||||
}}
|
||||
onBlur={() => {
|
||||
useKeyEventStore.getState().setFocusInput(false)
|
||||
}}
|
||||
/>
|
||||
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
||||
<Flex align="center" gap={4}>
|
||||
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||
节点
|
||||
</Text>
|
||||
<NumberInput
|
||||
value={nodeSizeInput}
|
||||
w={50}
|
||||
min={NODE_SIZE_MIN}
|
||||
max={NODE_SIZE_MAX}
|
||||
step={1}
|
||||
allowDecimal={false}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
hideControls
|
||||
onChange={(value) => {
|
||||
setNodeSizeInput(value)
|
||||
}}
|
||||
onFocus={() => {
|
||||
useKeyEventStore.getState().setFocusInput(true)
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
useKeyEventStore.getState().setFocusInput(false)
|
||||
const rawValue = event.currentTarget.value.trim()
|
||||
const nextValue =
|
||||
rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
|
||||
setNodeSize(
|
||||
Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
{renderEditIcon}
|
||||
<Button
|
||||
w="fit-content"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useKeyboardStore } from "./useKeyBoardStore"
|
||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||
import { usePaperSupportStore } from "./usePaperSupportStore"
|
||||
import { useRightToolsStore } from "./useRightToolsStore"
|
||||
import { useTopToolsStore } from "./useTopToolsStore"
|
||||
import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore"
|
||||
import { safeClone } from "./utils/clone"
|
||||
|
||||
interface ContinuePolygonTarget {
|
||||
@@ -183,6 +183,7 @@ interface PaperState {
|
||||
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
|
||||
beginContinuePolygonDraft: () => boolean
|
||||
setLoadingData: (flag: boolean) => void
|
||||
refreshNodeVisualSize: () => void
|
||||
resizeGhostRequestToken: number
|
||||
requestResizeGhost: () => void
|
||||
}
|
||||
@@ -298,6 +299,39 @@ const handleDelete = () => {
|
||||
// });
|
||||
}
|
||||
|
||||
const cleanupDeletedObjectItems = (
|
||||
imageId: string,
|
||||
pathId?: number,
|
||||
parentGroupId?: number
|
||||
) => {
|
||||
if (!pathId) return
|
||||
|
||||
usePaperStore
|
||||
.getState()
|
||||
.group?.getItems({
|
||||
data: { id: pathId },
|
||||
})
|
||||
.forEach((item) => {
|
||||
item.remove()
|
||||
})
|
||||
|
||||
useObjectStore.getState().updateSelectedPath(imageId, "DELETE", pathId)
|
||||
|
||||
if (!parentGroupId) return
|
||||
|
||||
const groupMap = useRightToolsStore.getState().pathGroupMap.get(imageId)
|
||||
const currentGroupIds = groupMap
|
||||
?.get(parentGroupId)
|
||||
?.filter((id) => id !== pathId)
|
||||
|
||||
if (!currentGroupIds || currentGroupIds.length <= 1) {
|
||||
groupMap?.delete(parentGroupId)
|
||||
return
|
||||
}
|
||||
|
||||
groupMap?.set(parentGroupId, currentGroupIds)
|
||||
}
|
||||
|
||||
const handleShift = (flag: boolean) => {
|
||||
useKeyEventStore.getState().setShift(flag)
|
||||
}
|
||||
@@ -328,6 +362,7 @@ const PAPER_CONTEXT_HIT_TYPES = [
|
||||
"pathBuff",
|
||||
"pathCircle",
|
||||
] as const
|
||||
const LEGACY_PAPER_NODE_DIAMETER = 6
|
||||
|
||||
const isPaperObjectType = (type: unknown) =>
|
||||
typeof type === "string" &&
|
||||
@@ -339,6 +374,39 @@ const isPaperContextHitType = (type: unknown) =>
|
||||
type as (typeof PAPER_CONTEXT_HIT_TYPES)[number]
|
||||
)
|
||||
|
||||
const getPaperNodeDiameter = () => {
|
||||
const configuredDiameter = useTopToolsStore.getState().nodeSize
|
||||
return Number.isFinite(configuredDiameter)
|
||||
? Math.round(configuredDiameter)
|
||||
: DEFAULT_NODE_SIZE
|
||||
}
|
||||
|
||||
const getPaperNodeRadius = () => getPaperNodeDiameter() / 2
|
||||
|
||||
const getPaperNodeScaleFactor = () => {
|
||||
const currentScaling = usePaperStore.getState().group?.scaling.x || 1
|
||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||
return newScale / currentScaling
|
||||
}
|
||||
|
||||
const syncPaperCircleDiameter = (circle: paper.Path) => {
|
||||
const nextDiameter = getPaperNodeDiameter()
|
||||
const currentDiameter =
|
||||
Number(circle.data?.diameter) || LEGACY_PAPER_NODE_DIAMETER
|
||||
|
||||
if (
|
||||
Number.isFinite(currentDiameter) &&
|
||||
currentDiameter > 0 &&
|
||||
Math.abs(currentDiameter - nextDiameter) > 0.001
|
||||
) {
|
||||
circle.scale(nextDiameter / currentDiameter)
|
||||
}
|
||||
|
||||
circle.data = Object.assign({}, circle.data, {
|
||||
diameter: nextDiameter,
|
||||
})
|
||||
}
|
||||
|
||||
const getPaperCursorByMode = (
|
||||
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||||
) => {
|
||||
@@ -462,6 +530,124 @@ const getPaperSelectionPath = (
|
||||
return nearestPath
|
||||
}
|
||||
|
||||
const getNearestPaperPathSegmentIndex = (
|
||||
path: paper.Path,
|
||||
localPoint: paper.Point
|
||||
) => {
|
||||
let nearestIndex = 0
|
||||
let nearestDistance = Infinity
|
||||
|
||||
path.segments.forEach((segment, index) => {
|
||||
const distance = segment.point.getDistance(localPoint)
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance
|
||||
nearestIndex = index
|
||||
}
|
||||
})
|
||||
|
||||
return nearestIndex
|
||||
}
|
||||
|
||||
const normalizePointPanHitResult = (
|
||||
hitResult: paper.HitResult | null,
|
||||
localPoint: paper.Point
|
||||
) => {
|
||||
if (!hitResult?.item?.data?.id || hitResult.item.data?.type !== "circle") {
|
||||
return hitResult
|
||||
}
|
||||
|
||||
const selectedPath = getPaperSelectionPath(hitResult.item.data.id, localPoint)
|
||||
if (
|
||||
!(selectedPath instanceof paper.Path) ||
|
||||
selectedPath.data?.type !== "point" ||
|
||||
!selectedPath.segments.length
|
||||
) {
|
||||
return hitResult
|
||||
}
|
||||
|
||||
const segmentIndex = getNearestPaperPathSegmentIndex(selectedPath, localPoint)
|
||||
|
||||
return {
|
||||
...hitResult,
|
||||
item: selectedPath,
|
||||
type: "segment",
|
||||
segment: selectedPath.segments[segmentIndex],
|
||||
} as paper.HitResult
|
||||
}
|
||||
|
||||
const getPointCirclesById = (id: number) =>
|
||||
(usePaperStore.getState().group?.getItems({
|
||||
data: {
|
||||
id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path.Circle[]) || []
|
||||
|
||||
const createPointCircleForPath = (
|
||||
path: paper.Path,
|
||||
point: paper.Point,
|
||||
index: number
|
||||
) => {
|
||||
const circle = usePaperStore
|
||||
.getState()
|
||||
.getPointCircle(point, index, path.data.id, path.data.blankColor)
|
||||
|
||||
circle.data = Object.assign({}, circle.data, {
|
||||
id: path.data.id,
|
||||
imageId: path.data.imageId,
|
||||
operationId: path.data.operationId,
|
||||
text: index + 1,
|
||||
diameter: getPaperNodeDiameter(),
|
||||
fillColor: path.data.blankColor,
|
||||
strokeColor: path.data.strokeColor,
|
||||
})
|
||||
circle.fillColor = path.data.blankColor || circle.fillColor || null
|
||||
circle.strokeColor = path.data.strokeColor || circle.strokeColor || null
|
||||
|
||||
return circle
|
||||
}
|
||||
|
||||
const syncPointCirclesWithPath = (path: paper.Path) => {
|
||||
if (path.data?.type !== "point" || !path.data?.id) return []
|
||||
|
||||
const unusedCircles = getPointCirclesById(path.data.id).slice()
|
||||
const orderedCircles = path.segments.map((segment, index) => {
|
||||
const matchedCircleIndex = unusedCircles.findIndex((circle) => {
|
||||
return (
|
||||
circle.position.getDistance(segment.point) <= 0.01 ||
|
||||
circle.contains(segment.point)
|
||||
)
|
||||
})
|
||||
|
||||
const circle =
|
||||
matchedCircleIndex >= 0
|
||||
? unusedCircles.splice(matchedCircleIndex, 1)[0]
|
||||
: createPointCircleForPath(path, segment.point, index)
|
||||
|
||||
circle.position.x = segment.point.x
|
||||
circle.position.y = segment.point.y
|
||||
circle.data = Object.assign({}, circle.data, {
|
||||
id: path.data.id,
|
||||
imageId: path.data.imageId,
|
||||
operationId: path.data.operationId,
|
||||
text: index + 1,
|
||||
diameter: getPaperNodeDiameter(),
|
||||
fillColor: path.data.blankColor,
|
||||
strokeColor: path.data.strokeColor,
|
||||
})
|
||||
circle.fillColor = path.data.blankColor || circle.fillColor || null
|
||||
circle.strokeColor = path.data.strokeColor || circle.strokeColor || null
|
||||
|
||||
return circle
|
||||
})
|
||||
|
||||
unusedCircles.forEach((circle) => {
|
||||
circle.remove()
|
||||
})
|
||||
|
||||
return orderedCircles
|
||||
}
|
||||
|
||||
const trySelectPaperObjectByPoint = (clickPoint: paper.Point) => {
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return false
|
||||
@@ -898,6 +1084,19 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
...state,
|
||||
loadingData: flag,
|
||||
})),
|
||||
refreshNodeVisualSize: () => {
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return
|
||||
;(
|
||||
group.getItems({
|
||||
match: (item: paper.Item) =>
|
||||
item instanceof paper.Path &&
|
||||
["circle", "pathCircle"].includes(item.data?.type || ""),
|
||||
}) as paper.Path[]
|
||||
).forEach((circle) => {
|
||||
syncPaperCircleDiameter(circle)
|
||||
})
|
||||
},
|
||||
requestResizeGhost: () =>
|
||||
set((state: PaperState) => ({
|
||||
...state,
|
||||
@@ -1090,8 +1289,6 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
? baseTolerance / useTopToolsStore.getState().scale
|
||||
: 0
|
||||
|
||||
false && getPanHitTolerance
|
||||
|
||||
const createPanHitOptions = (
|
||||
tolerance: number,
|
||||
match: (hit: paper.HitResult) => boolean
|
||||
@@ -1115,7 +1312,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
if (hoveredCircleInfo) {
|
||||
const pointHit = group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(0, (hit) => {
|
||||
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||||
return (
|
||||
hit.item.data?.type === "pathCircle" &&
|
||||
hit.item.data?.id === hoveredCircleInfo.id &&
|
||||
@@ -1132,7 +1329,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
if (hoveredBufferInfo) {
|
||||
const edgeHit = group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(0, (hit) => {
|
||||
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||||
return (
|
||||
hit.item.data?.type === "pathBuff" &&
|
||||
hit.item.data?.id === hoveredBufferInfo.id &&
|
||||
@@ -1145,8 +1342,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
|
||||
return group.hitTest(
|
||||
projectPoint,
|
||||
createPanHitOptions(0, (hit) =>
|
||||
["rectangle", "polygon", "brush", "point"].includes(
|
||||
createPanHitOptions(getPanHitTolerance(), (hit) =>
|
||||
["rectangle", "polygon", "brush", "point", "circle"].includes(
|
||||
hit.item.data?.type || ""
|
||||
)
|
||||
)
|
||||
@@ -1197,6 +1394,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
hasExceededPanDragThreshold = false
|
||||
|
||||
let hitResult = resolvePanHitResult(e.point)
|
||||
hitResult = normalizePointPanHitResult(hitResult, point)
|
||||
console.log("hitResult", hitResult)
|
||||
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||||
? hoveredBufferPathInfo
|
||||
@@ -1475,27 +1673,26 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
activePath = hitResult.item as paper.Path
|
||||
}
|
||||
} else if (hitResult.item.data.type === "point") {
|
||||
// 只有点击圆才视为选中
|
||||
if (hitResult.type === "segment") {
|
||||
// Path 类型
|
||||
if (hitResult.item instanceof paper.Path) {
|
||||
if (!hitResult.item.data.selected) {
|
||||
hitResult.item.data.selected = true
|
||||
// 绘制遮罩
|
||||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||||
// 绘制点对应的数字
|
||||
usePaperSupportStore
|
||||
.getState()
|
||||
.handleText(hitResult.item as paper.Path)
|
||||
// 绘制选中边
|
||||
usePaperSupportStore
|
||||
.getState()
|
||||
.handleBufferPaths(hitResult.item)
|
||||
// 绘制选中圆
|
||||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||||
}
|
||||
activePath = hitResult.item as paper.Path
|
||||
if (
|
||||
["segment", "stroke"].includes(hitResult.type) &&
|
||||
hitResult.item instanceof paper.Path
|
||||
) {
|
||||
if (!hitResult.item.data.selected) {
|
||||
hitResult.item.data.selected = true
|
||||
// 绘制遮罩
|
||||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||||
// 绘制点对应的数字
|
||||
usePaperSupportStore
|
||||
.getState()
|
||||
.handleText(hitResult.item as paper.Path)
|
||||
// 绘制选中边
|
||||
usePaperSupportStore
|
||||
.getState()
|
||||
.handleBufferPaths(hitResult.item)
|
||||
// 绘制选中圆
|
||||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||||
}
|
||||
activePath = hitResult.item as paper.Path
|
||||
}
|
||||
} else if (
|
||||
hitResult.item.data.type === "pathBuff" ||
|
||||
@@ -1598,21 +1795,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
}
|
||||
if (currentPath.data.type === "point") {
|
||||
usePaperSupportStore.getState().handleText(currentPath)
|
||||
let circles = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: currentPath.data.id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
|
||||
currentPath.segments.forEach((segment, index) => {
|
||||
let findIndex = circles.findIndex((circle) => {
|
||||
return circle.contains(segment.point)
|
||||
})
|
||||
circles[index].data = Object.assign({}, circles[index]?.data, {
|
||||
text: findIndex + 1,
|
||||
})
|
||||
})
|
||||
syncPointCirclesWithPath(currentPath)
|
||||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||||
usePaperSupportStore.getState().handleCircles(currentPath)
|
||||
}
|
||||
@@ -1677,15 +1860,9 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
}
|
||||
} else {
|
||||
activePath.insert(hitIndex + 1, hitResult.location.point)
|
||||
activePath?.data?.type === "point" &&
|
||||
usePaperStore
|
||||
.getState()
|
||||
.getPointCircle(
|
||||
hitResult.location.point,
|
||||
hitIndex + 1,
|
||||
activePath.data.id,
|
||||
activePath.data.blankColor
|
||||
)
|
||||
if (activePath?.data?.type === "point") {
|
||||
syncPointCirclesWithPath(activePath)
|
||||
}
|
||||
activePath.bringToFront()
|
||||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||||
usePaperSupportStore.getState().handleCircles(activePath)
|
||||
@@ -1699,21 +1876,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
if (activePath?.data?.type === "point") {
|
||||
// 绘制点对应的数字
|
||||
usePaperSupportStore.getState().handleText(activePath)
|
||||
let circles = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: activePath.data.id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
|
||||
activePath.segments.forEach((segment, index) => {
|
||||
let findIndex = circles.findIndex((circle) => {
|
||||
return circle.contains(segment.point)
|
||||
})
|
||||
circles[index].data = Object.assign({}, circles[index]?.data, {
|
||||
text: findIndex + 1,
|
||||
})
|
||||
})
|
||||
syncPointCirclesWithPath(activePath)
|
||||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||||
usePaperSupportStore.getState().handleCircles(activePath)
|
||||
}
|
||||
@@ -1807,39 +1970,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||||
}
|
||||
if (activePath?.data?.type === "point") {
|
||||
let paths = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: activePath.data.id,
|
||||
// text: activePath.segments.length - hitIndex + 1,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
|
||||
// paths = paths.filter((item) => {
|
||||
// if (item.data.text === hitIndex + 1) item.remove();
|
||||
// return item.data.text !== hitIndex + 1;
|
||||
// });
|
||||
paths = paths.filter((circle) => {
|
||||
let findIndex = activePath.segments.findIndex((segment) => {
|
||||
return circle.contains(segment.point)
|
||||
})
|
||||
if (findIndex === -1) {
|
||||
circle.remove()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// 1. 对数组按照 text 属性进行排序
|
||||
let sortedArr = paths
|
||||
.slice()
|
||||
.sort((a, b) => a.data.text - b.data.text)
|
||||
|
||||
// 2. 重新赋值 text 属性为从 1 开始的连续自然数
|
||||
sortedArr.forEach((item, index) => {
|
||||
item.data.text = index + 1
|
||||
})
|
||||
syncPointCirclesWithPath(activePath)
|
||||
|
||||
// paths.forEach((child) => {
|
||||
// if (
|
||||
@@ -2083,27 +2214,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
]!.position.y = point!.y
|
||||
usePaperSupportStore.getState().handleMask(path)
|
||||
} else if (path.data.type === "point") {
|
||||
let circles = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: path.data.id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
const circles = syncPointCirclesWithPath(path)
|
||||
|
||||
// let index = path.segments.length - hitIndex - 1
|
||||
// let index = hitIndex;
|
||||
|
||||
// 移动点
|
||||
if (circles && circles.length) {
|
||||
// let circle = circles.find(
|
||||
// (item) => item.data.text === index + 1
|
||||
// );
|
||||
|
||||
let circle = circles.find((circle) => {
|
||||
return circle.contains(path.segments[hitIndex].point)
|
||||
})
|
||||
circle!.position.x = point!.x
|
||||
circle!.position.y = point!.y
|
||||
if (circles[hitIndex]) {
|
||||
circles[hitIndex].position.x = point!.x
|
||||
circles[hitIndex].position.y = point!.y
|
||||
}
|
||||
// 移动路径
|
||||
path.segments[hitIndex].point.x = point!.x
|
||||
@@ -2315,45 +2430,12 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
}
|
||||
|
||||
const handlePointIntersectRaster = (path: paper.Path) => {
|
||||
let circles = usePaperStore.getState().group!.getItems({
|
||||
data: {
|
||||
id: path.data.id,
|
||||
type: "circle",
|
||||
},
|
||||
}) as paper.Path[]
|
||||
// 去除超出图片边界的部分
|
||||
let newCircles = circles.filter((circle) => {
|
||||
if (
|
||||
!usePaperStore
|
||||
.getState()
|
||||
.rasterPath!.bounds.contains(circle.bounds.center)
|
||||
) {
|
||||
circle.remove()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// 1. 对数组按照 text 属性进行排序
|
||||
let sortedArr = newCircles
|
||||
.slice()
|
||||
.sort((a, b) => a.data.text - b.data.text)
|
||||
|
||||
// 2. 重新赋值 text 属性为从 1 开始的连续自然数
|
||||
sortedArr.forEach((item, index) => {
|
||||
item.data.text = index + 1
|
||||
})
|
||||
|
||||
// newCircles.forEach((circle, circleIndex) => {
|
||||
// // 重新设置显示数字
|
||||
// circle.data.text = circleIndex + 1;
|
||||
// });
|
||||
path.segments = path.segments.filter((segment) => {
|
||||
return usePaperStore
|
||||
.getState()
|
||||
.rasterPath!.bounds.contains(segment.point)
|
||||
})
|
||||
syncPointCirclesWithPath(path)
|
||||
// 去除超出图片边界的部分 end
|
||||
usePaperSupportStore.getState().handleMask(path)
|
||||
usePaperSupportStore.getState().handleBufferPaths(path)
|
||||
@@ -2507,6 +2589,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
rectPath.data.operationId,
|
||||
rectPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
rectPath.data.imageId,
|
||||
rectPath.data.id,
|
||||
rectPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
|
||||
forceUpdate()
|
||||
@@ -2758,6 +2845,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
polygonPath.data.operationId,
|
||||
polygonPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
polygonPath.data.imageId,
|
||||
polygonPath.data.id,
|
||||
polygonPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2837,6 +2929,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
brushPath.data.operationId,
|
||||
brushPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
brushPath.data.imageId,
|
||||
brushPath.data.id,
|
||||
brushPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (brushPath instanceof paper.Path) {
|
||||
@@ -2864,6 +2961,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
brushPath.data.operationId,
|
||||
brushPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
brushPath.data.imageId,
|
||||
brushPath.data.id,
|
||||
brushPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
usePaperSupportStore.getState().removeMaskById(brushPath.data.id)
|
||||
@@ -2874,6 +2976,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
brushPath.data.operationId,
|
||||
brushPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
brushPath.data.imageId,
|
||||
brushPath.data.id,
|
||||
brushPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2934,6 +3041,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
pointPath.data.operationId,
|
||||
pointPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
pointPath.data.imageId,
|
||||
pointPath.data.id,
|
||||
pointPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
usePaperSupportStore.getState().removeMaskById(pointPath.data.id)
|
||||
@@ -2944,6 +3056,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
pointPath.data.operationId,
|
||||
pointPath.data.id
|
||||
)
|
||||
cleanupDeletedObjectItems(
|
||||
pointPath.data.imageId,
|
||||
pointPath.data.id,
|
||||
pointPath.data.parentGroupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5428,6 +5545,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||
myCircle.scale(newScale / currentScaling)
|
||||
myCircle.data = Object.assign({ type: "circle", ...option }, data)
|
||||
syncPaperCircleDiameter(myCircle)
|
||||
myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||||
if (e.event && e.event.button === 2) {
|
||||
if (
|
||||
@@ -5845,6 +5963,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
id: number,
|
||||
color: any
|
||||
) => {
|
||||
const nodeDiameter = getPaperNodeDiameter()
|
||||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||||
Object.assign(
|
||||
{ parent: usePaperStore.getState().group!, center: point },
|
||||
@@ -5852,12 +5971,13 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
fillColor: color,
|
||||
// center: point,
|
||||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||||
radius: 3,
|
||||
radius: getPaperNodeRadius(),
|
||||
strokeScaling: false,
|
||||
},
|
||||
{
|
||||
data: {
|
||||
type: "circle",
|
||||
diameter: nodeDiameter,
|
||||
fillColor: color,
|
||||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||||
imageId: useBottomToolsStore.getState().activeImage,
|
||||
@@ -5869,9 +5989,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
}
|
||||
)
|
||||
)
|
||||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||
circle.scale(newScale / currentScaling)
|
||||
circle.scale(getPaperNodeScaleFactor())
|
||||
circle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||||
if (e.event && e.event.button === 2) {
|
||||
if (
|
||||
@@ -6052,8 +6170,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
id: number,
|
||||
color: any
|
||||
) => {
|
||||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||
const nodeDiameter = getPaperNodeDiameter()
|
||||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||||
Object.assign(
|
||||
{ parent: usePaperStore.getState().group!, center: point },
|
||||
@@ -6061,15 +6178,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
fillColor: color,
|
||||
// center: point,
|
||||
strokeColor: "#FFF",
|
||||
radius: 3,
|
||||
strokeWidth: 2,
|
||||
radius: getPaperNodeRadius(),
|
||||
strokeScaling: false,
|
||||
},
|
||||
{
|
||||
data: { index, type: "pathCircle", id },
|
||||
data: { index, type: "pathCircle", id, diameter: nodeDiameter },
|
||||
}
|
||||
)
|
||||
)
|
||||
circle.scale(newScale / currentScaling)
|
||||
circle.scale(getPaperNodeScaleFactor())
|
||||
circle.onMouseEnter = (_e: any) => {
|
||||
hoveredPathCircleInfo = {
|
||||
id,
|
||||
|
||||
@@ -5,6 +5,145 @@ import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||
import { usePaperStore } from "./usePaperStore"
|
||||
import { useTopToolsStore } from "./useTopToolsStore"
|
||||
|
||||
const TAG_FONT_SIZE = 12
|
||||
const TAG_VERTICAL_GAP = 8
|
||||
const TAG_TEXT_PADDING_X = 6
|
||||
const TAG_TEXT_PADDING_Y = 4
|
||||
const TAG_MAX_LINE_LENGTH = 18
|
||||
|
||||
const wrapLongToken = (token: string, maxLineLength: number) => {
|
||||
if (token.length <= maxLineLength) return [token]
|
||||
|
||||
const parts: string[] = []
|
||||
for (let index = 0; index < token.length; index += maxLineLength) {
|
||||
parts.push(token.slice(index, index + maxLineLength))
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
const wrapTagContent = (
|
||||
content: string,
|
||||
maxLineLength = TAG_MAX_LINE_LENGTH
|
||||
) => {
|
||||
const normalized = content.trim().replace(/\s+/g, " ")
|
||||
if (!normalized) return ""
|
||||
|
||||
const lines: string[] = []
|
||||
let currentLine = ""
|
||||
|
||||
normalized.split(" ").forEach((token) => {
|
||||
if (!token) return
|
||||
|
||||
const wrappedTokens = wrapLongToken(token, maxLineLength)
|
||||
wrappedTokens.forEach((wrappedToken) => {
|
||||
const nextLine = currentLine
|
||||
? `${currentLine} ${wrappedToken}`
|
||||
: wrappedToken
|
||||
|
||||
if (nextLine.length <= maxLineLength) {
|
||||
currentLine = nextLine
|
||||
return
|
||||
}
|
||||
|
||||
if (currentLine) lines.push(currentLine)
|
||||
currentLine = wrappedToken
|
||||
})
|
||||
})
|
||||
|
||||
if (currentLine) lines.push(currentLine)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
const getTagAnchorPoint = (item: paper.Item) => {
|
||||
const segmentPoints: paper.Point[] = []
|
||||
|
||||
if (item instanceof paper.CompoundPath) {
|
||||
item.children.forEach((child) => {
|
||||
if (!(child instanceof paper.Path)) return
|
||||
child.segments.forEach((segment) => {
|
||||
segmentPoints.push(segment.point.clone())
|
||||
})
|
||||
})
|
||||
} else if (item instanceof paper.Path) {
|
||||
item.segments.forEach((segment) => {
|
||||
segmentPoints.push(segment.point.clone())
|
||||
})
|
||||
}
|
||||
|
||||
if (!segmentPoints.length) {
|
||||
return new paper.Point(item.bounds.left, item.bounds.top)
|
||||
}
|
||||
|
||||
return segmentPoints.reduce((highestPoint, point) =>
|
||||
point.y < highestPoint.y ? point : highestPoint
|
||||
)
|
||||
}
|
||||
|
||||
const getTextMaskBounds = (textBounds: paper.Rectangle) =>
|
||||
new paper.Rectangle(
|
||||
new paper.Point(
|
||||
textBounds.left - TAG_TEXT_PADDING_X,
|
||||
textBounds.top - TAG_TEXT_PADDING_Y
|
||||
),
|
||||
new paper.Size(
|
||||
textBounds.width + TAG_TEXT_PADDING_X * 2,
|
||||
textBounds.height + TAG_TEXT_PADDING_Y * 2
|
||||
)
|
||||
)
|
||||
|
||||
const toPaperColor = (color: paper.Color | string | null | undefined) => {
|
||||
if (color instanceof paper.Color) return color.clone()
|
||||
if (!color) return null
|
||||
|
||||
try {
|
||||
return new paper.Color(color)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getTagBackgroundColor = (
|
||||
fillColor: paper.Color | string | null | undefined,
|
||||
strokeColor: paper.Color | string | null | undefined
|
||||
) => {
|
||||
const resolvedColor = toPaperColor(strokeColor) ?? toPaperColor(fillColor)
|
||||
if (!resolvedColor) return new paper.Color(1, 1, 1, 0.9)
|
||||
|
||||
const tagBackgroundColor = resolvedColor.clone()
|
||||
tagBackgroundColor.alpha = 0.9
|
||||
return tagBackgroundColor
|
||||
}
|
||||
|
||||
const getRelativeLuminance = (
|
||||
color: paper.Color | string | null | undefined
|
||||
) => {
|
||||
const resolvedColor = toPaperColor(color)
|
||||
if (!resolvedColor) return 1
|
||||
|
||||
const red = resolvedColor.red
|
||||
const green = resolvedColor.green
|
||||
const blue = resolvedColor.blue
|
||||
|
||||
const toLinear = (channel: number) =>
|
||||
channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.pow((channel + 0.055) / 1.055, 2.4)
|
||||
|
||||
return (
|
||||
0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue)
|
||||
)
|
||||
}
|
||||
|
||||
const getContrastTextColor = (
|
||||
backgroundColor: paper.Color | string | null | undefined
|
||||
) => {
|
||||
const backgroundLuminance = getRelativeLuminance(backgroundColor)
|
||||
const whiteContrast = 1.05 / (backgroundLuminance + 0.05)
|
||||
const blackContrast = (backgroundLuminance + 0.05) / 0.05
|
||||
|
||||
return whiteContrast > blackContrast ? "#ffffff" : "#000000"
|
||||
}
|
||||
|
||||
export const removeTagTexts = (id?: number) => {
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return
|
||||
@@ -26,7 +165,7 @@ const useRenderTag = () => {
|
||||
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
|
||||
? group.scaling.x
|
||||
: 1
|
||||
const applyTagCompensation = (
|
||||
const applyPointTagCompensation = (
|
||||
item: paper.Item,
|
||||
anchor: [number, number]
|
||||
) => {
|
||||
@@ -37,146 +176,163 @@ const useRenderTag = () => {
|
||||
// 清空之前渲染的标签
|
||||
removeTagTexts()
|
||||
|
||||
for (const [imageId, categoryMap] of dataMap) {
|
||||
if (imageId === activeImage) {
|
||||
for (const [categoryId, labelList] of categoryMap) {
|
||||
let category = useTopToolsStore
|
||||
.getState()
|
||||
.objectOperations.find(
|
||||
(item) => item.category_id.toString() === categoryId
|
||||
)!
|
||||
const { label_class } = category
|
||||
labelList.forEach(([id, _d, detail]) => {
|
||||
const path = usePaperStore.getState().getItemById(id)
|
||||
if (path) {
|
||||
const bounds = path.bounds
|
||||
const tagAnchor: [number, number] = [bounds.left, bounds.top - 6]
|
||||
const pos = [...tagAnchor]
|
||||
let pathData = path.data
|
||||
let content = ``
|
||||
if (configs.includes("ID"))
|
||||
content += `${id}${
|
||||
path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
|
||||
}`
|
||||
if (configs.includes("类别")) content += ` ${label_class}`
|
||||
const categoryMap = dataMap.get(activeImage)
|
||||
if (!categoryMap) return
|
||||
|
||||
if (
|
||||
detail.sub_attributes &&
|
||||
Object.keys(detail.sub_attributes).length
|
||||
) {
|
||||
Object.keys(detail.sub_attributes).forEach((key) => {
|
||||
if (configs.includes("子属性")) content += ` ${key}:`
|
||||
if (configs.includes("属性值"))
|
||||
content += `${detail.sub_attributes[key]};`
|
||||
})
|
||||
}
|
||||
// 文本
|
||||
const text = new paper.PointText(pos)
|
||||
text.set({
|
||||
content,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: "black",
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
applyTagCompensation(text, tagAnchor)
|
||||
for (const [categoryId, labelList] of categoryMap) {
|
||||
let category = useTopToolsStore
|
||||
.getState()
|
||||
.objectOperations.find(
|
||||
(item) => item.category_id.toString() === categoryId
|
||||
)!
|
||||
const { label_class } = category
|
||||
labelList.forEach(([id, _d, detail]) => {
|
||||
const path = usePaperStore.getState().getItemById(id)
|
||||
if (!path) return
|
||||
|
||||
// 文本框
|
||||
const textMask = new paper.Path.Rectangle(text.bounds)
|
||||
textMask.set({
|
||||
fillColor: pathData.fillColor,
|
||||
strokeColor: pathData.strokeColor,
|
||||
strokeWidth: 1,
|
||||
strokeScaling: false,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text_mask",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
applyTagCompensation(textMask, tagAnchor)
|
||||
// 标注对象外接框
|
||||
const mask = new paper.Path.Rectangle(bounds)
|
||||
mask.set({
|
||||
strokeColor: pathData.strokeColor,
|
||||
strokeWidth: 2,
|
||||
strokeScaling: false,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "mask",
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("外框"),
|
||||
})
|
||||
// 标注对象点id
|
||||
if (path instanceof paper.CompoundPath) {
|
||||
let index = 1
|
||||
path.children.forEach((child) => {
|
||||
const segments = (child as paper.Path).segments
|
||||
segments.forEach((segment) => {
|
||||
const pointTextPosition: [number, number] = [
|
||||
segment.point.x - 10,
|
||||
segment.point.y + 5,
|
||||
]
|
||||
const pointText = new paper.PointText(pointTextPosition)
|
||||
pointText.set({
|
||||
content: index,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: pathData.strokeColor,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "point_text",
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyTagCompensation(pointText, pointTextPosition)
|
||||
index++
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const segments = (path as paper.Path).segments
|
||||
segments.forEach((segment, index) => {
|
||||
const pointTextPosition: [number, number] = [
|
||||
segment.point.x - 10,
|
||||
segment.point.y + 5,
|
||||
]
|
||||
const pointText = new paper.PointText(pointTextPosition)
|
||||
pointText.set({
|
||||
content: index + 1,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: pathData.strokeColor,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "point_text",
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyTagCompensation(pointText, pointTextPosition)
|
||||
})
|
||||
}
|
||||
}
|
||||
const bounds = path.bounds
|
||||
const pathData = path.data
|
||||
const anchorPoint = getTagAnchorPoint(path)
|
||||
const tagAnchor: [number, number] = [anchorPoint.x, anchorPoint.y]
|
||||
const tagBackgroundColor = getTagBackgroundColor(
|
||||
pathData.fillColor,
|
||||
pathData.strokeColor
|
||||
)
|
||||
const tagTextColor = getContrastTextColor(tagBackgroundColor)
|
||||
let content = ``
|
||||
|
||||
if (configs.includes("ID")) {
|
||||
content += `${id}${
|
||||
path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
|
||||
}`
|
||||
}
|
||||
if (configs.includes("类别")) content += ` ${label_class}`
|
||||
|
||||
if (
|
||||
detail.sub_attributes &&
|
||||
Object.keys(detail.sub_attributes).length
|
||||
) {
|
||||
Object.keys(detail.sub_attributes).forEach((key) => {
|
||||
if (configs.includes("子属性")) content += ` ${key}:`
|
||||
if (configs.includes("属性值"))
|
||||
content += `${detail.sub_attributes[key]};`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const wrappedContent = wrapTagContent(content)
|
||||
if (wrappedContent) {
|
||||
const text = new paper.PointText(anchorPoint)
|
||||
text.set({
|
||||
content: wrappedContent,
|
||||
fontSize: TAG_FONT_SIZE,
|
||||
fontWeight: "bold",
|
||||
fillColor: tagTextColor,
|
||||
justification: "center",
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
text.translate(
|
||||
new paper.Point(
|
||||
anchorPoint.x - text.bounds.center.x,
|
||||
anchorPoint.y - TAG_VERTICAL_GAP - text.bounds.bottom
|
||||
)
|
||||
)
|
||||
|
||||
const textMask = new paper.Path.Rectangle(
|
||||
getTextMaskBounds(text.bounds)
|
||||
)
|
||||
textMask.set({
|
||||
fillColor: tagBackgroundColor.clone(),
|
||||
strokeColor: pathData.strokeColor,
|
||||
strokeWidth: 1,
|
||||
strokeScaling: false,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text_mask",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
text.bringToFront()
|
||||
}
|
||||
|
||||
// 标注对象外接框
|
||||
const mask = new paper.Path.Rectangle(bounds)
|
||||
mask.set({
|
||||
strokeColor: pathData.strokeColor,
|
||||
strokeWidth: 2,
|
||||
strokeScaling: false,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "mask",
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("外框"),
|
||||
})
|
||||
// 标注对象点id
|
||||
if (path instanceof paper.CompoundPath) {
|
||||
let index = 1
|
||||
path.children.forEach((child) => {
|
||||
const segments = (child as paper.Path).segments
|
||||
segments.forEach((segment) => {
|
||||
const pointTextPosition: [number, number] = [
|
||||
segment.point.x - 10,
|
||||
segment.point.y + 5,
|
||||
]
|
||||
const pointText = new paper.PointText(pointTextPosition)
|
||||
pointText.set({
|
||||
content: index,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: pathData.strokeColor,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "point_text",
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyPointTagCompensation(pointText, pointTextPosition)
|
||||
index++
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const segments = (path as paper.Path).segments
|
||||
segments.forEach((segment, index) => {
|
||||
const pointTextPosition: [number, number] = [
|
||||
segment.point.x - 10,
|
||||
segment.point.y + 5,
|
||||
]
|
||||
const pointText = new paper.PointText(pointTextPosition)
|
||||
pointText.set({
|
||||
content: index + 1,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: pathData.strokeColor,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "point_text",
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyPointTagCompensation(pointText, pointTextPosition)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@ import { useObjectStore } from "./store"
|
||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||
import { safeClone } from "./utils/clone"
|
||||
|
||||
export const NODE_SIZE_MIN = 6
|
||||
export const NODE_SIZE_MAX = 24
|
||||
export const DEFAULT_NODE_SIZE = 10
|
||||
|
||||
const clampNodeSize = (value: number) => {
|
||||
const nextValue = Number.isFinite(value)
|
||||
? Math.round(value)
|
||||
: DEFAULT_NODE_SIZE
|
||||
return Math.min(NODE_SIZE_MAX, Math.max(NODE_SIZE_MIN, nextValue))
|
||||
}
|
||||
|
||||
interface TopToolsState {
|
||||
// 标注页面是否仅查看
|
||||
isView: boolean
|
||||
@@ -34,6 +45,8 @@ interface TopToolsState {
|
||||
setAssistToolEnabled: (val: boolean) => void
|
||||
assistToolSize: number
|
||||
setAssistToolSize: (val: number) => void
|
||||
nodeSize: number
|
||||
setNodeSize: (val: number) => void
|
||||
showTags: boolean
|
||||
setShowTags: (val: boolean) => void
|
||||
showTagsConfigs: string[]
|
||||
@@ -94,6 +107,7 @@ const initialTopToolsState = {
|
||||
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
||||
assistToolEnabled: false,
|
||||
assistToolSize: 10,
|
||||
nodeSize: DEFAULT_NODE_SIZE,
|
||||
drawOption: "default" as "default" | "intersect" | "unite",
|
||||
saveCurrentScale: false,
|
||||
scale: 1,
|
||||
@@ -183,6 +197,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
||||
...state,
|
||||
assistToolSize: Math.max(1, val),
|
||||
})),
|
||||
nodeSize: initialTopToolsState.nodeSize,
|
||||
setNodeSize: (val) =>
|
||||
set((state: TopToolsState) => ({
|
||||
...state,
|
||||
nodeSize: clampNodeSize(val),
|
||||
})),
|
||||
showTags: false,
|
||||
setShowTags: (val) =>
|
||||
set((state: TopToolsState) => ({
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
import { Project } from "./api/project/typing"
|
||||
|
||||
const nonEditableInputTypes = new Set([
|
||||
"button",
|
||||
"checkbox",
|
||||
"color",
|
||||
"file",
|
||||
"hidden",
|
||||
"image",
|
||||
"radio",
|
||||
"range",
|
||||
"reset",
|
||||
"submit",
|
||||
])
|
||||
|
||||
const editableRoles = new Set([
|
||||
"textbox",
|
||||
"searchbox",
|
||||
"combobox",
|
||||
"spinbutton",
|
||||
])
|
||||
|
||||
const resolveTargetElement = (target: EventTarget | null) => {
|
||||
if (target instanceof Element) return target
|
||||
if (target instanceof Node) return target.parentElement
|
||||
return null
|
||||
}
|
||||
|
||||
export const isEditableKeyboardTarget = (target: EventTarget | null) => {
|
||||
const element = resolveTargetElement(target)
|
||||
if (!element) return false
|
||||
|
||||
const editableElement = element.closest(
|
||||
[
|
||||
"input",
|
||||
"textarea",
|
||||
"select",
|
||||
"[contenteditable='']",
|
||||
"[contenteditable='true']",
|
||||
"[role='textbox']",
|
||||
"[role='searchbox']",
|
||||
"[role='combobox']",
|
||||
"[role='spinbutton']",
|
||||
].join(",")
|
||||
)
|
||||
|
||||
if (!editableElement) return false
|
||||
|
||||
if (editableElement instanceof HTMLInputElement) {
|
||||
return !nonEditableInputTypes.has(editableElement.type.toLowerCase())
|
||||
}
|
||||
|
||||
if (
|
||||
editableElement instanceof HTMLTextAreaElement ||
|
||||
editableElement instanceof HTMLSelectElement
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (editableElement instanceof HTMLElement) {
|
||||
if (editableElement.isContentEditable) return true
|
||||
const role = editableElement.getAttribute("role")
|
||||
return role ? editableRoles.has(role) : false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 是否存在不符合要求的子属性
|
||||
export const getSubAttrStatus = (
|
||||
operationSchema: Project.LabelSchemaList | null,
|
||||
|
||||
@@ -23,6 +23,17 @@ const currentComponentList: MenuItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const lidarList: MenuItem[] =
|
||||
process.env.NEXT_PUBLIC_ENV === "production"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: "点云标注",
|
||||
url: "lidar",
|
||||
icon: "LidarAnnotationIcon",
|
||||
},
|
||||
]
|
||||
|
||||
export const componentList: MenuItem[] = [
|
||||
{
|
||||
title: "个人中心",
|
||||
@@ -35,11 +46,7 @@ export const componentList: MenuItem[] = [
|
||||
url: "video",
|
||||
icon: "VideoAnnotationIcon",
|
||||
},
|
||||
{
|
||||
title: "点云标注",
|
||||
url: "lidar",
|
||||
icon: "LidarAnnotationIcon",
|
||||
},
|
||||
...lidarList,
|
||||
{
|
||||
title: "管理中心",
|
||||
url: "mgt",
|
||||
|
||||
Reference in New Issue
Block a user