Skip to content

Commit

Permalink
feat: add referenceCursors tool (#275)
Browse files Browse the repository at this point in the history
* added basic cursorCrosshairSync tool with example, TODO: for now cursorSync is displayed regardless of distance, create configurable distance and also sync the position of all viewports over which the mouse is not to scroll to a slice that is close to the currentMousePosition in 3d space

* addde stack syncing for StackViewport and syncing for volumeViewport on imageChange events, added configuration for max display distance

* refactored tool functions

* added comment to possible bug

* added configuration options to example

* changed look of crosshair to 4 lines with central space

* undid local tsconfig change

* undid yarn.lock changes

* added tool to example-info.json

* removed from example-runner because it broke build

* readded example and fixed typo

* readded example-info and changed example to trigger rebuild

* added cleanup for mouseoverElement when tool is disabled

* added cleanup when tool gets disabled, this does not get called when toolGroup gets destroyed, might cause remaining listeners

* applied naming changes, reworked adding annotation logic

* removed event listeners and moved logic to check for stack scrolling into rendering logic

* added planeDistanceToPoint to planar utilities

* added getClosestStackImageIndexForPoint

* rewrote logic to use onCameraModified

* updated example-info

* fixed bug with 0 being falsey

* added logic to remove cursor if wanted

* modified toolGroup so that setting a tool active only changes the cursor to default if there is no primary mouse cursor

* fixed bug not updating disable cursor

* fixed missing parentheses from merge

* readded scrollWheel scrolling and api changes

* fixed typos
  • Loading branch information
doepnern authored Nov 23, 2022
1 parent 049b292 commit 3303246
Show file tree
Hide file tree
Showing 12 changed files with 1,004 additions and 5 deletions.
10 changes: 9 additions & 1 deletion common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,9 @@ type FlipDirection = {
// @public (undocumented)
function getClosestImageId(imageVolume: IImageVolume, worldPos: Point3, viewPlaneNormal: Point3, viewUp: Point3): string;

// @public (undocumented)
function getClosestStackImageIndexForPoint(point: Point3, viewport: IStackViewport): number | null;

// @public (undocumented)
export function getEnabledElement(element: HTMLDivElement | undefined): IEnabledElement | undefined;

Expand Down Expand Up @@ -1521,13 +1524,17 @@ declare namespace planar {
export {
linePlaneIntersection,
planeEquation,
threePlaneIntersection
threePlaneIntersection,
planeDistanceToPoint
}
}

// @public (undocumented)
type Plane = [number, number, number, number];

// @public (undocumented)
function planeDistanceToPoint(plane: Plane, point: Point3, signed?: boolean): number;

// @public (undocumented)
function planeEquation(normal: Point3, point: Point3 | vec3): Plane;

Expand Down Expand Up @@ -1965,6 +1972,7 @@ declare namespace utilities {
getImageSliceDataForVolumeViewport,
isImageActor,
getViewportsWithImageURI,
getClosestStackImageIndexForPoint,
calculateViewportsSpatialRegistration,
spatialRegistrationMetadataProvider,
getViewportImageCornersInWorld
Expand Down
58 changes: 58 additions & 0 deletions common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3591,6 +3591,63 @@ export class RectangleScissorsTool extends BaseTool {
static toolName: any;
}

// @public (undocumented)
interface ReferenceCursor extends Annotation {
// (undocumented)
data: {
handles: {
points: [Types_2.Point3];
};
};
}

// @public (undocumented)
export class ReferenceCursors extends AnnotationDisplayTool {
constructor(toolProps?: PublicToolProps, defaultToolProps?: ToolProps);
// (undocumented)
_addAnnotation(element: HTMLDivElement, annotation: Annotation): string | null;
// (undocumented)
createInitialAnnotation: (worldPos: Types_2.Point3, element: HTMLDivElement) => void;
// (undocumented)
_currentCanvasPosition: null | Types_2.Point2;
// (undocumented)
_currentCursorWorldPosition: null | Types_2.Point3;
// (undocumented)
_disableCursorEnabled: boolean;
// (undocumented)
_elementWithCursor: null | HTMLDivElement;
// (undocumented)
filterInteractableAnnotationsForElement(element: HTMLDivElement, annotations: Annotations): Annotations;
// (undocumented)
getActiveAnnotation(element: HTMLDivElement): null | Annotation;
// (undocumented)
isDrawing: boolean;
// (undocumented)
isHandleOutsideImage: boolean;
// (undocumented)
mouseDragCallback: any;
// (undocumented)
mouseMoveCallback: (evt: EventTypes_2.MouseMoveEventType) => boolean;
// (undocumented)
onCameraModified: (evt: any) => void;
// (undocumented)
onSetToolActive(): void;
// (undocumented)
onSetToolDisabled(): void;
// (undocumented)
renderAnnotation: (enabledElement: Types_2.IEnabledElement, svgDrawingHelper: SVGDrawingHelper) => boolean;
// (undocumented)
_throttledCalculateCachedStats: any;
// (undocumented)
static toolName: any;
// (undocumented)
touchDragCallback: any;
// (undocumented)
updateAnnotationPosition(element: HTMLDivElement, annotation: Annotation): void;
// (undocumented)
updateViewportImage(viewport: Types_2.IStackViewport | Types_2.IVolumeViewport): void;
}

// @public (undocumented)
interface ReferenceLineAnnotation extends Annotation {
// (undocumented)
Expand Down Expand Up @@ -4336,6 +4393,7 @@ declare namespace ToolSpecificAnnotationTypes {
PlanarFreehandROIAnnotation,
ArrowAnnotation,
AngleAnnotation,
ReferenceCursor,
ReferenceLineAnnotation
}
}
Expand Down
116 changes: 116 additions & 0 deletions packages/core/src/utilities/getClosestStackImageIndexForPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { vec3 } from 'gl-matrix';
import { planar } from '.';
import { metaData } from '..';
import { IStackViewport, Point3 } from '../types';

/**
* Given a point in 3D space and a viewport it returns the index of the closest imageId, it assumes that stack images are sorted according to their sliceLocation
* @param point - [A, B, C] coordinates of the point in 3D space
* @param viewport - The StackViewport to search for the closest imageId
*
* @returns The imageId index of the closest imageId or null if no imageId is found
*/
export default function getClosestStackImageIndexForPoint(
point: Point3,
viewport: IStackViewport
): number | null {
const minimalDistance = calculateMinimalDistanceForStackViewport(
point,
viewport
);
return minimalDistance ? minimalDistance.index : null;
}

//assumes that imageIds are sorted by slice location
export function calculateMinimalDistanceForStackViewport(
point: Point3,
viewport: IStackViewport
): { distance: number; index: number } | null {
const imageIds = viewport.getImageIds();
const currentImageIdIndex = viewport.getCurrentImageIdIndex();

if (imageIds.length === 0) return null;

const getDistance = (imageId: string): null | number => {
const planeMetadata = getPlaneMetadata(imageId);
if (!planeMetadata) return null;
const plane = planar.planeEquation(
planeMetadata.planeNormal,
planeMetadata.imagePositionPatient
);
const distance = planar.planeDistanceToPoint(plane, point);
return distance;
};

const closestStack = {
distance: getDistance(imageIds[currentImageIdIndex]) ?? Infinity,
index: currentImageIdIndex,
};

//check higher indices
const higherImageIds = imageIds.slice(currentImageIdIndex + 1);

for (let i = 0; i < higherImageIds.length; i++) {
const id = higherImageIds[i];
const distance = getDistance(id);
if (distance === null) continue;
if (distance <= closestStack.distance) {
closestStack.distance = distance;
closestStack.index = i + currentImageIdIndex + 1;
} else break;
}
//check lower indices
const lowerImageIds = imageIds.slice(0, currentImageIdIndex);
for (let i = lowerImageIds.length - 1; i >= 0; i--) {
const id = lowerImageIds[i];
const distance = getDistance(id);
if (distance === null || distance === closestStack.distance) continue;
if (distance < closestStack.distance) {
closestStack.distance = distance;
closestStack.index = i;
} else break;
}
return closestStack.distance === Infinity ? null : closestStack;
}

function getPlaneMetadata(imageId: string): null | {
rowCosines: Point3;
columnCosines: Point3;
imagePositionPatient: Point3;
planeNormal: Point3;
} {
const targetImagePlane = metaData.get('imagePlaneModule', imageId);

if (
!targetImagePlane ||
!(
targetImagePlane.rowCosines instanceof Array &&
targetImagePlane.rowCosines.length === 3
) ||
!(
targetImagePlane.columnCosines instanceof Array &&
targetImagePlane.columnCosines.length === 3
) ||
!(
targetImagePlane.imagePositionPatient instanceof Array &&
targetImagePlane.imagePositionPatient.length === 3
)
) {
return null;
}
const {
rowCosines,
columnCosines,
imagePositionPatient,
}: {
rowCosines: Point3;
columnCosines: Point3;
imagePositionPatient: Point3;
} = targetImagePlane;

const rowVec = vec3.set(vec3.create(), ...rowCosines);
const colVec = vec3.set(vec3.create(), ...columnCosines);
const planeNormal = vec3.cross(vec3.create(), rowVec, colVec) as Point3;

return { rowCosines, columnCosines, imagePositionPatient, planeNormal };
}
2 changes: 2 additions & 0 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import snapFocalPointToSlice from './snapFocalPointToSlice';
import getImageSliceDataForVolumeViewport from './getImageSliceDataForVolumeViewport';
import isImageActor from './isImageActor';
import getViewportsWithImageURI from './getViewportsWithImageURI';
import getClosestStackImageIndexForPoint from './getClosestStackImageIndexForPoint';
import calculateViewportsSpatialRegistration from './calculateViewportsSpatialRegistration';
import spatialRegistrationMetadataProvider from './spatialRegistrationMetadataProvider';
import getViewportImageCornersInWorld from './getViewportImageCornersInWorld';
Expand Down Expand Up @@ -67,6 +68,7 @@ export {
getImageSliceDataForVolumeViewport,
isImageActor,
getViewportsWithImageURI,
getClosestStackImageIndexForPoint,
calculateViewportsSpatialRegistration,
spatialRegistrationMetadataProvider,
getViewportImageCornersInWorld,
Expand Down
27 changes: 26 additions & 1 deletion packages/core/src/utilities/planar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,29 @@ function threePlaneIntersection(
return [x, y, z];
}

export { linePlaneIntersection, planeEquation, threePlaneIntersection };
/**
* Computes the distance of a point in 3D space to a plane
* @param plane - [A, B, C, D] of plane equation A*X + B*Y + C*Z = D
* @param point - [A, B, C] the plane in World coordinate
* @param signed - if true, the distance is signed
* @returns - the distance of the point to the plane
* */
function planeDistanceToPoint(
plane: Plane,
point: Point3,
signed = false
): number {
const [A, B, C, D] = plane;
const [x, y, z] = point;
const numerator = A * x + B * y + C * z - D;
const distance = Math.abs(numerator) / Math.sqrt(A * A + B * B + C * C);
const sign = signed ? Math.sign(numerator) : 1;
return sign * distance;
}

export {
linePlaneIntersection,
planeEquation,
threePlaneIntersection,
planeDistanceToPoint,
};
Loading

0 comments on commit 3303246

Please sign in to comment.