| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720 |
- 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,
- drawLinkedTextBox,
- drawLine: drawLineSvg,
- drawCircle: drawCircleSvg,
- } = drawing;
- import {
- vectorSubtract,
- vectorAdd,
- vectorScale,
- calculateDistance,
- fitCircleFrom3Points,
- getLineIntersection,
- getLineSegmentIntersection,
- fromPoint2ToPoint3,
- } from './mathUtils';
- import {
- PublicToolProps,
- ToolProps,
- EventTypes,
- SVGDrawingHelper,
- } from '@cornerstonejs/tools/dist/esm/types';
- // 注解初始配置接口
- interface AnnotationInitialConfig {
- // 圆的3个点位置(相对于中心的偏移)
- point1Offset?: [number, number];
- point2Offset?: [number, number];
- point3Offset?: [number, number];
-
- // 直线的2个点位置(相对于中心的偏移)
- line45Point1Offset?: [number, number];
- line45Point2Offset?: [number, number];
- }
- // 注解数据接口
- interface HipDorsalCoverageAnnotation extends Types.Annotation {
- data: {
- handles: {
- points: CoreTypes.Point3[]; // 5个点的世界坐标
- activeHandleIndex: number | null;
- };
- textBox?: CoreTypes.Point2; // 文本框位置(canvas坐标)
- cachedStats?: {
- [targetId: string]: {
- // 拟合圆信息
- circleCenter?: CoreTypes.Point3; // 圆心M
- circleRadius?: number; // 半径R
-
- // 关键点
- pointA?: CoreTypes.Point3; // 交点A
- pointB?: CoreTypes.Point3; // 交点B
- pointC?: CoreTypes.Point3; // 交点C
-
- // 距离
- distanceAC?: number;
- distanceBC?: number;
- distanceAB?: number; // A和B之间的距离(圆的直径)
- // 覆盖率
- coverageRatio?: number; // 0-1之间
- coveragePercent?: string; // 百分比字符串
- // 辅助向量(用于渲染)
- perpVector?: CoreTypes.Point3; // 垂线方向向量
- };
- };
- };
- }
- // 导出的注解数据接口
- export interface ExportedDorsalCoverageData {
- // 输入点
- points: CoreTypes.Point3[]; // 5个点的世界坐标
-
- // 拟合圆
- circleCenter: CoreTypes.Point3; // 圆心M
- circleRadius: number; // 半径R
-
- // 关键点
- pointA: CoreTypes.Point3; // 交点A
- pointB: CoreTypes.Point3; // 交点B
- pointC: CoreTypes.Point3; // 交点C
-
- // 测量结果
- distanceAC: number; // AC距离
- distanceBC: number; // BC距离
- coverageRatio: number; // 覆盖率(0-1)
- coveragePercent: string; // 覆盖率百分比
-
- // 文本框位置
- textBoxPosition?: CoreTypes.Point2;
-
- // 元数据
- metadata: {
- viewPlaneNormal: CoreTypes.Point3;
- viewUp: CoreTypes.Point3;
- FrameOfReferenceUID: string;
- referencedImageId: string;
- };
- }
- export default class HipDorsalCoverageTool extends AnnotationTool {
- static toolName = 'HipDorsalCoverageTool';
- editData: {
- annotation: Types.Annotation;
- viewportIdsToRender: string[];
- handleIndex?: number;
- newAnnotation?: boolean;
- hasMoved?: boolean;
- textBoxBeingMoved?: boolean;
- textBoxOffset?: CoreTypes.Point2;
- movingWholeTool?: boolean; // 是否正在移动整个工具
- wholeToolOffset?: CoreTypes.Point2; // 移动整个工具时的偏移量
- } | null = null;
- isDrawing: boolean = false;
- /**
- * 创建一个预设的注解,带有5个默认点
- * 确保前3个点严格在圆周上,以保证圆拟合精度
- */
- static createDefaultAnnotation(
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
- config?: AnnotationInitialConfig
- ): HipDorsalCoverageAnnotation {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- throw new Error('Element is not enabled');
- }
- const canvas = viewport.canvas;
- const { width, height } = canvas;
- const centerX = width / 2;
- const centerY = height / 2;
- // 定义圆心和半径 - 使用固定的圆形参数确保点在圆周上
- const circleCenter: [number, number] = [centerX, centerY];
- const circleRadius = 80; // 固定的圆半径
- // 创建7个在圆周上的等分点(为了避免三点共线,选择非共线性角度)
- // 使用0°, 90°, 和270°的角度,确保这三个点确定一个圆
- const angles = [
- Math.PI, // 180° (最好避免0°因为可能共线)
- Math.PI / 2, // 90°
- Math.PI * 1.5 // 270°
- ];
- // 科学选择角度:0°, 120°, 240°是最好的,因为它们保证三点确定唯一圆
- const scientific_angles = [0, 2 * Math.PI / 3, 4 * Math.PI / 3]; // 0°, 120°, 240°
- const circlePoint1: CoreTypes.Point2 = [
- circleCenter[0] + circleRadius * Math.cos(scientific_angles[0]),
- circleCenter[1] + circleRadius * Math.sin(scientific_angles[0])
- ];
- const circlePoint2: CoreTypes.Point2 = [
- circleCenter[0] + circleRadius * Math.cos(scientific_angles[1]),
- circleCenter[1] + circleRadius * Math.sin(scientific_angles[1])
- ];
- const circlePoint3: CoreTypes.Point2 = [
- circleCenter[0] + circleRadius * Math.cos(scientific_angles[2]),
- circleCenter[1] + circleRadius * Math.sin(scientific_angles[2])
- ];
- // 直线的两个点(与圆无关) - 放置在圆的另一侧
- const line45Point1: CoreTypes.Point2 = [
- centerX + circleRadius * 1.2, // 圆右侧
- centerY - circleRadius * 0.6 // 偏上
- ];
- const line45Point2: CoreTypes.Point2 = [
- centerX - circleRadius * 0.8, // 圆左侧
- centerY + circleRadius * 0.6 // 偏下
- ];
- // 构建5个canvas坐标点:3个圆周点 + 2个直线上的点
- const canvasPoints: CoreTypes.Point2[] = [
- circlePoint1, // 点1: 圆周上的点 (0度)
- circlePoint2, // 点2: 圆周上的点 (120度)
- circlePoint3, // 点3: 圆周上的点 (240度)
- line45Point1, // 点4: 直线上的点
- line45Point2, // 点5: 直线上的点
- ];
- // 转换为world坐标
- const worldPoints = canvasPoints.map((canvasPoint) =>
- viewport.canvasToWorld(canvasPoint)
- );
- 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: HipDorsalCoverageTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: worldPoints,
- activeHandleIndex: null,
- },
- cachedStats: {},
- },
- } as HipDorsalCoverageAnnotation;
- return annotationData;
- }
- /**
- * 导出注解数据
- */
- static exportAnnotationData(
- annotation: HipDorsalCoverageAnnotation,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): ExportedDorsalCoverageData | null {
- const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
- const cachedStats = annotation.data.cachedStats?.[targetId];
- if (!cachedStats || !cachedStats.circleCenter || !cachedStats.pointA ||
- !cachedStats.pointB || !cachedStats.pointC) {
- return null;
- }
- return {
- points: [...annotation.data.handles.points],
- circleCenter: cachedStats.circleCenter,
- circleRadius: cachedStats.circleRadius || 0,
- pointA: cachedStats.pointA,
- pointB: cachedStats.pointB,
- pointC: cachedStats.pointC,
- distanceAC: cachedStats.distanceAC || 0,
- distanceBC: cachedStats.distanceBC || 0,
- coverageRatio: cachedStats.coverageRatio || 0,
- coveragePercent: cachedStats.coveragePercent || '0.0%',
- textBoxPosition: annotation.data.textBox,
- 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: ExportedDorsalCoverageData,
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): HipDorsalCoverageAnnotation {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- throw new Error('Element is not enabled');
- }
- const annotationData = {
- invalidated: true,
- highlighted: false,
- metadata: {
- ...exportedData.metadata,
- toolName: HipDorsalCoverageTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: [...exportedData.points],
- activeHandleIndex: null,
- },
- textBox: exportedData.textBoxPosition,
- cachedStats: {},
- },
- } as HipDorsalCoverageAnnotation;
- return annotationData;
- }
- constructor(
- toolProps: PublicToolProps = {},
- defaultToolProps: ToolProps = {
- supportedInteractionTypes: ['Mouse', 'Touch'],
- configuration: {
- shadow: true,
- preventHandleOutsideImage: false,
- },
- }
- ) {
- super(toolProps, defaultToolProps);
- }
- addNewAnnotation(
- evt: EventTypes.InteractionEventType
- ): HipDorsalCoverageAnnotation {
- evt.preventDefault();
- return {} as HipDorsalCoverageAnnotation;
- }
- isPointNearTool(
- element: HTMLDivElement,
- annotation: HipDorsalCoverageAnnotation,
- 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;
- }
- }
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
- // 检查是否靠近圆周
- if (this._isPointNearCircle(canvasCoords, annotation, enabledElement)) {
- return true;
- }
- // 检查是否靠近线段 MC
- if (cachedStats?.circleCenter && cachedStats?.pointC) {
- const centerCanvas = viewport.worldToCanvas(cachedStats.circleCenter);
- const pointCCanvas = viewport.worldToCanvas(cachedStats.pointC);
- const distance = this._distanceToSegment(canvasCoords, centerCanvas, pointCCanvas);
- if (distance < proximity) {
- return true;
- }
- }
- // 检查是否靠近直线45
- if (this._isPointNearLine45(canvasCoords, annotation, viewport, proximity)) {
- return true;
- }
- return false;
- }
- private _isPointInTextBox(
- canvasPoint: CoreTypes.Point2,
- textBoxPosition: CoreTypes.Point2,
- textWidth: number = 150,
- textHeight: number = 60
- ): 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 _distanceToSegment(
- point: CoreTypes.Point2,
- lineStart: CoreTypes.Point2,
- lineEnd: CoreTypes.Point2
- ): number {
- const x = point[0];
- const y = point[1];
- const x1 = lineStart[0];
- const y1 = lineStart[1];
- const x2 = lineEnd[0];
- const y2 = lineEnd[1];
- const A = x - x1;
- const B = y - y1;
- const C = x2 - x1;
- const D = y2 - y1;
- const dot = A * C + B * D;
- const lenSq = C * C + D * D;
- let param = -1;
- if (lenSq !== 0) {
- param = dot / lenSq;
- }
- let xx, yy;
- if (param < 0) {
- xx = x1;
- yy = y1;
- } else if (param > 1) {
- xx = x2;
- yy = y2;
- } else {
- xx = x1 + param * C;
- yy = y1 + param * D;
- }
- const dx = x - xx;
- const dy = y - yy;
- return Math.sqrt(dx * dx + dy * dy);
- }
- /**
- * 检查点是否靠近圆周
- */
- private _isPointNearCircle(
- canvasPoint: CoreTypes.Point2,
- annotation: HipDorsalCoverageAnnotation,
- enabledElement: CoreTypes.IEnabledElement
- ): boolean {
- const { viewport } = enabledElement;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? annotation.data.cachedStats?.[targetId] : undefined;
- if (!cachedStats?.circleCenter || !cachedStats?.circleRadius) {
- return false;
- }
- // 获取圆的像素半径
- const circleRadiusPixels = this._getCircleRadiusInPixels(
- {
- center: cachedStats.circleCenter,
- radius: cachedStats.circleRadius,
- },
- viewport
- );
- // 计算鼠标点到圆心的像素距离
- const circleCenterCanvas = viewport.worldToCanvas(cachedStats.circleCenter);
- const distanceToCenter = Math.sqrt(
- Math.pow(canvasPoint[0] - circleCenterCanvas[0], 2) +
- Math.pow(canvasPoint[1] - circleCenterCanvas[1], 2)
- );
- // 检查是否在圆周范围内(容差为8像素)
- const circleTolerance = 8;
- return Math.abs(distanceToCenter - circleRadiusPixels) <= circleTolerance;
- }
- /**
- * 检查点是否靠近直线45
- */
- private _isPointNearLine45(
- canvasPoint: CoreTypes.Point2,
- annotation: HipDorsalCoverageAnnotation,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
- proximity: number
- ): boolean {
- const points = annotation.data.handles.points;
- if (points.length < 5) {
- return false;
- }
- const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
- const distance = this._distanceToSegment(canvasPoint, canvasPoints[3], canvasPoints[4]);
- return distance < proximity;
- }
- /**
- * 将世界坐标圆半径转换为像素半径
- */
- private _getCircleRadiusInPixels(
- circle: { center: CoreTypes.Point3; radius: number },
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): number {
- const { center, radius } = circle;
- // 沿X轴方向获取距离圆心radius的点
- const radiusVector: CoreTypes.Point3 = [radius, 0, 0];
- const endPoint: CoreTypes.Point3 = vectorAdd(center, radiusVector);
- // 分别转换圆心和圆周上的点到canvas坐标
- const centerCanvas = viewport.worldToCanvas(center);
- const endCanvas = viewport.worldToCanvas(endPoint);
- // 计算两个canvas点之间的距离就是像素半径
- return Math.sqrt(
- Math.pow(endCanvas[0] - centerCanvas[0], 2) +
- Math.pow(endCanvas[1] - centerCanvas[1], 2)
- );
- }
- /**
- * 计算两条无限直线的交点(不限制方向)
- */
- private _calculateLineIntersection(
- line1Start: CoreTypes.Point3,
- line1Dir: CoreTypes.Point3,
- line2Start: CoreTypes.Point3,
- line2Dir: CoreTypes.Point3
- ): CoreTypes.Point3 | null {
- // 使用参数方程求解交点
- // line1: P1 + t * D1
- // line2: P2 + s * D2
- const dx = line2Start[0] - line1Start[0];
- const dy = line2Start[1] - line1Start[1];
- const det = line1Dir[0] * line2Dir[1] - line1Dir[1] * line2Dir[0];
- if (Math.abs(det) < 1e-10) {
- // 平行线,无交点
- return null;
- }
- const t = (dx * line2Dir[1] - dy * line2Dir[0]) / det;
- // 返回交点坐标,不检查方向限制
- return [
- line1Start[0] + t * line1Dir[0],
- line1Start[1] + t * line1Dir[1],
- 0
- ];
- }
- /**
- * 计算两条线段的交点(检查交点是否在线段范围内)
- */
- private _calculateLineSegmentIntersection(
- p1: CoreTypes.Point3,
- p2: CoreTypes.Point3,
- p3: CoreTypes.Point3,
- p4: CoreTypes.Point3
- ): CoreTypes.Point3 | null {
- // 提取2D坐标
- const x1 = p1[0], y1 = p1[1];
- const x2 = p2[0], y2 = p2[1];
- const x3 = p3[0], y3 = p3[1];
- const x4 = p4[0], y4 = p4[1];
- // 计算方向向量
- const dx1 = x2 - x1;
- const dy1 = y2 - y1;
- const dx2 = x4 - x3;
- const dy2 = y4 - y3;
- // 计算行列式
- const denominator = dx1 * dy2 - dy1 * dx2;
- // 检查是否平行
- if (Math.abs(denominator) < 1e-10) {
- return null; // 平行,无交点
- }
- // 计算参数t和s
- const t = ((x3 - x1) * dy2 - (y3 - y1) * dx2) / denominator;
- const s = ((x3 - x1) * dy1 - (y3 - y1) * dx1) / denominator;
- // 检查交点是否在线段范围内
- if (t >= 0 && t <= 1 && s >= 0 && s <= 1) {
- // 计算交点坐标
- const intersectionX = x1 + t * dx1;
- const intersectionY = y1 + t * dy1;
- return [intersectionX, intersectionY, 0];
- }
- return null; // 交点不在线段范围内
- }
- 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 HipDorsalCoverageAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.circleCenter && customAnn.data.textBox) {
- if (this._isPointInTextBox(canvasCoords, customAnn.data.textBox)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- const textBoxOffset: CoreTypes.Point2 = [
- canvasCoords[0] - customAnn.data.textBox[0],
- canvasCoords[1] - customAnn.data.textBox[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- hasMoved: false,
- textBoxBeingMoved: true,
- textBoxOffset: textBoxOffset,
- };
- customAnn.isSelected = true;
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- }
- // 次优先检测手柄点击
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- 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 HipDorsalCoverageAnnotation;
- if (this._isPointNearCircle(canvasCoords, customAnn, enabledElement) ||
- this._isPointNearLine45(canvasCoords, customAnn, viewport, 10)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- // 计算整体移动时的偏移量,使用第一个点作为基准
- const firstPoint = customAnn.data.handles.points[0];
- const firstPointCanvas = viewport.worldToCanvas(firstPoint);
- const wholeToolOffset: CoreTypes.Point2 = [
- canvasCoords[0] - firstPointCanvas[0],
- canvasCoords[1] - firstPointCanvas[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 HipDorsalCoverageAnnotation;
- 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 { viewport } = enabledElement;
- const { annotation: ann, viewportIdsToRender, textBoxBeingMoved, movingWholeTool } = this.editData;
- const customAnn = ann as HipDorsalCoverageAnnotation;
- const { data } = customAnn;
- // 处理文本框拖拽
- if (textBoxBeingMoved && this.editData.textBoxOffset) {
- const newTextBoxPosition: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.textBoxOffset[0],
- canvasCoords[1] - this.editData.textBoxOffset[1],
- ];
- data.textBox = newTextBoxPosition;
- this.editData.hasMoved = true;
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- // 处理整体移动工具
- if (movingWholeTool && this.editData.wholeToolOffset && data.handles.points) {
- // 计算新的中点位置(基于圆心或者几何中心)
- const newCenterCanvas: CoreTypes.Point2 = [
- canvasCoords[0] - this.editData.wholeToolOffset[0],
- canvasCoords[1] - this.editData.wholeToolOffset[1],
- ];
- const newCenterWorld = viewport.canvasToWorld(newCenterCanvas);
- // 计算移动偏移量
- // 使用第一个点作为基准点来计算偏移
- const oldCenter = data.handles.points[0];
- const offset = vectorSubtract(newCenterWorld, oldCenter);
- // 应用偏移量移动所有控制点
- for (let i = 0; i < data.handles.points.length; i++) {
- data.handles.points[i] = vectorAdd(data.handles.points[i], offset);
- }
- // 更新文本框位置(使用相同的偏移量)
- if (data.textBox) {
- data.textBox[0] += offset[0];
- data.textBox[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();
- }
- };
- /**
- * 鼠标移动回调 - 处理悬停高亮和光标反馈
- */
- _mouseMoveModifyCallback = (evt: EventTypes.InteractionEventType): void => {
- const eventDetail = evt.detail;
- const { element, currentPoints } = eventDetail;
- const canvasCoords = currentPoints?.canvas;
- if (!canvasCoords) {
- // 鼠标位置无效,重置所有高亮状态
- element.style.cursor = 'default';
- this._clearAllHighlights(element);
- return;
- }
- 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;
- }
- // 清除之前的高亮状态,为新的检测做准备
- this._clearAllHighlights(element);
- // 按优先级顺序检测鼠标悬停区域
- let hoverResult = this._detectHoverTarget(annotations, canvasCoords, viewport, enabledElement, element);
- // 应用检测到的悬停状态
- this._applyHoverEffect(hoverResult, element, annotations);
- // 触发渲染更新
- const viewportIdsToRender = utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- };
- /**
- * 按优先级检测鼠标悬停的目标
- */
- private _detectHoverTarget(
- annotations: Types.Annotation[],
- canvasCoords: CoreTypes.Point2,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
- enabledElement: CoreTypes.IEnabledElement,
- element: HTMLDivElement
- ): { annotation: HipDorsalCoverageAnnotation | null; cursor: string; area: string } {
- // 默认返回值
- const defaultResult = {
- annotation: null as HipDorsalCoverageAnnotation | null,
- cursor: 'default',
- area: 'none'
- };
- // 优先级1: 检查文本框悬停(最高优先级)
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.circleCenter && customAnn.data.textBox) {
- if (this._isPointInTextBox(canvasCoords, customAnn.data.textBox)) {
- return {
- annotation: customAnn,
- cursor: 'pointer',
- area: 'textbox'
- };
- }
- }
- }
- // 优先级2: 检查手柄点悬停
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- const handle = this.getHandleNearImagePoint(element, customAnn, canvasCoords, 6);
- if (handle) {
- return {
- annotation: customAnn,
- cursor: 'crosshair',
- area: 'handle'
- };
- }
- }
- // 优先级3: 检查圆周悬停
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- if (this._isPointNearCircle(canvasCoords, customAnn, enabledElement)) {
- return {
- annotation: customAnn,
- cursor: 'crosshair',
- area: 'circle'
- };
- }
- }
- // 优先级4: 检查直线45悬停
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.perpVector && this._isPointNearLine45(canvasCoords, customAnn, viewport, 10)) {
- return {
- annotation: customAnn,
- cursor: 'crosshair',
- area: 'line'
- };
- }
- }
- return defaultResult;
- };
- /**
- * 清除所有注解的高亮状态
- */
- private _clearAllHighlights(element: HTMLDivElement): void {
- const annotations = annotation.state.getAnnotations(this.getToolName(), element);
- if (annotations) {
- annotations.forEach(ann => {
- // 只清除临时悬停高亮,不影响持久选中状态
- if (!ann.isSelected) {
- ann.highlighted = false;
- }
- });
- }
- };
- /**
- * 应用悬停效果
- */
- private _applyHoverEffect(
- hoverResult: { annotation: HipDorsalCoverageAnnotation | null; cursor: string; area: string },
- element: HTMLDivElement,
- annotations: Types.Annotation[]
- ): void {
- const { annotation: hoveredAnn, cursor } = hoverResult;
- // 设置光标样式
- element.style.cursor = cursor;
- // 设置高亮状态
- if (hoveredAnn && hoverResult.area !== 'none') {
- // 只对未选中的工具应用悬停高亮
- if (!hoveredAnn.isSelected) {
- hoveredAnn.highlighted = true;
- }
- } else {
- // 确保所有未选中的工具都没有悬停高亮
- annotations.forEach(ann => {
- if (!ann.isSelected) {
- ann.highlighted = false;
- }
- });
- }
- };
- _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
- if (!this.editData) {
- return;
- }
- const { annotation: ann, hasMoved, movingWholeTool } = this.editData;
- const customAnn = ann as HipDorsalCoverageAnnotation;
- customAnn.data.handles.activeHandleIndex = null;
- // 如果没有拖拽且点击在线段/圆周上,则进入选中状态
- if (!hasMoved && movingWholeTool) {
- customAnn.isSelected = true;
- customAnn.highlighted = true;
- } else {
- customAnn.highlighted = false;
- }
- 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();
- };
- handleSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: HipDorsalCoverageAnnotation
- ): void {
- const eventDetail = evt.detail;
- const { element } = eventDetail;
- annotation.highlighted = true;
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- evt.preventDefault();
- }
- toolSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: HipDorsalCoverageAnnotation
- ): void {
- // 实现工具选中逻辑
- }
- /**
- * 键盘按下回调 - 处理键盘删除
- */
- _keyDownCallback = (evt: KeyboardEvent): void => {
- if (evt.key === 'Delete') {
- const targetElement = evt.target as HTMLDivElement;
- const enabledElement = getEnabledElement(targetElement);
- if (!enabledElement) {
- return;
- }
- const annotations = annotation.state.getAnnotations(this.getToolName(), targetElement);
- if (!annotations || annotations.length === 0) {
- return;
- }
- const targetId = this.getTargetId(enabledElement.viewport);
- if (!targetId) {
- return;
- }
- // 查找选中的注解并删除
- for (const ann of annotations) {
- const customAnn = ann as HipDorsalCoverageAnnotation;
- if (customAnn.isSelected) {
- annotation.state.removeAnnotation(ann.annotationUID??'');
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- targetElement,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- break;
- }
- }
- 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 HipDorsalCoverageAnnotation;
- 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 HipDorsalCoverageAnnotation,
- enabledElement
- );
- this.editData!.hasMoved = true;
- }
- utilities.triggerAnnotationRenderForViewportIds(
- viewportIdsToRender
- );
- }
- };
- _endCallback = (evt: EventTypes.InteractionEventType): void => {
- const eventDetail = evt.detail;
- const { element } = eventDetail;
- if (!this.editData) {
- return;
- }
- const { annotation, hasMoved } = this.editData;
- if (hasMoved) {
- this.editData.hasMoved = false;
- }
- };
- getHandleNearImagePoint(
- element: HTMLDivElement,
- annotation: HipDorsalCoverageAnnotation,
- 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: HipDorsalCoverageAnnotation,
- 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 >= 5) {
- const point1 = points[0];
- const point2 = points[1];
- const point3 = points[2];
- const point4 = points[3];
- const point5 = points[4];
- const circleResult = fitCircleFrom3Points(point1, point2, point3);
-
- if (!circleResult) {
- console.warn('圆拟合失败,三点可能共线');
- return;
- }
- const centerM = circleResult.center;
- const radius = circleResult.radius;
- stats.circleCenter = centerM;
- stats.circleRadius = radius;
- // 计算直线45的方向向量
- const line45Direction = vectorSubtract(point5, point4);
- const line45Length = Math.sqrt(
- line45Direction[0] * line45Direction[0] +
- line45Direction[1] * line45Direction[1]
- );
- if (line45Length === 0) {
- console.warn('直线45长度为0');
- return;
- }
- const line45Unit = [
- line45Direction[0] / line45Length,
- line45Direction[1] / line45Length,
- 0
- ];
- // 垂线方向:垂直于直线45
- // 垂线应该朝向股骨头覆盖的方向
- const perpVector: CoreTypes.Point3 = [
- -line45Unit[1], // -dy
- line45Unit[0], // dx
- 0
- ];
- stats.perpVector = perpVector;
- // 计算垂线与圆的交点A和B
- const pointA = vectorAdd(centerM, vectorScale(perpVector, radius));
- const pointB = vectorSubtract(centerM, vectorScale(perpVector, radius));
- stats.pointA = pointA;
- stats.pointB = pointB;
- // 计算直线45与AB线段的交点C
- // AB线段是从pointA到pointB的线段,直线45是从point4到point5
- // 我们需要计算AB线段与直线45(无限直线)的交点
- const pointC = this._calculateLineIntersection(
- pointA,
- vectorSubtract(pointB, pointA), // AB线段的方向向量
- point4,
- line45Direction // 直线45的方向向量
- );
- if (!pointC) {
- console.warn('直线45与AB线段平行或无交点');
- // 如果没有交点,显示NaN%
- stats.coverageRatio = NaN;
- stats.coveragePercent = 'NaN%';
- return;
- }
- // 检查交点C是否在AB线段上
- // 计算参数t,表示C在AB线段上的位置
- const abVector = vectorSubtract(pointB, pointA);
- const acVector = vectorSubtract(pointC, pointA);
- const abLengthSquared = abVector[0] * abVector[0] + abVector[1] * abVector[1];
- if (abLengthSquared === 0) {
- console.warn('AB线段长度为0');
- stats.coverageRatio = NaN;
- stats.coveragePercent = 'NaN%';
- return;
- }
- // 计算t = (C - A) · (B - A) / |B - A|²
- const t = (acVector[0] * abVector[0] + acVector[1] * abVector[1]) / abLengthSquared;
- // 检查t是否在[0, 1]范围内
- if (t < 0 || t > 1) {
- console.warn('交点C不在AB线段范围内, t =', t);
- // 如果交点不在AB线段上,显示NaN%
- stats.coverageRatio = NaN;
- stats.coveragePercent = 'NaN%';
- return;
- }
- stats.pointC = pointC;
- // 将点转换为canvas坐标再计算距离(在图像平面上进行测量)
- const pointACanvas = viewport.worldToCanvas(pointA);
- const pointBCanvas = viewport.worldToCanvas(pointB);
- const pointCCanvas = viewport.worldToCanvas(pointC);
- // 计算距离(在canvas坐标系中,将Point2转换为Point3)
- const distanceAC = calculateDistance(fromPoint2ToPoint3(pointACanvas), fromPoint2ToPoint3(pointCCanvas));
- const distanceBC = calculateDistance(fromPoint2ToPoint3(pointBCanvas), fromPoint2ToPoint3(pointCCanvas));
- const distanceAB = distanceAC + distanceBC; // A和B之间的距离,即圆的直径
- stats.distanceAC = distanceAC;
- stats.distanceBC = distanceBC;
- stats.distanceAB = distanceAB;
- // 新的覆盖率计算方式:选择较小的那一侧进行计算
- // 这是基于股骨头在髋臼中的实际覆盖情况
- let coverageRatio: number;
- if (distanceAC < distanceBC) {
- coverageRatio = distanceAB > 0 ? distanceAC / distanceAB : 0;
- } else {
- coverageRatio = distanceAB > 0 ? distanceBC / distanceAB : 0;
- }
- stats.coverageRatio = coverageRatio;
- stats.coveragePercent = `${(coverageRatio * 100).toFixed(1)}%`;
- }
- }
- 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 HipDorsalCoverageAnnotation;
- const { annotationUID, data } = annotation;
- const points = data.handles.points;
- if (points.length < 5) {
- continue;
- }
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? data.cachedStats?.[targetId] : undefined;
- const canvasPoints = points.map((p) => viewport.worldToCanvas(p));
- const lineColor = annotation.highlighted
- ? 'rgb(0, 120, 255)'
- : 'rgb(255, 255, 255)';
- // 绘制拟合圆
- if (cachedStats?.circleCenter && cachedStats?.circleRadius && annotationUID) {
- const circleCenterCanvas = viewport.worldToCanvas(cachedStats.circleCenter);
- const radiusCanvas = this._getCircleRadiusInPixels({
- center: cachedStats.circleCenter,
- radius: cachedStats.circleRadius
- }, viewport);
- const circleUID = `${annotationUID}-fitted-circle`;
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- circleUID,
- circleCenterCanvas,
- radiusCanvas,
- {
- color: annotation.isSelected ? 'rgb(0, 255, 0)' : 'rgb(0, 255, 255)', // 选中时绿色,未选中时青色
- lineWidth: 2,
- }
- );
- // 绘制圆心M
- const centerMarkUID = `${annotationUID}-center-mark`;
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- centerMarkUID,
- circleCenterCanvas,
- 5,
- {
- color: 'rgb(255, 0, 0)', // 红色
- fill: 'rgb(255, 0, 0)',
- }
- );
- }
- // 绘制直线45
- if (points.length >= 5 && annotationUID) {
- const lineUID = `${annotationUID}-line45`;
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- lineUID,
- canvasPoints[3],
- canvasPoints[4],
- {
- color: 'rgb(255, 255, 0)', // 黄色
- width: 2,
- }
- );
- }
- // 绘制垂线和交点
- if (cachedStats?.circleCenter && cachedStats?.pointA && cachedStats?.pointB && annotationUID) {
- const centerCanvas = viewport.worldToCanvas(cachedStats.circleCenter);
- const pointACanvas = viewport.worldToCanvas(cachedStats.pointA);
- const pointBCanvas = viewport.worldToCanvas(cachedStats.pointB);
- // MA段(蓝色虚线)- A点到圆心
- const lineMAUID = `${annotationUID}-line-MA`;
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- lineMAUID,
- centerCanvas,
- pointACanvas,
- {
- color: 'rgb(0, 0, 255)', // 蓝色
- width: 1,
- lineDash: '4,4',
- }
- );
- // MB段(紫色虚线)- B点到圆心
- const lineMBUID = `${annotationUID}-line-MB`;
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- lineMBUID,
- centerCanvas,
- pointBCanvas,
- {
- color: 'rgb(128, 0, 128)', // 紫色
- width: 1,
- lineDash: '4,4',
- }
- );
- // AB段(橙色虚线)- A点到B点
- const lineABUID = `${annotationUID}-line-AB`;
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- lineABUID,
- pointACanvas,
- pointBCanvas,
- {
- color: 'rgb(255, 165, 0)', // 橙色
- width: 1,
- lineDash: '2,2',
- }
- );
- // 绘制交点标记
- const pointAMarkUID = `${annotationUID}-point-A`;
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- pointAMarkUID,
- pointACanvas,
- 4,
- {
- color: 'rgb(0, 0, 255)', // 蓝色
- fill: 'rgb(0, 0, 255)',
- }
- );
- const pointBMarkUID = `${annotationUID}-point-B`;
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- pointBMarkUID,
- pointBCanvas,
- 4,
- {
- color: 'rgb(128, 0, 128)', // 紫色
- fill: 'rgb(128, 0, 128)',
- }
- );
- // 只有当pointC存在时才绘制MC段和C点
- if (cachedStats?.pointC) {
- const pointCCanvas = viewport.worldToCanvas(cachedStats.pointC);
- // MC段(绿色实线)
- const lineMCUID = `${annotationUID}-line-MC`;
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- lineMCUID,
- centerCanvas,
- pointCCanvas,
- {
- color: 'rgb(0, 255, 0)', // 绿色
- width: 2,
- }
- );
- const pointCMarkUID = `${annotationUID}-point-C`;
- drawCircleSvg(
- svgDrawingHelper,
- annotationUID,
- pointCMarkUID,
- pointCCanvas,
- 4,
- {
- color: 'rgb(0, 255, 0)', // 绿色
- fill: 'rgb(0, 255, 0)',
- }
- );
- }
- }
- // 绘制手柄点
- if (annotationUID) {
- const handleGroupUID = '0';
- // 根据选中或悬停状态调整手柄半径:选中或悬停时为8,否则为6
- const isHighlighted = annotation.isSelected || annotation.highlighted;
- const handleRadius = isHighlighted ? 8 : 6;
- drawHandles(
- svgDrawingHelper,
- annotationUID,
- handleGroupUID,
- canvasPoints,
- {
- color: annotation.isSelected ? 'rgb(0, 255, 0)' : 'rgb(255, 255, 255)', // 选中时绿色,未选中时白色
- handleRadius: handleRadius,
- }
- );
- }
- // 绘制覆盖率文本框
- if (cachedStats?.coveragePercent && cachedStats?.circleCenter && annotationUID) {
- const circleCenterCanvas = viewport.worldToCanvas(cachedStats.circleCenter);
-
- // 确保文本框位置被初始化
- if (!data.textBox) {
- data.textBox = [
- circleCenterCanvas[0] + 10,
- circleCenterCanvas[1] - 10,
- ];
- }
- const textLines = [
- `${cachedStats.coveragePercent}`,
- //`覆盖率: ${cachedStats.coveragePercent}`,
- // `AC: ${cachedStats.distanceAC?.toFixed(2)}`,
- // `BC: ${cachedStats.distanceBC?.toFixed(2)}`,
- // `AB: ${cachedStats.distanceAB?.toFixed(2)}`,
- ];
- const textBoxPosition: CoreTypes.Point2 = data.textBox;
- const textUID = `${annotationUID}-text-box`;
- drawLinkedTextBox(
- svgDrawingHelper,
- annotationUID,
- textUID,
- textLines,
- textBoxPosition,
- [circleCenterCanvas],
- {},
- { color: 'rgb(255, 255, 0)' } // 黄色
- );
- }
- renderStatus = true;
- }
- return renderStatus;
- };
- }
|