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 { taskDispatch } from "@/components/label/api/task"
|
||||||
import { Task } from "@/components/label/api/task/typing"
|
import { Task } from "@/components/label/api/task/typing"
|
||||||
import { usePermissionStore } from "@/components/label/store/auth"
|
import { usePermissionStore } from "@/components/label/store/auth"
|
||||||
import {
|
import { Badge, Button, Group, Modal, Select, Stack, Text } from "@mantine/core"
|
||||||
Button,
|
|
||||||
Group,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
Text,
|
|
||||||
} from "@mantine/core"
|
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
@@ -30,7 +22,6 @@ export default function DispatchModal(props: {
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
reject: false,
|
|
||||||
task_status_dst: "",
|
task_status_dst: "",
|
||||||
uid_dst: "",
|
uid_dst: "",
|
||||||
},
|
},
|
||||||
@@ -41,7 +32,6 @@ export default function DispatchModal(props: {
|
|||||||
|
|
||||||
const resetFormState = () => {
|
const resetFormState = () => {
|
||||||
form.setValues({
|
form.setValues({
|
||||||
reject: false,
|
|
||||||
task_status_dst: "",
|
task_status_dst: "",
|
||||||
uid_dst: "",
|
uid_dst: "",
|
||||||
})
|
})
|
||||||
@@ -61,6 +51,7 @@ export default function DispatchModal(props: {
|
|||||||
|
|
||||||
const taskStatusSrc = dispatchData?.status ?? 0
|
const taskStatusSrc = dispatchData?.status ?? 0
|
||||||
const taskStatusSrcLabel = dispatchOpts[taskStatusSrc]?.label ?? "-"
|
const taskStatusSrcLabel = dispatchOpts[taskStatusSrc]?.label ?? "-"
|
||||||
|
const taskRejected = Boolean(dispatchData?.reject)
|
||||||
const dstOptions =
|
const dstOptions =
|
||||||
dispatchOpts[taskStatusSrc]?.opts?.map((o) => ({
|
dispatchOpts[taskStatusSrc]?.opts?.map((o) => ({
|
||||||
label: o.label,
|
label: o.label,
|
||||||
@@ -116,7 +107,7 @@ export default function DispatchModal(props: {
|
|||||||
try {
|
try {
|
||||||
await taskDispatch({
|
await taskDispatch({
|
||||||
operation_uid: opUid,
|
operation_uid: opUid,
|
||||||
reject: values.reject,
|
reject: taskRejected,
|
||||||
task_ids: task_ids.map((item) => item.id),
|
task_ids: task_ids.map((item) => item.id),
|
||||||
task_status_src: taskStatusSrc,
|
task_status_src: taskStatusSrc,
|
||||||
task_status_dst: taskStatusDst,
|
task_status_dst: taskStatusDst,
|
||||||
@@ -143,9 +134,16 @@ export default function DispatchModal(props: {
|
|||||||
当前任务:
|
当前任务:
|
||||||
{task_ids.map((item) => item.project_name).join("、") ?? "-"}
|
{task_ids.map((item) => item.project_name).join("、") ?? "-"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Group gap={6}>
|
||||||
(源状态:{taskStatusSrcLabel})
|
<Text size="sm" c="dimmed">
|
||||||
</Text>
|
(源状态:{taskStatusSrcLabel})
|
||||||
|
</Text>
|
||||||
|
{taskRejected ? (
|
||||||
|
<Badge variant="outline" color="yellow" size="sm">
|
||||||
|
返工
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="目标状态"
|
label="目标状态"
|
||||||
@@ -168,14 +166,6 @@ export default function DispatchModal(props: {
|
|||||||
onChange={(v) => form.setFieldValue("uid_dst", v ?? "")}
|
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">
|
<Group justify="flex-end" gap="sm">
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import {
|
|||||||
useTimerStore,
|
useTimerStore,
|
||||||
} from "./useTimerStore"
|
} from "./useTimerStore"
|
||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { findGroupKey } from "./util"
|
import { findGroupKey, isEditableKeyboardTarget } from "./util"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
import { labelTypeMap } from "./utils/constants"
|
import { labelTypeMap } from "./utils/constants"
|
||||||
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"
|
||||||
@@ -83,6 +83,11 @@ let latestTaskDetailRequestSeq = 0
|
|||||||
const RIGHT_PANEL_TRANSITION =
|
const RIGHT_PANEL_TRANSITION =
|
||||||
"width 160ms ease, min-width 160ms ease, opacity 160ms ease"
|
"width 160ms ease, min-width 160ms ease, opacity 160ms ease"
|
||||||
const RIGHT_PANEL_WILL_CHANGE = "width, min-width, opacity"
|
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
|
// utils
|
||||||
const getRectPoints = ([sx, sy, w, h]: number[]) => {
|
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 videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i
|
||||||
const GLOBAL_LOADING_Z_INDEX = 900
|
const GLOBAL_LOADING_Z_INDEX = 900
|
||||||
const videoMimeByExt: Record<string, string> = {
|
const videoMimeByExt: Record<string, string> = {
|
||||||
@@ -233,7 +283,7 @@ const LabelPage = ({
|
|||||||
const qaToolContainerRef = useRef<any>(null)
|
const qaToolContainerRef = useRef<any>(null)
|
||||||
const [isConfirmSave, setIsConfirmSave] = useState(false)
|
const [isConfirmSave, setIsConfirmSave] = useState(false)
|
||||||
const [loading, setLoading] = 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 [videoFetchPendingCount, setVideoFetchPendingCount] = useState(0)
|
||||||
const [hasReadyVideoFrame, setHasReadyVideoFrame] = useState(false)
|
const [hasReadyVideoFrame, setHasReadyVideoFrame] = useState(false)
|
||||||
const [sourceVideoNames, setSourceVideoNames] = useState<string[]>([])
|
const [sourceVideoNames, setSourceVideoNames] = useState<string[]>([])
|
||||||
@@ -2114,7 +2164,7 @@ const LabelPage = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
// console.log(e, e.key);
|
// console.log(e, e.key);
|
||||||
if (focusInput) return
|
if (focusInput || isEditableKeyboardTarget(e.target)) return
|
||||||
|
|
||||||
console.log(e.key)
|
console.log(e.key)
|
||||||
const mode = usePaperStore.getState().mode
|
const mode = usePaperStore.getState().mode
|
||||||
@@ -2479,51 +2529,35 @@ const LabelPage = ({
|
|||||||
|
|
||||||
// 更新Splitter状态
|
// 更新Splitter状态
|
||||||
const updateSplitterSizes = useCallback(() => {
|
const updateSplitterSizes = useCallback(() => {
|
||||||
console.log(
|
const availableWidth = Math.max(
|
||||||
"document.documentElement.clientWidth",
|
document.documentElement.clientWidth - leftWidth - 4,
|
||||||
document.documentElement.clientWidth
|
0
|
||||||
)
|
)
|
||||||
|
const flags: boolean[] = [
|
||||||
const len = document.documentElement.clientWidth - 4
|
|
||||||
let arr = [len, 0, 0, 0, 0, 0]
|
|
||||||
const flags = [
|
|
||||||
showTaskList,
|
showTaskList,
|
||||||
showGroupList,
|
showGroupList,
|
||||||
showDescList && projectDetail && projectDetail.label_type === 5,
|
(showDescList && projectDetail && projectDetail.label_type === 5) ||
|
||||||
showDescList && projectDetail && projectDetail.label_type === 6,
|
false,
|
||||||
|
(showDescList && projectDetail && projectDetail.label_type === 6) ||
|
||||||
|
false,
|
||||||
showObjectList,
|
showObjectList,
|
||||||
]
|
]
|
||||||
let count = 0
|
setSizes(allocateRightPanelSizes(availableWidth, flags))
|
||||||
flags.forEach((flag) => {
|
}, [
|
||||||
if (flag) count++
|
leftWidth,
|
||||||
})
|
projectDetail,
|
||||||
if (count === 1 || count === 2) {
|
showDescList,
|
||||||
arr[0] = count === 1 ? len * 0.8 : len * 0.6
|
showGroupList,
|
||||||
flags.forEach((flag, index) => {
|
showObjectList,
|
||||||
if (flag) {
|
showTaskList,
|
||||||
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])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateSplitterSizes()
|
updateSplitterSizes()
|
||||||
|
window.addEventListener("resize", updateSplitterSizes)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", updateSplitterSizes)
|
||||||
|
}
|
||||||
}, [updateSplitterSizes])
|
}, [updateSplitterSizes])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2702,7 +2736,7 @@ const LabelPage = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Flex w="calc(100% - 4px)" h={mainBoxHeight}>
|
<Flex w="calc(100% - 4px)" h={mainBoxHeight}>
|
||||||
{/* <Flex h="100%" w={"100%"}> */}
|
{/* <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">
|
<Box h="100%" id="resize-container">
|
||||||
<Flex h="100%" w="100%">
|
<Flex h="100%" w="100%">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -2780,7 +2814,7 @@ const LabelPage = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
w={sizes[1]}
|
w={sizes[1]}
|
||||||
miw={showTaskList ? 300 : 0}
|
miw={showTaskList ? sizes[1] : 0}
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -2788,10 +2822,11 @@ const LabelPage = ({
|
|||||||
opacity: showTaskList ? 1 : 0,
|
opacity: showTaskList ? 1 : 0,
|
||||||
visibility: showTaskList ? "visible" : "hidden",
|
visibility: showTaskList ? "visible" : "hidden",
|
||||||
pointerEvents: showTaskList ? "auto" : "none",
|
pointerEvents: showTaskList ? "auto" : "none",
|
||||||
|
borderLeft: showTaskList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||||
transition: RIGHT_PANEL_TRANSITION,
|
transition: RIGHT_PANEL_TRANSITION,
|
||||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||||
}}>
|
}}>
|
||||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[1], 300))}>
|
<Box style={getRightPanelViewportStyle(sizes[1])}>
|
||||||
<RightTaskTools
|
<RightTaskTools
|
||||||
projectDetail={projectDetail}
|
projectDetail={projectDetail}
|
||||||
project_id={projectId}
|
project_id={projectId}
|
||||||
@@ -2801,7 +2836,7 @@ const LabelPage = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
w={sizes[2]}
|
w={sizes[2]}
|
||||||
miw={showGroupList ? 200 : 0}
|
miw={showGroupList ? sizes[2] : 0}
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -2809,10 +2844,11 @@ const LabelPage = ({
|
|||||||
opacity: showGroupList ? 1 : 0,
|
opacity: showGroupList ? 1 : 0,
|
||||||
visibility: showGroupList ? "visible" : "hidden",
|
visibility: showGroupList ? "visible" : "hidden",
|
||||||
pointerEvents: showGroupList ? "auto" : "none",
|
pointerEvents: showGroupList ? "auto" : "none",
|
||||||
|
borderLeft: showGroupList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||||
transition: RIGHT_PANEL_TRANSITION,
|
transition: RIGHT_PANEL_TRANSITION,
|
||||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||||
}}>
|
}}>
|
||||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[2], 200))}>
|
<Box style={getRightPanelViewportStyle(sizes[2])}>
|
||||||
<RightGroupTools
|
<RightGroupTools
|
||||||
labelState={labelState}
|
labelState={labelState}
|
||||||
projectDetail={projectDetail}
|
projectDetail={projectDetail}
|
||||||
@@ -2823,7 +2859,7 @@ const LabelPage = ({
|
|||||||
w={sizes[3]}
|
w={sizes[3]}
|
||||||
miw={
|
miw={
|
||||||
showDescList && projectDetail && projectDetail.label_type === 5
|
showDescList && projectDetail && projectDetail.label_type === 5
|
||||||
? 350
|
? sizes[3]
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
@@ -2842,10 +2878,14 @@ const LabelPage = ({
|
|||||||
showDescList && projectDetail && projectDetail.label_type === 5
|
showDescList && projectDetail && projectDetail.label_type === 5
|
||||||
? "auto"
|
? "auto"
|
||||||
: "none",
|
: "none",
|
||||||
|
borderLeft:
|
||||||
|
showDescList && projectDetail && projectDetail.label_type === 5
|
||||||
|
? RIGHT_PANEL_SEPARATOR
|
||||||
|
: "none",
|
||||||
transition: RIGHT_PANEL_TRANSITION,
|
transition: RIGHT_PANEL_TRANSITION,
|
||||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||||
}}>
|
}}>
|
||||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[3], 350))}>
|
<Box style={getRightPanelViewportStyle(sizes[3])}>
|
||||||
<RightDescTools taskDetail={taskDetail} />
|
<RightDescTools taskDetail={taskDetail} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -2853,7 +2893,7 @@ const LabelPage = ({
|
|||||||
w={sizes[4]}
|
w={sizes[4]}
|
||||||
miw={
|
miw={
|
||||||
showDescList && projectDetail && projectDetail.label_type === 6
|
showDescList && projectDetail && projectDetail.label_type === 6
|
||||||
? 350
|
? sizes[4]
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
@@ -2872,16 +2912,20 @@ const LabelPage = ({
|
|||||||
showDescList && projectDetail && projectDetail.label_type === 6
|
showDescList && projectDetail && projectDetail.label_type === 6
|
||||||
? "auto"
|
? "auto"
|
||||||
: "none",
|
: "none",
|
||||||
|
borderLeft:
|
||||||
|
showDescList && projectDetail && projectDetail.label_type === 6
|
||||||
|
? RIGHT_PANEL_SEPARATOR
|
||||||
|
: "none",
|
||||||
transition: RIGHT_PANEL_TRANSITION,
|
transition: RIGHT_PANEL_TRANSITION,
|
||||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||||
}}>
|
}}>
|
||||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[4], 350))}>
|
<Box style={getRightPanelViewportStyle(sizes[4])}>
|
||||||
<RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
|
<RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
w={sizes[5]}
|
w={sizes[5]}
|
||||||
miw={showObjectList ? 350 : 0}
|
miw={showObjectList ? sizes[5] : 0}
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -2889,10 +2933,11 @@ const LabelPage = ({
|
|||||||
opacity: showObjectList ? 1 : 0,
|
opacity: showObjectList ? 1 : 0,
|
||||||
visibility: showObjectList ? "visible" : "hidden",
|
visibility: showObjectList ? "visible" : "hidden",
|
||||||
pointerEvents: showObjectList ? "auto" : "none",
|
pointerEvents: showObjectList ? "auto" : "none",
|
||||||
|
borderLeft: showObjectList ? RIGHT_PANEL_SEPARATOR : "none",
|
||||||
transition: RIGHT_PANEL_TRANSITION,
|
transition: RIGHT_PANEL_TRANSITION,
|
||||||
willChange: RIGHT_PANEL_WILL_CHANGE,
|
willChange: RIGHT_PANEL_WILL_CHANGE,
|
||||||
}}>
|
}}>
|
||||||
<Box style={getRightPanelViewportStyle(Math.max(sizes[5], 350))}>
|
<Box style={getRightPanelViewportStyle(sizes[5])}>
|
||||||
<RightObjectTools
|
<RightObjectTools
|
||||||
taskDetail={taskDetail}
|
taskDetail={taskDetail}
|
||||||
labelState={labelState}
|
labelState={labelState}
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ const PaperContainer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!useTopToolsStore.getState().saveCurrentScale &&
|
||||||
RESIZE_INTERPOLATION_DURATION_MS > 0 &&
|
RESIZE_INTERPOLATION_DURATION_MS > 0 &&
|
||||||
firstRender &&
|
firstRender &&
|
||||||
lastSize?.clientWidth &&
|
lastSize?.clientWidth &&
|
||||||
@@ -1022,16 +1023,12 @@ const PaperContainer = (
|
|||||||
parent: usePaperStore.getState().group,
|
parent: usePaperStore.getState().group,
|
||||||
strokeScaling: false,
|
strokeScaling: false,
|
||||||
})
|
})
|
||||||
if (Math.abs(path.area) < useTopToolsStore.getState().checkSize) {
|
if (hollows[index]) {
|
||||||
path.remove()
|
path.data.isHollowPolygon = true
|
||||||
|
innerPaths.push(path)
|
||||||
} else {
|
} else {
|
||||||
if (hollows[index]) {
|
path.fillColor = new paper.Color(fillColor)
|
||||||
path.data.isHollowPolygon = true
|
outerPaths.push(path)
|
||||||
innerPaths.push(path)
|
|
||||||
} else {
|
|
||||||
path.fillColor = new paper.Color(fillColor)
|
|
||||||
outerPaths.push(path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -3136,6 +3133,8 @@ const PaperContainer = (
|
|||||||
const activeImageChanged = prevActiveImageRef.current !== activeImage
|
const activeImageChanged = prevActiveImageRef.current !== activeImage
|
||||||
prevActiveImageRef.current = activeImage
|
prevActiveImageRef.current = activeImage
|
||||||
const hasPicRaster = getPicRasters(currentGroup).length > 0
|
const hasPicRaster = getPicRasters(currentGroup).length > 0
|
||||||
|
const preserveViewportOnLayoutResize =
|
||||||
|
useTopToolsStore.getState().saveCurrentScale
|
||||||
|
|
||||||
if (!useTopToolsStore.getState().saveCurrentScale) {
|
if (!useTopToolsStore.getState().saveCurrentScale) {
|
||||||
currentGroup.scaling = new paper.Point(1, 1)
|
currentGroup.scaling = new paper.Point(1, 1)
|
||||||
@@ -3147,6 +3146,12 @@ const PaperContainer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!activeImageChanged) {
|
if (!activeImageChanged) {
|
||||||
|
if (
|
||||||
|
preserveViewportOnLayoutResize &&
|
||||||
|
(hasPicRaster || rasterLoadingImageRef.current === activeImage)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (resizeCurrentImage()) {
|
if (resizeCurrentImage()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Spline,
|
Spline,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
import { Project } from "../api/project/typing"
|
import { Project } from "../api/project/typing"
|
||||||
import { Task } from "../api/task/typing"
|
import { Task } from "../api/task/typing"
|
||||||
import { LabelState } from "../LabelNossr"
|
import { LabelState } from "../LabelNossr"
|
||||||
@@ -151,6 +151,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
continuePolygonTarget?.imageId === activeImage
|
continuePolygonTarget?.imageId === activeImage
|
||||||
? continuePolygonTarget.objectId
|
? continuePolygonTarget.objectId
|
||||||
: null
|
: null
|
||||||
|
const objectOperations = useTopToolsStore((state) => state.objectOperations)
|
||||||
|
|
||||||
const getOperationChildren = useCallback(
|
const getOperationChildren = useCallback(
|
||||||
(imageId: string, operationId: string) => {
|
(imageId: string, operationId: string) => {
|
||||||
@@ -415,9 +416,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
[activeImage]
|
[activeImage]
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeImageOperations = (labelState.get(activeImage) || []).filter(
|
const activeImageOperations = useMemo(() => {
|
||||||
(operationId) => getOperationChildren(activeImage, operationId).length
|
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) => {
|
const isObjectHidden = (imageId: string, objectId: number) => {
|
||||||
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
|
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
|
||||||
@@ -558,9 +563,17 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Box h="100%" w="100%">
|
<Box h="100%" w="100%">
|
||||||
<Flex direction="column" h="100%" gap="xs">
|
<Flex direction="column" h="100%" gap="xs">
|
||||||
<Flex justify="space-between" align="center" px="md" py="xs">
|
<Flex
|
||||||
<Group gap="xs">
|
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
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
|
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -571,20 +584,23 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}}>
|
}}>
|
||||||
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
|
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Title order={4} size="h4">
|
<Title
|
||||||
|
order={5}
|
||||||
|
size="h5"
|
||||||
|
style={{ whiteSpace: "nowrap", lineHeight: 1.1 }}>
|
||||||
对象列表
|
对象列表
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap={6} wrap="nowrap" style={{ minWidth: 0, flexShrink: 0 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
w={96}
|
w={72}
|
||||||
size="xs"
|
size="xs"
|
||||||
placeholder="输入ID"
|
placeholder="输入ID"
|
||||||
value={searchId}
|
value={searchId}
|
||||||
onChange={(e) => setSearchId(e.target.value)}
|
onChange={(e) => setSearchId(e.target.value)}
|
||||||
onKeyDown={handleSelectedBySearchId}
|
onKeyDown={handleSelectedBySearchId}
|
||||||
/>
|
/>
|
||||||
<Group gap={4}>
|
<Group gap={2} wrap="nowrap">
|
||||||
{[3, 2, 1, 0].map((type, index) => (
|
{[3, 2, 1, 0].map((type, index) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
key={type}
|
key={type}
|
||||||
@@ -603,6 +619,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateImageStatus(
|
updateImageStatus(
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ import {
|
|||||||
MousePointer,
|
MousePointer,
|
||||||
Paintbrush,
|
Paintbrush,
|
||||||
Pen,
|
Pen,
|
||||||
SquarePen,
|
|
||||||
SquareDashedMousePointer,
|
SquareDashedMousePointer,
|
||||||
|
SquarePen,
|
||||||
Tag,
|
Tag,
|
||||||
Timer,
|
Timer,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -92,7 +92,12 @@ import { useKeyboardStore } from "../useKeyBoardStore"
|
|||||||
import { usePaperStore } from "../usePaperStore"
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useRightToolsStore } from "../useRightToolsStore"
|
import { useRightToolsStore } from "../useRightToolsStore"
|
||||||
import { useIntervalStore, useLabelTimeStore } from "../useTimerStore"
|
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 { getSubAttrStatus } from "../util"
|
||||||
import { safeClone } from "../utils/clone"
|
import { safeClone } from "../utils/clone"
|
||||||
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
||||||
@@ -222,13 +227,14 @@ const TopTools = (
|
|||||||
setAssistToolEnabled,
|
setAssistToolEnabled,
|
||||||
assistToolSize,
|
assistToolSize,
|
||||||
setAssistToolSize,
|
setAssistToolSize,
|
||||||
|
nodeSize,
|
||||||
|
setNodeSize,
|
||||||
needBackup,
|
needBackup,
|
||||||
setNeedBackup,
|
setNeedBackup,
|
||||||
checkSize,
|
|
||||||
setCheckSize,
|
|
||||||
magnetFlag,
|
magnetFlag,
|
||||||
setMagnetFlag,
|
setMagnetFlag,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
|
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
||||||
|
|
||||||
const { pathGroupMap } = useRightToolsStore()
|
const { pathGroupMap } = useRightToolsStore()
|
||||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||||
@@ -247,19 +253,25 @@ const TopTools = (
|
|||||||
].filter(Boolean).length
|
].filter(Boolean).length
|
||||||
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
|
}, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNodeSizeInput(nodeSize)
|
||||||
|
usePaperStore.getState().refreshNodeVisualSize()
|
||||||
|
}, [nodeSize])
|
||||||
|
|
||||||
const toggleRightPanel = useCallback(
|
const toggleRightPanel = useCallback(
|
||||||
(visible: boolean, setter: (val: boolean) => void) => {
|
(visible: boolean, setter: (val: boolean) => void) => {
|
||||||
const nextVisible = !visible
|
const nextVisible = !visible
|
||||||
const nextCount = visibleRightPanelCount + (nextVisible ? 1 : -1)
|
const nextCount = visibleRightPanelCount + (nextVisible ? 1 : -1)
|
||||||
if (
|
if (
|
||||||
(visibleRightPanelCount === 0 && nextCount === 1) ||
|
!saveCurrentScale &&
|
||||||
(visibleRightPanelCount === 1 && nextCount === 0)
|
((visibleRightPanelCount === 0 && nextCount === 1) ||
|
||||||
|
(visibleRightPanelCount === 1 && nextCount === 0))
|
||||||
) {
|
) {
|
||||||
requestResizeGhost()
|
requestResizeGhost()
|
||||||
}
|
}
|
||||||
setter(nextVisible)
|
setter(nextVisible)
|
||||||
},
|
},
|
||||||
[requestResizeGhost, visibleRightPanelCount]
|
[requestResizeGhost, saveCurrentScale, visibleRightPanelCount]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当前是否可以操作功能按键
|
// 当前是否可以操作功能按键
|
||||||
@@ -1107,6 +1119,7 @@ const TopTools = (
|
|||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
await handleSave()
|
await handleSave()
|
||||||
|
notifications.hide("save")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
return
|
return
|
||||||
@@ -2188,23 +2201,39 @@ const TopTools = (
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
||||||
<NumberInput
|
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
||||||
value={checkSize}
|
<Flex align="center" gap={4}>
|
||||||
w={50}
|
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||||
min={1}
|
节点
|
||||||
size="xs"
|
</Text>
|
||||||
radius="sm"
|
<NumberInput
|
||||||
hideControls
|
value={nodeSizeInput}
|
||||||
onChange={(v) => {
|
w={50}
|
||||||
if (v) setCheckSize(Number(v))
|
min={NODE_SIZE_MIN}
|
||||||
}}
|
max={NODE_SIZE_MAX}
|
||||||
onFocus={() => {
|
step={1}
|
||||||
useKeyEventStore.getState().setFocusInput(true)
|
allowDecimal={false}
|
||||||
}}
|
size="xs"
|
||||||
onBlur={() => {
|
radius="sm"
|
||||||
useKeyEventStore.getState().setFocusInput(false)
|
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}
|
{renderEditIcon}
|
||||||
<Button
|
<Button
|
||||||
w="fit-content"
|
w="fit-content"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useKeyboardStore } from "./useKeyBoardStore"
|
|||||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||||
import { usePaperSupportStore } from "./usePaperSupportStore"
|
import { usePaperSupportStore } from "./usePaperSupportStore"
|
||||||
import { useRightToolsStore } from "./useRightToolsStore"
|
import { useRightToolsStore } from "./useRightToolsStore"
|
||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore"
|
||||||
import { safeClone } from "./utils/clone"
|
import { safeClone } from "./utils/clone"
|
||||||
|
|
||||||
interface ContinuePolygonTarget {
|
interface ContinuePolygonTarget {
|
||||||
@@ -183,6 +183,7 @@ interface PaperState {
|
|||||||
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
|
setContinuePolygonTarget: (target: ContinuePolygonTarget | null) => void
|
||||||
beginContinuePolygonDraft: () => boolean
|
beginContinuePolygonDraft: () => boolean
|
||||||
setLoadingData: (flag: boolean) => void
|
setLoadingData: (flag: boolean) => void
|
||||||
|
refreshNodeVisualSize: () => void
|
||||||
resizeGhostRequestToken: number
|
resizeGhostRequestToken: number
|
||||||
requestResizeGhost: () => void
|
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) => {
|
const handleShift = (flag: boolean) => {
|
||||||
useKeyEventStore.getState().setShift(flag)
|
useKeyEventStore.getState().setShift(flag)
|
||||||
}
|
}
|
||||||
@@ -328,6 +362,7 @@ const PAPER_CONTEXT_HIT_TYPES = [
|
|||||||
"pathBuff",
|
"pathBuff",
|
||||||
"pathCircle",
|
"pathCircle",
|
||||||
] as const
|
] as const
|
||||||
|
const LEGACY_PAPER_NODE_DIAMETER = 6
|
||||||
|
|
||||||
const isPaperObjectType = (type: unknown) =>
|
const isPaperObjectType = (type: unknown) =>
|
||||||
typeof type === "string" &&
|
typeof type === "string" &&
|
||||||
@@ -339,6 +374,39 @@ const isPaperContextHitType = (type: unknown) =>
|
|||||||
type as (typeof PAPER_CONTEXT_HIT_TYPES)[number]
|
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 = (
|
const getPaperCursorByMode = (
|
||||||
mode: PaperState["mode"] = usePaperStore.getState().mode
|
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||||||
) => {
|
) => {
|
||||||
@@ -462,6 +530,124 @@ const getPaperSelectionPath = (
|
|||||||
return nearestPath
|
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 trySelectPaperObjectByPoint = (clickPoint: paper.Point) => {
|
||||||
const group = usePaperStore.getState().group
|
const group = usePaperStore.getState().group
|
||||||
if (!group) return false
|
if (!group) return false
|
||||||
@@ -898,6 +1084,19 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
...state,
|
...state,
|
||||||
loadingData: flag,
|
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: () =>
|
requestResizeGhost: () =>
|
||||||
set((state: PaperState) => ({
|
set((state: PaperState) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -1090,8 +1289,6 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
? baseTolerance / useTopToolsStore.getState().scale
|
? baseTolerance / useTopToolsStore.getState().scale
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
false && getPanHitTolerance
|
|
||||||
|
|
||||||
const createPanHitOptions = (
|
const createPanHitOptions = (
|
||||||
tolerance: number,
|
tolerance: number,
|
||||||
match: (hit: paper.HitResult) => boolean
|
match: (hit: paper.HitResult) => boolean
|
||||||
@@ -1115,7 +1312,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (hoveredCircleInfo) {
|
if (hoveredCircleInfo) {
|
||||||
const pointHit = group.hitTest(
|
const pointHit = group.hitTest(
|
||||||
projectPoint,
|
projectPoint,
|
||||||
createPanHitOptions(0, (hit) => {
|
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||||||
return (
|
return (
|
||||||
hit.item.data?.type === "pathCircle" &&
|
hit.item.data?.type === "pathCircle" &&
|
||||||
hit.item.data?.id === hoveredCircleInfo.id &&
|
hit.item.data?.id === hoveredCircleInfo.id &&
|
||||||
@@ -1132,7 +1329,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (hoveredBufferInfo) {
|
if (hoveredBufferInfo) {
|
||||||
const edgeHit = group.hitTest(
|
const edgeHit = group.hitTest(
|
||||||
projectPoint,
|
projectPoint,
|
||||||
createPanHitOptions(0, (hit) => {
|
createPanHitOptions(getPanHitTolerance(), (hit) => {
|
||||||
return (
|
return (
|
||||||
hit.item.data?.type === "pathBuff" &&
|
hit.item.data?.type === "pathBuff" &&
|
||||||
hit.item.data?.id === hoveredBufferInfo.id &&
|
hit.item.data?.id === hoveredBufferInfo.id &&
|
||||||
@@ -1145,8 +1342,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
|
|
||||||
return group.hitTest(
|
return group.hitTest(
|
||||||
projectPoint,
|
projectPoint,
|
||||||
createPanHitOptions(0, (hit) =>
|
createPanHitOptions(getPanHitTolerance(), (hit) =>
|
||||||
["rectangle", "polygon", "brush", "point"].includes(
|
["rectangle", "polygon", "brush", "point", "circle"].includes(
|
||||||
hit.item.data?.type || ""
|
hit.item.data?.type || ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1197,6 +1394,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
hasExceededPanDragThreshold = false
|
hasExceededPanDragThreshold = false
|
||||||
|
|
||||||
let hitResult = resolvePanHitResult(e.point)
|
let hitResult = resolvePanHitResult(e.point)
|
||||||
|
hitResult = normalizePointPanHitResult(hitResult, point)
|
||||||
console.log("hitResult", hitResult)
|
console.log("hitResult", hitResult)
|
||||||
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||||||
? hoveredBufferPathInfo
|
? hoveredBufferPathInfo
|
||||||
@@ -1475,27 +1673,26 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
activePath = hitResult.item as paper.Path
|
activePath = hitResult.item as paper.Path
|
||||||
}
|
}
|
||||||
} else if (hitResult.item.data.type === "point") {
|
} else if (hitResult.item.data.type === "point") {
|
||||||
// 只有点击圆才视为选中
|
if (
|
||||||
if (hitResult.type === "segment") {
|
["segment", "stroke"].includes(hitResult.type) &&
|
||||||
// Path 类型
|
hitResult.item instanceof paper.Path
|
||||||
if (hitResult.item instanceof paper.Path) {
|
) {
|
||||||
if (!hitResult.item.data.selected) {
|
if (!hitResult.item.data.selected) {
|
||||||
hitResult.item.data.selected = true
|
hitResult.item.data.selected = true
|
||||||
// 绘制遮罩
|
// 绘制遮罩
|
||||||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||||||
// 绘制点对应的数字
|
// 绘制点对应的数字
|
||||||
usePaperSupportStore
|
usePaperSupportStore
|
||||||
.getState()
|
.getState()
|
||||||
.handleText(hitResult.item as paper.Path)
|
.handleText(hitResult.item as paper.Path)
|
||||||
// 绘制选中边
|
// 绘制选中边
|
||||||
usePaperSupportStore
|
usePaperSupportStore
|
||||||
.getState()
|
.getState()
|
||||||
.handleBufferPaths(hitResult.item)
|
.handleBufferPaths(hitResult.item)
|
||||||
// 绘制选中圆
|
// 绘制选中圆
|
||||||
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
usePaperSupportStore.getState().handleCircles(hitResult.item)
|
||||||
}
|
|
||||||
activePath = hitResult.item as paper.Path
|
|
||||||
}
|
}
|
||||||
|
activePath = hitResult.item as paper.Path
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
hitResult.item.data.type === "pathBuff" ||
|
hitResult.item.data.type === "pathBuff" ||
|
||||||
@@ -1598,21 +1795,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
if (currentPath.data.type === "point") {
|
if (currentPath.data.type === "point") {
|
||||||
usePaperSupportStore.getState().handleText(currentPath)
|
usePaperSupportStore.getState().handleText(currentPath)
|
||||||
let circles = usePaperStore.getState().group!.getItems({
|
syncPointCirclesWithPath(currentPath)
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||||||
usePaperSupportStore.getState().handleCircles(currentPath)
|
usePaperSupportStore.getState().handleCircles(currentPath)
|
||||||
}
|
}
|
||||||
@@ -1677,15 +1860,9 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
activePath.insert(hitIndex + 1, hitResult.location.point)
|
activePath.insert(hitIndex + 1, hitResult.location.point)
|
||||||
activePath?.data?.type === "point" &&
|
if (activePath?.data?.type === "point") {
|
||||||
usePaperStore
|
syncPointCirclesWithPath(activePath)
|
||||||
.getState()
|
}
|
||||||
.getPointCircle(
|
|
||||||
hitResult.location.point,
|
|
||||||
hitIndex + 1,
|
|
||||||
activePath.data.id,
|
|
||||||
activePath.data.blankColor
|
|
||||||
)
|
|
||||||
activePath.bringToFront()
|
activePath.bringToFront()
|
||||||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||||||
usePaperSupportStore.getState().handleCircles(activePath)
|
usePaperSupportStore.getState().handleCircles(activePath)
|
||||||
@@ -1699,21 +1876,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (activePath?.data?.type === "point") {
|
if (activePath?.data?.type === "point") {
|
||||||
// 绘制点对应的数字
|
// 绘制点对应的数字
|
||||||
usePaperSupportStore.getState().handleText(activePath)
|
usePaperSupportStore.getState().handleText(activePath)
|
||||||
let circles = usePaperStore.getState().group!.getItems({
|
syncPointCirclesWithPath(activePath)
|
||||||
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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||||||
usePaperSupportStore.getState().handleCircles(activePath)
|
usePaperSupportStore.getState().handleCircles(activePath)
|
||||||
}
|
}
|
||||||
@@ -1807,39 +1970,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
usePaperSupportStore.getState().handleMask(hitResult.item)
|
usePaperSupportStore.getState().handleMask(hitResult.item)
|
||||||
}
|
}
|
||||||
if (activePath?.data?.type === "point") {
|
if (activePath?.data?.type === "point") {
|
||||||
let paths = usePaperStore.getState().group!.getItems({
|
syncPointCirclesWithPath(activePath)
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// paths.forEach((child) => {
|
// paths.forEach((child) => {
|
||||||
// if (
|
// if (
|
||||||
@@ -2083,27 +2214,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
]!.position.y = point!.y
|
]!.position.y = point!.y
|
||||||
usePaperSupportStore.getState().handleMask(path)
|
usePaperSupportStore.getState().handleMask(path)
|
||||||
} else if (path.data.type === "point") {
|
} else if (path.data.type === "point") {
|
||||||
let circles = usePaperStore.getState().group!.getItems({
|
const circles = syncPointCirclesWithPath(path)
|
||||||
data: {
|
|
||||||
id: path.data.id,
|
|
||||||
type: "circle",
|
|
||||||
},
|
|
||||||
}) as paper.Path[]
|
|
||||||
|
|
||||||
// let index = path.segments.length - hitIndex - 1
|
if (circles[hitIndex]) {
|
||||||
// let index = hitIndex;
|
circles[hitIndex].position.x = point!.x
|
||||||
|
circles[hitIndex].position.y = point!.y
|
||||||
// 移动点
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
// 移动路径
|
// 移动路径
|
||||||
path.segments[hitIndex].point.x = point!.x
|
path.segments[hitIndex].point.x = point!.x
|
||||||
@@ -2315,45 +2430,12 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePointIntersectRaster = (path: paper.Path) => {
|
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) => {
|
path.segments = path.segments.filter((segment) => {
|
||||||
return usePaperStore
|
return usePaperStore
|
||||||
.getState()
|
.getState()
|
||||||
.rasterPath!.bounds.contains(segment.point)
|
.rasterPath!.bounds.contains(segment.point)
|
||||||
})
|
})
|
||||||
|
syncPointCirclesWithPath(path)
|
||||||
// 去除超出图片边界的部分 end
|
// 去除超出图片边界的部分 end
|
||||||
usePaperSupportStore.getState().handleMask(path)
|
usePaperSupportStore.getState().handleMask(path)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(path)
|
usePaperSupportStore.getState().handleBufferPaths(path)
|
||||||
@@ -2507,6 +2589,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
rectPath.data.operationId,
|
rectPath.data.operationId,
|
||||||
rectPath.data.id
|
rectPath.data.id
|
||||||
)
|
)
|
||||||
|
cleanupDeletedObjectItems(
|
||||||
|
rectPath.data.imageId,
|
||||||
|
rectPath.data.id,
|
||||||
|
rectPath.data.parentGroupId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
forceUpdate()
|
forceUpdate()
|
||||||
@@ -2758,6 +2845,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
polygonPath.data.operationId,
|
polygonPath.data.operationId,
|
||||||
polygonPath.data.id
|
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.operationId,
|
||||||
brushPath.data.id
|
brushPath.data.id
|
||||||
)
|
)
|
||||||
|
cleanupDeletedObjectItems(
|
||||||
|
brushPath.data.imageId,
|
||||||
|
brushPath.data.id,
|
||||||
|
brushPath.data.parentGroupId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (brushPath instanceof paper.Path) {
|
} else if (brushPath instanceof paper.Path) {
|
||||||
@@ -2864,6 +2961,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
brushPath.data.operationId,
|
brushPath.data.operationId,
|
||||||
brushPath.data.id
|
brushPath.data.id
|
||||||
)
|
)
|
||||||
|
cleanupDeletedObjectItems(
|
||||||
|
brushPath.data.imageId,
|
||||||
|
brushPath.data.id,
|
||||||
|
brushPath.data.parentGroupId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
usePaperSupportStore.getState().removeMaskById(brushPath.data.id)
|
usePaperSupportStore.getState().removeMaskById(brushPath.data.id)
|
||||||
@@ -2874,6 +2976,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
brushPath.data.operationId,
|
brushPath.data.operationId,
|
||||||
brushPath.data.id
|
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.operationId,
|
||||||
pointPath.data.id
|
pointPath.data.id
|
||||||
)
|
)
|
||||||
|
cleanupDeletedObjectItems(
|
||||||
|
pointPath.data.imageId,
|
||||||
|
pointPath.data.id,
|
||||||
|
pointPath.data.parentGroupId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
usePaperSupportStore.getState().removeMaskById(pointPath.data.id)
|
usePaperSupportStore.getState().removeMaskById(pointPath.data.id)
|
||||||
@@ -2944,6 +3056,11 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
pointPath.data.operationId,
|
pointPath.data.operationId,
|
||||||
pointPath.data.id
|
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
|
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||||
myCircle.scale(newScale / currentScaling)
|
myCircle.scale(newScale / currentScaling)
|
||||||
myCircle.data = Object.assign({ type: "circle", ...option }, data)
|
myCircle.data = Object.assign({ type: "circle", ...option }, data)
|
||||||
|
syncPaperCircleDiameter(myCircle)
|
||||||
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 (
|
if (
|
||||||
@@ -5845,6 +5963,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
id: number,
|
id: number,
|
||||||
color: any
|
color: any
|
||||||
) => {
|
) => {
|
||||||
|
const nodeDiameter = getPaperNodeDiameter()
|
||||||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{ parent: usePaperStore.getState().group!, center: point },
|
{ parent: usePaperStore.getState().group!, center: point },
|
||||||
@@ -5852,12 +5971,13 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
fillColor: color,
|
fillColor: color,
|
||||||
// center: point,
|
// center: point,
|
||||||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||||||
radius: 3,
|
radius: getPaperNodeRadius(),
|
||||||
strokeScaling: false,
|
strokeScaling: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
type: "circle",
|
type: "circle",
|
||||||
|
diameter: nodeDiameter,
|
||||||
fillColor: color,
|
fillColor: color,
|
||||||
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
strokeColor: usePaperStore.getState().toolOption?.strokeColor,
|
||||||
imageId: useBottomToolsStore.getState().activeImage,
|
imageId: useBottomToolsStore.getState().activeImage,
|
||||||
@@ -5869,9 +5989,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
circle.scale(getPaperNodeScaleFactor())
|
||||||
const newScale = useTopToolsStore.getState().scale / 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 (
|
if (
|
||||||
@@ -6052,8 +6170,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
id: number,
|
id: number,
|
||||||
color: any
|
color: any
|
||||||
) => {
|
) => {
|
||||||
const currentScaling = usePaperStore.getState().group!.scaling.x
|
const nodeDiameter = getPaperNodeDiameter()
|
||||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
|
||||||
let circle: paper.Path.Circle = new paper.Path.Circle(
|
let circle: paper.Path.Circle = new paper.Path.Circle(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{ parent: usePaperStore.getState().group!, center: point },
|
{ parent: usePaperStore.getState().group!, center: point },
|
||||||
@@ -6061,15 +6178,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
fillColor: color,
|
fillColor: color,
|
||||||
// center: point,
|
// center: point,
|
||||||
strokeColor: "#FFF",
|
strokeColor: "#FFF",
|
||||||
radius: 3,
|
strokeWidth: 2,
|
||||||
|
radius: getPaperNodeRadius(),
|
||||||
strokeScaling: false,
|
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) => {
|
circle.onMouseEnter = (_e: any) => {
|
||||||
hoveredPathCircleInfo = {
|
hoveredPathCircleInfo = {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -5,6 +5,145 @@ import { useBottomToolsStore } from "./useBottomToolsStore"
|
|||||||
import { usePaperStore } from "./usePaperStore"
|
import { usePaperStore } from "./usePaperStore"
|
||||||
import { useTopToolsStore } from "./useTopToolsStore"
|
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) => {
|
export const removeTagTexts = (id?: number) => {
|
||||||
const group = usePaperStore.getState().group
|
const group = usePaperStore.getState().group
|
||||||
if (!group) return
|
if (!group) return
|
||||||
@@ -26,7 +165,7 @@ const useRenderTag = () => {
|
|||||||
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
|
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
|
||||||
? group.scaling.x
|
? group.scaling.x
|
||||||
: 1
|
: 1
|
||||||
const applyTagCompensation = (
|
const applyPointTagCompensation = (
|
||||||
item: paper.Item,
|
item: paper.Item,
|
||||||
anchor: [number, number]
|
anchor: [number, number]
|
||||||
) => {
|
) => {
|
||||||
@@ -37,146 +176,163 @@ const useRenderTag = () => {
|
|||||||
// 清空之前渲染的标签
|
// 清空之前渲染的标签
|
||||||
removeTagTexts()
|
removeTagTexts()
|
||||||
|
|
||||||
for (const [imageId, categoryMap] of dataMap) {
|
const categoryMap = dataMap.get(activeImage)
|
||||||
if (imageId === activeImage) {
|
if (!categoryMap) return
|
||||||
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}`
|
|
||||||
|
|
||||||
if (
|
for (const [categoryId, labelList] of categoryMap) {
|
||||||
detail.sub_attributes &&
|
let category = useTopToolsStore
|
||||||
Object.keys(detail.sub_attributes).length
|
.getState()
|
||||||
) {
|
.objectOperations.find(
|
||||||
Object.keys(detail.sub_attributes).forEach((key) => {
|
(item) => item.category_id.toString() === categoryId
|
||||||
if (configs.includes("子属性")) content += ` ${key}:`
|
)!
|
||||||
if (configs.includes("属性值"))
|
const { label_class } = category
|
||||||
content += `${detail.sub_attributes[key]};`
|
labelList.forEach(([id, _d, detail]) => {
|
||||||
})
|
const path = usePaperStore.getState().getItemById(id)
|
||||||
}
|
if (!path) return
|
||||||
// 文本
|
|
||||||
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)
|
|
||||||
|
|
||||||
// 文本框
|
const bounds = path.bounds
|
||||||
const textMask = new paper.Path.Rectangle(text.bounds)
|
const pathData = path.data
|
||||||
textMask.set({
|
const anchorPoint = getTagAnchorPoint(path)
|
||||||
fillColor: pathData.fillColor,
|
const tagAnchor: [number, number] = [anchorPoint.x, anchorPoint.y]
|
||||||
strokeColor: pathData.strokeColor,
|
const tagBackgroundColor = getTagBackgroundColor(
|
||||||
strokeWidth: 1,
|
pathData.fillColor,
|
||||||
strokeScaling: false,
|
pathData.strokeColor
|
||||||
data: {
|
)
|
||||||
id,
|
const tagTextColor = getContrastTextColor(tagBackgroundColor)
|
||||||
type: "tag",
|
let content = ``
|
||||||
tag_category: "text_mask",
|
|
||||||
tag_anchor: tagAnchor,
|
if (configs.includes("ID")) {
|
||||||
},
|
content += `${id}${
|
||||||
parent: group,
|
path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
|
||||||
visible,
|
}`
|
||||||
})
|
}
|
||||||
applyTagCompensation(textMask, tagAnchor)
|
if (configs.includes("类别")) content += ` ${label_class}`
|
||||||
// 标注对象外接框
|
|
||||||
const mask = new paper.Path.Rectangle(bounds)
|
if (
|
||||||
mask.set({
|
detail.sub_attributes &&
|
||||||
strokeColor: pathData.strokeColor,
|
Object.keys(detail.sub_attributes).length
|
||||||
strokeWidth: 2,
|
) {
|
||||||
strokeScaling: false,
|
Object.keys(detail.sub_attributes).forEach((key) => {
|
||||||
data: {
|
if (configs.includes("子属性")) content += ` ${key}:`
|
||||||
id,
|
if (configs.includes("属性值"))
|
||||||
type: "tag",
|
content += `${detail.sub_attributes[key]};`
|
||||||
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 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 { useBottomToolsStore } from "./useBottomToolsStore"
|
||||||
import { safeClone } from "./utils/clone"
|
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 {
|
interface TopToolsState {
|
||||||
// 标注页面是否仅查看
|
// 标注页面是否仅查看
|
||||||
isView: boolean
|
isView: boolean
|
||||||
@@ -34,6 +45,8 @@ interface TopToolsState {
|
|||||||
setAssistToolEnabled: (val: boolean) => void
|
setAssistToolEnabled: (val: boolean) => void
|
||||||
assistToolSize: number
|
assistToolSize: number
|
||||||
setAssistToolSize: (val: number) => void
|
setAssistToolSize: (val: number) => void
|
||||||
|
nodeSize: number
|
||||||
|
setNodeSize: (val: number) => void
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
setShowTags: (val: boolean) => void
|
setShowTags: (val: boolean) => void
|
||||||
showTagsConfigs: string[]
|
showTagsConfigs: string[]
|
||||||
@@ -94,6 +107,7 @@ const initialTopToolsState = {
|
|||||||
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
||||||
assistToolEnabled: false,
|
assistToolEnabled: false,
|
||||||
assistToolSize: 10,
|
assistToolSize: 10,
|
||||||
|
nodeSize: DEFAULT_NODE_SIZE,
|
||||||
drawOption: "default" as "default" | "intersect" | "unite",
|
drawOption: "default" as "default" | "intersect" | "unite",
|
||||||
saveCurrentScale: false,
|
saveCurrentScale: false,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
@@ -183,6 +197,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
...state,
|
...state,
|
||||||
assistToolSize: Math.max(1, val),
|
assistToolSize: Math.max(1, val),
|
||||||
})),
|
})),
|
||||||
|
nodeSize: initialTopToolsState.nodeSize,
|
||||||
|
setNodeSize: (val) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
nodeSize: clampNodeSize(val),
|
||||||
|
})),
|
||||||
showTags: false,
|
showTags: false,
|
||||||
setShowTags: (val) =>
|
setShowTags: (val) =>
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
|
|||||||
@@ -1,5 +1,71 @@
|
|||||||
import { Project } from "./api/project/typing"
|
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 = (
|
export const getSubAttrStatus = (
|
||||||
operationSchema: Project.LabelSchemaList | null,
|
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[] = [
|
export const componentList: MenuItem[] = [
|
||||||
{
|
{
|
||||||
title: "个人中心",
|
title: "个人中心",
|
||||||
@@ -35,11 +46,7 @@ export const componentList: MenuItem[] = [
|
|||||||
url: "video",
|
url: "video",
|
||||||
icon: "VideoAnnotationIcon",
|
icon: "VideoAnnotationIcon",
|
||||||
},
|
},
|
||||||
{
|
...lidarList,
|
||||||
title: "点云标注",
|
|
||||||
url: "lidar",
|
|
||||||
icon: "LidarAnnotationIcon",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "管理中心",
|
title: "管理中心",
|
||||||
url: "mgt",
|
url: "mgt",
|
||||||
|
|||||||
Reference in New Issue
Block a user