Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!9
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import "@mantine/core/styles.css"
|
import "@mantine/core/styles.css"
|
||||||
import "@mantine/core/styles.layer.css"
|
import "@mantine/core/styles.layer.css"
|
||||||
|
import "@mantine/dates/styles.css"
|
||||||
import "@mantine/notifications/styles.css"
|
import "@mantine/notifications/styles.css"
|
||||||
import "mantine-datatable/styles.css"
|
import "mantine-datatable/styles.css"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|||||||
@@ -405,6 +405,7 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<SettingDataTable<Task.DataProps>
|
<SettingDataTable<Task.DataProps>
|
||||||
width="100%"
|
width="100%"
|
||||||
|
minHeight={0}
|
||||||
style={{ width: "100%", flex: 1 }}
|
style={{ width: "100%", flex: 1 }}
|
||||||
pinFirstColumn
|
pinFirstColumn
|
||||||
pinLastColumn
|
pinLastColumn
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core"
|
} from "@mantine/core"
|
||||||
|
import { DatePickerInput, type DatesRangeValue } from "@mantine/dates"
|
||||||
import { modals } from "@mantine/modals"
|
import { modals } from "@mantine/modals"
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import {
|
import {
|
||||||
@@ -41,8 +42,9 @@ import {
|
|||||||
IconSearch,
|
IconSearch,
|
||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
|
import "dayjs/locale/zh-cn"
|
||||||
import duration from "dayjs/plugin/duration"
|
import duration from "dayjs/plugin/duration"
|
||||||
import { DataTableColumn } from "mantine-datatable"
|
import { DataTableColumn, type DataTableSortStatus } from "mantine-datatable"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import * as XLSX from "xlsx-js-style"
|
import * as XLSX from "xlsx-js-style"
|
||||||
@@ -60,6 +62,8 @@ interface FilterFormRecord {
|
|||||||
label_user: string[]
|
label_user: string[]
|
||||||
review_user1: string[]
|
review_user1: string[]
|
||||||
review_user2: string[]
|
review_user2: string[]
|
||||||
|
first_commit_label_time: DatesRangeValue
|
||||||
|
first_commit_review1_time: DatesRangeValue
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInitialFilters(): FilterFormRecord {
|
function createInitialFilters(): FilterFormRecord {
|
||||||
@@ -71,10 +75,52 @@ function createInitialFilters(): FilterFormRecord {
|
|||||||
label_user: [],
|
label_user: [],
|
||||||
review_user1: [],
|
review_user1: [],
|
||||||
review_user2: [],
|
review_user2: [],
|
||||||
|
first_commit_label_time: [null, null],
|
||||||
|
first_commit_review1_time: [null, null],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20]
|
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20]
|
||||||
|
const INITIAL_SORT_ACCESSOR = "__initial__"
|
||||||
|
|
||||||
|
type SortableTaskSizeAccessor = keyof Task.SizeProps
|
||||||
|
|
||||||
|
const SORTABLE_TASK_SIZE_COLUMNS: Array<{
|
||||||
|
accessor: SortableTaskSizeAccessor
|
||||||
|
title: string
|
||||||
|
width: number
|
||||||
|
}> = [
|
||||||
|
{ accessor: "object_size", title: "对象总数", width: 110 },
|
||||||
|
{ accessor: "valid_size", title: "有效对象数", width: 120 },
|
||||||
|
{ accessor: "new_size", title: "新增对象数", width: 120 },
|
||||||
|
{ accessor: "key_frame_size", title: "关键帧数", width: 120 },
|
||||||
|
{ accessor: "dynamic_size", title: "动态对象数", width: 120 },
|
||||||
|
{ accessor: "static_size", title: "静态对象数", width: 120 },
|
||||||
|
{ accessor: "prelabel_size", title: "预标注对象数", width: 120 },
|
||||||
|
{ accessor: "prelabel_modify_size", title: "预标注对象修改数", width: 160 },
|
||||||
|
{ accessor: "question_size", title: "问题数", width: 120 },
|
||||||
|
{ accessor: "total_qa_group_size", title: "总问答组数", width: 120 },
|
||||||
|
{ accessor: "single_qa_group_size", title: "单轮问答组数", width: 120 },
|
||||||
|
{ accessor: "multiple_qa_group_size", title: "多轮问答组数", width: 120 },
|
||||||
|
{ accessor: "new_qa_group_size", title: "新增问答组数", width: 120 },
|
||||||
|
{
|
||||||
|
accessor: "new_single_qa_group_size",
|
||||||
|
title: "新增单轮问答组数",
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: "new_multiple_qa_group_size",
|
||||||
|
title: "新增多轮问答组数",
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{ accessor: "review_question_size", title: "审核问题数", width: 120 },
|
||||||
|
{ accessor: "comment_question_size", title: "首次批注问题数", width: 140 },
|
||||||
|
{ accessor: "review_size", title: "审核对象数", width: 120 },
|
||||||
|
{ accessor: "review_dynamic_size", title: "审核动态对象数", width: 140 },
|
||||||
|
{ accessor: "review_static_size", title: "审核静态对象数", width: 140 },
|
||||||
|
{ accessor: "first_comment_size", title: "批注数(1)", width: 120 },
|
||||||
|
{ accessor: "second_comment_size", title: "批注数(2)", width: 120 },
|
||||||
|
]
|
||||||
|
|
||||||
export interface selectedDataProps {
|
export interface selectedDataProps {
|
||||||
status: number
|
status: number
|
||||||
@@ -167,6 +213,12 @@ export default function TaskTableContainer(props: {
|
|||||||
const [selectedRecords, setSelectedRecords] = useState<Task.DataProps[]>([])
|
const [selectedRecords, setSelectedRecords] = useState<Task.DataProps[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [queryTrigger, setQueryTrigger] = useState(0)
|
const [queryTrigger, setQueryTrigger] = useState(0)
|
||||||
|
const [sortStatus, setSortStatus] = useState<
|
||||||
|
DataTableSortStatus<Task.DataProps>
|
||||||
|
>({
|
||||||
|
columnAccessor: INITIAL_SORT_ACCESSOR,
|
||||||
|
direction: "asc",
|
||||||
|
})
|
||||||
|
|
||||||
const [workflowTaskId, setWorkflowTaskId] = useState<number | null>(null)
|
const [workflowTaskId, setWorkflowTaskId] = useState<number | null>(null)
|
||||||
const [dispatchRecordModal, setDispatchModalConfig] = useState<{
|
const [dispatchRecordModal, setDispatchModalConfig] = useState<{
|
||||||
@@ -223,9 +275,33 @@ export default function TaskTableContainer(props: {
|
|||||||
if (form.review_user2?.length) {
|
if (form.review_user2?.length) {
|
||||||
obj.review_user2 = form.review_user2?.map((item) => Number(item))
|
obj.review_user2 = form.review_user2?.map((item) => Number(item))
|
||||||
}
|
}
|
||||||
|
if (form.first_commit_label_time[0] && form.first_commit_label_time[1]) {
|
||||||
|
obj.first_commit_label_time_start = dayjs(
|
||||||
|
form.first_commit_label_time[0]
|
||||||
|
).format("YYYY-MM-DD")
|
||||||
|
obj.first_commit_label_time_end = dayjs(
|
||||||
|
form.first_commit_label_time[1]
|
||||||
|
).format("YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
form.first_commit_review1_time[0] &&
|
||||||
|
form.first_commit_review1_time[1]
|
||||||
|
) {
|
||||||
|
obj.first_commit_review1_time_start = dayjs(
|
||||||
|
form.first_commit_review1_time[0]
|
||||||
|
).format("YYYY-MM-DD")
|
||||||
|
obj.first_commit_review1_time_end = dayjs(
|
||||||
|
form.first_commit_review1_time[1]
|
||||||
|
).format("YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
if (sortStatus.columnAccessor !== INITIAL_SORT_ACCESSOR) {
|
||||||
|
obj.order_by = `${sortStatus.columnAccessor} ${sortStatus.direction}`
|
||||||
|
}
|
||||||
return obj
|
return obj
|
||||||
}, [
|
}, [
|
||||||
form.current_uid,
|
form.current_uid,
|
||||||
|
form.first_commit_label_time,
|
||||||
|
form.first_commit_review1_time,
|
||||||
form.label_user,
|
form.label_user,
|
||||||
form.rejected,
|
form.rejected,
|
||||||
form.review_user1,
|
form.review_user1,
|
||||||
@@ -235,6 +311,8 @@ export default function TaskTableContainer(props: {
|
|||||||
pageNumber,
|
pageNumber,
|
||||||
pageSize,
|
pageSize,
|
||||||
projectId,
|
projectId,
|
||||||
|
sortStatus.columnAccessor,
|
||||||
|
sortStatus.direction,
|
||||||
])
|
])
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -528,6 +606,22 @@ export default function TaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
|
const createSortableSizeColumn = ({
|
||||||
|
accessor,
|
||||||
|
title,
|
||||||
|
width,
|
||||||
|
}: {
|
||||||
|
accessor: SortableTaskSizeAccessor
|
||||||
|
title: string
|
||||||
|
width: number
|
||||||
|
}): DataTableColumn<Task.DataProps> => ({
|
||||||
|
accessor,
|
||||||
|
title,
|
||||||
|
width,
|
||||||
|
sortable: true,
|
||||||
|
render: (record) => record.label_object_size?.[accessor] ?? "-",
|
||||||
|
})
|
||||||
|
|
||||||
const cols: DataTableColumn<Task.DataProps>[] = [
|
const cols: DataTableColumn<Task.DataProps>[] = [
|
||||||
{
|
{
|
||||||
accessor: "id",
|
accessor: "id",
|
||||||
@@ -595,148 +689,7 @@ export default function TaskTableContainer(props: {
|
|||||||
width: 120,
|
width: 120,
|
||||||
render: (record) => record.review2_username ?? "-",
|
render: (record) => record.review2_username ?? "-",
|
||||||
},
|
},
|
||||||
{
|
...SORTABLE_TASK_SIZE_COLUMNS.map(createSortableSizeColumn),
|
||||||
accessor: "label_object_size.object_size",
|
|
||||||
title: "对象总数",
|
|
||||||
width: 110,
|
|
||||||
render: (record) => record.label_object_size?.object_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.valid_size",
|
|
||||||
title: "有效对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.valid_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.new_size",
|
|
||||||
title: "新增对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.new_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.key_frame_size",
|
|
||||||
title: "关键帧数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.key_frame_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.dynamic_size",
|
|
||||||
title: "动态对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.dynamic_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.static_size",
|
|
||||||
title: "静态对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.static_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.prelabel_size",
|
|
||||||
title: "预标注对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.prelabel_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.prelabel_modify_size",
|
|
||||||
title: "预标注对象修改数",
|
|
||||||
width: 160,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.prelabel_modify_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.question_size",
|
|
||||||
title: "问题数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.question_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.total_qa_group_size",
|
|
||||||
title: "总问答组数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.total_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.single_qa_group_size",
|
|
||||||
title: "单轮问答组数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.single_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.multiple_qa_group_size",
|
|
||||||
title: "多轮问答组数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.multiple_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.new_qa_group_size",
|
|
||||||
title: "新增问答组数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.new_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.new_single_qa_group_size",
|
|
||||||
title: "新增单轮问答组数",
|
|
||||||
width: 160,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.new_single_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.new_multiple_qa_group_size",
|
|
||||||
title: "新增多轮问答组数",
|
|
||||||
width: 160,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.new_multiple_qa_group_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.review_question_size",
|
|
||||||
title: "审核问题数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.review_question_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.comment_question_size",
|
|
||||||
title: "首次批注问题数",
|
|
||||||
width: 140,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.comment_question_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.review_size",
|
|
||||||
title: "审核对象数",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.review_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.review_dynamic_size",
|
|
||||||
title: "审核动态对象数",
|
|
||||||
width: 140,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.review_dynamic_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.review_static_size",
|
|
||||||
title: "审核静态对象数",
|
|
||||||
width: 140,
|
|
||||||
render: (record) => record.label_object_size?.review_static_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.first_comment_size",
|
|
||||||
title: "批注数(1)",
|
|
||||||
width: 120,
|
|
||||||
render: (record) => record.label_object_size?.first_comment_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "label_object_size.second_comment_size",
|
|
||||||
title: "批注数(2)",
|
|
||||||
width: 120,
|
|
||||||
render: (record) =>
|
|
||||||
record.label_object_size?.second_comment_size ?? "-",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessor: "first_commit_label_time",
|
accessor: "first_commit_label_time",
|
||||||
title: "标注首次提交时间",
|
title: "标注首次提交时间",
|
||||||
@@ -966,6 +919,36 @@ export default function TaskTableContainer(props: {
|
|||||||
setFilters((s) => ({ ...s, review_user2: v }))
|
setFilters((s) => ({ ...s, review_user2: v }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<DatePickerInput
|
||||||
|
type="range"
|
||||||
|
label="标注首次提交时间"
|
||||||
|
placeholder="请选择时间范围"
|
||||||
|
valueFormat="YYYY-MM-DD"
|
||||||
|
locale="zh-cn"
|
||||||
|
clearable
|
||||||
|
value={filters.first_commit_label_time}
|
||||||
|
onChange={(value: DatesRangeValue) =>
|
||||||
|
setFilters((s) => ({
|
||||||
|
...s,
|
||||||
|
first_commit_label_time: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DatePickerInput
|
||||||
|
type="range"
|
||||||
|
label="审核首次提交时间"
|
||||||
|
placeholder="请选择时间范围"
|
||||||
|
valueFormat="YYYY-MM-DD"
|
||||||
|
locale="zh-cn"
|
||||||
|
clearable
|
||||||
|
value={filters.first_commit_review1_time}
|
||||||
|
onChange={(value: DatesRangeValue) =>
|
||||||
|
setFilters((s) => ({
|
||||||
|
...s,
|
||||||
|
first_commit_review1_time: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@@ -1031,6 +1014,7 @@ export default function TaskTableContainer(props: {
|
|||||||
/>
|
/>
|
||||||
<SettingDataTable<Task.DataProps>
|
<SettingDataTable<Task.DataProps>
|
||||||
width="100%"
|
width="100%"
|
||||||
|
minHeight={0}
|
||||||
style={{ width: "100%", flex: 1 }}
|
style={{ width: "100%", flex: 1 }}
|
||||||
pinFirstColumn
|
pinFirstColumn
|
||||||
pinLastColumn
|
pinLastColumn
|
||||||
@@ -1038,6 +1022,13 @@ export default function TaskTableContainer(props: {
|
|||||||
fetching={loading}
|
fetching={loading}
|
||||||
records={records}
|
records={records}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
sortStatus={sortStatus}
|
||||||
|
onSortStatusChange={(
|
||||||
|
nextSortStatus: DataTableSortStatus<Task.DataProps>
|
||||||
|
) => {
|
||||||
|
setPageNumber(1)
|
||||||
|
setSortStatus(nextSortStatus)
|
||||||
|
}}
|
||||||
totalRecords={total}
|
totalRecords={total}
|
||||||
recordsPerPage={pageSize}
|
recordsPerPage={pageSize}
|
||||||
page={pageNumber}
|
page={pageNumber}
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ const LabelPage = ({
|
|||||||
setShowMultiFrame,
|
setShowMultiFrame,
|
||||||
isView,
|
isView,
|
||||||
setIsView,
|
setIsView,
|
||||||
|
renderMode,
|
||||||
scale,
|
scale,
|
||||||
setScale,
|
setScale,
|
||||||
objectOperations,
|
objectOperations,
|
||||||
@@ -1307,8 +1308,8 @@ const LabelPage = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const setDrawType = useCallback(() => {
|
const setDrawType = useCallback(() => {
|
||||||
if (isView) {
|
if (isView || renderMode) {
|
||||||
setPaperMode("pan")
|
setPaperMode(null)
|
||||||
} else {
|
} else {
|
||||||
let operationSchema = getOperationSchema(activeOperation)
|
let operationSchema = getOperationSchema(activeOperation)
|
||||||
if (operationSchema) {
|
if (operationSchema) {
|
||||||
@@ -1371,6 +1372,7 @@ const LabelPage = ({
|
|||||||
editMode,
|
editMode,
|
||||||
getOperationSchema,
|
getOperationSchema,
|
||||||
isView,
|
isView,
|
||||||
|
renderMode,
|
||||||
setPaperMode,
|
setPaperMode,
|
||||||
setToolOption,
|
setToolOption,
|
||||||
])
|
])
|
||||||
@@ -2117,7 +2119,7 @@ const LabelPage = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isView) return
|
if (isView || renderMode) return
|
||||||
|
|
||||||
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -2265,6 +2267,7 @@ const LabelPage = ({
|
|||||||
// 追踪
|
// 追踪
|
||||||
if (e.key === "i") {
|
if (e.key === "i") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
// sam2 暂未有图片接口
|
||||||
paperContainerRef.current.renderTrackingAnnotation()
|
paperContainerRef.current.renderTrackingAnnotation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2426,6 +2429,7 @@ const LabelPage = ({
|
|||||||
handleContinueSelectedPolygon,
|
handleContinueSelectedPolygon,
|
||||||
handleShortcut,
|
handleShortcut,
|
||||||
isView,
|
isView,
|
||||||
|
renderMode,
|
||||||
projectDetail?.label_type,
|
projectDetail?.label_type,
|
||||||
setEditMode,
|
setEditMode,
|
||||||
setGroupScale,
|
setGroupScale,
|
||||||
|
|||||||
@@ -108,18 +108,6 @@ export const getLabelImage = (activeImage: string, path: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助标注
|
|
||||||
export const getAuxiliaryAnnotation = (data: any) => {
|
|
||||||
return httpFetch<any>({
|
|
||||||
url: BASE_LABEL_API + "/api/model_server/sam2/sa",
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headerAttr: {
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const BASE_SAM_API =
|
const BASE_SAM_API =
|
||||||
process.env.NEXT_PUBLIC_ENV === "production"
|
process.env.NEXT_PUBLIC_ENV === "production"
|
||||||
? "http://172.16.103.224:8000"
|
? "http://172.16.103.224:8000"
|
||||||
@@ -127,7 +115,7 @@ const BASE_SAM_API =
|
|||||||
? "http://172.30.21.211:8000"
|
? "http://172.30.21.211:8000"
|
||||||
: "http://172.30.21.211:8000"
|
: "http://172.30.21.211:8000"
|
||||||
|
|
||||||
export const getAuxiliaryAnnotation2 = (data: {
|
export const getAuxiliaryAnnotation = (data: {
|
||||||
project_id: number
|
project_id: number
|
||||||
image_name: string
|
image_name: string
|
||||||
prompt: {
|
prompt: {
|
||||||
@@ -146,18 +134,6 @@ export const getAuxiliaryAnnotation2 = (data: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模型标注 追踪
|
|
||||||
export const getTrackingAuxiliaryAnnotation = (data: any) => {
|
|
||||||
return httpFetch<any>({
|
|
||||||
url: BASE_LABEL_API + "/api/model_server/sam2/vos",
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headerAttr: {
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户登录
|
// 用户登录
|
||||||
export const userLogin = (data: { name: string; password: string }) => {
|
export const userLogin = (data: { name: string; password: string }) => {
|
||||||
return httpFetch<LoginInfo>({
|
return httpFetch<LoginInfo>({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export namespace Task {
|
export namespace Task {
|
||||||
export interface DataProps {
|
export interface DataProps {
|
||||||
|
[key: string]: unknown
|
||||||
id: number
|
id: number
|
||||||
create_date: string
|
create_date: string
|
||||||
data_type?: number
|
data_type?: number
|
||||||
@@ -74,6 +75,7 @@ export namespace Task {
|
|||||||
id?: number[]
|
id?: number[]
|
||||||
label_status?: number[]
|
label_status?: number[]
|
||||||
label_user?: number[]
|
label_user?: number[]
|
||||||
|
// sample: object_size asc/object_size desc
|
||||||
order_by?: string
|
order_by?: string
|
||||||
page_number: number
|
page_number: number
|
||||||
page_size: number
|
page_size: number
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# `renderTrackingAnnotation` 流程说明
|
|
||||||
|
|
||||||
来源文件:
|
|
||||||
|
|
||||||
- `labelimage/components/label/components/PaperContainer.tsx`
|
|
||||||
- `labelimage/components/label/LabelNossr.tsx`
|
|
||||||
- `labelimage/components/label/api/label/index.ts`
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
`renderTrackingAnnotation` 是单对象跨帧追踪的入口方法。
|
|
||||||
|
|
||||||
它会把当前激活图片上选中的标注对象作为 seed,将其轮廓数据发送给追踪模型接口,再把模型返回的后续帧轮廓结果写回 `label store`。
|
|
||||||
|
|
||||||
## Mermaid
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["在 `LabelNossr.tsx` 中按下 `i`"] --> B["调用 `paperContainerRef.current.renderTrackingAnnotation()`"]
|
|
||||||
B --> C["执行 `updateLoadingFlag(true)`<br/>并显示加载通知"]
|
|
||||||
C --> D["从 store 读取运行时状态:<br/>`selectedPath[activeImage]`<br/>`selectedItems`<br/>`rasterSize[activeImage]`<br/>`rasterScale[activeImage]`"]
|
|
||||||
|
|
||||||
D --> E{"是否恰好选中了一个标注对象?"}
|
|
||||||
E -->|未选中| F["提示错误:<br/>`请先选中标注对象后再进行模型调用`"]
|
|
||||||
E -->|选中了多个| G["提示错误:<br/>`请勿选择多个标注对象进行模型调用`"]
|
|
||||||
E -->|是| H["在 `label.get(activeImage)` 中查找<br/>满足 `item[0] === id` 的标注对象"]
|
|
||||||
|
|
||||||
H --> I{"是否找到 `categoryId` 和 `data`?"}
|
|
||||||
I -->|否| Z["跳过请求参数构造"]
|
|
||||||
I -->|是| J["提取源标注几何数据:<br/>`data[1]` => 外轮廓 `segmentation`<br/>`data[3]` => 镂空轮廓 `hollow_segmentation`"]
|
|
||||||
|
|
||||||
J --> K["将画布坐标转换为原图坐标:<br/>`Math.round(x / currentScale)`<br/>`Math.round(y / currentScale)`"]
|
|
||||||
K --> L["构造请求参数:<br/>`project_id`<br/>`file_names = [activeImage, ...selectedItems]`<br/>`image_hw = [height, width]`<br/>`contours`<br/>`hollows`"]
|
|
||||||
L --> M["使用 `msgpack.encode(params)` 序列化"]
|
|
||||||
M --> N["通过 `getTrackingAuxiliaryAnnotation()`<br/>POST 到 `/api/model_server/sam2/vos`"]
|
|
||||||
N --> O["以 `arraybuffer` 形式接收响应体"]
|
|
||||||
O --> P["使用<br/>`msgpack.decode(new Uint8Array(res))`<br/>解码响应数据"]
|
|
||||||
|
|
||||||
P --> Q{"resData.msg === 'success'?"}
|
|
||||||
Q -->|否| R["内层流程仅执行 `console.log(error/resData)`"]
|
|
||||||
Q -->|是| S["读取响应中的 `future_contours`"]
|
|
||||||
|
|
||||||
S --> T["克隆当前 `label store`:<br/>`safeClone(useLabelStore.getState().label)`"]
|
|
||||||
T --> U["遍历每个未来帧结果:<br/>目标文件 = `file_names[index + 1]`"]
|
|
||||||
U --> V["读取目标帧缩放比例:<br/>`rasterScale[fileName]`"]
|
|
||||||
V --> W["基于 `pathIds` 生成 `newId` 并注册"]
|
|
||||||
W --> X["将原图坐标还原为画布坐标:<br/>`x * imgScale`<br/>`y * imgScale`"]
|
|
||||||
X --> Y["将返回轮廓拆分为:<br/>`segmentation`<br/>`hollow_segmentation`"]
|
|
||||||
Y --> AA["构造标注元组:<br/>`[newId, segmentation, detail, hollow_segmentation]`"]
|
|
||||||
AA --> AB["将结果合并到<br/>`Map<fileName, Map<categoryId, annotation[]>>`"]
|
|
||||||
AB --> AC["持久化结果:<br/>`setLabel(nowTaskData)`<br/>`pushStateStack(nowTaskData)`"]
|
|
||||||
AC --> AD["显示成功通知"]
|
|
||||||
|
|
||||||
F --> AE["finally:执行 `updateLoadingFlag(false)`"]
|
|
||||||
G --> AE
|
|
||||||
Z --> AE
|
|
||||||
R --> AE
|
|
||||||
AD --> AE
|
|
||||||
C --> AF["外层 `try/catch`"]
|
|
||||||
AF -->|异常| AG["显示通用失败通知:<br/>`模型生成失败`"]
|
|
||||||
AG --> AE
|
|
||||||
```
|
|
||||||
|
|
||||||
## 数据结构
|
|
||||||
|
|
||||||
被追踪的源标注对象,使用和普通多边形标注一致的元组结构:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
;[id, segmentation, detail, hollow_segmentation]
|
|
||||||
```
|
|
||||||
|
|
||||||
各字段含义:
|
|
||||||
|
|
||||||
- `item[0]`:标注 id
|
|
||||||
- `item[1]`:外轮廓多边形
|
|
||||||
- `item[2]`:详情元数据
|
|
||||||
- `item[3]`:镂空轮廓多边形
|
|
||||||
|
|
||||||
## 为什么使用 `arraybuffer`
|
|
||||||
|
|
||||||
这条追踪请求并不是从头到尾都按普通 JSON 方式传输。
|
|
||||||
|
|
||||||
- 请求参数先通过 `msgpack.encode(params)` 打包。
|
|
||||||
- API 封装层把这份打包后的数据发给后端。
|
|
||||||
- 响应体以 `arraybuffer` 的形式返回。
|
|
||||||
- `renderTrackingAnnotation` 再通过 `msgpack.decode(new Uint8Array(res))` 还原出 JavaScript 对象。
|
|
||||||
|
|
||||||
因此这里必须配置 `responseType: "arraybuffer"`,因为前端预期拿到的是二进制响应体,再自行解码。
|
|
||||||
|
|
||||||
## 备注
|
|
||||||
|
|
||||||
- 当前帧是 seed 帧,模型返回的结果会写入 `file_names[index + 1]` 对应的后续帧。
|
|
||||||
- 这个方法更新的是 `label store`,不会直接在当前页面把其他帧的结果立刻画出来。
|
|
||||||
- 这个方法只支持一次追踪一个被选中的对象。
|
|
||||||
- 内层请求失败时目前主要是 `console.log`,不一定总会显示专门的失败提示。
|
|
||||||
- 这个方法里生成的 `detail.image_id` 使用的是 `activeImage`,建议再结合整体数据模型确认一下是否符合预期。
|
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import msgpack from "msgpack-lite"
|
// import msgpack from "msgpack-lite"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import React, {
|
import React, {
|
||||||
DispatchWithoutAction,
|
DispatchWithoutAction,
|
||||||
@@ -31,13 +31,9 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react"
|
} from "react"
|
||||||
import {
|
import { getAuxiliaryAnnotation, getServerImage } from "../api/label"
|
||||||
getAuxiliaryAnnotation,
|
|
||||||
getAuxiliaryAnnotation2,
|
|
||||||
getServerImage,
|
|
||||||
getTrackingAuxiliaryAnnotation,
|
|
||||||
} from "../api/label"
|
|
||||||
import { Project } from "../api/project/typing"
|
import { Project } from "../api/project/typing"
|
||||||
|
import { labelimagePerformanceConfig } from "../config/performance"
|
||||||
import { LabelState } from "../LabelNossr"
|
import { LabelState } from "../LabelNossr"
|
||||||
import {
|
import {
|
||||||
initialDetail,
|
initialDetail,
|
||||||
@@ -51,12 +47,12 @@ import { useBottomToolsStore } from "../useBottomToolsStore"
|
|||||||
import { useKeyboardStore } from "../useKeyBoardStore"
|
import { useKeyboardStore } from "../useKeyBoardStore"
|
||||||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||||||
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
||||||
|
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||||
import { renderGroupPath } from "../useRenderGroupPath"
|
import { renderGroupPath } from "../useRenderGroupPath"
|
||||||
import useRenderTag from "../useRenderTag"
|
import useRenderTag from "../useRenderTag"
|
||||||
import { useRightToolsStore } from "../useRightToolsStore"
|
import { useRightToolsStore } from "../useRightToolsStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
import { checkCommentsIsSame } from "../util"
|
import { checkCommentsIsSame } from "../util"
|
||||||
import { labelimagePerformanceConfig } from "../config/performance"
|
|
||||||
import { safeClone } from "../utils/clone"
|
import { safeClone } from "../utils/clone"
|
||||||
import { labelTypeMap } from "../utils/constants"
|
import { labelTypeMap } from "../utils/constants"
|
||||||
import { adjustPoints } from "../utils/paperjs"
|
import { adjustPoints } from "../utils/paperjs"
|
||||||
@@ -125,6 +121,11 @@ type FrameSwitchGhostState = {
|
|||||||
animating: boolean
|
animating: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clonePaperPoint = (point: paper.Point | null | undefined) => {
|
||||||
|
if (!point) return null
|
||||||
|
return new paper.Point(point.x, point.y)
|
||||||
|
}
|
||||||
|
|
||||||
const normalizePoint = (value: any): NormalizedPoint | null => {
|
const normalizePoint = (value: any): NormalizedPoint | null => {
|
||||||
if (value instanceof paper.Point) return [value.x, value.y]
|
if (value instanceof paper.Point) return [value.x, value.y]
|
||||||
if (value instanceof paper.Segment) {
|
if (value instanceof paper.Segment) {
|
||||||
@@ -179,6 +180,7 @@ const PaperContainer = (
|
|||||||
ref: React.Ref<unknown> | undefined
|
ref: React.Ref<unknown> | undefined
|
||||||
) => {
|
) => {
|
||||||
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
|
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
|
||||||
|
false && updateLoadingFlag
|
||||||
const containerRef = useRef<any>(null)
|
const containerRef = useRef<any>(null)
|
||||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||||
const [imgSize, setImageSize] = useState<{
|
const [imgSize, setImageSize] = useState<{
|
||||||
@@ -443,6 +445,8 @@ const PaperContainer = (
|
|||||||
crosshairStatus,
|
crosshairStatus,
|
||||||
assistToolEnabled,
|
assistToolEnabled,
|
||||||
assistToolSize,
|
assistToolSize,
|
||||||
|
renderMode,
|
||||||
|
saveCurrentScale,
|
||||||
showTags,
|
showTags,
|
||||||
showTagsConfigs,
|
showTagsConfigs,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
@@ -452,6 +456,8 @@ const PaperContainer = (
|
|||||||
selectedPath,
|
selectedPath,
|
||||||
updateSelectedPath,
|
updateSelectedPath,
|
||||||
} = useObjectStore()
|
} = useObjectStore()
|
||||||
|
const currentSelectedPath = selectedPath[activeImage] || []
|
||||||
|
const currentSelectedPathKey = currentSelectedPath.join(",")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showContextMenu,
|
showContextMenu,
|
||||||
@@ -511,6 +517,11 @@ const PaperContainer = (
|
|||||||
token: number
|
token: number
|
||||||
imageName: string
|
imageName: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const renderModePanRef = useRef({
|
||||||
|
active: false,
|
||||||
|
lastX: 0,
|
||||||
|
lastY: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const paperScope = useRef<paper.PaperScope>(null)
|
const paperScope = useRef<paper.PaperScope>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -869,164 +880,6 @@ const PaperContainer = (
|
|||||||
}, [clearPendingFrameSwitch, loadingData])
|
}, [clearPendingFrameSwitch, loadingData])
|
||||||
|
|
||||||
const renderSupportAnnotation = useCallback(
|
const renderSupportAnnotation = useCallback(
|
||||||
async (
|
|
||||||
points: Array<[number, number]>,
|
|
||||||
tags: number[],
|
|
||||||
clear_previous_state = false
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
notifications.show({
|
|
||||||
id: "sam2",
|
|
||||||
message: "模型生成中",
|
|
||||||
loading: true,
|
|
||||||
autoClose: false,
|
|
||||||
})
|
|
||||||
const operationSchema =
|
|
||||||
projectDetail?.label_schema_list?.find(
|
|
||||||
(item) =>
|
|
||||||
item.category_id === +useTopToolsStore.getState().activeOperation
|
|
||||||
) || null
|
|
||||||
const strokeColor = operationSchema
|
|
||||||
? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
|
||||||
: "rgb(0,0,0)"
|
|
||||||
const fillColor = operationSchema
|
|
||||||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
|
||||||
: "rgba(0,0,0,0.3)"
|
|
||||||
const blankColor = operationSchema
|
|
||||||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
|
||||||
: "rgba(0,0,0,0.01)"
|
|
||||||
const currentScale =
|
|
||||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
|
||||||
|
|
||||||
const draw = () => {
|
|
||||||
points.forEach((point, index) => {
|
|
||||||
const drawPoint = new paper.Path.Circle({
|
|
||||||
center: point,
|
|
||||||
radius: 2,
|
|
||||||
fillColor: tags[index] === 1 ? "red" : "blue",
|
|
||||||
strokeColor: tags[index] === 1 ? "red" : "blue",
|
|
||||||
data: {
|
|
||||||
id: "support",
|
|
||||||
type: "point",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
drawPoint.parent = usePaperStore.getState().group!
|
|
||||||
const raster = usePaperStore
|
|
||||||
.getState()
|
|
||||||
.group!.getItems({ data: { name: "pic" } })[0]
|
|
||||||
if (!raster.contains(drawPoint.position)) {
|
|
||||||
throw new Error("点位超出图片范围")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.log(points, tags)
|
|
||||||
// 绘制点位
|
|
||||||
clearSupportAnnotationItem()
|
|
||||||
draw()
|
|
||||||
|
|
||||||
const picSize = usePaperStore.getState().rasterSize[activeImage]
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
project_id: projectDetail?.id || 0,
|
|
||||||
file_name: activeImage,
|
|
||||||
image_hw:
|
|
||||||
picSize && picSize.length ? [picSize[1], picSize[0]] : [0, 0],
|
|
||||||
points_and_box: points.map(([x, y]) => [
|
|
||||||
Math.round(x / currentScale),
|
|
||||||
Math.round(y / currentScale),
|
|
||||||
]),
|
|
||||||
tags,
|
|
||||||
clear_previous_state,
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await getAuxiliaryAnnotation(
|
|
||||||
msgpack.encode(params) as any
|
|
||||||
)
|
|
||||||
const data = msgpack.decode(new Uint8Array(res))
|
|
||||||
console.log(data)
|
|
||||||
if (data.msg === "success") {
|
|
||||||
const { contours, hollows } = data
|
|
||||||
const outerPaths: any[] = []
|
|
||||||
const innerPaths: any[] = []
|
|
||||||
contours.forEach((contour: any, index: number) => {
|
|
||||||
if (contour.length > 3) {
|
|
||||||
const path = new paper.Path({
|
|
||||||
segments: contour.map(([x, y]: [number, number]) => [
|
|
||||||
x * currentScale,
|
|
||||||
y * currentScale,
|
|
||||||
]),
|
|
||||||
fillColor: blankColor,
|
|
||||||
strokeColor: strokeColor,
|
|
||||||
closed: true,
|
|
||||||
data: {
|
|
||||||
id: "support",
|
|
||||||
type: "polygon",
|
|
||||||
isHollowPolygon: false,
|
|
||||||
fillColor,
|
|
||||||
strokeColor,
|
|
||||||
blankColor,
|
|
||||||
},
|
|
||||||
parent: usePaperStore.getState().group,
|
|
||||||
strokeScaling: false,
|
|
||||||
})
|
|
||||||
if (
|
|
||||||
Math.abs(path.area) < useTopToolsStore.getState().checkSize
|
|
||||||
) {
|
|
||||||
path.remove()
|
|
||||||
} else {
|
|
||||||
// path.scaling = usePaperStore.getState().group!.scaling;
|
|
||||||
if (hollows[index]) {
|
|
||||||
// 更新标志位
|
|
||||||
path.data.isHollowPolygon = true
|
|
||||||
innerPaths.push(path)
|
|
||||||
} else {
|
|
||||||
// 上色
|
|
||||||
path.fillColor = new paper.Color(fillColor)
|
|
||||||
outerPaths.push(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const compoundPath = new paper.CompoundPath({
|
|
||||||
children: [...outerPaths, ...innerPaths],
|
|
||||||
data: {
|
|
||||||
id: "support",
|
|
||||||
type: "parent",
|
|
||||||
},
|
|
||||||
strokeColor: strokeColor,
|
|
||||||
strokeWidth: 2,
|
|
||||||
fillColor: fillColor,
|
|
||||||
})
|
|
||||||
compoundPath.parent = usePaperStore.getState().group!
|
|
||||||
notifications.show({
|
|
||||||
id: "sam2",
|
|
||||||
message: "模型生成成功",
|
|
||||||
color: "green",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log(error)
|
|
||||||
notifications.show({
|
|
||||||
id: "sam2",
|
|
||||||
message: error.message ?? "模型生成失败",
|
|
||||||
color: "red",
|
|
||||||
})
|
|
||||||
if (error.message === "点位超出图片范围") {
|
|
||||||
usePaperStore
|
|
||||||
.getState()
|
|
||||||
.supportTool?.emit("keydown", { key: "escape" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activeImage, projectDetail]
|
|
||||||
)
|
|
||||||
|
|
||||||
false && renderSupportAnnotation
|
|
||||||
|
|
||||||
const renderSupportAnnotation2 = useCallback(
|
|
||||||
async (
|
async (
|
||||||
points: Array<[number, number]>,
|
points: Array<[number, number]>,
|
||||||
tags: number[],
|
tags: number[],
|
||||||
@@ -1130,7 +983,7 @@ const PaperContainer = (
|
|||||||
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getAuxiliaryAnnotation2({
|
const data = await getAuxiliaryAnnotation({
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
image_name: activeImage,
|
image_name: activeImage,
|
||||||
// project_id: "image_test_bk",
|
// project_id: "image_test_bk",
|
||||||
@@ -1218,180 +1071,174 @@ const PaperContainer = (
|
|||||||
[activeImage, projectDetail]
|
[activeImage, projectDetail]
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderTrackingAnnotation = useCallback(async () => {
|
// const renderTrackingAnnotation = useCallback(async () => {
|
||||||
updateLoadingFlag(true)
|
// updateLoadingFlag(true)
|
||||||
try {
|
// try {
|
||||||
notifications.show({
|
// notifications.show({
|
||||||
id: "sam2",
|
// id: "sam2",
|
||||||
message: "模型生成中",
|
// message: "模型生成中",
|
||||||
loading: true,
|
// loading: true,
|
||||||
autoClose: false,
|
// autoClose: false,
|
||||||
})
|
// })
|
||||||
const ids =
|
// const ids =
|
||||||
useObjectStore.getState().selectedPath[
|
// useObjectStore.getState().selectedPath[
|
||||||
useBottomToolsStore.getState().activeImage
|
// useBottomToolsStore.getState().activeImage
|
||||||
]
|
// ]
|
||||||
const picArr = useBottomToolsStore.getState().selectedItems
|
// const picArr = useBottomToolsStore.getState().selectedItems
|
||||||
const [width, height] = usePaperStore.getState().rasterSize[
|
// const [width, height] = usePaperStore.getState().rasterSize[
|
||||||
activeImage
|
// activeImage
|
||||||
] ?? [0, 0]
|
// ] ?? [0, 0]
|
||||||
const currentScale =
|
// const currentScale =
|
||||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
// usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||||
if (ids.length === 1) {
|
// if (ids.length === 1) {
|
||||||
const id = ids[0]
|
// const id = ids[0]
|
||||||
let categoryId: any = null
|
// let categoryId: any = null
|
||||||
let data: any = null
|
// let data: any = null
|
||||||
const activeImageData = useLabelStore.getState().label.get(activeImage)!
|
// const activeImageData = useLabelStore.getState().label.get(activeImage)!
|
||||||
for (let [category, objArr] of activeImageData.entries()) {
|
// for (let [category, objArr] of activeImageData.entries()) {
|
||||||
objArr.forEach((item) => {
|
// objArr.forEach((item) => {
|
||||||
if (item[0] === id) {
|
// if (item[0] === id) {
|
||||||
categoryId = category
|
// categoryId = category
|
||||||
data = item
|
// data = item
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
if (categoryId && data) {
|
// if (categoryId && data) {
|
||||||
const contours: [number, number][] = []
|
// const contours: [number, number][] = []
|
||||||
const tags: boolean[] = []
|
// const tags: boolean[] = []
|
||||||
data[1].forEach((item: any) => {
|
// data[1].forEach((item: any) => {
|
||||||
contours.push(
|
// contours.push(
|
||||||
item.map(([x, y]: any) => [
|
// item.map(([x, y]: any) => [
|
||||||
Math.round(x / currentScale),
|
// Math.round(x / currentScale),
|
||||||
Math.round(y / currentScale),
|
// Math.round(y / currentScale),
|
||||||
])
|
// ])
|
||||||
)
|
// )
|
||||||
tags.push(false)
|
// tags.push(false)
|
||||||
})
|
// })
|
||||||
data[3].forEach((item: any) => {
|
// data[3].forEach((item: any) => {
|
||||||
contours.push(
|
// contours.push(
|
||||||
item.map(([x, y]: any) => [
|
// item.map(([x, y]: any) => [
|
||||||
Math.round(x / currentScale),
|
// Math.round(x / currentScale),
|
||||||
Math.round(y / currentScale),
|
// Math.round(y / currentScale),
|
||||||
])
|
// ])
|
||||||
)
|
// )
|
||||||
tags.push(true)
|
// tags.push(true)
|
||||||
})
|
// })
|
||||||
const file_names = [...new Set([activeImage, ...picArr])]
|
// const file_names = [...new Set([activeImage, ...picArr])]
|
||||||
const params = {
|
// const params = {
|
||||||
project_id: projectDetail?.id || 0,
|
// project_id: projectDetail?.id || 0,
|
||||||
file_names,
|
// file_names,
|
||||||
image_hw: [height, width],
|
// image_hw: [height, width],
|
||||||
contours,
|
// contours,
|
||||||
hollows: tags,
|
// hollows: tags,
|
||||||
}
|
// }
|
||||||
console.log("传参", params)
|
// console.log("传参", params)
|
||||||
try {
|
// try {
|
||||||
const res = await getTrackingAuxiliaryAnnotation(
|
// const res = await getTrackingAuxiliaryAnnotation(
|
||||||
msgpack.encode(params)
|
// msgpack.encode(params)
|
||||||
)
|
// )
|
||||||
const resData = msgpack.decode(new Uint8Array(res))
|
// const resData = msgpack.decode(new Uint8Array(res))
|
||||||
if (resData.msg === "success") {
|
// if (resData.msg === "success") {
|
||||||
const {
|
// const {
|
||||||
future_contours,
|
// future_contours,
|
||||||
}: {
|
// }: {
|
||||||
future_contours: {
|
// future_contours: {
|
||||||
contours: [number, number][][]
|
// contours: [number, number][][]
|
||||||
hollows: boolean[]
|
// hollows: boolean[]
|
||||||
}[]
|
// }[]
|
||||||
} = resData
|
// } = resData
|
||||||
const nowTaskData = safeClone(useLabelStore.getState().label)
|
// const nowTaskData = safeClone(useLabelStore.getState().label)
|
||||||
future_contours.forEach(({ contours, hollows }, index) => {
|
// future_contours.forEach(({ contours, hollows }, index) => {
|
||||||
const fileName = file_names[index + 1]
|
// const fileName = file_names[index + 1]
|
||||||
const imgScale =
|
// const imgScale =
|
||||||
usePaperStore.getState().rasterScale[fileName] ?? 1
|
// usePaperStore.getState().rasterScale[fileName] ?? 1
|
||||||
let newId =
|
// let newId =
|
||||||
useRightToolsStore
|
// useRightToolsStore
|
||||||
.getState()
|
// .getState()
|
||||||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
// .pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||||||
useRightToolsStore.getState().pushPathId(newId)
|
// useRightToolsStore.getState().pushPathId(newId)
|
||||||
// 处理点位数据
|
// // 处理点位数据
|
||||||
let segmentation: Array<[number, number][]> = []
|
// let segmentation: Array<[number, number][]> = []
|
||||||
let hollow_segmentation: Array<[number, number][]> = []
|
// let hollow_segmentation: Array<[number, number][]> = []
|
||||||
contours.forEach((contour, index) => {
|
// contours.forEach((contour, index) => {
|
||||||
if (!hollows[index])
|
// if (!hollows[index])
|
||||||
segmentation.push(
|
// segmentation.push(
|
||||||
contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
// contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||||||
)
|
// )
|
||||||
else
|
// else
|
||||||
hollow_segmentation.push(
|
// hollow_segmentation.push(
|
||||||
contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
// contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
// 详情数据
|
// // 详情数据
|
||||||
let detail = {
|
// let detail = {
|
||||||
...initialDetail,
|
// ...initialDetail,
|
||||||
uid: usePermissionStore.getState().user_id,
|
// uid: usePermissionStore.getState().user_id,
|
||||||
comment: [...useLabelStore.getState().labelDefaultComments],
|
// comment: [...useLabelStore.getState().labelDefaultComments],
|
||||||
create_timestamp: dayjs().unix(),
|
// create_timestamp: dayjs().unix(),
|
||||||
sub_attributes: {},
|
// sub_attributes: {},
|
||||||
image_id: activeImage,
|
// image_id: activeImage,
|
||||||
category_id: categoryId,
|
// category_id: categoryId,
|
||||||
}
|
// }
|
||||||
const updateData: any = [
|
// const updateData: any = [
|
||||||
newId,
|
// newId,
|
||||||
segmentation,
|
// segmentation,
|
||||||
detail,
|
// detail,
|
||||||
hollow_segmentation,
|
// hollow_segmentation,
|
||||||
]
|
// ]
|
||||||
if (nowTaskData.has(fileName)) {
|
// if (nowTaskData.has(fileName)) {
|
||||||
const categoryMap = nowTaskData.get(fileName)!
|
// const categoryMap = nowTaskData.get(fileName)!
|
||||||
if (categoryMap.has(categoryId)) {
|
// if (categoryMap.has(categoryId)) {
|
||||||
const existArr = categoryMap.get(categoryId)
|
// const existArr = categoryMap.get(categoryId)
|
||||||
existArr?.push(updateData)
|
// existArr?.push(updateData)
|
||||||
} else {
|
// } else {
|
||||||
categoryMap.set(categoryId, [updateData])
|
// categoryMap.set(categoryId, [updateData])
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
let m = new Map()
|
// let m = new Map()
|
||||||
m.set(categoryId, [updateData])
|
// m.set(categoryId, [updateData])
|
||||||
nowTaskData.set(fileName, m)
|
// nowTaskData.set(fileName, m)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
console.log(nowTaskData)
|
// console.log(nowTaskData)
|
||||||
useLabelStore.getState().setLabel(nowTaskData)
|
// useLabelStore.getState().setLabel(nowTaskData)
|
||||||
useLabelStore.getState().pushStateStack(nowTaskData)
|
// useLabelStore.getState().pushStateStack(nowTaskData)
|
||||||
notifications.show({
|
// notifications.show({
|
||||||
id: "sam2",
|
// id: "sam2",
|
||||||
message: "模型生成成功",
|
// message: "模型生成成功",
|
||||||
color: "green",
|
// color: "green",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.log(error)
|
// console.log(error)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else if (!ids.length) {
|
// } else if (!ids.length) {
|
||||||
notifications.show({
|
// notifications.show({
|
||||||
id: "sam2",
|
// id: "sam2",
|
||||||
message: "请先选中标注对象后再进行模型调用",
|
// message: "请先选中标注对象后再进行模型调用",
|
||||||
color: "red",
|
// color: "red",
|
||||||
})
|
// })
|
||||||
} else {
|
// } else {
|
||||||
notifications.show({
|
// notifications.show({
|
||||||
id: "sam2",
|
// id: "sam2",
|
||||||
message: "请勿选择多个标注对象进行模型调用",
|
// message: "请勿选择多个标注对象进行模型调用",
|
||||||
color: "red",
|
// color: "red",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.log(error)
|
// console.log(error)
|
||||||
notifications.show({
|
// notifications.show({
|
||||||
id: "sam2",
|
// id: "sam2",
|
||||||
message: "模型生成失败",
|
// message: "模型生成失败",
|
||||||
color: "red",
|
// color: "red",
|
||||||
})
|
// })
|
||||||
} finally {
|
// } finally {
|
||||||
updateLoadingFlag(false)
|
// updateLoadingFlag(false)
|
||||||
}
|
// }
|
||||||
}, [activeImage, projectDetail?.id, updateLoadingFlag])
|
// }, [activeImage, projectDetail?.id, updateLoadingFlag])
|
||||||
|
|
||||||
// const getBase64 = (file: any): Promise<string> =>
|
const renderTrackingAnnotation = useCallback(async () => {}, [])
|
||||||
// new Promise((resolve, reject) => {
|
|
||||||
// const reader = new FileReader();
|
|
||||||
// reader.readAsDataURL(file);
|
|
||||||
// reader.onload = () => resolve(reader.result as string);
|
|
||||||
// reader.onerror = (error) => reject(error);
|
|
||||||
// });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setup) {
|
if (setup) {
|
||||||
@@ -1424,7 +1271,7 @@ const PaperContainer = (
|
|||||||
let pointTool = new paper.Tool()
|
let pointTool = new paper.Tool()
|
||||||
initPointTool(pointTool, renderCircle, renderPath, forceUpdate)
|
initPointTool(pointTool, renderCircle, renderPath, forceUpdate)
|
||||||
let supportTool = new paper.Tool()
|
let supportTool = new paper.Tool()
|
||||||
initSupportTool(supportTool, renderSupportAnnotation2)
|
initSupportTool(supportTool, renderSupportAnnotation)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
newGroup.remove()
|
newGroup.remove()
|
||||||
@@ -1440,7 +1287,7 @@ const PaperContainer = (
|
|||||||
initRectangleTool,
|
initRectangleTool,
|
||||||
initSupportTool,
|
initSupportTool,
|
||||||
initViewTool,
|
initViewTool,
|
||||||
renderSupportAnnotation2,
|
renderSupportAnnotation,
|
||||||
setup,
|
setup,
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1812,6 +1659,52 @@ const PaperContainer = (
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const syncRenderModeVisuals = useCallback(
|
||||||
|
(targetGroup?: paper.Group | null) => {
|
||||||
|
const group = targetGroup ?? usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
group.children.forEach((item) => {
|
||||||
|
const itemData = item.data || {}
|
||||||
|
const itemType = itemData.type
|
||||||
|
|
||||||
|
if (
|
||||||
|
itemData.name === "pic" ||
|
||||||
|
itemData.id === "support" ||
|
||||||
|
!itemData.fillColor ||
|
||||||
|
!itemData.blankColor ||
|
||||||
|
[
|
||||||
|
"mask",
|
||||||
|
"text",
|
||||||
|
"tag",
|
||||||
|
"groupPath",
|
||||||
|
"pathCircle",
|
||||||
|
"pathBuff",
|
||||||
|
"circle",
|
||||||
|
].includes(itemType)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextFillState = renderMode || itemData.selected ? "fill" : "blank"
|
||||||
|
if (itemData.renderModeFillState === nextFillState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemData.renderModeFillState = nextFillState
|
||||||
|
|
||||||
|
const nextFillColor =
|
||||||
|
renderMode || itemData.selected
|
||||||
|
? itemData.fillColor
|
||||||
|
: itemData.blankColor
|
||||||
|
const pathItem = item as paper.PathItem
|
||||||
|
pathItem.fillColor = nextFillColor
|
||||||
|
? new paper.Color(nextFillColor)
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[renderMode]
|
||||||
|
)
|
||||||
|
|
||||||
// 生成辅助标注结果数据并绘制
|
// 生成辅助标注结果数据并绘制
|
||||||
const saveSupportAnnotationData = useCallback(() => {
|
const saveSupportAnnotationData = useCallback(() => {
|
||||||
const group = usePaperStore.getState().group
|
const group = usePaperStore.getState().group
|
||||||
@@ -2163,6 +2056,7 @@ const PaperContainer = (
|
|||||||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||||||
renderGroupPath()
|
renderGroupPath()
|
||||||
renderTag()
|
renderTag()
|
||||||
|
syncRenderModeVisuals()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2194,6 +2088,7 @@ const PaperContainer = (
|
|||||||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||||||
renderGroupPath()
|
renderGroupPath()
|
||||||
renderTag()
|
renderTag()
|
||||||
|
syncRenderModeVisuals()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2238,6 +2133,7 @@ const PaperContainer = (
|
|||||||
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
if (useBottomToolsStore.getState().activeImage !== expectedImage) return
|
||||||
renderGroupPath()
|
renderGroupPath()
|
||||||
renderTag()
|
renderTag()
|
||||||
|
syncRenderModeVisuals()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof globalThis.requestAnimationFrame === "function") {
|
if (typeof globalThis.requestAnimationFrame === "function") {
|
||||||
@@ -2246,7 +2142,13 @@ const PaperContainer = (
|
|||||||
setTimeout(runBatch, 0)
|
setTimeout(runBatch, 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[clearRenderedAnnotationItems, objectOperations, renderPolygons, renderTag]
|
[
|
||||||
|
clearRenderedAnnotationItems,
|
||||||
|
objectOperations,
|
||||||
|
renderPolygons,
|
||||||
|
renderTag,
|
||||||
|
syncRenderModeVisuals,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const withPaperGroup = useCallback(
|
const withPaperGroup = useCallback(
|
||||||
@@ -2365,6 +2267,7 @@ const PaperContainer = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
|
syncRenderModeVisuals(targetGroup)
|
||||||
resolve(true)
|
resolve(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2403,6 +2306,7 @@ const PaperContainer = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncRenderModeVisuals(targetGroup)
|
||||||
resolve(true)
|
resolve(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2416,10 +2320,125 @@ const PaperContainer = (
|
|||||||
isPendingFrameSwitchStale,
|
isPendingFrameSwitchStale,
|
||||||
objectOperations,
|
objectOperations,
|
||||||
renderPolygons,
|
renderPolygons,
|
||||||
|
syncRenderModeVisuals,
|
||||||
withPaperGroup,
|
withPaperGroup,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!setup || loadingData) return
|
||||||
|
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
syncRenderModeVisuals(group)
|
||||||
|
|
||||||
|
if (renderMode) {
|
||||||
|
usePaperSupportStore.getState().clearAll()
|
||||||
|
useOtherToolsStore.getState().setShowContextMenu({
|
||||||
|
showContextMenu: false,
|
||||||
|
position: { top: 0, left: 0 },
|
||||||
|
imageId: "",
|
||||||
|
operationId: "",
|
||||||
|
id: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
activeImage,
|
||||||
|
activeImageLabel,
|
||||||
|
currentSelectedPathKey,
|
||||||
|
loadingData,
|
||||||
|
renderMode,
|
||||||
|
setup,
|
||||||
|
syncRenderModeVisuals,
|
||||||
|
])
|
||||||
|
|
||||||
|
const stopRenderModePan = useCallback(() => {
|
||||||
|
renderModePanRef.current.active = false
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleRenderModeOverlayMouseDown = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
useOtherToolsStore.getState().setShowContextMenu({
|
||||||
|
showContextMenu: false,
|
||||||
|
position: { top: 0, left: 0 },
|
||||||
|
imageId: "",
|
||||||
|
operationId: "",
|
||||||
|
id: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (event.button !== 1) return
|
||||||
|
|
||||||
|
renderModePanRef.current = {
|
||||||
|
active: true,
|
||||||
|
lastX: event.clientX,
|
||||||
|
lastY: event.clientY,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!renderMode) {
|
||||||
|
stopRenderModePan()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
const currentPan = renderModePanRef.current
|
||||||
|
if (!currentPan.active) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
const deltaX = event.clientX - currentPan.lastX
|
||||||
|
const deltaY = event.clientY - currentPan.lastY
|
||||||
|
if (!deltaX && !deltaY) return
|
||||||
|
|
||||||
|
group.position = group.position.add(new paper.Point(deltaX, deltaY))
|
||||||
|
renderModePanRef.current.lastX = event.clientX
|
||||||
|
renderModePanRef.current.lastY = event.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
|
if (event.button === 1) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
stopRenderModePan()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", handleMouseMove, { passive: false })
|
||||||
|
window.addEventListener("mouseup", handleMouseUp, { passive: false })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp)
|
||||||
|
stopRenderModePan()
|
||||||
|
}
|
||||||
|
}, [renderMode, stopRenderModePan])
|
||||||
|
|
||||||
|
const getTargetGroupPosition = useCallback(
|
||||||
|
({
|
||||||
|
defaultPosition,
|
||||||
|
preservedPosition,
|
||||||
|
canPreserve,
|
||||||
|
}: {
|
||||||
|
defaultPosition: paper.Point
|
||||||
|
preservedPosition?: paper.Point | null
|
||||||
|
canPreserve?: boolean
|
||||||
|
}) => {
|
||||||
|
if (saveCurrentScale && canPreserve && preservedPosition) {
|
||||||
|
return clonePaperPoint(preservedPosition) ?? defaultPosition.clone()
|
||||||
|
}
|
||||||
|
return defaultPosition.clone()
|
||||||
|
},
|
||||||
|
[saveCurrentScale]
|
||||||
|
)
|
||||||
|
|
||||||
const preparePendingFrameSwitch = useCallback(
|
const preparePendingFrameSwitch = useCallback(
|
||||||
async (imageName: string, token: number) => {
|
async (imageName: string, token: number) => {
|
||||||
if (
|
if (
|
||||||
@@ -2437,6 +2456,12 @@ const PaperContainer = (
|
|||||||
clearPendingFrameSwitch(token)
|
clearPendingFrameSwitch(token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const preserveGroupPosition =
|
||||||
|
saveCurrentScale &&
|
||||||
|
currentVisibleGroup.getItems({ data: { name: "pic" } }).length > 0
|
||||||
|
const preservedGroupPosition = preserveGroupPosition
|
||||||
|
? clonePaperPoint(currentVisibleGroup.position)
|
||||||
|
: null
|
||||||
|
|
||||||
const stagingGroup = new paper.Group({
|
const stagingGroup = new paper.Group({
|
||||||
applyMatrix: false,
|
applyMatrix: false,
|
||||||
@@ -2521,10 +2546,15 @@ const PaperContainer = (
|
|||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
)
|
)
|
||||||
stagingGroup.position = new paper.Point(
|
const centeredGroupPosition = new paper.Point(
|
||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
)
|
)
|
||||||
|
stagingGroup.position = getTargetGroupPosition({
|
||||||
|
defaultPosition: centeredGroupPosition,
|
||||||
|
preservedPosition: preservedGroupPosition,
|
||||||
|
canPreserve: preserveGroupPosition,
|
||||||
|
})
|
||||||
raster.sendToBack()
|
raster.sendToBack()
|
||||||
|
|
||||||
const imageData = useLabelStore.getState().label.get(imageName)
|
const imageData = useLabelStore.getState().label.get(imageName)
|
||||||
@@ -2569,6 +2599,7 @@ const PaperContainer = (
|
|||||||
[
|
[
|
||||||
clearPendingFrameSwitch,
|
clearPendingFrameSwitch,
|
||||||
commitFrameSwitch,
|
commitFrameSwitch,
|
||||||
|
getTargetGroupPosition,
|
||||||
imgSize?.clientHeight,
|
imgSize?.clientHeight,
|
||||||
imgSize?.clientWidth,
|
imgSize?.clientWidth,
|
||||||
isPendingFrameSwitchStale,
|
isPendingFrameSwitchStale,
|
||||||
@@ -2576,6 +2607,7 @@ const PaperContainer = (
|
|||||||
reconcileRasterScaleForImage,
|
reconcileRasterScaleForImage,
|
||||||
renderImagePolygonsBatchedToGroup,
|
renderImagePolygonsBatchedToGroup,
|
||||||
renderTag,
|
renderTag,
|
||||||
|
saveCurrentScale,
|
||||||
setGroup,
|
setGroup,
|
||||||
setRasterPath,
|
setRasterPath,
|
||||||
setRasterSize,
|
setRasterSize,
|
||||||
@@ -2660,6 +2692,11 @@ const PaperContainer = (
|
|||||||
if (activeImage && projectDetail?.id) {
|
if (activeImage && projectDetail?.id) {
|
||||||
const requestId = ++rasterLoadRequestIdRef.current
|
const requestId = ++rasterLoadRequestIdRef.current
|
||||||
const requestImage = activeImage
|
const requestImage = activeImage
|
||||||
|
const preserveGroupPosition =
|
||||||
|
saveCurrentScale && getPicRasters(paperGroup).length > 0
|
||||||
|
const preservedGroupPosition = preserveGroupPosition
|
||||||
|
? clonePaperPoint(paperGroup.position)
|
||||||
|
: null
|
||||||
rasterLoadingImageRef.current = requestImage
|
rasterLoadingImageRef.current = requestImage
|
||||||
cancelRasterFadeTransition()
|
cancelRasterFadeTransition()
|
||||||
getPicRasters(paperGroup).forEach((item) => {
|
getPicRasters(paperGroup).forEach((item) => {
|
||||||
@@ -2818,6 +2855,10 @@ const PaperContainer = (
|
|||||||
raster.bounds.width / 2,
|
raster.bounds.width / 2,
|
||||||
raster.bounds.height / 2
|
raster.bounds.height / 2
|
||||||
)
|
)
|
||||||
|
const centeredGroupPosition = new paper.Point(
|
||||||
|
raster.bounds.width / 2,
|
||||||
|
raster.bounds.height / 2
|
||||||
|
)
|
||||||
raster.visible = true
|
raster.visible = true
|
||||||
const previousRasters = getPicRasters(paperGroup).filter(
|
const previousRasters = getPicRasters(paperGroup).filter(
|
||||||
(item) => item !== raster
|
(item) => item !== raster
|
||||||
@@ -2871,10 +2912,11 @@ const PaperContainer = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
usePaperStore.getState().group!.position = new paper.Point(
|
paperGroup.position = getTargetGroupPosition({
|
||||||
raster.bounds.width / 2,
|
defaultPosition: centeredGroupPosition,
|
||||||
raster.bounds.height / 2
|
preservedPosition: preservedGroupPosition,
|
||||||
)
|
canPreserve: preserveGroupPosition,
|
||||||
|
})
|
||||||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||||||
raster.sendToBack()
|
raster.sendToBack()
|
||||||
renderedImageRef.current = requestImage
|
renderedImageRef.current = requestImage
|
||||||
@@ -2896,9 +2938,11 @@ const PaperContainer = (
|
|||||||
cancelRasterFadeTransition,
|
cancelRasterFadeTransition,
|
||||||
decodeFrameImage,
|
decodeFrameImage,
|
||||||
getPicRasters,
|
getPicRasters,
|
||||||
|
getTargetGroupPosition,
|
||||||
imgSize?.clientHeight,
|
imgSize?.clientHeight,
|
||||||
imgSize?.clientWidth,
|
imgSize?.clientWidth,
|
||||||
projectDetail?.id,
|
projectDetail?.id,
|
||||||
|
saveCurrentScale,
|
||||||
setRasterPath,
|
setRasterPath,
|
||||||
setRasterScale,
|
setRasterScale,
|
||||||
setRasterSize,
|
setRasterSize,
|
||||||
@@ -3008,6 +3052,9 @@ const PaperContainer = (
|
|||||||
: undefined) ||
|
: undefined) ||
|
||||||
(rasterItems.length ? rasterItems[rasterItems.length - 1] : undefined)
|
(rasterItems.length ? rasterItems[rasterItems.length - 1] : undefined)
|
||||||
if (!currentRaster) return false
|
if (!currentRaster) return false
|
||||||
|
const preservedGroupPosition = saveCurrentScale
|
||||||
|
? clonePaperPoint(group.position)
|
||||||
|
: null
|
||||||
|
|
||||||
const rasterSizeStore = usePaperStore.getState().rasterSize
|
const rasterSizeStore = usePaperStore.getState().rasterSize
|
||||||
const rawRasterSize = rasterSizeStore[activeImage] ?? [
|
const rawRasterSize = rasterSizeStore[activeImage] ?? [
|
||||||
@@ -3061,10 +3108,15 @@ const PaperContainer = (
|
|||||||
currentRaster.bounds.width / 2,
|
currentRaster.bounds.width / 2,
|
||||||
currentRaster.bounds.height / 2
|
currentRaster.bounds.height / 2
|
||||||
)
|
)
|
||||||
group.position = new paper.Point(
|
const centeredGroupPosition = new paper.Point(
|
||||||
currentRaster.bounds.width / 2,
|
currentRaster.bounds.width / 2,
|
||||||
currentRaster.bounds.height / 2
|
currentRaster.bounds.height / 2
|
||||||
)
|
)
|
||||||
|
group.position = getTargetGroupPosition({
|
||||||
|
defaultPosition: centeredGroupPosition,
|
||||||
|
preservedPosition: preservedGroupPosition,
|
||||||
|
canPreserve: saveCurrentScale,
|
||||||
|
})
|
||||||
|
|
||||||
setRasterPath(new paper.Path.Rectangle(currentRaster.bounds))
|
setRasterPath(new paper.Path.Rectangle(currentRaster.bounds))
|
||||||
setRasterScale(activeImage, nextScale)
|
setRasterScale(activeImage, nextScale)
|
||||||
@@ -3078,9 +3130,11 @@ const PaperContainer = (
|
|||||||
}, [
|
}, [
|
||||||
activeImage,
|
activeImage,
|
||||||
getPicRasters,
|
getPicRasters,
|
||||||
|
getTargetGroupPosition,
|
||||||
imgSize?.clientHeight,
|
imgSize?.clientHeight,
|
||||||
imgSize?.clientWidth,
|
imgSize?.clientWidth,
|
||||||
renderTag,
|
renderTag,
|
||||||
|
saveCurrentScale,
|
||||||
scaleRenderedAnnotationItemsInPlace,
|
scaleRenderedAnnotationItemsInPlace,
|
||||||
setRasterPath,
|
setRasterPath,
|
||||||
setRasterScale,
|
setRasterScale,
|
||||||
@@ -3926,7 +3980,7 @@ const PaperContainer = (
|
|||||||
renderPolygons,
|
renderPolygons,
|
||||||
handleCrosshairMove,
|
handleCrosshairMove,
|
||||||
renderTrackingAnnotation,
|
renderTrackingAnnotation,
|
||||||
renderSupportAnnotation2,
|
renderSupportAnnotation,
|
||||||
saveSupportAnnotationData,
|
saveSupportAnnotationData,
|
||||||
copyAnnotationDataToMultiFrame,
|
copyAnnotationDataToMultiFrame,
|
||||||
}))
|
}))
|
||||||
@@ -4024,6 +4078,22 @@ const PaperContainer = (
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{renderMode && (
|
||||||
|
<Box
|
||||||
|
aria-hidden
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
}}
|
||||||
|
onMouseDown={handleRenderModeOverlayMouseDown}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 47,
|
||||||
|
background: "rgba(128, 128, 128, 0.24)",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
<Modal
|
<Modal
|
||||||
opened={confirmModalOpened}
|
opened={confirmModalOpened}
|
||||||
|
|||||||
@@ -355,13 +355,10 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
if (operationMap) {
|
if (operationMap) {
|
||||||
const objList = operationMap.get(operationId)
|
const objList = operationMap.get(operationId)
|
||||||
if (objList && objList.length) {
|
if (objList && objList.length) {
|
||||||
let hideFlag = true
|
const hideFlag = objList.every(([id]) => {
|
||||||
objList.forEach(([id]) => {
|
return useObjectStore.getState().pathStatus[`${imgId}${id}`]?.["HIDE"]
|
||||||
const objStatus =
|
? true
|
||||||
useObjectStore.getState().pathStatus[`${imgId}${id}`]
|
: false
|
||||||
if (objStatus && !objStatus["HIDE"]) {
|
|
||||||
hideFlag = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
updateOperationStatus(imgId + operationId, "HIDE", hideFlag)
|
updateOperationStatus(imgId + operationId, "HIDE", hideFlag)
|
||||||
}
|
}
|
||||||
@@ -401,6 +398,39 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
[activeImage]
|
[activeImage]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const activeImageOperations = labelState.get(activeImage) || []
|
||||||
|
|
||||||
|
const isObjectHidden = (imageId: string, objectId: number) => {
|
||||||
|
return pathStatus[imageId + objectId]?.["HIDE"] ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOperationHidden = (imageId: string, operationId: string) => {
|
||||||
|
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
|
||||||
|
return (
|
||||||
|
operationChildren.length > 0 &&
|
||||||
|
operationChildren.every(([objectId]) => isObjectHidden(imageId, objectId))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalObjectCount = activeImageOperations.reduce(
|
||||||
|
(count, operationId) => {
|
||||||
|
return (
|
||||||
|
count + (storeLabel.get(activeImage)?.get(operationId)?.length || 0)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
const areAllObjectsHidden =
|
||||||
|
totalObjectCount > 0 &&
|
||||||
|
activeImageOperations.every((operationId) => {
|
||||||
|
const operationChildren =
|
||||||
|
storeLabel.get(activeImage)?.get(operationId) || []
|
||||||
|
return operationChildren.every(([objectId]) =>
|
||||||
|
isObjectHidden(activeImage, objectId)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// const renderObjectName = useCallback((name: string, input: string) => {
|
// const renderObjectName = useCallback((name: string, input: string) => {
|
||||||
// const index = name?.indexOf(input);
|
// const index = name?.indexOf(input);
|
||||||
// const beforeStr = name?.substring(0, index);
|
// const beforeStr = name?.substring(0, index);
|
||||||
@@ -509,9 +539,22 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<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 justify="space-between" align="center" px="md" py="xs">
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
aria-label={areAllObjectsHidden ? "显示全部对象" : "隐藏全部对象"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!totalObjectCount) return
|
||||||
|
activeImageOperations.forEach((operationId) => {
|
||||||
|
operationHide(operationId, !areAllObjectsHidden)
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
{areAllObjectsHidden ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</ActionIcon>
|
||||||
<Title order={4} size="h4">
|
<Title order={4} size="h4">
|
||||||
对象列表
|
对象列表
|
||||||
</Title>
|
</Title>
|
||||||
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<TextInput
|
<TextInput
|
||||||
w={96}
|
w={96}
|
||||||
@@ -566,13 +609,11 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}}>
|
}}>
|
||||||
<ScrollArea style={{ flex: 1 }} px="md">
|
<ScrollArea style={{ flex: 1 }} px="md">
|
||||||
<Stack gap="xs" pb="md">
|
<Stack gap="xs" pb="md">
|
||||||
{labelState.get(activeImage) &&
|
{activeImageOperations.map((operationId) => {
|
||||||
labelState.get(activeImage)?.map((operationId) => {
|
|
||||||
const operationSchema = getOperationSchema(operationId)
|
const operationSchema = getOperationSchema(operationId)
|
||||||
const isCollapsed =
|
const isCollapsed =
|
||||||
operationStatus[activeImage + operationId]?.["COLLAPSE"]
|
operationStatus[activeImage + operationId]?.["COLLAPSE"]
|
||||||
const isHide =
|
const isHide = isOperationHidden(activeImage, operationId)
|
||||||
operationStatus[activeImage + operationId]?.HIDE
|
|
||||||
const hasSubAttrError = getOperationSubAttrStatus(operationId)
|
const hasSubAttrError = getOperationSubAttrStatus(operationId)
|
||||||
const operationLabel = operationSchema?.label_class
|
const operationLabel = operationSchema?.label_class
|
||||||
const operationChildren =
|
const operationChildren =
|
||||||
@@ -613,14 +654,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() => operationHide(operationId, !isHide)}>
|
||||||
operationHide(operationId, !isHide)
|
{!isHide ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||||
}>
|
|
||||||
{!isHide ? (
|
|
||||||
<Eye size={16} />
|
|
||||||
) : (
|
|
||||||
<EyeOff size={16} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Group
|
<Group
|
||||||
gap={8}
|
gap={8}
|
||||||
@@ -851,8 +886,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
{
|
{
|
||||||
userOpts.find(
|
userOpts.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.value ===
|
item.value === child[2]?.uid
|
||||||
child[2]?.uid
|
|
||||||
)?.label
|
)?.label
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -865,9 +899,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
</Group>
|
</Group>
|
||||||
{child[2]?.first_modified_uid && (
|
{child[2]?.first_modified_uid && (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">
|
<Text size="xs">首次修改:</Text>
|
||||||
首次修改:
|
|
||||||
</Text>
|
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{
|
||||||
userOpts.find(
|
userOpts.find(
|
||||||
@@ -883,17 +915,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
(child[2]
|
(child[2]
|
||||||
?.first_modified_timestamp ||
|
?.first_modified_timestamp ||
|
||||||
0) * 1000
|
0) * 1000
|
||||||
).format(
|
).format("YYYY-MM-DD HH:mm:ss")}
|
||||||
"YYYY-MM-DD HH:mm:ss"
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
{child[2]?.last_modified_uid && (
|
{child[2]?.last_modified_uid && (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">
|
<Text size="xs">最新修改:</Text>
|
||||||
最新修改:
|
|
||||||
</Text>
|
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{
|
||||||
userOpts.find(
|
userOpts.find(
|
||||||
@@ -909,9 +937,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
(child[2]
|
(child[2]
|
||||||
?.last_modified_timestamp ||
|
?.last_modified_timestamp ||
|
||||||
0) * 1000
|
0) * 1000
|
||||||
).format(
|
).format("YYYY-MM-DD HH:mm:ss")}
|
||||||
"YYYY-MM-DD HH:mm:ss"
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ const TopTools = (
|
|||||||
const {
|
const {
|
||||||
isView,
|
isView,
|
||||||
setIsView,
|
setIsView,
|
||||||
|
renderMode,
|
||||||
|
setRenderMode,
|
||||||
saveCurrentScale,
|
saveCurrentScale,
|
||||||
setSaveCurrentScale,
|
setSaveCurrentScale,
|
||||||
showMultiFrame,
|
showMultiFrame,
|
||||||
@@ -1896,10 +1898,16 @@ const TopTools = (
|
|||||||
<SquarePen style={{ width: 20, height: 20 }} />
|
<SquarePen style={{ width: 20, height: 20 }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
<Eclipse
|
<Tooltip label={renderMode ? "关闭渲染" : "开启渲染"}>
|
||||||
style={{ width: 20, height: 20, display: "none" }}
|
<ActionIcon
|
||||||
color="gray"
|
variant="transparent"
|
||||||
/>
|
c={renderMode ? "var(--mantine-color-text)" : "gray"}
|
||||||
|
onClick={() => {
|
||||||
|
setRenderMode(!renderMode)
|
||||||
|
}}>
|
||||||
|
<Eclipse style={{ width: 20, height: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
<CopySlash style={{ width: 20, height: 20, display: "none" }} />
|
<CopySlash style={{ width: 20, height: 20, display: "none" }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { Comment } from "./api/label/typing"
|
|
||||||
import { Project } from "./api/project/typing"
|
|
||||||
import { notifications } from "@mantine/notifications"
|
import { notifications } from "@mantine/notifications"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { DispatchWithoutAction } from "react"
|
import { DispatchWithoutAction } from "react"
|
||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
import { usePermissionStore } from "./store/auth"
|
import { Comment } from "./api/label/typing"
|
||||||
|
import { Project } from "./api/project/typing"
|
||||||
import {
|
import {
|
||||||
initialDetail,
|
initialDetail,
|
||||||
useKeyEventStore,
|
useKeyEventStore,
|
||||||
useLabelStore,
|
useLabelStore,
|
||||||
useObjectStore,
|
useObjectStore,
|
||||||
} from "./store"
|
} from "./store"
|
||||||
|
import { usePermissionStore } from "./store/auth"
|
||||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||||
import { useKeyboardStore } from "./useKeyBoardStore"
|
import { useKeyboardStore } from "./useKeyBoardStore"
|
||||||
import { useOtherToolsStore } from "./useOtherToolsStore"
|
import { useOtherToolsStore } from "./useOtherToolsStore"
|
||||||
@@ -318,7 +318,250 @@ const hidePaperContextMenu = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAPER_OBJECT_TYPES = ["rectangle", "polygon", "brush", "point"] as const
|
||||||
|
const PAPER_CONTEXT_HIT_TYPES = [
|
||||||
|
...PAPER_OBJECT_TYPES,
|
||||||
|
"circle",
|
||||||
|
"mask",
|
||||||
|
"pathBuff",
|
||||||
|
"pathCircle",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const isPaperObjectType = (type: unknown) =>
|
||||||
|
typeof type === "string" &&
|
||||||
|
PAPER_OBJECT_TYPES.includes(type as (typeof PAPER_OBJECT_TYPES)[number])
|
||||||
|
|
||||||
|
const isPaperContextHitType = (type: unknown) =>
|
||||||
|
typeof type === "string" &&
|
||||||
|
PAPER_CONTEXT_HIT_TYPES.includes(
|
||||||
|
type as (typeof PAPER_CONTEXT_HIT_TYPES)[number]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getPaperCursorByMode = (
|
||||||
|
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
["polygon", "rectangle", "brush", "point", "support"].includes(mode || "")
|
||||||
|
) {
|
||||||
|
return "crosshair"
|
||||||
|
}
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncPaperCursorByMode = (
|
||||||
|
mode: PaperState["mode"] = usePaperStore.getState().mode
|
||||||
|
) => {
|
||||||
|
if (usePaperStore.getState().el) {
|
||||||
|
usePaperStore.getState().el!.style.cursor = getPaperCursorByMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearPaperSelectedObjects = () => {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
group
|
||||||
|
.getItems({
|
||||||
|
match: (item: paper.Item) => !!item.data?.selected,
|
||||||
|
})
|
||||||
|
.forEach((item) => {
|
||||||
|
item.data.selected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
usePaperSupportStore.getState().clearAll()
|
||||||
|
|
||||||
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
|
const selectedIds = [
|
||||||
|
...(useObjectStore.getState().selectedPath[activeImage] || []),
|
||||||
|
]
|
||||||
|
|
||||||
|
selectedIds.forEach((id) => {
|
||||||
|
usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(id)
|
||||||
|
.forEach((item) => {
|
||||||
|
if (
|
||||||
|
(item instanceof paper.Path || item instanceof paper.CompoundPath) &&
|
||||||
|
["rectangle", "polygon"].includes(item.data?.type || "")
|
||||||
|
) {
|
||||||
|
item.fillColor = item.data?.blankColor || item.fillColor || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
selectedIds.forEach((id) => {
|
||||||
|
useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
|
||||||
|
if (item.data.type === "rectangle" && item instanceof paper.Path) {
|
||||||
|
item.data.selected = true
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||||
|
usePaperSupportStore.getState().handleCircles(item)
|
||||||
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
|
} else if (item.data.type === "brush") {
|
||||||
|
item.data.selected = true
|
||||||
|
if (item instanceof paper.Path) {
|
||||||
|
usePaperSupportStore.getState().handleMask(item)
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||||
|
usePaperSupportStore.getState().handleCircles(item)
|
||||||
|
}
|
||||||
|
} else if (item.data.type === "point" && item instanceof paper.Path) {
|
||||||
|
item.data.selected = true
|
||||||
|
usePaperSupportStore.getState().handleMask(item)
|
||||||
|
usePaperSupportStore.getState().handleText(item)
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(item)
|
||||||
|
usePaperSupportStore.getState().handleCircles(item)
|
||||||
|
} else if (item.data.type === "polygon") {
|
||||||
|
item.data.selected = true
|
||||||
|
if (!(item instanceof paper.Path) || !item.data?.isHollowPolygon) {
|
||||||
|
item.fillColor = item.data.fillColor || item.fillColor || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.bringToFront()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaperSelectionPath = (
|
||||||
|
id: number,
|
||||||
|
localPoint: paper.Point,
|
||||||
|
preferredItem?: paper.Item | null
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
preferredItem instanceof paper.Path &&
|
||||||
|
isPaperObjectType(preferredItem.data?.type)
|
||||||
|
) {
|
||||||
|
return preferredItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectPaths = usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(id)
|
||||||
|
.filter(
|
||||||
|
(item): item is paper.Path =>
|
||||||
|
item instanceof paper.Path && isPaperObjectType(item.data?.type)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!objectPaths.length) return null
|
||||||
|
if (objectPaths.length === 1) return objectPaths[0]
|
||||||
|
|
||||||
|
let nearestPath = objectPaths[0]
|
||||||
|
for (let index = 1; index < objectPaths.length; index += 1) {
|
||||||
|
const item = objectPaths[index]
|
||||||
|
if (
|
||||||
|
item.getNearestPoint(localPoint).getDistance(localPoint) <
|
||||||
|
nearestPath.getNearestPoint(localPoint).getDistance(localPoint)
|
||||||
|
) {
|
||||||
|
nearestPath = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nearestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const trySelectPaperObjectByPoint = (clickPoint: paper.Point) => {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return false
|
||||||
|
|
||||||
|
const hitResult = group.hitTest(clickPoint, {
|
||||||
|
segments: true,
|
||||||
|
stroke: true,
|
||||||
|
curves: true,
|
||||||
|
fill: true,
|
||||||
|
guides: false,
|
||||||
|
tolerance: usePaperStore.getState().paperScope
|
||||||
|
? 8 / useTopToolsStore.getState().scale
|
||||||
|
: 0,
|
||||||
|
match: (hit: paper.HitResult) =>
|
||||||
|
!!hit.item.data?.id && isPaperContextHitType(hit.item.data?.type),
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = hitResult?.item?.data?.id
|
||||||
|
if (!id) return false
|
||||||
|
|
||||||
|
clearPaperSelectedObjects()
|
||||||
|
|
||||||
|
const localPoint = group.globalToLocal(clickPoint)
|
||||||
|
const selectedPath = getPaperSelectionPath(id, localPoint, hitResult?.item)
|
||||||
|
const operationId = (
|
||||||
|
selectedPath?.data?.operationId ||
|
||||||
|
usePaperStore.getState().getItemById(id)?.data?.operationId ||
|
||||||
|
hitResult?.item?.data?.operationId ||
|
||||||
|
""
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
usePaperStore
|
||||||
|
.getState()
|
||||||
|
.getItemsById(id)
|
||||||
|
.forEach((item) => {
|
||||||
|
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath))
|
||||||
|
return
|
||||||
|
if (!isPaperObjectType(item.data?.type)) return
|
||||||
|
|
||||||
|
if (item.data.type === "polygon") {
|
||||||
|
applyPaperSelectedPath(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
item instanceof paper.CompoundPath ||
|
||||||
|
(selectedPath && item === selectedPath)
|
||||||
|
) {
|
||||||
|
applyPaperSelectedPath(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeImage = useBottomToolsStore.getState().activeImage
|
||||||
|
useObjectStore.getState().updateSelectedPath(activeImage, "ADD", id)
|
||||||
|
|
||||||
|
const category = useTopToolsStore
|
||||||
|
.getState()
|
||||||
|
.objectOperations.find(
|
||||||
|
(item) => item.category_id.toString() === operationId
|
||||||
|
)
|
||||||
|
const { top, left } = getShowContextMenuPosition(
|
||||||
|
clickPoint,
|
||||||
|
(category || {}) as Project.LabelSchemaList,
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
)
|
||||||
|
|
||||||
|
useOtherToolsStore.getState().setShowContextMenu({
|
||||||
|
showContextMenu: true,
|
||||||
|
position: { top, left },
|
||||||
|
imageId:
|
||||||
|
selectedPath?.data?.imageId ||
|
||||||
|
hitResult?.item?.data?.imageId ||
|
||||||
|
activeImage,
|
||||||
|
operationId,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
let isMiddleMousePanning = false
|
let isMiddleMousePanning = false
|
||||||
|
let hoveredBufferPathInfo: {
|
||||||
|
id: number
|
||||||
|
index: number
|
||||||
|
path: paper.Path
|
||||||
|
clearHighlight: () => void
|
||||||
|
scheduleClear: () => void
|
||||||
|
cancelScheduledClear: () => void
|
||||||
|
} | null = null
|
||||||
|
let hoveredPathCircleInfo: {
|
||||||
|
id: number
|
||||||
|
index: number
|
||||||
|
item: paper.Path.Circle
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
const clearHoveredBufferPathHighlight = () => {
|
||||||
|
if (hoveredBufferPathInfo) {
|
||||||
|
hoveredBufferPathInfo.clearHighlight()
|
||||||
|
}
|
||||||
|
hoveredBufferPathInfo = null
|
||||||
|
}
|
||||||
|
|
||||||
const startMiddleMousePan = (event?: MouseEvent) => {
|
const startMiddleMousePan = (event?: MouseEvent) => {
|
||||||
if (!event || event.button !== 1) return false
|
if (!event || event.button !== 1) return false
|
||||||
@@ -510,6 +753,8 @@ export const getShowContextMenuPosition = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DOUBLE_CLICK_TOLERANCE_PX = 8
|
const DOUBLE_CLICK_TOLERANCE_PX = 8
|
||||||
|
const PAN_DRAG_THRESHOLD_PX = 4
|
||||||
|
const BUFFER_HOVER_STICKY_MS = 180
|
||||||
|
|
||||||
const getPaperDoubleClickTolerance = (
|
const getPaperDoubleClickTolerance = (
|
||||||
coordinateSpace: "project" | "local" = "project"
|
coordinateSpace: "project" | "local" = "project"
|
||||||
@@ -525,6 +770,37 @@ const getPaperDoubleClickTolerance = (
|
|||||||
return projectTolerance
|
return projectTolerance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPaperDragThreshold = (
|
||||||
|
coordinateSpace: "project" | "local" = "project"
|
||||||
|
) => {
|
||||||
|
const viewZoom = usePaperStore.getState().paperScope?.view.zoom || 1
|
||||||
|
const projectThreshold = PAN_DRAG_THRESHOLD_PX / viewZoom
|
||||||
|
|
||||||
|
if (coordinateSpace === "local") {
|
||||||
|
const groupScale = usePaperStore.getState().group?.scaling.x || 1
|
||||||
|
return projectThreshold / groupScale
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDeletePointLimitNotification = (
|
||||||
|
type: "polygon" | "brush" | "point"
|
||||||
|
) => {
|
||||||
|
const messageMap = {
|
||||||
|
polygon: "当前对象至少保留3个点,无法继续删除",
|
||||||
|
brush: "当前对象至少保留2个点,无法继续删除",
|
||||||
|
point: "当前对象至少保留1个点,无法继续删除",
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
id: "label-delete-point-limit",
|
||||||
|
color: "yellow",
|
||||||
|
autoClose: 1500,
|
||||||
|
message: messageMap[type],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const isPaperDoubleClick = (
|
const isPaperDoubleClick = (
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
lastPoint: paper.Point | null | undefined,
|
lastPoint: paper.Point | null | undefined,
|
||||||
@@ -668,6 +944,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
let panTool = paperPanTool
|
let panTool = paperPanTool
|
||||||
let lastPoint: paper.Point
|
let lastPoint: paper.Point
|
||||||
let isChanged: boolean = false
|
let isChanged: boolean = false
|
||||||
|
let hasExceededPanDragThreshold = false
|
||||||
|
|
||||||
// const handleSelected = (path: paper.Path) => {
|
// const handleSelected = (path: paper.Path) => {
|
||||||
// // path.blendMode = "subtract";
|
// // path.blendMode = "subtract";
|
||||||
@@ -805,6 +1082,96 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
.updateSelectedPath(useBottomToolsStore.getState().activeImage, "ADD")
|
.updateSelectedPath(useBottomToolsStore.getState().activeImage, "ADD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPanHitTolerance = (baseTolerance = 8) =>
|
||||||
|
usePaperStore.getState().paperScope
|
||||||
|
? baseTolerance / useTopToolsStore.getState().scale
|
||||||
|
: 0
|
||||||
|
|
||||||
|
false && getPanHitTolerance
|
||||||
|
|
||||||
|
const createPanHitOptions = (
|
||||||
|
tolerance: number,
|
||||||
|
match: (hit: paper.HitResult) => boolean
|
||||||
|
) => ({
|
||||||
|
segments: true,
|
||||||
|
stroke: true,
|
||||||
|
curves: true,
|
||||||
|
fill: true,
|
||||||
|
guides: false,
|
||||||
|
tolerance,
|
||||||
|
match,
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvePanHitResult = (projectPoint: paper.Point) => {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return null
|
||||||
|
|
||||||
|
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
|
||||||
|
? hoveredPathCircleInfo
|
||||||
|
: null
|
||||||
|
if (hoveredCircleInfo) {
|
||||||
|
const pointHit = group.hitTest(
|
||||||
|
projectPoint,
|
||||||
|
createPanHitOptions(0, (hit) => {
|
||||||
|
return (
|
||||||
|
hit.item.data?.type === "pathCircle" &&
|
||||||
|
hit.item.data?.id === hoveredCircleInfo.id &&
|
||||||
|
hit.item.data?.index === hoveredCircleInfo.index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (pointHit) return pointHit
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||||||
|
? hoveredBufferPathInfo
|
||||||
|
: null
|
||||||
|
if (hoveredBufferInfo) {
|
||||||
|
const edgeHit = group.hitTest(
|
||||||
|
projectPoint,
|
||||||
|
createPanHitOptions(0, (hit) => {
|
||||||
|
return (
|
||||||
|
hit.item.data?.type === "pathBuff" &&
|
||||||
|
hit.item.data?.id === hoveredBufferInfo.id &&
|
||||||
|
hit.item.data?.index === hoveredBufferInfo.index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (edgeHit) return edgeHit
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.hitTest(
|
||||||
|
projectPoint,
|
||||||
|
createPanHitOptions(0, (hit) =>
|
||||||
|
["rectangle", "polygon", "brush", "point"].includes(
|
||||||
|
hit.item.data?.type || ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCurrentSelection = () => {
|
||||||
|
let shouldClearSupport = false
|
||||||
|
|
||||||
|
usePaperStore
|
||||||
|
.getState()
|
||||||
|
.group!.getItems({
|
||||||
|
data: { selected: true },
|
||||||
|
})
|
||||||
|
.forEach((item) => {
|
||||||
|
if (item.data.selected) {
|
||||||
|
item.data.selected = false
|
||||||
|
shouldClearSupport = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (shouldClearSupport) {
|
||||||
|
usePaperSupportStore.getState().clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelSelect()
|
||||||
|
}
|
||||||
|
|
||||||
panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
panTool.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||||||
if (startMiddleMousePan(e.event)) return
|
if (startMiddleMousePan(e.event)) return
|
||||||
if (pressCtrl) return
|
if (pressCtrl) return
|
||||||
@@ -824,18 +1191,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
point,
|
point,
|
||||||
"local"
|
"local"
|
||||||
)
|
)
|
||||||
|
hasExceededPanDragThreshold = false
|
||||||
|
|
||||||
let hitResult = usePaperStore.getState().group?.hitTest(e.point, {
|
let hitResult = resolvePanHitResult(e.point)
|
||||||
segments: true,
|
|
||||||
stroke: true,
|
|
||||||
curves: true,
|
|
||||||
fill: true,
|
|
||||||
guide: false,
|
|
||||||
tolerance: usePaperStore.getState().paperScope
|
|
||||||
? 8 / useTopToolsStore.getState().scale
|
|
||||||
: 0,
|
|
||||||
})
|
|
||||||
console.log("hitResult", hitResult)
|
console.log("hitResult", hitResult)
|
||||||
|
const hoveredBufferInfo = hoveredBufferPathInfo?.path.parent
|
||||||
|
? hoveredBufferPathInfo
|
||||||
|
: null
|
||||||
|
const hoveredCircleInfo = hoveredPathCircleInfo?.item.parent
|
||||||
|
? hoveredPathCircleInfo
|
||||||
|
: null
|
||||||
let lastTagDataId = -1
|
let lastTagDataId = -1
|
||||||
if (activePath?.data.id) {
|
if (activePath?.data.id) {
|
||||||
lastTagDataId = activePath.data.id
|
lastTagDataId = activePath.data.id
|
||||||
@@ -941,35 +1306,26 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
|
|
||||||
if (pressShift) {
|
if (pressShift) {
|
||||||
} else {
|
} else {
|
||||||
// not press shift
|
const selectedIds =
|
||||||
let selectedItems = usePaperStore.getState().group!.getItems({
|
useObjectStore.getState().selectedPath[
|
||||||
data: { selected: true },
|
useBottomToolsStore.getState().activeImage
|
||||||
})
|
] || []
|
||||||
if (selectedItems && selectedItems.length) {
|
const hitId = hitResult?.item?.data?.id
|
||||||
selectedItems.forEach((item) => {
|
const keepHoveredPointSelection =
|
||||||
if (item.data.selected) {
|
!!hoveredCircleInfo &&
|
||||||
let itemHitResult = item.hitTest(point, {
|
selectedIds.length === 1 &&
|
||||||
segments: true,
|
selectedIds[0] === hoveredCircleInfo.id
|
||||||
stroke: true,
|
const keepHoveredEdgeSelection =
|
||||||
curves: true,
|
!!hoveredBufferInfo &&
|
||||||
fill: true,
|
selectedIds.length === 1 &&
|
||||||
guide: false,
|
selectedIds[0] === hoveredBufferInfo.id
|
||||||
tolerance: usePaperStore.getState().paperScope
|
const keepCurrentSelection =
|
||||||
? 8 / usePaperStore.getState().paperScope!.view.zoom
|
keepHoveredPointSelection ||
|
||||||
: 0,
|
keepHoveredEdgeSelection ||
|
||||||
})
|
(hitId && selectedIds.length === 1 && selectedIds[0] === hitId)
|
||||||
|
|
||||||
if (!itemHitResult) {
|
if (!keepCurrentSelection) {
|
||||||
item.data.selected = false
|
clearCurrentSelection()
|
||||||
usePaperSupportStore.getState().clearAll()
|
|
||||||
|
|
||||||
handleCancelSelect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// item.data.selected = false;
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
handleCancelSelect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,6 +1564,56 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
id: data.id,
|
id: data.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
isDoubleClick &&
|
||||||
|
hoveredBufferInfo &&
|
||||||
|
hoveredBufferInfo.id === activePath?.data?.id &&
|
||||||
|
["polygon", "brush", "point"].includes(activePath?.data?.type || "")
|
||||||
|
) {
|
||||||
|
let currentPath: paper.Path | null = null
|
||||||
|
if (activePath instanceof paper.CompoundPath) {
|
||||||
|
currentPath =
|
||||||
|
(activePath.children as paper.Path[]).find(
|
||||||
|
(item) => item.data.selected
|
||||||
|
) || null
|
||||||
|
} else if (activePath instanceof paper.Path) {
|
||||||
|
currentPath = activePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPath) {
|
||||||
|
hitIndex = hoveredBufferInfo.index
|
||||||
|
const insertPoint = currentPath.getNearestPoint(point)
|
||||||
|
currentPath.insert(hitIndex + 1, insertPoint)
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(currentPath)
|
||||||
|
isChanged = true
|
||||||
|
panMode = "stroke"
|
||||||
|
hitIndex = hitIndex + 1
|
||||||
|
|
||||||
|
if (currentPath.data.type === "brush") {
|
||||||
|
usePaperSupportStore.getState().handleMask(currentPath)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(currentPath)
|
||||||
|
usePaperSupportStore.getState().handleCircles(currentPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (hitResult && hitResult.type === "fill") {
|
} else if (hitResult && hitResult.type === "fill") {
|
||||||
if (
|
if (
|
||||||
hitResult &&
|
hitResult &&
|
||||||
@@ -1242,28 +1648,30 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
// doubleclick
|
// doubleclick
|
||||||
if (isDoubleClick) {
|
if (isDoubleClick) {
|
||||||
if (activePath instanceof paper.CompoundPath) {
|
if (activePath instanceof paper.CompoundPath) {
|
||||||
;(activePath.children as paper.Path[]).forEach((item) => {
|
const selectedChild =
|
||||||
const childHitResult = item.hitTest(point, {
|
(activePath.children as paper.Path[]).find(
|
||||||
segments: true,
|
(item) => item.data.selected
|
||||||
stroke: true,
|
) ||
|
||||||
curves: true,
|
(activePath.children as paper.Path[]).reduce(
|
||||||
fill: true,
|
(nearest, item) => {
|
||||||
guide: false,
|
if (!nearest) return item
|
||||||
tolerance: usePaperStore.getState().paperScope
|
return item.getNearestPoint(point).getDistance(point) <
|
||||||
? 8 / usePaperStore.getState().paperScope!.view.zoom
|
nearest.getNearestPoint(point).getDistance(point)
|
||||||
: 0,
|
? item
|
||||||
})
|
: nearest
|
||||||
if (childHitResult) {
|
},
|
||||||
if (childHitResult.location.point) {
|
null as paper.Path | null
|
||||||
item.insert(hitIndex + 1, childHitResult.location.point)
|
)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(item)
|
|
||||||
usePaperSupportStore.getState().handleCircles(item)
|
if (selectedChild) {
|
||||||
|
const insertPoint = selectedChild.getNearestPoint(point)
|
||||||
|
selectedChild.insert(hitIndex + 1, insertPoint)
|
||||||
|
usePaperSupportStore.getState().handleBufferPaths(selectedChild)
|
||||||
|
usePaperSupportStore.getState().handleCircles(selectedChild)
|
||||||
isChanged = true
|
isChanged = true
|
||||||
panMode = "stroke"
|
panMode = "stroke"
|
||||||
hitIndex = hitIndex + 1
|
hitIndex = hitIndex + 1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
activePath.insert(hitIndex + 1, hitResult.location.point)
|
activePath.insert(hitIndex + 1, hitResult.location.point)
|
||||||
activePath?.data?.type === "point" &&
|
activePath?.data?.type === "point" &&
|
||||||
@@ -1346,8 +1754,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
let checkLength = 3
|
let checkLength = 3
|
||||||
if (item?.data?.type === "brush") checkLength = 2
|
if (item?.data?.type === "brush") checkLength = 2
|
||||||
if (item?.data?.type === "point") checkLength = 1
|
if (item?.data?.type === "point") checkLength = 1
|
||||||
if (item.segments && item.segments.length <= checkLength)
|
if (item.segments && item.segments.length <= checkLength) {
|
||||||
|
if (
|
||||||
|
item?.data?.type === "polygon" ||
|
||||||
|
item?.data?.type === "brush" ||
|
||||||
|
item?.data?.type === "point"
|
||||||
|
) {
|
||||||
|
showDeletePointLimitNotification(item.data.type)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
|
}
|
||||||
;(childHitResult.item as paper.Path).removeSegment(hitIndex)
|
;(childHitResult.item as paper.Path).removeSegment(hitIndex)
|
||||||
usePaperSupportStore
|
usePaperSupportStore
|
||||||
.getState()
|
.getState()
|
||||||
@@ -1367,8 +1783,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (
|
if (
|
||||||
activePath.segments &&
|
activePath.segments &&
|
||||||
activePath.segments.length <= checkLength
|
activePath.segments.length <= checkLength
|
||||||
)
|
) {
|
||||||
|
if (
|
||||||
|
activePath?.data?.type === "polygon" ||
|
||||||
|
activePath?.data?.type === "brush" ||
|
||||||
|
activePath?.data?.type === "point"
|
||||||
|
) {
|
||||||
|
showDeletePointLimitNotification(activePath.data.type)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
activePath.removeSegment(hitIndex)
|
activePath.removeSegment(hitIndex)
|
||||||
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
usePaperSupportStore.getState().handleBufferPaths(activePath)
|
||||||
@@ -1517,6 +1941,20 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
point = usePaperStore.getState().group!.globalToLocal(e.point)
|
point = usePaperStore.getState().group!.globalToLocal(e.point)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
activePath &&
|
||||||
|
["fill", "stroke", "segment"].includes(panMode) &&
|
||||||
|
!hasExceededPanDragThreshold
|
||||||
|
) {
|
||||||
|
const dragDistance = e.point.getDistance(e.downPoint)
|
||||||
|
if (dragDistance < getPaperDragThreshold()) return
|
||||||
|
|
||||||
|
hasExceededPanDragThreshold = true
|
||||||
|
if (usePaperStore.getState().el) {
|
||||||
|
usePaperStore.getState().el!.style.cursor = "move"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let movePath = (path: paper.Path) => {
|
let movePath = (path: paper.Path) => {
|
||||||
const currentGroupScaling = usePaperStore.getState().group!.scaling
|
const currentGroupScaling = usePaperStore.getState().group!.scaling
|
||||||
const delta = {
|
const delta = {
|
||||||
@@ -1719,6 +2157,10 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
panTool.onMouseUp = (_e: paper.ToolEvent) => {
|
panTool.onMouseUp = (_e: paper.ToolEvent) => {
|
||||||
if (stopMiddleMousePan()) return
|
if (stopMiddleMousePan()) return
|
||||||
if (useTopToolsStore.getState().isView) return
|
if (useTopToolsStore.getState().isView) return
|
||||||
|
hasExceededPanDragThreshold = false
|
||||||
|
if (usePaperStore.getState().el) {
|
||||||
|
usePaperStore.getState().el!.style.cursor = "default"
|
||||||
|
}
|
||||||
|
|
||||||
let newDrawPath: paper.Path | paper.CompoundPath | null = null
|
let newDrawPath: paper.Path | paper.CompoundPath | null = null
|
||||||
|
|
||||||
@@ -3311,10 +3753,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (startMiddleMousePan(e.event)) return
|
if (startMiddleMousePan(e.event)) return
|
||||||
if (e.event.button === 2) {
|
if (e.event.button === 2) {
|
||||||
e.event.preventDefault()
|
e.event.preventDefault()
|
||||||
|
if (polygon || points.length) {
|
||||||
hidePaperContextMenu()
|
hidePaperContextMenu()
|
||||||
undoPolygonPoint()
|
undoPolygonPoint()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!trySelectPaperObjectByPoint(e.point)) {
|
||||||
|
hidePaperContextMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (e.event.button !== 0) return
|
if (e.event.button !== 0) return
|
||||||
isMouseDown = true
|
isMouseDown = true
|
||||||
|
|
||||||
@@ -3894,8 +4342,18 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
let rect: paper.Path.Rectangle | null
|
let rect: paper.Path.Rectangle | null
|
||||||
let rectangleTool = paperRectangleTool
|
let rectangleTool = paperRectangleTool
|
||||||
|
|
||||||
rectangleTool.onMouseDown = (e: { event: MouseEvent }) => {
|
rectangleTool.onMouseDown = (e: {
|
||||||
|
event: MouseEvent
|
||||||
|
point: paper.Point
|
||||||
|
}) => {
|
||||||
if (startMiddleMousePan(e.event)) return
|
if (startMiddleMousePan(e.event)) return
|
||||||
|
if (e.event.button === 2) {
|
||||||
|
e.event.preventDefault()
|
||||||
|
if (!trySelectPaperObjectByPoint(e.point)) {
|
||||||
|
hidePaperContextMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (e.event && e.event.button !== 2)
|
if (e.event && e.event.button !== 2)
|
||||||
useOtherToolsStore.getState().setShowContextMenu({
|
useOtherToolsStore.getState().setShowContextMenu({
|
||||||
showContextMenu: false,
|
showContextMenu: false,
|
||||||
@@ -4244,10 +4702,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (startMiddleMousePan(e.event)) return
|
if (startMiddleMousePan(e.event)) return
|
||||||
if (e.event.button === 2) {
|
if (e.event.button === 2) {
|
||||||
e.event.preventDefault()
|
e.event.preventDefault()
|
||||||
|
if (myPath || points.length) {
|
||||||
hidePaperContextMenu()
|
hidePaperContextMenu()
|
||||||
undoBrushPoint()
|
undoBrushPoint()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!trySelectPaperObjectByPoint(e.point)) {
|
||||||
|
hidePaperContextMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (e.event.button !== 0) return
|
if (e.event.button !== 0) return
|
||||||
|
|
||||||
// 绘制时去除选中辅助
|
// 绘制时去除选中辅助
|
||||||
@@ -4715,10 +5179,16 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
if (startMiddleMousePan(e.event)) return
|
if (startMiddleMousePan(e.event)) return
|
||||||
if (e.event.button === 2) {
|
if (e.event.button === 2) {
|
||||||
e.event.preventDefault()
|
e.event.preventDefault()
|
||||||
|
if (myPath || points.length) {
|
||||||
hidePaperContextMenu()
|
hidePaperContextMenu()
|
||||||
undoPoint()
|
undoPoint()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!trySelectPaperObjectByPoint(e.point)) {
|
||||||
|
hidePaperContextMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (e.event.button !== 0) return
|
if (e.event.button !== 0) return
|
||||||
|
|
||||||
// 绘制时去除选中辅助
|
// 绘制时去除选中辅助
|
||||||
@@ -5199,12 +5669,28 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
)
|
)
|
||||||
circle.scale(newScale / currentScaling)
|
circle.scale(newScale / currentScaling)
|
||||||
circle.onMouseEnter = (_e: any) => {
|
circle.onMouseEnter = (_e: any) => {
|
||||||
|
hoveredPathCircleInfo = {
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
item: circle,
|
||||||
|
}
|
||||||
|
clearHoveredBufferPathHighlight()
|
||||||
circle.strokeWidth = 4
|
circle.strokeWidth = 4
|
||||||
circle.fillColor = new paper.Color("#FFF")
|
circle.fillColor = new paper.Color("#FFF")
|
||||||
|
if (
|
||||||
|
usePaperStore.getState().mode === "pan" &&
|
||||||
|
usePaperStore.getState().el
|
||||||
|
) {
|
||||||
|
usePaperStore.getState().el!.style.cursor = "move"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
circle.onMouseLeave = (_e: any) => {
|
circle.onMouseLeave = (_e: any) => {
|
||||||
|
if (hoveredPathCircleInfo?.item === circle) {
|
||||||
|
hoveredPathCircleInfo = null
|
||||||
|
}
|
||||||
circle.strokeWidth = 2
|
circle.strokeWidth = 2
|
||||||
circle.fillColor = color
|
circle.fillColor = color
|
||||||
|
syncPaperCursorByMode()
|
||||||
}
|
}
|
||||||
return circle
|
return circle
|
||||||
},
|
},
|
||||||
@@ -5217,8 +5703,8 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
segments: [item.point, item.next],
|
segments: [item.point, item.next],
|
||||||
strokeColor: color, // 设置为透明,以便不可见
|
strokeColor: color, // 设置为透明,以便不可见
|
||||||
strokeScaling: false,
|
strokeScaling: false,
|
||||||
strokeWidth: 4, // 设置一个较大的strokeWidth作为缓冲区
|
strokeWidth: 6, // 适度放宽 hover 区域,但避免干扰对象外默认点击
|
||||||
strokeCap: "butt",
|
strokeCap: "round",
|
||||||
onSelect: function (event: { stopPropagation: () => void }) {
|
onSelect: function (event: { stopPropagation: () => void }) {
|
||||||
event.stopPropagation() // 阻止事件冒泡
|
event.stopPropagation() // 阻止事件冒泡
|
||||||
},
|
},
|
||||||
@@ -5232,10 +5718,54 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
// bufferPath.segments[0].handleIn = item.handleIn;
|
// bufferPath.segments[0].handleIn = item.handleIn;
|
||||||
// bufferPath.segments[1].handleOut = item.handleOut;
|
// bufferPath.segments[1].handleOut = item.handleOut;
|
||||||
let edge: paper.Path | null
|
let edge: paper.Path | null
|
||||||
|
let clearTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const cancelScheduledClear = () => {
|
||||||
|
if (clearTimer) {
|
||||||
|
clearTimeout(clearTimer)
|
||||||
|
clearTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clearHighlight = () => {
|
||||||
|
cancelScheduledClear()
|
||||||
|
edge?.remove()
|
||||||
|
edge = null
|
||||||
|
if (hoveredBufferPathInfo?.path === bufferPath) {
|
||||||
|
hoveredBufferPathInfo = null
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
usePaperStore.getState().el &&
|
||||||
|
!hoveredPathCircleInfo?.item.parent &&
|
||||||
|
!hoveredBufferPathInfo
|
||||||
|
) {
|
||||||
|
syncPaperCursorByMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const scheduleClear = () => {
|
||||||
|
cancelScheduledClear()
|
||||||
|
clearTimer = setTimeout(() => {
|
||||||
|
clearHighlight()
|
||||||
|
}, BUFFER_HOVER_STICKY_MS)
|
||||||
|
}
|
||||||
// 鼠标移入时高亮显示
|
// 鼠标移入时高亮显示
|
||||||
bufferPath.onMouseEnter = function (event: {
|
bufferPath.onMouseEnter = function (event: {
|
||||||
stopPropagation: () => void
|
stopPropagation?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
if (hoveredPathCircleInfo?.item.parent) {
|
||||||
|
event.stopPropagation?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancelScheduledClear()
|
||||||
|
hoveredBufferPathInfo = {
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
path: bufferPath,
|
||||||
|
clearHighlight,
|
||||||
|
scheduleClear,
|
||||||
|
cancelScheduledClear,
|
||||||
|
}
|
||||||
|
if (edge && !edge.parent) {
|
||||||
|
edge = null
|
||||||
|
}
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
edge = new paper.Path(
|
edge = new paper.Path(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@@ -5246,27 +5776,33 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
bufferPath.segments[1].point,
|
bufferPath.segments[1].point,
|
||||||
],
|
],
|
||||||
strokeColor: "white", // 初始颜色与原始路径相同
|
strokeColor: "white", // 初始颜色与原始路径相同
|
||||||
strokeWidth: 2,
|
strokeWidth: 3,
|
||||||
strokeScaling: false,
|
strokeScaling: false,
|
||||||
strokeCap: "butt",
|
strokeCap: "round",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: { index, type: "pathBuff" },
|
data: { index, type: "pathBuffHover" },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// edge.bringToFront();
|
// edge.bringToFront();
|
||||||
event.stopPropagation() // 阻止事件冒泡
|
if (usePaperStore.getState().el) {
|
||||||
|
usePaperStore.getState().el!.style.cursor = "crosshair"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
event.stopPropagation?.() // 阻止事件冒泡
|
||||||
|
}
|
||||||
|
|
||||||
// 鼠标移出时取消高亮
|
// 鼠标移出时取消高亮
|
||||||
bufferPath.onMouseLeave = function (event: {
|
bufferPath.onMouseLeave = function (event: {
|
||||||
stopPropagation: () => void
|
stopPropagation?: () => void
|
||||||
}) {
|
}) {
|
||||||
edge?.remove()
|
if (hoveredBufferPathInfo?.path === bufferPath) {
|
||||||
edge = null
|
hoveredBufferPathInfo.scheduleClear()
|
||||||
event.stopPropagation() // 阻止事件冒泡
|
} else {
|
||||||
|
scheduleClear()
|
||||||
|
}
|
||||||
|
event.stopPropagation?.() // 阻止事件冒泡
|
||||||
}
|
}
|
||||||
return bufferPath
|
return bufferPath
|
||||||
},
|
},
|
||||||
@@ -5280,42 +5816,31 @@ export const usePaperStore = create<PaperState>((set) => ({
|
|||||||
switch (modeParam) {
|
switch (modeParam) {
|
||||||
case "pan":
|
case "pan":
|
||||||
usePaperStore.getState().panTool?.activate()
|
usePaperStore.getState().panTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "default"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "polygon":
|
case "polygon":
|
||||||
usePaperStore.getState().polygonTool?.activate()
|
usePaperStore.getState().polygonTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
usePaperStore.getState().rectangleTool?.activate()
|
usePaperStore.getState().rectangleTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "brush":
|
case "brush":
|
||||||
usePaperStore.getState().brushTool?.activate()
|
usePaperStore.getState().brushTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "point":
|
case "point":
|
||||||
usePaperStore.getState().pointTool?.activate()
|
usePaperStore.getState().pointTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case "support":
|
case "support":
|
||||||
usePaperStore.getState().supportTool?.activate()
|
usePaperStore.getState().supportTool?.activate()
|
||||||
if (usePaperStore.getState().el) {
|
syncPaperCursorByMode(modeParam)
|
||||||
usePaperStore.getState().el!.style.cursor = "crosshair"
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
usePaperStore.getState().viewTool?.activate()
|
usePaperStore.getState().viewTool?.activate()
|
||||||
|
syncPaperCursorByMode(modeParam)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ interface TopToolsState {
|
|||||||
// 标注页面是否仅查看
|
// 标注页面是否仅查看
|
||||||
isView: boolean
|
isView: boolean
|
||||||
setIsView: (val: boolean) => void
|
setIsView: (val: boolean) => void
|
||||||
|
renderMode: boolean
|
||||||
|
setRenderMode: (val: boolean) => void
|
||||||
// 绘制模式
|
// 绘制模式
|
||||||
editMode: boolean
|
editMode: boolean
|
||||||
setEditMode: (val: boolean) => void
|
setEditMode: (val: boolean) => void
|
||||||
@@ -81,6 +83,7 @@ interface TopToolsState {
|
|||||||
|
|
||||||
const initialTopToolsState = {
|
const initialTopToolsState = {
|
||||||
isView: false,
|
isView: false,
|
||||||
|
renderMode: false,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
pressA: false,
|
pressA: false,
|
||||||
imageFilter: {
|
imageFilter: {
|
||||||
@@ -114,6 +117,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
...state,
|
...state,
|
||||||
isView: val,
|
isView: val,
|
||||||
})),
|
})),
|
||||||
|
renderMode: initialTopToolsState.renderMode,
|
||||||
|
setRenderMode: (val: boolean) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
renderMode: val,
|
||||||
|
})),
|
||||||
editMode: initialTopToolsState.editMode,
|
editMode: initialTopToolsState.editMode,
|
||||||
setEditMode: (val: boolean) =>
|
setEditMode: (val: boolean) =>
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
|
|||||||
@@ -195,11 +195,14 @@ const settingDataTableStyles = {
|
|||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
maxWidth: "100%",
|
||||||
|
minWidth: 0,
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
color: "#31373D",
|
color: "#31373D",
|
||||||
borderBottom: "1px solid #E8EDF3",
|
borderBottom: "1px solid #E8EDF3",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
boxSizing: "border-box",
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -307,6 +310,7 @@ export const settingTabsStyles = {
|
|||||||
panel: {
|
panel: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
paddingTop: 16,
|
paddingTop: 16,
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
|
"@mantine/dates": "^8.3.18",
|
||||||
"@mantine/form": "^8.3.1",
|
"@mantine/form": "^8.3.1",
|
||||||
"@mantine/hooks": "^8.3.1",
|
"@mantine/hooks": "^8.3.1",
|
||||||
"@mantine/modals": "^8.3.10",
|
"@mantine/modals": "^8.3.10",
|
||||||
|
|||||||
Reference in New Issue
Block a user