Merge branch 'zh' into 'main'
Zh See merge request prism/lable/labelimage!17
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
Group,
|
Group,
|
||||||
|
MultiSelect,
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -63,10 +64,17 @@ const EMPTY_TASK_SIZE: Task.SizeProps = Object.freeze({
|
|||||||
comment_question_size: 0,
|
comment_question_size: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
function createInitialFilters() {
|
interface FilterFormRecord {
|
||||||
|
search_id: string
|
||||||
|
search_status: string
|
||||||
|
current_uid: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialFilters(): FilterFormRecord {
|
||||||
return {
|
return {
|
||||||
search_id: "",
|
search_id: "",
|
||||||
search_status: "0",
|
search_status: "0",
|
||||||
|
current_uid: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +350,20 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
{searchExpanded ? "收起筛选" : "展开筛选"}
|
{searchExpanded ? "收起筛选" : "展开筛选"}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<SettingFilterActions>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</SettingFilterActions>
|
||||||
</Group>
|
</Group>
|
||||||
<Collapse
|
<Collapse
|
||||||
in={searchExpanded}
|
in={searchExpanded}
|
||||||
@@ -363,7 +385,6 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
search_id: e.target.value,
|
search_id: e.target.value,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
style={{ width: 160 }}
|
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
label="状态"
|
label="状态"
|
||||||
@@ -379,23 +400,20 @@ export default function OwnTaskTableContainer(props: {
|
|||||||
}
|
}
|
||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
/>
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="当前负责人"
|
||||||
|
data={Array.from(userEnums.allEnum.entries()).map(([x, y]) => ({
|
||||||
|
label: y,
|
||||||
|
value: x.toString(),
|
||||||
|
}))}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
value={filters.current_uid}
|
||||||
|
onChange={(v) => setFilters((s) => ({ ...s, current_uid: v }))}
|
||||||
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<SettingFilterActions>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
leftSection={<IconRefresh size={16} />}
|
|
||||||
onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconSearch size={16} />}
|
|
||||||
onClick={handleSearch}
|
|
||||||
loading={loading}>
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
</SettingFilterActions>
|
|
||||||
</SettingFilterPanel>
|
</SettingFilterPanel>
|
||||||
|
|
||||||
<SettingContentPanel
|
<SettingContentPanel
|
||||||
|
|||||||
@@ -865,6 +865,20 @@ export default function TaskTableContainer(props: {
|
|||||||
{searchExpanded ? "收起筛选" : "展开筛选"}
|
{searchExpanded ? "收起筛选" : "展开筛选"}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
<SettingFilterActions>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={loading}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
</SettingFilterActions>
|
||||||
</Group>
|
</Group>
|
||||||
<Collapse
|
<Collapse
|
||||||
in={searchExpanded}
|
in={searchExpanded}
|
||||||
@@ -993,20 +1007,6 @@ export default function TaskTableContainer(props: {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
<SettingFilterActions>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
leftSection={<IconRefresh size={16} />}
|
|
||||||
onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconSearch size={16} />}
|
|
||||||
onClick={handleSearch}
|
|
||||||
loading={loading}>
|
|
||||||
查询
|
|
||||||
</Button>
|
|
||||||
</SettingFilterActions>
|
|
||||||
</SettingFilterPanel>
|
</SettingFilterPanel>
|
||||||
<SettingContentPanel
|
<SettingContentPanel
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -2156,7 +2156,8 @@ const LabelPage = ({
|
|||||||
|
|
||||||
if (lowerKey === "s" && commandKey) {
|
if (lowerKey === "s" && commandKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsConfirmSave(true)
|
topRef.current?.handleSave()
|
||||||
|
// setIsConfirmSave(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lowerKey === "g" && commandKey) {
|
if (lowerKey === "g" && commandKey) {
|
||||||
@@ -2192,6 +2193,12 @@ const LabelPage = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === "F2") {
|
||||||
|
e.preventDefault()
|
||||||
|
paperContainerRef.current?.filterBySizeThreshold?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 追踪
|
// 追踪
|
||||||
if (e.key === "i") {
|
if (e.key === "i") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -2585,6 +2592,9 @@ const LabelPage = ({
|
|||||||
updateOldWorkLoad={updateOldWorkLoad}
|
updateOldWorkLoad={updateOldWorkLoad}
|
||||||
isLabelTask={isLabelTask}
|
isLabelTask={isLabelTask}
|
||||||
onOperationChange={handleSelectOperation}
|
onOperationChange={handleSelectOperation}
|
||||||
|
onFilterBySizeThreshold={() => {
|
||||||
|
paperContainerRef.current?.filterBySizeThreshold?.()
|
||||||
|
}}
|
||||||
renderPolygons={
|
renderPolygons={
|
||||||
paperContainerRef.current
|
paperContainerRef.current
|
||||||
? paperContainerRef.current.renderPolygons
|
? paperContainerRef.current.renderPolygons
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react"
|
} from "react"
|
||||||
|
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||||
|
import { usePaperStore } from "../usePaperStore"
|
||||||
import { useTopToolsStore } from "../useTopToolsStore"
|
import { useTopToolsStore } from "../useTopToolsStore"
|
||||||
|
|
||||||
interface AssistShapeComponentProps {
|
interface AssistShapeComponentProps {
|
||||||
@@ -18,6 +20,10 @@ const AssistShapeComponent = (
|
|||||||
) => {
|
) => {
|
||||||
const { size } = props
|
const { size } = props
|
||||||
const scale = useTopToolsStore((state) => state.scale)
|
const scale = useTopToolsStore((state) => state.scale)
|
||||||
|
const activeImage = useBottomToolsStore((state) => state.activeImage)
|
||||||
|
const rasterScale = usePaperStore(
|
||||||
|
(state) => state.rasterScale[activeImage] ?? 1
|
||||||
|
)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
|
||||||
|
|
||||||
@@ -27,7 +33,7 @@ const AssistShapeComponent = (
|
|||||||
const ctx = canvas?.getContext("2d")
|
const ctx = canvas?.getContext("2d")
|
||||||
if (!canvas || !ctx) return
|
if (!canvas || !ctx) return
|
||||||
|
|
||||||
const radius = Math.max(1, size * scale)
|
const radius = Math.max(1, size * scale * rasterScale)
|
||||||
const side = radius * 2
|
const side = radius * 2
|
||||||
const left = centerX - radius
|
const left = centerX - radius
|
||||||
const top = centerY - radius
|
const top = centerY - radius
|
||||||
@@ -41,7 +47,7 @@ const AssistShapeComponent = (
|
|||||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
},
|
},
|
||||||
[scale, size]
|
[rasterScale, scale, size]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ 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"
|
||||||
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
import {
|
||||||
|
clearSupportAnnotationItem,
|
||||||
|
syncDeletedPathGroupState,
|
||||||
|
usePaperStore,
|
||||||
|
} from "../usePaperStore"
|
||||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||||
import { renderGroupPath } from "../useRenderGroupPath"
|
import { renderGroupPath } from "../useRenderGroupPath"
|
||||||
import useRenderTag from "../useRenderTag"
|
import useRenderTag from "../useRenderTag"
|
||||||
@@ -55,6 +59,12 @@ import { useTopToolsStore } from "../useTopToolsStore"
|
|||||||
import { buildSubAttributeFormValues, 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 { filterMultiPolygonByAreaThreshold } from "../utils/geometry/areaFilter"
|
||||||
|
import {
|
||||||
|
contoursToMultiPolygon,
|
||||||
|
multiPolygonToStoredContours,
|
||||||
|
paperShapeToMultiPolygon,
|
||||||
|
} from "../utils/geometry/booleanAdapter"
|
||||||
import { adjustPoints } from "../utils/paperjs"
|
import { adjustPoints } from "../utils/paperjs"
|
||||||
import AssistShapeComponent from "./AssistShapeComponent"
|
import AssistShapeComponent from "./AssistShapeComponent"
|
||||||
import CrosshairComponent from "./CrosshairComponent"
|
import CrosshairComponent from "./CrosshairComponent"
|
||||||
@@ -158,6 +168,7 @@ const SHORTCUT_GUIDE_SECTIONS = [
|
|||||||
title: "专项工具",
|
title: "专项工具",
|
||||||
items: [
|
items: [
|
||||||
["F1", "进入辅助标注 / 在辅助模式下确认"],
|
["F1", "进入辅助标注 / 在辅助模式下确认"],
|
||||||
|
["F2", "按尺寸阈值过滤小区域 / 小镂空"],
|
||||||
["I", "执行追踪"],
|
["I", "执行追踪"],
|
||||||
["P", "QA 文本场景检查错词"],
|
["P", "QA 文本场景检查错词"],
|
||||||
],
|
],
|
||||||
@@ -1040,6 +1051,343 @@ const PaperContainer = (
|
|||||||
}
|
}
|
||||||
}, [clearPendingFrameSwitch, loadingData])
|
}, [clearPendingFrameSwitch, loadingData])
|
||||||
|
|
||||||
|
const getSupportPreviewColors = useCallback(() => {
|
||||||
|
const operationSchema =
|
||||||
|
projectDetail?.label_schema_list?.find(
|
||||||
|
(item) =>
|
||||||
|
item.category_id === +useTopToolsStore.getState().activeOperation
|
||||||
|
) || null
|
||||||
|
|
||||||
|
return {
|
||||||
|
strokeColor: operationSchema
|
||||||
|
? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||||||
|
: "rgb(0,0,0)",
|
||||||
|
fillColor: operationSchema
|
||||||
|
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||||||
|
: "rgba(0,0,0,0.3)",
|
||||||
|
blankColor: operationSchema
|
||||||
|
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||||||
|
: "rgba(0,0,0,0.01)",
|
||||||
|
}
|
||||||
|
}, [projectDetail?.label_schema_list])
|
||||||
|
|
||||||
|
const clearSupportPreviewShapeItems = useCallback(() => {
|
||||||
|
usePaperStore
|
||||||
|
.getState()
|
||||||
|
.group?.getItems({
|
||||||
|
match: (item: paper.Item) =>
|
||||||
|
item.data?.id === "support" &&
|
||||||
|
["polygon", "parent"].includes(item.data?.type || ""),
|
||||||
|
})
|
||||||
|
.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const renderSupportPreviewGeometry = useCallback(
|
||||||
|
(geometry: ReturnType<typeof paperShapeToMultiPolygon>) => {
|
||||||
|
const paperGroup = usePaperStore.getState().group
|
||||||
|
if (!paperGroup) return
|
||||||
|
|
||||||
|
clearSupportPreviewShapeItems()
|
||||||
|
|
||||||
|
const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
|
||||||
|
if (!points.length) return
|
||||||
|
|
||||||
|
const { strokeColor, fillColor, blankColor } = getSupportPreviewColors()
|
||||||
|
const outerPaths = points
|
||||||
|
.map((contour) => {
|
||||||
|
if (contour.length <= 3) return null
|
||||||
|
const path = new paper.Path({
|
||||||
|
segments: contour.slice(0, -1),
|
||||||
|
fillColor: blankColor,
|
||||||
|
strokeColor,
|
||||||
|
closed: true,
|
||||||
|
data: {
|
||||||
|
id: "support",
|
||||||
|
type: "polygon",
|
||||||
|
isHollowPolygon: false,
|
||||||
|
fillColor,
|
||||||
|
strokeColor,
|
||||||
|
blankColor,
|
||||||
|
},
|
||||||
|
parent: paperGroup,
|
||||||
|
strokeScaling: false,
|
||||||
|
})
|
||||||
|
path.fillColor = new paper.Color(fillColor)
|
||||||
|
return path
|
||||||
|
})
|
||||||
|
.filter((path): path is paper.Path => !!path)
|
||||||
|
const innerPaths = hollowPoints
|
||||||
|
.map((contour) => {
|
||||||
|
if (contour.length <= 3) return null
|
||||||
|
return new paper.Path({
|
||||||
|
segments: contour.slice(0, -1),
|
||||||
|
fillColor: blankColor,
|
||||||
|
strokeColor,
|
||||||
|
closed: true,
|
||||||
|
data: {
|
||||||
|
id: "support",
|
||||||
|
type: "polygon",
|
||||||
|
isHollowPolygon: true,
|
||||||
|
fillColor,
|
||||||
|
strokeColor,
|
||||||
|
blankColor,
|
||||||
|
},
|
||||||
|
parent: paperGroup,
|
||||||
|
strokeScaling: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter((path): path is paper.Path => !!path)
|
||||||
|
|
||||||
|
const compoundPath = new paper.CompoundPath({
|
||||||
|
children: [...outerPaths, ...innerPaths],
|
||||||
|
data: {
|
||||||
|
id: "support",
|
||||||
|
type: "parent",
|
||||||
|
},
|
||||||
|
strokeColor,
|
||||||
|
strokeWidth: 2,
|
||||||
|
fillColor,
|
||||||
|
strokeScaling: false,
|
||||||
|
})
|
||||||
|
compoundPath.parent = paperGroup
|
||||||
|
},
|
||||||
|
[clearSupportPreviewShapeItems, getSupportPreviewColors]
|
||||||
|
)
|
||||||
|
|
||||||
|
function rerenderActiveImageObjects() {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return
|
||||||
|
|
||||||
|
group
|
||||||
|
.getItems({})
|
||||||
|
.filter((item) => item.data.id)
|
||||||
|
.forEach((item) => {
|
||||||
|
item.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageData = useLabelStore.getState().label.get(activeImage)
|
||||||
|
if (!imageData) return
|
||||||
|
|
||||||
|
for (const key of imageData.keys()) {
|
||||||
|
renderPolygons(key, imageData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMinAreaBySizeThreshold = useCallback((sizeThreshold: number) => {
|
||||||
|
const normalizedSizeThreshold = Math.max(0, Number(sizeThreshold) || 0)
|
||||||
|
// Threshold input is already expressed in the visible canvas coordinate
|
||||||
|
// system, so the area threshold should be size^2 without reapplying
|
||||||
|
// rasterScale.
|
||||||
|
return normalizedSizeThreshold * normalizedSizeThreshold
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function filterSupportPreviewBySizeThreshold(sizeThreshold: number) {
|
||||||
|
const group = usePaperStore.getState().group
|
||||||
|
if (!group) return false
|
||||||
|
|
||||||
|
const compoundPath = group.getItems({
|
||||||
|
data: { id: "support", type: "parent" },
|
||||||
|
})?.[0] as paper.CompoundPath | undefined
|
||||||
|
if (!compoundPath) return false
|
||||||
|
|
||||||
|
const { geometry, removedOuterCount, removedHoleCount } =
|
||||||
|
filterMultiPolygonByAreaThreshold({
|
||||||
|
geometry: paperShapeToMultiPolygon(compoundPath),
|
||||||
|
minArea: getMinAreaBySizeThreshold(sizeThreshold),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!removedOuterCount && !removedHoleCount) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "辅助结果中未发现小于尺寸阈值的区域或镂空",
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSupportPreviewGeometry(geometry)
|
||||||
|
notifications.show({
|
||||||
|
color: "green",
|
||||||
|
message: `已按尺寸阈值过滤 ${removedOuterCount} 个小区域、${removedHoleCount} 个小镂空`,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSelectedPolygonBySizeThreshold(sizeThreshold: number) {
|
||||||
|
const selectedIds =
|
||||||
|
useObjectStore.getState().selectedPath[activeImage] || []
|
||||||
|
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先选中一个或多个 polygon 对象",
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const minArea = getMinAreaBySizeThreshold(sizeThreshold)
|
||||||
|
const nextLabel = safeClone(useLabelStore.getState().label)
|
||||||
|
let removedOuterCount = 0
|
||||||
|
let removedHoleCount = 0
|
||||||
|
let updatedObjectCount = 0
|
||||||
|
let removedObjectCount = 0
|
||||||
|
let skippedNonPolygonCount = 0
|
||||||
|
let skippedMissingCount = 0
|
||||||
|
const removedPathIds: number[] = []
|
||||||
|
const removedParentGroupIds: Array<number | undefined> = []
|
||||||
|
|
||||||
|
selectedIds.forEach((selectedId) => {
|
||||||
|
const selectedItems = usePaperStore.getState().getItemsById(selectedId)
|
||||||
|
const selectedShape =
|
||||||
|
selectedItems.find((item) => item instanceof paper.CompoundPath) ||
|
||||||
|
selectedItems.find((item) => item.data?.type === "polygon") ||
|
||||||
|
null
|
||||||
|
|
||||||
|
if (!selectedShape || selectedShape.data?.type !== "polygon") {
|
||||||
|
skippedNonPolygonCount += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationId = String(selectedShape.data?.operationId || "")
|
||||||
|
if (!operationId) {
|
||||||
|
skippedMissingCount += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationData = nextLabel.get(activeImage)?.get(operationId)
|
||||||
|
const pathIndex =
|
||||||
|
operationData?.findIndex((item) => item[0] === selectedId) ?? -1
|
||||||
|
const oldData = pathIndex >= 0 ? operationData?.[pathIndex] : null
|
||||||
|
|
||||||
|
if (!operationData || !oldData) {
|
||||||
|
skippedMissingCount += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
geometry,
|
||||||
|
removedOuterCount: currentRemovedOuterCount,
|
||||||
|
removedHoleCount: currentRemovedHoleCount,
|
||||||
|
} = filterMultiPolygonByAreaThreshold({
|
||||||
|
geometry: contoursToMultiPolygon({
|
||||||
|
outerContours: oldData[1],
|
||||||
|
holeContours: oldData[3],
|
||||||
|
}),
|
||||||
|
minArea,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!currentRemovedOuterCount && !currentRemovedHoleCount) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removedOuterCount += currentRemovedOuterCount
|
||||||
|
removedHoleCount += currentRemovedHoleCount
|
||||||
|
|
||||||
|
if (!geometry.length) {
|
||||||
|
nextLabel.get(activeImage)?.set(
|
||||||
|
operationId,
|
||||||
|
operationData.filter((item) => item[0] !== selectedId)
|
||||||
|
)
|
||||||
|
removedObjectCount += 1
|
||||||
|
removedPathIds.push(selectedId)
|
||||||
|
removedParentGroupIds.push(oldData[2]?.parentGroupId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
|
||||||
|
const currentDetail = oldData[2]
|
||||||
|
const nextDetail = currentDetail
|
||||||
|
? {
|
||||||
|
...currentDetail,
|
||||||
|
first_modified_timestamp:
|
||||||
|
currentDetail.first_modified_timestamp || dayjs().unix(),
|
||||||
|
first_modified_uid:
|
||||||
|
currentDetail.first_modified_uid ||
|
||||||
|
usePermissionStore.getState().user_id,
|
||||||
|
last_modified_timestamp: dayjs().unix(),
|
||||||
|
last_modified_uid: usePermissionStore.getState().user_id,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...initialDetail,
|
||||||
|
create_timestamp: dayjs().unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
operationData[pathIndex] = [selectedId, points, nextDetail, hollowPoints]
|
||||||
|
updatedObjectCount += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!updatedObjectCount && !removedObjectCount) {
|
||||||
|
notifications.show({
|
||||||
|
color: skippedNonPolygonCount ? "yellow" : "yellow",
|
||||||
|
message:
|
||||||
|
skippedNonPolygonCount === selectedIds.length
|
||||||
|
? "当前选中对象仅 polygon 支持尺寸过滤"
|
||||||
|
: "当前选中对象中未发现小于尺寸阈值的区域或镂空",
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedPathIds.length) {
|
||||||
|
syncDeletedPathGroupState({
|
||||||
|
imageId: activeImage,
|
||||||
|
removedPathIds,
|
||||||
|
removedParentGroupIds,
|
||||||
|
labelData: nextLabel,
|
||||||
|
})
|
||||||
|
removedPathIds.forEach((pathId) => {
|
||||||
|
useObjectStore
|
||||||
|
.getState()
|
||||||
|
.updateSelectedPath(activeImage, "DELETE", pathId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useLabelStore.getState().setLabel(nextLabel)
|
||||||
|
useLabelStore.getState().pushStateStack(nextLabel)
|
||||||
|
rerenderActiveImageObjects()
|
||||||
|
|
||||||
|
const summaryParts = [
|
||||||
|
`已按尺寸阈值过滤 ${updatedObjectCount + removedObjectCount} 个 polygon`,
|
||||||
|
`删除 ${removedOuterCount} 个小区域`,
|
||||||
|
`删除 ${removedHoleCount} 个小镂空`,
|
||||||
|
]
|
||||||
|
if (removedObjectCount) {
|
||||||
|
summaryParts.push(`移除 ${removedObjectCount} 个空对象`)
|
||||||
|
}
|
||||||
|
if (skippedNonPolygonCount) {
|
||||||
|
summaryParts.push(`跳过 ${skippedNonPolygonCount} 个非 polygon 对象`)
|
||||||
|
}
|
||||||
|
if (skippedMissingCount) {
|
||||||
|
summaryParts.push(`忽略 ${skippedMissingCount} 个异常对象`)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
color: "green",
|
||||||
|
message: summaryParts.join(","),
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBySizeThreshold() {
|
||||||
|
const sizeThreshold = Number(
|
||||||
|
useTopToolsStore.getState().auxiliarySizeThreshold
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!Number.isFinite(sizeThreshold) || sizeThreshold <= 0) {
|
||||||
|
notifications.show({
|
||||||
|
color: "yellow",
|
||||||
|
message: "请先设置大于 0 的尺寸阈值",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterSupportPreviewBySizeThreshold(sizeThreshold)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSelectedPolygonBySizeThreshold(sizeThreshold)
|
||||||
|
}
|
||||||
|
|
||||||
const renderSupportAnnotation = useCallback(
|
const renderSupportAnnotation = useCallback(
|
||||||
async (
|
async (
|
||||||
points: Array<[number, number]>,
|
points: Array<[number, number]>,
|
||||||
@@ -1055,20 +1403,7 @@ const PaperContainer = (
|
|||||||
loading: true,
|
loading: true,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
})
|
})
|
||||||
const operationSchema =
|
// const { strokeColor, fillColor, blankColor } = getSupportPreviewColors()
|
||||||
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 =
|
const currentScale =
|
||||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||||
|
|
||||||
@@ -1141,13 +1476,11 @@ const PaperContainer = (
|
|||||||
if (!promptPoints.length && !normalizedBoxes.length) {
|
if (!promptPoints.length && !normalizedBoxes.length) {
|
||||||
throw new Error("缺少有效提示,无法调用模型接口")
|
throw new Error("缺少有效提示,无法调用模型接口")
|
||||||
}
|
}
|
||||||
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getAuxiliaryAnnotation({
|
const data = await getAuxiliaryAnnotation({
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
image_name: activeImage,
|
image_name: activeImage,
|
||||||
// project_id: "image_test_bk",
|
|
||||||
prompt: {
|
prompt: {
|
||||||
...(promptPoints.length
|
...(promptPoints.length
|
||||||
? { foreground_points: promptPoints }
|
? { foreground_points: promptPoints }
|
||||||
@@ -1160,50 +1493,31 @@ const PaperContainer = (
|
|||||||
contour_approximation: 0,
|
contour_approximation: 0,
|
||||||
})
|
})
|
||||||
const { contours = [], hollows = [] } = data || {}
|
const { contours = [], hollows = [] } = data || {}
|
||||||
const outerPaths: any[] = []
|
const geometry = contoursToMultiPolygon({
|
||||||
const innerPaths: any[] = []
|
outerContours: contours
|
||||||
contours.forEach((contour: any, index: number) => {
|
.map((contour: any, index: number) => {
|
||||||
const normalizedContour = normalizeContour(contour)
|
if (hollows[index]) return null
|
||||||
if (normalizedContour.length > 3) {
|
return normalizeContour(contour).map(
|
||||||
const path = new paper.Path({
|
([x, y]) =>
|
||||||
segments: normalizedContour.map(([x, y]) => [
|
[x * currentScale, y * currentScale] as [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 (hollows[index]) {
|
.filter(
|
||||||
path.data.isHollowPolygon = true
|
(contour): contour is Array<[number, number]> => !!contour
|
||||||
innerPaths.push(path)
|
),
|
||||||
} else {
|
holeContours: contours
|
||||||
path.fillColor = new paper.Color(fillColor)
|
.map((contour: any, index: number) => {
|
||||||
outerPaths.push(path)
|
if (!hollows[index]) return null
|
||||||
}
|
return normalizeContour(contour).map(
|
||||||
}
|
([x, y]) =>
|
||||||
|
[x * currentScale, y * currentScale] as [number, number]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(contour): contour is Array<[number, number]> => !!contour
|
||||||
|
),
|
||||||
})
|
})
|
||||||
const compoundPath = new paper.CompoundPath({
|
renderSupportPreviewGeometry(geometry)
|
||||||
children: [...outerPaths, ...innerPaths],
|
|
||||||
data: {
|
|
||||||
id: "support",
|
|
||||||
type: "parent",
|
|
||||||
},
|
|
||||||
strokeColor: strokeColor,
|
|
||||||
strokeWidth: 2,
|
|
||||||
fillColor: fillColor,
|
|
||||||
})
|
|
||||||
compoundPath.parent = usePaperStore.getState().group!
|
|
||||||
notifications.show({
|
notifications.show({
|
||||||
id: "sam2",
|
id: "sam2",
|
||||||
message: "模型生成成功",
|
message: "模型生成成功",
|
||||||
@@ -1228,7 +1542,12 @@ const PaperContainer = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeImage, projectDetail]
|
[
|
||||||
|
activeImage,
|
||||||
|
// getSupportPreviewColors,
|
||||||
|
projectDetail,
|
||||||
|
renderSupportPreviewGeometry,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
// const renderTrackingAnnotation = useCallback(async () => {
|
// const renderTrackingAnnotation = useCallback(async () => {
|
||||||
@@ -4153,6 +4472,7 @@ const PaperContainer = (
|
|||||||
renderTrackingAnnotation,
|
renderTrackingAnnotation,
|
||||||
renderSupportAnnotation,
|
renderSupportAnnotation,
|
||||||
saveSupportAnnotationData,
|
saveSupportAnnotationData,
|
||||||
|
filterBySizeThreshold,
|
||||||
copyAnnotationDataToMultiFrame,
|
copyAnnotationDataToMultiFrame,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
|
HoverCard,
|
||||||
Paper,
|
Paper,
|
||||||
Popover,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import paper from "paper"
|
import paper from "paper"
|
||||||
import { useCallback, useMemo, useState } from "react"
|
import { useCallback, useMemo, useState } from "react"
|
||||||
|
import { Comment } from "../api/label/typing"
|
||||||
import { Project } from "../api/project/typing"
|
import { Project } from "../api/project/typing"
|
||||||
import { Task } from "../api/task/typing"
|
import { Task } from "../api/task/typing"
|
||||||
import { LabelState } from "../LabelNossr"
|
import { LabelState } from "../LabelNossr"
|
||||||
@@ -141,6 +142,65 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const getCommentTextColor = useCallback((type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 3:
|
||||||
|
return "yellow.9"
|
||||||
|
case 2:
|
||||||
|
return "red.7"
|
||||||
|
case 1:
|
||||||
|
return "green.7"
|
||||||
|
case 0:
|
||||||
|
return "gray.7"
|
||||||
|
default:
|
||||||
|
return "blue.7"
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getUserLabel = useCallback(
|
||||||
|
(uid?: number) => {
|
||||||
|
if (uid === undefined || uid === null) return "-"
|
||||||
|
return (
|
||||||
|
userOpts.find((item) => item.value === uid)?.label || uid.toString()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[userOpts]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getCommentCount = useCallback((commentItem: Comment) => {
|
||||||
|
const checkBoxComments = (commentItem.check_box_comments || []).filter(
|
||||||
|
(item) => item?.trim()
|
||||||
|
)
|
||||||
|
const textComments = (commentItem.text_comments || []).filter((item) =>
|
||||||
|
item?.trim()
|
||||||
|
)
|
||||||
|
return checkBoxComments.length + textComments.length
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCommentContent = useCallback((commentItem: Comment) => {
|
||||||
|
const mergedComments = [
|
||||||
|
...(commentItem.check_box_comments || []),
|
||||||
|
...(commentItem.text_comments || []),
|
||||||
|
].filter((item) => item?.trim())
|
||||||
|
return mergedComments.length ? mergedComments.join(",") : "无批注"
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getCommentDisplayType = useCallback(
|
||||||
|
(commentItem: Comment, lastModifiedTimestamp?: number) => {
|
||||||
|
const type = Math.max(0, Math.min(3, commentItem.comment_type ?? 0))
|
||||||
|
if (
|
||||||
|
type === 2 &&
|
||||||
|
lastModifiedTimestamp &&
|
||||||
|
lastModifiedTimestamp > commentItem.date &&
|
||||||
|
getCommentCount(commentItem) > 0
|
||||||
|
) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
},
|
||||||
|
[getCommentCount]
|
||||||
|
)
|
||||||
|
|
||||||
const { shift: pressShift } = useKeyEventStore()
|
const { shift: pressShift } = useKeyEventStore()
|
||||||
|
|
||||||
const continuePolygonTarget = usePaperStore(
|
const continuePolygonTarget = usePaperStore(
|
||||||
@@ -696,15 +756,10 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
comment.length === 1
|
comment.length === 1
|
||||||
? comment[comment.length - 1]
|
? comment[comment.length - 1]
|
||||||
: comment[comment.length - 2]
|
: comment[comment.length - 2]
|
||||||
let type = latest_comment.comment_type
|
const type = getCommentDisplayType(
|
||||||
if (type === 2) {
|
latest_comment,
|
||||||
const { last_modified_timestamp } = child[2]
|
child[2].last_modified_timestamp
|
||||||
if (
|
)
|
||||||
last_modified_timestamp &&
|
|
||||||
last_modified_timestamp > latest_comment.date
|
|
||||||
)
|
|
||||||
type = 3
|
|
||||||
}
|
|
||||||
visibleFlag = checkBoxsChecked[3 - type]
|
visibleFlag = checkBoxsChecked[3 - type]
|
||||||
}
|
}
|
||||||
if (checkBoxsChecked.every((value) => !value))
|
if (checkBoxsChecked.every((value) => !value))
|
||||||
@@ -954,12 +1009,7 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">创建:</Text>
|
<Text size="xs">创建:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(child[2]?.uid)}
|
||||||
userOpts.find(
|
|
||||||
(item) =>
|
|
||||||
item.value === child[2]?.uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -972,14 +1022,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">首次修改:</Text>
|
<Text size="xs">首次修改:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(
|
||||||
userOpts.find(
|
child[2]?.first_modified_uid
|
||||||
(item) =>
|
)}
|
||||||
item.value ===
|
|
||||||
child[2]
|
|
||||||
?.first_modified_uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -994,14 +1039,9 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
<Text size="xs">最新修改:</Text>
|
<Text size="xs">最新修改:</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{
|
{getUserLabel(
|
||||||
userOpts.find(
|
child[2]?.last_modified_uid
|
||||||
(item) =>
|
)}
|
||||||
item.value ===
|
|
||||||
child[2]
|
|
||||||
?.last_modified_uid
|
|
||||||
)?.label
|
|
||||||
}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
{dayjs(
|
{dayjs(
|
||||||
@@ -1027,33 +1067,28 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
</Group>
|
</Group>
|
||||||
{comment ? (
|
{comment ? (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{comment.map((c: any) => {
|
{comment.map((c: Comment) => {
|
||||||
const count =
|
const count = getCommentCount(c)
|
||||||
c.check_box_comments.length +
|
const commentType =
|
||||||
c.text_comments.length
|
getCommentDisplayType(
|
||||||
const { last_modified_timestamp } =
|
c,
|
||||||
child[2]
|
child[2]?.last_modified_timestamp
|
||||||
let flag = false
|
)
|
||||||
if (
|
const badgeColor =
|
||||||
count > 0 &&
|
getCheckBoxColor(commentType)
|
||||||
last_modified_timestamp &&
|
const commentTextColor =
|
||||||
last_modified_timestamp > c.date
|
getCommentTextColor(commentType)
|
||||||
)
|
|
||||||
flag = true
|
|
||||||
|
|
||||||
let badgeColor = "gray"
|
|
||||||
if (c.comment_type === 1)
|
|
||||||
badgeColor = "green"
|
|
||||||
if (c.comment_type === 2)
|
|
||||||
badgeColor = "red"
|
|
||||||
if (flag) badgeColor = "yellow"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<HoverCard
|
||||||
key={`${child[0]}${c.turn}${c.times}`}
|
key={`${child[0]}${c.turn}${c.times}`}
|
||||||
position="left"
|
position="left"
|
||||||
withArrow>
|
withArrow
|
||||||
<Popover.Target>
|
shadow="md"
|
||||||
|
openDelay={100}
|
||||||
|
closeDelay={100}
|
||||||
|
withinPortal>
|
||||||
|
<HoverCard.Target>
|
||||||
<Badge
|
<Badge
|
||||||
size="xs"
|
size="xs"
|
||||||
circle
|
circle
|
||||||
@@ -1063,16 +1098,14 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
}}>
|
}}>
|
||||||
{count}
|
{count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Popover.Target>
|
</HoverCard.Target>
|
||||||
<Popover.Dropdown>
|
<HoverCard.Dropdown>
|
||||||
<Text size="xs" c={badgeColor}>
|
<Text
|
||||||
{`P${child[2].image_id}:${
|
size="xs"
|
||||||
child[2].uid
|
c={commentTextColor}>
|
||||||
}:${c.check_box_comments.join(
|
{`P${child[2].image_id}:${getUserLabel(
|
||||||
","
|
c.uid
|
||||||
)},${c.text_comments.join(
|
)}:${getCommentContent(c)} ${
|
||||||
","
|
|
||||||
)} ${
|
|
||||||
c.date
|
c.date
|
||||||
? dayjs(
|
? dayjs(
|
||||||
c.date * 1000
|
c.date * 1000
|
||||||
@@ -1082,8 +1115,8 @@ const RightObjectTools: React.FC<RightObjectToolsComponentProps> = (props) => {
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Popover.Dropdown>
|
</HoverCard.Dropdown>
|
||||||
</Popover>
|
</HoverCard>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
import { Project } from "../api/project/typing"
|
import { Project } from "../api/project/typing"
|
||||||
|
import { Task } from "../api/task/typing"
|
||||||
import { getTaskList } from "../api/task"
|
import { getTaskList } from "../api/task"
|
||||||
import { useObjectStore } from "../store"
|
import { useObjectStore } from "../store"
|
||||||
import type { Option } from "../store/auth"
|
import type { Option } from "../store/auth"
|
||||||
@@ -38,11 +39,54 @@ interface RightTaskToolsComponentProps {
|
|||||||
task_id: number
|
task_id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FilterOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
interface FilterOptions {
|
interface FilterOptions {
|
||||||
all_user: Option[]
|
all_user: FilterOption[]
|
||||||
label_user: Option[]
|
label_user: FilterOption[]
|
||||||
review_user1: Option[]
|
review_user1: FilterOption[]
|
||||||
review_user2: Option[]
|
review_user2: FilterOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterFormValues {
|
||||||
|
label_status: string[]
|
||||||
|
current_uid: string[]
|
||||||
|
label_user: string[]
|
||||||
|
review_user1: string[]
|
||||||
|
review_user2: string[]
|
||||||
|
rejected: string | null
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskListFilterParams = Partial<
|
||||||
|
Pick<
|
||||||
|
Task.ListRequest,
|
||||||
|
| "label_status"
|
||||||
|
| "current_uid"
|
||||||
|
| "label_user"
|
||||||
|
| "review_user1"
|
||||||
|
| "review_user2"
|
||||||
|
| "rejected"
|
||||||
|
| "id"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
const normalizeFilterOption = (option: Option): FilterOption | null => {
|
||||||
|
if (option.value === null || option.value === undefined) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: option.label,
|
||||||
|
value: String(option.value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushUniqueOption = (options: FilterOption[], option: FilterOption) => {
|
||||||
|
if (!options.some((item) => item.value === option.value)) {
|
||||||
|
options.push(option)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
||||||
@@ -57,11 +101,12 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
|
|
||||||
const { userOpts } = useAllUserStore()
|
const { userOpts } = useAllUserStore()
|
||||||
|
|
||||||
const [taskList, setTaskList] = useState<any[]>([])
|
const [taskList, setTaskList] = useState<Task.DataProps[]>([])
|
||||||
const [activePage, setPage] = useState(1)
|
const [activePage, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [filterOpened, setFilterOpened] = useState(false)
|
||||||
|
|
||||||
const filterForm = useForm({
|
const filterForm = useForm<FilterFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
label_status: [],
|
label_status: [],
|
||||||
current_uid: [],
|
current_uid: [],
|
||||||
@@ -72,32 +117,25 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
id: "",
|
id: "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [filterParams, setFilterParams] = useState<any>({})
|
const [filterParams, setFilterParams] = useState<TaskListFilterParams>({})
|
||||||
const isParamsEmpty: boolean = JSON.stringify(filterParams) === "{}"
|
const isParamsEmpty = Object.keys(filterParams).length === 0
|
||||||
|
|
||||||
const getListData = useCallback(async () => {
|
const getListData = useCallback(async () => {
|
||||||
let pg_num = 0,
|
let pg_num = 0,
|
||||||
pg_size = 1000
|
pg_size = 1000
|
||||||
let total = 0
|
let total = 0
|
||||||
let tasksCount = 1000000
|
let tasksCount = Number.POSITIVE_INFINITY
|
||||||
let finalData: any[] = []
|
let finalData: Task.DataProps[] = []
|
||||||
|
|
||||||
while (total < tasksCount) {
|
while (total < tasksCount) {
|
||||||
pg_num += 1
|
pg_num += 1
|
||||||
const params = filterParams
|
const params: Task.ListRequest = {
|
||||||
? {
|
project_id: [projectId],
|
||||||
project_id: [projectId],
|
get_data: true,
|
||||||
get_data: true,
|
page_number: pg_num,
|
||||||
page_number: pg_num,
|
page_size: pg_size,
|
||||||
page_size: pg_size,
|
...filterParams,
|
||||||
...filterParams,
|
}
|
||||||
}
|
|
||||||
: {
|
|
||||||
project_id: [projectId],
|
|
||||||
get_data: true,
|
|
||||||
page_number: pg_num,
|
|
||||||
page_size: pg_size,
|
|
||||||
}
|
|
||||||
const res = await getTaskList(params)
|
const res = await getTaskList(params)
|
||||||
const { total_items, task_list } = res
|
const { total_items, task_list } = res
|
||||||
finalData = [...finalData, ...task_list]
|
finalData = [...finalData, ...task_list]
|
||||||
@@ -112,6 +150,18 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
queueMicrotask(() => getListData())
|
queueMicrotask(() => getListData())
|
||||||
}, [getListData])
|
}, [getListData])
|
||||||
|
|
||||||
|
const normalizedUserOpts = useMemo(() => {
|
||||||
|
return userOpts.reduce<FilterOption[]>((result, item) => {
|
||||||
|
const normalizedOption = normalizeFilterOption(item)
|
||||||
|
|
||||||
|
if (normalizedOption) {
|
||||||
|
result.push(normalizedOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}, [userOpts])
|
||||||
|
|
||||||
const filterUserOpts = useMemo(() => {
|
const filterUserOpts = useMemo(() => {
|
||||||
const opts: FilterOptions = {
|
const opts: FilterOptions = {
|
||||||
all_user: [],
|
all_user: [],
|
||||||
@@ -121,29 +171,33 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!projectDetail) return opts
|
if (!projectDetail) return opts
|
||||||
const steps = projectDetail.label_process.step_detail
|
const normalizedUserMap = new Map(
|
||||||
|
normalizedUserOpts.map((item) => [item.value, item])
|
||||||
|
)
|
||||||
|
const steps = projectDetail.label_process.step_detail ?? []
|
||||||
steps.forEach(({ action, user_list }) => {
|
steps.forEach(({ action, user_list }) => {
|
||||||
let users: any[] = []
|
const users: FilterOption[] = []
|
||||||
user_list.forEach((v) => {
|
|
||||||
userOpts.forEach((item) => {
|
user_list.forEach((userId) => {
|
||||||
if (item.value === v) {
|
const matchedUser = normalizedUserMap.get(String(userId))
|
||||||
users.push(item)
|
|
||||||
const flag = opts.all_user.find((u) => u.value === v)
|
if (!matchedUser) return
|
||||||
if (!flag) opts.all_user.push(item)
|
|
||||||
}
|
pushUniqueOption(users, matchedUser)
|
||||||
})
|
pushUniqueOption(opts.all_user, matchedUser)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (action === 1) {
|
if (action === 1) {
|
||||||
opts.label_user.push(...users)
|
users.forEach((user) => pushUniqueOption(opts.label_user, user))
|
||||||
} else if (action === 2) {
|
} else if (action === 2) {
|
||||||
opts.review_user1.push(...users)
|
users.forEach((user) => pushUniqueOption(opts.review_user1, user))
|
||||||
} else if (action === 3) {
|
} else if (action === 3) {
|
||||||
opts.review_user2.push(...users)
|
users.forEach((user) => pushUniqueOption(opts.review_user2, user))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}, [projectDetail, userOpts])
|
}, [normalizedUserOpts, projectDetail])
|
||||||
|
|
||||||
const handleTurnPrevious = () => {
|
const handleTurnPrevious = () => {
|
||||||
const index = taskList.findIndex((task) => task.id === taskId)
|
const index = taskList.findIndex((task) => task.id === taskId)
|
||||||
@@ -185,12 +239,14 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
label: value,
|
label: value,
|
||||||
value: key,
|
value: key,
|
||||||
}))}
|
}))}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("label_status")}
|
{...filterForm.getInputProps("label_status")}
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="当前负责人"
|
label="当前负责人"
|
||||||
data={filterUserOpts.all_user}
|
data={filterUserOpts.all_user}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("current_uid")}
|
{...filterForm.getInputProps("current_uid")}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
@@ -198,6 +254,7 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="标注员"
|
label="标注员"
|
||||||
data={filterUserOpts.label_user}
|
data={filterUserOpts.label_user}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("label_user")}
|
{...filterForm.getInputProps("label_user")}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
@@ -205,6 +262,7 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="审核员"
|
label="审核员"
|
||||||
data={filterUserOpts.review_user1}
|
data={filterUserOpts.review_user1}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("review_user1")}
|
{...filterForm.getInputProps("review_user1")}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
@@ -212,6 +270,7 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
label="复审员"
|
label="复审员"
|
||||||
data={filterUserOpts.review_user2}
|
data={filterUserOpts.review_user2}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("review_user2")}
|
{...filterForm.getInputProps("review_user2")}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
@@ -222,6 +281,7 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
{ label: "是", value: "1" },
|
{ label: "是", value: "1" },
|
||||||
{ label: "否", value: "0" },
|
{ label: "否", value: "0" },
|
||||||
]}
|
]}
|
||||||
|
comboboxProps={{ withinPortal: false }}
|
||||||
{...filterForm.getInputProps("rejected")}
|
{...filterForm.getInputProps("rejected")}
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
@@ -237,23 +297,36 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
filterForm.reset()
|
filterForm.reset()
|
||||||
setFilterParams({})
|
setFilterParams({})
|
||||||
setPage(1)
|
setPage(1)
|
||||||
|
setFilterOpened(false)
|
||||||
}}>
|
}}>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const obj = filterForm.values
|
const obj = filterForm.values
|
||||||
const params: any = {}
|
const params: TaskListFilterParams = {}
|
||||||
if (obj.label_status?.length)
|
if (obj.label_status?.length)
|
||||||
params.label_status = obj.label_status.map(Number)
|
params.label_status = obj.label_status.map(Number)
|
||||||
if (obj.current_uid?.length) params.current_uid = obj.current_uid
|
if (obj.current_uid?.length)
|
||||||
if (obj.label_user?.length) params.label_user = obj.label_user
|
params.current_uid = obj.current_uid.map(Number)
|
||||||
if (obj.review_user1?.length) params.review_user1 = obj.review_user1
|
if (obj.label_user?.length)
|
||||||
if (obj.review_user2?.length) params.review_user2 = obj.review_user2
|
params.label_user = obj.label_user.map(Number)
|
||||||
if (obj.rejected) params.rejected = Number(obj.rejected)
|
if (obj.review_user1?.length)
|
||||||
if (obj.id) params.id = obj.id.trim()
|
params.review_user1 = obj.review_user1.map(Number)
|
||||||
|
if (obj.review_user2?.length)
|
||||||
|
params.review_user2 = obj.review_user2.map(Number)
|
||||||
|
if (obj.rejected !== null)
|
||||||
|
params.rejected = Number(obj.rejected) === 1
|
||||||
|
if (obj.id.trim()) {
|
||||||
|
const taskId = Number(obj.id.trim())
|
||||||
|
|
||||||
|
if (Number.isFinite(taskId)) {
|
||||||
|
params.id = [taskId]
|
||||||
|
}
|
||||||
|
}
|
||||||
setFilterParams(params)
|
setFilterParams(params)
|
||||||
setPage(1)
|
setPage(1)
|
||||||
|
setFilterOpened(false)
|
||||||
}}>
|
}}>
|
||||||
确定
|
确定
|
||||||
</Button>
|
</Button>
|
||||||
@@ -276,12 +349,18 @@ const RightTaskTools: React.FC<RightTaskToolsComponentProps> = (props) => {
|
|||||||
<IconChevronRight size={16} />
|
<IconChevronRight size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
<Popover position="bottom-end" withArrow shadow="md">
|
<Popover
|
||||||
|
position="bottom-end"
|
||||||
|
withArrow
|
||||||
|
shadow="md"
|
||||||
|
opened={filterOpened}
|
||||||
|
onChange={setFilterOpened}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
color={!isParamsEmpty ? "blue" : "gray"}>
|
color={!isParamsEmpty ? "blue" : "gray"}
|
||||||
|
onClick={() => setFilterOpened((opened) => !opened)}>
|
||||||
<IconFilter size={16} />
|
<IconFilter size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ interface TopToolsComponentProps {
|
|||||||
updateOldWorkLoad: (data: WorkLoad) => void
|
updateOldWorkLoad: (data: WorkLoad) => void
|
||||||
isLabelTask: boolean
|
isLabelTask: boolean
|
||||||
onOperationChange: (operationId: string) => void
|
onOperationChange: (operationId: string) => void
|
||||||
|
onFilterBySizeThreshold?: () => void
|
||||||
renderPolygons: (
|
renderPolygons: (
|
||||||
operationId: string,
|
operationId: string,
|
||||||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||||||
@@ -164,6 +165,7 @@ const TopTools = (
|
|||||||
updateOldWorkLoad,
|
updateOldWorkLoad,
|
||||||
isLabelTask,
|
isLabelTask,
|
||||||
onOperationChange,
|
onOperationChange,
|
||||||
|
onFilterBySizeThreshold,
|
||||||
renderPolygons,
|
renderPolygons,
|
||||||
} = props
|
} = props
|
||||||
const { backUrl, basePath } = useBackUrlStore()
|
const { backUrl, basePath } = useBackUrlStore()
|
||||||
@@ -182,6 +184,9 @@ const TopTools = (
|
|||||||
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
|
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
|
||||||
const [operationKeyword, setOperationKeyword] = useState("")
|
const [operationKeyword, setOperationKeyword] = useState("")
|
||||||
const labelData = useLabelStore((state) => state.label)
|
const labelData = useLabelStore((state) => state.label)
|
||||||
|
const labelDefaultComments = useLabelStore(
|
||||||
|
(state) => state.labelDefaultComments
|
||||||
|
)
|
||||||
const setLabel = useLabelStore((state) => state.setLabel)
|
const setLabel = useLabelStore((state) => state.setLabel)
|
||||||
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
const pushStateStack = useLabelStore((state) => state.pushStateStack)
|
||||||
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
|
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
|
||||||
@@ -230,6 +235,8 @@ const TopTools = (
|
|||||||
setAssistToolEnabled,
|
setAssistToolEnabled,
|
||||||
assistToolSize,
|
assistToolSize,
|
||||||
setAssistToolSize,
|
setAssistToolSize,
|
||||||
|
auxiliarySizeThreshold,
|
||||||
|
setAuxiliarySizeThreshold,
|
||||||
nodeSize,
|
nodeSize,
|
||||||
setNodeSize,
|
setNodeSize,
|
||||||
needBackup,
|
needBackup,
|
||||||
@@ -238,6 +245,8 @@ const TopTools = (
|
|||||||
setMagnetFlag,
|
setMagnetFlag,
|
||||||
} = useTopToolsStore()
|
} = useTopToolsStore()
|
||||||
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
||||||
|
const [auxiliarySizeThresholdInput, setAuxiliarySizeThresholdInput] =
|
||||||
|
useState<number | string>(auxiliarySizeThreshold)
|
||||||
|
|
||||||
const { pathGroupMap } = useRightToolsStore()
|
const { pathGroupMap } = useRightToolsStore()
|
||||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||||
@@ -261,6 +270,10 @@ const TopTools = (
|
|||||||
usePaperStore.getState().refreshNodeVisualSize()
|
usePaperStore.getState().refreshNodeVisualSize()
|
||||||
}, [nodeSize])
|
}, [nodeSize])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAuxiliarySizeThresholdInput(auxiliarySizeThreshold)
|
||||||
|
}, [auxiliarySizeThreshold])
|
||||||
|
|
||||||
const toggleRightPanel = useCallback(
|
const toggleRightPanel = useCallback(
|
||||||
(visible: boolean, setter: (val: boolean) => void) => {
|
(visible: boolean, setter: (val: boolean) => void) => {
|
||||||
const nextVisible = !visible
|
const nextVisible = !visible
|
||||||
@@ -1025,10 +1038,41 @@ const TopTools = (
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const finalData = adjustAllPoints(
|
const recoveredData = adjustAllPoints(
|
||||||
backupData,
|
backupData,
|
||||||
usePaperStore.getState().rasterScale
|
usePaperStore.getState().rasterScale
|
||||||
)
|
)
|
||||||
|
const finalData = safeClone(recoveredData)
|
||||||
|
for (const [imageId, categoryMap] of finalData.entries()) {
|
||||||
|
const currentDetailById = new Map<number, any>()
|
||||||
|
labelData.get(imageId)?.forEach((objects) => {
|
||||||
|
objects.forEach(([id, _points, detail]) => {
|
||||||
|
currentDetailById.set(id, detail)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
for (const [operationId, objects] of categoryMap.entries()) {
|
||||||
|
categoryMap.set(
|
||||||
|
operationId,
|
||||||
|
objects.map(([id, points, detail, hollowPoints]) => {
|
||||||
|
const currentDetail = currentDetailById.get(id)
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
...detail,
|
||||||
|
comment: currentDetail?.comment
|
||||||
|
? safeClone(currentDetail.comment)
|
||||||
|
: safeClone(labelDefaultComments),
|
||||||
|
last_modified_timestamp:
|
||||||
|
currentDetail?.last_modified_timestamp ??
|
||||||
|
detail.last_modified_timestamp,
|
||||||
|
},
|
||||||
|
hollowPoints,
|
||||||
|
]
|
||||||
|
}) as any
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const entry of finalData.entries()) {
|
for (const entry of finalData.entries()) {
|
||||||
// 赋值用过的id
|
// 赋值用过的id
|
||||||
for (const category of entry[1].values()) {
|
for (const category of entry[1].values()) {
|
||||||
@@ -1089,6 +1133,8 @@ const TopTools = (
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeImage,
|
activeImage,
|
||||||
|
labelData,
|
||||||
|
labelDefaultComments,
|
||||||
projectDetail?.label_type,
|
projectDetail?.label_type,
|
||||||
pushStateStack,
|
pushStateStack,
|
||||||
renderPolygons,
|
renderPolygons,
|
||||||
@@ -2186,39 +2232,84 @@ const TopTools = (
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
||||||
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
<Flex align="center" gap="xs" wrap="nowrap">
|
||||||
<Flex align="center" gap={4}>
|
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
||||||
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
<Flex align="center" gap={4}>
|
||||||
节点
|
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||||
</Text>
|
节点
|
||||||
<NumberInput
|
</Text>
|
||||||
value={nodeSizeInput}
|
<NumberInput
|
||||||
w={50}
|
value={nodeSizeInput}
|
||||||
min={NODE_SIZE_MIN}
|
w={50}
|
||||||
max={NODE_SIZE_MAX}
|
min={NODE_SIZE_MIN}
|
||||||
step={1}
|
max={NODE_SIZE_MAX}
|
||||||
allowDecimal={false}
|
step={1}
|
||||||
size="xs"
|
allowDecimal={false}
|
||||||
radius="sm"
|
size="xs"
|
||||||
hideControls
|
radius="sm"
|
||||||
onChange={(value) => {
|
hideControls
|
||||||
setNodeSizeInput(value)
|
onChange={(value) => {
|
||||||
}}
|
setNodeSizeInput(value)
|
||||||
onFocus={() => {
|
}}
|
||||||
useKeyEventStore.getState().setFocusInput(true)
|
onFocus={() => {
|
||||||
}}
|
useKeyEventStore.getState().setFocusInput(true)
|
||||||
onBlur={(event) => {
|
}}
|
||||||
useKeyEventStore.getState().setFocusInput(false)
|
onBlur={(event) => {
|
||||||
const rawValue = event.currentTarget.value.trim()
|
useKeyEventStore.getState().setFocusInput(false)
|
||||||
const nextValue =
|
const rawValue = event.currentTarget.value.trim()
|
||||||
rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
|
const nextValue =
|
||||||
setNodeSize(
|
rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
|
||||||
Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
|
setNodeSize(
|
||||||
)
|
Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
|
||||||
}}
|
)
|
||||||
/>
|
}}
|
||||||
</Flex>
|
/>
|
||||||
</Tooltip>
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
label={`按当前可见画布尺寸阈值过滤辅助结果或选中 polygon(F2),面积阈值 = size² = ${(Number(auxiliarySizeThresholdInput) || 0) ** 2}`}>
|
||||||
|
<Flex align="center" gap={4} wrap="nowrap">
|
||||||
|
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||||
|
尺寸
|
||||||
|
</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={auxiliarySizeThresholdInput}
|
||||||
|
w={68}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
allowDecimal={false}
|
||||||
|
size="xs"
|
||||||
|
radius="sm"
|
||||||
|
hideControls
|
||||||
|
onChange={(value) => {
|
||||||
|
setAuxiliarySizeThresholdInput(value)
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
useKeyEventStore.getState().setFocusInput(true)
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
useKeyEventStore.getState().setFocusInput(false)
|
||||||
|
const rawValue = event.currentTarget.value.trim()
|
||||||
|
const nextValue = rawValue === "" ? 0 : Number(rawValue)
|
||||||
|
setAuxiliarySizeThreshold(
|
||||||
|
Number.isFinite(nextValue) ? nextValue : 0
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
px={8}
|
||||||
|
onClick={() => {
|
||||||
|
const nextValue = Number(auxiliarySizeThresholdInput) || 0
|
||||||
|
setAuxiliarySizeThreshold(nextValue)
|
||||||
|
onFilterBySizeThreshold?.()
|
||||||
|
}}>
|
||||||
|
过滤
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
{renderEditIcon}
|
{renderEditIcon}
|
||||||
<Button
|
<Button
|
||||||
w="fit-content"
|
w="fit-content"
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ const clearPathParentGroupMetadata = (
|
|||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncDeletedPathGroupState = ({
|
export const syncDeletedPathGroupState = ({
|
||||||
imageId,
|
imageId,
|
||||||
removedPathIds,
|
removedPathIds,
|
||||||
removedParentGroupIds,
|
removedParentGroupIds,
|
||||||
@@ -866,6 +866,17 @@ const cleanupDeletedObjectItems = (
|
|||||||
syncSelectedObjectsVisualState(imageId)
|
syncSelectedObjectsVisualState(imageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removePaperObjectById = (
|
||||||
|
imageId: string,
|
||||||
|
pathId?: number,
|
||||||
|
parentGroupId?: number,
|
||||||
|
options: {
|
||||||
|
pushState?: boolean
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
cleanupDeletedObjectItems(imageId, pathId, parentGroupId, options)
|
||||||
|
}
|
||||||
|
|
||||||
const handleShift = (flag: boolean) => {
|
const handleShift = (flag: boolean) => {
|
||||||
useKeyEventStore.getState().setShift(flag)
|
useKeyEventStore.getState().setShift(flag)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ interface TopToolsState {
|
|||||||
setAssistToolEnabled: (val: boolean) => void
|
setAssistToolEnabled: (val: boolean) => void
|
||||||
assistToolSize: number
|
assistToolSize: number
|
||||||
setAssistToolSize: (val: number) => void
|
setAssistToolSize: (val: number) => void
|
||||||
|
auxiliarySizeThreshold: number
|
||||||
|
setAuxiliarySizeThreshold: (val: number) => void
|
||||||
nodeSize: number
|
nodeSize: number
|
||||||
setNodeSize: (val: number) => void
|
setNodeSize: (val: number) => void
|
||||||
showTags: boolean
|
showTags: boolean
|
||||||
@@ -107,6 +109,7 @@ const initialTopToolsState = {
|
|||||||
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
||||||
assistToolEnabled: false,
|
assistToolEnabled: false,
|
||||||
assistToolSize: 10,
|
assistToolSize: 10,
|
||||||
|
auxiliarySizeThreshold: 0,
|
||||||
nodeSize: DEFAULT_NODE_SIZE,
|
nodeSize: DEFAULT_NODE_SIZE,
|
||||||
drawOption: "default" as "default" | "intersect" | "unite",
|
drawOption: "default" as "default" | "intersect" | "unite",
|
||||||
saveCurrentScale: false,
|
saveCurrentScale: false,
|
||||||
@@ -197,6 +200,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
|||||||
...state,
|
...state,
|
||||||
assistToolSize: Math.max(1, val),
|
assistToolSize: Math.max(1, val),
|
||||||
})),
|
})),
|
||||||
|
auxiliarySizeThreshold: initialTopToolsState.auxiliarySizeThreshold,
|
||||||
|
setAuxiliarySizeThreshold: (val) =>
|
||||||
|
set((state: TopToolsState) => ({
|
||||||
|
...state,
|
||||||
|
auxiliarySizeThreshold: Math.max(0, Number(val) || 0),
|
||||||
|
})),
|
||||||
nodeSize: initialTopToolsState.nodeSize,
|
nodeSize: initialTopToolsState.nodeSize,
|
||||||
setNodeSize: (val) =>
|
setNodeSize: (val) =>
|
||||||
set((state: TopToolsState) => ({
|
set((state: TopToolsState) => ({
|
||||||
|
|||||||
69
components/label/utils/geometry/areaFilter.ts
Normal file
69
components/label/utils/geometry/areaFilter.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
normalizeMultiPolygon,
|
||||||
|
type NormalizedMultiPolygon,
|
||||||
|
type NormalizedRing,
|
||||||
|
} from "./booleanAdapter"
|
||||||
|
|
||||||
|
export interface AreaFilterResult {
|
||||||
|
geometry: NormalizedMultiPolygon
|
||||||
|
removedOuterCount: number
|
||||||
|
removedHoleCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRingArea = (ring: NormalizedRing) => {
|
||||||
|
let area = 0
|
||||||
|
for (let index = 0; index < ring.length - 1; index += 1) {
|
||||||
|
const [x1, y1] = ring[index]
|
||||||
|
const [x2, y2] = ring[index + 1]
|
||||||
|
area += x1 * y2 - x2 * y1
|
||||||
|
}
|
||||||
|
return Math.abs(area / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterMultiPolygonByAreaThreshold = ({
|
||||||
|
geometry,
|
||||||
|
minArea,
|
||||||
|
}: {
|
||||||
|
geometry: NormalizedMultiPolygon | null | undefined
|
||||||
|
minArea: number
|
||||||
|
}): AreaFilterResult => {
|
||||||
|
const normalizedGeometry = normalizeMultiPolygon(geometry)
|
||||||
|
const normalizedMinArea = Math.max(0, Number(minArea) || 0)
|
||||||
|
|
||||||
|
if (!normalizedMinArea) {
|
||||||
|
return {
|
||||||
|
geometry: normalizedGeometry,
|
||||||
|
removedOuterCount: 0,
|
||||||
|
removedHoleCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let removedOuterCount = 0
|
||||||
|
let removedHoleCount = 0
|
||||||
|
|
||||||
|
const nextGeometry = normalizedGeometry.flatMap((polygon) => {
|
||||||
|
const [outerRing, ...holeRings] = polygon
|
||||||
|
if (!outerRing) return []
|
||||||
|
|
||||||
|
if (getRingArea(outerRing) < normalizedMinArea) {
|
||||||
|
removedOuterCount += 1
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextHoleRings = holeRings.filter((ring) => {
|
||||||
|
const keep = getRingArea(ring) >= normalizedMinArea
|
||||||
|
if (!keep) {
|
||||||
|
removedHoleCount += 1
|
||||||
|
}
|
||||||
|
return keep
|
||||||
|
})
|
||||||
|
|
||||||
|
return [[outerRing, ...nextHoleRings]]
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
geometry: nextGeometry,
|
||||||
|
removedOuterCount,
|
||||||
|
removedHoleCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user