diff --git a/packages/lb-annotation/src/core/pointCloud/index.ts b/packages/lb-annotation/src/core/pointCloud/index.ts index 7254ac0b..8aa69a8c 100644 --- a/packages/lb-annotation/src/core/pointCloud/index.ts +++ b/packages/lb-annotation/src/core/pointCloud/index.ts @@ -19,6 +19,7 @@ import { PointCloudUtils, DEFAULT_SPHERE_PARAMS, ICalib, + IPointCloudBoxList, } from '@labelbee/lb-utils'; import { BufferAttribute, OrthographicCamera, PerspectiveCamera } from 'three'; import HighlightWorker from 'web-worker:./highlightWorker.js'; @@ -57,6 +58,8 @@ interface IProps { isSegment?: boolean; checkMode?: boolean; + + hiddenText?: boolean; } interface IPipeTypes { @@ -154,6 +157,8 @@ export class PointCloud extends EventListener { private pipe?: IPipeTypes; + private hiddenText = false; + constructor({ container, noAppend, @@ -163,6 +168,7 @@ export class PointCloud extends EventListener { config, isSegment, checkMode, + hiddenText = false, }: IProps) { super(); this.container = container; @@ -170,6 +176,7 @@ export class PointCloud extends EventListener { this.backgroundColor = backgroundColor; this.config = config; this.checkMode = checkMode ?? false; + this.hiddenText = hiddenText; // TODO: Need to extracted. if (isOrthographicCamera && orthographicParams) { @@ -1373,52 +1380,149 @@ export class PointCloud extends EventListener { return arrowHelper; }; - public generateBoxTrackID = (boxParams: IPointCloudBox) => { - if (!boxParams.trackID) { - return; - } + /** + * Universal generation of label information + * @param text generation text + * @param scaleFactor scale size + * @returns { sprite, canvasWidth, canvasHeight } + */ + public generateLabel = (text: string, scaleFactor: number) => { + const canvas = this.getTextCanvas(text); + const texture = new THREE.Texture(canvas); - const texture = new THREE.Texture(this.getTextCanvas(boxParams.trackID.toString())); + // Use filters that are more suitable for UI and text + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; texture.needsUpdate = true; - const sprite = new THREE.SpriteMaterial({ map: texture, depthWrite: false }); - const boxID = new THREE.Sprite(sprite); - boxID.scale.set(5, 5, 5); - boxID.position.set(-boxParams.width / 2, 0, boxParams.depth / 2 + 0.5); - return boxID; + + // Calculate canvas width and height to avoid blurring + const canvasWidth = canvas.width / window.devicePixelRatio; + const canvasHeight = canvas.height / window.devicePixelRatio; + + const spriteMaterial = new THREE.SpriteMaterial({ map: texture, depthWrite: false }); + const sprite = new THREE.Sprite(spriteMaterial); + sprite.scale.set(canvasWidth / scaleFactor, canvasHeight / scaleFactor, 1); + + return { sprite, canvasWidth, canvasHeight }; }; + /** + * Generate label information for ID + * @param boxParams + * @returns sprite + */ + public generateBoxTrackID = (boxParams: IPointCloudBox) => { + if (!boxParams.trackID) return; + + const { sprite } = this.generateLabel(boxParams.trackID.toString(), 50); + + sprite.position.set(-boxParams.width / 2, 0, boxParams.depth / 2 + 0.5); + + return sprite; + }; + + /** + * Generate label information for secondary attributes + * @param boxParams + * @returns sprite + */ public generateBoxAttributeLabel = (boxParams: IPointCloudBox) => { - if (!boxParams.attribute) { - return; - } + if (!boxParams.attribute || this.hiddenText) return; - const texture = new THREE.Texture(this.getTextCanvas(boxParams.attribute.toString())); - texture.needsUpdate = true; - const sprite = new THREE.SpriteMaterial({ map: texture, depthWrite: false }); - const attributeLabel = new THREE.Sprite(sprite); - attributeLabel.scale.set(5, 5, 5); + const classLabel = this.findSubAttributeLabel(boxParams, this.config); + const subAttributeLabel = classLabel ? `${boxParams.attribute}\n${classLabel}` : `${boxParams.attribute}`; - // 将label放在box的下方 - attributeLabel.position.set(-boxParams.width / 2, boxParams.height, -boxParams.depth / 2); + const { sprite, canvasWidth, canvasHeight } = this.generateLabel(subAttributeLabel, 100); - return attributeLabel; + sprite.position.set( + -boxParams.width / 2, + boxParams.height / 2 - canvasWidth / 200, + -boxParams.depth / 2 - canvasHeight / 150, + ); + + return sprite; }; + /** + * Splicing sub attribute content + * @param boxParams + * @param config + * @returns + */ + public findSubAttributeLabel(boxParams: IPointCloudBox, config: IPointCloudConfig) { + const { inputList } = config; + let resultStr = ''; + // Return directly without any secondary attributes + if (Object.keys(boxParams.subAttribute).length === 0) return resultStr; + + Object.keys(boxParams.subAttribute).forEach((key) => { + const classInfo = inputList.find((item: { value: string }) => item.value === key); + // If the type of the secondary attribute cannot be found, it will be returned directly + if (!classInfo) return; + resultStr = `${resultStr + classInfo.key}:`; + const { subSelected } = classInfo; + subSelected.forEach((subItem: { value: string; key: string }, index: number) => { + if (subItem.value === boxParams.subAttribute[key]) { + resultStr += subItem.key; + if (index !== subSelected.length - 1) { + resultStr += '、'; + } + } + }); + }); + + return resultStr; + } + public getTextCanvas(text: string) { const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); + // Obtain the pixel ratio of the device + const dpr = window.devicePixelRatio || 1; + const fontSize = 50; + if (ctx) { - ctx.font = `${50}px " bold`; + ctx.font = `${fontSize}px bold`; + // Split text into multiple lines using line breaks + const lines = text.split('\n'); + + // Find the longest row and calculate its width + const maxWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)); + + // Calculate the logical width and height of the canvas + const canvasWidth = Math.ceil(maxWidth); + const lineHeight = fontSize * 1.5; // 每行的高度 + const canvasHeight = lineHeight * lines.length; + + // Modify the logical and physical width and height of the canvas + canvas.width = canvasWidth * dpr; + canvas.height = canvasHeight * dpr; + + canvas.style.width = `${canvasWidth}px`; + canvas.style.height = `${canvasHeight}px`; + + ctx.scale(dpr, dpr); + + // Reset font (font size using dpr scaling) + ctx.font = `${fontSize}px bold`; ctx.fillStyle = 'white'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(text, canvas.width / 2, canvas.height / 2); + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + // Draw text line by line + lines.forEach((line, index) => { + ctx.fillText(line, 0, index * lineHeight); // The Y coordinate of each line of text is index * line height + }); } return canvas; } + public updateHiddenTextAndRender(hiddenText: boolean, pointCloudBoxList: IPointCloudBoxList) { + this.hiddenText = hiddenText; + this.generateBoxes(pointCloudBoxList); + } + /** * Filter road points and noise in all directions * 1. The first 5% of the z-axis is used as the road coordinate diff --git a/packages/lb-annotation/src/core/toolOperation/ViewOperation.ts b/packages/lb-annotation/src/core/toolOperation/ViewOperation.ts index 0d8a849b..f5153909 100644 --- a/packages/lb-annotation/src/core/toolOperation/ViewOperation.ts +++ b/packages/lb-annotation/src/core/toolOperation/ViewOperation.ts @@ -12,6 +12,8 @@ import { IBasicStyle, TAnnotationViewCuboid, ImgPosUtils, + IPointCloudBox, + IPointCloudBoxList, } from '@labelbee/lb-utils'; import _ from 'lodash'; import rgba from 'color-rgba'; @@ -36,6 +38,8 @@ interface IViewOperationProps extends IBasicToolOperationProps { style: IBasicStyle; staticMode?: boolean; annotations: TAnnotationViewData[]; + pointCloudBoxList: IPointCloudBoxList; + hiddenText: boolean; } export interface ISpecificStyle { @@ -76,6 +80,10 @@ export default class ViewOperation extends BasicToolOperation { private convexHullGroup: IConvexHullGroupType = {}; + private pointCloudBoxList: IPointCloudBoxList; + + private hiddenText: boolean = false; + constructor(props: IViewOperationProps) { super({ ...props, showDefaultCursor: true }); this.style = props.style ?? { stroke: DEFAULT_STROKE_COLOR, thickness: 3 }; @@ -290,6 +298,15 @@ export default class ViewOperation extends BasicToolOperation { } } + public setPointCloudBoxList(pointCloudBoxList: IPointCloudBoxList) { + this.pointCloudBoxList = pointCloudBoxList; + } + + public setHiddenText(hiddenText: boolean) { + this.hiddenText = hiddenText; + this.render(); + } + public setConfig(config: { [a: string]: any } | string) { this.config = config; } @@ -432,6 +449,28 @@ export default class ViewOperation extends BasicToolOperation { }); } + /** + * Separate rendering of sub attribute content + * The principle is the same as other tools for rendering sub attribute content + */ + public renderAttribute() { + const annotationChunks = _.chunk(this.annotations, 6); + annotationChunks.forEach((annotationList) => { + const annotation = annotationList.find((item) => item.type === 'polygon'); + if (!annotation) return; + + const { fontStyle } = this.getRenderStyle(annotation); + const polygon = annotation.annotation; + const curPointCloudBox = this.pointCloudBoxList.find((item: IPointCloudBox) => item.id === polygon.id); + const headerText = this.hiddenText ? '' : curPointCloudBox.attribute; + const renderPolygon = AxisUtils.changePointListByZoom(polygon?.pointList ?? [], this.zoom, this.currentPos); + + if (headerText) { + DrawUtils.drawText(this.canvas, this.appendOffset(renderPolygon[0]), headerText, fontStyle); + } + }); + } + public getRenderStyle(annotation: TAnnotationViewData) { const style = this.getSpecificStyle(annotation.annotation); const fontStyle = this.getFontStyle(annotation.annotation, style); @@ -938,6 +977,7 @@ export default class ViewOperation extends BasicToolOperation { }); this.renderConnectionPoints(); + this.renderAttribute(); } catch (e) { console.error('ViewOperation Render Error', e); } diff --git a/packages/lb-components/src/components/AnnotationView/index.tsx b/packages/lb-components/src/components/AnnotationView/index.tsx index 7c152156..9b17380b 100644 --- a/packages/lb-components/src/components/AnnotationView/index.tsx +++ b/packages/lb-components/src/components/AnnotationView/index.tsx @@ -3,11 +3,18 @@ * @author laoluo */ -import React, { useEffect, useCallback, useRef, useImperativeHandle, useState, useContext } from 'react'; +import React, { + useEffect, + useCallback, + useRef, + useImperativeHandle, + useState, + useContext, +} from 'react'; import { ViewOperation, ImgUtils } from '@labelbee/lb-annotation'; import { Spin } from 'antd/es'; import useRefCache from '@/hooks/useRefCache'; -import { TAnnotationViewData } from '@labelbee/lb-utils'; +import { TAnnotationViewData, IPointCloudBoxList } from '@labelbee/lb-utils'; import MeasureCanvas from '../measureCanvas'; import { PointCloudContext } from '@/components/pointCloudView/PointCloudContext'; @@ -41,7 +48,9 @@ interface IProps { }; staticMode?: boolean; measureVisible?: boolean; - onRightClick?: (e: { event: MouseEvent, targetId: string }) => void; + onRightClick?: (e: { event: MouseEvent; targetId: string }) => void; + pointCloudBoxList: IPointCloudBoxList; + hiddenText: boolean; } const DEFAULT_SIZE = { @@ -88,7 +97,9 @@ const AnnotationView = (props: IProps, ref: any) => { globalStyle, afterImgOnLoad, measureVisible, - onRightClick + onRightClick, + pointCloudBoxList, + hiddenText, } = props; const size = sizeInitialized(props.size); const [loading, setLoading] = useState(false); @@ -168,7 +179,7 @@ const AnnotationView = (props: IProps, ref: any) => { viewOperation.current?.setLoading(false); setLoading(false); }, - [loadAndSetImage, fallbackSrc] + [loadAndSetImage, fallbackSrc], ); useEffect(() => { @@ -177,6 +188,14 @@ const AnnotationView = (props: IProps, ref: any) => { } }, [src, measureVisible, fallbackSrc, loadImage]); + useEffect(() => { + viewOperation.current.setPointCloudBoxList(pointCloudBoxList); + }, [pointCloudBoxList]); + + useEffect(() => { + viewOperation.current.setHiddenText(hiddenText); + }, [hiddenText]); + /** * 基础数据绘制监听 * diff --git a/packages/lb-components/src/components/pointCloudView/PointCloud2DSingleView.tsx b/packages/lb-components/src/components/pointCloudView/PointCloud2DSingleView.tsx index 94a6e270..3318081d 100644 --- a/packages/lb-components/src/components/pointCloudView/PointCloud2DSingleView.tsx +++ b/packages/lb-components/src/components/pointCloudView/PointCloud2DSingleView.tsx @@ -12,6 +12,7 @@ import { PointCloudContext } from './PointCloudContext'; import useDataLinkSwitch from './hooks/useDataLinkSwitch'; import PointCloud2DRectOperationView from '@/components/pointCloud2DRectOperationView'; +import { useToolStyleContext } from '@/hooks/useToolStyle'; const PointCloud2DSingleView = ({ view2dData, @@ -35,8 +36,18 @@ const PointCloud2DSingleView = ({ const { url, fallbackUrl, calib, path } = view2dData; const { toggle2dVisible, isHighlightVisible } = useHighlight({ currentData }); const [loading, setLoading] = useState(false); - const { highlight2DLoading, setHighlight2DLoading, cuboidBoxIn2DView, cacheImageNodeSize, setSelectedIDs } = - useContext(PointCloudContext); + const { + highlight2DLoading, + setHighlight2DLoading, + cuboidBoxIn2DView, + cacheImageNodeSize, + setSelectedIDs, + pointCloudBoxList, + } = useContext(PointCloudContext); + + const { value: toolStyle } = useToolStyleContext(); + const { hiddenText } = toolStyle || {}; + const hiddenData = !view2dData; const dataLinkSwitchOpts = useMemo(() => { @@ -119,7 +130,9 @@ const PointCloud2DSingleView = ({ ratio: 0.4, }} measureVisible={measureVisible} - onRightClick={({targetId}) =>setSelectedIDs(targetId)} + onRightClick={({ targetId }) => setSelectedIDs(targetId)} + pointCloudBoxList={pointCloudBoxList} + hiddenText={hiddenText} /> ) : ( <> diff --git a/packages/lb-components/src/components/pointCloudView/PointCloud3DView.tsx b/packages/lb-components/src/components/pointCloudView/PointCloud3DView.tsx index 2ef39a24..33a4975c 100644 --- a/packages/lb-components/src/components/pointCloudView/PointCloud3DView.tsx +++ b/packages/lb-components/src/components/pointCloudView/PointCloud3DView.tsx @@ -30,6 +30,7 @@ import { LabelBeeContext } from '@/store/ctx'; import PointCloudSizeSlider from './components/PointCloudSizeSlider'; import TitleButton from './components/TitleButton'; import { LeftOutlined } from '@ant-design/icons'; +import { useToolStyleContext } from '@/hooks/useToolStyle'; const EKeyCode = cKeyCode.default; const pointCloudID = 'LABELBEE-POINTCLOUD'; @@ -128,6 +129,16 @@ const PointCloud3D: React.FC = ({ currentData, config, highlig const { initPointCloud3d } = usePointCloudViews(); const size = useSize(ref); const { t } = useTranslation(); + const { value: toolStyle } = useToolStyleContext(); + const { hiddenText } = toolStyle || {}; + + useEffect(() => { + let pointCloud = ptCtx.mainViewInstance; + if (pointCloud) { + pointCloud.updateHiddenTextAndRender(hiddenText, ptCtx.pointCloudBoxList); + } + }, [toolStyle]); + useEffect(() => { if (!ptCtx.mainViewInstance) { return; @@ -181,6 +192,7 @@ const PointCloud3D: React.FC = ({ currentData, config, highlig isOrthographicCamera: true, orthographicParams: PointCloudUtils.getDefaultOrthographicParams(size), config, + hiddenText, }); pointCloud.setHandlerPipe({setSelectedIDs: ptCtx.setSelectedIDs, setNeedUpdateCenter}); ptCtx.setMainViewInstance(pointCloud);