| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249 |
- /**
- * 矩形区域灰度测量注解接口
- */
- interface RectangleGrayscaleAnnotation extends Types.Annotation {
- data: {
- handles: {
- // 矩形四个顶点(世界坐标)
- points: CoreTypes.Point3[];
- // 当前激活的手柄索引(0-7,对应4个角点和4条边中点)
- activeHandleIndex: number | null;
- };
- label?: string;
- // 文本框位置(Canvas坐标)
- textBox?: CoreTypes.Point2;
- // 缓存的统计结果
- cachedStats?: {
- [targetId: string]: {
- // 平均灰度值
- mean: number;
- // 最小灰度值
- min: number;
- // 最大灰度值
- max: number;
- // 采样像素数量
- pixelCount: number;
- // 矩形面积 (mm²)
- area: number;
- // 矩形宽度 (mm)
- width: number;
- // 矩形高度 (mm)
- height: number;
- };
- };
- };
- }
- /**
- * 像素采样结果
- */
- interface PixelSampleResult {
- values: number[];
- coordinates: Array<{ x: number; y: number }>;
- boundingBox: { minX: number; maxX: number; minY: number; maxY: number };
- totalPixels: number;
- }
- import {
- utilities as csUtils,
- Types as CoreTypes,
- getEnabledElement,
- cache,
- } from '@cornerstonejs/core';
- import * as cornerstone from '@cornerstonejs/core';
- import {
- AnnotationTool,
- utilities,
- Types,
- annotation,
- drawing,
- } from '@cornerstonejs/tools';
- const {
- drawHandles,
- drawLinkedTextBox,
- drawRect: drawRectSvg,
- drawPath: drawPathSvg,
- } = drawing;
- import {
- PublicToolProps,
- ToolProps,
- EventTypes,
- SVGDrawingHelper,
- } from '@cornerstonejs/tools/dist/esm/types';
- /**
- * 矩形区域灰度测量工具
- * 功能: 在医学影像上绘制矩形区域,计算并显示区域内像素的灰度值统计信息
- */
- export default class RectangleGrayscaleMeasurementTool extends AnnotationTool {
- static toolName = 'RectangleGrayscaleMeasurementTool';
- editData: {
- annotation: Types.Annotation;
- viewportIdsToRender: string[];
- handleIndex?: number;
- newAnnotation?: boolean;
- hasMoved?: boolean;
- textBoxBeingMoved?: boolean;
- textBoxOffset?: CoreTypes.Point2;
- wholeToolOffset?: CoreTypes.Point2;
- originalPoints?: CoreTypes.Point3[]; // 保存拖拽开始时的原始点坐标
- } | null = null;
- isDrawing: boolean = false;
- /**
- * 创建默认注解(工具激活时自动创建)
- */
- static createDefaultAnnotation(
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): RectangleGrayscaleAnnotation {
- 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;
- const rectWidth = 100;
- const rectHeight = 80;
- // 设置矩形四个顶点(Canvas坐标)
- const topLeftCanvas: CoreTypes.Point2 = [centerX - rectWidth / 2, centerY - rectHeight / 2];
- const topRightCanvas: CoreTypes.Point2 = [centerX + rectWidth / 2, centerY - rectHeight / 2];
- const bottomRightCanvas: CoreTypes.Point2 = [centerX + rectWidth / 2, centerY + rectHeight / 2];
- const bottomLeftCanvas: CoreTypes.Point2 = [centerX - rectWidth / 2, centerY + rectHeight / 2];
- // 转换为世界坐标
- const topLeftWorld = viewport.canvasToWorld(topLeftCanvas);
- const topRightWorld = viewport.canvasToWorld(topRightCanvas);
- const bottomRightWorld = viewport.canvasToWorld(bottomRightCanvas);
- const bottomLeftWorld = viewport.canvasToWorld(bottomLeftCanvas);
- const camera = viewport.getCamera();
- const { viewPlaneNormal, viewUp } = camera;
- if (viewPlaneNormal === undefined || viewUp === undefined) {
- throw new Error('Camera parameters are 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: RectangleGrayscaleMeasurementTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: [topLeftWorld, topRightWorld, bottomRightWorld, bottomLeftWorld],
- activeHandleIndex: null,
- },
- cachedStats: {},
- },
- } as RectangleGrayscaleAnnotation;
- return annotationData;
- }
- constructor(
- toolProps: PublicToolProps = {},
- defaultToolProps: ToolProps = {
- supportedInteractionTypes: ['Mouse', 'Touch'],
- configuration: {
- shadow: true,
- preventHandleOutsideImage: false,
- },
- }
- ) {
- super(toolProps, defaultToolProps);
- }
- /**
- * 禁用手动绘制(仅支持预设注解)
- */
- addNewAnnotation(
- evt: EventTypes.InteractionEventType
- ): RectangleGrayscaleAnnotation {
- evt.preventDefault();
- return {} as RectangleGrayscaleAnnotation;
- }
- /**
- * 检查点是否靠近工具
- */
- isPointNearTool(
- element: HTMLDivElement,
- annotation: RectangleGrayscaleAnnotation,
- 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;
- }
- }
- // 检查是否在矩形内部
- if (points.length >= 4) {
- return this._isPointInRectangle(canvasCoords, points, viewport);
- }
- return false;
- }
- /**
- * 检查点是否在矩形内
- */
- private _isPointInRectangle(
- point: CoreTypes.Point2,
- rectanglePoints: CoreTypes.Point3[],
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): boolean {
- // 将世界坐标转换为Canvas坐标
- const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p));
- // 使用射线投射算法判断点是否在多边形内
- let inside = false;
- for (let i = 0, j = canvasPoints.length - 1; i < canvasPoints.length; j = i++) {
- if (((canvasPoints[i][1] > point[1]) !== (canvasPoints[j][1] > point[1])) &&
- (point[0] < (canvasPoints[j][0] - canvasPoints[i][0]) * (point[1] - canvasPoints[i][1]) / (canvasPoints[j][1] - canvasPoints[i][1]) + canvasPoints[i][0])) {
- inside = !inside;
- }
- }
- return inside;
- }
- /**
- * 检查点是否在线段上(矩形的边)
- */
- private _isPointNearLine(
- point: CoreTypes.Point2,
- rectanglePoints: CoreTypes.Point3[],
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
- proximity: number = 6
- ): boolean {
- const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p));
- // 检查四条边
- for (let i = 0; i < canvasPoints.length; i++) {
- const start = canvasPoints[i];
- const end = canvasPoints[(i + 1) % canvasPoints.length];
-
- if (this._isPointNearLineSegment(point, start, end, proximity)) {
- return true;
- }
- }
- return false;
- }
- /**
- * 检查点是否靠近线段
- */
- private _isPointNearLineSegment(
- point: CoreTypes.Point2,
- lineStart: CoreTypes.Point2,
- lineEnd: CoreTypes.Point2,
- proximity: number
- ): boolean {
- const x0 = point[0];
- const y0 = point[1];
- const x1 = lineStart[0];
- const y1 = lineStart[1];
- const x2 = lineEnd[0];
- const y2 = lineEnd[1];
- const dx = x2 - x1;
- const dy = y2 - y1;
- const length = Math.sqrt(dx * dx + dy * dy);
- if (length === 0) {
- // 线段退化为点
- const distance = Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2));
- return distance <= proximity;
- }
- // 计算点到线段的最短距离
- const t = Math.max(0, Math.min(1, ((x0 - x1) * dx + (y0 - y1) * dy) / (length * length)));
- const projX = x1 + t * dx;
- const projY = y1 + t * dy;
- const distance = Math.sqrt(Math.pow(x0 - projX, 2) + Math.pow(y0 - projY, 2));
- return distance <= proximity;
- }
- /**
- * 检查点是否在文本框上
- */
- private _isPointInTextBox(
- element: HTMLDivElement,
- annotation: RectangleGrayscaleAnnotation,
- canvasCoords: CoreTypes.Point2
- ): boolean {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- return false;
- }
- const { viewport } = enabledElement;
- const textBox = annotation.data.textBox;
- if (!textBox) {
- // 如果没有自定义位置,使用默认位置(右下角)
- const points = annotation.data.handles.points;
- if (points.length < 4) return false;
- const defaultPos = viewport.worldToCanvas(points[2]);
- return this._checkTextBoxBounds(canvasCoords, defaultPos);
- }
- return this._checkTextBoxBounds(canvasCoords, textBox);
- }
- /**
- * 检查点是否在文本框边界内(估算文本框大小)
- */
- private _checkTextBoxBounds(
- point: CoreTypes.Point2,
- textBoxPos: CoreTypes.Point2
- ): boolean {
- // 估算文本框大小(基于典型的文本行数和字体大小)
- const padding = 8;
- const lineHeight = 18;
- const textWidth = 150; // 估算宽度
- const textHeight = lineHeight * 4 + padding * 2; // 4行文本
- const x = textBoxPos[0];
- const y = textBoxPos[1];
- return (
- point[0] >= x - padding &&
- point[0] <= x + textWidth &&
- point[1] >= y - padding &&
- point[1] <= y + textHeight
- );
- }
- /**
- * 取消操作
- */
- cancel(element: HTMLDivElement): string {
- if (this.isDrawing) {
- this.isDrawing = false;
- this._deactivateDraw(element);
- this._deactivateModify(element);
- const enabledElement = getEnabledElement(element);
- if (enabledElement) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- }
- this.editData = null;
- return this.getToolName();
- }
- return '';
- }
- /**
- * 处理工具选中
- */
- toolSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: RectangleGrayscaleAnnotation
- ): void {
- // 实现工具选中逻辑
- }
- /**
- * 处理注解选中
- */
- handleSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: RectangleGrayscaleAnnotation
- ): void {
- const eventDetail = evt.detail;
- const { element } = eventDetail;
- annotation.highlighted = true;
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- }
- /**
- * 渲染注解
- */
- renderAnnotation = (
- enabledElement: CoreTypes.IEnabledElement,
- svgDrawingHelper: SVGDrawingHelper
- ): boolean => {
- const { viewport } = enabledElement;
- const { element } = viewport;
- let annotations = annotation.state.getAnnotations(
- RectangleGrayscaleMeasurementTool.toolName,
- element
- ) as RectangleGrayscaleAnnotation[];
- if (!annotations || annotations.length === 0) {
- return false;
- }
- const targetId = `imageId:${(viewport as any).getCurrentImageId?.() || ''}`;
- const styleSpecifier: any = {
- toolGroupId: this.toolGroupId,
- toolName: this.getToolName(),
- viewportId: viewport.id,
- };
- for (const annotationItem of annotations) {
- const { annotationUID, data } = annotationItem;
- const { points } = data.handles;
- const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
- if (!annotationUID) {
- continue;
- }
- styleSpecifier.annotationUID = annotationUID;
- const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotationItem);
- const color = this.getStyle('color', styleSpecifier, annotationItem);
- // 绘制矩形边框 - 使用路径绘制以保持正确的矩形形状
- if (canvasCoordinates.length >= 4) {
- const rectUID = `${annotationUID}-rect`;
- // 创建封闭的矩形路径:从第一个点开始,顺时针遍历所有点,最后回到起点
- const rectanglePath = [...canvasCoordinates, canvasCoordinates[0]]; // 复制第一个点作为结束点
- drawPathSvg(
- svgDrawingHelper,
- annotationUID as string,
- rectUID,
- rectanglePath,
- {
- color: color as string,
- width: lineWidth as number,
- }
- );
- }
- // 绘制手柄 - 根据选中和高亮状态调整大小
- const handleGroupUID = `${annotationUID}-handles`;
-
- // 根据状态设置手柄半径
- let handleRadius = 6; // 默认半径
- if (annotationItem.isSelected) {
- handleRadius = 12; // 选中状态下半径变大
- } else if (annotationItem.highlighted) {
- handleRadius = 9; // 悬停高亮时半径适中
- }
-
- drawHandles(
- svgDrawingHelper,
- annotationUID,
- handleGroupUID,
- canvasCoordinates,
- {
- color,
- handleRadius,
- }
- );
- // 绘制统计文本
- const stats = data.cachedStats?.[targetId];
- if (stats) {
- const textLines = [
- `平均: ${stats.mean}`,
- `最小: ${stats.min}`,
- `最大: ${stats.max}`,
- `面积: ${stats.area.toFixed(2)}mm²`,
- ];
- const textBoxPosition = data.textBox || canvasCoordinates[2]; // 右下角
- const textBoxUID = `${annotationUID}-text`;
- drawLinkedTextBox(
- svgDrawingHelper,
- annotationUID,
- textBoxUID,
- textLines,
- textBoxPosition,
- [canvasCoordinates[2]],
- {},
- {
- color,
- }
- );
- }
- }
- return true;
- };
- /**
- * 更新缓存的统计数据
- */
- private _updateCachedStats(
- annotation: RectangleGrayscaleAnnotation,
- enabledElement: CoreTypes.IEnabledElement
- ): void {
- const { viewport } = enabledElement;
- const { points } = annotation.data.handles;
- if (points.length < 4) {
- return;
- }
- // 使用Cornerstone缓存系统获取图像
- const imageId = viewport.getCurrentImageId?.();
- if (!imageId) {
- console.warn('[RectangleGrayscaleTool] No imageId available');
- return;
- }
- const image = cornerstone.cache.getImage(imageId);
- if (!image) {
- console.warn('[RectangleGrayscaleTool] Image not found in cache');
- return;
- }
- // 采样矩形内的像素
- const sampleResult = this._samplePixelsInRectangle(points, image, viewport);
- // 计算统计值
- const stats = this._calculateGrayscaleStats(sampleResult.values, points);
- // 更新缓存
- const targetId = `imageId:${imageId}`;
- if (!annotation.data.cachedStats) {
- annotation.data.cachedStats = {};
- }
- annotation.data.cachedStats[targetId] = stats;
- }
- /**
- * 在矩形区域内采样像素
- */
- private _samplePixelsInRectangle(
- rectanglePoints: CoreTypes.Point3[],
- image: any,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): PixelSampleResult {
- const pixelData = image.getPixelData();
- const { width, height } = image;
- // 获取矩形的边界框(像素坐标)
- const canvasPoints = rectanglePoints.map(p => viewport.worldToCanvas(p));
- let minX = Math.min(...canvasPoints.map(p => p[0]));
- let maxX = Math.max(...canvasPoints.map(p => p[0]));
- let minY = Math.min(...canvasPoints.map(p => p[1]));
- let maxY = Math.max(...canvasPoints.map(p => p[1]));
- // 限制在图像边界内
- minX = Math.max(0, Math.floor(minX));
- maxX = Math.min(width - 1, Math.floor(maxX));
- minY = Math.max(0, Math.floor(minY));
- maxY = Math.min(height - 1, Math.floor(maxY));
- const values: number[] = [];
- const coordinates: Array<{ x: number; y: number }> = [];
- // 对每个像素进行采样
- for (let y = minY; y <= maxY; y++) {
- for (let x = minX; x <= maxX; x++) {
- // 检查点是否在矩形内(使用多边形内点测试)
- if (this._isPointInPolygon([x, y], canvasPoints)) {
- const index = y * width + x;
- const value = pixelData[index];
- values.push(value);
- coordinates.push({ x, y });
- }
- }
- }
- return {
- values,
- coordinates,
- boundingBox: { minX, maxX, minY, maxY },
- totalPixels: values.length,
- };
- }
- /**
- * 使用射线投射算法检查点是否在多边形内
- */
- private _isPointInPolygon(
- point: CoreTypes.Point2,
- polygon: CoreTypes.Point2[]
- ): boolean {
- let inside = false;
- for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
- if (((polygon[i][1] > point[1]) !== (polygon[j][1] > point[1])) &&
- (point[0] < (polygon[j][0] - polygon[i][0]) * (point[1] - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
- inside = !inside;
- }
- }
- return inside;
- }
- /**
- * 计算灰度统计值
- */
- private _calculateGrayscaleStats(
- values: number[],
- rectanglePoints: CoreTypes.Point3[]
- ): {
- mean: number;
- min: number;
- max: number;
- pixelCount: number;
- area: number;
- width: number;
- height: number;
- } {
- if (values.length === 0) {
- return {
- mean: 0,
- min: 0,
- max: 0,
- pixelCount: 0,
- area: 0,
- width: 0,
- height: 0,
- };
- }
- // 计算平均值
- const sum = values.reduce((acc, val) => acc + val, 0);
- const mean = Math.round(sum / values.length);
- // 计算最小值和最大值
- const min = Math.min(...values);
- const max = Math.max(...values);
- // 计算几何尺寸
- const p1 = rectanglePoints[0];
- const p2 = rectanglePoints[2]; // 对角点
- const width = Math.abs(p2[0] - p1[0]);
- const height = Math.abs(p2[1] - p1[1]);
- const area = width * height;
- return {
- mean,
- min,
- max,
- pixelCount: values.length,
- area: parseFloat(area.toFixed(2)),
- width: parseFloat(width.toFixed(2)),
- height: parseFloat(height.toFixed(2)),
- };
- }
- // 空的辅助方法声明,避免编译错误
- _activateDraw(element: HTMLDivElement): void {}
- _deactivateDraw(element: HTMLDivElement): void {}
- _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
- );
- }
- _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 RectangleGrayscaleAnnotation;
-
- // 优先检查是否点击在文本框上
- if (this._isPointInTextBox(element, customAnn, canvasCoords)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- const textBoxPos = customAnn.data.textBox || viewport.worldToCanvas(customAnn.data.handles.points[2]);
- const textBoxOffset: CoreTypes.Point2 = [
- canvasCoords[0] - textBoxPos[0],
- canvasCoords[1] - textBoxPos[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- handleIndex: -2, // -2表示拖拽文本框
- hasMoved: false,
- textBoxBeingMoved: true,
- textBoxOffset: textBoxOffset,
- };
- customAnn.isSelected = true;
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
-
- const handle = this.getHandleNearImagePoint(
- element,
- customAnn,
- 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;
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- // 检查是否点击在线段上
- if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- // 计算鼠标点击相对于工具中心的偏移(用于拖拽)
- const toolCenter = this._calculateRectangleCenter(customAnn.data.handles.points);
- const toolCenterCanvas = viewport.worldToCanvas(toolCenter);
- const wholeToolOffset: CoreTypes.Point2 = [
- canvasCoords[0] - toolCenterCanvas[0],
- canvasCoords[1] - toolCenterCanvas[1],
- ];
- // 开始拖拽整个矩形
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- handleIndex: -1, // -1表示拖拽整个矩形
- hasMoved: false,
- wholeToolOffset: wholeToolOffset,
- };
- customAnn.isSelected = true;
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- // 检查是否点击在矩形内部(但不在线段上)
- if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
- // 仅选中,不拖拽
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- customAnn.isSelected = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- // 如果没有点击在工具上,取消所有选中状态
- for (const ann of annotations) {
- const customAnn = ann as RectangleGrayscaleAnnotation;
- customAnn.isSelected = 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 } = this.editData;
- const customAnn = ann as RectangleGrayscaleAnnotation;
- const { data } = customAnn;
- // 如果正在拖拽文本框
- if (this.editData.handleIndex === -2 && this.editData.textBoxBeingMoved) {
- const newTextBoxPos: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.textBoxOffset![0],
- canvasCoords[1] - this.editData.textBoxOffset![1],
- ];
- // 更新文本框位置
- data.textBox = newTextBoxPos;
- this.editData.hasMoved = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- // 如果正在拖拽整个矩形
- if (this.editData.handleIndex === -1) {
- const { viewport } = enabledElement;
- // 计算当前矩形质心(四个角点的平均位置)
- const currentCenterWorld = this._calculateRectangleCenter(data.handles.points);
- // 计算新的质心位置 = 当前鼠标位置 - 初始偏移
- const newCenterCanvas: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.wholeToolOffset![0],
- canvasCoords[1] - this.editData.wholeToolOffset![1],
- ];
- // 将新的质心位置转换为world坐标
- const newCenterWorld = viewport.canvasToWorld(newCenterCanvas);
- // 计算从当前质心到新质心的偏移
- const worldOffset: CoreTypes.Point3 = [
- newCenterWorld[0] - currentCenterWorld[0],
- newCenterWorld[1] - currentCenterWorld[1],
- 0, // Z轴保持不变
- ];
- // 应用相同的偏移到所有四个顶点
- for (let i = 0; i < data.handles.points.length; i++) {
- data.handles.points[i] = [
- data.handles.points[i][0] + worldOffset[0],
- data.handles.points[i][1] + worldOffset[1],
- data.handles.points[i][2],
- ] as CoreTypes.Point3;
- }
- // 仅在拖拽结束时更新统计,避免拖拽过程中的频繁计算
- 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 && this.editData.originalPoints) {
- // 计算拖拽的增量
- const originalPos = this.editData.originalPoints[activeHandleIndex];
- const deltaX = worldPos[0] - originalPos[0];
- const deltaY = worldPos[1] - originalPos[1];
- // 根据被拖拽的角点索引,重新计算所有四个顶点的位置
- // 保持矩形几何关系:直角和平行边
- const originalPoints = this.editData.originalPoints;
- // 确定矩形的变换规则:
- // 如果拖拽顶点0(左上角),则改变矩形的左边和上边
- // 如果拖拽顶点1(右上角),相反,改变右边和上边
- // 如果拖拽顶点2(右下角),改变右边和下边
- // 如果拖拽顶点3(左下角),改变左边和下边
- switch (activeHandleIndex) {
- case 0: // 拖拽左上角
- data.handles.points[0] = worldPos;
- // 固定右下角(2)
- data.handles.points[2] = [...originalPoints[2]];
- // 更新右上角(1): x同右下角, y同左上角
- data.handles.points[1] = [originalPoints[2][0], worldPos[1], worldPos[2]];
- // 更新左下角(3): x同左上角, y同右下角
- data.handles.points[3] = [worldPos[0], originalPoints[2][1], worldPos[2]];
- break;
- case 1: // 拖拽右上角
- data.handles.points[1] = worldPos;
- // 固定左下角(3)
- data.handles.points[3] = [...originalPoints[3]];
- // 更新左上角(0): x同左下角, y同右上角
- data.handles.points[0] = [originalPoints[3][0], worldPos[1], worldPos[2]];
- // 更新右下角(2): x同右上角, y同左下角
- data.handles.points[2] = [worldPos[0], originalPoints[3][1], worldPos[2]];
- break;
- case 2: // 拖拽右下角
- data.handles.points[2] = worldPos;
- // 固定左上角(0)
- data.handles.points[0] = [...originalPoints[0]];
- // 更新右上角(1): x同右下角, y同左上角
- data.handles.points[1] = [worldPos[0], originalPoints[0][1], worldPos[2]];
- // 更新左下角(3): x同左上角, y同右下角
- data.handles.points[3] = [originalPoints[0][0], worldPos[1], worldPos[2]];
- break;
- case 3: // 拖拽左下角
- data.handles.points[3] = worldPos;
- // 固定右上角(1)
- data.handles.points[1] = [...originalPoints[1]];
- // 更新左上角(0): x同左下角, y同右上角
- data.handles.points[0] = [worldPos[0], originalPoints[1][1], worldPos[2]];
- // 更新右下角(2): x同右上角, y同左下角
- data.handles.points[2] = [originalPoints[1][0], worldPos[1], worldPos[2]];
- 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 } = this.editData;
- const customAnn = ann as RectangleGrayscaleAnnotation;
- customAnn.data.handles.activeHandleIndex = null;
- customAnn.highlighted = false;
- const eventDetail = evt.detail;
- const { element } = eventDetail;
- const enabledElement = getEnabledElement(element);
- if (enabledElement) {
- const { viewport } = enabledElement;
- // 如果工具被移动了,更新缓存的统计数据
- if (hasMoved) {
- this._updateCachedStats(customAnn, enabledElement);
- }
- }
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- this.editData = null;
- 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 RectangleGrayscaleAnnotation;
- // 优先检查文本框(手型光标)
- if (this._isPointInTextBox(element, customAnn, canvasCoords)) {
- element.style.cursor = 'pointer';
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- // 检查是否靠近手柄(十字准星光标)
- const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
- if (handle) {
- element.style.cursor = 'crosshair';
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- // 检查是否在线段上(十字准星光标)
- if (this._isPointNearLine(canvasCoords, customAnn.data.handles.points, viewport, 6)) {
- element.style.cursor = 'crosshair';
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- // 检查是否在矩形内部(默认光标)
- if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
- element.style.cursor = 'default';
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- }
- // 如果没有悬停,重置高亮和光标
- if (!isHovering) {
- for (const ann of annotations) {
- const customAnn = ann as RectangleGrayscaleAnnotation;
- customAnn.highlighted = false;
- }
- element.style.cursor = 'default';
- }
- // 触发渲染以更新高亮状态
- const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- };
- _keyDownCallback = (evt: KeyboardEvent): void => {
- if (evt.key === 'Delete') {
- const element = document.activeElement as HTMLDivElement;
- if (!element) return;
- const annotations = annotation.state.getAnnotations(this.getToolName(), element);
- // 查找选中的注解
- const selectedAnnotation = annotations.find(ann => ann.isSelected);
- if (selectedAnnotation) {
- // 删除选中的注解
- annotation.state.removeAnnotation(selectedAnnotation.annotationUID || '');
- // 触发视图更新
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- }
- }
- };
- getHandleNearImagePoint(
- element: HTMLDivElement,
- annotation: RectangleGrayscaleAnnotation,
- canvasCoords: CoreTypes.Point2,
- proximity: number
- ): Types.ToolHandle | undefined {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- return undefined;
- }
- const { viewport } = enabledElement;
- const points = annotation.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) {
- annotation.data.handles.activeHandleIndex = i;
- return {
- worldPosition: point,
- } as Types.ToolHandle;
- }
- }
- annotation.data.handles.activeHandleIndex = null;
- return undefined;
- }
- /**
- * 计算矩形四个顶点的质心(中心点)
- */
- private _calculateRectangleCenter(points: CoreTypes.Point3[]): CoreTypes.Point3 {
- if (points.length < 4) {
- return [0, 0, 0];
- }
- // 计算四个顶点的平均位置作为质心
- let sumX = 0, sumY = 0, sumZ = 0;
- for (const point of points) {
- sumX += point[0];
- sumY += point[1];
- sumZ += point[2];
- }
- return [
- sumX / points.length,
- sumY / points.length,
- sumZ / points.length,
- ];
- }
- }
|