/** * 矩形区域灰度测量注解接口 */ interface RectangleGrayscaleAnnotation extends Types.Annotation { data: { handles: { // 矩形四个顶点(世界坐标) points: CoreTypes.Point3[]; // 当前激活的手柄索引(0-7,对应4个角点和4条边中点) activeHandleIndex: number | null; }; label?: string; // 文本框位置(Canvas坐标) textBox?: CoreTypes.Point2; // 缓存的统计结果 cachedStats?: { [targetId: string]: { // 平均灰度值 mean: number; // 最小灰度值 min: number; // 最大灰度值 max: number; // 采样像素数量 pixelCount: number; // 矩形面积 (mm²) area: number; // 矩形宽度 (mm) width: number; // 矩形高度 (mm) height: number; }; }; }; } /** * 像素采样结果 */ interface PixelSampleResult { values: number[]; coordinates: Array<{ x: number; y: number }>; boundingBox: { minX: number; maxX: number; minY: number; maxY: number }; totalPixels: number; } 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, drawRect: drawRectSvg, drawPath: drawPathSvg, } = drawing; import { PublicToolProps, ToolProps, EventTypes, SVGDrawingHelper, } from '@cornerstonejs/tools/dist/esm/types'; /** * 矩形区域灰度测量工具 * 功能: 在医学影像上绘制矩形区域,计算并显示区域内像素的灰度值统计信息 */ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool { static toolName = 'RectangleGrayscaleMeasurementTool'; 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 ): RectangleGrayscaleAnnotation { 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; const rectWidth = 100; const rectHeight = 80; // 设置矩形四个顶点(Canvas坐标) const topLeftCanvas: CoreTypes.Point2 = [centerX - rectWidth / 2, centerY - rectHeight / 2]; const topRightCanvas: CoreTypes.Point2 = [centerX + rectWidth / 2, centerY - rectHeight / 2]; const bottomRightCanvas: CoreTypes.Point2 = [centerX + rectWidth / 2, centerY + rectHeight / 2]; const bottomLeftCanvas: CoreTypes.Point2 = [centerX - rectWidth / 2, centerY + rectHeight / 2]; // 转换为世界坐标 const topLeftWorld = viewport.canvasToWorld(topLeftCanvas); const topRightWorld = viewport.canvasToWorld(topRightCanvas); const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas); const bottomLeftWorld = viewport.canvasToWorld(bottomLeftCanvas); 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: RectangleGrayscaleMeasurementTool.toolName, }, data: { label: '', handles: { points: [topLeftWorld, topRightWorld, bottomRightWorld, bottomLeftWorld], activeHandleIndex: null, }, cachedStats: {}, }, } as RectangleGrayscaleAnnotation; return annotationData; } constructor( toolProps: PublicToolProps = {}, defaultToolProps: ToolProps = { supportedInteractionTypes: ['Mouse', 'Touch'], configuration: { shadow: true, preventHandleOutsideImage: false, }, } ) { super(toolProps, defaultToolProps); } /** * 禁用手动绘制(仅支持预设注解) */ addNewAnnotation( evt: EventTypes.InteractionEventType ): RectangleGrayscaleAnnotation { evt.preventDefault(); return {} as RectangleGrayscaleAnnotation; } /** * 检查点是否靠近工具 */ isPointNearTool( element: HTMLDivElement, annotation: RectangleGrayscaleAnnotation, 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 >= 4) { return this._isPointInRectangle(canvasCoords, points, viewport); } return false; } /** * 检查点是否在矩形内 */ private _isPointInRectangle( point: CoreTypes.Point2, rectanglePoints: CoreTypes.Point3[], viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport ): boolean { // 将世界坐标转换为Canvas坐标 const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p)); // 使用射线投射算法判断点是否在多边形内 let inside = false; for (let i = 0, j = canvasPoints.length - 1; i < canvasPoints.length; j = i++) { if (((canvasPoints[i][1] > point[1]) !== (canvasPoints[j][1] > point[1])) && (point[0] < (canvasPoints[j][0] - canvasPoints[i][0]) * (point[1] - canvasPoints[i][1]) / (canvasPoints[j][1] - canvasPoints[i][1]) + canvasPoints[i][0])) { inside = !inside; } } return inside; } /** * 检查点是否在线段上(矩形的边) */ private _isPointNearLine( point: CoreTypes.Point2, rectanglePoints: CoreTypes.Point3[], viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport, proximity: number = 6 ): boolean { const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p)); // 检查四条边 for (let i = 0; i < canvasPoints.length; i++) { const start = canvasPoints[i]; const end = canvasPoints[(i + 1) % canvasPoints.length]; if (this._isPointNearLineSegment(point, start, end, proximity)) { return true; } } return false; } /** * 检查点是否靠近线段 */ private _isPointNearLineSegment( point: CoreTypes.Point2, lineStart: CoreTypes.Point2, lineEnd: CoreTypes.Point2, proximity: number ): boolean { const x0 = point[0]; const y0 = point[1]; const x1 = lineStart[0]; const y1 = lineStart[1]; const x2 = lineEnd[0]; const y2 = lineEnd[1]; const dx = x2 - x1; const dy = y2 - y1; const length = Math.sqrt(dx * dx + dy * dy); if (length === 0) { // 线段退化为点 const distance = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2)); return distance <= proximity; } // 计算点到线段的最短距离 const t = Math.max(0, Math.min(1, ((x0 - x1) * dx + (y0 - y1) * dy) / (length * length))); const projX = x1 + t * dx; const projY = y1 + t * dy; const distance = Math.sqrt(Math.pow(x0 - projX, 2) + Math.pow(y0 - projY, 2)); return distance <= proximity; } /** * 检查点是否在文本框上 */ private _isPointInTextBox( element: HTMLDivElement, annotation: RectangleGrayscaleAnnotation, canvasCoords: CoreTypes.Point2 ): boolean { const enabledElement = getEnabledElement(element); if (!enabledElement) { return false; } const { viewport } = enabledElement; const textBox = annotation.data.textBox; if (!textBox) { // 如果没有自定义位置,使用默认位置(右下角) const points = annotation.data.handles.points; if (points.length < 4) return false; const defaultPos = viewport.worldToCanvas(points[2]); return this._checkTextBoxBounds(canvasCoords, defaultPos); } return this._checkTextBoxBounds(canvasCoords, textBox); } /** * 检查点是否在文本框边界内(估算文本框大小) */ private _checkTextBoxBounds( point: CoreTypes.Point2, textBoxPos: CoreTypes.Point2 ): boolean { // 估算文本框大小(基于典型的文本行数和字体大小) const padding = 8; const lineHeight = 18; const textWidth = 150; // 估算宽度 const textHeight = lineHeight * 4 + padding * 2; // 4行文本 const x = textBoxPos[0]; const y = textBoxPos[1]; return ( point[0] >= x - padding && point[0] <= x + textWidth && point[1] >= y - padding && point[1] <= y + textHeight ); } /** * 取消操作 */ 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 ''; } /** * 处理工具选中 */ toolSelectedCallback( evt: EventTypes.InteractionEventType, annotation: RectangleGrayscaleAnnotation ): void { // 实现工具选中逻辑 } /** * 处理注解选中 */ handleSelectedCallback( evt: EventTypes.InteractionEventType, annotation: RectangleGrayscaleAnnotation ): void { const eventDetail = evt.detail; const { element } = eventDetail; annotation.highlighted = true; const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); } /** * 渲染注解 */ renderAnnotation = ( enabledElement: CoreTypes.IEnabledElement, svgDrawingHelper: SVGDrawingHelper ): boolean => { const { viewport } = enabledElement; const { element } = viewport; let annotations = annotation.state.getAnnotations( RectangleGrayscaleMeasurementTool.toolName, element ) as RectangleGrayscaleAnnotation[]; 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 color = this.getStyle('color', styleSpecifier, annotationItem); // 绘制矩形边框 - 使用路径绘制以保持正确的矩形形状 if (canvasCoordinates.length >= 4) { const rectUID = `${annotationUID}-rect`; // 创建封闭的矩形路径:从第一个点开始,顺时针遍历所有点,最后回到起点 const rectanglePath = [...canvasCoordinates, canvasCoordinates[0]]; // 复制第一个点作为结束点 drawPathSvg( svgDrawingHelper, annotationUID as string, rectUID, rectanglePath, { color: color as string, width: lineWidth as number, } ); } // 绘制手柄 - 根据选中和高亮状态调整大小 const handleGroupUID = `${annotationUID}-handles`; // 根据状态设置手柄半径 let handleRadius = 6; // 默认半径 if (annotationItem.isSelected) { handleRadius = 12; // 选中状态下半径变大 } else if (annotationItem.highlighted) { handleRadius = 9; // 悬停高亮时半径适中 } drawHandles( svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, { color, handleRadius, } ); // 绘制统计文本 const stats = data.cachedStats?.[targetId]; if (stats) { const textLines = [ `平均: ${stats.mean}`, `最小: ${stats.min}`, `最大: ${stats.max}`, `面积: ${stats.area.toFixed(2)}mm²`, ]; const textBoxPosition = data.textBox || canvasCoordinates[2]; // 右下角 const textBoxUID = `${annotationUID}-text`; drawLinkedTextBox( svgDrawingHelper, annotationUID, textBoxUID, textLines, textBoxPosition, [canvasCoordinates[2]], {}, { color, } ); } } return true; }; /** * 更新缓存的统计数据 */ private _updateCachedStats( annotation: RectangleGrayscaleAnnotation, enabledElement: CoreTypes.IEnabledElement ): void { const { viewport } = enabledElement; const { points } = annotation.data.handles; if (points.length < 4) { return; } // 使用Cornerstone缓存系统获取图像 const imageId = viewport.getCurrentImageId?.(); if (!imageId) { console.warn('[RectangleGrayscaleTool] No imageId available'); return; } const image = cornerstone.cache.getImage(imageId); if (!image) { console.warn('[RectangleGrayscaleTool] Image not found in cache'); return; } // 采样矩形内的像素 const sampleResult = this._samplePixelsInRectangle(points, image, viewport); // 计算统计值 const stats = this._calculateGrayscaleStats(sampleResult.values, points); // 更新缓存 const targetId = `imageId:${imageId}`; if (!annotation.data.cachedStats) { annotation.data.cachedStats = {}; } annotation.data.cachedStats[targetId] = stats; } /** * 在矩形区域内采样像素 */ private _samplePixelsInRectangle( rectanglePoints: CoreTypes.Point3[], image: any, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport ): PixelSampleResult { const pixelData = image.getPixelData(); const { width, height } = image; // 获取矩形的边界框(像素坐标) const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p)); let minX = Math.min(...canvasPoints.map(p => p[0])); let maxX = Math.max(...canvasPoints.map(p => p[0])); let minY = Math.min(...canvasPoints.map(p => p[1])); let maxY = Math.max(...canvasPoints.map(p => p[1])); // 限制在图像边界内 minX = Math.max(0, Math.floor(minX)); maxX = Math.min(width - 1, Math.floor(maxX)); minY = Math.max(0, Math.floor(minY)); maxY = Math.min(height - 1, Math.floor(maxY)); const values: number[] = []; const coordinates: Array<{ x: number; y: number }> = []; // 对每个像素进行采样 for (let y = minY; y <= maxY; y++) { for (let x = minX; x <= maxX; x++) { // 检查点是否在矩形内(使用多边形内点测试) if (this._isPointInPolygon([x, y], canvasPoints)) { const index = y * width + x; const value = pixelData[index]; values.push(value); coordinates.push({ x, y }); } } } return { values, coordinates, boundingBox: { minX, maxX, minY, maxY }, totalPixels: values.length, }; } /** * 使用射线投射算法检查点是否在多边形内 */ private _isPointInPolygon( point: CoreTypes.Point2, polygon: CoreTypes.Point2[] ): boolean { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { if (((polygon[i][1] > point[1]) !== (polygon[j][1] > point[1])) && (point[0] < (polygon[j][0] - polygon[i][0]) * (point[1] - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) { inside = !inside; } } return inside; } /** * 计算灰度统计值 */ private _calculateGrayscaleStats( values: number[], rectanglePoints: CoreTypes.Point3[] ): { mean: number; min: number; max: number; pixelCount: number; area: number; width: number; height: number; } { if (values.length === 0) { return { mean: 0, min: 0, max: 0, pixelCount: 0, area: 0, width: 0, height: 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); // 计算几何尺寸 const p1 = rectanglePoints[0]; const p2 = rectanglePoints[2]; // 对角点 const width = Math.abs(p2[0] - p1[0]); const height = Math.abs(p2[1] - p1[1]); const area = width * height; return { mean, min, max, pixelCount: values.length, area: parseFloat(area.toFixed(2)), width: parseFloat(width.toFixed(2)), height: parseFloat(height.toFixed(2)), }; } // 空的辅助方法声明,避免编译错误 _activateDraw(element: HTMLDivElement): void {} _deactivateDraw(element: HTMLDivElement): void {} _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 RectangleGrayscaleAnnotation; // 优先检查是否点击在文本框上 if (this._isPointInTextBox(element, customAnn, canvasCoords)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); const textBoxPos = customAnn.data.textBox || viewport.worldToCanvas(customAnn.data.handles.points[2]); const textBoxOffset: CoreTypes.Point2 = [ canvasCoords[0] - textBoxPos[0], canvasCoords[1] - textBoxPos[1], ]; this.editData = { annotation: customAnn, viewportIdsToRender, handleIndex: -2, // -2表示拖拽文本框 hasMoved: false, textBoxBeingMoved: true, textBoxOffset: textBoxOffset, }; customAnn.isSelected = true; customAnn.highlighted = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } 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; } // 检查是否点击在线段上 if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) { const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); // 计算鼠标点击相对于工具中心的偏移(用于拖拽) const toolCenter = this._calculateRectangleCenter(customAnn.data.handles.points); const toolCenterCanvas = viewport.worldToCanvas(toolCenter); const wholeToolOffset: CoreTypes.Point2 = [ canvasCoords[0] - toolCenterCanvas[0], canvasCoords[1] - toolCenterCanvas[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; } // 检查是否点击在矩形内部(但不在线段上) if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { // 仅选中,不拖拽 const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender( element, this.getToolName() ); customAnn.isSelected = true; utilities.triggerAnnotationRenderForViewportIds( viewportIdsToRender ); evt.preventDefault(); evt.stopPropagation(); return; } } // 如果没有点击在工具上,取消所有选中状态 for (const ann of annotations) { const customAnn = ann as RectangleGrayscaleAnnotation; 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 { annotation: ann, viewportIdsToRender } = this.editData; const customAnn = ann as RectangleGrayscaleAnnotation; const { data } = customAnn; // 如果正在拖拽文本框 if (this.editData.handleIndex === -2 && this.editData.textBoxBeingMoved) { const newTextBoxPos: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.textBoxOffset![0], canvasCoords[1] - this.editData.textBoxOffset![1], ]; // 更新文本框位置 data.textBox = newTextBoxPos; this.editData.hasMoved = true; utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender); evt.preventDefault(); evt.stopPropagation(); return; } // 如果正在拖拽整个矩形 if (this.editData.handleIndex === -1) { const { viewport } = enabledElement; // 计算当前矩形质心(四个角点的平均位置) const currentCenterWorld = this._calculateRectangleCenter(data.handles.points); // 计算新的质心位置 = 当前鼠标位置 - 初始偏移 const newCenterCanvas: CoreTypes.Point2 = [ canvasCoords[0] - this.editData.wholeToolOffset![0], canvasCoords[1] - this.editData.wholeToolOffset![1], ]; // 将新的质心位置转换为world坐标 const newCenterWorld = viewport.canvasToWorld(newCenterCanvas); // 计算从当前质心到新质心的偏移 const worldOffset: CoreTypes.Point3 = [ newCenterWorld[0] - currentCenterWorld[0], newCenterWorld[1] - currentCenterWorld[1], 0, // Z轴保持不变 ]; // 应用相同的偏移到所有四个顶点 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; } // 处理单个手柄拖拽 - 实现矩形的自由伸缩 const worldPos = currentPoints.world; const activeHandleIndex = data.handles.activeHandleIndex; if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length && this.editData.originalPoints) { // 计算拖拽的增量 const originalPos = this.editData.originalPoints[activeHandleIndex]; const deltaX = worldPos[0] - originalPos[0]; const deltaY = worldPos[1] - originalPos[1]; // 根据被拖拽的角点索引,重新计算所有四个顶点的位置 // 保持矩形几何关系:直角和平行边 const originalPoints = this.editData.originalPoints; // 确定矩形的变换规则: // 如果拖拽顶点0(左上角),则改变矩形的左边和上边 // 如果拖拽顶点1(右上角),相反,改变右边和上边 // 如果拖拽顶点2(右下角),改变右边和下边 // 如果拖拽顶点3(左下角),改变左边和下边 switch (activeHandleIndex) { case 0: // 拖拽左上角 data.handles.points[0] = worldPos; // 固定右下角(2) data.handles.points[2] = [...originalPoints[2]]; // 更新右上角(1): x同右下角, y同左上角 data.handles.points[1] = [originalPoints[2][0], worldPos[1], worldPos[2]]; // 更新左下角(3): x同左上角, y同右下角 data.handles.points[3] = [worldPos[0], originalPoints[2][1], worldPos[2]]; break; case 1: // 拖拽右上角 data.handles.points[1] = worldPos; // 固定左下角(3) data.handles.points[3] = [...originalPoints[3]]; // 更新左上角(0): x同左下角, y同右上角 data.handles.points[0] = [originalPoints[3][0], worldPos[1], worldPos[2]]; // 更新右下角(2): x同右上角, y同左下角 data.handles.points[2] = [worldPos[0], originalPoints[3][1], worldPos[2]]; break; case 2: // 拖拽右下角 data.handles.points[2] = worldPos; // 固定左上角(0) data.handles.points[0] = [...originalPoints[0]]; // 更新右上角(1): x同右下角, y同左上角 data.handles.points[1] = [worldPos[0], originalPoints[0][1], worldPos[2]]; // 更新左下角(3): x同左上角, y同右下角 data.handles.points[3] = [originalPoints[0][0], worldPos[1], worldPos[2]]; break; case 3: // 拖拽左下角 data.handles.points[3] = worldPos; // 固定右上角(1) data.handles.points[1] = [...originalPoints[1]]; // 更新左上角(0): x同左下角, y同右上角 data.handles.points[0] = [worldPos[0], originalPoints[1][1], worldPos[2]]; // 更新右下角(2): x同右上角, y同左下角 data.handles.points[2] = [originalPoints[1][0], worldPos[1], worldPos[2]]; break; } 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 RectangleGrayscaleAnnotation; customAnn.data.handles.activeHandleIndex = null; customAnn.highlighted = false; const eventDetail = evt.detail; const { element } = eventDetail; const enabledElement = getEnabledElement(element); if (enabledElement) { const { viewport } = enabledElement; // 如果工具被移动了,更新缓存的统计数据 if (hasMoved) { 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; // 检查是否悬停在工具的不同部分上 for (const ann of annotations) { const customAnn = ann as RectangleGrayscaleAnnotation; // 优先检查文本框(手型光标) if (this._isPointInTextBox(element, customAnn, canvasCoords)) { element.style.cursor = 'pointer'; customAnn.highlighted = true; isHovering = true; break; } // 检查是否靠近手柄(十字准星光标) const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6); if (handle) { element.style.cursor = 'crosshair'; customAnn.highlighted = true; isHovering = true; break; } // 检查是否在线段上(十字准星光标) if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) { element.style.cursor = 'crosshair'; customAnn.highlighted = true; isHovering = true; break; } // 检查是否在矩形内部(默认光标) if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) { element.style.cursor = 'default'; customAnn.highlighted = true; isHovering = true; break; } } // 如果没有悬停,重置高亮和光标 if (!isHovering) { for (const ann of annotations) { const customAnn = ann as RectangleGrayscaleAnnotation; 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(); } } }; getHandleNearImagePoint( element: HTMLDivElement, annotation: RectangleGrayscaleAnnotation, 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 _calculateRectangleCenter(points: CoreTypes.Point3[]): CoreTypes.Point3 { if (points.length < 4) { return [0, 0, 0]; } // 计算四个顶点的平均位置作为质心 let sumX = 0, sumY = 0, sumZ = 0; for (const point of points) { sumX += point[0]; sumY += point[1]; sumZ += point[2]; } return [ sumX / points.length, sumY / points.length, sumZ / points.length, ]; } }