|
|
@@ -0,0 +1,1277 @@
|
|
|
+import { Point3, Point2 } from './mathUtils';
|
|
|
+import {
|
|
|
+ utilities as csUtils,
|
|
|
+ Types as CoreTypes,
|
|
|
+ getEnabledElement,
|
|
|
+} from '@cornerstonejs/core';
|
|
|
+
|
|
|
+import {
|
|
|
+ AnnotationTool,
|
|
|
+ utilities,
|
|
|
+ Types,
|
|
|
+ annotation,
|
|
|
+ drawing,
|
|
|
+} from '@cornerstonejs/tools';
|
|
|
+
|
|
|
+const {
|
|
|
+ drawHandles,
|
|
|
+ drawLine: drawLineSvg,
|
|
|
+ drawCircle: drawCircleSvg,
|
|
|
+} = drawing;
|
|
|
+
|
|
|
+import {
|
|
|
+ calculateMidpoint,
|
|
|
+ vectorSubtract,
|
|
|
+ vectorAdd,
|
|
|
+ getLineIntersection,
|
|
|
+} from './mathUtils';
|
|
|
+import {
|
|
|
+ PublicToolProps,
|
|
|
+ ToolProps,
|
|
|
+ EventTypes,
|
|
|
+ SVGDrawingHelper,
|
|
|
+} from '@cornerstonejs/tools/dist/esm/types';
|
|
|
+
|
|
|
+// 注解数据接口
|
|
|
+interface CircleCenterMeasurementAnnotation extends Types.Annotation {
|
|
|
+ data: {
|
|
|
+ handles: {
|
|
|
+ points: CoreTypes.Point3[];
|
|
|
+ activeHandleIndex: number | null;
|
|
|
+ };
|
|
|
+ cachedStats?: {
|
|
|
+ [targetId: string]: {
|
|
|
+ center: CoreTypes.Point3; // 圆心坐标
|
|
|
+ radius: number; // 圆半径
|
|
|
+ diameter: number; // 圆直径
|
|
|
+ };
|
|
|
+ };
|
|
|
+ };
|
|
|
+ // 选中状态
|
|
|
+ isSelected?: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+// 导出的注解数据接口
|
|
|
+export interface ExportedCircleCenterAnnotationData {
|
|
|
+ points: CoreTypes.Point3[]; // A和B两个点的世界坐标
|
|
|
+ circleInfo: {
|
|
|
+ center: CoreTypes.Point3; // 圆心坐标
|
|
|
+ radius: number; // 圆半径
|
|
|
+ diameter: number; // 圆直径
|
|
|
+ };
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: CoreTypes.Point3;
|
|
|
+ viewUp: CoreTypes.Point3;
|
|
|
+ FrameOfReferenceUID: string;
|
|
|
+ referencedImageId: string;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const cursorUrl='url("data:image/svg+xml;base64,PHN2ZyBpZD0ibW92ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC52aS1wcmltYXJ5IHsKICAgICAgICBmaWxsOiAjRkY2RTZFOwogICAgICB9CgogICAgICAudmktcHJpbWFyeSwgLnZpLWFjY2VudCB7CiAgICAgICAgZmlsbC1ydWxlOiBldmVub2RkOwogICAgICB9CgogICAgICAudmktYWNjZW50IHsKICAgICAgICBmaWxsOiAjMEMwMDU4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0idmktcHJpbWFyeSIgZD0iTTM0LDMwbC0yLTIsMi0ySDI2djhsMi0yLDIsMi01LDVIMjRsLTUtNSwyLTItMiwyaDhWMTVsLTIsMi0yLTIsNS01aDFsNSw1LTIsMi0yLTJ2OGg4bC0yLTIsMi0yLDUsNXYxWiIvPgogIDxwYXRoIGNsYXNzPSJ2aS1hY2NlbnQiIGQ9Ik0xOSwyM3YzaDR2NGgzVjI2aDRWMjNIMjZWMTlIMjN2NEgxOVoiLz4KPC9zdmc+Cg==") 24 24, crosshair';
|
|
|
+
|
|
|
+export default class CircleCenterMeasurementTool extends AnnotationTool {
|
|
|
+ handleSelectedCallback(evt: EventTypes.InteractionEventType, annotation: Types.Annotation, handle: Types.ToolHandle, interactionType: Types.InteractionTypes): void {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ const customAnnotation = annotation as CircleCenterMeasurementAnnotation;
|
|
|
+ customAnnotation.highlighted = true;
|
|
|
+
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ toolSelectedCallback(evt: EventTypes.InteractionEventType, annotation: Types.Annotation, interactionType: Types.InteractionTypes, canvasCoords?: CoreTypes.Point2): void {
|
|
|
+ const eventDetail = evt.detail;
|
|
|
+ const { element } = eventDetail;
|
|
|
+
|
|
|
+ const customAnnotation = annotation as CircleCenterMeasurementAnnotation;
|
|
|
+ customAnnotation.isSelected = true;
|
|
|
+ customAnnotation.highlighted = true;
|
|
|
+
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ viewportIdsToRender
|
|
|
+ );
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ }
|
|
|
+ static toolName = 'CircleCenterMeasurementTool';
|
|
|
+
|
|
|
+ editData: {
|
|
|
+ annotation: Types.Annotation;
|
|
|
+ viewportIdsToRender: string[];
|
|
|
+ handleIndex?: number;
|
|
|
+ newAnnotation?: boolean;
|
|
|
+ hasMoved?: boolean;
|
|
|
+ // 是否移动整个工具
|
|
|
+ movingWholeTool?: boolean;
|
|
|
+ // 移动整个工具时的初始偏移量
|
|
|
+ wholeToolOffset?: CoreTypes.Point2;
|
|
|
+ // 是否移动圆(通过圆环拖拽)
|
|
|
+ movingCircle?: boolean;
|
|
|
+ // 移动圆时的初始偏移量
|
|
|
+ circleOffset?: CoreTypes.Point2;
|
|
|
+ } | null = null;
|
|
|
+
|
|
|
+ isDrawing: boolean = false;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建一个预设的注解,带有两个默认点
|
|
|
+ */
|
|
|
+ static createDefaultAnnotation(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): CircleCenterMeasurementAnnotation {
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 创建两个点:A点在左侧,B点在右侧
|
|
|
+ const pointAX = centerX - 100;
|
|
|
+ const pointBX = centerX + 100;
|
|
|
+ const pointY = centerY;
|
|
|
+
|
|
|
+ const canvasPoints: CoreTypes.Point2[] = [
|
|
|
+ [pointAX, pointY], // 点A
|
|
|
+ [pointBX, pointY], // 点B
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 转换为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: CircleCenterMeasurementTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ label: '',
|
|
|
+ handles: {
|
|
|
+ points: worldPoints,
|
|
|
+ activeHandleIndex: null,
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出注解数据
|
|
|
+ */
|
|
|
+ static exportAnnotationData(
|
|
|
+ annotation: CircleCenterMeasurementAnnotation,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): ExportedCircleCenterAnnotationData | null {
|
|
|
+ const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
|
|
|
+ const cachedStats = annotation.data.cachedStats?.[targetId];
|
|
|
+
|
|
|
+ if (!cachedStats) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ points: [...annotation.data.handles.points],
|
|
|
+ circleInfo: {
|
|
|
+ center: cachedStats.center,
|
|
|
+ radius: cachedStats.radius,
|
|
|
+ diameter: cachedStats.diameter,
|
|
|
+ },
|
|
|
+ metadata: {
|
|
|
+ viewPlaneNormal: annotation.metadata?.viewPlaneNormal || [0, 0, 1],
|
|
|
+ viewUp: annotation.metadata?.viewUp || [0, 1, 0],
|
|
|
+ FrameOfReferenceUID: annotation.metadata?.FrameOfReferenceUID || '',
|
|
|
+ referencedImageId: annotation.metadata?.referencedImageId || '',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从导出的数据恢复注解
|
|
|
+ */
|
|
|
+ static restoreFromExportedData(
|
|
|
+ exportedData: ExportedCircleCenterAnnotationData,
|
|
|
+ element: HTMLDivElement,
|
|
|
+ viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
|
|
|
+ ): CircleCenterMeasurementAnnotation {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ throw new Error('Element is not enabled');
|
|
|
+ }
|
|
|
+
|
|
|
+ const annotationData = {
|
|
|
+ invalidated: true,
|
|
|
+ highlighted: false,
|
|
|
+ metadata: {
|
|
|
+ ...exportedData.metadata,
|
|
|
+ toolName: CircleCenterMeasurementTool.toolName,
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ label: '',
|
|
|
+ handles: {
|
|
|
+ points: [...exportedData.points],
|
|
|
+ activeHandleIndex: null,
|
|
|
+ },
|
|
|
+ cachedStats: {},
|
|
|
+ },
|
|
|
+ } as CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ return annotationData;
|
|
|
+ }
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ toolProps: PublicToolProps = {},
|
|
|
+ defaultToolProps: ToolProps = {
|
|
|
+ supportedInteractionTypes: ['Mouse', 'Touch'],
|
|
|
+ configuration: {
|
|
|
+ shadow: true,
|
|
|
+ preventHandleOutsideImage: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ) {
|
|
|
+ super(toolProps, defaultToolProps);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加新注解 - 禁用此功能,因为我们只使用预设的注解
|
|
|
+ */
|
|
|
+ addNewAnnotation(
|
|
|
+ evt: EventTypes.InteractionEventType
|
|
|
+ ): CircleCenterMeasurementAnnotation {
|
|
|
+ evt.preventDefault();
|
|
|
+ return {} as CircleCenterMeasurementAnnotation;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查点是否靠近工具
|
|
|
+ */
|
|
|
+ isPointNearTool(
|
|
|
+ element: HTMLDivElement,
|
|
|
+ annotation: CircleCenterMeasurementAnnotation,
|
|
|
+ canvasCoords: CoreTypes.Point2,
|
|
|
+ proximity: number
|
|
|
+ ): boolean {
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (!enabledElement) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+
|
|
|
+ const points = annotation.data.handles.points;
|
|
|
+
|
|
|
+ // 检查是否靠近任意一个手柄点
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近线段(A-B连线)
|
|
|
+ if (points.length >= 2) {
|
|
|
+ const p1Canvas = viewport.worldToCanvas(points[0]);
|
|
|
+ const p2Canvas = viewport.worldToCanvas(points[1]);
|
|
|
+
|
|
|
+ const dist = this._distanceToSegment(canvasCoords, p1Canvas, p2Canvas);
|
|
|
+ if (dist < proximity) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近圆环
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
|
|
|
+ if (cachedStats?.center && cachedStats.radius > 0) {
|
|
|
+ const centerCanvas = viewport.worldToCanvas(cachedStats.center);
|
|
|
+ const distanceToCenter = Math.sqrt(
|
|
|
+ Math.pow(canvasCoords[0] - centerCanvas[0], 2) +
|
|
|
+ Math.pow(canvasCoords[1] - centerCanvas[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 检查是否在圆环附近(圆周 ± 一定容差)
|
|
|
+ const ringTolerance = 5;
|
|
|
+ if (Math.abs(distanceToCenter - cachedStats.radius) < ringTolerance) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算点到线段的距离
|
|
|
+ */
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消操作
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+
|
|
|
+ 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 CircleCenterMeasurementAnnotation).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();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 鼠标移动回调 - 用于修改模式,处理悬停检测
|
|
|
+ */
|
|
|
+ _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 CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ // 检查是否靠近手柄
|
|
|
+ const handle = this.getHandleNearImagePoint(element, customAnn as Types.Annotation, canvasCoords, 6);
|
|
|
+ if (handle) {
|
|
|
+ element.style.cursor = cursorUrl;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近线段
|
|
|
+ if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
|
|
|
+ element.style.cursor = cursorUrl;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否靠近圆环
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
|
|
|
+ if (cachedStats?.center && cachedStats.radius > 0) {
|
|
|
+ const centerCanvas = viewport.worldToCanvas(cachedStats.center);
|
|
|
+ const distanceToCenter = Math.sqrt(
|
|
|
+ Math.pow(canvasCoords[0] - centerCanvas[0], 2) +
|
|
|
+ Math.pow(canvasCoords[1] - centerCanvas[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (Math.abs(distanceToCenter - cachedStats.radius) < 5) {
|
|
|
+ element.style.cursor = cursorUrl;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ isHovering = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有悬停在任何地方,重置高亮
|
|
|
+ if (!isHovering) {
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as CircleCenterMeasurementAnnotation;
|
|
|
+ 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 CircleCenterMeasurementAnnotation;
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
|
|
|
+
|
|
|
+ if (cachedStats?.center && cachedStats.radius > 0) {
|
|
|
+ const centerCanvas = viewport.worldToCanvas(cachedStats.center);
|
|
|
+ const distanceToCenter = Math.sqrt(
|
|
|
+ Math.pow(canvasCoords[0] - centerCanvas[0], 2) +
|
|
|
+ Math.pow(canvasCoords[1] - centerCanvas[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (Math.abs(distanceToCenter - cachedStats.radius) < 5) {
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ const circleOffset: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - centerCanvas[0],
|
|
|
+ canvasCoords[1] - centerCanvas[1],
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.editData = {
|
|
|
+ annotation: customAnn,
|
|
|
+ viewportIdsToRender,
|
|
|
+ hasMoved: false,
|
|
|
+ movingCircle: true,
|
|
|
+ circleOffset: circleOffset,
|
|
|
+ };
|
|
|
+
|
|
|
+ customAnn.isSelected = true;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找最近的手柄
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as CircleCenterMeasurementAnnotation;
|
|
|
+ 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,
|
|
|
+ };
|
|
|
+
|
|
|
+ customAnn.isSelected = true;
|
|
|
+ customAnn.highlighted = true;
|
|
|
+
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否点击在线段上
|
|
|
+ for (const ann of annotations) {
|
|
|
+ const customAnn = ann as CircleCenterMeasurementAnnotation;
|
|
|
+ if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
|
|
|
+ const viewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算相对于工具中心的偏移量
|
|
|
+ const points = customAnn.data.handles.points;
|
|
|
+ if (points.length >= 2) {
|
|
|
+ const center = calculateMidpoint(points[0], points[1]);
|
|
|
+ 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 CircleCenterMeasurementAnnotation;
|
|
|
+ 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, movingWholeTool, movingCircle } = this.editData;
|
|
|
+ const customAnn = ann as CircleCenterMeasurementAnnotation;
|
|
|
+ const { data } = customAnn;
|
|
|
+
|
|
|
+ // 如果正在移动圆
|
|
|
+ if (movingCircle && this.editData.circleOffset) {
|
|
|
+ const targetId = this.getTargetId(enabledElement.viewport);
|
|
|
+ const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
|
|
|
+
|
|
|
+ if (cachedStats?.center) {
|
|
|
+ // 计算新的圆心位置
|
|
|
+ const newCenterCanvas: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - this.editData.circleOffset[0],
|
|
|
+ canvasCoords[1] - this.editData.circleOffset[1],
|
|
|
+ ];
|
|
|
+ const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
|
|
|
+
|
|
|
+ // 计算移动偏移量
|
|
|
+ const offset = vectorSubtract(newCenterWorld, cachedStats.center);
|
|
|
+
|
|
|
+ // 移动A和B两个点
|
|
|
+ if (data.handles.points.length >= 2) {
|
|
|
+ data.handles.points[0] = vectorAdd(data.handles.points[0], offset);
|
|
|
+ data.handles.points[1] = vectorAdd(data.handles.points[1], offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新计算圆信息
|
|
|
+ this._updateCachedStats(customAnn, enabledElement);
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果正在移动整个工具
|
|
|
+ if (movingWholeTool && this.editData.wholeToolOffset) {
|
|
|
+ const points = data.handles.points;
|
|
|
+ if (points.length >= 2) {
|
|
|
+ // 计算新的中心位置
|
|
|
+ const newCenterCanvas: CoreTypes.Point2 = [
|
|
|
+ canvasCoords[0] - this.editData.wholeToolOffset[0],
|
|
|
+ canvasCoords[1] - this.editData.wholeToolOffset[1],
|
|
|
+ ];
|
|
|
+ const newCenterWorld = enabledElement.viewport.canvasToWorld(newCenterCanvas);
|
|
|
+
|
|
|
+ const oldCenter = calculateMidpoint(points[0], points[1]);
|
|
|
+ const offset = vectorSubtract(newCenterWorld, oldCenter);
|
|
|
+
|
|
|
+ // 移动所有点
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ points[i] = vectorAdd(points[i], offset);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新计算圆信息
|
|
|
+ this._updateCachedStats(customAnn, enabledElement);
|
|
|
+
|
|
|
+ this.editData.hasMoved = true;
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
|
|
+
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则处理手柄拖拽
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
|
|
|
+ data.handles.points[activeHandleIndex] = worldPos;
|
|
|
+
|
|
|
+ // 重新计算圆信息
|
|
|
+ this._updateCachedStats(customAnn, enabledElement);
|
|
|
+
|
|
|
+ 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, movingCircle } = this.editData;
|
|
|
+ const customAnn = ann as CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ customAnn.data.handles.activeHandleIndex = null;
|
|
|
+
|
|
|
+ // 如果没有拖拽且点击在线段上或圆环上,则进入选中状态
|
|
|
+ if (!hasMoved && (movingWholeTool || movingCircle)) {
|
|
|
+ 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 CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ if (annotation) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ const points = data.handles.points;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (activeHandleIndex !== null && activeHandleIndex < points.length) {
|
|
|
+ points[activeHandleIndex] = worldPos;
|
|
|
+
|
|
|
+ this._updateCachedStats(
|
|
|
+ annotation as CircleCenterMeasurementAnnotation,
|
|
|
+ enabledElement
|
|
|
+ );
|
|
|
+
|
|
|
+ this.editData!.hasMoved = true;
|
|
|
+ } else if (this.editData!.newAnnotation) {
|
|
|
+ const lastIndex = points.length - 1;
|
|
|
+ if (lastIndex >= 0) {
|
|
|
+ points[lastIndex] = worldPos;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 CircleCenterMeasurementAnnotation;
|
|
|
+
|
|
|
+ if (newAnnotation && !hasMoved) {
|
|
|
+ const points = data.handles.points;
|
|
|
+
|
|
|
+ if (points.length < 2) {
|
|
|
+ const { currentPoints } = eventDetail;
|
|
|
+ const worldPos = currentPoints.world;
|
|
|
+
|
|
|
+ if (points.length > 0) {
|
|
|
+ points[points.length - 1] = worldPos;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (points.length < 2) {
|
|
|
+ points.push(worldPos);
|
|
|
+ data.handles.activeHandleIndex = points.length - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (points.length === 2) {
|
|
|
+ this.isDrawing = false;
|
|
|
+ data.handles.activeHandleIndex = null;
|
|
|
+ annotation.highlighted = false;
|
|
|
+ this.editData.newAnnotation = false;
|
|
|
+ this._deactivateDraw(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ const enabledElement = getEnabledElement(element);
|
|
|
+ if (enabledElement) {
|
|
|
+ this._updateCachedStats(
|
|
|
+ annotation as CircleCenterMeasurementAnnotation,
|
|
|
+ enabledElement
|
|
|
+ );
|
|
|
+
|
|
|
+ const { renderingEngine } = enabledElement;
|
|
|
+ const newViewportIdsToRender =
|
|
|
+ utilities.viewportFilters.getViewportIdsWithToolToRender(
|
|
|
+ element,
|
|
|
+ this.getToolName()
|
|
|
+ );
|
|
|
+ utilities.triggerAnnotationRenderForViewportIds(
|
|
|
+ newViewportIdsToRender
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (hasMoved) {
|
|
|
+ this.editData.hasMoved = false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取目标ID
|
|
|
+ */
|
|
|
+ getTargetId(viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport): string | undefined {
|
|
|
+ return `imageId:${viewport.getCurrentImageId?.() || ''}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取靠近图像点的手柄
|
|
|
+ */
|
|
|
+ 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 customAnnotation = annotation as CircleCenterMeasurementAnnotation;
|
|
|
+ const points = customAnnotation.data.handles.points;
|
|
|
+ const handleProximity = Math.max(proximity, 15);
|
|
|
+
|
|
|
+ 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 < handleProximity) {
|
|
|
+ customAnnotation.data.handles.activeHandleIndex = i;
|
|
|
+ return {
|
|
|
+ worldPosition: point,
|
|
|
+ } as Types.ToolHandle;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ customAnnotation.data.handles.activeHandleIndex = null;
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新缓存的统计数据
|
|
|
+ */
|
|
|
+ _updateCachedStats(
|
|
|
+ annotation: CircleCenterMeasurementAnnotation,
|
|
|
+ enabledElement: CoreTypes.IEnabledElement
|
|
|
+ ): void {
|
|
|
+ const { viewport } = enabledElement;
|
|
|
+ const { data } = annotation;
|
|
|
+ const points = data.handles.points;
|
|
|
+
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ if (!targetId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!data.cachedStats) {
|
|
|
+ data.cachedStats = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!data.cachedStats[targetId]) {
|
|
|
+ data.cachedStats[targetId] = {
|
|
|
+ center: [0, 0, 0] as CoreTypes.Point3,
|
|
|
+ radius: 0,
|
|
|
+ diameter: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const stats = data.cachedStats[targetId];
|
|
|
+
|
|
|
+ if (points.length >= 2) {
|
|
|
+ // 计算圆心:A和B的中点
|
|
|
+ const center = calculateMidpoint(points[0], points[1]);
|
|
|
+
|
|
|
+ // 计算直径:A和B之间的距离
|
|
|
+ const diameter = Math.sqrt(
|
|
|
+ Math.pow(points[1][0] - points[0][0], 2) +
|
|
|
+ Math.pow(points[1][1] - points[0][1], 2) +
|
|
|
+ Math.pow(points[1][2] - points[0][2], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算半径
|
|
|
+ const radius = diameter / 2;
|
|
|
+
|
|
|
+ // 存储计算结果
|
|
|
+ stats.center = center;
|
|
|
+ stats.radius = radius;
|
|
|
+ stats.diameter = diameter;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 渲染注解
|
|
|
+ */
|
|
|
+ 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 CircleCenterMeasurementAnnotation;
|
|
|
+ const { annotationUID, data } = annotation;
|
|
|
+ const points = data.handles.points;
|
|
|
+ const activeHandleIndex = data.handles.activeHandleIndex;
|
|
|
+
|
|
|
+ if (points.length < 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const targetId = this.getTargetId(viewport);
|
|
|
+ const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
|
|
|
+
|
|
|
+ // 转换点为canvas坐标
|
|
|
+ const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
|
|
|
+
|
|
|
+ // 绘制A-B连线
|
|
|
+ if (annotationUID) {
|
|
|
+ const lineUID = `${annotationUID}-line`;
|
|
|
+ const lineOptions = {
|
|
|
+ color: 'rgb(0, 255, 0)', // 绿色线段
|
|
|
+ width: 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ lineUID,
|
|
|
+ canvasPoints[0],
|
|
|
+ canvasPoints[1],
|
|
|
+ lineOptions
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制圆
|
|
|
+ if (cachedStats?.center && cachedStats.radius > 0 && annotationUID) {
|
|
|
+ const centerCanvas = viewport.worldToCanvas(cachedStats.center);
|
|
|
+ const circleUID = `${annotationUID}-circle`;
|
|
|
+
|
|
|
+ // 计算canvas坐标系中的半径(像素距离)
|
|
|
+ const radiusCanvas = Math.sqrt(
|
|
|
+ Math.pow(canvasPoints[0][0] - centerCanvas[0], 2) +
|
|
|
+ Math.pow(canvasPoints[0][1] - centerCanvas[1], 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ drawCircleSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ circleUID,
|
|
|
+ centerCanvas,
|
|
|
+ radiusCanvas,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 0, 0)', // 红色圆
|
|
|
+ width: 2,
|
|
|
+ fill: 'transparent',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制圆心标记
|
|
|
+ const centerMarkUID = `${annotationUID}-center`;
|
|
|
+ drawCircleSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ centerMarkUID,
|
|
|
+ centerCanvas,
|
|
|
+ 4,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 0, 0)',
|
|
|
+ fill: 'rgb(255, 0, 0)',
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 绘制圆心十字标记
|
|
|
+ const crossSize = 6;
|
|
|
+ const crossUID1 = `${annotationUID}-cross1`;
|
|
|
+ const crossUID2 = `${annotationUID}-cross2`;
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ crossUID1,
|
|
|
+ [centerCanvas[0] - crossSize, centerCanvas[1]],
|
|
|
+ [centerCanvas[0] + crossSize, centerCanvas[1]],
|
|
|
+ { color: 'rgb(255, 0, 0)', width: 1 }
|
|
|
+ );
|
|
|
+
|
|
|
+ drawLineSvg(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ crossUID2,
|
|
|
+ [centerCanvas[0], centerCanvas[1] - crossSize],
|
|
|
+ [centerCanvas[0], centerCanvas[1] + crossSize],
|
|
|
+ { color: 'rgb(255, 0, 0)', width: 1 }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // 绘制手柄点
|
|
|
+ if (annotationUID) {
|
|
|
+ const handleGroupUID = '0';
|
|
|
+ const isSelected = annotation.isSelected || annotation.highlighted;
|
|
|
+ const handleRadius = isSelected ? 12 : 8;
|
|
|
+
|
|
|
+ drawHandles(
|
|
|
+ svgDrawingHelper,
|
|
|
+ annotationUID,
|
|
|
+ handleGroupUID,
|
|
|
+ canvasPoints,
|
|
|
+ {
|
|
|
+ color: 'rgb(255, 255, 255)',
|
|
|
+ handleRadius: handleRadius,
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ renderStatus = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return renderStatus;
|
|
|
+ };
|
|
|
+}
|