feat(shortcut): area
This commit is contained in:
@@ -2192,6 +2192,12 @@ const LabelPage = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "F2") {
|
||||
e.preventDefault()
|
||||
paperContainerRef.current?.filterBySizeThreshold?.()
|
||||
return
|
||||
}
|
||||
|
||||
// 追踪
|
||||
if (e.key === "i") {
|
||||
e.preventDefault()
|
||||
@@ -2585,6 +2591,9 @@ const LabelPage = ({
|
||||
updateOldWorkLoad={updateOldWorkLoad}
|
||||
isLabelTask={isLabelTask}
|
||||
onOperationChange={handleSelectOperation}
|
||||
onFilterBySizeThreshold={() => {
|
||||
paperContainerRef.current?.filterBySizeThreshold?.()
|
||||
}}
|
||||
renderPolygons={
|
||||
paperContainerRef.current
|
||||
? paperContainerRef.current.renderPolygons
|
||||
|
||||
@@ -46,7 +46,11 @@ import { usePermissionStore } from "../store/auth"
|
||||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||||
import { useKeyboardStore } from "../useKeyBoardStore"
|
||||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||||
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
||||
import {
|
||||
clearSupportAnnotationItem,
|
||||
syncDeletedPathGroupState,
|
||||
usePaperStore,
|
||||
} from "../usePaperStore"
|
||||
import { usePaperSupportStore } from "../usePaperSupportStore"
|
||||
import { renderGroupPath } from "../useRenderGroupPath"
|
||||
import useRenderTag from "../useRenderTag"
|
||||
@@ -55,6 +59,12 @@ import { useTopToolsStore } from "../useTopToolsStore"
|
||||
import { buildSubAttributeFormValues, checkCommentsIsSame } from "../util"
|
||||
import { safeClone } from "../utils/clone"
|
||||
import { labelTypeMap } from "../utils/constants"
|
||||
import {
|
||||
contoursToMultiPolygon,
|
||||
multiPolygonToStoredContours,
|
||||
paperShapeToMultiPolygon,
|
||||
} from "../utils/geometry/booleanAdapter"
|
||||
import { filterMultiPolygonByAreaThreshold } from "../utils/geometry/areaFilter"
|
||||
import { adjustPoints } from "../utils/paperjs"
|
||||
import AssistShapeComponent from "./AssistShapeComponent"
|
||||
import CrosshairComponent from "./CrosshairComponent"
|
||||
@@ -158,6 +168,7 @@ const SHORTCUT_GUIDE_SECTIONS = [
|
||||
title: "专项工具",
|
||||
items: [
|
||||
["F1", "进入辅助标注 / 在辅助模式下确认"],
|
||||
["F2", "按尺寸阈值过滤小区域 / 小镂空"],
|
||||
["I", "执行追踪"],
|
||||
["P", "QA 文本场景检查错词"],
|
||||
],
|
||||
@@ -1042,6 +1053,347 @@ const PaperContainer = (
|
||||
}
|
||||
}, [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
|
||||
)
|
||||
const currentScale = usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||
return (
|
||||
normalizedSizeThreshold *
|
||||
normalizedSizeThreshold *
|
||||
currentScale *
|
||||
currentScale
|
||||
)
|
||||
},
|
||||
[activeImage]
|
||||
)
|
||||
|
||||
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(
|
||||
async (
|
||||
points: Array<[number, number]>,
|
||||
@@ -1057,20 +1409,8 @@ const PaperContainer = (
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
})
|
||||
const operationSchema =
|
||||
projectDetail?.label_schema_list?.find(
|
||||
(item) =>
|
||||
item.category_id === +useTopToolsStore.getState().activeOperation
|
||||
) || null
|
||||
const strokeColor = operationSchema
|
||||
? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||||
: "rgb(0,0,0)"
|
||||
const fillColor = operationSchema
|
||||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||||
: "rgba(0,0,0,0.3)"
|
||||
const blankColor = operationSchema
|
||||
? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||||
: "rgba(0,0,0,0.01)"
|
||||
const { strokeColor, fillColor, blankColor } =
|
||||
getSupportPreviewColors()
|
||||
const currentScale =
|
||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||
|
||||
@@ -1143,13 +1483,11 @@ const PaperContainer = (
|
||||
if (!promptPoints.length && !normalizedBoxes.length) {
|
||||
throw new Error("缺少有效提示,无法调用模型接口")
|
||||
}
|
||||
// const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||||
|
||||
try {
|
||||
const data = await getAuxiliaryAnnotation({
|
||||
project_id: projectId,
|
||||
image_name: activeImage,
|
||||
// project_id: "image_test_bk",
|
||||
prompt: {
|
||||
...(promptPoints.length
|
||||
? { foreground_points: promptPoints }
|
||||
@@ -1162,50 +1500,29 @@ const PaperContainer = (
|
||||
contour_approximation: 0,
|
||||
})
|
||||
const { contours = [], hollows = [] } = data || {}
|
||||
const outerPaths: any[] = []
|
||||
const innerPaths: any[] = []
|
||||
contours.forEach((contour: any, index: number) => {
|
||||
const normalizedContour = normalizeContour(contour)
|
||||
if (normalizedContour.length > 3) {
|
||||
const path = new paper.Path({
|
||||
segments: normalizedContour.map(([x, y]) => [
|
||||
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,
|
||||
const geometry = contoursToMultiPolygon({
|
||||
outerContours: contours
|
||||
.map((contour: any, index: number) => {
|
||||
if (hollows[index]) return null
|
||||
return normalizeContour(contour).map(
|
||||
([x, y]) => [x * currentScale, y * currentScale] as [number, number]
|
||||
)
|
||||
})
|
||||
if (hollows[index]) {
|
||||
path.data.isHollowPolygon = true
|
||||
innerPaths.push(path)
|
||||
} else {
|
||||
path.fillColor = new paper.Color(fillColor)
|
||||
outerPaths.push(path)
|
||||
}
|
||||
}
|
||||
.filter(
|
||||
(contour): contour is Array<[number, number]> => !!contour
|
||||
),
|
||||
holeContours: contours
|
||||
.map((contour: any, index: number) => {
|
||||
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({
|
||||
children: [...outerPaths, ...innerPaths],
|
||||
data: {
|
||||
id: "support",
|
||||
type: "parent",
|
||||
},
|
||||
strokeColor: strokeColor,
|
||||
strokeWidth: 2,
|
||||
fillColor: fillColor,
|
||||
})
|
||||
compoundPath.parent = usePaperStore.getState().group!
|
||||
renderSupportPreviewGeometry(geometry)
|
||||
notifications.show({
|
||||
id: "sam2",
|
||||
message: "模型生成成功",
|
||||
@@ -1230,7 +1547,7 @@ const PaperContainer = (
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeImage, projectDetail]
|
||||
[activeImage, getSupportPreviewColors, projectDetail, renderSupportPreviewGeometry]
|
||||
)
|
||||
|
||||
// const renderTrackingAnnotation = useCallback(async () => {
|
||||
@@ -4155,6 +4472,7 @@ const PaperContainer = (
|
||||
renderTrackingAnnotation,
|
||||
renderSupportAnnotation,
|
||||
saveSupportAnnotationData,
|
||||
filterBySizeThreshold,
|
||||
copyAnnotationDataToMultiFrame,
|
||||
}))
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ interface TopToolsComponentProps {
|
||||
updateOldWorkLoad: (data: WorkLoad) => void
|
||||
isLabelTask: boolean
|
||||
onOperationChange: (operationId: string) => void
|
||||
onFilterBySizeThreshold?: () => void
|
||||
renderPolygons: (
|
||||
operationId: string,
|
||||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||||
@@ -164,6 +165,7 @@ const TopTools = (
|
||||
updateOldWorkLoad,
|
||||
isLabelTask,
|
||||
onOperationChange,
|
||||
onFilterBySizeThreshold,
|
||||
renderPolygons,
|
||||
} = props
|
||||
const { backUrl, basePath } = useBackUrlStore()
|
||||
@@ -230,6 +232,8 @@ const TopTools = (
|
||||
setAssistToolEnabled,
|
||||
assistToolSize,
|
||||
setAssistToolSize,
|
||||
auxiliarySizeThreshold,
|
||||
setAuxiliarySizeThreshold,
|
||||
nodeSize,
|
||||
setNodeSize,
|
||||
needBackup,
|
||||
@@ -238,6 +242,9 @@ const TopTools = (
|
||||
setMagnetFlag,
|
||||
} = useTopToolsStore()
|
||||
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
||||
const [auxiliarySizeThresholdInput, setAuxiliarySizeThresholdInput] = useState<
|
||||
number | string
|
||||
>(auxiliarySizeThreshold)
|
||||
|
||||
const { pathGroupMap } = useRightToolsStore()
|
||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||
@@ -261,6 +268,10 @@ const TopTools = (
|
||||
usePaperStore.getState().refreshNodeVisualSize()
|
||||
}, [nodeSize])
|
||||
|
||||
useEffect(() => {
|
||||
setAuxiliarySizeThresholdInput(auxiliarySizeThreshold)
|
||||
}, [auxiliarySizeThreshold])
|
||||
|
||||
const toggleRightPanel = useCallback(
|
||||
(visible: boolean, setter: (val: boolean) => void) => {
|
||||
const nextVisible = !visible
|
||||
@@ -2186,39 +2197,84 @@ const TopTools = (
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" px="xs" gap="xs">
|
||||
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
||||
<Flex align="center" gap={4}>
|
||||
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||
节点
|
||||
</Text>
|
||||
<NumberInput
|
||||
value={nodeSizeInput}
|
||||
w={50}
|
||||
min={NODE_SIZE_MIN}
|
||||
max={NODE_SIZE_MAX}
|
||||
step={1}
|
||||
allowDecimal={false}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
hideControls
|
||||
onChange={(value) => {
|
||||
setNodeSizeInput(value)
|
||||
}}
|
||||
onFocus={() => {
|
||||
useKeyEventStore.getState().setFocusInput(true)
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
useKeyEventStore.getState().setFocusInput(false)
|
||||
const rawValue = event.currentTarget.value.trim()
|
||||
const nextValue =
|
||||
rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
|
||||
setNodeSize(
|
||||
Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Flex align="center" gap="xs" wrap="nowrap">
|
||||
<Tooltip label={`节点直径范围 ${NODE_SIZE_MIN}-${NODE_SIZE_MAX}`}>
|
||||
<Flex align="center" gap={4}>
|
||||
<Text span size="xs" style={{ minWidth: "fit-content" }}>
|
||||
节点
|
||||
</Text>
|
||||
<NumberInput
|
||||
value={nodeSizeInput}
|
||||
w={50}
|
||||
min={NODE_SIZE_MIN}
|
||||
max={NODE_SIZE_MAX}
|
||||
step={1}
|
||||
allowDecimal={false}
|
||||
size="xs"
|
||||
radius="sm"
|
||||
hideControls
|
||||
onChange={(value) => {
|
||||
setNodeSizeInput(value)
|
||||
}}
|
||||
onFocus={() => {
|
||||
useKeyEventStore.getState().setFocusInput(true)
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
useKeyEventStore.getState().setFocusInput(false)
|
||||
const rawValue = event.currentTarget.value.trim()
|
||||
const nextValue =
|
||||
rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
|
||||
setNodeSize(
|
||||
Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={`按尺寸阈值过滤辅助结果或选中 polygon(F2),实际面积阈值为 ${(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}
|
||||
<Button
|
||||
w="fit-content"
|
||||
|
||||
@@ -688,7 +688,7 @@ const clearPathParentGroupMetadata = (
|
||||
return changed
|
||||
}
|
||||
|
||||
const syncDeletedPathGroupState = ({
|
||||
export const syncDeletedPathGroupState = ({
|
||||
imageId,
|
||||
removedPathIds,
|
||||
removedParentGroupIds,
|
||||
@@ -866,6 +866,17 @@ const cleanupDeletedObjectItems = (
|
||||
syncSelectedObjectsVisualState(imageId)
|
||||
}
|
||||
|
||||
export const removePaperObjectById = (
|
||||
imageId: string,
|
||||
pathId?: number,
|
||||
parentGroupId?: number,
|
||||
options: {
|
||||
pushState?: boolean
|
||||
} = {}
|
||||
) => {
|
||||
cleanupDeletedObjectItems(imageId, pathId, parentGroupId, options)
|
||||
}
|
||||
|
||||
const handleShift = (flag: boolean) => {
|
||||
useKeyEventStore.getState().setShift(flag)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ interface TopToolsState {
|
||||
setAssistToolEnabled: (val: boolean) => void
|
||||
assistToolSize: number
|
||||
setAssistToolSize: (val: number) => void
|
||||
auxiliarySizeThreshold: number
|
||||
setAuxiliarySizeThreshold: (val: number) => void
|
||||
nodeSize: number
|
||||
setNodeSize: (val: number) => void
|
||||
showTags: boolean
|
||||
@@ -107,6 +109,7 @@ const initialTopToolsState = {
|
||||
crosshairStatus: "hidden" as "hidden" | "line" | "carve",
|
||||
assistToolEnabled: false,
|
||||
assistToolSize: 10,
|
||||
auxiliarySizeThreshold: 0,
|
||||
nodeSize: DEFAULT_NODE_SIZE,
|
||||
drawOption: "default" as "default" | "intersect" | "unite",
|
||||
saveCurrentScale: false,
|
||||
@@ -197,6 +200,12 @@ export const useTopToolsStore = create<TopToolsState>()(
|
||||
...state,
|
||||
assistToolSize: Math.max(1, val),
|
||||
})),
|
||||
auxiliarySizeThreshold: initialTopToolsState.auxiliarySizeThreshold,
|
||||
setAuxiliarySizeThreshold: (val) =>
|
||||
set((state: TopToolsState) => ({
|
||||
...state,
|
||||
auxiliarySizeThreshold: Math.max(0, Number(val) || 0),
|
||||
})),
|
||||
nodeSize: initialTopToolsState.nodeSize,
|
||||
setNodeSize: (val) =>
|
||||
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