|
|
@@ -0,0 +1,937 @@
|
|
|
+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;
|
|
|
+ } | 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;
|
|
|
+ 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 LineGrayscaleAnnotation;
|
|
|
+ customAnn.isSelected = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ if (!this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(eventDetail.element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation: ann, viewportIdsToRender } = this.editData;
|
|
|
+ const customAnn = ann as LineGrayscaleAnnotation;
|
|
|
+ const { data } = customAnn;
|
|
|
+
|
|
|
+ // 处理手柄拖拽
|
|
|
+ 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 } = this.editData;
|
|
|
+ const customAnn = ann as LineGrayscaleAnnotation;
|
|
|
+
|
|
|
+ customAnn.data.handles.activeHandleIndex = null;
|
|
|
+ customAnn.highlighted = false;
|
|
|
+
|
|
|
+ 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();
|
|
|
+ };
|
|
|
+
|
|
|
+ _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 LineGrayscaleAnnotation;
|
|
|
+
|
|
|
+ // 检查是否靠近手柄
|
|
|
+ const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
|
|
|
+ if (handle) {
|
|
|
+ element.style.cursor = 'crosshair';
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近直线
|
|
|
+ if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
|
|
|
+ element.style.cursor = 'crosshair';
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有悬停,重置高亮和光标
|
|
|
+ if (!isHovering) {
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as LineGrayscaleAnnotation;
|
|
|
+ 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 _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`;
|
|
|
+ drawHandles(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ handleGroupUID,
|
|
|
+ canvasCoordinates,
|
|
|
+ {
|
|
|
+ color,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制统计文本
|
|
|
+ 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;
|
|
|
+ };
|
|
|
+
|
|
|
+}
|