|
|
@@ -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;
|