feat(tag): perf
This commit is contained in:
@@ -5,6 +5,145 @@ import { useBottomToolsStore } from "./useBottomToolsStore"
|
||||
import { usePaperStore } from "./usePaperStore"
|
||||
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) => {
|
||||
const group = usePaperStore.getState().group
|
||||
if (!group) return
|
||||
@@ -26,7 +165,7 @@ const useRenderTag = () => {
|
||||
Number.isFinite(group.scaling?.x) && group.scaling.x !== 0
|
||||
? group.scaling.x
|
||||
: 1
|
||||
const applyTagCompensation = (
|
||||
const applyPointTagCompensation = (
|
||||
item: paper.Item,
|
||||
anchor: [number, number]
|
||||
) => {
|
||||
@@ -37,146 +176,163 @@ const useRenderTag = () => {
|
||||
// 清空之前渲染的标签
|
||||
removeTagTexts()
|
||||
|
||||
for (const [imageId, categoryMap] of dataMap) {
|
||||
if (imageId === activeImage) {
|
||||
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) {
|
||||
const bounds = path.bounds
|
||||
const tagAnchor: [number, number] = [bounds.left, bounds.top - 6]
|
||||
const pos = [...tagAnchor]
|
||||
let pathData = path.data
|
||||
let content = ``
|
||||
if (configs.includes("ID"))
|
||||
content += `${id}${
|
||||
path.data.parentGroupId ? `@${path.data.parentGroupId}` : ""
|
||||
}`
|
||||
if (configs.includes("类别")) content += ` ${label_class}`
|
||||
const categoryMap = dataMap.get(activeImage)
|
||||
if (!categoryMap) return
|
||||
|
||||
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 text = new paper.PointText(pos)
|
||||
text.set({
|
||||
content,
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
fillColor: "black",
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
applyTagCompensation(text, tagAnchor)
|
||||
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 textMask = new paper.Path.Rectangle(text.bounds)
|
||||
textMask.set({
|
||||
fillColor: pathData.fillColor,
|
||||
strokeColor: pathData.strokeColor,
|
||||
strokeWidth: 1,
|
||||
strokeScaling: false,
|
||||
data: {
|
||||
id,
|
||||
type: "tag",
|
||||
tag_category: "text_mask",
|
||||
tag_anchor: tagAnchor,
|
||||
},
|
||||
parent: group,
|
||||
visible,
|
||||
})
|
||||
applyTagCompensation(textMask, tagAnchor)
|
||||
// 标注对象外接框
|
||||
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: visible && configs.includes("外框"),
|
||||
})
|
||||
// 标注对象点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: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyTagCompensation(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: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyTagCompensation(pointText, pointTextPosition)
|
||||
})
|
||||
}
|
||||
}
|
||||
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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
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: visible && configs.includes("外框"),
|
||||
})
|
||||
// 标注对象点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: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyPointTagCompensation(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: visible && configs.includes("点ID"),
|
||||
})
|
||||
applyPointTagCompensation(pointText, pointTextPosition)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user