Merge branch 'zh' into 'main'

Zh

See merge request prism/lable/labelimage!13
This commit is contained in:
刘耀勇
2026-04-16 20:28:45 +08:00
11 changed files with 392 additions and 142 deletions

View File

@@ -38,6 +38,31 @@ import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import TaskStatusTag from "./components/TaskStatusTag" import TaskStatusTag from "./components/TaskStatusTag"
const EMPTY_TASK_SIZE: Task.SizeProps = Object.freeze({
object_size: 0,
valid_size: 0,
first_comment_size: 0,
second_comment_size: 0,
new_size: 0,
prelabel_size: 0,
prelabel_modify_size: 0,
dynamic_size: 0,
static_size: 0,
review_size: 0,
review_dynamic_size: 0,
review_static_size: 0,
key_frame_size: 0,
question_size: 0,
total_qa_group_size: 0,
single_qa_group_size: 0,
multiple_qa_group_size: 0,
new_qa_group_size: 0,
new_single_qa_group_size: 0,
new_multiple_qa_group_size: 0,
review_question_size: 0,
comment_question_size: 0,
})
function createInitialFilters() { function createInitialFilters() {
return { return {
search_id: "", search_id: "",
@@ -67,6 +92,7 @@ export default function OwnTaskTableContainer(props: {
const user_id = usePermissionStore((s) => s.user_id) const user_id = usePermissionStore((s) => s.user_id)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [totalSize, setTotalSize] = useState<Task.SizeProps>(EMPTY_TASK_SIZE)
const [pageNumber, setPageNumber] = useState(1) const [pageNumber, setPageNumber] = useState(1)
const [pageSize, setPageSize] = useState(15) const [pageSize, setPageSize] = useState(15)
@@ -130,9 +156,11 @@ export default function OwnTaskTableContainer(props: {
const res = await getTaskList(appliedParams) const res = await getTaskList(appliedParams)
setRecords(res?.task_list ?? []) setRecords(res?.task_list ?? [])
setTotal(res?.total_items ?? 0) setTotal(res?.total_items ?? 0)
setTotalSize(res?.total_size ?? EMPTY_TASK_SIZE)
} catch (e) { } catch (e) {
setRecords([]) setRecords([])
setTotal(0) setTotal(0)
setTotalSize(EMPTY_TASK_SIZE)
notifications.show({ notifications.show({
color: "red", color: "red",
title: "加载失败", title: "加载失败",
@@ -201,6 +229,7 @@ export default function OwnTaskTableContainer(props: {
accessor: "id", accessor: "id",
title: "任务ID", title: "任务ID",
width: 100, width: 100,
footer: "总计",
render: (record) => ( render: (record) => (
<Button <Button
variant="transparent" variant="transparent"
@@ -243,30 +272,35 @@ export default function OwnTaskTableContainer(props: {
accessor: "label_object_size.object_size", accessor: "label_object_size.object_size",
title: "对象总数", title: "对象总数",
width: 110, width: 110,
footer: totalSize.object_size,
render: (record) => record.label_object_size?.object_size ?? "-", render: (record) => record.label_object_size?.object_size ?? "-",
}, },
{ {
accessor: "label_object_size.valid_size", accessor: "label_object_size.valid_size",
title: "有效数", title: "有效数",
width: 100, width: 100,
footer: totalSize.valid_size,
render: (record) => record.label_object_size?.valid_size ?? "-", render: (record) => record.label_object_size?.valid_size ?? "-",
}, },
{ {
accessor: "label_object_size.new_size", accessor: "label_object_size.new_size",
title: "新增数", title: "新增数",
width: 100, width: 100,
footer: totalSize.new_size,
render: (record) => record.label_object_size?.new_size ?? "-", render: (record) => record.label_object_size?.new_size ?? "-",
}, },
{ {
accessor: "label_object_size.prelabel_size", accessor: "label_object_size.prelabel_size",
title: "预标注总数", title: "预标注总数",
width: 120, width: 120,
footer: totalSize.prelabel_size,
render: (record) => record.label_object_size?.prelabel_size ?? "-", render: (record) => record.label_object_size?.prelabel_size ?? "-",
}, },
{ {
accessor: "label_object_size.prelabel_modify_size", accessor: "label_object_size.prelabel_modify_size",
title: "预标注修改数", title: "预标注修改数",
width: 130, width: 130,
footer: totalSize.prelabel_modify_size,
render: (record) => render: (record) =>
record.label_object_size?.prelabel_modify_size ?? "-", record.label_object_size?.prelabel_modify_size ?? "-",
}, },
@@ -274,18 +308,20 @@ export default function OwnTaskTableContainer(props: {
accessor: "label_object_size.first_comment_size", accessor: "label_object_size.first_comment_size",
title: "批注一轮", title: "批注一轮",
width: 110, width: 110,
footer: totalSize.first_comment_size,
render: (record) => record.label_object_size?.first_comment_size ?? "-", render: (record) => record.label_object_size?.first_comment_size ?? "-",
}, },
{ {
accessor: "label_object_size.second_comment_size", accessor: "label_object_size.second_comment_size",
title: "批注二轮", title: "批注二轮",
width: 110, width: 110,
footer: totalSize.second_comment_size,
render: (record) => render: (record) =>
record.label_object_size?.second_comment_size ?? "-", record.label_object_size?.second_comment_size ?? "-",
}, },
] ]
return cols return cols
}, [info, projectId, router, setBackProps, type]) }, [info, projectId, router, setBackProps, totalSize, type])
return ( return (
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}> <Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>

View File

@@ -122,6 +122,31 @@ const SORTABLE_TASK_SIZE_COLUMNS: Array<{
{ accessor: "second_comment_size", title: "批注数(2)", width: 120 }, { accessor: "second_comment_size", title: "批注数(2)", width: 120 },
] ]
const EMPTY_TASK_SIZE: Task.SizeProps = Object.freeze({
object_size: 0,
valid_size: 0,
first_comment_size: 0,
second_comment_size: 0,
new_size: 0,
prelabel_size: 0,
prelabel_modify_size: 0,
dynamic_size: 0,
static_size: 0,
review_size: 0,
review_dynamic_size: 0,
review_static_size: 0,
key_frame_size: 0,
question_size: 0,
total_qa_group_size: 0,
single_qa_group_size: 0,
multiple_qa_group_size: 0,
new_qa_group_size: 0,
new_single_qa_group_size: 0,
new_multiple_qa_group_size: 0,
review_question_size: 0,
comment_question_size: 0,
})
export interface selectedDataProps { export interface selectedDataProps {
status: number status: number
uid: number | null uid: number | null
@@ -195,6 +220,7 @@ export default function TaskTableContainer(props: {
const { setBackProps } = useBackUrlStore() const { setBackProps } = useBackUrlStore()
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [totalSize, setTotalSize] = useState<Task.SizeProps>(EMPTY_TASK_SIZE)
const [pageNumber, setPageNumber] = useState(1) const [pageNumber, setPageNumber] = useState(1)
const [pageSize, setPageSize] = useState(15) const [pageSize, setPageSize] = useState(15)
@@ -324,9 +350,11 @@ export default function TaskTableContainer(props: {
const res = await getTaskList(appliedParams) const res = await getTaskList(appliedParams)
setRecords(res?.task_list ?? []) setRecords(res?.task_list ?? [])
setTotal(res?.total_items ?? 0) setTotal(res?.total_items ?? 0)
setTotalSize(res?.total_size ?? EMPTY_TASK_SIZE)
} catch (e) { } catch (e) {
setRecords([]) setRecords([])
setTotal(0) setTotal(0)
setTotalSize(EMPTY_TASK_SIZE)
notifications.show({ notifications.show({
color: "red", color: "red",
title: "加载失败", title: "加载失败",
@@ -621,6 +649,7 @@ export default function TaskTableContainer(props: {
width, width,
sortable: true, sortable: true,
render: (record) => record.label_object_size?.[accessor] ?? "-", render: (record) => record.label_object_size?.[accessor] ?? "-",
footer: totalSize[accessor] ?? 0,
}) })
const cols: DataTableColumn<Task.DataProps>[] = [ const cols: DataTableColumn<Task.DataProps>[] = [
@@ -628,6 +657,7 @@ export default function TaskTableContainer(props: {
accessor: "id", accessor: "id",
title: "任务ID", title: "任务ID",
width: 90, width: 90,
footer: "总计",
render: (record) => ( render: (record) => (
<Button <Button
variant="transparent" variant="transparent"
@@ -805,7 +835,15 @@ export default function TaskTableContainer(props: {
}, },
] ]
return cols return cols
}, [hasRolePermissions, info, projectId, router, setBackProps, type]) }, [
hasRolePermissions,
info,
projectId,
router,
setBackProps,
totalSize,
type,
])
return ( return (
<> <>

View File

@@ -307,6 +307,7 @@ const LabelPage = ({
activeOperation, activeOperation,
setActiveOperation, setActiveOperation,
setDrawOption, setDrawOption,
setSubAttrPresetShow,
showObjectList, showObjectList,
showGroupList, showGroupList,
showTaskList, showTaskList,
@@ -1433,6 +1434,31 @@ const LabelPage = ({
const { setShift, focusInput } = useKeyEventStore() const { setShift, focusInput } = useKeyEventStore()
const drawAction = useKeyboardStore((state) => state.drawAction) const drawAction = useKeyboardStore((state) => state.drawAction)
const initializedDrawActionTaskIdRef = useRef<number | null>(null)
const handleSelectOperation = useCallback(
(operationId: string) => {
const operationSchema = getOperationSchema(operationId)
if (!operationSchema) return false
setActiveOperation(operationId)
setSubAttrPresetShow(!!operationSchema.sub_attributes_describe?.length)
if (isLabelTask) {
setEditMode(true)
}
return true
},
[
getOperationSchema,
isLabelTask,
setActiveOperation,
setEditMode,
setSubAttrPresetShow,
]
)
const drawShortcutSnapshotRef = useRef<{ const drawShortcutSnapshotRef = useRef<{
activeOperation: string activeOperation: string
drawOption: "default" | "intersect" | "unite" drawOption: "default" | "intersect" | "unite"
@@ -1441,20 +1467,17 @@ const LabelPage = ({
const handleShortcut = useCallback( const handleShortcut = useCallback(
(key: string, ctrl: boolean) => { (key: string, ctrl: boolean) => {
let checkOperation = getOperationSchema(key)
const mode = usePaperStore.getState().mode const mode = usePaperStore.getState().mode
const nextOperationId = ctrl ? (+key + 10).toString() : key
const checkOperation = getOperationSchema(nextOperationId)
// 标注方案里面存在的非文本类型标注才能切换 // 标注方案里面存在的非文本类型标注才能切换
if (checkOperation) { if (checkOperation) {
// 辅助标注模式下非多边形类型不能切换 // 辅助标注模式下非多边形类型不能切换
if (mode === "support" && checkOperation.label_type !== 102) return if (mode === "support" && checkOperation.label_type !== 102) return
if (ctrl) { handleSelectOperation(nextOperationId)
setActiveOperation((+key + 10).toString())
} else {
setActiveOperation(key)
}
} }
}, },
[getOperationSchema, setActiveOperation] [getOperationSchema, handleSelectOperation]
) )
const resolveSelectedObjectContext = useCallback(() => { const resolveSelectedObjectContext = useCallback(() => {
@@ -1594,6 +1617,50 @@ const LabelPage = ({
}) })
}, [activeImage, labelData]) }, [activeImage, labelData])
const handleToggleHideSelectionOrAll = useCallback(() => {
const selectedIds =
useObjectStore.getState().selectedPath[activeImage] || []
if (!selectedIds.length) {
handleToggleHideAllObjects()
return
}
const activeImageLabel = labelData.get(activeImage)
if (!activeImageLabel?.size) {
return
}
const selectedIdSet = new Set(selectedIds)
const selectedObjects: Array<{
operationId: string
objectId: number
}> = []
activeImageLabel.forEach((paths, operationId) => {
paths.forEach(([objectId]) => {
if (!selectedIdSet.has(objectId)) return
selectedObjects.push({
operationId,
objectId,
})
})
})
if (!selectedObjects.length) {
return
}
const { pathStatus } = useObjectStore.getState()
const areAllSelectedObjectsHidden = selectedObjects.every(
({ objectId }) => pathStatus[activeImage + objectId]?.HIDE ?? false
)
const nextHide = !areAllSelectedObjectsHidden
selectedObjects.forEach(({ operationId, objectId }) => {
toggleObjectHideByShortcut(activeImage, operationId, objectId, nextHide)
})
}, [activeImage, handleToggleHideAllObjects, labelData])
const rerenderActiveImageByLabel = useCallback( const rerenderActiveImageByLabel = useCallback(
( (
imageId: string, imageId: string,
@@ -2185,7 +2252,7 @@ const LabelPage = ({
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") { if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
e.preventDefault() e.preventDefault()
handleToggleHideAllObjects() handleToggleHideSelectionOrAll()
return return
} }
@@ -2487,6 +2554,7 @@ const LabelPage = ({
focusInput, focusInput,
getOperationSchema, getOperationSchema,
handleToggleHideAllObjects, handleToggleHideAllObjects,
handleToggleHideSelectionOrAll,
handleMergeSelectedObjects, handleMergeSelectedObjects,
handleContinueSelectedPolygon, handleContinueSelectedPolygon,
handleShortcut, handleShortcut,
@@ -2519,6 +2587,14 @@ const LabelPage = ({
drawShortcutSnapshotRef.current = null drawShortcutSnapshotRef.current = null
}, [activeImage]) }, [activeImage])
useEffect(() => {
if (!taskDetail?.id || !activeImage || !isLabelTask) return
if (initializedDrawActionTaskIdRef.current === taskDetail.id) return
useKeyboardStore.getState().setDrawAction("subtract")
initializedDrawActionTaskIdRef.current = taskDetail.id
}, [activeImage, isLabelTask, taskDetail?.id])
const handleBeforeUnload = (e: any) => { const handleBeforeUnload = (e: any) => {
e.preventDefault() e.preventDefault()
// 备份 // 备份
@@ -2708,6 +2784,7 @@ const LabelPage = ({
oldWorkLoad={oldWorkLoad} oldWorkLoad={oldWorkLoad}
updateOldWorkLoad={updateOldWorkLoad} updateOldWorkLoad={updateOldWorkLoad}
isLabelTask={isLabelTask} isLabelTask={isLabelTask}
onOperationChange={handleSelectOperation}
renderPolygons={ renderPolygons={
paperContainerRef.current paperContainerRef.current
? paperContainerRef.current.renderPolygons ? paperContainerRef.current.renderPolygons

View File

@@ -3,7 +3,6 @@
import { Box } from "@mantine/core" import { Box } from "@mantine/core"
import { RichTextEditor } from "@mantine/tiptap" import { RichTextEditor } from "@mantine/tiptap"
import "@mantine/tiptap/styles.css" import "@mantine/tiptap/styles.css"
import { IconPhoto } from "@tabler/icons-react"
import Image from "@tiptap/extension-image" import Image from "@tiptap/extension-image"
import Link from "@tiptap/extension-link" import Link from "@tiptap/extension-link"
import Placeholder from "@tiptap/extension-placeholder" import Placeholder from "@tiptap/extension-placeholder"
@@ -148,7 +147,7 @@ const EditorContainer = ({
<RichTextEditor.Hr /> <RichTextEditor.Hr />
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup> {/* <RichTextEditor.ControlsGroup>
<RichTextEditor.Link /> <RichTextEditor.Link />
<RichTextEditor.Unlink /> <RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup>
@@ -160,7 +159,7 @@ const EditorContainer = ({
title="上传图片"> title="上传图片">
<IconPhoto stroke={1.5} size="1rem" /> <IconPhoto stroke={1.5} size="1rem" />
</RichTextEditor.Control> </RichTextEditor.Control>
</RichTextEditor.ControlsGroup> </RichTextEditor.ControlsGroup> */}
</RichTextEditor.Toolbar> </RichTextEditor.Toolbar>
<RichTextEditor.Content h={144} style={{ overflowY: "auto" }} /> <RichTextEditor.Content h={144} style={{ overflowY: "auto" }} />

View File

@@ -52,7 +52,7 @@ 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 { buildSubAttributeFormValues, checkCommentsIsSame } from "../util"
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"
@@ -3315,50 +3315,26 @@ const PaperContainer = (
.get(activeImage) .get(activeImage)
?.get(operationId) ?.get(operationId)
?.find((item) => item[0] === id) ?.find((item) => item[0] === id)
const operationSchema = getOperationSchema(operationId)
form.setFieldValue("id", id.toString())
form.setFieldValue("operation", operationId)
if (currentPath?.[2].sub_attributes) {
let operationSchema = getOperationSchema(operationId)
let obj: {
[key: string]: string | string[]
} = {}
Object.entries(currentPath?.[2].sub_attributes).map(([key, value]) => {
let select_mode = operationSchema?.sub_attributes_describe?.find(
(item) => item.chinese_name === key
)?.select_mode
switch (select_mode) {
case 2:
obj[key] = Array.isArray(value)
? value
: (value as string)
.split(",")
.map((item) => item.trim())
.filter(Boolean)
return
default:
obj[key] = value as string
return
}
})
form.setFieldValue("sub_attributes", obj)
}
if (currentPath?.[2].comment) {
const nowComment = const nowComment =
currentPath?.[2].comment[currentPath?.[2].comment.length - 1] currentPath?.[2].comment?.[currentPath?.[2].comment.length - 1]
form.setFieldValue(
"check_box_comments", form.setValues({
id: id.toString(),
operation: operationId,
sub_attributes: buildSubAttributeFormValues(
operationSchema,
currentPath?.[2].sub_attributes || {}
),
check_box_comments:
nowComment && nowComment.check_box_comments.length nowComment && nowComment.check_box_comments.length
? nowComment.check_box_comments ? nowComment.check_box_comments
: [] : [],
) text_comments:
form.setFieldValue(
"text_comments",
nowComment && nowComment.text_comments.length nowComment && nowComment.text_comments.length
? nowComment.text_comments[0] ? nowComment.text_comments[0]
: "" : "",
) })
}
// 可选的ids // 可选的ids
let images = Array.from(labelState.keys()).filter( let images = Array.from(labelState.keys()).filter(
(key) => key !== activeImage (key) => key !== activeImage
@@ -3580,11 +3556,10 @@ const PaperContainer = (
}) })
} }
// newOperation的subAttributes // newOperation的subAttributes
let subAttr = Object.assign( let subAttr = safeClone(
{},
useTopToolsStore.getState().subAttributes[ useTopToolsStore.getState().subAttributes[
operationSchema?.label_class ?? "" operationSchema?.label_class ?? ""
] ] || {}
) )
// 修改labeldata // 修改labeldata
setOperation(activeImage, newOperation, [ setOperation(activeImage, newOperation, [
@@ -3612,16 +3587,21 @@ const PaperContainer = (
) )
const handleChangeContextMenuSubAttr = useCallback( const handleChangeContextMenuSubAttr = useCallback(
(key: string, value: string) => { (key: string, value: string | string[]) => {
const currentSubAttributes = safeClone(
(pathItem?.[2]?.sub_attributes ||
newDetail.sub_attributes ||
{}) as Record<string, any>
)
currentSubAttributes[key] = Array.isArray(value) ? value.join(",") : value
setOperation(activeImage, operationId, [ setOperation(activeImage, operationId, [
id, id,
pathItem?.[1] || [], pathItem?.[1] || [],
{ {
...newDetail, ...newDetail,
...(pathItem?.[2] || {}), ...(pathItem?.[2] || {}),
sub_attributes: Object.assign(newDetail.sub_attributes || {}, { sub_attributes: currentSubAttributes,
[key]: value,
}),
}, },
pathItem?.[3] || [], pathItem?.[3] || [],
]) ])
@@ -3892,7 +3872,7 @@ const PaperContainer = (
) )
handleChangeContextMenuSubAttr( handleChangeContextMenuSubAttr(
item.chinese_name, item.chinese_name,
value.toString() value
) )
}}> }}>
<Stack gap={6} mt={6}> <Stack gap={6} mt={6}>

View File

@@ -166,6 +166,24 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
[activeImage, continueObjectId, storeLabel] [activeImage, continueObjectId, storeLabel]
) )
const getObjectItemsById = useCallback(
(id: number) => {
return getItemsById(id).filter((item) => item.data?.type !== "tag")
},
[getItemsById]
)
const bringTagItemsToFront = useCallback(
(id: number) => {
getItemsById(id).forEach((item) => {
if (item.data?.type === "tag") {
item.bringToFront()
}
})
},
[getItemsById]
)
// 切换选中对象时,清除所有其他对象的当前选中状态 // 切换选中对象时,清除所有其他对象的当前选中状态
const clearSelectedItems = () => { const clearSelectedItems = () => {
usePaperStore usePaperStore
@@ -215,11 +233,16 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
const handleSelect = useCallback( const handleSelect = useCallback(
(id: number, operationId: any) => { (id: number, operationId: any) => {
if (pathStatus[activeImage + id]?.HIDE) return
const objectItems = getObjectItemsById(id)
const targetItem = getItemById(id)
if (selectedPath[activeImage]?.length) { if (selectedPath[activeImage]?.length) {
if (!selectedPath[activeImage]?.includes(id)) { if (!selectedPath[activeImage]?.includes(id)) {
if (pressShift) { if (pressShift) {
if (getItemsById(id)?.length) { if (objectItems.length && targetItem) {
getItemsById(id).forEach((item) => { objectItems.forEach((item) => {
if (item instanceof paper.Path) { if (item instanceof paper.Path) {
handleSelectedPath(item) handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) { } else if (item instanceof paper.CompoundPath) {
@@ -228,13 +251,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
else item.data.selected = true else item.data.selected = true
} }
}) })
bringTagItemsToFront(id)
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
.objectOperations.find( .objectOperations.find(
(item) => item.category_id.toString() === operationId (item) => item.category_id.toString() === operationId
) )
const globalPosition = getItemById(id)!.globalMatrix.transform( const globalPosition = targetItem.globalMatrix.transform(
getItemById(id)!.position targetItem.position
) )
const { top, left } = getShowContextMenuPosition( const { top, left } = getShowContextMenuPosition(
globalPosition, globalPosition,
@@ -251,8 +275,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
} else { } else {
clearSelectedItems() clearSelectedItems()
selectedPath[activeImage].forEach((selectedId) => { selectedPath[activeImage].forEach((selectedId) => {
if (getItemsById(selectedId)?.length) { const selectedObjectItems = getObjectItemsById(selectedId)
getItemsById(selectedId).forEach((item) => { if (selectedObjectItems.length) {
selectedObjectItems.forEach((item) => {
cancelSelectedPathColor(item) cancelSelectedPathColor(item)
}) })
useOtherToolsStore.getState().setShowContextMenu({ useOtherToolsStore.getState().setShowContextMenu({
@@ -267,8 +292,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
}) })
} }
}) })
if (getItemsById(id)?.length) { if (objectItems.length && targetItem) {
getItemsById(id).forEach((item) => { objectItems.forEach((item) => {
if (item instanceof paper.Path) { if (item instanceof paper.Path) {
handleSelectedPath(item) handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) { } else if (item instanceof paper.CompoundPath) {
@@ -277,13 +302,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
else item.data.selected = true else item.data.selected = true
} }
}) })
bringTagItemsToFront(id)
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
.objectOperations.find( .objectOperations.find(
(item) => item.category_id.toString() === operationId (item) => item.category_id.toString() === operationId
) )
const globalPosition = getItemById(id)!.globalMatrix.transform( const globalPosition = targetItem.globalMatrix.transform(
getItemById(id)!.position targetItem.position
) )
const { top, left } = getShowContextMenuPosition( const { top, left } = getShowContextMenuPosition(
globalPosition, globalPosition,
@@ -302,8 +328,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
} else { } else {
clearSelectedItems() clearSelectedItems()
updateSelectedPath(activeImage, "DELETE", id) updateSelectedPath(activeImage, "DELETE", id)
if (getItemsById(id)?.length) { if (objectItems.length) {
getItemsById(id).forEach((item) => { objectItems.forEach((item) => {
cancelSelectedPathColor(item) cancelSelectedPathColor(item)
}) })
useOtherToolsStore.getState().setShowContextMenu({ useOtherToolsStore.getState().setShowContextMenu({
@@ -321,8 +347,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
} else { } else {
clearSelectedItems() clearSelectedItems()
updateSelectedPath(activeImage, "ADD", id) updateSelectedPath(activeImage, "ADD", id)
if (getItemsById(id)?.length) { if (objectItems.length && targetItem) {
getItemsById(id).forEach((item) => { objectItems.forEach((item) => {
if (item instanceof paper.Path) { if (item instanceof paper.Path) {
handleSelectedPath(item) handleSelectedPath(item)
} else if (item instanceof paper.CompoundPath) { } else if (item instanceof paper.CompoundPath) {
@@ -337,14 +363,15 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
// item.fillColor = item.data?.fillColor; // item.fillColor = item.data?.fillColor;
// item.bringToFront(); // item.bringToFront();
}) })
bringTagItemsToFront(id)
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
.objectOperations.find( .objectOperations.find(
(item) => item.category_id.toString() === operationId (item) => item.category_id.toString() === operationId
) )
const globalPosition = getItemById(id)!.globalMatrix.transform( const globalPosition = targetItem.globalMatrix.transform(
getItemById(id)!.position targetItem.position
) )
const { top, left } = getShowContextMenuPosition( const { top, left } = getShowContextMenuPosition(
globalPosition, globalPosition,
@@ -362,8 +389,10 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
}, },
[ [
activeImage, activeImage,
bringTagItemsToFront,
getObjectItemsById,
getItemById, getItemById,
getItemsById, pathStatus,
pressShift, pressShift,
selectedPath, selectedPath,
updateSelectedPath, updateSelectedPath,
@@ -889,10 +918,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
: "var(--mantine-color-gray-0)" : "var(--mantine-color-gray-0)"
} }
c={isSelected ? "white" : undefined} c={isSelected ? "white" : undefined}
onClick={() => onClick={() => {
if (isChildHide) return
handleSelect(child[0], operationId) handleSelect(child[0], operationId)
} }}
style={{ cursor: "pointer" }}> style={{
cursor: isChildHide ? "default" : "pointer",
}}>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Group gap="xs"> <Group gap="xs">
<ActionIcon <ActionIcon

View File

@@ -98,7 +98,7 @@ import {
NODE_SIZE_MIN, NODE_SIZE_MIN,
useTopToolsStore, useTopToolsStore,
} from "../useTopToolsStore" } from "../useTopToolsStore"
import { getSubAttrStatus } from "../util" import { buildSubAttributeFormValues, getSubAttrStatus } from "../util"
import { safeClone } from "../utils/clone" import { safeClone } from "../utils/clone"
import { labelTypeMap, TaskStatusEnum } from "../utils/constants" import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
import { adjustAllPoints } from "../utils/paperjs" import { adjustAllPoints } from "../utils/paperjs"
@@ -118,6 +118,7 @@ interface TopToolsComponentProps {
oldWorkLoad: WorkLoad | null oldWorkLoad: WorkLoad | null
updateOldWorkLoad: (data: WorkLoad) => void updateOldWorkLoad: (data: WorkLoad) => void
isLabelTask: boolean isLabelTask: boolean
onOperationChange: (operationId: string) => void
renderPolygons: ( renderPolygons: (
operationId: string, operationId: string,
imageData: Map<string, [number, any[], any, any[]][]> | undefined imageData: Map<string, [number, any[], any, any[]][]> | undefined
@@ -161,6 +162,7 @@ const TopTools = (
oldWorkLoad, oldWorkLoad,
updateOldWorkLoad, updateOldWorkLoad,
isLabelTask, isLabelTask,
onOperationChange,
renderPolygons, renderPolygons,
} = props } = props
const { backUrl, basePath } = useBackUrlStore() const { backUrl, basePath } = useBackUrlStore()
@@ -1488,36 +1490,10 @@ const TopTools = (
operationId: string, operationId: string,
values: Record<string, string | string[] | undefined> = {} values: Record<string, string | string[] | undefined> = {}
) => { ) => {
const schema = getOperationSchema(operationId) return buildSubAttributeFormValues(
if (!schema?.sub_attributes_describe?.length) return {} getOperationSchema(operationId),
values
const nextValues: Record<string, string | string[]> = {}
schema.sub_attributes_describe.forEach((item) => {
const currentValue = values[item.chinese_name]
if (
currentValue === undefined ||
currentValue === null ||
currentValue === ""
) )
return
if (item.select_mode === 2) {
nextValues[item.chinese_name] = Array.isArray(currentValue)
? currentValue
: currentValue
.split(",")
.map((value) => value.trim())
.filter(Boolean)
return
}
nextValues[item.chinese_name] = Array.isArray(currentValue)
? currentValue[0] || ""
: currentValue
})
return nextValues
}, },
[getOperationSchema] [getOperationSchema]
) )
@@ -1627,6 +1603,10 @@ const TopTools = (
size="xs" size="xs"
onClick={() => { onClick={() => {
form.reset() form.reset()
form.setFieldValue(
"sub_attributes",
getPresetFormValues(activeOperation, {})
)
if (operationSchema?.label_class) if (operationSchema?.label_class)
setsubAttributes(operationSchema.label_class, {}) setsubAttributes(operationSchema.label_class, {})
}}> }}>
@@ -2334,7 +2314,7 @@ const TopTools = (
<UnstyledButton <UnstyledButton
key={item.category_id} key={item.category_id}
onClick={() => { onClick={() => {
setActiveOperation(item.category_id.toString()) onOperationChange(item.category_id.toString())
setOperationPickerOpened(false) setOperationPickerOpened(false)
setOperationKeyword("") setOperationKeyword("")
}} }}

View File

@@ -20,6 +20,7 @@ import { usePaperSupportStore } from "./usePaperSupportStore"
import { useRightToolsStore } from "./useRightToolsStore" import { useRightToolsStore } from "./useRightToolsStore"
import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore" import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore"
import { safeClone } from "./utils/clone" import { safeClone } from "./utils/clone"
import { getObjectTagVisibility } from "./utils/objectVisibility"
interface ContinuePolygonTarget { interface ContinuePolygonTarget {
imageId: string imageId: string
@@ -216,6 +217,16 @@ const initialPaperState = {
reciprocalRasterScale: {}, reciprocalRasterScale: {},
resizeGhostRequestToken: 0, resizeGhostRequestToken: 0,
} }
const insertItemBelowTags = (item: paper.Item | null | undefined) => {
if (!item) return
const group = usePaperStore.getState().group
if (!group || item.parent !== group) return
const firstTagItem = group.getItems({ data: { type: "tag" } })[0]
if (!firstTagItem || firstTagItem === item) return
item.insertBelow(firstTagItem)
}
const handleDelete = () => { const handleDelete = () => {
const group = usePaperStore.getState().group! const group = usePaperStore.getState().group!
const imageId = useBottomToolsStore.getState().activeImage const imageId = useBottomToolsStore.getState().activeImage
@@ -450,12 +461,7 @@ const clearPaperSelectedObjects = () => {
.getState() .getState()
.getItemsById(id) .getItemsById(id)
.forEach((item) => { .forEach((item) => {
if ( resetPaperShapeFillColor(item)
(item instanceof paper.Path || item instanceof paper.CompoundPath) &&
["rectangle", "polygon"].includes(item.data?.type || "")
) {
item.fillColor = item.data?.blankColor || item.fillColor || null
}
}) })
}) })
@@ -464,6 +470,16 @@ const clearPaperSelectedObjects = () => {
}) })
} }
const resetPaperShapeFillColor = (item: paper.Item) => {
if (!(item instanceof paper.Path || item instanceof paper.CompoundPath)) {
return
}
if (!["rectangle", "polygon"].includes(item.data?.type || "")) {
return
}
item.fillColor = item.data?.blankColor || item.fillColor || null
}
const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => { const applyPaperSelectedPath = (item: paper.Path | paper.CompoundPath) => {
if (item.data.type === "rectangle" && item instanceof paper.Path) { if (item.data.type === "rectangle" && item instanceof paper.Path) {
item.data.selected = true item.data.selected = true
@@ -2011,15 +2027,14 @@ export const usePaperStore = create<PaperState>((set) => ({
type: "tag", type: "tag",
}, },
}) })
// 标识位为true时选中路径隐藏tags; 没选中显示tags // 仅恢复当前应显示的标签,隐藏对象的标签保持不可见
const configs = useTopToolsStore.getState().showTagsConfigs
if (useTopToolsStore.getState().showTags) { if (useTopToolsStore.getState().showTags) {
const setTagVisibleByConfig = (tag: any) => { const setTagVisibleByConfig = (tag: any) => {
if (tag.data.tag_category === "mask") tag.visible = getObjectTagVisibility(
tag.visible = configs.includes("外框") useBottomToolsStore.getState().activeImage,
else if (tag.data.tag_category === "point_text") Number(tag.data?.id),
tag.visible = configs.includes("点ID") tag.data?.tag_category
else tag.visible = true )
} }
lastTags.forEach(setTagVisibleByConfig) lastTags.forEach(setTagVisibleByConfig)
tags.forEach(setTagVisibleByConfig) tags.forEach(setTagVisibleByConfig)
@@ -3147,6 +3162,7 @@ export const usePaperStore = create<PaperState>((set) => ({
}) })
) )
polygon.data = Object.assign({ type: "polygon", ...option }, data) polygon.data = Object.assign({ type: "polygon", ...option }, data)
insertItemBelowTags(polygon)
points?.forEach((point) => { points?.forEach((point) => {
polygon.add(point) polygon.add(point)
}) })
@@ -3219,6 +3235,7 @@ export const usePaperStore = create<PaperState>((set) => ({
}) })
) )
compoundPath.data = Object.assign({ type: "polygon", ...option }, data) compoundPath.data = Object.assign({ type: "polygon", ...option }, data)
insertItemBelowTags(compoundPath)
compoundPath.onMouseDown = (e: { compoundPath.onMouseDown = (e: {
event: MouseEvent event: MouseEvent
point: paper.Point point: paper.Point
@@ -4606,8 +4623,7 @@ export const usePaperStore = create<PaperState>((set) => ({
.getState() .getState()
.group!.getItems({}) .group!.getItems({})
.forEach((item) => { .forEach((item) => {
// clear color resetPaperShapeFillColor(item)
item.fillColor = item?.data?.blankColor
}) })
} }
@@ -4803,6 +4819,7 @@ export const usePaperStore = create<PaperState>((set) => ({
Object.assign(option, { strokeScaling: false }) Object.assign(option, { strokeScaling: false })
) )
rect.data = Object.assign({ type: "rectangle", ...option }, data) rect.data = Object.assign({ type: "rectangle", ...option }, data)
insertItemBelowTags(rect)
rect.onMouseEnter = (_e: paper.MouseEvent) => { rect.onMouseEnter = (_e: paper.MouseEvent) => {
// if (usePaperStore.getState().mode === "pan") { // if (usePaperStore.getState().mode === "pan") {
// e.target.strokeWidth += 2; // e.target.strokeWidth += 2;
@@ -5045,6 +5062,7 @@ export const usePaperStore = create<PaperState>((set) => ({
) )
) )
brush.data = Object.assign({ type: "brush", ...option }, data) brush.data = Object.assign({ type: "brush", ...option }, data)
insertItemBelowTags(brush)
// points?.forEach((point) => { // points?.forEach((point) => {
// brush.add(point); // brush.add(point);
// }); // });
@@ -5545,6 +5563,7 @@ export const usePaperStore = create<PaperState>((set) => ({
const newScale = useTopToolsStore.getState().scale / currentScaling const newScale = useTopToolsStore.getState().scale / currentScaling
myCircle.scale(newScale / currentScaling) myCircle.scale(newScale / currentScaling)
myCircle.data = Object.assign({ type: "circle", ...option }, data) myCircle.data = Object.assign({ type: "circle", ...option }, data)
insertItemBelowTags(myCircle)
syncPaperCircleDiameter(myCircle) syncPaperCircleDiameter(myCircle)
myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => { myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
if (e.event && e.event.button === 2) { if (e.event && e.event.button === 2) {
@@ -5614,6 +5633,7 @@ export const usePaperStore = create<PaperState>((set) => ({
) )
) )
myCircleLine.data = Object.assign({ type: "point", ...option }, data) myCircleLine.data = Object.assign({ type: "point", ...option }, data)
insertItemBelowTags(myCircleLine)
return myCircleLine return myCircleLine
}, },

View File

@@ -4,6 +4,7 @@ import { useLabelStore } from "./store"
import { useBottomToolsStore } from "./useBottomToolsStore" import { useBottomToolsStore } from "./useBottomToolsStore"
import { usePaperStore } from "./usePaperStore" import { usePaperStore } from "./usePaperStore"
import { useTopToolsStore } from "./useTopToolsStore" import { useTopToolsStore } from "./useTopToolsStore"
import { getObjectTagVisibility } from "./utils/objectVisibility"
const TAG_FONT_SIZE = 12 const TAG_FONT_SIZE = 12
const TAG_VERTICAL_GAP = 8 const TAG_VERTICAL_GAP = 8
@@ -159,13 +160,12 @@ const useRenderTag = () => {
const activeImage = useBottomToolsStore.getState().activeImage const activeImage = useBottomToolsStore.getState().activeImage
const group = usePaperStore.getState().group const group = usePaperStore.getState().group
if (!group) return if (!group) return
const visible = useTopToolsStore.getState().showTags
const configs = useTopToolsStore.getState().showTagsConfigs const configs = useTopToolsStore.getState().showTagsConfigs
const groupScale = const groupScale =
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0 Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
? group.scaling.x ? group.scaling.x
: 1 : 1
const applyPointTagCompensation = ( const applyScreenSpaceTagCompensation = (
item: paper.Item, item: paper.Item,
anchor: [number, number] anchor: [number, number]
) => { ) => {
@@ -235,7 +235,7 @@ const useRenderTag = () => {
tag_anchor: tagAnchor, tag_anchor: tagAnchor,
}, },
parent: group, parent: group,
visible, visible: getObjectTagVisibility(activeImage, id, "text"),
}) })
text.translate( text.translate(
new paper.Point( new paper.Point(
@@ -259,8 +259,10 @@ const useRenderTag = () => {
tag_anchor: tagAnchor, tag_anchor: tagAnchor,
}, },
parent: group, parent: group,
visible, visible: getObjectTagVisibility(activeImage, id, "text_mask"),
}) })
applyScreenSpaceTagCompensation(text, tagAnchor)
applyScreenSpaceTagCompensation(textMask, tagAnchor)
text.bringToFront() text.bringToFront()
} }
@@ -276,7 +278,7 @@ const useRenderTag = () => {
tag_category: "mask", tag_category: "mask",
}, },
parent: group, parent: group,
visible: visible && configs.includes("外框"), visible: getObjectTagVisibility(activeImage, id, "mask"),
}) })
// 标注对象点id // 标注对象点id
if (path instanceof paper.CompoundPath) { if (path instanceof paper.CompoundPath) {
@@ -301,9 +303,9 @@ const useRenderTag = () => {
tag_anchor: pointTextPosition, tag_anchor: pointTextPosition,
}, },
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: getObjectTagVisibility(activeImage, id, "point_text"),
}) })
applyPointTagCompensation(pointText, pointTextPosition) applyScreenSpaceTagCompensation(pointText, pointTextPosition)
index++ index++
}) })
}) })
@@ -327,9 +329,9 @@ const useRenderTag = () => {
tag_anchor: pointTextPosition, tag_anchor: pointTextPosition,
}, },
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: getObjectTagVisibility(activeImage, id, "point_text"),
}) })
applyPointTagCompensation(pointText, pointTextPosition) applyScreenSpaceTagCompensation(pointText, pointTextPosition)
}) })
} }
}) })

View File

@@ -86,6 +86,45 @@ export const getSubAttrStatus = (
return bool return bool
} }
export const buildSubAttributeFormValues = (
operationSchema: Project.LabelSchemaList | null,
detail: Record<string, any> = {}
) => {
const nextValues: Record<string, string | string[]> = {}
if (!operationSchema?.sub_attributes_describe?.length) return nextValues
operationSchema.sub_attributes_describe.forEach((item) => {
nextValues[item.chinese_name] = item.select_mode === 2 ? [] : ""
const currentValue = detail?.[item.chinese_name]
if (
currentValue === undefined ||
currentValue === null ||
currentValue === "" ||
(Array.isArray(currentValue) && currentValue.length === 0)
) {
return
}
if (item.select_mode === 2) {
nextValues[item.chinese_name] = Array.isArray(currentValue)
? currentValue
: currentValue
.toString()
.split(",")
.map((value: string) => value.trim())
.filter(Boolean)
return
}
nextValues[item.chinese_name] = Array.isArray(currentValue)
? currentValue[0] || ""
: currentValue.toString()
})
return nextValues
}
// //
export const checkCommentsIsSame: ( export const checkCommentsIsSame: (
a: any[] | null, a: any[] | null,

View File

@@ -1,9 +1,11 @@
import paper from "paper" import paper from "paper"
import { useLabelStore, useObjectStore } from "../store" import { useLabelStore, useObjectStore } from "../store"
import { useOtherToolsStore } from "../useOtherToolsStore"
import { usePaperStore } from "../usePaperStore" import { usePaperStore } from "../usePaperStore"
import { usePaperSupportStore } from "../usePaperSupportStore"
import { useTopToolsStore } from "../useTopToolsStore" import { useTopToolsStore } from "../useTopToolsStore"
const getTagVisibility = (tagCategory?: string) => { const getTagVisibilityByConfig = (tagCategory?: string) => {
const { showTags, showTagsConfigs } = useTopToolsStore.getState() const { showTags, showTagsConfigs } = useTopToolsStore.getState()
if (!showTags) return false if (!showTags) return false
if (tagCategory === "mask") return showTagsConfigs.includes("外框") if (tagCategory === "mask") return showTagsConfigs.includes("外框")
@@ -11,6 +13,16 @@ const getTagVisibility = (tagCategory?: string) => {
return true return true
} }
export const getObjectTagVisibility = (
imageId: string,
objectId: number,
tagCategory?: string
) => {
const { pathStatus } = useObjectStore.getState()
if (pathStatus[imageId + objectId]?.HIDE) return false
return getTagVisibilityByConfig(tagCategory)
}
const setSelectedItemFalse = (id: number) => { const setSelectedItemFalse = (id: number) => {
if (!id) return if (!id) return
const sameIdItems = usePaperStore.getState().getItemsById(id) const sameIdItems = usePaperStore.getState().getItemsById(id)
@@ -22,7 +34,37 @@ const setSelectedItemFalse = (id: number) => {
}) })
} }
const syncObjectItemsVisibility = (objectId: number, hide: boolean) => { const clearHiddenObjectSelectionState = (imageId: string, objectId: number) => {
const { selectedPath, updateSelectedPath } = useObjectStore.getState()
if (selectedPath[imageId]?.includes(objectId)) {
updateSelectedPath(imageId, "DELETE", objectId)
usePaperSupportStore.getState().clearAll()
}
const otherToolsState = useOtherToolsStore.getState()
if (
otherToolsState.showContextMenu &&
otherToolsState.imageId === imageId &&
otherToolsState.id === objectId
) {
otherToolsState.setShowContextMenu({
showContextMenu: false,
position: {
top: 0,
left: 0,
},
imageId: "",
operationId: "",
id: 0,
})
}
}
const syncObjectItemsVisibility = (
imageId: string,
objectId: number,
hide: boolean
) => {
const sameIdItems = usePaperStore.getState().getItemsById(objectId) const sameIdItems = usePaperStore.getState().getItemsById(objectId)
if (!sameIdItems?.length) return if (!sameIdItems?.length) return
sameIdItems.forEach((item) => { sameIdItems.forEach((item) => {
@@ -31,7 +73,11 @@ const syncObjectItemsVisibility = (objectId: number, hide: boolean) => {
return return
} }
if (item.data?.type === "tag") { if (item.data?.type === "tag") {
item.visible = getTagVisibility(item.data?.tag_category) item.visible = getObjectTagVisibility(
imageId,
objectId,
item.data?.tag_category
)
return return
} }
item.visible = true item.visible = true
@@ -49,8 +95,9 @@ export const toggleObjectHideByShortcut = (
if (hide) { if (hide) {
updatePathStatus(imageId + objectId, "HIDE", hide) updatePathStatus(imageId + objectId, "HIDE", hide)
syncObjectItemsVisibility(objectId, hide) syncObjectItemsVisibility(imageId, objectId, hide)
setSelectedItemFalse(objectId) setSelectedItemFalse(objectId)
clearHiddenObjectSelectionState(imageId, objectId)
const operationChildren = storeLabel.get(imageId)?.get(operationId) || [] const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
const hasVisibleChild = operationChildren.some((child) => { const hasVisibleChild = operationChildren.some((child) => {
@@ -67,5 +114,5 @@ export const toggleObjectHideByShortcut = (
updateOperationStatus(imageId + operationId, "HIDE", hide) updateOperationStatus(imageId + operationId, "HIDE", hide)
updatePathStatus(imageId + objectId, "HIDE", hide) updatePathStatus(imageId + objectId, "HIDE", hide)
syncObjectItemsVisibility(objectId, hide) syncObjectItemsVisibility(imageId, objectId, hide)
} }