feat(tag): perf
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user