import { Point3, Point2 } from './mathUtils'; import { utilities as csUtils, Types as CoreTypes, getEnabledElement, cache, } from '@cornerstonejs/core'; import * as cornerstone from '@cornerstonejs/core'; import { AnnotationTool, utilities, Types, annotation, drawing, } from '@cornerstonejs/tools'; const { drawHandles, drawLinkedTextBox, drawLine: drawLineSvg, } = drawing; import { PublicToolProps, ToolProps, EventTypes, SVGDrawingHelper, } from '@cornerstonejs/tools/dist/esm/types'; /** * 直线灰度测量注解接口 */ interface LineGrayscaleAnnotation extends Types.Annotation { data: { handles: { // 控制点(两个端点,世界坐标) points: CoreTypes.Point3[]; // 当前激活的手柄索引(0或1,null表示未激活) activeHandleIndex: number | null; }; label?: string; // 文本框位置(Canvas坐标) textBox?: CoreTypes.Point2; // 缓存的统计结果 cachedStats?: { [targetId: string]: { // 平均灰度值 mean: number; // 最小灰度值 min: number; // 最大灰度值 max: number; // 采样点数量 sampleCount: number; // 直线长度(mm) lineLength: number; }; }; }; } /** * 像素采样结果 */ interface PixelSampleResult { values: number[]; coordinates: Array<{ x: number; y: number }>; count: number; } /** * 直线灰度测量工具 * 功能: 在医学影像上绘制直线,计算并显示直线路径上的灰度值统计信息 */ export default class LineGrayscaleMeasurementTool extends AnnotationTool { static toolName = 'LineGrayscaleMeasurementTool'; editData: { annotation: Types.Annotation; viewportIdsToRender: string[]; handleIndex?: number; newAnnotation?: boolean; hasMoved?: boolean; textBoxBeingMoved?: boolean; textBoxOffset?: CoreTypes.Point2; wholeToolOffset?: CoreTypes.Point2; originalPoints?: CoreTypes.Point3[]; // 保存拖拽开始时的原始点坐标 } | null = null; isDrawing: boolean = false; /** * 创建默认注解(工具激活时自动创建) */ static createDefaultAnnotation( element: HTMLDivElement, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport ): LineGrayscaleAnnotation { const enabledElement = getEnabledElement(element); if (!enabledElement) { throw new Error('Element is not enabled'); } // 获取viewport的尺寸 const canvas = viewport.canvas; const { width, height } = canvas; const centerX = width / 2; const centerY = height / 2; // 设置起点和终点(Canvas坐标,水平线,长度100px) const startCanvas: CoreTypes.Point2 = [centerX - 50, centerY]; const endCanvas: CoreTypes.Point2 = [centerX + 50, centerY]; // 转换为世界坐标 const startWorld = viewport.canvasToWorld(startCanvas); const endWorld = viewport.canvasToWorld(endCanvas); const camera = viewport.getCamera(); const { viewPlaneNormal, viewUp } = camera; if (viewPlaneNormal === undefined || viewUp === undefined) { throw new Error('Camera parameters are undefined'); } // 创建注解对象 const annotationData = { invalidated: true, highlighted: false, metadata: { viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3, viewUp: [...viewUp] as CoreTypes.Point3, FrameOfReferenceUID: viewport.getFrameOfReferenceUID(), referencedImageId: viewport.getCurrentImageId?.() || '', toolName: LineGrayscaleMeasurementTool.toolName, }, data: { label: '', handles: { points: [startWorld, endWorld], activeHandleIndex: null, }, cachedStats: {}, }, } as LineGrayscaleAnnotation; return annotationData; } constructor( toolProps: PublicToolProps = {}, defaultToolProps: ToolProps = { supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { shadow: true, preventHandleOutsideImage: false, }, } ) { super(toolProps, defaultToolProps); } /** * 禁用手动绘制(仅支持预设注解) */ addNewAnnotation( evt: EventTypes.InteractionEventType ): LineGrayscaleAnnotation { evt.preventDefault(); return {} as LineGrayscaleAnnotation; } /** * 检查点是否靠近工具 */ isPointNearTool( element: HTMLDivElement, annotation: LineGrayscaleAnnotation, canvasCoords: CoreTypes.Point2, proximity: number ): boolean { const enabledElement = getEnabledElement(element); if (!enabledElement) { return false; } const { viewport } = enabledElement; const points = annotation.data.handles.points; // 检查是否靠近任意一个手柄点 for (let i = 0; i < points.length; i++) { const point = points[i]; const canvasPoint = viewport.worldToCanvas(point); const distance = Math.sqrt( Math.pow(canvasPoint[0] - canvasCoords[0], 2) + Math.pow(canvasPoint[1] - canvasCoords[1], 2) ); if (distance < proximity) { return true; } } // 检查是否靠近直线 if (points.length >= 2) { const p1Canvas = viewport.worldToCanvas(points[0]); const p2Canvas = viewport.worldToCanvas(points[1]); const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas); if (dist < proximity) { return true; } } return false; } /** * 计算点到线段的距离 */ private _distanceToSegment( point: CoreTypes.Point2, lineStart: CoreTypes.Point2, lineEnd: CoreTypes.Point2 ): number { const x = point[0]; const y = point[1]; const x1 = lineStart[0]; const y1 = lineStart[1]; const x2 = lineEnd[0]; const y2 = lineEnd[1]; const A = x - x1; const B = y - y1; const C = x2 - x1; const D = y2 - y1; const dot = A * C + B * D; const lenSq = C * C + D * D; let param = -1; if (lenSq !== 0) { param = dot / lenSq; } let xx, yy; if (param < 0) { xx = x1; yy = y1; } else if (param > 1) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } const dx = x - xx; const dy = y - yy; return Math.sqrt(dx * dx + dy * dy); } /** * 取消操作 */ cancel(element: HTMLDivElement): string { if (this.isDrawing) { this.isDrawing = false; this._deactivateDraw(element); this._deactivateModify(element); const enabledElement = getEnabledElement(element); if (enabledElement) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); } this.editData = null; return this.getToolName(); } return ''; } _activateDraw(element: HTMLDivElement): void { element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_DRAG', this._dragCallback as EventListener ); element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_UP', this._endCallback as EventListener ); } _deactivateDraw(element: HTMLDivElement): void { element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_DRAG', this._dragCallback as EventListener ); element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_UP', this._endCallback as EventListener ); } _activateModify(element: HTMLDivElement): void { element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_DOWN', this._mouseDownModifyCallback as EventListener ); element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_DRAG', this._mouseDragModifyCallback as EventListener ); element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_UP', this._mouseUpModifyCallback as EventListener ); element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveModifyCallback as EventListener ); element.addEventListener( 'keydown', this._keyDownCallback as EventListener ); } _deactivateModify(element: HTMLDivElement): void { element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_DOWN', this._mouseDownModifyCallback as EventListener ); element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_DRAG', this._mouseDragModifyCallback as EventListener ); element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_UP', this._mouseUpModifyCallback as EventListener ); element.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveModifyCallback as EventListener ); element.removeEventListener( 'keydown', this._keyDownCallback as EventListener ); } _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => { const eventDetail = evt.detail; const { element, currentPoints } = eventDetail; const canvasCoords = currentPoints.canvas; const enabledElement = getEnabledElement(element); if (!enabledElement) { return; } const { viewport } = enabledElement; const annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations || annotations.length === 0) { return; } // 查找最近的手柄或工具 for (const ann of annotations) { const customAnn = ann as LineGrayscaleAnnotation; // 1. 检查是否点击在文本框上 // 使用默认位置(第二个端点)如果 textBox 未定义 const textBoxCanvas = customAnn.data.textBox || viewport.worldToCanvas(customAnn.data.handles.points[1]); if (this._isPointNearTextBox(canvasCoords, textBoxCanvas)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); this.editData = { annotation: customAnn, viewportIdsToRender, textBoxBeingMoved: true, textBoxOffset: [ canvasCoords[0] - textBoxCanvas[0], canvasCoords[1] - textBoxCanvas[1], ], hasMoved: false, }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } // 2. 检查是否点击在手柄上 const handle = this.getHandleNearImagePoint( element, customAnn, canvasCoords, 6 ); if (handle) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); this.editData = { annotation: customAnn, viewportIdsToRender, handleIndex: customAnn.data.handles.activeHandleIndex || 0, hasMoved: false, originalPoints: [...customAnn.data.handles.points], }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } // 3. 检查是否点击在线段上(拖拽整个工具) if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); // 计算线段中心点 const points = customAnn.data.handles.points; const centerWorld: CoreTypes.Point3 = [ (points[0][0] + points[1][0]) / 2, (points[0][1] + points[1][1]) / 2, (points[0][2] + points[1][2]) / 2, ]; const centerCanvas = viewport.worldToCanvas(centerWorld); const wholeToolOffset: CoreTypes.Point2 = [ canvasCoords[0] - centerCanvas[0], canvasCoords[1] - centerCanvas[1], ]; // 开始拖拽整个工具 this.editData = { annotation: customAnn, viewportIdsToRender, handleIndex: -1, // -1表示拖拽整个工具 hasMoved: false, wholeToolOffset: wholeToolOffset, }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } } // 如果没有点击在工具上,取消所有选中状态 for (const ann of annotations) { const customAnn = ann as LineGrayscaleAnnotation; customAnn.isSelected = false; } const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); }; _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.editData) { return; } const eventDetail = evt.detail; const { currentPoints } = eventDetail; const canvasCoords = currentPoints.canvas; const enabledElement = getEnabledElement(eventDetail.element); if (!enabledElement) { return; } const { viewport } = enabledElement; const { annotation: ann, viewportIdsToRender } = this.editData; const customAnn = ann as LineGrayscaleAnnotation; const { data } = customAnn; // 1. 处理文本框拖拽 if (this.editData.textBoxBeingMoved && this.editData.textBoxOffset) { const newTextBoxPosition: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.textBoxOffset[0], canvasCoords[1] - this.editData.textBoxOffset[1], ]; data.textBox = newTextBoxPosition; this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } // 2. 处理整个工具的平移 if (this.editData.handleIndex === -1 && this.editData.wholeToolOffset) { // 计算新的中心位置 const newCenterCanvas: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.wholeToolOffset[0], canvasCoords[1] - this.editData.wholeToolOffset[1], ]; const newCenterWorld = viewport.canvasToWorld(newCenterCanvas); // 计算当前中心位置 const points = data.handles.points; const currentCenterWorld: CoreTypes.Point3 = [ (points[0][0] + points[1][0]) / 2, (points[0][1] + points[1][1]) / 2, (points[0][2] + points[1][2]) / 2, ]; // 计算偏移量 const worldOffset: CoreTypes.Point3 = [ newCenterWorld[0] - currentCenterWorld[0], newCenterWorld[1] - currentCenterWorld[1], 0, ]; // 应用偏移到所有点 for (let i = 0; i < data.handles.points.length; i++) { data.handles.points[i] = [ data.handles.points[i][0] + worldOffset[0], data.handles.points[i][1] + worldOffset[1], data.handles.points[i][2], ] as CoreTypes.Point3; } this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } // 3. 处理手柄拖拽 const worldPos = currentPoints.world; const activeHandleIndex = data.handles.activeHandleIndex; if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) { data.handles.points[activeHandleIndex] = worldPos; this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); } }; _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.editData) { return; } const { annotation: ann, hasMoved } = this.editData; const customAnn = ann as LineGrayscaleAnnotation; customAnn.data.handles.activeHandleIndex = null; customAnn.highlighted = false; const eventDetail = evt.detail; const { element } = eventDetail; const enabledElement = getEnabledElement(element); // 如果工具被移动了,更新缓存的统计数据 if (hasMoved && enabledElement && !this.editData.textBoxBeingMoved) { this._updateCachedStats(customAnn, enabledElement); } const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); this.editData = null; evt.preventDefault(); evt.stopPropagation(); }; _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => { const eventDetail = evt.detail; const { element, currentPoints } = eventDetail; if (!currentPoints || !currentPoints.canvas) { return; } const canvasCoords = currentPoints.canvas; const enabledElement = getEnabledElement(element); if (!enabledElement) { return; } const { viewport } = enabledElement; const annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations || annotations.length === 0) { element.style.cursor = 'default'; return; } let isHovering = false; let cursorSet = false; // 检查是否悬停在文本、手柄或直线上 for (const ann of annotations) { const customAnn = ann as LineGrayscaleAnnotation; // 1. 检查是否悬停在文本框上 // 使用默认位置(第二个端点)如果 textBox 未定义 const textBoxCanvas = customAnn.data.textBox || viewport.worldToCanvas(customAnn.data.handles.points[1]); if (this._isPointNearTextBox(canvasCoords, textBoxCanvas)) { element.style.cursor = 'pointer'; customAnn.highlighted = true; isHovering = true; cursorSet = true; break; } // 2. 检查是否靠近手柄 const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6); if (handle) { element.style.cursor = 'crosshair'; customAnn.highlighted = true; isHovering = true; cursorSet = true; break; } // 3. 检查是否靠近直线 if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { element.style.cursor = 'crosshair'; customAnn.highlighted = true; isHovering = true; cursorSet = true; break; } } // 如果没有悬停,重置高亮和光标 if (!isHovering) { for (const ann of annotations) { const customAnn = ann as LineGrayscaleAnnotation; if (!customAnn.isSelected) { customAnn.highlighted = false; } } element.style.cursor = 'default'; } // 触发渲染以更新高亮状态 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); }; _keyDownCallback = (evt: KeyboardEvent): void => { if (evt.key === 'Delete') { const element = document.activeElement as HTMLDivElement; if (!element) return; const annotations = annotation.state.getAnnotations(this.getToolName(), element); // 查找选中的注解 const selectedAnnotation = annotations.find(ann => ann.isSelected); if (selectedAnnotation) { // 删除选中的注解 annotation.state.removeAnnotation(selectedAnnotation.annotationUID || ''); // 触发视图更新 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); evt.preventDefault(); evt.stopPropagation(); } } }; handleSelectedCallback( evt: EventTypes.InteractionEventType, annotation: LineGrayscaleAnnotation ): void { const eventDetail = evt.detail; const { element } = eventDetail; annotation.highlighted = true; const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); } toolSelectedCallback( evt: EventTypes.InteractionEventType, annotation: LineGrayscaleAnnotation ): void { // 实现工具选中逻辑 } _dragCallback = (evt: EventTypes.InteractionEventType): void => { this.isDrawing = true; // 实现拖拽逻辑 }; _endCallback = (evt: EventTypes.InteractionEventType): void => { // 实现结束逻辑 }; getHandleNearImagePoint( element: HTMLDivElement, annotation: LineGrayscaleAnnotation, canvasCoords: CoreTypes.Point2, proximity: number ): Types.ToolHandle | undefined { const enabledElement = getEnabledElement(element); if (!enabledElement) { return undefined; } const { viewport } = enabledElement; const points = annotation.data.handles.points; const handleProximity = Math.max(proximity, 15); for (let i = 0; i < points.length; i++) { const point = points[i]; const canvasPoint = viewport.worldToCanvas(point); const distance = Math.sqrt( Math.pow(canvasPoint[0] - canvasCoords[0], 2) + Math.pow(canvasPoint[1] - canvasCoords[1], 2) ); if (distance < handleProximity) { annotation.data.handles.activeHandleIndex = i; return { worldPosition: point, } as Types.ToolHandle; } } annotation.data.handles.activeHandleIndex = null; return undefined; } /** * 检查点是否靠近文本框 */ private _isPointNearTextBox( point: CoreTypes.Point2, textBoxPosition: CoreTypes.Point2, padding: number = 10 ): boolean { // 简单的矩形碰撞检测,假设文本框大小约为 150x80 const textBoxWidth = 150; const textBoxHeight = 80; return ( point[0] >= textBoxPosition[0] - padding && point[0] <= textBoxPosition[0] + textBoxWidth + padding && point[1] >= textBoxPosition[1] - padding && point[1] <= textBoxPosition[1] + textBoxHeight + padding ); } /** * 更新缓存的统计数据 */ private _updateCachedStats( annotation: LineGrayscaleAnnotation, enabledElement: CoreTypes.IEnabledElement ): void { const { viewport } = enabledElement; const { points } = annotation.data.handles; if (points.length < 2) { return; } // 使用Cornerstone缓存系统获取图像 const imageId = viewport.getCurrentImageId?.(); if (!imageId) { console.warn('[LineGrayscaleTool] No imageId available'); return; } const image = cornerstone.cache.getImage(imageId); if (!image) { console.warn('[LineGrayscaleTool] Image not found in cache'); return; } // 采样像素 const sampleResult = this._samplePixelsAlongLine(points[0], points[1], image, viewport); // 计算统计值 const stats = this._calculateGrayscaleStats(sampleResult.values, points[0], points[1]); // 更新缓存 const targetId = `imageId:${imageId}`; if (!annotation.data.cachedStats) { annotation.data.cachedStats = {}; } annotation.data.cachedStats[targetId] = stats; } /** * 沿直线采样像素(Bresenham算法) */ private _samplePixelsAlongLine( startWorld: CoreTypes.Point3, endWorld: CoreTypes.Point3, image: any, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport ): PixelSampleResult { // 使用正确的API获取像素数据 const pixelData = image.getPixelData(); const { width, height } = image; // 使用viewport坐标转换(世界坐标→Canvas坐标) const startCanvas = viewport.worldToCanvas(startWorld); const endCanvas = viewport.worldToCanvas(endWorld); // Canvas坐标直接对应像素坐标(对于2D视图) const startPixel = [ Math.floor(startCanvas[0]), Math.floor(startCanvas[1]), ]; const endPixel = [ Math.floor(endCanvas[0]), Math.floor(endCanvas[1]), ]; // Bresenham直线算法 const pixels = this._bresenhamLine( startPixel[0], startPixel[1], endPixel[0], endPixel[1] ); // 采样灰度值 const values: number[] = []; const coordinates: Array<{ x: number; y: number }> = []; for (const pixel of pixels) { const { x, y } = pixel; // 边界检查 if (x < 0 || x >= width || y < 0 || y >= height) { continue; } // 计算像素索引(行优先) const index = y * width + x; const value = pixelData[index]; values.push(value); coordinates.push({ x, y }); } return { values, coordinates, count: values.length, }; } /** * Bresenham直线算法实现 */ private _bresenhamLine( x0: number, y0: number, x1: number, y1: number ): Array<{ x: number; y: number }> { const pixels: Array<{ x: number; y: number }> = []; const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; let x = x0; let y = y0; while (true) { pixels.push({ x, y }); if (x === x1 && y === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } return pixels; } /** * 计算灰度统计值 */ private _calculateGrayscaleStats( values: number[], startWorld: CoreTypes.Point3, endWorld: CoreTypes.Point3 ): { mean: number; min: number; max: number; sampleCount: number; lineLength: number; } { if (values.length === 0) { return { mean: 0, min: 0, max: 0, sampleCount: 0, lineLength: 0, }; } // 计算平均值 const sum = values.reduce((acc, val) => acc + val, 0); const mean = Math.round(sum / values.length); // 计算最小值和最大值 const min = Math.min(...values); const max = Math.max(...values); // 计算物理距离(mm) const dx = endWorld[0] - startWorld[0]; const dy = endWorld[1] - startWorld[1]; const dz = endWorld[2] - startWorld[2]; const lineLength = Math.sqrt(dx * dx + dy * dy + dz * dz); return { mean, min, max, sampleCount: values.length, lineLength: parseFloat(lineLength.toFixed(2)), }; } /** * 渲染注解 */ renderAnnotation = ( enabledElement: CoreTypes.IEnabledElement, svgDrawingHelper: SVGDrawingHelper ): boolean => { const { viewport } = enabledElement; const { element } = viewport; let annotations = annotation.state.getAnnotations( LineGrayscaleMeasurementTool.toolName, element ) as LineGrayscaleAnnotation[]; if (!annotations || annotations.length === 0) { return false; } const targetId = `imageId:${(viewport as any).getCurrentImageId?.() || ''}`; const styleSpecifier: any = { toolGroupId: this.toolGroupId, toolName: this.getToolName(), viewportId: viewport.id, }; for (const annotationItem of annotations) { const { annotationUID, data } = annotationItem; const { points } = data.handles; const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p)); if (!annotationUID) { continue; } styleSpecifier.annotationUID = annotationUID; const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotationItem); const lineDash = this.getStyle('lineDash', styleSpecifier, annotationItem); const color = this.getStyle('color', styleSpecifier, annotationItem); // 绘制直线 const lineUID = `${annotationUID}-line`; drawLineSvg( svgDrawingHelper, annotationUID, lineUID, canvasCoordinates[0], canvasCoordinates[1], { color, width: lineWidth, lineDash, } ); // 绘制手柄 - 选中时使用更大的半径 const handleGroupUID = `${annotationUID}-handles`; const handleRadius = annotationItem.isSelected ? 12 : 6; drawHandles( svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, { color, handleRadius, } ); // 绘制统计文本 const stats = data.cachedStats?.[targetId]; if (stats) { const textLines = [ `平均: ${stats.mean}`, `最小: ${stats.min}`, `最大: ${stats.max}`, `长度: ${stats.lineLength.toFixed(2)}mm`, ]; const textBoxPosition = data.textBox || canvasCoordinates[1]; const textBoxUID = `${annotationUID}-text`; drawLinkedTextBox( svgDrawingHelper, annotationUID, textBoxUID, textLines, textBoxPosition, [canvasCoordinates[1]], {}, { color, } ); } } return true; }; }