| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935 |
- 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,
- drawPath: drawPathSvg,
- } = drawing;
- import {
- calculateMidpoint,
- vectorSubtract,
- vectorAdd,
- vectorScale,
- getAngleBetweenLines,
- degrees,
- } from './mathUtils';
- import {
- PublicToolProps,
- ToolProps,
- EventTypes,
- SVGDrawingHelper,
- } from '@cornerstonejs/tools/dist/esm/types';
- // 注解初始配置接口
- interface AnnotationInitialConfig {
- axisLineLength?: number;
- plateauLineLength?: number;
- axisLineCenterX?: number;
- axisLineCenterY?: number;
- plateauLineCenterX?: number;
- plateauLineCenterY?: number;
- }
- // 注解数据接口
- interface TibialPlateauAngleAnnotation extends Types.Annotation {
- data: {
- handles: {
- points: CoreTypes.Point3[];
- activeHandleIndex: number | null;
- textBoxTPA?: CoreTypes.Point2;
- };
- cachedStats?: {
- [targetId: string]: {
- TPA: number;
- functionalAxisVector: CoreTypes.Point3;
- plateauLineVector: CoreTypes.Point3;
- perpendicularVector: CoreTypes.Point3;
- plateauMidpoint: CoreTypes.Point3;
- intersectionPoint?: CoreTypes.Point3; // 功能轴线与中垂线的交点
- };
- };
- };
- }
- // 导出的注解数据接口
- export interface ExportedAnnotationData {
- points: CoreTypes.Point3[];
- TPA: number;
- textBoxPosition?: CoreTypes.Point2;
- metadata: {
- viewPlaneNormal: CoreTypes.Point3;
- viewUp: CoreTypes.Point3;
- FrameOfReferenceUID: string;
- referencedImageId: string;
- };
- }
- export default class TibialPlateauAngleTool extends AnnotationTool {
- static toolName = 'TibialPlateauAngleTool';
- editData: {
- annotation: Types.Annotation;
- viewportIdsToRender: string[];
- handleIndex?: number;
- newAnnotation?: boolean;
- hasMoved?: boolean;
- textBoxBeingMoved?: boolean;
- textBoxOffset?: CoreTypes.Point2;
- } | null = null;
- isDrawing: boolean = false;
- static createDefaultAnnotation(
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport,
- config?: AnnotationInitialConfig
- ): TibialPlateauAngleAnnotation {
- 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 axisLineLength = config?.axisLineLength ?? 200;
- const plateauLineLength = config?.plateauLineLength ?? 150;
- const axisLineCenterX = config?.axisLineCenterX ?? centerX - 100;
- const axisLineCenterY = config?.axisLineCenterY ?? centerY;
- const plateauLineCenterX = config?.plateauLineCenterX ?? centerX + 100;
- const plateauLineCenterY = config?.plateauLineCenterY ?? centerY;
- // 点1、2: 胫骨内侧平台线 (水平线)
- const plateauHalfLength = plateauLineLength / 2;
- const point1Canvas: CoreTypes.Point2 = [
- plateauLineCenterX - plateauHalfLength,
- plateauLineCenterY
- ];
- const point2Canvas: CoreTypes.Point2 = [
- plateauLineCenterX + plateauHalfLength,
- plateauLineCenterY
- ];
- // 点3、4: 胫骨功能轴线 (垂直线)
- const axisHalfLength = axisLineLength / 2;
- const point3Canvas: CoreTypes.Point2 = [
- axisLineCenterX,
- axisLineCenterY - axisHalfLength
- ];
- const point4Canvas: CoreTypes.Point2 = [
- axisLineCenterX,
- axisLineCenterY + axisHalfLength
- ];
- const canvasPoints: CoreTypes.Point2[] = [
- point1Canvas,
- point2Canvas,
- point3Canvas,
- point4Canvas,
- ];
- 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 undefined');
- }
- return {
- invalidated: true,
- highlighted: false,
- metadata: {
- viewPlaneNormal: [...viewPlaneNormal] as CoreTypes.Point3,
- viewUp: [...viewUp] as CoreTypes.Point3,
- FrameOfReferenceUID: viewport.getFrameOfReferenceUID(),
- referencedImageId: viewport.getCurrentImageId?.() || '',
- toolName: TibialPlateauAngleTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: worldPoints,
- activeHandleIndex: null,
- },
- cachedStats: {},
- },
- } as TibialPlateauAngleAnnotation;
- }
- static exportAnnotationData(
- annotation: TibialPlateauAngleAnnotation,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): ExportedAnnotationData | null {
- const targetId = `imageId:${viewport.getCurrentImageId?.() || ''}`;
- const cachedStats = annotation.data.cachedStats?.[targetId];
- if (!cachedStats || cachedStats.TPA === undefined) {
- return null;
- }
- return {
- points: [...annotation.data.handles.points],
- TPA: cachedStats.TPA,
- textBoxPosition: annotation.data.handles.textBoxTPA,
- 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: ExportedAnnotationData,
- element: HTMLDivElement,
- viewport: CoreTypes.IStackViewport | CoreTypes.IVolumeViewport
- ): TibialPlateauAngleAnnotation {
- const enabledElement = getEnabledElement(element);
- if (!enabledElement) {
- throw new Error('Element is not enabled');
- }
- return {
- invalidated: true,
- highlighted: false,
- metadata: {
- ...exportedData.metadata,
- toolName: TibialPlateauAngleTool.toolName,
- },
- data: {
- label: '',
- handles: {
- points: [...exportedData.points],
- activeHandleIndex: null,
- textBoxTPA: exportedData.textBoxPosition,
- },
- cachedStats: {},
- },
- } as TibialPlateauAngleAnnotation;
- }
- constructor(
- toolProps: PublicToolProps = {},
- defaultToolProps: ToolProps = {
- supportedInteractionTypes: ['Mouse', 'Touch'],
- configuration: {
- shadow: true,
- preventHandleOutsideImage: false,
- },
- }
- ) {
- super(toolProps, defaultToolProps);
- }
- addNewAnnotation(evt: EventTypes.InteractionEventType): TibialPlateauAngleAnnotation {
- evt.preventDefault();
- return {} as TibialPlateauAngleAnnotation;
- }
- isPointNearTool(
- element: HTMLDivElement,
- annotation: TibialPlateauAngleAnnotation,
- 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 canvasPoint = viewport.worldToCanvas(points[i]);
- const distance = Math.sqrt(
- Math.pow(canvasPoint[0] - canvasCoords[0], 2) +
- Math.pow(canvasPoint[1] - canvasCoords[1], 2)
- );
- if (distance < proximity) return true;
- }
- return false;
- }
- private _isPointInTextBox(
- canvasPoint: CoreTypes.Point2,
- textBoxPosition: CoreTypes.Point2,
- textWidth: number = 100,
- 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;
- points.push([
- center[0] + radius * Math.cos(angle),
- center[1] + radius * Math.sin(angle)
- ]);
- }
- 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);
- }
- _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);
- }
- _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 TibialPlateauAngleAnnotation;
- const targetId = this.getTargetId(viewport);
- const cachedStats = targetId ? customAnn.data.cachedStats?.[targetId] : undefined;
- if (cachedStats?.intersectionPoint) {
- const intersectionCanvas = viewport.worldToCanvas(cachedStats.intersectionPoint);
- // 获取文本框位置,如果不存在则使用默认位置
- const textBoxTPA: CoreTypes.Point2 = customAnn.data.handles.textBoxTPA || [
- intersectionCanvas[0] + 40,
- intersectionCanvas[1] - 10,
- ];
- if (this._isPointInTextBox(canvasCoords, textBoxTPA)) {
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(element, this.getToolName());
- // 计算鼠标点击位置相对于文本框左上角的偏移量
- const textBoxOffset: CoreTypes.Point2 = [
- canvasCoords[0] - textBoxTPA[0],
- canvasCoords[1] - textBoxTPA[1],
- ];
- this.editData = {
- annotation: customAnn,
- viewportIdsToRender,
- hasMoved: false,
- textBoxBeingMoved: true,
- textBoxOffset: textBoxOffset,
- };
- customAnn.highlighted = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- }
- }
- // 如果没有点击文本框,再检查是否点击在手柄上
- for (const ann of annotations) {
- const customAnn = ann as TibialPlateauAngleAnnotation;
- 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.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 } = this.editData;
- const customAnn = ann as TibialPlateauAngleAnnotation;
- const { data } = customAnn;
- if (textBoxBeingMoved && this.editData.textBoxOffset) {
- data.handles.textBoxTPA = [
- canvasCoords[0] - this.editData.textBoxOffset[0],
- canvasCoords[1] - this.editData.textBoxOffset[1],
- ];
- this.editData.hasMoved = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- return;
- }
- const worldPos = currentPoints.world;
- const activeHandleIndex = data.handles.activeHandleIndex;
- if (activeHandleIndex !== null && activeHandleIndex >= 0 && activeHandleIndex < data.handles.points.length) {
- data.handles.points[activeHandleIndex] = worldPos;
- this._updateCachedStats(customAnn, enabledElement);
- this.editData.hasMoved = true;
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- evt.stopPropagation();
- }
- };
- _mouseUpModifyCallback = (evt: EventTypes.InteractionEventType): void => {
- if (!this.editData) return;
- const { annotation: ann } = this.editData;
- const customAnn = ann as TibialPlateauAngleAnnotation;
- customAnn.data.handles.activeHandleIndex = null;
- customAnn.highlighted = false;
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- evt.detail.element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- this.editData = null;
- evt.preventDefault();
- evt.stopPropagation();
- };
- handleSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: TibialPlateauAngleAnnotation
- ): void {
- annotation.highlighted = true;
- const viewportIdsToRender =
- utilities.viewportFilters.getViewportIdsWithToolToRender(
- evt.detail.element,
- this.getToolName()
- );
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- evt.preventDefault();
- }
- toolSelectedCallback(
- evt: EventTypes.InteractionEventType,
- annotation: TibialPlateauAngleAnnotation
- ): void {
- // 实现工具选中逻辑
- }
- _dragCallback = (evt: EventTypes.InteractionEventType): void => {
- this.isDrawing = true;
- const enabledElement = getEnabledElement(evt.detail.element);
- if (!enabledElement || !this.editData) return;
- const { annotation, viewportIdsToRender } = this.editData;
- const { data } = annotation as TibialPlateauAngleAnnotation;
- if (annotation) {
- const worldPos = evt.detail.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 TibialPlateauAngleAnnotation, enabledElement);
- this.editData!.hasMoved = true;
- }
- utilities.triggerAnnotationRenderForViewportIds(viewportIdsToRender);
- }
- };
- _endCallback = (evt: EventTypes.InteractionEventType): void => {
- if (!this.editData) return;
- if (this.editData.hasMoved) {
- this.editData.hasMoved = false;
- }
- };
- getHandleNearImagePoint(
- element: HTMLDivElement,
- annotation: TibialPlateauAngleAnnotation,
- 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 canvasPoint = viewport.worldToCanvas(points[i]);
- 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: points[i] } as Types.ToolHandle;
- }
- }
-
- annotation.data.handles.activeHandleIndex = null;
- return undefined;
- }
- _updateCachedStats(
- annotation: TibialPlateauAngleAnnotation,
- 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] = {
- TPA: 0,
- functionalAxisVector: [0, 0, 0],
- plateauLineVector: [0, 0, 0],
- perpendicularVector: [0, 0, 0],
- plateauMidpoint: [0, 0, 0],
- };
- }
- const stats = data.cachedStats[targetId];
- if (points.length >= 4) {
- const point1 = points[0]; // 平台线起点
- const point2 = points[1]; // 平台线终点
- const point3 = points[2]; // 功能轴线起点
- const point4 = points[3]; // 功能轴线终点
- // 计算胫骨内侧平台线向量 (点1→点2)
- const plateauLineVector = vectorSubtract(point2, point1);
- stats.plateauLineVector = plateauLineVector;
- // 计算胫骨功能轴线向量 (点3→点4)
- const functionalAxisVector = vectorSubtract(point4, point3);
- stats.functionalAxisVector = functionalAxisVector;
- // 计算胫骨内侧平台线中点(用于显示参考)
- const plateauMidpoint = calculateMidpoint(point1, point2);
- stats.plateauMidpoint = plateauMidpoint;
- // 计算平台线与功能轴线的交点(作为中垂线的起点)
- const intersection = this._getLineIntersection(
- point1, // 平台线起点
- plateauLineVector, // 平台线方向
- point3, // 功能轴线起点
- functionalAxisVector // 功能轴线方向
- );
-
- if (intersection) {
- stats.intersectionPoint = intersection;
- }
- // 计算平台线的中垂线向量(2D平面上,向量(x,y)的逆时针垂直向量为(-y,x))
- // 这条垂线从交点出发,垂直于平台线
- const perpendicularVector: CoreTypes.Point3 = [
- -plateauLineVector[1],
- plateauLineVector[0],
- 0
- ];
- stats.perpendicularVector = perpendicularVector;
- // 计算TPA角度:功能轴线与平台线中垂线的夹角
- const angleRad = getAngleBetweenLines(functionalAxisVector, perpendicularVector);
- stats.TPA = Math.round(degrees(angleRad));
- }
- }
- private _normalizeVector(v: CoreTypes.Point3): CoreTypes.Point3 {
- const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
- if (length === 0) return [0, 0, 0];
- return [v[0] / length, v[1] / length, v[2] / length];
- }
- /**
- * 计算两条直线的交点
- */
- private _getLineIntersection(
- line1Start: CoreTypes.Point3,
- line1Dir: CoreTypes.Point3,
- line2Start: CoreTypes.Point3,
- line2Dir: CoreTypes.Point3
- ): CoreTypes.Point3 | null {
- 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],
- line1Start[2] + t * line1Dir[2]
- ];
- }
- 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 TibialPlateauAngleAnnotation;
- const { annotationUID, data } = annotation;
- const points = data.handles.points;
- if (points.length < 4) 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, 150, 255)' : 'rgb(255, 255, 255)';
- // 绘制胫骨内侧平台线(点1→点2)
- if (annotationUID) {
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-plateauLine`,
- canvasPoints[0],
- canvasPoints[1],
- { color: lineColor, width: 2 }
- );
- // 绘制胫骨功能轴线(点3→点4)
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-functionalAxis`,
- canvasPoints[2],
- canvasPoints[3],
- { color: lineColor, width: 2 }
- );
- // 绘制胫骨内侧平台线的延长线(虚线):从最近的端点延伸到交点,并在交点后继续延长
- if (cachedStats && cachedStats.intersectionPoint && points.length >= 2) {
- const point1 = points[0]; // 平台线起点
- const point2 = points[1]; // 平台线终点
- const intersection = cachedStats.intersectionPoint;
-
- // 计算交点到两个端点的距离,选择较近的端点
- const dist1 = Math.sqrt(
- Math.pow(intersection[0] - point1[0], 2) +
- Math.pow(intersection[1] - point1[1], 2)
- );
- const dist2 = Math.sqrt(
- Math.pow(intersection[0] - point2[0], 2) +
- Math.pow(intersection[1] - point2[1], 2)
- );
-
- // 选择离交点较近的端点作为延长线起点
- const extensionStart = dist1 < dist2 ? point1 : point2;
-
- // 计算从端点到交点的方向向量
- const directionVector: CoreTypes.Point3 = [
- intersection[0] - extensionStart[0],
- intersection[1] - extensionStart[1],
- intersection[2] - extensionStart[2]
- ];
-
- // 归一化方向向量
- const normalizedDir = this._normalizeVector(directionVector);
-
- // 在交点后继续延长的长度(像素)
- const extraExtensionLength = 150;
-
- // 计算延长线的最终终点:交点 + 延长距离
- const extensionEnd = vectorAdd(
- intersection,
- vectorScale(normalizedDir, extraExtensionLength)
- );
-
- const extensionStartCanvas = viewport.worldToCanvas(extensionStart);
- const extensionEndCanvas = viewport.worldToCanvas(extensionEnd);
-
- // 绘制完整的延长线:从端点经过交点到延伸终点
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-plateauExtension`,
- extensionStartCanvas,
- extensionEndCanvas,
- { color: 'rgb(200, 200, 200)', width: 1, lineDash: '4,4' }
- );
- }
- // 绘制功能轴线的延长线(虚线):从最近的端点延伸到交点,并在交点后继续延长
- if (cachedStats && cachedStats.intersectionPoint && points.length >= 4) {
- const point3 = points[2]; // 功能轴线起点
- const point4 = points[3]; // 功能轴线终点
- const intersection = cachedStats.intersectionPoint;
-
- // 计算交点到两个端点的距离,选择较近的端点
- const dist3 = Math.sqrt(
- Math.pow(intersection[0] - point3[0], 2) +
- Math.pow(intersection[1] - point3[1], 2)
- );
- const dist4 = Math.sqrt(
- Math.pow(intersection[0] - point4[0], 2) +
- Math.pow(intersection[1] - point4[1], 2)
- );
-
- // 选择离交点较近的端点作为延长线起点
- const extensionStart = dist3 < dist4 ? point3 : point4;
-
- // 计算从端点到交点的方向向量
- const directionVector: CoreTypes.Point3 = [
- intersection[0] - extensionStart[0],
- intersection[1] - extensionStart[1],
- intersection[2] - extensionStart[2]
- ];
-
- // 归一化方向向量
- const normalizedDir = this._normalizeVector(directionVector);
-
- // 在交点后继续延长的长度(像素)
- const extraExtensionLength = 150;
-
- // 计算延长线的最终终点:交点 + 延长距离
- const extensionEnd = vectorAdd(
- intersection,
- vectorScale(normalizedDir, extraExtensionLength)
- );
-
- const extensionStartCanvas = viewport.worldToCanvas(extensionStart);
- const extensionEndCanvas = viewport.worldToCanvas(extensionEnd);
-
- // 绘制完整的延长线:从端点经过交点到延伸终点
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-axisExtension`,
- extensionStartCanvas,
- extensionEndCanvas,
- { color: 'rgb(200, 200, 200)', width: 1, lineDash: '4,4' }
- );
- }
- // 绘制平台线的中垂线(从交点向两端延伸)
- if (cachedStats && cachedStats.perpendicularVector && cachedStats.intersectionPoint) {
- // 归一化中垂线向量
- const normalizedPerp = this._normalizeVector(cachedStats.perpendicularVector);
-
- // 延长长度:从交点向两端各延伸150像素
- const extendLength = 150;
- // 中垂线起点:从交点向一个方向延伸
- const perpStart = vectorAdd(
- cachedStats.intersectionPoint,
- vectorScale(normalizedPerp, -extendLength)
- );
- // 中垂线终点:从交点向另一个方向延伸
- const perpEnd = vectorAdd(
- cachedStats.intersectionPoint,
- vectorScale(normalizedPerp, extendLength)
- );
- const perpStartCanvas = viewport.worldToCanvas(perpStart);
- const perpEndCanvas = viewport.worldToCanvas(perpEnd);
- drawLineSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-perpendicular`,
- perpStartCanvas,
- perpEndCanvas,
- { color: 'rgb(0, 255, 0)', width: 2, lineDash: '4,4' }
- );
- }
- // 绘制角度弧线和文本(在交点位置)
- if (cachedStats && cachedStats.functionalAxisVector && cachedStats.perpendicularVector && cachedStats.intersectionPoint) {
- // 使用交点作为角度弧线的中心
- const intersectionCanvas = viewport.worldToCanvas(cachedStats.intersectionPoint);
- const axisVector2D: CoreTypes.Point2 = [
- cachedStats.functionalAxisVector[0],
- cachedStats.functionalAxisVector[1]
- ];
- const perpVector2D: CoreTypes.Point2 = [
- cachedStats.perpendicularVector[0],
- cachedStats.perpendicularVector[1]
- ];
- const arcPoints = this._createAngleArcPoints(intersectionCanvas, axisVector2D, perpVector2D, 30);
- drawPathSvg(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-arc`,
- arcPoints,
- { color: 'rgb(255, 255, 0)', width: 2 }
- );
- // 计算弧线中间位置:取弧线点数组的中间点
- const arcMidIndex = Math.floor(arcPoints.length / 2);
- const arcMidPoint: CoreTypes.Point2 = arcPoints[arcMidIndex];
- const textLines = [`TPA: ${cachedStats.TPA}°`];
- const textBoxPosition: CoreTypes.Point2 = data.handles.textBoxTPA || [
- intersectionCanvas[0] + 40,
- intersectionCanvas[1] - 10,
- ];
- // 文本虚线连接到弧线中间位置
- drawLinkedTextBox(
- svgDrawingHelper,
- annotationUID,
- `${annotationUID}-text`,
- textLines,
- textBoxPosition,
- [arcMidPoint],
- {},
- { color: 'rgb(255, 255, 0)' }
- );
- }
- // 绘制控制点
- drawHandles(
- svgDrawingHelper,
- annotationUID,
- '0',
- canvasPoints,
- { color: 'rgb(255, 255, 255)', handleRadius: 6 }
- );
- }
- renderStatus = true;
- }
- return renderStatus;
- };
- }
|