|
|
@@ -0,0 +1,1716 @@
|
|
|
+import { Point3, Point2, fromPoint3ToPoint2 } from './mathUtils';
|
|
|
+import {
|
|
|
+ utilities as csUtils,
|
|
|
+ Types as CoreTypes,
|
|
|
+ getEnabledElement,
|
|
|
+} from '@cornerstonejs/core';
|
|
|
+
|
|
|
+import {
|
|
|
+ AnnotationTool,
|
|
|
+ utilities,
|
|
|
+ Types,
|
|
|
+ annotation,
|
|
|
+ drawing,
|
|
|
+} from '@cornerstonejs/tools';
|
|
|
+
|
|
|
+const {
|
|
|
+ drawHandles,
|
|
|
+ drawLinkedTextBox,
|
|
|
+ drawTextBox,
|
|
|
+ drawRect: drawRectSvg,
|
|
|
+ drawPath: drawPathSvg,
|
|
|
+ drawLine,
|
|
|
+} = drawing;
|
|
|
+
|
|
|
+import {
|
|
|
+ calculateMidpoint,
|
|
|
+ vectorSubtract,
|
|
|
+ vectorAdd,
|
|
|
+ fromPoint2ToPoint3,
|
|
|
+} from './mathUtils';
|
|
|
+import {
|
|
|
+ PublicToolProps,
|
|
|
+ ToolProps,
|
|
|
+ EventTypes,
|
|
|
+ SVGDrawingHelper,
|
|
|
+} from '@cornerstonejs/tools/dist/esm/types';
|
|
|
+import { s } from 'msw/lib/core/HttpResponse-CCdkF1fJ';
|
|
|
+import { style } from '@cornerstonejs/tools/dist/esm/stateManagement/annotation/config';
|
|
|
+// import { drawLine } from '@cornerstonejs/tools/dist/esm/drawingSvg';
|
|
|
+
|
|
|
+// 十字准星光标
|
|
|
+const cursorUrl = 'url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
|
|
|
+
|
|
|
+// 文本注解数据接口
|
|
|
+interface TextAnnotation extends Types.Annotation {
|
|
|
+ data: {
|
|
|
+ handles: {
|
|
|
+ points: CoreTypes.Point3[]; // 四个角点 [左上, 右上, 右下, 左下]
|
|
|
+ activeHandleIndex: number | null;
|
|
|
+ textData: {
|
|
|
+ position: CoreTypes.Point2; // 文本框左上角位置
|
|
|
+ text: string; // 文本内容
|
|
|
+ width: number; // 文本框宽度
|
|
|
+ height: number; // 文本框高度
|
|
|
+ rotation?: number; // 旋转角度(度数)
|
|
|
+ };
|
|
|
+ rotationHandle?: CoreTypes.Point3; // 旋转手柄位置
|
|
|
+ };
|
|
|
+ cachedStats?: {};
|
|
|
+ };
|
|
|
+ // 选中状态
|
|
|
+ isSelected?: boolean;
|
|
|
+ // 高亮状态
|
|
|
+ highlighted?: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+// 导出的注解数据接口
|
|
|
+export interface ExportedTextAnnotationData {
|
|
|
+ points: CoreTypes.Point3[]; // 四个角点的世界坐标
|
|
|
+ text: string; // 文本内容
|
|
|
+ rotation?: number; // 旋转角度
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: CoreTypes.Point3;
|
|
|
+ viewUp: CoreTypes.Point3;
|
|
|
+ FrameOfReferenceUID: string;
|
|
|
+ referencedImageId: string;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export default class TextAnnotationTool extends AnnotationTool {
|
|
|
+ static toolName = 'TextAnnotationTool';
|
|
|
+
|
|
|
+ editData: {
|
|
|
+ annotation: Types.Annotation;
|
|
|
+ viewportIdsToRender: string[];
|
|
|
+ handleIndex?: number;
|
|
|
+ newAnnotation?: boolean;
|
|
|
+ hasMoved?: boolean;
|
|
|
+ // 是否拖拽文本框
|
|
|
+ movingTextBox?: boolean;
|
|
|
+ // 是否移动整个工具
|
|
|
+ movingWholeTool?: boolean;
|
|
|
+ // 是否正在旋转
|
|
|
+ rotatingText?: boolean;
|
|
|
+ // 移动文本框时的初始偏移量
|
|
|
+ textBoxOffset?: CoreTypes.Point2;
|
|
|
+ // 移动整个工具时的初始偏移量
|
|
|
+ wholeToolOffset?: CoreTypes.Point2;
|
|
|
+ // 保存拖拽开始时的原始点坐标
|
|
|
+ originalPoints?: CoreTypes.Point3[];
|
|
|
+ // 旋转开始时的角度
|
|
|
+ initialRotation?: number;
|
|
|
+ // 鼠标初始角度
|
|
|
+ initialMouseAngle?: number;
|
|
|
+ // 是否为选中状态
|
|
|
+ selected?: boolean;
|
|
|
+ } | null = null;
|
|
|
+
|
|
|
+ isDrawing: boolean = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取工具名称
|
|
|
+ */
|
|
|
+ getToolName(): string {
|
|
|
+ return TextAnnotationTool.toolName;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建一个预设的文本注解,带有默认文本框
|
|
|
+ * @param element HTML元素
|
|
|
+ * @param viewport Viewport实例
|
|
|
+ */
|
|
|
+ static createDefaultAnnotation(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
|
|
|
+ text: string
|
|
|
+ ): TextAnnotation {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ throw new Error('Element is not enabled');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取viewport的尺寸
|
|
|
+ const canvas = viewport.canvas;
|
|
|
+ const { width, height } = canvas;
|
|
|
+ const centerX = width / 2;
|
|
|
+ const centerY = height / 2;
|
|
|
+
|
|
|
+ // 创建默认文本框:中心位置,宽100,高40
|
|
|
+ const textBoxWidth = 100;
|
|
|
+ const textBoxHeight = 40;
|
|
|
+ const textBoxLeft = centerX - textBoxWidth / 2;
|
|
|
+ const textBoxTop = centerY - textBoxHeight / 2;
|
|
|
+
|
|
|
+ // 创建四个角点的canvas坐标
|
|
|
+ const canvasPoints: CoreTypes.Point2[] = [
|
|
|
+ [textBoxLeft, textBoxTop], // 左上
|
|
|
+ [textBoxLeft + textBoxWidth, textBoxTop], // 右上
|
|
|
+ [textBoxLeft + textBoxWidth, textBoxTop + textBoxHeight], // 右下
|
|
|
+ [textBoxLeft, textBoxTop + textBoxHeight], // 左下
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 转换为world坐标
|
|
|
+ const worldPoints = canvasPoints.map((canvasPoint) =>
|
|
|
+ viewport.canvasToWorld(canvasPoint)
|
|
|
+ );
|
|
|
+
|
|
|
+ const camera = viewport.getCamera();
|
|
|
+ const { viewPlaneNormal, viewUp } = camera;
|
|
|
+ if (viewPlaneNormal === undefined) {
|
|
|
+ throw new Error('viewPlaneNormal is undefined');
|
|
|
+ }
|
|
|
+ if (viewUp === undefined) {
|
|
|
+ throw new Error('viewUp is undefined');
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotationData = {
|
|
|
+ invalidated: true,
|
|
|
+ highlighted: false,
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
|
|
|
+ viewUp: [...viewUp] as CoreTypes.Point3,
|
|
|
+ FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
|
|
|
+ referencedImageId: viewport.getCurrentImageId?.() || '',
|
|
|
+ toolName: TextAnnotationTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ handles: {
|
|
|
+ points: worldPoints,
|
|
|
+ activeHandleIndex: null,
|
|
|
+ textData: {
|
|
|
+ position: [textBoxLeft, textBoxTop],
|
|
|
+ text: text,
|
|
|
+ width: textBoxWidth,
|
|
|
+ height: textBoxHeight,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as TextAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出注解数据
|
|
|
+ * @param annotation 要导出的注解
|
|
|
+ * @returns 导出的数据对象
|
|
|
+ */
|
|
|
+ static exportAnnotationData(
|
|
|
+ annotation: TextAnnotation,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): ExportedTextAnnotationData | null {
|
|
|
+ return {
|
|
|
+ points: [...annotation.data.handles.points],
|
|
|
+ text: annotation.data.handles.textData.text,
|
|
|
+ rotation: annotation.data.handles.textData.rotation,
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
|
|
|
+ viewUp: annotation.metadata?.viewUp || [0, 1, 0],
|
|
|
+ FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
|
|
|
+ referencedImageId: annotation.metadata?.referencedImageId || '',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从导出的数据恢复注解
|
|
|
+ * @param exportedData 导出的数据
|
|
|
+ * @param element HTML元素
|
|
|
+ * @param viewport Viewport实例
|
|
|
+ * @returns 恢复的注解
|
|
|
+ */
|
|
|
+ static restoreFromExportedData(
|
|
|
+ exportedData: ExportedTextAnnotationData,
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): TextAnnotation {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ throw new Error('Element is not enabled');
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotationData = {
|
|
|
+ invalidated: true,
|
|
|
+ highlighted: false,
|
|
|
+ metadata: {
|
|
|
+ ...exportedData.metadata,
|
|
|
+ toolName: TextAnnotationTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ handles: {
|
|
|
+ points: [...exportedData.points],
|
|
|
+ activeHandleIndex: null,
|
|
|
+ textData: {
|
|
|
+ position: [0, 0], // 将在render时计算
|
|
|
+ text: exportedData.text,
|
|
|
+ width: 100, // 默认值,将在render时根据points计算
|
|
|
+ height: 40,
|
|
|
+ rotation: exportedData.rotation || 0,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as TextAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ toolProps: PublicToolProps = {},
|
|
|
+ defaultToolProps: ToolProps = {
|
|
|
+ supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
|
+ configuration: {
|
|
|
+ shadow: true,
|
|
|
+ preventHandleOutsideImage: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ) {
|
|
|
+ super(toolProps, defaultToolProps);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
|
|
|
+ */
|
|
|
+ addNewAnnotation(
|
|
|
+ evt: EventTypes.InteractionEventType,
|
|
|
+ interactionType: string
|
|
|
+ ): Types.Annotation {
|
|
|
+ // 不创建新注解,直接返回空对象
|
|
|
+ // 因为我们只使用预设的注解
|
|
|
+ evt.preventDefault();
|
|
|
+ return {} as Types.Annotation;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点是否靠近工具
|
|
|
+ */
|
|
|
+ isPointNearTool(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ annotation: Types.Annotation | TextAnnotation,
|
|
|
+ canvasCoords: CoreTypes.Point2,
|
|
|
+ proximity: number,
|
|
|
+ interactionType: string
|
|
|
+ ): boolean {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+
|
|
|
+ const customAnn = annotation as TextAnnotation;
|
|
|
+ const handles = customAnn.data.handles;
|
|
|
+ if (!handles || !handles.points) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const points = handles.points;
|
|
|
+
|
|
|
+ // 检查是否在文本框内
|
|
|
+ if (this._isPointInTextBox(canvasCoords, handles.textData)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近任意一个手柄点
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ const point = points[i];
|
|
|
+ const canvasPoint = viewport.worldToCanvas(point);
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
|
|
|
+ Math.pow(canvasPoint[1] - canvasCoords[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (distance < proximity) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否在线段上(文本框边框)
|
|
|
+ const edges = [
|
|
|
+ [points[0], points[1]], // 上边
|
|
|
+ [points[1], points[2]], // 右边
|
|
|
+ [points[2], points[3]], // 下边
|
|
|
+ [points[3], points[0]], // 左边
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const [p1, p2] of edges) {
|
|
|
+ const canvasP1 = viewport.worldToCanvas(p1);
|
|
|
+ const canvasP2 = viewport.worldToCanvas(p2);
|
|
|
+ const dist = this._distanceToSegment(canvasCoords, canvasP1, canvasP2);
|
|
|
+ if (dist < proximity) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点击是否在文本区域内(只检测文本中心的小区域,不检测整个矩形框)
|
|
|
+ */
|
|
|
+ private _isPointInTextBox(
|
|
|
+ canvasPoint: CoreTypes.Point2,
|
|
|
+ textBox: { position: CoreTypes.Point2; width: number; height: number }
|
|
|
+ ): boolean {
|
|
|
+ const [x, y] = canvasPoint;
|
|
|
+ const [tx, ty] = textBox.position;
|
|
|
+ const { width, height } = textBox;
|
|
|
+
|
|
|
+ // 计算文本中心位置
|
|
|
+ const centerX = tx + width / 2;
|
|
|
+ const centerY = ty + height / 2;
|
|
|
+
|
|
|
+ // 只检测文本中心的小区域(假设文本大小约为矩形的 60%)
|
|
|
+ const textWidth = width * 0.6;
|
|
|
+ const textHeight = height * 0.6;
|
|
|
+
|
|
|
+ const margin = 5;
|
|
|
+ return (
|
|
|
+ x >= centerX - textWidth / 2 - margin &&
|
|
|
+ x <= centerX + textWidth / 2 + margin &&
|
|
|
+ y >= centerY - textHeight / 2 - margin &&
|
|
|
+ y <= centerY + textHeight / 2 + margin
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算点到线段的距离
|
|
|
+ */
|
|
|
+ private _distanceToSegment(
|
|
|
+ point: CoreTypes.Point2,
|
|
|
+ lineStart: CoreTypes.Point2,
|
|
|
+ lineEnd: CoreTypes.Point2
|
|
|
+ ): number {
|
|
|
+ const x = point[0];
|
|
|
+ const y = point[1];
|
|
|
+ const x1 = lineStart[0];
|
|
|
+ const y1 = lineStart[1];
|
|
|
+ const x2 = lineEnd[0];
|
|
|
+ const y2 = lineEnd[1];
|
|
|
+
|
|
|
+ const A = x - x1;
|
|
|
+ const B = y - y1;
|
|
|
+ const C = x2 - x1;
|
|
|
+ const D = y2 - y1;
|
|
|
+
|
|
|
+ const dot = A * C + B * D;
|
|
|
+ const lenSq = C * C + D * D;
|
|
|
+ let param = -1;
|
|
|
+
|
|
|
+ if (lenSq !== 0) {
|
|
|
+ param = dot / lenSq;
|
|
|
+ }
|
|
|
+
|
|
|
+ let xx, yy;
|
|
|
+
|
|
|
+ if (param < 0) {
|
|
|
+ xx = x1;
|
|
|
+ yy = y1;
|
|
|
+ } else if (param > 1) {
|
|
|
+ xx = x2;
|
|
|
+ yy = y2;
|
|
|
+ } else {
|
|
|
+ xx = x1 + param * C;
|
|
|
+ yy = y1 + param * D;
|
|
|
+ }
|
|
|
+
|
|
|
+ const dx = x - xx;
|
|
|
+ const dy = y - yy;
|
|
|
+
|
|
|
+ return Math.sqrt(dx * dx + dy * dy);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点是否靠近矩形边框(线段)
|
|
|
+ */
|
|
|
+ private _isPointNearLine(
|
|
|
+ canvasPoint: CoreTypes.Point2,
|
|
|
+ points: CoreTypes.Point3[],
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
|
|
|
+ proximity: number = 6
|
|
|
+ ): boolean {
|
|
|
+ if (points.length < 4) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const canvasPoints = points.map(p => viewport.worldToCanvas(p));
|
|
|
+
|
|
|
+ // 检查四条边
|
|
|
+ const edges = [
|
|
|
+ [canvasPoints[0], canvasPoints[1]], // 上边
|
|
|
+ [canvasPoints[1], canvasPoints[2]], // 右边
|
|
|
+ [canvasPoints[2], canvasPoints[3]], // 下边
|
|
|
+ [canvasPoints[3], canvasPoints[0]], // 左边
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const [start, end] of edges) {
|
|
|
+ const dist = this._distanceToSegment(canvasPoint, start, end);
|
|
|
+ if (dist < proximity) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新文本框位置和尺寸基于四个角点
|
|
|
+ */
|
|
|
+ private _updateTextBoxFromPoints(annotation: TextAnnotation, viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport): void {
|
|
|
+ const points = annotation.data.handles.points;
|
|
|
+ if (points.length < 4) return;
|
|
|
+
|
|
|
+ const canvasPoints = points.map(p => viewport.worldToCanvas(p));
|
|
|
+
|
|
|
+ // 计算边界框
|
|
|
+ const minX = Math.min(...canvasPoints.map(p => p[0]));
|
|
|
+ const minY = Math.min(...canvasPoints.map(p => p[1]));
|
|
|
+ const maxX = Math.max(...canvasPoints.map(p => p[0]));
|
|
|
+ const maxY = Math.max(...canvasPoints.map(p => p[1]));
|
|
|
+
|
|
|
+ annotation.data.handles.textData.position = [minX, minY];
|
|
|
+ annotation.data.handles.textData.width = maxX - minX;
|
|
|
+ annotation.data.handles.textData.height = maxY - minY;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算鼠标相对于矩形中心的角度
|
|
|
+ */
|
|
|
+ private _calculateAngle(
|
|
|
+ mouseCanvas: CoreTypes.Point2,
|
|
|
+ centerCanvas: CoreTypes.Point2
|
|
|
+ ): number {
|
|
|
+ const dx = mouseCanvas[0] - centerCanvas[0];
|
|
|
+ const dy = mouseCanvas[1] - centerCanvas[1];
|
|
|
+ return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取旋转手柄的位置(跟随矩形旋转,始终在逻辑顶部边上方30像素)
|
|
|
+ * 使用向量计算:从矩形中心点沿旋转角度方向移动"高度/2 + 30"距离
|
|
|
+ */
|
|
|
+ private _getRotationHandlePosition(
|
|
|
+ annotation: TextAnnotation,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): CoreTypes.Point2 | null {
|
|
|
+ const points = annotation.data.handles.points;
|
|
|
+ if (points.length < 4) return null;
|
|
|
+
|
|
|
+ const canvasPoints = points.map(p => viewport.worldToCanvas(p));
|
|
|
+ const rotation = annotation.data.handles.textData.rotation || 0;
|
|
|
+
|
|
|
+ // 1️⃣ 计算矩形中心点
|
|
|
+ const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+
|
|
|
+ // 2️⃣ 计算矩形实际高度(欧几里得距离)
|
|
|
+ const heightEdge = Math.sqrt(
|
|
|
+ Math.pow(canvasPoints[3][0] - canvasPoints[0][0], 2) +
|
|
|
+ Math.pow(canvasPoints[3][1] - canvasPoints[0][1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 3️⃣ 计算旋转方向向量(单位向量) 减少90度是为了让0度时手柄在上方
|
|
|
+ const angleRad = (rotation-90) * Math.PI / 180;
|
|
|
+ const directionX = Math.cos(angleRad);
|
|
|
+ const directionY = Math.sin(angleRad);
|
|
|
+
|
|
|
+ // 4️⃣ 计算移动距离:矩形高度的一半 + 70像素
|
|
|
+ const moveDistance = heightEdge / 2 + 70;
|
|
|
+
|
|
|
+ // 5️⃣ 沿着旋转方向移动距离
|
|
|
+ const handleX = centerX + directionX * moveDistance;
|
|
|
+ const handleY = centerY + directionY * moveDistance;
|
|
|
+
|
|
|
+ return [handleX, handleY];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点是否靠近旋转手柄
|
|
|
+ */
|
|
|
+ private _isPointNearRotationHandle(
|
|
|
+ canvasPoint: CoreTypes.Point2,
|
|
|
+ annotation: TextAnnotation,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
|
|
|
+ proximity: number = 15
|
|
|
+ ): boolean {
|
|
|
+ const rotationHandlePos = this._getRotationHandlePosition(annotation, viewport);
|
|
|
+ if (!rotationHandlePos) return false;
|
|
|
+
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(canvasPoint[0] - rotationHandlePos[0], 2) +
|
|
|
+ Math.pow(canvasPoint[1] - rotationHandlePos[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ return distance < proximity;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消操作
|
|
|
+ */
|
|
|
+ cancel(element: HTMLDivElement): string {
|
|
|
+ if (this.isDrawing) {
|
|
|
+ this.isDrawing = false;
|
|
|
+ this._deactivateDraw(element);
|
|
|
+ this._deactivateModify(element);
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (enabledElement) {
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ this.editData = null;
|
|
|
+ return this.getToolName();
|
|
|
+ }
|
|
|
+
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 激活绘制模式
|
|
|
+ */
|
|
|
+ _activateDraw(element: HTMLDivElement): void {
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DRAG',
|
|
|
+ this._dragCallback as EventListener
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
|
+ this._endCallback as EventListener
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_CLICK',
|
|
|
+ this._endCallback as EventListener
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消激活绘制模式
|
|
|
+ */
|
|
|
+ _deactivateDraw(element: HTMLDivElement): void {
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DRAG',
|
|
|
+ this._dragCallback as EventListener
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
|
+ this._endCallback as EventListener
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_CLICK',
|
|
|
+ this._endCallback as EventListener
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 激活修改模式
|
|
|
+ */
|
|
|
+ _activateModify(element: HTMLDivElement): void {
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DOWN',
|
|
|
+ this._mouseDownModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DRAG',
|
|
|
+ this._mouseDragModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
|
+ this._mouseUpModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.addEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_MOVE',
|
|
|
+ this._mouseMoveModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ // 添加键盘事件监听用于删除功能
|
|
|
+ element.addEventListener(
|
|
|
+ 'keydown',
|
|
|
+ this._keyDownCallback as EventListener
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消激活修改模式
|
|
|
+ */
|
|
|
+ _deactivateModify(element: HTMLDivElement): void {
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DOWN',
|
|
|
+ this._mouseDownModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_DRAG',
|
|
|
+ this._mouseDragModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_UP',
|
|
|
+ this._mouseUpModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ element.removeEventListener(
|
|
|
+ 'CORNERSTONE_TOOLS_MOUSE_MOVE',
|
|
|
+ this._mouseMoveModifyCallback as EventListener
|
|
|
+ );
|
|
|
+ // 移除键盘事件监听
|
|
|
+ element.removeEventListener(
|
|
|
+ 'keydown',
|
|
|
+ this._keyDownCallback as EventListener
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 键盘按下回调 - 用于删除功能
|
|
|
+ */
|
|
|
+ _keyDownCallback = (evt: KeyboardEvent): void => {
|
|
|
+ const element = evt.currentTarget as HTMLDivElement;
|
|
|
+
|
|
|
+ // 检查是否按下 Delete 或 Backspace 键
|
|
|
+ if (evt.key === 'Delete' || evt.key === 'Backspace') {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
|
|
+ if (!annotations || annotations.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找被选中的注解
|
|
|
+ const selectedAnnotations = annotations.filter(
|
|
|
+ (ann) => (ann as TextAnnotation).isSelected
|
|
|
+ );
|
|
|
+
|
|
|
+ if (selectedAnnotations.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除所有选中的注解
|
|
|
+ for (const ann of selectedAnnotations) {
|
|
|
+ annotation.state.removeAnnotation(ann.annotationUID!);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 触发渲染更新
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标移动回调 - 用于修改模式,处理悬停检测
|
|
|
+ * 实现功能1和2:鼠标悬停在文本上时光标为手型,悬停在线段或手柄上时光标为十字准星
|
|
|
+ */
|
|
|
+ _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element, currentPoints } = eventDetail;
|
|
|
+ // 添加空值检查
|
|
|
+ if (!currentPoints || !currentPoints.canvas) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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 TextAnnotation;
|
|
|
+ if (customAnn.isSelected && this._isPointNearRotationHandle(canvasCoords, customAnn, viewport)) {
|
|
|
+ element.style.cursor = 'grab'; // 抓手光标表示可以旋转
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否悬停在文本框上
|
|
|
+ if (!isHovering) {
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ if (this._isPointInTextBox(canvasCoords, customAnn.data.handles.textData)) {
|
|
|
+ element.style.cursor = 'pointer'; // 手型光标
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
|
|
|
+ if (!isHovering) {
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+
|
|
|
+ // 检查是否靠近手柄
|
|
|
+ const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
|
|
|
+ if (handle) {
|
|
|
+ element.style.cursor = cursorUrl; // 十字准星光标
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用专门的线段检测方法
|
|
|
+ if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
|
|
|
+ element.style.cursor = cursorUrl; // 十字准星光标
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有悬停在任何地方,重置高亮(但保留选中状态)
|
|
|
+ if (!isHovering) {
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ // 只有未选中的工具才重置 highlighted
|
|
|
+ // 已选中的工具保持 highlighted = true
|
|
|
+ if (!customAnn.isSelected) {
|
|
|
+ customAnn.highlighted = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ element.style.cursor = 'default';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 触发渲染以更新高亮状态
|
|
|
+ const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标按下回调 - 用于修改模式
|
|
|
+ */
|
|
|
+ _mouseDownModifyCallback = (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) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先检查是否点击在文本框上
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ if (this._isPointInTextBox(canvasCoords, customAnn.data.handles.textData)) {
|
|
|
+ // 点击在文本框上
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算鼠标点击位置相对于文本框左上角的偏移量
|
|
|
+ const textBoxOffset: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - customAnn.data.handles.textData.position[0],
|
|
|
+ canvasCoords[1] - customAnn.data.handles.textData.position[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ movingTextBox: true,
|
|
|
+ textBoxOffset: textBoxOffset,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置选中状态(不设置 highlighted,因为选中状态已经足够)
|
|
|
+ customAnn.isSelected = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有点击文本框,再查找最近的手柄
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ const handle = this.getHandleNearImagePoint(
|
|
|
+ element,
|
|
|
+ customAnn,
|
|
|
+ canvasCoords,
|
|
|
+ 6
|
|
|
+ );
|
|
|
+
|
|
|
+ if (handle) {
|
|
|
+ // 找到了手柄,开始调整大小
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ handleIndex: customAnn.data.handles.activeHandleIndex || 0,
|
|
|
+ hasMoved: false,
|
|
|
+ originalPoints: [...customAnn.data.handles.points], // 保存拖拽开始时的原始点坐标
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置选中状态
|
|
|
+ customAnn.isSelected = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其次检查是否点击在旋转手柄上(对于选中的注解)
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ if (customAnn.isSelected && this._isPointNearRotationHandle(canvasCoords, customAnn, viewport)) {
|
|
|
+ // 点击在旋转手柄上,开始旋转
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算矩形中心点
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+ const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
|
|
|
+ const centerCanvas = viewport.worldToCanvas(center);
|
|
|
+
|
|
|
+ // 保存初始旋转角度
|
|
|
+ const initialRotation = customAnn.data.handles.textData.rotation || 0;
|
|
|
+ const initialMouseAngle = this._calculateAngle(canvasCoords, centerCanvas);
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ rotatingText: true,
|
|
|
+ initialRotation: initialRotation,
|
|
|
+ initialMouseAngle: initialMouseAngle,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置选中状态
|
|
|
+ customAnn.isSelected = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有找到手柄,检查是否点击在线段上(移动整个工具)
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ if (this.isPointNearTool(element, customAnn as Types.Annotation, canvasCoords, 10, 'mouse')) {
|
|
|
+ // 点击在线段上,开始移动整个工具
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算鼠标点击位置相对于工具中心的偏移量
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+ const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
|
|
|
+ const centerCanvas = viewport.worldToCanvas(center);
|
|
|
+ const wholeToolOffset: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - centerCanvas[0],
|
|
|
+ canvasCoords[1] - centerCanvas[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ movingWholeTool: true,
|
|
|
+ wholeToolOffset: wholeToolOffset,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置选中状态
|
|
|
+ customAnn.isSelected = true;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有点击在工具上,取消所有工具的选中状态
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+ customAnn.isSelected = false;
|
|
|
+ customAnn.highlighted = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标拖拽回调 - 用于修改模式
|
|
|
+ */
|
|
|
+ _mouseDragModifyCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ if (!this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const canvasCoords = currentPoints.canvas;
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(eventDetail.element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation: ann, viewportIdsToRender, movingTextBox, movingWholeTool } = this.editData;
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+
|
|
|
+ // 如果正在拖拽文本框
|
|
|
+ if (movingTextBox && this.editData.textBoxOffset) {
|
|
|
+ // 从当前鼠标位置减去偏移量,得到文本框左上角应该在的位置
|
|
|
+ const newTextBoxPosition: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - this.editData.textBoxOffset[0],
|
|
|
+ canvasCoords[1] - this.editData.textBoxOffset[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 更新四个角点的位置
|
|
|
+ const { width, height } = customAnn.data.handles.textData;
|
|
|
+ customAnn.data.handles.points[0] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0], newTextBoxPosition[1]]);
|
|
|
+ customAnn.data.handles.points[1] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0] + width, newTextBoxPosition[1]]);
|
|
|
+ customAnn.data.handles.points[2] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0] + width, newTextBoxPosition[1] + height]);
|
|
|
+ customAnn.data.handles.points[3] = enabledElement.viewport.canvasToWorld([newTextBoxPosition[0], newTextBoxPosition[1] + height]);
|
|
|
+
|
|
|
+ customAnn.data.handles.textData.position = newTextBoxPosition;
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+
|
|
|
+ // 触发渲染更新
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在移动整个工具
|
|
|
+ if (movingWholeTool && this.editData.wholeToolOffset) {
|
|
|
+ // 计算新的中心位置
|
|
|
+ const newCenterCanvas: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - this.editData.wholeToolOffset[0],
|
|
|
+ canvasCoords[1] - this.editData.wholeToolOffset[1],
|
|
|
+ ];
|
|
|
+ const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
|
|
|
+
|
|
|
+ // 计算所有点的偏移量
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+ const oldCenter = calculateMidpoint(points[0], points[2]);
|
|
|
+ const offset = vectorSubtract(newCenterWorld, oldCenter);
|
|
|
+
|
|
|
+ // 移动所有点
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ points[i] = vectorAdd(points[i], offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新文本框位置
|
|
|
+ this._updateTextBoxFromPoints(customAnn, enabledElement.viewport);
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+
|
|
|
+ // 触发渲染更新
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在旋转
|
|
|
+ if (this.editData.rotatingText && this.editData.initialRotation !== undefined && this.editData.initialMouseAngle !== undefined) {
|
|
|
+ // 计算矩形中心点
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+ const center = calculateMidpoint(points[0], points[2]); // 左上和右下中点
|
|
|
+ const centerCanvas = enabledElement.viewport.worldToCanvas(center);
|
|
|
+
|
|
|
+ // 计算当前鼠标角度
|
|
|
+ const currentMouseAngle = this._calculateAngle(canvasCoords, centerCanvas);
|
|
|
+
|
|
|
+ // 计算角度差
|
|
|
+ const angleDelta = currentMouseAngle - this.editData.initialMouseAngle;
|
|
|
+
|
|
|
+ // 更新旋转角度
|
|
|
+ const newRotation = (this.editData.initialRotation + angleDelta) % 360;
|
|
|
+ customAnn.data.handles.textData.rotation = newRotation;
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+
|
|
|
+ // 触发渲染更新
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理单个手柄拖拽 - 实现矩形的自由伸缩(考虑旋转状态)
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+ const activeHandleIndex = customAnn.data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < customAnn.data.handles.points.length && this.editData.originalPoints) {
|
|
|
+ // 获取当前旋转角度
|
|
|
+ const rotation = customAnn.data.handles.textData.rotation || 0;
|
|
|
+
|
|
|
+ // 根据被拖拽的角点索引,重新计算所有四个顶点的位置
|
|
|
+ // 保持矩形几何关系:直角和平行边
|
|
|
+ const originalPoints = this.editData.originalPoints;
|
|
|
+
|
|
|
+ // 计算矩形中心点
|
|
|
+ const center = calculateMidpoint(originalPoints[0], originalPoints[2]);
|
|
|
+ const centerCanvas = enabledElement.viewport.worldToCanvas(center);
|
|
|
+
|
|
|
+ // 将鼠标世界坐标转换为canvas坐标
|
|
|
+ const mouseCanvas = enabledElement.viewport.worldToCanvas(worldPos);
|
|
|
+
|
|
|
+ // 如果有旋转,需要将鼠标位置转换到未旋转的坐标系中进行计算
|
|
|
+ let effectiveMousePos = worldPos;
|
|
|
+ if (rotation !== 0) {
|
|
|
+ // 计算鼠标在旋转坐标系中的位置
|
|
|
+ const dx = mouseCanvas[0] - centerCanvas[0];
|
|
|
+ const dy = mouseCanvas[1] - centerCanvas[1];
|
|
|
+ const cos = Math.cos(-rotation * Math.PI / 180); // 反向旋转
|
|
|
+ const sin = Math.sin(-rotation * Math.PI / 180);
|
|
|
+
|
|
|
+ const unrotatedX = centerCanvas[0] + dx * cos - dy * sin;
|
|
|
+ const unrotatedY = centerCanvas[1] + dx * sin + dy * cos;
|
|
|
+
|
|
|
+ // 转换回世界坐标
|
|
|
+ effectiveMousePos = enabledElement.viewport.canvasToWorld([unrotatedX, unrotatedY]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用转换后的鼠标位置进行几何计算
|
|
|
+ const effectiveWorldPos = effectiveMousePos;
|
|
|
+
|
|
|
+ // 确定矩形的变换规则(在未旋转坐标系中进行)
|
|
|
+ switch (activeHandleIndex) {
|
|
|
+ case 0: // 拖拽左上角
|
|
|
+ customAnn.data.handles.points[0] = effectiveWorldPos;
|
|
|
+ // 固定右下角(2)
|
|
|
+ customAnn.data.handles.points[2] = [...originalPoints[2]] as CoreTypes.Point3;
|
|
|
+ // 更新右上角(1): x同右下角, y同左上角
|
|
|
+ customAnn.data.handles.points[1] = [originalPoints[2][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ // 更新左下角(3): x同左上角, y同右下角
|
|
|
+ customAnn.data.handles.points[3] = [effectiveWorldPos[0], originalPoints[2][1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 1: // 拖拽右上角
|
|
|
+ customAnn.data.handles.points[1] = effectiveWorldPos;
|
|
|
+ // 固定左下角(3)
|
|
|
+ customAnn.data.handles.points[3] = [...originalPoints[3]] as CoreTypes.Point3;
|
|
|
+ // 更新左上角(0): x同左下角, y同右上角
|
|
|
+ customAnn.data.handles.points[0] = [originalPoints[3][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ // 更新右下角(2): x同右上角, y同左下角
|
|
|
+ customAnn.data.handles.points[2] = [effectiveWorldPos[0], originalPoints[3][1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 2: // 拖拽右下角
|
|
|
+ customAnn.data.handles.points[2] = effectiveWorldPos;
|
|
|
+ // 固定左上角(0)
|
|
|
+ customAnn.data.handles.points[0] = [...originalPoints[0]] as CoreTypes.Point3;
|
|
|
+ // 更新右上角(1): x同右下角, y同左上角
|
|
|
+ customAnn.data.handles.points[1] = [effectiveWorldPos[0], originalPoints[0][1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ // 更新左下角(3): x同左上角, y同右下角
|
|
|
+ customAnn.data.handles.points[3] = [originalPoints[0][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 3: // 拖拽左下角
|
|
|
+ customAnn.data.handles.points[3] = effectiveWorldPos;
|
|
|
+ // 固定右上角(1)
|
|
|
+ customAnn.data.handles.points[1] = [...originalPoints[1]] as CoreTypes.Point3;
|
|
|
+ // 更新左上角(0): x同左下角, y同右上角
|
|
|
+ customAnn.data.handles.points[0] = [effectiveWorldPos[0], originalPoints[1][1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ // 更新右下角(2): x同右上角, y同左下角
|
|
|
+ customAnn.data.handles.points[2] = [originalPoints[1][0], effectiveWorldPos[1], effectiveWorldPos[2]] as CoreTypes.Point3;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标释放回调 - 用于修改模式
|
|
|
+ */
|
|
|
+ _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ if (!this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
|
|
|
+ const customAnn = ann as TextAnnotation;
|
|
|
+
|
|
|
+ // 清除active状态
|
|
|
+ customAnn.data.handles.activeHandleIndex = null;
|
|
|
+
|
|
|
+ // 如果没有拖拽且点击在线段上(movingWholeTool为true),则进入选中状态
|
|
|
+ if (!hasMoved && movingWholeTool) {
|
|
|
+ customAnn.isSelected = true;
|
|
|
+ customAnn.highlighted = true; // 保持高亮以显示选中
|
|
|
+ }
|
|
|
+
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ this.editData = null;
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标拖拽
|
|
|
+ */
|
|
|
+ _dragCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ this.isDrawing = true;
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement || !this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation, viewportIdsToRender } = this.editData;
|
|
|
+ const { data } = annotation as TextAnnotation;
|
|
|
+
|
|
|
+ if (annotation) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ const points = data.handles.points;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < points.length) {
|
|
|
+ // 更新手柄位置
|
|
|
+ points[activeHandleIndex] = worldPos;
|
|
|
+
|
|
|
+ this._updateTextBoxFromPoints(annotation as TextAnnotation, enabledElement.viewport);
|
|
|
+
|
|
|
+ this.editData!.hasMoved = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理鼠标抬起/点击
|
|
|
+ */
|
|
|
+ _endCallback = (evt: EventTypes.InteractionEventType): void => {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ if (!this.editData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { annotation, viewportIdsToRender, newAnnotation, hasMoved } =
|
|
|
+ this.editData;
|
|
|
+ const { data } = annotation as TextAnnotation;
|
|
|
+
|
|
|
+ if (newAnnotation && !hasMoved) {
|
|
|
+ // 添加新点
|
|
|
+ const points = data.handles.points;
|
|
|
+
|
|
|
+ if (points.length < 4) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ // 添加下一个点
|
|
|
+ if (points.length < 4) {
|
|
|
+ points.push(worldPos);
|
|
|
+ data.handles.activeHandleIndex = points.length - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (points.length === 4) {
|
|
|
+ // 完成注解创建
|
|
|
+ this.isDrawing = false;
|
|
|
+ data.handles.activeHandleIndex = null;
|
|
|
+ annotation.highlighted = false;
|
|
|
+ this.editData.newAnnotation = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (enabledElement) {
|
|
|
+ this._updateTextBoxFromPoints(
|
|
|
+ annotation as TextAnnotation,
|
|
|
+ enabledElement.viewport
|
|
|
+ );
|
|
|
+
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+ const newViewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ newViewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (hasMoved) {
|
|
|
+ // 拖拽结束
|
|
|
+ this.editData.hasMoved = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取靠近图像点的手柄
|
|
|
+ */
|
|
|
+ getHandleNearImagePoint(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ annotation: Types.Annotation,
|
|
|
+ canvasCoords: CoreTypes.Point2,
|
|
|
+ proximity: number
|
|
|
+ ): Types.ToolHandle | undefined {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+
|
|
|
+ const customAnn = annotation as TextAnnotation;
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+
|
|
|
+ // 使用更大的proximity来更容易选中手柄
|
|
|
+ const handleProximity = Math.max(proximity, 15);
|
|
|
+
|
|
|
+ // 获取旋转角度
|
|
|
+ const rotation = customAnn.data.handles.textData.rotation || 0;
|
|
|
+
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ const point = points[i];
|
|
|
+ let canvasPoint = viewport.worldToCanvas(point);
|
|
|
+
|
|
|
+ // 如果有旋转,计算旋转后的手柄位置
|
|
|
+ if (rotation !== 0) {
|
|
|
+ const canvasPoints = points.map(p => viewport.worldToCanvas(p));
|
|
|
+ const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+
|
|
|
+ const dx = canvasPoint[0] - centerX;
|
|
|
+ const dy = canvasPoint[1] - centerY;
|
|
|
+ const cos = Math.cos(rotation * Math.PI / 180);
|
|
|
+ const sin = Math.sin(rotation * Math.PI / 180);
|
|
|
+
|
|
|
+ canvasPoint = [
|
|
|
+ centerX + dx * cos - dy * sin,
|
|
|
+ centerY + dx * sin + dy * cos
|
|
|
+ ] as CoreTypes.Point2;
|
|
|
+ }
|
|
|
+
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
|
|
|
+ Math.pow(canvasPoint[1] - canvasCoords[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (distance < handleProximity) {
|
|
|
+ // 设置activeHandleIndex以便拖拽
|
|
|
+ customAnn.data.handles.activeHandleIndex = i;
|
|
|
+ return {
|
|
|
+ worldPosition: point,
|
|
|
+ } as Types.ToolHandle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有找到手柄,重置activeHandleIndex
|
|
|
+ customAnn.data.handles.activeHandleIndex = null;
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理注解选中
|
|
|
+ */
|
|
|
+ handleSelectedCallback(
|
|
|
+ evt: EventTypes.InteractionEventType,
|
|
|
+ annotation: Types.Annotation
|
|
|
+ ): void {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ annotation.highlighted = true;
|
|
|
+
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理工具选中
|
|
|
+ */
|
|
|
+ toolSelectedCallback(
|
|
|
+ evt: EventTypes.InteractionEventType,
|
|
|
+ annotation: Types.Annotation
|
|
|
+ ): void {
|
|
|
+ // 实现工具选中逻辑
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 渲染注解
|
|
|
+ */
|
|
|
+ renderAnnotation = (
|
|
|
+ enabledElement: CoreTypes.IEnabledElement,
|
|
|
+ svgDrawingHelper: SVGDrawingHelper
|
|
|
+ ): boolean => {
|
|
|
+ let renderStatus = false;
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+ const { element } = viewport;
|
|
|
+
|
|
|
+ let annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
|
|
+
|
|
|
+ if (!annotations?.length) {
|
|
|
+ return renderStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 0; i < annotations.length; i++) {
|
|
|
+ const annotation = annotations[i] as TextAnnotation;
|
|
|
+ const { annotationUID, data } = annotation;
|
|
|
+ const points = data.handles.points;
|
|
|
+
|
|
|
+ if (points.length < 4) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新文本框位置和尺寸
|
|
|
+ this._updateTextBoxFromPoints(annotation, viewport);
|
|
|
+
|
|
|
+ // 转换所有点为 canvas 坐标
|
|
|
+ const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
|
|
|
+
|
|
|
+ // 获取样式
|
|
|
+ const styleSpecifier: any = {
|
|
|
+ toolGroupId: this.toolGroupId,
|
|
|
+ toolName: this.getToolName(),
|
|
|
+ viewportId: viewport.id,
|
|
|
+ annotationUID: annotationUID,
|
|
|
+ };
|
|
|
+
|
|
|
+ const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
|
|
+ const color = this.getStyle('color', styleSpecifier, annotation);
|
|
|
+
|
|
|
+ // 绘制文本框边框 - 使用旋转后的手柄位置绘制
|
|
|
+ if (annotationUID && canvasPoints.length >= 4) {
|
|
|
+ const rectUID = `${annotationUID}-rect`;
|
|
|
+
|
|
|
+ // 获取旋转角度
|
|
|
+ const rotation = annotation.data.handles.textData.rotation || 0;
|
|
|
+
|
|
|
+ // 如果有旋转,计算旋转后的矩形路径
|
|
|
+ let rectanglePath = canvasPoints;
|
|
|
+ if (rotation !== 0) {
|
|
|
+ // 计算矩形中心点
|
|
|
+ const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+
|
|
|
+ // 对每个点应用旋转变换
|
|
|
+ rectanglePath = canvasPoints.map(point => {
|
|
|
+ const dx = point[0] - centerX;
|
|
|
+ const dy = point[1] - centerY;
|
|
|
+ const cos = Math.cos(rotation * Math.PI / 180);
|
|
|
+ const sin = Math.sin(rotation * Math.PI / 180);
|
|
|
+
|
|
|
+ return [
|
|
|
+ centerX + dx * cos - dy * sin,
|
|
|
+ centerY + dx * sin + dy * cos
|
|
|
+ ] as CoreTypes.Point2;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建封闭的矩形路径:从第一个点开始,顺时针遍历所有点,最后回到起点
|
|
|
+ const closedRectanglePath = [...rectanglePath, rectanglePath[0]];
|
|
|
+
|
|
|
+ drawPathSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ rectUID,
|
|
|
+ closedRectanglePath,
|
|
|
+ {
|
|
|
+ color: 'rgb(0, 255, 0)',
|
|
|
+ width: (lineWidth as number) || 1,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 直接在矩形左上角绘制文本,不使用 drawLinkedTextBox(它会画连线)
|
|
|
+ if (annotationUID && canvasPoints.length >= 4) {
|
|
|
+ const textUID = `${annotationUID}-text`;
|
|
|
+
|
|
|
+ // 计算矩形中心位置
|
|
|
+ // const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ // const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+ const centerX = canvasPoints[0][0];//(canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ const centerY = canvasPoints[0][1];//(canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+
|
|
|
+ // // 以下两行用于调试,查看文本左上角位置
|
|
|
+ // drawLine(svgDrawingHelper, annotationUID, 'cx', [centerX - 10, centerY], [centerX + 10, centerY]);
|
|
|
+ // drawLine(svgDrawingHelper, annotationUID, 'cy', [centerX, centerY - 10], [centerX, centerY + 10], { color: 'rgb(255, 0, 0)', style: 'dashed', lineWidth: '5px' });
|
|
|
+ // 2️⃣ 文本参数
|
|
|
+ const fontSize = 14;
|
|
|
+ const correctedX = centerX;
|
|
|
+ const correctedY = centerY;
|
|
|
+ const textContent = annotation.data.handles.textData.text.split('\n');
|
|
|
+ // 3️⃣ 绘制文本
|
|
|
+ drawTextBox(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ textUID,
|
|
|
+ textContent,
|
|
|
+ [correctedX, correctedY],
|
|
|
+ {
|
|
|
+ fontSize,
|
|
|
+ fontFamily: 'Arial, sans-serif',
|
|
|
+ centerX: false,
|
|
|
+ //centerY: false,
|
|
|
+ // textAnchor: 'left',
|
|
|
+ // dominantBaseline: 'hanging',
|
|
|
+ padding: 0,//覆盖内部实现的padding 25
|
|
|
+ margin: 0,
|
|
|
+ color: color as string || 'rgb(0, 0, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+ //拉伸变形
|
|
|
+ const textGroup = svgDrawingHelper.getSvgNode(`${annotationUID}::text::${textUID}`);
|
|
|
+ if (textGroup) {
|
|
|
+ //计算scalex和scaley
|
|
|
+ const textEl = textGroup;
|
|
|
+ //先调整tspan
|
|
|
+ // 获取 textEl 的所有 tspan 子元素
|
|
|
+ const tspans = textEl.querySelectorAll('tspan');
|
|
|
+ // 遍历所有 tspan 元素,设置 dy 属性
|
|
|
+ tspans.forEach((tspan: SVGElement) => {
|
|
|
+ tspan.setAttribute('dy', '1em');
|
|
|
+ });
|
|
|
+
|
|
|
+ const bbox = textEl.getBBox();
|
|
|
+
|
|
|
+ const textWidth = bbox.width;
|
|
|
+ const textHeight = bbox.height;
|
|
|
+ const targetWidth = Math.abs(canvasPoints[0][0] - canvasPoints[2][0]); // 矩形宽度(canvas/SVG 坐标)
|
|
|
+ const targetHeight = Math.abs(canvasPoints[0][1] - canvasPoints[2][1]); // 矩形高度
|
|
|
+ const scaleX = targetWidth / textWidth;
|
|
|
+ const scaleY = targetHeight / textHeight;
|
|
|
+ // 以文本左上角为缩放中心,保持左上角位置不变
|
|
|
+ const cx = bbox.x;
|
|
|
+ const cy = bbox.y;
|
|
|
+ // 统一使用矩形中心作为旋转中心,确保所有元素同步旋转
|
|
|
+ const centerX = cx + targetWidth / 2;
|
|
|
+ const centerY = cy + targetHeight / 2;
|
|
|
+ // 获取旋转角度(度数)
|
|
|
+ const rotation = annotation.data.handles.textData.rotation || 0;
|
|
|
+ this.acculateTransform(textEl, scaleX, scaleY, cx, cy, rotation, centerX, centerY,);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制手柄点
|
|
|
+ if (annotationUID) {
|
|
|
+ const handleGroupUID = '0';
|
|
|
+ // 根据选中状态调整手柄半径:选中时为12,否则为8
|
|
|
+ // 如果悬停(highlighted)但未选中,则使用10
|
|
|
+ const isSelected = annotation.isSelected;
|
|
|
+ const isHighlighted = annotation.highlighted;
|
|
|
+ let handleRadius = 8;
|
|
|
+ if (isSelected) {
|
|
|
+ handleRadius = 12;
|
|
|
+ } else if (isHighlighted) {
|
|
|
+ handleRadius = 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取旋转角度
|
|
|
+ const rotation = annotation.data.handles.textData.rotation || 0;
|
|
|
+
|
|
|
+ // 如果有旋转,计算旋转后的手柄位置
|
|
|
+ let rotatedHandlePoints = canvasPoints;
|
|
|
+ if (rotation !== 0) {
|
|
|
+ // 计算矩形中心点
|
|
|
+ const centerX = (canvasPoints[0][0] + canvasPoints[2][0]) / 2;
|
|
|
+ const centerY = (canvasPoints[0][1] + canvasPoints[2][1]) / 2;
|
|
|
+
|
|
|
+ // 对每个手柄点应用旋转变换
|
|
|
+ rotatedHandlePoints = canvasPoints.map(point => {
|
|
|
+ const dx = point[0] - centerX;
|
|
|
+ const dy = point[1] - centerY;
|
|
|
+ const cos = Math.cos(rotation * Math.PI / 180);
|
|
|
+ const sin = Math.sin(rotation * Math.PI / 180);
|
|
|
+
|
|
|
+ return [
|
|
|
+ centerX + dx * cos - dy * sin,
|
|
|
+ centerY + dx * sin + dy * cos
|
|
|
+ ] as CoreTypes.Point2;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ drawHandles(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ handleGroupUID,
|
|
|
+ rotatedHandlePoints,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 255)',
|
|
|
+ handleRadius: handleRadius,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制旋转手柄(仅对选中的注解)
|
|
|
+ if (annotationUID && annotation.isSelected) {
|
|
|
+ const rotationHandlePos = this._getRotationHandlePosition(annotation, viewport);
|
|
|
+ if (rotationHandlePos) {
|
|
|
+ const rotationHandleGroupUID = 'rotation-handle';
|
|
|
+
|
|
|
+ drawHandles(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ rotationHandleGroupUID,
|
|
|
+ [rotationHandlePos],
|
|
|
+ {
|
|
|
+ color: 'rgb(0, 150, 255)',
|
|
|
+ handleRadius: 8,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ renderStatus = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return renderStatus;
|
|
|
+ };
|
|
|
+
|
|
|
+ private acculateTransform(
|
|
|
+ textEl: SVGGElement,
|
|
|
+ scaleX: number,
|
|
|
+ scaleY: number,
|
|
|
+ scaleCenterX: number, // 缩放中心X
|
|
|
+ scaleCenterY: number, // 缩放中心Y
|
|
|
+ rotation: number = 0,
|
|
|
+ rotationCenterX?: number, // 旋转中心X(可选,默认使用缩放中心)
|
|
|
+ rotationCenterY?: number // 旋转中心Y(可选,默认使用缩放中心)
|
|
|
+ ): void {
|
|
|
+ const svg = textEl.ownerSVGElement!;
|
|
|
+ const baseCTM = textEl.getCTM();
|
|
|
+
|
|
|
+ if (!baseCTM) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有指定旋转中心,使用缩放中心
|
|
|
+ const rotCenterX = rotationCenterX ?? scaleCenterX;
|
|
|
+ const rotCenterY = rotationCenterY ?? scaleCenterY;
|
|
|
+
|
|
|
+ // 1. 构造变换矩阵(顺序:先旋转,再缩放)
|
|
|
+ const transformMatrix = svg
|
|
|
+ .createSVGMatrix()
|
|
|
+ .translate(rotCenterX, rotCenterY) // 移动到旋转中心
|
|
|
+ .rotate(rotation) // 应用旋转
|
|
|
+ .translate(-rotCenterX, -rotCenterY) // 移回原位
|
|
|
+ .translate(scaleCenterX, scaleCenterY) // 移动到缩放中心
|
|
|
+ .scaleNonUniform(scaleX, scaleY) // 应用缩放
|
|
|
+ .translate(-scaleCenterX, -scaleCenterY); // 移回原位
|
|
|
+
|
|
|
+ // 2. 合成最终矩阵
|
|
|
+ const finalMatrix = baseCTM.multiply(transformMatrix);
|
|
|
+
|
|
|
+ // 3. 清空 transform(避免叠加)
|
|
|
+ textEl.transform.baseVal.clear();
|
|
|
+
|
|
|
+ // 4. 设置最终矩阵
|
|
|
+ const finalTransform = svg.createSVGTransform();
|
|
|
+ finalTransform.setMatrix(finalMatrix);
|
|
|
+
|
|
|
+ textEl.transform.baseVal.appendItem(finalTransform);
|
|
|
+ }
|
|
|
+}
|