Просмотр исходного кода

feat: 完善矩形灰度测量工具交互功能

- 新增线段精确检测功能,支持点击矩形边框进行整体拖拽
- 新增文本框独立拖拽功能,支持自由移动测量结果显示位置
- 实现智能光标反馈系统:文本框(pointer)、线段/手柄(crosshair)、内部(default)
- 实现选中状态下手柄半径动态调整(默认6  悬停9  选中12)
- 实现三级交互优先级:文本框  手柄  线段  内部区域
- 修复整体拖拽时坐标计算错误导致工具跑出视区的问题
- 优化矩形内部点击行为:仅选中,不触发拖拽

技术改进:
- 新增 _isPointNearLine 和 _isPointNearLineSegment 方法实现精确线段检测
- 新增 _isPointInTextBox 和 _checkTextBoxBounds 方法实现文本框检测
- 优化 _mouseDownModifyCallback 实现文本框、手柄、线段的优先级检测
- 优化 _mouseDragModifyCallback 支持文本框拖拽和修复整体拖拽坐标计算
- 优化 _mouseMoveModifyCallback 实现智能光标样式切换和悬停高亮
- 优化 renderAnnotation 根据选中和高亮状态动态调整手柄大小

改动文件:
- CHANGELOG.md (更新版本日志)
- package.json (版本号 1.4.2  1.5.0)
- src/components/measures/RectangleGrayscaleMeasurementTool.ts
dengdx 1 месяц назад
Родитель
Сommit
b876f99cf2
3 измененных файлов с 260 добавлено и 19 удалено
  1. 33 0
      CHANGELOG.md
  2. 1 1
      package.json
  3. 226 18
      src/components/measures/RectangleGrayscaleMeasurementTool.ts

+ 33 - 0
CHANGELOG.md

@@ -21,6 +21,39 @@
 
 ---
 
+## [1.5.0] - 2025-12-11
+
+### 新增 (Added)
+- **矩形灰度测量工具完整交互功能** ([#rectangle-grayscale-interaction](src/components/measures/RectangleGrayscaleMeasurementTool.ts))
+  - 实现线段精确检测功能,支持点击矩形边框进行整体拖拽
+  - 实现文本框独立拖拽功能,支持自由移动测量结果显示位置
+  - 实现智能光标反馈系统:
+    - 文本框悬停显示手型光标(pointer)
+    - 线段和手柄悬停显示十字准星光标(crosshair)
+    - 矩形内部显示默认光标
+  - 实现选中状态下手柄半径动态调整(默认6 → 悬停9 → 选中12)
+  - 实现三级交互优先级:文本框 → 手柄 → 线段 → 内部区域
+  - 优化矩形内部点击行为:仅选中,不触发拖拽
+
+### 修复 (Fixed)
+- **矩形拖拽坐标计算问题修复**
+  - 修复整体拖拽时坐标计算错误导致工具跑出视区的问题
+  - 简化偏移计算逻辑:新质心位置 = 当前鼠标位置 - 初始偏移
+  - 确保矩形能够准确跟随鼠标移动,不产生额外偏移
+
+**核心改进:**
+- 新增 `_isPointNearLine` 和 `_isPointNearLineSegment` 方法实现精确线段检测
+- 新增 `_isPointInTextBox` 和 `_checkTextBoxBounds` 方法实现文本框检测
+- 优化 `_mouseDownModifyCallback` 实现文本框、手柄、线段的优先级检测
+- 优化 `_mouseDragModifyCallback` 支持文本框拖拽和修复整体拖拽坐标计算
+- 优化 `_mouseMoveModifyCallback` 实现智能光标样式切换和悬停高亮
+- 优化 `renderAnnotation` 根据选中和高亮状态动态调整手柄大小
+
+**改动文件:**
+- src/components/measures/RectangleGrayscaleMeasurementTool.ts
+
+---
+
 ## [1.4.2] - 2025-12-11
 
 ### 新增 (Added)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.4.2",
+  "version": "1.5.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 226 - 18
src/components/measures/RectangleGrayscaleMeasurementTool.ts

@@ -241,6 +241,115 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
     return inside;
   }
 
+  /**
+   * 检查点是否在线段上(矩形的边)
+   */
+  private _isPointNearLine(
+    point: CoreTypes.Point2,
+    rectanglePoints: CoreTypes.Point3[],
+    viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
+    proximity: number = 6
+  ): boolean {
+    const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p));
+
+    // 检查四条边
+    for (let i = 0; i < canvasPoints.length; i++) {
+      const start = canvasPoints[i];
+      const end = canvasPoints[(i + 1) % canvasPoints.length];
+      
+      if (this._isPointNearLineSegment(point, start, end, proximity)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 检查点是否靠近线段
+   */
+  private _isPointNearLineSegment(
+    point: CoreTypes.Point2,
+    lineStart: CoreTypes.Point2,
+    lineEnd: CoreTypes.Point2,
+    proximity: number
+  ): boolean {
+    const x0 = point[0];
+    const y0 = point[1];
+    const x1 = lineStart[0];
+    const y1 = lineStart[1];
+    const x2 = lineEnd[0];
+    const y2 = lineEnd[1];
+
+    const dx = x2 - x1;
+    const dy = y2 - y1;
+    const length = Math.sqrt(dx * dx + dy * dy);
+
+    if (length === 0) {
+      // 线段退化为点
+      const distance = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2));
+      return distance <= proximity;
+    }
+
+    // 计算点到线段的最短距离
+    const t = Math.max(0, Math.min(1, ((x0 - x1) * dx + (y0 - y1) * dy) / (length * length)));
+    const projX = x1 + t * dx;
+    const projY = y1 + t * dy;
+    const distance = Math.sqrt(Math.pow(x0 - projX, 2) + Math.pow(y0 - projY, 2));
+
+    return distance <= proximity;
+  }
+
+  /**
+   * 检查点是否在文本框上
+   */
+  private _isPointInTextBox(
+    element: HTMLDivElement,
+    annotation: RectangleGrayscaleAnnotation,
+    canvasCoords: CoreTypes.Point2
+  ): boolean {
+    const enabledElement = getEnabledElement(element);
+    if (!enabledElement) {
+      return false;
+    }
+    const { viewport } = enabledElement;
+
+    const textBox = annotation.data.textBox;
+    if (!textBox) {
+      // 如果没有自定义位置,使用默认位置(右下角)
+      const points = annotation.data.handles.points;
+      if (points.length < 4) return false;
+      const defaultPos = viewport.worldToCanvas(points[2]);
+      return this._checkTextBoxBounds(canvasCoords, defaultPos);
+    }
+
+    return this._checkTextBoxBounds(canvasCoords, textBox);
+  }
+
+  /**
+   * 检查点是否在文本框边界内(估算文本框大小)
+   */
+  private _checkTextBoxBounds(
+    point: CoreTypes.Point2,
+    textBoxPos: CoreTypes.Point2
+  ): boolean {
+    // 估算文本框大小(基于典型的文本行数和字体大小)
+    const padding = 8;
+    const lineHeight = 18;
+    const textWidth = 150; // 估算宽度
+    const textHeight = lineHeight * 4 + padding * 2; // 4行文本
+
+    const x = textBoxPos[0];
+    const y = textBoxPos[1];
+
+    return (
+      point[0] >= x - padding &&
+      point[0] <= x + textWidth &&
+      point[1] >= y - padding &&
+      point[1] <= y + textHeight
+    );
+  }
+
   /**
    * 取消操作
    */
@@ -363,8 +472,17 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
         );
       }
 
-      // 绘制手柄
+      // 绘制手柄 - 根据选中和高亮状态调整大小
       const handleGroupUID = `${annotationUID}-handles`;
+      
+      // 根据状态设置手柄半径
+      let handleRadius = 6; // 默认半径
+      if (annotationItem.isSelected) {
+        handleRadius = 12; // 选中状态下半径变大
+      } else if (annotationItem.highlighted) {
+        handleRadius = 9; // 悬停高亮时半径适中
+      }
+      
       drawHandles(
         svgDrawingHelper,
         annotationUID,
@@ -372,6 +490,7 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
         canvasCoordinates,
         {
           color,
+          handleRadius,
         }
       );
 
@@ -635,6 +754,42 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
     // 查找最近的手柄或矩形区域
     for (const ann of annotations) {
       const customAnn = ann as RectangleGrayscaleAnnotation;
+      
+      // 优先检查是否点击在文本框上
+      if (this._isPointInTextBox(element, customAnn, canvasCoords)) {
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        const textBoxPos = customAnn.data.textBox || viewport.worldToCanvas(customAnn.data.handles.points[2]);
+        const textBoxOffset: CoreTypes.Point2 = [
+          canvasCoords[0] - textBoxPos[0],
+          canvasCoords[1] - textBoxPos[1],
+        ];
+
+        this.editData = {
+          annotation: customAnn,
+          viewportIdsToRender,
+          handleIndex: -2, // -2表示拖拽文本框
+          hasMoved: false,
+          textBoxBeingMoved: true,
+          textBoxOffset: textBoxOffset,
+        };
+
+        customAnn.isSelected = true;
+        customAnn.highlighted = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
+      
       const handle = this.getHandleNearImagePoint(
         element,
         customAnn,
@@ -671,8 +826,8 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
         return;
       }
 
-      // 检查是否点击在矩形内部
-      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+      // 检查是否点击在线段上
+      if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
         const viewportIdsToRender =
           utilities.viewportFilters.getViewportIdsWithToolToRender(
             element,
@@ -707,6 +862,26 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
         evt.stopPropagation();
         return;
       }
+
+      // 检查是否点击在矩形内部(但不在线段上)
+      if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
+        // 仅选中,不拖拽
+        const viewportIdsToRender =
+          utilities.viewportFilters.getViewportIdsWithToolToRender(
+            element,
+            this.getToolName()
+          );
+
+        customAnn.isSelected = true;
+
+        utilities.triggerAnnotationRenderForViewportIds(
+          viewportIdsToRender
+        );
+
+        evt.preventDefault();
+        evt.stopPropagation();
+        return;
+      }
     }
 
     // 如果没有点击在工具上,取消所有选中状态
@@ -714,6 +889,14 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
       const customAnn = ann as RectangleGrayscaleAnnotation;
       customAnn.isSelected = false;
     }
+    
+    // 触发渲染以更新选中状态
+    const viewportIdsToRender =
+      utilities.viewportFilters.getViewportIdsWithToolToRender(
+        element,
+        this.getToolName()
+      );
+    utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
   };
 
   _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
@@ -734,28 +917,37 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
     const customAnn = ann as RectangleGrayscaleAnnotation;
     const { data } = customAnn;
 
+    // 如果正在拖拽文本框
+    if (this.editData.handleIndex === -2 && this.editData.textBoxBeingMoved) {
+      const newTextBoxPos: CoreTypes.Point2 = [
+        canvasCoords[0] - this.editData.textBoxOffset![0],
+        canvasCoords[1] - this.editData.textBoxOffset![1],
+      ];
+
+      // 更新文本框位置
+      data.textBox = newTextBoxPos;
+      this.editData.hasMoved = true;
+
+      utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
+      evt.preventDefault();
+      evt.stopPropagation();
+      return;
+    }
+
     // 如果正在拖拽整个矩形
     if (this.editData.handleIndex === -1) {
       const { viewport } = enabledElement;
 
-      // 使用Canvas坐标计算偏移,确保矩形形状保持不变
       // 计算当前矩形质心(四个角点的平均位置)
       const currentCenterWorld = this._calculateRectangleCenter(data.handles.points);
-      const currentCenterCanvas = viewport.worldToCanvas(currentCenterWorld);
 
-      // 计算鼠标在canvas坐标系中的偏移
-      const offsetCanvas: CoreTypes.Point2 = [
+      // 计算新的质心位置 = 当前鼠标位置 - 初始偏移
+      const newCenterCanvas: CoreTypes.Point2 = [
         canvasCoords[0] - this.editData.wholeToolOffset![0],
         canvasCoords[1] - this.editData.wholeToolOffset![1],
       ];
 
-      // 计算新的质心位置
-      const newCenterCanvas: CoreTypes.Point2 = [
-        currentCenterCanvas[0] + offsetCanvas[0],
-        currentCenterCanvas[1] + offsetCanvas[1],
-      ];
-
-      // 将canvas偏移转换为world坐标偏移
+      // 将新的质心位置转换为world坐标
       const newCenterWorld = viewport.canvasToWorld(newCenterCanvas);
 
       // 计算从当前质心到新质心的偏移
@@ -914,11 +1106,19 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
 
     let isHovering = false;
 
-    // 检查是否悬停在手柄或矩形
+    // 检查是否悬停在工具的不同部分
     for (const ann of annotations) {
       const customAnn = ann as RectangleGrayscaleAnnotation;
 
-      // 检查是否靠近手柄
+      // 优先检查文本框(手型光标)
+      if (this._isPointInTextBox(element, customAnn, canvasCoords)) {
+        element.style.cursor = 'pointer';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+
+      // 检查是否靠近手柄(十字准星光标)
       const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
       if (handle) {
         element.style.cursor = 'crosshair';
@@ -927,9 +1127,17 @@ export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
         break;
       }
 
-      // 检查是否在矩形内部
+      // 检查是否在线段上(十字准星光标)
+      if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
+        element.style.cursor = 'crosshair';
+        customAnn.highlighted = true;
+        isHovering = true;
+        break;
+      }
+
+      // 检查是否在矩形内部(默认光标)
       if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
-        element.style.cursor = 'move';
+        element.style.cursor = 'default';
         customAnn.highlighted = true;
         isHovering = true;
         break;