|
|
@@ -1,6 +1,7 @@
|
|
|
import * as cornerstone from '@cornerstonejs/core';
|
|
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
|
|
-import TextAnnotationTool from '@/components/measures/TextAnnotationTool';
|
|
|
+import TextAnnotationTool, { TextAnnotation } from '@/components/measures/TextAnnotationTool';
|
|
|
+import { calculateMidpoint, vectorSubtract, vectorAdd } from '@/components/measures/mathUtils';
|
|
|
|
|
|
const { MouseBindings } = cornerstoneTools.Enums;
|
|
|
|
|
|
@@ -172,4 +173,305 @@ export class TextAnnotationToolManager {
|
|
|
console.error('[TextAnnotationToolManager] Failed to create default TextAnnotation annotation:', error);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建用于操作指定注解的工具实例
|
|
|
+ * @param viewportId 视口ID
|
|
|
+ * @param annotation 要操作的注解
|
|
|
+ * @returns TextAnnotationOperator 实例
|
|
|
+ */
|
|
|
+ static createOperatorForAnnotation(viewportId: string, annotation: TextAnnotation): TextAnnotationOperator {
|
|
|
+ return new TextAnnotationOperator(viewportId, annotation);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建用于操作选中注解的工具实例
|
|
|
+ * @param viewportId 视口ID
|
|
|
+ * @returns TextAnnotationOperator 实例,如果没有选中的注解则返回 null
|
|
|
+ */
|
|
|
+ static createOperatorForSelectedAnnotation(viewportId: string): TextAnnotationOperator | null {
|
|
|
+ try {
|
|
|
+ const element = document.getElementById(viewportId) as HTMLDivElement;
|
|
|
+ if (!element) {
|
|
|
+ console.error(`[TextAnnotationToolManager] Element not found for viewport: ${viewportId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotations = cornerstoneTools.annotation.state.getAnnotations(
|
|
|
+ TextAnnotationTool.toolName,
|
|
|
+ element
|
|
|
+ );
|
|
|
+ const selectedAnnotation = annotations.find((ann: any) => ann.isSelected) as TextAnnotation;
|
|
|
+ if (!selectedAnnotation) {
|
|
|
+ console.warn(`[TextAnnotationToolManager] No selected TextAnnotation found for viewport: ${viewportId}`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return new TextAnnotationOperator(viewportId, selectedAnnotation);
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`[TextAnnotationToolManager] Error creating operator for selected annotation:`, error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 文本注解操作器类
|
|
|
+ * 提供对单个 TextAnnotation 的直接操作接口
|
|
|
+ */
|
|
|
+export class TextAnnotationOperator {
|
|
|
+ private viewportId: string;
|
|
|
+ private annotation: TextAnnotation;
|
|
|
+
|
|
|
+ constructor(viewportId: string, annotation: TextAnnotation) {
|
|
|
+ this.viewportId = viewportId;
|
|
|
+ this.annotation = annotation;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 移动注解
|
|
|
+ * @param deltaX X轴偏移量(canvas坐标)
|
|
|
+ * @param deltaY Y轴偏移量(canvas坐标)
|
|
|
+ */
|
|
|
+ move(deltaX: number, deltaY: number): boolean {
|
|
|
+ try {
|
|
|
+ const element = document.getElementById(this.viewportId) as HTMLDivElement;
|
|
|
+ if (!element) return false;
|
|
|
+
|
|
|
+ const enabledElement = cornerstone.getEnabledElement(element);
|
|
|
+ if (!enabledElement) return false;
|
|
|
+
|
|
|
+ // 计算新的中心位置
|
|
|
+ const points = this.annotation.data.handles.points;
|
|
|
+ const center = calculateMidpoint(points[0], points[2]);
|
|
|
+ const centerCanvas = enabledElement.viewport.worldToCanvas(center);
|
|
|
+ const newCenterCanvas: cornerstone.Types.Point2 = [
|
|
|
+ centerCanvas[0] + deltaX,
|
|
|
+ centerCanvas[1] + deltaY
|
|
|
+ ];
|
|
|
+ const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
|
|
|
+
|
|
|
+ // 计算所有点的偏移量
|
|
|
+ const offset = vectorSubtract(newCenterWorld, center);
|
|
|
+
|
|
|
+ // 移动所有点
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ points[i] = vectorAdd(points[i], offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新文本框位置
|
|
|
+ this.updateTextBoxFromPoints();
|
|
|
+
|
|
|
+ this.annotation.invalidated = true;
|
|
|
+ enabledElement.viewport.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error moving annotation:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缩放注解
|
|
|
+ * @param scaleFactor 缩放因子(相对于当前尺寸)
|
|
|
+ */
|
|
|
+ scale(scaleFactor: number): boolean {
|
|
|
+ try {
|
|
|
+ const currentWidth = this.annotation.data.handles.textData.originalWidth;
|
|
|
+ const currentHeight = this.annotation.data.handles.textData.originalHeight;
|
|
|
+
|
|
|
+ // 计算新的尺寸
|
|
|
+ const newWidth = Math.max(50, currentWidth * scaleFactor);
|
|
|
+ const newHeight = Math.max(30, currentHeight * scaleFactor);
|
|
|
+
|
|
|
+ // 计算中心点
|
|
|
+ const centerX = this.annotation.data.handles.textData.position[0] + this.annotation.data.handles.textData.width / 2;
|
|
|
+ const centerY = this.annotation.data.handles.textData.position[1] + this.annotation.data.handles.textData.height / 2;
|
|
|
+
|
|
|
+ // 更新位置(保持中心点不变)
|
|
|
+ const newPosition: cornerstone.Types.Point2 = [
|
|
|
+ centerX - newWidth / 2,
|
|
|
+ centerY - newHeight / 2
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 更新文本框数据
|
|
|
+ this.annotation.data.handles.textData.width = newWidth;
|
|
|
+ this.annotation.data.handles.textData.height = newHeight;
|
|
|
+ this.annotation.data.handles.textData.position = newPosition;
|
|
|
+
|
|
|
+ // 更新四个角点的位置
|
|
|
+ const element = document.getElementById(this.viewportId) as HTMLDivElement;
|
|
|
+ if (element) {
|
|
|
+ const enabledElement = cornerstone.getEnabledElement(element);
|
|
|
+ if (enabledElement) {
|
|
|
+ this.annotation.data.handles.points[0] = enabledElement.viewport.canvasToWorld([newPosition[0], newPosition[1]]);
|
|
|
+ this.annotation.data.handles.points[1] = enabledElement.viewport.canvasToWorld([newPosition[0] + newWidth, newPosition[1]]);
|
|
|
+ this.annotation.data.handles.points[2] = enabledElement.viewport.canvasToWorld([newPosition[0] + newWidth, newPosition[1] + newHeight]);
|
|
|
+ this.annotation.data.handles.points[3] = enabledElement.viewport.canvasToWorld([newPosition[0], newPosition[1] + newHeight]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.annotation.invalidated = true;
|
|
|
+ const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
|
|
|
+ viewport?.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error scaling annotation:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 旋转注解
|
|
|
+ * @param angleDegrees 旋转角度(度数)
|
|
|
+ */
|
|
|
+ rotate(angleDegrees: number): boolean {
|
|
|
+ try {
|
|
|
+ // 更新旋转角度
|
|
|
+ const currentRotation = this.annotation.data.handles.textData.rotation || 0;
|
|
|
+ this.annotation.data.handles.textData.rotation = (currentRotation + angleDegrees) % 360;
|
|
|
+
|
|
|
+ this.annotation.invalidated = true;
|
|
|
+ const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
|
|
|
+ viewport?.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error rotating annotation:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置文本内容
|
|
|
+ * @param text 新的文本内容
|
|
|
+ */
|
|
|
+ setText(text: string): boolean {
|
|
|
+ try {
|
|
|
+ this.annotation.data.handles.textData.text = text;
|
|
|
+ this.annotation.invalidated = true;
|
|
|
+
|
|
|
+ const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
|
|
|
+ viewport?.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error setting text:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置位置
|
|
|
+ * @param x X坐标(canvas坐标)
|
|
|
+ * @param y Y坐标(canvas坐标)
|
|
|
+ */
|
|
|
+ setPosition(x: number, y: number): boolean {
|
|
|
+ try {
|
|
|
+ const element = document.getElementById(this.viewportId) as HTMLDivElement;
|
|
|
+ if (!element) return false;
|
|
|
+
|
|
|
+ const enabledElement = cornerstone.getEnabledElement(element);
|
|
|
+ if (!enabledElement) return false;
|
|
|
+
|
|
|
+ const width = this.annotation.data.handles.textData.width;
|
|
|
+ const height = this.annotation.data.handles.textData.height;
|
|
|
+
|
|
|
+ // 更新文本框位置
|
|
|
+ this.annotation.data.handles.textData.position = [x, y];
|
|
|
+
|
|
|
+ // 更新四个角点的位置
|
|
|
+ this.annotation.data.handles.points[0] = enabledElement.viewport.canvasToWorld([x, y]);
|
|
|
+ this.annotation.data.handles.points[1] = enabledElement.viewport.canvasToWorld([x + width, y]);
|
|
|
+ this.annotation.data.handles.points[2] = enabledElement.viewport.canvasToWorld([x + width, y + height]);
|
|
|
+ this.annotation.data.handles.points[3] = enabledElement.viewport.canvasToWorld([x, y + height]);
|
|
|
+
|
|
|
+ this.annotation.invalidated = true;
|
|
|
+ enabledElement.viewport.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error setting position:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取边界信息
|
|
|
+ */
|
|
|
+ getBounds(): { x: number; y: number; width: number; height: number } | null {
|
|
|
+ try {
|
|
|
+ return {
|
|
|
+ x: this.annotation.data.handles.textData.position[0],
|
|
|
+ y: this.annotation.data.handles.textData.position[1],
|
|
|
+ width: this.annotation.data.handles.textData.width,
|
|
|
+ height: this.annotation.data.handles.textData.height
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error getting bounds:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除注解
|
|
|
+ */
|
|
|
+ delete(): boolean {
|
|
|
+ try {
|
|
|
+ if (this.annotation.annotationUID) {
|
|
|
+ cornerstoneTools.annotation.state.removeAnnotation(this.annotation.annotationUID);
|
|
|
+
|
|
|
+ const viewport = cornerstone.getEnabledElementByViewportId(this.viewportId)?.viewport;
|
|
|
+ viewport?.render();
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[TextAnnotationOperator] Error deleting annotation:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前文本内容
|
|
|
+ */
|
|
|
+ getText(): string {
|
|
|
+ return this.annotation.data.handles.textData.text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前旋转角度
|
|
|
+ */
|
|
|
+ getRotation(): number {
|
|
|
+ return this.annotation.data.handles.textData.rotation || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新文本框位置基于四个角点
|
|
|
+ */
|
|
|
+ private updateTextBoxFromPoints(): void {
|
|
|
+ const points = this.annotation.data.handles.points;
|
|
|
+ if (points.length < 4) return;
|
|
|
+
|
|
|
+ const element = document.getElementById(this.viewportId) as HTMLDivElement;
|
|
|
+ if (!element) return;
|
|
|
+
|
|
|
+ const enabledElement = cornerstone.getEnabledElement(element);
|
|
|
+ if (!enabledElement) return;
|
|
|
+
|
|
|
+ const canvasPoints = points.map(p => enabledElement.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]));
|
|
|
+
|
|
|
+ this.annotation.data.handles.textData.position = [minX, minY];
|
|
|
+ this.annotation.data.handles.textData.width = maxX - minX;
|
|
|
+ this.annotation.data.handles.textData.height = maxY - minY;
|
|
|
+ }
|
|
|
}
|