| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687 |
- import { Point3, Point2, fitCircleFrom3Points, calculateOppositeVector } 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,
- drawLine: drawLineSvg,
- drawCircle: drawCircleSvg,
- drawPath: drawPathSvg,
- } = drawing;
- import {
- vectorSubtract,
- vectorAdd,
- vectorScale,
- getAngleBetweenLines,
- degrees,
- } from './mathUtils';
- import {
- PublicToolProps,
- ToolProps,
- EventTypes,
- SVGDrawingHelper,
- } from '@cornerstonejs/tools/dist/esm/types';
- // 注解数据接口
- interface HipNHAAngleMeasurementAnnotation extends Types.Annotation {
- data: {
- handles: {
- points: CoreTypes.Point3[]; // 8个点的世界坐标
- activeHandleIndex: number | null; // 当前激活的手柄索引
- textBoxLeft?: CoreTypes.Point2; // 左侧角度文本框位置
- textBoxRight?: CoreTypes.Point2; // 右侧角度文本框位置
- };
- cachedStats?: {
- [targetId: string]: {
- // 左侧圆信息
- centerM1?: CoreTypes.Point3; // 左侧圆心M1
- radiusLeft?: number; // 左侧半径
- // 右侧圆信息
- centerM2?: CoreTypes.Point3; // 右侧圆心M2
- radiusRight?: number; // 右侧半径
- // 角度信息
- angleLeft?: number; // 左侧NHA角(度数)
- angleRight?: number; // 右侧NHA角(度数)
- // 辅助向量(用于绘制角度弧线)
- baselineVector?: CoreTypes.Point3; // M1-M2基准线向量
- leftTangentVector?: CoreTypes.Point3; // M1-4切线向量
- rightTangentVector?: CoreTypes.Point3; // M2-8切线向量
- };
- };
- };
- }
- // 导出的注解数据接口
- export interface ExportedHipNHAAnnotationData {
- points: CoreTypes.Point3[]; // 8个点的世界坐标
- circles: {
- centerM1: CoreTypes.Point3; // 左侧圆心
- radiusLeft: number; // 左侧半径
- centerM2: CoreTypes.Point3; // 右侧圆心
- radiusRight: number; // 右侧半径
- };
- angles: {
- angleLeft: number; // 左侧NHA角
- angleRight: number; // 右侧NHA角
- };
- textBoxPositions?: {
- textBoxLeft?: CoreTypes.Point2; // 左侧文本框位置
- textBoxRight?: CoreTypes.Point2; // 右侧文本框位置
- };
- metadata: {
- viewPlaneNormal: CoreTypes.Point3;
- viewUp: CoreTypes.Point3;
- FrameOfReferenceUID: string;
- referencedImageId: string;
- };
- }
- export default class HipNHAAngleMeasurementTool extends AnnotationTool {
- static toolName = 'HipNHAAngleMeasurementTool';
- editData: {
- annotation: Types.Annotation;
- viewportIdsToRender: string[];
- handleIndex?: number;
- newAnnotation?: boolean;
- hasMoved?: boolean;
- textBoxBeingMoved?: 'Left' | 'Right'; // 正在拖拽的文本框
- textBoxOffset?: CoreTypes.Point2; // 文本框拖拽时的偏移量
- movingWholeTool?: boolean; // 是否正在移动整个工具
- wholeToolOffset?: CoreTypes.Point2; // 移动整个工具时的偏移量
- selected?: boolean; // 工具选中状态
- } | null = null;
- isDrawing: boolean = false;
- /**
- * 创建一个预设的注解,带有8个默认点
- * @param element HTML元素
- * @param viewport Viewport实例
- */
- static createDefaultAnnotation(
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): HipNHAAngleMeasurementAnnotation {
- 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;
- // 左侧圆:圆心在中心左侧150px,半径60px
- const leftCircleCenter: CoreTypes.Point2 = [centerX - 150, centerY];
- const leftRadius = 60;
- // 右侧圆:圆心在中心右侧150px,半径60px
- const rightCircleCenter: CoreTypes.Point2 = [centerX + 150, centerY];
- const rightRadius = 60;
- // 生成左侧圆上的3个点(120度间隔)
- const leftPoint1: CoreTypes.Point2 = [
- leftCircleCenter[0] + leftRadius * Math.cos(0),
- leftCircleCenter[1] + leftRadius * Math.sin(0)
- ];
- const leftPoint2: CoreTypes.Point2 = [
- leftCircleCenter[0] + leftRadius * Math.cos(Math.PI * 2 / 3),
- leftCircleCenter[1] + leftRadius * Math.sin(Math.PI * 2 / 3)
- ];
- const leftPoint3: CoreTypes.Point2 = [
- leftCircleCenter[0] + leftRadius * Math.cos(Math.PI * 4 / 3),
- leftCircleCenter[1] + leftRadius * Math.sin(Math.PI * 4 / 3)
- ];
- // 左侧髋臼边缘点(在圆心上方)
- const leftAcetabularPoint: CoreTypes.Point2 = [
- leftCircleCenter[0] - 20,
- leftCircleCenter[1] - 80
- ];
- // 生成右侧圆上的3个点(120度间隔)
- const rightPoint1: CoreTypes.Point2 = [
- rightCircleCenter[0] + rightRadius * Math.cos(0),
- rightCircleCenter[1] + rightRadius * Math.sin(0)
- ];
- const rightPoint2: CoreTypes.Point2 = [
- rightCircleCenter[0] + rightRadius * Math.cos(Math.PI * 2 / 3),
- rightCircleCenter[1] + rightRadius * Math.sin(Math.PI * 2 / 3)
- ];
- const rightPoint3: CoreTypes.Point2 = [
- rightCircleCenter[0] + rightRadius * Math.cos(Math.PI * 4 / 3),
- rightCircleCenter[1] + rightRadius * Math.sin(Math.PI * 4 / 3)
- ];
- // 右侧髋臼边缘点(在圆心上方)
- const rightAcetabularPoint: CoreTypes.Point2 = [
- rightCircleCenter[0] + 20,
- rightCircleCenter[1] - 80
- ];
- const canvasPoints: CoreTypes.Point2[] = [
- leftPoint1, // 点0: 左圆点1
- leftPoint2, // 点1: 左圆点2
- leftPoint3, // 点2: 左圆点3
- leftAcetabularPoint, // 点3: 左侧髋臼边缘点
- rightPoint1, // 点4: 右圆点1
- rightPoint2, // 点5: 右圆点2
- rightPoint3, // 点6: 右圆点3
- rightAcetabularPoint, // 点7: 右侧髋臼边缘点
- ];
- // 转换为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: HipNHAAngleMeasurementTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: worldPoints,
- activeHandleIndex: null,
- },
- cachedStats: {},
- },
- } as HipNHAAngleMeasurementAnnotation;
- return annotationData;
- }
- /**
- * 导出注解数据
- * @param annotation 要导出的注解
- * @param viewport Viewport实例
- * @returns 导出的数据对象
- */
- static exportAnnotationData(
- annotation: HipNHAAngleMeasurementAnnotation,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): ExportedHipNHAAnnotationData | null {
- const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
- const cachedStats = annotation.data.cachedStats?.[targetId];
- if (!cachedStats || !cachedStats.centerM1 || !cachedStats.centerM2) {
- return null;
- }
- return {
- points: [...annotation.data.handles.points],
- circles: {
- centerM1: cachedStats.centerM1,
- radiusLeft: cachedStats.radiusLeft || 0,
- centerM2: cachedStats.centerM2,
- radiusRight: cachedStats.radiusRight || 0,
- },
- angles: {
- angleLeft: cachedStats.angleLeft || 0,
- angleRight: cachedStats.angleRight || 0,
- },
- textBoxPositions: {
- textBoxLeft: annotation.data.handles.textBoxLeft,
- textBoxRight: annotation.data.handles.textBoxRight,
- },
- 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: ExportedHipNHAAnnotationData,
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): HipNHAAngleMeasurementAnnotation {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- throw new Error('Element is not enabled');
- }
- const annotationData = {
- invalidated: true,
- highlighted: false,
- metadata: {
- ...exportedData.metadata,
- toolName: HipNHAAngleMeasurementTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: [...exportedData.points],
- activeHandleIndex: null,
- textBoxLeft: exportedData.textBoxPositions?.textBoxLeft,
- textBoxRight: exportedData.textBoxPositions?.textBoxRight,
- },
- cachedStats: {},
- },
- } as HipNHAAngleMeasurementAnnotation;
- return annotationData;
- }
- constructor(
- toolProps: PublicToolProps = {},
- defaultToolProps: ToolProps = {
- supportedInteractionTypes: ['Mouse', 'Touch'],
- configuration: {
- shadow: true,
- preventHandleOutsideImage: false,
- },
- }
- ) {
- super(toolProps, defaultToolProps);
- }
- /**
- * 设置注解选中状态
- * @param annotation 要设置的注解
- * @param selected 选中状态
- */
- private _setAnnotationSelected(annotation: HipNHAAngleMeasurementAnnotation, selected: boolean): void {
- annotation.isSelected = selected;
- if (selected) {
- annotation.highlighted = true;
- }
- }
- /**
- * 添加新注解 - 禁用此功能
- */
- addNewAnnotation(
- evt: EventTypes.InteractionEventType
- ): HipNHAAngleMeasurementAnnotation {
- evt.preventDefault();
- return {} as HipNHAAngleMeasurementAnnotation;
- }
- /**
- * 计算点到线段的距离
- */
- 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);
- }
- /**
- * 检查点是否靠近工具
- */
- isPointNearTool(
- element: HTMLDivElement,
- annotation: HipNHAAngleMeasurementAnnotation,
- canvasCoords: CoreTypes.Point2,
- proximity: number
- ): boolean {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- return false;
- }
- const { viewport } = enabledElement;
- const points = annotation.data.handles.points;
- //检查是否在文本框上
- if (this._isPointInTextBox(canvasCoords, annotation.data.handles.textBoxLeft || [0, 0]) ||
- this._isPointInTextBox(canvasCoords, annotation.data.handles.textBoxRight || [0, 0])) {
- 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;
- }
- }
- // 检查是否靠近线段
- if (points.length >= 8) {
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
- if (cachedStats) {
- // 检查基准线 M1-M2
- if (cachedStats.centerM1 && cachedStats.centerM2) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const dist = this._distanceToSegment(canvasCoords, centerM1Canvas, centerM2Canvas);
- if (dist < proximity) {
- return true;
- }
- }
- // 检查左侧切线 M1-point3
- if (cachedStats.centerM1 && points.length > 3) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const point3Canvas = viewport.worldToCanvas(points[3]);
- const dist = this._distanceToSegment(canvasCoords, centerM1Canvas, point3Canvas);
- if (dist < proximity) {
- return true;
- }
- }
- // 检查右侧切线 M2-point7
- if (cachedStats.centerM2 && points.length > 7) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const point7Canvas = viewport.worldToCanvas(points[7]);
- const dist = this._distanceToSegment(canvasCoords, centerM2Canvas, point7Canvas);
- if (dist < proximity) {
- return true;
- }
- }
- }
- }
- return false;
- }
- /**
- * 检查点击是否在文本框内
- */
- private _isPointInTextBox(
- canvasPoint: CoreTypes.Point2,
- textBoxPosition: CoreTypes.Point2,
- textWidth: number = 120,
- textHeight: number = 40
- ): boolean {
- const [x, y] = canvasPoint;
- const [tx, ty] = textBoxPosition;
- const margin = 10;
- return (
- x >= tx - margin &&
- x <= tx + textWidth + margin &&
- y >= ty - margin &&
- y <= ty + textHeight + margin
- );
- }
- /**
- * 创建角度弧线的点数组
- */
- private _createAngleArcPoints(
- center: CoreTypes.Point2,
- vector1: CoreTypes.Point2,
- vector2: CoreTypes.Point2,
- radius: number
- ): CoreTypes.Point2[] {
- const angle1 = Math.atan2(vector1[1], vector1[0]);
- const angle2 = Math.atan2(vector2[1], vector2[0]);
- let startAngle = angle1;
- let endAngle = angle2;
- let angleDiff = endAngle - startAngle;
- if (angleDiff > Math.PI) {
- angleDiff -= 2 * Math.PI;
- } else if (angleDiff < -Math.PI) {
- angleDiff += 2 * Math.PI;
- }
- if (angleDiff < 0) {
- [startAngle, endAngle] = [endAngle, startAngle];
- angleDiff = -angleDiff;
- }
- const numPoints = Math.max(10, Math.ceil(Math.abs(angleDiff) / (Math.PI / 18)));
- const points: CoreTypes.Point2[] = [];
- for (let i = 0; i <= numPoints; i++) {
- const t = i / numPoints;
- const angle = startAngle + angleDiff * t;
- const x = center[0] + radius * Math.cos(angle);
- const y = center[1] + radius * Math.sin(angle);
- points.push([x, y]);
- }
- return points;
- }
- /**
- * 取消操作
- */
- 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 '';
- }
- /**
- * 激活绘制模式
- */
- _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
- );
- }
- /**
- * 鼠标按下回调 - 用于修改模式
- */
- _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 HipNHAAngleMeasurementAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.centerM1) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
- centerM1Canvas[0] - 60,
- centerM1Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- const textBoxOffset: CoreTypes.Point2 = [
- canvasCoords[0] - textBoxLeft[0],
- canvasCoords[1] - textBoxLeft[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- hasMoved: false,
- textBoxBeingMoved: 'Left',
- textBoxOffset: textBoxOffset,
- };
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- if (cachedStats?.centerM2) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
- centerM2Canvas[0] + 20,
- centerM2Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- const textBoxOffset: CoreTypes.Point2 = [
- canvasCoords[0] - textBoxRight[0],
- canvasCoords[1] - textBoxRight[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- hasMoved: false,
- textBoxBeingMoved: 'Right',
- textBoxOffset: textBoxOffset,
- };
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- }
- // 如果没有点击文本框,再查找最近的手柄
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- const handle = this.getHandleNearImagePoint(
- element,
- customAnn,
- canvasCoords,
- 6
- );
- if (handle) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- // 选中工具
- this._setAnnotationSelected(customAnn, true);
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- handleIndex: customAnn.data.handles.activeHandleIndex || 0,
- hasMoved: false,
- };
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- // 如果没有找到手柄,检查是否点击在线段上(开始移动整个工具)
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- // 选中工具
- this._setAnnotationSelected(customAnn, true);
- // 计算鼠标点击位置相对于工具的偏移量
- const points = customAnn.data.handles.points;
- if (points.length >= 8) {
- const midPoint = vectorAdd(points[0], vectorScale(vectorSubtract(points[1], points[0]), 0.5));
- const midCanvas = viewport.worldToCanvas(midPoint);
- const wholeToolOffset: CoreTypes.Point2 = [
- canvasCoords[0] - midCanvas[0],
- canvasCoords[1] - midCanvas[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- hasMoved: false,
- movingWholeTool: true,
- wholeToolOffset: wholeToolOffset,
- };
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- }
- };
- /**
- * 鼠标拖拽回调 - 用于修改模式
- */
- _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, textBoxBeingMoved, movingWholeTool } = this.editData;
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- const { data } = customAnn;
- // 如果正在拖拽文本框
- if (textBoxBeingMoved && this.editData.textBoxOffset) {
- const newTextBoxPosition: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.textBoxOffset[0],
- canvasCoords[1] - this.editData.textBoxOffset[1],
- ];
- if (textBoxBeingMoved === 'Left') {
- data.handles.textBoxLeft = newTextBoxPosition;
- } else if (textBoxBeingMoved === 'Right') {
- data.handles.textBoxRight = newTextBoxPosition;
- }
- this.editData.hasMoved = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- // 如果正在移动整个工具
- if (movingWholeTool && this.editData.wholeToolOffset) {
- // 计算新的中点位置
- const newMidCanvas: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.wholeToolOffset[0],
- canvasCoords[1] - this.editData.wholeToolOffset[1],
- ];
- const newMidWorld = enabledElement.viewport.canvasToWorld(newMidCanvas);
- // 计算所有点的偏移量
- const points = data.handles.points;
- if (points.length >= 8) {
- const oldMid = vectorAdd(points[0], vectorScale(vectorSubtract(points[1], points[0]), 0.5));
- const offset = vectorSubtract(newMidWorld, oldMid);
- // 移动所有点
- for (let i = 0; i < points.length; i++) {
- points[i] = vectorAdd(points[i], offset);
- }
- // 移动文本框位置(使用相同的偏移量)
- if (data.handles.textBoxLeft) {
- data.handles.textBoxLeft = [
- data.handles.textBoxLeft[0] + offset[0],
- data.handles.textBoxLeft[1] + offset[1]
- ];
- }
- if (data.handles.textBoxRight) {
- data.handles.textBoxRight = [
- data.handles.textBoxRight[0] + offset[0],
- data.handles.textBoxRight[1] + offset[1]
- ];
- }
- // 重新计算角度
- 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 eventDetail = evt.detail;
- const { element, currentPoints } = eventDetail;
- const canvasCoords = currentPoints.canvas;
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- return;
- }
- const { viewport } = enabledElement;
- const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- customAnn.data.handles.activeHandleIndex = null;
- // 如果没有拖拽且点击在线段上(movingWholeTool为true),则进入选中状态
- if (!hasMoved && movingWholeTool) {
- // 设置选中状态:使用注解的selected字段
- this._setAnnotationSelected(customAnn, true);
- // 保持高亮以显示选中
- customAnn.highlighted = true;
- } else {
- customAnn.highlighted = false;
- }
- // 如果是简单点击(没有移动),检查是否点击到其他位置以取消选中
- if (!hasMoved) {
- const annotations = annotation.state.getAnnotations(this.getToolName(), element);
- // 检查是否点击到任何工具部分
- let clickedOnTool = false;
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- // 检查点击位置是否在工具范围内
- if (this.isPointNearTool(element, customAnn, canvasCoords, 25)) {
- clickedOnTool = true;
- break;
- }
- // 检查是否在文本框范围内
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.centerM1) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
- centerM1Canvas[0] - 60,
- centerM1Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
- clickedOnTool = true;
- break;
- }
- }
- if (cachedStats?.centerM2) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
- centerM2Canvas[0] + 20,
- centerM2Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
- clickedOnTool = true;
- break;
- }
- }
- }
- // 如果没有点击到任何工具,清除所有选中状态
- if (!clickedOnTool) {
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- this._setAnnotationSelected(customAnn, false);
- customAnn.highlighted = false;
- }
- }
- }
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- this.editData = null;
- 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 HipNHAAngleMeasurementAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.centerM1) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const textBoxLeft: CoreTypes.Point2 = customAnn.data.handles.textBoxLeft || [
- centerM1Canvas[0] - 60,
- centerM1Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxLeft)) {
- element.style.cursor = 'pointer'; // 手型光标
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- }
- if (cachedStats?.centerM2) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const textBoxRight: CoreTypes.Point2 = customAnn.data.handles.textBoxRight || [
- centerM2Canvas[0] + 20,
- centerM2Canvas[1] - 50,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxRight)) {
- element.style.cursor = 'pointer'; // 手型光标
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- }
- }
- // 如果没有悬停在文本框上,检查是否悬停在手柄或线段上
- if (!isHovering) {
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- // 检查是否靠近手柄
- const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
- if (handle) {
- element.style.cursor = 'crosshair'; // 十字准星光标
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- // 检查是否靠近线段(圆弧路径)
- if (this.isPointNearTool(element, customAnn, canvasCoords, 10)) {
- element.style.cursor = 'crosshair'; // 十字准星光标
- customAnn.highlighted = true;
- isHovering = true;
- break;
- }
- }
- }
- // 如果没有悬停在任何地方,重置高亮(但保留选中状态)
- if (!isHovering) {
- for (const ann of annotations) {
- const customAnn = ann as HipNHAAngleMeasurementAnnotation;
- // 只有未选中的工具才重置 highlighted
- // 已选中的工具保持 highlighted = true
- if (!customAnn.isSelected) {
- customAnn.highlighted = false;
- }
- }
- element.style.cursor = 'default';
- }
- // 触发渲染以更新高亮状态
- const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- };
- /**
- * 键盘按下回调 - 处理Delete键删除选中注解
- */
- _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 HipNHAAngleMeasurementAnnotation).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();
- }
- };
- /**
- * 处理选中回调
- */
- handleSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: HipNHAAngleMeasurementAnnotation
- ): void {
- const eventDetail = evt.detail;
- const { element } = eventDetail;
- annotation.highlighted = true;
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- return;
- }
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- }
- /**
- * 工具选中回调
- */
- toolSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: HipNHAAngleMeasurementAnnotation
- ): void {
- // 实现工具选中逻辑
- }
- /**
- * 处理鼠标拖拽
- */
- _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 HipNHAAngleMeasurementAnnotation;
- 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 HipNHAAngleMeasurementAnnotation,
- 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, hasMoved } = this.editData;
- const { data } = annotation as HipNHAAngleMeasurementAnnotation;
- if (hasMoved) {
- this.editData.hasMoved = false;
- }
- data.handles.activeHandleIndex = null;
- };
- /**
- * 获取靠近图像点的手柄
- */
- getHandleNearImagePoint(
- element: HTMLDivElement,
- annotation: HipNHAAngleMeasurementAnnotation,
- 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;
- }
- /**
- * 更新缓存的统计数据
- */
- _updateCachedStats(
- annotation: HipNHAAngleMeasurementAnnotation,
- 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] = {};
- }
- const stats = data.cachedStats[targetId];
- if (points.length >= 8) {
- // 拟合左侧圆(点0, 1, 2)
- const leftCircleResult = fitCircleFrom3Points(points[0], points[1], points[2]);
- if (leftCircleResult) {
- stats.centerM1 = leftCircleResult.center;
- stats.radiusLeft = leftCircleResult.radius;
- }
- // 拟合右侧圆(点4, 5, 6)
- const rightCircleResult = fitCircleFrom3Points(points[4], points[5], points[6]);
- if (rightCircleResult) {
- stats.centerM2 = rightCircleResult.center;
- stats.radiusRight = rightCircleResult.radius;
- }
- // 如果两个圆都拟合成功,计算角度
- if (stats.centerM1 && stats.centerM2) {
- // 基准线向量 M1 -> M2
- stats.baselineVector = vectorSubtract(stats.centerM2, stats.centerM1);
- // 左侧切线向量 M1 -> point3
- stats.leftTangentVector = vectorSubtract(points[3], stats.centerM1);
- // 右侧切线向量 M2 -> point7
- stats.rightTangentVector = vectorSubtract(points[7], stats.centerM2);
- // 计算左侧NHA角
- const leftAngleRad = getAngleBetweenLines(
- stats.baselineVector,
- stats.leftTangentVector
- );
- stats.angleLeft = Math.round(degrees(leftAngleRad));
- // 计算右侧NHA角
- const rightAngleRad = getAngleBetweenLines(
- stats.baselineVector,
- calculateOppositeVector(stats.rightTangentVector)
- );
- stats.angleRight = Math.round(degrees(rightAngleRad));
- }
- }
- }
- /**
- * 渲染注解
- */
- 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 ann = annotations[i] as HipNHAAngleMeasurementAnnotation;
- const { annotationUID, data } = ann;
- const points = data.handles.points;
- if (points.length < 1) {
- continue;
- }
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
- // 转换所有点为 canvas 坐标
- const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
- // 确定颜色(选中时蓝色,未选中时白色)
- const mainColor = ann.highlighted ? 'rgb(0, 120, 255)' : 'rgb(255, 255, 255)';
- // 根据选中或悬停状态调整手柄半径:选中或悬停时为12,否则为8
- const isHighlighted = ann.isSelected || ann.highlighted;
- const handleRadius = isHighlighted ? 12 : 8;
- // 1. 绘制8个手柄点
- if (annotationUID) {
- const handleGroupUID = '0';
- drawHandles(
- svgDrawingHelper,
- annotationUID,
- handleGroupUID,
- canvasPoints,
- {
- color: 'rgb(255, 255, 255)',
- handleRadius: handleRadius,
- }
- );
- }
- // 如果缓存统计数据存在,绘制圆和线
- if (cachedStats && annotationUID) {
- // 2. 绘制左侧股骨头圆
- if (cachedStats.centerM1 && cachedStats.radiusLeft) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- // 计算像素半径
- const pointOnCircle: CoreTypes.Point3 = [
- cachedStats.centerM1[0] + cachedStats.radiusLeft,
- cachedStats.centerM1[1],
- cachedStats.centerM1[2]
- ];
- const pointOnCircleCanvas = viewport.worldToCanvas(pointOnCircle);
- const radiusPixels = Math.abs(pointOnCircleCanvas[0] - centerM1Canvas[0]);
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- 'leftCircle',
- centerM1Canvas,
- radiusPixels,
- {
- color: mainColor,
- width: 2,
- }
- );
- }
- // 3. 绘制右侧股骨头圆
- if (cachedStats.centerM2 && cachedStats.radiusRight) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- // 计算像素半径
- const pointOnCircle: CoreTypes.Point3 = [
- cachedStats.centerM2[0] + cachedStats.radiusRight,
- cachedStats.centerM2[1],
- cachedStats.centerM2[2]
- ];
- const pointOnCircleCanvas = viewport.worldToCanvas(pointOnCircle);
- const radiusPixels = Math.abs(pointOnCircleCanvas[0] - centerM2Canvas[0]);
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- 'rightCircle',
- centerM2Canvas,
- radiusPixels,
- {
- color: mainColor,
- width: 2,
- }
- );
- }
- // 4. 绘制基准线 M1-M2
- if (cachedStats.centerM1 && cachedStats.centerM2) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- 'baseline',
- centerM1Canvas,
- centerM2Canvas,
- {
- color: mainColor,
- width: 2,
- }
- );
- }
- // 5. 绘制左侧切线 M1-point3
- if (cachedStats.centerM1 && points.length > 3) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- const point3Canvas = viewport.worldToCanvas(points[3]);
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- 'leftTangent',
- centerM1Canvas,
- point3Canvas,
- {
- color: mainColor,
- width: 2,
- }
- );
- }
- // 6. 绘制右侧切线 M2-point7
- if (cachedStats.centerM2 && points.length > 7) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- const point7Canvas = viewport.worldToCanvas(points[7]);
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- 'rightTangent',
- centerM2Canvas,
- point7Canvas,
- {
- color: mainColor,
- width: 2,
- }
- );
- }
- // 7. 绘制左侧角度弧线和文本
- if (
- cachedStats.centerM1 &&
- cachedStats.baselineVector &&
- cachedStats.leftTangentVector &&
- cachedStats.angleLeft !== undefined
- ) {
- const centerM1Canvas = viewport.worldToCanvas(cachedStats.centerM1);
- // 绘制角度弧线
- const leftArcPoints = this._createAngleArcPoints(
- centerM1Canvas,
- [cachedStats.baselineVector[0], cachedStats.baselineVector[1]],
- [cachedStats.leftTangentVector[0], cachedStats.leftTangentVector[1]],
- 40
- );
- drawPathSvg(
- svgDrawingHelper,
- annotationUID,
- 'leftArc',
- leftArcPoints,
- {
- color: mainColor,
- width: 1,
- lineDash: '4,4',
- }
- );
- // 绘制左侧角度文本框
- const textBoxLeft: CoreTypes.Point2 = data.handles.textBoxLeft || [
- centerM1Canvas[0] - 60,
- centerM1Canvas[1] - 50,
- ];
- drawLinkedTextBox(
- svgDrawingHelper,
- annotationUID,
- 'leftAngleText',
- [`左侧NHA: ${cachedStats.angleLeft}°`],
- textBoxLeft,
- [centerM1Canvas],
- {},
- {
- color: mainColor,
- }
- );
- }
- // 8. 绘制右侧角度弧线和文本
- if (
- cachedStats.centerM2 &&
- cachedStats.baselineVector &&
- cachedStats.rightTangentVector &&
- cachedStats.angleRight !== undefined
- ) {
- const centerM2Canvas = viewport.worldToCanvas(cachedStats.centerM2);
- // 绘制角度弧线
- const rightArcPoints = this._createAngleArcPoints(
- centerM2Canvas,
- [-cachedStats.baselineVector[0], -cachedStats.baselineVector[1]],
- [cachedStats.rightTangentVector[0], cachedStats.rightTangentVector[1]],
- 40
- );
- drawPathSvg(
- svgDrawingHelper,
- annotationUID,
- 'rightArc',
- rightArcPoints,
- {
- color: mainColor,
- width: 1,
- lineDash: '4,4',
- }
- );
- // 绘制右侧角度文本框
- const textBoxRight: CoreTypes.Point2 = data.handles.textBoxRight || [
- centerM2Canvas[0] + 20,
- centerM2Canvas[1] - 50,
- ];
- drawLinkedTextBox(
- svgDrawingHelper,
- annotationUID,
- 'rightAngleText',
- [`右侧NHA: ${cachedStats.angleRight}°`],
- textBoxRight,
- [centerM2Canvas],
- {},
- {
- color: mainColor,
- }
- );
- }
- }
- renderStatus = true;
- }
- return renderStatus;
- };
- }
|