feat(tag): perf

This commit is contained in:
zhangheng
2026-04-15 18:24:28 +08:00
parent 5146124722
commit 2ada8f5a90

View File

@@ -5,6 +5,145 @@ import { useBottomToolsStore } from "./useBottomToolsStore"
import { usePaperStore } from "./usePaperStore" import { usePaperStore } from "./usePaperStore"
import { useTopToolsStore } from "./useTopToolsStore" import { useTopToolsStore } from "./useTopToolsStore"
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) => { export const removeTagTexts = (id?: number) => {
const group = usePaperStore.getState().group const group = usePaperStore.getState().group
if (!group) return if (!group) return
@@ -26,7 +165,7 @@ const useRenderTag = () => {
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0 Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
? group.scaling.x ? group.scaling.x
: 1 : 1
const applyTagCompensation = ( const applyPointTagCompensation = (
item: paper.Item, item: paper.Item,
anchor: [number, number] anchor: [number, number]
) => { ) => {
@@ -37,8 +176,9 @@ const useRenderTag = () => {
// 清空之前渲染的标签 // 清空之前渲染的标签
removeTagTexts() removeTagTexts()
for (const [imageId, categoryMap] of dataMap) { const categoryMap = dataMap.get(activeImage)
if (imageId === activeImage) { if (!categoryMap) return
for (const [categoryId, labelList] of categoryMap) { for (const [categoryId, labelList] of categoryMap) {
let category = useTopToolsStore let category = useTopToolsStore
.getState() .getState()
@@ -48,16 +188,24 @@ const useRenderTag = () => {
const { label_class } = category const { label_class } = category
labelList.forEach(([id, _d, detail]) => { labelList.forEach(([id, _d, detail]) => {
const path = usePaperStore.getState().getItemById(id) const path = usePaperStore.getState().getItemById(id)
if (path) { if (!path) return
const bounds = path.bounds const bounds = path.bounds
const tagAnchor: [number, number] = [bounds.left, bounds.top - 6] const pathData = path.data
const pos = [...tagAnchor] const anchorPoint = getTagAnchorPoint(path)
let pathData = path.data const tagAnchor: [number, number] = [anchorPoint.x, anchorPoint.y]
const tagBackgroundColor = getTagBackgroundColor(
pathData.fillColor,
pathData.strokeColor
)
const tagTextColor = getContrastTextColor(tagBackgroundColor)
let content = `` let content = ``
if (configs.includes("ID"))
if (configs.includes("ID")) {
content += `${id}${ content += `${id}${
path.data.parentGroupId ? `@${path.data.parentGroupId}` : "" path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
}` }`
}
if (configs.includes("类别")) content += ` ${label_class}` if (configs.includes("类别")) content += ` ${label_class}`
if ( if (
@@ -70,13 +218,16 @@ const useRenderTag = () => {
content += `${detail.sub_attributes[key]};` content += `${detail.sub_attributes[key]};`
}) })
} }
// 文本
const text = new paper.PointText(pos) const wrappedContent = wrapTagContent(content)
if (wrappedContent) {
const text = new paper.PointText(anchorPoint)
text.set({ text.set({
content, content: wrappedContent,
fontSize: 12, fontSize: TAG_FONT_SIZE,
fontWeight: "bold", fontWeight: "bold",
fillColor: "black", fillColor: tagTextColor,
justification: "center",
data: { data: {
id, id,
type: "tag", type: "tag",
@@ -86,12 +237,18 @@ const useRenderTag = () => {
parent: group, parent: group,
visible, visible,
}) })
applyTagCompensation(text, tagAnchor) 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(
const textMask = new paper.Path.Rectangle(text.bounds) getTextMaskBounds(text.bounds)
)
textMask.set({ textMask.set({
fillColor: pathData.fillColor, fillColor: tagBackgroundColor.clone(),
strokeColor: pathData.strokeColor, strokeColor: pathData.strokeColor,
strokeWidth: 1, strokeWidth: 1,
strokeScaling: false, strokeScaling: false,
@@ -104,7 +261,9 @@ const useRenderTag = () => {
parent: group, parent: group,
visible, visible,
}) })
applyTagCompensation(textMask, tagAnchor) text.bringToFront()
}
// 标注对象外接框 // 标注对象外接框
const mask = new paper.Path.Rectangle(bounds) const mask = new paper.Path.Rectangle(bounds)
mask.set({ mask.set({
@@ -144,7 +303,7 @@ const useRenderTag = () => {
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: visible && configs.includes("点ID"),
}) })
applyTagCompensation(pointText, pointTextPosition) applyPointTagCompensation(pointText, pointTextPosition)
index++ index++
}) })
}) })
@@ -170,14 +329,11 @@ const useRenderTag = () => {
parent: group, parent: group,
visible: visible && configs.includes("点ID"), visible: visible && configs.includes("点ID"),
}) })
applyTagCompensation(pointText, pointTextPosition) applyPointTagCompensation(pointText, pointTextPosition)
}) })
} }
}
}) })
} }
}
}
}, []) }, [])
return { return {