ソースを参照

1. 鼠标悬停在文本上时,光标是手型;
2. 鼠标悬停在线段或者手柄(端点)上时,光标是**十字准星光标**
3. 鼠标在文本上接下时,可拖拽移动文本
4. 鼠标在线段上按下时,可以移动整个测量工具
5. 鼠标在端点(手柄)上按下时,可拖拽移动端点(手柄)
6. 鼠标点击在工具的任何位置时,选中整个工具,相当于工具得到焦点
7. 鼠标点击在其他位置时,工具失去焦点,失去选中状态

sw 1 ヶ月 前
コミット
fd96f6ef65

+ 44 - 6
src/components/measures/VHSMeasurementTool.README.md

@@ -38,8 +38,10 @@ VHS = L + S (心椎比指数)
 ### 1.4 交互规则
 - **6个可拖拽控制点**: 实时更新线段和计算值
 - **显示值**: VHS, L, S, L/S (黄色标注, 可自由拖动)
-- **选中状态**: 线段蓝色, 控制点白色圆圈
-- **非选中状态**: 线段白色, 控制点白色圆圈
+- **悬停状态**: 鼠标悬停在文本框上时,光标变为手型 (pointer),线段高亮为黄色;悬停在端点或线段上时,光标变为移动光标 (move),线段高亮为黄色,端点半径保持正常 (6px)
+- **整个工具移动**: 鼠标在线段上按下时,可以拖拽整个测量工具,所有点和文本框一起移动
+- **选中状态**: 线段蓝色, 控制点白色圆圈, 端点半径变大 (12px)
+- **非选中状态**: 线段白色, 控制点白色圆圈, 端点半径正常 (6px)
 
 ---
 
@@ -270,9 +272,14 @@ renderAnnotation = (
   for (const ann of annotations) {
     const { points } = ann.data.handles;
     const canvasPoints = points.map(p => viewport.worldToCanvas(p));
-    
-    // 确定线段颜色(选中蓝色,未选中白色)
-    const lineColor = ann.highlighted ? 'rgb(0, 120, 255)' : 'rgb(255, 255, 255)';
+
+    // 确定线段颜色和状态
+    const isSelected = this.editData && this.editData.annotation === ann;
+    const lineColor = isSelected
+      ? VHSMeasurementTool.SELECTED_COLOR
+      : ann.highlighted
+      ? VHSMeasurementTool.HOVER_COLOR
+      : 'rgb(255, 255, 255)';
     
     // 绘制胸椎长度线(点1-2)
     drawLineSvg(
@@ -310,7 +317,10 @@ renderAnnotation = (
       annotationUID,
       'handles',
       canvasPoints,
-      { color: 'rgb(255, 255, 255)', handleRadius: 6 }
+      {
+        color: 'rgb(255, 255, 255)',
+        handleRadius: isSelected ? 12 : 6
+      }
     );
     
     // 绘制文本框(黄色)
@@ -450,6 +460,34 @@ const newTextBoxPosition = [
 ];
 ```
 
+### 4.3 鼠标移动事件阻止导致文本框拖拽失效
+
+**问题**: 在添加悬停检测后,鼠标移动事件被阻止,导致文本框拖拽功能失效。
+
+**原因**: 在 `_mouseMoveModifyCallback` 中添加了 `evt.preventDefault(); evt.stopPropagation();` 来阻止事件传播,但这也阻止了拖拽事件的正常触发。
+
+**解决方案**: 
+1. 移除鼠标移动事件中的事件阻止,让拖拽事件正常工作。
+2. 修改文本框悬停检测,使用与渲染相同的逻辑计算文本框位置。
+3. 对于文本框点击,不阻止事件,让拖拽事件正常开始。
+
+**修复后的代码**:
+```typescript
+// 在鼠标移动回调中,移除事件阻止
+// 悬停检测只设置光标和状态,不阻止事件
+if (this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+  element.style.cursor = 'pointer';
+  customAnn.highlighted = true;
+  isHovering = true;
+}
+
+// 在鼠标按下回调中,对于文本框点击,不阻止事件
+if (this._isPointInTextBox(canvasCoords, textBoxPosition)) {
+  // 设置 editData
+  // 不调用 evt.preventDefault(); evt.stopPropagation();
+}
+```
+
 ### 4.3 除以零的处理
 
 **问题**: 当胸椎线长度为0时,V=0,会导致除以零错误

+ 216 - 10
src/components/measures/VHSMeasurementTool.ts

@@ -93,6 +93,10 @@ export interface VHSExportedData {
 export default class VHSMeasurementTool extends AnnotationTool {
   static toolName = 'VHSMeasurementTool';
 
+  // 高亮颜色定义,方便修改
+  static readonly HOVER_COLOR = 'rgb(255, 255, 0)'; // 悬停时线段颜色:黄色
+  static readonly SELECTED_COLOR = 'rgb(0, 120, 255)'; // 选中时线段颜色:蓝色
+
   editData: {
     annotation: Types.Annotation;
     viewportIdsToRender: string[];
@@ -101,6 +105,8 @@ export default class VHSMeasurementTool extends AnnotationTool {
     hasMoved?: boolean;
     textBoxBeingMoved?: boolean;
     textBoxOffset?: CoreTypes.Point2;
+    toolBeingMoved?: boolean;
+    initialCanvasPosition?: CoreTypes.Point2;
   } | null = null;
 
   isDrawing: boolean = false;
@@ -603,6 +609,10 @@ export default class VHSMeasurementTool extends AnnotationTool {
       'CORNERSTONE_TOOLS_MOUSE_UP',
       this._mouseUpModifyCallback as EventListener
     );
+    element.addEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveModifyCallback as EventListener
+    );
   }
 
   /**
@@ -621,6 +631,10 @@ export default class VHSMeasurementTool extends AnnotationTool {
       'CORNERSTONE_TOOLS_MOUSE_UP',
       this._mouseUpModifyCallback as EventListener
     );
+    element.removeEventListener(
+      'CORNERSTONE_TOOLS_MOUSE_MOVE',
+      this._mouseMoveModifyCallback as EventListener
+    );
   }
 
   /**
@@ -649,8 +663,15 @@ export default class VHSMeasurementTool extends AnnotationTool {
       const targetId = this.getTargetId(viewport);
       const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
 
-      if (cachedStats && customAnn.data.textBox) {
-        const textBoxPosition = customAnn.data.textBox;
+      if (cachedStats) {
+        // 计算文本框位置(与渲染逻辑一致)
+        const points = customAnn.data.handles.points;
+        const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+        const defaultTextBoxPosition: CoreTypes.Point2 = [
+          canvasPoints[3][0] + 20,
+          canvasPoints[3][1]
+        ];
+        const textBoxPosition = customAnn.data.textBox || defaultTextBoxPosition;
 
         if (this._isPointInTextBox(canvasCoords, textBoxPosition)) {
           const viewportIdsToRender =
@@ -671,12 +692,14 @@ export default class VHSMeasurementTool extends AnnotationTool {
             textBoxBeingMoved: true,
             textBoxOffset: textBoxOffset,
           };
+          console.log(`文本框开始移动--`);
 
           customAnn.highlighted = true;
           utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
 
-          evt.preventDefault();
-          evt.stopPropagation();
+          // 不阻止事件,让拖拽事件正常开始
+          // evt.preventDefault();
+          // evt.stopPropagation();
           return;
         }
       }
@@ -717,6 +740,42 @@ export default class VHSMeasurementTool extends AnnotationTool {
         return;
       }
     }
+
+    // 如果没有点击手柄,检查是否点击在线段上(用于整个工具移动)
+    for (const ann of annotations) {
+      const customAnn = ann as VHSMeasurementAnnotation;
+
+      // 检查是否靠近线段(但不是手柄)
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 6)) {
+        // 额外检查是否真的靠近线段而不是手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (!handle) {
+          const viewportIdsToRender =
+            utilities.viewportFilters.getViewportIdsWithToolToRender(
+              element,
+              this.getToolName()
+            );
+
+          this.editData = {
+            annotation: customAnn,
+            viewportIdsToRender,
+            hasMoved: false,
+            toolBeingMoved: true,
+            initialCanvasPosition: canvasCoords,
+          };
+
+          customAnn.highlighted = true;
+
+          utilities.triggerAnnotationRenderForViewportIds(
+            viewportIdsToRender
+          );
+
+          evt.preventDefault();
+          evt.stopPropagation();
+          return;
+        }
+      }
+    }
   };
 
   /**
@@ -736,10 +795,55 @@ export default class VHSMeasurementTool extends AnnotationTool {
       return;
     }
 
-    const { annotation: ann, viewportIdsToRender, textBoxBeingMoved } = this.editData;
+    const { annotation: ann, viewportIdsToRender, textBoxBeingMoved, toolBeingMoved, initialCanvasPosition } = this.editData;
     const customAnn = ann as VHSMeasurementAnnotation;
     const { data } = customAnn;
 
+    // 如果正在拖拽整个工具
+    if (toolBeingMoved && initialCanvasPosition) {
+      // 计算位移
+      const deltaX = canvasCoords[0] - initialCanvasPosition[0];
+      const deltaY = canvasCoords[1] - initialCanvasPosition[1];
+
+      // 将所有点转换为canvas坐标,应用位移,然后转换回世界坐标
+      const points = data.handles.points;
+      for (let i = 0; i < points.length; i++) {
+        const canvasPoint = enabledElement.viewport.worldToCanvas(points[i]);
+        const newCanvasPoint: CoreTypes.Point2 = [
+          canvasPoint[0] + deltaX,
+          canvasPoint[1] + deltaY
+        ];
+        points[i] = enabledElement.viewport.canvasToWorld(newCanvasPoint);
+      }
+      // 初始位置经过拖拽后,也需要更新,否则会造成移动距离的累加效果
+      if (this.editData.initialCanvasPosition) {
+        this.editData.initialCanvasPosition[0] = this.editData.initialCanvasPosition[0] + deltaX;
+        this.editData.initialCanvasPosition[1] = this.editData.initialCanvasPosition[1] + deltaY;
+      }
+
+
+      // 更新文本框位置(如果存在)
+      if (data.textBox) {
+        data.textBox = [
+          data.textBox[0] + deltaX,
+          data.textBox[1] + deltaY
+        ];
+      }
+
+      // 重新计算统计数据
+      this._updateCachedStats(customAnn, enabledElement);
+
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(
+        viewportIdsToRender
+      );
+
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
     // 如果正在拖拽文本框
     if (textBoxBeingMoved && this.editData.textBoxOffset) {
       const newTextBoxPosition: CoreTypes.Point2 = [
@@ -781,6 +885,105 @@ export default class VHSMeasurementTool extends AnnotationTool {
     }
   };
 
+  /**
+   * 鼠标移动回调 - 用于修改模式,处理悬停检测
+   */
+  _mouseMoveModifyCallback = (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) {
+      // 没有注解时,重置光标
+      element.style.cursor = 'default';
+      return;
+    }
+
+    let isHovering = false;
+
+    // 检查是否悬停在文本框上
+    for (const ann of annotations) {
+      const customAnn = ann as VHSMeasurementAnnotation;
+      const targetId = this.getTargetId(viewport);
+      const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
+
+      if (cachedStats) {
+        // 计算文本框位置(与渲染逻辑一致)
+        const points = customAnn.data.handles.points;
+        const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
+        const defaultTextBoxPosition: CoreTypes.Point2 = [
+          canvasPoints[3][0] + 20,
+          canvasPoints[3][1]
+        ];
+        const textBoxPosition = customAnn.data.textBox || defaultTextBoxPosition;
+
+        if (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 VHSMeasurementAnnotation;
+
+        // 检查是否靠近手柄
+        const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
+        if (handle) {
+          // 悬停在手柄上,光标为移动光标
+          element.style.cursor = 'move';
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+
+        // 检查是否靠近线段
+        if (this.isPointNearTool(element, customAnn, canvasCoords, 6)) {
+          // 悬停在线段上,光标为移动光标
+          element.style.cursor = 'move';
+          customAnn.highlighted = true;
+          isHovering = true;
+          break;
+        }
+      }
+    }
+
+    // 如果没有悬停在任何地方,重置高亮和光标
+    if (!isHovering) {
+      for (const ann of annotations) {
+        const customAnn = ann as VHSMeasurementAnnotation;
+        customAnn.highlighted = false;
+      }
+      element.style.cursor = 'default';
+    }
+
+    // 触发渲染以更新高亮状态
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+
+    // 移除事件阻止,让拖拽事件正常工作
+    // evt.preventDefault();
+    // evt.stopPropagation();
+  };
+
   /**
    * 鼠标释放回调 - 用于修改模式
    */
@@ -1060,10 +1263,13 @@ export default class VHSMeasurementTool extends AnnotationTool {
       // 转换所有点为 canvas 坐标
       const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
 
-      // 确定线段颜色(选中蓝色,未选中白色)
-      const lineColor = annotation.highlighted
-        ? 'rgb(0, 120, 255)'
-        : 'rgb(255, 255, 255)';
+      // 确定线段颜色和状态
+      const isSelected = this.editData && this.editData.annotation === annotation;
+      const lineColor = isSelected
+        ? VHSMeasurementTool.SELECTED_COLOR
+        : annotation.highlighted
+          ? VHSMeasurementTool.HOVER_COLOR
+          : 'rgb(255, 255, 255)';
 
       // 绘制胸椎长度线(点1-2)
       if (points.length >= 2 && annotationUID) {
@@ -1129,7 +1335,7 @@ export default class VHSMeasurementTool extends AnnotationTool {
           canvasPoints,
           {
             color: 'rgb(255, 255, 255)',
-            handleRadius: 6,
+            handleRadius: isSelected ? 12 : 6,
           }
         );
       }