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