347 lines
10 KiB
TypeScript
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
|