diff --git a/packages/lb-annotation/src/constant/keyCode.ts b/packages/lb-annotation/src/constant/keyCode.ts index f9fd2873e..46d2f92df 100644 --- a/packages/lb-annotation/src/constant/keyCode.ts +++ b/packages/lb-annotation/src/constant/keyCode.ts @@ -12,6 +12,7 @@ enum EKeyCode { L = 76, R = 82, Z = 90, + V = 86, W = 87, X = 88, Y = 89, diff --git a/packages/lb-annotation/src/core/basicLayer.ts b/packages/lb-annotation/src/core/basicLayer.ts new file mode 100644 index 000000000..adcfc5083 --- /dev/null +++ b/packages/lb-annotation/src/core/basicLayer.ts @@ -0,0 +1,381 @@ +/** + * @file render layer of basic results, should be initialized in annotationEngine + * @Author wanghaiqing wanghaiqing@sensetime.com + * @Date 2024-01-31 + */ + +import { EToolName, THybridToolName } from '@/constant/tool'; +import CanvasUtils from '@/utils/tool/CanvasUtils'; +import DrawUtils from '@/utils/tool/DrawUtils'; +import AxisUtils, { CoordinateUtils } from '@/utils/tool/AxisUtils'; +import { HybridToolUtils } from '@/core/scheduler'; +import EventListener from '@/core/toolOperation/eventListener'; +import { IPolygonConfig } from '@/types/tool/polygon'; +import { IPolygonData, toolStyleConverter } from '@labelbee/lb-utils'; +import CommonToolUtils from '@/utils/tool/CommonToolUtils'; +import { styleString } from '@/constant/style'; +import StyleUtils from '@/utils/tool/StyleUtils'; +import { ICommonProps } from './index'; + +type IReferenceConfig = IRectConfig | IPolygonConfig | ILineConfig | IPointToolConfig; + +export interface IReferenceInfoProps { + referenceConfig: IReferenceConfig; + referenceResult: any; + referenceToolName: EToolName; +} + +interface IBasicLayerProps extends ICommonProps { + container: HTMLElement; + size: ISize; + toolName?: THybridToolName; + imgNode?: HTMLImageElement; // dom node of image + config?: string; // config of annotation task + style?: any; + forbidBasicResultRender?: boolean; +} + +export default class BasicLayer extends EventListener { + public container: HTMLElement; // external dom node + + public config: any; + + public style: any; + + public size: ISize; + + public imgNode?: HTMLImageElement; + + public basicCanvas!: HTMLCanvasElement; // dom for basic layer + + public basicResult?: any; // data of depended tool + + public referenceInfo?: IReferenceInfoProps; + + public dependToolName?: EToolName; + + public forbidBasicResultRender: boolean; // disable rendering of basic result + + public zoom: number; + + public currentPos: ICoordinate; // store real-time offset position + + public coordUtils?: CoordinateUtils; + + public basicImgInfo: any; // store info of current image + + public imgInfo?: ISize; + + private hiddenImg: boolean; + + private _imgAttribute?: any; + + private currentPosStorage?: ICoordinate; // store the current clicked translation position + + constructor(props: IBasicLayerProps) { + super(); + this.container = props.container; + this.config = CommonToolUtils.jsonParser(props.config); + this.style = props.style ?? CommonToolUtils.jsonParser(styleString); + + this.size = props.size; + this.zoom = props.zoom ?? 1; + this.currentPos = props.currentPos ?? { x: 0, y: 0 }; + this.basicImgInfo = props.basicImgInfo; + this.coordUtils = props.coordUtils; + this._imgAttribute = props.imgAttribute ?? {}; + + this.imgNode = props.imgNode; + this.hiddenImg = props.toolName ? !HybridToolUtils.isSingleTool(props.toolName) : false; + this.forbidBasicResultRender = props.forbidBasicResultRender ?? false; + + this.destroyBasicCanvas(); + this.createBasicCanvas(props.size); + } + + // getters + get basicCtx() { + return this.basicCanvas?.getContext('2d'); + } + + public get pixelRatio() { + return CanvasUtils.getPixelRatio(this.basicCanvas?.getContext('2d')); + } + + get rotate() { + return this.basicImgInfo?.rotate ?? 0; + } + + // setters + public setZoom(zoom: number) { + this.zoom = zoom; + } + + public setCurrentPos(currentPos: ICoordinate) { + this.currentPos = currentPos; + this.currentPosStorage = currentPos; + } + + public setBasicImgInfo(basicImgInfo: any) { + this.basicImgInfo = basicImgInfo; + } + + public setImgAttribute(imgAttribute: IImageAttribute) { + this._imgAttribute = imgAttribute; + this.renderBasicCanvas(); + } + + public setImgInfo(size?: ISize) { + this.imgInfo = size; + } + + public setDependName(dependToolName?: EToolName, dependToolConfig?: IRectConfig | IPolygonConfig) { + this.dependToolName = dependToolName; + this.coordUtils?.setDependInfo(dependToolName, dependToolConfig); + } + + public setBasicResult(basicResult: any) { + this.basicResult = basicResult; + this.coordUtils?.setBasicResult(basicResult); + } + + public setReferenceInfo(referenceInfo: IReferenceInfoProps) { + this.referenceInfo = referenceInfo; + } + + /** + * Synchronize common information such as currentPos, zoom, etc + */ + public syncCommonInfo(info: ICommonProps) { + this.setZoom(info?.zoom ?? this.zoom); + this.setCurrentPos(info?.currentPos ?? this.currentPos); + this.setBasicImgInfo(info?.basicImgInfo ?? this.basicImgInfo); + this.setImgInfo(info?.imgInfo ?? this.imgInfo); + } + + /** + * Resize the current canvas and reinitialize + * @param size + */ + public setSize(size: ISize) { + this.size = size; + if (this.container.contains(this.basicCanvas)) { + this.destroyBasicCanvas(); + this.createBasicCanvas(size); + this.renderBasicCanvas(); + } + } + + /** Get the current property color */ + public getColor(attribute = '', config = this.config) { + return toolStyleConverter.getColorByConfig({ attribute, config, style: this.style }); + } + + /** + * Notice. It needs to set the default imgInfo. Because it will needs to create info when it doesn't have + * @param imgNode + * @param basicImgInfo + */ + public setImgNode(imgNode: HTMLImageElement, basicImgInfo: Partial<{ valid: boolean; rotate: number }> = {}) { + this.imgNode = imgNode; + + this.setBasicImgInfo({ + width: imgNode.width, + height: imgNode.height, + valid: true, + rotate: 0, + ...basicImgInfo, + }); + + this.renderBasicCanvas(); + } + + public createBasicCanvas(size: ISize) { + const basicCanvas = document.createElement('canvas'); + basicCanvas.className = 'bee-basic-layer'; + this.updateCanvasBasicStyle(basicCanvas, size, 0); + this.basicCanvas = basicCanvas; + if (this.container.hasChildNodes()) { + this.container.insertBefore(basicCanvas, this.container.childNodes[0]); + } else { + this.container.appendChild(basicCanvas); + } + this.basicCtx?.scale(this.pixelRatio, this.pixelRatio); + } + + public updateCanvasBasicStyle(canvas: HTMLCanvasElement, size: ISize, zIndex: number) { + const pixel = this.pixelRatio; + canvas.style.position = 'absolute'; + canvas.width = size.width * pixel; + canvas.height = size.height * pixel; + canvas.style.width = `${size.width}px`; + canvas.style.height = `${size.height}px`; + canvas.style.left = '0'; + canvas.style.top = '0'; + canvas.style.zIndex = `${zIndex} `; + } + + public drawImg = () => { + if (!this.imgNode || this.hiddenImg === true) return; + + DrawUtils.drawImg(this.basicCanvas, this.imgNode, { + zoom: this.zoom, + currentPos: this.currentPos, + rotate: this.rotate, + imgAttribute: this._imgAttribute, + }); + }; + + public destroyBasicCanvas() { + if (this.basicCanvas && this.container.contains(this.basicCanvas)) { + this.container.removeChild(this.basicCanvas); + } + } + + public clearBasicCanvas() { + this.basicCtx?.clearRect(0, 0, this.size.width, this.size.height); + } + + public renderBasicCanvas() { + if (!this.basicCanvas) { + return; + } + + this.clearBasicCanvas(); + this.drawImg(); + + const thickness = 2; + + if (this.basicResult && this.dependToolName && !this.forbidBasicResultRender) { + switch (this.dependToolName) { + case EToolName.Rect: { + DrawUtils.drawRect( + this.basicCanvas, + AxisUtils.changeRectByZoom(this.basicResult, this.zoom, this.currentPos), + { + color: 'rgba(204,204,204,1.00)', + thickness, + }, + ); + break; + } + + case EToolName.Polygon: { + DrawUtils.drawPolygonWithFillAndLine( + this.basicCanvas, + AxisUtils.changePointListByZoom(this.basicResult.pointList, this.zoom, this.currentPos), + { + fillColor: 'transparent', + strokeColor: 'rgba(204,204,204,1.00)', + isClose: true, + thickness, + }, + ); + + break; + } + + case EToolName.Line: { + DrawUtils.drawLineWithPointList( + this.basicCanvas, + AxisUtils.changePointListByZoom(this.basicResult.pointList, this.zoom, this.currentPos), + { + color: 'rgba(204,204,204,1.00)', + thickness, + }, + ); + + break; + } + + default: { + // + } + } + } + + this.renderReference(); + } + + public renderReference() { + const thickness = 2; + + if (this.referenceInfo) { + const { referenceResult, referenceToolName, referenceConfig } = this.referenceInfo; + switch (referenceToolName) { + case EToolName.RectTrack: + case EToolName.Rect: { + const rectList = referenceResult; + rectList.forEach((rect: IRect) => { + const toolColor = this.getColor(rect.attribute, referenceConfig); + const toolData = StyleUtils.getStrokeAndFill(toolColor, rect.valid); + DrawUtils.drawRect(this.basicCanvas, AxisUtils.changeRectByZoom(rect, this.zoom, this.currentPos), { + color: toolData.stroke, + thickness, + lineDash: [24], + }); + }); + break; + } + + case EToolName.Point: { + const pointList = referenceResult; + pointList.forEach((point: IPointUnit) => { + const toolColor = this.getColor(point.attribute, referenceConfig); + const toolData = StyleUtils.getStrokeAndFill(toolColor, point.valid); + DrawUtils.drawCircle(this.basicCanvas, AxisUtils.changePointByZoom(point, this.zoom, this.currentPos), 5, { + color: toolData.stroke, + fill: toolData.fill, + }); + }); + break; + } + + case EToolName.Polygon: { + const polygonList = referenceResult; + polygonList.forEach((polygon: IPolygonData) => { + const toolColor = this.getColor(polygon.attribute, referenceConfig); + const toolData = StyleUtils.getStrokeAndFill(toolColor, polygon.valid); + + DrawUtils.drawPolygonWithFillAndLine( + this.basicCanvas, + AxisUtils.changePointListByZoom(polygon.pointList, this.zoom, this.currentPos), + { + fillColor: toolData.fill, + strokeColor: toolData.stroke, + isClose: true, + thickness, + }, + ); + }); + + break; + } + + case EToolName.LineMarker: + case EToolName.Line: { + const lineList = referenceResult; + lineList.forEach((line: any) => { + const toolColor = this.getColor(line.attribute, referenceConfig); + const toolData = StyleUtils.getStrokeAndFill(toolColor, line.valid); + + DrawUtils.drawLineWithPointList( + this.basicCanvas, + AxisUtils.changePointListByZoom(line.pointList, this.zoom, this.currentPos), + { + color: toolData.stroke, + thickness, + }, + ); + }); + + break; + } + + default: { + // + } + } + } + } +} diff --git a/packages/lb-annotation/src/core/index.ts b/packages/lb-annotation/src/core/index.ts index 8aebf65f2..c85a0d90d 100644 --- a/packages/lb-annotation/src/core/index.ts +++ b/packages/lb-annotation/src/core/index.ts @@ -6,6 +6,8 @@ import { ELang } from '@/constant/annotation'; import { getConfig, styleDefaultConfig } from '@/constant/defaultConfig'; import { EToolName, THybridToolName } from '@/constant/tool'; import { IPolygonData } from '@/types/tool/polygon'; +import BasicLayer, { IReferenceInfoProps } from '@/core/basicLayer'; +import { CoordinateUtils } from '@/utils/tool/AxisUtils'; import { HybridToolUtils, ToolScheduler } from './scheduler'; interface IProps { @@ -17,6 +19,15 @@ interface IProps { style?: any; } +export interface ICommonProps { + zoom?: number; + currentPos?: ICoordinate; + coordUtils?: CoordinateUtils; + basicImgInfo?: any; + imgAttribute?: IImageAttribute; + imgInfo?: ISize; +} + const loadImage = (imgSrc: string) => { return new Promise((resolve, reject) => { const img = document.createElement('img'); @@ -39,6 +50,16 @@ export default class AnnotationEngine { public i18nLanguage: 'en' | 'cn'; // 存储当前 i18n 初始化数据 + public zoom: number; + + public currentPos: ICoordinate; // 存储实时偏移的位置 + + public coordUtils: CoordinateUtils; + + public basicImgInfo: any; // 用于存储当前图片的信息 + + public imgInfo?: ISize; + private container: HTMLElement; // 当前结构绑定 container private size: ISize; @@ -56,18 +77,42 @@ export default class AnnotationEngine { private toolScheduler: ToolScheduler; // For multi-level management of tools + private basicInstance: BasicLayer; + constructor(props: IProps) { this.container = props.container; this.size = props.size; this.toolName = props.toolName; this.imgNode = props.imgNode; + this.zoom = 1; + this.currentPos = { + x: 0, + y: 0, + }; + this.basicImgInfo = { + width: props.imgNode?.width ?? 0, + height: props.imgNode?.height ?? 0, + valid: true, + rotate: 0, + }; + this.coordUtils = new CoordinateUtils(this); this.config = props.config ?? JSON.stringify(getConfig(HybridToolUtils.getTopToolName(props.toolName))); // 设置默认操作 this.style = props.style ?? styleDefaultConfig; // 设置默认操作 - this.toolScheduler = new ToolScheduler(props); - + this.toolScheduler = new ToolScheduler({ ...props, ...this.commonProps }); + this.basicInstance = new BasicLayer({ ...props, ...this.commonProps }); this.i18nLanguage = 'cn'; // 默认为中文(跟 basicOperation 内同步) this._initToolOperation(); + this._initBasicLayer(); + } + + get commonProps() { + return { + zoom: this.zoom, + currentPos: this.currentPos, + basicImgInfo: this.basicImgInfo, + coordUtils: this.coordUtils, + }; } /** @@ -78,6 +123,28 @@ export default class AnnotationEngine { * 4. style */ + public syncBasicImgInfo(basicImgInfo: any) { + this.basicImgInfo = basicImgInfo; + this.toolScheduler.setBasicImgInfo(basicImgInfo); + this.basicInstance.setBasicImgInfo(basicImgInfo); + this.coordUtils.setBasicImgInfo(basicImgInfo); + } + + public syncImgAttribute(imgAttribute: IImageAttribute) { + this.toolScheduler.setImgAttribute(imgAttribute); + this.basicInstance.setImgAttribute(imgAttribute); + } + + public syncZoomAndCurrentPos(zoom: number, currentPos: ICoordinate) { + this.zoom = zoom; + this.currentPos = currentPos; + this.toolScheduler.setZoom(zoom); + this.basicInstance.setZoom(zoom); + this.toolScheduler.setCurrentPos(currentPos); + this.basicInstance.setCurrentPos(currentPos); + this.coordUtils.setZoomAndCurrentPos(zoom, currentPos); + } + /** * 设置当前工具类型 * @param toolName @@ -107,16 +174,14 @@ export default class AnnotationEngine { }>, ) { this.toolScheduler.setImgNode(imgNode, basicImgInfo); + this.basicInstance.setImgNode(imgNode, basicImgInfo); this.imgNode = imgNode; } - public setImgAttribute(imgAttribute: IImageAttribute) { - this.toolScheduler.setImgAttribute(imgAttribute); - } - public setSize(size: ISize) { this.size = size; this.toolScheduler.setSize(size); + this.basicInstance.setSize(size); } public setStyle(style: any) { @@ -154,10 +219,18 @@ export default class AnnotationEngine { } }); + this.toolScheduler.setBasicInstance(this.basicInstance); // 实时同步语言 this.setLang(this.i18nLanguage); } + /** + * 初始化依赖渲染层 + */ + private _initBasicLayer() { + this.basicInstance.renderBasicCanvas(); + } + /** * 设置当前依赖物体渲染 * @param dependToolName @@ -167,9 +240,21 @@ export default class AnnotationEngine { this.dependToolName = dependToolName; this.basicResult = basicResult; - this.toolInstance.setDependName(dependToolName); - this.toolInstance.setBasicResult(basicResult); - this.toolInstance.renderBasicCanvas(); + this.toolScheduler.setDependName(dependToolName); + this.toolScheduler.setBasicResult(basicResult); + this.basicInstance.setBasicResult(basicResult); + this.basicInstance.setDependName(dependToolName); + this.basicInstance.renderBasicCanvas(); + } + + /** + * set reference info + * @param referenceInfo + */ + + public setReferenceInfo(referenceInfo: IReferenceInfoProps) { + this.basicInstance.setReferenceInfo(referenceInfo); + this.basicInstance.renderBasicCanvas(); } /** diff --git a/packages/lb-annotation/src/core/scheduler.ts b/packages/lb-annotation/src/core/scheduler.ts index c5f43ae09..ebb7882fa 100644 --- a/packages/lb-annotation/src/core/scheduler.ts +++ b/packages/lb-annotation/src/core/scheduler.ts @@ -7,14 +7,18 @@ import { getConfig, styleDefaultConfig } from '@/constant/defaultConfig'; import { EToolName, THybridToolName } from '@/constant/tool'; import { getCurrentOperation } from '@/utils/tool/EnhanceCommonToolUtils'; +import { CoordinateUtils } from '@/utils/tool/AxisUtils'; +import BasicLayer from '@/core/basicLayer'; +import { IPolygonData } from '@/types/tool/polygon'; import { RectOperation } from './toolOperation/rectOperation'; import PolygonOperation from './toolOperation/polygonOperation'; import { BasicToolOperation } from './toolOperation/basicToolOperation'; import SegmentByRect from './toolOperation/segmentByRect'; +import { ICommonProps } from './index'; interface IToolSchedulerOperation {} -interface IToolSchedulerProps { +interface IToolSchedulerProps extends ICommonProps { container: HTMLElement; size: ISize; toolName: THybridToolName; @@ -82,6 +86,8 @@ export class ToolScheduler implements IToolSchedulerOperation { private proxyMode?: boolean; + private coordUtils?: CoordinateUtils; + constructor(props: IToolSchedulerProps) { this.container = props.container; this.size = props.size; @@ -89,6 +95,7 @@ export class ToolScheduler implements IToolSchedulerOperation { this.config = props.config ?? JSON.stringify(getConfig(HybridToolUtils.getTopToolName(props.toolName))); // 设置默认操作 this.style = props.style ?? styleDefaultConfig; // 设置默认操作 this.proxyMode = props.proxyMode; + this.coordUtils = props.coordUtils; this.onWheel = this.onWheel.bind(this); this.onMouseDown = this.onMouseDown.bind(this); this.onMouseMove = this.onMouseMove.bind(this); @@ -108,12 +115,48 @@ export class ToolScheduler implements IToolSchedulerOperation { }); } + public setZoom(zoom: number) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setZoom(zoom); + }); + } + + public setCurrentPos(currentPos: ICoordinate) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setCurrentPos(currentPos); + }); + } + + public setBasicImgInfo(basicImgInfo: any) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setBasicImgInfo(basicImgInfo); + }); + } + public setImgAttribute(imgAttribute: IImageAttribute) { this.toolOperationList.forEach((toolInstance) => { toolInstance.setImgAttribute(imgAttribute); }); } + public setBasicInstance(basicInstance: BasicLayer) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setBasicInstance(basicInstance); + }); + } + + public setDependName(dependToolName?: EToolName) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setDependName(dependToolName); + }); + } + + public setBasicResult(basicResult?: IRect | IPolygonData) { + this.toolOperationList.forEach((toolInstance) => { + toolInstance.setBasicResult(basicResult); + }); + } + public syncAllAttributeListInConfig(attributeList: any[]) { this.toolOperationList.forEach((toolInstance) => { const newConfig = { @@ -198,6 +241,18 @@ export class ToolScheduler implements IToolSchedulerOperation { style: this.style, imgNode: imgNode || emptyImgNode, hiddenImg: !!tool, + zoom: 1, + currentPos: { + x: 0, + y: 0, + }, + basicImgInfo: { + width: imgNode?.width ?? 0, + height: imgNode?.height ?? 0, + valid: true, + rotate: 0, + }, + coordUtils: this.coordUtils, }; if (config) { Object.assign(defaultData, config); @@ -367,7 +422,8 @@ export class ToolScheduler implements IToolSchedulerOperation { public destroyAllLayer() { this.toolOperationList.forEach((toolInstance) => { - toolInstance.destroyCanvas(); + // use destroy instead of destroyCanvas to delete textAttribute layer + toolInstance.destroy(); this.init(); }); } diff --git a/packages/lb-annotation/src/core/toolOperation/basicToolOperation.ts b/packages/lb-annotation/src/core/toolOperation/basicToolOperation.ts index 0672f5735..a37c1d688 100644 --- a/packages/lb-annotation/src/core/toolOperation/basicToolOperation.ts +++ b/packages/lb-annotation/src/core/toolOperation/basicToolOperation.ts @@ -3,7 +3,7 @@ import { isNumber } from 'lodash'; import { EOperationMode, EToolName } from '@/constant/tool'; import { IPolygonConfig, IPolygonData } from '@/types/tool/polygon'; import MathUtils from '@/utils/MathUtils'; -import AxisUtils, { CoordinateUtils } from '@/utils/tool/AxisUtils'; +import { CoordinateUtils } from '@/utils/tool/AxisUtils'; import CanvasUtils from '@/utils/tool/CanvasUtils'; import CommonToolUtils from '@/utils/tool/CommonToolUtils'; import LineToolUtils from '@/utils/tool/LineToolUtils'; @@ -19,33 +19,35 @@ import DrawUtils from '../../utils/tool/DrawUtils'; import RenderDomUtils from '../../utils/tool/RenderDomUtils'; import ZoomUtils from '../../utils/tool/ZoomUtils'; import EventListener from './eventListener'; +import { ICommonProps } from '../index'; +import BasicLayer from '../basicLayer'; const LANGUAGE_MAP = { [ELang.Zh]: 'cn', [ELang.US]: 'en', }; -interface IBasicToolOperationProps { +interface IBasicToolOperationProps extends ICommonProps { container: HTMLElement; size: ISize; - imgNode?: HTMLImageElement; // 展示图片的内容 - staticMode?: boolean; // 是否为静态模式 - style?: any; // 后期一定要补上!! + imgNode?: HTMLImageElement; // dom for image + staticMode?: boolean; // whether it is static mode + style?: any; // need to be done!! rotate?: number; - imgAttribute?: any; // 占个坑,用于全局的一些配置,是否展示原图比例 + imgAttribute?: any; // Used for some global configurations, such as whether to display the original image ratio forbidOperation?: boolean; - config?: string; // 任务配置 + config?: string; // config of annotation task defaultAttribute?: string; forbidCursorLine?: boolean; - showDefaultCursor?: boolean; // 默认会展示为 none + showDefaultCursor?: boolean; // default value: none forbidBasicResultRender?: boolean; - isAppend?: boolean; // 用于 canvas 层次的关闭 - hiddenImg?: boolean; // 隐藏图片渲染 + isAppend?: boolean; // Used for closing the canvas hierarchy + hiddenImg?: boolean; // Whether to hide image zoomInfo?: { min: number; @@ -53,10 +55,11 @@ interface IBasicToolOperationProps { ratio: number; }; language?: ELang; + basicInstance?: BasicLayer; } /** - * 参考显示数据 + * Reference display data */ interface IReferenceData { toolName: EToolName.Polygon | EToolName.Line | EToolName.LineMarker; @@ -64,7 +67,7 @@ interface IReferenceData { config: any; } -// zoom 的限制 +// limit of zoom const DEFAULT_ZOOM_INFO = { min: 0.2, max: 1000, @@ -76,8 +79,6 @@ class BasicToolOperation extends EventListener { public canvas!: HTMLCanvasElement; - public basicCanvas!: HTMLCanvasElement; - public imgNode?: HTMLImageElement; public staticImgNode?: HTMLImageElement; @@ -178,6 +179,8 @@ class BasicToolOperation extends EventListener { private hiddenImg: boolean; + public basicInstance?: BasicLayer; + public coordUtils: CoordinateUtils; public zoomInfo = DEFAULT_ZOOM_INFO; @@ -193,7 +196,7 @@ class BasicToolOperation extends EventListener { this.imgNode = props.imgNode; this.staticMode = props.staticMode ?? false; this.isImgError = !props.imgNode; - this.basicImgInfo = { + this.basicImgInfo = props.basicImgInfo ?? { width: props.imgNode?.width ?? 0, height: props.imgNode?.height ?? 0, valid: true, @@ -203,11 +206,8 @@ class BasicToolOperation extends EventListener { this.forbidBasicResultRender = props.forbidBasicResultRender ?? false; this.size = props.size; - this.currentPos = { - x: 0, - y: 0, - }; - this.zoom = 1; + this.zoom = props.zoom ?? 1; + this.currentPos = props.currentPos ?? { x: 0, y: 0 }; this.coord = { x: -1, y: -1, @@ -242,7 +242,7 @@ class BasicToolOperation extends EventListener { // 初始化监听事件 this.dblClickListener = new DblClickEventListener(this.container, 200); - this.coordUtils = new CoordinateUtils(this); + this.coordUtils = props.coordUtils ?? new CoordinateUtils(this); this.coordUtils.setBasicImgInfo(this.basicImgInfo); this.hiddenImg = props.hiddenImg || false; @@ -260,6 +260,10 @@ class BasicToolOperation extends EventListener { return this._ctx || this.canvas?.getContext('2d'); } + get basicCanvas() { + return this.basicInstance?.basicCanvas; + } + get basicCtx() { return this.basicCanvas?.getContext('2d'); } @@ -312,11 +316,17 @@ class BasicToolOperation extends EventListener { this.zoom = zoom; this.innerZoom = zoom; this.coordUtils.setZoomAndCurrentPos(this.zoom, this.currentPos); + this.basicInstance?.syncCommonInfo({ + zoom, + }); } public setCurrentPos(currentPos: ICoordinate) { this.currentPos = currentPos; this.coordUtils.setZoomAndCurrentPos(this.zoom, this.currentPos); + this.basicInstance?.syncCommonInfo({ + currentPos, + }); } public setReferenceData(referenceData: IReferenceData) { @@ -325,6 +335,9 @@ class BasicToolOperation extends EventListener { public setImgInfo(size: ISize) { this.imgInfo = size; + this.basicInstance?.syncCommonInfo({ + imgInfo: size, + }); } public setCurrentPosStorage(currentPosStorage: ICoordinate) { @@ -335,6 +348,10 @@ class BasicToolOperation extends EventListener { this.operationMode = operationMode; } + public setBasicInstance(basicInstance: BasicLayer) { + this.basicInstance = basicInstance; + } + public recoverOperationMode() { if (this.operationMode === EOperationMode.MultiMove) { this.setOperationMode(EOperationMode.General); @@ -411,11 +428,6 @@ class BasicToolOperation extends EventListener { // TODO 后续需要将 canvas 抽离出来,迭代器叠加 const pixel = this.pixelRatio; - const basicCanvas = document.createElement('canvas'); - this.updateCanvasBasicStyle(basicCanvas, size, 0); - - this.basicCanvas = basicCanvas; - const canvas = document.createElement('canvas'); this.updateCanvasBasicStyle(canvas, size, 10); @@ -425,9 +437,7 @@ class BasicToolOperation extends EventListener { if (isAppend) { if (this.container.hasChildNodes()) { this.container.insertBefore(canvas, this.container.childNodes[0]); - this.container.insertBefore(basicCanvas, this.container.childNodes[0]); } else { - this.container.appendChild(basicCanvas); this.container.appendChild(canvas); } } @@ -446,11 +456,6 @@ class BasicToolOperation extends EventListener { // container 内可能包含其他元素,故需单独清楚 this.container.removeChild(this.canvas); } - - if (this.basicCanvas && this.container.contains(this.basicCanvas)) { - this.container.removeChild(this.basicCanvas); - } - // 恢复初始状态 this.clearInvalidPage(); this.clearImgDrag(); @@ -495,6 +500,7 @@ class BasicToolOperation extends EventListener { this.initImgPos(); this.render(); + this.renderBasicCanvas(); } @@ -539,6 +545,9 @@ class BasicToolOperation extends EventListener { public setBasicImgInfo(basicImgInfo: any) { this.basicImgInfo = basicImgInfo; + this.basicInstance?.syncCommonInfo({ + basicImgInfo, + }); this.coordUtils.setBasicImgInfo(basicImgInfo); } @@ -709,6 +718,7 @@ class BasicToolOperation extends EventListener { this.setImgInfo(imgInfo); this.setZoom(zoom); this.render(); + this.renderBasicCanvas(); this.emit('dependRender'); @@ -757,13 +767,14 @@ class BasicToolOperation extends EventListener { _imgAttribute?.isOriginalSize, ); if (pos) { - this.setCurrentPos(pos.currentPos); - this.currentPosStorage = this.currentPos; - this.setImgInfo({ + const newImgInfo = { ...imgInfo, width: (imgInfo.width / this.innerZoom) * pos.innerZoom, height: (imgInfo.height / this.innerZoom) * pos.innerZoom, - }); + }; + this.setCurrentPos(pos.currentPos); + this.currentPosStorage = this.currentPos; + this.setImgInfo(newImgInfo); // 需要加载下更改当前的 imgInfo this.setZoom(pos.innerZoom); @@ -816,10 +827,6 @@ class BasicToolOperation extends EventListener { this.ctx?.clearRect(0, 0, this.size.width, this.size.height); } - public clearBasicCanvas() { - this.basicCtx?.clearRect(0, 0, this.size.width, this.size.height); - } - /** 事件绑定 */ public eventBinding() { this.dblClickListener.addEvent(() => {}, this.onLeftDblClick, this.onRightDblClick); @@ -906,6 +913,7 @@ class BasicToolOperation extends EventListener { this.isDrag = true; this.container.style.cursor = 'grabbing'; this.forbidCursorLine = true; + this.renderBasicCanvas(); // 依赖渲染触发 @@ -1071,6 +1079,7 @@ class BasicToolOperation extends EventListener { if (isRender) { this.render(); } + this.renderBasicCanvas(); } @@ -1172,18 +1181,6 @@ class BasicToolOperation extends EventListener { } }; - public drawImg = () => { - if (!this.imgNode || this.hiddenImg === true) return; - - DrawUtils.drawImg(this.basicCanvas, this.imgNode, { - zoom: this.zoom, - currentPos: this.currentPos, - rotate: this.rotate, - imgAttribute: this._imgAttribute, - }); - this.drawStaticImg(); - }; - public drawStaticImg = () => { if (!this.staticImgNode || !this.staticMode) return; @@ -1238,6 +1235,9 @@ class BasicToolOperation extends EventListener { public setValid(valid: boolean) { this.basicImgInfo.valid = valid; + this.basicInstance?.syncCommonInfo({ + basicImgInfo: this.basicImgInfo, + }); if (valid === false) { this.renderInvalidPage(); this.clearResult(false); @@ -1248,6 +1248,9 @@ class BasicToolOperation extends EventListener { public setRotate(rotate: number) { this.basicImgInfo.rotate = rotate; + this.basicInstance?.syncCommonInfo({ + basicImgInfo: this.basicImgInfo, + }); } public setBasicResult(basicResult: any) { @@ -1257,7 +1260,7 @@ class BasicToolOperation extends EventListener { this.emit('dependRender'); } - public setDependName(dependToolName: EToolName, dependToolConfig?: IRectConfig | IPolygonConfig) { + public setDependName(dependToolName?: EToolName, dependToolConfig?: IRectConfig | IPolygonConfig) { this.dependToolName = dependToolName; this.coordUtils.setDependInfo(dependToolName, dependToolConfig); } @@ -1265,6 +1268,7 @@ class BasicToolOperation extends EventListener { public setAttributeLockList(attributeLockList: string[]) { this.attributeLockList = attributeLockList; this.render(); + this.emit('attributeLockChanged', attributeLockList); } public setConfig(config: string) { @@ -1305,6 +1309,9 @@ class BasicToolOperation extends EventListener { // 更改当前图片的旋转方式 const rotate = MathUtils.getRotate(this.basicImgInfo.rotate); this.basicImgInfo.rotate = rotate; + this.basicInstance?.syncCommonInfo({ + basicImgInfo: this.basicImgInfo, + }); this.initImgPos(); // 触发外层 result 的更改 @@ -1348,66 +1355,7 @@ class BasicToolOperation extends EventListener { } public renderBasicCanvas() { - if (!this.basicCanvas) { - return; - } - - this.clearBasicCanvas(); - this.drawImg(); - - const thickness = 3; - - if (this.forbidBasicResultRender) { - return; - } - - if (this.basicResult && this.dependToolName) { - switch (this.dependToolName) { - case EToolName.Rect: { - DrawUtils.drawRect( - this.basicCanvas, - AxisUtils.changeRectByZoom(this.basicResult, this.zoom, this.currentPos), - { - color: 'rgba(204,204,204,1.00)', - thickness, - }, - ); - break; - } - - case EToolName.Polygon: { - DrawUtils.drawPolygonWithFillAndLine( - this.basicCanvas, - AxisUtils.changePointListByZoom(this.basicResult.pointList, this.zoom, this.currentPos), - { - fillColor: 'transparent', - strokeColor: 'rgba(204,204,204,1.00)', - isClose: true, - thickness, - }, - ); - - break; - } - - case EToolName.Line: { - DrawUtils.drawLineWithPointList( - this.basicCanvas, - AxisUtils.changePointListByZoom(this.basicResult.pointList, this.zoom, this.currentPos), - { - color: 'rgba(204,204,204,1.00)', - thickness, - }, - ); - - break; - } - - default: { - // - } - } - } + this.basicInstance?.renderBasicCanvas(); } public render() { diff --git a/packages/lb-annotation/src/core/toolOperation/pointCloud2dOperation.ts b/packages/lb-annotation/src/core/toolOperation/pointCloud2dOperation.ts index a9718d932..dd04dd5be 100644 --- a/packages/lb-annotation/src/core/toolOperation/pointCloud2dOperation.ts +++ b/packages/lb-annotation/src/core/toolOperation/pointCloud2dOperation.ts @@ -409,7 +409,9 @@ class PointCloud2dOperation extends PolygonOperation { this.setImgInfo(size); // Update canvas size. - this.updateCanvasBasicStyle(this.basicCanvas, size, 0); + if (this.basicCanvas) { + this.updateCanvasBasicStyle(this.basicCanvas, size, 0); + } this.updateCanvasBasicStyle(this.canvas, size, 10); this.ctx?.scale(pixel, pixel); this.basicCtx?.scale(pixel, pixel); diff --git a/packages/lb-annotation/src/index.ts b/packages/lb-annotation/src/index.ts index ee230bcb6..08518bb92 100644 --- a/packages/lb-annotation/src/index.ts +++ b/packages/lb-annotation/src/index.ts @@ -18,6 +18,7 @@ import ScribbleTool from './core/toolOperation/ScribbleTool'; import PointCloud2dOperation from './core/toolOperation/pointCloud2dOperation'; import SegmentByRect from './core/toolOperation/segmentByRect'; import SegmentBySAM from './core/toolOperation/segmentBySAM'; +import BasicLayer from './core/basicLayer'; // Constant import * as cAnnotation from './constant/annotation'; @@ -69,6 +70,7 @@ export { SegmentByRect, SegmentBySAM, CursorTextClass, + BasicLayer, // 固定操作 cAnnotation, cAnnotationTask, diff --git a/packages/lb-annotation/src/utils/tool/AxisUtils.ts b/packages/lb-annotation/src/utils/tool/AxisUtils.ts index f66de3d75..057b33ef0 100644 --- a/packages/lb-annotation/src/utils/tool/AxisUtils.ts +++ b/packages/lb-annotation/src/utils/tool/AxisUtils.ts @@ -530,7 +530,7 @@ export class CoordinateUtils { } } - public setDependInfo(dependToolName: EToolName | '', dependToolConfig?: IRectConfig | IPolygonConfig) { + public setDependInfo(dependToolName?: EToolName | '', dependToolConfig?: IRectConfig | IPolygonConfig) { this.dependToolName = dependToolName ?? ''; this.dependToolConfig = dependToolName ? dependToolConfig : undefined; } diff --git a/packages/lb-annotation/src/utils/tool/CanvasUtils.ts b/packages/lb-annotation/src/utils/tool/CanvasUtils.ts index c94c420fe..35dd6eeb0 100644 --- a/packages/lb-annotation/src/utils/tool/CanvasUtils.ts +++ b/packages/lb-annotation/src/utils/tool/CanvasUtils.ts @@ -91,7 +91,6 @@ export default class CanvasUtils { context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; return (window.devicePixelRatio || 1) / backingStore; diff --git a/packages/lb-components/src/App.tsx b/packages/lb-components/src/App.tsx index dc7054dc9..ce3e55c7e 100644 --- a/packages/lb-components/src/App.tsx +++ b/packages/lb-components/src/App.tsx @@ -29,6 +29,7 @@ import { ConfigProvider } from 'antd/es'; import zhCN from 'antd/es/locale/zh_CN'; import enUS from 'antd/es/locale/en_US'; import { EPointCloudName } from '@labelbee/lb-annotation'; +import { useError } from '@/components/errorBoundary'; interface IAnnotationStyle { strokeColor: string; @@ -132,6 +133,8 @@ const App: React.FC = (props) => { preDataProcess, } = props; + const { onError } = useError() + useEffect(() => { store.dispatch( InitTaskData({ @@ -208,6 +211,7 @@ const App: React.FC = (props) => { // 初始化imgList 优先以loadFileList方式加载数据 const initImgList = () => { + onError?.('test error provider') if (loadFileList) { loadImgList(store.dispatch, store.getState, initialIndex, true).then((isSuccess) => { if (isSuccess) { diff --git a/packages/lb-components/src/assets/annotation/rectTool/flag_active.svg b/packages/lb-components/src/assets/annotation/rectTool/flag_active.svg new file mode 100644 index 000000000..af2471a54 --- /dev/null +++ b/packages/lb-components/src/assets/annotation/rectTool/flag_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/lb-components/src/assets/annotation/rectTool/flag_default.svg b/packages/lb-components/src/assets/annotation/rectTool/flag_default.svg new file mode 100644 index 000000000..d8c89dc4c --- /dev/null +++ b/packages/lb-components/src/assets/annotation/rectTool/flag_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/lb-components/src/components/AnnotationView/index.tsx b/packages/lb-components/src/components/AnnotationView/index.tsx index 3ac8ed594..1948c2ad8 100644 --- a/packages/lb-components/src/components/AnnotationView/index.tsx +++ b/packages/lb-components/src/components/AnnotationView/index.tsx @@ -4,7 +4,7 @@ */ import React, { useEffect, useRef, useImperativeHandle, useState } from 'react'; -import { ViewOperation, ImgUtils } from '@labelbee/lb-annotation'; +import { ViewOperation, ImgUtils, BasicLayer } from '@labelbee/lb-annotation'; import { Spin } from 'antd/es'; import useRefCache from '@/hooks/useRefCache'; import { TAnnotationViewData } from '@labelbee/lb-utils'; @@ -89,6 +89,7 @@ const AnnotationView = (props: IProps, ref: any) => { const [loading, setLoading] = useState(false); const annotationRef = useRef(null); const viewOperation = useRef(); + const basicInstance = useRef(); const afterImgOnLoadRef = useRefCache(afterImgOnLoad); const annotationListCacheRef = useRef([]); const canUpdateRef = useRef(true); // Judge if rending is Possible. @@ -124,6 +125,13 @@ const AnnotationView = (props: IProps, ref: any) => { }); viewOperation.current.init(); + basicInstance.current = new BasicLayer({ + container: annotationRef.current, + size, + style, + config: '{}', + }) + viewOperation.current?.setBasicInstance(basicInstance.current) } return () => { @@ -141,6 +149,7 @@ const AnnotationView = (props: IProps, ref: any) => { setLoading(false); viewOperation.current?.setImgNode(imgNode); + basicInstance.current?.setImgNode(imgNode); if (afterImgOnLoadRef.current) { afterImgOnLoadRef.current(imgNode); diff --git a/packages/lb-components/src/components/NLPToolView/textContent/remarkMask.tsx b/packages/lb-components/src/components/NLPToolView/textContent/remarkMask.tsx new file mode 100644 index 000000000..993754e26 --- /dev/null +++ b/packages/lb-components/src/components/NLPToolView/textContent/remarkMask.tsx @@ -0,0 +1,56 @@ +/** + * @file remark mask + * @author lixinghua + * @date 2024.01.30 + */ + +import _ from 'lodash'; +import React from 'react'; +import { IRemarkInterval } from '../types'; + +export default ({ + remarkSplitIntervals, + remark, +}: { + remarkSplitIntervals: IRemarkInterval[]; + remark?: any; +}) => { + return ( +
+ {remarkSplitIntervals.map((interval: IRemarkInterval, index: number) => { + const remarkAnnotation = _.last(interval.remarkAnnotations); + const highlight = interval?.remarkAnnotations?.find( + (i) => i?.auditID === remark.hoverAuditID, + ); + const color = highlight ? '#ffc60a' : '#fcdf7e'; + let borderStyle = `2px solid ${color}`; + if (!remark?.isShowRemark) { + borderStyle = ''; + } + if (remarkAnnotation) { + return ( + + {interval.text} + + ); + } + return {interval.text}; + })} +
+ ); +}; diff --git a/packages/lb-components/src/components/attributeList/index.tsx b/packages/lb-components/src/components/attributeList/index.tsx index 0e4f47266..e80a170ee 100644 --- a/packages/lb-components/src/components/attributeList/index.tsx +++ b/packages/lb-components/src/components/attributeList/index.tsx @@ -12,6 +12,7 @@ import { ILimit, IDefaultSize } from '@labelbee/lb-utils'; import LimitPopover from './components/limitPopover'; import _ from 'lodash'; import { CommonToolUtils, MathUtils } from '@labelbee/lb-annotation'; +import { GraphToolInstance } from '@/store/annotation/types'; export const ATTRIBUTE_COLORS = [NULL_COLOR].concat(COLORS_ARRAY); @@ -34,12 +35,14 @@ interface IProps { updateSize?: (size: IDefaultSize) => void; attributeLockChange?: (list: string[]) => void; forbidShowLimitPopover?: boolean; + toolInstance?: GraphToolInstance; } const AttributeList = React.forwardRef((props: IProps, ref) => { const radioRef = React.useRef(); const { t } = useTranslation(); const list = props.list || []; + const { toolInstance } = props const [paletteVisible, setPaletteVisible] = useState(false); const [editConfigIndex, setEditConfigIndex] = useState(undefined); @@ -47,6 +50,19 @@ const AttributeList = React.forwardRef((props: IProps, ref) => { let NEW_ATTRIBUTE_COLORS = [...ATTRIBUTE_COLORS]; + useEffect(() => { + if (toolInstance) { + toolInstance.on('attributeLockChanged', onAttributeLockChanged) + } + return () => { + toolInstance?.unbind('attributeLockChanged', onAttributeLockChanged); + }; + }, [toolInstance]) + + const onAttributeLockChanged = (list: string[]) => { + setAttributeLockList(list) + } + // 去除默认的颜色 if (props.forbidDefault === true) { NEW_ATTRIBUTE_COLORS = NEW_ATTRIBUTE_COLORS.slice(1); diff --git a/packages/lb-components/src/components/audioAnnotate/audioSide/labelSidebar/index.module.scss b/packages/lb-components/src/components/audioAnnotate/audioSide/labelSidebar/index.module.scss index 2f1ab369b..fed0d64e7 100644 --- a/packages/lb-components/src/components/audioAnnotate/audioSide/labelSidebar/index.module.scss +++ b/packages/lb-components/src/components/audioAnnotate/audioSide/labelSidebar/index.module.scss @@ -28,6 +28,26 @@ -webkit-user-select: none; -ms-user-select: none; user-select: none; + :global { + .ant-collapse-borderless > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { + padding-top: 4px; + } + + .ant-collapse-content > .ant-collapse-content-box { + padding: 0; + } + + .ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header { + padding-right: 40px; + } + + .ant-collapse-borderless > .ant-collapse-item { + border-bottom: 0px; + .ant-collapse-header { + padding: 16px; + } + } + } } .main::-webkit-scrollbar { @@ -86,27 +106,6 @@ padding: 0 3px 0 8px; } -:global { - .ant-collapse-borderless > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box { - padding-top: 4px; - } - - .ant-collapse-content > .ant-collapse-content-box { - padding: 0; - } - - .ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header { - padding-right: 40px; - } - - .ant-collapse-borderless > .ant-collapse-item { - border-bottom: 0px; - .ant-collapse-header { - padding: 16px; - } - } -} - .filterContainer { position: relative; height: 100%; diff --git a/packages/lb-components/src/components/errorBoundary/index.tsx b/packages/lb-components/src/components/errorBoundary/index.tsx new file mode 100644 index 000000000..91686bc48 --- /dev/null +++ b/packages/lb-components/src/components/errorBoundary/index.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useContext, useMemo } from 'react'; + +// Create a Context to pass onError event +interface ErrorContextType { + onError?: (msg: string) => void; +} + +const ErrorContext = createContext({ + onError: () => {}, +}); + +// Error handling function +const handleError = () => { + console.log('Error occurred!'); +}; + +// Error Provider component +export const ErrorProvider: React.FC<{ onError?: () => void }> = ({ onError, children }) => { + const errorHandler = useMemo(() => { + return { + onError: onError || handleError, + } + }, [onError]) + return ( + + {children} + + ); +}; + +// Custom hook to get throwError method in child components +export const useError = (): ErrorContextType => useContext(ErrorContext) diff --git a/packages/lb-components/src/components/highlightBadge/index.module.scss b/packages/lb-components/src/components/highlightBadge/index.module.scss new file mode 100644 index 000000000..834c6f462 --- /dev/null +++ b/packages/lb-components/src/components/highlightBadge/index.module.scss @@ -0,0 +1,33 @@ +.highlightBadge { + cursor: pointer; + padding: 2px 4px; + gap: 2px; + border-radius: 2px; + font-size: 12px; + color: rgb(153, 153, 153); + path { + fill: #999; + } + &.active, + &.active:hover { + color: #666fff; + path { + fill: #666fff; + } + } + &:hover { + color: #8c9aff; + background-color: #eeefff; + path { + fill: #8c9aff; + } + } + + .flag { + display: flex; + justify-content: center; + } + .text { + margin-top: 8px; + } +} diff --git a/packages/lb-components/src/components/highlightBadge/index.tsx b/packages/lb-components/src/components/highlightBadge/index.tsx new file mode 100644 index 000000000..c678f3f9c --- /dev/null +++ b/packages/lb-components/src/components/highlightBadge/index.tsx @@ -0,0 +1,81 @@ +/** + * @file 拉框工具-标注模式-只看高亮框标识和本页高亮框个数 + * 切换标识需要将 attributeLockList 设置为空数组 + * @author lihuaqi + * @date 2023年12月25日 + */ + +import { ReactComponent as FlagActive } from '@/assets/annotation/rectTool/flag_active.svg'; +import { ReactComponent as FlagDefault } from '@/assets/annotation/rectTool/flag_default.svg'; +import { classnames } from '@/utils'; +import { Badge, Tooltip } from 'antd'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { connect } from 'react-redux'; +import styles from './index.module.scss'; +import { LabelBeeContext, useDispatch } from '@/store/ctx'; +import { IRect } from '@labelbee/lb-utils'; +import { useTranslation } from 'react-i18next'; + +const HighlightBadge = (props: any) => { + const { t } = useTranslation(); + + const [rectsVisible, setRectsVisible] = useState(false) + const toolInstance = props?.annotation?.toolInstance; + + const handleClick = useCallback(() => { + if (toolInstance) { + const nextRectsVisible = !rectsVisible; + setRectsVisible(nextRectsVisible) + toolInstance?.setAttributeLockList([]); + toolInstance?.setHighlightVisible(nextRectsVisible); + } + }, [rectsVisible, toolInstance]); + + const count = useMemo(() => { + if (!toolInstance?.rectList) return 0 + return toolInstance?.rectList?.filter((rect: IRect) => rect?.isHighlight)?.length + }, [toolInstance?.rectList]) + + useEffect(() => { + if (toolInstance) { + toolInstance.on('attributeLockChanged', onAttributeLockChanged) + } + return () => { + toolInstance?.unbind('attributeLockChanged', onAttributeLockChanged); + }; + }, [toolInstance]) + + const onAttributeLockChanged = (list: string[]) => { + if (list.length > 0) { + setRectsVisible(false) + } + } + if (count === 0) { + return null; + } + + return ( + + +
+
{rectsVisible ? : }
+ +
{t("ShowHighlightRect")}
+
+
+
+ ); +}; + +const mapStateToProps = ({ annotation }: any) => { + return { annotation }; +}; + +export default connect(mapStateToProps, null, null, { context: LabelBeeContext })(HighlightBadge); + diff --git a/packages/lb-components/src/constant/index.tsx b/packages/lb-components/src/constant/index.tsx index 932cf2968..31899e780 100644 --- a/packages/lb-components/src/constant/index.tsx +++ b/packages/lb-components/src/constant/index.tsx @@ -35,3 +35,9 @@ export enum ELLMDataType { Text = 'text', None = 'none', } + +// 侧边栏工具面板 +export const TOOL_PANEL_KEY = { + Tool: 'tool', + Remark: 'remark', +}; diff --git a/packages/lb-components/src/index.scss b/packages/lb-components/src/index.scss index dfb2dc111..ac1b053e7 100644 --- a/packages/lb-components/src/index.scss +++ b/packages/lb-components/src/index.scss @@ -793,7 +793,7 @@ $hotkey-container-padding: 7px; width: 100%; display: flex; flex-direction: column; - + overflow-x: hidden; .styleSlider { .title { min-width: 60px; @@ -2067,7 +2067,7 @@ $ptToolHeight: 40px; &__footer { display: flex; - justify-content: end; + justify-content: flex-end; span { display: inline-flex; align-items: center; diff --git a/packages/lb-components/src/index.tsx b/packages/lb-components/src/index.tsx index cbc8161e7..08bf04039 100644 --- a/packages/lb-components/src/index.tsx +++ b/packages/lb-components/src/index.tsx @@ -18,11 +18,13 @@ import { PointCloudProvider } from './components/pointCloudView/PointCloudContex import { AppState } from './store'; import { LabelBeeContext } from '@/store/ctx'; import PredictTracking from '@/components/predictTracking'; +import HighlightBadge from '@/components/highlightBadge'; import LLMToolView from '@/components/LLMToolView'; import SwitchCuboidBoxIn2DView from '@/views/MainView/toolFooter/SwitchCuboidBoxIn2DView'; import MeasureCanvas from './components/measureCanvas'; import AnnotatedBox from './views/MainView/sidebar/PointCloudToolSidebar/components/annotatedBox'; import { FindTrackIDIndexInCheckMode as FindTrackIDIndex } from './views/MainView/sidebar/PointCloudToolSidebar/components/findTrackIDIndex'; +import { ErrorProvider } from '@/components/errorBoundary'; export const store = configureStore(); @@ -79,6 +81,8 @@ export { MeasureCanvas, AnnotatedBox, FindTrackIDIndex, + HighlightBadge, + ErrorProvider }; export * from './constant'; diff --git a/packages/lb-components/src/store/annotation/reducer.ts b/packages/lb-components/src/store/annotation/reducer.ts index 4467b3831..5badb8327 100644 --- a/packages/lb-components/src/store/annotation/reducer.ts +++ b/packages/lb-components/src/store/annotation/reducer.ts @@ -7,6 +7,7 @@ import { IStepInfo } from '@/types/step'; import { jsonParser } from '@/utils'; import AnnotationDataUtils from '@/utils/AnnotationDataUtils'; import { ConfigUtils } from '@/utils/ConfigUtils'; +import { getBasicResult, getReferenceInfo} from '@/utils/Pipeline' import { composeResult, composeResultWithBasicImgInfo } from '@/utils/data'; import StepUtils from '@/utils/StepUtils'; import ToolUtils from '@/utils/ToolUtils'; @@ -457,6 +458,7 @@ export const annotationReducer = ( const basicIndex = nextBasicIndex ?? 0; const fileResult = jsonParser(imgList[nextIndex]?.result); + const preResult = imgList[nextIndex]?.preResult const stepResult = fileResult[`step_${currentStepInfo.step}`]; @@ -480,7 +482,20 @@ export const annotationReducer = ( const { dataSourceStep, tool } = stepConfig; const dependStepConfig = getStepConfig(stepList, dataSourceStep); const hasDataSourceStep = dataSourceStep && tool; - const stepBasicResultList = fileResult[`step_${dataSourceStep}`]?.result ?? []; + + const stepBasicResultList = getBasicResult( + currentStepInfo.step, + currentStepInfo.dataSourceStep, + stepList, + fileResult, + true, + preResult, + ); + + const referenceInfo = getReferenceInfo(step, stepList, { + result: imgList[nextIndex].result, + preResult, + }) const result = AnnotationDataUtils.getInitialResultList( stepResult?.result, @@ -492,6 +507,10 @@ export const annotationReducer = ( annotationEngine?.launchOperation(); + if (referenceInfo?.referenceToolName) { + annotationEngine?.setReferenceInfo(referenceInfo) + } + if (hasDataSourceStep) { if (stepBasicResultList?.length > 0) { annotationEngine?.setBasicInfo(dependStepConfig.tool, stepBasicResultList[basicIndex]); diff --git a/packages/lb-components/src/store/toolStyle/reducer.ts b/packages/lb-components/src/store/toolStyle/reducer.ts index a6e2de257..60cd9cdb6 100644 --- a/packages/lb-components/src/store/toolStyle/reducer.ts +++ b/packages/lb-components/src/store/toolStyle/reducer.ts @@ -12,6 +12,7 @@ const initialState: ToolStyleState = { attributeColor: ToolStyleUtils.getAttributeColors(), lineColor: ToolStyleUtils.getDefaultToolLineColors(), attributeLineColor: [NULL_COLOR].concat(COLORS_ARRAY), + hiddenText: false, }; export function toolStyleReducer( diff --git a/packages/lb-components/src/store/toolStyle/types.ts b/packages/lb-components/src/store/toolStyle/types.ts index 29321ded4..cc27f1d33 100644 --- a/packages/lb-components/src/store/toolStyle/types.ts +++ b/packages/lb-components/src/store/toolStyle/types.ts @@ -12,6 +12,7 @@ export interface ToolStyleState { attributeColor: any[]; lineColor: string; attributeLineColor: string[]; + hiddenText: boolean; } interface UpdateToolStyleConfig { diff --git a/packages/lb-components/src/types/main.d.ts b/packages/lb-components/src/types/main.d.ts index 4abe29f2d..b25f969d9 100644 --- a/packages/lb-components/src/types/main.d.ts +++ b/packages/lb-components/src/types/main.d.ts @@ -84,6 +84,8 @@ interface IFooter { shortCutTable: { [a: string]: any; }; + /** hide text info */ + hiddenTextSwitch: React.ReactNode; } export type RenderFooter = ({ @@ -96,6 +98,7 @@ export type RenderFooter = ({ footerDivider, ToolHotKeyCom, shortCutTable, + hiddenTextSwitch, }: IFooter) => React.ReactNode; export type Header = ({ diff --git a/packages/lb-components/src/utils/Pipeline.ts b/packages/lb-components/src/utils/Pipeline.ts new file mode 100644 index 000000000..2868a79bd --- /dev/null +++ b/packages/lb-components/src/utils/Pipeline.ts @@ -0,0 +1,548 @@ +import { CommonToolUtils, EToolName } from '@labelbee/lb-annotation'; +import { IStepInfo } from '@/types/step'; +import { jsonParser } from '@/utils'; + +/** + * Retrieve the dependencies for the current step to be used in displaying the dependency framework + * + * + * @export + * @param {number} currentStep current annotation step(needs to be converted into normal annotation steps if in quality inspection case) + * @param {number} dataSourceStep step of datasource + * @param {IStepInfo[]} stepList + * @param {*} result all results for the current image + * @param {boolean} [filterNeeded=true] + * @param {boolean} [forbidFilter=false] + * @returns + */ +export function getBasicResult( + currentStep: number, + dataSourceStepParam: number, + stepList: IStepInfo[], + resultParam: any, + filterNeeded = true, + preResult = '{}', + forbidFilter = false, +) { + let result = resultParam; + let dataSourceStep = dataSourceStepParam; + + const currentStepInfo = CommonToolUtils.getCurrentStepInfo(currentStep, stepList); + let dataSourceStepInfo = CommonToolUtils.getCurrentStepInfo(dataSourceStep, stepList); + + if (currentStepInfo?.preDataSourceStep > 0) { + if (typeof preResult === 'string') { + result = jsonParser(preResult); + } else { + result = preResult; + } + + dataSourceStep = currentStepInfo?.preDataSourceStep; + dataSourceStepInfo = { + tool: result[`step_${currentStepInfo?.preDataSourceStep}`]?.toolName, + step: 1, + }; + } + + if (!result[`step_${dataSourceStep}`] || !result[`step_${dataSourceStep}`].result) { + return []; + } + const sourceData = result[`step_${dataSourceStep}`].result; + + if (!dataSourceStepInfo && currentStepInfo?.preDataSourceStep <= 0) { + return []; + } + try { + const currentStepInfoConfig = jsonParser(currentStepInfo.config); + // Recursion is needed if it is a non-box tool + const filterData = filterDataAdaptor(currentStepInfoConfig.filterData, dataSourceStepInfo); + if (!filterData && filterNeeded) { + return []; + } + + if (dataSourceStepInfo.tool === EToolName.Tag) { + // Filtering the data from the tagging tool + if ( + dataSourceStepInfo.dataSourceStep === 0 && + (!dataSourceStepInfo?.preDataSourceStep || dataSourceStepInfo?.preDataSourceStep === 0) + ) { + return []; + } + + // Filter out the current tags data that meets the criteria + const newData = filterTagResult(sourceData, filterData, forbidFilter); + const sourceIDList = newData.map((v) => ({ id: v.id, sourceID: v.sourceID })); + return getSourceData( + dataSourceStepInfo.step, + dataSourceStepInfo.dataSourceStep, + stepList, + result, + sourceIDList, + preResult, + ); + } else if ( + [ + EToolName.Rect, + EToolName.RectTrack, + EToolName.Polygon, + EToolName.Point, + EToolName.Line, + ].includes(dataSourceStepInfo.tool) + ) { + return useConfigFilterData(currentStep, stepList, sourceData, forbidFilter); + } else if (dataSourceStepInfo.tool === EToolName.Filter) { + // Filtering the data from the selection tool + if ( + dataSourceStepInfo.dataSourceStep === 0 && + (!dataSourceStepInfo?.preDataSourceStep || dataSourceStepInfo?.preDataSourceStep === 0) + ) { + return []; + } + // Filter out the current tags data that meets the criteria + const newData = sourceData.filter( + (v: any) => filterData.indexOf(v.filterLabel) > -1 || forbidFilter, + ); + const sourceIDList = newData.map((v: any) => ({ id: v.id, sourceID: v.sourceID })); + return getSourceData( + dataSourceStepInfo.step, + dataSourceStepInfo.dataSourceStep, + stepList, + result, + sourceIDList, + preResult, + ); + } + } catch (e) { + console.error(e); + } +} + +export const DEFAULT_LINK = '@@'; +const DEFAULT_TOOL_ATTRIBUTE = ['valid', 'invalid']; +/** + * turn filterData into new filterData + * + * @export + * @param {*} oldFilterData + * @param {IStepInfo} dataSourceStepInfo attention: this is stepInfo of data source + * @returns + */ +export function filterDataAdaptor( + oldFilterData: any, + dataSourceStepInfo: IStepInfo, +): string[] { + const config = jsonParser(dataSourceStepInfo?.config); + + if (oldFilterData?.constructor === Object) { + // means old data + const keyList = Object.keys(oldFilterData).reduce((acc: any[], cur: string) => { + if (Array.isArray(oldFilterData[cur])) { + // In the old format, it only consists of the tagging tool + return [...acc, ...oldFilterData[cur].map((v: any) => `${cur}${DEFAULT_LINK}${v}`)]; + } + + // Adaptation for the previous old image tools + if ( + DEFAULT_TOOL_ATTRIBUTE.includes(cur) && + oldFilterData[cur] === true && + judgeIsAttribute(config) + ) { + return [ + ...acc, + `${cur}${DEFAULT_LINK}`, // no attribute + ...config.attributeList.reduce((a: any, v: any) => { + return [...a, `${cur}${DEFAULT_LINK}${v?.value}`]; + }, []), + ]; + } + + return [...acc, cur]; + }, []); + + return keyList; + } + + return oldFilterData; +} + +/** + * Determine whether the annotation is based on attributes + * + * @export + * @param {*} config + * @returns + */ +export function judgeIsAttribute(config: any) { + return ( + config?.attributeConfigurable && config?.attributeList && config?.attributeList?.length > 0 + ); +} + +/** + * Filtering tag results + * @param sourceData + * @param filterData + * @param forbidFilter + */ +export function filterTagResult( + sourceData: any[], + filterData: string[], + forbidFilter?: boolean, +) { + const tagInputList = transformFilterDataToObject(filterData); + + return sourceData.filter((data) => { + if (forbidFilter === true) { + return true; + } + + for (const i of Object.keys(data.result)) { + // Note that the result of the tagging tool is an object + + if (tagInputList.hasOwnProperty(i)) { + const dataList = data.result[i].split(';'); + for (const d of dataList) { + if (tagInputList[i].indexOf(d) > -1) { + return true; + } + } + } + } + return false; + }); +} + +/** + * turn filterData into Object format,making it easy to determine + * + * @export + * @param {FilterDataState} filterData + * @returns + */ + +export function transformFilterDataToObject(filterData: string[]): any { + return filterData.reduce((acc: { [key: string]: any }, cur) => { + const [key, value] = cur.split(DEFAULT_LINK); + + if (typeof value === 'undefined') { + // means empty, like: ['valid', 'invalid'] + return { + ...acc, + [key]: [], + }; + } + + if (acc.hasOwnProperty(key)) { + return { + ...acc, + [key]: [...acc[key], value], + }; + } else { + return { + ...acc, + [key]: [value], + }; + } + }, {}); +} + +/** + * Filtering current data by config + * + * Note: This function is currently only applicable for filtering steps related to graphical elements. + * + * @export + * @param {number} currentStep + * @param {IStepInfo[]} stepList + * @param {any[]} basicData + * @returns + */ +export function useConfigFilterData( + currentStep: number, + stepList: IStepInfo[], + basicData: any[], + forbidFilter = false, +) { + const currentStepInfo = CommonToolUtils.getStepInfo(currentStep, stepList); + const dataSourceStepInfo = CommonToolUtils.getCurrentStepInfo(currentStepInfo.dataSourceStep, stepList); + let res = basicData; + if (currentStepInfo.config) { + const config = jsonParser(currentStepInfo.config); + if (config.filterData) { + const filterData = filterDataAdaptor(config.filterData, dataSourceStepInfo); + + const filterDataObject = transformFilterDataToObject(filterData); + + res = basicData.filter((v) => { + if (forbidFilter === true) { + return true; + } + + if (filterData.some((v) => v === 'valid') || filterData.some((v) => v === 'invalid')) { + // This means centralized management of graphical elements + if (filterData.some((v) => v === 'valid') && v?.valid === true) { + // This indicates filtering based on original graphical elements + return true; + } + + if (filterData.some((v) => v === 'invalid') && v?.valid === false) { + // This indicates filtering based on original graphical elements + return true; + } + + return false; + } + // Filtering data containing attributes + if (judgeIsAttribute(jsonParser(dataSourceStepInfo.config))) { + if (v?.valid === true) { + return filterDataObject?.valid?.includes(v?.attribute ?? ''); + } else { + return filterDataObject?.invalid?.includes(v?.attribute ?? ''); + } + } + + return false; + }); + } + } + return res; +} + +/** + * Filter out specified dependency boxes in a specific step + * @param currentStep + * @param dataSourceStep + * @param stepList + * @param result + * @param sourceIDList + * @param preResult + */ +export function getSourceData( + currentStep: number, + dataSourceStep: number, + stepList: IStepInfo[], + result: any, + sourceIDList: any[], + preResult = '{}', +): any { + const currentStepInfo = CommonToolUtils.getCurrentStepInfo(currentStep, stepList); + + if (currentStepInfo?.preDataSourceStep > 0) { + // This indicates that the step relies on pre-annotated data + const stepName = `step_${currentStepInfo?.preDataSourceStep}`; + const result = jsonParser(preResult)[stepName]?.result; + + if (result) { + return ( + result?.filter((v: any) => { + for (const i of sourceIDList) { + if (i?.sourceID === v.id) { + return true; + } + } + return false; + }) ?? [] + ); + } + } + + if (currentStep === 0 || !result[`step_${dataSourceStep}`]?.result) { + return []; + } + const dataSourceStepInfo = CommonToolUtils.getCurrentStepInfo(dataSourceStep, stepList); + if (!dataSourceStepInfo) { + return []; + } + + const sourceData = result[`step_${dataSourceStep}`].result; + let resultList = []; + resultList = sourceData.filter((v: any) => { + for (const i of sourceIDList) { + if (i?.sourceID === v.id) { + return true; + } + } + return false; + }); + + if (dataSourceStepInfo.tool === EToolName.Tag || dataSourceStepInfo.tool === EToolName.Filter) { + // If the dataSource tool is still tag tool, continue the recursion + return getSourceData( + dataSourceStepInfo.step, + dataSourceStepInfo.dataSourceStep, + stepList, + result, + sourceIDList, // Note: There is some doubt about whether this part can be used correctly. It indicates skepticism - further attention is needed todo + preResult, + ); + } else if ( + [EToolName.Rect, EToolName.RectTrack, EToolName.Polygon].includes(dataSourceStepInfo.tool) + ) { + return resultList; + } + + return resultList; +} + + +/** + * 查找当前依赖步骤的配置信息 + * @param dataSourceStep + * @param stepList + */ +export function getBasicConfig(dataSourceStep: number, stepList: IStepInfo[]) : any{ + const dataSourceStepInfo = CommonToolUtils.getCurrentStepInfo(dataSourceStep, stepList); + if (!dataSourceStepInfo) { + return {}; + } + if ( + [ + EToolName.Rect, + EToolName.RectTrack, + EToolName.Polygon, + EToolName.Point, + EToolName.Line, + ].includes(dataSourceStepInfo.tool) + ) { + return jsonParser(dataSourceStepInfo?.config); + } + + return getBasicConfig(dataSourceStepInfo.dataSourceStep, stepList); +} + +/** + * 获取当前依赖下的工具名 + * @param dataSourceStep + * @param stepList + * @param preData + */ +export function getBasicToolName( + dataSourceStep: number, + stepList: IStepInfo[], + preData?: { preDataSourceStep: number; preResult?: string }, +): any { + if (preData && preData.preDataSourceStep > 0 && preData.preResult) { + const preResult = jsonParser(preData.preResult); + const stepResult = preResult[`step_${preData.preDataSourceStep}`]; + return stepResult?.toolName; + } + + const dataSourceStepInfo = CommonToolUtils.getCurrentStepInfo(dataSourceStep, stepList); + if (!dataSourceStepInfo) { + return EToolName.Empty; + } + if ( + [ + EToolName.Rect, + EToolName.RectTrack, + EToolName.Polygon, + EToolName.Point, + EToolName.Line, + ].includes(dataSourceStepInfo.tool) + ) { + return dataSourceStepInfo.tool; + } + + if (preData?.preResult && dataSourceStepInfo.preDataSourceStep > 0) { + const preResult = jsonParser(preData.preResult); + // 依赖预标注 + return preResult[`step_${dataSourceStepInfo.preDataSourceStep}`]?.toolName ?? EToolName.Empty; + } + + let newPreData; + if (preData) { + newPreData = { + ...preData, + preDataSourceStep: dataSourceStepInfo.preDataSourceStep, + }; + } + + return getBasicToolName(dataSourceStepInfo.dataSourceStep, stepList, newPreData); +} + +/** + * 获取参考显示的配置、结果、工具名 + * @param stepList + * @param step + * @param result + * @param preResult + */ +export const getReferenceConfig = ( + stepList: IStepInfo[], + step: number, + result: any, + preResult: any, +) => { + if (stepList.length === 0) { + return {}; + } + + let referenceResult; + let referenceConfig; + let referenceToolName; + + const stepInfo = CommonToolUtils.getCurrentStepInfo(step, stepList); + const stepConfig = jsonParser(stepInfo.config); + + const { referenceStep, referenceFilterData, preReferenceStep } = stepConfig; + + const hasReferenceStep = referenceStep > 0 || preReferenceStep > 0; + + if (hasReferenceStep && referenceFilterData) { + const referenceStepList = stepList.map((v) => { + if (v.step === stepInfo.step) { + return { + ...v, + preDataSourceStep: preReferenceStep ?? 0, + dataSourceStep: referenceStep, + config: JSON.stringify({ + ...jsonParser(v?.config ?? '{}'), + filterData: referenceFilterData, + }), + }; + } + return v; + }); + + referenceResult = getBasicResult( + stepInfo.step, + referenceStep, + referenceStepList, + jsonParser(result), + true, + preResult, + ); + referenceConfig = getBasicConfig(referenceStep, stepList); + referenceToolName = getBasicToolName(referenceStep, stepList, { + preDataSourceStep: preReferenceStep, + preResult, + }); + } + + return { referenceResult, referenceConfig, referenceToolName }; +}; + +export const getReferenceInfo = ( + step: number, + stepList: IStepInfo[], + fileData: { preResult: any; result: any } = { preResult: '', result: '' }, +) => { + if (!stepList?.length || !step) { + return; + } + + const { result, preResult } = fileData; + + const { referenceResult, referenceConfig, referenceToolName } = getReferenceConfig( + stepList, + step, + result, + preResult, + ); + + return { + referenceResult, + referenceConfig, + referenceToolName, + }; +}; diff --git a/packages/lb-components/src/views/MainView/annotationOperation/index.tsx b/packages/lb-components/src/views/MainView/annotationOperation/index.tsx index 2a36a9757..1a8e1ca7b 100644 --- a/packages/lb-components/src/views/MainView/annotationOperation/index.tsx +++ b/packages/lb-components/src/views/MainView/annotationOperation/index.tsx @@ -109,8 +109,8 @@ const AnnotationOperation: React.FC = (props: IProps) => { }, [toolInstance]); useEffect(() => { - if (annotationEngine?.setImgAttribute) { - annotationEngine.setImgAttribute(imgAttribute); + if (annotationEngine?.syncImgAttribute) { + annotationEngine.syncImgAttribute(imgAttribute); } else { // Old version. toolInstance?.setImgAttribute?.(imgAttribute); diff --git a/packages/lb-components/src/views/MainView/sidebar/SwitchAttributeList/index.tsx b/packages/lb-components/src/views/MainView/sidebar/SwitchAttributeList/index.tsx index 52020d59f..56651c516 100644 --- a/packages/lb-components/src/views/MainView/sidebar/SwitchAttributeList/index.tsx +++ b/packages/lb-components/src/views/MainView/sidebar/SwitchAttributeList/index.tsx @@ -68,6 +68,10 @@ const SwitchAttributeList: React.FC = (props) => { } }; + const defaultAttributeLockChange = (list: string[]) => { + toolInstance?.setAttributeLockList(list); + }; + return ( = (props) => { selectedAttribute={toolInstance?.defaultAttribute ?? ''} ref={listRef} forbidDefault={isScribbleTool} - attributeLockChange={attributeLockChange} + attributeLockChange={attributeLockChange ?? defaultAttributeLockChange} + toolInstance={toolInstance} /> ); } diff --git a/packages/lb-components/src/views/MainView/toolFooter/FooterPopover.tsx b/packages/lb-components/src/views/MainView/toolFooter/FooterPopover.tsx index ffe6b70f3..2babb05e5 100644 --- a/packages/lb-components/src/views/MainView/toolFooter/FooterPopover.tsx +++ b/packages/lb-components/src/views/MainView/toolFooter/FooterPopover.tsx @@ -5,7 +5,7 @@ type Source = string | React.ReactElement; const Icon = ({ source }: { source: Source }) => { if (typeof source === 'string') { - return ; + return ; } return source; diff --git a/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.module.scss b/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.module.scss new file mode 100644 index 000000000..725175648 --- /dev/null +++ b/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.module.scss @@ -0,0 +1,16 @@ +.hiddenTextSwitch { + display: flex; + align-items: center; + margin-right: 10px; + color: #fff; + font-size: 12px; + :global { + .ant-switch { + margin-left: 5px; + background-color: #a3a3a3; + } + .ant-switch-checked { + background-color: #666fff; + } + } +} diff --git a/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.tsx b/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.tsx new file mode 100644 index 000000000..b8d3088f0 --- /dev/null +++ b/packages/lb-components/src/views/MainView/toolFooter/HiddenTextSwitch/index.tsx @@ -0,0 +1,84 @@ +import { cKeyCode, EToolName } from '@labelbee/lb-annotation'; +import { Switch } from 'antd'; +import React, { useEffect, useRef } from 'react'; +import styles from './index.module.scss'; +import { AppState } from '@/store'; +import { connect } from 'react-redux'; +import { LabelBeeContext } from '@/store/ctx'; +import { UpdateToolStyleConfig } from '@/store/toolStyle/actionCreators'; +import { store } from '@/index'; +import { useTranslation } from 'react-i18next'; + +const EKeyCode = cKeyCode.default; + +interface IProps { + toolName: string; + toolStyle: any; +} + +// 不看图形信息 +const HiddenTextSwitch = (props: IProps) => { + const { + toolName, + toolStyle: { hiddenText }, + } = props; + + const {t} = useTranslation(); + + const prevHiddenText = useRef(hiddenText); + const showHiddenText = [ + EToolName.Rect, + EToolName.Point, + EToolName.Polygon, + EToolName.Line, + ].includes(toolName as EToolName); + + const changeHiddenText = (hiddenText: boolean) => { + prevHiddenText.current = hiddenText; + store.dispatch(UpdateToolStyleConfig({ hiddenText })) + }; + + const onKeydown = (e: KeyboardEvent) => { + switch (e.keyCode) { + case EKeyCode.V: + if (showHiddenText && !e.ctrlKey) { + changeHiddenText(!prevHiddenText.current); + } + break; + } + }; + + useEffect(() => { + // 查看模式切换标注工具时 对于不显示hiddenText的工具设为false + if (showHiddenText) { + changeHiddenText(prevHiddenText.current); + } else { + store.dispatch(UpdateToolStyleConfig({ hiddenText: false })) + } + window.addEventListener('keydown', onKeydown); + return () => { + window.removeEventListener('keydown', onKeydown); + }; + }, [toolName]); + + useEffect(() => { + return () => changeHiddenText(false); + }, []); + + if (!showHiddenText) { + return null; + } + + return ( +
+ {t("HideTextInfo")}(V) + +
+ ); +}; + +const mapStateToProps = ({ toolStyle }: AppState) => ({ + toolStyle, +}); + +export default connect(mapStateToProps, null, null, { context: LabelBeeContext })(HiddenTextSwitch); diff --git a/packages/lb-components/src/views/MainView/toolFooter/index.tsx b/packages/lb-components/src/views/MainView/toolFooter/index.tsx index 9159509be..e78b47224 100644 --- a/packages/lb-components/src/views/MainView/toolFooter/index.tsx +++ b/packages/lb-components/src/views/MainView/toolFooter/index.tsx @@ -18,6 +18,7 @@ import { Pagination } from './Pagination'; import { AnnotatedAttributesIcon } from './AnnotatedAttributes'; import { cTool } from '@labelbee/lb-annotation'; import { shortCutTable, ToolHotKeyCom } from './FooterTips/ToolHotKey'; +import HiddenTextSwitch from './HiddenTextSwitch'; const { EPointCloudName } = cTool; @@ -56,6 +57,7 @@ const renderFooter: RenderFooter = ({ curItems, footerDivider, annotateAttrList, + hiddenTextSwitch, }) => { return ( <> @@ -63,6 +65,7 @@ const renderFooter: RenderFooter = ({ {annotateAttrList}
{hiddenTips} + {hiddenTextSwitch} {pageNumber} {pagination} {curItems} @@ -125,6 +128,8 @@ const ToolFooter: React.FC = (props: IProps) => { /> ); + const hiddenTextSwitch = + const curItems = hasSourceStep && basicResultList.length > 0 ? ( {t('curItems', { current: basicIndex + 1, total: basicResultList.length })} @@ -156,6 +161,7 @@ const ToolFooter: React.FC = (props: IProps) => { footerDivider: , shortCutTable, ToolHotKeyCom, + hiddenTextSwitch, })}
); diff --git a/packages/lb-components/src/views/MainView/toolHeader/headerOption/index.tsx b/packages/lb-components/src/views/MainView/toolHeader/headerOption/index.tsx index 10416b352..ae8380e28 100644 --- a/packages/lb-components/src/views/MainView/toolHeader/headerOption/index.tsx +++ b/packages/lb-components/src/views/MainView/toolHeader/headerOption/index.tsx @@ -170,8 +170,9 @@ const HeaderOption: React.FC = (props) => { className='item' onMouseEnter={() => setToolHover(info.toolName)} onMouseLeave={() => setToolHover('')} + onClick={info.click} > - + { step: step ?? 1, dataSourceStep: sourceStep || 0, tool: toolList, - config: JSON.stringify(getConfig(toolName)), + config: JSON.stringify({ + ...getConfig(toolName), + referenceStep: sourceStep || 0, + referenceFilterData: ['valid', 'invalid'], + }), }; }; diff --git a/packages/lb-utils/src/i18n/resources.json b/packages/lb-utils/src/i18n/resources.json index 372dde294..d11fdcd7c 100644 --- a/packages/lb-utils/src/i18n/resources.json +++ b/packages/lb-utils/src/i18n/resources.json @@ -356,6 +356,9 @@ "ClickOnTheIdToHighlightTheMarkupBox": "Click on the ID to highlight the markup box", "DoubleClickOnTheIdToContinuouslyHighlightBoxesAcrossFrames": "Double-click on the ID to continuously highlight boxes across frames", "FindTheFrameCorrespondingToTheLabeledFrameId": "Find the frame corresponding to the labeled frame ID", + "ShowHighlightRect": "View Highlighted Rects", + "ShowHighlightRectTip": "Rects with this label need special attention", + "HideTextInfo": "Hide graphical information", "GlobalAnnotation": "Global annotation", "QualifiedAnnotation": "Qualified annotation" }, @@ -716,6 +719,9 @@ "ClickOnTheIdToHighlightTheMarkupBox": "点击ID可以高亮标注框", "DoubleClickOnTheIdToContinuouslyHighlightBoxesAcrossFrames": "双击ID可以跨帧连续高亮标注框", "FindTheFrameCorrespondingToTheLabeledFrameId": "查找标注框ID对应帧", + "ShowHighlightRect": "查看高亮框", + "ShowHighlightRectTip": "带有该标识的框需要重点关注", + "HideTextInfo": "不看图形信息", "GlobalAnnotation":"全局标注", "QualifiedAnnotation":"限定标注" } diff --git a/packages/lb-utils/src/types/index.ts b/packages/lb-utils/src/types/index.ts index 236ed4faa..fb0126b97 100644 --- a/packages/lb-utils/src/types/index.ts +++ b/packages/lb-utils/src/types/index.ts @@ -8,3 +8,4 @@ export * from './base'; export * from './audio'; export * from './styles'; export * from './video'; +export * from './rect'; diff --git a/packages/lb-utils/src/types/rect.ts b/packages/lb-utils/src/types/rect.ts new file mode 100644 index 000000000..8cd7f0cc1 --- /dev/null +++ b/packages/lb-utils/src/types/rect.ts @@ -0,0 +1,40 @@ +import { IToolConfig } from './common'; +import { IInputList } from './base' + +export interface IRect { + x: number; + y: number; + width: number; + height: number; + id: string; + sourceID: string; + valid: boolean; + order?: number; + attribute: string; + textAttribute: string; + disableDelete?: boolean; // 是否允许被删除 + isHighlight?: boolean; // 是否为高亮框 + + label?: string; // 列表标签 +} + +export interface RectStyle { + width?: number; + color?: number; + opacity?: number; +} + +export interface IRectConfig extends IToolConfig { + attributeList: IInputList[]; + attributeConfigurable: boolean; + drawOutsideTarget: boolean; + textConfigurable: boolean; + copyBackwardResult: boolean; + minWidth: number; + minHeight: number; + isShowOrder: boolean; + textCheckType: number; + + markerConfigurable?: boolean; + markerList?: IInputList[]; +} diff --git a/packages/lb-utils/tsconfig.json b/packages/lb-utils/tsconfig.json index 657d59a3d..6d85eb748 100644 --- a/packages/lb-utils/tsconfig.json +++ b/packages/lb-utils/tsconfig.json @@ -17,6 +17,10 @@ }, "resolveJsonModule": true }, - "include": ["src", "lib/index.d.ts"], + "include": [ + "src", + "lib/index.d.ts", + "../lb-annotation/src/types/tool/common.d.ts" + ], "exclude": ["node_modules", "**/__tests__/*", "node_modules/**/*.d.ts"] }