瀏覽代碼

修改心锥比工具,实现 **位置端点**(点6):拖拽时,可以任意移动,固定端点自动调整沿平行于长轴的方向移动,以保持连线垂直。

sw 1 月之前
父節點
當前提交
363e85355d
共有 2 個文件被更改,包括 138 次插入89 次删除
  1. 41 38
      src/components/measures/VHSMeasurementTool.README.md
  2. 97 51
      src/components/measures/VHSMeasurementTool.ts

+ 41 - 38
src/components/measures/VHSMeasurementTool.README.md

@@ -138,55 +138,58 @@ const vhs = longAxisCount + shortAxisCount;    // VHS
 ### 2.4 垂直约束实现
 
 #### **核心概念**
-当拖动短轴的点(点5或点6)时,需要保持短轴垂直于长轴
+无论拖动长轴还是短轴的点,都需要保持短轴垂直于长轴。短轴分为固定端点和位置端点:
+- **固定端点**(点5):拖拽时,只能沿垂直于长轴的方向移动,位置端点不动。
+- **位置端点**(点6):拖拽时,可以任意移动,固定端点自动调整沿平行于长轴的方向移动,以保持连线垂直。
 
-#### **Step 1: 计算长轴方向向量**
-```typescript
-// 长轴向量
-const longAxisVector = vectorSubtract(points[3], points[2]);
+#### **情况1: 拖动长轴点(点3或点4)**
 
-// 归一化(单位化)
-const longAxisLength = Math.sqrt(
-  longAxisVector[0] ** 2 + 
-  longAxisVector[1] ** 2 + 
-  longAxisVector[2] ** 2
-);
-const normalizedLongAxis = vectorScale(longAxisVector, 1 / longAxisLength);
-```
+当拖动长轴端点时,需要实时更新短轴位置以保持垂直关系(与之前相同)。
+
+#### **情况2: 拖动短轴固定端点(点5)**
+
+固定端点只能沿垂直于长轴的方向移动,位置端点保持不动:
 
-#### **Step 2: 计算垂直向量**
 ```typescript
-// 2D平面的垂直向量: 如果向量是(x, y, z),垂直向量是(-y, x, 0)
-// 因为在视图平面内,z通常为0或保持不变
+// 计算长轴方向向量和垂直向量
+const longAxisVector = vectorSubtract(points[3], points[2]);
 const perpendicularVector: Point3 = [
-  -normalizedLongAxis[1],
-  normalizedLongAxis[0],
-  0  // 假设在2D视图平面
+  -longAxisVector[1],
+  longAxisVector[0],
+  0
 ];
-```
 
-#### **Step 3: 拖动时应用垂直约束**
-```typescript
-// 当拖动点5或点6时:
-// 1. 计算短轴中点
-const shortAxisMidpoint = calculateMidpoint(points[4], points[5]);
-
-// 2. 计算当前短轴长度
-const currentShortAxisLength = calculateDistance(points[4], points[5]);
-
-// 3. 如果拖动点5,更新点5使其沿垂直方向
-// 保持短轴长度,沿垂直向量放置
-const halfLength = currentShortAxisLength / 2;
-points[4] = vectorAdd(
-  shortAxisMidpoint, 
-  vectorScale(perpendicularVector, -halfLength)
-);
+// 计算拖拽向量在垂直方向上的投影
+const dragVector = vectorSubtract(newPosition, points[5]);
+const projection = 
+  dragVector[0] * perpendicularVector[0] + 
+  dragVector[1] * perpendicularVector[1];
+
+// 更新固定端点位置
 points[5] = vectorAdd(
-  shortAxisMidpoint, 
-  vectorScale(perpendicularVector, halfLength)
+  points[5],
+  vectorScale(perpendicularVector, projection)
 );
 ```
 
+**关键点**:
+- 位置端点(点6)保持不动。
+- 固定端点(点5)只在垂直方向移动。
+
+#### **情况3: 拖动短轴位置端点(点6)**
+
+位置端点可以任意移动,固定端点自动调整沿平行于长轴的方向移动,以保持连线垂直:
+
+核心思路:
+1. 位置端点有旧位置和新位置,可以计算出移动向量
+2. 知道长轴的向量
+3. 计算移动向量在长轴向量上的投影向量,即位置端点沿着长轴移动的方向和距离--长轴位移向量
+4. 把固定端点沿着“长轴位移向量”移动
+
+**关键点**:
+- 固定端点(点5)沿长轴方向移动。
+- 确保连线垂直于长轴。
+
 ### 2.5 初始点位置计算
 
 ```typescript

+ 97 - 51
src/components/measures/VHSMeasurementTool.ts

@@ -25,6 +25,7 @@ import {
   vectorAdd,
   vectorScale,
   calculateDistance,
+  dotProduct,
 } from './mathUtils';
 
 import {
@@ -49,7 +50,7 @@ interface VHSMeasurementAnnotation extends Types.Annotation {
     handles: {
       points: CoreTypes.Point3[];  // 6个点的世界坐标
       activeHandleIndex: number | null;
-      
+
     };
     textBox?: CoreTypes.Point2;   // 文本框位置(canvas坐标)
     cachedStats?: {
@@ -144,7 +145,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
       // 点1、2: 胸椎线(竖直)
       [spineX, spineY - spineLineLength / 2],
       [spineX, spineY + spineLineLength / 2],
-      
+
       // 点3、4: 心脏长轴(可配置角度)
       [
         heartCenterX - (longAxisLength / 2) * Math.cos(angleRad),
@@ -154,7 +155,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
         heartCenterX + (longAxisLength / 2) * Math.cos(angleRad),
         heartCenterY + (longAxisLength / 2) * Math.sin(angleRad)
       ],
-      
+
       // 点5、6: 心脏短轴(垂直于长轴)
       [
         heartCenterX - (shortAxisLength / 2) * Math.cos(perpAngleRad),
@@ -173,7 +174,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
 
     const camera = viewport.getCamera();
     const { viewPlaneNormal, viewUp } = camera;
-    
+
     if (viewPlaneNormal === undefined) {
       throw new Error('viewPlaneNormal is undefined');
     }
@@ -355,7 +356,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
   ): boolean {
     const [x, y] = canvasPoint;
     const [tx, ty] = textBoxPosition;
-    
+
     const margin = 10;
     return (
       x >= tx - margin &&
@@ -422,58 +423,103 @@ export default class VHSMeasurementTool extends AnnotationTool {
   ): void {
     const points = annotation.data.handles.points;
 
-    // 只对短轴点(索引4或5)应用垂直约束
-    if (draggedPointIndex !== 4 && draggedPointIndex !== 5) {
+    // 情况1: 拖拽长轴点(索引2或3) - 更新长轴并重新计算短轴保持垂直
+    if (draggedPointIndex === 2 || draggedPointIndex === 3) {
+      // 更新被拖拽的长轴点
       points[draggedPointIndex] = newPosition;
-      return;
-    }
 
-    // 计算长轴方向向量
-    const longAxisVector = vectorSubtract(points[3], points[2]);
-    const longAxisLength = calculateDistance(points[2], points[3]);
+      // 计算新的长轴方向向量
+      const longAxisVector = vectorSubtract(points[3], points[2]);
+      const longAxisLength = calculateDistance(points[2], points[3]);
+
+      if (longAxisLength === 0) {
+        // 长轴长度为0,无法计算垂直方向
+        return;
+      }
+
+      // 归一化长轴向量
+      const normalizedLongAxis = vectorScale(longAxisVector, 1 / longAxisLength);
+
+      // 计算垂直向量(在2D平面内)
+      const perpendicularVector: Point3 = [
+        -normalizedLongAxis[1],
+        normalizedLongAxis[0],
+        0
+      ];
+
+      // 获取短轴的当前中点和长度
+      const shortAxisMidpoint = calculateMidpoint(points[4], points[5]);
+      const currentShortAxisLength = calculateDistance(points[4], points[5]);
+      const shortAxisHalfLength = currentShortAxisLength / 2;
+
+      // 重新计算短轴的两个点,使其垂直于新的长轴方向
+      points[4] = vectorAdd(
+        shortAxisMidpoint,
+        vectorScale(perpendicularVector, -shortAxisHalfLength)
+      );
+      points[5] = vectorAdd(
+        shortAxisMidpoint,
+        vectorScale(perpendicularVector, shortAxisHalfLength)
+      );
 
-    if (longAxisLength === 0) {
-      // 长轴长度为0,无法计算垂直方向
-      points[draggedPointIndex] = newPosition;
       return;
     }
 
-    // 归一化长轴向量
-    const normalizedLongAxis = vectorScale(longAxisVector, 1 / longAxisLength);
+    // 情况2: 拖拽短轴固定端点(索引4) - 只能沿垂直于长轴的方向移动,位置端点不动
+    if (draggedPointIndex === 4) {
+      // 计算长轴方向向量
+      const longAxisVector = vectorSubtract(points[3], points[2]);
+      const longAxisLength = calculateDistance(points[2], points[3]);
 
-    // 计算垂直向量(在2D平面内)
-    const perpendicularVector: Point3 = [
-      -normalizedLongAxis[1],
-      normalizedLongAxis[0],
-      0
-    ];
+      if (longAxisLength === 0) {
+        // 长轴长度为0,无法计算垂直方向
+        points[draggedPointIndex] = newPosition;
+        return;
+      }
+
+      // 计算垂直向量(在2D平面内)
+      const perpendicularVector: Point3 = [
+        -longAxisVector[1],
+        longAxisVector[0],
+        0
+      ];
 
-    // 计算短轴的当前中点
-    const shortAxisMidpoint = calculateMidpoint(points[4], points[5]);
+      // 计算拖拽向量在垂直方向上的投影
+      const dragVector = vectorSubtract(newPosition, points[4]);
+      const projection = dotProduct(dragVector, perpendicularVector) / dotProduct(perpendicularVector, perpendicularVector);
 
-    // 计算新拖拽位置到短轴中点的向量
-    const dragVector = vectorSubtract(newPosition, shortAxisMidpoint);
+      // 更新固定端点位置
+      points[4] = vectorAdd(points[4], vectorScale(perpendicularVector, projection));
 
-    // 计算沿垂直方向的投影长度(点积)
-    const projectionLength = 
-      dragVector[0] * perpendicularVector[0] + 
-      dragVector[1] * perpendicularVector[1];
+      return;
+    }
 
-    // 计算新的短轴长度(保持拖拽意图)
-    const newShortAxisHalfLength = Math.abs(projectionLength);
+    // 情况3: 拖拽短轴位置端点(索引5) - 固定端点沿平行于长轴的方向移动
+    if (draggedPointIndex === 5) {
+      // 旧的 位置端点
+      const oldPositon = points[5]
+      // 更新位置端点到新位置
+      points[5] = newPosition;
+      // 位置端点的位移向量
+      const deltaP = vectorAdd(newPosition, vectorScale(oldPositon, -1))
 
-    // 根据拖拽的方向决定短轴的方向
-    const direction = projectionLength >= 0 ? 1 : -1;
 
-    // 更新短轴两个点,使其沿垂直方向
-    points[4] = vectorAdd(
-      shortAxisMidpoint,
-      vectorScale(perpendicularVector, -direction * newShortAxisHalfLength)
-    );
-    points[5] = vectorAdd(
-      shortAxisMidpoint,
-      vectorScale(perpendicularVector, direction * newShortAxisHalfLength)
-    );
+      // 计算长轴方向向量
+      const longAxisVector = vectorSubtract(points[3], points[2]);
+      const longAxisLength = calculateDistance(points[2], points[3]);
+      if (longAxisLength === 0) {
+        // 长轴长度为0,无法计算
+        return;
+      }
+      // 位置端点在长轴向量 longAxisVector 上的位移向量
+      const 投影向量 = vectorScale(longAxisVector, dotProduct(deltaP, longAxisVector) / dotProduct(longAxisVector, longAxisVector))
+      // 把固定端点也按照以上的  位移向量 移动就行了
+      points[4] = vectorAdd(points[4], 投影向量)
+      return
+    }
+
+    // 情况4: 拖拽其他点(胸椎线点0或1) - 直接更新,不影响心脏轴线
+    points[draggedPointIndex] = newPosition;
   }
 
   /**
@@ -592,7 +638,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
     const { viewport } = enabledElement;
 
     const annotations = annotation.state.getAnnotations(this.getToolName(), element);
-    
+
     if (!annotations || annotations.length === 0) {
       return;
     }
@@ -900,7 +946,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
         } as Types.ToolHandle;
       }
     }
-    
+
     annotation.data.handles.activeHandleIndex = null;
     return undefined;
   }
@@ -1015,8 +1061,8 @@ export default class VHSMeasurementTool extends AnnotationTool {
       const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
 
       // 确定线段颜色(选中蓝色,未选中白色)
-      const lineColor = annotation.highlighted 
-        ? 'rgb(0, 120, 255)' 
+      const lineColor = annotation.highlighted
+        ? 'rgb(0, 120, 255)'
         : 'rgb(255, 255, 255)';
 
       // 绘制胸椎长度线(点1-2)
@@ -1096,14 +1142,14 @@ export default class VHSMeasurementTool extends AnnotationTool {
           `S: ${cachedStats.shortAxisCount.toFixed(2)}`,
           `L/S: ${cachedStats.ratio.toFixed(2)}`
         ];
-        
+
         // 文本框默认位置在长轴右侧
-        const defaultTextBoxPosition: CoreTypes.Point2 = 
+        const defaultTextBoxPosition: CoreTypes.Point2 =
           data.textBox || [
             canvasPoints[3][0] + 20,
             canvasPoints[3][1]
           ];
-        
+
         const textUID = `${annotationUID}-text-box`;
         drawLinkedTextBox(
           svgDrawingHelper,