import { Point3, Point2, fromPoint3ToPoint2, decomposeVectorOnAxes } from './mathUtils'; import { utilities as csUtils, Types as CoreTypes, getEnabledElement, } from '@cornerstonejs/core'; import { AnnotationTool, utilities, Types, annotation, drawing, } from '@cornerstonejs/tools'; const { drawHandles, drawLinkedTextBox, drawLine: drawLineSvg, drawCircle: drawCircleSvg, drawPath: drawPathSvg, } = drawing; import { calculateMidpoint, vectorSubtract, vectorAdd, vectorScale, getAngleBetweenLines, degrees, getLineIntersection, fromPoint2ToPoint3, } from './mathUtils'; import { PublicToolProps, ToolProps, EventTypes, SVGDrawingHelper, } from '@cornerstonejs/tools/dist/esm/types'; // 十字准星光标 const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair'; // 类型定义 type PolygonLengthMeasurementAnnotation = Types.Annotation & { data: { handles: { points: CoreTypes.Point3[]; // 多边形的所有顶点(世界坐标) activeHandleIndex: number | null; // 当前激活的手柄索引 textBox?: CoreTypes.Point2; // 文本框位置 }; cachedStats?: { [targetId: string]: { totalLength: number; // 总周长(像素) totalLengthMm: number; // 总周长(mm) segmentLengths: number[]; // 各边长度(包括闭合边) }; }; }; }; // 导出的注解数据接口 export interface ExportedAnnotationData { points: CoreTypes.Point3[]; // 多边形的所有顶点 totalLengthMm: number; // 总周长(mm) metadata: { viewPlaneNormal: CoreTypes.Point3; viewUp: CoreTypes.Point3; FrameOfReferenceUID: string; referencedImageId: string; }; } export default class PolygonLengthMeasurementTool extends AnnotationTool { static toolName = 'PolygonLengthMeasurementTool'; editData: { annotation: Types.Annotation; viewportIdsToRender: string[]; handleIndex?: number; newAnnotation?: boolean; hasMoved?: boolean; textBoxBeingMoved?: boolean; // 是否正在拖拽文本框 textBoxOffset?: CoreTypes.Point2; // 文本框拖拽偏移量 movingWholeTool?: boolean; // 是否移动整个工具 wholeToolOffset?: CoreTypes.Point2; // 整体移动偏移量 } | null = null; currentAnnotation: PolygonLengthMeasurementAnnotation | null = null; // 当前正在绘制的注解 isDrawing: boolean = false; drawingElement: HTMLDivElement | null = null; // 当前绘制模式的元素 drawingCompleted: boolean = false; // 标记绘制是否已完成(双击后反激活) // 预览鼠标位置(canvas坐标) private previewMousePosition: CoreTypes.Point2 | null = null; constructor( toolProps: PublicToolProps = {}, defaultToolProps: ToolProps = { supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { shadow: true, preventHandleOutsideImage: false, }, } ) { super(toolProps, defaultToolProps); } /** * 创建默认注解(用于预设模式) */ static createDefaultAnnotation( element: HTMLDivElement, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport, points: CoreTypes.Point3[] = [] ): PolygonLengthMeasurementAnnotation { const enabledElement = getEnabledElement(element); if (!enabledElement) { throw new Error('Element is not enabled'); } const camera = viewport.getCamera(); const { viewPlaneNormal, viewUp } = camera; if (viewPlaneNormal === undefined) { throw new Error('viewPlaneNormal is undefined'); } if (viewUp === undefined) { throw new Error('viewUp is 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: PolygonLengthMeasurementTool.toolName, }, data: { label: '', handles: { points: points, activeHandleIndex: null, }, cachedStats: {}, }, } as PolygonLengthMeasurementAnnotation; return annotationData; } /** * 导出注解数据 */ static exportAnnotationData( annotation: PolygonLengthMeasurementAnnotation, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport ): ExportedAnnotationData | null { const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`; const cachedStats = annotation.data.cachedStats?.[targetId]; if (!cachedStats) { return null; } return { points: [...annotation.data.handles.points], totalLengthMm: cachedStats.totalLengthMm, metadata: { viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1], viewUp: annotation.metadata?.viewUp || [0, 1, 0], FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '', referencedImageId: annotation.metadata?.referencedImageId || '', }, }; } /** * 添加新注解 - 创建空的闭合多边形注解用于绘制模式 */ addNewAnnotation( evt: EventTypes.InteractionEventType ): PolygonLengthMeasurementAnnotation { const eventDetail = evt.detail; const { element, currentPoints } = eventDetail; const worldPos = currentPoints.world; const enabledElement = getEnabledElement(element); if (!enabledElement) { throw new Error('Element is not enabled'); } const camera = enabledElement.viewport.getCamera(); const { viewPlaneNormal, viewUp } = camera; if (viewPlaneNormal === undefined) { throw new Error('viewPlaneNormal is undefined'); } if (viewUp === undefined) { throw new Error('viewUp is undefined'); } // 创建新的空注解 const annotationData = { invalidated: true, highlighted: false, metadata: { viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3, viewUp: [...viewUp] as CoreTypes.Point3, FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(), referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '', toolName: PolygonLengthMeasurementTool.toolName, }, data: { label: '', handles: { points: [worldPos], // 添加第一个点 activeHandleIndex: 0, }, cachedStats: {}, }, } as PolygonLengthMeasurementAnnotation; // 设置编辑数据 this.editData = { annotation: annotationData, viewportIdsToRender: [enabledElement.viewportId], newAnnotation: true, hasMoved: false, }; // 激活绘制模式 this._activateDraw(element); return annotationData; } /** * 取消操作 */ cancel(element: HTMLDivElement): string { if (this.isDrawing) { this.isDrawing = false; this._deactivateModify(element); const enabledElement = getEnabledElement(element); if (enabledElement) { const { renderingEngine } = enabledElement; const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); } this.editData = null; return this.getToolName(); } return ''; } /** * 处理 控制点/端点 选中回调 */ handleSelectedCallback( evt: EventTypes.InteractionEventType, annotation: PolygonLengthMeasurementAnnotation ): void { // 实现控制点选中逻辑 annotation.isSelected = true; annotation.highlighted = true; const { element } = evt.detail; const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); } /** * 工具选中回调 - 当工具被激活时调用 */ toolSelectedCallback( evt: EventTypes.InteractionEventType, annotation: PolygonLengthMeasurementAnnotation ): void { const { element } = evt.detail; // 如果 annotation 不为空,说明是点击已有的注释,激活修改模式 if (annotation) { console.log('PolygonLengthMeasurementTool selected existing annotation'); this._activateModify(element); return; } // 工具被激活时,总是进入绘制模式,允许开始新的绘制 // 完全重置所有绘制状态,确保全新的开始 this._deactivateDraw(element); // 先清理之前的绘制状态 this._deactivateModify(element); // 清理修改模式 this.isDrawing = false; // 重置绘制状态 this.drawingCompleted = false; // 允许新的点击操作 this.currentAnnotation = null; this.drawingElement = null; // 激活绘制模式,等待用户开始绘制 this._activateDraw(element); console.log('PolygonLengthMeasurementTool activated for new drawing'); } /** * 激活时被调用 */ onSetToolActive(): void { // 示例:重置相关状态 this.isDrawing = false; this.editData = null; this.drawingCompleted = false;//允许开始新的注解 // 示例:添加工具特定的初始化 // this.initializeCustomState(); } /** * 检查点是否靠近工具 */ isPointNearTool( element: HTMLDivElement, annotation: PolygonLengthMeasurementAnnotation, canvasCoords: CoreTypes.Point2, proximity: number ): boolean { const enabledElement = getEnabledElement(element); if (!enabledElement) { return false; } const { viewport } = enabledElement; const points = annotation.data.handles.points; // 检查是否靠近文本框 const textBox = annotation.data.handles.textBox; if (textBox && Array.isArray(textBox) && textBox.length >= 2) { if (this._isPointInTextBox(canvasCoords, textBox as CoreTypes.Point2)) { return true; } } // 检查是否靠近任意一个手柄点 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) { for (let i = 0; i < points.length - 1; i++) { const p1Canvas = viewport.worldToCanvas(points[i]); const p2Canvas = viewport.worldToCanvas(points[i + 1]); const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas); if (dist < proximity) { return true; } } } // 检查是否靠近闭合线段 if (points.length >= 3) { if (this._isPointNearClosingSegment(canvasCoords, points, viewport, proximity)) { return true; } } return false; } /** * 检查是否靠近闭合线段 */ private _isPointNearClosingSegment( canvasCoords: CoreTypes.Point2, points: CoreTypes.Point3[], viewport: CoreTypes.IViewport, proximity: number ): boolean { if (points.length < 3) return false; const lastPoint = points[points.length - 1]; const firstPoint = points[0]; const lastCanvas = viewport.worldToCanvas(lastPoint); const firstCanvas = viewport.worldToCanvas(firstPoint); const dist = this._distanceToSegment(canvasCoords, lastCanvas, firstCanvas); return dist < proximity; } /** * 检查点击是否在文本框内 */ private _isPointInTextBox( canvasPoint: CoreTypes.Point2, textBoxPosition: CoreTypes.Point2, textWidth: number = 100, textHeight: number = 40 ): boolean { // 确保参数有效 if (!Array.isArray(canvasPoint) || canvasPoint.length < 2 || !Array.isArray(textBoxPosition) || textBoxPosition.length < 2) { return false; } const [x, y] = canvasPoint; const [tx, ty] = textBoxPosition; const margin = 10; return ( x >= tx - margin && x <= tx + textWidth + margin && y >= ty - margin && y <= ty + textHeight + margin ); } /** * 计算点到线段的距离 */ 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); } /** * 计算两点间距离(像素) */ private _calculateDistance(point1: CoreTypes.Point3, point2: CoreTypes.Point3, viewport: CoreTypes.IViewport): number { const canvas1 = viewport.worldToCanvas(point1); const canvas2 = viewport.worldToCanvas(point2); return Math.sqrt( Math.pow(canvas2[0] - canvas1[0], 2) + Math.pow(canvas2[1] - canvas1[1], 2) ); } /** * 将像素距离转换为mm */ private _convertPixelsToMm(pixels: number, viewport: CoreTypes.IViewport): number { // 这里需要根据DICOM元数据获取像素间距 // 暂时使用固定值,实际应该从图像元数据中获取 const pixelSpacing = 0.5; // mm per pixel,示例值 return pixels * pixelSpacing; } /** * 更新缓存的统计数据(包含闭合边计算) */ _updateCachedStats( annotation: PolygonLengthMeasurementAnnotation, enabledElement: CoreTypes.IEnabledElement ): void { const { viewport } = enabledElement; const { data } = annotation; const points = data.handles.points; const targetId = this.getTargetId(viewport); if (!targetId) { return; } if (!data.cachedStats) { data.cachedStats = {}; } if (!data.cachedStats[targetId]) { data.cachedStats[targetId] = { totalLength: 0, totalLengthMm: 0, segmentLengths: [] }; } const stats = data.cachedStats[targetId]; stats.segmentLengths = []; let totalPixels = 0; // 计算所有相邻顶点间的边长 for (let i = 0; i < points.length - 1; i++) { const segmentLength = this._calculateDistance(points[i], points[i + 1], viewport); stats.segmentLengths.push(segmentLength); totalPixels += segmentLength; } // 计算闭合边(最后一个点到第一个点) if (points.length >= 3) { const closingSegmentLength = this._calculateDistance(points[points.length - 1], points[0], viewport); stats.segmentLengths.push(closingSegmentLength); totalPixels += closingSegmentLength; } stats.totalLength = totalPixels; stats.totalLengthMm = this._convertPixelsToMm(totalPixels, viewport); } /** * 激活绘制模式 */ _activateDraw(element: HTMLDivElement): void { this.drawingElement = element; // 存储当前绘制模式的元素 this.isDrawing = true; element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_CLICK', this._mouseClickDrawCallback as EventListener ); element.addEventListener( 'dblclick', this._mouseDoubleClickDrawCallback as EventListener ); // 添加鼠标移动监听用于预览 element.addEventListener( 'CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveDrawCallback as EventListener ); } /** * 取消激活绘制模式 */ _deactivateDraw(element?: HTMLDivElement): void { const targetElement = element || this.drawingElement; if (!targetElement) return; this.isDrawing = false; targetElement.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_CLICK', this._mouseClickDrawCallback as EventListener ); targetElement.removeEventListener( 'dblclick', this._mouseDoubleClickDrawCallback as EventListener ); // 清理鼠标移动监听器 targetElement.removeEventListener( 'CORNERSTONE_TOOLS_MOUSE_MOVE', this._mouseMoveDrawCallback as EventListener ); this.drawingElement = null; // 清理 this.previewMousePosition = null; // 清理预览位置 } /** * 激活修改模式 */ _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 ); } /** * 鼠标点击回调 - 用于绘制模式,添加新的点 */ _mouseClickDrawCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.isDrawing || this.drawingCompleted) { return; } const eventDetail = evt.detail; const { element, currentPoints } = eventDetail; const worldPos = currentPoints.world; const enabledElement = getEnabledElement(element); if (!enabledElement) { return; } // 如果还没有当前注解,创建第一个 if (!this.currentAnnotation) { // 创建新的注解 const camera = enabledElement.viewport.getCamera(); const { viewPlaneNormal, viewUp } = camera; if (viewPlaneNormal === undefined || viewUp === undefined) { return; } this.currentAnnotation = { invalidated: true, highlighted: false, metadata: { viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3, viewUp: [...viewUp] as CoreTypes.Point3, FrameOfReferenceUID: enabledElement.viewport.getFrameOfReferenceUID(), referencedImageId: enabledElement.viewport.getCurrentImageId?.() || '', toolName: PolygonLengthMeasurementTool.toolName, }, data: { label: '', handles: { points: [worldPos], activeHandleIndex: 0, }, cachedStats: {}, }, } as PolygonLengthMeasurementAnnotation; // 添加到临时状态以便渲染 annotation.state.addAnnotation(this.currentAnnotation, element); console.log('Created first point for polygon'); } else { // 添加新点到现有注解 this.currentAnnotation.data.handles.points.push(worldPos); this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1; // 更新统计数据 this._updateCachedStats(this.currentAnnotation, enabledElement); console.log(`Added point ${this.currentAnnotation.data.handles.points.length} to polygon`); } // 触发渲染更新 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); evt.preventDefault(); evt.stopPropagation(); }; /** * 鼠标移动回调 - 用于绘制模式,更新预览位置 */ _mouseMoveDrawCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.isDrawing) { return; } const eventDetail = evt.detail; const canvasCoords = eventDetail.currentPoints?.canvas; if (canvasCoords) { this.previewMousePosition = [...canvasCoords]; // 触发重新渲染以显示预览线 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( eventDetail.element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); } }; /** * 鼠标双击回调 - 用于绘制模式,在双击位置添加最后一个点并结束绘制 */ _mouseDoubleClickDrawCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.isDrawing || !this.currentAnnotation) { return; } const eventDetail = evt.detail; const element = eventDetail.element || this.drawingElement; const currentPoints = eventDetail.currentPoints; if (!element) { return; } // 获取双击位置,支持多种情况 let worldPos: CoreTypes.Point3 | undefined; if (currentPoints?.world) { // 优先使用 world 坐标 worldPos = currentPoints.world; } else if (currentPoints?.canvas) { // 如果没有 world 坐标但有 canvas 坐标,转换得到 world 坐标 const enabledElement = getEnabledElement(element); if (enabledElement) { worldPos = enabledElement.viewport.canvasToWorld(currentPoints.canvas); } } else { // 如果 CornerstoneJS 没有提供坐标,使用原生 DOM 事件坐标 // __类型转换__:将 `evt` 转换为 `MouseEvent` const mouseEvent = evt as unknown as MouseEvent; // __获取鼠标坐标__:使用 `clientX`, `clientY` const clientX = mouseEvent.clientX; const clientY = mouseEvent.clientY; // __计算 canvas 坐标__:减去 canvas 的偏移量 const rect = element.getBoundingClientRect(); const canvasX = clientX - rect.left; const canvasY = clientY - rect.top; const canvasPos: CoreTypes.Point2 = [canvasX, canvasY]; // __转换为 world 坐标__:使用 `viewport.canvasToWorld()` const enabledElement = getEnabledElement(element); if (enabledElement) { worldPos = enabledElement.viewport.canvasToWorld(canvasPos); } } if (!worldPos) { // 如果仍然没有位置信息,记录警告但不中断流程 console.warn('Unable to get double-click position for polygon'); return; } // 先在双击位置添加最后一个点 this.currentAnnotation.data.handles.points.push(worldPos); this.currentAnnotation.data.handles.activeHandleIndex = this.currentAnnotation.data.handles.points.length - 1; // 更新统计数据 const enabledElement = getEnabledElement(element); if (enabledElement) { this._updateCachedStats(this.currentAnnotation, enabledElement); } // 然后检查点数决定是否结束绘制 if (this.currentAnnotation.data.handles.points.length >= 3) { // 结束绘制模式,激活修改模式 this._deactivateDraw(element); this._activateModify(element); this.isDrawing = false; this.drawingCompleted = true; // 标记绘制已完成,防止再次添加点 this.currentAnnotation = null; // 完全清理当前注解引用,确保下次激活时全新的开始 // 取消所有注解的选中状态,确保下次激活时 annotation 参数为 null const annotations = annotation.state.getAnnotations(this.getToolName(), element); if (annotations) { for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; customAnn.isSelected = false; customAnn.highlighted = false; } } console.log('Finished drawing polygon'); } else { // 如果点数仍然少于3个,删除这个注解 annotation.state.removeAnnotation(this.currentAnnotation.annotationUID!); this.currentAnnotation = null; } // 触发渲染更新 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); evt.preventDefault(); evt.stopPropagation(); }; /** * 键盘按下回调 - 用于删除功能 */ _keyDownCallback = (evt: KeyboardEvent): void => { const element = evt.currentTarget as HTMLDivElement; if (evt.key === 'Delete' || evt.key === 'Backspace') { const enabledElement = getEnabledElement(element); if (!enabledElement) { return; } const annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations || annotations.length === 0) { return; } const selectedAnnotations = annotations.filter( (ann) => (ann as PolygonLengthMeasurementAnnotation).isSelected ); if (selectedAnnotations.length === 0) { return; } for (const ann of selectedAnnotations) { annotation.state.removeAnnotation(ann.annotationUID!); } const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); 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; // 检查是否悬停在文本框上 for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; const textBoxPosition = customAnn.data.handles.textBox; if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) { element.style.cursor = 'pointer'; customAnn.highlighted = true; isHovering = true; break; } } // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上 if (!isHovering) { for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; // 检查是否靠近手柄 const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6); if (handle) { element.style.cursor = cursorUrl; customAnn.highlighted = true; isHovering = true; break; } // 检查是否靠近线段(包括闭合线段) if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { element.style.cursor = cursorUrl; customAnn.highlighted = true; isHovering = true; break; } } } // 如果没有悬停在任何地方,重置高亮 if (!isHovering) { for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; if (!customAnn.isSelected) { customAnn.highlighted = false; } } element.style.cursor = 'default'; } const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); }; /** * 鼠标按下回调 - 用于修改模式 */ _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 PolygonLengthMeasurementAnnotation; const textBoxPosition = customAnn.data.handles.textBox; if (textBoxPosition && this._isPointInTextBox(canvasCoords, textBoxPosition)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); const textBoxOffset: CoreTypes.Point2 = [ canvasCoords[0] - textBoxPosition[0], canvasCoords[1] - textBoxPosition[1], ]; this.editData = { annotation: customAnn, viewportIdsToRender, hasMoved: false, textBoxBeingMoved: true, textBoxOffset: textBoxOffset, }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); evt.preventDefault(); evt.stopPropagation(); return; } } // 如果没有点击文本框,再查找最近的手柄 for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; 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, }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } } // 如果没有找到手柄,检查是否点击在工具的其他位置(线段上,包括闭合线段) for (const ann of annotations) { const customAnn = ann as PolygonLengthMeasurementAnnotation; if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); const points = customAnn.data.handles.points; if (points.length >= 2) { const midPoint = calculateMidpoint(points[0], points[1]); const midCanvas = viewport.worldToCanvas(midPoint); const wholeToolOffset: CoreTypes.Point2 = [ canvasCoords[0] - midCanvas[0], canvasCoords[1] - midCanvas[1], ]; this.editData = { annotation: customAnn, viewportIdsToRender, hasMoved: false, movingWholeTool: true, 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 PolygonLengthMeasurementAnnotation; customAnn.isSelected = false; customAnn.highlighted = 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 { annotation: ann, viewportIdsToRender, textBoxBeingMoved, movingWholeTool } = this.editData; const customAnn = ann as PolygonLengthMeasurementAnnotation; const { data } = customAnn; // 如果正在拖拽文本框 if (textBoxBeingMoved && this.editData.textBoxOffset) { const newTextBoxPosition: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.textBoxOffset[0], canvasCoords[1] - this.editData.textBoxOffset[1], ]; data.handles.textBox = newTextBoxPosition; this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } // 如果正在移动整个工具 if (movingWholeTool && this.editData.wholeToolOffset) { const newMidCanvas: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.wholeToolOffset[0], canvasCoords[1] - this.editData.wholeToolOffset[1], ]; const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas); const points = data.handles.points; if (points.length >= 2) { const oldMid = calculateMidpoint(points[0], points[1]); const oldMidCanvas = enabledElement.viewport.worldToCanvas(oldMid); const offset = vectorSubtract(newMidWorld, oldMid); const canvasOffset: CoreTypes.Point2 = [ newMidCanvas[0] - oldMidCanvas[0], newMidCanvas[1] - oldMidCanvas[1], ]; // 移动所有点 for (let i = 0; i < points.length; i++) { points[i] = vectorAdd(points[i], offset); } // 移动文本框位置 (使用canvas坐标偏移) if (data.handles.textBox && Array.isArray(data.handles.textBox) && data.handles.textBox.length >= 2) { data.handles.textBox[0] += canvasOffset[0]; data.handles.textBox[1] += canvasOffset[1]; } this._updateCachedStats(customAnn, enabledElement); this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } } // 否则处理手柄拖拽 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._updateCachedStats(customAnn, enabledElement); this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); } }; /** * 鼠标释放回调 - 用于修改模式 */ _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => { if (!this.editData) { return; } const { annotation: ann, hasMoved, movingWholeTool } = this.editData; const customAnn = ann as PolygonLengthMeasurementAnnotation; customAnn.data.handles.activeHandleIndex = null; if (!hasMoved && movingWholeTool) { customAnn.isSelected = true; customAnn.highlighted = true; } const eventDetail = evt.detail; const { element } = eventDetail; const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); this.editData = null; evt.preventDefault(); evt.stopPropagation(); }; /** * 获取靠近图像点的手柄 */ getHandleNearImagePoint( element: HTMLDivElement, annotation: PolygonLengthMeasurementAnnotation, 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; } /** * 渲染注解 */ renderAnnotation = ( enabledElement: CoreTypes.IEnabledElement, svgDrawingHelper: SVGDrawingHelper ): boolean => { let renderStatus = false; const { viewport } = enabledElement; const { element } = viewport; let annotations = annotation.state.getAnnotations(this.getToolName(), element); if (!annotations?.length) { return renderStatus; } for (let i = 0; i < annotations.length; i++) { const annotation = annotations[i] as PolygonLengthMeasurementAnnotation; const { annotationUID, data } = annotation; const points = data.handles.points; // 如果点数为0,跳过 if (points.length === 0) { continue; } const targetId = this.getTargetId(viewport); const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined; // 转换所有点为 canvas 坐标 const canvasPoints = points.map((p) => viewport.worldToCanvas(p)); const lineOptions = { color: 'rgb(0, 255, 0)', width: 2, }; // 绘制多边形线段(需要至少2个点) if (annotationUID && points.length >= 2) { for (let j = 0; j < canvasPoints.length - 1; j++) { const lineUID = `${annotationUID}-line-${j}`; drawLineSvg( svgDrawingHelper, annotationUID, lineUID, canvasPoints[j], canvasPoints[j + 1], lineOptions ); } // 绘制闭合线段(从最后一个点到第一个点) if (points.length >= 3) { const closingLineUID = `${annotationUID}-closing-line`; drawLineSvg( svgDrawingHelper, annotationUID, closingLineUID, canvasPoints[canvasPoints.length - 1], canvasPoints[0], lineOptions ); } } // 绘制周长文本(需要至少3个点且有统计数据) if (cachedStats?.totalLengthMm !== undefined && annotationUID && points.length >= 3) { const textLines = [`周长: ${cachedStats.totalLengthMm.toFixed(2)} mm`]; const textUID = `${annotationUID}-length-text`; const textBoxPosition: CoreTypes.Point2 = (data.handles.textBox && Array.isArray(data.handles.textBox) && data.handles.textBox.length >= 2) ? data.handles.textBox : [ canvasPoints[0][0] + 10, canvasPoints[0][1] - 10, ]; console.log('textBoxPosition:', textBoxPosition); // 保存文本框位置到注解数据中 data.handles.textBox = textBoxPosition; drawLinkedTextBox( svgDrawingHelper, annotationUID, textUID, textLines, textBoxPosition, [canvasPoints[0] as CoreTypes.Point2], {}, { color: 'rgb(0, 255, 0)', } ); } // 绘制手柄点(至少有1个点就绘制) if (annotationUID && points.length >= 1) { const handleGroupUID = '0'; const isSelected = annotation.isSelected || annotation.highlighted; const handleRadius = isSelected ? 12 : 8; drawHandles( svgDrawingHelper, annotationUID, handleGroupUID, canvasPoints, { color: 'rgb(255, 255, 255)', handleRadius: handleRadius, } ); } renderStatus = true; } // 绘制预览线(只在绘制模式且有鼠标位置时) if (this.isDrawing && this.previewMousePosition && this.currentAnnotation && this.currentAnnotation.data.handles.points.length > 0) { const points = this.currentAnnotation.data.handles.points; const lastPoint = points[points.length - 1]; const lastCanvasPoint = enabledElement.viewport.worldToCanvas(lastPoint); // 总是绘制从最后一个点到鼠标位置的预览线(用于显示即将添加的下一个线段) const previewLineUID = 'preview-line-to-mouse'; drawLineSvg( svgDrawingHelper, 'preview', // 使用固定UID previewLineUID, lastCanvasPoint, this.previewMousePosition, { color: 'rgba(0, 255, 0, 0.7)', // 半透明绿色 width: 2, lineDash: [8, 4], // 虚线样式:8px实线,4px空白 } ); // 当至少有两个点时,额外绘制从鼠标位置到起始点的预览线(用于预览闭合形状) if (points.length >= 2) { const firstPoint = points[0]; const firstCanvasPoint = enabledElement.viewport.worldToCanvas(firstPoint); const closingPreviewLineUID = 'preview-line-closing'; drawLineSvg( svgDrawingHelper, 'preview', // 使用固定UID closingPreviewLineUID, this.previewMousePosition, firstCanvasPoint, { color: 'rgba(255, 255, 0, 0.7)', // 半透明黄色,用于区分 width: 2, lineDash: [8, 4], // 虚线样式:8px实线,4px空白 } ); } } return renderStatus; }; }