Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!13
This commit is contained in:
@@ -38,6 +38,31 @@ import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
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() {
|
||||
return {
|
||||
search_id: "",
|
||||
@@ -67,6 +92,7 @@ export default function OwnTaskTableContainer(props: {
|
||||
const user_id = usePermissionStore((s) => s.user_id)
|
||||
|
||||
const [total, setTotal] = useState(0)
|
||||
const [totalSize, setTotalSize] = useState<Task.SizeProps>(EMPTY_TASK_SIZE)
|
||||
|
||||
const [pageNumber, setPageNumber] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(15)
|
||||
@@ -130,9 +156,11 @@ export default function OwnTaskTableContainer(props: {
|
||||
const res = await getTaskList(appliedParams)
|
||||
setRecords(res?.task_list ?? [])
|
||||
setTotal(res?.total_items ?? 0)
|
||||
setTotalSize(res?.total_size ?? EMPTY_TASK_SIZE)
|
||||
} catch (e) {
|
||||
setRecords([])
|
||||
setTotal(0)
|
||||
setTotalSize(EMPTY_TASK_SIZE)
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: "加载失败",
|
||||
@@ -201,6 +229,7 @@ export default function OwnTaskTableContainer(props: {
|
||||
accessor: "id",
|
||||
title: "任务ID",
|
||||
width: 100,
|
||||
footer: "总计",
|
||||
render: (record) => (
|
||||
<Button
|
||||
variant="transparent"
|
||||
@@ -243,30 +272,35 @@ export default function OwnTaskTableContainer(props: {
|
||||
accessor: "label_object_size.object_size",
|
||||
title: "对象总数",
|
||||
width: 110,
|
||||
footer: totalSize.object_size,
|
||||
render: (record) => record.label_object_size?.object_size ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "label_object_size.valid_size",
|
||||
title: "有效数",
|
||||
width: 100,
|
||||
footer: totalSize.valid_size,
|
||||
render: (record) => record.label_object_size?.valid_size ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "label_object_size.new_size",
|
||||
title: "新增数",
|
||||
width: 100,
|
||||
footer: totalSize.new_size,
|
||||
render: (record) => record.label_object_size?.new_size ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "label_object_size.prelabel_size",
|
||||
title: "预标注总数",
|
||||
width: 120,
|
||||
footer: totalSize.prelabel_size,
|
||||
render: (record) => record.label_object_size?.prelabel_size ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "label_object_size.prelabel_modify_size",
|
||||
title: "预标注修改数",
|
||||
width: 130,
|
||||
footer: totalSize.prelabel_modify_size,
|
||||
render: (record) =>
|
||||
record.label_object_size?.prelabel_modify_size ?? "-",
|
||||
},
|
||||
@@ -274,18 +308,20 @@ export default function OwnTaskTableContainer(props: {
|
||||
accessor: "label_object_size.first_comment_size",
|
||||
title: "批注一轮",
|
||||
width: 110,
|
||||
footer: totalSize.first_comment_size,
|
||||
render: (record) => record.label_object_size?.first_comment_size ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "label_object_size.second_comment_size",
|
||||
title: "批注二轮",
|
||||
width: 110,
|
||||
footer: totalSize.second_comment_size,
|
||||
render: (record) =>
|
||||
record.label_object_size?.second_comment_size ?? "-",
|
||||
},
|
||||
]
|
||||
return cols
|
||||
}, [info, projectId, router, setBackProps, type])
|
||||
}, [info, projectId, router, setBackProps, totalSize, type])
|
||||
|
||||
return (
|
||||
<Stack gap="sm" style={{ flex: 1, minHeight: 0 }}>
|
||||
|
||||
@@ -122,6 +122,31 @@ const SORTABLE_TASK_SIZE_COLUMNS: Array<{
|
||||
{ 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 {
|
||||
status: number
|
||||
uid: number | null
|
||||
@@ -195,6 +220,7 @@ export default function TaskTableContainer(props: {
|
||||
const { setBackProps } = useBackUrlStore()
|
||||
|
||||
const [total, setTotal] = useState(0)
|
||||
const [totalSize, setTotalSize] = useState<Task.SizeProps>(EMPTY_TASK_SIZE)
|
||||
|
||||
const [pageNumber, setPageNumber] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(15)
|
||||
@@ -324,9 +350,11 @@ export default function TaskTableContainer(props: {
|
||||
const res = await getTaskList(appliedParams)
|
||||
setRecords(res?.task_list ?? [])
|
||||
setTotal(res?.total_items ?? 0)
|
||||
setTotalSize(res?.total_size ?? EMPTY_TASK_SIZE)
|
||||
} catch (e) {
|
||||
setRecords([])
|
||||
setTotal(0)
|
||||
setTotalSize(EMPTY_TASK_SIZE)
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: "加载失败",
|
||||
@@ -621,6 +649,7 @@ export default function TaskTableContainer(props: {
|
||||
width,
|
||||
sortable: true,
|
||||
render: (record) => record.label_object_size?.[accessor] ?? "-",
|
||||
footer: totalSize[accessor] ?? 0,
|
||||
})
|
||||
|
||||
const cols: DataTableColumn<Task.DataProps>[] = [
|
||||
@@ -628,6 +657,7 @@ export default function TaskTableContainer(props: {
|
||||
accessor: "id",
|
||||
title: "任务ID",
|
||||
width: 90,
|
||||
footer: "总计",
|
||||
render: (record) => (
|
||||
<Button
|
||||
variant="transparent"
|
||||
@@ -805,7 +835,15 @@ export default function TaskTableContainer(props: {
|
||||
},
|
||||
]
|
||||
return cols
|
||||
}, [hasRolePermissions, info, projectId, router, setBackProps, type])
|
||||
}, [
|
||||
hasRolePermissions,
|
||||
info,
|
||||
projectId,
|
||||
router,
|
||||
setBackProps,
|
||||
totalSize,
|
||||
type,
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -307,6 +307,7 @@ const LabelPage = ({
|
||||
activeOperation,
|
||||
setActiveOperation,
|
||||
setDrawOption,
|
||||
setSubAttrPresetShow,
|
||||
showObjectList,
|
||||
showGroupList,
|
||||
showTaskList,
|
||||
@@ -1433,6 +1434,31 @@ const LabelPage = ({
|
||||
|
||||
const { setShift, focusInput } = useKeyEventStore()
|
||||
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<{
|
||||
activeOperation: string
|
||||
drawOption: "default" | "intersect" | "unite"
|
||||
@@ -1441,20 +1467,17 @@ const LabelPage = ({
|
||||
|
||||
const handleShortcut = useCallback(
|
||||
(key: string, ctrl: boolean) => {
|
||||
let checkOperation = getOperationSchema(key)
|
||||
const mode = usePaperStore.getState().mode
|
||||
const nextOperationId = ctrl ? (+key + 10).toString() : key
|
||||
const checkOperation = getOperationSchema(nextOperationId)
|
||||
// 标注方案里面存在的非文本类型标注才能切换
|
||||
if (checkOperation) {
|
||||
// 辅助标注模式下非多边形类型不能切换
|
||||
if (mode === "support" && checkOperation.label_type !== 102) return
|
||||
if (ctrl) {
|
||||
setActiveOperation((+key + 10).toString())
|
||||
} else {
|
||||
setActiveOperation(key)
|
||||
}
|
||||
handleSelectOperation(nextOperationId)
|
||||
}
|
||||
},
|
||||
[getOperationSchema, setActiveOperation]
|
||||
[getOperationSchema, handleSelectOperation]
|
||||
)
|
||||
|
||||
const resolveSelectedObjectContext = useCallback(() => {
|
||||
@@ -1594,6 +1617,50 @@ const LabelPage = ({
|
||||
})
|
||||
}, [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(
|
||||
(
|
||||
imageId: string,
|
||||
@@ -2185,7 +2252,7 @@ const LabelPage = ({
|
||||
|
||||
if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
|
||||
e.preventDefault()
|
||||
handleToggleHideAllObjects()
|
||||
handleToggleHideSelectionOrAll()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2487,6 +2554,7 @@ const LabelPage = ({
|
||||
focusInput,
|
||||
getOperationSchema,
|
||||
handleToggleHideAllObjects,
|
||||
handleToggleHideSelectionOrAll,
|
||||
handleMergeSelectedObjects,
|
||||
handleContinueSelectedPolygon,
|
||||
handleShortcut,
|
||||
@@ -2519,6 +2587,14 @@ const LabelPage = ({
|
||||
drawShortcutSnapshotRef.current = null
|
||||
}, [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) => {
|
||||
e.preventDefault()
|
||||
// 备份
|
||||
@@ -2708,6 +2784,7 @@ const LabelPage = ({
|
||||
oldWorkLoad={oldWorkLoad}
|
||||
updateOldWorkLoad={updateOldWorkLoad}
|
||||
isLabelTask={isLabelTask}
|
||||
onOperationChange={handleSelectOperation}
|
||||
renderPolygons={
|
||||
paperContainerRef.current
|
||||
? paperContainerRef.current.renderPolygons
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { RichTextEditor } from "@mantine/tiptap"
|
||||
import "@mantine/tiptap/styles.css"
|
||||
import { IconPhoto } from "@tabler/icons-react"
|
||||
import Image from "@tiptap/extension-image"
|
||||
import Link from "@tiptap/extension-link"
|
||||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
@@ -148,7 +147,7 @@ const EditorContainer = ({
|
||||
<RichTextEditor.Hr />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
{/* <RichTextEditor.ControlsGroup>
|
||||
<RichTextEditor.Link />
|
||||
<RichTextEditor.Unlink />
|
||||
</RichTextEditor.ControlsGroup>
|
||||
@@ -160,7 +159,7 @@ const EditorContainer = ({
|
||||
title="上传图片">
|
||||
<IconPhoto stroke={1.5} size="1rem" />
|
||||
</RichTextEditor.Control>
|
||||
</RichTextEditor.ControlsGroup>
|
||||
</RichTextEditor.ControlsGroup> */}
|
||||
</RichTextEditor.Toolbar>
|
||||
|
||||
<RichTextEditor.Content h={144} style={{ overflowY: "auto" }} />
|
||||
|
||||
@@ -52,7 +52,7 @@ import { renderGroupPath } from "../useRenderGroupPath"
|
||||
import useRenderTag from "../useRenderTag"
|
||||
import { useRightToolsStore } from "../useRightToolsStore"
|
||||
import { useTopToolsStore } from "../useTopToolsStore"
|
||||
import { checkCommentsIsSame } from "../util"
|
||||
import { buildSubAttributeFormValues, checkCommentsIsSame } from "../util"
|
||||
import { safeClone } from "../utils/clone"
|
||||
import { labelTypeMap } from "../utils/constants"
|
||||
import { adjustPoints } from "../utils/paperjs"
|
||||
@@ -3315,50 +3315,26 @@ const PaperContainer = (
|
||||
.get(activeImage)
|
||||
?.get(operationId)
|
||||
?.find((item) => item[0] === id)
|
||||
const operationSchema = getOperationSchema(operationId)
|
||||
const nowComment =
|
||||
currentPath?.[2].comment?.[currentPath?.[2].comment.length - 1]
|
||||
|
||||
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 =
|
||||
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.check_box_comments
|
||||
: []
|
||||
)
|
||||
form.setFieldValue(
|
||||
"text_comments",
|
||||
: [],
|
||||
text_comments:
|
||||
nowComment && nowComment.text_comments.length
|
||||
? nowComment.text_comments[0]
|
||||
: ""
|
||||
)
|
||||
}
|
||||
: "",
|
||||
})
|
||||
// 可选的ids
|
||||
let images = Array.from(labelState.keys()).filter(
|
||||
(key) => key !== activeImage
|
||||
@@ -3580,11 +3556,10 @@ const PaperContainer = (
|
||||
})
|
||||
}
|
||||
// newOperation的subAttributes
|
||||
let subAttr = Object.assign(
|
||||
{},
|
||||
let subAttr = safeClone(
|
||||
useTopToolsStore.getState().subAttributes[
|
||||
operationSchema?.label_class ?? ""
|
||||
]
|
||||
] || {}
|
||||
)
|
||||
// 修改labeldata
|
||||
setOperation(activeImage, newOperation, [
|
||||
@@ -3612,16 +3587,21 @@ const PaperContainer = (
|
||||
)
|
||||
|
||||
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, [
|
||||
id,
|
||||
pathItem?.[1] || [],
|
||||
{
|
||||
...newDetail,
|
||||
...(pathItem?.[2] || {}),
|
||||
sub_attributes: Object.assign(newDetail.sub_attributes || {}, {
|
||||
[key]: value,
|
||||
}),
|
||||
sub_attributes: currentSubAttributes,
|
||||
},
|
||||
pathItem?.[3] || [],
|
||||
])
|
||||
@@ -3892,7 +3872,7 @@ const PaperContainer = (
|
||||
)
|
||||
handleChangeContextMenuSubAttr(
|
||||
item.chinese_name,
|
||||
value.toString()
|
||||
value
|
||||
)
|
||||
}}>
|
||||
<Stack gap={6} mt={6}>
|
||||
|
||||
@@ -166,6 +166,24 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
[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 = () => {
|
||||
usePaperStore
|
||||
@@ -215,11 +233,16 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(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]?.includes(id)) {
|
||||
if (pressShift) {
|
||||
if (getItemsById(id)?.length) {
|
||||
getItemsById(id).forEach((item) => {
|
||||
if (objectItems.length && targetItem) {
|
||||
objectItems.forEach((item) => {
|
||||
if (item instanceof paper.Path) {
|
||||
handleSelectedPath(item)
|
||||
} else if (item instanceof paper.CompoundPath) {
|
||||
@@ -228,13 +251,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
else item.data.selected = true
|
||||
}
|
||||
})
|
||||
bringTagItemsToFront(id)
|
||||
let category = useTopToolsStore
|
||||
.getState()
|
||||
.objectOperations.find(
|
||||
(item) => item.category_id.toString() === operationId
|
||||
)
|
||||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||||
getItemById(id)!.position
|
||||
const globalPosition = targetItem.globalMatrix.transform(
|
||||
targetItem.position
|
||||
)
|
||||
const { top, left } = getShowContextMenuPosition(
|
||||
globalPosition,
|
||||
@@ -251,8 +275,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
} else {
|
||||
clearSelectedItems()
|
||||
selectedPath[activeImage].forEach((selectedId) => {
|
||||
if (getItemsById(selectedId)?.length) {
|
||||
getItemsById(selectedId).forEach((item) => {
|
||||
const selectedObjectItems = getObjectItemsById(selectedId)
|
||||
if (selectedObjectItems.length) {
|
||||
selectedObjectItems.forEach((item) => {
|
||||
cancelSelectedPathColor(item)
|
||||
})
|
||||
useOtherToolsStore.getState().setShowContextMenu({
|
||||
@@ -267,8 +292,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
})
|
||||
}
|
||||
})
|
||||
if (getItemsById(id)?.length) {
|
||||
getItemsById(id).forEach((item) => {
|
||||
if (objectItems.length && targetItem) {
|
||||
objectItems.forEach((item) => {
|
||||
if (item instanceof paper.Path) {
|
||||
handleSelectedPath(item)
|
||||
} else if (item instanceof paper.CompoundPath) {
|
||||
@@ -277,13 +302,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
else item.data.selected = true
|
||||
}
|
||||
})
|
||||
bringTagItemsToFront(id)
|
||||
let category = useTopToolsStore
|
||||
.getState()
|
||||
.objectOperations.find(
|
||||
(item) => item.category_id.toString() === operationId
|
||||
)
|
||||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||||
getItemById(id)!.position
|
||||
const globalPosition = targetItem.globalMatrix.transform(
|
||||
targetItem.position
|
||||
)
|
||||
const { top, left } = getShowContextMenuPosition(
|
||||
globalPosition,
|
||||
@@ -302,8 +328,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
} else {
|
||||
clearSelectedItems()
|
||||
updateSelectedPath(activeImage, "DELETE", id)
|
||||
if (getItemsById(id)?.length) {
|
||||
getItemsById(id).forEach((item) => {
|
||||
if (objectItems.length) {
|
||||
objectItems.forEach((item) => {
|
||||
cancelSelectedPathColor(item)
|
||||
})
|
||||
useOtherToolsStore.getState().setShowContextMenu({
|
||||
@@ -321,8 +347,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
} else {
|
||||
clearSelectedItems()
|
||||
updateSelectedPath(activeImage, "ADD", id)
|
||||
if (getItemsById(id)?.length) {
|
||||
getItemsById(id).forEach((item) => {
|
||||
if (objectItems.length && targetItem) {
|
||||
objectItems.forEach((item) => {
|
||||
if (item instanceof paper.Path) {
|
||||
handleSelectedPath(item)
|
||||
} else if (item instanceof paper.CompoundPath) {
|
||||
@@ -337,14 +363,15 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
// item.fillColor = item.data?.fillColor;
|
||||
// item.bringToFront();
|
||||
})
|
||||
bringTagItemsToFront(id)
|
||||
|
||||
let category = useTopToolsStore
|
||||
.getState()
|
||||
.objectOperations.find(
|
||||
(item) => item.category_id.toString() === operationId
|
||||
)
|
||||
const globalPosition = getItemById(id)!.globalMatrix.transform(
|
||||
getItemById(id)!.position
|
||||
const globalPosition = targetItem.globalMatrix.transform(
|
||||
targetItem.position
|
||||
)
|
||||
const { top, left } = getShowContextMenuPosition(
|
||||
globalPosition,
|
||||
@@ -362,8 +389,10 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
},
|
||||
[
|
||||
activeImage,
|
||||
bringTagItemsToFront,
|
||||
getObjectItemsById,
|
||||
getItemById,
|
||||
getItemsById,
|
||||
pathStatus,
|
||||
pressShift,
|
||||
selectedPath,
|
||||
updateSelectedPath,
|
||||
@@ -889,10 +918,13 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
||||
: "var(--mantine-color-gray-0)"
|
||||
}
|
||||
c={isSelected ? "white" : undefined}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
if (isChildHide) return
|
||||
handleSelect(child[0], operationId)
|
||||
}
|
||||
style={{ cursor: "pointer" }}>
|
||||
}}
|
||||
style={{
|
||||
cursor: isChildHide ? "default" : "pointer",
|
||||
}}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
|
||||
@@ -98,7 +98,7 @@ import {
|
||||
NODE_SIZE_MIN,
|
||||
useTopToolsStore,
|
||||
} from "../useTopToolsStore"
|
||||
import { getSubAttrStatus } from "../util"
|
||||
import { buildSubAttributeFormValues, getSubAttrStatus } from "../util"
|
||||
import { safeClone } from "../utils/clone"
|
||||
import { labelTypeMap, TaskStatusEnum } from "../utils/constants"
|
||||
import { adjustAllPoints } from "../utils/paperjs"
|
||||
@@ -118,6 +118,7 @@ interface TopToolsComponentProps {
|
||||
oldWorkLoad: WorkLoad | null
|
||||
updateOldWorkLoad: (data: WorkLoad) => void
|
||||
isLabelTask: boolean
|
||||
onOperationChange: (operationId: string) => void
|
||||
renderPolygons: (
|
||||
operationId: string,
|
||||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||||
@@ -161,6 +162,7 @@ const TopTools = (
|
||||
oldWorkLoad,
|
||||
updateOldWorkLoad,
|
||||
isLabelTask,
|
||||
onOperationChange,
|
||||
renderPolygons,
|
||||
} = props
|
||||
const { backUrl, basePath } = useBackUrlStore()
|
||||
@@ -1488,36 +1490,10 @@ const TopTools = (
|
||||
operationId: string,
|
||||
values: Record<string, string | string[] | undefined> = {}
|
||||
) => {
|
||||
const schema = getOperationSchema(operationId)
|
||||
if (!schema?.sub_attributes_describe?.length) return {}
|
||||
|
||||
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
|
||||
return buildSubAttributeFormValues(
|
||||
getOperationSchema(operationId),
|
||||
values
|
||||
)
|
||||
},
|
||||
[getOperationSchema]
|
||||
)
|
||||
@@ -1627,6 +1603,10 @@ const TopTools = (
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
form.reset()
|
||||
form.setFieldValue(
|
||||
"sub_attributes",
|
||||
getPresetFormValues(activeOperation, {})
|
||||
)
|
||||
if (operationSchema?.label_class)
|
||||
setsubAttributes(operationSchema.label_class, {})
|
||||
}}>
|
||||
@@ -2334,7 +2314,7 @@ const TopTools = (
|
||||
<UnstyledButton
|
||||
key={item.category_id}
|
||||
onClick={() => {
|
||||
setActiveOperation(item.category_id.toString())
|
||||
onOperationChange(item.category_id.toString())
|
||||
setOperationPickerOpened(false)
|
||||
setOperationKeyword("")
|
||||
}}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { usePaperSupportStore } from "./usePaperSupportStore"
|
||||
import { useRightToolsStore } from "./useRightToolsStore"
|
||||
import { DEFAULT_NODE_SIZE, useTopToolsStore } from "./useTopToolsStore"
|
||||
import { safeClone } from "./utils/clone"
|
||||
import { getObjectTagVisibility } from "./utils/objectVisibility"
|
||||
|
||||
interface ContinuePolygonTarget {
|
||||
imageId: string
|
||||
@@ -216,6 +217,16 @@ const initialPaperState = {
|
||||
reciprocalRasterScale: {},
|
||||
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 group = usePaperStore.getState().group!
|
||||
const imageId = useBottomToolsStore.getState().activeImage
|
||||
@@ -450,12 +461,7 @@ const clearPaperSelectedObjects = () => {
|
||||
.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
|
||||
}
|
||||
resetPaperShapeFillColor(item)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
if (item.data.type === "rectangle" && item instanceof paper.Path) {
|
||||
item.data.selected = true
|
||||
@@ -2011,15 +2027,14 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
type: "tag",
|
||||
},
|
||||
})
|
||||
// 标识位为true时:选中路径隐藏tags; 没选中显示tags
|
||||
const configs = useTopToolsStore.getState().showTagsConfigs
|
||||
// 仅恢复当前应显示的标签,隐藏对象的标签保持不可见
|
||||
if (useTopToolsStore.getState().showTags) {
|
||||
const setTagVisibleByConfig = (tag: any) => {
|
||||
if (tag.data.tag_category === "mask")
|
||||
tag.visible = configs.includes("外框")
|
||||
else if (tag.data.tag_category === "point_text")
|
||||
tag.visible = configs.includes("点ID")
|
||||
else tag.visible = true
|
||||
tag.visible = getObjectTagVisibility(
|
||||
useBottomToolsStore.getState().activeImage,
|
||||
Number(tag.data?.id),
|
||||
tag.data?.tag_category
|
||||
)
|
||||
}
|
||||
lastTags.forEach(setTagVisibleByConfig)
|
||||
tags.forEach(setTagVisibleByConfig)
|
||||
@@ -3147,6 +3162,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
})
|
||||
)
|
||||
polygon.data = Object.assign({ type: "polygon", ...option }, data)
|
||||
insertItemBelowTags(polygon)
|
||||
points?.forEach((point) => {
|
||||
polygon.add(point)
|
||||
})
|
||||
@@ -3219,6 +3235,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
})
|
||||
)
|
||||
compoundPath.data = Object.assign({ type: "polygon", ...option }, data)
|
||||
insertItemBelowTags(compoundPath)
|
||||
compoundPath.onMouseDown = (e: {
|
||||
event: MouseEvent
|
||||
point: paper.Point
|
||||
@@ -4606,8 +4623,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
.getState()
|
||||
.group!.getItems({})
|
||||
.forEach((item) => {
|
||||
// clear color
|
||||
item.fillColor = item?.data?.blankColor
|
||||
resetPaperShapeFillColor(item)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4803,6 +4819,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
Object.assign(option, { strokeScaling: false })
|
||||
)
|
||||
rect.data = Object.assign({ type: "rectangle", ...option }, data)
|
||||
insertItemBelowTags(rect)
|
||||
rect.onMouseEnter = (_e: paper.MouseEvent) => {
|
||||
// if (usePaperStore.getState().mode === "pan") {
|
||||
// e.target.strokeWidth += 2;
|
||||
@@ -5045,6 +5062,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
)
|
||||
)
|
||||
brush.data = Object.assign({ type: "brush", ...option }, data)
|
||||
insertItemBelowTags(brush)
|
||||
// points?.forEach((point) => {
|
||||
// brush.add(point);
|
||||
// });
|
||||
@@ -5545,6 +5563,7 @@ export const usePaperStore = create<PaperState>((set) => ({
|
||||
const newScale = useTopToolsStore.getState().scale / currentScaling
|
||||
myCircle.scale(newScale / currentScaling)
|
||||
myCircle.data = Object.assign({ type: "circle", ...option }, data)
|
||||
insertItemBelowTags(myCircle)
|
||||
syncPaperCircleDiameter(myCircle)
|
||||
myCircle.onMouseDown = (e: { event: MouseEvent; point: paper.Point }) => {
|
||||
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)
|
||||
insertItemBelowTags(myCircleLine)
|
||||
return myCircleLine
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useLabelStore } from "./store"
|
||||
import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||
import { usePaperStore } from "./usePaperStore"
|
||||
import { useTopToolsStore } from "./useTopToolsStore"
|
||||
import { getObjectTagVisibility } from "./utils/objectVisibility"
|
||||
|
||||
const TAG_FONT_SIZE = 12
|
||||
const TAG_VERTICAL_GAP = 8
|
||||
@@ -159,13 +160,12 @@ const useRenderTag = () => {
|
||||
const activeImage = useBottomToolsStore.getState().activeImage
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return
|
||||
const visible = useTopToolsStore.getState().showTags
|
||||
const configs = useTopToolsStore.getState().showTagsConfigs
|
||||
const groupScale =
|
||||
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
|
||||
? group.scaling.x
|
||||
: 1
|
||||
const applyPointTagCompensation = (
|
||||
const applyScreenSpaceTagCompensation = (
|
||||
item: paper.Item,
|
||||
anchor: [number, number]
|
||||
) => {
|
||||
@@ -235,7 +235,7 @@ const useRenderTag = () => {
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
visible: getObjectTagVisibility(activeImage, id, "text"),
|
||||
})
|
||||
text.translate(
|
||||
new paper.Point(
|
||||
@@ -259,8 +259,10 @@ const useRenderTag = () => {
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
visible: getObjectTagVisibility(activeImage, id, "text_mask"),
|
||||
})
|
||||
applyScreenSpaceTagCompensation(text, tagAnchor)
|
||||
applyScreenSpaceTagCompensation(textMask, tagAnchor)
|
||||
text.bringToFront()
|
||||
}
|
||||
|
||||
@@ -276,7 +278,7 @@ const useRenderTag = () => {
|
||||
tag_category: "mask",
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("外框"),
|
||||
visible: getObjectTagVisibility(activeImage, id, "mask"),
|
||||
})
|
||||
// 标注对象点id
|
||||
if (path instanceof paper.CompoundPath) {
|
||||
@@ -301,9 +303,9 @@ const useRenderTag = () => {
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
visible: getObjectTagVisibility(activeImage, id, "point_text"),
|
||||
})
|
||||
applyPointTagCompensation(pointText, pointTextPosition)
|
||||
applyScreenSpaceTagCompensation(pointText, pointTextPosition)
|
||||
index++
|
||||
})
|
||||
})
|
||||
@@ -327,9 +329,9 @@ const useRenderTag = () => {
|
||||
tag_anchor: pointTextPosition,
|
||||
},
|
||||
parent: group,
|
||||
visible: visible && configs.includes("点ID"),
|
||||
visible: getObjectTagVisibility(activeImage, id, "point_text"),
|
||||
})
|
||||
applyPointTagCompensation(pointText, pointTextPosition)
|
||||
applyScreenSpaceTagCompensation(pointText, pointTextPosition)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -86,6 +86,45 @@ export const getSubAttrStatus = (
|
||||
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: (
|
||||
a: any[] | null,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import paper from "paper"
|
||||
import { useLabelStore, useObjectStore } from "../store"
|
||||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||||
import { usePaperStore } from "../usePaperStore"
|
||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||
import { useTopToolsStore } from "../useTopToolsStore"
|
||||
|
||||
const getTagVisibility = (tagCategory?: string) => {
|
||||
const getTagVisibilityByConfig = (tagCategory?: string) => {
|
||||
const { showTags, showTagsConfigs } = useTopToolsStore.getState()
|
||||
if (!showTags) return false
|
||||
if (tagCategory === "mask") return showTagsConfigs.includes("外框")
|
||||
@@ -11,6 +13,16 @@ const getTagVisibility = (tagCategory?: string) => {
|
||||
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) => {
|
||||
if (!id) return
|
||||
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)
|
||||
if (!sameIdItems?.length) return
|
||||
sameIdItems.forEach((item) => {
|
||||
@@ -31,7 +73,11 @@ const syncObjectItemsVisibility = (objectId: number, hide: boolean) => {
|
||||
return
|
||||
}
|
||||
if (item.data?.type === "tag") {
|
||||
item.visible = getTagVisibility(item.data?.tag_category)
|
||||
item.visible = getObjectTagVisibility(
|
||||
imageId,
|
||||
objectId,
|
||||
item.data?.tag_category
|
||||
)
|
||||
return
|
||||
}
|
||||
item.visible = true
|
||||
@@ -49,8 +95,9 @@ export const toggleObjectHideByShortcut = (
|
||||
|
||||
if (hide) {
|
||||
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||
syncObjectItemsVisibility(objectId, hide)
|
||||
syncObjectItemsVisibility(imageId, objectId, hide)
|
||||
setSelectedItemFalse(objectId)
|
||||
clearHiddenObjectSelectionState(imageId, objectId)
|
||||
|
||||
const operationChildren = storeLabel.get(imageId)?.get(operationId) || []
|
||||
const hasVisibleChild = operationChildren.some((child) => {
|
||||
@@ -67,5 +114,5 @@ export const toggleObjectHideByShortcut = (
|
||||
|
||||
updateOperationStatus(imageId + operationId, "HIDE", hide)
|
||||
updatePathStatus(imageId + objectId, "HIDE", hide)
|
||||
syncObjectItemsVisibility(objectId, hide)
|
||||
syncObjectItemsVisibility(imageId, objectId, hide)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user