Files
labelimage/components/label/useRenderTag.ts
2026-04-16 16:36:24 +08:00

347 lines
10 KiB
TypeScript

import { useCallback } from "react"
import paper from "paper"
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
const TAG_TEXT_PADDING_X = 6
const TAG_TEXT_PADDING_Y = 4
const TAG_MAX_LINE_LENGTH = 18
const wrapLongToken = (token: string, maxLineLength: number) => {
if (token.length <= maxLineLength) return [token]
const parts: string[] = []
for (let index = 0; index < token.length; index += maxLineLength) {
parts.push(token.slice(index, index + maxLineLength))
}
return parts
}
const wrapTagContent = (
content: string,
maxLineLength = TAG_MAX_LINE_LENGTH
) => {
const normalized = content.trim().replace(/\s+/g, " ")
if (!normalized) return ""
const lines: string[] = []
let currentLine = ""
normalized.split(" ").forEach((token) => {
if (!token) return
const wrappedTokens = wrapLongToken(token, maxLineLength)
wrappedTokens.forEach((wrappedToken) => {
const nextLine = currentLine
? `${currentLine} ${wrappedToken}`
: wrappedToken
if (nextLine.length <= maxLineLength) {
currentLine = nextLine
return
}
if (currentLine) lines.push(currentLine)
currentLine = wrappedToken
})
})
if (currentLine) lines.push(currentLine)
return lines.join("\n")
}
const getTagAnchorPoint = (item: paper.Item) => {
const segmentPoints: paper.Point[] = []
if (item instanceof paper.CompoundPath) {
item.children.forEach((child) => {
if (!(child instanceof paper.Path)) return
child.segments.forEach((segment) => {
segmentPoints.push(segment.point.clone())
})
})
} else if (item instanceof paper.Path) {
item.segments.forEach((segment) => {
segmentPoints.push(segment.point.clone())
})
}
if (!segmentPoints.length) {
return new paper.Point(item.bounds.left, item.bounds.top)
}
return segmentPoints.reduce((highestPoint, point) =>
point.y < highestPoint.y ? point : highestPoint
)
}
const getTextMaskBounds = (textBounds: paper.Rectangle) =>
new paper.Rectangle(
new paper.Point(
textBounds.left - TAG_TEXT_PADDING_X,
textBounds.top - TAG_TEXT_PADDING_Y
),
new paper.Size(
textBounds.width + TAG_TEXT_PADDING_X * 2,
textBounds.height + TAG_TEXT_PADDING_Y * 2
)
)
const toPaperColor = (color: paper.Color | string | null | undefined) => {
if (color instanceof paper.Color) return color.clone()
if (!color) return null
try {
return new paper.Color(color)
} catch {
return null
}
}
const getTagBackgroundColor = (
fillColor: paper.Color | string | null | undefined,
strokeColor: paper.Color | string | null | undefined
) => {
const resolvedColor = toPaperColor(strokeColor) ?? toPaperColor(fillColor)
if (!resolvedColor) return new paper.Color(1, 1, 1, 0.9)
const tagBackgroundColor = resolvedColor.clone()
tagBackgroundColor.alpha = 0.9
return tagBackgroundColor
}
const getRelativeLuminance = (
color: paper.Color | string | null | undefined
) => {
const resolvedColor = toPaperColor(color)
if (!resolvedColor) return 1
const red = resolvedColor.red
const green = resolvedColor.green
const blue = resolvedColor.blue
const toLinear = (channel: number) =>
channel <= 0.03928
? channel / 12.92
: Math.pow((channel + 0.055) / 1.055, 2.4)
return (
0.2126 * toLinear(red) + 0.7152 * toLinear(green) + 0.0722 * toLinear(blue)
)
}
const getContrastTextColor = (
backgroundColor: paper.Color | string | null | undefined
) => {
const backgroundLuminance = getRelativeLuminance(backgroundColor)
const whiteContrast = 1.05 / (backgroundLuminance + 0.05)
const blackContrast = (backgroundLuminance + 0.05) / 0.05
return whiteContrast > blackContrast ? "#ffffff" : "#000000"
}
export const removeTagTexts = (id?: number) => {
const group = usePaperStore.getState().group
if (!group) return
const items = id
? group.getItems({ data: { id, type: "tag" } })
: group.getItems({ data: { type: "tag" } })
items.forEach((item) => item.remove())
}
const useRenderTag = () => {
const renderTag = useCallback(() => {
const dataMap = useLabelStore.getState().label
const activeImage = useBottomToolsStore.getState().activeImage
const group = usePaperStore.getState().group
if (!group) return
const configs = useTopToolsStore.getState().showTagsConfigs
const groupScale =
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
? group.scaling.x
: 1
const applyScreenSpaceTagCompensation = (
item: paper.Item,
anchor: [number, number]
) => {
if (Math.abs(groupScale - 1) < 0.000001) return
item.scale(1 / groupScale, new paper.Point(anchor[0], anchor[1]))
}
// 清空之前渲染的标签
removeTagTexts()
const categoryMap = dataMap.get(activeImage)
if (!categoryMap) return
for (const [categoryId, labelList] of categoryMap) {
let category = useTopToolsStore
.getState()
.objectOperations.find(
(item) => item.category_id.toString() === categoryId
)!
const { label_class } = category
labelList.forEach(([id, _d, detail]) => {
const path = usePaperStore.getState().getItemById(id)
if (!path) return
const bounds = path.bounds
const pathData = path.data
const anchorPoint = getTagAnchorPoint(path)
const tagAnchor: [number, number] = [anchorPoint.x, anchorPoint.y]
const tagBackgroundColor = getTagBackgroundColor(
pathData.fillColor,
pathData.strokeColor
)
const tagTextColor = getContrastTextColor(tagBackgroundColor)
let content = ``
if (configs.includes("ID")) {
content += `${id}${
path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
}`
}
if (configs.includes("类别")) content += ` ${label_class}`
if (
detail.sub_attributes &&
Object.keys(detail.sub_attributes).length
) {
Object.keys(detail.sub_attributes).forEach((key) => {
if (configs.includes("子属性")) content += ` ${key}:`
if (configs.includes("属性值"))
content += `${detail.sub_attributes[key]};`
})
}
const wrappedContent = wrapTagContent(content)
if (wrappedContent) {
const text = new paper.PointText(anchorPoint)
text.set({
content: wrappedContent,
fontSize: TAG_FONT_SIZE,
fontWeight: "bold",
fillColor: tagTextColor,
justification: "center",
data: {
id,
type: "tag",
tag_category: "text",
tag_anchor: tagAnchor,
},
parent: group,
visible: getObjectTagVisibility(activeImage, id, "text"),
})
text.translate(
new paper.Point(
anchorPoint.x - text.bounds.center.x,
anchorPoint.y - TAG_VERTICAL_GAP - text.bounds.bottom
)
)
const textMask = new paper.Path.Rectangle(
getTextMaskBounds(text.bounds)
)
textMask.set({
fillColor: tagBackgroundColor.clone(),
strokeColor: pathData.strokeColor,
strokeWidth: 1,
strokeScaling: false,
data: {
id,
type: "tag",
tag_category: "text_mask",
tag_anchor: tagAnchor,
},
parent: group,
visible: getObjectTagVisibility(activeImage, id, "text_mask"),
})
applyScreenSpaceTagCompensation(text, tagAnchor)
applyScreenSpaceTagCompensation(textMask, tagAnchor)
text.bringToFront()
}
// 标注对象外接框
const mask = new paper.Path.Rectangle(bounds)
mask.set({
strokeColor: pathData.strokeColor,
strokeWidth: 2,
strokeScaling: false,
data: {
id,
type: "tag",
tag_category: "mask",
},
parent: group,
visible: getObjectTagVisibility(activeImage, id, "mask"),
})
// 标注对象点id
if (path instanceof paper.CompoundPath) {
let index = 1
path.children.forEach((child) => {
const segments = (child as paper.Path).segments
segments.forEach((segment) => {
const pointTextPosition: [number, number] = [
segment.point.x - 10,
segment.point.y + 5,
]
const pointText = new paper.PointText(pointTextPosition)
pointText.set({
content: index,
fontSize: 12,
fontWeight: "bold",
fillColor: pathData.strokeColor,
data: {
id,
type: "tag",
tag_category: "point_text",
tag_anchor: pointTextPosition,
},
parent: group,
visible: getObjectTagVisibility(activeImage, id, "point_text"),
})
applyScreenSpaceTagCompensation(pointText, pointTextPosition)
index++
})
})
} else {
const segments = (path as paper.Path).segments
segments.forEach((segment, index) => {
const pointTextPosition: [number, number] = [
segment.point.x - 10,
segment.point.y + 5,
]
const pointText = new paper.PointText(pointTextPosition)
pointText.set({
content: index + 1,
fontSize: 12,
fontWeight: "bold",
fillColor: pathData.strokeColor,
data: {
id,
type: "tag",
tag_category: "point_text",
tag_anchor: pointTextPosition,
},
parent: group,
visible: getObjectTagVisibility(activeImage, id, "point_text"),
})
applyScreenSpaceTagCompensation(pointText, pointTextPosition)
})
}
})
}
}, [])
return {
renderTag,
}
}
export default useRenderTag