|
|
@@ -0,0 +1,1473 @@
|
|
|
+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;
|
|
|
+ };
|
|
|
+}
|