Jelajahi Sumber

更新TTA的实现,原来平行线平行于M1M2,现在是平行于MM1,M点是M2M3的中点

dengdx 2 bulan lalu
induk
melakukan
d800e38693

+ 104 - 8
src/components/measures/TTAMeasurementTool.README.md

@@ -18,7 +18,7 @@
 1. **拟合圆1**: 通过点(1, 2, 3)拟合胫骨平台圆,得到圆心M1
 2. **拟合圆2**: 通过点(4, 5, 6)拟合一侧髁骨圆,得到圆心M2
 3. **拟合圆3**: 通过点(7, 8, 9)拟合另一侧髁骨圆,得到圆心M3
-4. **计算中点M**: 连接M1M2,得到其中点M
+4. **计算中点M**: 连接M2M3,得到其中点M
 5. **建立参考线**: 连接MM1,形成参考线方向
 6. **作平行线L**: 在点10处作一条与MM1平行的直线L
 7. **计算TTA**: TTA距离 = 点11到直线L的垂直距离
@@ -97,7 +97,7 @@
 | 线段绘制 | `drawLine(svgHelper, uid, lineId, start, end, options)` | 绘制M1M2、MM1、平行线L |
 | 点的绘制 | `drawHandles(svgHelper, uid, groupId, points, options)` | 绘制11个可拖拽手柄 |
 | 文本绘制 | `drawLinkedTextBox(...)` | 显示TTA距离值 |
-| 虚线绘制 | `lineDash: '4,4'` | 用于平行线L的虚线样式 |
+| 实线绘制 | 默认样式 | 平行线L和垂线都使用实线 |
 
 ### 3.2 数据结构设计
 
@@ -157,8 +157,8 @@ interface TTAMeasurementAnnotation extends Types.Annotation {
 | 髁骨圆2 | 绿色 | 虚线 | 拟合圆3 |
 | M1M2线段 | 橙色 | 实线 | 连接两个圆心 |
 | MM1线段 | 橙色 | 实线 | 参考线 |
-| 平行线L | 黄色 | 线 | 通过点10的平行线 |
-| 垂直线 | 紫色 | 线 | 点11到平行线L的垂线 |
+| 平行线L | 黄色 | 线 | 通过点10的平行线 |
+| 垂直线 | 紫色 | 线 | 点11到平行线L的垂线 |
 | 圆心标记 | 白色 | 小十字 | M1, M2, M3 |
 | TTA距离文本 | 白色 | 带背景 | 显示数值 |
 
@@ -170,10 +170,106 @@ interface TTAMeasurementAnnotation extends Types.Annotation {
    - M1M2线段(橙色实线)
    - 中点M的标记
    - MM1线段(橙色实线)
-4. **平行线L**: 通过点10的平行线(黄色虚线)
-5. **垂直线**: 点11到平行线L的垂线(紫色虚线)
-6. **手柄点**: 11个可拖拽的点
-7. **文本**: TTA距离值
+4. **平行线L**: 通过点10的平行线(黄色实线)
+5. **测量线段**: 点10到垂足的实线(黄色,突出显示测量线段)
+6. **垂直线**: 点11到垂足的垂线(紫色实线)
+7. **手柄点**: 11个可拖拽的点
+8. **文本**: TTA距离值
+
+### 4.3 TTA测量几何关系绘制思路
+
+#### **绘制逻辑分析**
+
+在TTA测量中,核心是计算点11到平行线L的垂直距离。为了更清楚地展示测量几何关系,我们需要绘制:
+
+1. **平行线L**: 通过点10,与MM1方向平行的参考线
+2. **垂足计算**: 点11到平行线L的垂线与平行线L的交点
+3. **测量线段**: 从点10到垂足的实线,表示测量基准线段
+4. **垂直线**: 从点11到垂足的实线,表示垂直距离
+
+#### **具体实现步骤**
+
+```typescript
+// 1. 计算垂足(交点)
+const AP = vectorSubtract(points[10], point10);  // 向量 P10->P11
+const dotProduct = AP[0] * lineDirection[0] + AP[1] * lineDirection[1];
+const directionMagSq = lineDirection[0]**2 + lineDirection[1]**2;
+const t = dotProduct / directionMagSq;
+
+// 垂足坐标 = 点10 + t * 方向向量
+const footPoint = [
+  point10[0] + t * lineDirection[0],
+  point10[1] + t * lineDirection[1],
+  point10[2]
+];
+
+// 2. 绘制测量线段(点10到垂足)
+drawLineSvg(svgDrawingHelper, annotationUID, 'measurementLine',
+  point10Canvas, footPointCanvas,
+  { color: colors.point10, width: 3 }  // 黄色实线,较粗
+);
+
+// 3. 绘制垂直线(点11到垂足)
+drawLineSvg(svgDrawingHelper, annotationUID, 'perpLine',
+  point11Canvas, footPointCanvas,
+  { color: colors.perpLine, width: 2 }  // 紫色实线
+);
+```
+
+#### **视觉效果说明**
+
+- **平行线L**: 黄色实线,作为测量基准线
+- **测量线段**: 黄色实线,线宽3,突出显示从点10到垂足的距离
+- **垂直线**: 紫色实线,线宽2,显示点11到垂足的垂直距离
+
+这样的绘制方式清晰地展示了:
+1. 平行线L是测量基准
+2. 垂足是垂直线的交点
+3. 点10到垂足的线段是测量线段
+4. 点11到垂足的线段是垂直距离
+
+#### **交互意义**
+
+- 拖拽点10:改变平行线L的位置
+- 拖拽点11:改变垂足位置,实时更新TTA距离
+- 垂线始终垂直于平行线L
+- 测量线段实时反映当前的测量基准
+
+#### **平行线L几何关系修正**
+
+**问题**: 最初的实现中,平行线L被绘制为无限长的直线,但根据TTA测量几何关系,平行线L应该是从点10到垂足的线段。
+
+**几何关系**: 平行线L是通过点10,与MM1方向平行的直线。垂足是点11到平行线L的垂线与平行线L的交点。因此平行线L应该只绘制从点10到垂足的部分。
+
+**修复**: 修改绘制逻辑,让平行线L只绘制从点10到垂足的线段。
+
+```typescript
+// 修正后的代码:平行线L从点10到垂足
+if (cachedStats?.parallelLine && points.length >= 11 && annotationUID) {
+  const point10Canvas = canvasPoints[9];
+  const point10 = points[9];
+  const lineDirection = cachedStats.parallelLine.direction;
+
+  // 计算垂足
+  const AP = vectorSubtract(points[10], point10);
+  const dotProduct = AP[0] * lineDirection[0] + AP[1] * lineDirection[1];
+  const directionMagSq = lineDirection[0] * lineDirection[0] + lineDirection[1] * lineDirection[1];
+  const t = dotProduct / directionMagSq;
+
+  const footPoint = [
+    point10[0] + t * lineDirection[0],
+    point10[1] + t * lineDirection[1],
+    point10[2]
+  ] as CoreTypes.Point3;
+  const footPointCanvas = viewport.worldToCanvas(footPoint);
+
+  // 绘制从点10到垂足的平行线L
+  drawLineSvg(svgDrawingHelper, annotationUID, 'parallelLine',
+    point10Canvas, footPointCanvas,
+    { color: colors.parallelLine, width: 2 }
+  );
+}
+```
 
 ---
 

+ 82 - 42
src/components/measures/TTAMeasurementTool.ts

@@ -300,10 +300,10 @@ export default class TTAMeasurementTool extends AnnotationTool {
       },
       textBoxPosition: annotation.data.textBox,
       metadata: {
-        viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
-        viewUp: annotation.metadata?.viewUp || [0, 1, 0],
-        FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
-        referencedImageId: annotation.metadata?.referencedImageId || '',
+        viewPlaneNormal: annotation.metadata.viewPlaneNormal || [0, 0, 1],
+        viewUp: annotation.metadata.viewUp || [0, 1, 0],
+        FrameOfReferenceUID: annotation.metadata.FrameOfReferenceUID || '',
+        referencedImageId: annotation.metadata.referencedImageId || '',
       },
     };
   }
@@ -673,11 +673,11 @@ export default class TTAMeasurementTool extends AnnotationTool {
       }
 
       // 优先级3: 检测线段悬停
-      if (cachedStats?.tibialCircle && cachedStats?.condyleCircle1) {
-        const M1 = cachedStats.tibialCircle.center;
+      if (cachedStats?.condyleCircle1 && cachedStats?.condyleCircle2) {
         const M2 = cachedStats.condyleCircle1.center;
-        
-        if (this._isPointNearLine(canvasCoords, M1, M2, viewport, 10)) {
+        const M3 = cachedStats.condyleCircle2.center;
+
+        if (this._isPointNearLine(canvasCoords, M2, M3, viewport, 10)) {
           element.style.cursor = cursorUrl;
           customAnn.highlighted = true;
           isHovering = true;
@@ -687,13 +687,37 @@ export default class TTAMeasurementTool extends AnnotationTool {
 
       if (cachedStats?.parallelLine && customAnn.data.handles.points.length >= 10) {
         const point10 = customAnn.data.handles.points[9];
-        const lineEnd = vectorAdd(point10, cachedStats.parallelLine.direction);
-        
-        if (this._isPointNearLine(canvasCoords, point10, lineEnd, viewport, 10)) {
-          element.style.cursor = cursorUrl;
-          customAnn.highlighted = true;
-          isHovering = true;
-          break;
+        const lineDirection = cachedStats.parallelLine.direction;
+
+        // 计算方向向量的长度
+        const directionLength = Math.sqrt(
+          lineDirection[0] * lineDirection[0] + lineDirection[1] * lineDirection[1]
+        );
+
+        if (directionLength > 0) {
+          // 归一化方向向量
+          const normalizedDirection = [
+            lineDirection[0] / directionLength,
+            lineDirection[1] / directionLength,
+            0
+          ] as CoreTypes.Point3;
+
+          // 计算平行线的端点(从point10开始,沿着方向延伸100px)
+          const lineLength = 100; // 像素长度
+          const worldLineLength = lineLength / viewport.getZoom(); // 转换为世界坐标长度
+
+          const lineEndPoint = [
+            point10[0] + normalizedDirection[0] * worldLineLength,
+            point10[1] + normalizedDirection[1] * worldLineLength,
+            point10[2]
+          ] as CoreTypes.Point3;
+
+          if (this._isPointNearLine(canvasCoords, point10, lineEndPoint, viewport, 10)) {
+            element.style.cursor = cursorUrl;
+            customAnn.highlighted = true;
+            isHovering = true;
+            break;
+          }
         }
       }
 
@@ -872,12 +896,12 @@ export default class TTAMeasurementTool extends AnnotationTool {
       const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
       const points = customAnn.data.handles.points;
 
-      // 检测 M1M2 连线
-      if (cachedStats?.tibialCircle && cachedStats?.condyleCircle1) {
-        const M1 = cachedStats.tibialCircle.center;
+      // 检测 M2M3 连线
+      if (cachedStats?.condyleCircle1 && cachedStats?.condyleCircle2) {
         const M2 = cachedStats.condyleCircle1.center;
-        
-        if (this._isPointNearLine(canvasCoords, M1, M2, viewport, 10)) {
+        const M3 = cachedStats.condyleCircle2.center;
+
+        if (this._isPointNearLine(canvasCoords, M2, M3, viewport, 10)) {
           this._setupWholeToolMove(customAnn, element, viewport, canvasCoords, points[0]);
           anyToolClicked = true;
           evt.preventDefault();
@@ -1318,8 +1342,8 @@ export default class TTAMeasurementTool extends AnnotationTool {
       const M2 = condyleCircle1.center;
       const M3 = condyleCircle2.center;
 
-      // 计算M1M2的中点M
-      const M = calculateMidpoint(M1, M2);
+      // 计算M2M3的中点M
+      const M = calculateMidpoint(M2, M3);
 
       // 计算MM1的方向向量
       const MM1_direction = vectorSubtract(M1, M);
@@ -1384,7 +1408,7 @@ export default class TTAMeasurementTool extends AnnotationTool {
         condyle2: 'rgb(0, 255, 0)',    // 绿色
         point10: 'rgb(255, 255, 0)',   // 黄色
         point11: 'rgb(255, 0, 255)',   // 紫色
-        m1m2Line: 'rgb(255, 165, 0)',  // 橙色
+        m3m2Line: 'rgb(255, 165, 0)',  // 橙色
         parallelLine: 'rgb(255, 255, 0)', // 黄色
         perpLine: 'rgb(255, 0, 255)',  // 紫色
         center: 'rgb(255, 255, 255)',  // 白色
@@ -1519,18 +1543,18 @@ export default class TTAMeasurementTool extends AnnotationTool {
         );
       }
 
-      // 绘制M1M2线段和中点
-      if (cachedStats?.tibialCircle && cachedStats?.condyleCircle1 && annotationUID) {
-        const M1Canvas = viewport.worldToCanvas(cachedStats.tibialCircle.center);
+      // 绘制M2M3线段和中点
+      if (cachedStats?.condyleCircle1 && cachedStats?.condyleCircle2 && annotationUID) {
         const M2Canvas = viewport.worldToCanvas(cachedStats.condyleCircle1.center);
+        const M3Canvas = viewport.worldToCanvas(cachedStats.condyleCircle2.center);
 
         drawLineSvg(
           svgDrawingHelper,
           annotationUID,
-          `${annotationUID}-M1M2Line`,
-          M1Canvas,
+          `${annotationUID}-M2M3Line`,
           M2Canvas,
-          { color: colors.m1m2Line, width: 2 }
+          M3Canvas,
+          { color: colors.m3m2Line, width: 2 }
         );
 
         if (cachedStats.midPoint) {
@@ -1541,7 +1565,7 @@ export default class TTAMeasurementTool extends AnnotationTool {
             `${annotationUID}-midPoint`,
             MCanvas,
             5,
-            { color: colors.m1m2Line, fill: colors.m1m2Line }
+            { color: colors.m3m2Line, fill: colors.m3m2Line }
           );
         }
       }
@@ -1562,35 +1586,51 @@ export default class TTAMeasurementTool extends AnnotationTool {
           `${annotationUID}-referenceLine`,
           startCanvas,
           endCanvas,
-          { color: colors.m1m2Line, width: 2 }
+          { color: colors.m3m2Line, width: 2 }
         );
       }
 
-      // 绘制平行线L(从点10到垂足
+      // 绘制平行线L(从点10开始,沿着MM1方向延伸固定长度
       if (cachedStats?.parallelLine && points.length >= 11 && annotationUID) {
         const point10Canvas = canvasPoints[9];
         const point10 = points[9];
         const lineDirection = cachedStats.parallelLine.direction;
 
-        // 计算垂足
-        const AP = vectorSubtract(points[10], point10);
-        const dotProduct = AP[0] * lineDirection[0] + AP[1] * lineDirection[1];
-        const directionMagSq = lineDirection[0] * lineDirection[0] + lineDirection[1] * lineDirection[1];
-        const t = dotProduct / directionMagSq;
+        // 计算方向向量的长度
+        const directionLength = Math.sqrt(
+          lineDirection[0] * lineDirection[0] + lineDirection[1] * lineDirection[1]
+        );
 
-        const footPoint = [
-          point10[0] + t * lineDirection[0],
-          point10[1] + t * lineDirection[1],
+        // 如果方向向量长度为0,则不绘制
+        if (directionLength === 0) {
+          return renderStatus;
+        }
+
+        // 归一化方向向量
+        const normalizedDirection = [
+          lineDirection[0] / directionLength,
+          lineDirection[1] / directionLength,
+          0
+        ] as CoreTypes.Point3;
+
+        // 计算平行线的端点(从point10开始,沿着方向延伸100px)
+        const lineLength = 100; // 像素长度
+        const worldLineLength = lineLength / viewport.getZoom(); // 转换为世界坐标长度
+
+        const lineEndPoint = [
+          point10[0] + normalizedDirection[0] * worldLineLength,
+          point10[1] + normalizedDirection[1] * worldLineLength,
           point10[2]
         ] as CoreTypes.Point3;
-        const footPointCanvas = viewport.worldToCanvas(footPoint);
+
+        const lineEndPointCanvas = viewport.worldToCanvas(lineEndPoint);
 
         drawLineSvg(
           svgDrawingHelper,
           annotationUID,
           `${annotationUID}-parallelLine`,
           point10Canvas,
-          footPointCanvas,
+          lineEndPointCanvas,
           {
             color: colors.parallelLine,
             width: 2,