|
@@ -0,0 +1,1637 @@
|
|
|
+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';
|
|
|
+
|
|
|
+// 注解初始配置接口
|
|
|
+interface AnnotationInitialConfig {
|
|
|
+ verticalDistance?: number; // 脊柱线段之间的竖直距离,默认200px
|
|
|
+ lineAWidth?: number; // 第一条脊柱线段的宽度,默认180px
|
|
|
+ lineBWidth?: number; // 第二条脊柱线段的宽度,默认220px
|
|
|
+ leftTangentAngle?: number; // 左侧髋臼切线的角度(度数),默认-30度
|
|
|
+ rightTangentAngle?: number; // 右侧髋臼切线的角度(度数),默认30度
|
|
|
+ leftTangentLength?: number; // 左侧髋臼切线长度,默认150px
|
|
|
+ rightTangentLength?: number; // 右侧髋臼切线长度,默认150px
|
|
|
+
|
|
|
+ // 左侧髋臼垂线样式配置
|
|
|
+ leftPerpendicularLine?: {
|
|
|
+ length?: number; // 垂线长度,默认100px
|
|
|
+ color?: string; // 颜色,默认 'rgb(255, 255, 0)' (黄色)
|
|
|
+ width?: number; // 线宽,默认1px
|
|
|
+ lineDash?: string; // 虚线样式,默认'4,4'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 右侧髋臼垂线样式配置
|
|
|
+ rightPerpendicularLine?: {
|
|
|
+ length?: number; // 垂线长度,默认100px
|
|
|
+ color?: string; // 颜色,默认 'rgb(255, 165, 0)' (橙色)
|
|
|
+ width?: number; // 线宽,默认1px
|
|
|
+ lineDash?: string; // 虚线样式,默认'4,4'
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 注解数据接口
|
|
|
+interface DARAMeasurementAnnotation extends Types.Annotation {
|
|
|
+ data: {
|
|
|
+ handles: {
|
|
|
+ points: CoreTypes.Point3[];
|
|
|
+ activeHandleIndex: number | null;
|
|
|
+ textBoxLeft?: CoreTypes.Point2; // 左侧角度文本框位置
|
|
|
+ textBoxRight?: CoreTypes.Point2; // 右侧角度文本框位置
|
|
|
+ };
|
|
|
+ cachedStats?: {
|
|
|
+ [targetId: string]: {
|
|
|
+ angleLeft: number; // 左侧髋臼角度,取锐角
|
|
|
+ angleLeftOrigin: number;// 左侧髋臼角度 有可能是钝角,会影响画圆弧
|
|
|
+ angleRight: number; // 右侧髋臼角度
|
|
|
+ angleRightOrigin: number;// 右侧髋臼角度 有可能是钝角,,会影响画圆弧
|
|
|
+ midPointM1: CoreTypes.Point3; // 第一条脊柱线中点
|
|
|
+ midPointM2: CoreTypes.Point3; // 第二条脊柱线中点
|
|
|
+ spineAxisVector: CoreTypes.Point3; // 脊柱轴心线向量
|
|
|
+ leftTangentVector: CoreTypes.Point3; // 左侧髋臼切线向量
|
|
|
+ rightTangentVector: CoreTypes.Point3; // 右侧髋臼切线向量
|
|
|
+ /**
|
|
|
+ * 脊柱轴线的垂线向量
|
|
|
+ */
|
|
|
+ perpendicularVector: CoreTypes.Point3; // 脊柱轴线的垂线向量
|
|
|
+ leftIntersection?: CoreTypes.Point3; // 左侧髋臼切线与脊柱轴心线的交点
|
|
|
+ rightIntersection?: CoreTypes.Point3; // 右侧髋臼切线与脊柱轴心线的交点
|
|
|
+
|
|
|
+ };
|
|
|
+ };
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 导出的注解数据接口
|
|
|
+export interface ExportedAnnotationData {
|
|
|
+ points: CoreTypes.Point3[]; // 8个点的世界坐标
|
|
|
+ angles: {
|
|
|
+ angleLeft: number; // 左侧髋臼角度(度数)
|
|
|
+ angleRight: number; // 右侧髋臼角度(度数)
|
|
|
+ };
|
|
|
+ spineAxis: {
|
|
|
+ midPointM1: CoreTypes.Point3; // 第一条脊柱线中点
|
|
|
+ midPointM2: CoreTypes.Point3; // 第二条脊柱线中点
|
|
|
+ spineAxisVector: CoreTypes.Point3; // 脊柱轴心线向量
|
|
|
+ };
|
|
|
+ tangentVectors: {
|
|
|
+ leftTangentVector: CoreTypes.Point3; // 左侧髋臼切线向量
|
|
|
+ rightTangentVector: CoreTypes.Point3; // 右侧髋臼切线向量
|
|
|
+ };
|
|
|
+ textBoxPositions?: {
|
|
|
+ textBoxLeft?: CoreTypes.Point2; // 左侧文本框位置
|
|
|
+ textBoxRight?: CoreTypes.Point2; // 右侧文本框位置
|
|
|
+ };
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: CoreTypes.Point3;
|
|
|
+ viewUp: CoreTypes.Point3;
|
|
|
+ FrameOfReferenceUID: string;
|
|
|
+ referencedImageId: string;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export default class DARAMeasurementTool extends AnnotationTool {
|
|
|
+ static toolName = 'DARAMeasurementTool';
|
|
|
+
|
|
|
+ editData: {
|
|
|
+ annotation: Types.Annotation;
|
|
|
+ viewportIdsToRender: string[];
|
|
|
+ handleIndex?: number;
|
|
|
+ newAnnotation?: boolean;
|
|
|
+ hasMoved?: boolean;
|
|
|
+ textBoxBeingMoved?: 'C' | 'D';
|
|
|
+ // 记录鼠标点击位置相对于文本框左上角的偏移量,用于防止拖拽时跳跃
|
|
|
+ textBoxOffset?: CoreTypes.Point2;
|
|
|
+ } | null = null;
|
|
|
+
|
|
|
+ isDrawing: boolean = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建一个预设的注解,带有8个默认点
|
|
|
+ * @param element HTML元素
|
|
|
+ * @param viewport Viewport实例
|
|
|
+ * @param config 可选的初始配置
|
|
|
+ */
|
|
|
+ static createDefaultAnnotation(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
|
|
|
+ config?: AnnotationInitialConfig
|
|
|
+ ): DARAMeasurementAnnotation {
|
|
|
+ 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 verticalDistance = config?.verticalDistance ?? 200;
|
|
|
+ const lineAWidth = config?.lineAWidth ?? 180;
|
|
|
+ const lineBWidth = config?.lineBWidth ?? 220;
|
|
|
+ const lineCAngle = config?.leftTangentAngle ?? 30; // 左上到右下
|
|
|
+ const lineDAngle = config?.rightTangentAngle ?? 165; // 右上到左下
|
|
|
+ const lineCLength = config?.leftTangentLength ?? 150;
|
|
|
+ const lineDLength = config?.rightTangentLength ?? 100;
|
|
|
+
|
|
|
+ // 创建8个canvas坐标点
|
|
|
+ // 线段A: 水平线,在中心上方 verticalDistance/2
|
|
|
+ const lineAY = centerY - verticalDistance / 2;
|
|
|
+ const lineAHalfWidth = lineAWidth / 2;
|
|
|
+
|
|
|
+ // 线段B: 水平线,在中心下方 verticalDistance/2
|
|
|
+ const lineBY = centerY + verticalDistance / 2;
|
|
|
+ const lineBHalfWidth = lineBWidth / 2;
|
|
|
+
|
|
|
+ // 线段C: 从左上斜向右下,角度为lineCAngle(相对于水平线)
|
|
|
+ const lineCAngleRad = (lineCAngle * Math.PI) / 180;
|
|
|
+ const lineCHalfLength = lineCLength / 2;
|
|
|
+ const lineCCenterX = centerX - 100; // C线段中心点在中心线左侧100px
|
|
|
+ const lineCCenterY = centerY;
|
|
|
+ const lineC1X = lineCCenterX - lineCHalfLength * Math.cos(lineCAngleRad);
|
|
|
+ const lineC1Y = lineCCenterY - lineCHalfLength * Math.sin(lineCAngleRad);
|
|
|
+ const lineC2X = lineCCenterX + lineCHalfLength * Math.cos(lineCAngleRad);
|
|
|
+ const lineC2Y = lineCCenterY + lineCHalfLength * Math.sin(lineCAngleRad);
|
|
|
+
|
|
|
+ // 线段D: 从右上斜向左下,角度为lineDAngle(相对于水平线)
|
|
|
+ const lineDAngleRad = (lineDAngle * Math.PI) / 180;
|
|
|
+ const lineDHalfLength = lineDLength / 2;
|
|
|
+ const lineDCenterX = centerX + 100; // D线段中心点在中心线右侧100px
|
|
|
+ const lineDCenterY = centerY;
|
|
|
+ const lineD1X = lineDCenterX - lineDHalfLength * Math.cos(lineDAngleRad);
|
|
|
+ const lineD1Y = lineDCenterY - lineDHalfLength * Math.sin(lineDAngleRad) - 50;//50表示向上移动一些
|
|
|
+ const lineD2X = lineDCenterX + lineDHalfLength * Math.cos(lineDAngleRad);
|
|
|
+ const lineD2Y = lineDCenterY + lineDHalfLength * Math.sin(lineDAngleRad) - 50;
|
|
|
+
|
|
|
+ const canvasPoints: CoreTypes.Point2[] = [
|
|
|
+ // 线段A: 水平线(点1, 点2)
|
|
|
+ [centerX - lineAHalfWidth, lineAY],
|
|
|
+ [centerX + lineAHalfWidth, lineAY],
|
|
|
+ // 线段B: 水平线(点3, 点4)
|
|
|
+ [centerX - lineBHalfWidth, lineBY],
|
|
|
+ [centerX + lineBHalfWidth, lineBY],
|
|
|
+ // 线段C: 斜线(点5, 点6)
|
|
|
+ [lineC1X, lineC1Y],
|
|
|
+ [lineC2X, lineC2Y],
|
|
|
+ // 线段D: 斜线(点7, 点8)
|
|
|
+ [lineD1X, lineD1Y],
|
|
|
+ [lineD2X, lineD2Y],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 转换为world坐标
|
|
|
+ const worldPoints = canvasPoints.map((canvasPoint) =>
|
|
|
+ viewport.canvasToWorld(canvasPoint)
|
|
|
+ );
|
|
|
+
|
|
|
+ const camera = viewport.getCamera();
|
|
|
+ const { viewPlaneNormal, viewUp } = camera;
|
|
|
+ if (viewPlaneNormal === undefined) {
|
|
|
+ throw new Error('----')
|
|
|
+ }
|
|
|
+ if (viewUp === undefined) {
|
|
|
+ throw new Error('----')
|
|
|
+ }
|
|
|
+ const annotationData = {
|
|
|
+ invalidated: true,
|
|
|
+ highlighted: false,
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
|
|
|
+ viewUp: [...viewUp] as CoreTypes.Point3,
|
|
|
+ FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
|
|
|
+ referencedImageId: viewport.getCurrentImageId?.() || '',
|
|
|
+ toolName: DARAMeasurementTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ label: '',
|
|
|
+ handles: {
|
|
|
+ points: worldPoints,
|
|
|
+ activeHandleIndex: null,
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as DARAMeasurementAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出注解数据
|
|
|
+ * @param annotation 要导出的注解
|
|
|
+ * @returns 导出的数据对象
|
|
|
+ */
|
|
|
+ static exportAnnotationData(
|
|
|
+ annotation: DARAMeasurementAnnotation,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): ExportedAnnotationData | null {
|
|
|
+ const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
|
|
|
+ const cachedStats = annotation.data.cachedStats?.[targetId];
|
|
|
+
|
|
|
+ if (!cachedStats || !cachedStats.midPointM1 || !cachedStats.midPointM2) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ points: [...annotation.data.handles.points],
|
|
|
+ angles: {
|
|
|
+ angleLeft: cachedStats.angleLeft,
|
|
|
+ angleRight: cachedStats.angleRight,
|
|
|
+ },
|
|
|
+ spineAxis: {
|
|
|
+ midPointM1: cachedStats.midPointM1,
|
|
|
+ midPointM2: cachedStats.midPointM2,
|
|
|
+ spineAxisVector: cachedStats.spineAxisVector,
|
|
|
+ },
|
|
|
+ tangentVectors: {
|
|
|
+ leftTangentVector: cachedStats.leftTangentVector,
|
|
|
+ rightTangentVector: cachedStats.rightTangentVector,
|
|
|
+ },
|
|
|
+ textBoxPositions: {
|
|
|
+ textBoxLeft: annotation.data.handles.textBoxLeft,
|
|
|
+ textBoxRight: annotation.data.handles.textBoxRight,
|
|
|
+ },
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
|
|
|
+ viewUp: annotation.metadata?.viewUp || [0, 1, 0],
|
|
|
+ FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
|
|
|
+ referencedImageId: annotation.metadata?.referencedImageId || '',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从导出的数据恢复注解
|
|
|
+ * @param exportedData 导出的数据
|
|
|
+ * @param element HTML元素
|
|
|
+ * @param viewport Viewport实例
|
|
|
+ * @returns 恢复的注解
|
|
|
+ */
|
|
|
+ static restoreFromExportedData(
|
|
|
+ exportedData: ExportedAnnotationData,
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): DARAMeasurementAnnotation {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ throw new Error('Element is not enabled');
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotationData = {
|
|
|
+ invalidated: true,
|
|
|
+ highlighted: false,
|
|
|
+ metadata: {
|
|
|
+ ...exportedData.metadata,
|
|
|
+ toolName: DARAMeasurementTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ label: '',
|
|
|
+ handles: {
|
|
|
+ points: [...exportedData.points],
|
|
|
+ activeHandleIndex: null,
|
|
|
+ textBoxLeft: exportedData.textBoxPositions?.textBoxLeft,
|
|
|
+ textBoxRight: exportedData.textBoxPositions?.textBoxRight,
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as DARAMeasurementAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ toolProps: PublicToolProps = {},
|
|
|
+ defaultToolProps: ToolProps = {
|
|
|
+ supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
|
+ configuration: {
|
|
|
+ shadow: true,
|
|
|
+ preventHandleOutsideImage: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ) {
|
|
|
+ super(toolProps, defaultToolProps);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
|
|
|
+ */
|
|
|
+ addNewAnnotation(
|
|
|
+ evt: EventTypes.InteractionEventType
|
|
|
+ ): DARAMeasurementAnnotation {
|
|
|
+ // 不创建新注解,直接返回空对象
|
|
|
+ // 因为我们只使用预设的8点注解
|
|
|
+ evt.preventDefault();
|
|
|
+ return {} as DARAMeasurementAnnotation;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点是否靠近工具
|
|
|
+ */
|
|
|
+ isPointNearTool(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ annotation: DARAMeasurementAnnotation,
|
|
|
+ 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) {
|
|
|
+ for (let i = 0; i < points.length - 1; i += 2) {
|
|
|
+ if (i + 1 < points.length) {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点击是否在文本框内
|
|
|
+ */
|
|
|
+ private _isPointInTextBox(
|
|
|
+ canvasPoint: CoreTypes.Point2,
|
|
|
+ textBoxPosition: CoreTypes.Point2,
|
|
|
+ textWidth: number = 100,
|
|
|
+ textHeight: number = 40
|
|
|
+ ): boolean {
|
|
|
+ const [x, y] = canvasPoint;
|
|
|
+ const [tx, ty] = textBoxPosition;
|
|
|
+
|
|
|
+ // 扩大检测区域,包括文本框周围的区域
|
|
|
+ // drawLinkedTextBox会在textBoxPosition周围绘制,所以需要考虑偏移
|
|
|
+ const margin = 10;
|
|
|
+ const result = (
|
|
|
+ x >= tx - margin &&
|
|
|
+ x <= tx + textWidth + margin &&
|
|
|
+ y >= ty - margin &&
|
|
|
+ y <= ty + textHeight + margin
|
|
|
+ );
|
|
|
+
|
|
|
+ // 添加调试日志
|
|
|
+ if (result) {
|
|
|
+ console.log('文本框碰撞检测成功:', {
|
|
|
+ 点击位置: canvasPoint,
|
|
|
+ 文本框位置: textBoxPosition,
|
|
|
+ 检测范围: {
|
|
|
+ x: [tx - margin, tx + textWidth + margin],
|
|
|
+ y: [ty - margin, ty + textHeight + margin]
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建角度弧线的点数组
|
|
|
+ * @param center 弧线的圆心(交点)
|
|
|
+ * @param vector1 第一条线段的方向向量(中点线)
|
|
|
+ * @param vector2 第二条线段的方向向量(C或D线)
|
|
|
+ * @param radius 弧线半径
|
|
|
+ * @returns 弧线的点数组
|
|
|
+ */
|
|
|
+ private _createAngleArcPoints(
|
|
|
+ center: CoreTypes.Point2,
|
|
|
+ vector1: CoreTypes.Point2,
|
|
|
+ vector2: CoreTypes.Point2,
|
|
|
+ radius: number
|
|
|
+ ): CoreTypes.Point2[] {
|
|
|
+ // 计算两个向量的角度(相对于X轴正方向)
|
|
|
+ const angle1 = Math.atan2(vector1[1], vector1[0]);
|
|
|
+ const angle2 = Math.atan2(vector2[1], vector2[0]);
|
|
|
+
|
|
|
+ // 确定起始角度和结束角度
|
|
|
+ let startAngle = angle1;
|
|
|
+ let endAngle = angle2;
|
|
|
+
|
|
|
+ // 计算角度差,确保是正值且小于180度
|
|
|
+ let angleDiff = endAngle - startAngle;
|
|
|
+ if (angleDiff > Math.PI) {
|
|
|
+ angleDiff -= 2 * Math.PI;
|
|
|
+ } else if (angleDiff < -Math.PI) {
|
|
|
+ angleDiff += 2 * Math.PI;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果角度差是负的,交换起始和结束角度
|
|
|
+ if (angleDiff < 0) {
|
|
|
+ [startAngle, endAngle] = [endAngle, startAngle];
|
|
|
+ angleDiff = -angleDiff;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成弧线上的点
|
|
|
+ // 根据角度大小决定需要多少个点
|
|
|
+ const numPoints = Math.max(10, Math.ceil(Math.abs(angleDiff) / (Math.PI / 18))); // 至少10个点
|
|
|
+ const points: CoreTypes.Point2[] = [];
|
|
|
+
|
|
|
+ for (let i = 0; i <= numPoints; i++) {
|
|
|
+ const t = i / numPoints;
|
|
|
+ const angle = startAngle + angleDiff * t;
|
|
|
+ const x = center[0] + radius * Math.cos(angle);
|
|
|
+ const y = center[1] + radius * Math.sin(angle);
|
|
|
+ points.push([x, y]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return points;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算点到线段的距离
|
|
|
+ */
|
|
|
+ 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 { renderingEngine } = enabledElement;
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ //renderingEngine,
|
|
|
+ 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
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_CLICK',
|
|
|
+ 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
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_CLICK',
|
|
|
+ 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
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消激活修改模式
|
|
|
+ */
|
|
|
+ _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
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标按下回调 - 用于修改模式
|
|
|
+ */
|
|
|
+ _mouseDownModifyCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ console.log('_mouseDownModifyCallback 被调用');
|
|
|
+
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element, currentPoints } = eventDetail;
|
|
|
+ const canvasCoords = currentPoints.canvas;
|
|
|
+
|
|
|
+ console.log('点击位置:', canvasCoords);
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ console.log('enabledElement 不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+
|
|
|
+ const annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
|
|
+ console.log('找到的注解数量:', annotations?.length || 0);
|
|
|
+
|
|
|
+ if (!annotations || annotations.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先检查是否点击在文本框上
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as DARAMeasurementAnnotation;
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
|
|
|
+
|
|
|
+ console.log('检查注解的cachedStats:', cachedStats);
|
|
|
+
|
|
|
+ if (cachedStats?.leftIntersection) {
|
|
|
+ const intersectionCanvas = viewport.worldToCanvas(cachedStats.leftIntersection);
|
|
|
+ const textBoxC: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
|
|
|
+ intersectionCanvas[0] + 10,
|
|
|
+ intersectionCanvas[1] - 10,
|
|
|
+ ];
|
|
|
+
|
|
|
+ console.log('C文本框位置:', textBoxC);
|
|
|
+
|
|
|
+ if (this._isPointInTextBox(canvasCoords, textBoxC)) {
|
|
|
+ // 点击在C文本框上
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算鼠标点击位置相对于文本框左上角的偏移量
|
|
|
+ // 这样在拖拽时可以保持鼠标和文本框的相对位置不变
|
|
|
+ const textBoxOffset: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - textBoxC[0],
|
|
|
+ canvasCoords[1] - textBoxC[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ textBoxBeingMoved: 'C',
|
|
|
+ textBoxOffset: textBoxOffset, // 保存偏移量
|
|
|
+ };
|
|
|
+
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cachedStats?.rightIntersection) {
|
|
|
+ const intersectionDCanvas = viewport.worldToCanvas(cachedStats.rightIntersection);
|
|
|
+ const textBoxD: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
|
|
|
+ intersectionDCanvas[0] + 10,
|
|
|
+ intersectionDCanvas[1] + 20,
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (this._isPointInTextBox(canvasCoords, textBoxD)) {
|
|
|
+ // 点击在D文本框上
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算鼠标点击位置相对于文本框左上角的偏移量
|
|
|
+ // 这样在拖拽时可以保持鼠标和文本框的相对位置不变
|
|
|
+ const textBoxOffset: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - textBoxD[0],
|
|
|
+ canvasCoords[1] - textBoxD[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ textBoxBeingMoved: 'D',
|
|
|
+ textBoxOffset: textBoxOffset, // 保存偏移量
|
|
|
+ };
|
|
|
+
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有点击文本框,再查找最近的手柄
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as DARAMeasurementAnnotation;
|
|
|
+ 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.highlighted = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标拖拽回调 - 用于修改模式
|
|
|
+ */
|
|
|
+ _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 } = this.editData;
|
|
|
+ const customAnn = ann as DARAMeasurementAnnotation;
|
|
|
+ const { data } = customAnn;
|
|
|
+
|
|
|
+ // 如果正在拖拽文本框
|
|
|
+ if (textBoxBeingMoved && this.editData.textBoxOffset) {
|
|
|
+ // 从当前鼠标位置减去偏移量,得到文本框左上角应该在的位置
|
|
|
+ // 这样可以保持鼠标和文本框的相对位置不变,防止跳跃
|
|
|
+ const newTextBoxPosition: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - this.editData.textBoxOffset[0],
|
|
|
+ canvasCoords[1] - this.editData.textBoxOffset[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (textBoxBeingMoved === 'C') {
|
|
|
+ data.handles.textBoxLeft = newTextBoxPosition;
|
|
|
+ } else if (textBoxBeingMoved === 'D') {
|
|
|
+ data.handles.textBoxRight = newTextBoxPosition;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 } = this.editData;
|
|
|
+ const customAnn = ann as DARAMeasurementAnnotation;
|
|
|
+
|
|
|
+ // 清除active状态
|
|
|
+ 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();
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理选中回调
|
|
|
+ */
|
|
|
+ handleSelectedCallback(
|
|
|
+ evt: EventTypes.InteractionEventType,
|
|
|
+ annotation: DARAMeasurementAnnotation
|
|
|
+ ): void {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ annotation.highlighted = true;
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ //renderingEngine,
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具选中回调
|
|
|
+ */
|
|
|
+ toolSelectedCallback(
|
|
|
+ evt: EventTypes.InteractionEventType,
|
|
|
+ annotation: DARAMeasurementAnnotation
|
|
|
+ ): void {
|
|
|
+ // 实现工具选中逻辑
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标拖拽
|
|
|
+ */
|
|
|
+ _dragCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ this.isDrawing = true;
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement || !this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+
|
|
|
+ const { annotation, viewportIdsToRender } = this.editData;
|
|
|
+ const { data } = annotation as DARAMeasurementAnnotation;
|
|
|
+
|
|
|
+ if (annotation) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ const points = data.handles.points;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (activeHandleIndex !== null && activeHandleIndex < points.length) {
|
|
|
+ points[activeHandleIndex] = worldPos;
|
|
|
+
|
|
|
+ this._updateCachedStats(
|
|
|
+ annotation as DARAMeasurementAnnotation,
|
|
|
+ enabledElement
|
|
|
+ );
|
|
|
+
|
|
|
+ this.editData!.hasMoved = true;
|
|
|
+ } else if (this.editData!.newAnnotation) {
|
|
|
+ // 绘制新注解时的拖拽
|
|
|
+ const lastIndex = points.length - 1;
|
|
|
+ if (lastIndex >= 0) {
|
|
|
+ points[lastIndex] = worldPos;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ //renderingEngine,
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标抬起/点击
|
|
|
+ */
|
|
|
+ _endCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ if (!this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
|
|
|
+ this.editData;
|
|
|
+ const { data } = annotation as DARAMeasurementAnnotation;
|
|
|
+
|
|
|
+ if (newAnnotation && !hasMoved) {
|
|
|
+ // 添加新点
|
|
|
+ const points = data.handles.points;
|
|
|
+
|
|
|
+ if (points.length < 6) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ if (points.length > 0) {
|
|
|
+ // 更新最后一个点的位置(如果已经在拖拽中添加了)
|
|
|
+ points[points.length - 1] = worldPos;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加下一个点
|
|
|
+ if (points.length < 6) {
|
|
|
+ points.push(worldPos);
|
|
|
+ data.handles.activeHandleIndex = points.length - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (points.length === 6) {
|
|
|
+ // 完成注解创建
|
|
|
+ this.isDrawing = false;
|
|
|
+ data.handles.activeHandleIndex = null;
|
|
|
+ annotation.highlighted = false;
|
|
|
+ this.editData.newAnnotation = false;
|
|
|
+ this._deactivateDraw(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (enabledElement) {
|
|
|
+ this._updateCachedStats(
|
|
|
+ annotation as DARAMeasurementAnnotation,
|
|
|
+ enabledElement
|
|
|
+ );
|
|
|
+
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+ const newViewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ //renderingEngine,
|
|
|
+ newViewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (hasMoved) {
|
|
|
+ // 拖拽结束
|
|
|
+ this.editData.hasMoved = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取靠近图像点的手柄
|
|
|
+ */
|
|
|
+ getHandleNearImagePoint(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ annotation: DARAMeasurementAnnotation,
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 使用更大的proximity来更容易选中手柄
|
|
|
+ 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) {
|
|
|
+ // 设置activeHandleIndex以便拖拽
|
|
|
+ annotation.data.handles.activeHandleIndex = i;
|
|
|
+ return {
|
|
|
+ worldPosition: point,
|
|
|
+ } as Types.ToolHandle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有找到手柄,重置activeHandleIndex
|
|
|
+ annotation.data.handles.activeHandleIndex = null;
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新缓存的统计数据
|
|
|
+ */
|
|
|
+ _updateCachedStats(
|
|
|
+ annotation: DARAMeasurementAnnotation,
|
|
|
+ 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]) {
|
|
|
+ console.log(`完全变0`);
|
|
|
+ data.cachedStats[targetId] = {
|
|
|
+ angleLeft: 0,
|
|
|
+ angleLeftOrigin: 0,
|
|
|
+ angleRight: 0,
|
|
|
+ angleRightOrigin: 0,
|
|
|
+ midPointM1: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ midPointM2: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ spineAxisVector: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ leftTangentVector: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ rightTangentVector: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ perpendicularVector: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const stats = data.cachedStats[targetId];
|
|
|
+
|
|
|
+ if (points.length >= 8) {
|
|
|
+ // 提取点: 脊柱线A = [0,1], 脊柱线B=[2,3], 左侧髋臼=[4,5], 右侧髋臼=[6,7]
|
|
|
+ const spineA1 = points[0],
|
|
|
+ spineA2 = points[1];
|
|
|
+ const spineB1 = points[2],
|
|
|
+ spineB2 = points[3];
|
|
|
+ const leftHip1 = points[4],
|
|
|
+ leftHip2 = points[5];
|
|
|
+ const rightHip1 = points[6],
|
|
|
+ rightHip2 = points[7];
|
|
|
+
|
|
|
+ // 计算脊柱线中点
|
|
|
+ const midPointM1 = calculateMidpoint(spineA1, spineA2);
|
|
|
+ const midPointM2 = calculateMidpoint(spineB1, spineB2);
|
|
|
+ console.log(`计算两个中点:${midPointM1} = ${midPointM2}`);
|
|
|
+ // 脊柱轴心线向量
|
|
|
+ const spineAxisVector = vectorSubtract(midPointM2, midPointM1);
|
|
|
+
|
|
|
+ // 髋臼切线向量
|
|
|
+ const leftTangentVector = vectorSubtract(leftHip2, leftHip1);
|
|
|
+ const rightTangentVector = vectorSubtract(rightHip2, rightHip1);
|
|
|
+
|
|
|
+ // 计算脊柱轴线的垂线向量(用于角度计算)
|
|
|
+ // 垂线向量可以通过叉积计算得到
|
|
|
+ const perpendicularVector = this._calculatePerpendicularVector(spineAxisVector);
|
|
|
+ console.log(`计算出垂线向量:${perpendicularVector}`)
|
|
|
+
|
|
|
+ // 计算左髋臼切线与脊柱轴心的交点
|
|
|
+ const leftIntersection = getLineIntersection(midPointM1, spineAxisVector, leftHip1, leftTangentVector) || undefined;
|
|
|
+
|
|
|
+ // 计算右髋臼切线与脊柱轴心的交点
|
|
|
+ const rightIntersection = getLineIntersection(midPointM1, spineAxisVector, rightHip1, rightTangentVector) || undefined;
|
|
|
+
|
|
|
+ // 存储计算结果
|
|
|
+ stats.midPointM1 = midPointM1;
|
|
|
+ stats.midPointM2 = midPointM2;
|
|
|
+ stats.spineAxisVector = spineAxisVector;
|
|
|
+ stats.leftTangentVector = leftTangentVector;
|
|
|
+ stats.rightTangentVector = rightTangentVector;
|
|
|
+ stats.perpendicularVector = perpendicularVector;
|
|
|
+ stats.leftIntersection = leftIntersection;
|
|
|
+ stats.rightIntersection = rightIntersection;
|
|
|
+
|
|
|
+ // 计算左侧髋臼角度(切线与脊柱轴线垂线的夹角)
|
|
|
+ const leftAngleRad = getAngleBetweenLines(leftTangentVector, perpendicularVector);
|
|
|
+ stats.angleLeftOrigin = Math.round(degrees(leftAngleRad));
|
|
|
+ // 约束:使用切线的向量或者反向向量,分别计算和垂线的夹角,取那个锐角
|
|
|
+ if (stats.angleLeftOrigin < 90) {
|
|
|
+ stats.angleLeft = stats.angleLeftOrigin;
|
|
|
+ } else {
|
|
|
+ stats.angleLeft = 180 - stats.angleLeftOrigin;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算右侧髋臼角度(切线与脊柱轴线垂线的夹角)
|
|
|
+ const rightAngleRad = getAngleBetweenLines(rightTangentVector, perpendicularVector);
|
|
|
+ stats.angleRightOrigin = Math.round(degrees(rightAngleRad));
|
|
|
+ if (stats.angleRightOrigin < 90) {
|
|
|
+ stats.angleRight = stats.angleRightOrigin;
|
|
|
+ } else {
|
|
|
+ stats.angleRight = 180 - stats.angleRightOrigin;
|
|
|
+ }
|
|
|
+ // 兼容性字段已移除,不再存储向后兼容的数据
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // private _calculatePerpendicularVector(vector: CoreTypes.Point3): CoreTypes.Point3 {
|
|
|
+ // const vx = vector[0], vy = vector[1], vz = vector[2];
|
|
|
+ // // 正确叉积:与 Y 轴 [0,1,0]
|
|
|
+ // const perpendicular: CoreTypes.Point3 = [-vz, 0, vx];
|
|
|
+
|
|
|
+ // // 计算长度
|
|
|
+ // const length = Math.sqrt(perpendicular[0] ** 2 + perpendicular[1] ** 2 + perpendicular[2] ** 2);
|
|
|
+ // // 使用 epsilon 避免浮点误差
|
|
|
+ // if (length < 1e-10) {
|
|
|
+ // // 输入平行于 Y 轴,使用 X 轴作为辅助
|
|
|
+ // return [0, vz, -vy];
|
|
|
+ // }
|
|
|
+
|
|
|
+ // return perpendicular;
|
|
|
+ // }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算向量的垂线向量(2D 平面,忽略 z 轴)
|
|
|
+ * @param vector 输入向量 [x, y, z],但只使用 [x, y]
|
|
|
+ * @returns 垂直于输入向量的向量 [px, py, 0]
|
|
|
+ */
|
|
|
+ private _calculatePerpendicularVector(vector: CoreTypes.Point3): CoreTypes.Point3 {
|
|
|
+ const vx = vector[0];
|
|
|
+ const vy = vector[1];
|
|
|
+ // 2D 垂直向量:[-vy, vx, 0](顺时针 90° 旋转)
|
|
|
+ // 或者 [vy, -vx, 0](逆时针),取决于您需要的方向
|
|
|
+ return [-vy, vx, 0];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 渲染注解
|
|
|
+ */
|
|
|
+ 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 DARAMeasurementAnnotation;
|
|
|
+ const { annotationUID, data } = annotation;
|
|
|
+ const points = data.handles.points;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (points.length < 1) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
|
|
|
+
|
|
|
+ // 转换所有点为 canvas 坐标
|
|
|
+ const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
|
|
|
+
|
|
|
+ // 绘制线段 A (红色)
|
|
|
+ if (points.length >= 2 && annotationUID) {
|
|
|
+ const lineUID = `${annotationUID}-lineA`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(255, 0, 0)',
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ canvasPoints[0],
|
|
|
+ canvasPoints[1],
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制线段 B (蓝色)
|
|
|
+ if (points.length >= 4 && annotationUID) {
|
|
|
+ const lineUID = `${annotationUID}-lineB`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(0, 0, 255)',
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ canvasPoints[2],
|
|
|
+ canvasPoints[3],
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制中点线 (绿色)
|
|
|
+ if (points.length >= 4 && annotationUID) {
|
|
|
+ const midA = calculateMidpoint(points[0], points[1]);
|
|
|
+ const midB = calculateMidpoint(points[2], points[3]);
|
|
|
+ const midACanvas = viewport.worldToCanvas(midA);
|
|
|
+ const midBCanvas = viewport.worldToCanvas(midB);
|
|
|
+ const lineUID = `${annotationUID}-midLine`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(0, 255, 0)',
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ midACanvas,
|
|
|
+ midBCanvas,
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制线段 C (黄色)
|
|
|
+ if (points.length >= 6 && annotationUID) {
|
|
|
+ const lineUID = `${annotationUID}-lineC`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ canvasPoints[4],
|
|
|
+ canvasPoints[5],
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制 C 线段延长线 (黄色虚线)
|
|
|
+ const CVector = vectorSubtract(points[5], points[4]);
|
|
|
+ const extendEnd = vectorAdd(points[5], vectorScale(CVector, 2));
|
|
|
+ const extendEndCanvas = viewport.worldToCanvas(extendEnd);
|
|
|
+ const extendLineUID = `${annotationUID}-lineC-extend`;
|
|
|
+ const extendLineOptions = {
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ width: 1,
|
|
|
+ lineDash: '4,4',
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ extendLineUID,
|
|
|
+ canvasPoints[5],
|
|
|
+ extendEndCanvas,
|
|
|
+ extendLineOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制线段 D (橙色)
|
|
|
+ if (points.length >= 8 && annotationUID) {
|
|
|
+ const lineUID = `${annotationUID}-lineD`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ canvasPoints[6],
|
|
|
+ canvasPoints[7],
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制 D 线段延长线 (橙色虚线)
|
|
|
+ const DVector = vectorSubtract(points[7], points[6]);
|
|
|
+ const extendEndD = vectorAdd(points[7], vectorScale(DVector, 2));
|
|
|
+ const extendEndDCanvas = viewport.worldToCanvas(extendEndD);
|
|
|
+ const extendLineDUID = `${annotationUID}-lineD-extend`;
|
|
|
+ const extendLineDOptions = {
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ width: 1,
|
|
|
+ lineDash: '4,4',
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ extendLineDUID,
|
|
|
+ canvasPoints[7],
|
|
|
+ extendEndDCanvas,
|
|
|
+ extendLineDOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制左侧髋臼的交点、垂线、角度弧线和文本
|
|
|
+ if (cachedStats?.leftIntersection && cachedStats.angleLeft !== undefined && annotationUID && cachedStats.spineAxisVector && cachedStats.perpendicularVector) {
|
|
|
+ const intersection = cachedStats.leftIntersection;
|
|
|
+ const intersectionCanvas = viewport.worldToCanvas(intersection);
|
|
|
+
|
|
|
+ // 绘制交点标记(小圆点)
|
|
|
+ const circleUID = `${annotationUID}-intersection-left`;
|
|
|
+ drawCircleSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ circleUID,
|
|
|
+ intersectionCanvas,
|
|
|
+ 3,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ fill: 'rgb(255, 255, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制脊柱轴线垂线(虚线,从交点出发)
|
|
|
+ // 定义左侧垂线样式配置
|
|
|
+ const DEFAULT_LEFT_PERPENDICULAR = {
|
|
|
+ length: 100,
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ width: 1,
|
|
|
+ lineDash: '4,4'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 从配置中读取左侧垂线样式(如果有的话)
|
|
|
+ const leftPerpConfig = { ...DEFAULT_LEFT_PERPENDICULAR };
|
|
|
+
|
|
|
+ const perpendicularLength = leftPerpConfig.length;
|
|
|
+ // console.log(`脊柱轴线的垂线向量 ${cachedStats.perpendicularVector}`)
|
|
|
+ const perpendicularEnd = vectorAdd(intersection, decomposeVectorOnAxes(cachedStats.perpendicularVector, perpendicularLength));
|
|
|
+ // console.log(`垂线终点的向量 : ${perpendicularEnd}`)
|
|
|
+ const perpendicularEndCanvas = viewport.worldToCanvas(perpendicularEnd);
|
|
|
+ // console.log(`垂线终点转换为世界坐标 : ${perpendicularEndCanvas}`)
|
|
|
+ // console.log(`垂线的起点,也就是交点 : ${intersectionCanvas}`)
|
|
|
+ const perpendicularLineUID = `${annotationUID}-perpendicular-left`;
|
|
|
+ const perpendicularLineOptions = {
|
|
|
+ color: leftPerpConfig.color,
|
|
|
+ width: leftPerpConfig.width,
|
|
|
+ lineDash: leftPerpConfig.lineDash,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ perpendicularLineUID,
|
|
|
+ intersectionCanvas,
|
|
|
+ perpendicularEndCanvas,
|
|
|
+ perpendicularLineOptions
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制角度弧线(垂线与左髋臼切线之间的夹角)
|
|
|
+ // 将3D向量转换为2D向量用于绘制
|
|
|
+ const perpendicularVector2D: CoreTypes.Point2 = [cachedStats.perpendicularVector[0], cachedStats.perpendicularVector[1]];
|
|
|
+ let leftTangentVector2D: CoreTypes.Point2 = [cachedStats.leftTangentVector[0], cachedStats.leftTangentVector[1]];
|
|
|
+ //如果钝角,使用反向向量,为了画锐角的弧线
|
|
|
+ if (cachedStats.angleLeftOrigin > 90) {
|
|
|
+ leftTangentVector2D = [-cachedStats.leftTangentVector[0], -cachedStats.leftTangentVector[1]];
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // 创建弧线点数组
|
|
|
+ const arcRadius = 30; // 弧线半径
|
|
|
+ const arcPoints = this._createAngleArcPoints(
|
|
|
+ intersectionCanvas,
|
|
|
+ perpendicularVector2D,
|
|
|
+ leftTangentVector2D,
|
|
|
+ arcRadius
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制弧线
|
|
|
+ const arcUID = `${annotationUID}-arc-left`;
|
|
|
+ drawPathSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ arcUID,
|
|
|
+ arcPoints,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ width: 2,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制角度文本 (左侧髋臼)
|
|
|
+ const textLines = [`左侧: ${cachedStats.angleLeft}°`];
|
|
|
+ const textUID = `${annotationUID}-angle-text-left`;
|
|
|
+ const textBoxPosition: CoreTypes.Point2 = data.handles.textBoxLeft || [
|
|
|
+ intersectionCanvas[0] + 10,
|
|
|
+ intersectionCanvas[1] - 10,
|
|
|
+ ];
|
|
|
+
|
|
|
+ drawLinkedTextBox(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ textUID,
|
|
|
+ textLines,
|
|
|
+ textBoxPosition,
|
|
|
+ [intersectionCanvas],
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制右侧髋臼的交点、垂线、角度弧线和文本
|
|
|
+ if (cachedStats?.rightIntersection && cachedStats.angleRight !== undefined && annotationUID && cachedStats.spineAxisVector && cachedStats.perpendicularVector) {
|
|
|
+ const intersectionD = cachedStats.rightIntersection;
|
|
|
+ const intersectionDCanvas = viewport.worldToCanvas(intersectionD);
|
|
|
+
|
|
|
+ // 绘制交点标记(小圆点)
|
|
|
+ const circleDUID = `${annotationUID}-intersection-right`;
|
|
|
+ drawCircleSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ circleDUID,
|
|
|
+ intersectionDCanvas,
|
|
|
+ 3,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ fill: 'rgb(255, 165, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制脊柱轴线垂线(虚线,从交点出发)
|
|
|
+ // 定义右侧垂线样式配置
|
|
|
+ const DEFAULT_RIGHT_PERPENDICULAR = {
|
|
|
+ length: 100,
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ width: 1,
|
|
|
+ lineDash: '4,4'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 从配置中读取右侧垂线样式(如果有的话)
|
|
|
+ const rightPerpConfig = { ...DEFAULT_RIGHT_PERPENDICULAR };
|
|
|
+
|
|
|
+ const perpendicularLengthD = rightPerpConfig.length;
|
|
|
+ const perpendicularEndD = vectorAdd(intersectionD, decomposeVectorOnAxes(cachedStats.perpendicularVector, perpendicularLengthD));
|
|
|
+ const perpendicularEndDCanvas = viewport.worldToCanvas(perpendicularEndD);
|
|
|
+ const perpendicularLineDUID = `${annotationUID}-perpendicular-right`;
|
|
|
+ const perpendicularLineDOptions = {
|
|
|
+ color: rightPerpConfig.color,
|
|
|
+ width: rightPerpConfig.width,
|
|
|
+ lineDash: rightPerpConfig.lineDash,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ perpendicularLineDUID,
|
|
|
+ intersectionDCanvas,
|
|
|
+ perpendicularEndDCanvas,
|
|
|
+ perpendicularLineDOptions
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制角度弧线(垂线与右髋臼切线之间的夹角)
|
|
|
+ // 将3D向量转换为2D向量用于绘制
|
|
|
+ const perpendicularVector2D: CoreTypes.Point2 = [cachedStats.perpendicularVector[0], cachedStats.perpendicularVector[1]];
|
|
|
+ let rightTangentVector2D: CoreTypes.Point2 = [cachedStats.rightTangentVector[0], cachedStats.rightTangentVector[1]];
|
|
|
+ //如果钝角,使用反向向量,为了画锐角的弧线
|
|
|
+ if (cachedStats.angleRightOrigin > 90) {
|
|
|
+ rightTangentVector2D = [-cachedStats.rightTangentVector[0], -cachedStats.rightTangentVector[1]];
|
|
|
+ }
|
|
|
+ // 创建弧线点数组
|
|
|
+ const arcRadiusD = 30; // 弧线半径
|
|
|
+ const arcPointsD = this._createAngleArcPoints(
|
|
|
+ intersectionDCanvas,
|
|
|
+ perpendicularVector2D,
|
|
|
+ rightTangentVector2D,
|
|
|
+ arcRadiusD
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制弧线
|
|
|
+ const arcDUID = `${annotationUID}-arc-right`;
|
|
|
+ drawPathSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ arcDUID,
|
|
|
+ arcPointsD,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ width: 2,
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制角度文本 (右侧髋臼)
|
|
|
+ const textLinesD = [`右侧: ${cachedStats.angleRight}°`];
|
|
|
+ const textDUID = `${annotationUID}-angleD-text-right`;
|
|
|
+ const textBoxPositionD: CoreTypes.Point2 = data.handles.textBoxRight || [
|
|
|
+ intersectionDCanvas[0] + 10,
|
|
|
+ intersectionDCanvas[1] + 20,
|
|
|
+ ];
|
|
|
+
|
|
|
+ drawLinkedTextBox(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ textDUID,
|
|
|
+ textLinesD,
|
|
|
+ textBoxPositionD,
|
|
|
+ [intersectionDCanvas],
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 165, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制手柄点
|
|
|
+ if (annotationUID) {
|
|
|
+ const handleGroupUID = '0';
|
|
|
+ drawHandles(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ handleGroupUID,
|
|
|
+ canvasPoints,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 255)',
|
|
|
+ handleRadius: 8,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ renderStatus = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return renderStatus;
|
|
|
+ };
|
|
|
+}
|